From a5d727e7f972dd0b013d4a347466777482730d55 Mon Sep 17 00:00:00 2001 From: Dax Raad Date: Sat, 28 Feb 2026 15:30:42 -0500 Subject: [PATCH] core: enable workspace-aware configuration and account management commands Switch from boolean active flag to workspace_id tracking so users can select which workspace context to operate in. Login now automatically selects the first available workspace and stores it on the account record. Logout command now actually removes account records and supports targeting specific accounts by email. Switch command provides an interactive picker to change active workspace. Workspaces command lists all available workspaces across accounts. Configuration now loads workspace-specific settings from the server when an active workspace is selected, enabling per-workspace customization of opencode behavior. --- .../migration.sql | 2 + .../snapshot.json | 1018 +++++++++++++++++ packages/opencode/src/account/account.sql.ts | 4 +- packages/opencode/src/account/index.ts | 47 +- packages/opencode/src/cli/cmd/account.ts | 80 +- packages/opencode/src/config/config.ts | 14 +- 6 files changed, 1147 insertions(+), 18 deletions(-) create mode 100644 packages/opencode/migration/20260228200329_account-workspace-id/migration.sql create mode 100644 packages/opencode/migration/20260228200329_account-workspace-id/snapshot.json diff --git a/packages/opencode/migration/20260228200329_account-workspace-id/migration.sql b/packages/opencode/migration/20260228200329_account-workspace-id/migration.sql new file mode 100644 index 0000000000..7572fed8b8 --- /dev/null +++ b/packages/opencode/migration/20260228200329_account-workspace-id/migration.sql @@ -0,0 +1,2 @@ +ALTER TABLE `account` ADD `workspace_id` text;--> statement-breakpoint +ALTER TABLE `account` DROP COLUMN `active`; \ No newline at end of file diff --git a/packages/opencode/migration/20260228200329_account-workspace-id/snapshot.json b/packages/opencode/migration/20260228200329_account-workspace-id/snapshot.json new file mode 100644 index 0000000000..e46b5fe1fa --- /dev/null +++ b/packages/opencode/migration/20260228200329_account-workspace-id/snapshot.json @@ -0,0 +1,1018 @@ +{ + "version": "7", + "dialect": "sqlite", + "id": "1f59b6d9-6292-4cbd-8db1-7e5631d46b77", + "prevIds": [ + "b8dc7400-9dbb-4556-b353-68e6b3d4906e" + ], + "ddl": [ + { + "name": "account", + "entityType": "tables" + }, + { + "name": "workspace", + "entityType": "tables" + }, + { + "name": "project", + "entityType": "tables" + }, + { + "name": "message", + "entityType": "tables" + }, + { + "name": "part", + "entityType": "tables" + }, + { + "name": "permission", + "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": "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": "text", + "notNull": false, + "autoincrement": false, + "default": null, + "generated": null, + "name": "workspace_id", + "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": false, + "autoincrement": false, + "default": null, + "generated": null, + "name": "id", + "entityType": "columns", + "table": "workspace" + }, + { + "type": "text", + "notNull": false, + "autoincrement": false, + "default": null, + "generated": null, + "name": "branch", + "entityType": "columns", + "table": "workspace" + }, + { + "type": "text", + "notNull": true, + "autoincrement": false, + "default": null, + "generated": null, + "name": "project_id", + "entityType": "columns", + "table": "workspace" + }, + { + "type": "text", + "notNull": true, + "autoincrement": false, + "default": null, + "generated": null, + "name": "config", + "entityType": "columns", + "table": "workspace" + }, + { + "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_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" + }, + { + "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": "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": 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": "revert", + "entityType": "columns", + "table": "session" + }, + { + "type": "text", + "notNull": false, + "autoincrement": false, + "default": null, + "generated": null, + "name": "permission", + "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": [ + "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": [ + "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": [ + "session_id", + "position" + ], + "nameExplicit": false, + "name": "todo_pk", + "entityType": "pks", + "table": "todo" + }, + { + "columns": [ + "id" + ], + "nameExplicit": false, + "name": "account_pk", + "table": "account", + "entityType": "pks" + }, + { + "columns": [ + "id" + ], + "nameExplicit": false, + "name": "workspace_pk", + "table": "workspace", + "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_pk", + "table": "session", + "entityType": "pks" + }, + { + "columns": [ + "session_id" + ], + "nameExplicit": false, + "name": "session_share_pk", + "table": "session_share", + "entityType": "pks" + }, + { + "columns": [ + { + "value": "session_id", + "isExpression": false + } + ], + "isUnique": false, + "where": null, + "origin": "manual", + "name": "message_session_idx", + "entityType": "indexes", + "table": "message" + }, + { + "columns": [ + { + "value": "message_id", + "isExpression": false + } + ], + "isUnique": false, + "where": null, + "origin": "manual", + "name": "part_message_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": "project_id", + "isExpression": false + } + ], + "isUnique": false, + "where": null, + "origin": "manual", + "name": "session_project_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": [] +} \ No newline at end of file diff --git a/packages/opencode/src/account/account.sql.ts b/packages/opencode/src/account/account.sql.ts index 3073ca02f1..67dfccf6ab 100644 --- a/packages/opencode/src/account/account.sql.ts +++ b/packages/opencode/src/account/account.sql.ts @@ -8,8 +8,6 @@ export const AccountTable = sqliteTable("account", { access_token: text().notNull(), refresh_token: text().notNull(), token_expiry: integer(), - active: integer({ mode: "boolean" }) - .notNull() - .$default(() => false), + workspace_id: text(), ...Timestamps, }) diff --git a/packages/opencode/src/account/index.ts b/packages/opencode/src/account/index.ts index 47ef6ebbfd..d6b503cf33 100644 --- a/packages/opencode/src/account/index.ts +++ b/packages/opencode/src/account/index.ts @@ -1,4 +1,4 @@ -import { eq } from "drizzle-orm" +import { eq, sql, isNotNull } from "drizzle-orm" import { Database } from "@/storage/db" import { AccountTable } from "./account.sql" import z from "zod" @@ -8,6 +8,7 @@ export namespace Account { id: z.string(), email: z.string(), url: z.string(), + workspace_id: z.string().nullable(), }) export type Account = z.infer @@ -16,11 +17,12 @@ export namespace Account { id: row.id, email: row.email, url: row.url, + workspace_id: row.workspace_id, } } export function active(): Account | undefined { - const row = Database.use((db) => db.select().from(AccountTable).where(eq(AccountTable.active, true)).get()) + const row = Database.use((db) => db.select().from(AccountTable).where(isNotNull(AccountTable.workspace_id)).get()) return row ? fromRow(row) : undefined } @@ -28,6 +30,16 @@ export namespace Account { return Database.use((db) => db.select().from(AccountTable).all().map(fromRow)) } + export function remove(accountID: string) { + Database.use((db) => db.delete(AccountTable).where(eq(AccountTable.id, accountID)).run()) + } + + export function use(accountID: string, workspaceID: string | null) { + Database.use((db) => + db.update(AccountTable).set({ workspace_id: workspaceID }).where(eq(AccountTable.id, accountID)).run(), + ) + } + export async function workspaces(accountID: string): Promise<{ id: string; name: string }[]> { const row = Database.use((db) => db.select().from(AccountTable).where(eq(AccountTable.id, accountID)).get()) if (!row) return [] @@ -45,6 +57,22 @@ export namespace Account { return json.map((x) => ({ id: x.id ?? "", name: x.name ?? "" })) } + export async function config(accountID: string, workspaceID: string): Promise | undefined> { + const row = Database.use((db) => db.select().from(AccountTable).where(eq(AccountTable.id, accountID)).get()) + if (!row) return undefined + + const access = await token(accountID) + if (!access) return undefined + + const res = await fetch(`${row.url}/api/config`, { + headers: { authorization: `Bearer ${access}`, "x-org-id": workspaceID }, + }) + + if (!res.ok) return undefined + const result = (await res.json()) as Record + return result.config + } + export async function token(accountID: string): Promise { const row = Database.use((db) => db.select().from(AccountTable).where(eq(AccountTable.id, accountID)).get()) if (!row) return undefined @@ -164,17 +192,24 @@ export namespace Account { const expiry = Date.now() + json.expires_in! * 1000 const refresh = json.refresh_token ?? "" + // Fetch workspaces and get first one + const orgsRes = await fetch(`${input.server}/api/orgs`, { + headers: { authorization: `Bearer ${access}` }, + }) + const orgs = (await orgsRes.json()) as Array<{ id?: string; name?: string }> + const firstWorkspaceId = orgs.length > 0 ? orgs[0].id : null + Database.use((db) => { - db.update(AccountTable).set({ active: false }).run() + db.update(AccountTable).set({ workspace_id: null }).run() db.insert(AccountTable) .values({ id, email, - url: input.url, + url: input.server, access_token: access, refresh_token: refresh, token_expiry: expiry, - active: true, + workspace_id: firstWorkspaceId, }) .onConflictDoUpdate({ target: AccountTable.id, @@ -182,7 +217,7 @@ export namespace Account { access_token: access, refresh_token: refresh, token_expiry: expiry, - active: true, + workspace_id: firstWorkspaceId, }, }) .run() diff --git a/packages/opencode/src/cli/cmd/account.ts b/packages/opencode/src/cli/cmd/account.ts index 00fbcf2085..ef1c64c806 100644 --- a/packages/opencode/src/cli/cmd/account.ts +++ b/packages/opencode/src/cli/cmd/account.ts @@ -70,20 +70,92 @@ export const LoginCommand = cmd({ }) export const LogoutCommand = cmd({ - command: "logout", + command: "logout [email]", describe: "log out from an account", - async handler() {}, + builder: (yargs) => + yargs.positional("email", { + describe: "account email to log out from", + type: "string", + }), + async handler(args) { + const email = args.email as string | undefined + + if (email) { + const accounts = Account.list() + const match = accounts.find((a) => a.email === email) + if (!match) { + UI.println("Account not found: " + email) + return + } + Account.remove(match.id) + UI.println("Logged out from " + email) + return + } + + const active = Account.active() + if (!active) { + UI.println("Not logged in") + return + } + Account.remove(active.id) + UI.println("Logged out from " + active.email) + }, }) export const SwitchCommand = cmd({ command: "switch", describe: "switch active workspace", - async handler() {}, + async handler() { + UI.empty() + + const active = Account.active() + if (!active) { + UI.println("Not logged in") + return + } + + const workspaces = await Account.workspaces(active.id) + if (workspaces.length === 0) { + UI.println("No workspaces found") + return + } + + prompts.intro("Switch workspace") + + const opts = workspaces.map((w) => ({ + value: w.id, + label: w.id === active.workspace_id ? w.name + UI.Style.TEXT_DIM + " (active)" : w.name, + })) + + const selected = await prompts.select({ + message: "Select workspace", + options: opts, + }) + + if (prompts.isCancel(selected)) return + + Account.use(active.id, selected as string) + prompts.outro("Switched to " + workspaces.find((w) => w.id === selected)?.name) + }, }) export const WorkspacesCommand = cmd({ command: "workspaces", aliases: ["workspace"], describe: "list all workspaces", - async handler() {}, + async handler() { + const accounts = Account.list() + + if (accounts.length === 0) { + UI.println("No accounts found") + return + } + + for (const account of accounts) { + const workspaces = await Account.workspaces(account.id) + for (const space of workspaces) { + UI.println([space.name, account.email, space.id].join("\t")) + } + } + }, }) diff --git a/packages/opencode/src/config/config.ts b/packages/opencode/src/config/config.ts index 5e458b7bd0..405bd8b979 100644 --- a/packages/opencode/src/config/config.ts +++ b/packages/opencode/src/config/config.ts @@ -107,11 +107,6 @@ export namespace Config { } } - const active = Account.active() - const token = active ? await Account.token(active.id) : undefined - if (token) { - } - // Global user config overrides remote config. result = mergeConfigConcatArrays(result, await global()) @@ -178,6 +173,15 @@ export namespace Config { log.debug("loaded custom config from OPENCODE_CONFIG_CONTENT") } + const active = Account.active() + if (active?.workspace_id) { + const config = await Account.config(active.id, active.workspace_id) + result = mergeConfigConcatArrays(result, config ?? {}) + const token = await Account.token(active.id) + // TODO: this is bad + process.env["OPENCODE_CONTROL_TOKEN"] = token + } + // Load managed config files last (highest priority) - enterprise admin-controlled // Kept separate from directories array to avoid write operations when installing plugins // which would fail on system directories requiring elevated permissions