Compare commits
2 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| decb166858 | |||
| 49f3ceebc2 |
@@ -0,0 +1,11 @@
|
||||
CREATE TABLE `project_path` (
|
||||
`project_id` text NOT NULL,
|
||||
`path` text NOT NULL,
|
||||
`primary` integer DEFAULT false NOT NULL,
|
||||
`time_created` integer NOT NULL,
|
||||
`time_updated` integer NOT NULL,
|
||||
CONSTRAINT `project_path_pk` PRIMARY KEY(`project_id`, `path`),
|
||||
CONSTRAINT `fk_project_path_project_id_project_id_fk` FOREIGN KEY (`project_id`) REFERENCES `project`(`id`) ON DELETE CASCADE
|
||||
);
|
||||
--> statement-breakpoint
|
||||
CREATE UNIQUE INDEX `project_path_project_primary_idx` ON `project_path` (`project_id`) WHERE "project_path"."primary" = 1;
|
||||
@@ -0,0 +1,1727 @@
|
||||
{
|
||||
"version": "7",
|
||||
"dialect": "sqlite",
|
||||
"id": "0555502d-640a-4fba-97dc-ec9bfc2de2e3",
|
||||
"prevIds": [
|
||||
"bf93c73b-5a48-4d63-9909-3c36a79b9788"
|
||||
],
|
||||
"ddl": [
|
||||
{
|
||||
"name": "workspace",
|
||||
"entityType": "tables"
|
||||
},
|
||||
{
|
||||
"name": "data_migration",
|
||||
"entityType": "tables"
|
||||
},
|
||||
{
|
||||
"name": "project_path",
|
||||
"entityType": "tables"
|
||||
},
|
||||
{
|
||||
"name": "account_state",
|
||||
"entityType": "tables"
|
||||
},
|
||||
{
|
||||
"name": "account",
|
||||
"entityType": "tables"
|
||||
},
|
||||
{
|
||||
"name": "control_account",
|
||||
"entityType": "tables"
|
||||
},
|
||||
{
|
||||
"name": "event_sequence",
|
||||
"entityType": "tables"
|
||||
},
|
||||
{
|
||||
"name": "event",
|
||||
"entityType": "tables"
|
||||
},
|
||||
{
|
||||
"name": "project",
|
||||
"entityType": "tables"
|
||||
},
|
||||
{
|
||||
"name": "message",
|
||||
"entityType": "tables"
|
||||
},
|
||||
{
|
||||
"name": "part",
|
||||
"entityType": "tables"
|
||||
},
|
||||
{
|
||||
"name": "permission",
|
||||
"entityType": "tables"
|
||||
},
|
||||
{
|
||||
"name": "session_message",
|
||||
"entityType": "tables"
|
||||
},
|
||||
{
|
||||
"name": "session",
|
||||
"entityType": "tables"
|
||||
},
|
||||
{
|
||||
"name": "todo",
|
||||
"entityType": "tables"
|
||||
},
|
||||
{
|
||||
"name": "session_share",
|
||||
"entityType": "tables"
|
||||
},
|
||||
{
|
||||
"type": "text",
|
||||
"notNull": false,
|
||||
"autoincrement": false,
|
||||
"default": null,
|
||||
"generated": null,
|
||||
"name": "id",
|
||||
"entityType": "columns",
|
||||
"table": "workspace"
|
||||
},
|
||||
{
|
||||
"type": "text",
|
||||
"notNull": true,
|
||||
"autoincrement": false,
|
||||
"default": null,
|
||||
"generated": null,
|
||||
"name": "type",
|
||||
"entityType": "columns",
|
||||
"table": "workspace"
|
||||
},
|
||||
{
|
||||
"type": "text",
|
||||
"notNull": true,
|
||||
"autoincrement": false,
|
||||
"default": "''",
|
||||
"generated": null,
|
||||
"name": "name",
|
||||
"entityType": "columns",
|
||||
"table": "workspace"
|
||||
},
|
||||
{
|
||||
"type": "text",
|
||||
"notNull": false,
|
||||
"autoincrement": false,
|
||||
"default": null,
|
||||
"generated": null,
|
||||
"name": "branch",
|
||||
"entityType": "columns",
|
||||
"table": "workspace"
|
||||
},
|
||||
{
|
||||
"type": "text",
|
||||
"notNull": false,
|
||||
"autoincrement": false,
|
||||
"default": null,
|
||||
"generated": null,
|
||||
"name": "directory",
|
||||
"entityType": "columns",
|
||||
"table": "workspace"
|
||||
},
|
||||
{
|
||||
"type": "text",
|
||||
"notNull": false,
|
||||
"autoincrement": false,
|
||||
"default": null,
|
||||
"generated": null,
|
||||
"name": "extra",
|
||||
"entityType": "columns",
|
||||
"table": "workspace"
|
||||
},
|
||||
{
|
||||
"type": "text",
|
||||
"notNull": true,
|
||||
"autoincrement": false,
|
||||
"default": null,
|
||||
"generated": null,
|
||||
"name": "project_id",
|
||||
"entityType": "columns",
|
||||
"table": "workspace"
|
||||
},
|
||||
{
|
||||
"type": "integer",
|
||||
"notNull": true,
|
||||
"autoincrement": false,
|
||||
"default": null,
|
||||
"generated": null,
|
||||
"name": "time_used",
|
||||
"entityType": "columns",
|
||||
"table": "workspace"
|
||||
},
|
||||
{
|
||||
"type": "text",
|
||||
"notNull": false,
|
||||
"autoincrement": false,
|
||||
"default": null,
|
||||
"generated": null,
|
||||
"name": "name",
|
||||
"entityType": "columns",
|
||||
"table": "data_migration"
|
||||
},
|
||||
{
|
||||
"type": "integer",
|
||||
"notNull": true,
|
||||
"autoincrement": false,
|
||||
"default": null,
|
||||
"generated": null,
|
||||
"name": "time_completed",
|
||||
"entityType": "columns",
|
||||
"table": "data_migration"
|
||||
},
|
||||
{
|
||||
"type": "text",
|
||||
"notNull": true,
|
||||
"autoincrement": false,
|
||||
"default": null,
|
||||
"generated": null,
|
||||
"name": "project_id",
|
||||
"entityType": "columns",
|
||||
"table": "project_path"
|
||||
},
|
||||
{
|
||||
"type": "text",
|
||||
"notNull": true,
|
||||
"autoincrement": false,
|
||||
"default": null,
|
||||
"generated": null,
|
||||
"name": "path",
|
||||
"entityType": "columns",
|
||||
"table": "project_path"
|
||||
},
|
||||
{
|
||||
"type": "integer",
|
||||
"notNull": true,
|
||||
"autoincrement": false,
|
||||
"default": "false",
|
||||
"generated": null,
|
||||
"name": "primary",
|
||||
"entityType": "columns",
|
||||
"table": "project_path"
|
||||
},
|
||||
{
|
||||
"type": "integer",
|
||||
"notNull": true,
|
||||
"autoincrement": false,
|
||||
"default": null,
|
||||
"generated": null,
|
||||
"name": "time_created",
|
||||
"entityType": "columns",
|
||||
"table": "project_path"
|
||||
},
|
||||
{
|
||||
"type": "integer",
|
||||
"notNull": true,
|
||||
"autoincrement": false,
|
||||
"default": null,
|
||||
"generated": null,
|
||||
"name": "time_updated",
|
||||
"entityType": "columns",
|
||||
"table": "project_path"
|
||||
},
|
||||
{
|
||||
"type": "integer",
|
||||
"notNull": false,
|
||||
"autoincrement": false,
|
||||
"default": null,
|
||||
"generated": null,
|
||||
"name": "id",
|
||||
"entityType": "columns",
|
||||
"table": "account_state"
|
||||
},
|
||||
{
|
||||
"type": "text",
|
||||
"notNull": false,
|
||||
"autoincrement": false,
|
||||
"default": null,
|
||||
"generated": null,
|
||||
"name": "active_account_id",
|
||||
"entityType": "columns",
|
||||
"table": "account_state"
|
||||
},
|
||||
{
|
||||
"type": "text",
|
||||
"notNull": false,
|
||||
"autoincrement": false,
|
||||
"default": null,
|
||||
"generated": null,
|
||||
"name": "active_org_id",
|
||||
"entityType": "columns",
|
||||
"table": "account_state"
|
||||
},
|
||||
{
|
||||
"type": "text",
|
||||
"notNull": false,
|
||||
"autoincrement": false,
|
||||
"default": null,
|
||||
"generated": null,
|
||||
"name": "id",
|
||||
"entityType": "columns",
|
||||
"table": "account"
|
||||
},
|
||||
{
|
||||
"type": "text",
|
||||
"notNull": true,
|
||||
"autoincrement": false,
|
||||
"default": null,
|
||||
"generated": null,
|
||||
"name": "email",
|
||||
"entityType": "columns",
|
||||
"table": "account"
|
||||
},
|
||||
{
|
||||
"type": "text",
|
||||
"notNull": true,
|
||||
"autoincrement": false,
|
||||
"default": null,
|
||||
"generated": null,
|
||||
"name": "url",
|
||||
"entityType": "columns",
|
||||
"table": "account"
|
||||
},
|
||||
{
|
||||
"type": "text",
|
||||
"notNull": true,
|
||||
"autoincrement": false,
|
||||
"default": null,
|
||||
"generated": null,
|
||||
"name": "access_token",
|
||||
"entityType": "columns",
|
||||
"table": "account"
|
||||
},
|
||||
{
|
||||
"type": "text",
|
||||
"notNull": true,
|
||||
"autoincrement": false,
|
||||
"default": null,
|
||||
"generated": null,
|
||||
"name": "refresh_token",
|
||||
"entityType": "columns",
|
||||
"table": "account"
|
||||
},
|
||||
{
|
||||
"type": "integer",
|
||||
"notNull": false,
|
||||
"autoincrement": false,
|
||||
"default": null,
|
||||
"generated": null,
|
||||
"name": "token_expiry",
|
||||
"entityType": "columns",
|
||||
"table": "account"
|
||||
},
|
||||
{
|
||||
"type": "integer",
|
||||
"notNull": true,
|
||||
"autoincrement": false,
|
||||
"default": null,
|
||||
"generated": null,
|
||||
"name": "time_created",
|
||||
"entityType": "columns",
|
||||
"table": "account"
|
||||
},
|
||||
{
|
||||
"type": "integer",
|
||||
"notNull": true,
|
||||
"autoincrement": false,
|
||||
"default": null,
|
||||
"generated": null,
|
||||
"name": "time_updated",
|
||||
"entityType": "columns",
|
||||
"table": "account"
|
||||
},
|
||||
{
|
||||
"type": "text",
|
||||
"notNull": true,
|
||||
"autoincrement": false,
|
||||
"default": null,
|
||||
"generated": null,
|
||||
"name": "email",
|
||||
"entityType": "columns",
|
||||
"table": "control_account"
|
||||
},
|
||||
{
|
||||
"type": "text",
|
||||
"notNull": true,
|
||||
"autoincrement": false,
|
||||
"default": null,
|
||||
"generated": null,
|
||||
"name": "url",
|
||||
"entityType": "columns",
|
||||
"table": "control_account"
|
||||
},
|
||||
{
|
||||
"type": "text",
|
||||
"notNull": true,
|
||||
"autoincrement": false,
|
||||
"default": null,
|
||||
"generated": null,
|
||||
"name": "access_token",
|
||||
"entityType": "columns",
|
||||
"table": "control_account"
|
||||
},
|
||||
{
|
||||
"type": "text",
|
||||
"notNull": true,
|
||||
"autoincrement": false,
|
||||
"default": null,
|
||||
"generated": null,
|
||||
"name": "refresh_token",
|
||||
"entityType": "columns",
|
||||
"table": "control_account"
|
||||
},
|
||||
{
|
||||
"type": "integer",
|
||||
"notNull": false,
|
||||
"autoincrement": false,
|
||||
"default": null,
|
||||
"generated": null,
|
||||
"name": "token_expiry",
|
||||
"entityType": "columns",
|
||||
"table": "control_account"
|
||||
},
|
||||
{
|
||||
"type": "integer",
|
||||
"notNull": true,
|
||||
"autoincrement": false,
|
||||
"default": null,
|
||||
"generated": null,
|
||||
"name": "active",
|
||||
"entityType": "columns",
|
||||
"table": "control_account"
|
||||
},
|
||||
{
|
||||
"type": "integer",
|
||||
"notNull": true,
|
||||
"autoincrement": false,
|
||||
"default": null,
|
||||
"generated": null,
|
||||
"name": "time_created",
|
||||
"entityType": "columns",
|
||||
"table": "control_account"
|
||||
},
|
||||
{
|
||||
"type": "integer",
|
||||
"notNull": true,
|
||||
"autoincrement": false,
|
||||
"default": null,
|
||||
"generated": null,
|
||||
"name": "time_updated",
|
||||
"entityType": "columns",
|
||||
"table": "control_account"
|
||||
},
|
||||
{
|
||||
"type": "text",
|
||||
"notNull": false,
|
||||
"autoincrement": false,
|
||||
"default": null,
|
||||
"generated": null,
|
||||
"name": "aggregate_id",
|
||||
"entityType": "columns",
|
||||
"table": "event_sequence"
|
||||
},
|
||||
{
|
||||
"type": "integer",
|
||||
"notNull": true,
|
||||
"autoincrement": false,
|
||||
"default": null,
|
||||
"generated": null,
|
||||
"name": "seq",
|
||||
"entityType": "columns",
|
||||
"table": "event_sequence"
|
||||
},
|
||||
{
|
||||
"type": "text",
|
||||
"notNull": false,
|
||||
"autoincrement": false,
|
||||
"default": null,
|
||||
"generated": null,
|
||||
"name": "owner_id",
|
||||
"entityType": "columns",
|
||||
"table": "event_sequence"
|
||||
},
|
||||
{
|
||||
"type": "text",
|
||||
"notNull": false,
|
||||
"autoincrement": false,
|
||||
"default": null,
|
||||
"generated": null,
|
||||
"name": "id",
|
||||
"entityType": "columns",
|
||||
"table": "event"
|
||||
},
|
||||
{
|
||||
"type": "text",
|
||||
"notNull": true,
|
||||
"autoincrement": false,
|
||||
"default": null,
|
||||
"generated": null,
|
||||
"name": "aggregate_id",
|
||||
"entityType": "columns",
|
||||
"table": "event"
|
||||
},
|
||||
{
|
||||
"type": "integer",
|
||||
"notNull": true,
|
||||
"autoincrement": false,
|
||||
"default": null,
|
||||
"generated": null,
|
||||
"name": "seq",
|
||||
"entityType": "columns",
|
||||
"table": "event"
|
||||
},
|
||||
{
|
||||
"type": "text",
|
||||
"notNull": true,
|
||||
"autoincrement": false,
|
||||
"default": null,
|
||||
"generated": null,
|
||||
"name": "type",
|
||||
"entityType": "columns",
|
||||
"table": "event"
|
||||
},
|
||||
{
|
||||
"type": "text",
|
||||
"notNull": true,
|
||||
"autoincrement": false,
|
||||
"default": null,
|
||||
"generated": null,
|
||||
"name": "data",
|
||||
"entityType": "columns",
|
||||
"table": "event"
|
||||
},
|
||||
{
|
||||
"type": "text",
|
||||
"notNull": false,
|
||||
"autoincrement": false,
|
||||
"default": null,
|
||||
"generated": null,
|
||||
"name": "id",
|
||||
"entityType": "columns",
|
||||
"table": "project"
|
||||
},
|
||||
{
|
||||
"type": "text",
|
||||
"notNull": true,
|
||||
"autoincrement": false,
|
||||
"default": null,
|
||||
"generated": null,
|
||||
"name": "worktree",
|
||||
"entityType": "columns",
|
||||
"table": "project"
|
||||
},
|
||||
{
|
||||
"type": "text",
|
||||
"notNull": false,
|
||||
"autoincrement": false,
|
||||
"default": null,
|
||||
"generated": null,
|
||||
"name": "vcs",
|
||||
"entityType": "columns",
|
||||
"table": "project"
|
||||
},
|
||||
{
|
||||
"type": "text",
|
||||
"notNull": false,
|
||||
"autoincrement": false,
|
||||
"default": null,
|
||||
"generated": null,
|
||||
"name": "name",
|
||||
"entityType": "columns",
|
||||
"table": "project"
|
||||
},
|
||||
{
|
||||
"type": "text",
|
||||
"notNull": false,
|
||||
"autoincrement": false,
|
||||
"default": null,
|
||||
"generated": null,
|
||||
"name": "icon_url",
|
||||
"entityType": "columns",
|
||||
"table": "project"
|
||||
},
|
||||
{
|
||||
"type": "text",
|
||||
"notNull": false,
|
||||
"autoincrement": false,
|
||||
"default": null,
|
||||
"generated": null,
|
||||
"name": "icon_url_override",
|
||||
"entityType": "columns",
|
||||
"table": "project"
|
||||
},
|
||||
{
|
||||
"type": "text",
|
||||
"notNull": false,
|
||||
"autoincrement": false,
|
||||
"default": null,
|
||||
"generated": null,
|
||||
"name": "icon_color",
|
||||
"entityType": "columns",
|
||||
"table": "project"
|
||||
},
|
||||
{
|
||||
"type": "integer",
|
||||
"notNull": true,
|
||||
"autoincrement": false,
|
||||
"default": null,
|
||||
"generated": null,
|
||||
"name": "time_created",
|
||||
"entityType": "columns",
|
||||
"table": "project"
|
||||
},
|
||||
{
|
||||
"type": "integer",
|
||||
"notNull": true,
|
||||
"autoincrement": false,
|
||||
"default": null,
|
||||
"generated": null,
|
||||
"name": "time_updated",
|
||||
"entityType": "columns",
|
||||
"table": "project"
|
||||
},
|
||||
{
|
||||
"type": "integer",
|
||||
"notNull": false,
|
||||
"autoincrement": false,
|
||||
"default": null,
|
||||
"generated": null,
|
||||
"name": "time_initialized",
|
||||
"entityType": "columns",
|
||||
"table": "project"
|
||||
},
|
||||
{
|
||||
"type": "text",
|
||||
"notNull": true,
|
||||
"autoincrement": false,
|
||||
"default": null,
|
||||
"generated": null,
|
||||
"name": "sandboxes",
|
||||
"entityType": "columns",
|
||||
"table": "project"
|
||||
},
|
||||
{
|
||||
"type": "text",
|
||||
"notNull": false,
|
||||
"autoincrement": false,
|
||||
"default": null,
|
||||
"generated": null,
|
||||
"name": "commands",
|
||||
"entityType": "columns",
|
||||
"table": "project"
|
||||
},
|
||||
{
|
||||
"type": "text",
|
||||
"notNull": false,
|
||||
"autoincrement": false,
|
||||
"default": null,
|
||||
"generated": null,
|
||||
"name": "id",
|
||||
"entityType": "columns",
|
||||
"table": "message"
|
||||
},
|
||||
{
|
||||
"type": "text",
|
||||
"notNull": true,
|
||||
"autoincrement": false,
|
||||
"default": null,
|
||||
"generated": null,
|
||||
"name": "session_id",
|
||||
"entityType": "columns",
|
||||
"table": "message"
|
||||
},
|
||||
{
|
||||
"type": "integer",
|
||||
"notNull": true,
|
||||
"autoincrement": false,
|
||||
"default": null,
|
||||
"generated": null,
|
||||
"name": "time_created",
|
||||
"entityType": "columns",
|
||||
"table": "message"
|
||||
},
|
||||
{
|
||||
"type": "integer",
|
||||
"notNull": true,
|
||||
"autoincrement": false,
|
||||
"default": null,
|
||||
"generated": null,
|
||||
"name": "time_updated",
|
||||
"entityType": "columns",
|
||||
"table": "message"
|
||||
},
|
||||
{
|
||||
"type": "text",
|
||||
"notNull": true,
|
||||
"autoincrement": false,
|
||||
"default": null,
|
||||
"generated": null,
|
||||
"name": "data",
|
||||
"entityType": "columns",
|
||||
"table": "message"
|
||||
},
|
||||
{
|
||||
"type": "text",
|
||||
"notNull": false,
|
||||
"autoincrement": false,
|
||||
"default": null,
|
||||
"generated": null,
|
||||
"name": "id",
|
||||
"entityType": "columns",
|
||||
"table": "part"
|
||||
},
|
||||
{
|
||||
"type": "text",
|
||||
"notNull": true,
|
||||
"autoincrement": false,
|
||||
"default": null,
|
||||
"generated": null,
|
||||
"name": "message_id",
|
||||
"entityType": "columns",
|
||||
"table": "part"
|
||||
},
|
||||
{
|
||||
"type": "text",
|
||||
"notNull": true,
|
||||
"autoincrement": false,
|
||||
"default": null,
|
||||
"generated": null,
|
||||
"name": "session_id",
|
||||
"entityType": "columns",
|
||||
"table": "part"
|
||||
},
|
||||
{
|
||||
"type": "integer",
|
||||
"notNull": true,
|
||||
"autoincrement": false,
|
||||
"default": null,
|
||||
"generated": null,
|
||||
"name": "time_created",
|
||||
"entityType": "columns",
|
||||
"table": "part"
|
||||
},
|
||||
{
|
||||
"type": "integer",
|
||||
"notNull": true,
|
||||
"autoincrement": false,
|
||||
"default": null,
|
||||
"generated": null,
|
||||
"name": "time_updated",
|
||||
"entityType": "columns",
|
||||
"table": "part"
|
||||
},
|
||||
{
|
||||
"type": "text",
|
||||
"notNull": true,
|
||||
"autoincrement": false,
|
||||
"default": null,
|
||||
"generated": null,
|
||||
"name": "data",
|
||||
"entityType": "columns",
|
||||
"table": "part"
|
||||
},
|
||||
{
|
||||
"type": "text",
|
||||
"notNull": false,
|
||||
"autoincrement": false,
|
||||
"default": null,
|
||||
"generated": null,
|
||||
"name": "project_id",
|
||||
"entityType": "columns",
|
||||
"table": "permission"
|
||||
},
|
||||
{
|
||||
"type": "integer",
|
||||
"notNull": true,
|
||||
"autoincrement": false,
|
||||
"default": null,
|
||||
"generated": null,
|
||||
"name": "time_created",
|
||||
"entityType": "columns",
|
||||
"table": "permission"
|
||||
},
|
||||
{
|
||||
"type": "integer",
|
||||
"notNull": true,
|
||||
"autoincrement": false,
|
||||
"default": null,
|
||||
"generated": null,
|
||||
"name": "time_updated",
|
||||
"entityType": "columns",
|
||||
"table": "permission"
|
||||
},
|
||||
{
|
||||
"type": "text",
|
||||
"notNull": true,
|
||||
"autoincrement": false,
|
||||
"default": null,
|
||||
"generated": null,
|
||||
"name": "data",
|
||||
"entityType": "columns",
|
||||
"table": "permission"
|
||||
},
|
||||
{
|
||||
"type": "text",
|
||||
"notNull": false,
|
||||
"autoincrement": false,
|
||||
"default": null,
|
||||
"generated": null,
|
||||
"name": "id",
|
||||
"entityType": "columns",
|
||||
"table": "session_message"
|
||||
},
|
||||
{
|
||||
"type": "text",
|
||||
"notNull": true,
|
||||
"autoincrement": false,
|
||||
"default": null,
|
||||
"generated": null,
|
||||
"name": "session_id",
|
||||
"entityType": "columns",
|
||||
"table": "session_message"
|
||||
},
|
||||
{
|
||||
"type": "text",
|
||||
"notNull": true,
|
||||
"autoincrement": false,
|
||||
"default": null,
|
||||
"generated": null,
|
||||
"name": "type",
|
||||
"entityType": "columns",
|
||||
"table": "session_message"
|
||||
},
|
||||
{
|
||||
"type": "integer",
|
||||
"notNull": true,
|
||||
"autoincrement": false,
|
||||
"default": null,
|
||||
"generated": null,
|
||||
"name": "time_created",
|
||||
"entityType": "columns",
|
||||
"table": "session_message"
|
||||
},
|
||||
{
|
||||
"type": "integer",
|
||||
"notNull": true,
|
||||
"autoincrement": false,
|
||||
"default": null,
|
||||
"generated": null,
|
||||
"name": "time_updated",
|
||||
"entityType": "columns",
|
||||
"table": "session_message"
|
||||
},
|
||||
{
|
||||
"type": "text",
|
||||
"notNull": true,
|
||||
"autoincrement": false,
|
||||
"default": null,
|
||||
"generated": null,
|
||||
"name": "data",
|
||||
"entityType": "columns",
|
||||
"table": "session_message"
|
||||
},
|
||||
{
|
||||
"type": "text",
|
||||
"notNull": false,
|
||||
"autoincrement": false,
|
||||
"default": null,
|
||||
"generated": null,
|
||||
"name": "id",
|
||||
"entityType": "columns",
|
||||
"table": "session"
|
||||
},
|
||||
{
|
||||
"type": "text",
|
||||
"notNull": true,
|
||||
"autoincrement": false,
|
||||
"default": null,
|
||||
"generated": null,
|
||||
"name": "project_id",
|
||||
"entityType": "columns",
|
||||
"table": "session"
|
||||
},
|
||||
{
|
||||
"type": "text",
|
||||
"notNull": false,
|
||||
"autoincrement": false,
|
||||
"default": null,
|
||||
"generated": null,
|
||||
"name": "workspace_id",
|
||||
"entityType": "columns",
|
||||
"table": "session"
|
||||
},
|
||||
{
|
||||
"type": "text",
|
||||
"notNull": false,
|
||||
"autoincrement": false,
|
||||
"default": null,
|
||||
"generated": null,
|
||||
"name": "parent_id",
|
||||
"entityType": "columns",
|
||||
"table": "session"
|
||||
},
|
||||
{
|
||||
"type": "text",
|
||||
"notNull": true,
|
||||
"autoincrement": false,
|
||||
"default": null,
|
||||
"generated": null,
|
||||
"name": "slug",
|
||||
"entityType": "columns",
|
||||
"table": "session"
|
||||
},
|
||||
{
|
||||
"type": "text",
|
||||
"notNull": true,
|
||||
"autoincrement": false,
|
||||
"default": null,
|
||||
"generated": null,
|
||||
"name": "directory",
|
||||
"entityType": "columns",
|
||||
"table": "session"
|
||||
},
|
||||
{
|
||||
"type": "text",
|
||||
"notNull": false,
|
||||
"autoincrement": false,
|
||||
"default": null,
|
||||
"generated": null,
|
||||
"name": "path",
|
||||
"entityType": "columns",
|
||||
"table": "session"
|
||||
},
|
||||
{
|
||||
"type": "text",
|
||||
"notNull": true,
|
||||
"autoincrement": false,
|
||||
"default": null,
|
||||
"generated": null,
|
||||
"name": "title",
|
||||
"entityType": "columns",
|
||||
"table": "session"
|
||||
},
|
||||
{
|
||||
"type": "text",
|
||||
"notNull": true,
|
||||
"autoincrement": false,
|
||||
"default": null,
|
||||
"generated": null,
|
||||
"name": "version",
|
||||
"entityType": "columns",
|
||||
"table": "session"
|
||||
},
|
||||
{
|
||||
"type": "text",
|
||||
"notNull": false,
|
||||
"autoincrement": false,
|
||||
"default": null,
|
||||
"generated": null,
|
||||
"name": "share_url",
|
||||
"entityType": "columns",
|
||||
"table": "session"
|
||||
},
|
||||
{
|
||||
"type": "integer",
|
||||
"notNull": false,
|
||||
"autoincrement": false,
|
||||
"default": null,
|
||||
"generated": null,
|
||||
"name": "summary_additions",
|
||||
"entityType": "columns",
|
||||
"table": "session"
|
||||
},
|
||||
{
|
||||
"type": "integer",
|
||||
"notNull": false,
|
||||
"autoincrement": false,
|
||||
"default": null,
|
||||
"generated": null,
|
||||
"name": "summary_deletions",
|
||||
"entityType": "columns",
|
||||
"table": "session"
|
||||
},
|
||||
{
|
||||
"type": "integer",
|
||||
"notNull": false,
|
||||
"autoincrement": false,
|
||||
"default": null,
|
||||
"generated": null,
|
||||
"name": "summary_files",
|
||||
"entityType": "columns",
|
||||
"table": "session"
|
||||
},
|
||||
{
|
||||
"type": "text",
|
||||
"notNull": false,
|
||||
"autoincrement": false,
|
||||
"default": null,
|
||||
"generated": null,
|
||||
"name": "summary_diffs",
|
||||
"entityType": "columns",
|
||||
"table": "session"
|
||||
},
|
||||
{
|
||||
"type": "text",
|
||||
"notNull": false,
|
||||
"autoincrement": false,
|
||||
"default": null,
|
||||
"generated": null,
|
||||
"name": "metadata",
|
||||
"entityType": "columns",
|
||||
"table": "session"
|
||||
},
|
||||
{
|
||||
"type": "real",
|
||||
"notNull": true,
|
||||
"autoincrement": false,
|
||||
"default": "0",
|
||||
"generated": null,
|
||||
"name": "cost",
|
||||
"entityType": "columns",
|
||||
"table": "session"
|
||||
},
|
||||
{
|
||||
"type": "integer",
|
||||
"notNull": true,
|
||||
"autoincrement": false,
|
||||
"default": "0",
|
||||
"generated": null,
|
||||
"name": "tokens_input",
|
||||
"entityType": "columns",
|
||||
"table": "session"
|
||||
},
|
||||
{
|
||||
"type": "integer",
|
||||
"notNull": true,
|
||||
"autoincrement": false,
|
||||
"default": "0",
|
||||
"generated": null,
|
||||
"name": "tokens_output",
|
||||
"entityType": "columns",
|
||||
"table": "session"
|
||||
},
|
||||
{
|
||||
"type": "integer",
|
||||
"notNull": true,
|
||||
"autoincrement": false,
|
||||
"default": "0",
|
||||
"generated": null,
|
||||
"name": "tokens_reasoning",
|
||||
"entityType": "columns",
|
||||
"table": "session"
|
||||
},
|
||||
{
|
||||
"type": "integer",
|
||||
"notNull": true,
|
||||
"autoincrement": false,
|
||||
"default": "0",
|
||||
"generated": null,
|
||||
"name": "tokens_cache_read",
|
||||
"entityType": "columns",
|
||||
"table": "session"
|
||||
},
|
||||
{
|
||||
"type": "integer",
|
||||
"notNull": true,
|
||||
"autoincrement": false,
|
||||
"default": "0",
|
||||
"generated": null,
|
||||
"name": "tokens_cache_write",
|
||||
"entityType": "columns",
|
||||
"table": "session"
|
||||
},
|
||||
{
|
||||
"type": "text",
|
||||
"notNull": false,
|
||||
"autoincrement": false,
|
||||
"default": null,
|
||||
"generated": null,
|
||||
"name": "revert",
|
||||
"entityType": "columns",
|
||||
"table": "session"
|
||||
},
|
||||
{
|
||||
"type": "text",
|
||||
"notNull": false,
|
||||
"autoincrement": false,
|
||||
"default": null,
|
||||
"generated": null,
|
||||
"name": "permission",
|
||||
"entityType": "columns",
|
||||
"table": "session"
|
||||
},
|
||||
{
|
||||
"type": "text",
|
||||
"notNull": false,
|
||||
"autoincrement": false,
|
||||
"default": null,
|
||||
"generated": null,
|
||||
"name": "agent",
|
||||
"entityType": "columns",
|
||||
"table": "session"
|
||||
},
|
||||
{
|
||||
"type": "text",
|
||||
"notNull": false,
|
||||
"autoincrement": false,
|
||||
"default": null,
|
||||
"generated": null,
|
||||
"name": "model",
|
||||
"entityType": "columns",
|
||||
"table": "session"
|
||||
},
|
||||
{
|
||||
"type": "integer",
|
||||
"notNull": true,
|
||||
"autoincrement": false,
|
||||
"default": null,
|
||||
"generated": null,
|
||||
"name": "time_created",
|
||||
"entityType": "columns",
|
||||
"table": "session"
|
||||
},
|
||||
{
|
||||
"type": "integer",
|
||||
"notNull": true,
|
||||
"autoincrement": false,
|
||||
"default": null,
|
||||
"generated": null,
|
||||
"name": "time_updated",
|
||||
"entityType": "columns",
|
||||
"table": "session"
|
||||
},
|
||||
{
|
||||
"type": "integer",
|
||||
"notNull": false,
|
||||
"autoincrement": false,
|
||||
"default": null,
|
||||
"generated": null,
|
||||
"name": "time_compacting",
|
||||
"entityType": "columns",
|
||||
"table": "session"
|
||||
},
|
||||
{
|
||||
"type": "integer",
|
||||
"notNull": false,
|
||||
"autoincrement": false,
|
||||
"default": null,
|
||||
"generated": null,
|
||||
"name": "time_archived",
|
||||
"entityType": "columns",
|
||||
"table": "session"
|
||||
},
|
||||
{
|
||||
"type": "text",
|
||||
"notNull": true,
|
||||
"autoincrement": false,
|
||||
"default": null,
|
||||
"generated": null,
|
||||
"name": "session_id",
|
||||
"entityType": "columns",
|
||||
"table": "todo"
|
||||
},
|
||||
{
|
||||
"type": "text",
|
||||
"notNull": true,
|
||||
"autoincrement": false,
|
||||
"default": null,
|
||||
"generated": null,
|
||||
"name": "content",
|
||||
"entityType": "columns",
|
||||
"table": "todo"
|
||||
},
|
||||
{
|
||||
"type": "text",
|
||||
"notNull": true,
|
||||
"autoincrement": false,
|
||||
"default": null,
|
||||
"generated": null,
|
||||
"name": "status",
|
||||
"entityType": "columns",
|
||||
"table": "todo"
|
||||
},
|
||||
{
|
||||
"type": "text",
|
||||
"notNull": true,
|
||||
"autoincrement": false,
|
||||
"default": null,
|
||||
"generated": null,
|
||||
"name": "priority",
|
||||
"entityType": "columns",
|
||||
"table": "todo"
|
||||
},
|
||||
{
|
||||
"type": "integer",
|
||||
"notNull": true,
|
||||
"autoincrement": false,
|
||||
"default": null,
|
||||
"generated": null,
|
||||
"name": "position",
|
||||
"entityType": "columns",
|
||||
"table": "todo"
|
||||
},
|
||||
{
|
||||
"type": "integer",
|
||||
"notNull": true,
|
||||
"autoincrement": false,
|
||||
"default": null,
|
||||
"generated": null,
|
||||
"name": "time_created",
|
||||
"entityType": "columns",
|
||||
"table": "todo"
|
||||
},
|
||||
{
|
||||
"type": "integer",
|
||||
"notNull": true,
|
||||
"autoincrement": false,
|
||||
"default": null,
|
||||
"generated": null,
|
||||
"name": "time_updated",
|
||||
"entityType": "columns",
|
||||
"table": "todo"
|
||||
},
|
||||
{
|
||||
"type": "text",
|
||||
"notNull": false,
|
||||
"autoincrement": false,
|
||||
"default": null,
|
||||
"generated": null,
|
||||
"name": "session_id",
|
||||
"entityType": "columns",
|
||||
"table": "session_share"
|
||||
},
|
||||
{
|
||||
"type": "text",
|
||||
"notNull": true,
|
||||
"autoincrement": false,
|
||||
"default": null,
|
||||
"generated": null,
|
||||
"name": "id",
|
||||
"entityType": "columns",
|
||||
"table": "session_share"
|
||||
},
|
||||
{
|
||||
"type": "text",
|
||||
"notNull": true,
|
||||
"autoincrement": false,
|
||||
"default": null,
|
||||
"generated": null,
|
||||
"name": "secret",
|
||||
"entityType": "columns",
|
||||
"table": "session_share"
|
||||
},
|
||||
{
|
||||
"type": "text",
|
||||
"notNull": true,
|
||||
"autoincrement": false,
|
||||
"default": null,
|
||||
"generated": null,
|
||||
"name": "url",
|
||||
"entityType": "columns",
|
||||
"table": "session_share"
|
||||
},
|
||||
{
|
||||
"type": "integer",
|
||||
"notNull": true,
|
||||
"autoincrement": false,
|
||||
"default": null,
|
||||
"generated": null,
|
||||
"name": "time_created",
|
||||
"entityType": "columns",
|
||||
"table": "session_share"
|
||||
},
|
||||
{
|
||||
"type": "integer",
|
||||
"notNull": true,
|
||||
"autoincrement": false,
|
||||
"default": null,
|
||||
"generated": null,
|
||||
"name": "time_updated",
|
||||
"entityType": "columns",
|
||||
"table": "session_share"
|
||||
},
|
||||
{
|
||||
"columns": [
|
||||
"project_id"
|
||||
],
|
||||
"tableTo": "project",
|
||||
"columnsTo": [
|
||||
"id"
|
||||
],
|
||||
"onUpdate": "NO ACTION",
|
||||
"onDelete": "CASCADE",
|
||||
"nameExplicit": false,
|
||||
"name": "fk_workspace_project_id_project_id_fk",
|
||||
"entityType": "fks",
|
||||
"table": "workspace"
|
||||
},
|
||||
{
|
||||
"columns": [
|
||||
"project_id"
|
||||
],
|
||||
"tableTo": "project",
|
||||
"columnsTo": [
|
||||
"id"
|
||||
],
|
||||
"onUpdate": "NO ACTION",
|
||||
"onDelete": "CASCADE",
|
||||
"nameExplicit": false,
|
||||
"name": "fk_project_path_project_id_project_id_fk",
|
||||
"entityType": "fks",
|
||||
"table": "project_path"
|
||||
},
|
||||
{
|
||||
"columns": [
|
||||
"active_account_id"
|
||||
],
|
||||
"tableTo": "account",
|
||||
"columnsTo": [
|
||||
"id"
|
||||
],
|
||||
"onUpdate": "NO ACTION",
|
||||
"onDelete": "SET NULL",
|
||||
"nameExplicit": false,
|
||||
"name": "fk_account_state_active_account_id_account_id_fk",
|
||||
"entityType": "fks",
|
||||
"table": "account_state"
|
||||
},
|
||||
{
|
||||
"columns": [
|
||||
"aggregate_id"
|
||||
],
|
||||
"tableTo": "event_sequence",
|
||||
"columnsTo": [
|
||||
"aggregate_id"
|
||||
],
|
||||
"onUpdate": "NO ACTION",
|
||||
"onDelete": "CASCADE",
|
||||
"nameExplicit": false,
|
||||
"name": "fk_event_aggregate_id_event_sequence_aggregate_id_fk",
|
||||
"entityType": "fks",
|
||||
"table": "event"
|
||||
},
|
||||
{
|
||||
"columns": [
|
||||
"session_id"
|
||||
],
|
||||
"tableTo": "session",
|
||||
"columnsTo": [
|
||||
"id"
|
||||
],
|
||||
"onUpdate": "NO ACTION",
|
||||
"onDelete": "CASCADE",
|
||||
"nameExplicit": false,
|
||||
"name": "fk_message_session_id_session_id_fk",
|
||||
"entityType": "fks",
|
||||
"table": "message"
|
||||
},
|
||||
{
|
||||
"columns": [
|
||||
"message_id"
|
||||
],
|
||||
"tableTo": "message",
|
||||
"columnsTo": [
|
||||
"id"
|
||||
],
|
||||
"onUpdate": "NO ACTION",
|
||||
"onDelete": "CASCADE",
|
||||
"nameExplicit": false,
|
||||
"name": "fk_part_message_id_message_id_fk",
|
||||
"entityType": "fks",
|
||||
"table": "part"
|
||||
},
|
||||
{
|
||||
"columns": [
|
||||
"project_id"
|
||||
],
|
||||
"tableTo": "project",
|
||||
"columnsTo": [
|
||||
"id"
|
||||
],
|
||||
"onUpdate": "NO ACTION",
|
||||
"onDelete": "CASCADE",
|
||||
"nameExplicit": false,
|
||||
"name": "fk_permission_project_id_project_id_fk",
|
||||
"entityType": "fks",
|
||||
"table": "permission"
|
||||
},
|
||||
{
|
||||
"columns": [
|
||||
"session_id"
|
||||
],
|
||||
"tableTo": "session",
|
||||
"columnsTo": [
|
||||
"id"
|
||||
],
|
||||
"onUpdate": "NO ACTION",
|
||||
"onDelete": "CASCADE",
|
||||
"nameExplicit": false,
|
||||
"name": "fk_session_message_session_id_session_id_fk",
|
||||
"entityType": "fks",
|
||||
"table": "session_message"
|
||||
},
|
||||
{
|
||||
"columns": [
|
||||
"project_id"
|
||||
],
|
||||
"tableTo": "project",
|
||||
"columnsTo": [
|
||||
"id"
|
||||
],
|
||||
"onUpdate": "NO ACTION",
|
||||
"onDelete": "CASCADE",
|
||||
"nameExplicit": false,
|
||||
"name": "fk_session_project_id_project_id_fk",
|
||||
"entityType": "fks",
|
||||
"table": "session"
|
||||
},
|
||||
{
|
||||
"columns": [
|
||||
"session_id"
|
||||
],
|
||||
"tableTo": "session",
|
||||
"columnsTo": [
|
||||
"id"
|
||||
],
|
||||
"onUpdate": "NO ACTION",
|
||||
"onDelete": "CASCADE",
|
||||
"nameExplicit": false,
|
||||
"name": "fk_todo_session_id_session_id_fk",
|
||||
"entityType": "fks",
|
||||
"table": "todo"
|
||||
},
|
||||
{
|
||||
"columns": [
|
||||
"session_id"
|
||||
],
|
||||
"tableTo": "session",
|
||||
"columnsTo": [
|
||||
"id"
|
||||
],
|
||||
"onUpdate": "NO ACTION",
|
||||
"onDelete": "CASCADE",
|
||||
"nameExplicit": false,
|
||||
"name": "fk_session_share_session_id_session_id_fk",
|
||||
"entityType": "fks",
|
||||
"table": "session_share"
|
||||
},
|
||||
{
|
||||
"columns": [
|
||||
"project_id",
|
||||
"path"
|
||||
],
|
||||
"nameExplicit": false,
|
||||
"name": "project_path_pk",
|
||||
"entityType": "pks",
|
||||
"table": "project_path"
|
||||
},
|
||||
{
|
||||
"columns": [
|
||||
"email",
|
||||
"url"
|
||||
],
|
||||
"nameExplicit": false,
|
||||
"name": "control_account_pk",
|
||||
"entityType": "pks",
|
||||
"table": "control_account"
|
||||
},
|
||||
{
|
||||
"columns": [
|
||||
"session_id",
|
||||
"position"
|
||||
],
|
||||
"nameExplicit": false,
|
||||
"name": "todo_pk",
|
||||
"entityType": "pks",
|
||||
"table": "todo"
|
||||
},
|
||||
{
|
||||
"columns": [
|
||||
"id"
|
||||
],
|
||||
"nameExplicit": false,
|
||||
"name": "workspace_pk",
|
||||
"table": "workspace",
|
||||
"entityType": "pks"
|
||||
},
|
||||
{
|
||||
"columns": [
|
||||
"name"
|
||||
],
|
||||
"nameExplicit": false,
|
||||
"name": "data_migration_pk",
|
||||
"table": "data_migration",
|
||||
"entityType": "pks"
|
||||
},
|
||||
{
|
||||
"columns": [
|
||||
"id"
|
||||
],
|
||||
"nameExplicit": false,
|
||||
"name": "account_state_pk",
|
||||
"table": "account_state",
|
||||
"entityType": "pks"
|
||||
},
|
||||
{
|
||||
"columns": [
|
||||
"id"
|
||||
],
|
||||
"nameExplicit": false,
|
||||
"name": "account_pk",
|
||||
"table": "account",
|
||||
"entityType": "pks"
|
||||
},
|
||||
{
|
||||
"columns": [
|
||||
"aggregate_id"
|
||||
],
|
||||
"nameExplicit": false,
|
||||
"name": "event_sequence_pk",
|
||||
"table": "event_sequence",
|
||||
"entityType": "pks"
|
||||
},
|
||||
{
|
||||
"columns": [
|
||||
"id"
|
||||
],
|
||||
"nameExplicit": false,
|
||||
"name": "event_pk",
|
||||
"table": "event",
|
||||
"entityType": "pks"
|
||||
},
|
||||
{
|
||||
"columns": [
|
||||
"id"
|
||||
],
|
||||
"nameExplicit": false,
|
||||
"name": "project_pk",
|
||||
"table": "project",
|
||||
"entityType": "pks"
|
||||
},
|
||||
{
|
||||
"columns": [
|
||||
"id"
|
||||
],
|
||||
"nameExplicit": false,
|
||||
"name": "message_pk",
|
||||
"table": "message",
|
||||
"entityType": "pks"
|
||||
},
|
||||
{
|
||||
"columns": [
|
||||
"id"
|
||||
],
|
||||
"nameExplicit": false,
|
||||
"name": "part_pk",
|
||||
"table": "part",
|
||||
"entityType": "pks"
|
||||
},
|
||||
{
|
||||
"columns": [
|
||||
"project_id"
|
||||
],
|
||||
"nameExplicit": false,
|
||||
"name": "permission_pk",
|
||||
"table": "permission",
|
||||
"entityType": "pks"
|
||||
},
|
||||
{
|
||||
"columns": [
|
||||
"id"
|
||||
],
|
||||
"nameExplicit": false,
|
||||
"name": "session_message_pk",
|
||||
"table": "session_message",
|
||||
"entityType": "pks"
|
||||
},
|
||||
{
|
||||
"columns": [
|
||||
"id"
|
||||
],
|
||||
"nameExplicit": false,
|
||||
"name": "session_pk",
|
||||
"table": "session",
|
||||
"entityType": "pks"
|
||||
},
|
||||
{
|
||||
"columns": [
|
||||
"session_id"
|
||||
],
|
||||
"nameExplicit": false,
|
||||
"name": "session_share_pk",
|
||||
"table": "session_share",
|
||||
"entityType": "pks"
|
||||
},
|
||||
{
|
||||
"columns": [
|
||||
{
|
||||
"value": "project_id",
|
||||
"isExpression": false
|
||||
}
|
||||
],
|
||||
"isUnique": true,
|
||||
"where": "\"project_path\".\"primary\" = 1",
|
||||
"origin": "manual",
|
||||
"name": "project_path_project_primary_idx",
|
||||
"entityType": "indexes",
|
||||
"table": "project_path"
|
||||
},
|
||||
{
|
||||
"columns": [
|
||||
{
|
||||
"value": "session_id",
|
||||
"isExpression": false
|
||||
},
|
||||
{
|
||||
"value": "time_created",
|
||||
"isExpression": false
|
||||
},
|
||||
{
|
||||
"value": "id",
|
||||
"isExpression": false
|
||||
}
|
||||
],
|
||||
"isUnique": false,
|
||||
"where": null,
|
||||
"origin": "manual",
|
||||
"name": "message_session_time_created_id_idx",
|
||||
"entityType": "indexes",
|
||||
"table": "message"
|
||||
},
|
||||
{
|
||||
"columns": [
|
||||
{
|
||||
"value": "message_id",
|
||||
"isExpression": false
|
||||
},
|
||||
{
|
||||
"value": "id",
|
||||
"isExpression": false
|
||||
}
|
||||
],
|
||||
"isUnique": false,
|
||||
"where": null,
|
||||
"origin": "manual",
|
||||
"name": "part_message_id_id_idx",
|
||||
"entityType": "indexes",
|
||||
"table": "part"
|
||||
},
|
||||
{
|
||||
"columns": [
|
||||
{
|
||||
"value": "session_id",
|
||||
"isExpression": false
|
||||
}
|
||||
],
|
||||
"isUnique": false,
|
||||
"where": null,
|
||||
"origin": "manual",
|
||||
"name": "part_session_idx",
|
||||
"entityType": "indexes",
|
||||
"table": "part"
|
||||
},
|
||||
{
|
||||
"columns": [
|
||||
{
|
||||
"value": "session_id",
|
||||
"isExpression": false
|
||||
}
|
||||
],
|
||||
"isUnique": false,
|
||||
"where": null,
|
||||
"origin": "manual",
|
||||
"name": "session_message_session_idx",
|
||||
"entityType": "indexes",
|
||||
"table": "session_message"
|
||||
},
|
||||
{
|
||||
"columns": [
|
||||
{
|
||||
"value": "session_id",
|
||||
"isExpression": false
|
||||
},
|
||||
{
|
||||
"value": "type",
|
||||
"isExpression": false
|
||||
}
|
||||
],
|
||||
"isUnique": false,
|
||||
"where": null,
|
||||
"origin": "manual",
|
||||
"name": "session_message_session_type_idx",
|
||||
"entityType": "indexes",
|
||||
"table": "session_message"
|
||||
},
|
||||
{
|
||||
"columns": [
|
||||
{
|
||||
"value": "time_created",
|
||||
"isExpression": false
|
||||
}
|
||||
],
|
||||
"isUnique": false,
|
||||
"where": null,
|
||||
"origin": "manual",
|
||||
"name": "session_message_time_created_idx",
|
||||
"entityType": "indexes",
|
||||
"table": "session_message"
|
||||
},
|
||||
{
|
||||
"columns": [
|
||||
{
|
||||
"value": "project_id",
|
||||
"isExpression": false
|
||||
}
|
||||
],
|
||||
"isUnique": false,
|
||||
"where": null,
|
||||
"origin": "manual",
|
||||
"name": "session_project_idx",
|
||||
"entityType": "indexes",
|
||||
"table": "session"
|
||||
},
|
||||
{
|
||||
"columns": [
|
||||
{
|
||||
"value": "workspace_id",
|
||||
"isExpression": false
|
||||
}
|
||||
],
|
||||
"isUnique": false,
|
||||
"where": null,
|
||||
"origin": "manual",
|
||||
"name": "session_workspace_idx",
|
||||
"entityType": "indexes",
|
||||
"table": "session"
|
||||
},
|
||||
{
|
||||
"columns": [
|
||||
{
|
||||
"value": "parent_id",
|
||||
"isExpression": false
|
||||
}
|
||||
],
|
||||
"isUnique": false,
|
||||
"where": null,
|
||||
"origin": "manual",
|
||||
"name": "session_parent_idx",
|
||||
"entityType": "indexes",
|
||||
"table": "session"
|
||||
},
|
||||
{
|
||||
"columns": [
|
||||
{
|
||||
"value": "session_id",
|
||||
"isExpression": false
|
||||
}
|
||||
],
|
||||
"isUnique": false,
|
||||
"where": null,
|
||||
"origin": "manual",
|
||||
"name": "todo_session_idx",
|
||||
"entityType": "indexes",
|
||||
"table": "todo"
|
||||
}
|
||||
],
|
||||
"renames": []
|
||||
}
|
||||
@@ -23,5 +23,6 @@ export const migrations = (
|
||||
import("./migration/20260510033149_session_usage"),
|
||||
import("./migration/20260511000411_data_migration_state"),
|
||||
import("./migration/20260530232709_lovely_romulus"),
|
||||
import("./migration/20260531185402_jittery_blink"),
|
||||
])
|
||||
).map((module) => module.default) satisfies DatabaseMigration.Migration[]
|
||||
|
||||
@@ -0,0 +1,22 @@
|
||||
import { Effect } from "effect"
|
||||
import type { DatabaseMigration } from "../migration"
|
||||
|
||||
export default {
|
||||
id: "20260531185402_jittery_blink",
|
||||
up(tx) {
|
||||
return Effect.gen(function* () {
|
||||
yield* tx.run(`
|
||||
CREATE TABLE \`project_path\` (
|
||||
\`project_id\` text NOT NULL,
|
||||
\`path\` text NOT NULL,
|
||||
\`primary\` integer DEFAULT false NOT NULL,
|
||||
\`time_created\` integer NOT NULL,
|
||||
\`time_updated\` integer NOT NULL,
|
||||
CONSTRAINT \`project_path_pk\` PRIMARY KEY(\`project_id\`, \`path\`),
|
||||
CONSTRAINT \`fk_project_path_project_id_project_id_fk\` FOREIGN KEY (\`project_id\`) REFERENCES \`project\`(\`id\`) ON DELETE CASCADE
|
||||
);
|
||||
`)
|
||||
yield* tx.run(`CREATE UNIQUE INDEX \`project_path_project_primary_idx\` ON \`project_path\` (\`project_id\`) WHERE "project_path"."primary" = 1;`)
|
||||
})
|
||||
},
|
||||
} satisfies DatabaseMigration.Migration
|
||||
@@ -1,7 +1,7 @@
|
||||
export * as Git from "./git"
|
||||
|
||||
import path from "path"
|
||||
import { Context, Effect, Layer } from "effect"
|
||||
import { Context, Effect, Layer, Schema } from "effect"
|
||||
import { ChildProcess } from "effect/unstable/process"
|
||||
import { AbsolutePath } from "./schema"
|
||||
import { AppFileSystem } from "./filesystem"
|
||||
@@ -26,10 +26,20 @@ export interface Repo {
|
||||
readonly store: AbsolutePath
|
||||
}
|
||||
|
||||
export class WorktreeError extends Schema.TaggedErrorClass<WorktreeError>()("Git.WorktreeError", {
|
||||
operation: Schema.Literals(["create", "remove", "list"]),
|
||||
message: Schema.String,
|
||||
path: Schema.optional(AbsolutePath),
|
||||
cause: Schema.optional(Schema.Defect),
|
||||
}) {}
|
||||
|
||||
export interface Interface {
|
||||
readonly find: (input: AbsolutePath) => Effect.Effect<Repo | undefined>
|
||||
readonly remote: (repo: Repo, name?: string) => Effect.Effect<string | undefined>
|
||||
readonly roots: (repo: Repo) => Effect.Effect<string[]>
|
||||
readonly worktreeCreate: (input: { repo: Repo; path: AbsolutePath }) => Effect.Effect<void, WorktreeError>
|
||||
readonly worktreeRemove: (input: { repo: Repo; path: AbsolutePath }) => Effect.Effect<void, WorktreeError>
|
||||
readonly worktreeList: (repo: Repo) => Effect.Effect<AbsolutePath[], WorktreeError>
|
||||
}
|
||||
|
||||
export class Service extends Context.Service<Service, Interface>()("@opencode/GitV2") {}
|
||||
@@ -75,7 +85,43 @@ export const layer = Layer.effect(
|
||||
.toSorted()
|
||||
})
|
||||
|
||||
return Service.of({ find, remote, roots })
|
||||
const worktree = Effect.fnUntraced(function* (
|
||||
operation: "create" | "remove" | "list",
|
||||
repo: Repo,
|
||||
args: string[],
|
||||
worktreePath?: AbsolutePath,
|
||||
) {
|
||||
const result = yield* proc
|
||||
.run(ChildProcess.make("git", args, { cwd: repo.directory, extendEnv: true, stdin: "ignore" }))
|
||||
.pipe(
|
||||
Effect.mapError(
|
||||
(cause) => new WorktreeError({ operation, path: worktreePath, message: cause.message, cause }),
|
||||
),
|
||||
)
|
||||
if (result.exitCode === 0) return result.stdout.toString("utf8")
|
||||
return yield* new WorktreeError({
|
||||
operation,
|
||||
path: worktreePath,
|
||||
message: result.stderr.toString("utf8").trim() || result.stdout.toString("utf8").trim() || "Git failed",
|
||||
})
|
||||
})
|
||||
|
||||
const worktreeCreate = Effect.fn("Git.worktreeCreate")(function* (input: { repo: Repo; path: AbsolutePath }) {
|
||||
yield* worktree("create", input.repo, ["worktree", "add", "--detach", input.path, "HEAD"], input.path)
|
||||
})
|
||||
|
||||
const worktreeRemove = Effect.fn("Git.worktreeRemove")(function* (input: { repo: Repo; path: AbsolutePath }) {
|
||||
yield* worktree("remove", input.repo, ["worktree", "remove", "--force", input.path], input.path)
|
||||
})
|
||||
|
||||
const worktreeList = Effect.fn("Git.worktreeList")(function* (repo: Repo) {
|
||||
return (yield* worktree("list", repo, ["worktree", "list", "--porcelain"]))
|
||||
.split("\n")
|
||||
.filter((line) => line.startsWith("worktree "))
|
||||
.map((line) => AbsolutePath.make(resolvePath(repo.directory, line.slice("worktree ".length).trim())))
|
||||
})
|
||||
|
||||
return Service.of({ find, remote, roots, worktreeCreate, worktreeRemove, worktreeList })
|
||||
}),
|
||||
)
|
||||
|
||||
|
||||
@@ -2,11 +2,14 @@ export * as ProjectV2 from "./project"
|
||||
export * as Project from "./project"
|
||||
|
||||
import { Context, Effect, Layer, Schema } from "effect"
|
||||
import { eq } from "drizzle-orm"
|
||||
import path from "path"
|
||||
import { AbsolutePath, withStatics } from "./schema"
|
||||
import { Database } from "./database/database"
|
||||
import { AppFileSystem } from "./filesystem"
|
||||
import { Git } from "./git"
|
||||
import { Hash } from "./util/hash"
|
||||
import { ProjectPathTable } from "./project/path.sql"
|
||||
|
||||
export const ID = Schema.String.pipe(
|
||||
Schema.brand("Project.ID"),
|
||||
@@ -28,7 +31,21 @@ export class Info extends Schema.Class<Info>("Project.Info")({
|
||||
id: ID,
|
||||
}) {}
|
||||
|
||||
export const PathsInput = Schema.Struct({
|
||||
projectID: ID,
|
||||
}).annotate({ identifier: "Project.PathsInput" })
|
||||
export type PathsInput = typeof PathsInput.Type
|
||||
|
||||
export const Paths = Schema.Array(
|
||||
Schema.Struct({
|
||||
path: AbsolutePath,
|
||||
primary: Schema.Boolean,
|
||||
}),
|
||||
).annotate({ identifier: "Project.Paths" })
|
||||
export type Paths = typeof Paths.Type
|
||||
|
||||
export interface Interface {
|
||||
readonly paths: (input: PathsInput) => Effect.Effect<Paths>
|
||||
readonly resolve: (input: AbsolutePath) => Effect.Effect<
|
||||
{
|
||||
previous?: ID
|
||||
@@ -55,9 +72,22 @@ export class Service extends Context.Service<Service, Interface>()("@opencode/Pr
|
||||
export const layer = Layer.effect(
|
||||
Service,
|
||||
Effect.gen(function* () {
|
||||
const db = (yield* Database.Service).db
|
||||
const fs = yield* AppFileSystem.Service
|
||||
const git = yield* Git.Service
|
||||
|
||||
const paths = Effect.fn("Project.paths")(function* (input: PathsInput) {
|
||||
const rows = yield* db
|
||||
.select({ path: ProjectPathTable.path, primary: ProjectPathTable.primary })
|
||||
.from(ProjectPathTable)
|
||||
.where(eq(ProjectPathTable.project_id, input.projectID))
|
||||
.all()
|
||||
.pipe(Effect.orDie)
|
||||
return rows
|
||||
.toSorted((a, b) => a.path.localeCompare(b.path))
|
||||
.map((row) => ({ path: AbsolutePath.make(row.path), primary: row.primary }))
|
||||
})
|
||||
|
||||
const cached = Effect.fnUntraced(function* (dir: string) {
|
||||
return yield* fs.readFileString(path.join(dir, "opencode")).pipe(
|
||||
Effect.map((value) => value.trim()),
|
||||
@@ -122,8 +152,12 @@ export const layer = Layer.effect(
|
||||
yield* fs.writeFileString(path.join(input.store, "opencode"), input.id).pipe(Effect.ignore)
|
||||
})
|
||||
|
||||
return Service.of({ resolve, commit })
|
||||
return Service.of({ paths, resolve, commit })
|
||||
}),
|
||||
)
|
||||
|
||||
export const defaultLayer = layer.pipe(Layer.provide(AppFileSystem.defaultLayer), Layer.provide(Git.defaultLayer))
|
||||
export const defaultLayer = layer.pipe(
|
||||
Layer.provide(Database.defaultLayer),
|
||||
Layer.provide(AppFileSystem.defaultLayer),
|
||||
Layer.provide(Git.defaultLayer),
|
||||
)
|
||||
|
||||
@@ -0,0 +1,42 @@
|
||||
import { Effect } from "effect"
|
||||
import { AbsolutePath } from "../schema"
|
||||
import { Git } from "../git"
|
||||
import { type Copy, type Strategy, type StrategyID } from "./copy"
|
||||
import type { PathUnavailableError } from "./copy"
|
||||
|
||||
export function makeStrategies(input: {
|
||||
git: Git.Interface
|
||||
canonical: (path: AbsolutePath) => Effect.Effect<AbsolutePath, PathUnavailableError>
|
||||
}) {
|
||||
const repo = (primaryPath: AbsolutePath) => ({ directory: primaryPath, store: primaryPath } satisfies Git.Repo)
|
||||
|
||||
const gitWorktree: Strategy = {
|
||||
id: "git_worktree",
|
||||
name: "Git worktree",
|
||||
create: Effect.fn("ProjectCopy.GitWorktree.create")(function* (options) {
|
||||
yield* input.git.worktreeCreate({ repo: repo(options.primaryPath), path: options.path })
|
||||
return { path: yield* input.canonical(options.path) }
|
||||
}),
|
||||
remove: Effect.fn("ProjectCopy.GitWorktree.remove")(function* (options) {
|
||||
const entries = yield* gitWorktree.list(options)
|
||||
if (!entries.some((entry) => entry.path === options.path)) {
|
||||
return yield* new Git.WorktreeError({
|
||||
operation: "remove",
|
||||
path: options.path,
|
||||
message: "Path is not a linked Git worktree",
|
||||
})
|
||||
}
|
||||
yield* input.git.worktreeRemove({ repo: repo(options.primaryPath), path: options.path })
|
||||
}),
|
||||
list: Effect.fn("ProjectCopy.GitWorktree.list")(function* (options) {
|
||||
const entries = yield* input.git.worktreeList(repo(options.primaryPath))
|
||||
return yield* Effect.forEach(entries, (entry) =>
|
||||
entry === options.primaryPath
|
||||
? Effect.succeed(undefined)
|
||||
: input.canonical(entry).pipe(Effect.map((path) => ({ path }))),
|
||||
).pipe(Effect.map((items) => items.filter((item): item is Copy => item !== undefined)))
|
||||
}),
|
||||
}
|
||||
|
||||
return new Map<StrategyID, Strategy>([[gitWorktree.id, gitWorktree]])
|
||||
}
|
||||
@@ -0,0 +1,236 @@
|
||||
export * as ProjectCopy from "./copy"
|
||||
|
||||
import { and, eq } from "drizzle-orm"
|
||||
import { Context, Effect, Layer, Schema } from "effect"
|
||||
import { AbsolutePath } from "../schema"
|
||||
import { AppFileSystem } from "../filesystem"
|
||||
import { Git } from "../git"
|
||||
import { Database } from "../database/database"
|
||||
import { EventV2 } from "../event"
|
||||
import { Project } from "../project"
|
||||
import { ProjectPathTable } from "./path.sql"
|
||||
import { makeStrategies } from "./copy-strategies"
|
||||
|
||||
export const StrategyID = Schema.Literal("git_worktree")
|
||||
export type StrategyID = typeof StrategyID.Type
|
||||
|
||||
export const StrategiesInput = Schema.Struct({
|
||||
projectID: Project.ID,
|
||||
}).annotate({ identifier: "ProjectCopy.StrategiesInput" })
|
||||
export type StrategiesInput = typeof StrategiesInput.Type
|
||||
|
||||
export const CreateInput = Schema.Struct({
|
||||
projectID: Project.ID,
|
||||
strategy: StrategyID,
|
||||
path: AbsolutePath,
|
||||
}).annotate({ identifier: "ProjectCopy.CreateInput" })
|
||||
export type CreateInput = typeof CreateInput.Type
|
||||
|
||||
export const RemoveInput = CreateInput.annotate({ identifier: "ProjectCopy.RemoveInput" })
|
||||
export type RemoveInput = typeof RemoveInput.Type
|
||||
|
||||
export const RefreshInput = Schema.Struct({
|
||||
projectID: Project.ID,
|
||||
strategy: Schema.optional(StrategyID),
|
||||
}).annotate({ identifier: "ProjectCopy.RefreshInput" })
|
||||
export type RefreshInput = typeof RefreshInput.Type
|
||||
|
||||
export const StrategyInfo = Schema.Struct({
|
||||
id: StrategyID,
|
||||
name: Schema.String,
|
||||
}).annotate({ identifier: "ProjectCopy.StrategyInfo" })
|
||||
export type StrategyInfo = typeof StrategyInfo.Type
|
||||
|
||||
export const Copy = Schema.Struct({
|
||||
path: AbsolutePath,
|
||||
}).annotate({ identifier: "ProjectCopy.Copy" })
|
||||
export type Copy = typeof Copy.Type
|
||||
|
||||
export class PrimaryPathNotFoundError extends Schema.TaggedErrorClass<PrimaryPathNotFoundError>()(
|
||||
"ProjectCopy.PrimaryPathNotFoundError",
|
||||
{ projectID: Project.ID },
|
||||
) {}
|
||||
|
||||
export class CannotRemovePrimaryPathError extends Schema.TaggedErrorClass<CannotRemovePrimaryPathError>()(
|
||||
"ProjectCopy.CannotRemovePrimaryPathError",
|
||||
{ projectID: Project.ID, path: AbsolutePath },
|
||||
) {}
|
||||
|
||||
export class DestinationExistsError extends Schema.TaggedErrorClass<DestinationExistsError>()(
|
||||
"ProjectCopy.DestinationExistsError",
|
||||
{ path: AbsolutePath },
|
||||
) {}
|
||||
|
||||
export class PathUnavailableError extends Schema.TaggedErrorClass<PathUnavailableError>()(
|
||||
"ProjectCopy.PathUnavailableError",
|
||||
{ path: AbsolutePath },
|
||||
) {}
|
||||
|
||||
export type Error =
|
||||
| PrimaryPathNotFoundError
|
||||
| CannotRemovePrimaryPathError
|
||||
| DestinationExistsError
|
||||
| PathUnavailableError
|
||||
| Git.WorktreeError
|
||||
|
||||
export interface Strategy {
|
||||
readonly id: StrategyID
|
||||
readonly name: string
|
||||
readonly create: (input: {
|
||||
projectID: Project.ID
|
||||
primaryPath: AbsolutePath
|
||||
path: AbsolutePath
|
||||
}) => Effect.Effect<Copy, Git.WorktreeError | PathUnavailableError>
|
||||
readonly remove: (input: {
|
||||
projectID: Project.ID
|
||||
primaryPath: AbsolutePath
|
||||
path: AbsolutePath
|
||||
}) => Effect.Effect<void, Git.WorktreeError | PathUnavailableError>
|
||||
readonly list: (input: {
|
||||
projectID: Project.ID
|
||||
primaryPath: AbsolutePath
|
||||
}) => Effect.Effect<Copy[], Git.WorktreeError | PathUnavailableError>
|
||||
}
|
||||
|
||||
export const Event = {
|
||||
Updated: EventV2.define({
|
||||
type: "project.paths.updated",
|
||||
schema: { projectID: Project.ID },
|
||||
}),
|
||||
}
|
||||
|
||||
export interface Interface {
|
||||
readonly strategies: (input: StrategiesInput) => Effect.Effect<StrategyInfo[]>
|
||||
readonly create: (input: CreateInput) => Effect.Effect<Copy, Error>
|
||||
readonly remove: (input: RemoveInput) => Effect.Effect<void, Error>
|
||||
readonly refresh: (input: RefreshInput) => Effect.Effect<void, Error>
|
||||
}
|
||||
|
||||
export class Service extends Context.Service<Service, Interface>()("@opencode/ProjectCopy") {}
|
||||
|
||||
export const layer = Layer.effect(
|
||||
Service,
|
||||
Effect.gen(function* () {
|
||||
const fs = yield* AppFileSystem.Service
|
||||
const git = yield* Git.Service
|
||||
const events = yield* EventV2.Service
|
||||
const db = (yield* Database.Service).db
|
||||
|
||||
const canonical = Effect.fnUntraced(function* (input: AbsolutePath) {
|
||||
const real = yield* fs.realPath(input).pipe(Effect.catch(() => Effect.succeed(undefined)))
|
||||
if (!real || !(yield* fs.isDir(real))) return yield* new PathUnavailableError({ path: input })
|
||||
return AbsolutePath.make(AppFileSystem.normalizePath(real))
|
||||
})
|
||||
|
||||
const registry = makeStrategies({ git, canonical })
|
||||
|
||||
const primary = Effect.fnUntraced(function* (projectID: Project.ID) {
|
||||
const row = yield* db
|
||||
.select({ path: ProjectPathTable.path })
|
||||
.from(ProjectPathTable)
|
||||
.where(and(eq(ProjectPathTable.project_id, projectID), eq(ProjectPathTable.primary, true)))
|
||||
.get()
|
||||
.pipe(Effect.orDie)
|
||||
if (!row) return yield* new PrimaryPathNotFoundError({ projectID })
|
||||
return yield* canonical(AbsolutePath.make(row.path))
|
||||
})
|
||||
|
||||
const insert = Effect.fnUntraced(function* (projectID: Project.ID, copyPath: AbsolutePath) {
|
||||
return yield* db
|
||||
.transaction(
|
||||
(tx) =>
|
||||
Effect.gen(function* () {
|
||||
const row = yield* tx
|
||||
.select({ path: ProjectPathTable.path })
|
||||
.from(ProjectPathTable)
|
||||
.where(and(eq(ProjectPathTable.project_id, projectID), eq(ProjectPathTable.path, copyPath)))
|
||||
.get()
|
||||
if (row) return false
|
||||
yield* tx.insert(ProjectPathTable).values({ project_id: projectID, path: copyPath, primary: false }).run()
|
||||
return true
|
||||
}),
|
||||
{ behavior: "immediate" },
|
||||
)
|
||||
.pipe(Effect.orDie)
|
||||
})
|
||||
|
||||
const removeStored = Effect.fnUntraced(function* (projectID: Project.ID, copyPath: AbsolutePath) {
|
||||
const row = yield* db
|
||||
.select({ path: ProjectPathTable.path })
|
||||
.from(ProjectPathTable)
|
||||
.where(and(eq(ProjectPathTable.project_id, projectID), eq(ProjectPathTable.path, copyPath)))
|
||||
.get()
|
||||
.pipe(Effect.orDie)
|
||||
if (!row) return false
|
||||
yield* db
|
||||
.delete(ProjectPathTable)
|
||||
.where(and(eq(ProjectPathTable.project_id, projectID), eq(ProjectPathTable.path, copyPath)))
|
||||
.run()
|
||||
.pipe(Effect.orDie)
|
||||
return true
|
||||
})
|
||||
|
||||
const changed = Effect.fnUntraced(function* (projectID: Project.ID, update: boolean) {
|
||||
if (update) yield* events.publish(Event.Updated, { projectID })
|
||||
})
|
||||
|
||||
const strategy = (id: StrategyID) => registry.get(id) as Strategy
|
||||
|
||||
const strategies = Effect.fn("ProjectCopy.strategies")(function* (_input: StrategiesInput) {
|
||||
return Array.from(registry.values()).map((strategy) => ({ id: strategy.id, name: strategy.name }))
|
||||
})
|
||||
|
||||
const create = Effect.fn("ProjectCopy.create")(function* (input: CreateInput) {
|
||||
if (yield* fs.existsSafe(input.path)) return yield* new DestinationExistsError({ path: input.path })
|
||||
const result = yield* strategy(input.strategy).create({ ...input, primaryPath: yield* primary(input.projectID) })
|
||||
yield* changed(input.projectID, yield* insert(input.projectID, result.path))
|
||||
return result
|
||||
})
|
||||
|
||||
const remove = Effect.fn("ProjectCopy.remove")(function* (input: RemoveInput) {
|
||||
const primaryPath = yield* primary(input.projectID)
|
||||
const copyPath = yield* canonical(input.path)
|
||||
if (copyPath === primaryPath) {
|
||||
return yield* new CannotRemovePrimaryPathError({ projectID: input.projectID, path: copyPath })
|
||||
}
|
||||
yield* strategy(input.strategy).remove({ ...input, path: copyPath, primaryPath })
|
||||
yield* changed(input.projectID, yield* removeStored(input.projectID, copyPath))
|
||||
})
|
||||
|
||||
const refresh = Effect.fn("ProjectCopy.refresh")(function* (input: RefreshInput) {
|
||||
const primaryPath = yield* primary(input.projectID)
|
||||
const strategies = input.strategy ? [strategy(input.strategy)] : Array.from(registry.values())
|
||||
const discovered = yield* Effect.forEach(strategies, (strategy) => strategy.list({ ...input, primaryPath }), {
|
||||
concurrency: "unbounded",
|
||||
}).pipe(Effect.map((sets) => new Set(sets.flatMap((set) => set.map((item) => item.path)))))
|
||||
const stored = yield* db
|
||||
.select({ path: ProjectPathTable.path, primary: ProjectPathTable.primary })
|
||||
.from(ProjectPathTable)
|
||||
.where(eq(ProjectPathTable.project_id, input.projectID))
|
||||
.all()
|
||||
.pipe(Effect.orDie)
|
||||
const inserted = yield* Effect.forEach(discovered, (copyPath) => insert(input.projectID, copyPath)).pipe(
|
||||
Effect.map((items) => items.some(Boolean)),
|
||||
)
|
||||
const removed = yield* Effect.forEach(
|
||||
stored.filter((item) => !item.primary),
|
||||
(item) =>
|
||||
fs.isDir(item.path).pipe(
|
||||
Effect.flatMap((exists) =>
|
||||
exists ? Effect.succeed(false) : removeStored(input.projectID, AbsolutePath.make(item.path)),
|
||||
),
|
||||
),
|
||||
).pipe(Effect.map((items) => items.some(Boolean)))
|
||||
yield* changed(input.projectID, inserted || removed)
|
||||
})
|
||||
|
||||
return Service.of({ strategies, create, remove, refresh })
|
||||
}),
|
||||
)
|
||||
|
||||
export const defaultLayer = layer.pipe(
|
||||
Layer.provide(Database.defaultLayer),
|
||||
Layer.provide(AppFileSystem.defaultLayer),
|
||||
Layer.provide(Git.defaultLayer),
|
||||
Layer.provide(EventV2.defaultLayer),
|
||||
)
|
||||
@@ -0,0 +1,24 @@
|
||||
import { sql } from "drizzle-orm"
|
||||
import { integer, primaryKey, sqliteTable, text, uniqueIndex } from "drizzle-orm/sqlite-core"
|
||||
import { Timestamps } from "../database/schema.sql"
|
||||
import { ProjectTable } from "./sql"
|
||||
import { ProjectV2 } from "../project"
|
||||
|
||||
export const ProjectPathTable = sqliteTable(
|
||||
"project_path",
|
||||
{
|
||||
project_id: text()
|
||||
.$type<ProjectV2.ID>()
|
||||
.notNull()
|
||||
.references(() => ProjectTable.id, { onDelete: "cascade" }),
|
||||
path: text().notNull(),
|
||||
primary: integer({ mode: "boolean" }).notNull().default(false),
|
||||
...Timestamps,
|
||||
},
|
||||
(table) => [
|
||||
primaryKey({ columns: [table.project_id, table.path] }),
|
||||
uniqueIndex("project_path_project_primary_idx")
|
||||
.on(table.project_id)
|
||||
.where(sql`${table.primary} = 1`),
|
||||
],
|
||||
)
|
||||
@@ -0,0 +1,44 @@
|
||||
import { describe, expect } from "bun:test"
|
||||
import { $ } from "bun"
|
||||
import fs from "fs/promises"
|
||||
import { Effect } from "effect"
|
||||
import { Git } from "@opencode-ai/core/git"
|
||||
import { AbsolutePath } from "@opencode-ai/core/schema"
|
||||
import { tmpdir } from "./fixture/tmpdir"
|
||||
import { testEffect } from "./lib/effect"
|
||||
|
||||
const it = testEffect(Git.defaultLayer)
|
||||
|
||||
async function initRepo(directory: string) {
|
||||
await $`git init`.cwd(directory).quiet()
|
||||
await $`git config core.fsmonitor false`.cwd(directory).quiet()
|
||||
await $`git config commit.gpgsign false`.cwd(directory).quiet()
|
||||
await $`git config user.email test@opencode.test`.cwd(directory).quiet()
|
||||
await $`git config user.name Test`.cwd(directory).quiet()
|
||||
await $`git commit --allow-empty -m root`.cwd(directory).quiet()
|
||||
}
|
||||
|
||||
describe("Git worktrees", () => {
|
||||
it.live("creates, lists, and removes linked worktrees", () =>
|
||||
Effect.gen(function* () {
|
||||
const root = yield* Effect.acquireRelease(
|
||||
Effect.promise(() => tmpdir()),
|
||||
(dir) => Effect.promise(() => dir[Symbol.asyncDispose]()),
|
||||
)
|
||||
yield* Effect.promise(() => initRepo(root.path))
|
||||
const directory = AbsolutePath.make(yield* Effect.promise(() => fs.realpath(root.path)))
|
||||
const worktree = AbsolutePath.make(`${root.path}-git-worktree`)
|
||||
yield* Effect.addFinalizer(() =>
|
||||
Effect.promise(() => fs.rm(worktree, { recursive: true, force: true })).pipe(Effect.ignore),
|
||||
)
|
||||
const git = yield* Git.Service
|
||||
const repo = { directory, store: AbsolutePath.make(`${directory}/.git`) }
|
||||
|
||||
yield* git.worktreeCreate({ repo, path: worktree })
|
||||
|
||||
expect((yield* git.worktreeList(repo)).some((entry) => entry.endsWith("-git-worktree"))).toBe(true)
|
||||
yield* git.worktreeRemove({ repo, path: worktree })
|
||||
expect((yield* git.worktreeList(repo)).some((entry) => entry.endsWith("-git-worktree"))).toBe(false)
|
||||
}),
|
||||
)
|
||||
})
|
||||
@@ -9,6 +9,7 @@ const ref = { directory: AbsolutePath.make("/repo/packages/app"), workspaceID: "
|
||||
const projectLayer = Layer.succeed(
|
||||
Project.Service,
|
||||
Project.Service.of({
|
||||
paths: () => Effect.succeed([]),
|
||||
resolve: () =>
|
||||
Effect.succeed({
|
||||
id: Project.ID.make("project"),
|
||||
|
||||
@@ -0,0 +1,149 @@
|
||||
import { describe, expect } from "bun:test"
|
||||
import { $ } from "bun"
|
||||
import fs from "fs/promises"
|
||||
import path from "path"
|
||||
import { eq } from "drizzle-orm"
|
||||
import { Effect, Fiber, Layer, Stream } from "effect"
|
||||
import { AbsolutePath } from "@opencode-ai/core/schema"
|
||||
import { AppFileSystem } from "@opencode-ai/core/filesystem"
|
||||
import { Git } from "@opencode-ai/core/git"
|
||||
import { Database } from "@opencode-ai/core/database/database"
|
||||
import { EventV2 } from "@opencode-ai/core/event"
|
||||
import { Project } from "@opencode-ai/core/project"
|
||||
import { ProjectPathTable } from "@opencode-ai/core/project/path.sql"
|
||||
import { ProjectTable } from "@opencode-ai/core/project/sql"
|
||||
import { ProjectCopy } from "@opencode-ai/core/project/copy"
|
||||
import { tmpdir } from "./fixture/tmpdir"
|
||||
import { testEffect } from "./lib/effect"
|
||||
|
||||
const databaseLayer = Database.layerFromPath(":memory:")
|
||||
const eventLayer = EventV2.layer.pipe(Layer.provide(databaseLayer))
|
||||
const copyLayer = ProjectCopy.layer.pipe(
|
||||
Layer.provide(databaseLayer),
|
||||
Layer.provide(eventLayer),
|
||||
Layer.provide(AppFileSystem.defaultLayer),
|
||||
Layer.provide(Git.defaultLayer),
|
||||
)
|
||||
const it = testEffect(Layer.mergeAll(copyLayer, databaseLayer, eventLayer))
|
||||
|
||||
function abs(input: string) {
|
||||
return AbsolutePath.make(input)
|
||||
}
|
||||
|
||||
async function initRepo(directory: string) {
|
||||
await $`git init`.cwd(directory).quiet()
|
||||
await $`git config core.fsmonitor false`.cwd(directory).quiet()
|
||||
await $`git config commit.gpgsign false`.cwd(directory).quiet()
|
||||
await $`git config user.email test@opencode.test`.cwd(directory).quiet()
|
||||
await $`git config user.name Test`.cwd(directory).quiet()
|
||||
await $`git commit --allow-empty -m root`.cwd(directory).quiet()
|
||||
}
|
||||
|
||||
function setup() {
|
||||
return Effect.gen(function* () {
|
||||
const root = yield* Effect.acquireRelease(
|
||||
Effect.promise(() => tmpdir()),
|
||||
(dir) => Effect.promise(() => dir[Symbol.asyncDispose]()),
|
||||
)
|
||||
yield* Effect.promise(() => initRepo(root.path))
|
||||
const primaryPath = abs(yield* Effect.promise(() => fs.realpath(root.path)))
|
||||
const projectID = Project.ID.make("copy-project")
|
||||
const { db } = yield* Database.Service
|
||||
yield* db
|
||||
.insert(ProjectTable)
|
||||
.values({ id: projectID, worktree: root.path, sandboxes: [], time_created: 1, time_updated: 1 })
|
||||
.run()
|
||||
.pipe(Effect.orDie)
|
||||
yield* db
|
||||
.insert(ProjectPathTable)
|
||||
.values({ project_id: projectID, path: primaryPath, primary: true })
|
||||
.run()
|
||||
.pipe(Effect.orDie)
|
||||
return { root, primaryPath, projectID, db }
|
||||
})
|
||||
}
|
||||
|
||||
function stored(projectID: Project.ID) {
|
||||
return Database.Service.use(({ db }) =>
|
||||
db
|
||||
.select({ path: ProjectPathTable.path, primary: ProjectPathTable.primary })
|
||||
.from(ProjectPathTable)
|
||||
.where(eq(ProjectPathTable.project_id, projectID))
|
||||
.all()
|
||||
.pipe(Effect.orDie, Effect.map((rows) => rows.toSorted((a, b) => a.path.localeCompare(b.path)))),
|
||||
)
|
||||
}
|
||||
|
||||
describe("ProjectCopy", () => {
|
||||
it.effect("lists the initial git worktree strategy", () =>
|
||||
Effect.gen(function* () {
|
||||
const copy = yield* ProjectCopy.Service
|
||||
|
||||
expect(yield* copy.strategies({ projectID: Project.ID.make("project") })).toEqual([
|
||||
{ id: "git_worktree", name: "Git worktree" },
|
||||
])
|
||||
}),
|
||||
)
|
||||
|
||||
it.live("creates and removes a git worktree path", () =>
|
||||
Effect.gen(function* () {
|
||||
const input = yield* setup()
|
||||
const copy = yield* ProjectCopy.Service
|
||||
const target = abs(`${input.root.path}-copy-created`)
|
||||
yield* Effect.addFinalizer(() =>
|
||||
Effect.promise(() => fs.rm(target, { recursive: true, force: true })).pipe(Effect.ignore),
|
||||
)
|
||||
|
||||
const created = yield* copy.create({ projectID: input.projectID, strategy: "git_worktree", path: target })
|
||||
expect(yield* stored(input.projectID)).toEqual([
|
||||
{ path: input.primaryPath, primary: true },
|
||||
{ path: created.path, primary: false },
|
||||
].toSorted((a, b) => a.path.localeCompare(b.path)))
|
||||
|
||||
yield* copy.remove({ projectID: input.projectID, strategy: "git_worktree", path: created.path })
|
||||
|
||||
expect(yield* stored(input.projectID)).toEqual([{ path: input.primaryPath, primary: true }])
|
||||
expect(yield* Effect.promise(() => Bun.file(target).exists())).toBe(false)
|
||||
}),
|
||||
)
|
||||
|
||||
it.live("refresh discovers and prunes an externally managed git worktree", () =>
|
||||
Effect.gen(function* () {
|
||||
const input = yield* setup()
|
||||
const copy = yield* ProjectCopy.Service
|
||||
const events = yield* EventV2.Service
|
||||
const target = abs(`${input.root.path}-copy-external`)
|
||||
yield* Effect.addFinalizer(() =>
|
||||
Effect.promise(() => fs.rm(target, { recursive: true, force: true })).pipe(Effect.ignore),
|
||||
)
|
||||
yield* Effect.promise(() => $`git worktree add --detach ${target} HEAD`.cwd(input.root.path).quiet())
|
||||
const fiber = yield* events.subscribe(ProjectCopy.Event.Updated).pipe(Stream.take(1), Stream.runCollect, Effect.forkScoped)
|
||||
yield* Effect.yieldNow
|
||||
|
||||
yield* copy.refresh({ projectID: input.projectID, strategy: "git_worktree" })
|
||||
|
||||
const discovered = abs(yield* Effect.promise(() => fs.realpath(target)))
|
||||
expect(yield* stored(input.projectID)).toEqual([
|
||||
{ path: input.primaryPath, primary: true },
|
||||
{ path: discovered, primary: false },
|
||||
].toSorted((a, b) => a.path.localeCompare(b.path)))
|
||||
expect(Array.from(yield* Fiber.join(fiber))[0]?.data).toEqual({ projectID: input.projectID })
|
||||
|
||||
yield* Effect.promise(() => $`git worktree remove --force ${target}`.cwd(input.root.path).quiet())
|
||||
yield* copy.refresh({ projectID: input.projectID, strategy: "git_worktree" })
|
||||
expect(yield* stored(input.projectID)).toEqual([{ path: input.primaryPath, primary: true }])
|
||||
}),
|
||||
)
|
||||
|
||||
it.live("refuses to remove the primary project path", () =>
|
||||
Effect.gen(function* () {
|
||||
const input = yield* setup()
|
||||
const copy = yield* ProjectCopy.Service
|
||||
const result = yield* Effect.exit(
|
||||
copy.remove({ projectID: input.projectID, strategy: "git_worktree", path: input.primaryPath }),
|
||||
)
|
||||
|
||||
expect(result._tag).toBe("Failure")
|
||||
}),
|
||||
)
|
||||
})
|
||||
@@ -2,14 +2,30 @@ import { describe, expect } from "bun:test"
|
||||
import { $ } from "bun"
|
||||
import fs from "fs/promises"
|
||||
import path from "path"
|
||||
import { Effect } from "effect"
|
||||
import { Effect, Layer, Schema } from "effect"
|
||||
import { ProjectV2 } from "@opencode-ai/core/project"
|
||||
import { ProjectPathTable } from "@opencode-ai/core/project/path.sql"
|
||||
import { ProjectTable } from "@opencode-ai/core/project/sql"
|
||||
import { Database } from "@opencode-ai/core/database/database"
|
||||
import { AppFileSystem } from "@opencode-ai/core/filesystem"
|
||||
import { Git } from "@opencode-ai/core/git"
|
||||
import { AbsolutePath } from "@opencode-ai/core/schema"
|
||||
import { Hash } from "@opencode-ai/core/util/hash"
|
||||
import { tmpdir } from "./fixture/tmpdir"
|
||||
import { testEffect } from "./lib/effect"
|
||||
|
||||
const it = testEffect(ProjectV2.defaultLayer)
|
||||
const databaseLayer = Database.layerFromPath(":memory:")
|
||||
const pathsIt = testEffect(
|
||||
Layer.mergeAll(
|
||||
ProjectV2.layer.pipe(
|
||||
Layer.provide(databaseLayer),
|
||||
Layer.provide(AppFileSystem.defaultLayer),
|
||||
Layer.provide(Git.defaultLayer),
|
||||
),
|
||||
databaseLayer,
|
||||
),
|
||||
)
|
||||
|
||||
function remoteID(remote: string) {
|
||||
return ProjectV2.ID.make(Hash.fast(`git-remote:${remote}`))
|
||||
@@ -37,6 +53,50 @@ async function rootCommit(dir: string) {
|
||||
return (await $`git rev-list --max-parents=0 HEAD`.cwd(dir).text()).trim()
|
||||
}
|
||||
|
||||
describe("Project paths schemas", () => {
|
||||
it.effect("decodes project path input and inline path results", () =>
|
||||
Effect.sync(() => {
|
||||
expect(Schema.decodeUnknownSync(ProjectV2.PathsInput)({ projectID: ProjectV2.ID.make("project") })).toEqual({
|
||||
projectID: ProjectV2.ID.make("project"),
|
||||
})
|
||||
expect(
|
||||
Schema.decodeUnknownSync(ProjectV2.Paths)([{ path: AbsolutePath.make("/tmp/project"), primary: true }]),
|
||||
).toEqual([{ path: AbsolutePath.make("/tmp/project"), primary: true }])
|
||||
}),
|
||||
)
|
||||
|
||||
pathsIt.effect("lists stored project paths only for the requested project", () =>
|
||||
Effect.gen(function* () {
|
||||
const project = yield* ProjectV2.Service
|
||||
const { db } = yield* Database.Service
|
||||
const projectID = ProjectV2.ID.make("paths-project")
|
||||
const otherID = ProjectV2.ID.make("paths-other")
|
||||
yield* db
|
||||
.insert(ProjectTable)
|
||||
.values([
|
||||
{ id: projectID, worktree: "/repo", sandboxes: [], time_created: 1, time_updated: 1 },
|
||||
{ id: otherID, worktree: "/other", sandboxes: [], time_created: 1, time_updated: 1 },
|
||||
])
|
||||
.run()
|
||||
.pipe(Effect.orDie)
|
||||
yield* db
|
||||
.insert(ProjectPathTable)
|
||||
.values([
|
||||
{ project_id: projectID, path: AbsolutePath.make("/repo/z"), primary: false },
|
||||
{ project_id: projectID, path: AbsolutePath.make("/repo/a"), primary: true },
|
||||
{ project_id: otherID, path: AbsolutePath.make("/other"), primary: true },
|
||||
])
|
||||
.run()
|
||||
.pipe(Effect.orDie)
|
||||
|
||||
expect(yield* project.paths({ projectID })).toEqual([
|
||||
{ path: AbsolutePath.make("/repo/a"), primary: true },
|
||||
{ path: AbsolutePath.make("/repo/z"), primary: false },
|
||||
])
|
||||
}),
|
||||
)
|
||||
})
|
||||
|
||||
describe("ProjectV2.resolve", () => {
|
||||
it.live("returns global for non-git directory", () =>
|
||||
Effect.gen(function* () {
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
import { and, eq, sql } from "drizzle-orm"
|
||||
import { Database } from "@opencode-ai/core/database/database"
|
||||
import { ProjectTable } from "@opencode-ai/core/project/sql"
|
||||
import { ProjectPathTable } from "@opencode-ai/core/project/path.sql"
|
||||
import { PermissionTable, SessionTable } from "@opencode-ai/core/session/sql"
|
||||
import { WorkspaceTable } from "@opencode-ai/core/control-plane/workspace.sql"
|
||||
import * as Log from "@opencode-ai/core/util/log"
|
||||
@@ -249,6 +250,56 @@ export const layer = Layer.effect(
|
||||
.pipe(Effect.orDie)
|
||||
})
|
||||
|
||||
const rememberProjectPath = Effect.fn("Project.rememberProjectPath")(function* (input: {
|
||||
projectID: ProjectV2.ID
|
||||
path: string
|
||||
}) {
|
||||
if (input.projectID === ProjectV2.ID.global) return
|
||||
|
||||
yield* db
|
||||
.transaction(
|
||||
(d) =>
|
||||
Effect.gen(function* () {
|
||||
const existing = yield* d
|
||||
.select()
|
||||
.from(ProjectPathTable)
|
||||
.where(and(eq(ProjectPathTable.project_id, input.projectID), eq(ProjectPathTable.path, input.path)))
|
||||
.get()
|
||||
const primary = yield* d
|
||||
.select({ path: ProjectPathTable.path })
|
||||
.from(ProjectPathTable)
|
||||
.where(and(eq(ProjectPathTable.project_id, input.projectID), eq(ProjectPathTable.primary, true)))
|
||||
.get()
|
||||
|
||||
if (existing) {
|
||||
if (!primary) {
|
||||
yield* d
|
||||
.update(ProjectPathTable)
|
||||
.set({ primary: true })
|
||||
.where(and(eq(ProjectPathTable.project_id, input.projectID), eq(ProjectPathTable.path, input.path)))
|
||||
.run()
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
yield* d
|
||||
.insert(ProjectPathTable)
|
||||
.values({
|
||||
path: input.path,
|
||||
primary: !primary,
|
||||
project_id: input.projectID,
|
||||
})
|
||||
.run()
|
||||
}),
|
||||
{ behavior: "immediate" },
|
||||
)
|
||||
.pipe(
|
||||
Effect.catchCause((cause) =>
|
||||
Effect.sync(() => log.warn("project path persistence failed", { projectID: input.projectID, cause })),
|
||||
),
|
||||
)
|
||||
})
|
||||
|
||||
const fromDirectory = Effect.fn("Project.fromDirectory")(function* (directory: string) {
|
||||
log.info("fromDirectory", { directory })
|
||||
|
||||
@@ -336,6 +387,11 @@ export const layer = Layer.effect(
|
||||
.pipe(Effect.orDie)
|
||||
}
|
||||
|
||||
yield* rememberProjectPath({
|
||||
projectID,
|
||||
path: data.directory,
|
||||
})
|
||||
|
||||
yield* emitUpdated(result)
|
||||
if (projectID !== ProjectV2.ID.global && data.vcs?.type === "git") {
|
||||
yield* projectV2.commit({ store: data.vcs.store, id: data.id })
|
||||
|
||||
@@ -12,6 +12,7 @@ import { InstanceApi } from "./groups/instance"
|
||||
import { McpApi } from "./groups/mcp"
|
||||
import { PermissionApi } from "./groups/permission"
|
||||
import { ProjectApi } from "./groups/project"
|
||||
import { ProjectCopyApi } from "./groups/project-copy"
|
||||
import { ProviderApi } from "./groups/provider"
|
||||
import { PtyApi, PtyConnectApi } from "./groups/pty"
|
||||
import { QuestionApi } from "./groups/question"
|
||||
@@ -52,6 +53,7 @@ export const InstanceHttpApi = HttpApi.make("opencode-instance")
|
||||
.addHttpApi(InstanceApi)
|
||||
.addHttpApi(McpApi)
|
||||
.addHttpApi(ProjectApi)
|
||||
.addHttpApi(ProjectCopyApi)
|
||||
.addHttpApi(PtyApi)
|
||||
.addHttpApi(QuestionApi)
|
||||
.addHttpApi(PermissionApi)
|
||||
|
||||
@@ -0,0 +1,83 @@
|
||||
import { ProjectCopy } from "@opencode-ai/core/project/copy"
|
||||
import { ProjectV2 } from "@opencode-ai/core/project"
|
||||
import { Schema } from "effect"
|
||||
import { HttpApi, HttpApiEndpoint, HttpApiError, HttpApiGroup, HttpApiSchema, OpenApi } from "effect/unstable/httpapi"
|
||||
import { Authorization } from "../middleware/authorization"
|
||||
import { InstanceContextMiddleware } from "../middleware/instance-context"
|
||||
import { WorkspaceRoutingMiddleware, WorkspaceRoutingQuery } from "../middleware/workspace-routing"
|
||||
import { described } from "./metadata"
|
||||
|
||||
const root = "/project/:projectID/copy"
|
||||
|
||||
export const CreatePayload = Schema.Struct({
|
||||
strategy: ProjectCopy.StrategyID,
|
||||
path: ProjectCopy.CreateInput.fields.path,
|
||||
})
|
||||
export const RemovePayload = Schema.Struct({
|
||||
strategy: ProjectCopy.StrategyID,
|
||||
path: ProjectCopy.RemoveInput.fields.path,
|
||||
})
|
||||
export const RefreshPayload = Schema.Struct({
|
||||
strategy: Schema.optional(ProjectCopy.StrategyID),
|
||||
})
|
||||
|
||||
export const ProjectCopyApi = HttpApi.make("projectCopy")
|
||||
.add(
|
||||
HttpApiGroup.make("projectCopy")
|
||||
.add(
|
||||
HttpApiEndpoint.get("strategies", `${root}/strategy`, {
|
||||
params: { projectID: ProjectV2.ID },
|
||||
query: WorkspaceRoutingQuery,
|
||||
success: described(Schema.Array(ProjectCopy.StrategyInfo), "Project copy strategies"),
|
||||
}).annotateMerge(
|
||||
OpenApi.annotations({
|
||||
identifier: "projectCopy.strategies",
|
||||
summary: "List project copy strategies",
|
||||
description: "List mechanisms available for managing local copies of a project.",
|
||||
}),
|
||||
),
|
||||
HttpApiEndpoint.post("create", root, {
|
||||
params: { projectID: ProjectV2.ID },
|
||||
query: WorkspaceRoutingQuery,
|
||||
payload: CreatePayload,
|
||||
success: described(ProjectCopy.Copy, "Project copy created"),
|
||||
error: HttpApiError.BadRequest,
|
||||
}).annotateMerge(
|
||||
OpenApi.annotations({
|
||||
identifier: "projectCopy.create",
|
||||
summary: "Create project copy",
|
||||
description: "Create a local physical copy of a project using the selected strategy.",
|
||||
}),
|
||||
),
|
||||
HttpApiEndpoint.delete("remove", root, {
|
||||
params: { projectID: ProjectV2.ID },
|
||||
query: WorkspaceRoutingQuery,
|
||||
payload: RemovePayload,
|
||||
success: described(HttpApiSchema.NoContent, "Project copy removed"),
|
||||
error: HttpApiError.BadRequest,
|
||||
}).annotateMerge(
|
||||
OpenApi.annotations({
|
||||
identifier: "projectCopy.remove",
|
||||
summary: "Remove project copy",
|
||||
description: "Remove a local physical copy of a project using the selected strategy.",
|
||||
}),
|
||||
),
|
||||
HttpApiEndpoint.post("refresh", `${root}/refresh`, {
|
||||
params: { projectID: ProjectV2.ID },
|
||||
query: WorkspaceRoutingQuery,
|
||||
payload: RefreshPayload,
|
||||
success: described(HttpApiSchema.NoContent, "Project copies refreshed"),
|
||||
error: HttpApiError.BadRequest,
|
||||
}).annotateMerge(
|
||||
OpenApi.annotations({
|
||||
identifier: "projectCopy.refresh",
|
||||
summary: "Refresh project copies",
|
||||
description: "Discover local project copies using one or all configured strategies.",
|
||||
}),
|
||||
),
|
||||
)
|
||||
.annotateMerge(OpenApi.annotations({ title: "projectCopy", description: "Project copy management routes." }))
|
||||
.middleware(InstanceContextMiddleware)
|
||||
.middleware(WorkspaceRoutingMiddleware)
|
||||
.middleware(Authorization),
|
||||
)
|
||||
@@ -62,6 +62,17 @@ export const ProjectApi = HttpApi.make("project")
|
||||
description: "Update project properties such as name, icon, and commands.",
|
||||
}),
|
||||
),
|
||||
HttpApiEndpoint.get("paths", `${root}/:projectID/paths`, {
|
||||
params: { projectID: ProjectV2.ID },
|
||||
query: WorkspaceRoutingQuery,
|
||||
success: described(ProjectV2.Paths, "Project paths"),
|
||||
}).annotateMerge(
|
||||
OpenApi.annotations({
|
||||
identifier: "project.paths",
|
||||
summary: "List project paths",
|
||||
description: "List known local absolute paths for a project.",
|
||||
}),
|
||||
),
|
||||
)
|
||||
.annotateMerge(
|
||||
OpenApi.annotations({
|
||||
|
||||
@@ -0,0 +1,47 @@
|
||||
import { ProjectCopy } from "@opencode-ai/core/project/copy"
|
||||
import { ProjectV2 } from "@opencode-ai/core/project"
|
||||
import { Effect } from "effect"
|
||||
import { HttpApiBuilder, HttpApiError } from "effect/unstable/httpapi"
|
||||
import { InstanceHttpApi } from "../api"
|
||||
import { CreatePayload, RefreshPayload, RemovePayload } from "../groups/project-copy"
|
||||
|
||||
function badRequest<A, R>(effect: Effect.Effect<A, ProjectCopy.Error, R>) {
|
||||
return effect.pipe(Effect.mapError(() => new HttpApiError.BadRequest({})))
|
||||
}
|
||||
|
||||
export const projectCopyHandlers = HttpApiBuilder.group(InstanceHttpApi, "projectCopy", (handlers) =>
|
||||
Effect.gen(function* () {
|
||||
const service = yield* ProjectCopy.Service
|
||||
|
||||
const strategies = Effect.fn("ProjectCopyHttpApi.strategies")((ctx: { params: { projectID: ProjectV2.ID } }) =>
|
||||
service.strategies({ projectID: ctx.params.projectID }),
|
||||
)
|
||||
|
||||
const create = Effect.fn("ProjectCopyHttpApi.create")(function* (ctx: {
|
||||
params: { projectID: ProjectV2.ID }
|
||||
payload: typeof CreatePayload.Type
|
||||
}) {
|
||||
return yield* badRequest(service.create({ ...ctx.payload, projectID: ctx.params.projectID }))
|
||||
})
|
||||
|
||||
const remove = Effect.fn("ProjectCopyHttpApi.remove")(function* (ctx: {
|
||||
params: { projectID: ProjectV2.ID }
|
||||
payload: typeof RemovePayload.Type
|
||||
}) {
|
||||
yield* badRequest(service.remove({ ...ctx.payload, projectID: ctx.params.projectID }))
|
||||
})
|
||||
|
||||
const refresh = Effect.fn("ProjectCopyHttpApi.refresh")(function* (ctx: {
|
||||
params: { projectID: ProjectV2.ID }
|
||||
payload: typeof RefreshPayload.Type
|
||||
}) {
|
||||
yield* badRequest(service.refresh({ ...ctx.payload, projectID: ctx.params.projectID }))
|
||||
})
|
||||
|
||||
return handlers
|
||||
.handle("strategies", strategies)
|
||||
.handle("create", create)
|
||||
.handle("remove", remove)
|
||||
.handle("refresh", refresh)
|
||||
}),
|
||||
)
|
||||
@@ -10,6 +10,7 @@ import { markInstanceForReload } from "../lifecycle"
|
||||
export const projectHandlers = HttpApiBuilder.group(InstanceHttpApi, "project", (handlers) =>
|
||||
Effect.gen(function* () {
|
||||
const svc = yield* Project.Service
|
||||
const project = yield* ProjectV2.Service
|
||||
|
||||
const list = Effect.fn("ProjectHttpApi.list")(function* () {
|
||||
return yield* svc.list()
|
||||
@@ -48,6 +49,15 @@ export const projectHandlers = HttpApiBuilder.group(InstanceHttpApi, "project",
|
||||
)
|
||||
})
|
||||
|
||||
return handlers.handle("list", list).handle("current", current).handle("initGit", initGit).handle("update", update)
|
||||
const paths = Effect.fn("ProjectHttpApi.paths")((ctx: { params: { projectID: ProjectV2.ID } }) =>
|
||||
project.paths({ projectID: ctx.params.projectID }),
|
||||
)
|
||||
|
||||
return handlers
|
||||
.handle("list", list)
|
||||
.handle("current", current)
|
||||
.handle("initGit", initGit)
|
||||
.handle("update", update)
|
||||
.handle("paths", paths)
|
||||
}),
|
||||
)
|
||||
|
||||
@@ -28,6 +28,8 @@ import { Installation } from "@/installation"
|
||||
import { InstanceLayer } from "@/project/instance-layer"
|
||||
import { Plugin } from "@/plugin"
|
||||
import { Project } from "@/project/project"
|
||||
import { ProjectV2 } from "@opencode-ai/core/project"
|
||||
import { ProjectCopy } from "@opencode-ai/core/project/copy"
|
||||
import { ProviderAuth } from "@/provider/auth"
|
||||
import { ModelsDev } from "@opencode-ai/core/models-dev"
|
||||
import { Provider } from "@/provider/provider"
|
||||
@@ -76,6 +78,7 @@ import { instanceHandlers } from "./handlers/instance"
|
||||
import { mcpHandlers } from "./handlers/mcp"
|
||||
import { permissionHandlers } from "./handlers/permission"
|
||||
import { projectHandlers } from "./handlers/project"
|
||||
import { projectCopyHandlers } from "./handlers/project-copy"
|
||||
import { providerHandlers } from "./handlers/provider"
|
||||
import { ptyConnectHandlers, ptyHandlers } from "./handlers/pty"
|
||||
import { questionHandlers } from "./handlers/question"
|
||||
@@ -137,6 +140,7 @@ const instanceApiRoutes = HttpApiBuilder.layer(InstanceHttpApi).pipe(
|
||||
instanceHandlers,
|
||||
mcpHandlers,
|
||||
projectHandlers,
|
||||
projectCopyHandlers,
|
||||
ptyHandlers,
|
||||
questionHandlers,
|
||||
permissionHandlers,
|
||||
@@ -208,6 +212,8 @@ export function createRoutes(
|
||||
Permission.defaultLayer,
|
||||
Plugin.defaultLayer,
|
||||
Project.defaultLayer,
|
||||
ProjectV2.defaultLayer,
|
||||
ProjectCopy.defaultLayer,
|
||||
ProviderAuth.defaultLayer,
|
||||
Provider.defaultLayer,
|
||||
Pty.defaultLayer,
|
||||
|
||||
@@ -0,0 +1,128 @@
|
||||
import { describe, expect } from "bun:test"
|
||||
import { $ } from "bun"
|
||||
import path from "path"
|
||||
import { eq } from "drizzle-orm"
|
||||
import { Effect, Layer } from "effect"
|
||||
import { CrossSpawnSpawner } from "@opencode-ai/core/cross-spawn-spawner"
|
||||
import { Hash } from "@opencode-ai/core/util/hash"
|
||||
import { Database } from "@opencode-ai/core/database/database"
|
||||
import { ProjectPathTable } from "@opencode-ai/core/project/path.sql"
|
||||
import { ProjectTable } from "@opencode-ai/core/project/sql"
|
||||
import { ProjectV2 } from "@opencode-ai/core/project"
|
||||
import { Project } from "@/project/project"
|
||||
import { tmpdirScoped } from "../fixture/fixture"
|
||||
import { testEffect } from "../lib/effect"
|
||||
|
||||
const it = testEffect(Layer.mergeAll(Project.defaultLayer, Database.defaultLayer, CrossSpawnSpawner.defaultLayer))
|
||||
|
||||
function paths(projectID: ProjectV2.ID) {
|
||||
return Database.Service.use(({ db }) =>
|
||||
db
|
||||
.select()
|
||||
.from(ProjectPathTable)
|
||||
.where(eq(ProjectPathTable.project_id, projectID))
|
||||
.all()
|
||||
.pipe(
|
||||
Effect.orDie,
|
||||
Effect.map((rows) =>
|
||||
rows.map((row) => ({ path: row.path, primary: row.primary })).toSorted((a, b) => a.path.localeCompare(b.path)),
|
||||
),
|
||||
),
|
||||
)
|
||||
}
|
||||
|
||||
describe("Project path persistence", () => {
|
||||
it.live("stores the first opened checkout as the primary path", () =>
|
||||
Effect.gen(function* () {
|
||||
const tmp = yield* tmpdirScoped({ git: true })
|
||||
const project = yield* Project.Service
|
||||
|
||||
const result = yield* project.fromDirectory(tmp)
|
||||
|
||||
expect(yield* paths(result.project.id)).toEqual([{ path: tmp, primary: true }])
|
||||
}),
|
||||
)
|
||||
|
||||
it.live("stores an opened linked worktree as a secondary path", () =>
|
||||
Effect.gen(function* () {
|
||||
const tmp = yield* tmpdirScoped({ git: true })
|
||||
const project = yield* Project.Service
|
||||
const main = yield* project.fromDirectory(tmp)
|
||||
const worktree = path.join(tmp, "..", path.basename(tmp) + "-project-path-worktree")
|
||||
yield* Effect.addFinalizer(() =>
|
||||
Effect.promise(() => $`git worktree remove ${worktree}`.cwd(tmp).quiet().nothrow()).pipe(Effect.ignore),
|
||||
)
|
||||
yield* Effect.promise(() => $`git worktree add ${worktree} -b project-path-${Date.now()}`.cwd(tmp).quiet())
|
||||
|
||||
yield* project.fromDirectory(worktree)
|
||||
|
||||
expect(yield* paths(main.project.id)).toEqual(
|
||||
[
|
||||
{ path: tmp, primary: true },
|
||||
{ path: worktree, primary: false },
|
||||
].toSorted((a, b) => a.path.localeCompare(b.path)),
|
||||
)
|
||||
}),
|
||||
)
|
||||
|
||||
it.live("stores a separately opened clone as a secondary path", () =>
|
||||
Effect.gen(function* () {
|
||||
const tmp = yield* tmpdirScoped({ git: true })
|
||||
const bare = tmp + "-project-path-bare"
|
||||
const clone = tmp + "-project-path-clone"
|
||||
yield* Effect.addFinalizer(() =>
|
||||
Effect.promise(() => $`rm -rf ${bare} ${clone}`.quiet().nothrow()).pipe(Effect.ignore),
|
||||
)
|
||||
yield* Effect.promise(() => $`git clone --bare ${tmp} ${bare}`.quiet())
|
||||
yield* Effect.promise(() => $`git clone ${bare} ${clone}`.quiet())
|
||||
const project = yield* Project.Service
|
||||
const main = yield* project.fromDirectory(tmp)
|
||||
|
||||
yield* project.fromDirectory(clone)
|
||||
|
||||
expect(yield* paths(main.project.id)).toEqual(
|
||||
[
|
||||
{ path: tmp, primary: true },
|
||||
{ path: clone, primary: false },
|
||||
].toSorted((a, b) => a.path.localeCompare(b.path)),
|
||||
)
|
||||
}),
|
||||
)
|
||||
|
||||
it.live("keeps an existing remote project primary when migrating an inferred project id", () =>
|
||||
Effect.gen(function* () {
|
||||
const tmp = yield* tmpdirScoped({ git: true })
|
||||
const project = yield* Project.Service
|
||||
yield* project.fromDirectory(tmp)
|
||||
const remoteID = ProjectV2.ID.make(Hash.fast("git-remote:github.com/project-path-test/collision"))
|
||||
const { db } = yield* Database.Service
|
||||
yield* db
|
||||
.insert(ProjectTable)
|
||||
.values({
|
||||
id: remoteID,
|
||||
worktree: "/tmp/existing",
|
||||
vcs: "git",
|
||||
time_created: Date.now(),
|
||||
time_updated: Date.now(),
|
||||
sandboxes: [],
|
||||
})
|
||||
.run()
|
||||
.pipe(Effect.orDie)
|
||||
yield* db
|
||||
.insert(ProjectPathTable)
|
||||
.values({ project_id: remoteID, path: "/tmp/existing", primary: true })
|
||||
.run()
|
||||
.pipe(Effect.orDie)
|
||||
yield* Effect.promise(() => $`git remote add origin git@github.com:project-path-test/collision.git`.cwd(tmp).quiet())
|
||||
|
||||
yield* project.fromDirectory(tmp)
|
||||
|
||||
expect(yield* paths(remoteID)).toEqual(
|
||||
[
|
||||
{ path: "/tmp/existing", primary: true },
|
||||
{ path: tmp, primary: false },
|
||||
].toSorted((a, b) => a.path.localeCompare(b.path)),
|
||||
)
|
||||
}),
|
||||
)
|
||||
})
|
||||
@@ -0,0 +1,93 @@
|
||||
import { afterEach, describe, expect } from "bun:test"
|
||||
import { $ } from "bun"
|
||||
import fs from "fs/promises"
|
||||
import path from "path"
|
||||
import { Effect, Layer } from "effect"
|
||||
import { HttpClientResponse } from "effect/unstable/http"
|
||||
import { AppFileSystem } from "@opencode-ai/core/filesystem"
|
||||
import { Snapshot } from "@/snapshot"
|
||||
import { InstanceBootstrap } from "@/project/bootstrap-service"
|
||||
import { InstanceStore } from "@/project/instance-store"
|
||||
import { resetDatabase } from "../fixture/db"
|
||||
import { disposeAllInstances, TestInstance } from "../fixture/fixture"
|
||||
import { testEffect } from "../lib/effect"
|
||||
import { httpApiLayer, requestInDirectory } from "./httpapi-layer"
|
||||
|
||||
afterEach(async () => {
|
||||
await disposeAllInstances()
|
||||
await resetDatabase()
|
||||
})
|
||||
|
||||
const noopBootstrap = Layer.succeed(InstanceBootstrap.Service, InstanceBootstrap.Service.of({ run: Effect.void }))
|
||||
const testInstanceStore = InstanceStore.defaultLayer.pipe(Layer.provide(noopBootstrap))
|
||||
const it = testEffect(Layer.mergeAll(AppFileSystem.defaultLayer, Snapshot.defaultLayer, testInstanceStore, httpApiLayer))
|
||||
|
||||
function request(directory: string, url: string, init: RequestInit = {}) {
|
||||
return requestInDirectory(url, directory, init)
|
||||
}
|
||||
|
||||
function json<T>(response: HttpClientResponse.HttpClientResponse) {
|
||||
return response.json.pipe(Effect.map((value) => value as T))
|
||||
}
|
||||
|
||||
describe("project paths and copies endpoints", () => {
|
||||
it.instance(
|
||||
"lists paths and manages git worktree copies",
|
||||
() =>
|
||||
Effect.gen(function* () {
|
||||
const test = yield* TestInstance
|
||||
const current = yield* request(test.directory, "/project/current")
|
||||
const base = `/project/${(yield* json<{ id: string }>(current)).id}`
|
||||
const createdPath = path.join(test.directory, "..", path.basename(test.directory) + "-http-copy")
|
||||
yield* Effect.addFinalizer(() =>
|
||||
Effect.promise(() => fs.rm(createdPath, { recursive: true, force: true })).pipe(Effect.ignore),
|
||||
)
|
||||
|
||||
const strategies = yield* request(test.directory, `${base}/copy/strategy`)
|
||||
expect(strategies.status).toBe(200)
|
||||
expect(yield* json(strategies)).toEqual([{ id: "git_worktree", name: "Git worktree" }])
|
||||
|
||||
const initial = yield* request(test.directory, `${base}/paths`)
|
||||
expect(initial.status).toBe(200)
|
||||
expect(yield* json<Array<{ path: string; primary: boolean }>>(initial)).toEqual([
|
||||
{ path: test.directory, primary: true },
|
||||
])
|
||||
|
||||
const create = yield* request(test.directory, `${base}/copy`, {
|
||||
method: "POST",
|
||||
headers: { "content-type": "application/json" },
|
||||
body: JSON.stringify({ strategy: "git_worktree", path: createdPath }),
|
||||
})
|
||||
expect(create.status).toBe(200)
|
||||
const created = yield* json<{ path: string }>(create)
|
||||
expect(created.path).toContain("-http-copy")
|
||||
|
||||
const listed = yield* request(test.directory, `${base}/paths`)
|
||||
expect((yield* json<Array<{ path: string; primary: boolean }>>(listed)).map((item) => item.path)).toContain(
|
||||
created.path,
|
||||
)
|
||||
|
||||
const remove = yield* request(test.directory, `${base}/copy`, {
|
||||
method: "DELETE",
|
||||
headers: { "content-type": "application/json" },
|
||||
body: JSON.stringify({ strategy: "git_worktree", path: created.path }),
|
||||
})
|
||||
expect(remove.status).toBe(204)
|
||||
|
||||
const externalPath = path.join(test.directory, "..", path.basename(test.directory) + "-http-refresh")
|
||||
yield* Effect.addFinalizer(() =>
|
||||
Effect.promise(() => fs.rm(externalPath, { recursive: true, force: true })).pipe(Effect.ignore),
|
||||
)
|
||||
yield* Effect.promise(() => $`git worktree add --detach ${externalPath} HEAD`.cwd(test.directory).quiet())
|
||||
const refresh = yield* request(test.directory, `${base}/copy/refresh`, {
|
||||
method: "POST",
|
||||
headers: { "content-type": "application/json" },
|
||||
body: JSON.stringify({ strategy: "git_worktree" }),
|
||||
})
|
||||
expect(refresh.status).toBe(204)
|
||||
const refreshed = yield* request(test.directory, `${base}/paths`)
|
||||
expect((yield* json<Array<{ path: string; primary: boolean }>>(refreshed)).length).toBe(2)
|
||||
}),
|
||||
{ git: true },
|
||||
)
|
||||
})
|
||||
@@ -117,12 +117,22 @@ import type {
|
||||
PermissionRespondErrors,
|
||||
PermissionRespondResponses,
|
||||
PermissionRuleset,
|
||||
ProjectCopyCreateErrors,
|
||||
ProjectCopyCreateResponses,
|
||||
ProjectCopyRefreshErrors,
|
||||
ProjectCopyRefreshResponses,
|
||||
ProjectCopyRemoveErrors,
|
||||
ProjectCopyRemoveResponses,
|
||||
ProjectCopyStrategiesErrors,
|
||||
ProjectCopyStrategiesResponses,
|
||||
ProjectCurrentErrors,
|
||||
ProjectCurrentResponses,
|
||||
ProjectInitGitErrors,
|
||||
ProjectInitGitResponses,
|
||||
ProjectListErrors,
|
||||
ProjectListResponses,
|
||||
ProjectPathsErrors,
|
||||
ProjectPathsResponses,
|
||||
ProjectUpdateErrors,
|
||||
ProjectUpdateResponses,
|
||||
Prompt,
|
||||
@@ -2373,6 +2383,197 @@ export class Project extends HeyApiClient {
|
||||
},
|
||||
})
|
||||
}
|
||||
|
||||
/**
|
||||
* List project paths
|
||||
*
|
||||
* List known local absolute paths for a project.
|
||||
*/
|
||||
public paths<ThrowOnError extends boolean = false>(
|
||||
parameters: {
|
||||
projectID: string
|
||||
directory?: string
|
||||
workspace?: string
|
||||
},
|
||||
options?: Options<never, ThrowOnError>,
|
||||
) {
|
||||
const params = buildClientParams(
|
||||
[parameters],
|
||||
[
|
||||
{
|
||||
args: [
|
||||
{ in: "path", key: "projectID" },
|
||||
{ in: "query", key: "directory" },
|
||||
{ in: "query", key: "workspace" },
|
||||
],
|
||||
},
|
||||
],
|
||||
)
|
||||
return (options?.client ?? this.client).get<ProjectPathsResponses, ProjectPathsErrors, ThrowOnError>({
|
||||
url: "/project/{projectID}/paths",
|
||||
...options,
|
||||
...params,
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
export class ProjectCopy extends HeyApiClient {
|
||||
/**
|
||||
* List project copy strategies
|
||||
*
|
||||
* List mechanisms available for managing local copies of a project.
|
||||
*/
|
||||
public strategies<ThrowOnError extends boolean = false>(
|
||||
parameters: {
|
||||
projectID: string
|
||||
directory?: string
|
||||
workspace?: string
|
||||
},
|
||||
options?: Options<never, ThrowOnError>,
|
||||
) {
|
||||
const params = buildClientParams(
|
||||
[parameters],
|
||||
[
|
||||
{
|
||||
args: [
|
||||
{ in: "path", key: "projectID" },
|
||||
{ in: "query", key: "directory" },
|
||||
{ in: "query", key: "workspace" },
|
||||
],
|
||||
},
|
||||
],
|
||||
)
|
||||
return (options?.client ?? this.client).get<
|
||||
ProjectCopyStrategiesResponses,
|
||||
ProjectCopyStrategiesErrors,
|
||||
ThrowOnError
|
||||
>({
|
||||
url: "/project/{projectID}/copy/strategy",
|
||||
...options,
|
||||
...params,
|
||||
})
|
||||
}
|
||||
|
||||
/**
|
||||
* Remove project copy
|
||||
*
|
||||
* Remove a local physical copy of a project using the selected strategy.
|
||||
*/
|
||||
public remove<ThrowOnError extends boolean = false>(
|
||||
parameters: {
|
||||
projectID: string
|
||||
directory?: string
|
||||
workspace?: string
|
||||
strategy?: "git_worktree"
|
||||
path?: string
|
||||
},
|
||||
options?: Options<never, ThrowOnError>,
|
||||
) {
|
||||
const params = buildClientParams(
|
||||
[parameters],
|
||||
[
|
||||
{
|
||||
args: [
|
||||
{ in: "path", key: "projectID" },
|
||||
{ in: "query", key: "directory" },
|
||||
{ in: "query", key: "workspace" },
|
||||
{ in: "body", key: "strategy" },
|
||||
{ in: "body", key: "path" },
|
||||
],
|
||||
},
|
||||
],
|
||||
)
|
||||
return (options?.client ?? this.client).delete<ProjectCopyRemoveResponses, ProjectCopyRemoveErrors, ThrowOnError>({
|
||||
url: "/project/{projectID}/copy",
|
||||
...options,
|
||||
...params,
|
||||
headers: {
|
||||
"Content-Type": "application/json",
|
||||
...options?.headers,
|
||||
...params.headers,
|
||||
},
|
||||
})
|
||||
}
|
||||
|
||||
/**
|
||||
* Create project copy
|
||||
*
|
||||
* Create a local physical copy of a project using the selected strategy.
|
||||
*/
|
||||
public create<ThrowOnError extends boolean = false>(
|
||||
parameters: {
|
||||
projectID: string
|
||||
directory?: string
|
||||
workspace?: string
|
||||
strategy?: "git_worktree"
|
||||
path?: string
|
||||
},
|
||||
options?: Options<never, ThrowOnError>,
|
||||
) {
|
||||
const params = buildClientParams(
|
||||
[parameters],
|
||||
[
|
||||
{
|
||||
args: [
|
||||
{ in: "path", key: "projectID" },
|
||||
{ in: "query", key: "directory" },
|
||||
{ in: "query", key: "workspace" },
|
||||
{ in: "body", key: "strategy" },
|
||||
{ in: "body", key: "path" },
|
||||
],
|
||||
},
|
||||
],
|
||||
)
|
||||
return (options?.client ?? this.client).post<ProjectCopyCreateResponses, ProjectCopyCreateErrors, ThrowOnError>({
|
||||
url: "/project/{projectID}/copy",
|
||||
...options,
|
||||
...params,
|
||||
headers: {
|
||||
"Content-Type": "application/json",
|
||||
...options?.headers,
|
||||
...params.headers,
|
||||
},
|
||||
})
|
||||
}
|
||||
|
||||
/**
|
||||
* Refresh project copies
|
||||
*
|
||||
* Discover local project copies using one or all configured strategies.
|
||||
*/
|
||||
public refresh<ThrowOnError extends boolean = false>(
|
||||
parameters: {
|
||||
projectID: string
|
||||
directory?: string
|
||||
workspace?: string
|
||||
strategy?: "git_worktree"
|
||||
},
|
||||
options?: Options<never, ThrowOnError>,
|
||||
) {
|
||||
const params = buildClientParams(
|
||||
[parameters],
|
||||
[
|
||||
{
|
||||
args: [
|
||||
{ in: "path", key: "projectID" },
|
||||
{ in: "query", key: "directory" },
|
||||
{ in: "query", key: "workspace" },
|
||||
{ in: "body", key: "strategy" },
|
||||
],
|
||||
},
|
||||
],
|
||||
)
|
||||
return (options?.client ?? this.client).post<ProjectCopyRefreshResponses, ProjectCopyRefreshErrors, ThrowOnError>({
|
||||
url: "/project/{projectID}/copy/refresh",
|
||||
...options,
|
||||
...params,
|
||||
headers: {
|
||||
"Content-Type": "application/json",
|
||||
...options?.headers,
|
||||
...params.headers,
|
||||
},
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
export class Pty extends HeyApiClient {
|
||||
@@ -5119,6 +5320,11 @@ export class OpencodeClient extends HeyApiClient {
|
||||
return (this._project ??= new Project({ client: this.client }))
|
||||
}
|
||||
|
||||
private _projectCopy?: ProjectCopy
|
||||
get projectCopy(): ProjectCopy {
|
||||
return (this._projectCopy ??= new ProjectCopy({ client: this.client }))
|
||||
}
|
||||
|
||||
private _pty?: Pty
|
||||
get pty(): Pty {
|
||||
return (this._pty ??= new Pty({ client: this.client }))
|
||||
|
||||
@@ -76,6 +76,7 @@ export type Event =
|
||||
| EventPtyDeleted
|
||||
| EventInstallationUpdated
|
||||
| EventInstallationUpdateAvailable
|
||||
| EventProjectPathsUpdated
|
||||
| EventServerConnected
|
||||
| EventGlobalDisposed
|
||||
| EventAccountAdded
|
||||
@@ -1401,6 +1402,13 @@ export type GlobalEvent = {
|
||||
version: string
|
||||
}
|
||||
}
|
||||
| {
|
||||
id: string
|
||||
type: "project.paths.updated"
|
||||
properties: {
|
||||
projectID: string
|
||||
}
|
||||
}
|
||||
| {
|
||||
id: string
|
||||
type: "server.connected"
|
||||
@@ -3335,6 +3343,20 @@ export type ConfigV2ExperimentalPolicy = {
|
||||
resource: string
|
||||
}
|
||||
|
||||
export type ProjectPaths = Array<{
|
||||
path: string
|
||||
primary: boolean
|
||||
}>
|
||||
|
||||
export type ProjectCopyStrategyInfo = {
|
||||
id: "git_worktree"
|
||||
name: string
|
||||
}
|
||||
|
||||
export type ProjectCopyCopy = {
|
||||
path: string
|
||||
}
|
||||
|
||||
export type SessionInfo = {
|
||||
id: string
|
||||
parentID?: string
|
||||
@@ -4457,6 +4479,14 @@ export type EventInstallationUpdateAvailable = {
|
||||
}
|
||||
}
|
||||
|
||||
export type EventProjectPathsUpdated = {
|
||||
id: string
|
||||
type: "project.paths.updated"
|
||||
properties: {
|
||||
projectID: string
|
||||
}
|
||||
}
|
||||
|
||||
export type EventServerConnected = {
|
||||
id: string
|
||||
type: "server.connected"
|
||||
@@ -6153,6 +6183,164 @@ export type ProjectUpdateResponses = {
|
||||
|
||||
export type ProjectUpdateResponse = ProjectUpdateResponses[keyof ProjectUpdateResponses]
|
||||
|
||||
export type ProjectPathsData = {
|
||||
body?: never
|
||||
path: {
|
||||
projectID: string
|
||||
}
|
||||
query?: {
|
||||
directory?: string
|
||||
workspace?: string
|
||||
}
|
||||
url: "/project/{projectID}/paths"
|
||||
}
|
||||
|
||||
export type ProjectPathsErrors = {
|
||||
/**
|
||||
* Bad request
|
||||
*/
|
||||
400: BadRequestError
|
||||
}
|
||||
|
||||
export type ProjectPathsError = ProjectPathsErrors[keyof ProjectPathsErrors]
|
||||
|
||||
export type ProjectPathsResponses = {
|
||||
/**
|
||||
* Project paths
|
||||
*/
|
||||
200: ProjectPaths
|
||||
}
|
||||
|
||||
export type ProjectPathsResponse = ProjectPathsResponses[keyof ProjectPathsResponses]
|
||||
|
||||
export type ProjectCopyStrategiesData = {
|
||||
body?: never
|
||||
path: {
|
||||
projectID: string
|
||||
}
|
||||
query?: {
|
||||
directory?: string
|
||||
workspace?: string
|
||||
}
|
||||
url: "/project/{projectID}/copy/strategy"
|
||||
}
|
||||
|
||||
export type ProjectCopyStrategiesErrors = {
|
||||
/**
|
||||
* Bad request
|
||||
*/
|
||||
400: BadRequestError
|
||||
}
|
||||
|
||||
export type ProjectCopyStrategiesError = ProjectCopyStrategiesErrors[keyof ProjectCopyStrategiesErrors]
|
||||
|
||||
export type ProjectCopyStrategiesResponses = {
|
||||
/**
|
||||
* Project copy strategies
|
||||
*/
|
||||
200: Array<ProjectCopyStrategyInfo>
|
||||
}
|
||||
|
||||
export type ProjectCopyStrategiesResponse = ProjectCopyStrategiesResponses[keyof ProjectCopyStrategiesResponses]
|
||||
|
||||
export type ProjectCopyRemoveData = {
|
||||
body?: {
|
||||
strategy: "git_worktree"
|
||||
path: string
|
||||
}
|
||||
path: {
|
||||
projectID: string
|
||||
}
|
||||
query?: {
|
||||
directory?: string
|
||||
workspace?: string
|
||||
}
|
||||
url: "/project/{projectID}/copy"
|
||||
}
|
||||
|
||||
export type ProjectCopyRemoveErrors = {
|
||||
/**
|
||||
* BadRequest | InvalidRequestError
|
||||
*/
|
||||
400: EffectHttpApiErrorBadRequest | InvalidRequestError
|
||||
}
|
||||
|
||||
export type ProjectCopyRemoveError = ProjectCopyRemoveErrors[keyof ProjectCopyRemoveErrors]
|
||||
|
||||
export type ProjectCopyRemoveResponses = {
|
||||
/**
|
||||
* Project copy removed
|
||||
*/
|
||||
204: void
|
||||
}
|
||||
|
||||
export type ProjectCopyRemoveResponse = ProjectCopyRemoveResponses[keyof ProjectCopyRemoveResponses]
|
||||
|
||||
export type ProjectCopyCreateData = {
|
||||
body?: {
|
||||
strategy: "git_worktree"
|
||||
path: string
|
||||
}
|
||||
path: {
|
||||
projectID: string
|
||||
}
|
||||
query?: {
|
||||
directory?: string
|
||||
workspace?: string
|
||||
}
|
||||
url: "/project/{projectID}/copy"
|
||||
}
|
||||
|
||||
export type ProjectCopyCreateErrors = {
|
||||
/**
|
||||
* BadRequest | InvalidRequestError
|
||||
*/
|
||||
400: EffectHttpApiErrorBadRequest | InvalidRequestError
|
||||
}
|
||||
|
||||
export type ProjectCopyCreateError = ProjectCopyCreateErrors[keyof ProjectCopyCreateErrors]
|
||||
|
||||
export type ProjectCopyCreateResponses = {
|
||||
/**
|
||||
* Project copy created
|
||||
*/
|
||||
200: ProjectCopyCopy
|
||||
}
|
||||
|
||||
export type ProjectCopyCreateResponse = ProjectCopyCreateResponses[keyof ProjectCopyCreateResponses]
|
||||
|
||||
export type ProjectCopyRefreshData = {
|
||||
body?: {
|
||||
strategy?: "git_worktree"
|
||||
}
|
||||
path: {
|
||||
projectID: string
|
||||
}
|
||||
query?: {
|
||||
directory?: string
|
||||
workspace?: string
|
||||
}
|
||||
url: "/project/{projectID}/copy/refresh"
|
||||
}
|
||||
|
||||
export type ProjectCopyRefreshErrors = {
|
||||
/**
|
||||
* BadRequest | InvalidRequestError
|
||||
*/
|
||||
400: EffectHttpApiErrorBadRequest | InvalidRequestError
|
||||
}
|
||||
|
||||
export type ProjectCopyRefreshError = ProjectCopyRefreshErrors[keyof ProjectCopyRefreshErrors]
|
||||
|
||||
export type ProjectCopyRefreshResponses = {
|
||||
/**
|
||||
* Project copies refreshed
|
||||
*/
|
||||
204: void
|
||||
}
|
||||
|
||||
export type ProjectCopyRefreshResponse = ProjectCopyRefreshResponses[keyof ProjectCopyRefreshResponses]
|
||||
|
||||
export type PtyShellsData = {
|
||||
body?: never
|
||||
path?: never
|
||||
|
||||
Reference in New Issue
Block a user