feat: phase 1 of python scripting
This commit is contained in:
@@ -108,10 +108,10 @@ Baseline benchmark (22 Feb 2026, local macOS run):
|
|||||||
Objective: user can create/run scripts and see output.
|
Objective: user can create/run scripts and see output.
|
||||||
|
|
||||||
Deliverables:
|
Deliverables:
|
||||||
- [ ] Script storage model (DB index + filesystem source in `scripts/*.py`).
|
- [x] Script storage model (DB index + filesystem source in `scripts/*.py`).
|
||||||
- [ ] CRUD APIs in `main/engine` + `ipc` handlers.
|
- [x] CRUD APIs in `main/engine` + `ipc` handlers.
|
||||||
- [ ] Renderer scripts list + editor + run button.
|
- [x] Renderer scripts list + editor + run button.
|
||||||
- [ ] Console/output capture in existing bottom output area.
|
- [x] Console/output capture in existing bottom output area.
|
||||||
- [ ] Project rebuild picks up `scripts/` changes.
|
- [ ] Project rebuild picks up `scripts/` changes.
|
||||||
|
|
||||||
Out of scope for MVP:
|
Out of scope for MVP:
|
||||||
@@ -454,15 +454,18 @@ PR-14+: Optional advanced capabilities
|
|||||||
|
|
||||||
## 10. Current Status
|
## 10. Current Status
|
||||||
|
|
||||||
Status: Phase 0 in progress (MVP-first, full-scope preserved).
|
Status: Phase 1 in progress (MVP-first, full-scope preserved).
|
||||||
|
|
||||||
Progress update (22 Feb 2026):
|
Progress update (22 Feb 2026):
|
||||||
- [x] PR-00 complete: Pyodide dependency + renderer worker bootstrap + ready signal.
|
- [x] PR-00 complete: Pyodide dependency + renderer worker bootstrap + ready signal.
|
||||||
- [x] PR-01 complete: worker run/stdout/error protocol + timeout watchdog + runtime recovery.
|
- [x] PR-01 complete: worker run/stdout/error protocol + timeout watchdog + runtime recovery.
|
||||||
- [x] PR-02 complete: ABI v1 shared types/schemas + caller/worker validation.
|
- [x] PR-02 complete: ABI v1 shared types/schemas + caller/worker validation.
|
||||||
- [x] Phase 0 benchmark harness + baseline capture complete.
|
- [x] Phase 0 benchmark harness + baseline capture complete.
|
||||||
|
- [x] PR-03 complete: scripts table + `ScriptEngine` filesystem/DB round-trip persistence.
|
||||||
|
- [x] PR-04 complete: script CRUD IPC handlers + preload/shared API typing + IPC tests.
|
||||||
|
- [x] PR-05 complete: renderer scripts list/editor/run flow + output panel integration.
|
||||||
|
|
||||||
Recommended next action:
|
Recommended next action:
|
||||||
1. Start Phase 1 PR-03: script persistence model (`scripts/*.py` + index metadata).
|
1. Start Phase 1 PR-06: include `scripts/` in rebuild/meta-diff synchronization.
|
||||||
2. Add round-trip tests for create/update/delete between filesystem and DB.
|
2. Keep scripts API access in renderer views/store paths only (no deep component IPC sprawl).
|
||||||
3. Keep benchmark command in CI/manual perf checks for regressions.
|
3. Keep benchmark command in CI/manual perf checks for regressions.
|
||||||
|
|||||||
15
drizzle/0005_short_sally_floyd.sql
Normal file
15
drizzle/0005_short_sally_floyd.sql
Normal file
@@ -0,0 +1,15 @@
|
|||||||
|
CREATE TABLE `scripts` (
|
||||||
|
`id` text PRIMARY KEY NOT NULL,
|
||||||
|
`project_id` text NOT NULL,
|
||||||
|
`slug` text NOT NULL,
|
||||||
|
`title` text NOT NULL,
|
||||||
|
`kind` text DEFAULT 'utility' NOT NULL,
|
||||||
|
`entrypoint` text DEFAULT 'render' NOT NULL,
|
||||||
|
`enabled` integer DEFAULT true NOT NULL,
|
||||||
|
`version` integer DEFAULT 1 NOT NULL,
|
||||||
|
`file_path` text NOT NULL,
|
||||||
|
`created_at` integer NOT NULL,
|
||||||
|
`updated_at` integer NOT NULL
|
||||||
|
);
|
||||||
|
--> statement-breakpoint
|
||||||
|
CREATE UNIQUE INDEX `scripts_project_slug_idx` ON `scripts` (`project_id`,`slug`);
|
||||||
913
drizzle/meta/0005_snapshot.json
Normal file
913
drizzle/meta/0005_snapshot.json
Normal file
@@ -0,0 +1,913 @@
|
|||||||
|
{
|
||||||
|
"version": "6",
|
||||||
|
"dialect": "sqlite",
|
||||||
|
"id": "b157a762-0743-4499-a635-16ac3fb5ee18",
|
||||||
|
"prevId": "46702982-9f8a-4c7e-8fb6-3270c3fbe120",
|
||||||
|
"tables": {
|
||||||
|
"chat_conversations": {
|
||||||
|
"name": "chat_conversations",
|
||||||
|
"columns": {
|
||||||
|
"id": {
|
||||||
|
"name": "id",
|
||||||
|
"type": "text",
|
||||||
|
"primaryKey": true,
|
||||||
|
"notNull": true,
|
||||||
|
"autoincrement": false
|
||||||
|
},
|
||||||
|
"title": {
|
||||||
|
"name": "title",
|
||||||
|
"type": "text",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": true,
|
||||||
|
"autoincrement": false
|
||||||
|
},
|
||||||
|
"model": {
|
||||||
|
"name": "model",
|
||||||
|
"type": "text",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": false,
|
||||||
|
"autoincrement": false
|
||||||
|
},
|
||||||
|
"copilot_session_id": {
|
||||||
|
"name": "copilot_session_id",
|
||||||
|
"type": "text",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": false,
|
||||||
|
"autoincrement": false
|
||||||
|
},
|
||||||
|
"created_at": {
|
||||||
|
"name": "created_at",
|
||||||
|
"type": "integer",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": true,
|
||||||
|
"autoincrement": false
|
||||||
|
},
|
||||||
|
"updated_at": {
|
||||||
|
"name": "updated_at",
|
||||||
|
"type": "integer",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": true,
|
||||||
|
"autoincrement": false
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"indexes": {},
|
||||||
|
"foreignKeys": {},
|
||||||
|
"compositePrimaryKeys": {},
|
||||||
|
"uniqueConstraints": {},
|
||||||
|
"checkConstraints": {}
|
||||||
|
},
|
||||||
|
"chat_messages": {
|
||||||
|
"name": "chat_messages",
|
||||||
|
"columns": {
|
||||||
|
"id": {
|
||||||
|
"name": "id",
|
||||||
|
"type": "integer",
|
||||||
|
"primaryKey": true,
|
||||||
|
"notNull": true,
|
||||||
|
"autoincrement": true
|
||||||
|
},
|
||||||
|
"conversation_id": {
|
||||||
|
"name": "conversation_id",
|
||||||
|
"type": "text",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": true,
|
||||||
|
"autoincrement": false
|
||||||
|
},
|
||||||
|
"role": {
|
||||||
|
"name": "role",
|
||||||
|
"type": "text",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": true,
|
||||||
|
"autoincrement": false
|
||||||
|
},
|
||||||
|
"content": {
|
||||||
|
"name": "content",
|
||||||
|
"type": "text",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": false,
|
||||||
|
"autoincrement": false
|
||||||
|
},
|
||||||
|
"tool_call_id": {
|
||||||
|
"name": "tool_call_id",
|
||||||
|
"type": "text",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": false,
|
||||||
|
"autoincrement": false
|
||||||
|
},
|
||||||
|
"tool_calls": {
|
||||||
|
"name": "tool_calls",
|
||||||
|
"type": "text",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": false,
|
||||||
|
"autoincrement": false
|
||||||
|
},
|
||||||
|
"created_at": {
|
||||||
|
"name": "created_at",
|
||||||
|
"type": "integer",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": true,
|
||||||
|
"autoincrement": false
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"indexes": {},
|
||||||
|
"foreignKeys": {},
|
||||||
|
"compositePrimaryKeys": {},
|
||||||
|
"uniqueConstraints": {},
|
||||||
|
"checkConstraints": {}
|
||||||
|
},
|
||||||
|
"generated_file_hashes": {
|
||||||
|
"name": "generated_file_hashes",
|
||||||
|
"columns": {
|
||||||
|
"project_id": {
|
||||||
|
"name": "project_id",
|
||||||
|
"type": "text",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": true,
|
||||||
|
"autoincrement": false
|
||||||
|
},
|
||||||
|
"relative_path": {
|
||||||
|
"name": "relative_path",
|
||||||
|
"type": "text",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": true,
|
||||||
|
"autoincrement": false
|
||||||
|
},
|
||||||
|
"content_hash": {
|
||||||
|
"name": "content_hash",
|
||||||
|
"type": "text",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": true,
|
||||||
|
"autoincrement": false
|
||||||
|
},
|
||||||
|
"updated_at": {
|
||||||
|
"name": "updated_at",
|
||||||
|
"type": "integer",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": true,
|
||||||
|
"autoincrement": false
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"indexes": {
|
||||||
|
"generated_file_hashes_project_path_idx": {
|
||||||
|
"name": "generated_file_hashes_project_path_idx",
|
||||||
|
"columns": [
|
||||||
|
"project_id",
|
||||||
|
"relative_path"
|
||||||
|
],
|
||||||
|
"isUnique": true
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"foreignKeys": {},
|
||||||
|
"compositePrimaryKeys": {},
|
||||||
|
"uniqueConstraints": {},
|
||||||
|
"checkConstraints": {}
|
||||||
|
},
|
||||||
|
"import_definitions": {
|
||||||
|
"name": "import_definitions",
|
||||||
|
"columns": {
|
||||||
|
"id": {
|
||||||
|
"name": "id",
|
||||||
|
"type": "text",
|
||||||
|
"primaryKey": true,
|
||||||
|
"notNull": true,
|
||||||
|
"autoincrement": false
|
||||||
|
},
|
||||||
|
"project_id": {
|
||||||
|
"name": "project_id",
|
||||||
|
"type": "text",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": true,
|
||||||
|
"autoincrement": false
|
||||||
|
},
|
||||||
|
"name": {
|
||||||
|
"name": "name",
|
||||||
|
"type": "text",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": true,
|
||||||
|
"autoincrement": false
|
||||||
|
},
|
||||||
|
"wxr_file_path": {
|
||||||
|
"name": "wxr_file_path",
|
||||||
|
"type": "text",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": false,
|
||||||
|
"autoincrement": false
|
||||||
|
},
|
||||||
|
"uploads_folder_path": {
|
||||||
|
"name": "uploads_folder_path",
|
||||||
|
"type": "text",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": false,
|
||||||
|
"autoincrement": false
|
||||||
|
},
|
||||||
|
"last_analysis_result": {
|
||||||
|
"name": "last_analysis_result",
|
||||||
|
"type": "text",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": false,
|
||||||
|
"autoincrement": false
|
||||||
|
},
|
||||||
|
"created_at": {
|
||||||
|
"name": "created_at",
|
||||||
|
"type": "integer",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": true,
|
||||||
|
"autoincrement": false
|
||||||
|
},
|
||||||
|
"updated_at": {
|
||||||
|
"name": "updated_at",
|
||||||
|
"type": "integer",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": true,
|
||||||
|
"autoincrement": false
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"indexes": {},
|
||||||
|
"foreignKeys": {},
|
||||||
|
"compositePrimaryKeys": {},
|
||||||
|
"uniqueConstraints": {},
|
||||||
|
"checkConstraints": {}
|
||||||
|
},
|
||||||
|
"media": {
|
||||||
|
"name": "media",
|
||||||
|
"columns": {
|
||||||
|
"project_id": {
|
||||||
|
"name": "project_id",
|
||||||
|
"type": "text",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": true,
|
||||||
|
"autoincrement": false
|
||||||
|
},
|
||||||
|
"id": {
|
||||||
|
"name": "id",
|
||||||
|
"type": "text",
|
||||||
|
"primaryKey": true,
|
||||||
|
"notNull": true,
|
||||||
|
"autoincrement": false
|
||||||
|
},
|
||||||
|
"filename": {
|
||||||
|
"name": "filename",
|
||||||
|
"type": "text",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": true,
|
||||||
|
"autoincrement": false
|
||||||
|
},
|
||||||
|
"original_name": {
|
||||||
|
"name": "original_name",
|
||||||
|
"type": "text",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": true,
|
||||||
|
"autoincrement": false
|
||||||
|
},
|
||||||
|
"mime_type": {
|
||||||
|
"name": "mime_type",
|
||||||
|
"type": "text",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": true,
|
||||||
|
"autoincrement": false
|
||||||
|
},
|
||||||
|
"size": {
|
||||||
|
"name": "size",
|
||||||
|
"type": "integer",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": true,
|
||||||
|
"autoincrement": false
|
||||||
|
},
|
||||||
|
"width": {
|
||||||
|
"name": "width",
|
||||||
|
"type": "integer",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": false,
|
||||||
|
"autoincrement": false
|
||||||
|
},
|
||||||
|
"height": {
|
||||||
|
"name": "height",
|
||||||
|
"type": "integer",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": false,
|
||||||
|
"autoincrement": false
|
||||||
|
},
|
||||||
|
"title": {
|
||||||
|
"name": "title",
|
||||||
|
"type": "text",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": false,
|
||||||
|
"autoincrement": false
|
||||||
|
},
|
||||||
|
"alt": {
|
||||||
|
"name": "alt",
|
||||||
|
"type": "text",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": false,
|
||||||
|
"autoincrement": false
|
||||||
|
},
|
||||||
|
"caption": {
|
||||||
|
"name": "caption",
|
||||||
|
"type": "text",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": false,
|
||||||
|
"autoincrement": false
|
||||||
|
},
|
||||||
|
"author": {
|
||||||
|
"name": "author",
|
||||||
|
"type": "text",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": false,
|
||||||
|
"autoincrement": false
|
||||||
|
},
|
||||||
|
"file_path": {
|
||||||
|
"name": "file_path",
|
||||||
|
"type": "text",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": true,
|
||||||
|
"autoincrement": false
|
||||||
|
},
|
||||||
|
"sidecar_path": {
|
||||||
|
"name": "sidecar_path",
|
||||||
|
"type": "text",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": true,
|
||||||
|
"autoincrement": false
|
||||||
|
},
|
||||||
|
"created_at": {
|
||||||
|
"name": "created_at",
|
||||||
|
"type": "integer",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": true,
|
||||||
|
"autoincrement": false
|
||||||
|
},
|
||||||
|
"updated_at": {
|
||||||
|
"name": "updated_at",
|
||||||
|
"type": "integer",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": true,
|
||||||
|
"autoincrement": false
|
||||||
|
},
|
||||||
|
"checksum": {
|
||||||
|
"name": "checksum",
|
||||||
|
"type": "text",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": false,
|
||||||
|
"autoincrement": false
|
||||||
|
},
|
||||||
|
"tags": {
|
||||||
|
"name": "tags",
|
||||||
|
"type": "text",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": false,
|
||||||
|
"autoincrement": false
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"indexes": {},
|
||||||
|
"foreignKeys": {},
|
||||||
|
"compositePrimaryKeys": {},
|
||||||
|
"uniqueConstraints": {},
|
||||||
|
"checkConstraints": {}
|
||||||
|
},
|
||||||
|
"post_links": {
|
||||||
|
"name": "post_links",
|
||||||
|
"columns": {
|
||||||
|
"id": {
|
||||||
|
"name": "id",
|
||||||
|
"type": "text",
|
||||||
|
"primaryKey": true,
|
||||||
|
"notNull": true,
|
||||||
|
"autoincrement": false
|
||||||
|
},
|
||||||
|
"source_post_id": {
|
||||||
|
"name": "source_post_id",
|
||||||
|
"type": "text",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": true,
|
||||||
|
"autoincrement": false
|
||||||
|
},
|
||||||
|
"target_post_id": {
|
||||||
|
"name": "target_post_id",
|
||||||
|
"type": "text",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": true,
|
||||||
|
"autoincrement": false
|
||||||
|
},
|
||||||
|
"link_text": {
|
||||||
|
"name": "link_text",
|
||||||
|
"type": "text",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": false,
|
||||||
|
"autoincrement": false
|
||||||
|
},
|
||||||
|
"created_at": {
|
||||||
|
"name": "created_at",
|
||||||
|
"type": "integer",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": true,
|
||||||
|
"autoincrement": false
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"indexes": {},
|
||||||
|
"foreignKeys": {},
|
||||||
|
"compositePrimaryKeys": {},
|
||||||
|
"uniqueConstraints": {},
|
||||||
|
"checkConstraints": {}
|
||||||
|
},
|
||||||
|
"post_media": {
|
||||||
|
"name": "post_media",
|
||||||
|
"columns": {
|
||||||
|
"id": {
|
||||||
|
"name": "id",
|
||||||
|
"type": "text",
|
||||||
|
"primaryKey": true,
|
||||||
|
"notNull": true,
|
||||||
|
"autoincrement": false
|
||||||
|
},
|
||||||
|
"project_id": {
|
||||||
|
"name": "project_id",
|
||||||
|
"type": "text",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": true,
|
||||||
|
"autoincrement": false
|
||||||
|
},
|
||||||
|
"post_id": {
|
||||||
|
"name": "post_id",
|
||||||
|
"type": "text",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": true,
|
||||||
|
"autoincrement": false
|
||||||
|
},
|
||||||
|
"media_id": {
|
||||||
|
"name": "media_id",
|
||||||
|
"type": "text",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": true,
|
||||||
|
"autoincrement": false
|
||||||
|
},
|
||||||
|
"sort_order": {
|
||||||
|
"name": "sort_order",
|
||||||
|
"type": "integer",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": true,
|
||||||
|
"autoincrement": false,
|
||||||
|
"default": 0
|
||||||
|
},
|
||||||
|
"created_at": {
|
||||||
|
"name": "created_at",
|
||||||
|
"type": "integer",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": true,
|
||||||
|
"autoincrement": false
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"indexes": {
|
||||||
|
"post_media_post_media_idx": {
|
||||||
|
"name": "post_media_post_media_idx",
|
||||||
|
"columns": [
|
||||||
|
"post_id",
|
||||||
|
"media_id"
|
||||||
|
],
|
||||||
|
"isUnique": true
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"foreignKeys": {},
|
||||||
|
"compositePrimaryKeys": {},
|
||||||
|
"uniqueConstraints": {},
|
||||||
|
"checkConstraints": {}
|
||||||
|
},
|
||||||
|
"posts": {
|
||||||
|
"name": "posts",
|
||||||
|
"columns": {
|
||||||
|
"project_id": {
|
||||||
|
"name": "project_id",
|
||||||
|
"type": "text",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": true,
|
||||||
|
"autoincrement": false
|
||||||
|
},
|
||||||
|
"id": {
|
||||||
|
"name": "id",
|
||||||
|
"type": "text",
|
||||||
|
"primaryKey": true,
|
||||||
|
"notNull": true,
|
||||||
|
"autoincrement": false
|
||||||
|
},
|
||||||
|
"title": {
|
||||||
|
"name": "title",
|
||||||
|
"type": "text",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": true,
|
||||||
|
"autoincrement": false
|
||||||
|
},
|
||||||
|
"slug": {
|
||||||
|
"name": "slug",
|
||||||
|
"type": "text",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": true,
|
||||||
|
"autoincrement": false
|
||||||
|
},
|
||||||
|
"excerpt": {
|
||||||
|
"name": "excerpt",
|
||||||
|
"type": "text",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": false,
|
||||||
|
"autoincrement": false
|
||||||
|
},
|
||||||
|
"content": {
|
||||||
|
"name": "content",
|
||||||
|
"type": "text",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": false,
|
||||||
|
"autoincrement": false
|
||||||
|
},
|
||||||
|
"status": {
|
||||||
|
"name": "status",
|
||||||
|
"type": "text",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": true,
|
||||||
|
"autoincrement": false,
|
||||||
|
"default": "'draft'"
|
||||||
|
},
|
||||||
|
"author": {
|
||||||
|
"name": "author",
|
||||||
|
"type": "text",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": false,
|
||||||
|
"autoincrement": false
|
||||||
|
},
|
||||||
|
"created_at": {
|
||||||
|
"name": "created_at",
|
||||||
|
"type": "integer",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": true,
|
||||||
|
"autoincrement": false
|
||||||
|
},
|
||||||
|
"updated_at": {
|
||||||
|
"name": "updated_at",
|
||||||
|
"type": "integer",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": true,
|
||||||
|
"autoincrement": false
|
||||||
|
},
|
||||||
|
"published_at": {
|
||||||
|
"name": "published_at",
|
||||||
|
"type": "integer",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": false,
|
||||||
|
"autoincrement": false
|
||||||
|
},
|
||||||
|
"file_path": {
|
||||||
|
"name": "file_path",
|
||||||
|
"type": "text",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": true,
|
||||||
|
"autoincrement": false,
|
||||||
|
"default": "''"
|
||||||
|
},
|
||||||
|
"checksum": {
|
||||||
|
"name": "checksum",
|
||||||
|
"type": "text",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": false,
|
||||||
|
"autoincrement": false
|
||||||
|
},
|
||||||
|
"tags": {
|
||||||
|
"name": "tags",
|
||||||
|
"type": "text",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": false,
|
||||||
|
"autoincrement": false
|
||||||
|
},
|
||||||
|
"categories": {
|
||||||
|
"name": "categories",
|
||||||
|
"type": "text",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": false,
|
||||||
|
"autoincrement": false
|
||||||
|
},
|
||||||
|
"published_title": {
|
||||||
|
"name": "published_title",
|
||||||
|
"type": "text",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": false,
|
||||||
|
"autoincrement": false
|
||||||
|
},
|
||||||
|
"published_content": {
|
||||||
|
"name": "published_content",
|
||||||
|
"type": "text",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": false,
|
||||||
|
"autoincrement": false
|
||||||
|
},
|
||||||
|
"published_tags": {
|
||||||
|
"name": "published_tags",
|
||||||
|
"type": "text",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": false,
|
||||||
|
"autoincrement": false
|
||||||
|
},
|
||||||
|
"published_categories": {
|
||||||
|
"name": "published_categories",
|
||||||
|
"type": "text",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": false,
|
||||||
|
"autoincrement": false
|
||||||
|
},
|
||||||
|
"published_excerpt": {
|
||||||
|
"name": "published_excerpt",
|
||||||
|
"type": "text",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": false,
|
||||||
|
"autoincrement": false
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"indexes": {
|
||||||
|
"posts_project_slug_idx": {
|
||||||
|
"name": "posts_project_slug_idx",
|
||||||
|
"columns": [
|
||||||
|
"project_id",
|
||||||
|
"slug"
|
||||||
|
],
|
||||||
|
"isUnique": true
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"foreignKeys": {},
|
||||||
|
"compositePrimaryKeys": {},
|
||||||
|
"uniqueConstraints": {},
|
||||||
|
"checkConstraints": {}
|
||||||
|
},
|
||||||
|
"projects": {
|
||||||
|
"name": "projects",
|
||||||
|
"columns": {
|
||||||
|
"id": {
|
||||||
|
"name": "id",
|
||||||
|
"type": "text",
|
||||||
|
"primaryKey": true,
|
||||||
|
"notNull": true,
|
||||||
|
"autoincrement": false
|
||||||
|
},
|
||||||
|
"name": {
|
||||||
|
"name": "name",
|
||||||
|
"type": "text",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": true,
|
||||||
|
"autoincrement": false
|
||||||
|
},
|
||||||
|
"slug": {
|
||||||
|
"name": "slug",
|
||||||
|
"type": "text",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": true,
|
||||||
|
"autoincrement": false
|
||||||
|
},
|
||||||
|
"description": {
|
||||||
|
"name": "description",
|
||||||
|
"type": "text",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": false,
|
||||||
|
"autoincrement": false
|
||||||
|
},
|
||||||
|
"data_path": {
|
||||||
|
"name": "data_path",
|
||||||
|
"type": "text",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": false,
|
||||||
|
"autoincrement": false
|
||||||
|
},
|
||||||
|
"created_at": {
|
||||||
|
"name": "created_at",
|
||||||
|
"type": "integer",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": true,
|
||||||
|
"autoincrement": false
|
||||||
|
},
|
||||||
|
"updated_at": {
|
||||||
|
"name": "updated_at",
|
||||||
|
"type": "integer",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": true,
|
||||||
|
"autoincrement": false
|
||||||
|
},
|
||||||
|
"is_active": {
|
||||||
|
"name": "is_active",
|
||||||
|
"type": "integer",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": true,
|
||||||
|
"autoincrement": false,
|
||||||
|
"default": false
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"indexes": {
|
||||||
|
"projects_slug_unique": {
|
||||||
|
"name": "projects_slug_unique",
|
||||||
|
"columns": [
|
||||||
|
"slug"
|
||||||
|
],
|
||||||
|
"isUnique": true
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"foreignKeys": {},
|
||||||
|
"compositePrimaryKeys": {},
|
||||||
|
"uniqueConstraints": {},
|
||||||
|
"checkConstraints": {}
|
||||||
|
},
|
||||||
|
"scripts": {
|
||||||
|
"name": "scripts",
|
||||||
|
"columns": {
|
||||||
|
"id": {
|
||||||
|
"name": "id",
|
||||||
|
"type": "text",
|
||||||
|
"primaryKey": true,
|
||||||
|
"notNull": true,
|
||||||
|
"autoincrement": false
|
||||||
|
},
|
||||||
|
"project_id": {
|
||||||
|
"name": "project_id",
|
||||||
|
"type": "text",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": true,
|
||||||
|
"autoincrement": false
|
||||||
|
},
|
||||||
|
"slug": {
|
||||||
|
"name": "slug",
|
||||||
|
"type": "text",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": true,
|
||||||
|
"autoincrement": false
|
||||||
|
},
|
||||||
|
"title": {
|
||||||
|
"name": "title",
|
||||||
|
"type": "text",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": true,
|
||||||
|
"autoincrement": false
|
||||||
|
},
|
||||||
|
"kind": {
|
||||||
|
"name": "kind",
|
||||||
|
"type": "text",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": true,
|
||||||
|
"autoincrement": false,
|
||||||
|
"default": "'utility'"
|
||||||
|
},
|
||||||
|
"entrypoint": {
|
||||||
|
"name": "entrypoint",
|
||||||
|
"type": "text",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": true,
|
||||||
|
"autoincrement": false,
|
||||||
|
"default": "'render'"
|
||||||
|
},
|
||||||
|
"enabled": {
|
||||||
|
"name": "enabled",
|
||||||
|
"type": "integer",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": true,
|
||||||
|
"autoincrement": false,
|
||||||
|
"default": true
|
||||||
|
},
|
||||||
|
"version": {
|
||||||
|
"name": "version",
|
||||||
|
"type": "integer",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": true,
|
||||||
|
"autoincrement": false,
|
||||||
|
"default": 1
|
||||||
|
},
|
||||||
|
"file_path": {
|
||||||
|
"name": "file_path",
|
||||||
|
"type": "text",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": true,
|
||||||
|
"autoincrement": false
|
||||||
|
},
|
||||||
|
"created_at": {
|
||||||
|
"name": "created_at",
|
||||||
|
"type": "integer",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": true,
|
||||||
|
"autoincrement": false
|
||||||
|
},
|
||||||
|
"updated_at": {
|
||||||
|
"name": "updated_at",
|
||||||
|
"type": "integer",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": true,
|
||||||
|
"autoincrement": false
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"indexes": {
|
||||||
|
"scripts_project_slug_idx": {
|
||||||
|
"name": "scripts_project_slug_idx",
|
||||||
|
"columns": [
|
||||||
|
"project_id",
|
||||||
|
"slug"
|
||||||
|
],
|
||||||
|
"isUnique": true
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"foreignKeys": {},
|
||||||
|
"compositePrimaryKeys": {},
|
||||||
|
"uniqueConstraints": {},
|
||||||
|
"checkConstraints": {}
|
||||||
|
},
|
||||||
|
"settings": {
|
||||||
|
"name": "settings",
|
||||||
|
"columns": {
|
||||||
|
"key": {
|
||||||
|
"name": "key",
|
||||||
|
"type": "text",
|
||||||
|
"primaryKey": true,
|
||||||
|
"notNull": true,
|
||||||
|
"autoincrement": false
|
||||||
|
},
|
||||||
|
"value": {
|
||||||
|
"name": "value",
|
||||||
|
"type": "text",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": true,
|
||||||
|
"autoincrement": false
|
||||||
|
},
|
||||||
|
"updated_at": {
|
||||||
|
"name": "updated_at",
|
||||||
|
"type": "integer",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": true,
|
||||||
|
"autoincrement": false
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"indexes": {},
|
||||||
|
"foreignKeys": {},
|
||||||
|
"compositePrimaryKeys": {},
|
||||||
|
"uniqueConstraints": {},
|
||||||
|
"checkConstraints": {}
|
||||||
|
},
|
||||||
|
"tags": {
|
||||||
|
"name": "tags",
|
||||||
|
"columns": {
|
||||||
|
"id": {
|
||||||
|
"name": "id",
|
||||||
|
"type": "text",
|
||||||
|
"primaryKey": true,
|
||||||
|
"notNull": true,
|
||||||
|
"autoincrement": false
|
||||||
|
},
|
||||||
|
"project_id": {
|
||||||
|
"name": "project_id",
|
||||||
|
"type": "text",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": true,
|
||||||
|
"autoincrement": false
|
||||||
|
},
|
||||||
|
"name": {
|
||||||
|
"name": "name",
|
||||||
|
"type": "text",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": true,
|
||||||
|
"autoincrement": false
|
||||||
|
},
|
||||||
|
"color": {
|
||||||
|
"name": "color",
|
||||||
|
"type": "text",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": false,
|
||||||
|
"autoincrement": false
|
||||||
|
},
|
||||||
|
"created_at": {
|
||||||
|
"name": "created_at",
|
||||||
|
"type": "integer",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": true,
|
||||||
|
"autoincrement": false
|
||||||
|
},
|
||||||
|
"updated_at": {
|
||||||
|
"name": "updated_at",
|
||||||
|
"type": "integer",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": true,
|
||||||
|
"autoincrement": false
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"indexes": {
|
||||||
|
"tags_project_name_idx": {
|
||||||
|
"name": "tags_project_name_idx",
|
||||||
|
"columns": [
|
||||||
|
"project_id",
|
||||||
|
"name"
|
||||||
|
],
|
||||||
|
"isUnique": true
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"foreignKeys": {},
|
||||||
|
"compositePrimaryKeys": {},
|
||||||
|
"uniqueConstraints": {},
|
||||||
|
"checkConstraints": {}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"views": {},
|
||||||
|
"enums": {},
|
||||||
|
"_meta": {
|
||||||
|
"schemas": {},
|
||||||
|
"tables": {},
|
||||||
|
"columns": {}
|
||||||
|
},
|
||||||
|
"internal": {
|
||||||
|
"indexes": {}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -36,6 +36,13 @@
|
|||||||
"when": 1771605253203,
|
"when": 1771605253203,
|
||||||
"tag": "0004_overjoyed_paper_doll",
|
"tag": "0004_overjoyed_paper_doll",
|
||||||
"breakpoints": true
|
"breakpoints": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"idx": 5,
|
||||||
|
"version": "6",
|
||||||
|
"when": 1771792324840,
|
||||||
|
"tag": "0005_short_sally_floyd",
|
||||||
|
"breakpoints": true
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
@@ -151,6 +151,24 @@ export const importDefinitions = sqliteTable('import_definitions', {
|
|||||||
updatedAt: integer('updated_at', { mode: 'timestamp' }).notNull(),
|
updatedAt: integer('updated_at', { mode: 'timestamp' }).notNull(),
|
||||||
});
|
});
|
||||||
|
|
||||||
|
// Scripts table - stores metadata for Python scripts persisted in scripts/*.py
|
||||||
|
export const scripts = sqliteTable('scripts', {
|
||||||
|
id: text('id').primaryKey(),
|
||||||
|
projectId: text('project_id').notNull(),
|
||||||
|
slug: text('slug').notNull(),
|
||||||
|
title: text('title').notNull(),
|
||||||
|
kind: text('kind', { enum: ['macro', 'utility', 'transform'] }).notNull().default('utility'),
|
||||||
|
entrypoint: text('entrypoint').notNull().default('render'),
|
||||||
|
enabled: integer('enabled', { mode: 'boolean' }).notNull().default(true),
|
||||||
|
version: integer('version').notNull().default(1),
|
||||||
|
filePath: text('file_path').notNull(),
|
||||||
|
createdAt: integer('created_at', { mode: 'timestamp' }).notNull(),
|
||||||
|
updatedAt: integer('updated_at', { mode: 'timestamp' }).notNull(),
|
||||||
|
}, (table) => ({
|
||||||
|
// Composite unique index: slug must be unique within each project
|
||||||
|
projectSlugIdx: uniqueIndex('scripts_project_slug_idx').on(table.projectId, table.slug),
|
||||||
|
}));
|
||||||
|
|
||||||
// Types for TypeScript
|
// Types for TypeScript
|
||||||
export type Project = typeof projects.$inferSelect;
|
export type Project = typeof projects.$inferSelect;
|
||||||
export type NewProject = typeof projects.$inferInsert;
|
export type NewProject = typeof projects.$inferInsert;
|
||||||
@@ -174,3 +192,5 @@ export type ChatMessage = typeof chatMessages.$inferSelect;
|
|||||||
export type NewChatMessage = typeof chatMessages.$inferInsert;
|
export type NewChatMessage = typeof chatMessages.$inferInsert;
|
||||||
export type ImportDefinition = typeof importDefinitions.$inferSelect;
|
export type ImportDefinition = typeof importDefinitions.$inferSelect;
|
||||||
export type NewImportDefinition = typeof importDefinitions.$inferInsert;
|
export type NewImportDefinition = typeof importDefinitions.$inferInsert;
|
||||||
|
export type Script = typeof scripts.$inferSelect;
|
||||||
|
export type NewScript = typeof scripts.$inferInsert;
|
||||||
|
|||||||
264
src/main/engine/ScriptEngine.ts
Normal file
264
src/main/engine/ScriptEngine.ts
Normal file
@@ -0,0 +1,264 @@
|
|||||||
|
import { EventEmitter } from 'events';
|
||||||
|
import { v4 as uuidv4 } from 'uuid';
|
||||||
|
import * as fs from 'fs/promises';
|
||||||
|
import * as path from 'path';
|
||||||
|
import { app } from 'electron';
|
||||||
|
import { and, desc, eq } from 'drizzle-orm';
|
||||||
|
import { getDatabase } from '../database';
|
||||||
|
import { scripts, type NewScript, type Script } from '../database/schema';
|
||||||
|
|
||||||
|
export type ScriptKind = 'macro' | 'utility' | 'transform';
|
||||||
|
|
||||||
|
export interface ScriptData {
|
||||||
|
id: string;
|
||||||
|
projectId: string;
|
||||||
|
slug: string;
|
||||||
|
title: string;
|
||||||
|
kind: ScriptKind;
|
||||||
|
entrypoint: string;
|
||||||
|
enabled: boolean;
|
||||||
|
version: number;
|
||||||
|
filePath: string;
|
||||||
|
content: string;
|
||||||
|
createdAt: Date;
|
||||||
|
updatedAt: Date;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface CreateScriptInput {
|
||||||
|
title: string;
|
||||||
|
kind: ScriptKind;
|
||||||
|
content: string;
|
||||||
|
slug?: string;
|
||||||
|
entrypoint?: string;
|
||||||
|
enabled?: boolean;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface UpdateScriptInput {
|
||||||
|
title?: string;
|
||||||
|
kind?: ScriptKind;
|
||||||
|
content?: string;
|
||||||
|
slug?: string;
|
||||||
|
entrypoint?: string;
|
||||||
|
enabled?: boolean;
|
||||||
|
}
|
||||||
|
|
||||||
|
export class ScriptEngine extends EventEmitter {
|
||||||
|
private currentProjectId = 'default';
|
||||||
|
private dataDir: string | null = null;
|
||||||
|
|
||||||
|
setProjectContext(projectId: string, dataDir?: string): void {
|
||||||
|
this.currentProjectId = projectId;
|
||||||
|
this.dataDir = dataDir || null;
|
||||||
|
}
|
||||||
|
|
||||||
|
getProjectContext(): string {
|
||||||
|
return this.currentProjectId;
|
||||||
|
}
|
||||||
|
|
||||||
|
async createScript(input: CreateScriptInput): Promise<ScriptData> {
|
||||||
|
const now = new Date();
|
||||||
|
const allScripts = await this.getAllScriptRows();
|
||||||
|
const desiredSlug = this.normalizeSlug(input.slug || input.title || 'script');
|
||||||
|
const uniqueSlug = this.ensureUniqueSlug(desiredSlug, allScripts);
|
||||||
|
const scriptId = uuidv4();
|
||||||
|
const filePath = this.getScriptFilePath(uniqueSlug);
|
||||||
|
|
||||||
|
await fs.mkdir(this.getScriptsDir(), { recursive: true });
|
||||||
|
await fs.writeFile(filePath, input.content, 'utf-8');
|
||||||
|
|
||||||
|
const row: NewScript = {
|
||||||
|
id: scriptId,
|
||||||
|
projectId: this.currentProjectId,
|
||||||
|
slug: uniqueSlug,
|
||||||
|
title: input.title,
|
||||||
|
kind: input.kind,
|
||||||
|
entrypoint: input.entrypoint || 'render',
|
||||||
|
enabled: input.enabled ?? true,
|
||||||
|
version: 1,
|
||||||
|
filePath,
|
||||||
|
createdAt: now,
|
||||||
|
updatedAt: now,
|
||||||
|
};
|
||||||
|
|
||||||
|
await getDatabase().getLocal().insert(scripts).values(row);
|
||||||
|
|
||||||
|
const created = await this.toScriptData(row as Script);
|
||||||
|
this.emit('scriptCreated', created);
|
||||||
|
return created;
|
||||||
|
}
|
||||||
|
|
||||||
|
async updateScript(id: string, updates: UpdateScriptInput): Promise<ScriptData | null> {
|
||||||
|
const existing = await this.getScriptRow(id);
|
||||||
|
if (!existing) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
const allScripts = await this.getAllScriptRows();
|
||||||
|
const desiredSlug = this.normalizeSlug(updates.slug || updates.title || existing.slug);
|
||||||
|
const nextSlug = this.ensureUniqueSlug(desiredSlug, allScripts, existing.id);
|
||||||
|
const nextFilePath = this.getScriptFilePath(nextSlug);
|
||||||
|
const now = new Date();
|
||||||
|
|
||||||
|
if (existing.filePath !== nextFilePath) {
|
||||||
|
await fs.mkdir(this.getScriptsDir(), { recursive: true });
|
||||||
|
await fs.rename(existing.filePath, nextFilePath);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (typeof updates.content === 'string') {
|
||||||
|
await fs.writeFile(nextFilePath, updates.content, 'utf-8');
|
||||||
|
}
|
||||||
|
|
||||||
|
await getDatabase().getLocal()
|
||||||
|
.update(scripts)
|
||||||
|
.set({
|
||||||
|
title: updates.title ?? existing.title,
|
||||||
|
slug: nextSlug,
|
||||||
|
kind: updates.kind ?? existing.kind,
|
||||||
|
entrypoint: updates.entrypoint ?? existing.entrypoint,
|
||||||
|
enabled: updates.enabled ?? existing.enabled,
|
||||||
|
filePath: nextFilePath,
|
||||||
|
version: existing.version + 1,
|
||||||
|
updatedAt: now,
|
||||||
|
})
|
||||||
|
.where(and(eq(scripts.id, existing.id), eq(scripts.projectId, this.currentProjectId)));
|
||||||
|
|
||||||
|
const updatedRow = await this.getScriptRow(existing.id);
|
||||||
|
if (!updatedRow) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
const updated = await this.toScriptData(updatedRow);
|
||||||
|
this.emit('scriptUpdated', updated);
|
||||||
|
return updated;
|
||||||
|
}
|
||||||
|
|
||||||
|
async deleteScript(id: string): Promise<boolean> {
|
||||||
|
const existing = await this.getScriptRow(id);
|
||||||
|
if (!existing) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
await getDatabase().getLocal()
|
||||||
|
.delete(scripts)
|
||||||
|
.where(and(eq(scripts.id, existing.id), eq(scripts.projectId, this.currentProjectId)));
|
||||||
|
|
||||||
|
try {
|
||||||
|
await fs.unlink(existing.filePath);
|
||||||
|
} catch (error) {
|
||||||
|
const fsError = error as NodeJS.ErrnoException;
|
||||||
|
if (fsError.code !== 'ENOENT') {
|
||||||
|
throw error;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
this.emit('scriptDeleted', id);
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
async getScript(id: string): Promise<ScriptData | null> {
|
||||||
|
const row = await this.getScriptRow(id);
|
||||||
|
if (!row) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
return this.toScriptData(row);
|
||||||
|
}
|
||||||
|
|
||||||
|
async getAllScripts(): Promise<ScriptData[]> {
|
||||||
|
const rows = await this.getAllScriptRows();
|
||||||
|
return Promise.all(rows.map((item) => this.toScriptData(item)));
|
||||||
|
}
|
||||||
|
|
||||||
|
private async getScriptRow(id: string): Promise<Script | null> {
|
||||||
|
const rows = await this.getAllScriptRows();
|
||||||
|
return rows.find((item) => item.id === id) || null;
|
||||||
|
}
|
||||||
|
|
||||||
|
private async getAllScriptRows(): Promise<Script[]> {
|
||||||
|
return getDatabase().getLocal()
|
||||||
|
.select()
|
||||||
|
.from(scripts)
|
||||||
|
.where(eq(scripts.projectId, this.currentProjectId))
|
||||||
|
.orderBy(desc(scripts.updatedAt))
|
||||||
|
.all();
|
||||||
|
}
|
||||||
|
|
||||||
|
private async toScriptData(row: Script): Promise<ScriptData> {
|
||||||
|
let content = '';
|
||||||
|
try {
|
||||||
|
content = await fs.readFile(row.filePath, 'utf-8');
|
||||||
|
} catch (error) {
|
||||||
|
const fsError = error as NodeJS.ErrnoException;
|
||||||
|
if (fsError.code !== 'ENOENT') {
|
||||||
|
throw error;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
id: row.id,
|
||||||
|
projectId: row.projectId,
|
||||||
|
slug: row.slug,
|
||||||
|
title: row.title,
|
||||||
|
kind: row.kind,
|
||||||
|
entrypoint: row.entrypoint,
|
||||||
|
enabled: row.enabled,
|
||||||
|
version: row.version,
|
||||||
|
filePath: row.filePath,
|
||||||
|
content,
|
||||||
|
createdAt: row.createdAt,
|
||||||
|
updatedAt: row.updatedAt,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
private getDataDir(): string {
|
||||||
|
if (this.dataDir) {
|
||||||
|
return this.dataDir;
|
||||||
|
}
|
||||||
|
|
||||||
|
return path.join(app.getPath('userData'), 'projects', this.currentProjectId);
|
||||||
|
}
|
||||||
|
|
||||||
|
private getScriptsDir(): string {
|
||||||
|
return path.join(this.getDataDir(), 'scripts');
|
||||||
|
}
|
||||||
|
|
||||||
|
private getScriptFilePath(slug: string): string {
|
||||||
|
return path.join(this.getScriptsDir(), `${slug}.py`);
|
||||||
|
}
|
||||||
|
|
||||||
|
private normalizeSlug(value: string): string {
|
||||||
|
const normalized = value
|
||||||
|
.toLowerCase()
|
||||||
|
.replace(/[^a-z0-9]+/g, '-')
|
||||||
|
.replace(/^-|-$/g, '');
|
||||||
|
return normalized || 'script';
|
||||||
|
}
|
||||||
|
|
||||||
|
private ensureUniqueSlug(slug: string, rows: Script[], excludeId?: string): string {
|
||||||
|
const baseSlug = slug;
|
||||||
|
const taken = new Set(
|
||||||
|
rows
|
||||||
|
.filter((item) => item.id !== excludeId)
|
||||||
|
.map((item) => item.slug)
|
||||||
|
);
|
||||||
|
|
||||||
|
if (!taken.has(baseSlug)) {
|
||||||
|
return baseSlug;
|
||||||
|
}
|
||||||
|
|
||||||
|
let suffix = 2;
|
||||||
|
while (taken.has(`${baseSlug}-${suffix}`)) {
|
||||||
|
suffix += 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
return `${baseSlug}-${suffix}`;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
let scriptEngineInstance: ScriptEngine | null = null;
|
||||||
|
|
||||||
|
export function getScriptEngine(): ScriptEngine {
|
||||||
|
if (!scriptEngineInstance) {
|
||||||
|
scriptEngineInstance = new ScriptEngine();
|
||||||
|
}
|
||||||
|
return scriptEngineInstance;
|
||||||
|
}
|
||||||
@@ -100,3 +100,11 @@ export {
|
|||||||
type MenuDocument,
|
type MenuDocument,
|
||||||
type MenuItemKind,
|
type MenuItemKind,
|
||||||
} from './MenuEngine';
|
} from './MenuEngine';
|
||||||
|
export {
|
||||||
|
ScriptEngine,
|
||||||
|
getScriptEngine,
|
||||||
|
type ScriptData,
|
||||||
|
type ScriptKind,
|
||||||
|
type CreateScriptInput,
|
||||||
|
type UpdateScriptInput,
|
||||||
|
} from './ScriptEngine';
|
||||||
|
|||||||
@@ -9,6 +9,7 @@ import { getMetaEngine } from '../engine/MetaEngine';
|
|||||||
import { getMenuEngine, type MenuDocument } from '../engine/MenuEngine';
|
import { getMenuEngine, type MenuDocument } from '../engine/MenuEngine';
|
||||||
import { getTagEngine } from '../engine/TagEngine';
|
import { getTagEngine } from '../engine/TagEngine';
|
||||||
import { getPostMediaEngine } from '../engine/PostMediaEngine';
|
import { getPostMediaEngine } from '../engine/PostMediaEngine';
|
||||||
|
import { getScriptEngine, type CreateScriptInput, type UpdateScriptInput } from '../engine/ScriptEngine';
|
||||||
import { getGitEngine } from '../engine/GitEngine';
|
import { getGitEngine } from '../engine/GitEngine';
|
||||||
import { taskManager, TaskProgress } from '../engine/TaskManager';
|
import { taskManager, TaskProgress } from '../engine/TaskManager';
|
||||||
import { getDatabase } from '../database';
|
import { getDatabase } from '../database';
|
||||||
@@ -284,11 +285,13 @@ export function registerIpcHandlers(): void {
|
|||||||
const metaEngine = getMetaEngine();
|
const metaEngine = getMetaEngine();
|
||||||
const menuEngine = getMenuEngine();
|
const menuEngine = getMenuEngine();
|
||||||
const tagEngine = getTagEngine();
|
const tagEngine = getTagEngine();
|
||||||
|
const scriptEngine = getScriptEngine();
|
||||||
postEngine.setProjectContext(project.id, dataDir);
|
postEngine.setProjectContext(project.id, dataDir);
|
||||||
mediaEngine.setProjectContext(project.id, dataDir, dataDir);
|
mediaEngine.setProjectContext(project.id, dataDir, dataDir);
|
||||||
metaEngine.setProjectContext(project.id, dataDir);
|
metaEngine.setProjectContext(project.id, dataDir);
|
||||||
menuEngine.setProjectContext(project.id, dataDir);
|
menuEngine.setProjectContext(project.id, dataDir);
|
||||||
tagEngine.setProjectContext(project.id, dataDir);
|
tagEngine.setProjectContext(project.id, dataDir);
|
||||||
|
scriptEngine.setProjectContext(project.id, dataDir);
|
||||||
const postMediaEngine = getPostMediaEngine();
|
const postMediaEngine = getPostMediaEngine();
|
||||||
postMediaEngine.setProjectContext(project.id);
|
postMediaEngine.setProjectContext(project.id);
|
||||||
|
|
||||||
@@ -322,11 +325,13 @@ export function registerIpcHandlers(): void {
|
|||||||
const metaEngine = getMetaEngine();
|
const metaEngine = getMetaEngine();
|
||||||
const menuEngine = getMenuEngine();
|
const menuEngine = getMenuEngine();
|
||||||
const tagEngine = getTagEngine();
|
const tagEngine = getTagEngine();
|
||||||
|
const scriptEngine = getScriptEngine();
|
||||||
postEngine.setProjectContext(project.id, dataDir);
|
postEngine.setProjectContext(project.id, dataDir);
|
||||||
mediaEngine.setProjectContext(project.id, dataDir, dataDir);
|
mediaEngine.setProjectContext(project.id, dataDir, dataDir);
|
||||||
metaEngine.setProjectContext(project.id, dataDir);
|
metaEngine.setProjectContext(project.id, dataDir);
|
||||||
menuEngine.setProjectContext(project.id, dataDir);
|
menuEngine.setProjectContext(project.id, dataDir);
|
||||||
tagEngine.setProjectContext(project.id, dataDir);
|
tagEngine.setProjectContext(project.id, dataDir);
|
||||||
|
scriptEngine.setProjectContext(project.id, dataDir);
|
||||||
const postMediaEngine = getPostMediaEngine();
|
const postMediaEngine = getPostMediaEngine();
|
||||||
postMediaEngine.setProjectContext(project.id);
|
postMediaEngine.setProjectContext(project.id);
|
||||||
|
|
||||||
@@ -723,6 +728,33 @@ export function registerIpcHandlers(): void {
|
|||||||
return engine.regenerateMissingThumbnails();
|
return engine.regenerateMissingThumbnails();
|
||||||
});
|
});
|
||||||
|
|
||||||
|
// ============ Script Handlers ============
|
||||||
|
|
||||||
|
safeHandle('scripts:create', async (_, data: CreateScriptInput) => {
|
||||||
|
const engine = getScriptEngine();
|
||||||
|
return engine.createScript(data);
|
||||||
|
});
|
||||||
|
|
||||||
|
safeHandle('scripts:update', async (_, id: string, data: UpdateScriptInput) => {
|
||||||
|
const engine = getScriptEngine();
|
||||||
|
return engine.updateScript(id, data);
|
||||||
|
});
|
||||||
|
|
||||||
|
safeHandle('scripts:delete', async (_, id: string) => {
|
||||||
|
const engine = getScriptEngine();
|
||||||
|
return engine.deleteScript(id);
|
||||||
|
});
|
||||||
|
|
||||||
|
safeHandle('scripts:get', async (_, id: string) => {
|
||||||
|
const engine = getScriptEngine();
|
||||||
|
return engine.getScript(id);
|
||||||
|
});
|
||||||
|
|
||||||
|
safeHandle('scripts:getAll', async () => {
|
||||||
|
const engine = getScriptEngine();
|
||||||
|
return engine.getAllScripts();
|
||||||
|
});
|
||||||
|
|
||||||
// ============ Task Handlers ============
|
// ============ Task Handlers ============
|
||||||
|
|
||||||
safeHandle('tasks:getAll', async () => {
|
safeHandle('tasks:getAll', async () => {
|
||||||
|
|||||||
@@ -101,6 +101,15 @@ export const electronAPI: ElectronAPI = {
|
|||||||
regenerateMissingThumbnails: () => ipcRenderer.invoke('media:regenerateMissingThumbnails'),
|
regenerateMissingThumbnails: () => ipcRenderer.invoke('media:regenerateMissingThumbnails'),
|
||||||
},
|
},
|
||||||
|
|
||||||
|
// Scripts
|
||||||
|
scripts: {
|
||||||
|
create: (data: { title: string; kind: import('./shared/electronApi').ScriptKind; content: string; slug?: string; entrypoint?: string; enabled?: boolean }) => ipcRenderer.invoke('scripts:create', data),
|
||||||
|
update: (id: string, data: { title?: string; kind?: import('./shared/electronApi').ScriptKind; content?: string; slug?: string; entrypoint?: string; enabled?: boolean }) => ipcRenderer.invoke('scripts:update', id, data),
|
||||||
|
delete: (id: string) => ipcRenderer.invoke('scripts:delete', id),
|
||||||
|
get: (id: string) => ipcRenderer.invoke('scripts:get', id),
|
||||||
|
getAll: () => ipcRenderer.invoke('scripts:getAll'),
|
||||||
|
},
|
||||||
|
|
||||||
// Post-Media Links
|
// Post-Media Links
|
||||||
postMedia: {
|
postMedia: {
|
||||||
link: (postId: string, mediaId: string) => ipcRenderer.invoke('postMedia:link', postId, mediaId),
|
link: (postId: string, mediaId: string) => ipcRenderer.invoke('postMedia:link', postId, mediaId),
|
||||||
|
|||||||
@@ -133,6 +133,23 @@ export interface MediaSearchResult {
|
|||||||
createdAt: string;
|
createdAt: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export type ScriptKind = 'macro' | 'utility' | 'transform';
|
||||||
|
|
||||||
|
export interface ScriptData {
|
||||||
|
id: string;
|
||||||
|
projectId: string;
|
||||||
|
slug: string;
|
||||||
|
title: string;
|
||||||
|
kind: ScriptKind;
|
||||||
|
entrypoint: string;
|
||||||
|
enabled: boolean;
|
||||||
|
version: number;
|
||||||
|
filePath: string;
|
||||||
|
content: string;
|
||||||
|
createdAt: string;
|
||||||
|
updatedAt: string;
|
||||||
|
}
|
||||||
|
|
||||||
export interface TaskProgress {
|
export interface TaskProgress {
|
||||||
taskId: string;
|
taskId: string;
|
||||||
name: string;
|
name: string;
|
||||||
@@ -528,6 +545,27 @@ export interface ElectronAPI {
|
|||||||
getTags: () => Promise<string[]>;
|
getTags: () => Promise<string[]>;
|
||||||
getTagsWithCounts: () => Promise<TagCount[]>;
|
getTagsWithCounts: () => Promise<TagCount[]>;
|
||||||
};
|
};
|
||||||
|
scripts: {
|
||||||
|
create: (data: {
|
||||||
|
title: string;
|
||||||
|
kind: ScriptKind;
|
||||||
|
content: string;
|
||||||
|
slug?: string;
|
||||||
|
entrypoint?: string;
|
||||||
|
enabled?: boolean;
|
||||||
|
}) => Promise<ScriptData>;
|
||||||
|
update: (id: string, data: {
|
||||||
|
title?: string;
|
||||||
|
kind?: ScriptKind;
|
||||||
|
content?: string;
|
||||||
|
slug?: string;
|
||||||
|
entrypoint?: string;
|
||||||
|
enabled?: boolean;
|
||||||
|
}) => Promise<ScriptData | null>;
|
||||||
|
delete: (id: string) => Promise<boolean>;
|
||||||
|
get: (id: string) => Promise<ScriptData | null>;
|
||||||
|
getAll: () => Promise<ScriptData[]>;
|
||||||
|
};
|
||||||
postMedia: {
|
postMedia: {
|
||||||
link: (postId: string, mediaId: string) => Promise<MediaLinkData>;
|
link: (postId: string, mediaId: string) => Promise<MediaLinkData>;
|
||||||
unlink: (postId: string, mediaId: string) => Promise<void>;
|
unlink: (postId: string, mediaId: string) => Promise<void>;
|
||||||
|
|||||||
@@ -30,6 +30,12 @@ const MediaIcon = () => (
|
|||||||
</svg>
|
</svg>
|
||||||
);
|
);
|
||||||
|
|
||||||
|
const ScriptsIcon = () => (
|
||||||
|
<svg width="24" height="24" viewBox="0 0 24 24" fill="currentColor">
|
||||||
|
<path d="M20 3H4a1 1 0 0 0-1 1v11a1 1 0 0 0 1 1h7v2H8v2h8v-2h-3v-2h7a1 1 0 0 0 1-1V4a1 1 0 0 0-1-1zM5 14V5h14v9H5zm2-7.5L9.5 9 7 11.5l1.4 1.4L12.3 9 8.4 5.1 7 6.5zm6.5 5.5h4v-2h-4v2z"/>
|
||||||
|
</svg>
|
||||||
|
);
|
||||||
|
|
||||||
const SettingsIcon = () => (
|
const SettingsIcon = () => (
|
||||||
<svg width="24" height="24" viewBox="0 0 24 24" fill="currentColor">
|
<svg width="24" height="24" viewBox="0 0 24 24" fill="currentColor">
|
||||||
<path d="M19.14 12.94c.04-.31.06-.63.06-.94 0-.31-.02-.63-.06-.94l2.03-1.58c.18-.14.23-.41.12-.61l-1.92-3.32c-.12-.22-.37-.29-.59-.22l-2.39.96c-.5-.38-1.03-.7-1.62-.94l-.36-2.54c-.04-.24-.24-.41-.48-.41h-3.84c-.24 0-.43.17-.47.41l-.36 2.54c-.59.24-1.13.57-1.62.94l-2.39-.96c-.22-.08-.47 0-.59.22L2.74 8.87c-.12.21-.08.47.12.61l2.03 1.58c-.04.31-.06.63-.06.94s.02.63.06.94l-2.03 1.58c-.18.14-.23.41-.12.61l1.92 3.32c.12.22.37.29.59.22l2.39-.96c.5.38 1.03.7 1.62.94l.36 2.54c.05.24.24.41.48.41h3.84c.24 0 .44-.17.47-.41l.36-2.54c.59-.24 1.13-.56 1.62-.94l2.39.96c.22.08.47 0 .59-.22l1.92-3.32c.12-.22.07-.47-.12-.61l-2.01-1.58zM12 15.6c-1.98 0-3.6-1.62-3.6-3.6s1.62-3.6 3.6-3.6 3.6 1.62 3.6 3.6-1.62 3.6-3.6 3.6z"/>
|
<path d="M19.14 12.94c.04-.31.06-.63.06-.94 0-.31-.02-.63-.06-.94l2.03-1.58c.18-.14.23-.41.12-.61l-1.92-3.32c-.12-.22-.37-.29-.59-.22l-2.39.96c-.5-.38-1.03-.7-1.62-.94l-.36-2.54c-.04-.24-.24-.41-.48-.41h-3.84c-.24 0-.43.17-.47.41l-.36 2.54c-.59.24-1.13.57-1.62.94l-2.39-.96c-.22-.08-.47 0-.59.22L2.74 8.87c-.12.21-.08.47.12.61l2.03 1.58c-.04.31-.06.63-.06.94s.02.63.06.94l-2.03 1.58c-.18.14-.23.41-.12.61l1.92 3.32c.12.22.37.29.59.22l2.39-.96c.5.38 1.03.7 1.62.94l.36 2.54c.05.24.24.41.48.41h3.84c.24 0 .44-.17.47-.41l.36-2.54c.59-.24 1.13-.56 1.62-.94l2.39.96c.22.08.47 0 .59-.22l1.92-3.32c.12-.22.07-.47-.12-.61l-2.01-1.58zM12 15.6c-1.98 0-3.6-1.62-3.6-3.6s1.62-3.6 3.6-3.6 3.6 1.62 3.6 3.6-1.62 3.6-3.6 3.6z"/>
|
||||||
@@ -170,6 +176,13 @@ export const ActivityBar: React.FC = () => {
|
|||||||
>
|
>
|
||||||
<MediaIcon />
|
<MediaIcon />
|
||||||
</button>
|
</button>
|
||||||
|
<button
|
||||||
|
className={`activity-bar-item ${isActivityActive(snapshot, 'scripts') ? 'active' : ''}`}
|
||||||
|
onClick={() => executeActivityClick('scripts')}
|
||||||
|
title={getTitle('scripts')}
|
||||||
|
>
|
||||||
|
<ScriptsIcon />
|
||||||
|
</button>
|
||||||
<button
|
<button
|
||||||
className={`activity-bar-item ${isActivityActive(snapshot, 'tags') ? 'active' : ''}`}
|
className={`activity-bar-item ${isActivityActive(snapshot, 'tags') ? 'active' : ''}`}
|
||||||
onClick={() => executeActivityClick('tags')}
|
onClick={() => executeActivityClick('tags')}
|
||||||
|
|||||||
@@ -19,6 +19,7 @@ import { MetadataDiffPanel } from '../MetadataDiffPanel';
|
|||||||
import { GitDiffView } from '../GitDiffView/GitDiffView';
|
import { GitDiffView } from '../GitDiffView/GitDiffView';
|
||||||
import { DocumentationView } from '../DocumentationView/DocumentationView';
|
import { DocumentationView } from '../DocumentationView/DocumentationView';
|
||||||
import { SiteValidationView } from '../SiteValidationView';
|
import { SiteValidationView } from '../SiteValidationView';
|
||||||
|
import { ScriptsView } from '../ScriptsView/ScriptsView';
|
||||||
import { AutoSaveManager, getContrastColor } from '../../utils';
|
import { AutoSaveManager, getContrastColor } from '../../utils';
|
||||||
import { InsertModal } from '../InsertModal';
|
import { InsertModal } from '../InsertModal';
|
||||||
import { AISuggestionsModal, AISuggestions } from '../AISuggestionsModal/AISuggestionsModal';
|
import { AISuggestionsModal, AISuggestions } from '../AISuggestionsModal/AISuggestionsModal';
|
||||||
@@ -1796,6 +1797,7 @@ export const Editor: React.FC = () => {
|
|||||||
: <Dashboard />,
|
: <Dashboard />,
|
||||||
documentation: () => <DocumentationView />,
|
documentation: () => <DocumentationView />,
|
||||||
'site-validation': () => <SiteValidationView />,
|
'site-validation': () => <SiteValidationView />,
|
||||||
|
scripts: () => <ScriptsView scriptId={editorRoute.tabId} />,
|
||||||
post: () => (editorRoute.tabId ? <PostEditor key={editorRoute.tabId} postId={editorRoute.tabId} /> : <Dashboard />),
|
post: () => (editorRoute.tabId ? <PostEditor key={editorRoute.tabId} postId={editorRoute.tabId} /> : <Dashboard />),
|
||||||
media: () => (editorRoute.tabId ? <MediaEditor key={editorRoute.tabId} mediaId={editorRoute.tabId} /> : <Dashboard />),
|
media: () => (editorRoute.tabId ? <MediaEditor key={editorRoute.tabId} mediaId={editorRoute.tabId} /> : <Dashboard />),
|
||||||
dashboard: () => <Dashboard />,
|
dashboard: () => <Dashboard />,
|
||||||
|
|||||||
@@ -86,6 +86,22 @@
|
|||||||
gap: 4px;
|
gap: 4px;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.output-list {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
gap: 6px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.output-item {
|
||||||
|
background-color: var(--vscode-sideBar-background);
|
||||||
|
border-radius: 4px;
|
||||||
|
padding: 8px;
|
||||||
|
font-size: 12px;
|
||||||
|
color: var(--vscode-editor-foreground);
|
||||||
|
white-space: pre-wrap;
|
||||||
|
word-break: break-word;
|
||||||
|
}
|
||||||
|
|
||||||
.task-group-row {
|
.task-group-row {
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
|
|||||||
@@ -43,6 +43,7 @@ export const Panel: React.FC = () => {
|
|||||||
panelVisible,
|
panelVisible,
|
||||||
panelActiveTab,
|
panelActiveTab,
|
||||||
setPanelActiveTab,
|
setPanelActiveTab,
|
||||||
|
panelOutputEntries,
|
||||||
tasks,
|
tasks,
|
||||||
tabs,
|
tabs,
|
||||||
activeTabId,
|
activeTabId,
|
||||||
@@ -383,7 +384,17 @@ export const Panel: React.FC = () => {
|
|||||||
)}
|
)}
|
||||||
|
|
||||||
{effectiveActivePanelTab === 'output' && (
|
{effectiveActivePanelTab === 'output' && (
|
||||||
<div className="panel-empty">{t('panel.noOutput')}</div>
|
panelOutputEntries.length === 0 ? (
|
||||||
|
<div className="panel-empty">{t('panel.noOutput')}</div>
|
||||||
|
) : (
|
||||||
|
<div className="output-list">
|
||||||
|
{panelOutputEntries.map((entry) => (
|
||||||
|
<div key={entry.id} className={`output-item output-${entry.kind}`}>
|
||||||
|
{entry.message}
|
||||||
|
</div>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
)
|
||||||
)}
|
)}
|
||||||
|
|
||||||
{effectiveActivePanelTab === 'post-links' && (
|
{effectiveActivePanelTab === 'post-links' && (
|
||||||
|
|||||||
32
src/renderer/components/ScriptsView/ScriptsView.css
Normal file
32
src/renderer/components/ScriptsView/ScriptsView.css
Normal file
@@ -0,0 +1,32 @@
|
|||||||
|
.scripts-view {
|
||||||
|
display: flex;
|
||||||
|
flex: 1;
|
||||||
|
min-height: 0;
|
||||||
|
width: 100%;
|
||||||
|
}
|
||||||
|
|
||||||
|
.scripts-editor {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
flex: 1;
|
||||||
|
min-height: 0;
|
||||||
|
width: 100%;
|
||||||
|
padding: 10px;
|
||||||
|
gap: 8px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.scripts-toolbar {
|
||||||
|
display: flex;
|
||||||
|
justify-content: flex-end;
|
||||||
|
}
|
||||||
|
|
||||||
|
.scripts-label {
|
||||||
|
font-size: 12px;
|
||||||
|
color: var(--vscode-descriptionForeground);
|
||||||
|
}
|
||||||
|
|
||||||
|
.scripts-textarea {
|
||||||
|
width: 100%;
|
||||||
|
flex: 1;
|
||||||
|
min-height: 0;
|
||||||
|
}
|
||||||
114
src/renderer/components/ScriptsView/ScriptsView.tsx
Normal file
114
src/renderer/components/ScriptsView/ScriptsView.tsx
Normal file
@@ -0,0 +1,114 @@
|
|||||||
|
import React, { useEffect, useState } from 'react';
|
||||||
|
import type { ScriptData } from '../../../main/shared/electronApi';
|
||||||
|
import { useAppStore } from '../../store';
|
||||||
|
import { getPythonRuntimeManager } from '../../python/runtimeManagerInstance';
|
||||||
|
import { useI18n } from '../../i18n';
|
||||||
|
import './ScriptsView.css';
|
||||||
|
|
||||||
|
interface ScriptsViewProps {
|
||||||
|
scriptId: string | null;
|
||||||
|
}
|
||||||
|
|
||||||
|
export const ScriptsView: React.FC<ScriptsViewProps> = ({ scriptId }) => {
|
||||||
|
const { t } = useI18n();
|
||||||
|
const appendPanelOutputEntry = useAppStore((state) => state.appendPanelOutputEntry);
|
||||||
|
const [script, setScript] = useState<ScriptData | null>(null);
|
||||||
|
const [scriptContent, setScriptContent] = useState('');
|
||||||
|
const [isRunning, setIsRunning] = useState(false);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
let cancelled = false;
|
||||||
|
|
||||||
|
const loadScript = async () => {
|
||||||
|
if (!scriptId) {
|
||||||
|
setScript(null);
|
||||||
|
setScriptContent('');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const item = await window.electronAPI?.scripts.get(scriptId);
|
||||||
|
if (cancelled || !item) {
|
||||||
|
setScript(null);
|
||||||
|
setScriptContent('');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
setScript(item);
|
||||||
|
setScriptContent(item.content || '');
|
||||||
|
};
|
||||||
|
|
||||||
|
void loadScript();
|
||||||
|
|
||||||
|
return () => {
|
||||||
|
cancelled = true;
|
||||||
|
};
|
||||||
|
}, [scriptId]);
|
||||||
|
|
||||||
|
const handleRunScript = async () => {
|
||||||
|
if (!script || isRunning) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
setIsRunning(true);
|
||||||
|
|
||||||
|
try {
|
||||||
|
const runtimeManager = getPythonRuntimeManager();
|
||||||
|
const result = await runtimeManager.execute(scriptContent);
|
||||||
|
|
||||||
|
const now = new Date().toISOString();
|
||||||
|
if (result.result.trim().length > 0) {
|
||||||
|
appendPanelOutputEntry({
|
||||||
|
id: `output-${Date.now()}-result`,
|
||||||
|
message: result.result,
|
||||||
|
createdAt: now,
|
||||||
|
kind: 'result',
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
if (result.stdout.trim().length > 0) {
|
||||||
|
appendPanelOutputEntry({
|
||||||
|
id: `output-${Date.now()}-stdout`,
|
||||||
|
message: result.stdout,
|
||||||
|
createdAt: now,
|
||||||
|
kind: 'stdout',
|
||||||
|
});
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
appendPanelOutputEntry({
|
||||||
|
id: `output-${Date.now()}-error`,
|
||||||
|
message: error instanceof Error ? error.message : String(error),
|
||||||
|
createdAt: new Date().toISOString(),
|
||||||
|
kind: 'error',
|
||||||
|
});
|
||||||
|
} finally {
|
||||||
|
useAppStore.setState({
|
||||||
|
panelVisible: true,
|
||||||
|
panelActiveTab: 'output',
|
||||||
|
});
|
||||||
|
setIsRunning(false);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="scripts-view">
|
||||||
|
<div className="scripts-editor">
|
||||||
|
<div className="scripts-toolbar">
|
||||||
|
<button type="button" onClick={handleRunScript} disabled={!script || isRunning}>
|
||||||
|
{t('scripts.run')}
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<label className="scripts-label" htmlFor="scripts-content">
|
||||||
|
{t('scripts.content')}
|
||||||
|
</label>
|
||||||
|
<textarea
|
||||||
|
id="scripts-content"
|
||||||
|
className="scripts-textarea"
|
||||||
|
value={scriptContent}
|
||||||
|
onChange={(event) => setScriptContent(event.target.value)}
|
||||||
|
disabled={!script}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
@@ -8,7 +8,7 @@ import { scrollToSettingsSection, SettingsCategory } from '../SettingsView/Setti
|
|||||||
import { scrollToTagsSection, TagsCategory } from '../TagsView';
|
import { scrollToTagsSection, TagsCategory } from '../TagsView';
|
||||||
import { activateSidebarSection } from '../../navigation/sectionActivation';
|
import { activateSidebarSection } from '../../navigation/sectionActivation';
|
||||||
import { getPersistedSidebarSection, setPersistedSidebarSection } from '../../navigation/sidebarUiPersistence';
|
import { getPersistedSidebarSection, setPersistedSidebarSection } from '../../navigation/sidebarUiPersistence';
|
||||||
import { openChatTab, openEntityTab, openImportTab, openSingletonToolTab } from '../../navigation/tabPolicy';
|
import { openChatTab, openEntityTab, openImportTab, openScriptTab, openSingletonToolTab } from '../../navigation/tabPolicy';
|
||||||
import { createAndFocusPost } from '../../navigation/postCreation';
|
import { createAndFocusPost } from '../../navigation/postCreation';
|
||||||
import type { SidebarView } from '../../navigation/sidebarViewRegistry';
|
import type { SidebarView } from '../../navigation/sidebarViewRegistry';
|
||||||
import { useI18n } from '../../i18n';
|
import { useI18n } from '../../i18n';
|
||||||
@@ -1666,6 +1666,125 @@ const ImportList: React.FC = () => {
|
|||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const ScriptsList: React.FC = () => {
|
||||||
|
const { t } = useI18n();
|
||||||
|
const { openTab, activeTabId } = useAppStore();
|
||||||
|
const [scripts, setScripts] = useState<Array<{ id: string; title: string }>>([]);
|
||||||
|
const [isLoading, setIsLoading] = useState(true);
|
||||||
|
|
||||||
|
const loadScripts = useCallback(async () => {
|
||||||
|
const items = await window.electronAPI?.scripts.getAll();
|
||||||
|
if (!items) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
setScripts(items.map((item) => ({ id: item.id, title: item.title })));
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
let cancelled = false;
|
||||||
|
|
||||||
|
const loadInitialScripts = async () => {
|
||||||
|
setIsLoading(true);
|
||||||
|
try {
|
||||||
|
const items = await window.electronAPI?.scripts.getAll();
|
||||||
|
if (cancelled) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
setScripts((items ?? []).map((item) => ({ id: item.id, title: item.title })));
|
||||||
|
} finally {
|
||||||
|
if (!cancelled) {
|
||||||
|
setIsLoading(false);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
void loadInitialScripts();
|
||||||
|
|
||||||
|
return () => {
|
||||||
|
cancelled = true;
|
||||||
|
};
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
const handleCreateScript = async () => {
|
||||||
|
try {
|
||||||
|
const created = await window.electronAPI?.scripts.create({
|
||||||
|
title: t('sidebar.scripts.newScript'),
|
||||||
|
kind: 'utility',
|
||||||
|
content: 'print("new script")',
|
||||||
|
entrypoint: 'render',
|
||||||
|
enabled: true,
|
||||||
|
});
|
||||||
|
|
||||||
|
if (!created) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
setScripts((prev) => [
|
||||||
|
{ id: created.id, title: created.title },
|
||||||
|
...prev.filter((script) => script.id !== created.id),
|
||||||
|
]);
|
||||||
|
openScriptTab(openTab, created.id, 'pin');
|
||||||
|
void loadScripts();
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Failed to create script:', error);
|
||||||
|
showToast.error(t('sidebar.scripts.createFailed'));
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
if (isLoading) {
|
||||||
|
return (
|
||||||
|
<div className="chat-list">
|
||||||
|
<div className="chat-list-header">
|
||||||
|
<span>{t('sidebar.scripts.header')}</span>
|
||||||
|
</div>
|
||||||
|
<div className="chat-loading">{t('sidebar.loading')}</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="chat-list">
|
||||||
|
<div className="chat-list-header">
|
||||||
|
<span>{t('sidebar.scripts.header')}</span>
|
||||||
|
<button
|
||||||
|
className="chat-new-button"
|
||||||
|
onClick={handleCreateScript}
|
||||||
|
aria-label={t('sidebar.scripts.newScript')}
|
||||||
|
title={t('sidebar.scripts.newScript')}
|
||||||
|
>
|
||||||
|
+
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
<div className="chat-list-items">
|
||||||
|
{scripts.length === 0 ? (
|
||||||
|
<div className="chat-empty">
|
||||||
|
<p>{t('sidebar.scripts.none')}</p>
|
||||||
|
<button className="chat-start-button" onClick={handleCreateScript}>
|
||||||
|
{t('sidebar.scripts.createScript')}
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
) : (
|
||||||
|
scripts.map((script) => (
|
||||||
|
<button
|
||||||
|
key={script.id}
|
||||||
|
type="button"
|
||||||
|
className={`chat-list-item ${activeTabId === script.id ? 'active' : ''}`}
|
||||||
|
onClick={() => openScriptTab(openTab, script.id, 'preview')}
|
||||||
|
onDoubleClick={() => openScriptTab(openTab, script.id, 'pin')}
|
||||||
|
>
|
||||||
|
<div className="chat-item-content">
|
||||||
|
<div className="chat-item-title">{script.title}</div>
|
||||||
|
</div>
|
||||||
|
</button>
|
||||||
|
))
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
export const Sidebar: React.FC = () => {
|
export const Sidebar: React.FC = () => {
|
||||||
const { activeView, sidebarVisible } = useAppStore();
|
const { activeView, sidebarVisible } = useAppStore();
|
||||||
|
|
||||||
@@ -1677,6 +1796,7 @@ export const Sidebar: React.FC = () => {
|
|||||||
posts: <PostsList mode="posts" isActive={true} />,
|
posts: <PostsList mode="posts" isActive={true} />,
|
||||||
pages: <PostsList mode="pages" isActive={true} />,
|
pages: <PostsList mode="pages" isActive={true} />,
|
||||||
media: <MediaList />,
|
media: <MediaList />,
|
||||||
|
scripts: <ScriptsList />,
|
||||||
settings: <SettingsNav />,
|
settings: <SettingsNav />,
|
||||||
tags: <TagsNav />,
|
tags: <TagsNav />,
|
||||||
chat: <ChatList />,
|
chat: <ChatList />,
|
||||||
|
|||||||
@@ -80,6 +80,10 @@ const getTabTitle = (
|
|||||||
return tr('siteValidation.tabTitle');
|
return tr('siteValidation.tabTitle');
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (tab.type === 'scripts') {
|
||||||
|
return tr('tabBar.scripts');
|
||||||
|
}
|
||||||
|
|
||||||
return tr('tabBar.unknown');
|
return tr('tabBar.unknown');
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -158,6 +162,12 @@ const getTabIcon = (tab: Tab): React.ReactNode => {
|
|||||||
<path d="M8 1.5a6.5 6.5 0 1 0 6.5 6.5A6.5 6.5 0 0 0 8 1.5zm0 1a5.5 5.5 0 0 1 4.39 8.82l-.88-.88a.5.5 0 0 0-.7.7l.8.8A5.5 5.5 0 1 1 8 2.5zm2.35 3.15L7 9 5.65 7.65a.5.5 0 1 0-.7.7l1.7 1.7a.5.5 0 0 0 .7 0l3.7-3.7a.5.5 0 1 0-.7-.7z"/>
|
<path d="M8 1.5a6.5 6.5 0 1 0 6.5 6.5A6.5 6.5 0 0 0 8 1.5zm0 1a5.5 5.5 0 0 1 4.39 8.82l-.88-.88a.5.5 0 0 0-.7.7l.8.8A5.5 5.5 0 1 1 8 2.5zm2.35 3.15L7 9 5.65 7.65a.5.5 0 1 0-.7.7l1.7 1.7a.5.5 0 0 0 .7 0l3.7-3.7a.5.5 0 1 0-.7-.7z"/>
|
||||||
</svg>
|
</svg>
|
||||||
);
|
);
|
||||||
|
case 'scripts':
|
||||||
|
return (
|
||||||
|
<svg width="16" height="16" viewBox="0 0 24 24" fill="currentColor">
|
||||||
|
<path d="M20 3H4a1 1 0 0 0-1 1v11a1 1 0 0 0 1 1h7v2H8v2h8v-2h-3v-2h7a1 1 0 0 0 1-1V4a1 1 0 0 0-1-1zM5 14V5h14v9H5zm2-7.5L9.5 9 7 11.5l1.4 1.4L12.3 9 8.4 5.1 7 6.5zm6.5 5.5h4v-2h-4v2z"/>
|
||||||
|
</svg>
|
||||||
|
);
|
||||||
default:
|
default:
|
||||||
return (
|
return (
|
||||||
<svg width="16" height="16" viewBox="0 0 16 16" fill="currentColor">
|
<svg width="16" height="16" viewBox="0 0 16 16" fill="currentColor">
|
||||||
|
|||||||
@@ -26,3 +26,4 @@ export { InsertModal } from './InsertModal';
|
|||||||
export { WindowTitleBar } from './WindowTitleBar';
|
export { WindowTitleBar } from './WindowTitleBar';
|
||||||
export { DocumentationView } from './DocumentationView/DocumentationView';
|
export { DocumentationView } from './DocumentationView/DocumentationView';
|
||||||
export { SiteValidationView } from './SiteValidationView';
|
export { SiteValidationView } from './SiteValidationView';
|
||||||
|
export { ScriptsView } from './ScriptsView/ScriptsView';
|
||||||
|
|||||||
@@ -9,6 +9,7 @@
|
|||||||
"activity.posts": "Beiträge",
|
"activity.posts": "Beiträge",
|
||||||
"activity.pages": "Seiten",
|
"activity.pages": "Seiten",
|
||||||
"activity.media": "Medien",
|
"activity.media": "Medien",
|
||||||
|
"activity.scripts": "Skripte",
|
||||||
"activity.tags": "Schlagwörter",
|
"activity.tags": "Schlagwörter",
|
||||||
"activity.aiAssistant": "KI-Assistent",
|
"activity.aiAssistant": "KI-Assistent",
|
||||||
"activity.import": "Importieren",
|
"activity.import": "Importieren",
|
||||||
@@ -339,6 +340,7 @@
|
|||||||
"gitSidebar.placeholder.commitMessage": "Commit-Nachricht",
|
"gitSidebar.placeholder.commitMessage": "Commit-Nachricht",
|
||||||
"editor.untitled": "Unbenannt",
|
"editor.untitled": "Unbenannt",
|
||||||
"tabBar.style": "Stil",
|
"tabBar.style": "Stil",
|
||||||
|
"tabBar.scripts": "Skripte",
|
||||||
"tabBar.loading": "Laden...",
|
"tabBar.loading": "Laden...",
|
||||||
"tabBar.unknown": "Unbekannt",
|
"tabBar.unknown": "Unbekannt",
|
||||||
"tabBar.preview": "Vorschau",
|
"tabBar.preview": "Vorschau",
|
||||||
@@ -415,6 +417,9 @@
|
|||||||
"sidebar.nav.publishing": "Veröffentlichung",
|
"sidebar.nav.publishing": "Veröffentlichung",
|
||||||
"sidebar.nav.data": "Daten",
|
"sidebar.nav.data": "Daten",
|
||||||
"sidebar.nav.style": "Stil",
|
"sidebar.nav.style": "Stil",
|
||||||
|
"sidebar.nav.scripts": "Skripte",
|
||||||
|
"scripts.run": "Skript ausführen",
|
||||||
|
"scripts.content": "Skriptinhalt",
|
||||||
"sidebar.tagCloud": "Tag-Wolke",
|
"sidebar.tagCloud": "Tag-Wolke",
|
||||||
"sidebar.createEdit": "Erstellen & Bearbeiten",
|
"sidebar.createEdit": "Erstellen & Bearbeiten",
|
||||||
"sidebar.mergeTags": "Tags zusammenführen",
|
"sidebar.mergeTags": "Tags zusammenführen",
|
||||||
@@ -695,6 +700,11 @@
|
|||||||
"sidebar.chat.yesterday": "Gestern",
|
"sidebar.chat.yesterday": "Gestern",
|
||||||
"sidebar.import.header": "IMPORTE",
|
"sidebar.import.header": "IMPORTE",
|
||||||
"sidebar.import.newDefinition": "Neue Importdefinition",
|
"sidebar.import.newDefinition": "Neue Importdefinition",
|
||||||
|
"sidebar.scripts.header": "SKRIPTE",
|
||||||
|
"sidebar.scripts.newScript": "Neues Skript",
|
||||||
|
"sidebar.scripts.none": "Noch keine Skripte",
|
||||||
|
"sidebar.scripts.createScript": "Ein Skript erstellen",
|
||||||
|
"sidebar.scripts.createFailed": "Skript konnte nicht erstellt werden",
|
||||||
"sidebar.import.none": "Noch keine Importdefinitionen",
|
"sidebar.import.none": "Noch keine Importdefinitionen",
|
||||||
"sidebar.import.createDefinition": "Eine Importdefinition erstellen",
|
"sidebar.import.createDefinition": "Eine Importdefinition erstellen",
|
||||||
"sidebar.import.deleteDefinition": "Importdefinition löschen",
|
"sidebar.import.deleteDefinition": "Importdefinition löschen",
|
||||||
|
|||||||
@@ -9,6 +9,7 @@
|
|||||||
"activity.posts": "Posts",
|
"activity.posts": "Posts",
|
||||||
"activity.pages": "Pages",
|
"activity.pages": "Pages",
|
||||||
"activity.media": "Media",
|
"activity.media": "Media",
|
||||||
|
"activity.scripts": "Scripts",
|
||||||
"activity.tags": "Tags",
|
"activity.tags": "Tags",
|
||||||
"activity.aiAssistant": "AI Assistant",
|
"activity.aiAssistant": "AI Assistant",
|
||||||
"activity.import": "Import",
|
"activity.import": "Import",
|
||||||
@@ -339,6 +340,7 @@
|
|||||||
"gitSidebar.placeholder.commitMessage": "Commit message",
|
"gitSidebar.placeholder.commitMessage": "Commit message",
|
||||||
"editor.untitled": "Untitled",
|
"editor.untitled": "Untitled",
|
||||||
"tabBar.style": "Style",
|
"tabBar.style": "Style",
|
||||||
|
"tabBar.scripts": "Scripts",
|
||||||
"tabBar.loading": "Loading...",
|
"tabBar.loading": "Loading...",
|
||||||
"tabBar.unknown": "Unknown",
|
"tabBar.unknown": "Unknown",
|
||||||
"tabBar.preview": "Preview",
|
"tabBar.preview": "Preview",
|
||||||
@@ -415,6 +417,9 @@
|
|||||||
"sidebar.nav.publishing": "Publishing",
|
"sidebar.nav.publishing": "Publishing",
|
||||||
"sidebar.nav.data": "Data",
|
"sidebar.nav.data": "Data",
|
||||||
"sidebar.nav.style": "Style",
|
"sidebar.nav.style": "Style",
|
||||||
|
"sidebar.nav.scripts": "Scripts",
|
||||||
|
"scripts.run": "Run Script",
|
||||||
|
"scripts.content": "Script Content",
|
||||||
"sidebar.tagCloud": "Tag Cloud",
|
"sidebar.tagCloud": "Tag Cloud",
|
||||||
"sidebar.createEdit": "Create & Edit",
|
"sidebar.createEdit": "Create & Edit",
|
||||||
"sidebar.mergeTags": "Merge Tags",
|
"sidebar.mergeTags": "Merge Tags",
|
||||||
@@ -695,6 +700,11 @@
|
|||||||
"sidebar.chat.yesterday": "Yesterday",
|
"sidebar.chat.yesterday": "Yesterday",
|
||||||
"sidebar.import.header": "IMPORTS",
|
"sidebar.import.header": "IMPORTS",
|
||||||
"sidebar.import.newDefinition": "New Import Definition",
|
"sidebar.import.newDefinition": "New Import Definition",
|
||||||
|
"sidebar.scripts.header": "SCRIPTS",
|
||||||
|
"sidebar.scripts.newScript": "New Script",
|
||||||
|
"sidebar.scripts.none": "No scripts yet",
|
||||||
|
"sidebar.scripts.createScript": "Create a script",
|
||||||
|
"sidebar.scripts.createFailed": "Failed to create script",
|
||||||
"sidebar.import.none": "No import definitions yet",
|
"sidebar.import.none": "No import definitions yet",
|
||||||
"sidebar.import.createDefinition": "Create an import definition",
|
"sidebar.import.createDefinition": "Create an import definition",
|
||||||
"sidebar.import.deleteDefinition": "Delete import definition",
|
"sidebar.import.deleteDefinition": "Delete import definition",
|
||||||
|
|||||||
@@ -9,6 +9,7 @@
|
|||||||
"activity.posts": "Entradas",
|
"activity.posts": "Entradas",
|
||||||
"activity.pages": "Páginas",
|
"activity.pages": "Páginas",
|
||||||
"activity.media": "Medios",
|
"activity.media": "Medios",
|
||||||
|
"activity.scripts": "Scripts",
|
||||||
"activity.tags": "Etiquetas",
|
"activity.tags": "Etiquetas",
|
||||||
"activity.aiAssistant": "Asistente IA",
|
"activity.aiAssistant": "Asistente IA",
|
||||||
"activity.import": "Importar",
|
"activity.import": "Importar",
|
||||||
@@ -339,6 +340,7 @@
|
|||||||
"gitSidebar.placeholder.commitMessage": "Mensaje de commit",
|
"gitSidebar.placeholder.commitMessage": "Mensaje de commit",
|
||||||
"editor.untitled": "Sin título",
|
"editor.untitled": "Sin título",
|
||||||
"tabBar.style": "Estilo",
|
"tabBar.style": "Estilo",
|
||||||
|
"tabBar.scripts": "Scripts",
|
||||||
"tabBar.loading": "Cargando...",
|
"tabBar.loading": "Cargando...",
|
||||||
"tabBar.unknown": "Desconocido",
|
"tabBar.unknown": "Desconocido",
|
||||||
"tabBar.preview": "Vista previa",
|
"tabBar.preview": "Vista previa",
|
||||||
@@ -415,6 +417,9 @@
|
|||||||
"sidebar.nav.publishing": "Publicación",
|
"sidebar.nav.publishing": "Publicación",
|
||||||
"sidebar.nav.data": "Datos",
|
"sidebar.nav.data": "Datos",
|
||||||
"sidebar.nav.style": "Estilo",
|
"sidebar.nav.style": "Estilo",
|
||||||
|
"sidebar.nav.scripts": "Scripts",
|
||||||
|
"scripts.run": "Ejecutar script",
|
||||||
|
"scripts.content": "Contenido del script",
|
||||||
"sidebar.tagCloud": "Nube de etiquetas",
|
"sidebar.tagCloud": "Nube de etiquetas",
|
||||||
"sidebar.createEdit": "Crear y editar",
|
"sidebar.createEdit": "Crear y editar",
|
||||||
"sidebar.mergeTags": "Combinar etiquetas",
|
"sidebar.mergeTags": "Combinar etiquetas",
|
||||||
@@ -695,6 +700,11 @@
|
|||||||
"sidebar.chat.yesterday": "Ayer",
|
"sidebar.chat.yesterday": "Ayer",
|
||||||
"sidebar.import.header": "Importación",
|
"sidebar.import.header": "Importación",
|
||||||
"sidebar.import.newDefinition": "Nueva definición",
|
"sidebar.import.newDefinition": "Nueva definición",
|
||||||
|
"sidebar.scripts.header": "SCRIPTS",
|
||||||
|
"sidebar.scripts.newScript": "Nuevo script",
|
||||||
|
"sidebar.scripts.none": "Aún no hay scripts",
|
||||||
|
"sidebar.scripts.createScript": "Crear un script",
|
||||||
|
"sidebar.scripts.createFailed": "No se pudo crear el script",
|
||||||
"sidebar.import.none": "Sin definiciones de importación",
|
"sidebar.import.none": "Sin definiciones de importación",
|
||||||
"sidebar.import.createDefinition": "Crear definición",
|
"sidebar.import.createDefinition": "Crear definición",
|
||||||
"sidebar.import.deleteDefinition": "Eliminar definición",
|
"sidebar.import.deleteDefinition": "Eliminar definición",
|
||||||
|
|||||||
@@ -9,6 +9,7 @@
|
|||||||
"activity.posts": "Articles",
|
"activity.posts": "Articles",
|
||||||
"activity.pages": "Pages du site",
|
"activity.pages": "Pages du site",
|
||||||
"activity.media": "Médias",
|
"activity.media": "Médias",
|
||||||
|
"activity.scripts": "Scripts",
|
||||||
"activity.tags": "Étiquettes",
|
"activity.tags": "Étiquettes",
|
||||||
"activity.aiAssistant": "Assistant IA",
|
"activity.aiAssistant": "Assistant IA",
|
||||||
"activity.import": "Importation",
|
"activity.import": "Importation",
|
||||||
@@ -339,6 +340,7 @@
|
|||||||
"gitSidebar.placeholder.commitMessage": "Message de commit",
|
"gitSidebar.placeholder.commitMessage": "Message de commit",
|
||||||
"editor.untitled": "Sans titre",
|
"editor.untitled": "Sans titre",
|
||||||
"tabBar.style": "Apparence",
|
"tabBar.style": "Apparence",
|
||||||
|
"tabBar.scripts": "Scripts",
|
||||||
"tabBar.loading": "Chargement...",
|
"tabBar.loading": "Chargement...",
|
||||||
"tabBar.unknown": "Inconnu",
|
"tabBar.unknown": "Inconnu",
|
||||||
"tabBar.preview": "Aperçu",
|
"tabBar.preview": "Aperçu",
|
||||||
@@ -415,6 +417,9 @@
|
|||||||
"sidebar.nav.publishing": "Publication",
|
"sidebar.nav.publishing": "Publication",
|
||||||
"sidebar.nav.data": "Données",
|
"sidebar.nav.data": "Données",
|
||||||
"sidebar.nav.style": "Style",
|
"sidebar.nav.style": "Style",
|
||||||
|
"sidebar.nav.scripts": "Scripts",
|
||||||
|
"scripts.run": "Exécuter le script",
|
||||||
|
"scripts.content": "Contenu du script",
|
||||||
"sidebar.tagCloud": "Nuage d’étiquettes",
|
"sidebar.tagCloud": "Nuage d’étiquettes",
|
||||||
"sidebar.createEdit": "Créer & modifier",
|
"sidebar.createEdit": "Créer & modifier",
|
||||||
"sidebar.mergeTags": "Fusionner les étiquettes",
|
"sidebar.mergeTags": "Fusionner les étiquettes",
|
||||||
@@ -695,6 +700,11 @@
|
|||||||
"sidebar.chat.yesterday": "Hier",
|
"sidebar.chat.yesterday": "Hier",
|
||||||
"sidebar.import.header": "Import",
|
"sidebar.import.header": "Import",
|
||||||
"sidebar.import.newDefinition": "Nouvelle définition",
|
"sidebar.import.newDefinition": "Nouvelle définition",
|
||||||
|
"sidebar.scripts.header": "SCRIPTS",
|
||||||
|
"sidebar.scripts.newScript": "Nouveau script",
|
||||||
|
"sidebar.scripts.none": "Aucun script",
|
||||||
|
"sidebar.scripts.createScript": "Créer un script",
|
||||||
|
"sidebar.scripts.createFailed": "Impossible de créer le script",
|
||||||
"sidebar.import.none": "Aucune définition d’import",
|
"sidebar.import.none": "Aucune définition d’import",
|
||||||
"sidebar.import.createDefinition": "Créer une définition",
|
"sidebar.import.createDefinition": "Créer une définition",
|
||||||
"sidebar.import.deleteDefinition": "Supprimer la définition",
|
"sidebar.import.deleteDefinition": "Supprimer la définition",
|
||||||
|
|||||||
@@ -9,6 +9,7 @@
|
|||||||
"activity.posts": "Post",
|
"activity.posts": "Post",
|
||||||
"activity.pages": "Pagine",
|
"activity.pages": "Pagine",
|
||||||
"activity.media": "Contenuti media",
|
"activity.media": "Contenuti media",
|
||||||
|
"activity.scripts": "Script",
|
||||||
"activity.tags": "Tag",
|
"activity.tags": "Tag",
|
||||||
"activity.aiAssistant": "Assistente IA",
|
"activity.aiAssistant": "Assistente IA",
|
||||||
"activity.import": "Importa",
|
"activity.import": "Importa",
|
||||||
@@ -339,6 +340,7 @@
|
|||||||
"gitSidebar.placeholder.commitMessage": "Messaggio di commit",
|
"gitSidebar.placeholder.commitMessage": "Messaggio di commit",
|
||||||
"editor.untitled": "Senza titolo",
|
"editor.untitled": "Senza titolo",
|
||||||
"tabBar.style": "Stile",
|
"tabBar.style": "Stile",
|
||||||
|
"tabBar.scripts": "Script",
|
||||||
"tabBar.loading": "Caricamento...",
|
"tabBar.loading": "Caricamento...",
|
||||||
"tabBar.unknown": "Sconosciuto",
|
"tabBar.unknown": "Sconosciuto",
|
||||||
"tabBar.preview": "Anteprima",
|
"tabBar.preview": "Anteprima",
|
||||||
@@ -415,6 +417,9 @@
|
|||||||
"sidebar.nav.publishing": "Pubblicazione",
|
"sidebar.nav.publishing": "Pubblicazione",
|
||||||
"sidebar.nav.data": "Dati",
|
"sidebar.nav.data": "Dati",
|
||||||
"sidebar.nav.style": "Stile",
|
"sidebar.nav.style": "Stile",
|
||||||
|
"sidebar.nav.scripts": "Script",
|
||||||
|
"scripts.run": "Esegui script",
|
||||||
|
"scripts.content": "Contenuto script",
|
||||||
"sidebar.tagCloud": "Nuvola tag",
|
"sidebar.tagCloud": "Nuvola tag",
|
||||||
"sidebar.createEdit": "Crea e modifica",
|
"sidebar.createEdit": "Crea e modifica",
|
||||||
"sidebar.mergeTags": "Unisci tag",
|
"sidebar.mergeTags": "Unisci tag",
|
||||||
@@ -695,6 +700,11 @@
|
|||||||
"sidebar.chat.yesterday": "Ieri",
|
"sidebar.chat.yesterday": "Ieri",
|
||||||
"sidebar.import.header": "Importazione",
|
"sidebar.import.header": "Importazione",
|
||||||
"sidebar.import.newDefinition": "Nuova definizione",
|
"sidebar.import.newDefinition": "Nuova definizione",
|
||||||
|
"sidebar.scripts.header": "SCRIPTS",
|
||||||
|
"sidebar.scripts.newScript": "Nuovo script",
|
||||||
|
"sidebar.scripts.none": "Nessuno script",
|
||||||
|
"sidebar.scripts.createScript": "Crea uno script",
|
||||||
|
"sidebar.scripts.createFailed": "Impossibile creare lo script",
|
||||||
"sidebar.import.none": "Nessuna definizione di importazione",
|
"sidebar.import.none": "Nessuna definizione di importazione",
|
||||||
"sidebar.import.createDefinition": "Crea definizione",
|
"sidebar.import.createDefinition": "Crea definizione",
|
||||||
"sidebar.import.deleteDefinition": "Elimina definizione",
|
"sidebar.import.deleteDefinition": "Elimina definizione",
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
import type { Tab } from '../store/appStore';
|
import type { Tab } from '../store/appStore';
|
||||||
import type { SidebarView } from './sidebarViewRegistry';
|
import type { SidebarView } from './sidebarViewRegistry';
|
||||||
|
|
||||||
export type ActivityId = 'posts' | 'pages' | 'media' | 'tags' | 'chat' | 'import' | 'git' | 'settings';
|
export type ActivityId = 'posts' | 'pages' | 'media' | 'scripts' | 'tags' | 'chat' | 'import' | 'git' | 'settings';
|
||||||
|
|
||||||
export interface ActivitySnapshot {
|
export interface ActivitySnapshot {
|
||||||
activeView: SidebarView;
|
activeView: SidebarView;
|
||||||
@@ -43,6 +43,13 @@ const ACTIVITY_CONFIG: Record<ActivityId, ActivityConfig> = {
|
|||||||
activeStrategy: 'sidebar-owner',
|
activeStrategy: 'sidebar-owner',
|
||||||
clickStrategy: 'sidebar-toggle',
|
clickStrategy: 'sidebar-toggle',
|
||||||
},
|
},
|
||||||
|
scripts: {
|
||||||
|
id: 'scripts',
|
||||||
|
view: 'scripts',
|
||||||
|
labelKey: 'activity.scripts',
|
||||||
|
activeStrategy: 'sidebar-owner',
|
||||||
|
clickStrategy: 'sidebar-toggle',
|
||||||
|
},
|
||||||
tags: {
|
tags: {
|
||||||
id: 'tags',
|
id: 'tags',
|
||||||
view: 'tags',
|
view: 'tags',
|
||||||
|
|||||||
@@ -14,7 +14,8 @@ export type EditorRoute =
|
|||||||
| 'metadata-diff'
|
| 'metadata-diff'
|
||||||
| 'git-diff'
|
| 'git-diff'
|
||||||
| 'documentation'
|
| 'documentation'
|
||||||
| 'site-validation';
|
| 'site-validation'
|
||||||
|
| 'scripts';
|
||||||
|
|
||||||
export const EDITOR_TAB_ROUTE_REGISTRY: Record<TabType, Exclude<EditorRoute, 'dashboard'>> = {
|
export const EDITOR_TAB_ROUTE_REGISTRY: Record<TabType, Exclude<EditorRoute, 'dashboard'>> = {
|
||||||
post: 'post',
|
post: 'post',
|
||||||
@@ -29,6 +30,7 @@ export const EDITOR_TAB_ROUTE_REGISTRY: Record<TabType, Exclude<EditorRoute, 'da
|
|||||||
'git-diff': 'git-diff',
|
'git-diff': 'git-diff',
|
||||||
documentation: 'documentation',
|
documentation: 'documentation',
|
||||||
'site-validation': 'site-validation',
|
'site-validation': 'site-validation',
|
||||||
|
scripts: 'scripts',
|
||||||
};
|
};
|
||||||
|
|
||||||
export interface EditorRouteResolution {
|
export interface EditorRouteResolution {
|
||||||
|
|||||||
@@ -2,6 +2,7 @@ export const SIDEBAR_VIEW_REGISTRY = [
|
|||||||
'posts',
|
'posts',
|
||||||
'pages',
|
'pages',
|
||||||
'media',
|
'media',
|
||||||
|
'scripts',
|
||||||
'settings',
|
'settings',
|
||||||
'tags',
|
'tags',
|
||||||
'chat',
|
'chat',
|
||||||
|
|||||||
@@ -4,6 +4,7 @@ export type SingletonToolTabKey =
|
|||||||
| 'settings'
|
| 'settings'
|
||||||
| 'tags'
|
| 'tags'
|
||||||
| 'style'
|
| 'style'
|
||||||
|
| 'scripts'
|
||||||
| 'menu-editor'
|
| 'menu-editor'
|
||||||
| 'documentation'
|
| 'documentation'
|
||||||
| 'metadata-diff'
|
| 'metadata-diff'
|
||||||
@@ -17,12 +18,14 @@ export interface CanonicalTabSpec {
|
|||||||
|
|
||||||
export type EntityTabType = 'post' | 'media';
|
export type EntityTabType = 'post' | 'media';
|
||||||
export type EntityTabOpenIntent = 'preview' | 'pin';
|
export type EntityTabOpenIntent = 'preview' | 'pin';
|
||||||
|
export type ScriptTabOpenIntent = 'preview' | 'pin';
|
||||||
export type GitDiffResourceOpenIntent = 'preview' | 'pin';
|
export type GitDiffResourceOpenIntent = 'preview' | 'pin';
|
||||||
|
|
||||||
const SINGLETON_TOOL_TAB_REGISTRY: Record<SingletonToolTabKey, CanonicalTabSpec> = {
|
const SINGLETON_TOOL_TAB_REGISTRY: Record<SingletonToolTabKey, CanonicalTabSpec> = {
|
||||||
settings: { type: 'settings', id: 'settings', isTransient: false },
|
settings: { type: 'settings', id: 'settings', isTransient: false },
|
||||||
tags: { type: 'tags', id: 'tags', isTransient: false },
|
tags: { type: 'tags', id: 'tags', isTransient: false },
|
||||||
style: { type: 'style', id: 'style', isTransient: false },
|
style: { type: 'style', id: 'style', isTransient: false },
|
||||||
|
scripts: { type: 'scripts', id: 'scripts', isTransient: false },
|
||||||
'menu-editor': { type: 'menu-editor', id: 'menu-editor', isTransient: false },
|
'menu-editor': { type: 'menu-editor', id: 'menu-editor', isTransient: false },
|
||||||
documentation: { type: 'documentation', id: 'documentation', isTransient: false },
|
documentation: { type: 'documentation', id: 'documentation', isTransient: false },
|
||||||
'metadata-diff': { type: 'metadata-diff', id: 'metadata-diff', isTransient: false },
|
'metadata-diff': { type: 'metadata-diff', id: 'metadata-diff', isTransient: false },
|
||||||
@@ -91,6 +94,22 @@ export function openImportTab(
|
|||||||
openTab(getImportTabSpec(definitionId));
|
openTab(getImportTabSpec(definitionId));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export function getScriptTabSpec(scriptId: string, intent: ScriptTabOpenIntent): CanonicalTabSpec {
|
||||||
|
return {
|
||||||
|
type: 'scripts',
|
||||||
|
id: scriptId,
|
||||||
|
isTransient: intent === 'preview',
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
export function openScriptTab(
|
||||||
|
openTab: (tab: CanonicalTabSpec) => void,
|
||||||
|
scriptId: string,
|
||||||
|
intent: ScriptTabOpenIntent,
|
||||||
|
): void {
|
||||||
|
openTab(getScriptTabSpec(scriptId, intent));
|
||||||
|
}
|
||||||
|
|
||||||
export function getGitDiffFileTabId(filePath: string): string {
|
export function getGitDiffFileTabId(filePath: string): string {
|
||||||
return `git-diff:${filePath}`;
|
return `git-diff:${filePath}`;
|
||||||
}
|
}
|
||||||
|
|||||||
7
src/renderer/python/runtimeManagerInstance.ts
Normal file
7
src/renderer/python/runtimeManagerInstance.ts
Normal file
@@ -0,0 +1,7 @@
|
|||||||
|
import { PythonRuntimeManager } from './PythonRuntimeManager';
|
||||||
|
|
||||||
|
const runtimeManager = new PythonRuntimeManager();
|
||||||
|
|
||||||
|
export function getPythonRuntimeManager(): PythonRuntimeManager {
|
||||||
|
return runtimeManager;
|
||||||
|
}
|
||||||
@@ -13,7 +13,7 @@ import type {
|
|||||||
const STORAGE_KEY = 'bds-app-state';
|
const STORAGE_KEY = 'bds-app-state';
|
||||||
|
|
||||||
// Tab types
|
// Tab types
|
||||||
export type TabType = 'post' | 'media' | 'settings' | 'style' | 'tags' | 'chat' | 'import' | 'menu-editor' | 'metadata-diff' | 'git-diff' | 'documentation' | 'site-validation';
|
export type TabType = 'post' | 'media' | 'settings' | 'style' | 'tags' | 'chat' | 'import' | 'menu-editor' | 'metadata-diff' | 'git-diff' | 'documentation' | 'site-validation' | 'scripts';
|
||||||
|
|
||||||
export interface Tab {
|
export interface Tab {
|
||||||
type: TabType;
|
type: TabType;
|
||||||
@@ -42,6 +42,13 @@ export type EditorMode = 'wysiwyg' | 'markdown' | 'preview';
|
|||||||
export type GitDiffViewStyle = 'inline' | 'side-by-side';
|
export type GitDiffViewStyle = 'inline' | 'side-by-side';
|
||||||
export type PanelTab = 'tasks' | 'output' | 'post-links' | 'git-log';
|
export type PanelTab = 'tasks' | 'output' | 'post-links' | 'git-log';
|
||||||
|
|
||||||
|
export interface PanelOutputEntry {
|
||||||
|
id: string;
|
||||||
|
message: string;
|
||||||
|
createdAt: string;
|
||||||
|
kind: 'stdout' | 'result' | 'error';
|
||||||
|
}
|
||||||
|
|
||||||
export interface GitDiffPreferences {
|
export interface GitDiffPreferences {
|
||||||
wordWrap: boolean;
|
wordWrap: boolean;
|
||||||
viewStyle: GitDiffViewStyle;
|
viewStyle: GitDiffViewStyle;
|
||||||
@@ -63,6 +70,7 @@ interface AppState {
|
|||||||
sidebarVisible: boolean;
|
sidebarVisible: boolean;
|
||||||
panelVisible: boolean;
|
panelVisible: boolean;
|
||||||
panelActiveTab: PanelTab;
|
panelActiveTab: PanelTab;
|
||||||
|
panelOutputEntries: PanelOutputEntry[];
|
||||||
selectedPostId: string | null;
|
selectedPostId: string | null;
|
||||||
selectedMediaId: string | null;
|
selectedMediaId: string | null;
|
||||||
preferredEditorMode: EditorMode;
|
preferredEditorMode: EditorMode;
|
||||||
@@ -112,6 +120,8 @@ interface AppState {
|
|||||||
toggleSidebar: () => void;
|
toggleSidebar: () => void;
|
||||||
togglePanel: () => void;
|
togglePanel: () => void;
|
||||||
setPanelActiveTab: (tab: PanelTab) => void;
|
setPanelActiveTab: (tab: PanelTab) => void;
|
||||||
|
appendPanelOutputEntry: (entry: PanelOutputEntry) => void;
|
||||||
|
clearPanelOutputEntries: () => void;
|
||||||
setSelectedPost: (id: string | null) => void;
|
setSelectedPost: (id: string | null) => void;
|
||||||
setSelectedMedia: (id: string | null) => void;
|
setSelectedMedia: (id: string | null) => void;
|
||||||
setPreferredEditorMode: (mode: EditorMode) => void;
|
setPreferredEditorMode: (mode: EditorMode) => void;
|
||||||
@@ -166,6 +176,7 @@ export const useAppStore = create<AppState>()(
|
|||||||
sidebarVisible: true,
|
sidebarVisible: true,
|
||||||
panelVisible: false,
|
panelVisible: false,
|
||||||
panelActiveTab: 'tasks',
|
panelActiveTab: 'tasks',
|
||||||
|
panelOutputEntries: [],
|
||||||
selectedPostId: null,
|
selectedPostId: null,
|
||||||
selectedMediaId: null,
|
selectedMediaId: null,
|
||||||
preferredEditorMode: 'wysiwyg',
|
preferredEditorMode: 'wysiwyg',
|
||||||
@@ -290,6 +301,10 @@ export const useAppStore = create<AppState>()(
|
|||||||
toggleSidebar: () => set((state) => ({ sidebarVisible: !state.sidebarVisible })),
|
toggleSidebar: () => set((state) => ({ sidebarVisible: !state.sidebarVisible })),
|
||||||
togglePanel: () => set((state) => ({ panelVisible: !state.panelVisible })),
|
togglePanel: () => set((state) => ({ panelVisible: !state.panelVisible })),
|
||||||
setPanelActiveTab: (panelActiveTab) => set({ panelActiveTab }),
|
setPanelActiveTab: (panelActiveTab) => set({ panelActiveTab }),
|
||||||
|
appendPanelOutputEntry: (entry) => set((state) => ({
|
||||||
|
panelOutputEntries: [...state.panelOutputEntries, entry],
|
||||||
|
})),
|
||||||
|
clearPanelOutputEntries: () => set({ panelOutputEntries: [] }),
|
||||||
setSelectedPost: (id) => set({ selectedPostId: id }),
|
setSelectedPost: (id) => set({ selectedPostId: id }),
|
||||||
setSelectedMedia: (id) => set({ selectedMediaId: id }),
|
setSelectedMedia: (id) => set({ selectedMediaId: id }),
|
||||||
setPreferredEditorMode: (mode) => set({ preferredEditorMode: mode }),
|
setPreferredEditorMode: (mode) => set({ preferredEditorMode: mode }),
|
||||||
|
|||||||
@@ -6,6 +6,7 @@ export {
|
|||||||
type TaskProgress,
|
type TaskProgress,
|
||||||
type EditorMode,
|
type EditorMode,
|
||||||
type ErrorDetails,
|
type ErrorDetails,
|
||||||
|
type PanelOutputEntry,
|
||||||
type Tab,
|
type Tab,
|
||||||
type TabType,
|
type TabType,
|
||||||
type TabState
|
type TabState
|
||||||
|
|||||||
137
tests/engine/ScriptEngine.test.ts
Normal file
137
tests/engine/ScriptEngine.test.ts
Normal file
@@ -0,0 +1,137 @@
|
|||||||
|
import { describe, it, expect, beforeEach, vi } from 'vitest';
|
||||||
|
import * as fs from 'fs/promises';
|
||||||
|
import { ScriptEngine } from '../../src/main/engine/ScriptEngine';
|
||||||
|
|
||||||
|
const mockScripts = new Map<string, any>();
|
||||||
|
const mockFiles = new Map<string, string>();
|
||||||
|
|
||||||
|
function createSelectChain() {
|
||||||
|
return {
|
||||||
|
from: vi.fn().mockReturnThis(),
|
||||||
|
where: vi.fn().mockReturnThis(),
|
||||||
|
orderBy: vi.fn().mockReturnThis(),
|
||||||
|
all: vi.fn().mockImplementation(() => Promise.resolve(Array.from(mockScripts.values()))),
|
||||||
|
get: vi.fn().mockImplementation(() => Promise.resolve(undefined)),
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
function createDrizzleMock() {
|
||||||
|
return {
|
||||||
|
select: vi.fn(() => createSelectChain()),
|
||||||
|
insert: vi.fn(() => ({
|
||||||
|
values: vi.fn((data: any) => {
|
||||||
|
mockScripts.set(data.id, data);
|
||||||
|
return Promise.resolve();
|
||||||
|
}),
|
||||||
|
})),
|
||||||
|
update: vi.fn(() => ({
|
||||||
|
set: vi.fn((updates: any) => ({
|
||||||
|
where: vi.fn(async () => {
|
||||||
|
for (const [scriptId, existing] of mockScripts.entries()) {
|
||||||
|
mockScripts.set(scriptId, { ...existing, ...updates });
|
||||||
|
}
|
||||||
|
}),
|
||||||
|
})),
|
||||||
|
})),
|
||||||
|
delete: vi.fn(() => ({
|
||||||
|
where: vi.fn(async () => {
|
||||||
|
mockScripts.clear();
|
||||||
|
return Promise.resolve();
|
||||||
|
}),
|
||||||
|
})),
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
const mockLocalDb = createDrizzleMock();
|
||||||
|
|
||||||
|
vi.mock('../../src/main/database', () => ({
|
||||||
|
getDatabase: vi.fn(() => ({
|
||||||
|
getLocal: vi.fn(() => mockLocalDb),
|
||||||
|
})),
|
||||||
|
}));
|
||||||
|
|
||||||
|
vi.mock('uuid', () => ({
|
||||||
|
v4: vi.fn(() => 'mock-script-id'),
|
||||||
|
}));
|
||||||
|
|
||||||
|
vi.mock('fs/promises', () => ({
|
||||||
|
readFile: vi.fn(async (filePath: string) => {
|
||||||
|
const value = (globalThis as any).__mockScriptFiles.get(filePath);
|
||||||
|
if (typeof value !== 'string') {
|
||||||
|
const error = new Error('ENOENT');
|
||||||
|
(error as any).code = 'ENOENT';
|
||||||
|
throw error;
|
||||||
|
}
|
||||||
|
return value;
|
||||||
|
}),
|
||||||
|
writeFile: vi.fn(async (filePath: string, content: string) => {
|
||||||
|
(globalThis as any).__mockScriptFiles.set(filePath, content);
|
||||||
|
}),
|
||||||
|
unlink: vi.fn(async (filePath: string) => {
|
||||||
|
(globalThis as any).__mockScriptFiles.delete(filePath);
|
||||||
|
}),
|
||||||
|
rename: vi.fn(async (fromPath: string, toPath: string) => {
|
||||||
|
const files = (globalThis as any).__mockScriptFiles;
|
||||||
|
const content = files.get(fromPath);
|
||||||
|
files.delete(fromPath);
|
||||||
|
files.set(toPath, content);
|
||||||
|
}),
|
||||||
|
mkdir: vi.fn(async () => {}),
|
||||||
|
}));
|
||||||
|
|
||||||
|
describe('ScriptEngine', () => {
|
||||||
|
let scriptEngine: ScriptEngine;
|
||||||
|
|
||||||
|
beforeEach(() => {
|
||||||
|
vi.clearAllMocks();
|
||||||
|
mockScripts.clear();
|
||||||
|
mockFiles.clear();
|
||||||
|
(globalThis as any).__mockScriptFiles = mockFiles;
|
||||||
|
vi.mocked(mockLocalDb.select).mockImplementation(() => createSelectChain());
|
||||||
|
|
||||||
|
scriptEngine = new ScriptEngine();
|
||||||
|
scriptEngine.setProjectContext('default', '/mock/userData/projects/default');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('creates script metadata and source file', async () => {
|
||||||
|
const created = await scriptEngine.createScript({
|
||||||
|
title: 'Render Hero',
|
||||||
|
kind: 'macro',
|
||||||
|
content: 'def render(context):\n return {"html": "<h1>Hi</h1>"}',
|
||||||
|
});
|
||||||
|
|
||||||
|
expect(created.slug).toBe('render-hero');
|
||||||
|
expect(mockScripts.has(created.id)).toBe(true);
|
||||||
|
expect(mockFiles.get('/mock/userData/projects/default/scripts/render-hero.py')).toContain('def render');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('updates script metadata and file content', async () => {
|
||||||
|
const created = await scriptEngine.createScript({
|
||||||
|
title: 'Render Hero',
|
||||||
|
kind: 'macro',
|
||||||
|
content: 'def render(context):\n return {"html": "<h1>Hi</h1>"}',
|
||||||
|
});
|
||||||
|
|
||||||
|
const updated = await scriptEngine.updateScript(created.id, {
|
||||||
|
title: 'Render Hero Banner',
|
||||||
|
content: 'def render(context):\n return {"html": "<h1>Banner</h1>"}',
|
||||||
|
});
|
||||||
|
|
||||||
|
expect(updated?.slug).toBe('render-hero-banner');
|
||||||
|
expect(mockFiles.get('/mock/userData/projects/default/scripts/render-hero-banner.py')).toContain('Banner');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('deletes script metadata and source file', async () => {
|
||||||
|
const created = await scriptEngine.createScript({
|
||||||
|
title: 'Delete Me',
|
||||||
|
kind: 'utility',
|
||||||
|
content: 'print("bye")',
|
||||||
|
});
|
||||||
|
|
||||||
|
const deleted = await scriptEngine.deleteScript(created.id);
|
||||||
|
|
||||||
|
expect(deleted).toBe(true);
|
||||||
|
expect(mockScripts.has(created.id)).toBe(false);
|
||||||
|
expect(mockFiles.has('/mock/userData/projects/default/scripts/delete-me.py')).toBe(false);
|
||||||
|
});
|
||||||
|
});
|
||||||
@@ -158,6 +158,16 @@ const mockPostMediaEngine = {
|
|||||||
rebuildFromSidecars: vi.fn(),
|
rebuildFromSidecars: vi.fn(),
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const mockScriptEngine = {
|
||||||
|
on: vi.fn(),
|
||||||
|
setProjectContext: vi.fn(),
|
||||||
|
createScript: vi.fn(),
|
||||||
|
updateScript: vi.fn(),
|
||||||
|
deleteScript: vi.fn(),
|
||||||
|
getScript: vi.fn(),
|
||||||
|
getAllScripts: vi.fn(),
|
||||||
|
};
|
||||||
|
|
||||||
const mockGitEngine = {
|
const mockGitEngine = {
|
||||||
checkAvailability: vi.fn(),
|
checkAvailability: vi.fn(),
|
||||||
getHeadCommit: vi.fn(),
|
getHeadCommit: vi.fn(),
|
||||||
@@ -263,6 +273,10 @@ vi.mock('../../src/main/engine/PostMediaEngine', () => ({
|
|||||||
getPostMediaEngine: vi.fn(() => mockPostMediaEngine),
|
getPostMediaEngine: vi.fn(() => mockPostMediaEngine),
|
||||||
}));
|
}));
|
||||||
|
|
||||||
|
vi.mock('../../src/main/engine/ScriptEngine', () => ({
|
||||||
|
getScriptEngine: vi.fn(() => mockScriptEngine),
|
||||||
|
}));
|
||||||
|
|
||||||
vi.mock('../../src/main/engine/GitEngine', () => ({
|
vi.mock('../../src/main/engine/GitEngine', () => ({
|
||||||
getGitEngine: vi.fn(() => mockGitEngine),
|
getGitEngine: vi.fn(() => mockGitEngine),
|
||||||
}));
|
}));
|
||||||
@@ -2593,6 +2607,113 @@ describe('IPC Handlers', () => {
|
|||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
// ============ Script Handlers ============
|
||||||
|
describe('Script Handlers', () => {
|
||||||
|
describe('scripts:create', () => {
|
||||||
|
it('should call ScriptEngine.createScript with payload', async () => {
|
||||||
|
const payload = {
|
||||||
|
title: 'Render Hero',
|
||||||
|
kind: 'macro',
|
||||||
|
content: 'def render(context):\n return {"html":"<h1>Hi</h1>"}',
|
||||||
|
};
|
||||||
|
const expected = {
|
||||||
|
id: 'script-1',
|
||||||
|
projectId: 'default',
|
||||||
|
...payload,
|
||||||
|
slug: 'render-hero',
|
||||||
|
entrypoint: 'render',
|
||||||
|
enabled: true,
|
||||||
|
version: 1,
|
||||||
|
filePath: '/mock/userData/projects/default/scripts/render-hero.py',
|
||||||
|
createdAt: new Date().toISOString(),
|
||||||
|
updatedAt: new Date().toISOString(),
|
||||||
|
};
|
||||||
|
|
||||||
|
mockScriptEngine.createScript.mockResolvedValue(expected);
|
||||||
|
|
||||||
|
const result = await invokeHandler('scripts:create', payload);
|
||||||
|
|
||||||
|
expect(mockScriptEngine.createScript).toHaveBeenCalledWith(payload);
|
||||||
|
expect(result).toEqual(expected);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('scripts:update', () => {
|
||||||
|
it('should call ScriptEngine.updateScript with id and updates', async () => {
|
||||||
|
const updates = { title: 'Updated Script', content: 'print("updated")' };
|
||||||
|
const expected = {
|
||||||
|
id: 'script-1',
|
||||||
|
projectId: 'default',
|
||||||
|
slug: 'updated-script',
|
||||||
|
title: 'Updated Script',
|
||||||
|
kind: 'utility',
|
||||||
|
entrypoint: 'render',
|
||||||
|
enabled: true,
|
||||||
|
version: 2,
|
||||||
|
filePath: '/mock/userData/projects/default/scripts/updated-script.py',
|
||||||
|
content: 'print("updated")',
|
||||||
|
createdAt: new Date().toISOString(),
|
||||||
|
updatedAt: new Date().toISOString(),
|
||||||
|
};
|
||||||
|
|
||||||
|
mockScriptEngine.updateScript.mockResolvedValue(expected);
|
||||||
|
|
||||||
|
const result = await invokeHandler('scripts:update', 'script-1', updates);
|
||||||
|
|
||||||
|
expect(mockScriptEngine.updateScript).toHaveBeenCalledWith('script-1', updates);
|
||||||
|
expect(result).toEqual(expected);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('scripts:delete', () => {
|
||||||
|
it('should call ScriptEngine.deleteScript with id', async () => {
|
||||||
|
mockScriptEngine.deleteScript.mockResolvedValue(true);
|
||||||
|
|
||||||
|
const result = await invokeHandler('scripts:delete', 'script-1');
|
||||||
|
|
||||||
|
expect(mockScriptEngine.deleteScript).toHaveBeenCalledWith('script-1');
|
||||||
|
expect(result).toBe(true);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('scripts:get', () => {
|
||||||
|
it('should call ScriptEngine.getScript with id', async () => {
|
||||||
|
const expected = {
|
||||||
|
id: 'script-1',
|
||||||
|
projectId: 'default',
|
||||||
|
slug: 'render-hero',
|
||||||
|
title: 'Render Hero',
|
||||||
|
kind: 'macro',
|
||||||
|
entrypoint: 'render',
|
||||||
|
enabled: true,
|
||||||
|
version: 1,
|
||||||
|
filePath: '/mock/userData/projects/default/scripts/render-hero.py',
|
||||||
|
content: 'def render(context):\n return {"html":"<h1>Hi</h1>"}',
|
||||||
|
createdAt: new Date().toISOString(),
|
||||||
|
updatedAt: new Date().toISOString(),
|
||||||
|
};
|
||||||
|
mockScriptEngine.getScript.mockResolvedValue(expected);
|
||||||
|
|
||||||
|
const result = await invokeHandler('scripts:get', 'script-1');
|
||||||
|
|
||||||
|
expect(mockScriptEngine.getScript).toHaveBeenCalledWith('script-1');
|
||||||
|
expect(result).toEqual(expected);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('scripts:getAll', () => {
|
||||||
|
it('should call ScriptEngine.getAllScripts', async () => {
|
||||||
|
const expected = [{ id: 'script-1' }, { id: 'script-2' }];
|
||||||
|
mockScriptEngine.getAllScripts.mockResolvedValue(expected);
|
||||||
|
|
||||||
|
const result = await invokeHandler('scripts:getAll');
|
||||||
|
|
||||||
|
expect(mockScriptEngine.getAllScripts).toHaveBeenCalled();
|
||||||
|
expect(result).toEqual(expected);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
// ============ Error Handling ============
|
// ============ Error Handling ============
|
||||||
describe('Error Handling', () => {
|
describe('Error Handling', () => {
|
||||||
it('should silently handle "Database is closing" errors', async () => {
|
it('should silently handle "Database is closing" errors', async () => {
|
||||||
|
|||||||
@@ -235,6 +235,24 @@ describe('Panel', () => {
|
|||||||
expect(screen.getByRole('tab', { name: 'Output' })).toHaveAttribute('aria-selected', 'true');
|
expect(screen.getByRole('tab', { name: 'Output' })).toHaveAttribute('aria-selected', 'true');
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it('renders output entries when output tab is active', () => {
|
||||||
|
useAppStore.setState({
|
||||||
|
panelActiveTab: 'output',
|
||||||
|
panelOutputEntries: [
|
||||||
|
{
|
||||||
|
id: 'output-1',
|
||||||
|
message: 'hello from script',
|
||||||
|
createdAt: '2026-02-22T00:00:00.000Z',
|
||||||
|
kind: 'stdout',
|
||||||
|
},
|
||||||
|
],
|
||||||
|
});
|
||||||
|
|
||||||
|
render(<Panel />);
|
||||||
|
|
||||||
|
expect(screen.getByText('hello from script')).toBeInTheDocument();
|
||||||
|
});
|
||||||
|
|
||||||
it('renders grouped tasks as expandable parent rows with child task names in tasks tab', async () => {
|
it('renders grouped tasks as expandable parent rows with child task names in tasks tab', async () => {
|
||||||
useAppStore.setState({
|
useAppStore.setState({
|
||||||
tasks: [
|
tasks: [
|
||||||
|
|||||||
23
tests/renderer/components/ScriptsView.styles.test.ts
Normal file
23
tests/renderer/components/ScriptsView.styles.test.ts
Normal file
@@ -0,0 +1,23 @@
|
|||||||
|
import { describe, expect, it } from 'vitest';
|
||||||
|
import * as fs from 'node:fs';
|
||||||
|
import * as path from 'node:path';
|
||||||
|
|
||||||
|
describe('ScriptsView styles', () => {
|
||||||
|
const cssPath = path.resolve(
|
||||||
|
__dirname,
|
||||||
|
'../../../src/renderer/components/ScriptsView/ScriptsView.css'
|
||||||
|
);
|
||||||
|
|
||||||
|
it('uses full editor area layout for the scripts container', () => {
|
||||||
|
const css = fs.readFileSync(cssPath, 'utf8');
|
||||||
|
|
||||||
|
expect(css).toMatch(/\.scripts-view\s*\{[^}]*flex:\s*1;[^}]*min-height:\s*0;[^}]*\}/s);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('keeps editor and textarea stretched to fill available space', () => {
|
||||||
|
const css = fs.readFileSync(cssPath, 'utf8');
|
||||||
|
|
||||||
|
expect(css).toMatch(/\.scripts-editor\s*\{[^}]*flex:\s*1;[^}]*min-height:\s*0;[^}]*\}/s);
|
||||||
|
expect(css).toMatch(/\.scripts-textarea\s*\{[^}]*flex:\s*1;[^}]*min-height:\s*0;[^}]*\}/s);
|
||||||
|
});
|
||||||
|
});
|
||||||
80
tests/renderer/components/ScriptsView.test.tsx
Normal file
80
tests/renderer/components/ScriptsView.test.tsx
Normal file
@@ -0,0 +1,80 @@
|
|||||||
|
import React from 'react';
|
||||||
|
import { describe, it, expect, beforeEach, vi } from 'vitest';
|
||||||
|
import { render, screen, fireEvent } from '@testing-library/react';
|
||||||
|
import { ScriptsView } from '../../../src/renderer/components/ScriptsView/ScriptsView';
|
||||||
|
import { useAppStore } from '../../../src/renderer/store';
|
||||||
|
|
||||||
|
const executeMock = vi.fn();
|
||||||
|
|
||||||
|
vi.mock('../../../src/renderer/python/runtimeManagerInstance', () => ({
|
||||||
|
getPythonRuntimeManager: () => ({
|
||||||
|
execute: executeMock,
|
||||||
|
}),
|
||||||
|
}));
|
||||||
|
|
||||||
|
describe('ScriptsView', () => {
|
||||||
|
beforeEach(() => {
|
||||||
|
vi.clearAllMocks();
|
||||||
|
|
||||||
|
executeMock.mockResolvedValue({ result: '2', stdout: 'hello\n' });
|
||||||
|
|
||||||
|
(window as any).electronAPI = {
|
||||||
|
...(window as any).electronAPI,
|
||||||
|
scripts: {
|
||||||
|
create: vi.fn(),
|
||||||
|
update: vi.fn(),
|
||||||
|
delete: vi.fn(),
|
||||||
|
get: vi.fn().mockResolvedValue({
|
||||||
|
id: 'script-1',
|
||||||
|
projectId: 'default',
|
||||||
|
slug: 'hello-script',
|
||||||
|
title: 'Hello Script',
|
||||||
|
kind: 'utility',
|
||||||
|
entrypoint: 'render',
|
||||||
|
enabled: true,
|
||||||
|
version: 1,
|
||||||
|
filePath: '/tmp/hello-script.py',
|
||||||
|
content: 'print("hello")',
|
||||||
|
createdAt: '2026-02-22T00:00:00.000Z',
|
||||||
|
updatedAt: '2026-02-22T00:00:00.000Z',
|
||||||
|
}),
|
||||||
|
getAll: vi.fn(),
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
useAppStore.setState({
|
||||||
|
panelVisible: false,
|
||||||
|
panelActiveTab: 'tasks',
|
||||||
|
panelOutputEntries: [],
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
it('loads scripts and allows editing content', async () => {
|
||||||
|
render(<ScriptsView scriptId="script-1" />);
|
||||||
|
|
||||||
|
const textarea = screen.getByLabelText('Script Content') as HTMLTextAreaElement;
|
||||||
|
await vi.waitFor(() => {
|
||||||
|
expect(textarea.value).toContain('print("hello")');
|
||||||
|
});
|
||||||
|
|
||||||
|
fireEvent.change(textarea, { target: { value: 'print("updated")' } });
|
||||||
|
expect(textarea.value).toContain('updated');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('runs selected script and writes output into panel output log', async () => {
|
||||||
|
render(<ScriptsView scriptId="script-1" />);
|
||||||
|
|
||||||
|
await screen.findByLabelText('Script Content');
|
||||||
|
fireEvent.click(screen.getByRole('button', { name: 'Run Script' }));
|
||||||
|
|
||||||
|
await vi.waitFor(() => {
|
||||||
|
expect(executeMock).toHaveBeenCalledWith('print("hello")');
|
||||||
|
});
|
||||||
|
|
||||||
|
const state = useAppStore.getState();
|
||||||
|
expect(state.panelVisible).toBe(true);
|
||||||
|
expect(state.panelActiveTab).toBe('output');
|
||||||
|
expect(state.panelOutputEntries.length).toBeGreaterThan(0);
|
||||||
|
expect(state.panelOutputEntries[state.panelOutputEntries.length - 1].message).toContain('hello');
|
||||||
|
});
|
||||||
|
});
|
||||||
147
tests/renderer/components/SidebarScripts.test.tsx
Normal file
147
tests/renderer/components/SidebarScripts.test.tsx
Normal file
@@ -0,0 +1,147 @@
|
|||||||
|
import React from 'react';
|
||||||
|
import { describe, it, expect, beforeEach, vi } from 'vitest';
|
||||||
|
import { render, screen, fireEvent } from '@testing-library/react';
|
||||||
|
import { Sidebar } from '../../../src/renderer/components/Sidebar/Sidebar';
|
||||||
|
import { useAppStore } from '../../../src/renderer/store';
|
||||||
|
|
||||||
|
describe('Sidebar scripts list behavior', () => {
|
||||||
|
beforeEach(() => {
|
||||||
|
vi.clearAllMocks();
|
||||||
|
|
||||||
|
(window as any).electronAPI = {
|
||||||
|
...(window as any).electronAPI,
|
||||||
|
scripts: {
|
||||||
|
create: vi.fn(),
|
||||||
|
update: vi.fn(),
|
||||||
|
delete: vi.fn(),
|
||||||
|
get: vi.fn(),
|
||||||
|
getAll: vi.fn().mockResolvedValue([
|
||||||
|
{
|
||||||
|
id: 'script-1',
|
||||||
|
projectId: 'default',
|
||||||
|
slug: 'hello-script',
|
||||||
|
title: 'Hello Script',
|
||||||
|
kind: 'utility',
|
||||||
|
entrypoint: 'render',
|
||||||
|
enabled: true,
|
||||||
|
version: 1,
|
||||||
|
filePath: '/tmp/hello-script.py',
|
||||||
|
content: 'print("hello")',
|
||||||
|
createdAt: '2026-02-22T00:00:00.000Z',
|
||||||
|
updatedAt: '2026-02-22T00:00:00.000Z',
|
||||||
|
},
|
||||||
|
]),
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
useAppStore.setState({
|
||||||
|
activeView: 'scripts',
|
||||||
|
sidebarVisible: true,
|
||||||
|
tabs: [],
|
||||||
|
activeTabId: null,
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
it('opens a transient script tab on single click', async () => {
|
||||||
|
render(<Sidebar />);
|
||||||
|
|
||||||
|
const scriptRow = await screen.findByRole('button', { name: 'Hello Script' });
|
||||||
|
fireEvent.click(scriptRow);
|
||||||
|
|
||||||
|
expect(useAppStore.getState().tabs).toEqual([
|
||||||
|
{
|
||||||
|
type: 'scripts',
|
||||||
|
id: 'script-1',
|
||||||
|
isTransient: true,
|
||||||
|
},
|
||||||
|
]);
|
||||||
|
expect(useAppStore.getState().activeTabId).toBe('script-1');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('renders scripts section title and create button', async () => {
|
||||||
|
render(<Sidebar />);
|
||||||
|
|
||||||
|
expect(screen.getByText('SCRIPTS')).toBeInTheDocument();
|
||||||
|
expect(await screen.findByRole('button', { name: 'New Script' })).toBeInTheDocument();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('shows loading state while scripts are being fetched', () => {
|
||||||
|
(window as any).electronAPI.scripts.getAll = vi.fn().mockImplementation(
|
||||||
|
() => new Promise(() => {}),
|
||||||
|
);
|
||||||
|
|
||||||
|
render(<Sidebar />);
|
||||||
|
|
||||||
|
expect(screen.getByText('Loading...')).toBeInTheDocument();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('shows empty state with create action when no scripts exist', async () => {
|
||||||
|
(window as any).electronAPI.scripts.getAll = vi.fn().mockResolvedValue([]);
|
||||||
|
|
||||||
|
render(<Sidebar />);
|
||||||
|
|
||||||
|
expect(await screen.findByText('No scripts yet')).toBeInTheDocument();
|
||||||
|
expect(screen.getByRole('button', { name: 'Create a script' })).toBeInTheDocument();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('creates a new script from the create button and opens it pinned', async () => {
|
||||||
|
const createMock = vi.fn().mockResolvedValue({
|
||||||
|
id: 'script-new',
|
||||||
|
projectId: 'default',
|
||||||
|
slug: 'new-script',
|
||||||
|
title: 'New Script',
|
||||||
|
kind: 'utility',
|
||||||
|
entrypoint: 'render',
|
||||||
|
enabled: true,
|
||||||
|
version: 1,
|
||||||
|
filePath: '/tmp/new-script.py',
|
||||||
|
content: 'print("new script")',
|
||||||
|
createdAt: '2026-02-22T00:00:00.000Z',
|
||||||
|
updatedAt: '2026-02-22T00:00:00.000Z',
|
||||||
|
});
|
||||||
|
|
||||||
|
(window as any).electronAPI.scripts.create = createMock;
|
||||||
|
|
||||||
|
render(<Sidebar />);
|
||||||
|
|
||||||
|
fireEvent.click(await screen.findByRole('button', { name: 'New Script' }));
|
||||||
|
|
||||||
|
await vi.waitFor(() => {
|
||||||
|
expect(createMock).toHaveBeenCalledWith(
|
||||||
|
expect.objectContaining({
|
||||||
|
title: 'New Script',
|
||||||
|
kind: 'utility',
|
||||||
|
entrypoint: 'render',
|
||||||
|
enabled: true,
|
||||||
|
}),
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
await vi.waitFor(() => {
|
||||||
|
expect(useAppStore.getState().tabs).toEqual([
|
||||||
|
{
|
||||||
|
type: 'scripts',
|
||||||
|
id: 'script-new',
|
||||||
|
isTransient: false,
|
||||||
|
},
|
||||||
|
]);
|
||||||
|
expect(useAppStore.getState().activeTabId).toBe('script-new');
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
it('opens a pinned script tab on double click', async () => {
|
||||||
|
render(<Sidebar />);
|
||||||
|
|
||||||
|
const scriptRow = await screen.findByRole('button', { name: 'Hello Script' });
|
||||||
|
fireEvent.doubleClick(scriptRow);
|
||||||
|
|
||||||
|
expect(useAppStore.getState().tabs).toEqual([
|
||||||
|
{
|
||||||
|
type: 'scripts',
|
||||||
|
id: 'script-1',
|
||||||
|
isTransient: false,
|
||||||
|
},
|
||||||
|
]);
|
||||||
|
expect(useAppStore.getState().activeTabId).toBe('script-1');
|
||||||
|
});
|
||||||
|
});
|
||||||
@@ -87,7 +87,20 @@ describe('activityBehavior', () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
it('supports all expected activity ids', () => {
|
it('supports all expected activity ids', () => {
|
||||||
const ids: ActivityId[] = ['posts', 'pages', 'media', 'tags', 'chat', 'import', 'git', 'settings'];
|
const ids: ActivityId[] = ['posts', 'pages', 'media', 'scripts', 'tags', 'chat', 'import', 'git', 'settings'];
|
||||||
expect(ids).toHaveLength(8);
|
expect(ids).toHaveLength(9);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('returns posts-style sidebar actions for scripts', () => {
|
||||||
|
const hiddenSidebarSnapshot = createSnapshot({ activeView: 'posts', sidebarVisible: false });
|
||||||
|
expect(getActivityClickActions(hiddenSidebarSnapshot, 'scripts')).toEqual([
|
||||||
|
{ type: 'setActiveView', view: 'scripts' },
|
||||||
|
{ type: 'toggleSidebar' },
|
||||||
|
]);
|
||||||
|
|
||||||
|
const visibleSidebarSnapshot = createSnapshot({ activeView: 'posts', sidebarVisible: true });
|
||||||
|
expect(getActivityClickActions(visibleSidebarSnapshot, 'scripts')).toEqual([
|
||||||
|
{ type: 'setActiveView', view: 'scripts' },
|
||||||
|
]);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -20,6 +20,7 @@ describe('editorRouting', () => {
|
|||||||
'git-diff': 'git-diff',
|
'git-diff': 'git-diff',
|
||||||
documentation: 'documentation',
|
documentation: 'documentation',
|
||||||
'site-validation': 'site-validation',
|
'site-validation': 'site-validation',
|
||||||
|
scripts: 'scripts',
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|||||||
@@ -11,6 +11,7 @@ describe('sidebarViewRegistry', () => {
|
|||||||
'posts',
|
'posts',
|
||||||
'pages',
|
'pages',
|
||||||
'media',
|
'media',
|
||||||
|
'scripts',
|
||||||
'settings',
|
'settings',
|
||||||
'tags',
|
'tags',
|
||||||
'chat',
|
'chat',
|
||||||
|
|||||||
@@ -5,6 +5,7 @@ import {
|
|||||||
getGitDiffCommitTabSpec,
|
getGitDiffCommitTabSpec,
|
||||||
getGitDiffFileTabSpec,
|
getGitDiffFileTabSpec,
|
||||||
getImportTabSpec,
|
getImportTabSpec,
|
||||||
|
getScriptTabSpec,
|
||||||
parseGitDiffTabId,
|
parseGitDiffTabId,
|
||||||
openChatTab,
|
openChatTab,
|
||||||
getSingletonToolTabSpec,
|
getSingletonToolTabSpec,
|
||||||
@@ -12,6 +13,7 @@ import {
|
|||||||
openGitDiffCommitTab,
|
openGitDiffCommitTab,
|
||||||
openGitDiffFileTab,
|
openGitDiffFileTab,
|
||||||
openImportTab,
|
openImportTab,
|
||||||
|
openScriptTab,
|
||||||
openSingletonToolTab,
|
openSingletonToolTab,
|
||||||
} from '../../../src/renderer/navigation/tabPolicy';
|
} from '../../../src/renderer/navigation/tabPolicy';
|
||||||
|
|
||||||
@@ -20,6 +22,7 @@ describe('tabPolicy', () => {
|
|||||||
expect(getSingletonToolTabSpec('settings')).toEqual({ type: 'settings', id: 'settings', isTransient: false });
|
expect(getSingletonToolTabSpec('settings')).toEqual({ type: 'settings', id: 'settings', isTransient: false });
|
||||||
expect(getSingletonToolTabSpec('tags')).toEqual({ type: 'tags', id: 'tags', isTransient: false });
|
expect(getSingletonToolTabSpec('tags')).toEqual({ type: 'tags', id: 'tags', isTransient: false });
|
||||||
expect(getSingletonToolTabSpec('style')).toEqual({ type: 'style', id: 'style', isTransient: false });
|
expect(getSingletonToolTabSpec('style')).toEqual({ type: 'style', id: 'style', isTransient: false });
|
||||||
|
expect(getSingletonToolTabSpec('scripts')).toEqual({ type: 'scripts', id: 'scripts', isTransient: false });
|
||||||
expect(getSingletonToolTabSpec('menu-editor')).toEqual({ type: 'menu-editor', id: 'menu-editor', isTransient: false });
|
expect(getSingletonToolTabSpec('menu-editor')).toEqual({ type: 'menu-editor', id: 'menu-editor', isTransient: false });
|
||||||
expect(getSingletonToolTabSpec('documentation')).toEqual({ type: 'documentation', id: 'documentation', isTransient: false });
|
expect(getSingletonToolTabSpec('documentation')).toEqual({ type: 'documentation', id: 'documentation', isTransient: false });
|
||||||
expect(getSingletonToolTabSpec('metadata-diff')).toEqual({ type: 'metadata-diff', id: 'metadata-diff', isTransient: false });
|
expect(getSingletonToolTabSpec('metadata-diff')).toEqual({ type: 'metadata-diff', id: 'metadata-diff', isTransient: false });
|
||||||
@@ -93,6 +96,35 @@ describe('tabPolicy', () => {
|
|||||||
]);
|
]);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it('provides canonical script tab spec for preview and pin intents', () => {
|
||||||
|
expect(getScriptTabSpec('script-1', 'preview')).toEqual({
|
||||||
|
type: 'scripts',
|
||||||
|
id: 'script-1',
|
||||||
|
isTransient: true,
|
||||||
|
});
|
||||||
|
|
||||||
|
expect(getScriptTabSpec('script-1', 'pin')).toEqual({
|
||||||
|
type: 'scripts',
|
||||||
|
id: 'script-1',
|
||||||
|
isTransient: false,
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
it('opens script tabs from shared policy', () => {
|
||||||
|
const opened: Array<{ type: string; id: string; isTransient: boolean }> = [];
|
||||||
|
const openTab = (tab: { type: string; id: string; isTransient: boolean }) => {
|
||||||
|
opened.push(tab);
|
||||||
|
};
|
||||||
|
|
||||||
|
openScriptTab(openTab, 'script-preview', 'preview');
|
||||||
|
openScriptTab(openTab, 'script-pin', 'pin');
|
||||||
|
|
||||||
|
expect(opened).toEqual([
|
||||||
|
{ type: 'scripts', id: 'script-preview', isTransient: true },
|
||||||
|
{ type: 'scripts', id: 'script-pin', isTransient: false },
|
||||||
|
]);
|
||||||
|
});
|
||||||
|
|
||||||
it('builds and parses git-diff file and commit tab specs', () => {
|
it('builds and parses git-diff file and commit tab specs', () => {
|
||||||
expect(getGitDiffFileTabSpec('posts/first.md', 'preview')).toEqual({
|
expect(getGitDiffFileTabSpec('posts/first.md', 'preview')).toEqual({
|
||||||
type: 'git-diff',
|
type: 'git-diff',
|
||||||
|
|||||||
@@ -22,4 +22,10 @@ describe('vite renderer chunking', () => {
|
|||||||
const resolved = typeof viteConfig === 'function' ? viteConfig({ command: 'build', mode: 'production', isSsrBuild: false, isPreview: false }) : viteConfig;
|
const resolved = typeof viteConfig === 'function' ? viteConfig({ command: 'build', mode: 'production', isSsrBuild: false, isPreview: false }) : viteConfig;
|
||||||
expect(resolved.build?.chunkSizeWarningLimit).toBe(8000);
|
expect(resolved.build?.chunkSizeWarningLimit).toBe(8000);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it('excludes pyodide from optimizeDeps pre-bundling', () => {
|
||||||
|
const resolved = typeof viteConfig === 'function' ? viteConfig({ command: 'serve', mode: 'development', isSsrBuild: false, isPreview: false }) : viteConfig;
|
||||||
|
const excluded = resolved.optimizeDeps?.exclude ?? [];
|
||||||
|
expect(excluded).toContain('pyodide');
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -41,4 +41,10 @@ export default defineConfig({
|
|||||||
server: {
|
server: {
|
||||||
port: 5173,
|
port: 5173,
|
||||||
},
|
},
|
||||||
|
optimizeDeps: {
|
||||||
|
exclude: ['pyodide'],
|
||||||
|
},
|
||||||
|
worker: {
|
||||||
|
format: 'es',
|
||||||
|
},
|
||||||
});
|
});
|
||||||
|
|||||||
Reference in New Issue
Block a user