From 7046fbb21ed85b66c0cc977080671c8416273353 Mon Sep 17 00:00:00 2001 From: devenv Date: Fri, 17 Apr 2026 15:29:49 +0600 Subject: [PATCH 1/8] =?UTF-8?q?=F0=9F=A7=AA=20Update=20CLI=20test=20files?= =?UTF-8?q?=20with=20refactored=20patterns=20and=20assertions?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- cli/src/__tests__/agent-jwt-env.test.ts | 12 +- cli/src/__tests__/allowed-hostname.test.ts | 12 +- .../auth-command-registration.test.ts | 4 +- cli/src/__tests__/board-auth.test.ts | 12 +- cli/src/__tests__/common.test.ts | 10 +- cli/src/__tests__/company-delete.test.ts | 44 +- .../company-import-export-e2e.test.ts | 200 ++++--- cli/src/__tests__/company-import-url.test.ts | 4 +- cli/src/__tests__/company-import-zip.test.ts | 4 +- cli/src/__tests__/company.test.ts | 157 +++++- cli/src/__tests__/data-dir.test.ts | 45 +- cli/src/__tests__/feedback.test.ts | 23 +- cli/src/__tests__/helpers/zip.ts | 18 +- cli/src/__tests__/home-paths.test.ts | 14 +- cli/src/__tests__/http.test.ts | 88 ++- cli/src/__tests__/network-bind.test.ts | 9 +- cli/src/__tests__/onboard.test.ts | 60 +- cli/src/__tests__/routines.test.ts | 34 +- cli/src/__tests__/telemetry.test.ts | 160 ++++-- .../__tests__/worktree-merge-history.test.ts | 60 +- cli/src/__tests__/worktree.test.ts | 530 ++++++++++++++---- 21 files changed, 1114 insertions(+), 386 deletions(-) diff --git a/cli/src/__tests__/agent-jwt-env.test.ts b/cli/src/__tests__/agent-jwt-env.test.ts index 49c5774..77a8f90 100644 --- a/cli/src/__tests__/agent-jwt-env.test.ts +++ b/cli/src/__tests__/agent-jwt-env.test.ts @@ -45,7 +45,9 @@ describe("agent jwt env helpers", () => { it("loads secret from .env next to explicit config path", () => { const configPath = tempConfigPath(); const envPath = resolveAgentJwtEnvFile(configPath); - fs.writeFileSync(envPath, "TASKCORE_AGENT_JWT_SECRET=test-secret\n", { mode: 0o600 }); + fs.writeFileSync(envPath, "TASKCORE_AGENT_JWT_SECRET=test-secret\n", { + mode: 0o600, + }); const loaded = readAgentJwtSecretFromEnv(configPath); expect(loaded).toBe("test-secret"); @@ -55,7 +57,9 @@ describe("agent jwt env helpers", () => { it("doctor check passes when secret exists in adjacent .env", () => { const configPath = tempConfigPath(); const envPath = resolveAgentJwtEnvFile(configPath); - fs.writeFileSync(envPath, "TASKCORE_AGENT_JWT_SECRET=check-secret\n", { mode: 0o600 }); + fs.writeFileSync(envPath, "TASKCORE_AGENT_JWT_SECRET=check-secret\n", { + mode: 0o600, + }); const result = agentJwtSecretCheck(configPath); expect(result.status).toBe("pass"); @@ -74,6 +78,8 @@ describe("agent jwt env helpers", () => { const contents = fs.readFileSync(envPath, "utf-8"); expect(contents).toContain('TASKCORE_WORKTREE_COLOR="#439edb"'); - expect(readTaskcoreEnvEntries(envPath).TASKCORE_WORKTREE_COLOR).toBe("#439edb"); + expect(readTaskcoreEnvEntries(envPath).TASKCORE_WORKTREE_COLOR).toBe( + "#439edb", + ); }); }); diff --git a/cli/src/__tests__/allowed-hostname.test.ts b/cli/src/__tests__/allowed-hostname.test.ts index 8024449..883f7b2 100644 --- a/cli/src/__tests__/allowed-hostname.test.ts +++ b/cli/src/__tests__/allowed-hostname.test.ts @@ -6,7 +6,9 @@ import type { TaskcoreConfig } from "../config/schema.js"; import { addAllowedHostname } from "../commands/allowed-hostname.js"; function createTempConfigPath() { - const dir = fs.mkdtempSync(path.join(os.tmpdir(), "taskcore-allowed-hostname-")); + const dir = fs.mkdtempSync( + path.join(os.tmpdir(), "taskcore-allowed-hostname-"), + ); return path.join(dir, "config.json"); } @@ -71,10 +73,14 @@ describe("allowed-hostname command", () => { const configPath = createTempConfigPath(); writeBaseConfig(configPath); - await addAllowedHostname("https://Dotta-MacBook-Pro:3100", { config: configPath }); + await addAllowedHostname("https://Dotta-MacBook-Pro:3100", { + config: configPath, + }); await addAllowedHostname("dotta-macbook-pro", { config: configPath }); - const raw = JSON.parse(fs.readFileSync(configPath, "utf-8")) as TaskcoreConfig; + const raw = JSON.parse( + fs.readFileSync(configPath, "utf-8"), + ) as TaskcoreConfig; expect(raw.server.allowedHostnames).toEqual(["dotta-macbook-pro"]); }); }); diff --git a/cli/src/__tests__/auth-command-registration.test.ts b/cli/src/__tests__/auth-command-registration.test.ts index a93d8fa..a5461aa 100644 --- a/cli/src/__tests__/auth-command-registration.test.ts +++ b/cli/src/__tests__/auth-command-registration.test.ts @@ -11,6 +11,8 @@ describe("registerClientAuthCommands", () => { const login = auth.commands.find((command) => command.name() === "login"); expect(login).toBeDefined(); - expect(login?.options.filter((option) => option.long === "--company-id")).toHaveLength(1); + expect( + login?.options.filter((option) => option.long === "--company-id"), + ).toHaveLength(1); }); }); diff --git a/cli/src/__tests__/board-auth.test.ts b/cli/src/__tests__/board-auth.test.ts index 000504f..d10a04a 100644 --- a/cli/src/__tests__/board-auth.test.ts +++ b/cli/src/__tests__/board-auth.test.ts @@ -32,7 +32,9 @@ describe("board auth store", () => { storePath: authPath, }); - expect(getStoredBoardCredential("http://localhost:3100", authPath)).toMatchObject({ + expect( + getStoredBoardCredential("http://localhost:3100", authPath), + ).toMatchObject({ apiBase: "http://localhost:3100", token: "token-123", userId: "user-1", @@ -47,7 +49,11 @@ describe("board auth store", () => { storePath: authPath, }); - expect(removeStoredBoardCredential("http://localhost:3100", authPath)).toBe(true); - expect(getStoredBoardCredential("http://localhost:3100", authPath)).toBeNull(); + expect(removeStoredBoardCredential("http://localhost:3100", authPath)).toBe( + true, + ); + expect( + getStoredBoardCredential("http://localhost:3100", authPath), + ).toBeNull(); }); }); diff --git a/cli/src/__tests__/common.test.ts b/cli/src/__tests__/common.test.ts index 11f4f54..33aad41 100644 --- a/cli/src/__tests__/common.test.ts +++ b/cli/src/__tests__/common.test.ts @@ -43,7 +43,10 @@ describe("resolveCommandContext", () => { ); process.env.AGENT_KEY = "key-from-env"; - const resolved = resolveCommandContext({ context: contextPath }, { requireCompany: true }); + const resolved = resolveCommandContext( + { context: contextPath }, + { requireCompany: true }, + ); expect(resolved.api.apiBase).toBe("http://127.0.0.1:9999"); expect(resolved.companyId).toBe("company-profile"); expect(resolved.api.apiKey).toBe("key-from-env"); @@ -92,7 +95,10 @@ describe("resolveCommandContext", () => { ); expect(() => - resolveCommandContext({ context: contextPath, apiBase: "http://localhost:3100" }, { requireCompany: true }), + resolveCommandContext( + { context: contextPath, apiBase: "http://localhost:3100" }, + { requireCompany: true }, + ), ).toThrow(/Company ID is required/); }); }); diff --git a/cli/src/__tests__/company-delete.test.ts b/cli/src/__tests__/company-delete.test.ts index 7bbd508..c9c1172 100644 --- a/cli/src/__tests__/company-delete.test.ts +++ b/cli/src/__tests__/company-delete.test.ts @@ -1,6 +1,9 @@ import { describe, expect, it } from "vitest"; import type { Company } from "@taskcore/shared"; -import { assertDeleteConfirmation, resolveCompanyForDeletion } from "../commands/client/company.js"; +import { + assertDeleteConfirmation, + resolveCompanyForDeletion, +} from "../commands/client/company.js"; function makeCompany(overrides: Partial): Company { return { @@ -43,7 +46,11 @@ describe("resolveCompanyForDeletion", () => { ]; it("resolves by ID in auto mode", () => { - const result = resolveCompanyForDeletion(companies, "22222222-2222-2222-2222-222222222222", "auto"); + const result = resolveCompanyForDeletion( + companies, + "22222222-2222-2222-2222-222222222222", + "auto", + ); expect(result.issuePrefix).toBe("PAP"); }); @@ -53,16 +60,25 @@ describe("resolveCompanyForDeletion", () => { }); it("throws when selector is not found", () => { - expect(() => resolveCompanyForDeletion(companies, "MISSING", "auto")).toThrow(/No company found/); + expect(() => + resolveCompanyForDeletion(companies, "MISSING", "auto"), + ).toThrow(/No company found/); }); it("respects explicit id mode", () => { - expect(() => resolveCompanyForDeletion(companies, "PAP", "id")).toThrow(/No company found by ID/); + expect(() => resolveCompanyForDeletion(companies, "PAP", "id")).toThrow( + /No company found by ID/, + ); }); it("respects explicit prefix mode", () => { - expect(() => resolveCompanyForDeletion(companies, "22222222-2222-2222-2222-222222222222", "prefix")) - .toThrow(/No company found by shortname/); + expect(() => + resolveCompanyForDeletion( + companies, + "22222222-2222-2222-2222-222222222222", + "prefix", + ), + ).toThrow(/No company found by shortname/); }); }); @@ -73,11 +89,15 @@ describe("assertDeleteConfirmation", () => { }); it("requires --yes", () => { - expect(() => assertDeleteConfirmation(company, { confirm: "PAP" })).toThrow(/requires --yes/); + expect(() => assertDeleteConfirmation(company, { confirm: "PAP" })).toThrow( + /requires --yes/, + ); }); it("accepts matching prefix confirmation", () => { - expect(() => assertDeleteConfirmation(company, { yes: true, confirm: "pap" })).not.toThrow(); + expect(() => + assertDeleteConfirmation(company, { yes: true, confirm: "pap" }), + ).not.toThrow(); }); it("accepts matching id confirmation", () => { @@ -85,11 +105,13 @@ describe("assertDeleteConfirmation", () => { assertDeleteConfirmation(company, { yes: true, confirm: "22222222-2222-2222-2222-222222222222", - })).not.toThrow(); + }), + ).not.toThrow(); }); it("rejects mismatched confirmation", () => { - expect(() => assertDeleteConfirmation(company, { yes: true, confirm: "nope" })) - .toThrow(/does not match target company/); + expect(() => + assertDeleteConfirmation(company, { yes: true, confirm: "nope" }), + ).toThrow(/does not match target company/); }); }); diff --git a/cli/src/__tests__/company-import-export-e2e.test.ts b/cli/src/__tests__/company-import-export-e2e.test.ts index 5b93f10..5f836c5 100644 --- a/cli/src/__tests__/company-import-export-e2e.test.ts +++ b/cli/src/__tests__/company-import-export-e2e.test.ts @@ -1,5 +1,12 @@ import { execFile, spawn } from "node:child_process"; -import { mkdirSync, mkdtempSync, readFileSync, readdirSync, rmSync, writeFileSync } from "node:fs"; +import { + mkdirSync, + mkdtempSync, + readFileSync, + readdirSync, + rmSync, + writeFileSync, +} from "node:fs"; import net from "node:net"; import os from "node:os"; import path from "node:path"; @@ -36,7 +43,9 @@ async function getAvailablePort(): Promise { } const embeddedPostgresSupport = await getEmbeddedPostgresTestSupport(); -const describeEmbeddedPostgres = embeddedPostgresSupport.supported ? describe : describe.skip; +const describeEmbeddedPostgres = embeddedPostgresSupport.supported + ? describe + : describe.skip; if (!embeddedPostgresSupport.supported) { console.warn( @@ -44,7 +53,12 @@ if (!embeddedPostgresSupport.supported) { ); } -function writeTestConfig(configPath: string, tempRoot: string, port: number, connectionString: string) { +function writeTestConfig( + configPath: string, + tempRoot: string, + port: number, + connectionString: string, +) { const config = { $meta: { version: 1, @@ -104,7 +118,11 @@ function writeTestConfig(configPath: string, tempRoot: string, port: number, con writeFileSync(configPath, `${JSON.stringify(config, null, 2)}\n`, "utf8"); } -function createServerEnv(configPath: string, port: number, connectionString: string) { +function createServerEnv( + configPath: string, + port: number, + connectionString: string, +) { const env = { ...process.env }; for (const key of Object.keys(env)) { if (key.startsWith("TASKCORE_")) { @@ -148,7 +166,11 @@ function createCliEnv() { return env; } -function collectTextFiles(root: string, current: string, files: Record) { +function collectTextFiles( + root: string, + current: string, + files: Record, +) { for (const entry of readdirSync(current, { withFileTypes: true })) { const absolutePath = path.join(current, entry.name); if (entry.isDirectory()) { @@ -174,20 +196,39 @@ async function stopServerProcess(child: ServerProcess | null) { }); } -async function api(baseUrl: string, pathname: string, init?: RequestInit): Promise { +async function api( + baseUrl: string, + pathname: string, + init?: RequestInit, +): Promise { const res = await fetch(`${baseUrl}${pathname}`, init); const text = await res.text(); if (!res.ok) { throw new Error(`Request failed ${res.status} ${pathname}: ${text}`); } - return text ? JSON.parse(text) as T : (null as T); + return text ? (JSON.parse(text) as T) : (null as T); } -async function runCliJson(args: string[], opts: { apiBase: string; configPath: string }) { - const repoRoot = path.resolve(path.dirname(fileURLToPath(import.meta.url)), "../../.."); +async function runCliJson( + args: string[], + opts: { apiBase: string; configPath: string }, +) { + const repoRoot = path.resolve( + path.dirname(fileURLToPath(import.meta.url)), + "../../..", + ); const result = await execFileAsync( "pnpm", - ["--silent", "taskcore", ...args, "--api-base", opts.apiBase, "--config", opts.configPath, "--json"], + [ + "--silent", + "taskcore", + ...args, + "--api-base", + opts.apiBase, + "--config", + opts.configPath, + "--json", + ], { cwd: repoRoot, env: createCliEnv(), @@ -197,7 +238,9 @@ async function runCliJson(args: string[], opts: { apiBase: string; configPath const stdout = result.stdout.trim(); const jsonStart = stdout.search(/[\[{]/); if (jsonStart === -1) { - throw new Error(`CLI did not emit JSON.\nstdout:\n${result.stdout}\nstderr:\n${result.stderr}`); + throw new Error( + `CLI did not emit JSON.\nstdout:\n${result.stdout}\nstderr:\n${result.stderr}`, + ); } return JSON.parse(stdout.slice(jsonStart)) as T; } @@ -236,30 +279,33 @@ describeEmbeddedPostgres("taskcore company import/export e2e", () => { let exportDir = ""; let apiBase = ""; let serverProcess: ServerProcess | null = null; - let tempDb: Awaited> | null = null; + let tempDb: Awaited< + ReturnType + > | null = null; beforeAll(async () => { tempRoot = mkdtempSync(path.join(os.tmpdir(), "taskcore-company-cli-e2e-")); configPath = path.join(tempRoot, "config", "config.json"); exportDir = path.join(tempRoot, "exported-company"); - tempDb = await startEmbeddedPostgresTestDatabase("taskcore-company-cli-db-"); + tempDb = await startEmbeddedPostgresTestDatabase( + "taskcore-company-cli-db-", + ); const port = await getAvailablePort(); writeTestConfig(configPath, tempRoot, port, tempDb.connectionString); apiBase = `http://127.0.0.1:${port}`; - const repoRoot = path.resolve(path.dirname(fileURLToPath(import.meta.url)), "../../.."); - const output = { stdout: [] as string[], stderr: [] as string[] }; - const child = spawn( - "pnpm", - ["taskcore", "run", "--config", configPath], - { - cwd: repoRoot, - env: createServerEnv(configPath, port, tempDb.connectionString), - stdio: ["ignore", "pipe", "pipe"], - }, + const repoRoot = path.resolve( + path.dirname(fileURLToPath(import.meta.url)), + "../../..", ); + const output = { stdout: [] as string[], stderr: [] as string[] }; + const child = spawn("pnpm", ["taskcore", "run", "--config", configPath], { + cwd: repoRoot, + env: createServerEnv(configPath, port, tempDb.connectionString), + stdio: ["ignore", "pipe", "pipe"], + }); serverProcess = child; child.stdout?.on("data", (chunk) => { output.stdout.push(String(chunk)); @@ -282,7 +328,11 @@ describeEmbeddedPostgres("taskcore company import/export e2e", () => { it("exports a company package and imports it into new and existing companies", async () => { expect(serverProcess).not.toBeNull(); - const sourceCompany = await api<{ id: string; name: string; issuePrefix: string }>(apiBase, "/api/companies", { + const sourceCompany = await api<{ + id: string; + name: string; + issuePrefix: string; + }>(apiBase, "/api/companies", { method: "POST", headers: { "content-type": "application/json" }, body: JSON.stringify({ name: `CLI Export Source ${Date.now()}` }), @@ -320,21 +370,21 @@ describeEmbeddedPostgres("taskcore company import/export e2e", () => { const largeIssueDescription = `Round-trip the company package through the CLI.\n\n${"portable-data ".repeat(12_000)}`; - const sourceIssue = await api<{ id: string; title: string; identifier: string }>( - apiBase, - `/api/companies/${sourceCompany.id}/issues`, - { - method: "POST", - headers: { "content-type": "application/json" }, - body: JSON.stringify({ - title: "Validate company import/export", - description: largeIssueDescription, - status: "todo", - projectId: sourceProject.id, - assigneeAgentId: sourceAgent.id, - }), - }, - ); + const sourceIssue = await api<{ + id: string; + title: string; + identifier: string; + }>(apiBase, `/api/companies/${sourceCompany.id}/issues`, { + method: "POST", + headers: { "content-type": "application/json" }, + body: JSON.stringify({ + title: "Validate company import/export", + description: largeIssueDescription, + status: "todo", + projectId: sourceProject.id, + assigneeAgentId: sourceAgent.id, + }), + }); const exportResult = await runCliJson<{ ok: boolean; @@ -355,8 +405,12 @@ describeEmbeddedPostgres("taskcore company import/export e2e", () => { expect(exportResult.ok).toBe(true); expect(exportResult.filesWritten).toBeGreaterThan(0); - expect(readFileSync(path.join(exportDir, "COMPANY.md"), "utf8")).toContain(sourceCompany.name); - expect(readFileSync(path.join(exportDir, ".taskcore.yaml"), "utf8")).toContain('schema: "taskcore/v1"'); + expect(readFileSync(path.join(exportDir, "COMPANY.md"), "utf8")).toContain( + sourceCompany.name, + ); + expect( + readFileSync(path.join(exportDir, ".taskcore.yaml"), "utf8"), + ).toContain('schema: "taskcore/v1"'); const importedNew = await runCliJson<{ company: { id: string; name: string; action: string }; @@ -389,14 +443,19 @@ describeEmbeddedPostgres("taskcore company import/export e2e", () => { apiBase, `/api/companies/${importedNew.company.id}/projects`, ); - const importedIssues = await api>( - apiBase, - `/api/companies/${importedNew.company.id}/issues`, - ); + const importedIssues = await api< + Array<{ id: string; title: string; identifier: string }> + >(apiBase, `/api/companies/${importedNew.company.id}/issues`); - expect(importedAgents.map((agent) => agent.name)).toContain(sourceAgent.name); - expect(importedProjects.map((project) => project.name)).toContain(sourceProject.name); - expect(importedIssues.map((issue) => issue.title)).toContain(sourceIssue.title); + expect(importedAgents.map((agent) => agent.name)).toContain( + sourceAgent.name, + ); + expect(importedProjects.map((project) => project.name)).toContain( + sourceProject.name, + ); + expect(importedIssues.map((issue) => issue.title)).toContain( + sourceIssue.title, + ); const previewExisting = await runCliJson<{ errors: string[]; @@ -426,9 +485,17 @@ describeEmbeddedPostgres("taskcore company import/export e2e", () => { expect(previewExisting.errors).toEqual([]); expect(previewExisting.plan.companyAction).toBe("none"); - expect(previewExisting.plan.agentPlans.some((plan) => plan.action === "create")).toBe(true); - expect(previewExisting.plan.projectPlans.some((plan) => plan.action === "create")).toBe(true); - expect(previewExisting.plan.issuePlans.some((plan) => plan.action === "create")).toBe(true); + expect( + previewExisting.plan.agentPlans.some((plan) => plan.action === "create"), + ).toBe(true); + expect( + previewExisting.plan.projectPlans.some( + (plan) => plan.action === "create", + ), + ).toBe(true); + expect( + previewExisting.plan.issuePlans.some((plan) => plan.action === "create"), + ).toBe(true); const importedExisting = await runCliJson<{ company: { id: string; action: string }; @@ -452,30 +519,35 @@ describeEmbeddedPostgres("taskcore company import/export e2e", () => { ); expect(importedExisting.company.action).toBe("unchanged"); - expect(importedExisting.agents.some((agent) => agent.action === "created")).toBe(true); + expect( + importedExisting.agents.some((agent) => agent.action === "created"), + ).toBe(true); const twiceImportedAgents = await api>( apiBase, `/api/companies/${importedNew.company.id}/agents`, ); - const twiceImportedProjects = await api>( - apiBase, - `/api/companies/${importedNew.company.id}/projects`, - ); - const twiceImportedIssues = await api>( - apiBase, - `/api/companies/${importedNew.company.id}/issues`, - ); + const twiceImportedProjects = await api< + Array<{ id: string; name: string }> + >(apiBase, `/api/companies/${importedNew.company.id}/projects`); + const twiceImportedIssues = await api< + Array<{ id: string; title: string; identifier: string }> + >(apiBase, `/api/companies/${importedNew.company.id}/issues`); expect(twiceImportedAgents).toHaveLength(2); - expect(new Set(twiceImportedAgents.map((agent) => agent.name)).size).toBe(2); + expect(new Set(twiceImportedAgents.map((agent) => agent.name)).size).toBe( + 2, + ); expect(twiceImportedProjects).toHaveLength(2); expect(twiceImportedIssues).toHaveLength(2); const zipPath = path.join(tempRoot, "exported-company.zip"); const portableFiles: Record = {}; collectTextFiles(exportDir, exportDir, portableFiles); - writeFileSync(zipPath, createStoredZipArchive(portableFiles, "taskcore-demo")); + writeFileSync( + zipPath, + createStoredZipArchive(portableFiles, "taskcore-demo"), + ); const importedFromZip = await runCliJson<{ company: { id: string; name: string; action: string }; @@ -497,6 +569,8 @@ describeEmbeddedPostgres("taskcore company import/export e2e", () => { ); expect(importedFromZip.company.action).toBe("created"); - expect(importedFromZip.agents.some((agent) => agent.action === "created")).toBe(true); + expect( + importedFromZip.agents.some((agent) => agent.action === "created"), + ).toBe(true); }, 60_000); }); diff --git a/cli/src/__tests__/company-import-url.test.ts b/cli/src/__tests__/company-import-url.test.ts index 1e12cd9..b19deb4 100644 --- a/cli/src/__tests__/company-import-url.test.ts +++ b/cli/src/__tests__/company-import-url.test.ts @@ -56,7 +56,9 @@ describe("normalizeGithubImportSource", () => { }); it("applies --ref to shorthand imports", () => { - expect(normalizeGithubImportSource("taskcore/companies/gstack", "feature/demo")).toBe( + expect( + normalizeGithubImportSource("taskcore/companies/gstack", "feature/demo"), + ).toBe( "https://github.com/khulnasoft/companies?ref=feature%2Fdemo&path=gstack", ); }); diff --git a/cli/src/__tests__/company-import-zip.test.ts b/cli/src/__tests__/company-import-zip.test.ts index 9a66f1b..c1c7052 100644 --- a/cli/src/__tests__/company-import-zip.test.ts +++ b/cli/src/__tests__/company-import-zip.test.ts @@ -15,7 +15,9 @@ afterEach(async () => { describe("resolveInlineSourceFromPath", () => { it("imports portable files from a zip archive instead of scanning the parent directory", async () => { - const tempDir = await mkdtemp(path.join(os.tmpdir(), "taskcore-company-import-zip-")); + const tempDir = await mkdtemp( + path.join(os.tmpdir(), "taskcore-company-import-zip-"), + ); tempDirs.push(tempDir); const archivePath = path.join(tempDir, "taskcore-demo.zip"); diff --git a/cli/src/__tests__/company.test.ts b/cli/src/__tests__/company.test.ts index a136120..50df16f 100644 --- a/cli/src/__tests__/company.test.ts +++ b/cli/src/__tests__/company.test.ts @@ -55,7 +55,7 @@ describe("resolveCompanyImportApiPath", () => { dryRun: true, targetMode: "existing_company", companyId: " ", - }) + }), ).toThrow(/require a companyId/i); }); }); @@ -87,7 +87,7 @@ describe("resolveCompanyImportApplyConfirmationMode", () => { yes: false, interactive: false, json: false, - }) + }), ).toThrow(/non-interactive terminal requires --yes/i); }); @@ -97,16 +97,16 @@ describe("resolveCompanyImportApplyConfirmationMode", () => { yes: false, interactive: false, json: true, - }) + }), ).toThrow(/with --json requires --yes/i); }); }); describe("buildCompanyDashboardUrl", () => { it("preserves the configured base path when building a dashboard URL", () => { - expect(buildCompanyDashboardUrl("https://taskcore.example/app/", "PAP")).toBe( - "https://taskcore.example/app/PAP/dashboard", - ); + expect( + buildCompanyDashboardUrl("https://taskcore.example/app/", "PAP"), + ).toBe("https://taskcore.example/app/PAP/dashboard"); }); }); @@ -123,23 +123,84 @@ describe("renderCompanyImportPreview", () => { targetCompanyId: "company-123", targetCompanyName: "Imported Co", collisionStrategy: "rename", - selectedAgentSlugs: ["ceo", "cto", "eng-1", "eng-2", "eng-3", "eng-4", "eng-5"], + selectedAgentSlugs: [ + "ceo", + "cto", + "eng-1", + "eng-2", + "eng-3", + "eng-4", + "eng-5", + ], plan: { companyAction: "update", agentPlans: [ - { slug: "ceo", action: "create", plannedName: "CEO", existingAgentId: null, reason: null }, - { slug: "cto", action: "update", plannedName: "CTO", existingAgentId: "agent-2", reason: "replace strategy" }, - { slug: "eng-1", action: "skip", plannedName: "Engineer 1", existingAgentId: "agent-3", reason: "skip strategy" }, - { slug: "eng-2", action: "create", plannedName: "Engineer 2", existingAgentId: null, reason: null }, - { slug: "eng-3", action: "create", plannedName: "Engineer 3", existingAgentId: null, reason: null }, - { slug: "eng-4", action: "create", plannedName: "Engineer 4", existingAgentId: null, reason: null }, - { slug: "eng-5", action: "create", plannedName: "Engineer 5", existingAgentId: null, reason: null }, + { + slug: "ceo", + action: "create", + plannedName: "CEO", + existingAgentId: null, + reason: null, + }, + { + slug: "cto", + action: "update", + plannedName: "CTO", + existingAgentId: "agent-2", + reason: "replace strategy", + }, + { + slug: "eng-1", + action: "skip", + plannedName: "Engineer 1", + existingAgentId: "agent-3", + reason: "skip strategy", + }, + { + slug: "eng-2", + action: "create", + plannedName: "Engineer 2", + existingAgentId: null, + reason: null, + }, + { + slug: "eng-3", + action: "create", + plannedName: "Engineer 3", + existingAgentId: null, + reason: null, + }, + { + slug: "eng-4", + action: "create", + plannedName: "Engineer 4", + existingAgentId: null, + reason: null, + }, + { + slug: "eng-5", + action: "create", + plannedName: "Engineer 5", + existingAgentId: null, + reason: null, + }, ], projectPlans: [ - { slug: "alpha", action: "create", plannedName: "Alpha", existingProjectId: null, reason: null }, + { + slug: "alpha", + action: "create", + plannedName: "Alpha", + existingProjectId: null, + reason: null, + }, ], issuePlans: [ - { slug: "kickoff", action: "create", plannedTitle: "Kickoff", reason: null }, + { + slug: "kickoff", + action: "create", + plannedTitle: "Kickoff", + reason: null, + }, ], }, manifest: { @@ -307,14 +368,50 @@ describe("renderCompanyImportResult", () => { action: "updated", }, agents: [ - { slug: "ceo", id: "agent-1", action: "created", name: "CEO", reason: null }, - { slug: "cto", id: "agent-2", action: "updated", name: "CTO", reason: "replace strategy" }, - { slug: "ops", id: null, action: "skipped", name: "Ops", reason: "skip strategy" }, + { + slug: "ceo", + id: "agent-1", + action: "created", + name: "CEO", + reason: null, + }, + { + slug: "cto", + id: "agent-2", + action: "updated", + name: "CTO", + reason: "replace strategy", + }, + { + slug: "ops", + id: null, + action: "skipped", + name: "Ops", + reason: "skip strategy", + }, ], projects: [ - { slug: "app", id: "project-1", action: "created", name: "App", reason: null }, - { slug: "ops", id: "project-2", action: "updated", name: "Operations", reason: "replace strategy" }, - { slug: "archive", id: null, action: "skipped", name: "Archive", reason: "skip strategy" }, + { + slug: "app", + id: "project-1", + action: "created", + name: "App", + reason: null, + }, + { + slug: "ops", + id: "project-2", + action: "updated", + name: "Operations", + reason: "replace strategy", + }, + { + slug: "archive", + id: null, + action: "skipped", + name: "Archive", + reason: "skip strategy", + }, ], envInputs: [], warnings: ["Review API keys"], @@ -328,8 +425,12 @@ describe("renderCompanyImportResult", () => { expect(rendered).toContain("Company"); expect(rendered).toContain("https://taskcore.example/PAP/dashboard"); - expect(rendered).toContain("3 agents total (1 created, 1 updated, 1 skipped)"); - expect(rendered).toContain("3 projects total (1 created, 1 updated, 1 skipped)"); + expect(rendered).toContain( + "3 agents total (1 created, 1 updated, 1 skipped)", + ); + expect(rendered).toContain( + "3 projects total (1 created, 1 updated, 1 skipped)", + ); expect(rendered).toContain("Agent results"); expect(rendered).toContain("Project results"); expect(rendered).toContain("Using claude-local adapter"); @@ -505,8 +606,12 @@ describe("import selection catalog", () => { expect(selectedFiles).toContain(".taskcore.yaml"); expect(selectedFiles).toContain("projects/alpha/PROJECT.md"); expect(selectedFiles).toContain("projects/alpha/notes.md"); - expect(selectedFiles).not.toContain("projects/alpha/issues/kickoff/TASK.md"); - expect(selectedFiles).not.toContain("projects/alpha/issues/kickoff/details.md"); + expect(selectedFiles).not.toContain( + "projects/alpha/issues/kickoff/TASK.md", + ); + expect(selectedFiles).not.toContain( + "projects/alpha/issues/kickoff/details.md", + ); }); }); diff --git a/cli/src/__tests__/data-dir.test.ts b/cli/src/__tests__/data-dir.test.ts index 6134287..4349195 100644 --- a/cli/src/__tests__/data-dir.test.ts +++ b/cli/src/__tests__/data-dir.test.ts @@ -19,11 +19,14 @@ describe("applyDataDirOverride", () => { }); it("sets TASKCORE_HOME and isolated default config/context paths", () => { - const home = applyDataDirOverride({ - dataDir: "~/taskcore-data", - config: undefined, - context: undefined, - }, { hasConfigOption: true, hasContextOption: true }); + const home = applyDataDirOverride( + { + dataDir: "~/taskcore-data", + config: undefined, + context: undefined, + }, + { hasConfigOption: true, hasContextOption: true }, + ); const expectedHome = path.resolve(os.homedir(), "taskcore-data"); expect(home).toBe(expectedHome); @@ -31,17 +34,22 @@ describe("applyDataDirOverride", () => { expect(process.env.TASKCORE_CONFIG).toBe( path.resolve(expectedHome, "instances", "default", "config.json"), ); - expect(process.env.TASKCORE_CONTEXT).toBe(path.resolve(expectedHome, "context.json")); + expect(process.env.TASKCORE_CONTEXT).toBe( + path.resolve(expectedHome, "context.json"), + ); expect(process.env.TASKCORE_INSTANCE_ID).toBe("default"); }); it("uses the provided instance id when deriving default config path", () => { - const home = applyDataDirOverride({ - dataDir: "/tmp/taskcore-alt", - instance: "dev_1", - config: undefined, - context: undefined, - }, { hasConfigOption: true, hasContextOption: true }); + const home = applyDataDirOverride( + { + dataDir: "/tmp/taskcore-alt", + instance: "dev_1", + config: undefined, + context: undefined, + }, + { hasConfigOption: true, hasContextOption: true }, + ); expect(home).toBe(path.resolve("/tmp/taskcore-alt")); expect(process.env.TASKCORE_INSTANCE_ID).toBe("dev_1"); @@ -54,11 +62,14 @@ describe("applyDataDirOverride", () => { process.env.TASKCORE_CONFIG = "/env/config.json"; process.env.TASKCORE_CONTEXT = "/env/context.json"; - applyDataDirOverride({ - dataDir: "/tmp/taskcore-alt", - config: "/flag/config.json", - context: "/flag/context.json", - }, { hasConfigOption: true, hasContextOption: true }); + applyDataDirOverride( + { + dataDir: "/tmp/taskcore-alt", + config: "/flag/config.json", + context: "/flag/context.json", + }, + { hasConfigOption: true, hasContextOption: true }, + ); expect(process.env.TASKCORE_CONFIG).toBe("/env/config.json"); expect(process.env.TASKCORE_CONTEXT).toBe("/env/context.json"); diff --git a/cli/src/__tests__/feedback.test.ts b/cli/src/__tests__/feedback.test.ts index 2ac08d7..9e589ff 100644 --- a/cli/src/__tests__/feedback.test.ts +++ b/cli/src/__tests__/feedback.test.ts @@ -67,10 +67,19 @@ describe("registerFeedbackCommands", () => { expect(() => registerFeedbackCommands(program)).not.toThrow(); - const feedback = program.commands.find((command) => command.name() === "feedback"); + const feedback = program.commands.find( + (command) => command.name() === "feedback", + ); expect(feedback).toBeDefined(); - expect(feedback?.commands.map((command) => command.name())).toEqual(["report", "export"]); - expect(feedback?.commands[0]?.options.filter((option) => option.long === "--company-id")).toHaveLength(1); + expect(feedback?.commands.map((command) => command.name())).toEqual([ + "report", + "export", + ]); + expect( + feedback?.commands[0]?.options.filter( + (option) => option.long === "--company-id", + ), + ).toHaveLength(1); }); }); @@ -128,7 +137,9 @@ describe("renderFeedbackReport", () => { describe("writeFeedbackExportBundle", () => { it("writes votes, traces, a manifest, and a zip archive", async () => { - const tempDir = await mkdtemp(path.join(os.tmpdir(), "taskcore-feedback-export-")); + const tempDir = await mkdtemp( + path.join(os.tmpdir(), "taskcore-feedback-export-"), + ); const outputDir = path.join(tempDir, "feedback-export"); const traces = [ makeTrace(), @@ -158,7 +169,9 @@ describe("writeFeedbackExportBundle", () => { expect(exported.manifest.summary.total).toBe(2); expect(exported.manifest.summary.withReason).toBe(1); - const manifest = JSON.parse(await readFile(path.join(outputDir, "index.json"), "utf8")) as { + const manifest = JSON.parse( + await readFile(path.join(outputDir, "index.json"), "utf8"), + ) as { files: { votes: string[]; traces: string[]; zip: string }; }; expect(manifest.files.votes).toHaveLength(2); diff --git a/cli/src/__tests__/helpers/zip.ts b/cli/src/__tests__/helpers/zip.ts index ef79b5b..a074a0c 100644 --- a/cli/src/__tests__/helpers/zip.ts +++ b/cli/src/__tests__/helpers/zip.ts @@ -21,14 +21,19 @@ function crc32(bytes: Uint8Array) { return (crc ^ 0xffffffff) >>> 0; } -export function createStoredZipArchive(files: Record, rootPath: string) { +export function createStoredZipArchive( + files: Record, + rootPath: string, +) { const encoder = new TextEncoder(); const localChunks: Uint8Array[] = []; const centralChunks: Uint8Array[] = []; let localOffset = 0; let entryCount = 0; - for (const [relativePath, content] of Object.entries(files).sort(([left], [right]) => left.localeCompare(right))) { + for (const [relativePath, content] of Object.entries(files).sort( + ([left], [right]) => left.localeCompare(right), + )) { const fileName = encoder.encode(`${rootPath}/${relativePath}`); const body = encoder.encode(content); const checksum = crc32(body); @@ -63,9 +68,14 @@ export function createStoredZipArchive(files: Record, rootPath: entryCount += 1; } - const centralDirectoryLength = centralChunks.reduce((sum, chunk) => sum + chunk.length, 0); + const centralDirectoryLength = centralChunks.reduce( + (sum, chunk) => sum + chunk.length, + 0, + ); const archive = new Uint8Array( - localChunks.reduce((sum, chunk) => sum + chunk.length, 0) + centralDirectoryLength + 22, + localChunks.reduce((sum, chunk) => sum + chunk.length, 0) + + centralDirectoryLength + + 22, ); let offset = 0; for (const chunk of localChunks) { diff --git a/cli/src/__tests__/home-paths.test.ts b/cli/src/__tests__/home-paths.test.ts index 8c36328..e9aa36c 100644 --- a/cli/src/__tests__/home-paths.test.ts +++ b/cli/src/__tests__/home-paths.test.ts @@ -22,7 +22,15 @@ describe("home path resolution", () => { const paths = describeLocalInstancePaths(); expect(paths.homeDir).toBe(path.resolve(os.homedir(), ".taskcore")); expect(paths.instanceId).toBe("default"); - expect(paths.configPath).toBe(path.resolve(os.homedir(), ".taskcore", "instances", "default", "config.json")); + expect(paths.configPath).toBe( + path.resolve( + os.homedir(), + ".taskcore", + "instances", + "default", + "config.json", + ), + ); }); it("supports TASKCORE_HOME and explicit instance ids", () => { @@ -34,7 +42,9 @@ describe("home path resolution", () => { }); it("rejects invalid instance ids", () => { - expect(() => resolveTaskcoreInstanceId("bad/id")).toThrow(/Invalid instance id/); + expect(() => resolveTaskcoreInstanceId("bad/id")).toThrow( + /Invalid instance id/, + ); }); it("expands ~ prefixes", () => { diff --git a/cli/src/__tests__/http.test.ts b/cli/src/__tests__/http.test.ts index 4309e2d..b4cced6 100644 --- a/cli/src/__tests__/http.test.ts +++ b/cli/src/__tests__/http.test.ts @@ -1,5 +1,9 @@ import { afterEach, describe, expect, it, vi } from "vitest"; -import { ApiConnectionError, ApiRequestError, TaskcoreApiClient } from "../client/http.js"; +import { + ApiConnectionError, + ApiRequestError, + TaskcoreApiClient, +} from "../client/http.js"; describe("TaskcoreApiClient", () => { afterEach(() => { @@ -7,9 +11,11 @@ describe("TaskcoreApiClient", () => { }); it("adds authorization and run-id headers", async () => { - const fetchMock = vi.fn().mockResolvedValue( - new Response(JSON.stringify({ ok: true }), { status: 200 }), - ); + const fetchMock = vi + .fn() + .mockResolvedValue( + new Response(JSON.stringify({ ok: true }), { status: 200 }), + ); vi.stubGlobal("fetch", fetchMock); const client = new TaskcoreApiClient({ @@ -31,9 +37,11 @@ describe("TaskcoreApiClient", () => { }); it("returns null on ignoreNotFound", async () => { - const fetchMock = vi.fn().mockResolvedValue( - new Response(JSON.stringify({ error: "Not found" }), { status: 404 }), - ); + const fetchMock = vi + .fn() + .mockResolvedValue( + new Response(JSON.stringify({ error: "Not found" }), { status: 404 }), + ); vi.stubGlobal("fetch", fetchMock); const client = new TaskcoreApiClient({ apiBase: "http://localhost:3100" }); @@ -42,17 +50,24 @@ describe("TaskcoreApiClient", () => { }); it("throws ApiRequestError with details", async () => { - const fetchMock = vi.fn().mockResolvedValue( - new Response( - JSON.stringify({ error: "Issue checkout conflict", details: { issueId: "1" } }), - { status: 409 }, - ), - ); + const fetchMock = vi + .fn() + .mockResolvedValue( + new Response( + JSON.stringify({ + error: "Issue checkout conflict", + details: { issueId: "1" }, + }), + { status: 409 }, + ), + ); vi.stubGlobal("fetch", fetchMock); const client = new TaskcoreApiClient({ apiBase: "http://localhost:3100" }); - await expect(client.post("/api/issues/1/checkout", {})).rejects.toMatchObject({ + await expect( + client.post("/api/issues/1/checkout", {}), + ).rejects.toMatchObject({ status: 409, message: "Issue checkout conflict", details: { issueId: "1" }, @@ -65,28 +80,38 @@ describe("TaskcoreApiClient", () => { const client = new TaskcoreApiClient({ apiBase: "http://localhost:3100" }); - await expect(client.post("/api/companies/import/preview", {})).rejects.toBeInstanceOf(ApiConnectionError); - await expect(client.post("/api/companies/import/preview", {})).rejects.toMatchObject({ + await expect( + client.post("/api/companies/import/preview", {}), + ).rejects.toBeInstanceOf(ApiConnectionError); + await expect( + client.post("/api/companies/import/preview", {}), + ).rejects.toMatchObject({ url: "http://localhost:3100/api/companies/import/preview", method: "POST", causeMessage: "fetch failed", } satisfies Partial); - await expect(client.post("/api/companies/import/preview", {})).rejects.toThrow( - /Could not reach the Taskcore API\./, - ); - await expect(client.post("/api/companies/import/preview", {})).rejects.toThrow( - /curl http:\/\/localhost:3100\/api\/health/, - ); - await expect(client.post("/api/companies/import/preview", {})).rejects.toThrow( - /pnpm dev|pnpm taskcore run/, - ); + await expect( + client.post("/api/companies/import/preview", {}), + ).rejects.toThrow(/Could not reach the Taskcore API\./); + await expect( + client.post("/api/companies/import/preview", {}), + ).rejects.toThrow(/curl http:\/\/localhost:3100\/api\/health/); + await expect( + client.post("/api/companies/import/preview", {}), + ).rejects.toThrow(/pnpm dev|pnpm taskcore run/); }); it("retries once after interactive auth recovery", async () => { const fetchMock = vi .fn() - .mockResolvedValueOnce(new Response(JSON.stringify({ error: "Board access required" }), { status: 403 })) - .mockResolvedValueOnce(new Response(JSON.stringify({ ok: true }), { status: 200 })); + .mockResolvedValueOnce( + new Response(JSON.stringify({ error: "Board access required" }), { + status: 403, + }), + ) + .mockResolvedValueOnce( + new Response(JSON.stringify({ ok: true }), { status: 200 }), + ); vi.stubGlobal("fetch", fetchMock); const recoverAuth = vi.fn().mockResolvedValue("board-token-123"); @@ -95,12 +120,17 @@ describe("TaskcoreApiClient", () => { recoverAuth, }); - const result = await client.post<{ ok: boolean }>("/api/test", { hello: "world" }); + const result = await client.post<{ ok: boolean }>("/api/test", { + hello: "world", + }); expect(result).toEqual({ ok: true }); expect(recoverAuth).toHaveBeenCalledOnce(); expect(fetchMock).toHaveBeenCalledTimes(2); - const retryHeaders = fetchMock.mock.calls[1]?.[1]?.headers as Record; + const retryHeaders = fetchMock.mock.calls[1]?.[1]?.headers as Record< + string, + string + >; expect(retryHeaders.authorization).toBe("Bearer board-token-123"); }); }); diff --git a/cli/src/__tests__/network-bind.test.ts b/cli/src/__tests__/network-bind.test.ts index 48554b7..76591ba 100644 --- a/cli/src/__tests__/network-bind.test.ts +++ b/cli/src/__tests__/network-bind.test.ts @@ -1,5 +1,8 @@ import { describe, expect, it } from "vitest"; -import { resolveRuntimeBind, validateConfiguredBindMode } from "@taskcore/shared"; +import { + resolveRuntimeBind, + validateConfiguredBindMode, +} from "@taskcore/shared"; import { buildPresetServerConfig } from "../config/server-bind.js"; describe("network bind helpers", () => { @@ -31,7 +34,9 @@ describe("network bind helpers", () => { host: "127.0.0.1", }); - expect(resolved.errors).toContain("server.customBindHost is required when server.bind=custom"); + expect(resolved.errors).toContain( + "server.customBindHost is required when server.bind=custom", + ); }); it("stores the detected tailscale address for tailnet presets", () => { diff --git a/cli/src/__tests__/onboard.test.ts b/cli/src/__tests__/onboard.test.ts index dfd7be0..dd08844 100644 --- a/cli/src/__tests__/onboard.test.ts +++ b/cli/src/__tests__/onboard.test.ts @@ -69,13 +69,17 @@ function createExistingConfigFixture() { }; fs.mkdirSync(path.dirname(configPath), { recursive: true }); - fs.writeFileSync(configPath, `${JSON.stringify(config, null, 2)}\n`, { mode: 0o600 }); + fs.writeFileSync(configPath, `${JSON.stringify(config, null, 2)}\n`, { + mode: 0o600, + }); return { configPath, configText: fs.readFileSync(configPath, "utf8") }; } function createFreshConfigPath() { - const root = fs.mkdtempSync(path.join(os.tmpdir(), "taskcore-onboard-fresh-")); + const root = fs.mkdtempSync( + path.join(os.tmpdir(), "taskcore-onboard-fresh-"), + ); return path.join(root, ".taskcore", "config.json"); } @@ -96,19 +100,31 @@ describe("onboard", () => { await onboard({ config: fixture.configPath }); - expect(fs.readFileSync(fixture.configPath, "utf8")).toBe(fixture.configText); + expect(fs.readFileSync(fixture.configPath, "utf8")).toBe( + fixture.configText, + ); expect(fs.existsSync(`${fixture.configPath}.backup`)).toBe(false); - expect(fs.existsSync(path.join(path.dirname(fixture.configPath), ".env"))).toBe(true); + expect( + fs.existsSync(path.join(path.dirname(fixture.configPath), ".env")), + ).toBe(true); }); it("preserves an existing config when rerun with --yes", async () => { const fixture = createExistingConfigFixture(); - await onboard({ config: fixture.configPath, yes: true, invokedByRun: true }); + await onboard({ + config: fixture.configPath, + yes: true, + invokedByRun: true, + }); - expect(fs.readFileSync(fixture.configPath, "utf8")).toBe(fixture.configText); + expect(fs.readFileSync(fixture.configPath, "utf8")).toBe( + fixture.configText, + ); expect(fs.existsSync(`${fixture.configPath}.backup`)).toBe(false); - expect(fs.existsSync(path.join(path.dirname(fixture.configPath), ".env"))).toBe(true); + expect( + fs.existsSync(path.join(path.dirname(fixture.configPath), ".env")), + ).toBe(true); }); it("keeps --yes onboarding on local trusted loopback defaults", async () => { @@ -118,7 +134,9 @@ describe("onboard", () => { await onboard({ config: configPath, yes: true, invokedByRun: true }); - const raw = JSON.parse(fs.readFileSync(configPath, "utf8")) as TaskcoreConfig; + const raw = JSON.parse( + fs.readFileSync(configPath, "utf8"), + ) as TaskcoreConfig; expect(raw.server.deploymentMode).toBe("local_trusted"); expect(raw.server.exposure).toBe("private"); expect(raw.server.bind).toBe("loopback"); @@ -129,9 +147,16 @@ describe("onboard", () => { const configPath = createFreshConfigPath(); process.env.TASKCORE_TAILNET_BIND_HOST = "100.64.0.8"; - await onboard({ config: configPath, yes: true, invokedByRun: true, bind: "tailnet" }); + await onboard({ + config: configPath, + yes: true, + invokedByRun: true, + bind: "tailnet", + }); - const raw = JSON.parse(fs.readFileSync(configPath, "utf8")) as TaskcoreConfig; + const raw = JSON.parse( + fs.readFileSync(configPath, "utf8"), + ) as TaskcoreConfig; expect(raw.server.deploymentMode).toBe("authenticated"); expect(raw.server.exposure).toBe("private"); expect(raw.server.bind).toBe("tailnet"); @@ -142,9 +167,16 @@ describe("onboard", () => { const configPath = createFreshConfigPath(); delete process.env.TASKCORE_TAILNET_BIND_HOST; - await onboard({ config: configPath, yes: true, invokedByRun: true, bind: "tailnet" }); + await onboard({ + config: configPath, + yes: true, + invokedByRun: true, + bind: "tailnet", + }); - const raw = JSON.parse(fs.readFileSync(configPath, "utf8")) as TaskcoreConfig; + const raw = JSON.parse( + fs.readFileSync(configPath, "utf8"), + ) as TaskcoreConfig; expect(raw.server.deploymentMode).toBe("authenticated"); expect(raw.server.exposure).toBe("private"); expect(raw.server.bind).toBe("tailnet"); @@ -157,7 +189,9 @@ describe("onboard", () => { await onboard({ config: configPath, yes: true, invokedByRun: true }); - const raw = JSON.parse(fs.readFileSync(configPath, "utf8")) as TaskcoreConfig; + const raw = JSON.parse( + fs.readFileSync(configPath, "utf8"), + ) as TaskcoreConfig; expect(raw.server.deploymentMode).toBe("local_trusted"); expect(raw.server.exposure).toBe("private"); expect(raw.server.bind).toBe("loopback"); diff --git a/cli/src/__tests__/routines.test.ts b/cli/src/__tests__/routines.test.ts index e785ddf..56e3164 100644 --- a/cli/src/__tests__/routines.test.ts +++ b/cli/src/__tests__/routines.test.ts @@ -4,13 +4,7 @@ import os from "node:os"; import path from "node:path"; import { afterAll, afterEach, beforeAll, describe, expect, it } from "vitest"; import { eq } from "drizzle-orm"; -import { - agents, - companies, - createDb, - projects, - routines, -} from "@taskcore/db"; +import { agents, companies, createDb, projects, routines } from "@taskcore/db"; import { getEmbeddedPostgresTestSupport, startEmbeddedPostgresTestDatabase, @@ -18,7 +12,9 @@ import { import { disableAllRoutinesInConfig } from "../commands/routines.js"; const embeddedPostgresSupport = await getEmbeddedPostgresTestSupport(); -const describeEmbeddedPostgres = embeddedPostgresSupport.supported ? describe : describe.skip; +const describeEmbeddedPostgres = embeddedPostgresSupport.supported + ? describe + : describe.skip; if (!embeddedPostgresSupport.supported) { console.warn( @@ -26,7 +22,11 @@ if (!embeddedPostgresSupport.supported) { ); } -function writeTestConfig(configPath: string, tempRoot: string, connectionString: string) { +function writeTestConfig( + configPath: string, + tempRoot: string, + connectionString: string, +) { const config = { $meta: { version: 1, @@ -88,14 +88,20 @@ function writeTestConfig(configPath: string, tempRoot: string, connectionString: describeEmbeddedPostgres("disableAllRoutinesInConfig", () => { let db!: ReturnType; - let tempDb: Awaited> | null = null; + let tempDb: Awaited< + ReturnType + > | null = null; let tempRoot = ""; let configPath = ""; beforeAll(async () => { - tempDb = await startEmbeddedPostgresTestDatabase("taskcore-routines-cli-db-"); + tempDb = await startEmbeddedPostgresTestDatabase( + "taskcore-routines-cli-db-", + ); db = createDb(tempDb.connectionString); - tempRoot = mkdtempSync(path.join(os.tmpdir(), "taskcore-routines-cli-config-")); + tempRoot = mkdtempSync( + path.join(os.tmpdir(), "taskcore-routines-cli-config-"), + ); configPath = path.join(tempRoot, "config.json"); writeTestConfig(configPath, tempRoot, tempDb.connectionString); }, 20_000); @@ -232,7 +238,9 @@ describeEmbeddedPostgres("disableAllRoutinesInConfig", () => { }) .from(routines) .where(eq(routines.companyId, companyId)); - const statusById = new Map(companyRoutines.map((routine) => [routine.id, routine.status])); + const statusById = new Map( + companyRoutines.map((routine) => [routine.id, routine.status]), + ); expect(statusById.get(activeRoutineId)).toBe("paused"); expect(statusById.get(pausedRoutineId)).toBe("paused"); diff --git a/cli/src/__tests__/telemetry.test.ts b/cli/src/__tests__/telemetry.test.ts index 5875c65..90864bc 100644 --- a/cli/src/__tests__/telemetry.test.ts +++ b/cli/src/__tests__/telemetry.test.ts @@ -4,67 +4,80 @@ import path from "node:path"; import { afterEach, beforeEach, describe, expect, it, vi } from "vitest"; const ORIGINAL_ENV = { ...process.env }; -const CI_ENV_VARS = ["CI", "CONTINUOUS_INTEGRATION", "BUILD_NUMBER", "GITHUB_ACTIONS", "GITLAB_CI"]; +const CI_ENV_VARS = [ + "CI", + "CONTINUOUS_INTEGRATION", + "BUILD_NUMBER", + "GITHUB_ACTIONS", + "GITLAB_CI", +]; function makeConfigPath(root: string, enabled: boolean): string { const configPath = path.join(root, ".taskcore", "config.json"); fs.mkdirSync(path.dirname(configPath), { recursive: true }); - fs.writeFileSync(configPath, JSON.stringify({ - $meta: { - version: 1, - updatedAt: "2026-03-31T00:00:00.000Z", - source: "configure", - }, - database: { - mode: "embedded-postgres", - embeddedPostgresDataDir: path.join(root, "runtime", "db"), - embeddedPostgresPort: 54329, - backup: { - enabled: true, - intervalMinutes: 60, - retentionDays: 30, - dir: path.join(root, "runtime", "backups"), + fs.writeFileSync( + configPath, + JSON.stringify( + { + $meta: { + version: 1, + updatedAt: "2026-03-31T00:00:00.000Z", + source: "configure", + }, + database: { + mode: "embedded-postgres", + embeddedPostgresDataDir: path.join(root, "runtime", "db"), + embeddedPostgresPort: 54329, + backup: { + enabled: true, + intervalMinutes: 60, + retentionDays: 30, + dir: path.join(root, "runtime", "backups"), + }, + }, + logging: { + mode: "file", + logDir: path.join(root, "runtime", "logs"), + }, + server: { + deploymentMode: "local_trusted", + exposure: "private", + host: "127.0.0.1", + port: 3100, + allowedHostnames: [], + serveUi: true, + }, + auth: { + baseUrlMode: "auto", + disableSignUp: false, + }, + telemetry: { + enabled, + }, + storage: { + provider: "local_disk", + localDisk: { + baseDir: path.join(root, "runtime", "storage"), + }, + s3: { + bucket: "taskcore", + region: "us-east-1", + prefix: "", + forcePathStyle: false, + }, + }, + secrets: { + provider: "local_encrypted", + strictMode: false, + localEncrypted: { + keyFilePath: path.join(root, "runtime", "secrets", "master.key"), + }, + }, }, - }, - logging: { - mode: "file", - logDir: path.join(root, "runtime", "logs"), - }, - server: { - deploymentMode: "local_trusted", - exposure: "private", - host: "127.0.0.1", - port: 3100, - allowedHostnames: [], - serveUi: true, - }, - auth: { - baseUrlMode: "auto", - disableSignUp: false, - }, - telemetry: { - enabled, - }, - storage: { - provider: "local_disk", - localDisk: { - baseDir: path.join(root, "runtime", "storage"), - }, - s3: { - bucket: "taskcore", - region: "us-east-1", - prefix: "", - forcePathStyle: false, - }, - }, - secrets: { - provider: "local_encrypted", - strictMode: false, - localEncrypted: { - keyFilePath: path.join(root, "runtime", "secrets", "master.key"), - }, - }, - }, null, 2)); + null, + 2, + ), + ); return configPath; } @@ -74,7 +87,10 @@ describe("cli telemetry", () => { for (const key of CI_ENV_VARS) { delete process.env[key]; } - vi.stubGlobal("fetch", vi.fn(async () => ({ ok: true }))); + vi.stubGlobal( + "fetch", + vi.fn(async () => ({ ok: true })), + ); }); afterEach(() => { @@ -84,7 +100,9 @@ describe("cli telemetry", () => { }); it("respects telemetry.enabled=false from the config file", async () => { - const root = fs.mkdtempSync(path.join(os.tmpdir(), "taskcore-cli-telemetry-")); + const root = fs.mkdtempSync( + path.join(os.tmpdir(), "taskcore-cli-telemetry-"), + ); const configPath = makeConfigPath(root, false); process.env.TASKCORE_HOME = path.join(root, "home"); process.env.TASKCORE_INSTANCE_ID = "telemetry-test"; @@ -93,17 +111,37 @@ describe("cli telemetry", () => { const client = initTelemetryFromConfigFile(configPath); expect(client).toBeNull(); - expect(fs.existsSync(path.join(root, "home", "instances", "telemetry-test", "telemetry", "state.json"))).toBe(false); + expect( + fs.existsSync( + path.join( + root, + "home", + "instances", + "telemetry-test", + "telemetry", + "state.json", + ), + ), + ).toBe(false); }); it("creates telemetry state only after the first event is tracked", async () => { - const root = fs.mkdtempSync(path.join(os.tmpdir(), "taskcore-cli-telemetry-")); + const root = fs.mkdtempSync( + path.join(os.tmpdir(), "taskcore-cli-telemetry-"), + ); process.env.TASKCORE_HOME = path.join(root, "home"); process.env.TASKCORE_INSTANCE_ID = "telemetry-test"; const { initTelemetry, flushTelemetry } = await import("../telemetry.js"); const client = initTelemetry({ enabled: true }); - const statePath = path.join(root, "home", "instances", "telemetry-test", "telemetry", "state.json"); + const statePath = path.join( + root, + "home", + "instances", + "telemetry-test", + "telemetry", + "state.json", + ); expect(client).not.toBeNull(); expect(fs.existsSync(statePath)).toBe(false); diff --git a/cli/src/__tests__/worktree-merge-history.test.ts b/cli/src/__tests__/worktree-merge-history.test.ts index de14549..4580c34 100644 --- a/cli/src/__tests__/worktree-merge-history.test.ts +++ b/cli/src/__tests__/worktree-merge-history.test.ts @@ -1,5 +1,8 @@ import { describe, expect, it } from "vitest"; -import { buildWorktreeMergePlan, parseWorktreeMergeScopes } from "../commands/worktree-merge-history-lib.js"; +import { + buildWorktreeMergePlan, + parseWorktreeMergeScopes, +} from "../commands/worktree-merge-history-lib.js"; function makeIssue(overrides: Record = {}) { return { @@ -168,7 +171,11 @@ describe("worktree merge history planner", () => { }); it("dedupes nested worktree issues by preserved source uuid", () => { - const sharedIssue = makeIssue({ id: "issue-a", identifier: "PAP-10", title: "Shared" }); + const sharedIssue = makeIssue({ + id: "issue-a", + identifier: "PAP-10", + title: "Shared", + }); const branchOneIssue = makeIssue({ id: "issue-b", identifier: "PAP-22", @@ -199,8 +206,16 @@ describe("worktree merge history planner", () => { }); expect(plan.counts.issuesToInsert).toBe(1); - expect(plan.issuePlans.filter((item) => item.action === "insert").map((item) => item.source.id)).toEqual(["issue-c"]); - expect(plan.issuePlans.find((item) => item.source.id === "issue-c" && item.action === "insert")).toMatchObject({ + expect( + plan.issuePlans + .filter((item) => item.action === "insert") + .map((item) => item.source.id), + ).toEqual(["issue-c"]); + expect( + plan.issuePlans.find( + (item) => item.source.id === "issue-c" && item.action === "insert", + ), + ).toMatchObject({ previewIdentifier: "PAP-501", }); }); @@ -266,7 +281,13 @@ describe("worktree merge history planner", () => { sourceComments: [], targetComments: [], targetAgents: [], - targetProjects: [{ id: "target-project-1", name: "Mapped project", status: "in_progress" }] as any, + targetProjects: [ + { + id: "target-project-1", + name: "Mapped project", + status: "in_progress", + }, + ] as any, targetProjectWorkspaces: [], targetGoals: [{ id: "goal-1" }] as any, projectIdOverrides: { @@ -343,8 +364,14 @@ describe("worktree merge history planner", () => { identifier: "PAP-11", createdAt: new Date("2026-03-20T01:00:00.000Z"), }); - const existingComment = makeComment({ id: "comment-existing", issueId: "issue-a" }); - const sharedIssueComment = makeComment({ id: "comment-shared", issueId: "issue-a" }); + const existingComment = makeComment({ + id: "comment-existing", + issueId: "issue-a", + }); + const sharedIssueComment = makeComment({ + id: "comment-shared", + issueId: "issue-a", + }); const newIssueComment = makeComment({ id: "comment-new-issue", issueId: "issue-b", @@ -370,10 +397,11 @@ describe("worktree merge history planner", () => { expect(plan.counts.commentsToInsert).toBe(2); expect(plan.counts.commentsExisting).toBe(1); - expect(plan.commentPlans.filter((item) => item.action === "insert").map((item) => item.source.id)).toEqual([ - "comment-shared", - "comment-new-issue", - ]); + expect( + plan.commentPlans + .filter((item) => item.action === "insert") + .map((item) => item.source.id), + ).toEqual(["comment-shared", "comment-new-issue"]); expect(plan.adjustments.clear_author_agent).toBe(1); }); @@ -397,7 +425,10 @@ describe("worktree merge history planner", () => { documentUpdatedAt: new Date("2026-03-20T01:00:00.000Z"), linkUpdatedAt: new Date("2026-03-20T01:00:00.000Z"), }); - const sourceRevisionOne = makeDocumentRevision({ documentId: "document-a", id: "revision-1" }); + const sourceRevisionOne = makeDocumentRevision({ + documentId: "document-a", + id: "revision-1", + }); const sourceRevisionTwo = makeDocumentRevision({ documentId: "document-a", id: "revision-branch-2", @@ -405,7 +436,10 @@ describe("worktree merge history planner", () => { body: "# Branch plan", createdAt: new Date("2026-03-20T02:00:00.000Z"), }); - const targetRevisionOne = makeDocumentRevision({ documentId: "document-a", id: "revision-1" }); + const targetRevisionOne = makeDocumentRevision({ + documentId: "document-a", + id: "revision-1", + }); const targetRevisionTwo = makeDocumentRevision({ documentId: "document-a", id: "revision-main-2", diff --git a/cli/src/__tests__/worktree.test.ts b/cli/src/__tests__/worktree.test.ts index eee4bf7..69eee64 100644 --- a/cli/src/__tests__/worktree.test.ts +++ b/cli/src/__tests__/worktree.test.ts @@ -47,7 +47,9 @@ import { const ORIGINAL_CWD = process.cwd(); const ORIGINAL_ENV = { ...process.env }; const embeddedPostgresSupport = await getEmbeddedPostgresTestSupport(); -const describeEmbeddedPostgres = embeddedPostgresSupport.supported ? describe : describe.skip; +const describeEmbeddedPostgres = embeddedPostgresSupport.supported + ? describe + : describe.skip; if (!embeddedPostgresSupport.supported) { console.warn( @@ -128,7 +130,9 @@ function buildSourceConfig(): TaskcoreConfig { describe("worktree helpers", () => { it("sanitizes instance ids", () => { - expect(sanitizeWorktreeInstanceId("feature/worktree-support")).toBe("feature-worktree-support"); + expect(sanitizeWorktreeInstanceId("feature/worktree-support")).toBe( + "feature-worktree-support", + ); expect(sanitizeWorktreeInstanceId(" ")).toBe("worktree"); }); @@ -151,7 +155,14 @@ describe("worktree helpers", () => { targetPath: "/tmp/feature-branch", branchExists: false, }), - ).toEqual(["worktree", "add", "-b", "feature-branch", "/tmp/feature-branch", "HEAD"]); + ).toEqual([ + "worktree", + "add", + "-b", + "feature-branch", + "/tmp/feature-branch", + "HEAD", + ]); expect( resolveGitWorktreeAddArgs({ @@ -170,7 +181,14 @@ describe("worktree helpers", () => { branchExists: false, startPoint: "public-gh/master", }), - ).toEqual(["worktree", "add", "-b", "my-worktree", "/tmp/my-worktree", "public-gh/master"]); + ).toEqual([ + "worktree", + "add", + "-b", + "my-worktree", + "/tmp/my-worktree", + "public-gh/master", + ]); }); it("uses start point even when a local branch with the same name exists", () => { @@ -181,12 +199,23 @@ describe("worktree helpers", () => { branchExists: true, startPoint: "origin/main", }), - ).toEqual(["worktree", "add", "-b", "my-worktree", "/tmp/my-worktree", "origin/main"]); + ).toEqual([ + "worktree", + "add", + "-b", + "my-worktree", + "/tmp/my-worktree", + "origin/main", + ]); }); it("rewrites loopback auth URLs to the new port only", () => { - expect(rewriteLocalUrlPort("http://127.0.0.1:3100", 3110)).toBe("http://127.0.0.1:3110/"); - expect(rewriteLocalUrlPort("https://taskcore.example", 3110)).toBe("https://taskcore.example"); + expect(rewriteLocalUrlPort("http://127.0.0.1:3100", 3110)).toBe( + "http://127.0.0.1:3110/", + ); + expect(rewriteLocalUrlPort("https://taskcore.example", 3110)).toBe( + "https://taskcore.example", + ); }); it("builds isolated config and env paths for a worktree", () => { @@ -204,13 +233,24 @@ describe("worktree helpers", () => { }); expect(config.database.embeddedPostgresDataDir).toBe( - path.resolve("/tmp/taskcore-worktrees", "instances", "feature-worktree-support", "db"), + path.resolve( + "/tmp/taskcore-worktrees", + "instances", + "feature-worktree-support", + "db", + ), ); expect(config.database.embeddedPostgresPort).toBe(54339); expect(config.server.port).toBe(3110); expect(config.auth.publicBaseUrl).toBe("http://127.0.0.1:3110/"); expect(config.storage.localDisk.baseDir).toBe( - path.resolve("/tmp/taskcore-worktrees", "instances", "feature-worktree-support", "data", "storage"), + path.resolve( + "/tmp/taskcore-worktrees", + "instances", + "feature-worktree-support", + "data", + "storage", + ), ); const env = buildWorktreeEnvEntries(paths, { @@ -222,7 +262,9 @@ describe("worktree helpers", () => { expect(env.TASKCORE_IN_WORKTREE).toBe("true"); expect(env.TASKCORE_WORKTREE_NAME).toBe("feature-worktree-support"); expect(env.TASKCORE_WORKTREE_COLOR).toBe("#3abf7a"); - expect(formatShellExports(env)).toContain("export TASKCORE_INSTANCE_ID='feature-worktree-support'"); + expect(formatShellExports(env)).toContain( + "export TASKCORE_INSTANCE_ID='feature-worktree-support'", + ); }); it("falls back across storage roots before skipping a missing attachment object", async () => { @@ -253,7 +295,11 @@ describe("worktree helpers", () => { getObject: vi.fn().mockRejectedValue(missingErr), }, { - getObject: vi.fn().mockRejectedValue(Object.assign(new Error("missing"), { status: 404 })), + getObject: vi + .fn() + .mockRejectedValue( + Object.assign(new Error("missing"), { status: 404 }), + ), }, ], "company-1", @@ -274,22 +320,37 @@ describe("worktree helpers", () => { expect(minimal.excludedTables).toContain("heartbeat_run_events"); expect(minimal.excludedTables).toContain("workspace_runtime_services"); expect(minimal.excludedTables).toContain("agent_task_sessions"); - expect(minimal.nullifyColumns.issues).toEqual(["checkout_run_id", "execution_run_id"]); + expect(minimal.nullifyColumns.issues).toEqual([ + "checkout_run_id", + "execution_run_id", + ]); expect(full.excludedTables).toEqual([]); expect(full.nullifyColumns).toEqual({}); }); it("copies the source local_encrypted secrets key into the seeded worktree instance", () => { - const tempRoot = fs.mkdtempSync(path.join(os.tmpdir(), "taskcore-worktree-secrets-")); + const tempRoot = fs.mkdtempSync( + path.join(os.tmpdir(), "taskcore-worktree-secrets-"), + ); const originalInlineMasterKey = process.env.TASKCORE_SECRETS_MASTER_KEY; const originalKeyFile = process.env.TASKCORE_SECRETS_MASTER_KEY_FILE; try { delete process.env.TASKCORE_SECRETS_MASTER_KEY; delete process.env.TASKCORE_SECRETS_MASTER_KEY_FILE; const sourceConfigPath = path.join(tempRoot, "source", "config.json"); - const sourceKeyPath = path.join(tempRoot, "source", "secrets", "master.key"); - const targetKeyPath = path.join(tempRoot, "target", "secrets", "master.key"); + const sourceKeyPath = path.join( + tempRoot, + "source", + "secrets", + "master.key", + ); + const targetKeyPath = path.join( + tempRoot, + "target", + "secrets", + "master.key", + ); fs.mkdirSync(path.dirname(sourceKeyPath), { recursive: true }); fs.writeFileSync(sourceKeyPath, "source-master-key", "utf8"); @@ -320,10 +381,17 @@ describe("worktree helpers", () => { }); it("writes the source inline secrets master key into the seeded worktree instance", () => { - const tempRoot = fs.mkdtempSync(path.join(os.tmpdir(), "taskcore-worktree-secrets-")); + const tempRoot = fs.mkdtempSync( + path.join(os.tmpdir(), "taskcore-worktree-secrets-"), + ); try { const sourceConfigPath = path.join(tempRoot, "source", "config.json"); - const targetKeyPath = path.join(tempRoot, "target", "secrets", "master.key"); + const targetKeyPath = path.join( + tempRoot, + "target", + "secrets", + "master.key", + ); copySeededSecretsKey({ sourceConfigPath, @@ -334,14 +402,18 @@ describe("worktree helpers", () => { targetKeyFilePath: targetKeyPath, }); - expect(fs.readFileSync(targetKeyPath, "utf8")).toBe("inline-source-master-key"); + expect(fs.readFileSync(targetKeyPath, "utf8")).toBe( + "inline-source-master-key", + ); } finally { fs.rmSync(tempRoot, { recursive: true, force: true }); } }); it("persists the current agent jwt secret into the worktree env file", async () => { - const tempRoot = fs.mkdtempSync(path.join(os.tmpdir(), "taskcore-worktree-jwt-")); + const tempRoot = fs.mkdtempSync( + path.join(os.tmpdir(), "taskcore-worktree-jwt-"), + ); const repoRoot = path.join(tempRoot, "repo"); const originalCwd = process.cwd(); const originalJwtSecret = process.env.TASKCORE_AGENT_JWT_SECRET; @@ -359,7 +431,9 @@ describe("worktree helpers", () => { const envPath = path.join(repoRoot, ".taskcore", ".env"); const envContents = fs.readFileSync(envPath, "utf8"); - expect(envContents).toContain("TASKCORE_AGENT_JWT_SECRET=worktree-shared-secret"); + expect(envContents).toContain( + "TASKCORE_AGENT_JWT_SECRET=worktree-shared-secret", + ); expect(envContents).toContain("TASKCORE_WORKTREE_NAME=repo"); expect(envContents).toMatch(/TASKCORE_WORKTREE_COLOR=\"#[0-9a-f]{6}\"/); } finally { @@ -374,10 +448,16 @@ describe("worktree helpers", () => { }); it("avoids ports already claimed by sibling worktree instance configs", async () => { - const tempRoot = fs.mkdtempSync(path.join(os.tmpdir(), "taskcore-worktree-claimed-ports-")); + const tempRoot = fs.mkdtempSync( + path.join(os.tmpdir(), "taskcore-worktree-claimed-ports-"), + ); const repoRoot = path.join(tempRoot, "repo"); const homeDir = path.join(tempRoot, ".taskcore-worktrees"); - const siblingInstanceRoot = path.join(homeDir, "instances", "existing-worktree"); + const siblingInstanceRoot = path.join( + homeDir, + "instances", + "existing-worktree", + ); const originalCwd = process.cwd(); try { @@ -427,7 +507,11 @@ describe("worktree helpers", () => { provider: "local_encrypted", strictMode: false, localEncrypted: { - keyFilePath: path.join(siblingInstanceRoot, "secrets", "master.key"), + keyFilePath: path.join( + siblingInstanceRoot, + "secrets", + "master.key", + ), }, }, }, @@ -443,7 +527,12 @@ describe("worktree helpers", () => { home: homeDir, }); - const config = JSON.parse(fs.readFileSync(path.join(repoRoot, ".taskcore", "config.json"), "utf8")); + const config = JSON.parse( + fs.readFileSync( + path.join(repoRoot, ".taskcore", "config.json"), + "utf8", + ), + ); expect(config.server.port).toBeGreaterThan(3101); expect(config.database.embeddedPostgresPort).not.toBe(54330); expect(config.database.embeddedPostgresPort).not.toBe(config.server.port); @@ -455,7 +544,9 @@ describe("worktree helpers", () => { }); it("defaults the seed source config to the current repo-local Taskcore config", () => { - const tempRoot = fs.mkdtempSync(path.join(os.tmpdir(), "taskcore-worktree-source-config-")); + const tempRoot = fs.mkdtempSync( + path.join(os.tmpdir(), "taskcore-worktree-source-config-"), + ); const repoRoot = path.join(tempRoot, "repo"); const localConfigPath = path.join(repoRoot, ".taskcore", "config.json"); const originalCwd = process.cwd(); @@ -463,11 +554,17 @@ describe("worktree helpers", () => { try { fs.mkdirSync(path.dirname(localConfigPath), { recursive: true }); - fs.writeFileSync(localConfigPath, JSON.stringify(buildSourceConfig()), "utf8"); + fs.writeFileSync( + localConfigPath, + JSON.stringify(buildSourceConfig()), + "utf8", + ); delete process.env.TASKCORE_CONFIG; process.chdir(repoRoot); - expect(fs.realpathSync(resolveSourceConfigPath({}))).toBe(fs.realpathSync(localConfigPath)); + expect(fs.realpathSync(resolveSourceConfigPath({}))).toBe( + fs.realpathSync(localConfigPath), + ); } finally { process.chdir(originalCwd); if (originalTaskcoreConfig === undefined) { @@ -480,7 +577,9 @@ describe("worktree helpers", () => { }); it("preserves the source config path across worktree:make cwd changes", () => { - const tempRoot = fs.mkdtempSync(path.join(os.tmpdir(), "taskcore-worktree-source-override-")); + const tempRoot = fs.mkdtempSync( + path.join(os.tmpdir(), "taskcore-worktree-source-override-"), + ); const sourceConfigPath = path.join(tempRoot, "source", "config.json"); const targetRoot = path.join(tempRoot, "target"); const originalCwd = process.cwd(); @@ -489,13 +588,17 @@ describe("worktree helpers", () => { try { fs.mkdirSync(path.dirname(sourceConfigPath), { recursive: true }); fs.mkdirSync(targetRoot, { recursive: true }); - fs.writeFileSync(sourceConfigPath, JSON.stringify(buildSourceConfig()), "utf8"); + fs.writeFileSync( + sourceConfigPath, + JSON.stringify(buildSourceConfig()), + "utf8", + ); delete process.env.TASKCORE_CONFIG; process.chdir(targetRoot); - expect(resolveSourceConfigPath({ sourceConfigPathOverride: sourceConfigPath })).toBe( - path.resolve(sourceConfigPath), - ); + expect( + resolveSourceConfigPath({ sourceConfigPathOverride: sourceConfigPath }), + ).toBe(path.resolve(sourceConfigPath)); } finally { process.chdir(originalCwd); if (originalTaskcoreConfig === undefined) { @@ -514,16 +617,20 @@ describe("worktree helpers", () => { }); it("rejects mixed reseed source selectors", () => { - expect(() => resolveWorktreeReseedSource({ - from: "current", - fromInstance: "default", - })).toThrow( + expect(() => + resolveWorktreeReseedSource({ + from: "current", + fromInstance: "default", + }), + ).toThrow( "Use either --from or --from-config/--from-data-dir/--from-instance, not both.", ); }); it("derives worktree reseed target paths from the adjacent env file", () => { - const tempRoot = fs.mkdtempSync(path.join(os.tmpdir(), "taskcore-worktree-reseed-target-")); + const tempRoot = fs.mkdtempSync( + path.join(os.tmpdir(), "taskcore-worktree-reseed-target-"), + ); const worktreeRoot = path.join(tempRoot, "repo"); const configPath = path.join(worktreeRoot, ".taskcore", "config.json"); const envPath = path.join(worktreeRoot, ".taskcore", ".env"); @@ -555,27 +662,36 @@ describe("worktree helpers", () => { }); it("rejects reseed targets without worktree env metadata", () => { - const tempRoot = fs.mkdtempSync(path.join(os.tmpdir(), "taskcore-worktree-reseed-target-missing-")); + const tempRoot = fs.mkdtempSync( + path.join(os.tmpdir(), "taskcore-worktree-reseed-target-missing-"), + ); const worktreeRoot = path.join(tempRoot, "repo"); const configPath = path.join(worktreeRoot, ".taskcore", "config.json"); try { fs.mkdirSync(path.dirname(configPath), { recursive: true }); fs.writeFileSync(configPath, JSON.stringify(buildSourceConfig()), "utf8"); - fs.writeFileSync(path.join(worktreeRoot, ".taskcore", ".env"), "", "utf8"); + fs.writeFileSync( + path.join(worktreeRoot, ".taskcore", ".env"), + "", + "utf8", + ); expect(() => resolveWorktreeReseedTargetPaths({ configPath, rootPath: worktreeRoot, - })).toThrow("does not look like a worktree-local Taskcore instance"); + }), + ).toThrow("does not look like a worktree-local Taskcore instance"); } finally { fs.rmSync(tempRoot, { recursive: true, force: true }); } }); it("reseed preserves the current worktree ports, instance id, and branding", async () => { - const tempRoot = fs.mkdtempSync(path.join(os.tmpdir(), "taskcore-worktree-reseed-")); + const tempRoot = fs.mkdtempSync( + path.join(os.tmpdir(), "taskcore-worktree-reseed-"), + ); const repoRoot = path.join(tempRoot, "repo"); const sourceRoot = path.join(tempRoot, "source"); const homeDir = path.join(tempRoot, ".taskcore-worktrees"); @@ -596,7 +712,9 @@ describe("worktree helpers", () => { try { fs.mkdirSync(path.dirname(currentPaths.configPath), { recursive: true }); fs.mkdirSync(path.dirname(sourcePaths.configPath), { recursive: true }); - fs.mkdirSync(path.dirname(sourcePaths.secretsKeyFilePath), { recursive: true }); + fs.mkdirSync(path.dirname(sourcePaths.secretsKeyFilePath), { + recursive: true, + }); fs.mkdirSync(repoRoot, { recursive: true }); fs.mkdirSync(sourceRoot, { recursive: true }); @@ -612,8 +730,16 @@ describe("worktree helpers", () => { serverPort: 3200, databasePort: 54400, }); - fs.writeFileSync(currentPaths.configPath, JSON.stringify(currentConfig, null, 2), "utf8"); - fs.writeFileSync(sourcePaths.configPath, JSON.stringify(sourceConfig, null, 2), "utf8"); + fs.writeFileSync( + currentPaths.configPath, + JSON.stringify(currentConfig, null, 2), + "utf8", + ); + fs.writeFileSync( + sourcePaths.configPath, + JSON.stringify(sourceConfig, null, 2), + "utf8", + ); fs.writeFileSync(sourcePaths.secretsKeyFilePath, "source-secret", "utf8"); fs.writeFileSync( currentPaths.envPath, @@ -621,7 +747,7 @@ describe("worktree helpers", () => { `TASKCORE_HOME=${homeDir}`, `TASKCORE_INSTANCE_ID=${currentInstanceId}`, "TASKCORE_WORKTREE_NAME=existing-name", - "TASKCORE_WORKTREE_COLOR=\"#112233\"", + 'TASKCORE_WORKTREE_COLOR="#112233"', ].join("\n"), "utf8", ); @@ -634,15 +760,21 @@ describe("worktree helpers", () => { yes: true, }); - const rewrittenConfig = JSON.parse(fs.readFileSync(currentPaths.configPath, "utf8")); + const rewrittenConfig = JSON.parse( + fs.readFileSync(currentPaths.configPath, "utf8"), + ); const rewrittenEnv = fs.readFileSync(currentPaths.envPath, "utf8"); expect(rewrittenConfig.server.port).toBe(3114); expect(rewrittenConfig.database.embeddedPostgresPort).toBe(54341); - expect(rewrittenConfig.database.embeddedPostgresDataDir).toBe(currentPaths.embeddedPostgresDataDir); - expect(rewrittenEnv).toContain(`TASKCORE_INSTANCE_ID=${currentInstanceId}`); + expect(rewrittenConfig.database.embeddedPostgresDataDir).toBe( + currentPaths.embeddedPostgresDataDir, + ); + expect(rewrittenEnv).toContain( + `TASKCORE_INSTANCE_ID=${currentInstanceId}`, + ); expect(rewrittenEnv).toContain("TASKCORE_WORKTREE_NAME=existing-name"); - expect(rewrittenEnv).toContain("TASKCORE_WORKTREE_COLOR=\"#112233\""); + expect(rewrittenEnv).toContain('TASKCORE_WORKTREE_COLOR="#112233"'); } finally { process.chdir(originalCwd); if (originalTaskcoreConfig === undefined) { @@ -655,7 +787,9 @@ describe("worktree helpers", () => { }, 20_000); it("restores the current worktree config and instance data if reseed fails", async () => { - const tempRoot = fs.mkdtempSync(path.join(os.tmpdir(), "taskcore-worktree-reseed-rollback-")); + const tempRoot = fs.mkdtempSync( + path.join(os.tmpdir(), "taskcore-worktree-reseed-rollback-"), + ); const repoRoot = path.join(tempRoot, "repo"); const sourceRoot = path.join(tempRoot, "source"); const homeDir = path.join(tempRoot, ".taskcore-worktrees"); @@ -677,7 +811,9 @@ describe("worktree helpers", () => { fs.mkdirSync(path.dirname(currentPaths.configPath), { recursive: true }); fs.mkdirSync(path.dirname(sourcePaths.configPath), { recursive: true }); fs.mkdirSync(currentPaths.instanceRoot, { recursive: true }); - fs.mkdirSync(path.dirname(sourcePaths.secretsKeyFilePath), { recursive: true }); + fs.mkdirSync(path.dirname(sourcePaths.secretsKeyFilePath), { + recursive: true, + }); fs.mkdirSync(repoRoot, { recursive: true }); fs.mkdirSync(sourceRoot, { recursive: true }); @@ -702,27 +838,54 @@ describe("worktree helpers", () => { }, } as TaskcoreConfig; - fs.writeFileSync(currentPaths.configPath, JSON.stringify(currentConfig, null, 2), "utf8"); - fs.writeFileSync(currentPaths.envPath, `TASKCORE_HOME=${homeDir}\nTASKCORE_INSTANCE_ID=${currentInstanceId}\n`, "utf8"); - fs.writeFileSync(path.join(currentPaths.instanceRoot, "marker.txt"), "keep me", "utf8"); - fs.writeFileSync(sourcePaths.configPath, JSON.stringify(sourceConfig, null, 2), "utf8"); + fs.writeFileSync( + currentPaths.configPath, + JSON.stringify(currentConfig, null, 2), + "utf8", + ); + fs.writeFileSync( + currentPaths.envPath, + `TASKCORE_HOME=${homeDir}\nTASKCORE_INSTANCE_ID=${currentInstanceId}\n`, + "utf8", + ); + fs.writeFileSync( + path.join(currentPaths.instanceRoot, "marker.txt"), + "keep me", + "utf8", + ); + fs.writeFileSync( + sourcePaths.configPath, + JSON.stringify(sourceConfig, null, 2), + "utf8", + ); fs.writeFileSync(sourcePaths.secretsKeyFilePath, "source-secret", "utf8"); delete process.env.TASKCORE_CONFIG; process.chdir(repoRoot); - await expect(worktreeReseedCommand({ - fromConfig: sourcePaths.configPath, - yes: true, - })).rejects.toThrow("Source instance uses postgres mode but has no connection string"); + await expect( + worktreeReseedCommand({ + fromConfig: sourcePaths.configPath, + yes: true, + }), + ).rejects.toThrow( + "Source instance uses postgres mode but has no connection string", + ); - const restoredConfig = JSON.parse(fs.readFileSync(currentPaths.configPath, "utf8")); + const restoredConfig = JSON.parse( + fs.readFileSync(currentPaths.configPath, "utf8"), + ); const restoredEnv = fs.readFileSync(currentPaths.envPath, "utf8"); - const restoredMarker = fs.readFileSync(path.join(currentPaths.instanceRoot, "marker.txt"), "utf8"); + const restoredMarker = fs.readFileSync( + path.join(currentPaths.instanceRoot, "marker.txt"), + "utf8", + ); expect(restoredConfig.server.port).toBe(3114); expect(restoredConfig.database.embeddedPostgresPort).toBe(54341); - expect(restoredEnv).toContain(`TASKCORE_INSTANCE_ID=${currentInstanceId}`); + expect(restoredEnv).toContain( + `TASKCORE_INSTANCE_ID=${currentInstanceId}`, + ); expect(restoredMarker).toBe("keep me"); } finally { process.chdir(originalCwd); @@ -764,27 +927,50 @@ describe("worktree helpers", () => { }); it("copies shared git hooks into a linked worktree git dir", () => { - const tempRoot = fs.mkdtempSync(path.join(os.tmpdir(), "taskcore-worktree-hooks-")); + const tempRoot = fs.mkdtempSync( + path.join(os.tmpdir(), "taskcore-worktree-hooks-"), + ); const repoRoot = path.join(tempRoot, "repo"); const worktreePath = path.join(tempRoot, "repo-feature"); try { fs.mkdirSync(repoRoot, { recursive: true }); execFileSync("git", ["init"], { cwd: repoRoot, stdio: "ignore" }); - execFileSync("git", ["config", "user.email", "test@example.com"], { cwd: repoRoot, stdio: "ignore" }); - execFileSync("git", ["config", "user.name", "Test User"], { cwd: repoRoot, stdio: "ignore" }); + execFileSync("git", ["config", "user.email", "test@example.com"], { + cwd: repoRoot, + stdio: "ignore", + }); + execFileSync("git", ["config", "user.name", "Test User"], { + cwd: repoRoot, + stdio: "ignore", + }); fs.writeFileSync(path.join(repoRoot, "README.md"), "# temp\n", "utf8"); - execFileSync("git", ["add", "README.md"], { cwd: repoRoot, stdio: "ignore" }); - execFileSync("git", ["commit", "-m", "Initial commit"], { cwd: repoRoot, stdio: "ignore" }); + execFileSync("git", ["add", "README.md"], { + cwd: repoRoot, + stdio: "ignore", + }); + execFileSync("git", ["commit", "-m", "Initial commit"], { + cwd: repoRoot, + stdio: "ignore", + }); const sourceHooksDir = path.join(repoRoot, ".git", "hooks"); const sourceHookPath = path.join(sourceHooksDir, "pre-commit"); - const sourceTokensPath = path.join(sourceHooksDir, "forbidden-tokens.txt"); - fs.writeFileSync(sourceHookPath, "#!/usr/bin/env bash\nexit 0\n", { encoding: "utf8", mode: 0o755 }); + const sourceTokensPath = path.join( + sourceHooksDir, + "forbidden-tokens.txt", + ); + fs.writeFileSync(sourceHookPath, "#!/usr/bin/env bash\nexit 0\n", { + encoding: "utf8", + mode: 0o755, + }); fs.chmodSync(sourceHookPath, 0o755); fs.writeFileSync(sourceTokensPath, "secret-token\n", "utf8"); - execFileSync("git", ["worktree", "add", "--detach", worktreePath], { cwd: repoRoot, stdio: "ignore" }); + execFileSync("git", ["worktree", "add", "--detach", worktreePath], { + cwd: repoRoot, + stdio: "ignore", + }); const copied = copyGitHooksToWorktreeGitDir(worktreePath); const worktreeGitDir = execFileSync("git", ["rev-parse", "--git-dir"], { @@ -793,26 +979,38 @@ describe("worktree helpers", () => { stdio: ["ignore", "pipe", "ignore"], }).trim(); const resolvedSourceHooksDir = fs.realpathSync(sourceHooksDir); - const resolvedTargetHooksDir = fs.realpathSync(path.resolve(worktreePath, worktreeGitDir, "hooks")); + const resolvedTargetHooksDir = fs.realpathSync( + path.resolve(worktreePath, worktreeGitDir, "hooks"), + ); const targetHookPath = path.join(resolvedTargetHooksDir, "pre-commit"); - const targetTokensPath = path.join(resolvedTargetHooksDir, "forbidden-tokens.txt"); + const targetTokensPath = path.join( + resolvedTargetHooksDir, + "forbidden-tokens.txt", + ); expect(copied).toMatchObject({ sourceHooksPath: resolvedSourceHooksDir, targetHooksPath: resolvedTargetHooksDir, copied: true, }); - expect(fs.readFileSync(targetHookPath, "utf8")).toBe("#!/usr/bin/env bash\nexit 0\n"); + expect(fs.readFileSync(targetHookPath, "utf8")).toBe( + "#!/usr/bin/env bash\nexit 0\n", + ); expect(fs.statSync(targetHookPath).mode & 0o111).not.toBe(0); expect(fs.readFileSync(targetTokensPath, "utf8")).toBe("secret-token\n"); } finally { - execFileSync("git", ["worktree", "remove", "--force", worktreePath], { cwd: repoRoot, stdio: "ignore" }); + execFileSync("git", ["worktree", "remove", "--force", worktreePath], { + cwd: repoRoot, + stdio: "ignore", + }); fs.rmSync(tempRoot, { recursive: true, force: true }); } }); it("creates and initializes a worktree from the top-level worktree:make command", async () => { - const tempRoot = fs.mkdtempSync(path.join(os.tmpdir(), "taskcore-worktree-make-")); + const tempRoot = fs.mkdtempSync( + path.join(os.tmpdir(), "taskcore-worktree-make-"), + ); const repoRoot = path.join(tempRoot, "repo"); const fakeHome = path.join(tempRoot, "home"); const worktreePath = path.join(fakeHome, "taskcore-make-test"); @@ -823,11 +1021,23 @@ describe("worktree helpers", () => { fs.mkdirSync(repoRoot, { recursive: true }); fs.mkdirSync(fakeHome, { recursive: true }); execFileSync("git", ["init"], { cwd: repoRoot, stdio: "ignore" }); - execFileSync("git", ["config", "user.email", "test@example.com"], { cwd: repoRoot, stdio: "ignore" }); - execFileSync("git", ["config", "user.name", "Test User"], { cwd: repoRoot, stdio: "ignore" }); + execFileSync("git", ["config", "user.email", "test@example.com"], { + cwd: repoRoot, + stdio: "ignore", + }); + execFileSync("git", ["config", "user.name", "Test User"], { + cwd: repoRoot, + stdio: "ignore", + }); fs.writeFileSync(path.join(repoRoot, "README.md"), "# temp\n", "utf8"); - execFileSync("git", ["add", "README.md"], { cwd: repoRoot, stdio: "ignore" }); - execFileSync("git", ["commit", "-m", "Initial commit"], { cwd: repoRoot, stdio: "ignore" }); + execFileSync("git", ["add", "README.md"], { + cwd: repoRoot, + stdio: "ignore", + }); + execFileSync("git", ["commit", "-m", "Initial commit"], { + cwd: repoRoot, + stdio: "ignore", + }); process.chdir(repoRoot); @@ -837,8 +1047,12 @@ describe("worktree helpers", () => { }); expect(fs.existsSync(path.join(worktreePath, ".git"))).toBe(true); - expect(fs.existsSync(path.join(worktreePath, ".taskcore", "config.json"))).toBe(true); - expect(fs.existsSync(path.join(worktreePath, ".taskcore", ".env"))).toBe(true); + expect( + fs.existsSync(path.join(worktreePath, ".taskcore", "config.json")), + ).toBe(true); + expect(fs.existsSync(path.join(worktreePath, ".taskcore", ".env"))).toBe( + true, + ); } finally { process.chdir(originalCwd); homedirSpy.mockRestore(); @@ -847,24 +1061,42 @@ describe("worktree helpers", () => { }, 20_000); it("no-ops on the primary checkout unless --branch is provided", async () => { - const tempRoot = fs.mkdtempSync(path.join(os.tmpdir(), "taskcore-worktree-repair-primary-")); + const tempRoot = fs.mkdtempSync( + path.join(os.tmpdir(), "taskcore-worktree-repair-primary-"), + ); const repoRoot = path.join(tempRoot, "repo"); const originalCwd = process.cwd(); try { fs.mkdirSync(repoRoot, { recursive: true }); execFileSync("git", ["init"], { cwd: repoRoot, stdio: "ignore" }); - execFileSync("git", ["config", "user.email", "test@example.com"], { cwd: repoRoot, stdio: "ignore" }); - execFileSync("git", ["config", "user.name", "Test User"], { cwd: repoRoot, stdio: "ignore" }); + execFileSync("git", ["config", "user.email", "test@example.com"], { + cwd: repoRoot, + stdio: "ignore", + }); + execFileSync("git", ["config", "user.name", "Test User"], { + cwd: repoRoot, + stdio: "ignore", + }); fs.writeFileSync(path.join(repoRoot, "README.md"), "# temp\n", "utf8"); - execFileSync("git", ["add", "README.md"], { cwd: repoRoot, stdio: "ignore" }); - execFileSync("git", ["commit", "-m", "Initial commit"], { cwd: repoRoot, stdio: "ignore" }); + execFileSync("git", ["add", "README.md"], { + cwd: repoRoot, + stdio: "ignore", + }); + execFileSync("git", ["commit", "-m", "Initial commit"], { + cwd: repoRoot, + stdio: "ignore", + }); process.chdir(repoRoot); await worktreeRepairCommand({}); - expect(fs.existsSync(path.join(repoRoot, ".taskcore", "config.json"))).toBe(false); - expect(fs.existsSync(path.join(repoRoot, ".taskcore", "worktrees"))).toBe(false); + expect( + fs.existsSync(path.join(repoRoot, ".taskcore", "config.json")), + ).toBe(false); + expect(fs.existsSync(path.join(repoRoot, ".taskcore", "worktrees"))).toBe( + false, + ); } finally { process.chdir(originalCwd); fs.rmSync(tempRoot, { recursive: true, force: true }); @@ -872,9 +1104,16 @@ describe("worktree helpers", () => { }); it("repairs the current linked worktree when Taskcore metadata is missing", async () => { - const tempRoot = fs.mkdtempSync(path.join(os.tmpdir(), "taskcore-worktree-repair-current-")); + const tempRoot = fs.mkdtempSync( + path.join(os.tmpdir(), "taskcore-worktree-repair-current-"), + ); const repoRoot = path.join(tempRoot, "repo"); - const worktreePath = path.join(repoRoot, ".taskcore", "worktrees", "repair-me"); + const worktreePath = path.join( + repoRoot, + ".taskcore", + "worktrees", + "repair-me", + ); const sourceConfigPath = path.join(tempRoot, "source-config.json"); const worktreeHome = path.join(tempRoot, ".taskcore-worktrees"); const worktreePaths = resolveWorktreeLocalPaths({ @@ -887,20 +1126,44 @@ describe("worktree helpers", () => { try { fs.mkdirSync(repoRoot, { recursive: true }); execFileSync("git", ["init"], { cwd: repoRoot, stdio: "ignore" }); - execFileSync("git", ["config", "user.email", "test@example.com"], { cwd: repoRoot, stdio: "ignore" }); - execFileSync("git", ["config", "user.name", "Test User"], { cwd: repoRoot, stdio: "ignore" }); + execFileSync("git", ["config", "user.email", "test@example.com"], { + cwd: repoRoot, + stdio: "ignore", + }); + execFileSync("git", ["config", "user.name", "Test User"], { + cwd: repoRoot, + stdio: "ignore", + }); fs.writeFileSync(path.join(repoRoot, "README.md"), "# temp\n", "utf8"); - execFileSync("git", ["add", "README.md"], { cwd: repoRoot, stdio: "ignore" }); - execFileSync("git", ["commit", "-m", "Initial commit"], { cwd: repoRoot, stdio: "ignore" }); - fs.mkdirSync(path.dirname(worktreePath), { recursive: true }); - execFileSync("git", ["worktree", "add", "-b", "repair-me", worktreePath, "HEAD"], { + execFileSync("git", ["add", "README.md"], { cwd: repoRoot, stdio: "ignore", }); + execFileSync("git", ["commit", "-m", "Initial commit"], { + cwd: repoRoot, + stdio: "ignore", + }); + fs.mkdirSync(path.dirname(worktreePath), { recursive: true }); + execFileSync( + "git", + ["worktree", "add", "-b", "repair-me", worktreePath, "HEAD"], + { + cwd: repoRoot, + stdio: "ignore", + }, + ); - fs.writeFileSync(sourceConfigPath, JSON.stringify(buildSourceConfig(), null, 2), "utf8"); + fs.writeFileSync( + sourceConfigPath, + JSON.stringify(buildSourceConfig(), null, 2), + "utf8", + ); fs.mkdirSync(worktreePaths.instanceRoot, { recursive: true }); - fs.writeFileSync(path.join(worktreePaths.instanceRoot, "marker.txt"), "stale", "utf8"); + fs.writeFileSync( + path.join(worktreePaths.instanceRoot, "marker.txt"), + "stale", + "utf8", + ); process.chdir(worktreePath); await worktreeRepairCommand({ @@ -909,9 +1172,15 @@ describe("worktree helpers", () => { noSeed: true, }); - expect(fs.existsSync(path.join(worktreePath, ".taskcore", "config.json"))).toBe(true); - expect(fs.existsSync(path.join(worktreePath, ".taskcore", ".env"))).toBe(true); - expect(fs.existsSync(path.join(worktreePaths.instanceRoot, "marker.txt"))).toBe(false); + expect( + fs.existsSync(path.join(worktreePath, ".taskcore", "config.json")), + ).toBe(true); + expect(fs.existsSync(path.join(worktreePath, ".taskcore", ".env"))).toBe( + true, + ); + expect( + fs.existsSync(path.join(worktreePaths.instanceRoot, "marker.txt")), + ).toBe(false); } finally { process.chdir(originalCwd); fs.rmSync(tempRoot, { recursive: true, force: true }); @@ -919,22 +1188,45 @@ describe("worktree helpers", () => { }, 20_000); it("creates and repairs a missing branch worktree when --branch is provided", async () => { - const tempRoot = fs.mkdtempSync(path.join(os.tmpdir(), "taskcore-worktree-repair-branch-")); + const tempRoot = fs.mkdtempSync( + path.join(os.tmpdir(), "taskcore-worktree-repair-branch-"), + ); const repoRoot = path.join(tempRoot, "repo"); const sourceConfigPath = path.join(tempRoot, "source-config.json"); const worktreeHome = path.join(tempRoot, ".taskcore-worktrees"); const originalCwd = process.cwd(); - const expectedWorktreePath = path.join(repoRoot, ".taskcore", "worktrees", "feature-repair-me"); + const expectedWorktreePath = path.join( + repoRoot, + ".taskcore", + "worktrees", + "feature-repair-me", + ); try { fs.mkdirSync(repoRoot, { recursive: true }); execFileSync("git", ["init"], { cwd: repoRoot, stdio: "ignore" }); - execFileSync("git", ["config", "user.email", "test@example.com"], { cwd: repoRoot, stdio: "ignore" }); - execFileSync("git", ["config", "user.name", "Test User"], { cwd: repoRoot, stdio: "ignore" }); + execFileSync("git", ["config", "user.email", "test@example.com"], { + cwd: repoRoot, + stdio: "ignore", + }); + execFileSync("git", ["config", "user.name", "Test User"], { + cwd: repoRoot, + stdio: "ignore", + }); fs.writeFileSync(path.join(repoRoot, "README.md"), "# temp\n", "utf8"); - execFileSync("git", ["add", "README.md"], { cwd: repoRoot, stdio: "ignore" }); - execFileSync("git", ["commit", "-m", "Initial commit"], { cwd: repoRoot, stdio: "ignore" }); - fs.writeFileSync(sourceConfigPath, JSON.stringify(buildSourceConfig(), null, 2), "utf8"); + execFileSync("git", ["add", "README.md"], { + cwd: repoRoot, + stdio: "ignore", + }); + execFileSync("git", ["commit", "-m", "Initial commit"], { + cwd: repoRoot, + stdio: "ignore", + }); + fs.writeFileSync( + sourceConfigPath, + JSON.stringify(buildSourceConfig(), null, 2), + "utf8", + ); process.chdir(repoRoot); await worktreeRepairCommand({ @@ -945,8 +1237,14 @@ describe("worktree helpers", () => { }); expect(fs.existsSync(path.join(expectedWorktreePath, ".git"))).toBe(true); - expect(fs.existsSync(path.join(expectedWorktreePath, ".taskcore", "config.json"))).toBe(true); - expect(fs.existsSync(path.join(expectedWorktreePath, ".taskcore", ".env"))).toBe(true); + expect( + fs.existsSync( + path.join(expectedWorktreePath, ".taskcore", "config.json"), + ), + ).toBe(true); + expect( + fs.existsSync(path.join(expectedWorktreePath, ".taskcore", ".env")), + ).toBe(true); } finally { process.chdir(originalCwd); fs.rmSync(tempRoot, { recursive: true, force: true }); @@ -956,7 +1254,9 @@ describe("worktree helpers", () => { describeEmbeddedPostgres("pauseSeededScheduledRoutines", () => { it("pauses only routines with enabled schedule triggers", async () => { - const tempDb = await startEmbeddedPostgresTestDatabase("taskcore-worktree-routines-"); + const tempDb = await startEmbeddedPostgresTestDatabase( + "taskcore-worktree-routines-", + ); const db = createDb(tempDb.connectionString); const companyId = randomUUID(); const projectId = randomUUID(); @@ -1072,10 +1372,14 @@ describeEmbeddedPostgres("pauseSeededScheduledRoutines", () => { }, ]); - const pausedCount = await pauseSeededScheduledRoutines(tempDb.connectionString); + const pausedCount = await pauseSeededScheduledRoutines( + tempDb.connectionString, + ); expect(pausedCount).toBe(1); - const rows = await db.select({ id: routines.id, status: routines.status }).from(routines); + const rows = await db + .select({ id: routines.id, status: routines.status }) + .from(routines); const statusById = new Map(rows.map((row) => [row.id, row.status])); expect(statusById.get(activeScheduledRoutineId)).toBe("paused"); expect(statusById.get(activeApiRoutineId)).toBe("active"); From 07e36df351020d5d6c0e14a23371562ab0765d13 Mon Sep 17 00:00:00 2001 From: devenv Date: Fri, 17 Apr 2026 15:30:17 +0600 Subject: [PATCH 2/8] =?UTF-8?q?=E2=9A=99=EF=B8=8F=20Refactor=20CLI=20comma?= =?UTF-8?q?nds,=20checks,=20prompts,=20and=20client=20modules?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- cli/src/checks/config-check.ts | 9 +- cli/src/checks/database-check.ts | 13 +- cli/src/checks/deployment-auth-check.ts | 15 +- cli/src/checks/llm-check.ts | 12 +- cli/src/checks/log-check.ts | 5 +- cli/src/checks/secrets-check.ts | 25 +- cli/src/checks/storage-check.ts | 11 +- cli/src/client/board-auth.ts | 80 +- cli/src/client/context.ts | 56 +- cli/src/client/http.ts | 67 +- cli/src/commands/allowed-hostname.ts | 27 +- cli/src/commands/auth-bootstrap-ceo.ts | 41 +- cli/src/commands/client/activity.ts | 4 +- cli/src/commands/client/agent.ts | 42 +- cli/src/commands/client/approval.ts | 66 +- cli/src/commands/client/auth.ts | 28 +- cli/src/commands/client/common.ts | 105 +- cli/src/commands/client/company.ts | 763 ++++++++--- cli/src/commands/client/context.ts | 32 +- cli/src/commands/client/dashboard.ts | 8 +- cli/src/commands/client/feedback.ts | 243 +++- cli/src/commands/client/issue.ts | 110 +- cli/src/commands/client/plugin.ts | 80 +- cli/src/commands/client/zip.ts | 53 +- cli/src/commands/configure.ts | 41 +- cli/src/commands/db-backup.ts | 27 +- cli/src/commands/doctor.ts | 20 +- cli/src/commands/env.ts | 90 +- cli/src/commands/heartbeat-run.ts | 129 +- cli/src/commands/onboard.ts | 314 +++-- cli/src/commands/routines.ts | 65 +- cli/src/commands/run.ts | 65 +- cli/src/commands/worktree-lib.ts | 35 +- .../commands/worktree-merge-history-lib.ts | 348 +++-- cli/src/commands/worktree.ts | 1201 ++++++++++++----- cli/src/prompts/database.ts | 30 +- cli/src/prompts/logging.ts | 11 +- cli/src/prompts/secrets.ts | 12 +- cli/src/prompts/server.ts | 12 +- cli/src/prompts/storage.ts | 13 +- 40 files changed, 3172 insertions(+), 1136 deletions(-) diff --git a/cli/src/checks/config-check.ts b/cli/src/checks/config-check.ts index fd34669..dde69da 100644 --- a/cli/src/checks/config-check.ts +++ b/cli/src/checks/config-check.ts @@ -1,4 +1,8 @@ -import { readConfig, configExists, resolveConfigPath } from "../config/store.js"; +import { + readConfig, + configExists, + resolveConfigPath, +} from "../config/store.js"; import type { CheckResult } from "./index.js"; export function configCheck(configPath?: string): CheckResult { @@ -27,7 +31,8 @@ export function configCheck(configPath?: string): CheckResult { status: "fail", message: `Invalid config: ${err instanceof Error ? err.message : String(err)}`, canRepair: false, - repairHint: "Run `taskcore configure --section database` (or `taskcore onboard` to recreate)", + repairHint: + "Run `taskcore configure --section database` (or `taskcore onboard` to recreate)", }; } } diff --git a/cli/src/checks/database-check.ts b/cli/src/checks/database-check.ts index b50ca35..2579f2e 100644 --- a/cli/src/checks/database-check.ts +++ b/cli/src/checks/database-check.ts @@ -3,7 +3,10 @@ import type { TaskcoreConfig } from "../config/schema.js"; import type { CheckResult } from "./index.js"; import { resolveRuntimeLikePath } from "./path-resolver.js"; -export async function databaseCheck(config: TaskcoreConfig, configPath?: string): Promise { +export async function databaseCheck( + config: TaskcoreConfig, + configPath?: string, +): Promise { if (config.database.mode === "postgres") { if (!config.database.connectionString) { return { @@ -30,13 +33,17 @@ export async function databaseCheck(config: TaskcoreConfig, configPath?: string) status: "fail", message: `Cannot connect to PostgreSQL: ${err instanceof Error ? err.message : String(err)}`, canRepair: false, - repairHint: "Check your connection string and ensure PostgreSQL is running", + repairHint: + "Check your connection string and ensure PostgreSQL is running", }; } } if (config.database.mode === "embedded-postgres") { - const dataDir = resolveRuntimeLikePath(config.database.embeddedPostgresDataDir, configPath); + const dataDir = resolveRuntimeLikePath( + config.database.embeddedPostgresDataDir, + configPath, + ); const reportedPath = dataDir; if (!fs.existsSync(dataDir)) { fs.mkdirSync(reportedPath, { recursive: true }); diff --git a/cli/src/checks/deployment-auth-check.ts b/cli/src/checks/deployment-auth-check.ts index 0b6c097..f5a142d 100644 --- a/cli/src/checks/deployment-auth-check.ts +++ b/cli/src/checks/deployment-auth-check.ts @@ -15,7 +15,8 @@ export function deploymentAuthCheck(config: TaskcoreConfig): CheckResult { status: "fail", message: `local_trusted requires loopback binding (found ${bind})`, canRepair: false, - repairHint: "Run `taskcore configure --section server` and choose Local trusted / loopback reachability", + repairHint: + "Run `taskcore configure --section server` and choose Local trusted / loopback reachability", }; } return { @@ -32,7 +33,8 @@ export function deploymentAuthCheck(config: TaskcoreConfig): CheckResult { return { name: "Deployment/auth mode", status: "fail", - message: "authenticated mode requires BETTER_AUTH_SECRET (or TASKCORE_AGENT_JWT_SECRET)", + message: + "authenticated mode requires BETTER_AUTH_SECRET (or TASKCORE_AGENT_JWT_SECRET)", canRepair: false, repairHint: "Set BETTER_AUTH_SECRET before starting Taskcore", }; @@ -44,7 +46,8 @@ export function deploymentAuthCheck(config: TaskcoreConfig): CheckResult { status: "fail", message: "auth.baseUrlMode=explicit requires auth.publicBaseUrl", canRepair: false, - repairHint: "Run `taskcore configure --section server` and provide a base URL", + repairHint: + "Run `taskcore configure --section server` and provide a base URL", }; } @@ -55,7 +58,8 @@ export function deploymentAuthCheck(config: TaskcoreConfig): CheckResult { status: "fail", message: "authenticated/public requires explicit auth.publicBaseUrl", canRepair: false, - repairHint: "Run `taskcore configure --section server` and select public exposure", + repairHint: + "Run `taskcore configure --section server` and select public exposure", }; } try { @@ -75,7 +79,8 @@ export function deploymentAuthCheck(config: TaskcoreConfig): CheckResult { status: "fail", message: "auth.publicBaseUrl is not a valid URL", canRepair: false, - repairHint: "Run `taskcore configure --section server` and provide a valid URL", + repairHint: + "Run `taskcore configure --section server` and provide a valid URL", }; } } diff --git a/cli/src/checks/llm-check.ts b/cli/src/checks/llm-check.ts index abc635e..e450351 100644 --- a/cli/src/checks/llm-check.ts +++ b/cli/src/checks/llm-check.ts @@ -34,7 +34,11 @@ export async function llmCheck(config: TaskcoreConfig): Promise { }), }); if (res.ok || res.status === 400) { - return { name: "LLM provider", status: "pass", message: "Claude API key is valid" }; + return { + name: "LLM provider", + status: "pass", + message: "Claude API key is valid", + }; } if (res.status === 401) { return { @@ -55,7 +59,11 @@ export async function llmCheck(config: TaskcoreConfig): Promise { headers: { Authorization: `Bearer ${config.llm.apiKey}` }, }); if (res.ok) { - return { name: "LLM provider", status: "pass", message: "OpenAI API key is valid" }; + return { + name: "LLM provider", + status: "pass", + message: "OpenAI API key is valid", + }; } if (res.status === 401) { return { diff --git a/cli/src/checks/log-check.ts b/cli/src/checks/log-check.ts index 6e4e14e..8a2f217 100644 --- a/cli/src/checks/log-check.ts +++ b/cli/src/checks/log-check.ts @@ -3,7 +3,10 @@ import type { TaskcoreConfig } from "../config/schema.js"; import type { CheckResult } from "./index.js"; import { resolveRuntimeLikePath } from "./path-resolver.js"; -export function logCheck(config: TaskcoreConfig, configPath?: string): CheckResult { +export function logCheck( + config: TaskcoreConfig, + configPath?: string, +): CheckResult { const logDir = resolveRuntimeLikePath(config.logging.logDir, configPath); const reportedDir = logDir; diff --git a/cli/src/checks/secrets-check.ts b/cli/src/checks/secrets-check.ts index 1584ba6..a5e5637 100644 --- a/cli/src/checks/secrets-check.ts +++ b/cli/src/checks/secrets-check.ts @@ -27,7 +27,10 @@ function decodeMasterKey(raw: string): Buffer | null { } function withStrictModeNote( - base: Pick, + base: Pick< + CheckResult, + "name" | "status" | "message" | "canRepair" | "repair" | "repairHint" + >, config: TaskcoreConfig, ): CheckResult { const strictModeDisabledInDeployedSetup = @@ -45,7 +48,10 @@ function withStrictModeNote( }; } -export function secretsCheck(config: TaskcoreConfig, configPath?: string): CheckResult { +export function secretsCheck( + config: TaskcoreConfig, + configPath?: string, +): CheckResult { const provider = config.secrets.provider; if (provider !== "local_encrypted") { return { @@ -53,7 +59,8 @@ export function secretsCheck(config: TaskcoreConfig, configPath?: string): Check status: "fail", message: `${provider} is configured, but this build only supports local_encrypted`, canRepair: false, - repairHint: "Run `taskcore configure --section secrets` and set provider to local_encrypted", + repairHint: + "Run `taskcore configure --section secrets` and set provider to local_encrypted", }; } @@ -66,7 +73,8 @@ export function secretsCheck(config: TaskcoreConfig, configPath?: string): Check message: "TASKCORE_SECRETS_MASTER_KEY is invalid (expected 32-byte base64, 64-char hex, or raw 32-char string)", canRepair: false, - repairHint: "Set TASKCORE_SECRETS_MASTER_KEY to a valid key or unset it to use a key file", + repairHint: + "Set TASKCORE_SECRETS_MASTER_KEY to a valid key or unset it to use a key file", }; } @@ -74,7 +82,8 @@ export function secretsCheck(config: TaskcoreConfig, configPath?: string): Check { name: "Secrets adapter", status: "pass", - message: "Local encrypted provider configured via TASKCORE_SECRETS_MASTER_KEY", + message: + "Local encrypted provider configured via TASKCORE_SECRETS_MASTER_KEY", }, config, ); @@ -106,7 +115,8 @@ export function secretsCheck(config: TaskcoreConfig, configPath?: string): Check // best effort } }, - repairHint: "Run with --repair to create a local encrypted secrets key file", + repairHint: + "Run with --repair to create a local encrypted secrets key file", }, config, ); @@ -131,7 +141,8 @@ export function secretsCheck(config: TaskcoreConfig, configPath?: string): Check status: "fail", message: `Invalid key material in ${keyFilePath}`, canRepair: false, - repairHint: "Replace with valid key material or delete it and run doctor --repair", + repairHint: + "Replace with valid key material or delete it and run doctor --repair", }; } diff --git a/cli/src/checks/storage-check.ts b/cli/src/checks/storage-check.ts index b217b3b..55d3083 100644 --- a/cli/src/checks/storage-check.ts +++ b/cli/src/checks/storage-check.ts @@ -3,9 +3,15 @@ import type { TaskcoreConfig } from "../config/schema.js"; import type { CheckResult } from "./index.js"; import { resolveRuntimeLikePath } from "./path-resolver.js"; -export function storageCheck(config: TaskcoreConfig, configPath?: string): CheckResult { +export function storageCheck( + config: TaskcoreConfig, + configPath?: string, +): CheckResult { if (config.storage.provider === "local_disk") { - const baseDir = resolveRuntimeLikePath(config.storage.localDisk.baseDir, configPath); + const baseDir = resolveRuntimeLikePath( + config.storage.localDisk.baseDir, + configPath, + ); if (!fs.existsSync(baseDir)) { fs.mkdirSync(baseDir, { recursive: true }); } @@ -48,4 +54,3 @@ export function storageCheck(config: TaskcoreConfig, configPath?: string): Check repairHint: "Verify credentials and endpoint in deployment environment", }; } - diff --git a/cli/src/client/board-auth.ts b/cli/src/client/board-auth.ts index 0c182a7..4cfad22 100644 --- a/cli/src/client/board-auth.ts +++ b/cli/src/client/board-auth.ts @@ -53,7 +53,9 @@ function defaultBoardAuthStore(): BoardAuthStore { } function toStringOrNull(value: unknown): string | null { - return typeof value === "string" && value.trim().length > 0 ? value.trim() : null; + return typeof value === "string" && value.trim().length > 0 + ? value.trim() + : null; } function normalizeApiBase(apiBase: string): string { @@ -62,7 +64,8 @@ function normalizeApiBase(apiBase: string): string { export function resolveBoardAuthStorePath(overridePath?: string): string { if (overridePath?.trim()) return path.resolve(overridePath.trim()); - if (process.env.TASKCORE_AUTH_STORE?.trim()) return path.resolve(process.env.TASKCORE_AUTH_STORE.trim()); + if (process.env.TASKCORE_AUTH_STORE?.trim()) + return path.resolve(process.env.TASKCORE_AUTH_STORE.trim()); return resolveDefaultCliAuthPath(); } @@ -70,8 +73,13 @@ export function readBoardAuthStore(storePath?: string): BoardAuthStore { const filePath = resolveBoardAuthStorePath(storePath); if (!fs.existsSync(filePath)) return defaultBoardAuthStore(); - const raw = JSON.parse(fs.readFileSync(filePath, "utf8")) as Partial | null; - const credentials = raw?.credentials && typeof raw.credentials === "object" ? raw.credentials : {}; + const raw = JSON.parse( + fs.readFileSync(filePath, "utf8"), + ) as Partial | null; + const credentials = + raw?.credentials && typeof raw.credentials === "object" + ? raw.credentials + : {}; const normalized: Record = {}; for (const [key, value] of Object.entries(credentials)) { @@ -97,13 +105,21 @@ export function readBoardAuthStore(storePath?: string): BoardAuthStore { }; } -export function writeBoardAuthStore(store: BoardAuthStore, storePath?: string): void { +export function writeBoardAuthStore( + store: BoardAuthStore, + storePath?: string, +): void { const filePath = resolveBoardAuthStorePath(storePath); fs.mkdirSync(path.dirname(filePath), { recursive: true }); - fs.writeFileSync(filePath, `${JSON.stringify(store, null, 2)}\n`, { mode: 0o600 }); + fs.writeFileSync(filePath, `${JSON.stringify(store, null, 2)}\n`, { + mode: 0o600, + }); } -export function getStoredBoardCredential(apiBase: string, storePath?: string): BoardAuthCredential | null { +export function getStoredBoardCredential( + apiBase: string, + storePath?: string, +): BoardAuthCredential | null { const store = readBoardAuthStore(storePath); return store.credentials[normalizeApiBase(apiBase)] ?? null; } @@ -130,7 +146,10 @@ export function setStoredBoardCredential(input: { return credential; } -export function removeStoredBoardCredential(apiBase: string, storePath?: string): boolean { +export function removeStoredBoardCredential( + apiBase: string, + storePath?: string, +): boolean { const normalizedApiBase = normalizeApiBase(apiBase); const store = readBoardAuthStore(storePath); if (!store.credentials[normalizedApiBase]) return false; @@ -160,7 +179,9 @@ async function requestJson(url: string, init?: RequestInit): Promise { if (!response.ok) { const body = await response.json().catch(() => null); const message = - body && typeof body === "object" && typeof (body as { error?: unknown }).error === "string" + body && + typeof body === "object" && + typeof (body as { error?: unknown }).error === "string" ? (body as { error: string }).error : `Request failed: ${response.status}`; throw new Error(message); @@ -178,7 +199,10 @@ export function openUrl(url: string): boolean { return true; } if (platform === "win32") { - const child = spawn("cmd", ["/c", "start", "", url], { detached: true, stdio: "ignore" }); + const child = spawn("cmd", ["/c", "start", "", url], { + detached: true, + stdio: "ignore", + }); child.unref(); return true; } @@ -213,10 +237,13 @@ export async function loginBoardCli(params: { }), }); - const approvalUrl = challenge.approvalUrl ?? `${apiBase}${challenge.approvalPath}`; + const approvalUrl = + challenge.approvalUrl ?? `${apiBase}${challenge.approvalPath}`; if (params.print !== false) { console.error(pc.bold("Board authentication required")); - console.error(`Open this URL in your browser to approve CLI access:\n${approvalUrl}`); + console.error( + `Open this URL in your browser to approve CLI access:\n${approvalUrl}`, + ); } const opened = openUrl(approvalUrl); @@ -233,14 +260,14 @@ export async function loginBoardCli(params: { ); if (status.status === "approved") { - const me = await requestJson<{ userId: string; user?: { id: string } | null }>( - `${apiBase}/api/cli-auth/me`, - { - headers: { - authorization: `Bearer ${challenge.boardApiToken}`, - }, + const me = await requestJson<{ + userId: string; + user?: { id: string } | null; + }>(`${apiBase}/api/cli-auth/me`, { + headers: { + authorization: `Bearer ${challenge.boardApiToken}`, }, - ); + }); setStoredBoardCredential({ apiBase, token: challenge.boardApiToken, @@ -272,11 +299,14 @@ export async function revokeStoredBoardCredential(params: { token: string; }): Promise { const apiBase = normalizeApiBase(params.apiBase); - await requestJson<{ revoked: boolean }>(`${apiBase}/api/cli-auth/revoke-current`, { - method: "POST", - headers: { - authorization: `Bearer ${params.token}`, + await requestJson<{ revoked: boolean }>( + `${apiBase}/api/cli-auth/revoke-current`, + { + method: "POST", + headers: { + authorization: `Bearer ${params.token}`, + }, + body: JSON.stringify({}), }, - body: JSON.stringify({}), - }); + ); } diff --git a/cli/src/client/context.ts b/cli/src/client/context.ts index 734cdac..e1aebce 100644 --- a/cli/src/client/context.ts +++ b/cli/src/client/context.ts @@ -22,7 +22,11 @@ function findContextFileFromAncestors(startDir: string): string | null { let currentDir = absoluteStartDir; while (true) { - const candidate = path.resolve(currentDir, ".taskcore", DEFAULT_CONTEXT_BASENAME); + const candidate = path.resolve( + currentDir, + ".taskcore", + DEFAULT_CONTEXT_BASENAME, + ); if (fs.existsSync(candidate)) { return candidate; } @@ -37,8 +41,11 @@ function findContextFileFromAncestors(startDir: string): string | null { export function resolveContextPath(overridePath?: string): string { if (overridePath) return path.resolve(overridePath); - if (process.env.TASKCORE_CONTEXT) return path.resolve(process.env.TASKCORE_CONTEXT); - return findContextFileFromAncestors(process.cwd()) ?? resolveDefaultContextPath(); + if (process.env.TASKCORE_CONTEXT) + return path.resolve(process.env.TASKCORE_CONTEXT); + return ( + findContextFileFromAncestors(process.cwd()) ?? resolveDefaultContextPath() + ); } export function defaultClientContext(): ClientContext { @@ -55,16 +62,21 @@ function parseJson(filePath: string): unknown { try { return JSON.parse(fs.readFileSync(filePath, "utf-8")); } catch (err) { - throw new Error(`Failed to parse JSON at ${filePath}: ${err instanceof Error ? err.message : String(err)}`); + throw new Error( + `Failed to parse JSON at ${filePath}: ${err instanceof Error ? err.message : String(err)}`, + ); } } function toStringOrUndefined(value: unknown): string | undefined { - return typeof value === "string" && value.trim().length > 0 ? value.trim() : undefined; + return typeof value === "string" && value.trim().length > 0 + ? value.trim() + : undefined; } function normalizeProfile(value: unknown): ClientContextProfile { - if (typeof value !== "object" || value === null || Array.isArray(value)) return {}; + if (typeof value !== "object" || value === null || Array.isArray(value)) + return {}; const profile = value as Record; return { @@ -81,13 +93,20 @@ function normalizeContext(raw: unknown): ClientContext { const record = raw as Record; const version = record.version === 1 ? 1 : 1; - const currentProfile = toStringOrUndefined(record.currentProfile) ?? DEFAULT_PROFILE; + const currentProfile = + toStringOrUndefined(record.currentProfile) ?? DEFAULT_PROFILE; const rawProfiles = record.profiles; const profiles: Record = {}; - if (typeof rawProfiles === "object" && rawProfiles !== null && !Array.isArray(rawProfiles)) { - for (const [name, profile] of Object.entries(rawProfiles as Record)) { + if ( + typeof rawProfiles === "object" && + rawProfiles !== null && + !Array.isArray(rawProfiles) + ) { + for (const [name, profile] of Object.entries( + rawProfiles as Record, + )) { if (!name.trim()) continue; profiles[name] = normalizeProfile(profile); } @@ -118,13 +137,18 @@ export function readContext(contextPath?: string): ClientContext { return normalizeContext(raw); } -export function writeContext(context: ClientContext, contextPath?: string): void { +export function writeContext( + context: ClientContext, + contextPath?: string, +): void { const filePath = resolveContextPath(contextPath); const dir = path.dirname(filePath); fs.mkdirSync(dir, { recursive: true }); const normalized = normalizeContext(context); - fs.writeFileSync(filePath, `${JSON.stringify(normalized, null, 2)}\n`, { mode: 0o600 }); + fs.writeFileSync(filePath, `${JSON.stringify(normalized, null, 2)}\n`, { + mode: 0o600, + }); } export function upsertProfile( @@ -145,7 +169,10 @@ export function upsertProfile( if (patch.companyId !== undefined && patch.companyId.trim().length === 0) { delete merged.companyId; } - if (patch.apiKeyEnvVarName !== undefined && patch.apiKeyEnvVarName.trim().length === 0) { + if ( + patch.apiKeyEnvVarName !== undefined && + patch.apiKeyEnvVarName.trim().length === 0 + ) { delete merged.apiKeyEnvVarName; } @@ -155,7 +182,10 @@ export function upsertProfile( return context; } -export function setCurrentProfile(profileName: string, contextPath?: string): ClientContext { +export function setCurrentProfile( + profileName: string, + contextPath?: string, +): ClientContext { const context = readContext(contextPath); if (!context.profiles[profileName]) { context.profiles[profileName] = {}; diff --git a/cli/src/client/http.ts b/cli/src/client/http.ts index 732aac1..db0b8aa 100644 --- a/cli/src/client/http.ts +++ b/cli/src/client/http.ts @@ -5,7 +5,12 @@ export class ApiRequestError extends Error { details?: unknown; body?: unknown; - constructor(status: number, message: string, details?: unknown, body?: unknown) { + constructor( + status: number, + message: string, + details?: unknown, + body?: unknown, + ) { super(message); this.status = status; this.details = details; @@ -26,7 +31,14 @@ export class ApiConnectionError extends Error { }) { const url = buildUrl(input.apiBase, input.path); const causeMessage = formatConnectionCause(input.cause); - super(buildConnectionErrorMessage({ apiBase: input.apiBase, url, method: input.method, causeMessage })); + super( + buildConnectionErrorMessage({ + apiBase: input.apiBase, + url, + method: input.method, + causeMessage, + }), + ); this.url = url; this.method = input.method; this.causeMessage = causeMessage; @@ -67,18 +79,34 @@ export class TaskcoreApiClient { return this.request(path, { method: "GET" }, opts); } - post(path: string, body?: unknown, opts?: RequestOptions): Promise { - return this.request(path, { - method: "POST", - body: body === undefined ? undefined : JSON.stringify(body), - }, opts); + post( + path: string, + body?: unknown, + opts?: RequestOptions, + ): Promise { + return this.request( + path, + { + method: "POST", + body: body === undefined ? undefined : JSON.stringify(body), + }, + opts, + ); } - patch(path: string, body?: unknown, opts?: RequestOptions): Promise { - return this.request(path, { - method: "PATCH", - body: body === undefined ? undefined : JSON.stringify(body), - }, opts); + patch( + path: string, + body?: unknown, + opts?: RequestOptions, + ): Promise { + return this.request( + path, + { + method: "PATCH", + body: body === undefined ? undefined : JSON.stringify(body), + }, + opts, + ); } delete(path: string, opts?: RequestOptions): Promise { @@ -194,7 +222,12 @@ async function toApiError(response: Response): Promise { return new ApiRequestError(response.status, message, body.details, parsed); } - return new ApiRequestError(response.status, `Request failed with status ${response.status}`, undefined, parsed); + return new ApiRequestError( + response.status, + `Request failed with status ${response.status}`, + undefined, + parsed, + ); } function buildConnectionErrorMessage(input: { @@ -241,10 +274,14 @@ function formatConnectionCause(error: unknown): string | undefined { return message || undefined; } -function toStringRecord(headers: HeadersInit | undefined): Record { +function toStringRecord( + headers: HeadersInit | undefined, +): Record { if (!headers) return {}; if (Array.isArray(headers)) { - return Object.fromEntries(headers.map(([key, value]) => [key, String(value)])); + return Object.fromEntries( + headers.map(([key, value]) => [key, String(value)]), + ); } if (headers instanceof Headers) { return Object.fromEntries(headers.entries()); diff --git a/cli/src/commands/allowed-hostname.ts b/cli/src/commands/allowed-hostname.ts index c7bc9e1..af23f1f 100644 --- a/cli/src/commands/allowed-hostname.ts +++ b/cli/src/commands/allowed-hostname.ts @@ -3,17 +3,26 @@ import pc from "picocolors"; import { normalizeHostnameInput } from "../config/hostnames.js"; import { readConfig, resolveConfigPath, writeConfig } from "../config/store.js"; -export async function addAllowedHostname(host: string, opts: { config?: string }): Promise { +export async function addAllowedHostname( + host: string, + opts: { config?: string }, +): Promise { const configPath = resolveConfigPath(opts.config); const config = readConfig(opts.config); if (!config) { - p.log.error(`No config found at ${configPath}. Run ${pc.cyan("taskcore onboard")} first.`); + p.log.error( + `No config found at ${configPath}. Run ${pc.cyan("taskcore onboard")} first.`, + ); return; } const normalized = normalizeHostnameInput(host); - const current = new Set((config.server.allowedHostnames ?? []).map((value) => value.trim().toLowerCase()).filter(Boolean)); + const current = new Set( + (config.server.allowedHostnames ?? []) + .map((value) => value.trim().toLowerCase()) + .filter(Boolean), + ); const existed = current.has(normalized); current.add(normalized); @@ -31,10 +40,16 @@ export async function addAllowedHostname(host: string, opts: { config?: string } ); } - if (!(config.server.deploymentMode === "authenticated" && config.server.exposure === "private")) { + if ( + !( + config.server.deploymentMode === "authenticated" && + config.server.exposure === "private" + ) + ) { p.log.message( - pc.dim("Note: allowed hostnames are enforced only in authenticated/private mode."), + pc.dim( + "Note: allowed hostnames are enforced only in authenticated/private mode.", + ), ); } } - diff --git a/cli/src/commands/auth-bootstrap-ceo.ts b/cli/src/commands/auth-bootstrap-ceo.ts index bfeaf82..ce39260 100644 --- a/cli/src/commands/auth-bootstrap-ceo.ts +++ b/cli/src/commands/auth-bootstrap-ceo.ts @@ -19,7 +19,10 @@ function resolveDbUrl(configPath?: string, explicitDbUrl?: string) { if (explicitDbUrl) return explicitDbUrl; const config = readConfig(configPath); if (process.env.DATABASE_URL) return process.env.DATABASE_URL; - if (config?.database.mode === "postgres" && config.database.connectionString) { + if ( + config?.database.mode === "postgres" && + config.database.connectionString + ) { return config.database.connectionString; } if (config?.database.mode === "embedded-postgres") { @@ -41,11 +44,12 @@ function resolveBaseUrl(configPath?: string, explicitBaseUrl?: string) { if (config?.auth.baseUrlMode === "explicit" && config.auth.publicBaseUrl) { return config.auth.publicBaseUrl.replace(/\/+$/, ""); } - const bind = config?.server.bind ?? inferBindModeFromHost(config?.server.host); + const bind = + config?.server.bind ?? inferBindModeFromHost(config?.server.host); const host = bind === "custom" - ? config?.server.customBindHost ?? config?.server.host ?? "localhost" - : config?.server.host ?? "localhost"; + ? (config?.server.customBindHost ?? config?.server.host ?? "localhost") + : (config?.server.host ?? "localhost"); const port = config?.server.port ?? 3100; const publicHost = host === "0.0.0.0" || bind === "lan" ? "localhost" : host; return `http://${publicHost}:${port}`; @@ -62,20 +66,22 @@ export async function bootstrapCeoInvite(opts: { loadTaskcoreEnvFile(configPath); const config = readConfig(configPath); if (!config) { - p.log.error(`No config found at ${configPath}. Run ${pc.cyan("taskcore onboard")} first.`); + p.log.error( + `No config found at ${configPath}. Run ${pc.cyan("taskcore onboard")} first.`, + ); return; } if (config.server.deploymentMode !== "authenticated") { - p.log.info("Deployment mode is local_trusted. Bootstrap CEO invite is only required for authenticated mode."); + p.log.info( + "Deployment mode is local_trusted. Bootstrap CEO invite is only required for authenticated mode.", + ); return; } const dbUrl = resolveDbUrl(configPath, opts.dbUrl); if (!dbUrl) { - p.log.error( - "Could not resolve database connection for bootstrap.", - ); + p.log.error("Could not resolve database connection for bootstrap."); return; } @@ -93,7 +99,9 @@ export async function bootstrapCeoInvite(opts: { .then((rows) => rows.length); if (existingAdminCount > 0 && !opts.force) { - p.log.info("Instance already has an admin user. Use --force to generate a new bootstrap invite."); + p.log.info( + "Instance already has an admin user. Use --force to generate a new bootstrap invite.", + ); return; } @@ -111,7 +119,10 @@ export async function bootstrapCeoInvite(opts: { ); const token = createInviteToken(); - const expiresHours = Math.max(1, Math.min(24 * 30, opts.expiresHours ?? 72)); + const expiresHours = Math.max( + 1, + Math.min(24 * 30, opts.expiresHours ?? 72), + ); const created = await db .insert(invites) .values({ @@ -130,8 +141,12 @@ export async function bootstrapCeoInvite(opts: { p.log.message(`Invite URL: ${pc.cyan(inviteUrl)}`); p.log.message(`Expires: ${pc.dim(created.expiresAt.toISOString())}`); } catch (err) { - p.log.error(`Could not create bootstrap invite: ${err instanceof Error ? err.message : String(err)}`); - p.log.info("If using embedded-postgres, start the Taskcore server and run this command again."); + p.log.error( + `Could not create bootstrap invite: ${err instanceof Error ? err.message : String(err)}`, + ); + p.log.info( + "If using embedded-postgres, start the Taskcore server and run this command again.", + ); } finally { await closableDb.$client?.end?.({ timeout: 5 }).catch(() => undefined); } diff --git a/cli/src/commands/client/activity.ts b/cli/src/commands/client/activity.ts index a72f0c1..74e8e4c 100644 --- a/cli/src/commands/client/activity.ts +++ b/cli/src/commands/client/activity.ts @@ -17,7 +17,9 @@ interface ActivityListOptions extends BaseClientOptions { } export function registerActivityCommands(program: Command): void { - const activity = program.command("activity").description("Activity log operations"); + const activity = program + .command("activity") + .description("Activity log operations"); addCommonClientOptions( activity diff --git a/cli/src/commands/client/agent.ts b/cli/src/commands/client/agent.ts index 89625d9..d214dd3 100644 --- a/cli/src/commands/client/agent.ts +++ b/cli/src/commands/client/agent.ts @@ -47,13 +47,17 @@ const __moduleDir = path.dirname(fileURLToPath(import.meta.url)); function codexSkillsHome(): string { const fromEnv = process.env.CODEX_HOME?.trim(); - const base = fromEnv && fromEnv.length > 0 ? fromEnv : path.join(os.homedir(), ".codex"); + const base = + fromEnv && fromEnv.length > 0 ? fromEnv : path.join(os.homedir(), ".codex"); return path.join(base, "skills"); } function claudeSkillsHome(): string { const fromEnv = process.env.CLAUDE_HOME?.trim(); - const base = fromEnv && fromEnv.length > 0 ? fromEnv : path.join(os.homedir(), ".claude"); + const base = + fromEnv && fromEnv.length > 0 + ? fromEnv + : path.join(os.homedir(), ".claude"); return path.join(base, "skills"); } @@ -167,7 +171,10 @@ export function registerAgentCommands(program: Command): void { .action(async (opts: AgentListOptions) => { try { const ctx = resolveCommandContext(opts, { requireCompany: true }); - const rows = (await ctx.api.get(`/api/companies/${ctx.companyId}/agents`)) ?? []; + const rows = + (await ctx.api.get( + `/api/companies/${ctx.companyId}/agents`, + )) ?? []; if (ctx.json) { printOutput(rows, { json: true }); @@ -240,15 +247,22 @@ export function registerAgentCommands(program: Command): void { } const now = new Date().toISOString().replaceAll(":", "-"); - const keyName = opts.keyName?.trim() ? opts.keyName.trim() : `local-cli-${now}`; - const key = await ctx.api.post(`/api/agents/${agentRow.id}/keys`, { name: keyName }); + const keyName = opts.keyName?.trim() + ? opts.keyName.trim() + : `local-cli-${now}`; + const key = await ctx.api.post( + `/api/agents/${agentRow.id}/keys`, + { name: keyName }, + ); if (!key) { throw new Error("Failed to create API key"); } const installSummaries: SkillsInstallSummary[] = []; if (opts.installSkills !== false) { - const skillsDir = await resolveTaskcoreSkillsDir(__moduleDir, [path.resolve(process.cwd(), "skills")]); + const skillsDir = await resolveTaskcoreSkillsDir(__moduleDir, [ + path.resolve(process.cwd(), "skills"), + ]); if (!skillsDir) { throw new Error( "Could not locate local Taskcore skills directory. Expected ./skills in the repo checkout.", @@ -256,8 +270,16 @@ export function registerAgentCommands(program: Command): void { } installSummaries.push( - await installSkillsForTarget(skillsDir, codexSkillsHome(), "codex"), - await installSkillsForTarget(skillsDir, claudeSkillsHome(), "claude"), + await installSkillsForTarget( + skillsDir, + codexSkillsHome(), + "codex", + ), + await installSkillsForTarget( + skillsDir, + claudeSkillsHome(), + "claude", + ), ); } @@ -304,7 +326,9 @@ export function registerAgentCommands(program: Command): void { } } console.log(""); - console.log("# Run this in your shell before launching codex/claude:"); + console.log( + "# Run this in your shell before launching codex/claude:", + ); console.log(exportsText); } catch (err) { handleCommandError(err); diff --git a/cli/src/commands/client/approval.ts b/cli/src/commands/client/approval.ts index 469563c..49ea125 100644 --- a/cli/src/commands/client/approval.ts +++ b/cli/src/commands/client/approval.ts @@ -43,7 +43,9 @@ interface ApprovalCommentOptions extends BaseClientOptions { } export function registerApprovalCommands(program: Command): void { - const approval = program.command("approval").description("Approval operations"); + const approval = program + .command("approval") + .description("Approval operations"); addCommonClientOptions( approval @@ -98,7 +100,9 @@ export function registerApprovalCommands(program: Command): void { .action(async (approvalId: string, opts: BaseClientOptions) => { try { const ctx = resolveCommandContext(opts); - const row = await ctx.api.get(`/api/approvals/${approvalId}`); + const row = await ctx.api.get( + `/api/approvals/${approvalId}`, + ); printOutput(row, { json: ctx.json }); } catch (err) { handleCommandError(err); @@ -111,7 +115,10 @@ export function registerApprovalCommands(program: Command): void { .command("create") .description("Create an approval request") .requiredOption("-C, --company-id ", "Company ID") - .requiredOption("--type ", "Approval type (hire_agent|approve_ceo_strategy)") + .requiredOption( + "--type ", + "Approval type (hire_agent|approve_ceo_strategy)", + ) .requiredOption("--payload ", "Approval payload as JSON object") .option("--requested-by-agent-id ", "Requesting agent ID") .option("--issue-ids ", "Comma-separated linked issue IDs") @@ -125,7 +132,10 @@ export function registerApprovalCommands(program: Command): void { requestedByAgentId: opts.requestedByAgentId, issueIds: parseCsv(opts.issueIds), }); - const created = await ctx.api.post(`/api/companies/${ctx.companyId}/approvals`, payload); + const created = await ctx.api.post( + `/api/companies/${ctx.companyId}/approvals`, + payload, + ); printOutput(created, { json: ctx.json }); } catch (err) { handleCommandError(err); @@ -148,7 +158,10 @@ export function registerApprovalCommands(program: Command): void { decisionNote: opts.decisionNote, decidedByUserId: opts.decidedByUserId, }); - const updated = await ctx.api.post(`/api/approvals/${approvalId}/approve`, payload); + const updated = await ctx.api.post( + `/api/approvals/${approvalId}/approve`, + payload, + ); printOutput(updated, { json: ctx.json }); } catch (err) { handleCommandError(err); @@ -170,7 +183,10 @@ export function registerApprovalCommands(program: Command): void { decisionNote: opts.decisionNote, decidedByUserId: opts.decidedByUserId, }); - const updated = await ctx.api.post(`/api/approvals/${approvalId}/reject`, payload); + const updated = await ctx.api.post( + `/api/approvals/${approvalId}/reject`, + payload, + ); printOutput(updated, { json: ctx.json }); } catch (err) { handleCommandError(err); @@ -192,7 +208,10 @@ export function registerApprovalCommands(program: Command): void { decisionNote: opts.decisionNote, decidedByUserId: opts.decidedByUserId, }); - const updated = await ctx.api.post(`/api/approvals/${approvalId}/request-revision`, payload); + const updated = await ctx.api.post( + `/api/approvals/${approvalId}/request-revision`, + payload, + ); printOutput(updated, { json: ctx.json }); } catch (err) { handleCommandError(err); @@ -210,9 +229,14 @@ export function registerApprovalCommands(program: Command): void { try { const ctx = resolveCommandContext(opts); const payload = resubmitApprovalSchema.parse({ - payload: opts.payload ? parseJsonObject(opts.payload, "payload") : undefined, + payload: opts.payload + ? parseJsonObject(opts.payload, "payload") + : undefined, }); - const updated = await ctx.api.post(`/api/approvals/${approvalId}/resubmit`, payload); + const updated = await ctx.api.post( + `/api/approvals/${approvalId}/resubmit`, + payload, + ); printOutput(updated, { json: ctx.json }); } catch (err) { handleCommandError(err); @@ -229,9 +253,12 @@ export function registerApprovalCommands(program: Command): void { .action(async (approvalId: string, opts: ApprovalCommentOptions) => { try { const ctx = resolveCommandContext(opts); - const created = await ctx.api.post(`/api/approvals/${approvalId}/comments`, { - body: opts.body, - }); + const created = await ctx.api.post( + `/api/approvals/${approvalId}/comments`, + { + body: opts.body, + }, + ); printOutput(created, { json: ctx.json }); } catch (err) { handleCommandError(err); @@ -242,18 +269,27 @@ export function registerApprovalCommands(program: Command): void { function parseCsv(value: string | undefined): string[] | undefined { if (!value) return undefined; - const rows = value.split(",").map((v) => v.trim()).filter(Boolean); + const rows = value + .split(",") + .map((v) => v.trim()) + .filter(Boolean); return rows.length > 0 ? rows : undefined; } function parseJsonObject(value: string, name: string): Record { try { const parsed = JSON.parse(value) as unknown; - if (typeof parsed !== "object" || parsed === null || Array.isArray(parsed)) { + if ( + typeof parsed !== "object" || + parsed === null || + Array.isArray(parsed) + ) { throw new Error(`${name} must be a JSON object`); } return parsed as Record; } catch (err) { - throw new Error(`Invalid ${name} JSON: ${err instanceof Error ? err.message : String(err)}`); + throw new Error( + `Invalid ${name} JSON: ${err instanceof Error ? err.message : String(err)}`, + ); } } diff --git a/cli/src/commands/client/auth.ts b/cli/src/commands/client/auth.ts index 09f896b..0a54f22 100644 --- a/cli/src/commands/client/auth.ts +++ b/cli/src/commands/client/auth.ts @@ -17,21 +17,27 @@ interface AuthLoginOptions extends BaseClientOptions { instanceAdmin?: boolean; } -interface AuthLogoutOptions extends BaseClientOptions { } -interface AuthWhoamiOptions extends BaseClientOptions { } +interface AuthLogoutOptions extends BaseClientOptions {} +interface AuthWhoamiOptions extends BaseClientOptions {} export function registerClientAuthCommands(auth: Command): void { addCommonClientOptions( auth .command("login") .description("Authenticate the CLI for board-user access") - .option("--instance-admin", "Request instance-admin approval instead of plain board access", false) + .option( + "--instance-admin", + "Request instance-admin approval instead of plain board access", + false, + ) .action(async (opts: AuthLoginOptions) => { try { const ctx = resolveCommandContext(opts); const login = await loginBoardCli({ apiBase: ctx.api.apiBase, - requestedAccess: opts.instanceAdmin ? "instance_admin_required" : "board", + requestedAccess: opts.instanceAdmin + ? "instance_admin_required" + : "board", requestedCompanyId: ctx.companyId ?? null, command: "taskcore auth login", }); @@ -60,7 +66,15 @@ export function registerClientAuthCommands(auth: Command): void { const ctx = resolveCommandContext(opts); const credential = getStoredBoardCredential(ctx.api.apiBase); if (!credential) { - printOutput({ ok: true, apiBase: ctx.api.apiBase, revoked: false, removedLocalCredential: false }, { json: ctx.json }); + printOutput( + { + ok: true, + apiBase: ctx.api.apiBase, + revoked: false, + removedLocalCredential: false, + }, + { json: ctx.json }, + ); return; } let revoked = false; @@ -73,7 +87,9 @@ export function registerClientAuthCommands(auth: Command): void { } catch { // Remove the local credential even if the server-side revoke fails. } - const removedLocalCredential = removeStoredBoardCredential(ctx.api.apiBase); + const removedLocalCredential = removeStoredBoardCredential( + ctx.api.apiBase, + ); printOutput( { ok: true, diff --git a/cli/src/commands/client/common.ts b/cli/src/commands/client/common.ts index 979d0a6..c931a6c 100644 --- a/cli/src/commands/client/common.ts +++ b/cli/src/commands/client/common.ts @@ -1,9 +1,16 @@ import pc from "picocolors"; import type { Command } from "commander"; -import { getStoredBoardCredential, loginBoardCli } from "../../client/board-auth.js"; +import { + getStoredBoardCredential, + loginBoardCli, +} from "../../client/board-auth.js"; import { buildCliCommandLabel } from "../../client/command-label.js"; import { readConfig } from "../../config/store.js"; -import { readContext, resolveProfile, type ClientContextProfile } from "../../client/context.js"; +import { + readContext, + resolveProfile, + type ClientContextProfile, +} from "../../client/context.js"; import { ApiRequestError, TaskcoreApiClient } from "../../client/http.js"; export interface BaseClientOptions { @@ -25,10 +32,16 @@ export interface ResolvedClientContext { json: boolean; } -export function addCommonClientOptions(command: Command, opts?: { includeCompany?: boolean }): Command { +export function addCommonClientOptions( + command: Command, + opts?: { includeCompany?: boolean }, +): Command { command .option("-c, --config ", "Path to Taskcore config file") - .option("-d, --data-dir ", "Taskcore data directory root (isolates state from ~/.taskcore)") + .option( + "-d, --data-dir ", + "Taskcore data directory root (isolates state from ~/.taskcore)", + ) .option("--context ", "Path to CLI context file") .option("--profile ", "CLI context profile name") .option("--api-base ", "Base URL for the Taskcore API") @@ -36,7 +49,10 @@ export function addCommonClientOptions(command: Command, opts?: { includeCompany .option("--json", "Output raw JSON"); if (opts?.includeCompany) { - command.option("-C, --company-id ", "Company ID (overrides context default)"); + command.option( + "-C, --company-id ", + "Company ID (overrides context default)", + ); } return command; @@ -47,7 +63,10 @@ export function resolveCommandContext( opts?: { requireCompany?: boolean }, ): ResolvedClientContext { const context = readContext(options.context); - const { name: profileName, profile } = resolveProfile(context, options.profile); + const { name: profileName, profile } = resolveProfile( + context, + options.profile, + ); const apiBase = options.apiBase?.trim() || @@ -59,7 +78,9 @@ export function resolveCommandContext( options.apiKey?.trim() || process.env.TASKCORE_API_KEY?.trim() || readKeyFromProfileEnv(profile); - const storedBoardCredential = explicitApiKey ? null : getStoredBoardCredential(apiBase); + const storedBoardCredential = explicitApiKey + ? null + : getStoredBoardCredential(apiBase); const apiKey = explicitApiKey || storedBoardCredential?.token; const companyId = @@ -76,23 +97,26 @@ export function resolveCommandContext( const api = new TaskcoreApiClient({ apiBase, apiKey, - recoverAuth: explicitApiKey || !canAttemptInteractiveBoardAuth() - ? undefined - : async ({ error }) => { - const requestedAccess = error.message.includes("Instance admin required") - ? "instance_admin_required" - : "board"; - if (!shouldRecoverBoardAuth(error)) { - return null; - } - const login = await loginBoardCli({ - apiBase, - requestedAccess, - requestedCompanyId: companyId ?? null, - command: buildCliCommandLabel(), - }); - return login.token; - }, + recoverAuth: + explicitApiKey || !canAttemptInteractiveBoardAuth() + ? undefined + : async ({ error }) => { + const requestedAccess = error.message.includes( + "Instance admin required", + ) + ? "instance_admin_required" + : "board"; + if (!shouldRecoverBoardAuth(error)) { + return null; + } + const login = await loginBoardCli({ + apiBase, + requestedAccess, + requestedCompanyId: companyId ?? null, + command: buildCliCommandLabel(), + }); + return login.token; + }, }); return { api, @@ -106,14 +130,20 @@ export function resolveCommandContext( function shouldRecoverBoardAuth(error: ApiRequestError): boolean { if (error.status === 401) return true; if (error.status !== 403) return false; - return error.message.includes("Board access required") || error.message.includes("Instance admin required"); + return ( + error.message.includes("Board access required") || + error.message.includes("Instance admin required") + ); } function canAttemptInteractiveBoardAuth(): boolean { return Boolean(process.stdin.isTTY && process.stdout.isTTY); } -export function printOutput(data: unknown, opts: { json?: boolean; label?: string } = {}): void { +export function printOutput( + data: unknown, + opts: { json?: boolean; label?: string } = {}, +): void { if (opts.json) { console.log(JSON.stringify(data, null, 2)); return; @@ -152,7 +182,15 @@ export function printOutput(data: unknown, opts: { json?: boolean; label?: strin } export function formatInlineRecord(record: Record): string { - const keyOrder = ["identifier", "id", "name", "status", "priority", "title", "action"]; + const keyOrder = [ + "identifier", + "id", + "name", + "status", + "priority", + "title", + "action", + ]; const seen = new Set(); const parts: string[] = []; @@ -203,15 +241,22 @@ function inferApiBaseFromConfig(configPath?: string): string { return `http://${envHost}:${port}`; } -function readKeyFromProfileEnv(profile: ClientContextProfile): string | undefined { +function readKeyFromProfileEnv( + profile: ClientContextProfile, +): string | undefined { if (!profile.apiKeyEnvVarName) return undefined; return process.env[profile.apiKeyEnvVarName]?.trim() || undefined; } export function handleCommandError(error: unknown): never { if (error instanceof ApiRequestError) { - const detailSuffix = error.details !== undefined ? ` details=${JSON.stringify(error.details)}` : ""; - console.error(pc.red(`API error ${error.status}: ${error.message}${detailSuffix}`)); + const detailSuffix = + error.details !== undefined + ? ` details=${JSON.stringify(error.details)}` + : ""; + console.error( + pc.red(`API error ${error.status}: ${error.message}${detailSuffix}`), + ); process.exit(1); } diff --git a/cli/src/commands/client/company.ts b/cli/src/commands/client/company.ts index b56748c..79229c2 100644 --- a/cli/src/commands/client/company.ts +++ b/cli/src/commands/client/company.ts @@ -30,7 +30,7 @@ import { serializeFeedbackTraces, } from "./feedback.js"; -interface CompanyCommandOptions extends BaseClientOptions { } +interface CompanyCommandOptions extends BaseClientOptions {} type CompanyDeleteSelectorMode = "auto" | "id" | "prefix"; type CompanyImportTargetMode = "new" | "existing"; type CompanyCollisionMode = "rename" | "skip" | "replace"; @@ -99,12 +99,24 @@ const IMPORT_INCLUDE_OPTIONS: Array<{ label: string; hint: string; }> = [ - { value: "company", label: "Company", hint: "name, branding, and company settings" }, - { value: "projects", label: "Projects", hint: "projects and workspace metadata" }, - { value: "issues", label: "Tasks", hint: "tasks and recurring routines" }, - { value: "agents", label: "Agents", hint: "agent records and org structure" }, - { value: "skills", label: "Skills", hint: "company skill packages and references" }, - ]; + { + value: "company", + label: "Company", + hint: "name, branding, and company settings", + }, + { + value: "projects", + label: "Projects", + hint: "projects and workspace metadata", + }, + { value: "issues", label: "Tasks", hint: "tasks and recurring routines" }, + { value: "agents", label: "Agents", hint: "agent records and org structure" }, + { + value: "skills", + label: "Skills", + hint: "company skill packages and references", + }, +]; const IMPORT_PREVIEW_SAMPLE_LIMIT = 6; @@ -115,7 +127,12 @@ type ImportSelectionCatalog = { includedByDefault: boolean; files: string[]; }; - projects: Array<{ key: string; label: string; hint?: string; files: string[] }>; + projects: Array<{ + key: string; + label: string; + hint?: string; + files: string[]; + }>; issues: Array<{ key: string; label: string; hint?: string; files: string[] }>; agents: Array<{ key: string; label: string; hint?: string; files: string[] }>; skills: Array<{ key: string; label: string; hint?: string; files: string[] }>; @@ -130,8 +147,12 @@ type ImportSelectionState = { skills: Set; }; -function readPortableFileEntry(filePath: string, contents: Buffer): CompanyPortabilityFileEntry { - const contentType = binaryContentTypeByExtension[path.extname(filePath).toLowerCase()]; +function readPortableFileEntry( + filePath: string, + contents: Buffer, +): CompanyPortabilityFileEntry { + const contentType = + binaryContentTypeByExtension[path.extname(filePath).toLowerCase()]; if (!contentType) return contents.toString("utf8"); return { encoding: "base64", @@ -140,13 +161,17 @@ function readPortableFileEntry(filePath: string, contents: Buffer): CompanyPorta }; } -function portableFileEntryToWriteValue(entry: CompanyPortabilityFileEntry): string | Uint8Array { +function portableFileEntryToWriteValue( + entry: CompanyPortabilityFileEntry, +): string | Uint8Array { if (typeof entry === "string") return entry; return Buffer.from(entry.data, "base64"); } function isUuidLike(value: string): boolean { - return /^[0-9a-f]{8}-[0-9a-f]{4}-[1-5][0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$/i.test(value); + return /^[0-9a-f]{8}-[0-9a-f]{4}-[1-5][0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$/i.test( + value, + ); } function normalizeSelector(input: string): string { @@ -158,7 +183,10 @@ function parseInclude( fallback: CompanyPortabilityInclude = DEFAULT_EXPORT_INCLUDE, ): CompanyPortabilityInclude { if (!input || !input.trim()) return { ...fallback }; - const values = input.split(",").map((part) => part.trim().toLowerCase()).filter(Boolean); + const values = input + .split(",") + .map((part) => part.trim().toLowerCase()) + .filter(Boolean); const include = { company: values.includes("company"), agents: values.includes("agents"), @@ -166,8 +194,16 @@ function parseInclude( issues: values.includes("issues") || values.includes("tasks"), skills: values.includes("skills"), }; - if (!include.company && !include.agents && !include.projects && !include.issues && !include.skills) { - throw new Error("Invalid --include value. Use one or more of: company,agents,projects,issues,tasks,skills"); + if ( + !include.company && + !include.agents && + !include.projects && + !include.issues && + !include.skills + ) { + throw new Error( + "Invalid --include value. Use one or more of: company,agents,projects,issues,tasks,skills", + ); } return include; } @@ -176,21 +212,33 @@ function parseAgents(input: string | undefined): "all" | string[] { if (!input || !input.trim()) return "all"; const normalized = input.trim().toLowerCase(); if (normalized === "all") return "all"; - const values = input.split(",").map((part) => part.trim()).filter(Boolean); + const values = input + .split(",") + .map((part) => part.trim()) + .filter(Boolean); if (values.length === 0) return "all"; return Array.from(new Set(values)); } function parseCsvValues(input: string | undefined): string[] { if (!input || !input.trim()) return []; - return Array.from(new Set(input.split(",").map((part) => part.trim()).filter(Boolean))); + return Array.from( + new Set( + input + .split(",") + .map((part) => part.trim()) + .filter(Boolean), + ), + ); } function isInteractiveTerminal(): boolean { return Boolean(process.stdin.isTTY && process.stdout.isTTY); } -function resolveImportInclude(input: string | undefined): CompanyPortabilityInclude { +function resolveImportInclude( + input: string | undefined, +): CompanyPortabilityInclude { return parseInclude(input, DEFAULT_IMPORT_INCLUDE); } @@ -201,15 +249,24 @@ function normalizePortablePath(filePath: string): string { function shouldIncludePortableFile(filePath: string): boolean { const baseName = path.basename(filePath); const isMarkdown = baseName.endsWith(".md"); - const isTaskcoreYaml = baseName === ".taskcore.yaml" || baseName === ".taskcore.yml"; - const contentType = binaryContentTypeByExtension[path.extname(baseName).toLowerCase()]; + const isTaskcoreYaml = + baseName === ".taskcore.yaml" || baseName === ".taskcore.yml"; + const contentType = + binaryContentTypeByExtension[path.extname(baseName).toLowerCase()]; return isMarkdown || isTaskcoreYaml || Boolean(contentType); } -function findPortableExtensionPath(files: Record): string | null { +function findPortableExtensionPath( + files: Record, +): string | null { if (files[".taskcore.yaml"] !== undefined) return ".taskcore.yaml"; if (files[".taskcore.yml"] !== undefined) return ".taskcore.yml"; - return Object.keys(files).find((entry) => entry.endsWith("/.taskcore.yaml") || entry.endsWith("/.taskcore.yml")) ?? null; + return ( + Object.keys(files).find( + (entry) => + entry.endsWith("/.taskcore.yaml") || entry.endsWith("/.taskcore.yml"), + ) ?? null + ); } function collectFilesUnderDirectory( @@ -217,14 +274,24 @@ function collectFilesUnderDirectory( directory: string, opts?: { excludePrefixes?: string[] }, ): string[] { - const normalizedDirectory = normalizePortablePath(directory).replace(/\/+$/, ""); + const normalizedDirectory = normalizePortablePath(directory).replace( + /\/+$/, + "", + ); if (!normalizedDirectory) return []; const prefix = `${normalizedDirectory}/`; - const excluded = (opts?.excludePrefixes ?? []).map((entry) => normalizePortablePath(entry).replace(/\/+$/, "")).filter(Boolean); + const excluded = (opts?.excludePrefixes ?? []) + .map((entry) => normalizePortablePath(entry).replace(/\/+$/, "")) + .filter(Boolean); return Object.keys(files) .map(normalizePortablePath) .filter((filePath) => filePath.startsWith(prefix)) - .filter((filePath) => !excluded.some((excludePrefix) => filePath.startsWith(`${excludePrefix}/`))) + .filter( + (filePath) => + !excluded.some((excludePrefix) => + filePath.startsWith(`${excludePrefix}/`), + ), + ) .sort((left, right) => left.localeCompare(right)); } @@ -234,7 +301,9 @@ function collectEntityFiles( opts?: { excludePrefixes?: string[] }, ): string[] { const normalizedPath = normalizePortablePath(entryPath); - const directory = normalizedPath.includes("/") ? normalizedPath.slice(0, normalizedPath.lastIndexOf("/")) : ""; + const directory = normalizedPath.includes("/") + ? normalizedPath.slice(0, normalizedPath.lastIndexOf("/")) + : ""; const selected = new Set([normalizedPath]); if (directory) { for (const filePath of collectFilesUnderDirectory(files, directory, opts)) { @@ -244,30 +313,43 @@ function collectEntityFiles( return Array.from(selected).sort((left, right) => left.localeCompare(right)); } -export function buildImportSelectionCatalog(preview: CompanyPortabilityPreviewResult): ImportSelectionCatalog { +export function buildImportSelectionCatalog( + preview: CompanyPortabilityPreviewResult, +): ImportSelectionCatalog { const selectedAgentSlugs = new Set(preview.selectedAgentSlugs); const companyFiles = new Set(); - const companyPath = preview.manifest.company?.path ? normalizePortablePath(preview.manifest.company.path) : null; + const companyPath = preview.manifest.company?.path + ? normalizePortablePath(preview.manifest.company.path) + : null; if (companyPath) { companyFiles.add(companyPath); } - const readmePath = Object.keys(preview.files).find((entry) => normalizePortablePath(entry) === "README.md"); + const readmePath = Object.keys(preview.files).find( + (entry) => normalizePortablePath(entry) === "README.md", + ); if (readmePath) { companyFiles.add(normalizePortablePath(readmePath)); } - const logoPath = preview.manifest.company?.logoPath ? normalizePortablePath(preview.manifest.company.logoPath) : null; + const logoPath = preview.manifest.company?.logoPath + ? normalizePortablePath(preview.manifest.company.logoPath) + : null; if (logoPath && preview.files[logoPath] !== undefined) { companyFiles.add(logoPath); } return { company: { - includedByDefault: preview.include.company && preview.manifest.company !== null, - files: Array.from(companyFiles).sort((left, right) => left.localeCompare(right)), + includedByDefault: + preview.include.company && preview.manifest.company !== null, + files: Array.from(companyFiles).sort((left, right) => + left.localeCompare(right), + ), }, projects: preview.manifest.projects.map((project) => { const projectPath = normalizePortablePath(project.path); - const projectDir = projectPath.includes("/") ? projectPath.slice(0, projectPath.lastIndexOf("/")) : ""; + const projectDir = projectPath.includes("/") + ? projectPath.slice(0, projectPath.lastIndexOf("/")) + : ""; return { key: project.slug, label: project.name, @@ -281,21 +363,33 @@ export function buildImportSelectionCatalog(preview: CompanyPortabilityPreviewRe key: issue.slug, label: issue.title, hint: issue.identifier ?? issue.slug, - files: collectEntityFiles(preview.files, normalizePortablePath(issue.path)), + files: collectEntityFiles( + preview.files, + normalizePortablePath(issue.path), + ), })), agents: preview.manifest.agents - .filter((agent) => selectedAgentSlugs.size === 0 || selectedAgentSlugs.has(agent.slug)) + .filter( + (agent) => + selectedAgentSlugs.size === 0 || selectedAgentSlugs.has(agent.slug), + ) .map((agent) => ({ key: agent.slug, label: agent.name, hint: agent.slug, - files: collectEntityFiles(preview.files, normalizePortablePath(agent.path)), + files: collectEntityFiles( + preview.files, + normalizePortablePath(agent.path), + ), })), skills: preview.manifest.skills.map((skill) => ({ key: skill.slug, label: skill.name, hint: skill.slug, - files: collectEntityFiles(preview.files, normalizePortablePath(skill.path)), + files: collectEntityFiles( + preview.files, + normalizePortablePath(skill.path), + ), })), extensionPath: findPortableExtensionPath(preview.files), }; @@ -305,7 +399,9 @@ function toKeySet(items: Array<{ key: string }>): Set { return new Set(items.map((item) => item.key)); } -export function buildDefaultImportSelectionState(catalog: ImportSelectionCatalog): ImportSelectionState { +export function buildDefaultImportSelectionState( + catalog: ImportSelectionCatalog, +): ImportSelectionState { return { company: catalog.company.includedByDefault, projects: toKeySet(catalog.projects), @@ -315,15 +411,25 @@ export function buildDefaultImportSelectionState(catalog: ImportSelectionCatalog }; } -function countSelected(state: ImportSelectionState, group: ImportSelectableGroup): number { +function countSelected( + state: ImportSelectionState, + group: ImportSelectableGroup, +): number { return state[group].size; } -function countTotal(catalog: ImportSelectionCatalog, group: ImportSelectableGroup): number { +function countTotal( + catalog: ImportSelectionCatalog, + group: ImportSelectableGroup, +): number { return catalog[group].length; } -function summarizeGroupSelection(catalog: ImportSelectionCatalog, state: ImportSelectionState, group: ImportSelectableGroup): string { +function summarizeGroupSelection( + catalog: ImportSelectionCatalog, + state: ImportSelectionState, + group: ImportSelectableGroup, +): string { return `${countSelected(state, group)}/${countTotal(catalog, group)} selected`; } @@ -370,12 +476,18 @@ export function buildSelectedFilesFromImportSelection( } export function buildDefaultImportAdapterOverrides( - preview: Pick, + preview: Pick< + CompanyPortabilityPreviewResult, + "manifest" | "selectedAgentSlugs" + >, ): Record | undefined { const selectedAgentSlugs = new Set(preview.selectedAgentSlugs); const overrides = Object.fromEntries( preview.manifest.agents - .filter((agent) => selectedAgentSlugs.size === 0 || selectedAgentSlugs.has(agent.slug)) + .filter( + (agent) => + selectedAgentSlugs.size === 0 || selectedAgentSlugs.has(agent.slug), + ) .filter((agent) => agent.adapterType === "process") .map((agent) => [ agent.slug, @@ -392,26 +504,34 @@ function buildDefaultImportAdapterMessages( overrides: Record | undefined, ): string[] { if (!overrides) return []; - const adapterTypes = Array.from(new Set(Object.values(overrides).map((override) => override.adapterType))) - .map((adapterType) => adapterType.replace(/_/g, "-")); + const adapterTypes = Array.from( + new Set(Object.values(overrides).map((override) => override.adapterType)), + ).map((adapterType) => adapterType.replace(/_/g, "-")); const agentCount = Object.keys(overrides).length; return [ `Using ${adapterTypes.join(", ")} adapter${adapterTypes.length === 1 ? "" : "s"} for ${agentCount} imported ${pluralize(agentCount, "agent")} without an explicit adapter.`, ]; } -async function promptForImportSelection(preview: CompanyPortabilityPreviewResult): Promise { +async function promptForImportSelection( + preview: CompanyPortabilityPreviewResult, +): Promise { const catalog = buildImportSelectionCatalog(preview); const state = buildDefaultImportSelectionState(catalog); while (true) { - const choice = await p.select({ + const choice = await p.select< + ImportSelectableGroup | "company" | "confirm" + >({ message: "Select what Taskcore should import", options: [ { value: "company", label: state.company ? "Company: included" : "Company: skipped", - hint: catalog.company.files.length > 0 ? "toggle company metadata" : "no company metadata in package", + hint: + catalog.company.files.length > 0 + ? "toggle company metadata" + : "no company metadata in package", }, { value: "projects", @@ -448,9 +568,15 @@ async function promptForImportSelection(preview: CompanyPortabilityPreviewResult } if (choice === "confirm") { - const selectedFiles = buildSelectedFilesFromImportSelection(catalog, state); + const selectedFiles = buildSelectedFilesFromImportSelection( + catalog, + state, + ); if (selectedFiles.length === 0) { - p.note("Select at least one import target before confirming.", "Nothing selected"); + p.note( + "Select at least one import target before confirming.", + "Nothing selected", + ); continue; } return selectedFiles; @@ -458,7 +584,10 @@ async function promptForImportSelection(preview: CompanyPortabilityPreviewResult if (choice === "company") { if (catalog.company.files.length === 0) { - p.note("This package does not include company metadata to toggle.", "No company metadata"); + p.note( + "This package does not include company metadata to toggle.", + "No company metadata", + ); continue; } state.company = !state.company; @@ -468,7 +597,10 @@ async function promptForImportSelection(preview: CompanyPortabilityPreviewResult const group = choice; const groupItems = catalog[group]; if (groupItems.length === 0) { - p.note(`This package does not include any ${getGroupLabel(group).toLowerCase()}.`, `No ${getGroupLabel(group)}`); + p.note( + `This package does not include any ${getGroupLabel(group).toLowerCase()}.`, + `No ${getGroupLabel(group)}`, + ); continue; } @@ -492,13 +624,17 @@ async function promptForImportSelection(preview: CompanyPortabilityPreviewResult } function summarizeInclude(include: CompanyPortabilityInclude): string { - const labels = IMPORT_INCLUDE_OPTIONS - .filter((option) => include[option.value]) - .map((option) => option.label.toLowerCase()); + const labels = IMPORT_INCLUDE_OPTIONS.filter( + (option) => include[option.value], + ).map((option) => option.label.toLowerCase()); return labels.length > 0 ? labels.join(", ") : "nothing selected"; } -function formatSourceLabel(source: { type: "inline"; rootPath?: string | null } | { type: "github"; url: string }): string { +function formatSourceLabel( + source: + | { type: "inline"; rootPath?: string | null } + | { type: "github"; url: string }, +): string { if (source.type === "github") { return `GitHub: ${source.url}`; } @@ -506,18 +642,31 @@ function formatSourceLabel(source: { type: "inline"; rootPath?: string | null } } function formatTargetLabel( - target: { mode: "existing_company"; companyId?: string | null } | { mode: "new_company"; newCompanyName?: string | null }, + target: + | { mode: "existing_company"; companyId?: string | null } + | { mode: "new_company"; newCompanyName?: string | null }, preview?: CompanyPortabilityPreviewResult, ): string { if (target.mode === "existing_company") { const targetName = preview?.targetCompanyName?.trim(); - const targetId = preview?.targetCompanyId?.trim() || target.companyId?.trim() || "unknown-company"; + const targetId = + preview?.targetCompanyId?.trim() || + target.companyId?.trim() || + "unknown-company"; return targetName ? `${targetName} (${targetId})` : targetId; } - return target.newCompanyName?.trim() || preview?.manifest.company?.name || "new company"; + return ( + target.newCompanyName?.trim() || + preview?.manifest.company?.name || + "new company" + ); } -function pluralize(count: number, singular: string, plural = `${singular}s`): string { +function pluralize( + count: number, + singular: string, + plural = `${singular}s`, +): string { return count === 1 ? singular : plural; } @@ -536,7 +685,9 @@ function summarizePlanCounts( return `${plans.length} ${pluralize(plans.length, noun)} total (${parts.join(", ")})`; } -function summarizeImportAgentResults(agents: CompanyPortabilityImportResult["agents"]): string { +function summarizeImportAgentResults( + agents: CompanyPortabilityImportResult["agents"], +): string { if (agents.length === 0) return "0 agents changed"; const created = agents.filter((agent) => agent.action === "created").length; const updated = agents.filter((agent) => agent.action === "updated").length; @@ -548,11 +699,19 @@ function summarizeImportAgentResults(agents: CompanyPortabilityImportResult["age return `${agents.length} ${pluralize(agents.length, "agent")} total (${parts.join(", ")})`; } -function summarizeImportProjectResults(projects: CompanyPortabilityImportResult["projects"]): string { +function summarizeImportProjectResults( + projects: CompanyPortabilityImportResult["projects"], +): string { if (projects.length === 0) return "0 projects changed"; - const created = projects.filter((project) => project.action === "created").length; - const updated = projects.filter((project) => project.action === "updated").length; - const skipped = projects.filter((project) => project.action === "skipped").length; + const created = projects.filter( + (project) => project.action === "created", + ).length; + const updated = projects.filter( + (project) => project.action === "updated", + ).length; + const skipped = projects.filter( + (project) => project.action === "skipped", + ).length; const parts: string[] = []; if (created > 0) parts.push(`${created} created`); if (updated > 0) parts.push(`${updated} updated`); @@ -588,7 +747,9 @@ function appendPreviewExamples( lines.push(pc.bold(title)); const shown = entries.slice(0, IMPORT_PREVIEW_SAMPLE_LIMIT); for (const entry of shown) { - const reason = entry.reason?.trim() ? pc.dim(` (${entry.reason.trim()})`) : ""; + const reason = entry.reason?.trim() + ? pc.dim(` (${entry.reason.trim()})`) + : ""; lines.push(`- ${actionChip(entry.action)} ${entry.label}${reason}`); } if (entries.length > shown.length) { @@ -596,7 +757,11 @@ function appendPreviewExamples( } } -function appendMessageBlock(lines: string[], title: string, messages: string[]): void { +function appendMessageBlock( + lines: string[], + title: string, + messages: string[], +): void { if (messages.length === 0) return; lines.push(""); lines.push(pc.bold(title)); @@ -628,18 +793,32 @@ export function renderCompanyImportPreview( ]; if (preview.envInputs.length > 0) { - const requiredCount = preview.envInputs.filter((item) => item.requirement === "required").length; - lines.push(`- env inputs: ${preview.envInputs.length} (${requiredCount} required)`); + const requiredCount = preview.envInputs.filter( + (item) => item.requirement === "required", + ).length; + lines.push( + `- env inputs: ${preview.envInputs.length} (${requiredCount} required)`, + ); } lines.push(""); lines.push(pc.bold("Plan")); - lines.push(`- company: ${actionChip(preview.plan.companyAction === "none" ? "unchanged" : preview.plan.companyAction)}`); - lines.push(`- agents: ${summarizePlanCounts(preview.plan.agentPlans, "agent")}`); - lines.push(`- projects: ${summarizePlanCounts(preview.plan.projectPlans, "project")}`); - lines.push(`- tasks: ${summarizePlanCounts(preview.plan.issuePlans, "task")}`); + lines.push( + `- company: ${actionChip(preview.plan.companyAction === "none" ? "unchanged" : preview.plan.companyAction)}`, + ); + lines.push( + `- agents: ${summarizePlanCounts(preview.plan.agentPlans, "agent")}`, + ); + lines.push( + `- projects: ${summarizePlanCounts(preview.plan.projectPlans, "project")}`, + ); + lines.push( + `- tasks: ${summarizePlanCounts(preview.plan.issuePlans, "task")}`, + ); if (preview.include.skills) { - lines.push(`- skills: ${preview.manifest.skills.length} ${pluralize(preview.manifest.skills.length, "skill")} packaged`); + lines.push( + `- skills: ${preview.manifest.skills.length} ${pluralize(preview.manifest.skills.length, "skill")} packaged`, + ); } appendPreviewExamples( @@ -725,7 +904,11 @@ export function renderCompanyImportResult( return lines.join("\n"); } -function printCompanyImportView(title: string, body: string, opts?: { interactive?: boolean }): void { +function printCompanyImportView( + title: string, + body: string, + opts?: { interactive?: boolean }, +): void { if (opts?.interactive) { p.note(body, title); return; @@ -742,17 +925,24 @@ export function resolveCompanyImportApiPath(input: { if (input.targetMode === "existing_company") { const companyId = input.companyId?.trim(); if (!companyId) { - throw new Error("Existing-company imports require a companyId to resolve the API route."); + throw new Error( + "Existing-company imports require a companyId to resolve the API route.", + ); } return input.dryRun ? `/api/companies/${companyId}/imports/preview` : `/api/companies/${companyId}/imports/apply`; } - return input.dryRun ? "/api/companies/import/preview" : "/api/companies/import"; + return input.dryRun + ? "/api/companies/import/preview" + : "/api/companies/import"; } -export function buildCompanyDashboardUrl(apiBase: string, issuePrefix: string): string { +export function buildCompanyDashboardUrl( + apiBase: string, + issuePrefix: string, +): string { const url = new URL(apiBase); const normalizedPrefix = issuePrefix.trim().replace(/^\/+|\/+$/g, ""); url.pathname = `${url.pathname.replace(/\/+$/, "")}/${normalizedPrefix}/dashboard`; @@ -818,7 +1008,9 @@ export function isGithubShorthand(input: string): boolean { return segments.length >= 2 && segments.every(isGithubSegment); } -function normalizeGithubImportPath(input: string | null | undefined): string | null { +function normalizeGithubImportPath( + input: string | null | undefined, +): string | null { if (!input) return null; const trimmed = input.trim().replace(/^\/+|\/+$/g, ""); return trimmed || null; @@ -833,7 +1025,9 @@ function buildGithubImportUrl(input: { companyPath?: string | null; }): string { const host = input.hostname || "github.com"; - const url = new URL(`https://${host}/${input.owner}/${input.repo.replace(/\.git$/i, "")}`); + const url = new URL( + `https://${host}/${input.owner}/${input.repo.replace(/\.git$/i, "")}`, + ); const ref = input.ref?.trim(); if (ref) { url.searchParams.set("ref", ref); @@ -850,7 +1044,10 @@ function buildGithubImportUrl(input: { return url.toString(); } -export function normalizeGithubImportSource(input: string, refOverride?: string): string { +export function normalizeGithubImportSource( + input: string, + refOverride?: string, +): string { const trimmed = input.trim(); const ref = refOverride?.trim(); @@ -865,7 +1062,9 @@ export function normalizeGithubImportSource(input: string, refOverride?: string) } if (!looksLikeRepoUrl(trimmed)) { - throw new Error("GitHub source must be a GitHub or GitHub Enterprise URL, or owner/repo[/path] shorthand."); + throw new Error( + "GitHub source must be a GitHub or GitHub Enterprise URL, or owner/repo[/path] shorthand.", + ); } if (!ref) { return trimmed; @@ -881,18 +1080,44 @@ export function normalizeGithubImportSource(input: string, refOverride?: string) const owner = parts[0]!; const repo = parts[1]!; const existingPath = normalizeGithubImportPath(url.searchParams.get("path")); - const existingCompanyPath = normalizeGithubImportPath(url.searchParams.get("companyPath")); + const existingCompanyPath = normalizeGithubImportPath( + url.searchParams.get("companyPath"), + ); if (existingCompanyPath) { - return buildGithubImportUrl({ hostname, owner, repo, ref, companyPath: existingCompanyPath }); + return buildGithubImportUrl({ + hostname, + owner, + repo, + ref, + companyPath: existingCompanyPath, + }); } if (existingPath) { - return buildGithubImportUrl({ hostname, owner, repo, ref, path: existingPath }); + return buildGithubImportUrl({ + hostname, + owner, + repo, + ref, + path: existingPath, + }); } if (parts[2] === "tree") { - return buildGithubImportUrl({ hostname, owner, repo, ref, path: parts.slice(4).join("/") }); + return buildGithubImportUrl({ + hostname, + owner, + repo, + ref, + path: parts.slice(4).join("/"), + }); } if (parts[2] === "blob") { - return buildGithubImportUrl({ hostname, owner, repo, ref, companyPath: parts.slice(4).join("/") }); + return buildGithubImportUrl({ + hostname, + owner, + repo, + ref, + companyPath: parts.slice(4).join("/"), + }); } return buildGithubImportUrl({ hostname, owner, repo, ref }); } @@ -922,7 +1147,10 @@ async function collectPackageFiles( if (!entry.isFile()) continue; const relativePath = path.relative(root, absolutePath).replace(/\\/g, "/"); if (!shouldIncludePortableFile(relativePath)) continue; - files[relativePath] = readPortableFileEntry(relativePath, await readFile(absolutePath)); + files[relativePath] = readPortableFileEntry( + relativePath, + await readFile(absolutePath), + ); } } @@ -932,10 +1160,15 @@ export async function resolveInlineSourceFromPath(inputPath: string): Promise<{ }> { const resolved = path.resolve(inputPath); const resolvedStat = await stat(resolved); - if (resolvedStat.isFile() && path.extname(resolved).toLowerCase() === ".zip") { + if ( + resolvedStat.isFile() && + path.extname(resolved).toLowerCase() === ".zip" + ) { const archive = await readZipArchive(await readFile(resolved)); const filteredFiles = Object.fromEntries( - Object.entries(archive.files).filter(([relativePath]) => shouldIncludePortableFile(relativePath)), + Object.entries(archive.files).filter(([relativePath]) => + shouldIncludePortableFile(relativePath), + ), ); return { rootPath: archive.rootPath ?? path.basename(resolved, ".zip"), @@ -943,7 +1176,9 @@ export async function resolveInlineSourceFromPath(inputPath: string): Promise<{ }; } - const rootDir = resolvedStat.isDirectory() ? resolved : path.dirname(resolved); + const rootDir = resolvedStat.isDirectory() + ? resolved + : path.dirname(resolved); const files: Record = {}; await collectPackageFiles(rootDir, rootDir, files); return { @@ -952,7 +1187,10 @@ export async function resolveInlineSourceFromPath(inputPath: string): Promise<{ }; } -async function writeExportToFolder(outDir: string, exported: CompanyPortabilityExportResult): Promise { +async function writeExportToFolder( + outDir: string, + exported: CompanyPortabilityExportResult, +): Promise { const root = path.resolve(outDir); await mkdir(root, { recursive: true }); for (const [relativePath, content] of Object.entries(exported.files)) { @@ -973,14 +1211,18 @@ async function confirmOverwriteExportDirectory(outDir: string): Promise { const stats = await stat(root).catch(() => null); if (!stats) return; if (!stats.isDirectory()) { - throw new Error(`Export output path ${root} exists and is not a directory.`); + throw new Error( + `Export output path ${root} exists and is not a directory.`, + ); } const entries = await readdir(root); if (entries.length === 0) return; if (!process.stdin.isTTY || !process.stdout.isTTY) { - throw new Error(`Export output directory ${root} already contains files. Re-run interactively or choose an empty directory.`); + throw new Error( + `Export output directory ${root} already contains files. Re-run interactively or choose an empty directory.`, + ); } const confirmed = await p.confirm({ @@ -1008,7 +1250,9 @@ export function resolveCompanyForDeletion( } const idMatch = companies.find((company) => company.id === selector); - const prefixMatch = companies.find((company) => matchesPrefix(company, selector)); + const prefixMatch = companies.find((company) => + matchesPrefix(company, selector), + ); if (by === "id") { if (!idMatch) { @@ -1038,7 +1282,10 @@ export function resolveCompanyForDeletion( ); } -export function assertDeleteConfirmation(company: Company, opts: CompanyDeleteOptions): void { +export function assertDeleteConfirmation( + company: Company, + opts: CompanyDeleteOptions, +): void { if (!opts.yes) { throw new Error("Deletion requires --yes."); } @@ -1051,7 +1298,8 @@ export function assertDeleteConfirmation(company: Company, opts: CompanyDeleteOp } const confirmsById = confirm === company.id; - const confirmsByPrefix = confirm.toUpperCase() === company.issuePrefix.toUpperCase(); + const confirmsByPrefix = + confirm.toUpperCase() === company.issuePrefix.toUpperCase(); if (!confirmsById && !confirmsByPrefix) { throw new Error( `Confirmation '${confirm}' does not match target company. Expected ID '${company.id}' or prefix '${company.issuePrefix}'.`, @@ -1097,7 +1345,8 @@ export function registerCompanyCommands(program: Command): void { status: row.status, budgetMonthlyCents: row.budgetMonthlyCents, spentMonthlyCents: row.spentMonthlyCents, - requireBoardApprovalForNewAgents: row.requireBoardApprovalForNewAgents, + requireBoardApprovalForNewAgents: + row.requireBoardApprovalForNewAgents, })); for (const row of formatted) { console.log(formatInlineRecord(row)); @@ -1134,16 +1383,29 @@ export function registerCompanyCommands(program: Command): void { .option("--status ", "Filter by trace status") .option("--project-id ", "Filter by project ID") .option("--issue-id ", "Filter by issue ID") - .option("--from ", "Only include traces created at or after this timestamp") - .option("--to ", "Only include traces created at or before this timestamp") - .option("--shared-only", "Only include traces eligible for sharing/export") - .option("--include-payload", "Include stored payload snapshots in the response") + .option( + "--from ", + "Only include traces created at or after this timestamp", + ) + .option( + "--to ", + "Only include traces created at or before this timestamp", + ) + .option( + "--shared-only", + "Only include traces eligible for sharing/export", + ) + .option( + "--include-payload", + "Include stored payload snapshots in the response", + ) .action(async (opts: CompanyFeedbackOptions) => { try { const ctx = resolveCommandContext(opts, { requireCompany: true }); - const traces = (await ctx.api.get( - `/api/companies/${ctx.companyId}/feedback-traces${buildFeedbackTraceQuery(opts)}`, - )) ?? []; + const traces = + (await ctx.api.get( + `/api/companies/${ctx.companyId}/feedback-traces${buildFeedbackTraceQuery(opts)}`, + )) ?? []; if (ctx.json) { printOutput(traces, { json: true }); return; @@ -1176,32 +1438,53 @@ export function registerCompanyCommands(program: Command): void { .option("--status ", "Filter by trace status") .option("--project-id ", "Filter by project ID") .option("--issue-id ", "Filter by issue ID") - .option("--from ", "Only include traces created at or after this timestamp") - .option("--to ", "Only include traces created at or before this timestamp") - .option("--shared-only", "Only include traces eligible for sharing/export") - .option("--include-payload", "Include stored payload snapshots in the export") + .option( + "--from ", + "Only include traces created at or after this timestamp", + ) + .option( + "--to ", + "Only include traces created at or before this timestamp", + ) + .option( + "--shared-only", + "Only include traces eligible for sharing/export", + ) + .option( + "--include-payload", + "Include stored payload snapshots in the export", + ) .option("--out ", "Write export to a file path instead of stdout") .option("--format ", "Export format: json or ndjson", "ndjson") .action(async (opts: CompanyFeedbackOptions) => { try { const ctx = resolveCommandContext(opts, { requireCompany: true }); - const traces = (await ctx.api.get( - `/api/companies/${ctx.companyId}/feedback-traces${buildFeedbackTraceQuery(opts, opts.includePayload ?? true)}`, - )) ?? []; + const traces = + (await ctx.api.get( + `/api/companies/${ctx.companyId}/feedback-traces${buildFeedbackTraceQuery(opts, opts.includePayload ?? true)}`, + )) ?? []; const serialized = serializeFeedbackTraces(traces, opts.format); if (opts.out?.trim()) { await writeFile(opts.out, serialized, "utf8"); if (ctx.json) { printOutput( - { out: opts.out, count: traces.length, format: normalizeFeedbackTraceExportFormat(opts.format) }, + { + out: opts.out, + count: traces.length, + format: normalizeFeedbackTraceExportFormat(opts.format), + }, { json: true }, ); return; } - console.log(`Wrote ${traces.length} feedback trace(s) to ${opts.out}`); + console.log( + `Wrote ${traces.length} feedback trace(s) to ${opts.out}`, + ); return; } - process.stdout.write(`${serialized}${serialized.endsWith("\n") ? "" : "\n"}`); + process.stdout.write( + `${serialized}${serialized.endsWith("\n") ? "" : "\n"}`, + ); } catch (err) { handleCommandError(err); } @@ -1215,12 +1498,29 @@ export function registerCompanyCommands(program: Command): void { .description("Export a company into a portable markdown package") .argument("", "Company ID") .requiredOption("--out ", "Output directory") - .option("--include ", "Comma-separated include set: company,agents,projects,issues,tasks,skills", "company,agents") + .option( + "--include ", + "Comma-separated include set: company,agents,projects,issues,tasks,skills", + "company,agents", + ) .option("--skills ", "Comma-separated skill slugs/keys to export") - .option("--projects ", "Comma-separated project shortnames/ids to export") - .option("--issues ", "Comma-separated issue identifiers/ids to export") - .option("--project-issues ", "Comma-separated project shortnames/ids whose issues should be exported") - .option("--expand-referenced-skills", "Vendor skill contents instead of exporting upstream references", false) + .option( + "--projects ", + "Comma-separated project shortnames/ids to export", + ) + .option( + "--issues ", + "Comma-separated issue identifiers/ids to export", + ) + .option( + "--project-issues ", + "Comma-separated project shortnames/ids whose issues should be exported", + ) + .option( + "--expand-referenced-skills", + "Vendor skill contents instead of exporting upstream references", + false, + ) .action(async (companyId: string, opts: CompanyExportOptions) => { try { const ctx = resolveCommandContext(opts); @@ -1266,17 +1566,37 @@ export function registerCompanyCommands(program: Command): void { addCommonClientOptions( company .command("import") - .description("Import a portable markdown company package from local path, URL, or GitHub") + .description( + "Import a portable markdown company package from local path, URL, or GitHub", + ) .argument("", "Source path or URL") - .option("--include ", "Comma-separated include set: company,agents,projects,issues,tasks,skills") + .option( + "--include ", + "Comma-separated include set: company,agents,projects,issues,tasks,skills", + ) .option("--target ", "Target mode: new | existing") .option("-C, --company-id ", "Existing target company ID") .option("--new-company-name ", "Name override for --target new") - .option("--agents ", "Comma-separated agent slugs to import, or all", "all") - .option("--collision ", "Collision strategy: rename | skip | replace", "rename") - .option("--ref ", "Git ref to use for GitHub imports (branch, tag, or commit)") + .option( + "--agents ", + "Comma-separated agent slugs to import, or all", + "all", + ) + .option( + "--collision ", + "Collision strategy: rename | skip | replace", + "rename", + ) + .option( + "--ref ", + "Git ref to use for GitHub imports (branch, tag, or commit)", + ) .option("--taskcore-url ", "Alias for --api-base on this command") - .option("--yes", "Accept default selection and skip the pre-import confirmation prompt", false) + .option( + "--yes", + "Accept default selection and skip the pre-import confirmation prompt", + false, + ) .option("--dry-run", "Run preview only without applying", false) .action(async (fromPathOrUrl: string, opts: CompanyImportOptions) => { try { @@ -1292,51 +1612,75 @@ export function registerCompanyCommands(program: Command): void { const include = resolveImportInclude(opts.include); const agents = parseAgents(opts.agents); - const collision = (opts.collision ?? "rename").toLowerCase() as CompanyCollisionMode; + const collision = ( + opts.collision ?? "rename" + ).toLowerCase() as CompanyCollisionMode; if (!["rename", "skip", "replace"].includes(collision)) { - throw new Error("Invalid --collision value. Use: rename, skip, replace"); + throw new Error( + "Invalid --collision value. Use: rename, skip, replace", + ); } - const inferredTarget = opts.target ?? (opts.companyId || ctx.companyId ? "existing" : "new"); - const target = inferredTarget.toLowerCase() as CompanyImportTargetMode; + const inferredTarget = + opts.target ?? + (opts.companyId || ctx.companyId ? "existing" : "new"); + const target = + inferredTarget.toLowerCase() as CompanyImportTargetMode; if (!["new", "existing"].includes(target)) { throw new Error("Invalid --target value. Use: new | existing"); } - const existingTargetCompanyId = opts.companyId?.trim() || ctx.companyId; + const existingTargetCompanyId = + opts.companyId?.trim() || ctx.companyId; const targetPayload = target === "existing" ? { - mode: "existing_company" as const, - companyId: existingTargetCompanyId, - } + mode: "existing_company" as const, + companyId: existingTargetCompanyId, + } : { - mode: "new_company" as const, - newCompanyName: opts.newCompanyName?.trim() || null, - }; - - if (targetPayload.mode === "existing_company" && !targetPayload.companyId) { - throw new Error("Target existing company requires --company-id (or context default companyId)."); + mode: "new_company" as const, + newCompanyName: opts.newCompanyName?.trim() || null, + }; + + if ( + targetPayload.mode === "existing_company" && + !targetPayload.companyId + ) { + throw new Error( + "Target existing company requires --company-id (or context default companyId).", + ); } let sourcePayload: - | { type: "inline"; rootPath?: string | null; files: Record } + | { + type: "inline"; + rootPath?: string | null; + files: Record; + } | { type: "github"; url: string }; - const treatAsLocalPath = !isHttpUrl(from) && await pathExists(from); - const isGithubSource = looksLikeRepoUrl(from) || (isGithubShorthand(from) && !treatAsLocalPath); + const treatAsLocalPath = !isHttpUrl(from) && (await pathExists(from)); + const isGithubSource = + looksLikeRepoUrl(from) || + (isGithubShorthand(from) && !treatAsLocalPath); if (isHttpUrl(from) || isGithubSource) { if (!looksLikeRepoUrl(from) && !isGithubShorthand(from)) { throw new Error( "Only GitHub URLs and local paths are supported for import. " + - "Generic HTTP URLs are not supported. Use a GitHub or GitHub Enterprise URL (https://github.com/... or https://ghe.example.com/...) or a local directory path.", + "Generic HTTP URLs are not supported. Use a GitHub or GitHub Enterprise URL (https://github.com/... or https://ghe.example.com/...) or a local directory path.", ); } - sourcePayload = { type: "github", url: normalizeGithubImportSource(from, opts.ref) }; + sourcePayload = { + type: "github", + url: normalizeGithubImportSource(from, opts.ref), + }; } else { if (opts.ref?.trim()) { - throw new Error("--ref is only supported for GitHub import sources."); + throw new Error( + "--ref is only supported for GitHub import sources.", + ); } const inline = await resolveInlineSourceFromPath(from); sourcePayload = { @@ -1351,18 +1695,25 @@ export function registerCompanyCommands(program: Command): void { const previewApiPath = resolveCompanyImportApiPath({ dryRun: true, targetMode: targetPayload.mode, - companyId: targetPayload.mode === "existing_company" ? targetPayload.companyId : null, + companyId: + targetPayload.mode === "existing_company" + ? targetPayload.companyId + : null, }); let selectedFiles: string[] | undefined; if (interactiveView && !opts.yes && !opts.include?.trim()) { - const initialPreview = await ctx.api.post(previewApiPath, { - source: sourcePayload, - include, - target: targetPayload, - agents, - collisionStrategy: collision, - }); + const initialPreview = + await ctx.api.post( + previewApiPath, + { + source: sourcePayload, + include, + target: targetPayload, + agents, + collisionStrategy: collision, + }, + ); if (!initialPreview) { throw new Error("Import preview returned no data."); } @@ -1377,12 +1728,16 @@ export function registerCompanyCommands(program: Command): void { collisionStrategy: collision, selectedFiles, }; - const preview = await ctx.api.post(previewApiPath, previewPayload); + const preview = await ctx.api.post( + previewApiPath, + previewPayload, + ); if (!preview) { throw new Error("Import preview returned no data."); } const adapterOverrides = buildDefaultImportAdapterOverrides(preview); - const adapterMessages = buildDefaultImportAdapterMessages(adapterOverrides); + const adapterMessages = + buildDefaultImportAdapterMessages(adapterOverrides); if (opts.dryRun) { if (ctx.json) { @@ -1432,28 +1787,44 @@ export function registerCompanyCommands(program: Command): void { const importApiPath = resolveCompanyImportApiPath({ dryRun: false, targetMode: targetPayload.mode, - companyId: targetPayload.mode === "existing_company" ? targetPayload.companyId : null, - }); - const imported = await ctx.api.post(importApiPath, { - ...previewPayload, - adapterOverrides, + companyId: + targetPayload.mode === "existing_company" + ? targetPayload.companyId + : null, }); + const imported = await ctx.api.post( + importApiPath, + { + ...previewPayload, + adapterOverrides, + }, + ); if (!imported) { throw new Error("Import request returned no data."); } const tc = getTelemetryClient(); if (tc) { const isPrivate = sourcePayload.type !== "github"; - const sourceRef = sourcePayload.type === "github" ? sourcePayload.url : from; - trackCompanyImported(tc, { sourceType: sourcePayload.type, sourceRef, isPrivate }); + const sourceRef = + sourcePayload.type === "github" ? sourcePayload.url : from; + trackCompanyImported(tc, { + sourceType: sourcePayload.type, + sourceRef, + isPrivate, + }); } let companyUrl: string | undefined; if (!ctx.json) { try { - const importedCompany = await ctx.api.get(`/api/companies/${imported.company.id}`); + const importedCompany = await ctx.api.get( + `/api/companies/${imported.company.id}`, + ); const issuePrefix = importedCompany?.issuePrefix?.trim(); if (issuePrefix) { - companyUrl = buildCompanyDashboardUrl(ctx.api.apiBase, issuePrefix); + companyUrl = buildCompanyDashboardUrl( + ctx.api.apiBase, + issuePrefix, + ); } } catch { companyUrl = undefined; @@ -1480,7 +1851,9 @@ export function registerCompanyCommands(program: Command): void { if (openUrl(companyUrl)) { p.log.info(`Opened ${companyUrl}`); } else { - p.log.warn(`Could not open your browser automatically. Open this URL manually:\n${companyUrl}`); + p.log.warn( + `Could not open your browser automatically. Open this URL manually:\n${companyUrl}`, + ); } } } @@ -1496,21 +1869,25 @@ export function registerCompanyCommands(program: Command): void { .command("delete") .description("Delete a company by ID or shortname/prefix (destructive)") .argument("", "Company ID or issue prefix (for example PAP)") + .option("--by ", "Selector mode: auto | id | prefix", "auto") .option( - "--by ", - "Selector mode: auto | id | prefix", - "auto", + "--yes", + "Required safety flag to confirm destructive action", + false, ) - .option("--yes", "Required safety flag to confirm destructive action", false) .option( "--confirm ", "Required safety value: target company ID or shortname/prefix", ) .action(async (selector: string, opts: CompanyDeleteOptions) => { try { - const by = (opts.by ?? "auto").trim().toLowerCase() as CompanyDeleteSelectorMode; + const by = (opts.by ?? "auto") + .trim() + .toLowerCase() as CompanyDeleteSelectorMode; if (!["auto", "id", "prefix"].includes(by)) { - throw new Error(`Invalid --by mode '${opts.by}'. Expected one of: auto, id, prefix.`); + throw new Error( + `Invalid --by mode '${opts.by}'. Expected one of: auto, id, prefix.`, + ); } const ctx = resolveCommandContext(opts); @@ -1518,21 +1895,34 @@ export function registerCompanyCommands(program: Command): void { assertDeleteFlags(opts); let target: Company | null = null; - const shouldTryIdLookup = by === "id" || (by === "auto" && isUuidLike(normalizedSelector)); + const shouldTryIdLookup = + by === "id" || (by === "auto" && isUuidLike(normalizedSelector)); if (shouldTryIdLookup) { - const byId = await ctx.api.get(`/api/companies/${normalizedSelector}`, { ignoreNotFound: true }); + const byId = await ctx.api.get( + `/api/companies/${normalizedSelector}`, + { ignoreNotFound: true }, + ); if (byId) { target = byId; } else if (by === "id") { - throw new Error(`No company found by ID '${normalizedSelector}'.`); + throw new Error( + `No company found by ID '${normalizedSelector}'.`, + ); } } if (!target && ctx.companyId) { - const scoped = await ctx.api.get(`/api/companies/${ctx.companyId}`, { ignoreNotFound: true }); + const scoped = await ctx.api.get( + `/api/companies/${ctx.companyId}`, + { ignoreNotFound: true }, + ); if (scoped) { try { - target = resolveCompanyForDeletion([scoped], normalizedSelector, by); + target = resolveCompanyForDeletion( + [scoped], + normalizedSelector, + by, + ); } catch { // Fallback to board-wide lookup below. } @@ -1541,10 +1931,19 @@ export function registerCompanyCommands(program: Command): void { if (!target) { try { - const companies = (await ctx.api.get("/api/companies")) ?? []; - target = resolveCompanyForDeletion(companies, normalizedSelector, by); + const companies = + (await ctx.api.get("/api/companies")) ?? []; + target = resolveCompanyForDeletion( + companies, + normalizedSelector, + by, + ); } catch (error) { - if (error instanceof ApiRequestError && error.status === 403 && error.message.includes("Board access required")) { + if ( + error instanceof ApiRequestError && + error.status === 403 && + error.message.includes("Board access required") + ) { throw new Error( "Board access is required to resolve companies across the instance. Use a company ID/prefix for your current company, or run with board authentication.", ); @@ -1554,7 +1953,9 @@ export function registerCompanyCommands(program: Command): void { } if (!target) { - throw new Error(`No company found for selector '${normalizedSelector}'.`); + throw new Error( + `No company found for selector '${normalizedSelector}'.`, + ); } assertDeleteConfirmation(target, opts); diff --git a/cli/src/commands/client/context.ts b/cli/src/commands/client/context.ts index 30563cc..5263b8d 100644 --- a/cli/src/commands/client/context.ts +++ b/cli/src/commands/client/context.ts @@ -24,12 +24,17 @@ interface ContextSetOptions extends ContextOptions { } export function registerContextCommands(program: Command): void { - const context = program.command("context").description("Manage CLI client context profiles"); + const context = program + .command("context") + .description("Manage CLI client context profiles"); context .command("show") .description("Show current context and active profile") - .option("-d, --data-dir ", "Taskcore data directory root (isolates state from ~/.taskcore)") + .option( + "-d, --data-dir ", + "Taskcore data directory root (isolates state from ~/.taskcore)", + ) .option("--context ", "Path to CLI context file") .option("--profile ", "Profile to inspect") .option("--json", "Output raw JSON") @@ -50,7 +55,10 @@ export function registerContextCommands(program: Command): void { context .command("list") .description("List available context profiles") - .option("-d, --data-dir ", "Taskcore data directory root (isolates state from ~/.taskcore)") + .option( + "-d, --data-dir ", + "Taskcore data directory root (isolates state from ~/.taskcore)", + ) .option("--context ", "Path to CLI context file") .option("--json", "Output raw JSON") .action((opts: ContextOptions) => { @@ -69,7 +77,10 @@ export function registerContextCommands(program: Command): void { .command("use") .description("Set active context profile") .argument("", "Profile name") - .option("-d, --data-dir ", "Taskcore data directory root (isolates state from ~/.taskcore)") + .option( + "-d, --data-dir ", + "Taskcore data directory root (isolates state from ~/.taskcore)", + ) .option("--context ", "Path to CLI context file") .action((profile: string, opts: ContextOptions) => { setCurrentProfile(profile, opts.context); @@ -79,17 +90,24 @@ export function registerContextCommands(program: Command): void { context .command("set") .description("Set values on a profile") - .option("-d, --data-dir ", "Taskcore data directory root (isolates state from ~/.taskcore)") + .option( + "-d, --data-dir ", + "Taskcore data directory root (isolates state from ~/.taskcore)", + ) .option("--context ", "Path to CLI context file") .option("--profile ", "Profile name (default: current profile)") .option("--api-base ", "Default API base URL") .option("--company-id ", "Default company ID") - .option("--api-key-env-var-name ", "Env var containing API key (recommended)") + .option( + "--api-key-env-var-name ", + "Env var containing API key (recommended)", + ) .option("--use", "Set this profile as active") .option("--json", "Output raw JSON") .action((opts: ContextSetOptions) => { const existing = readContext(opts.context); - const targetProfile = opts.profile?.trim() || existing.currentProfile || "default"; + const targetProfile = + opts.profile?.trim() || existing.currentProfile || "default"; upsertProfile( targetProfile, diff --git a/cli/src/commands/client/dashboard.ts b/cli/src/commands/client/dashboard.ts index a3f22e5..0e19b6e 100644 --- a/cli/src/commands/client/dashboard.ts +++ b/cli/src/commands/client/dashboard.ts @@ -13,7 +13,9 @@ interface DashboardGetOptions extends BaseClientOptions { } export function registerDashboardCommands(program: Command): void { - const dashboard = program.command("dashboard").description("Dashboard summary operations"); + const dashboard = program + .command("dashboard") + .description("Dashboard summary operations"); addCommonClientOptions( dashboard @@ -23,7 +25,9 @@ export function registerDashboardCommands(program: Command): void { .action(async (opts: DashboardGetOptions) => { try { const ctx = resolveCommandContext(opts, { requireCompany: true }); - const row = await ctx.api.get(`/api/companies/${ctx.companyId}/dashboard`); + const row = await ctx.api.get( + `/api/companies/${ctx.companyId}/dashboard`, + ); printOutput(row, { json: ctx.json }); } catch (err) { handleCommandError(err); diff --git a/cli/src/commands/client/feedback.ts b/cli/src/commands/client/feedback.ts index d1d4d2f..c34f1d9 100644 --- a/cli/src/commands/client/feedback.ts +++ b/cli/src/commands/client/feedback.ts @@ -2,7 +2,11 @@ import { mkdir, readdir, readFile, stat, writeFile } from "node:fs/promises"; import path from "node:path"; import pc from "picocolors"; import { Command } from "commander"; -import type { Company, FeedbackTrace, FeedbackTraceBundle } from "@taskcore/shared"; +import type { + Company, + FeedbackTrace, + FeedbackTraceBundle, +} from "@taskcore/shared"; import { addCommonClientOptions, handleCommandError, @@ -73,7 +77,9 @@ interface FeedbackExportResult { } export function registerFeedbackCommands(program: Command): void { - const feedback = program.command("feedback").description("Inspect and export local feedback traces"); + const feedback = program + .command("feedback") + .description("Inspect and export local feedback traces"); addCommonClientOptions( feedback @@ -85,10 +91,23 @@ export function registerFeedbackCommands(program: Command): void { .option("--status ", "Filter by trace status") .option("--project-id ", "Filter by project ID") .option("--issue-id ", "Filter by issue ID") - .option("--from ", "Only include traces created at or after this timestamp") - .option("--to ", "Only include traces created at or before this timestamp") - .option("--shared-only", "Only include traces eligible for sharing/export") - .option("--payloads", "Include raw payload dumps in the terminal report", false) + .option( + "--from ", + "Only include traces created at or after this timestamp", + ) + .option( + "--to ", + "Only include traces created at or before this timestamp", + ) + .option( + "--shared-only", + "Only include traces eligible for sharing/export", + ) + .option( + "--payloads", + "Include raw payload dumps in the terminal report", + false, + ) .action(async (opts: FeedbackReportOptions) => { try { const ctx = resolveCommandContext(opts); @@ -107,13 +126,15 @@ export function registerFeedbackCommands(program: Command): void { ); return; } - console.log(renderFeedbackReport({ - apiBase: ctx.api.apiBase, - companyId, - traces, - summary, - includePayloads: Boolean(opts.payloads), - })); + console.log( + renderFeedbackReport({ + apiBase: ctx.api.apiBase, + companyId, + traces, + summary, + includePayloads: Boolean(opts.payloads), + }), + ); } catch (err) { handleCommandError(err); } @@ -124,29 +145,46 @@ export function registerFeedbackCommands(program: Command): void { addCommonClientOptions( feedback .command("export") - .description("Export feedback votes and raw trace bundles into a folder plus zip archive") + .description( + "Export feedback votes and raw trace bundles into a folder plus zip archive", + ) .option("-C, --company-id ", "Company ID (overrides context default)") .option("--target-type ", "Filter by target type") .option("--vote ", "Filter by vote value") .option("--status ", "Filter by trace status") .option("--project-id ", "Filter by project ID") .option("--issue-id ", "Filter by issue ID") - .option("--from ", "Only include traces created at or after this timestamp") - .option("--to ", "Only include traces created at or before this timestamp") - .option("--shared-only", "Only include traces eligible for sharing/export") - .option("--out ", "Output directory (default: ./feedback-export-)") + .option( + "--from ", + "Only include traces created at or after this timestamp", + ) + .option( + "--to ", + "Only include traces created at or before this timestamp", + ) + .option( + "--shared-only", + "Only include traces eligible for sharing/export", + ) + .option( + "--out ", + "Output directory (default: ./feedback-export-)", + ) .action(async (opts: FeedbackExportOptions) => { try { const ctx = resolveCommandContext(opts); const companyId = await resolveFeedbackCompanyId(ctx, opts.companyId); const traces = await fetchCompanyFeedbackTraces(ctx, companyId, opts); - const outputDir = path.resolve(opts.out?.trim() || defaultFeedbackExportDirName()); + const outputDir = path.resolve( + opts.out?.trim() || defaultFeedbackExportDirName(), + ); const exported = await writeFeedbackExportBundle({ apiBase: ctx.api.apiBase, companyId, traces, outputDir, - traceBundleFetcher: (trace) => fetchFeedbackTraceBundle(ctx, trace.id), + traceBundleFetcher: (trace) => + fetchFeedbackTraceBundle(ctx, trace.id), }); if (ctx.json) { printOutput( @@ -185,7 +223,10 @@ export async function resolveFeedbackCompanyId( return companyId; } -export function buildFeedbackTraceQuery(opts: FeedbackTraceQueryOptions, includePayload = true): string { +export function buildFeedbackTraceQuery( + opts: FeedbackTraceQueryOptions, + includePayload = true, +): string { const params = new URLSearchParams(); if (opts.targetType) params.set("targetType", opts.targetType); if (opts.vote) params.set("vote", opts.vote); @@ -200,13 +241,18 @@ export function buildFeedbackTraceQuery(opts: FeedbackTraceQueryOptions, include return query ? `?${query}` : ""; } -export function normalizeFeedbackTraceExportFormat(value: string | undefined): "json" | "ndjson" { +export function normalizeFeedbackTraceExportFormat( + value: string | undefined, +): "json" | "ndjson" { if (!value || value === "ndjson") return "ndjson"; if (value === "json") return "json"; throw new Error(`Unsupported export format: ${value}`); } -export function serializeFeedbackTraces(traces: FeedbackTrace[], format: string | undefined): string { +export function serializeFeedbackTraces( + traces: FeedbackTrace[], + format: string | undefined, +): string { if (normalizeFeedbackTraceExportFormat(format) === "json") { return JSON.stringify(traces, null, 2); } @@ -229,14 +275,18 @@ export async function fetchFeedbackTraceBundle( ctx: ResolvedClientContext, traceId: string, ): Promise { - const bundle = await ctx.api.get(`/api/feedback-traces/${traceId}/bundle`); + const bundle = await ctx.api.get( + `/api/feedback-traces/${traceId}/bundle`, + ); if (!bundle) { throw new Error(`Feedback trace bundle ${traceId} not found`); } return bundle; } -export function summarizeFeedbackTraces(traces: FeedbackTrace[]): FeedbackSummary { +export function summarizeFeedbackTraces( + traces: FeedbackTrace[], +): FeedbackSummary { const statuses: Record = {}; let thumbsUp = 0; let thumbsDown = 0; @@ -282,14 +332,22 @@ export function renderFeedbackReport(input: { lines.push(pc.bold(pc.cyan("Summary"))); lines.push(horizontalRule()); - lines.push(` ${pc.green(pc.bold(String(input.summary.thumbsUp)))} thumbs up`); - lines.push(` ${pc.red(pc.bold(String(input.summary.thumbsDown)))} thumbs down`); - lines.push(` ${pc.yellow(pc.bold(String(input.summary.withReason)))} downvotes with a reason`); + lines.push( + ` ${pc.green(pc.bold(String(input.summary.thumbsUp)))} thumbs up`, + ); + lines.push( + ` ${pc.red(pc.bold(String(input.summary.thumbsDown)))} thumbs down`, + ); + lines.push( + ` ${pc.yellow(pc.bold(String(input.summary.withReason)))} downvotes with a reason`, + ); lines.push(` ${pc.bold(String(input.summary.total))} total traces`); lines.push(""); lines.push(pc.dim("Export status:")); for (const status of ["pending", "sent", "local_only", "failed"]) { - lines.push(` ${padRight(status, 10)} ${input.summary.statuses[status] ?? 0}`); + lines.push( + ` ${padRight(status, 10)} ${input.summary.statuses[status] ?? 0}`, + ); } lines.push(""); lines.push(pc.bold(pc.cyan("Trace Details"))); @@ -325,7 +383,8 @@ export function renderFeedbackReport(input: { if (!trace.payloadSnapshot) continue; const issueRef = trace.issueIdentifier ?? trace.issueId; lines.push(` ${pc.bold(`${issueRef} (${trace.id.slice(0, 8)})`)}`); - const body = JSON.stringify(trace.payloadSnapshot, null, 2)?.split("\n") ?? []; + const body = + JSON.stringify(trace.payloadSnapshot, null, 2)?.split("\n") ?? []; for (const line of body) { lines.push(` ${pc.dim(line)}`); } @@ -334,7 +393,9 @@ export function renderFeedbackReport(input: { } lines.push(horizontalRule()); - lines.push(pc.dim(`Report complete. ${input.traces.length} trace(s) displayed.`)); + lines.push( + pc.dim(`Report complete. ${input.traces.length} trace(s) displayed.`), + ); lines.push(""); return lines.join("\n"); } @@ -359,7 +420,9 @@ export async function writeFeedbackExportBundle(input: { const issueSet = new Set(); for (const trace of input.traces) { - const issueRef = sanitizeFileSegment(trace.issueIdentifier ?? trace.issueId); + const issueRef = sanitizeFileSegment( + trace.issueIdentifier ?? trace.issueId, + ); const voteRecord = buildFeedbackVoteRecord(trace); const voteFileName = `${issueRef}-${trace.feedbackVoteId.slice(0, 8)}.json`; const traceFileName = `${issueRef}-${trace.id.slice(0, 8)}.json`; @@ -380,7 +443,11 @@ export async function writeFeedbackExportBundle(input: { if (input.traceBundleFetcher) { const bundle = await input.traceBundleFetcher(trace); const bundleDirName = `${issueRef}-${trace.id.slice(0, 8)}`; - const bundleDir = path.join(input.outputDir, "full-traces", bundleDirName); + const bundleDir = path.join( + input.outputDir, + "full-traces", + bundleDirName, + ); await mkdir(bundleDir, { recursive: true }); fullTraceDirs.push(bundleDirName); await writeFile( @@ -388,12 +455,20 @@ export async function writeFeedbackExportBundle(input: { `${JSON.stringify(bundle, null, 2)}\n`, "utf8", ); - fullTraceFiles.push(path.posix.join("full-traces", bundleDirName, "bundle.json")); + fullTraceFiles.push( + path.posix.join("full-traces", bundleDirName, "bundle.json"), + ); for (const file of bundle.files) { const targetPath = path.join(bundleDir, file.path); await mkdir(path.dirname(targetPath), { recursive: true }); await writeFile(targetPath, file.contents, "utf8"); - fullTraceFiles.push(path.posix.join("full-traces", bundleDirName, file.path.replace(/\\/g, "/"))); + fullTraceFiles.push( + path.posix.join( + "full-traces", + bundleDirName, + file.path.replace(/\\/g, "/"), + ), + ); } } } @@ -406,12 +481,18 @@ export async function writeFeedbackExportBundle(input: { summary: { ...summary, uniqueIssues: issueSet.size, - issues: Array.from(issueSet).sort((left, right) => left.localeCompare(right)), + issues: Array.from(issueSet).sort((left, right) => + left.localeCompare(right), + ), }, files: { votes: voteFiles.slice().sort((left, right) => left.localeCompare(right)), - traces: traceFiles.slice().sort((left, right) => left.localeCompare(right)), - fullTraces: fullTraceDirs.slice().sort((left, right) => left.localeCompare(right)), + traces: traceFiles + .slice() + .sort((left, right) => left.localeCompare(right)), + fullTraces: fullTraceDirs + .slice() + .sort((left, right) => left.localeCompare(right)), zip: path.basename(zipPath), }, }; @@ -427,7 +508,10 @@ export async function writeFeedbackExportBundle(input: { ...manifest.files.traces.map((file) => path.posix.join("traces", file)), ...fullTraceFiles, ]); - await writeFile(zipPath, createStoredZipArchive(archiveFiles, path.basename(input.outputDir))); + await writeFile( + zipPath, + createStoredZipArchive(archiveFiles, path.basename(input.outputDir)), + ); return { outputDir: input.outputDir, @@ -436,7 +520,9 @@ export async function writeFeedbackExportBundle(input: { }; } -export function renderFeedbackExportSummary(exported: FeedbackExportResult): string { +export function renderFeedbackExportSummary( + exported: FeedbackExportResult, +): string { const lines: string[] = []; lines.push(""); lines.push(pc.bold(pc.magenta("Taskcore Feedback Export"))); @@ -448,16 +534,30 @@ export function renderFeedbackExportSummary(exported: FeedbackExportResult): str lines.push(""); lines.push(pc.bold("Export Summary")); lines.push(horizontalRule()); - lines.push(` ${pc.green(pc.bold(String(exported.manifest.summary.thumbsUp)))} thumbs up`); - lines.push(` ${pc.red(pc.bold(String(exported.manifest.summary.thumbsDown)))} thumbs down`); - lines.push(` ${pc.yellow(pc.bold(String(exported.manifest.summary.withReason)))} with reason`); - lines.push(` ${pc.bold(String(exported.manifest.summary.uniqueIssues))} unique issues`); + lines.push( + ` ${pc.green(pc.bold(String(exported.manifest.summary.thumbsUp)))} thumbs up`, + ); + lines.push( + ` ${pc.red(pc.bold(String(exported.manifest.summary.thumbsDown)))} thumbs down`, + ); + lines.push( + ` ${pc.yellow(pc.bold(String(exported.manifest.summary.withReason)))} with reason`, + ); + lines.push( + ` ${pc.bold(String(exported.manifest.summary.uniqueIssues))} unique issues`, + ); lines.push(""); lines.push(pc.dim("Files:")); lines.push(` ${path.join(exported.outputDir, "index.json")}`); - lines.push(` ${path.join(exported.outputDir, "votes")} (${exported.manifest.files.votes.length} files)`); - lines.push(` ${path.join(exported.outputDir, "traces")} (${exported.manifest.files.traces.length} files)`); - lines.push(` ${path.join(exported.outputDir, "full-traces")} (${exported.manifest.files.fullTraces.length} bundles)`); + lines.push( + ` ${path.join(exported.outputDir, "votes")} (${exported.manifest.files.votes.length} files)`, + ); + lines.push( + ` ${path.join(exported.outputDir, "traces")} (${exported.manifest.files.traces.length} files)`, + ); + lines.push( + ` ${path.join(exported.outputDir, "full-traces")} (${exported.manifest.files.fullTraces.length} bundles)`, + ); lines.push(` ${exported.zipPath}`); lines.push(""); return lines.join("\n"); @@ -494,7 +594,10 @@ function asRecord(value: unknown): Record | null { return value as Record; } -function compactText(value: string | null | undefined, maxLength = 88): string | null { +function compactText( + value: string | null | undefined, + maxLength = 88, +): string | null { if (!value) return null; const compact = value.replace(/\s+/g, " ").trim(); if (!compact) return null; @@ -503,7 +606,8 @@ function compactText(value: string | null | undefined, maxLength = 88): string | } function formatTimestamp(value: unknown): string { - if (value instanceof Date) return value.toISOString().slice(0, 19).replace("T", " "); + if (value instanceof Date) + return value.toISOString().slice(0, 19).replace("T", " "); if (typeof value === "string") return value.slice(0, 19).replace("T", " "); return "-"; } @@ -517,7 +621,10 @@ function padRight(value: string, width: number): string { } function defaultFeedbackExportDirName(): string { - const iso = new Date().toISOString().replace(/[-:]/g, "").replace(/\.\d{3}Z$/, "Z"); + const iso = new Date() + .toISOString() + .replace(/[-:]/g, "") + .replace(/\.\d{3}Z$/, "Z"); return `feedback-export-${iso}`; } @@ -525,11 +632,15 @@ async function ensureEmptyOutputDirectory(outputDir: string): Promise { try { const info = await stat(outputDir); if (!info.isDirectory()) { - throw new Error(`Output path already exists and is not a directory: ${outputDir}`); + throw new Error( + `Output path already exists and is not a directory: ${outputDir}`, + ); } const entries = await readdir(outputDir); if (entries.length > 0) { - throw new Error(`Output directory already exists and is not empty: ${outputDir}`); + throw new Error( + `Output directory already exists and is not empty: ${outputDir}`, + ); } } catch (error) { const message = error instanceof Error ? error.message : ""; @@ -548,13 +659,19 @@ async function collectJsonFilesForArchive( const files: Record = {}; for (const relativePath of relativePaths) { const normalized = relativePath.replace(/\\/g, "/"); - files[normalized] = await readFile(path.join(outputDir, normalized), "utf8"); + files[normalized] = await readFile( + path.join(outputDir, normalized), + "utf8", + ); } return files; } function sanitizeFileSegment(value: string): string { - return value.replace(/[^a-zA-Z0-9._-]+/g, "-").replace(/^-+|-+$/g, "") || "feedback"; + return ( + value.replace(/[^a-zA-Z0-9._-]+/g, "-").replace(/^-+|-+$/g, "") || + "feedback" + ); } function writeUint16(target: Uint8Array, offset: number, value: number) { @@ -580,14 +697,19 @@ function crc32(bytes: Uint8Array) { return (crc ^ 0xffffffff) >>> 0; } -function createStoredZipArchive(files: Record, rootPath: string): Uint8Array { +function createStoredZipArchive( + files: Record, + rootPath: string, +): Uint8Array { const encoder = new TextEncoder(); const localChunks: Uint8Array[] = []; const centralChunks: Uint8Array[] = []; let localOffset = 0; let entryCount = 0; - for (const [relativePath, content] of Object.entries(files).sort(([left], [right]) => left.localeCompare(right))) { + for (const [relativePath, content] of Object.entries(files).sort( + ([left], [right]) => left.localeCompare(right), + )) { const fileName = encoder.encode(`${rootPath}/${relativePath}`); const body = encoder.encode(content); const checksum = crc32(body); @@ -622,9 +744,14 @@ function createStoredZipArchive(files: Record, rootPath: string) entryCount += 1; } - const centralDirectoryLength = centralChunks.reduce((sum, chunk) => sum + chunk.length, 0); + const centralDirectoryLength = centralChunks.reduce( + (sum, chunk) => sum + chunk.length, + 0, + ); const archive = new Uint8Array( - localChunks.reduce((sum, chunk) => sum + chunk.length, 0) + centralDirectoryLength + 22, + localChunks.reduce((sum, chunk) => sum + chunk.length, 0) + + centralDirectoryLength + + 22, ); let offset = 0; for (const chunk of localChunks) { diff --git a/cli/src/commands/client/issue.ts b/cli/src/commands/client/issue.ts index 7ab0a39..f5ff96c 100644 --- a/cli/src/commands/client/issue.ts +++ b/cli/src/commands/client/issue.ts @@ -91,13 +91,17 @@ export function registerIssueCommands(program: Command): void { .option("--status ", "Comma-separated statuses") .option("--assignee-agent-id ", "Filter by assignee agent ID") .option("--project-id ", "Filter by project ID") - .option("--match ", "Local text match on identifier/title/description") + .option( + "--match ", + "Local text match on identifier/title/description", + ) .action(async (opts: IssueBaseOptions) => { try { const ctx = resolveCommandContext(opts, { requireCompany: true }); const params = new URLSearchParams(); if (opts.status) params.set("status", opts.status); - if (opts.assigneeAgentId) params.set("assigneeAgentId", opts.assigneeAgentId); + if (opts.assigneeAgentId) + params.set("assigneeAgentId", opts.assigneeAgentId); if (opts.projectId) params.set("projectId", opts.projectId); const query = params.toString(); @@ -182,7 +186,10 @@ export function registerIssueCommands(program: Command): void { billingCode: opts.billingCode, }); - const created = await ctx.api.post(`/api/companies/${ctx.companyId}/issues`, payload); + const created = await ctx.api.post( + `/api/companies/${ctx.companyId}/issues`, + payload, + ); printOutput(created, { json: ctx.json }); } catch (err) { handleCommandError(err); @@ -207,7 +214,10 @@ export function registerIssueCommands(program: Command): void { .option("--request-depth ", "Request depth integer") .option("--billing-code ", "Billing code") .option("--comment ", "Optional comment to add with update") - .option("--hidden-at ", "Set hiddenAt timestamp or literal 'null'") + .option( + "--hidden-at ", + "Set hiddenAt timestamp or literal 'null'", + ) .action(async (issueId: string, opts: IssueUpdateOptions) => { try { const ctx = resolveCommandContext(opts); @@ -226,7 +236,9 @@ export function registerIssueCommands(program: Command): void { hiddenAt: parseHiddenAt(opts.hiddenAt), }); - const updated = await ctx.api.patch(`/api/issues/${issueId}`, payload); + const updated = await ctx.api.patch< + Issue & { comment?: IssueComment | null } + >(`/api/issues/${issueId}`, payload); printOutput(updated, { json: ctx.json }); } catch (err) { handleCommandError(err); @@ -248,7 +260,10 @@ export function registerIssueCommands(program: Command): void { body: opts.body, reopen: opts.reopen, }); - const comment = await ctx.api.post(`/api/issues/${issueId}/comments`, payload); + const comment = await ctx.api.post( + `/api/issues/${issueId}/comments`, + payload, + ); printOutput(comment, { json: ctx.json }); } catch (err) { handleCommandError(err); @@ -264,16 +279,29 @@ export function registerIssueCommands(program: Command): void { .option("--target-type ", "Filter by target type") .option("--vote ", "Filter by vote value") .option("--status ", "Filter by trace status") - .option("--from ", "Only include traces created at or after this timestamp") - .option("--to ", "Only include traces created at or before this timestamp") - .option("--shared-only", "Only include traces eligible for sharing/export") - .option("--include-payload", "Include stored payload snapshots in the response") + .option( + "--from ", + "Only include traces created at or after this timestamp", + ) + .option( + "--to ", + "Only include traces created at or before this timestamp", + ) + .option( + "--shared-only", + "Only include traces eligible for sharing/export", + ) + .option( + "--include-payload", + "Include stored payload snapshots in the response", + ) .action(async (issueId: string, opts: IssueFeedbackOptions) => { try { const ctx = resolveCommandContext(opts); - const traces = (await ctx.api.get( - `/api/issues/${issueId}/feedback-traces${buildFeedbackTraceQuery(opts)}`, - )) ?? []; + const traces = + (await ctx.api.get( + `/api/issues/${issueId}/feedback-traces${buildFeedbackTraceQuery(opts)}`, + )) ?? []; if (ctx.json) { printOutput(traces, { json: true }); return; @@ -303,32 +331,53 @@ export function registerIssueCommands(program: Command): void { .option("--target-type ", "Filter by target type") .option("--vote ", "Filter by vote value") .option("--status ", "Filter by trace status") - .option("--from ", "Only include traces created at or after this timestamp") - .option("--to ", "Only include traces created at or before this timestamp") - .option("--shared-only", "Only include traces eligible for sharing/export") - .option("--include-payload", "Include stored payload snapshots in the export") + .option( + "--from ", + "Only include traces created at or after this timestamp", + ) + .option( + "--to ", + "Only include traces created at or before this timestamp", + ) + .option( + "--shared-only", + "Only include traces eligible for sharing/export", + ) + .option( + "--include-payload", + "Include stored payload snapshots in the export", + ) .option("--out ", "Write export to a file path instead of stdout") .option("--format ", "Export format: json or ndjson", "ndjson") .action(async (issueId: string, opts: IssueFeedbackOptions) => { try { const ctx = resolveCommandContext(opts); - const traces = (await ctx.api.get( - `/api/issues/${issueId}/feedback-traces${buildFeedbackTraceQuery(opts, opts.includePayload ?? true)}`, - )) ?? []; + const traces = + (await ctx.api.get( + `/api/issues/${issueId}/feedback-traces${buildFeedbackTraceQuery(opts, opts.includePayload ?? true)}`, + )) ?? []; const serialized = serializeFeedbackTraces(traces, opts.format); if (opts.out?.trim()) { await writeFile(opts.out, serialized, "utf8"); if (ctx.json) { printOutput( - { out: opts.out, count: traces.length, format: normalizeFeedbackTraceExportFormat(opts.format) }, + { + out: opts.out, + count: traces.length, + format: normalizeFeedbackTraceExportFormat(opts.format), + }, { json: true }, ); return; } - console.log(`Wrote ${traces.length} feedback trace(s) to ${opts.out}`); + console.log( + `Wrote ${traces.length} feedback trace(s) to ${opts.out}`, + ); return; } - process.stdout.write(`${serialized}${serialized.endsWith("\n") ? "" : "\n"}`); + process.stdout.write( + `${serialized}${serialized.endsWith("\n") ? "" : "\n"}`, + ); } catch (err) { handleCommandError(err); } @@ -353,7 +402,10 @@ export function registerIssueCommands(program: Command): void { agentId: opts.agentId, expectedStatuses: parseCsv(opts.expectedStatuses), }); - const updated = await ctx.api.post(`/api/issues/${issueId}/checkout`, payload); + const updated = await ctx.api.post( + `/api/issues/${issueId}/checkout`, + payload, + ); printOutput(updated, { json: ctx.json }); } catch (err) { handleCommandError(err); @@ -369,7 +421,10 @@ export function registerIssueCommands(program: Command): void { .action(async (issueId: string, opts: BaseClientOptions) => { try { const ctx = resolveCommandContext(opts); - const updated = await ctx.api.post(`/api/issues/${issueId}/release`, {}); + const updated = await ctx.api.post( + `/api/issues/${issueId}/release`, + {}, + ); printOutput(updated, { json: ctx.json }); } catch (err) { handleCommandError(err); @@ -380,7 +435,10 @@ export function registerIssueCommands(program: Command): void { function parseCsv(value: string | undefined): string[] { if (!value) return []; - return value.split(",").map((v) => v.trim()).filter(Boolean); + return value + .split(",") + .map((v) => v.trim()) + .filter(Boolean); } function parseOptionalInt(value: string | undefined): number | undefined { diff --git a/cli/src/commands/client/plugin.ts b/cli/src/commands/client/plugin.ts index 6b9c34a..9c7b742 100644 --- a/cli/src/commands/client/plugin.ts +++ b/cli/src/commands/client/plugin.ts @@ -25,7 +25,6 @@ interface PluginRecord { updatedAt: string; } - // --------------------------------------------------------------------------- // Option types // --------------------------------------------------------------------------- @@ -92,7 +91,9 @@ function formatPlugin(p: PluginRecord): string { // --------------------------------------------------------------------------- export function registerPluginCommands(program: Command): void { - const plugin = program.command("plugin").description("Plugin lifecycle management"); + const plugin = program + .command("plugin") + .description("Plugin lifecycle management"); // ------------------------------------------------------------------------- // plugin list @@ -101,12 +102,19 @@ export function registerPluginCommands(program: Command): void { plugin .command("list") .description("List installed plugins") - .option("--status ", "Filter by status (ready, error, disabled, installed, upgrade_pending)") + .option( + "--status ", + "Filter by status (ready, error, disabled, installed, upgrade_pending)", + ) .action(async (opts: PluginListOptions) => { try { const ctx = resolveCommandContext(opts); - const qs = opts.status ? `?status=${encodeURIComponent(opts.status)}` : ""; - const plugins = await ctx.api.get(`/api/plugins${qs}`); + const qs = opts.status + ? `?status=${encodeURIComponent(opts.status)}` + : ""; + const plugins = await ctx.api.get( + `/api/plugins${qs}`, + ); if (ctx.json) { printOutput(plugins, { json: true }); @@ -136,13 +144,20 @@ export function registerPluginCommands(program: Command): void { .command("install ") .description( "Install a plugin from a local path or npm package.\n" + - " Examples:\n" + - " taskcore plugin install ./my-plugin # local path\n" + - " taskcore plugin install @acme/plugin-linear # npm package\n" + - " taskcore plugin install @acme/plugin-linear@1.2 # pinned version", + " Examples:\n" + + " taskcore plugin install ./my-plugin # local path\n" + + " taskcore plugin install @acme/plugin-linear # npm package\n" + + " taskcore plugin install @acme/plugin-linear@1.2 # pinned version", + ) + .option( + "-l, --local", + "Treat as a local filesystem path", + false, + ) + .option( + "--version ", + "Specific npm version to install (npm packages only)", ) - .option("-l, --local", "Treat as a local filesystem path", false) - .option("--version ", "Specific npm version to install (npm packages only)") .action(async (packageArg: string, opts: PluginInstallOptions) => { try { const ctx = resolveCommandContext(opts); @@ -167,11 +182,14 @@ export function registerPluginCommands(program: Command): void { ); } - const installedPlugin = await ctx.api.post("/api/plugins/install", { - packageName: resolvedPackage, - version: opts.version, - isLocalPath: isLocal, - }); + const installedPlugin = await ctx.api.post( + "/api/plugins/install", + { + packageName: resolvedPackage, + version: opts.version, + isLocalPath: isLocal, + }, + ); if (ctx.json) { printOutput(installedPlugin, { json: true }); @@ -206,9 +224,13 @@ export function registerPluginCommands(program: Command): void { .command("uninstall ") .description( "Uninstall a plugin by its plugin key or database ID.\n" + - " Use --force to hard-purge all state and config.", + " Use --force to hard-purge all state and config.", + ) + .option( + "--force", + "Purge all plugin state and config (hard delete)", + false, ) - .option("--force", "Purge all plugin state and config (hard delete)", false) .action(async (pluginKey: string, opts: PluginUninstallOptions) => { try { const ctx = resolveCommandContext(opts); @@ -234,7 +256,11 @@ export function registerPluginCommands(program: Command): void { return; } - console.log(pc.green(`βœ“ Uninstalled ${pc.bold(pluginKey)}${purge ? " (purged)" : ""}`)); + console.log( + pc.green( + `βœ“ Uninstalled ${pc.bold(pluginKey)}${purge ? " (purged)" : ""}`, + ), + ); } catch (err) { handleCommandError(err); } @@ -260,7 +286,11 @@ export function registerPluginCommands(program: Command): void { return; } - console.log(pc.green(`βœ“ Enabled ${pc.bold(pluginKey)} β€” status: ${result?.status ?? "unknown"}`)); + console.log( + pc.green( + `βœ“ Enabled ${pc.bold(pluginKey)} β€” status: ${result?.status ?? "unknown"}`, + ), + ); } catch (err) { handleCommandError(err); } @@ -286,7 +316,11 @@ export function registerPluginCommands(program: Command): void { return; } - console.log(pc.dim(`Disabled ${pc.bold(pluginKey)} β€” status: ${result?.status ?? "unknown"}`)); + console.log( + pc.dim( + `Disabled ${pc.bold(pluginKey)} β€” status: ${result?.status ?? "unknown"}`, + ), + ); } catch (err) { handleCommandError(err); } @@ -362,8 +396,8 @@ export function registerPluginCommands(program: Command): void { for (const ex of rows) { console.log( `${pc.bold(ex.displayName)} ${pc.dim(ex.pluginKey)}\n` + - ` ${ex.description}\n` + - ` ${pc.cyan(`taskcore plugin install ${ex.localPath}`)}`, + ` ${ex.description}\n` + + ` ${pc.cyan(`taskcore plugin install ${ex.localPath}`)}`, ); } } catch (err) { diff --git a/cli/src/commands/client/zip.ts b/cli/src/commands/client/zip.ts index 1324a3c..f1ff2fd 100644 --- a/cli/src/commands/client/zip.ts +++ b/cli/src/commands/client/zip.ts @@ -14,11 +14,7 @@ export const binaryContentTypeByExtension: Record = { }; function normalizeArchivePath(pathValue: string) { - return pathValue - .replace(/\\/g, "/") - .split("/") - .filter(Boolean) - .join("/"); + return pathValue.replace(/\\/g, "/").split("/").filter(Boolean).join("/"); } function readUint16(source: Uint8Array, offset: number) { @@ -27,11 +23,12 @@ function readUint16(source: Uint8Array, offset: number) { function readUint32(source: Uint8Array, offset: number) { return ( - source[offset]! | - (source[offset + 1]! << 8) | - (source[offset + 2]! << 16) | - (source[offset + 3]! << 24) - ) >>> 0; + (source[offset]! | + (source[offset + 1]! << 8) | + (source[offset + 2]! << 16) | + (source[offset + 3]! << 24)) >>> + 0 + ); } function sharedArchiveRoot(paths: string[]) { @@ -41,13 +38,19 @@ function sharedArchiveRoot(paths: string[]) { .filter((parts) => parts.length > 0); if (firstSegments.length === 0) return null; const candidate = firstSegments[0]![0]!; - return firstSegments.every((parts) => parts.length > 1 && parts[0] === candidate) + return firstSegments.every( + (parts) => parts.length > 1 && parts[0] === candidate, + ) ? candidate : null; } -function bytesToPortableFileEntry(pathValue: string, bytes: Uint8Array): CompanyPortabilityFileEntry { - const contentType = binaryContentTypeByExtension[path.extname(pathValue).toLowerCase()]; +function bytesToPortableFileEntry( + pathValue: string, + bytes: Uint8Array, +): CompanyPortabilityFileEntry { + const contentType = + binaryContentTypeByExtension[path.extname(pathValue).toLowerCase()]; if (!contentType) return textDecoder.decode(bytes); return { encoding: "base64", @@ -59,17 +62,22 @@ function bytesToPortableFileEntry(pathValue: string, bytes: Uint8Array): Company async function inflateZipEntry(compressionMethod: number, bytes: Uint8Array) { if (compressionMethod === 0) return bytes; if (compressionMethod !== 8) { - throw new Error("Unsupported zip archive: only STORE and DEFLATE entries are supported."); + throw new Error( + "Unsupported zip archive: only STORE and DEFLATE entries are supported.", + ); } return new Uint8Array(inflateRawSync(bytes)); } -export async function readZipArchive(source: ArrayBuffer | Uint8Array): Promise<{ +export async function readZipArchive( + source: ArrayBuffer | Uint8Array, +): Promise<{ rootPath: string | null; files: Record; }> { const bytes = source instanceof Uint8Array ? source : new Uint8Array(source); - const entries: Array<{ path: string; body: CompanyPortabilityFileEntry }> = []; + const entries: Array<{ path: string; body: CompanyPortabilityFileEntry }> = + []; let offset = 0; while (offset + 4 <= bytes.length) { @@ -90,7 +98,9 @@ export async function readZipArchive(source: ArrayBuffer | Uint8Array): Promise< const extraFieldLength = readUint16(bytes, offset + 28); if ((generalPurposeFlag & 0x0008) !== 0) { - throw new Error("Unsupported zip archive: data descriptors are not supported."); + throw new Error( + "Unsupported zip archive: data descriptors are not supported.", + ); } const nameOffset = offset + 30; @@ -100,11 +110,16 @@ export async function readZipArchive(source: ArrayBuffer | Uint8Array): Promise< throw new Error("Invalid zip archive: truncated file contents."); } - const rawArchivePath = textDecoder.decode(bytes.slice(nameOffset, nameOffset + fileNameLength)); + const rawArchivePath = textDecoder.decode( + bytes.slice(nameOffset, nameOffset + fileNameLength), + ); const archivePath = normalizeArchivePath(rawArchivePath); const isDirectoryEntry = /\/$/.test(rawArchivePath.replace(/\\/g, "/")); if (archivePath && !isDirectoryEntry) { - const entryBytes = await inflateZipEntry(compressionMethod, bytes.slice(bodyOffset, bodyEnd)); + const entryBytes = await inflateZipEntry( + compressionMethod, + bytes.slice(bodyOffset, bodyEnd), + ); entries.push({ path: archivePath, body: bytesToPortableFileEntry(archivePath, entryBytes), diff --git a/cli/src/commands/configure.ts b/cli/src/commands/configure.ts index c4faf0a..83ca1ba 100644 --- a/cli/src/commands/configure.ts +++ b/cli/src/commands/configure.ts @@ -1,6 +1,11 @@ import * as p from "@clack/prompts"; import pc from "picocolors"; -import { readConfig, writeConfig, configExists, resolveConfigPath } from "../config/store.js"; +import { + readConfig, + writeConfig, + configExists, + resolveConfigPath, +} from "../config/store.js"; import type { TaskcoreConfig } from "../config/schema.js"; import { ensureLocalSecretsKeyFile } from "../config/secrets-key.js"; import { promptDatabase } from "../prompts/database.js"; @@ -17,7 +22,13 @@ import { } from "../config/home.js"; import { printTaskcoreCliBanner } from "../utils/banner.js"; -type Section = "llm" | "database" | "logging" | "server" | "storage" | "secrets"; +type Section = + | "llm" + | "database" + | "logging" + | "server" + | "storage" + | "secrets"; const SECTION_LABELS: Record = { llm: "LLM Provider", @@ -101,7 +112,9 @@ export async function configure(opts: { let section: Section | undefined = opts.section as Section | undefined; if (section && !SECTION_LABELS[section]) { - p.log.error(`Unknown section: ${section}. Choose from: ${Object.keys(SECTION_LABELS).join(", ")}`); + p.log.error( + `Unknown section: ${section}. Choose from: ${Object.keys(SECTION_LABELS).join(", ")}`, + ); p.outro(""); return; } @@ -162,13 +175,27 @@ export async function configure(opts: { { const keyResult = ensureLocalSecretsKeyFile(config, configPath); if (keyResult.status === "created") { - p.log.success(`Created local secrets key file at ${pc.dim(keyResult.path)}`); + p.log.success( + `Created local secrets key file at ${pc.dim(keyResult.path)}`, + ); } else if (keyResult.status === "existing") { - p.log.message(pc.dim(`Using existing local secrets key file at ${keyResult.path}`)); + p.log.message( + pc.dim( + `Using existing local secrets key file at ${keyResult.path}`, + ), + ); } else if (keyResult.status === "skipped_provider") { - p.log.message(pc.dim("Skipping local key file management for non-local provider")); + p.log.message( + pc.dim( + "Skipping local key file management for non-local provider", + ), + ); } else { - p.log.message(pc.dim("Skipping local key file management because TASKCORE_SECRETS_MASTER_KEY is set")); + p.log.message( + pc.dim( + "Skipping local key file management because TASKCORE_SECRETS_MASTER_KEY is set", + ), + ); } } break; diff --git a/cli/src/commands/db-backup.ts b/cli/src/commands/db-backup.ts index b65beb8..b402bd9 100644 --- a/cli/src/commands/db-backup.ts +++ b/cli/src/commands/db-backup.ts @@ -18,13 +18,22 @@ type DbBackupOptions = { json?: boolean; }; -function resolveConnectionString(configPath?: string): { value: string; source: string } { +function resolveConnectionString(configPath?: string): { + value: string; + source: string; +} { const envUrl = process.env.DATABASE_URL?.trim(); if (envUrl) return { value: envUrl, source: "DATABASE_URL" }; const config = readConfig(configPath); - if (config?.database.mode === "postgres" && config.database.connectionString?.trim()) { - return { value: config.database.connectionString.trim(), source: "config.database.connectionString" }; + if ( + config?.database.mode === "postgres" && + config.database.connectionString?.trim() + ) { + return { + value: config.database.connectionString.trim(), + source: "config.database.connectionString", + }; } const port = config?.database.embeddedPostgresPort ?? 54329; @@ -34,10 +43,15 @@ function resolveConnectionString(configPath?: string): { value: string; source: }; } -function normalizeRetentionDays(value: number | undefined, fallback: number): number { +function normalizeRetentionDays( + value: number | undefined, + fallback: number, +): number { const candidate = value ?? fallback; if (!Number.isInteger(candidate) || candidate < 1) { - throw new Error(`Invalid retention days '${String(candidate)}'. Use a positive integer.`); + throw new Error( + `Invalid retention days '${String(candidate)}'. Use a positive integer.`, + ); } return candidate; } @@ -54,7 +68,8 @@ export async function dbBackupCommand(opts: DbBackupOptions): Promise { const config = readConfig(opts.config); const connection = resolveConnectionString(opts.config); const defaultDir = resolveDefaultBackupDir(resolveTaskcoreInstanceId()); - const configuredDir = opts.dir?.trim() || config?.database.backup.dir || defaultDir; + const configuredDir = + opts.dir?.trim() || config?.database.backup.dir || defaultDir; const backupDir = resolveBackupDir(configuredDir); const retentionDays = normalizeRetentionDays( opts.retentionDays, diff --git a/cli/src/commands/doctor.ts b/cli/src/commands/doctor.ts index 5ea380c..c152274 100644 --- a/cli/src/commands/doctor.ts +++ b/cli/src/commands/doctor.ts @@ -53,7 +53,8 @@ export async function doctor(opts: { status: "fail", message: `Could not read config: ${err instanceof Error ? err.message : String(err)}`, canRepair: false, - repairHint: "Run `taskcore configure --section database` or `taskcore onboard`", + repairHint: + "Run `taskcore configure --section database` or `taskcore onboard`", }; results.push(readResult); printResult(readResult); @@ -136,7 +137,8 @@ async function maybeRepair( result: CheckResult, opts: { repair?: boolean; yes?: boolean }, ): Promise { - if (result.status === "pass" || !result.canRepair || !result.repair) return false; + if (result.status === "pass" || !result.canRepair || !result.repair) + return false; if (!opts.repair) return false; let shouldRepair = opts.yes; @@ -155,7 +157,9 @@ async function maybeRepair( p.log.success(`Repaired: ${result.name}`); return true; } catch (err) { - p.log.error(`Repair failed: ${err instanceof Error ? err.message : String(err)}`); + p.log.error( + `Repair failed: ${err instanceof Error ? err.message : String(err)}`, + ); } } return false; @@ -179,7 +183,11 @@ async function runRepairableCheck(input: { return result; } -function printSummary(results: CheckResult[]): { passed: number; warned: number; failed: number } { +function printSummary(results: CheckResult[]): { + passed: number; + warned: number; + failed: number; +} { const passed = results.filter((r) => r.status === "pass").length; const warned = results.filter((r) => r.status === "warn").length; const failed = results.filter((r) => r.status === "fail").length; @@ -192,7 +200,9 @@ function printSummary(results: CheckResult[]): { passed: number; warned: number; p.note(parts.join(", "), "Summary"); if (failed > 0) { - p.outro(pc.red("Some checks failed. Fix the issues above and re-run doctor.")); + p.outro( + pc.red("Some checks failed. Fix the issues above and re-run doctor."), + ); } else if (warned > 0) { p.outro(pc.yellow("All critical checks passed with some warnings.")); } else { diff --git a/cli/src/commands/env.ts b/cli/src/commands/env.ts index 7e669df..3469b4d 100644 --- a/cli/src/commands/env.ts +++ b/cli/src/commands/env.ts @@ -1,7 +1,11 @@ import * as p from "@clack/prompts"; import pc from "picocolors"; import type { TaskcoreConfig } from "../config/schema.js"; -import { configExists, readConfig, resolveConfigPath } from "../config/store.js"; +import { + configExists, + readConfig, + resolveConfigPath, +} from "../config/store.js"; import { readAgentJwtSecretFromEnv, readAgentJwtSecretFromEnvFile, @@ -56,8 +60,13 @@ export async function envCommand(opts: { config?: string }): Promise { } const rows = collectDeploymentEnvRows(config, configPath); - const missingRequired = rows.filter((row) => row.required && row.source === "missing"); - const sortedRows = rows.sort((a, b) => Number(b.required) - Number(a.required) || a.key.localeCompare(b.key)); + const missingRequired = rows.filter( + (row) => row.required && row.source === "missing", + ); + const sortedRows = rows.sort( + (a, b) => + Number(b.required) - Number(a.required) || a.key.localeCompare(b.key), + ); const requiredRows = sortedRows.filter((row) => row.required); const optionalRows = sortedRows.filter((row) => !row.required); @@ -67,7 +76,12 @@ export async function envCommand(opts: { config?: string }): Promise { p.log.message(pc.bold(title)); for (const entry of entries) { - const status = entry.source === "missing" ? pc.red("missing") : entry.source === "default" ? pc.yellow("default") : pc.green("set"); + const status = + entry.source === "missing" + ? pc.red("missing") + : entry.source === "default" + ? pc.yellow("default") + : pc.green("set"); const sourceNote = { env: "environment", config: "config", @@ -84,9 +98,13 @@ export async function envCommand(opts: { config?: string }): Promise { formatSection("Required environment variables", requiredRows); formatSection("Optional environment variables", optionalRows); - const exportRows = rows.map((row) => (row.source === "missing" ? { ...row, value: "" } : row)); + const exportRows = rows.map((row) => + row.source === "missing" ? { ...row, value: "" } : row, + ); const uniqueRows = uniqueByKey(exportRows); - const exportBlock = uniqueRows.map((row) => `export ${row.key}=${quoteShellValue(row.value)}`).join("\n"); + const exportBlock = uniqueRows + .map((row) => `export ${row.key}=${quoteShellValue(row.value)}`) + .join("\n"); if (configReadError) { p.log.error(`Could not load config cleanly: ${configReadError}`); @@ -109,15 +127,25 @@ export async function envCommand(opts: { config?: string }): Promise { p.outro("Done"); } -function collectDeploymentEnvRows(config: TaskcoreConfig | null, configPath: string): EnvVarRow[] { +function collectDeploymentEnvRows( + config: TaskcoreConfig | null, + configPath: string, +): EnvVarRow[] { const agentJwtEnvFile = resolveAgentJwtEnvFile(configPath); const jwtEnv = readAgentJwtSecretFromEnv(configPath); - const jwtFile = jwtEnv ? null : readAgentJwtSecretFromEnvFile(agentJwtEnvFile); + const jwtFile = jwtEnv + ? null + : readAgentJwtSecretFromEnvFile(agentJwtEnvFile); const jwtSource = jwtEnv ? "env" : jwtFile ? "file" : "missing"; - const dbUrl = process.env.DATABASE_URL ?? config?.database?.connectionString ?? ""; + const dbUrl = + process.env.DATABASE_URL ?? config?.database?.connectionString ?? ""; const databaseMode = config?.database?.mode ?? "embedded-postgres"; - const dbUrlSource: EnvSource = process.env.DATABASE_URL ? "env" : config?.database?.connectionString ? "config" : "missing"; + const dbUrlSource: EnvSource = process.env.DATABASE_URL + ? "env" + : config?.database?.connectionString + ? "config" + : "missing"; const publicUrl = process.env.TASKCORE_PUBLIC_URL ?? process.env.TASKCORE_AUTH_PUBLIC_BASE_URL ?? @@ -125,14 +153,15 @@ function collectDeploymentEnvRows(config: TaskcoreConfig | null, configPath: str process.env.BETTER_AUTH_BASE_URL ?? config?.auth?.publicBaseUrl ?? ""; - const publicUrlSource: EnvSource = - process.env.TASKCORE_PUBLIC_URL + const publicUrlSource: EnvSource = process.env.TASKCORE_PUBLIC_URL + ? "env" + : process.env.TASKCORE_AUTH_PUBLIC_BASE_URL || + process.env.BETTER_AUTH_URL || + process.env.BETTER_AUTH_BASE_URL ? "env" - : process.env.TASKCORE_AUTH_PUBLIC_BASE_URL || process.env.BETTER_AUTH_URL || process.env.BETTER_AUTH_BASE_URL - ? "env" - : config?.auth?.publicBaseUrl - ? "config" - : "missing"; + : config?.auth?.publicBaseUrl + ? "config" + : "missing"; let trustedOriginsDefault = ""; if (publicUrl) { try { @@ -142,7 +171,9 @@ function collectDeploymentEnvRows(config: TaskcoreConfig | null, configPath: str } } - const heartbeatInterval = process.env.HEARTBEAT_SCHEDULER_INTERVAL_MS ?? DEFAULT_HEARTBEAT_SCHEDULER_INTERVAL_MS; + const heartbeatInterval = + process.env.HEARTBEAT_SCHEDULER_INTERVAL_MS ?? + DEFAULT_HEARTBEAT_SCHEDULER_INTERVAL_MS; const heartbeatEnabled = process.env.HEARTBEAT_SCHEDULER_ENABLED ?? "true"; const secretsProvider = process.env.TASKCORE_SECRETS_PROVIDER ?? @@ -176,9 +207,7 @@ function collectDeploymentEnvRows(config: TaskcoreConfig | null, configPath: str config?.storage?.s3?.endpoint ?? ""; const storageS3Prefix = - process.env.TASKCORE_STORAGE_S3_PREFIX ?? - config?.storage?.s3?.prefix ?? - ""; + process.env.TASKCORE_STORAGE_S3_PREFIX ?? config?.storage?.s3?.prefix ?? ""; const storageS3ForcePathStyle = process.env.TASKCORE_STORAGE_S3_FORCE_PATH_STYLE ?? String(config?.storage?.s3?.forcePathStyle ?? false); @@ -210,8 +239,14 @@ function collectDeploymentEnvRows(config: TaskcoreConfig | null, configPath: str key: "PORT", value: process.env.PORT ?? - (config?.server?.port !== undefined ? String(config.server.port) : "3100"), - source: process.env.PORT ? "env" : config?.server?.port !== undefined ? "config" : "default", + (config?.server?.port !== undefined + ? String(config.server.port) + : "3100"), + source: process.env.PORT + ? "env" + : config?.server?.port !== undefined + ? "config" + : "default", required: false, note: "HTTP listen port", }, @@ -235,7 +270,9 @@ function collectDeploymentEnvRows(config: TaskcoreConfig | null, configPath: str }, { key: "TASKCORE_AGENT_JWT_TTL_SECONDS", - value: process.env.TASKCORE_AGENT_JWT_TTL_SECONDS ?? DEFAULT_AGENT_JWT_TTL_SECONDS, + value: + process.env.TASKCORE_AGENT_JWT_TTL_SECONDS ?? + DEFAULT_AGENT_JWT_TTL_SECONDS, source: process.env.TASKCORE_AGENT_JWT_TTL_SECONDS ? "env" : "default", required: false, note: "JWT lifetime in seconds", @@ -249,7 +286,8 @@ function collectDeploymentEnvRows(config: TaskcoreConfig | null, configPath: str }, { key: "TASKCORE_AGENT_JWT_AUDIENCE", - value: process.env.TASKCORE_AGENT_JWT_AUDIENCE ?? DEFAULT_AGENT_JWT_AUDIENCE, + value: + process.env.TASKCORE_AGENT_JWT_AUDIENCE ?? DEFAULT_AGENT_JWT_AUDIENCE, source: process.env.TASKCORE_AGENT_JWT_AUDIENCE ? "env" : "default", required: false, note: "JWT audience", @@ -406,6 +444,6 @@ function uniqueByKey(rows: EnvVarRow[]): EnvVarRow[] { } function quoteShellValue(value: string): string { - if (value === "") return "\"\""; + if (value === "") return '""'; return `'${value.replaceAll("'", "'\\''")}'`; } diff --git a/cli/src/commands/heartbeat-run.ts b/cli/src/commands/heartbeat-run.ts index 0de141f..2d6dde5 100644 --- a/cli/src/commands/heartbeat-run.ts +++ b/cli/src/commands/heartbeat-run.ts @@ -1,12 +1,27 @@ import { setTimeout as delay } from "node:timers/promises"; import pc from "picocolors"; -import type { Agent, HeartbeatRun, HeartbeatRunEvent, HeartbeatRunStatus } from "@taskcore/shared"; +import type { + Agent, + HeartbeatRun, + HeartbeatRunEvent, + HeartbeatRunStatus, +} from "@taskcore/shared"; import { getCLIAdapter } from "../adapters/index.js"; import { resolveCommandContext } from "./client/common.js"; -const HEARTBEAT_SOURCES = ["timer", "assignment", "on_demand", "automation"] as const; +const HEARTBEAT_SOURCES = [ + "timer", + "assignment", + "on_demand", + "automation", +] as const; const HEARTBEAT_TRIGGERS = ["manual", "ping", "callback", "system"] as const; -const TERMINAL_STATUSES = new Set(["succeeded", "failed", "cancelled", "timed_out"]); +const TERMINAL_STATUSES = new Set([ + "succeeded", + "failed", + "cancelled", + "timed_out", +]); const POLL_INTERVAL_MS = 200; type HeartbeatSource = (typeof HEARTBEAT_SOURCES)[number]; @@ -62,7 +77,9 @@ export async function heartbeatRun(opts: HeartbeatRunOptions): Promise { const source = HEARTBEAT_SOURCES.includes(opts.source as HeartbeatSource) ? (opts.source as HeartbeatSource) : "on_demand"; - const triggerDetail = HEARTBEAT_TRIGGERS.includes(opts.trigger as HeartbeatTrigger) + const triggerDetail = HEARTBEAT_TRIGGERS.includes( + opts.trigger as HeartbeatTrigger, + ) ? (opts.trigger as HeartbeatTrigger) : "manual"; @@ -99,7 +116,11 @@ export async function heartbeatRun(opts: HeartbeatRunOptions): Promise { } const run = invokeRes as HeartbeatRun; - console.log(pc.cyan(`Invoked heartbeat run ${run.id} for agent ${agent.name} (${agent.id})`)); + console.log( + pc.cyan( + `Invoked heartbeat run ${run.id} for agent ${agent.name} (${agent.id})`, + ), + ); const runId = run.id; let activeRunId: string | null = null; @@ -107,35 +128,46 @@ export async function heartbeatRun(opts: HeartbeatRunOptions): Promise { let logOffset = 0; let stdoutJsonBuffer = ""; - const printRawChunk = (stream: "stdout" | "stderr" | "system", chunk: string) => { - if (stream === "stdout") process.stdout.write(pc.green("[stdout] ") + chunk); - else if (stream === "stderr") process.stdout.write(pc.red("[stderr] ") + chunk); + const printRawChunk = ( + stream: "stdout" | "stderr" | "system", + chunk: string, + ) => { + if (stream === "stdout") + process.stdout.write(pc.green("[stdout] ") + chunk); + else if (stream === "stderr") + process.stdout.write(pc.red("[stderr] ") + chunk); else process.stdout.write(pc.yellow("[system] ") + chunk); }; const printAdapterInvoke = (payload: Record) => { - const adapterType = typeof payload.adapterType === "string" ? payload.adapterType : "unknown"; + const adapterType = + typeof payload.adapterType === "string" ? payload.adapterType : "unknown"; const command = typeof payload.command === "string" ? payload.command : ""; const cwd = typeof payload.cwd === "string" ? payload.cwd : ""; const args = Array.isArray(payload.commandArgs) && - (payload.commandArgs as unknown[]).every((v) => typeof v === "string") + (payload.commandArgs as unknown[]).every((v) => typeof v === "string") ? (payload.commandArgs as string[]) : []; const env = - typeof payload.env === "object" && payload.env !== null && !Array.isArray(payload.env) + typeof payload.env === "object" && + payload.env !== null && + !Array.isArray(payload.env) ? (payload.env as Record) : null; const prompt = typeof payload.prompt === "string" ? payload.prompt : ""; const context = - typeof payload.context === "object" && payload.context !== null && !Array.isArray(payload.context) + typeof payload.context === "object" && + payload.context !== null && + !Array.isArray(payload.context) ? (payload.context as Record) : null; console.log(pc.cyan(`Adapter: ${adapterType}`)); if (cwd) console.log(pc.cyan(`Working dir: ${cwd}`)); if (command) { - const rendered = args.length > 0 ? `${command} ${args.join(" ")}` : command; + const rendered = + args.length > 0 ? `${command} ${args.join(" ")}` : command; console.log(pc.cyan(`Command: ${rendered}`)); } if (env) { @@ -155,7 +187,10 @@ export async function heartbeatRun(opts: HeartbeatRunOptions): Promise { const adapterType: AdapterType = agent.adapterType ?? "claude_local"; const cliAdapter = getCLIAdapter(adapterType); - const handleStreamChunk = (stream: "stdout" | "stderr" | "system", chunk: string) => { + const handleStreamChunk = ( + stream: "stdout" | "stderr" | "system", + chunk: string, + ) => { if (debug) { printRawChunk(stream, chunk); return; @@ -177,11 +212,12 @@ export async function heartbeatRun(opts: HeartbeatRunOptions): Promise { const handleEvent = (event: HeartbeatRunEventRecord) => { const payload = normalizePayload(event.payload); if (event.runId !== runId) return; - const eventType = typeof event.eventType === "string" - ? event.eventType - : typeof event.type === "string" - ? event.type - : ""; + const eventType = + typeof event.eventType === "string" + ? event.eventType + : typeof event.type === "string" + ? event.type + : ""; if (eventType === "heartbeat.run.status") { const status = typeof payload.status === "string" ? payload.status : null; @@ -191,14 +227,19 @@ export async function heartbeatRun(opts: HeartbeatRunOptions): Promise { } else if (eventType === "adapter.invoke") { printAdapterInvoke(payload); } else if (eventType === "heartbeat.run.log") { - const stream = typeof payload.stream === "string" ? payload.stream : "system"; + const stream = + typeof payload.stream === "string" ? payload.stream : "system"; const chunk = typeof payload.chunk === "string" ? payload.chunk : ""; if (!chunk) return; if (stream === "stdout" || stream === "stderr" || stream === "system") { handleStreamChunk(stream, chunk); } } else if (typeof event.message === "string") { - console.log(pc.gray(`[event] ${eventType || "heartbeat.run.event"}: ${event.message}`)); + console.log( + pc.gray( + `[event] ${eventType || "heartbeat.run.event"}: ${event.message}`, + ), + ); } lastEventSeq = Math.max(lastEventSeq, event.seq ?? 0); @@ -219,13 +260,16 @@ export async function heartbeatRun(opts: HeartbeatRunOptions): Promise { const events = await api.get( `/api/heartbeat-runs/${activeRunId}/events?afterSeq=${lastEventSeq}&limit=100`, ); - for (const event of Array.isArray(events) ? (events as HeartbeatRunEventRecord[]) : []) { + for (const event of Array.isArray(events) + ? (events as HeartbeatRunEventRecord[]) + : []) { handleEvent(event); } - const runList = (await api.get<(HeartbeatRun | null)[]>( - `/api/companies/${agent.companyId}/heartbeat-runs?agentId=${agent.id}`, - )) || []; + const runList = + (await api.get<(HeartbeatRun | null)[]>( + `/api/companies/${agent.companyId}/heartbeat-runs?agentId=${agent.id}`, + )) || []; const currentRun = runList.find((r) => r && r.id === activeRunId) ?? null; if (!currentRun) { @@ -292,21 +336,32 @@ export async function heartbeatRun(opts: HeartbeatRunOptions): Promise { if (finalRun) { const resultObj = asRecord(finalRun.resultJson); if (resultObj) { - const subtype = typeof resultObj.subtype === "string" ? resultObj.subtype : ""; + const subtype = + typeof resultObj.subtype === "string" ? resultObj.subtype : ""; const isError = resultObj.is_error === true; - const errors = Array.isArray(resultObj.errors) ? resultObj.errors.map(asErrorText).filter(Boolean) : []; - const resultText = typeof resultObj.result === "string" ? resultObj.result.trim() : ""; + const errors = Array.isArray(resultObj.errors) + ? resultObj.errors.map(asErrorText).filter(Boolean) + : []; + const resultText = + typeof resultObj.result === "string" ? resultObj.result.trim() : ""; if (subtype || isError || errors.length > 0 || resultText) { console.log(pc.red("Claude result details:")); if (subtype) console.log(pc.red(` subtype: ${subtype}`)); if (isError) console.log(pc.red(" is_error: true")); - if (errors.length > 0) console.log(pc.red(` errors: ${errors.join(" | ")}`)); + if (errors.length > 0) + console.log(pc.red(` errors: ${errors.join(" | ")}`)); if (resultText) console.log(pc.red(` result: ${resultText}`)); } } - const stderrExcerpt = typeof finalRun.stderrExcerpt === "string" ? finalRun.stderrExcerpt.trim() : ""; - const stdoutExcerpt = typeof finalRun.stdoutExcerpt === "string" ? finalRun.stdoutExcerpt.trim() : ""; + const stderrExcerpt = + typeof finalRun.stderrExcerpt === "string" + ? finalRun.stderrExcerpt.trim() + : ""; + const stdoutExcerpt = + typeof finalRun.stdoutExcerpt === "string" + ? finalRun.stdoutExcerpt.trim() + : ""; if (stderrExcerpt) { console.log(pc.red("stderr excerpt:")); console.log(stderrExcerpt); @@ -324,14 +379,20 @@ export async function heartbeatRun(opts: HeartbeatRunOptions): Promise { } function normalizePayload(payload: unknown): Record { - return typeof payload === "object" && payload !== null ? (payload as Record) : {}; + return typeof payload === "object" && payload !== null + ? (payload as Record) + : {}; } -function safeParseLogLine(line: string): { stream: "stdout" | "stderr" | "system"; chunk: string } | null { +function safeParseLogLine( + line: string, +): { stream: "stdout" | "stderr" | "system"; chunk: string } | null { try { const parsed = JSON.parse(line) as { stream?: unknown; chunk?: unknown }; const stream = - parsed.stream === "stdout" || parsed.stream === "stderr" || parsed.stream === "system" + parsed.stream === "stdout" || + parsed.stream === "stderr" || + parsed.stream === "system" ? parsed.stream : "system"; const chunk = typeof parsed.chunk === "string" ? parsed.chunk : ""; diff --git a/cli/src/commands/onboard.ts b/cli/src/commands/onboard.ts index 7d0298b..1fa77ff 100644 --- a/cli/src/commands/onboard.ts +++ b/cli/src/commands/onboard.ts @@ -17,7 +17,12 @@ import { type SecretProvider, type StorageProvider, } from "@taskcore/shared"; -import { configExists, readConfig, resolveConfigPath, writeConfig } from "../config/store.js"; +import { + configExists, + readConfig, + resolveConfigPath, + writeConfig, +} from "../config/store.js"; import type { TaskcoreConfig } from "../config/schema.js"; import { ensureAgentJwtSecret, resolveAgentJwtEnvFile } from "../config/env.js"; import { ensureLocalSecretsKeyFile } from "../config/secrets-key.js"; @@ -54,7 +59,10 @@ type OnboardOptions = { bind?: BindMode; }; -type OnboardDefaults = Pick; +type OnboardDefaults = Pick< + TaskcoreConfig, + "database" | "logging" | "server" | "auth" | "storage" | "secrets" +>; const TAILNET_BIND_WARNING = "No Tailscale address was detected during setup. The saved config will stay on loopback until Tailscale is available or TASKCORE_TAILNET_BIND_HOST is set."; @@ -106,7 +114,10 @@ function parseNumberFromEnv(rawValue: string | undefined): number | null { return parsed; } -function parseEnumFromEnv(rawValue: string | undefined, allowedValues: readonly T[]): T | null { +function parseEnumFromEnv( + rawValue: string | undefined, + allowedValues: readonly T[], +): T | null { if (!rawValue) return null; return allowedValues.includes(rawValue as T) ? (rawValue as T) : null; } @@ -116,11 +127,16 @@ function resolvePathFromEnv(rawValue: string | undefined): string | null { return path.resolve(expandHomePrefix(rawValue.trim())); } -function describeServerBinding(server: Pick): string { +function describeServerBinding( + server: Pick< + TaskcoreConfig["server"], + "bind" | "customBindHost" | "host" | "port" + >, +): string { const bind = server.bind ?? inferBindModeFromHost(server.host); const detail = bind === "custom" - ? server.customBindHost ?? server.host + ? (server.customBindHost ?? server.host) : bind === "tailnet" ? "detected tailscale address" : server.host; @@ -139,33 +155,41 @@ function quickstartDefaultsFromEnv(opts?: { preferTrustedLocal?: boolean }): { const databaseUrl = process.env.DATABASE_URL?.trim() || undefined; const publicUrl = preferTrustedLocal ? undefined - : ( - process.env.TASKCORE_PUBLIC_URL?.trim() || + : process.env.TASKCORE_PUBLIC_URL?.trim() || process.env.TASKCORE_AUTH_PUBLIC_BASE_URL?.trim() || process.env.BETTER_AUTH_URL?.trim() || process.env.BETTER_AUTH_BASE_URL?.trim() || - undefined - ); + undefined; const deploymentMode = preferTrustedLocal ? "local_trusted" - : (parseEnumFromEnv(process.env.TASKCORE_DEPLOYMENT_MODE, DEPLOYMENT_MODES) ?? "local_trusted"); + : (parseEnumFromEnv( + process.env.TASKCORE_DEPLOYMENT_MODE, + DEPLOYMENT_MODES, + ) ?? "local_trusted"); const deploymentExposureFromEnv = parseEnumFromEnv( process.env.TASKCORE_DEPLOYMENT_EXPOSURE, DEPLOYMENT_EXPOSURES, ); const deploymentExposure = - deploymentMode === "local_trusted" ? "private" : (deploymentExposureFromEnv ?? "private"); - const bindFromEnv = parseEnumFromEnv(process.env.TASKCORE_BIND, BIND_MODES); - const customBindHostFromEnv = process.env.TASKCORE_BIND_HOST?.trim() || undefined; + deploymentMode === "local_trusted" + ? "private" + : (deploymentExposureFromEnv ?? "private"); + const bindFromEnv = parseEnumFromEnv( + process.env.TASKCORE_BIND, + BIND_MODES, + ); + const customBindHostFromEnv = + process.env.TASKCORE_BIND_HOST?.trim() || undefined; const hostFromEnv = process.env.HOST?.trim() || undefined; const configuredBindHost = customBindHostFromEnv ?? hostFromEnv; const bind = preferTrustedLocal ? "loopback" - : ( - deploymentMode === "local_trusted" - ? "loopback" - : (bindFromEnv ?? (configuredBindHost ? inferBindModeFromHost(configuredBindHost) : "lan")) - ); + : deploymentMode === "local_trusted" + ? "loopback" + : (bindFromEnv ?? + (configuredBindHost + ? inferBindModeFromHost(configuredBindHost) + : "lan")); const resolvedBind = resolveRuntimeBind({ bind, host: hostFromEnv ?? (bind === "loopback" ? "127.0.0.1" : "0.0.0.0"), @@ -177,29 +201,34 @@ function quickstartDefaultsFromEnv(opts?: { preferTrustedLocal?: boolean }): { process.env.TASKCORE_AUTH_BASE_URL_MODE, AUTH_BASE_URL_MODES, ); - const authBaseUrlMode = authBaseUrlModeFromEnv ?? (authPublicBaseUrl ? "explicit" : "auto"); + const authBaseUrlMode = + authBaseUrlModeFromEnv ?? (authPublicBaseUrl ? "explicit" : "auto"); const allowedHostnamesFromEnv = process.env.TASKCORE_ALLOWED_HOSTNAMES - ? process.env.TASKCORE_ALLOWED_HOSTNAMES - .split(",") - .map((value) => value.trim().toLowerCase()) - .filter((value) => value.length > 0) + ? process.env.TASKCORE_ALLOWED_HOSTNAMES.split(",") + .map((value) => value.trim().toLowerCase()) + .filter((value) => value.length > 0) : []; const hostnameFromPublicUrl = publicUrl ? (() => { - try { - return new URL(publicUrl).hostname.trim().toLowerCase(); - } catch { - return null; - } - })() + try { + return new URL(publicUrl).hostname.trim().toLowerCase(); + } catch { + return null; + } + })() : null; const storageProvider = - parseEnumFromEnv(process.env.TASKCORE_STORAGE_PROVIDER, STORAGE_PROVIDERS) ?? - defaultStorage.provider; + parseEnumFromEnv( + process.env.TASKCORE_STORAGE_PROVIDER, + STORAGE_PROVIDERS, + ) ?? defaultStorage.provider; const secretsProvider = - parseEnumFromEnv(process.env.TASKCORE_SECRETS_PROVIDER, SECRET_PROVIDERS) ?? - defaultSecrets.provider; - const databaseBackupEnabled = parseBooleanFromEnv(process.env.TASKCORE_DB_BACKUP_ENABLED) ?? true; + parseEnumFromEnv( + process.env.TASKCORE_SECRETS_PROVIDER, + SECRET_PROVIDERS, + ) ?? defaultSecrets.provider; + const databaseBackupEnabled = + parseBooleanFromEnv(process.env.TASKCORE_DB_BACKUP_ENABLED) ?? true; const databaseBackupIntervalMinutes = Math.max( 1, parseNumberFromEnv(process.env.TASKCORE_DB_BACKUP_INTERVAL_MINUTES) ?? 60, @@ -218,7 +247,9 @@ function quickstartDefaultsFromEnv(opts?: { preferTrustedLocal?: boolean }): { enabled: databaseBackupEnabled, intervalMinutes: databaseBackupIntervalMinutes, retentionDays: databaseBackupRetentionDays, - dir: resolvePathFromEnv(process.env.TASKCORE_DB_BACKUP_DIR) ?? resolveDefaultBackupDir(instanceId), + dir: + resolvePathFromEnv(process.env.TASKCORE_DB_BACKUP_DIR) ?? + resolveDefaultBackupDir(instanceId), }, }, logging: { @@ -229,10 +260,17 @@ function quickstartDefaultsFromEnv(opts?: { preferTrustedLocal?: boolean }): { deploymentMode, exposure: deploymentExposure, bind: resolvedBind.bind, - ...(resolvedBind.customBindHost ? { customBindHost: resolvedBind.customBindHost } : {}), + ...(resolvedBind.customBindHost + ? { customBindHost: resolvedBind.customBindHost } + : {}), host: resolvedBind.host, port: Number(process.env.PORT) || 3100, - allowedHostnames: Array.from(new Set([...allowedHostnamesFromEnv, ...(hostnameFromPublicUrl ? [hostnameFromPublicUrl] : [])])), + allowedHostnames: Array.from( + new Set([ + ...allowedHostnamesFromEnv, + ...(hostnameFromPublicUrl ? [hostnameFromPublicUrl] : []), + ]), + ), serveUi: parseBooleanFromEnv(process.env.SERVE_UI) ?? true, }, auth: { @@ -244,21 +282,30 @@ function quickstartDefaultsFromEnv(opts?: { preferTrustedLocal?: boolean }): { provider: storageProvider, localDisk: { baseDir: - resolvePathFromEnv(process.env.TASKCORE_STORAGE_LOCAL_DIR) ?? defaultStorage.localDisk.baseDir, + resolvePathFromEnv(process.env.TASKCORE_STORAGE_LOCAL_DIR) ?? + defaultStorage.localDisk.baseDir, }, s3: { - bucket: process.env.TASKCORE_STORAGE_S3_BUCKET ?? defaultStorage.s3.bucket, - region: process.env.TASKCORE_STORAGE_S3_REGION ?? defaultStorage.s3.region, - endpoint: process.env.TASKCORE_STORAGE_S3_ENDPOINT ?? defaultStorage.s3.endpoint, - prefix: process.env.TASKCORE_STORAGE_S3_PREFIX ?? defaultStorage.s3.prefix, + bucket: + process.env.TASKCORE_STORAGE_S3_BUCKET ?? defaultStorage.s3.bucket, + region: + process.env.TASKCORE_STORAGE_S3_REGION ?? defaultStorage.s3.region, + endpoint: + process.env.TASKCORE_STORAGE_S3_ENDPOINT ?? + defaultStorage.s3.endpoint, + prefix: + process.env.TASKCORE_STORAGE_S3_PREFIX ?? defaultStorage.s3.prefix, forcePathStyle: - parseBooleanFromEnv(process.env.TASKCORE_STORAGE_S3_FORCE_PATH_STYLE) ?? - defaultStorage.s3.forcePathStyle, + parseBooleanFromEnv( + process.env.TASKCORE_STORAGE_S3_FORCE_PATH_STYLE, + ) ?? defaultStorage.s3.forcePathStyle, }, }, secrets: { provider: secretsProvider, - strictMode: parseBooleanFromEnv(process.env.TASKCORE_SECRETS_STRICT_MODE) ?? defaultSecrets.strictMode, + strictMode: + parseBooleanFromEnv(process.env.TASKCORE_SECRETS_STRICT_MODE) ?? + defaultSecrets.strictMode, localEncrypted: { keyFilePath: resolvePathFromEnv(process.env.TASKCORE_SECRETS_MASTER_KEY_FILE) ?? @@ -268,7 +315,8 @@ function quickstartDefaultsFromEnv(opts?: { preferTrustedLocal?: boolean }): { }; const ignoredEnvKeys: Array<{ key: string; reason: string }> = []; if (preferTrustedLocal) { - const forcedLocalReason = "Ignored because --yes quickstart forces trusted local loopback defaults"; + const forcedLocalReason = + "Ignored because --yes quickstart forces trusted local loopback defaults"; for (const key of [ "TASKCORE_DEPLOYMENT_MODE", "TASKCORE_DEPLOYMENT_EXPOSURE", @@ -286,28 +334,41 @@ function quickstartDefaultsFromEnv(opts?: { preferTrustedLocal?: boolean }): { } } } - if (deploymentMode === "local_trusted" && process.env.TASKCORE_DEPLOYMENT_EXPOSURE !== undefined) { + if ( + deploymentMode === "local_trusted" && + process.env.TASKCORE_DEPLOYMENT_EXPOSURE !== undefined + ) { ignoredEnvKeys.push({ key: "TASKCORE_DEPLOYMENT_EXPOSURE", - reason: "Ignored because deployment mode local_trusted always forces private exposure", + reason: + "Ignored because deployment mode local_trusted always forces private exposure", }); } - if (deploymentMode === "local_trusted" && process.env.TASKCORE_BIND !== undefined) { + if ( + deploymentMode === "local_trusted" && + process.env.TASKCORE_BIND !== undefined + ) { ignoredEnvKeys.push({ key: "TASKCORE_BIND", - reason: "Ignored because deployment mode local_trusted always uses loopback reachability", + reason: + "Ignored because deployment mode local_trusted always uses loopback reachability", }); } - if (deploymentMode === "local_trusted" && process.env.TASKCORE_BIND_HOST !== undefined) { + if ( + deploymentMode === "local_trusted" && + process.env.TASKCORE_BIND_HOST !== undefined + ) { ignoredEnvKeys.push({ key: "TASKCORE_BIND_HOST", - reason: "Ignored because deployment mode local_trusted always uses loopback reachability", + reason: + "Ignored because deployment mode local_trusted always uses loopback reachability", }); } if (deploymentMode === "local_trusted" && process.env.HOST !== undefined) { ignoredEnvKeys.push({ key: "HOST", - reason: "Ignored because deployment mode local_trusted always uses loopback reachability", + reason: + "Ignored because deployment mode local_trusted always uses loopback reachability", }); } @@ -318,13 +379,20 @@ function quickstartDefaultsFromEnv(opts?: { preferTrustedLocal?: boolean }): { return { defaults, usedEnvKeys, ignoredEnvKeys }; } -function canCreateBootstrapInviteImmediately(config: Pick): boolean { - return config.server.deploymentMode === "authenticated" && config.database.mode !== "embedded-postgres"; +function canCreateBootstrapInviteImmediately( + config: Pick, +): boolean { + return ( + config.server.deploymentMode === "authenticated" && + config.database.mode !== "embedded-postgres" + ); } export async function onboard(opts: OnboardOptions): Promise { if (opts.bind && !["loopback", "lan", "tailnet"].includes(opts.bind)) { - throw new Error(`Unsupported bind preset for onboard: ${opts.bind}. Use loopback, lan, or tailnet.`); + throw new Error( + `Unsupported bind preset for onboard: ${opts.bind}. Use loopback, lan, or tailnet.`, + ); } printTaskcoreCliBanner(); @@ -354,32 +422,50 @@ export async function onboard(opts: OnboardOptions): Promise { if (existingConfig) { p.log.message( - pc.dim("Existing Taskcore install detected; keeping the current configuration unchanged."), + pc.dim( + "Existing Taskcore install detected; keeping the current configuration unchanged.", + ), + ); + p.log.message( + pc.dim( + `Use ${pc.cyan("taskcore configure")} if you want to change settings.`, + ), ); - p.log.message(pc.dim(`Use ${pc.cyan("taskcore configure")} if you want to change settings.`)); const jwtSecret = ensureAgentJwtSecret(configPath); const envFilePath = resolveAgentJwtEnvFile(configPath); if (jwtSecret.created) { - p.log.success(`Created ${pc.cyan("TASKCORE_AGENT_JWT_SECRET")} in ${pc.dim(envFilePath)}`); + p.log.success( + `Created ${pc.cyan("TASKCORE_AGENT_JWT_SECRET")} in ${pc.dim(envFilePath)}`, + ); } else if (process.env.TASKCORE_AGENT_JWT_SECRET?.trim()) { - p.log.info(`Using existing ${pc.cyan("TASKCORE_AGENT_JWT_SECRET")} from environment`); + p.log.info( + `Using existing ${pc.cyan("TASKCORE_AGENT_JWT_SECRET")} from environment`, + ); } else { - p.log.info(`Using existing ${pc.cyan("TASKCORE_AGENT_JWT_SECRET")} in ${pc.dim(envFilePath)}`); + p.log.info( + `Using existing ${pc.cyan("TASKCORE_AGENT_JWT_SECRET")} in ${pc.dim(envFilePath)}`, + ); } const keyResult = ensureLocalSecretsKeyFile(existingConfig, configPath); if (keyResult.status === "created") { - p.log.success(`Created local secrets key file at ${pc.dim(keyResult.path)}`); + p.log.success( + `Created local secrets key file at ${pc.dim(keyResult.path)}`, + ); } else if (keyResult.status === "existing") { - p.log.message(pc.dim(`Using existing local secrets key file at ${keyResult.path}`)); + p.log.message( + pc.dim(`Using existing local secrets key file at ${keyResult.path}`), + ); } p.note( [ "Existing config preserved", `Database: ${existingConfig.database.mode}`, - existingConfig.llm ? `LLM: ${existingConfig.llm.provider}` : "LLM: not configured", + existingConfig.llm + ? `LLM: ${existingConfig.llm.provider}` + : "LLM: not configured", `Logging: ${existingConfig.logging.mode} -> ${existingConfig.logging.logDir}`, `Server: ${existingConfig.server.deploymentMode}/${existingConfig.server.exposure} @ ${describeServerBinding(existingConfig.server)}`, `Allowed hosts: ${existingConfig.server.allowedHostnames.length > 0 ? existingConfig.server.allowedHostnames.join(", ") : "(loopback only)"}`, @@ -401,7 +487,12 @@ export async function onboard(opts: OnboardOptions): Promise { ); let shouldRunNow = opts.run === true || opts.yes === true; - if (!shouldRunNow && !opts.invokedByRun && process.stdin.isTTY && process.stdout.isTTY) { + if ( + !shouldRunNow && + !opts.invokedByRun && + process.stdin.isTTY && + process.stdout.isTTY + ) { const answer = await p.confirm({ message: "Start Taskcore now?", initialValue: true, @@ -459,19 +550,20 @@ export async function onboard(opts: OnboardOptions): Promise { if (tc) trackInstallStarted(tc); let llm: TaskcoreConfig["llm"] | undefined; - const { defaults: derivedDefaults, usedEnvKeys, ignoredEnvKeys } = quickstartDefaultsFromEnv({ + const { + defaults: derivedDefaults, + usedEnvKeys, + ignoredEnvKeys, + } = quickstartDefaultsFromEnv({ preferTrustedLocal: opts.yes === true && !opts.bind, }); - let { - database, - logging, - server, - auth, - storage, - secrets, - } = derivedDefaults; + let { database, logging, server, auth, storage, secrets } = derivedDefaults; - if (opts.bind === "loopback" || opts.bind === "lan" || opts.bind === "tailnet") { + if ( + opts.bind === "loopback" || + opts.bind === "lan" || + opts.bind === "tailnet" + ) { const preset = buildPresetServerConfig(opts.bind, { port: server.port, allowedHostnames: server.allowedHostnames, @@ -497,7 +589,11 @@ export async function onboard(opts: OnboardOptions): Promise { await db.execute("SELECT 1"); s.stop("Database connection successful"); } catch { - s.stop(pc.yellow("Could not connect to database β€” you can fix this later with `taskcore doctor`")); + s.stop( + pc.yellow( + "Could not connect to database β€” you can fix this later with `taskcore doctor`", + ), + ); } } @@ -525,7 +621,9 @@ export async function onboard(opts: OnboardOptions): Promise { if (res.ok || res.status === 400) { s.stop("API key is valid"); } else if (res.status === 401) { - s.stop(pc.yellow("API key appears invalid β€” you can update it later")); + s.stop( + pc.yellow("API key appears invalid β€” you can update it later"), + ); } else { s.stop(pc.yellow("Could not validate API key β€” continuing anyway")); } @@ -536,7 +634,9 @@ export async function onboard(opts: OnboardOptions): Promise { if (res.ok) { s.stop("API key is valid"); } else if (res.status === 401) { - s.stop(pc.yellow("API key appears invalid β€” you can update it later")); + s.stop( + pc.yellow("API key appears invalid β€” you can update it later"), + ); } else { s.stop(pc.yellow("Could not validate API key β€” continuing anyway")); } @@ -550,7 +650,10 @@ export async function onboard(opts: OnboardOptions): Promise { logging = await promptLogging(); p.log.step(pc.bold("Server")); - ({ server, auth } = await promptServer({ currentServer: server, currentAuth: auth })); + ({ server, auth } = await promptServer({ + currentServer: server, + currentAuth: auth, + })); p.log.step(pc.bold("Storage")); storage = await promptStorage(storage); @@ -561,7 +664,9 @@ export async function onboard(opts: OnboardOptions): Promise { provider: secrets.provider ?? secretsDefaults.provider, strictMode: secrets.strictMode ?? secretsDefaults.strictMode, localEncrypted: { - keyFilePath: secrets.localEncrypted?.keyFilePath ?? secretsDefaults.localEncrypted.keyFilePath, + keyFilePath: + secrets.localEncrypted?.keyFilePath ?? + secretsDefaults.localEncrypted.keyFilePath, }, }; p.log.message( @@ -579,10 +684,16 @@ export async function onboard(opts: OnboardOptions): Promise { ), ); if (usedEnvKeys.length > 0) { - p.log.message(pc.dim(`Environment-aware defaults active (${usedEnvKeys.length} env var(s) detected).`)); + p.log.message( + pc.dim( + `Environment-aware defaults active (${usedEnvKeys.length} env var(s) detected).`, + ), + ); } else { p.log.message( - pc.dim("No environment overrides detected: embedded database, file storage, local encrypted secrets."), + pc.dim( + "No environment overrides detected: embedded database, file storage, local encrypted secrets.", + ), ); } for (const ignored of ignoredEnvKeys) { @@ -593,11 +704,17 @@ export async function onboard(opts: OnboardOptions): Promise { const jwtSecret = ensureAgentJwtSecret(configPath); const envFilePath = resolveAgentJwtEnvFile(configPath); if (jwtSecret.created) { - p.log.success(`Created ${pc.cyan("TASKCORE_AGENT_JWT_SECRET")} in ${pc.dim(envFilePath)}`); + p.log.success( + `Created ${pc.cyan("TASKCORE_AGENT_JWT_SECRET")} in ${pc.dim(envFilePath)}`, + ); } else if (process.env.TASKCORE_AGENT_JWT_SECRET?.trim()) { - p.log.info(`Using existing ${pc.cyan("TASKCORE_AGENT_JWT_SECRET")} from environment`); + p.log.info( + `Using existing ${pc.cyan("TASKCORE_AGENT_JWT_SECRET")} from environment`, + ); } else { - p.log.info(`Using existing ${pc.cyan("TASKCORE_AGENT_JWT_SECRET")} in ${pc.dim(envFilePath)}`); + p.log.info( + `Using existing ${pc.cyan("TASKCORE_AGENT_JWT_SECRET")} in ${pc.dim(envFilePath)}`, + ); } const config: TaskcoreConfig = { @@ -620,16 +737,21 @@ export async function onboard(opts: OnboardOptions): Promise { const keyResult = ensureLocalSecretsKeyFile(config, configPath); if (keyResult.status === "created") { - p.log.success(`Created local secrets key file at ${pc.dim(keyResult.path)}`); + p.log.success( + `Created local secrets key file at ${pc.dim(keyResult.path)}`, + ); } else if (keyResult.status === "existing") { - p.log.message(pc.dim(`Using existing local secrets key file at ${keyResult.path}`)); + p.log.message( + pc.dim(`Using existing local secrets key file at ${keyResult.path}`), + ); } writeConfig(config, opts.config); - if (tc) trackInstallCompleted(tc, { - adapterType: server.deploymentMode, - }); + if (tc) + trackInstallCompleted(tc, { + adapterType: server.deploymentMode, + }); p.note( [ @@ -661,7 +783,12 @@ export async function onboard(opts: OnboardOptions): Promise { } let shouldRunNow = opts.run === true || opts.yes === true; - if (!shouldRunNow && !opts.invokedByRun && process.stdin.isTTY && process.stdout.isTTY) { + if ( + !shouldRunNow && + !opts.invokedByRun && + process.stdin.isTTY && + process.stdout.isTTY + ) { const answer = await p.confirm({ message: "Start Taskcore now?", initialValue: true, @@ -678,7 +805,10 @@ export async function onboard(opts: OnboardOptions): Promise { return; } - if (server.deploymentMode === "authenticated" && database.mode === "embedded-postgres") { + if ( + server.deploymentMode === "authenticated" && + database.mode === "embedded-postgres" + ) { p.log.info( [ "Bootstrap CEO invite will be created after the server starts.", diff --git a/cli/src/commands/routines.ts b/cli/src/commands/routines.ts index f6f6eb9..7126e39 100644 --- a/cli/src/commands/routines.ts +++ b/cli/src/commands/routines.ts @@ -60,7 +60,9 @@ type ClosableDb = ReturnType & { }; function nonEmpty(value: string | null | undefined): string | null { - return typeof value === "string" && value.trim().length > 0 ? value.trim() : null; + return typeof value === "string" && value.trim().length > 0 + ? value.trim() + : null; } async function isPortAvailable(port: number): Promise { @@ -96,7 +98,9 @@ function readPidFilePort(postmasterPidFile: string): number | null { function readRunningPostmasterPid(postmasterPidFile: string): number | null { if (!fs.existsSync(postmasterPidFile)) return null; try { - const pid = Number(fs.readFileSync(postmasterPidFile, "utf8").split("\n")[0]?.trim()); + const pid = Number( + fs.readFileSync(postmasterPidFile, "utf8").split("\n")[0]?.trim(), + ); if (!Number.isInteger(pid) || pid <= 0) return null; process.kill(pid, 0); return pid; @@ -105,7 +109,10 @@ function readRunningPostmasterPid(postmasterPidFile: string): number | null { } } -async function ensureEmbeddedPostgres(dataDir: string, preferredPort: number): Promise { +async function ensureEmbeddedPostgres( + dataDir: string, + preferredPort: number, +): Promise { const moduleName = "embedded-postgres"; let EmbeddedPostgres: EmbeddedPostgresCtor; try { @@ -123,7 +130,7 @@ async function ensureEmbeddedPostgres(dataDir: string, preferredPort: number): P return { port: readPidFilePort(postmasterPidFile) ?? preferredPort, startedByThisProcess: false, - stop: async () => { }, + stop: async () => {}, }; } @@ -211,7 +218,9 @@ async function openConfiguredDb(configPath: string): Promise<{ const connectionString = nonEmpty(config.database.connectionString); if (!connectionString) { - throw new Error(`Config at ${configPath} does not define a database connection string.`); + throw new Error( + `Config at ${configPath} does not define a database connection string.`, + ); } await applyPendingMigrations(connectionString); @@ -236,11 +245,13 @@ export async function disableAllRoutinesInConfig( const configPath = resolveConfigPath(options.config); loadTaskcoreEnvFile(configPath); const companyId = - nonEmpty(options.companyId) - ?? nonEmpty(process.env.TASKCORE_COMPANY_ID) - ?? null; + nonEmpty(options.companyId) ?? + nonEmpty(process.env.TASKCORE_COMPANY_ID) ?? + null; if (!companyId) { - throw new Error("Company ID is required. Pass --company-id or set TASKCORE_COMPANY_ID."); + throw new Error( + "Company ID is required. Pass --company-id or set TASKCORE_COMPANY_ID.", + ); } const config = readConfig(configPath); @@ -264,7 +275,9 @@ export async function disableAllRoutinesInConfig( } else { const connectionString = nonEmpty(config.database.connectionString); if (!connectionString) { - throw new Error(`Config at ${configPath} does not define a database connection string.`); + throw new Error( + `Config at ${configPath} does not define a database connection string.`, + ); } await applyPendingMigrations(connectionString); db = createDb(connectionString) as ClosableDb; @@ -278,10 +291,17 @@ export async function disableAllRoutinesInConfig( .from(routines) .where(eq(routines.companyId, companyId)); - const alreadyPausedCount = existing.filter((routine) => routine.status === "paused").length; - const archivedCount = existing.filter((routine) => routine.status === "archived").length; + const alreadyPausedCount = existing.filter( + (routine) => routine.status === "paused", + ).length; + const archivedCount = existing.filter( + (routine) => routine.status === "archived", + ).length; const idsToPause = existing - .filter((routine) => routine.status !== "paused" && routine.status !== "archived") + .filter( + (routine) => + routine.status !== "paused" && routine.status !== "archived", + ) .map((routine) => routine.id); if (idsToPause.length > 0) { @@ -311,7 +331,9 @@ export async function disableAllRoutinesInConfig( } } -export async function disableAllRoutinesCommand(options: RoutinesDisableAllOptions): Promise { +export async function disableAllRoutinesCommand( + options: RoutinesDisableAllOptions, +): Promise { const result = await disableAllRoutinesInConfig(options); if (options.json) { @@ -326,18 +348,25 @@ export async function disableAllRoutinesCommand(options: RoutinesDisableAllOptio console.log( `Paused ${result.pausedCount} routine(s) for company ${result.companyId} ` + - `(${result.alreadyPausedCount} already paused, ${result.archivedCount} archived).`, + `(${result.alreadyPausedCount} already paused, ${result.archivedCount} archived).`, ); } export function registerRoutineCommands(program: Command): void { - const routinesCommand = program.command("routines").description("Local routine maintenance commands"); + const routinesCommand = program + .command("routines") + .description("Local routine maintenance commands"); routinesCommand .command("disable-all") - .description("Pause all non-archived routines in the configured local instance for one company") + .description( + "Pause all non-archived routines in the configured local instance for one company", + ) .option("-c, --config ", "Path to config file") - .option("-d, --data-dir ", "Taskcore data directory root (isolates state from ~/.taskcore)") + .option( + "-d, --data-dir ", + "Taskcore data directory root (isolates state from ~/.taskcore)", + ) .option("-C, --company-id ", "Company ID") .option("--json", "Output raw JSON") .action(async (opts: RoutinesDisableAllOptions) => { diff --git a/cli/src/commands/run.ts b/cli/src/commands/run.ts index c3a9388..66f2fdf 100644 --- a/cli/src/commands/run.ts +++ b/cli/src/commands/run.ts @@ -54,7 +54,9 @@ export async function runCommand(opts: RunOptions): Promise { if (!configExists(configPath)) { if (!process.stdin.isTTY || !process.stdout.isTTY) { p.log.error("No config found and terminal is non-interactive."); - p.log.message(`Run ${pc.cyan("taskcore onboard")} once, then retry ${pc.cyan("taskcore run")}.`); + p.log.message( + `Run ${pc.cyan("taskcore onboard")} once, then retry ${pc.cyan("taskcore run")}.`, + ); process.exit(1); } @@ -102,9 +104,14 @@ function resolveBootstrapInviteBaseUrl( process.env.TASKCORE_AUTH_PUBLIC_BASE_URL ?? process.env.BETTER_AUTH_URL ?? process.env.BETTER_AUTH_BASE_URL ?? - (config.auth.baseUrlMode === "explicit" ? config.auth.publicBaseUrl : undefined); - - if (typeof explicitBaseUrl === "string" && explicitBaseUrl.trim().length > 0) { + (config.auth.baseUrlMode === "explicit" + ? config.auth.publicBaseUrl + : undefined); + + if ( + typeof explicitBaseUrl === "string" && + explicitBaseUrl.trim().length > 0 + ) { return explicitBaseUrl.trim().replace(/\/+$/, ""); } @@ -133,7 +140,9 @@ function isModuleNotFoundError(err: unknown): boolean { function getMissingModuleSpecifier(err: unknown): string | null { if (!(err instanceof Error)) return null; - const packageMatch = err.message.match(/Cannot find package '([^']+)' imported from/); + const packageMatch = err.message.match( + /Cannot find package '([^']+)' imported from/, + ); if (packageMatch?.[1]) return packageMatch[1]; const moduleMatch = err.message.match(/Cannot find module '([^']+)'/); if (moduleMatch?.[1]) return moduleMatch[1]; @@ -143,13 +152,19 @@ function getMissingModuleSpecifier(err: unknown): string | null { function maybeEnableUiDevMiddleware(entrypoint: string): void { if (process.env.TASKCORE_UI_DEV_MIDDLEWARE !== undefined) return; const normalized = entrypoint.replaceAll("\\", "/"); - if (normalized.endsWith("/server/src/index.ts") || normalized.endsWith("@taskcore/server/src/index.ts")) { + if ( + normalized.endsWith("/server/src/index.ts") || + normalized.endsWith("@taskcore/server/src/index.ts") + ) { process.env.TASKCORE_UI_DEV_MIDDLEWARE = "true"; } } function ensureDevWorkspaceBuildDeps(projectRoot: string): void { - const buildScript = path.resolve(projectRoot, "scripts/ensure-plugin-build-deps.mjs"); + const buildScript = path.resolve( + projectRoot, + "scripts/ensure-plugin-build-deps.mjs", + ); if (!fs.existsSync(buildScript)) return; const result = spawnSync(process.execPath, [buildScript], { @@ -173,7 +188,10 @@ function ensureDevWorkspaceBuildDeps(projectRoot: string): void { async function importServerEntry(): Promise { // Dev mode: try local workspace path (monorepo with tsx) - const projectRoot = path.resolve(path.dirname(fileURLToPath(import.meta.url)), "../../.."); + const projectRoot = path.resolve( + path.dirname(fileURLToPath(import.meta.url)), + "../../..", + ); const devEntry = path.resolve(projectRoot, "server/src/index.ts"); if (fs.existsSync(devEntry)) { ensureDevWorkspaceBuildDeps(projectRoot); @@ -188,29 +206,40 @@ async function importServerEntry(): Promise { return await startServerFromModule(mod, "@taskcore/server"); } catch (err) { const missingSpecifier = getMissingModuleSpecifier(err); - const missingServerEntrypoint = !missingSpecifier || missingSpecifier === "@taskcore/server"; + const missingServerEntrypoint = + !missingSpecifier || missingSpecifier === "@taskcore/server"; if (isModuleNotFoundError(err) && missingServerEntrypoint) { throw new Error( `Could not locate a Taskcore server entrypoint.\n` + - `Tried: ${devEntry}, @taskcore/server\n` + - `${formatError(err)}`, + `Tried: ${devEntry}, @taskcore/server\n` + + `${formatError(err)}`, ); } throw new Error( - `Taskcore server failed to start.\n` + - `${formatError(err)}`, + `Taskcore server failed to start.\n` + `${formatError(err)}`, ); } } -function shouldGenerateBootstrapInviteAfterStart(config: TaskcoreConfig): boolean { - return config.server.deploymentMode === "authenticated" && config.database.mode === "embedded-postgres"; +function shouldGenerateBootstrapInviteAfterStart( + config: TaskcoreConfig, +): boolean { + return ( + config.server.deploymentMode === "authenticated" && + config.database.mode === "embedded-postgres" + ); } -async function startServerFromModule(mod: unknown, label: string): Promise { - const startServer = (mod as { startServer?: () => Promise }).startServer; +async function startServerFromModule( + mod: unknown, + label: string, +): Promise { + const startServer = (mod as { startServer?: () => Promise }) + .startServer; if (typeof startServer !== "function") { - throw new Error(`Taskcore server entrypoint did not export startServer(): ${label}`); + throw new Error( + `Taskcore server entrypoint did not export startServer(): ${label}`, + ); } return await startServer(); } diff --git a/cli/src/commands/worktree-lib.ts b/cli/src/commands/worktree-lib.ts index cba2b8d..361a379 100644 --- a/cli/src/commands/worktree-lib.ts +++ b/cli/src/commands/worktree-lib.ts @@ -54,7 +54,9 @@ export function isWorktreeSeedMode(value: string): value is WorktreeSeedMode { return (WORKTREE_SEED_MODES as readonly string[]).includes(value); } -export function resolveWorktreeSeedPlan(mode: WorktreeSeedMode): WorktreeSeedPlan { +export function resolveWorktreeSeedPlan( + mode: WorktreeSeedMode, +): WorktreeSeedPlan { if (mode === "full") { return { mode, @@ -72,7 +74,9 @@ export function resolveWorktreeSeedPlan(mode: WorktreeSeedMode): WorktreeSeedPla } function nonEmpty(value: string | null | undefined): string | null { - return typeof value === "string" && value.trim().length > 0 ? value.trim() : null; + return typeof value === "string" && value.trim().length > 0 + ? value.trim() + : null; } function isLoopbackHost(hostname: string): boolean { @@ -89,7 +93,10 @@ export function sanitizeWorktreeInstanceId(rawValue: string): string { return normalized || "worktree"; } -export function resolveSuggestedWorktreeName(cwd: string, explicitName?: string): string { +export function resolveSuggestedWorktreeName( + cwd: string, + explicitName?: string, +): string { return nonEmpty(explicitName) ?? path.basename(path.resolve(cwd)); } @@ -102,10 +109,10 @@ function hslComponentToHex(n: number): string { function hslToHex(hue: number, saturation: number, lightness: number): string { const s = Math.max(0, Math.min(100, saturation)) / 100; const l = Math.max(0, Math.min(100, lightness)) / 100; - const c = (1 - Math.abs((2 * l) - 1)) * s; + const c = (1 - Math.abs(2 * l - 1)) * s; const h = ((hue % 360) + 360) % 360; const x = c * (1 - Math.abs(((h / 60) % 2) - 1)); - const m = l - (c / 2); + const m = l - c / 2; let r = 0; let g = 0; @@ -144,7 +151,9 @@ export function resolveWorktreeLocalPaths(opts: { instanceId: string; }): WorktreeLocalPaths { const cwd = path.resolve(opts.cwd); - const homeDir = path.resolve(expandHomePrefix(opts.homeDir ?? DEFAULT_WORKTREE_HOME)); + const homeDir = path.resolve( + expandHomePrefix(opts.homeDir ?? DEFAULT_WORKTREE_HOME), + ); const instanceRoot = path.resolve(homeDir, "instances", opts.instanceId); const repoConfigDir = path.resolve(cwd, ".taskcore"); return { @@ -164,7 +173,10 @@ export function resolveWorktreeLocalPaths(opts: { }; } -export function rewriteLocalUrlPort(rawUrl: string | undefined, port: number): string | undefined { +export function rewriteLocalUrlPort( + rawUrl: string | undefined, + port: number, +): string | undefined { if (!rawUrl) return undefined; try { const parsed = new URL(rawUrl); @@ -187,7 +199,10 @@ export function buildWorktreeConfig(input: { const nowIso = (input.now ?? new Date()).toISOString(); const source = sourceConfig; - const authPublicBaseUrl = rewriteLocalUrlPort(source?.auth.publicBaseUrl, serverPort); + const authPublicBaseUrl = rewriteLocalUrlPort( + source?.auth.publicBaseUrl, + serverPort, + ); return { $meta: { @@ -215,7 +230,9 @@ export function buildWorktreeConfig(input: { deploymentMode: source?.server.deploymentMode ?? "local_trusted", exposure: source?.server.exposure ?? "private", ...(source?.server.bind ? { bind: source.server.bind } : {}), - ...(source?.server.customBindHost ? { customBindHost: source.server.customBindHost } : {}), + ...(source?.server.customBindHost + ? { customBindHost: source.server.customBindHost } + : {}), host: source?.server.host ?? "127.0.0.1", port: serverPort, allowedHostnames: source?.server.allowedHostnames ?? [], diff --git a/cli/src/commands/worktree-merge-history-lib.ts b/cli/src/commands/worktree-merge-history-lib.ts index 532f53a..685ab3c 100644 --- a/cli/src/commands/worktree-merge-history-lib.ts +++ b/cli/src/commands/worktree-merge-history-lib.ts @@ -37,7 +37,10 @@ export type ImportAdjustment = | "clear_attachment_agent"; export type IssueMergeAction = "skip_existing" | "insert"; -export type CommentMergeAction = "skip_existing" | "skip_missing_parent" | "insert"; +export type CommentMergeAction = + | "skip_existing" + | "skip_missing_parent" + | "insert"; export type PlannedIssueInsert = { source: IssueRow; @@ -189,7 +192,11 @@ export type WorktreeMergePlan = { projectImports: PlannedProjectImport[]; issuePlans: Array; commentPlans: Array; - documentPlans: Array; + documentPlans: Array< + | PlannedIssueDocumentInsert + | PlannedIssueDocumentMerge + | PlannedIssueDocumentSkip + >; attachmentPlans: Array; counts: { projectsToImport: number; @@ -215,15 +222,24 @@ export type WorktreeMergePlan = { function compareIssueCoreFields(source: IssueRow, target: IssueRow): string[] { const driftKeys: string[] = []; if (source.title !== target.title) driftKeys.push("title"); - if ((source.description ?? null) !== (target.description ?? null)) driftKeys.push("description"); + if ((source.description ?? null) !== (target.description ?? null)) + driftKeys.push("description"); if (source.status !== target.status) driftKeys.push("status"); if (source.priority !== target.priority) driftKeys.push("priority"); - if ((source.parentId ?? null) !== (target.parentId ?? null)) driftKeys.push("parentId"); - if ((source.projectId ?? null) !== (target.projectId ?? null)) driftKeys.push("projectId"); - if ((source.projectWorkspaceId ?? null) !== (target.projectWorkspaceId ?? null)) driftKeys.push("projectWorkspaceId"); - if ((source.goalId ?? null) !== (target.goalId ?? null)) driftKeys.push("goalId"); - if ((source.assigneeAgentId ?? null) !== (target.assigneeAgentId ?? null)) driftKeys.push("assigneeAgentId"); - if ((source.assigneeUserId ?? null) !== (target.assigneeUserId ?? null)) driftKeys.push("assigneeUserId"); + if ((source.parentId ?? null) !== (target.parentId ?? null)) + driftKeys.push("parentId"); + if ((source.projectId ?? null) !== (target.projectId ?? null)) + driftKeys.push("projectId"); + if ( + (source.projectWorkspaceId ?? null) !== (target.projectWorkspaceId ?? null) + ) + driftKeys.push("projectWorkspaceId"); + if ((source.goalId ?? null) !== (target.goalId ?? null)) + driftKeys.push("goalId"); + if ((source.assigneeAgentId ?? null) !== (target.assigneeAgentId ?? null)) + driftKeys.push("assigneeAgentId"); + if ((source.assigneeUserId ?? null) !== (target.assigneeUserId ?? null)) + driftKeys.push("assigneeUserId"); return driftKeys; } @@ -254,15 +270,19 @@ function sameDate(left: Date, right: Date): boolean { function sortDocumentRows(rows: IssueDocumentRow[]): IssueDocumentRow[] { return [...rows].sort((left, right) => { - const createdDelta = left.documentCreatedAt.getTime() - right.documentCreatedAt.getTime(); + const createdDelta = + left.documentCreatedAt.getTime() - right.documentCreatedAt.getTime(); if (createdDelta !== 0) return createdDelta; - const linkDelta = left.linkCreatedAt.getTime() - right.linkCreatedAt.getTime(); + const linkDelta = + left.linkCreatedAt.getTime() - right.linkCreatedAt.getTime(); if (linkDelta !== 0) return linkDelta; return left.documentId.localeCompare(right.documentId); }); } -function sortDocumentRevisions(rows: DocumentRevisionRow[]): DocumentRevisionRow[] { +function sortDocumentRevisions( + rows: DocumentRevisionRow[], +): DocumentRevisionRow[] { return [...rows].sort((left, right) => { const revisionDelta = left.revisionNumber - right.revisionNumber; if (revisionDelta !== 0) return revisionDelta; @@ -274,7 +294,8 @@ function sortDocumentRevisions(rows: DocumentRevisionRow[]): DocumentRevisionRow function sortAttachments(rows: IssueAttachmentRow[]): IssueAttachmentRow[] { return [...rows].sort((left, right) => { - const createdDelta = left.attachmentCreatedAt.getTime() - right.attachmentCreatedAt.getTime(); + const createdDelta = + left.attachmentCreatedAt.getTime() - right.attachmentCreatedAt.getTime(); if (createdDelta !== 0) return createdDelta; return left.id.localeCompare(right.id); }); @@ -316,7 +337,9 @@ function sortIssuesForImport(sourceIssues: IssueRow[]): IssueRow[] { }); } -export function parseWorktreeMergeScopes(rawValue: string | undefined): WorktreeMergeScope[] { +export function parseWorktreeMergeScopes( + rawValue: string | undefined, +): WorktreeMergeScope[] { if (!rawValue || rawValue.trim().length === 0) { return ["issues", "comments"]; } @@ -362,16 +385,31 @@ export function buildWorktreeMergePlan(input: { importProjectIds?: Iterable; projectIdOverrides?: Record; }): WorktreeMergePlan { - const targetIssuesById = new Map(input.targetIssues.map((issue) => [issue.id, issue])); - const targetCommentIds = new Set(input.targetComments.map((comment) => comment.id)); + const targetIssuesById = new Map( + input.targetIssues.map((issue) => [issue.id, issue]), + ); + const targetCommentIds = new Set( + input.targetComments.map((comment) => comment.id), + ); const targetAgentIds = new Set(input.targetAgents.map((agent) => agent.id)); - const targetProjectIds = new Set(input.targetProjects.map((project) => project.id)); - const targetProjectsById = new Map(input.targetProjects.map((project) => [project.id, project])); - const targetProjectWorkspaceIds = new Set(input.targetProjectWorkspaces.map((workspace) => workspace.id)); + const targetProjectIds = new Set( + input.targetProjects.map((project) => project.id), + ); + const targetProjectsById = new Map( + input.targetProjects.map((project) => [project.id, project]), + ); + const targetProjectWorkspaceIds = new Set( + input.targetProjectWorkspaces.map((workspace) => workspace.id), + ); const targetGoalIds = new Set(input.targetGoals.map((goal) => goal.id)); - const sourceProjectsById = new Map((input.sourceProjects ?? []).map((project) => [project.id, project])); + const sourceProjectsById = new Map( + (input.sourceProjects ?? []).map((project) => [project.id, project]), + ); const sourceProjectWorkspaces = input.sourceProjectWorkspaces ?? []; - const sourceProjectWorkspacesByProjectId = groupBy(sourceProjectWorkspaces, (workspace) => workspace.projectId); + const sourceProjectWorkspacesByProjectId = groupBy( + sourceProjectWorkspaces, + (workspace) => workspace.projectId, + ); const importProjectIds = new Set(input.importProjectIds ?? []); const scopes = new Set(input.scopes); @@ -395,24 +433,30 @@ export function buildWorktreeMergePlan(input: { projectImports.push({ source: sourceProject, targetLeadAgentId: - sourceProject.leadAgentId && targetAgentIds.has(sourceProject.leadAgentId) + sourceProject.leadAgentId && + targetAgentIds.has(sourceProject.leadAgentId) ? sourceProject.leadAgentId : null, targetGoalId: sourceProject.goalId && targetGoalIds.has(sourceProject.goalId) ? sourceProject.goalId : null, - workspaces: [...(sourceProjectWorkspacesByProjectId.get(projectId) ?? [])].sort((left, right) => { + workspaces: [ + ...(sourceProjectWorkspacesByProjectId.get(projectId) ?? []), + ].sort((left, right) => { const primaryDelta = Number(right.isPrimary) - Number(left.isPrimary); if (primaryDelta !== 0) return primaryDelta; - const createdDelta = left.createdAt.getTime() - right.createdAt.getTime(); + const createdDelta = + left.createdAt.getTime() - right.createdAt.getTime(); if (createdDelta !== 0) return createdDelta; return left.id.localeCompare(right.id); }), }); } const importedProjectWorkspaceIds = new Set( - projectImports.flatMap((project) => project.workspaces.map((workspace) => workspace.id)), + projectImports.flatMap((project) => + project.workspaces.map((workspace) => workspace.id), + ), ); const issuePlans: Array = []; @@ -431,29 +475,45 @@ export function buildWorktreeMergePlan(input: { nextPreviewIssueNumber += 1; const adjustments: ImportAdjustment[] = []; const targetAssigneeAgentId = - issue.assigneeAgentId && targetAgentIds.has(issue.assigneeAgentId) ? issue.assigneeAgentId : null; + issue.assigneeAgentId && targetAgentIds.has(issue.assigneeAgentId) + ? issue.assigneeAgentId + : null; if (issue.assigneeAgentId && !targetAssigneeAgentId) { adjustments.push("clear_assignee_agent"); incrementAdjustment(adjustmentCounts, "clear_assignee_agent"); } const targetCreatedByAgentId = - issue.createdByAgentId && targetAgentIds.has(issue.createdByAgentId) ? issue.createdByAgentId : null; + issue.createdByAgentId && targetAgentIds.has(issue.createdByAgentId) + ? issue.createdByAgentId + : null; let targetProjectId = - issue.projectId && targetProjectIds.has(issue.projectId) ? issue.projectId : null; - let projectResolution: PlannedIssueInsert["projectResolution"] = targetProjectId ? "preserved" : "cleared"; + issue.projectId && targetProjectIds.has(issue.projectId) + ? issue.projectId + : null; + let projectResolution: PlannedIssueInsert["projectResolution"] = + targetProjectId ? "preserved" : "cleared"; let mappedProjectName: string | null = null; const overrideProjectId = issue.projectId && input.projectIdOverrides - ? input.projectIdOverrides[issue.projectId] ?? null + ? (input.projectIdOverrides[issue.projectId] ?? null) : null; - if (!targetProjectId && overrideProjectId && targetProjectIds.has(overrideProjectId)) { + if ( + !targetProjectId && + overrideProjectId && + targetProjectIds.has(overrideProjectId) + ) { targetProjectId = overrideProjectId; projectResolution = "mapped"; - mappedProjectName = targetProjectsById.get(overrideProjectId)?.name ?? null; + mappedProjectName = + targetProjectsById.get(overrideProjectId)?.name ?? null; } - if (!targetProjectId && issue.projectId && importProjectIds.has(issue.projectId)) { + if ( + !targetProjectId && + issue.projectId && + importProjectIds.has(issue.projectId) + ) { const sourceProject = sourceProjectsById.get(issue.projectId); if (sourceProject) { targetProjectId = sourceProject.id; @@ -467,11 +527,11 @@ export function buildWorktreeMergePlan(input: { } const targetProjectWorkspaceId = - targetProjectId - && targetProjectId === issue.projectId - && issue.projectWorkspaceId - && (targetProjectWorkspaceIds.has(issue.projectWorkspaceId) - || importedProjectWorkspaceIds.has(issue.projectWorkspaceId)) + targetProjectId && + targetProjectId === issue.projectId && + issue.projectWorkspaceId && + (targetProjectWorkspaceIds.has(issue.projectWorkspaceId) || + importedProjectWorkspaceIds.has(issue.projectWorkspaceId)) ? issue.projectWorkspaceId : null; if (issue.projectWorkspaceId && !targetProjectWorkspaceId) { @@ -488,9 +548,9 @@ export function buildWorktreeMergePlan(input: { let targetStatus = issue.status; if ( - targetStatus === "in_progress" - && !targetAssigneeAgentId - && !(issue.assigneeUserId && issue.assigneeUserId.trim().length > 0) + targetStatus === "in_progress" && + !targetAssigneeAgentId && + !(issue.assigneeUserId && issue.assigneeUserId.trim().length > 0) ) { targetStatus = "todo"; adjustments.push("coerce_in_progress_to_todo"); @@ -516,7 +576,9 @@ export function buildWorktreeMergePlan(input: { const issueIdsAvailableAfterImport = new Set([ ...input.targetIssues.map((issue) => issue.id), - ...issuePlans.filter((plan): plan is PlannedIssueInsert => plan.action === "insert").map((plan) => plan.source.id), + ...issuePlans + .filter((plan): plan is PlannedIssueInsert => plan.action === "insert") + .map((plan) => plan.source.id), ]); const commentPlans: Array = []; @@ -539,7 +601,9 @@ export function buildWorktreeMergePlan(input: { const adjustments: ImportAdjustment[] = []; const targetAuthorAgentId = - comment.authorAgentId && targetAgentIds.has(comment.authorAgentId) ? comment.authorAgentId : null; + comment.authorAgentId && targetAgentIds.has(comment.authorAgentId) + ? comment.authorAgentId + : null; if (comment.authorAgentId && !targetAuthorAgentId) { adjustments.push("clear_author_agent"); incrementAdjustment(adjustmentCounts, "clear_author_agent"); @@ -559,16 +623,35 @@ export function buildWorktreeMergePlan(input: { const sourceDocumentRevisions = input.sourceDocumentRevisions ?? []; const targetDocumentRevisions = input.targetDocumentRevisions ?? []; - const targetDocumentsById = new Map(targetDocuments.map((document) => [document.documentId, document])); - const targetDocumentsByIssueKey = new Map(targetDocuments.map((document) => [`${document.issueId}:${document.key}`, document])); - const sourceRevisionsByDocumentId = groupBy(sourceDocumentRevisions, (revision) => revision.documentId); - const targetRevisionsByDocumentId = groupBy(targetDocumentRevisions, (revision) => revision.documentId); + const targetDocumentsById = new Map( + targetDocuments.map((document) => [document.documentId, document]), + ); + const targetDocumentsByIssueKey = new Map( + targetDocuments.map((document) => [ + `${document.issueId}:${document.key}`, + document, + ]), + ); + const sourceRevisionsByDocumentId = groupBy( + sourceDocumentRevisions, + (revision) => revision.documentId, + ); + const targetRevisionsByDocumentId = groupBy( + targetDocumentRevisions, + (revision) => revision.documentId, + ); const commentIdsAvailableAfterImport = new Set([ ...input.targetComments.map((comment) => comment.id), - ...commentPlans.filter((plan): plan is PlannedCommentInsert => plan.action === "insert").map((plan) => plan.source.id), + ...commentPlans + .filter((plan): plan is PlannedCommentInsert => plan.action === "insert") + .map((plan) => plan.source.id), ]); - const documentPlans: Array = []; + const documentPlans: Array< + | PlannedIssueDocumentInsert + | PlannedIssueDocumentMerge + | PlannedIssueDocumentSkip + > = []; for (const document of sortDocumentRows(sourceDocuments)) { if (!issueIdsAvailableAfterImport.has(document.issueId)) { documentPlans.push({ source: document, action: "skip_missing_parent" }); @@ -576,33 +659,52 @@ export function buildWorktreeMergePlan(input: { } const existingDocument = targetDocumentsById.get(document.documentId); - const conflictingIssueKeyDocument = targetDocumentsByIssueKey.get(`${document.issueId}:${document.key}`); - if (!existingDocument && conflictingIssueKeyDocument && conflictingIssueKeyDocument.documentId !== document.documentId) { + const conflictingIssueKeyDocument = targetDocumentsByIssueKey.get( + `${document.issueId}:${document.key}`, + ); + if ( + !existingDocument && + conflictingIssueKeyDocument && + conflictingIssueKeyDocument.documentId !== document.documentId + ) { documentPlans.push({ source: document, action: "skip_conflicting_key" }); continue; } const adjustments: ImportAdjustment[] = []; const targetCreatedByAgentId = - document.createdByAgentId && targetAgentIds.has(document.createdByAgentId) ? document.createdByAgentId : null; + document.createdByAgentId && targetAgentIds.has(document.createdByAgentId) + ? document.createdByAgentId + : null; const targetUpdatedByAgentId = - document.updatedByAgentId && targetAgentIds.has(document.updatedByAgentId) ? document.updatedByAgentId : null; + document.updatedByAgentId && targetAgentIds.has(document.updatedByAgentId) + ? document.updatedByAgentId + : null; if ( - (document.createdByAgentId && !targetCreatedByAgentId) - || (document.updatedByAgentId && !targetUpdatedByAgentId) + (document.createdByAgentId && !targetCreatedByAgentId) || + (document.updatedByAgentId && !targetUpdatedByAgentId) ) { adjustments.push("clear_document_agent"); incrementAdjustment(adjustmentCounts, "clear_document_agent"); } - const sourceRevisions = sortDocumentRevisions(sourceRevisionsByDocumentId.get(document.documentId) ?? []); - const targetRevisions = sortDocumentRevisions(targetRevisionsByDocumentId.get(document.documentId) ?? []); - const existingRevisionIds = new Set(targetRevisions.map((revision) => revision.id)); - const usedRevisionNumbers = new Set(targetRevisions.map((revision) => revision.revisionNumber)); - let nextRevisionNumber = targetRevisions.reduce( - (maxValue, revision) => Math.max(maxValue, revision.revisionNumber), - 0, - ) + 1; + const sourceRevisions = sortDocumentRevisions( + sourceRevisionsByDocumentId.get(document.documentId) ?? [], + ); + const targetRevisions = sortDocumentRevisions( + targetRevisionsByDocumentId.get(document.documentId) ?? [], + ); + const existingRevisionIds = new Set( + targetRevisions.map((revision) => revision.id), + ); + const usedRevisionNumbers = new Set( + targetRevisions.map((revision) => revision.revisionNumber), + ); + let nextRevisionNumber = + targetRevisions.reduce( + (maxValue, revision) => Math.max(maxValue, revision.revisionNumber), + 0, + ) + 1; const targetRevisionNumberById = new Map( targetRevisions.map((revision) => [revision.id, revision.revisionNumber]), @@ -624,7 +726,10 @@ export function buildWorktreeMergePlan(input: { const revisionAdjustments: ImportAdjustment[] = []; const targetCreatedByAgentId = - revision.createdByAgentId && targetAgentIds.has(revision.createdByAgentId) ? revision.createdByAgentId : null; + revision.createdByAgentId && + targetAgentIds.has(revision.createdByAgentId) + ? revision.createdByAgentId + : null; if (revision.createdByAgentId && !targetCreatedByAgentId) { revisionAdjustments.push("clear_document_revision_agent"); incrementAdjustment(adjustmentCounts, "clear_document_revision_agent"); @@ -638,12 +743,15 @@ export function buildWorktreeMergePlan(input: { }); } - const latestRevisionId = document.latestRevisionId ?? existingDocument?.latestRevisionId ?? null; + const latestRevisionId = + document.latestRevisionId ?? existingDocument?.latestRevisionId ?? null; const latestRevisionNumber = - (latestRevisionId ? targetRevisionNumberById.get(latestRevisionId) : undefined) - ?? document.latestRevisionNumber - ?? existingDocument?.latestRevisionNumber - ?? 0; + (latestRevisionId + ? targetRevisionNumberById.get(latestRevisionId) + : undefined) ?? + document.latestRevisionNumber ?? + existingDocument?.latestRevisionNumber ?? + 0; if (!existingDocument) { documentPlans.push({ @@ -660,17 +768,21 @@ export function buildWorktreeMergePlan(input: { } const documentAlreadyMatches = - existingDocument.key === document.key - && existingDocument.title === document.title - && existingDocument.format === document.format - && existingDocument.latestBody === document.latestBody - && (existingDocument.latestRevisionId ?? null) === latestRevisionId - && existingDocument.latestRevisionNumber === latestRevisionNumber - && (existingDocument.updatedByAgentId ?? null) === targetUpdatedByAgentId - && (existingDocument.updatedByUserId ?? null) === (document.updatedByUserId ?? null) - && sameDate(existingDocument.documentUpdatedAt, document.documentUpdatedAt) - && sameDate(existingDocument.linkUpdatedAt, document.linkUpdatedAt) - && revisionsToInsert.length === 0; + existingDocument.key === document.key && + existingDocument.title === document.title && + existingDocument.format === document.format && + existingDocument.latestBody === document.latestBody && + (existingDocument.latestRevisionId ?? null) === latestRevisionId && + existingDocument.latestRevisionNumber === latestRevisionNumber && + (existingDocument.updatedByAgentId ?? null) === targetUpdatedByAgentId && + (existingDocument.updatedByUserId ?? null) === + (document.updatedByUserId ?? null) && + sameDate( + existingDocument.documentUpdatedAt, + document.documentUpdatedAt, + ) && + sameDate(existingDocument.linkUpdatedAt, document.linkUpdatedAt) && + revisionsToInsert.length === 0; if (documentAlreadyMatches) { documentPlans.push({ source: document, action: "skip_existing" }); @@ -690,21 +802,29 @@ export function buildWorktreeMergePlan(input: { } const sourceAttachments = input.sourceAttachments ?? []; - const targetAttachmentIds = new Set((input.targetAttachments ?? []).map((attachment) => attachment.id)); - const attachmentPlans: Array = []; + const targetAttachmentIds = new Set( + (input.targetAttachments ?? []).map((attachment) => attachment.id), + ); + const attachmentPlans: Array< + PlannedAttachmentInsert | PlannedAttachmentSkip + > = []; for (const attachment of sortAttachments(sourceAttachments)) { if (targetAttachmentIds.has(attachment.id)) { attachmentPlans.push({ source: attachment, action: "skip_existing" }); continue; } if (!issueIdsAvailableAfterImport.has(attachment.issueId)) { - attachmentPlans.push({ source: attachment, action: "skip_missing_parent" }); + attachmentPlans.push({ + source: attachment, + action: "skip_missing_parent", + }); continue; } const adjustments: ImportAdjustment[] = []; const targetCreatedByAgentId = - attachment.createdByAgentId && targetAgentIds.has(attachment.createdByAgentId) + attachment.createdByAgentId && + targetAgentIds.has(attachment.createdByAgentId) ? attachment.createdByAgentId : null; if (attachment.createdByAgentId && !targetCreatedByAgentId) { @@ -716,7 +836,8 @@ export function buildWorktreeMergePlan(input: { source: attachment, action: "insert", targetIssueCommentId: - attachment.issueCommentId && commentIdsAvailableAfterImport.has(attachment.issueCommentId) + attachment.issueCommentId && + commentIdsAvailableAfterImport.has(attachment.issueCommentId) ? attachment.issueCommentId : null, targetCreatedByAgentId, @@ -726,25 +847,52 @@ export function buildWorktreeMergePlan(input: { const counts = { projectsToImport: projectImports.length, - issuesToInsert: issuePlans.filter((plan) => plan.action === "insert").length, - issuesExisting: issuePlans.filter((plan) => plan.action === "skip_existing").length, - issueDrift: issuePlans.filter((plan) => plan.action === "skip_existing" && plan.driftKeys.length > 0).length, - commentsToInsert: commentPlans.filter((plan) => plan.action === "insert").length, - commentsExisting: commentPlans.filter((plan) => plan.action === "skip_existing").length, - commentsMissingParent: commentPlans.filter((plan) => plan.action === "skip_missing_parent").length, - documentsToInsert: documentPlans.filter((plan) => plan.action === "insert").length, - documentsToMerge: documentPlans.filter((plan) => plan.action === "merge_existing").length, - documentsExisting: documentPlans.filter((plan) => plan.action === "skip_existing").length, - documentsConflictingKey: documentPlans.filter((plan) => plan.action === "skip_conflicting_key").length, - documentsMissingParent: documentPlans.filter((plan) => plan.action === "skip_missing_parent").length, + issuesToInsert: issuePlans.filter((plan) => plan.action === "insert") + .length, + issuesExisting: issuePlans.filter((plan) => plan.action === "skip_existing") + .length, + issueDrift: issuePlans.filter( + (plan) => plan.action === "skip_existing" && plan.driftKeys.length > 0, + ).length, + commentsToInsert: commentPlans.filter((plan) => plan.action === "insert") + .length, + commentsExisting: commentPlans.filter( + (plan) => plan.action === "skip_existing", + ).length, + commentsMissingParent: commentPlans.filter( + (plan) => plan.action === "skip_missing_parent", + ).length, + documentsToInsert: documentPlans.filter((plan) => plan.action === "insert") + .length, + documentsToMerge: documentPlans.filter( + (plan) => plan.action === "merge_existing", + ).length, + documentsExisting: documentPlans.filter( + (plan) => plan.action === "skip_existing", + ).length, + documentsConflictingKey: documentPlans.filter( + (plan) => plan.action === "skip_conflicting_key", + ).length, + documentsMissingParent: documentPlans.filter( + (plan) => plan.action === "skip_missing_parent", + ).length, documentRevisionsToInsert: documentPlans.reduce( (sum, plan) => - sum + (plan.action === "insert" || plan.action === "merge_existing" ? plan.revisionsToInsert.length : 0), + sum + + (plan.action === "insert" || plan.action === "merge_existing" + ? plan.revisionsToInsert.length + : 0), 0, ), - attachmentsToInsert: attachmentPlans.filter((plan) => plan.action === "insert").length, - attachmentsExisting: attachmentPlans.filter((plan) => plan.action === "skip_existing").length, - attachmentsMissingParent: attachmentPlans.filter((plan) => plan.action === "skip_missing_parent").length, + attachmentsToInsert: attachmentPlans.filter( + (plan) => plan.action === "insert", + ).length, + attachmentsExisting: attachmentPlans.filter( + (plan) => plan.action === "skip_existing", + ).length, + attachmentsMissingParent: attachmentPlans.filter( + (plan) => plan.action === "skip_missing_parent", + ).length, }; return { diff --git a/cli/src/commands/worktree.ts b/cli/src/commands/worktree.ts index e6a3a2a..d533754 100644 --- a/cli/src/commands/worktree.ts +++ b/cli/src/commands/worktree.ts @@ -47,7 +47,13 @@ import { formatEmbeddedPostgresError, } from "@taskcore/db"; import type { Command } from "commander"; -import { ensureAgentJwtSecret, loadTaskcoreEnvFile, mergeTaskcoreEnvEntries, readTaskcoreEnvEntries, resolveTaskcoreEnvFile } from "../config/env.js"; +import { + ensureAgentJwtSecret, + loadTaskcoreEnvFile, + mergeTaskcoreEnvEntries, + readTaskcoreEnvEntries, + resolveTaskcoreEnvFile, +} from "../config/env.js"; import { expandHomePrefix } from "../config/home.js"; import type { TaskcoreConfig } from "../config/schema.js"; import { readConfig, resolveConfigPath, writeConfig } from "../config/store.js"; @@ -187,7 +193,9 @@ type SeedWorktreeDatabaseResult = { }; function nonEmpty(value: string | null | undefined): string | null { - return typeof value === "string" && value.trim().length > 0 ? value.trim() : null; + return typeof value === "string" && value.trim().length > 0 + ? value.trim() + : null; } function isCurrentSourceConfigPath(sourceConfigPath: string): boolean { @@ -210,23 +218,37 @@ function resolveWorktreeMakeName(name: string): string { "Worktree name must contain only letters, numbers, dots, underscores, or dashes.", ); } - return value.startsWith(WORKTREE_NAME_PREFIX) ? value : `${WORKTREE_NAME_PREFIX}${value}`; + return value.startsWith(WORKTREE_NAME_PREFIX) + ? value + : `${WORKTREE_NAME_PREFIX}${value}`; } function resolveWorktreeHome(explicit?: string): string { - return explicit ?? process.env.TASKCORE_WORKTREES_DIR ?? DEFAULT_WORKTREE_HOME; + return ( + explicit ?? process.env.TASKCORE_WORKTREES_DIR ?? DEFAULT_WORKTREE_HOME + ); } function resolveWorktreeStartPoint(explicit?: string): string | undefined { - return explicit ?? nonEmpty(process.env.TASKCORE_WORKTREE_START_POINT) ?? undefined; + return ( + explicit ?? nonEmpty(process.env.TASKCORE_WORKTREE_START_POINT) ?? undefined + ); } type ConfiguredStorage = { getObject(companyId: string, objectKey: string): Promise; - putObject(companyId: string, objectKey: string, body: Buffer, contentType: string): Promise; + putObject( + companyId: string, + objectKey: string, + body: Buffer, + contentType: string, + ): Promise; }; -function assertStorageCompanyPrefix(companyId: string, objectKey: string): void { +function assertStorageCompanyPrefix( + companyId: string, + objectKey: string, +): void { if (!objectKey.startsWith(`${companyId}/`) || objectKey.includes("..")) { throw new Error(`Invalid object key for company ${companyId}.`); } @@ -238,7 +260,10 @@ function normalizeStorageObjectKey(objectKey: string): string { throw new Error("Invalid object key."); } const parts = normalized.split("/").filter((part) => part.length > 0); - if (parts.length === 0 || parts.some((part) => part === "." || part === "..")) { + if ( + parts.length === 0 || + parts.some((part) => part === "." || part === "..") + ) { throw new Error("Invalid object key."); } return parts.join("/"); @@ -295,15 +320,22 @@ function buildS3ObjectKey(prefix: string, objectKey: string): string { return prefix ? `${prefix}/${objectKey}` : objectKey; } -const dynamicImport = new Function("specifier", "return import(specifier);") as (specifier: string) => Promise; +const dynamicImport = new Function( + "specifier", + "return import(specifier);", +) as (specifier: string) => Promise; -function createConfiguredStorageFromTaskcoreConfig(config: TaskcoreConfig): ConfiguredStorage { +function createConfiguredStorageFromTaskcoreConfig( + config: TaskcoreConfig, +): ConfiguredStorage { if (config.storage.provider === "local_disk") { const baseDir = expandHomePrefix(config.storage.localDisk.baseDir); return { async getObject(companyId: string, objectKey: string) { assertStorageCompanyPrefix(companyId, objectKey); - return await fsPromises.readFile(resolveLocalStoragePath(baseDir, objectKey)); + return await fsPromises.readFile( + resolveLocalStoragePath(baseDir, objectKey), + ); }, async putObject(companyId: string, objectKey: string, body: Buffer) { assertStorageCompanyPrefix(companyId, objectKey); @@ -345,7 +377,12 @@ function createConfiguredStorageFromTaskcoreConfig(config: TaskcoreConfig): Conf ); return await s3BodyToBuffer(response.Body); }, - async putObject(companyId: string, objectKey: string, body: Buffer, contentType: string) { + async putObject( + companyId: string, + objectKey: string, + body: Buffer, + contentType: string, + ) { assertStorageCompanyPrefix(companyId, objectKey); const { sdk, client } = await getS3Client(); await client.send( @@ -379,12 +416,19 @@ async function streamToBuffer(stream: NodeJS.ReadableStream): Promise { export function isMissingStorageObjectError(error: unknown): boolean { if (!error || typeof error !== "object") return false; - const candidate = error as { code?: unknown; status?: unknown; name?: unknown; message?: unknown }; - return candidate.code === "ENOENT" - || candidate.status === 404 - || candidate.name === "NoSuchKey" - || candidate.name === "NotFound" - || candidate.message === "Object not found."; + const candidate = error as { + code?: unknown; + status?: unknown; + name?: unknown; + message?: unknown; + }; + return ( + candidate.code === "ENOENT" || + candidate.status === 404 || + candidate.name === "NoSuchKey" || + candidate.name === "NotFound" || + candidate.message === "Object not found." + ); } export async function readSourceAttachmentBody( @@ -427,10 +471,14 @@ function extractExecSyncErrorMessage(error: unknown): string | null { function localBranchExists(cwd: string, branchName: string): boolean { try { - execFileSync("git", ["show-ref", "--verify", "--quiet", `refs/heads/${branchName}`], { - cwd, - stdio: "ignore", - }); + execFileSync( + "git", + ["show-ref", "--verify", "--quiet", `refs/heads/${branchName}`], + { + cwd, + stdio: "ignore", + }, + ); return true; } catch { return false; @@ -447,7 +495,14 @@ export function resolveGitWorktreeAddArgs(input: { return ["worktree", "add", input.targetPath, input.branchName]; } const commitish = input.startPoint ?? "HEAD"; - return ["worktree", "add", "-b", input.branchName, input.targetPath, commitish]; + return [ + "worktree", + "add", + "-b", + input.branchName, + input.targetPath, + commitish, + ]; } function readPidFilePort(postmasterPidFile: string): number | null { @@ -464,7 +519,9 @@ function readPidFilePort(postmasterPidFile: string): number | null { function readRunningPostmasterPid(postmasterPidFile: string): number | null { if (!existsSync(postmasterPidFile)) return null; try { - const pid = Number(readFileSync(postmasterPidFile, "utf8").split("\n")[0]?.trim()); + const pid = Number( + readFileSync(postmasterPidFile, "utf8").split("\n")[0]?.trim(), + ); if (!Number.isInteger(pid) || pid <= 0) return null; process.kill(pid, 0); return pid; @@ -484,7 +541,10 @@ async function isPortAvailable(port: number): Promise { }); } -async function findAvailablePort(preferredPort: number, reserved = new Set()): Promise { +async function findAvailablePort( + preferredPort: number, + reserved = new Set(), +): Promise { let port = Math.max(1, Math.trunc(preferredPort)); while (reserved.has(port) || !(await isPortAvailable(port))) { port += 1; @@ -501,7 +561,11 @@ function resolveRepoManagedWorktreesRoot(cwd: string): string | null { return path.resolve(repoRoot, ".taskcore", "worktrees"); } -function collectClaimedWorktreePorts(homeDir: string, currentInstanceId: string, cwd: string): { +function collectClaimedWorktreePorts( + homeDir: string, + currentInstanceId: string, + cwd: string, +): { serverPorts: Set; databasePorts: Set; } { @@ -522,9 +586,16 @@ function collectClaimedWorktreePorts(homeDir: string, currentInstanceId: string, const repoManagedWorktreesRoot = resolveRepoManagedWorktreesRoot(cwd); if (repoManagedWorktreesRoot && existsSync(repoManagedWorktreesRoot)) { - for (const entry of readdirSync(repoManagedWorktreesRoot, { withFileTypes: true })) { + for (const entry of readdirSync(repoManagedWorktreesRoot, { + withFileTypes: true, + })) { if (!entry.isDirectory()) continue; - const configPath = path.resolve(repoManagedWorktreesRoot, entry.name, ".taskcore", "config.json"); + const configPath = path.resolve( + repoManagedWorktreesRoot, + entry.name, + ".taskcore", + "config.json", + ); if (existsSync(configPath)) { configPaths.add(configPath); } @@ -572,7 +643,9 @@ function validateGitBranchName(cwd: string, branchName: string): string { stdio: ["ignore", "pipe", "pipe"], }); } catch (error) { - throw new Error(`Invalid branch name "${branchName}": ${extractExecSyncErrorMessage(error) ?? String(error)}`); + throw new Error( + `Invalid branch name "${branchName}": ${extractExecSyncErrorMessage(error) ?? String(error)}`, + ); } return value; } @@ -594,7 +667,8 @@ function resolvePrimaryGitRepoRoot(cwd: string): string { } function resolveRepairWorktreeDirName(branchName: string): string { - const normalized = branchName.trim() + const normalized = branchName + .trim() .replace(/[^A-Za-z0-9._-]+/g, "-") .replace(/-+/g, "-") .replace(/^[-._]+|[-._]+$/g, ""); @@ -608,21 +682,29 @@ function detectGitWorkspaceInfo(cwd: string): GitWorkspaceInfo | null { encoding: "utf8", stdio: ["ignore", "pipe", "ignore"], }).trim(); - const commonDirRaw = execFileSync("git", ["rev-parse", "--git-common-dir"], { - cwd: root, - encoding: "utf8", - stdio: ["ignore", "pipe", "ignore"], - }).trim(); + const commonDirRaw = execFileSync( + "git", + ["rev-parse", "--git-common-dir"], + { + cwd: root, + encoding: "utf8", + stdio: ["ignore", "pipe", "ignore"], + }, + ).trim(); const gitDirRaw = execFileSync("git", ["rev-parse", "--git-dir"], { cwd: root, encoding: "utf8", stdio: ["ignore", "pipe", "ignore"], }).trim(); - const hooksPathRaw = execFileSync("git", ["rev-parse", "--git-path", "hooks"], { - cwd: root, - encoding: "utf8", - stdio: ["ignore", "pipe", "ignore"], - }).trim(); + const hooksPathRaw = execFileSync( + "git", + ["rev-parse", "--git-path", "hooks"], + { + cwd: root, + encoding: "utf8", + stdio: ["ignore", "pipe", "ignore"], + }, + ).trim(); return { root: path.resolve(root), commonDir: path.resolve(root, commonDirRaw), @@ -673,7 +755,9 @@ function copyDirectoryContents(sourceDir: string, targetDir: string): boolean { return copied; } -export function copyGitHooksToWorktreeGitDir(cwd: string): CopiedGitHooksResult | null { +export function copyGitHooksToWorktreeGitDir( + cwd: string, +): CopiedGitHooksResult | null { const workspace = detectGitWorkspaceInfo(cwd); if (!workspace) return null; @@ -776,22 +860,31 @@ async function rebindSeededProjectWorkspaces(input: { } export function resolveSourceConfigPath(opts: WorktreeInitOptions): string { - if (opts.sourceConfigPathOverride) return path.resolve(opts.sourceConfigPathOverride); + if (opts.sourceConfigPathOverride) + return path.resolve(opts.sourceConfigPathOverride); if (opts.fromConfig) return path.resolve(opts.fromConfig); if (!opts.fromDataDir && !opts.fromInstance) { return resolveConfigPath(); } - const sourceHome = path.resolve(expandHomePrefix(opts.fromDataDir ?? "~/.taskcore")); - const sourceInstanceId = sanitizeWorktreeInstanceId(opts.fromInstance ?? "default"); + const sourceHome = path.resolve( + expandHomePrefix(opts.fromDataDir ?? "~/.taskcore"), + ); + const sourceInstanceId = sanitizeWorktreeInstanceId( + opts.fromInstance ?? "default", + ); return path.resolve(sourceHome, "instances", sourceInstanceId, "config.json"); } -export function resolveWorktreeReseedSource(input: WorktreeReseedOptions): ResolvedWorktreeReseedSource { +export function resolveWorktreeReseedSource( + input: WorktreeReseedOptions, +): ResolvedWorktreeReseedSource { const fromSelector = nonEmpty(input.from); const fromConfig = nonEmpty(input.fromConfig); const fromDataDir = nonEmpty(input.fromDataDir); const fromInstance = nonEmpty(input.fromInstance); - const hasExplicitConfigSource = Boolean(fromConfig || fromDataDir || fromInstance); + const hasExplicitConfigSource = Boolean( + fromConfig || fromDataDir || fromInstance, + ); if (fromSelector && hasExplicitConfigSource) { throw new Error( @@ -800,7 +893,9 @@ export function resolveWorktreeReseedSource(input: WorktreeReseedOptions): Resol } if (fromSelector) { - const endpoint = resolveWorktreeEndpointFromSelector(fromSelector, { allowCurrent: true }); + const endpoint = resolveWorktreeEndpointFromSelector(fromSelector, { + allowCurrent: true, + }); return { configPath: endpoint.configPath, label: endpoint.label, @@ -824,7 +919,9 @@ export function resolveWorktreeReseedSource(input: WorktreeReseedOptions): Resol ); } -function resolveWorktreeRepairSource(input: WorktreeRepairOptions): ResolvedWorktreeReseedSource { +function resolveWorktreeRepairSource( + input: WorktreeRepairOptions, +): ResolvedWorktreeReseedSource { const fromConfig = nonEmpty(input.fromConfig); const fromDataDir = nonEmpty(input.fromDataDir); const fromInstance = nonEmpty(input.fromInstance) ?? "default"; @@ -843,7 +940,9 @@ export function resolveWorktreeReseedTargetPaths(input: { configPath: string; rootPath: string; }): WorktreeLocalPaths { - const envEntries = readTaskcoreEnvEntries(resolveTaskcoreEnvFile(input.configPath)); + const envEntries = readTaskcoreEnvEntries( + resolveTaskcoreEnvFile(input.configPath), + ); const homeDir = nonEmpty(envEntries.TASKCORE_HOME); const instanceId = nonEmpty(envEntries.TASKCORE_INSTANCE_ID); @@ -860,7 +959,10 @@ export function resolveWorktreeReseedTargetPaths(input: { }); } -function resolveExistingGitWorktree(selector: string, cwd: string): MergeSourceChoice | null { +function resolveExistingGitWorktree( + selector: string, + cwd: string, +): MergeSourceChoice | null { const trimmed = selector.trim(); if (trimmed.length === 0) return null; @@ -870,17 +972,22 @@ function resolveExistingGitWorktree(selector: string, cwd: string): MergeSourceC worktree: directPath, branch: null, branchLabel: path.basename(directPath), - hasTaskcoreConfig: existsSync(path.resolve(directPath, ".taskcore", "config.json")), + hasTaskcoreConfig: existsSync( + path.resolve(directPath, ".taskcore", "config.json"), + ), isCurrent: directPath === path.resolve(cwd), }; } - return toMergeSourceChoices(cwd).find((choice) => - choice.worktree === directPath - || path.basename(choice.worktree) === trimmed - || choice.branchLabel === trimmed - || choice.branch === trimmed, - ) ?? null; + return ( + toMergeSourceChoices(cwd).find( + (choice) => + choice.worktree === directPath || + path.basename(choice.worktree) === trimmed || + choice.branchLabel === trimmed || + choice.branch === trimmed, + ) ?? null + ); } async function ensureRepairTargetWorktree(input: { @@ -890,7 +997,11 @@ async function ensureRepairTargetWorktree(input: { }): Promise { const cwd = process.cwd(); const currentRoot = path.resolve(cwd); - const currentConfigPath = path.resolve(currentRoot, ".taskcore", "config.json"); + const currentConfigPath = path.resolve( + currentRoot, + ".taskcore", + "config.json", + ); if (!input.selector) { if (isPrimaryGitWorktree(cwd)) { @@ -911,7 +1022,8 @@ async function ensureRepairTargetWorktree(input: { rootPath: existing.worktree, configPath: path.resolve(existing.worktree, ".taskcore", "config.json"), label: existing.branchLabel, - branchName: existing.branchLabel === "(detached)" ? null : existing.branchLabel, + branchName: + existing.branchLabel === "(detached)" ? null : existing.branchLabel, created: false, }; } @@ -926,7 +1038,9 @@ async function ensureRepairTargetWorktree(input: { ); if (existsSync(targetPath)) { - throw new Error(`Target path already exists but is not a registered git worktree: ${targetPath}`); + throw new Error( + `Target path already exists but is not a registered git worktree: ${targetPath}`, + ); } mkdirSync(path.dirname(targetPath), { recursive: true }); @@ -934,14 +1048,18 @@ async function ensureRepairTargetWorktree(input: { const spinner = p.spinner(); spinner.start(`Creating git worktree for ${branchName}...`); try { - execFileSync("git", resolveGitWorktreeAddArgs({ - branchName, - targetPath, - branchExists: localBranchExists(repoRoot, branchName), - }), { - cwd: repoRoot, - stdio: ["ignore", "pipe", "pipe"], - }); + execFileSync( + "git", + resolveGitWorktreeAddArgs({ + branchName, + targetPath, + branchExists: localBranchExists(repoRoot, branchName), + }), + { + cwd: repoRoot, + stdio: ["ignore", "pipe", "pipe"], + }, + ); spinner.stop(`Created git worktree at ${targetPath}.`); } catch (error) { spinner.stop(pc.red("Failed to create git worktree.")); @@ -959,9 +1077,15 @@ async function ensureRepairTargetWorktree(input: { }; } -function resolveSourceConnectionString(config: TaskcoreConfig, envEntries: Record, portOverride?: number): string { +function resolveSourceConnectionString( + config: TaskcoreConfig, + envEntries: Record, + portOverride?: number, +): string { if (config.database.mode === "postgres") { - const connectionString = nonEmpty(envEntries.DATABASE_URL) ?? nonEmpty(config.database.connectionString); + const connectionString = + nonEmpty(envEntries.DATABASE_URL) ?? + nonEmpty(config.database.connectionString); if (!connectionString) { throw new Error( "Source instance uses postgres mode but has no connection string in config or adjacent .env.", @@ -986,10 +1110,14 @@ export function copySeededSecretsKey(input: { mkdirSync(path.dirname(input.targetKeyFilePath), { recursive: true }); - const allowProcessEnvFallback = isCurrentSourceConfigPath(input.sourceConfigPath); + const allowProcessEnvFallback = isCurrentSourceConfigPath( + input.sourceConfigPath, + ); const sourceInlineMasterKey = nonEmpty(input.sourceEnvEntries.TASKCORE_SECRETS_MASTER_KEY) ?? - (allowProcessEnvFallback ? nonEmpty(process.env.TASKCORE_SECRETS_MASTER_KEY) : null); + (allowProcessEnvFallback + ? nonEmpty(process.env.TASKCORE_SECRETS_MASTER_KEY) + : null); if (sourceInlineMasterKey) { writeFileSync(input.targetKeyFilePath, sourceInlineMasterKey, { encoding: "utf8", @@ -1005,9 +1133,16 @@ export function copySeededSecretsKey(input: { const sourceKeyFileOverride = nonEmpty(input.sourceEnvEntries.TASKCORE_SECRETS_MASTER_KEY_FILE) ?? - (allowProcessEnvFallback ? nonEmpty(process.env.TASKCORE_SECRETS_MASTER_KEY_FILE) : null); - const sourceConfiguredKeyPath = sourceKeyFileOverride ?? input.sourceConfig.secrets.localEncrypted.keyFilePath; - const sourceKeyFilePath = resolveRuntimeLikePath(sourceConfiguredKeyPath, input.sourceConfigPath); + (allowProcessEnvFallback + ? nonEmpty(process.env.TASKCORE_SECRETS_MASTER_KEY_FILE) + : null); + const sourceConfiguredKeyPath = + sourceKeyFileOverride ?? + input.sourceConfig.secrets.localEncrypted.keyFilePath; + const sourceKeyFilePath = resolveRuntimeLikePath( + sourceConfiguredKeyPath, + input.sourceConfigPath, + ); if (!existsSync(sourceKeyFilePath)) { throw new Error( @@ -1023,7 +1158,10 @@ export function copySeededSecretsKey(input: { } } -async function ensureEmbeddedPostgres(dataDir: string, preferredPort: number): Promise { +async function ensureEmbeddedPostgres( + dataDir: string, + preferredPort: number, +): Promise { const moduleName = "embedded-postgres"; let EmbeddedPostgres: EmbeddedPostgresCtor; try { @@ -1041,7 +1179,7 @@ async function ensureEmbeddedPostgres(dataDir: string, preferredPort: number): P return { port: readPidFilePort(postmasterPidFile) ?? preferredPort, startedByThisProcess: false, - stop: async () => { }, + stop: async () => {}, }; } @@ -1089,13 +1227,20 @@ async function ensureEmbeddedPostgres(dataDir: string, preferredPort: number): P }; } -export async function pauseSeededScheduledRoutines(connectionString: string): Promise { +export async function pauseSeededScheduledRoutines( + connectionString: string, +): Promise { const db = createDb(connectionString); try { const scheduledRoutineIds = await db .selectDistinct({ routineId: routineTriggers.routineId }) .from(routineTriggers) - .where(and(eq(routineTriggers.kind, "schedule"), eq(routineTriggers.enabled, true))); + .where( + and( + eq(routineTriggers.kind, "schedule"), + eq(routineTriggers.enabled, true), + ), + ); const idsToPause = scheduledRoutineIds .map((row) => row.routineId) .filter((value): value is string => Boolean(value)); @@ -1110,7 +1255,13 @@ export async function pauseSeededScheduledRoutines(connectionString: string): Pr status: "paused", updatedAt: new Date(), }) - .where(and(inArray(routines.id, idsToPause), sql`${routines.status} <> 'paused'`, sql`${routines.status} <> 'archived'`)) + .where( + and( + inArray(routines.id, idsToPause), + sql`${routines.status} <> 'paused'`, + sql`${routines.status} <> 'archived'`, + ), + ) .returning({ id: routines.id }); return paused.length; @@ -1204,7 +1355,9 @@ async function runWorktreeInit(opts: WorktreeInitOptions): Promise { ); const seedMode = opts.seedMode ?? "minimal"; if (!isWorktreeSeedMode(seedMode)) { - throw new Error(`Unsupported seed mode "${seedMode}". Expected one of: minimal, full.`); + throw new Error( + `Unsupported seed mode "${seedMode}". Expected one of: minimal, full.`, + ); } const instanceId = sanitizeWorktreeInstanceId(opts.instance ?? worktreeName); const paths = resolveWorktreeLocalPaths({ @@ -1217,9 +1370,14 @@ async function runWorktreeInit(opts: WorktreeInitOptions): Promise { color: opts.color ?? generateWorktreeColor(), }; const sourceConfigPath = resolveSourceConfigPath(opts); - const sourceConfig = existsSync(sourceConfigPath) ? readConfig(sourceConfigPath) : null; - - if ((existsSync(paths.configPath) || existsSync(paths.instanceRoot)) && !opts.force) { + const sourceConfig = existsSync(sourceConfigPath) + ? readConfig(sourceConfigPath) + : null; + + if ( + (existsSync(paths.configPath) || existsSync(paths.instanceRoot)) && + !opts.force + ) { throw new Error( `Worktree config already exists at ${paths.configPath} or instance data exists at ${paths.instanceRoot}. Re-run with --force to replace it.`, ); @@ -1230,10 +1388,19 @@ async function runWorktreeInit(opts: WorktreeInitOptions): Promise { rmSync(paths.instanceRoot, { recursive: true, force: true }); } - const claimedPorts = collectClaimedWorktreePorts(paths.homeDir, paths.instanceId, paths.cwd); - const preferredServerPort = opts.serverPort ?? ((sourceConfig?.server.port ?? 3100) + 1); - const serverPort = await findAvailablePort(preferredServerPort, claimedPorts.serverPorts); - const preferredDbPort = opts.dbPort ?? ((sourceConfig?.database.embeddedPostgresPort ?? 54329) + 1); + const claimedPorts = collectClaimedWorktreePorts( + paths.homeDir, + paths.instanceId, + paths.cwd, + ); + const preferredServerPort = + opts.serverPort ?? (sourceConfig?.server.port ?? 3100) + 1; + const serverPort = await findAvailablePort( + preferredServerPort, + claimedPorts.serverPorts, + ); + const preferredDbPort = + opts.dbPort ?? (sourceConfig?.database.embeddedPostgresPort ?? 54329) + 1; const databasePort = await findAvailablePort( preferredDbPort, new Set([...claimedPorts.databasePorts, serverPort]), @@ -1246,14 +1413,18 @@ async function runWorktreeInit(opts: WorktreeInitOptions): Promise { }); writeConfig(targetConfig, paths.configPath); - const sourceEnvEntries = readTaskcoreEnvEntries(resolveTaskcoreEnvFile(sourceConfigPath)); + const sourceEnvEntries = readTaskcoreEnvEntries( + resolveTaskcoreEnvFile(sourceConfigPath), + ); const existingAgentJwtSecret = nonEmpty(sourceEnvEntries.TASKCORE_AGENT_JWT_SECRET) ?? nonEmpty(process.env.TASKCORE_AGENT_JWT_SECRET); mergeTaskcoreEnvEntries( { ...buildWorktreeEnvEntries(paths, branding), - ...(existingAgentJwtSecret ? { TASKCORE_AGENT_JWT_SECRET: existingAgentJwtSecret } : {}), + ...(existingAgentJwtSecret + ? { TASKCORE_AGENT_JWT_SECRET: existingAgentJwtSecret } + : {}), }, paths.envPath, ); @@ -1262,7 +1433,8 @@ async function runWorktreeInit(opts: WorktreeInitOptions): Promise { const copiedGitHooks = copyGitHooksToWorktreeGitDir(cwd); let seedSummary: string | null = null; - let reboundWorkspaceSummary: SeedWorktreeDatabaseResult["reboundWorkspaces"] = []; + let reboundWorkspaceSummary: SeedWorktreeDatabaseResult["reboundWorkspaces"] = + []; if (opts.seed !== false) { if (!sourceConfig) { throw new Error( @@ -1270,7 +1442,9 @@ async function runWorktreeInit(opts: WorktreeInitOptions): Promise { ); } const spinner = p.spinner(); - spinner.start(`Seeding isolated worktree database from source instance (${seedMode})...`); + spinner.start( + `Seeding isolated worktree database from source instance (${seedMode})...`, + ); try { const seeded = await seedWorktreeDatabase({ sourceConfigPath, @@ -1294,10 +1468,14 @@ async function runWorktreeInit(opts: WorktreeInitOptions): Promise { p.log.message(pc.dim(`Isolated home: ${paths.homeDir}`)); p.log.message(pc.dim(`Instance: ${paths.instanceId}`)); p.log.message(pc.dim(`Worktree badge: ${branding.name} (${branding.color})`)); - p.log.message(pc.dim(`Server port: ${serverPort} | DB port: ${databasePort}`)); + p.log.message( + pc.dim(`Server port: ${serverPort} | DB port: ${databasePort}`), + ); if (copiedGitHooks?.copied) { p.log.message( - pc.dim(`Mirrored git hooks: ${copiedGitHooks.sourceHooksPath} -> ${copiedGitHooks.targetHooksPath}`), + pc.dim( + `Mirrored git hooks: ${copiedGitHooks.sourceHooksPath} -> ${copiedGitHooks.targetHooksPath}`, + ), ); } if (seedSummary) { @@ -1305,7 +1483,9 @@ async function runWorktreeInit(opts: WorktreeInitOptions): Promise { p.log.message(pc.dim(`Seed snapshot: ${seedSummary}`)); for (const rebound of reboundWorkspaceSummary) { p.log.message( - pc.dim(`Rebound workspace ${rebound.name}: ${rebound.fromCwd} -> ${rebound.toCwd}`), + pc.dim( + `Rebound workspace ${rebound.name}: ${rebound.fromCwd} -> ${rebound.toCwd}`, + ), ); } } @@ -1316,13 +1496,18 @@ async function runWorktreeInit(opts: WorktreeInitOptions): Promise { ); } -export async function worktreeInitCommand(opts: WorktreeInitOptions): Promise { +export async function worktreeInitCommand( + opts: WorktreeInitOptions, +): Promise { printTaskcoreCliBanner(); p.intro(pc.bgCyan(pc.black(" taskcore worktree init "))); await runWorktreeInit(opts); } -export async function worktreeMakeCommand(nameArg: string, opts: WorktreeMakeOptions): Promise { +export async function worktreeMakeCommand( + nameArg: string, + opts: WorktreeMakeOptions, +): Promise { printTaskcoreCliBanner(); p.intro(pc.bgCyan(pc.black(" taskcore worktree:make "))); @@ -1397,7 +1582,9 @@ function installDependenciesBestEffort(targetPath: string): void { }); installSpinner.stop("Installed dependencies."); } catch (error) { - installSpinner.stop(pc.yellow("Failed to install dependencies (continuing anyway).")); + installSpinner.stop( + pc.yellow("Failed to install dependencies (continuing anyway)."), + ); p.log.warning(extractExecSyncErrorMessage(error) ?? String(error)); } } @@ -1484,13 +1671,16 @@ function parseGitWorktreeList(cwd: string): GitWorktreeListEntry[] { function toMergeSourceChoices(cwd: string): MergeSourceChoice[] { const currentCwd = path.resolve(cwd); return parseGitWorktreeList(cwd).map((entry) => { - const branchLabel = entry.branch?.replace(/^refs\/heads\//, "") ?? "(detached)"; + const branchLabel = + entry.branch?.replace(/^refs\/heads\//, "") ?? "(detached)"; const worktreePath = path.resolve(entry.worktree); return { worktree: worktreePath, branch: entry.branch, branchLabel, - hasTaskcoreConfig: existsSync(path.resolve(worktreePath, ".taskcore", "config.json")), + hasTaskcoreConfig: existsSync( + path.resolve(worktreePath, ".taskcore", "config.json"), + ), isCurrent: worktreePath === currentCwd, }; }); @@ -1500,7 +1690,16 @@ function branchHasUniqueCommits(cwd: string, branchName: string): boolean { try { const output = execFileSync( "git", - ["log", "--oneline", branchName, "--not", "--remotes", "--exclude", `refs/heads/${branchName}`, "--branches"], + [ + "log", + "--oneline", + branchName, + "--not", + "--remotes", + "--exclude", + `refs/heads/${branchName}`, + "--branches", + ], { cwd, encoding: "utf8", stdio: ["ignore", "pipe", "pipe"] }, ).trim(); return output.length > 0; @@ -1524,18 +1723,21 @@ function branchExistsOnAnyRemote(cwd: string, branchName: string): boolean { function worktreePathHasUncommittedChanges(worktreePath: string): boolean { try { - const output = execFileSync( - "git", - ["status", "--porcelain"], - { cwd: worktreePath, encoding: "utf8", stdio: ["ignore", "pipe", "pipe"] }, - ).trim(); + const output = execFileSync("git", ["status", "--porcelain"], { + cwd: worktreePath, + encoding: "utf8", + stdio: ["ignore", "pipe", "pipe"], + }).trim(); return output.length > 0; } catch { return false; } } -export async function worktreeCleanupCommand(nameArg: string, opts: WorktreeCleanupOptions): Promise { +export async function worktreeCleanupCommand( + nameArg: string, + opts: WorktreeCleanupOptions, +): Promise { printTaskcoreCliBanner(); p.intro(pc.bgCyan(pc.black(" taskcore worktree:cleanup "))); @@ -1543,7 +1745,9 @@ export async function worktreeCleanupCommand(nameArg: string, opts: WorktreeClea const sourceCwd = process.cwd(); const targetPath = resolveWorktreeMakeTargetPath(name); const instanceId = sanitizeWorktreeInstanceId(opts.instance ?? name); - const homeDir = path.resolve(expandHomePrefix(resolveWorktreeHome(opts.home))); + const homeDir = path.resolve( + expandHomePrefix(resolveWorktreeHome(opts.home)), + ); const instanceRoot = path.resolve(homeDir, "instances", instanceId); // ── 1. Assess current state ────────────────────────────────────────── @@ -1554,11 +1758,15 @@ export async function worktreeCleanupCommand(nameArg: string, opts: WorktreeClea const worktrees = parseGitWorktreeList(sourceCwd); const linkedWorktree = worktrees.find( - (wt) => wt.branch === `refs/heads/${name}` || path.resolve(wt.worktree) === path.resolve(targetPath), + (wt) => + wt.branch === `refs/heads/${name}` || + path.resolve(wt.worktree) === path.resolve(targetPath), ); if (!hasBranch && !hasTargetDir && !hasInstanceData && !linkedWorktree) { - p.log.info("Nothing to clean up β€” no branch, worktree directory, or instance data found."); + p.log.info( + "Nothing to clean up β€” no branch, worktree directory, or instance data found.", + ); p.outro(pc.green("Already clean.")); return; } @@ -1576,7 +1784,7 @@ export async function worktreeCleanupCommand(nameArg: string, opts: WorktreeClea } else { problems.push( `Branch "${name}" has commits not found on any other branch or remote. ` + - `Deleting it will lose work. Push it first, or use --force.`, + `Deleting it will lose work. Push it first, or use --force.`, ); } } @@ -1591,7 +1799,9 @@ export async function worktreeCleanupCommand(nameArg: string, opts: WorktreeClea for (const problem of problems) { p.log.error(problem); } - throw new Error("Safety checks failed. Resolve the issues above or re-run with --force."); + throw new Error( + "Safety checks failed. Resolve the issues above or re-run with --force.", + ); } if (problems.length > 0 && opts.force) { for (const problem of problems) { @@ -1616,7 +1826,9 @@ export async function worktreeCleanupCommand(nameArg: string, opts: WorktreeClea }); spinner.stop(`Removed git worktree at ${linkedWorktree.worktree}.`); } catch (error) { - spinner.stop(pc.yellow(`Could not remove worktree cleanly, will prune instead.`)); + spinner.stop( + pc.yellow(`Could not remove worktree cleanly, will prune instead.`), + ); p.log.warning(extractExecSyncErrorMessage(error) ?? String(error)); } } else { @@ -1671,15 +1883,23 @@ export async function worktreeCleanupCommand(nameArg: string, opts: WorktreeClea p.outro(pc.green("Cleanup complete.")); } -export async function worktreeEnvCommand(opts: WorktreeEnvOptions): Promise { +export async function worktreeEnvCommand( + opts: WorktreeEnvOptions, +): Promise { const configPath = resolveConfigPath(opts.config); const envPath = resolveTaskcoreEnvFile(configPath); const envEntries = readTaskcoreEnvEntries(envPath); const out = { TASKCORE_CONFIG: configPath, - ...(envEntries.TASKCORE_HOME ? { TASKCORE_HOME: envEntries.TASKCORE_HOME } : {}), - ...(envEntries.TASKCORE_INSTANCE_ID ? { TASKCORE_INSTANCE_ID: envEntries.TASKCORE_INSTANCE_ID } : {}), - ...(envEntries.TASKCORE_CONTEXT ? { TASKCORE_CONTEXT: envEntries.TASKCORE_CONTEXT } : {}), + ...(envEntries.TASKCORE_HOME + ? { TASKCORE_HOME: envEntries.TASKCORE_HOME } + : {}), + ...(envEntries.TASKCORE_INSTANCE_ID + ? { TASKCORE_INSTANCE_ID: envEntries.TASKCORE_INSTANCE_ID } + : {}), + ...(envEntries.TASKCORE_CONTEXT + ? { TASKCORE_CONTEXT: envEntries.TASKCORE_CONTEXT } + : {}), ...envEntries, }; @@ -1729,7 +1949,9 @@ function resolveAttachmentLookupStorages(input: { input.targetEndpoint.configPath, ...toMergeSourceChoices(process.cwd()) .filter((choice) => choice.hasTaskcoreConfig) - .map((choice) => path.resolve(choice.worktree, ".taskcore", "config.json")), + .map((choice) => + path.resolve(choice.worktree, ".taskcore", "config.json"), + ), ]; const seen = new Set(); const storages: ConfiguredStorage[] = []; @@ -1757,7 +1979,11 @@ async function openConfiguredDb(configPath: string): Promise { config.database.embeddedPostgresPort, ); } - const connectionString = resolveSourceConnectionString(config, envEntries, embeddedHandle?.port); + const connectionString = resolveSourceConnectionString( + config, + envEntries, + embeddedHandle?.port, + ); const migrationState = await inspectMigrations(connectionString); if (migrationState.status !== "upToDate") { const pending = @@ -1808,15 +2034,23 @@ async function resolveMergeCompany(input: { .from(companies), ]); - const targetById = new Map(targetCompanies.map((company) => [company.id, company])); - const shared = sourceCompanies.filter((company) => targetById.has(company.id)); + const targetById = new Map( + targetCompanies.map((company) => [company.id, company]), + ); + const shared = sourceCompanies.filter((company) => + targetById.has(company.id), + ); const selector = nonEmpty(input.selector); if (selector) { const matched = shared.find( - (company) => company.id === selector || company.issuePrefix.toLowerCase() === selector.toLowerCase(), + (company) => + company.id === selector || + company.issuePrefix.toLowerCase() === selector.toLowerCase(), ); if (!matched) { - throw new Error(`Could not resolve company "${selector}" in both source and target databases.`); + throw new Error( + `Could not resolve company "${selector}" in both source and target databases.`, + ); } return matched; } @@ -1826,20 +2060,27 @@ async function resolveMergeCompany(input: { } if (shared.length === 0) { - throw new Error("Source and target databases do not share a company id. Pass --company explicitly once both sides match."); + throw new Error( + "Source and target databases do not share a company id. Pass --company explicitly once both sides match.", + ); } const options = shared .map((company) => `${company.issuePrefix} (${company.name})`) .join(", "); - throw new Error(`Multiple shared companies found. Re-run with --company . Options: ${options}`); + throw new Error( + `Multiple shared companies found. Re-run with --company . Options: ${options}`, + ); } -function renderMergePlan(plan: Awaited>["plan"], extras: { - sourcePath: string; - targetPath: string; - unsupportedRunCount: number; -}): string { +function renderMergePlan( + plan: Awaited>["plan"], + extras: { + sourcePath: string; + targetPath: string; + unsupportedRunCount: number; + }, +): string { const terminalWidth = Math.max(60, process.stdout.columns ?? 100); const oneLine = (value: string) => value.replace(/\s+/g, " ").trim(); const truncateToWidth = (value: string, maxWidth: number) => { @@ -1872,17 +2113,23 @@ function renderMergePlan(plan: Awaited>["pla } } - const issueInserts = plan.issuePlans.filter((item): item is PlannedIssueInsert => item.action === "insert"); + const issueInserts = plan.issuePlans.filter( + (item): item is PlannedIssueInsert => item.action === "insert", + ); if (issueInserts.length > 0) { lines.push(""); lines.push("Planned issue imports"); for (const issue of issueInserts) { const projectNote = - (issue.projectResolution === "mapped" || issue.projectResolution === "imported") - && issue.mappedProjectName + (issue.projectResolution === "mapped" || + issue.projectResolution === "imported") && + issue.mappedProjectName ? ` project->${issue.projectResolution === "imported" ? "import:" : ""}${issue.mappedProjectName}` : ""; - const adjustments = issue.adjustments.length > 0 ? ` [${issue.adjustments.join(", ")}]` : ""; + const adjustments = + issue.adjustments.length > 0 + ? ` [${issue.adjustments.join(", ")}]` + : ""; const prefix = `- ${issue.source.identifier ?? issue.source.id} -> ${issue.previewIdentifier} (${issue.targetStatus}${projectNote})`; const title = oneLine(issue.source.title); const suffix = `${adjustments}${title ? ` ${title}` : ""}`; @@ -1897,7 +2144,9 @@ function renderMergePlan(plan: Awaited>["pla lines.push("Comments"); lines.push(`- insert: ${plan.counts.commentsToInsert}`); lines.push(`- already present: ${plan.counts.commentsExisting}`); - lines.push(`- skipped (missing parent): ${plan.counts.commentsMissingParent}`); + lines.push( + `- skipped (missing parent): ${plan.counts.commentsMissingParent}`, + ); } lines.push(""); @@ -1905,42 +2154,68 @@ function renderMergePlan(plan: Awaited>["pla lines.push(`- insert: ${plan.counts.documentsToInsert}`); lines.push(`- merge existing: ${plan.counts.documentsToMerge}`); lines.push(`- already present: ${plan.counts.documentsExisting}`); - lines.push(`- skipped (conflicting key): ${plan.counts.documentsConflictingKey}`); - lines.push(`- skipped (missing parent): ${plan.counts.documentsMissingParent}`); + lines.push( + `- skipped (conflicting key): ${plan.counts.documentsConflictingKey}`, + ); + lines.push( + `- skipped (missing parent): ${plan.counts.documentsMissingParent}`, + ); lines.push(`- revisions insert: ${plan.counts.documentRevisionsToInsert}`); lines.push(""); lines.push("Attachments"); lines.push(`- insert: ${plan.counts.attachmentsToInsert}`); lines.push(`- already present: ${plan.counts.attachmentsExisting}`); - lines.push(`- skipped (missing parent): ${plan.counts.attachmentsMissingParent}`); + lines.push( + `- skipped (missing parent): ${plan.counts.attachmentsMissingParent}`, + ); lines.push(""); lines.push("Adjustments"); - lines.push(`- cleared assignee agents: ${plan.adjustments.clear_assignee_agent}`); + lines.push( + `- cleared assignee agents: ${plan.adjustments.clear_assignee_agent}`, + ); lines.push(`- cleared projects: ${plan.adjustments.clear_project}`); - lines.push(`- cleared project workspaces: ${plan.adjustments.clear_project_workspace}`); + lines.push( + `- cleared project workspaces: ${plan.adjustments.clear_project_workspace}`, + ); lines.push(`- cleared goals: ${plan.adjustments.clear_goal}`); - lines.push(`- cleared comment author agents: ${plan.adjustments.clear_author_agent}`); - lines.push(`- cleared document agents: ${plan.adjustments.clear_document_agent}`); - lines.push(`- cleared document revision agents: ${plan.adjustments.clear_document_revision_agent}`); - lines.push(`- cleared attachment author agents: ${plan.adjustments.clear_attachment_agent}`); - lines.push(`- coerced in_progress to todo: ${plan.adjustments.coerce_in_progress_to_todo}`); + lines.push( + `- cleared comment author agents: ${plan.adjustments.clear_author_agent}`, + ); + lines.push( + `- cleared document agents: ${plan.adjustments.clear_document_agent}`, + ); + lines.push( + `- cleared document revision agents: ${plan.adjustments.clear_document_revision_agent}`, + ); + lines.push( + `- cleared attachment author agents: ${plan.adjustments.clear_attachment_agent}`, + ); + lines.push( + `- coerced in_progress to todo: ${plan.adjustments.coerce_in_progress_to_todo}`, + ); lines.push(""); lines.push("Not imported in this phase"); lines.push(`- heartbeat runs: ${extras.unsupportedRunCount}`); lines.push(""); - lines.push("Identifiers shown above are provisional preview values. `--apply` reserves fresh issue numbers at write time."); + lines.push( + "Identifiers shown above are provisional preview values. `--apply` reserves fresh issue numbers at write time.", + ); return lines.join("\n"); } -function resolveRunningEmbeddedPostgresPid(config: TaskcoreConfig): number | null { +function resolveRunningEmbeddedPostgresPid( + config: TaskcoreConfig, +): number | null { if (config.database.mode !== "embedded-postgres") { return null; } - return readRunningPostmasterPid(path.resolve(config.database.embeddedPostgresDataDir, "postmaster.pid")); + return readRunningPostmasterPid( + path.resolve(config.database.embeddedPostgresDataDir, "postmaster.pid"), + ); } async function collectMergePlan(input: { @@ -1979,19 +2254,13 @@ async function collectMergePlan(input: { .from(companies) .where(eq(companies.id, companyId)) .then((rows) => rows[0] ?? null), - input.sourceDb - .select() - .from(issues) - .where(eq(issues.companyId, companyId)), - input.targetDb - .select() - .from(issues) - .where(eq(issues.companyId, companyId)), + input.sourceDb.select().from(issues).where(eq(issues.companyId, companyId)), + input.targetDb.select().from(issues).where(eq(issues.companyId, companyId)), input.scopes.includes("comments") ? input.sourceDb - .select() - .from(issueComments) - .where(eq(issueComments.companyId, companyId)) + .select() + .from(issueComments) + .where(eq(issueComments.companyId, companyId)) : Promise.resolve([]), input.targetDb .select() @@ -2060,7 +2329,10 @@ async function collectMergePlan(input: { createdAt: documentRevisions.createdAt, }) .from(documentRevisions) - .innerJoin(issueDocuments, eq(documentRevisions.documentId, issueDocuments.documentId)) + .innerJoin( + issueDocuments, + eq(documentRevisions.documentId, issueDocuments.documentId), + ) .innerJoin(issues, eq(issueDocuments.issueId, issues.id)) .where(eq(issues.companyId, companyId)), input.targetDb @@ -2076,7 +2348,10 @@ async function collectMergePlan(input: { createdAt: documentRevisions.createdAt, }) .from(documentRevisions) - .innerJoin(issueDocuments, eq(documentRevisions.documentId, issueDocuments.documentId)) + .innerJoin( + issueDocuments, + eq(documentRevisions.documentId, issueDocuments.documentId), + ) .innerJoin(issues, eq(issueDocuments.issueId, issues.id)) .where(eq(issues.companyId, companyId)), input.sourceDb @@ -2139,18 +2414,12 @@ async function collectMergePlan(input: { .select() .from(projects) .where(eq(projects.companyId, companyId)), - input.targetDb - .select() - .from(agents) - .where(eq(agents.companyId, companyId)), + input.targetDb.select().from(agents).where(eq(agents.companyId, companyId)), input.targetDb .select() .from(projectWorkspaces) .where(eq(projectWorkspaces.companyId, companyId)), - input.targetDb - .select() - .from(goals) - .where(eq(goals.companyId, companyId)), + input.targetDb.select().from(goals).where(eq(goals.companyId, companyId)), input.sourceDb .select({ count: sql`count(*)::int` }) .from(heartbeatRuns) @@ -2175,8 +2444,10 @@ async function collectMergePlan(input: { sourceProjectWorkspaces: sourceProjectWorkspaceRows, sourceDocuments: sourceIssueDocumentsRows as IssueDocumentRow[], targetDocuments: targetIssueDocumentsRows as IssueDocumentRow[], - sourceDocumentRevisions: sourceDocumentRevisionRows as DocumentRevisionRow[], - targetDocumentRevisions: targetDocumentRevisionRows as DocumentRevisionRow[], + sourceDocumentRevisions: + sourceDocumentRevisionRows as DocumentRevisionRow[], + targetDocumentRevisions: + targetDocumentRevisionRows as DocumentRevisionRow[], sourceAttachments: sourceAttachmentRows as IssueAttachmentRow[], targetAttachments: targetAttachmentRows as IssueAttachmentRow[], targetAgents: targetAgentsRows, @@ -2202,14 +2473,21 @@ type ProjectMappingSelections = { async function promptForProjectMappings(input: { plan: Awaited>["plan"]; - sourceProjects: Awaited>["sourceProjects"]; - targetProjects: Awaited>["targetProjects"]; + sourceProjects: Awaited< + ReturnType + >["sourceProjects"]; + targetProjects: Awaited< + ReturnType + >["targetProjects"]; }): Promise { const missingProjectIds = [ ...new Set( input.plan.issuePlans .filter((plan): plan is PlannedIssueInsert => plan.action === "insert") - .filter((plan) => !!plan.source.projectId && plan.projectResolution === "cleared") + .filter( + (plan) => + !!plan.source.projectId && plan.projectResolution === "cleared", + ) .map((plan) => plan.source.projectId as string), ), ]; @@ -2220,7 +2498,9 @@ async function promptForProjectMappings(input: { }; } - const sourceProjectsById = new Map(input.sourceProjects.map((project) => [project.id, project])); + const sourceProjectsById = new Map( + input.sourceProjects.map((project) => [project.id, project]), + ); const targetChoices = [...input.targetProjects] .sort((left, right) => left.name.localeCompare(right.name)) .map((project) => ({ @@ -2235,7 +2515,9 @@ async function promptForProjectMappings(input: { const sourceProject = sourceProjectsById.get(sourceProjectId); if (!sourceProject) continue; const nameMatch = input.targetProjects.find( - (project) => project.name.trim().toLowerCase() === sourceProject.name.trim().toLowerCase(), + (project) => + project.name.trim().toLowerCase() === + sourceProject.name.trim().toLowerCase(), ); const importSelectionValue = `__import__:${sourceProjectId}`; const selection = await p.select({ @@ -2247,11 +2529,13 @@ async function promptForProjectMappings(input: { hint: "Create the project and copy its workspace settings", }, ...(nameMatch - ? [{ - value: nameMatch.id, - label: `Map to ${nameMatch.name}`, - hint: "Recommended: exact name match", - }] + ? [ + { + value: nameMatch.id, + label: `Map to ${nameMatch.name}`, + hint: "Recommended: exact name match", + }, + ] : []), { value: null, @@ -2278,7 +2562,9 @@ async function promptForProjectMappings(input: { }; } -export async function worktreeListCommand(opts: WorktreeListOptions): Promise { +export async function worktreeListCommand( + opts: WorktreeListOptions, +): Promise { const choices = toMergeSourceChoices(process.cwd()); if (opts.json) { console.log(JSON.stringify(choices, null, 2)); @@ -2290,11 +2576,15 @@ export async function worktreeListCommand(opts: WorktreeListOptions): Promise value !== null); - p.log.message(`${choice.branchLabel} ${choice.worktree} [${flags.join(", ")}]`); + p.log.message( + `${choice.branchLabel} ${choice.worktree} [${flags.join(", ")}]`, + ); } } -function resolveEndpointFromChoice(choice: MergeSourceChoice): ResolvedWorktreeEndpoint { +function resolveEndpointFromChoice( + choice: MergeSourceChoice, +): ResolvedWorktreeEndpoint { if (choice.isCurrent) { return resolveCurrentEndpoint(); } @@ -2329,7 +2619,9 @@ function resolveWorktreeEndpointFromSelector( } const configPath = path.resolve(directPath, ".taskcore", "config.json"); if (!existsSync(configPath)) { - throw new Error(`Resolved worktree path ${directPath} does not contain .taskcore/config.json.`); + throw new Error( + `Resolved worktree path ${directPath} does not contain .taskcore/config.json.`, + ); } return { rootPath: directPath, @@ -2339,11 +2631,12 @@ function resolveWorktreeEndpointFromSelector( }; } - const matched = choices.find((choice) => - (allowCurrent || !choice.isCurrent) - && (choice.worktree === directPath - || path.basename(choice.worktree) === trimmed - || choice.branchLabel === trimmed), + const matched = choices.find( + (choice) => + (allowCurrent || !choice.isCurrent) && + (choice.worktree === directPath || + path.basename(choice.worktree) === trimmed || + choice.branchLabel === trimmed), ); if (!matched) { throw new Error( @@ -2351,13 +2644,19 @@ function resolveWorktreeEndpointFromSelector( ); } if (!matched.hasTaskcoreConfig && !matched.isCurrent) { - throw new Error(`Resolved worktree "${selector}" does not look like a Taskcore worktree.`); + throw new Error( + `Resolved worktree "${selector}" does not look like a Taskcore worktree.`, + ); } return resolveEndpointFromChoice(matched); } -async function promptForSourceEndpoint(excludeWorktreePath?: string): Promise { - const excluded = excludeWorktreePath ? path.resolve(excludeWorktreePath) : null; +async function promptForSourceEndpoint( + excludeWorktreePath?: string, +): Promise { + const excluded = excludeWorktreePath + ? path.resolve(excludeWorktreePath) + : null; const currentEndpoint = resolveCurrentEndpoint(); const choices = toMergeSourceChoices(process.cwd()) .filter((choice) => choice.hasTaskcoreConfig || choice.isCurrent) @@ -2368,7 +2667,9 @@ async function promptForSourceEndpoint(excludeWorktreePath?: string): Promise({ message: "Choose the source worktree to import from", @@ -2393,27 +2694,37 @@ async function applyMergePlan(input: { const companyId = input.company.id; return await input.targetDb.transaction(async (tx) => { - const importedProjectIds = input.plan.projectImports.map((project) => project.source.id); - const existingImportedProjectIds = importedProjectIds.length > 0 - ? new Set( - (await tx - .select({ id: projects.id }) - .from(projects) - .where(inArray(projects.id, importedProjectIds))) - .map((row) => row.id), - ) - : new Set(); - const projectImports = input.plan.projectImports.filter((project) => !existingImportedProjectIds.has(project.source.id)); - const importedWorkspaceIds = projectImports.flatMap((project) => project.workspaces.map((workspace) => workspace.id)); - const existingImportedWorkspaceIds = importedWorkspaceIds.length > 0 - ? new Set( - (await tx - .select({ id: projectWorkspaces.id }) - .from(projectWorkspaces) - .where(inArray(projectWorkspaces.id, importedWorkspaceIds))) - .map((row) => row.id), - ) - : new Set(); + const importedProjectIds = input.plan.projectImports.map( + (project) => project.source.id, + ); + const existingImportedProjectIds = + importedProjectIds.length > 0 + ? new Set( + ( + await tx + .select({ id: projects.id }) + .from(projects) + .where(inArray(projects.id, importedProjectIds)) + ).map((row) => row.id), + ) + : new Set(); + const projectImports = input.plan.projectImports.filter( + (project) => !existingImportedProjectIds.has(project.source.id), + ); + const importedWorkspaceIds = projectImports.flatMap((project) => + project.workspaces.map((workspace) => workspace.id), + ); + const existingImportedWorkspaceIds = + importedWorkspaceIds.length > 0 + ? new Set( + ( + await tx + .select({ id: projectWorkspaces.id }) + .from(projectWorkspaces) + .where(inArray(projectWorkspaces.id, importedWorkspaceIds)) + ).map((row) => row.id), + ) + : new Set(); let insertedProjects = 0; let insertedProjectWorkspaces = 0; @@ -2468,22 +2779,28 @@ async function applyMergePlan(input: { (plan): plan is PlannedIssueInsert => plan.action === "insert", ); const issueCandidateIds = issueCandidates.map((issue) => issue.source.id); - const existingIssueIds = issueCandidateIds.length > 0 - ? new Set( - (await tx - .select({ id: issues.id }) - .from(issues) - .where(inArray(issues.id, issueCandidateIds))) - .map((row) => row.id), - ) - : new Set(); - const issueInserts = issueCandidates.filter((issue) => !existingIssueIds.has(issue.source.id)); + const existingIssueIds = + issueCandidateIds.length > 0 + ? new Set( + ( + await tx + .select({ id: issues.id }) + .from(issues) + .where(inArray(issues.id, issueCandidateIds)) + ).map((row) => row.id), + ) + : new Set(); + const issueInserts = issueCandidates.filter( + (issue) => !existingIssueIds.has(issue.source.id), + ); let nextIssueNumber = 0; if (issueInserts.length > 0) { const [companyRow] = await tx .update(companies) - .set({ issueCounter: sql`${companies.issueCounter} + ${issueInserts.length}` }) + .set({ + issueCounter: sql`${companies.issueCounter} + ${issueInserts.length}`, + }) .where(eq(companies.id, companyId)) .returning({ issueCounter: companies.issueCounter }); nextIssueNumber = companyRow.issueCounter - issueInserts.length + 1; @@ -2519,7 +2836,9 @@ async function applyMergePlan(input: { identifier, requestDepth: issue.source.requestDepth, billingCode: issue.source.billingCode, - assigneeAdapterOverrides: issue.targetAssigneeAgentId ? issue.source.assigneeAdapterOverrides : null, + assigneeAdapterOverrides: issue.targetAssigneeAgentId + ? issue.source.assigneeAdapterOverrides + : null, executionWorkspaceId: null, executionWorkspacePreference: null, executionWorkspaceSettings: null, @@ -2536,16 +2855,20 @@ async function applyMergePlan(input: { const commentCandidates = input.plan.commentPlans.filter( (plan): plan is PlannedCommentInsert => plan.action === "insert", ); - const commentCandidateIds = commentCandidates.map((comment) => comment.source.id); - const existingCommentIds = commentCandidateIds.length > 0 - ? new Set( - (await tx - .select({ id: issueComments.id }) - .from(issueComments) - .where(inArray(issueComments.id, commentCandidateIds))) - .map((row) => row.id), - ) - : new Set(); + const commentCandidateIds = commentCandidates.map( + (comment) => comment.source.id, + ); + const existingCommentIds = + commentCandidateIds.length > 0 + ? new Set( + ( + await tx + .select({ id: issueComments.id }) + .from(issueComments) + .where(inArray(issueComments.id, commentCandidateIds)) + ).map((row) => row.id), + ) + : new Set(); let insertedComments = 0; for (const comment of commentCandidates) { @@ -2553,7 +2876,12 @@ async function applyMergePlan(input: { const parentExists = await tx .select({ id: issues.id }) .from(issues) - .where(and(eq(issues.id, comment.source.issueId), eq(issues.companyId, companyId))) + .where( + and( + eq(issues.id, comment.source.issueId), + eq(issues.companyId, companyId), + ), + ) .then((rows) => rows[0] ?? null); if (!parentExists) continue; await tx.insert(issueComments).values({ @@ -2580,18 +2908,28 @@ async function applyMergePlan(input: { const parentExists = await tx .select({ id: issues.id }) .from(issues) - .where(and(eq(issues.id, documentPlan.source.issueId), eq(issues.companyId, companyId))) + .where( + and( + eq(issues.id, documentPlan.source.issueId), + eq(issues.companyId, companyId), + ), + ) .then((rows) => rows[0] ?? null); if (!parentExists) continue; const conflictingKeyDocument = await tx .select({ documentId: issueDocuments.documentId }) .from(issueDocuments) - .where(and(eq(issueDocuments.issueId, documentPlan.source.issueId), eq(issueDocuments.key, documentPlan.source.key))) + .where( + and( + eq(issueDocuments.issueId, documentPlan.source.issueId), + eq(issueDocuments.key, documentPlan.source.key), + ), + ) .then((rows) => rows[0] ?? null); if ( - conflictingKeyDocument - && conflictingKeyDocument.documentId !== documentPlan.source.documentId + conflictingKeyDocument && + conflictingKeyDocument.documentId !== documentPlan.source.documentId ) { continue; } @@ -2652,7 +2990,9 @@ async function applyMergePlan(input: { key: documentPlan.source.key, updatedAt: documentPlan.source.linkUpdatedAt, }) - .where(eq(issueDocuments.documentId, documentPlan.source.documentId)); + .where( + eq(issueDocuments.documentId, documentPlan.source.documentId), + ); } await tx @@ -2676,7 +3016,9 @@ async function applyMergePlan(input: { await tx .select({ id: documentRevisions.id }) .from(documentRevisions) - .where(eq(documentRevisions.documentId, documentPlan.source.documentId)) + .where( + eq(documentRevisions.documentId, documentPlan.source.documentId), + ) ).map((row) => row.id), ); for (const revisionPlan of documentPlan.revisionsToInsert) { @@ -2714,7 +3056,12 @@ async function applyMergePlan(input: { const parentExists = await tx .select({ id: issues.id }) .from(issues) - .where(and(eq(issues.id, attachment.source.issueId), eq(issues.companyId, companyId))) + .where( + and( + eq(issues.id, attachment.source.issueId), + eq(issues.companyId, companyId), + ), + ) .then((rows) => rows[0] ?? null); if (!parentExists) continue; @@ -2776,13 +3123,18 @@ async function applyMergePlan(input: { }); } -export async function worktreeMergeHistoryCommand(sourceArg: string | undefined, opts: WorktreeMergeHistoryOptions): Promise { +export async function worktreeMergeHistoryCommand( + sourceArg: string | undefined, + opts: WorktreeMergeHistoryOptions, +): Promise { if (opts.apply && opts.dry) { throw new Error("Use either --apply or --dry, not both."); } if (sourceArg && opts.from) { - throw new Error("Use either the positional source argument or --from, not both."); + throw new Error( + "Use either the positional source argument or --from, not both.", + ); } const targetEndpoint = opts.to @@ -2794,8 +3146,13 @@ export async function worktreeMergeHistoryCommand(sourceArg: string | undefined, ? resolveWorktreeEndpointFromSelector(sourceArg, { allowCurrent: true }) : await promptForSourceEndpoint(targetEndpoint.rootPath); - if (path.resolve(sourceEndpoint.configPath) === path.resolve(targetEndpoint.configPath)) { - throw new Error("Source and target Taskcore configs are the same. Choose different --from/--to worktrees."); + if ( + path.resolve(sourceEndpoint.configPath) === + path.resolve(targetEndpoint.configPath) + ) { + throw new Error( + "Source and target Taskcore configs are the same. Choose different --from/--to worktrees.", + ); } const scopes = parseWorktreeMergeScopes(opts.scope); @@ -2826,8 +3183,8 @@ export async function worktreeMergeHistoryCommand(sourceArg: string | undefined, targetProjects: collected.targetProjects, }); if ( - projectSelections.importProjectIds.length > 0 - || Object.keys(projectSelections.projectIdOverrides).length > 0 + projectSelections.importProjectIds.length > 0 || + Object.keys(projectSelections.projectIdOverrides).length > 0 ) { collected = await collectMergePlan({ sourceDb: sourceHandle.db, @@ -2840,11 +3197,13 @@ export async function worktreeMergeHistoryCommand(sourceArg: string | undefined, } } - console.log(renderMergePlan(collected.plan, { - sourcePath: `${sourceEndpoint.label} (${sourceEndpoint.rootPath})`, - targetPath: `${targetEndpoint.label} (${targetEndpoint.rootPath})`, - unsupportedRunCount: collected.unsupportedRunCount, - })); + console.log( + renderMergePlan(collected.plan, { + sourcePath: `${sourceEndpoint.label} (${sourceEndpoint.rootPath})`, + targetPath: `${targetEndpoint.label} (${targetEndpoint.rootPath})`, + unsupportedRunCount: collected.unsupportedRunCount, + }), + ); if (!opts.apply) { return; @@ -2853,9 +3212,9 @@ export async function worktreeMergeHistoryCommand(sourceArg: string | undefined, const confirmed = opts.yes ? true : await p.confirm({ - message: `Import ${collected.plan.counts.issuesToInsert} issues and ${collected.plan.counts.commentsToInsert} comments from ${sourceEndpoint.label} into ${targetEndpoint.label}?`, - initialValue: false, - }); + message: `Import ${collected.plan.counts.issuesToInsert} issues and ${collected.plan.counts.commentsToInsert} comments from ${sourceEndpoint.label} into ${targetEndpoint.label}?`, + initialValue: false, + }); if (p.isCancel(confirmed) || !confirmed) { p.log.warn("Import cancelled."); return; @@ -2887,7 +3246,9 @@ export async function worktreeMergeHistoryCommand(sourceArg: string | undefined, async function runWorktreeReseed(opts: WorktreeReseedOptions): Promise { const seedMode = opts.seedMode ?? "full"; if (!isWorktreeSeedMode(seedMode)) { - throw new Error(`Unsupported seed mode "${seedMode}". Expected one of: minimal, full.`); + throw new Error( + `Unsupported seed mode "${seedMode}". Expected one of: minimal, full.`, + ); } const targetEndpoint = opts.to @@ -2895,8 +3256,12 @@ async function runWorktreeReseed(opts: WorktreeReseedOptions): Promise { : resolveCurrentEndpoint(); const source = resolveWorktreeReseedSource(opts); - if (path.resolve(source.configPath) === path.resolve(targetEndpoint.configPath)) { - throw new Error("Source and target Taskcore configs are the same. Choose different --from/--to values."); + if ( + path.resolve(source.configPath) === path.resolve(targetEndpoint.configPath) + ) { + throw new Error( + "Source and target Taskcore configs are the same. Choose different --from/--to values.", + ); } if (!existsSync(source.configPath)) { throw new Error(`Source config not found at ${source.configPath}.`); @@ -2925,20 +3290,24 @@ async function runWorktreeReseed(opts: WorktreeReseedOptions): Promise { const confirmed = opts.yes ? true : await p.confirm({ - message: `Overwrite the isolated Taskcore DB for ${targetEndpoint.label} from ${source.label} using ${seedMode} seed mode?`, - initialValue: false, - }); + message: `Overwrite the isolated Taskcore DB for ${targetEndpoint.label} from ${source.label} using ${seedMode} seed mode?`, + initialValue: false, + }); if (p.isCancel(confirmed) || !confirmed) { p.log.warn("Reseed cancelled."); return; } if (runningTargetPid && opts.allowLiveTarget) { - p.log.warning(`Proceeding even though the target embedded PostgreSQL appears to be running (pid ${runningTargetPid}).`); + p.log.warning( + `Proceeding even though the target embedded PostgreSQL appears to be running (pid ${runningTargetPid}).`, + ); } const spinner = p.spinner(); - spinner.start(`Reseeding ${targetEndpoint.label} from ${source.label} (${seedMode})...`); + spinner.start( + `Reseeding ${targetEndpoint.label} from ${source.label} (${seedMode})...`, + ); try { const seeded = await seedWorktreeDatabase({ sourceConfigPath: source.configPath, @@ -2954,7 +3323,9 @@ async function runWorktreeReseed(opts: WorktreeReseedOptions): Promise { p.log.message(pc.dim(`Seed snapshot: ${seeded.backupSummary}`)); for (const rebound of seeded.reboundWorkspaces) { p.log.message( - pc.dim(`Rebound workspace ${rebound.name}: ${rebound.fromCwd} -> ${rebound.toCwd}`), + pc.dim( + `Rebound workspace ${rebound.name}: ${rebound.fromCwd} -> ${rebound.toCwd}`, + ), ); } p.outro(pc.green(`Reseed complete for ${targetEndpoint.label}.`)); @@ -2964,19 +3335,25 @@ async function runWorktreeReseed(opts: WorktreeReseedOptions): Promise { } } -export async function worktreeReseedCommand(opts: WorktreeReseedOptions): Promise { +export async function worktreeReseedCommand( + opts: WorktreeReseedOptions, +): Promise { printTaskcoreCliBanner(); p.intro(pc.bgCyan(pc.black(" taskcore worktree reseed "))); await runWorktreeReseed(opts); } -export async function worktreeRepairCommand(opts: WorktreeRepairOptions): Promise { +export async function worktreeRepairCommand( + opts: WorktreeRepairOptions, +): Promise { printTaskcoreCliBanner(); p.intro(pc.bgCyan(pc.black(" taskcore worktree repair "))); const seedMode = opts.seedMode ?? "minimal"; if (!isWorktreeSeedMode(seedMode)) { - throw new Error(`Unsupported seed mode "${seedMode}". Expected one of: minimal, full.`); + throw new Error( + `Unsupported seed mode "${seedMode}". Expected one of: minimal, full.`, + ); } const target = await ensureRepairTargetWorktree({ @@ -2985,7 +3362,9 @@ export async function worktreeRepairCommand(opts: WorktreeRepairOptions): Promis opts, }); if (!target) { - p.log.warn("Current checkout is the primary repo worktree. Pass --branch to create or repair a linked worktree."); + p.log.warn( + "Current checkout is the primary repo worktree. Pass --branch to create or repair a linked worktree.", + ); p.outro(pc.yellow("No worktree repaired.")); return; } @@ -2995,18 +3374,31 @@ export async function worktreeRepairCommand(opts: WorktreeRepairOptions): Promis throw new Error(`Source config not found at ${source.configPath}.`); } if (path.resolve(source.configPath) === path.resolve(target.configPath)) { - throw new Error("Source and target Taskcore configs are the same. Use --from-config/--from-instance to point repair at a different source."); + throw new Error( + "Source and target Taskcore configs are the same. Use --from-config/--from-instance to point repair at a different source.", + ); } - const targetConfig = existsSync(target.configPath) ? readConfig(target.configPath) : null; - const targetEnvEntries = readTaskcoreEnvEntries(resolveTaskcoreEnvFile(target.configPath)); + const targetConfig = existsSync(target.configPath) + ? readConfig(target.configPath) + : null; + const targetEnvEntries = readTaskcoreEnvEntries( + resolveTaskcoreEnvFile(target.configPath), + ); const targetHasWorktreeEnv = Boolean( - nonEmpty(targetEnvEntries.TASKCORE_HOME) && nonEmpty(targetEnvEntries.TASKCORE_INSTANCE_ID), + nonEmpty(targetEnvEntries.TASKCORE_HOME) && + nonEmpty(targetEnvEntries.TASKCORE_INSTANCE_ID), ); if (targetConfig && targetHasWorktreeEnv && opts.noSeed) { - p.log.message(pc.dim(`Target ${target.label} already has worktree-local config/env. Skipping reseed because --no-seed was passed.`)); - p.outro(pc.green(`Worktree metadata already looks healthy for ${target.label}.`)); + p.log.message( + pc.dim( + `Target ${target.label} already has worktree-local config/env. Skipping reseed because --no-seed was passed.`, + ), + ); + p.outro( + pc.green(`Worktree metadata already looks healthy for ${target.label}.`), + ); return; } @@ -3021,20 +3413,26 @@ export async function worktreeRepairCommand(opts: WorktreeRepairOptions): Promis return; } - const repairInstanceId = sanitizeWorktreeInstanceId(path.basename(target.rootPath)); + const repairInstanceId = sanitizeWorktreeInstanceId( + path.basename(target.rootPath), + ); const repairPaths = resolveWorktreeLocalPaths({ cwd: target.rootPath, homeDir: resolveWorktreeHome(opts.home), instanceId: repairInstanceId, }); - const runningTargetPid = readRunningPostmasterPid(path.resolve(repairPaths.embeddedPostgresDataDir, "postmaster.pid")); + const runningTargetPid = readRunningPostmasterPid( + path.resolve(repairPaths.embeddedPostgresDataDir, "postmaster.pid"), + ); if (runningTargetPid && !opts.allowLiveTarget) { throw new Error( `Target worktree database appears to be running (pid ${runningTargetPid}). Stop Taskcore in ${target.rootPath} before repairing, or re-run with --allow-live-target if you want to override this guard.`, ); } if (runningTargetPid && opts.allowLiveTarget) { - p.log.warning(`Proceeding even though the target embedded PostgreSQL appears to be running (pid ${runningTargetPid}).`); + p.log.warning( + `Proceeding even though the target embedded PostgreSQL appears to be running (pid ${runningTargetPid}).`, + ); } const originalCwd = process.cwd(); @@ -3055,99 +3453,244 @@ export async function worktreeRepairCommand(opts: WorktreeRepairOptions): Promis } export function registerWorktreeCommands(program: Command): void { - const worktree = program.command("worktree").description("Worktree-local Taskcore instance helpers"); + const worktree = program + .command("worktree") + .description("Worktree-local Taskcore instance helpers"); program .command("worktree:make") - .description("Create ~/NAME as a git worktree, then initialize an isolated Taskcore instance inside it") - .argument("", "Worktree name β€” auto-prefixed with taskcore- if needed (created at ~/taskcore-NAME)") - .option("--start-point ", "Remote ref to base the new branch on (env: TASKCORE_WORKTREE_START_POINT)") + .description( + "Create ~/NAME as a git worktree, then initialize an isolated Taskcore instance inside it", + ) + .argument( + "", + "Worktree name β€” auto-prefixed with taskcore- if needed (created at ~/taskcore-NAME)", + ) + .option( + "--start-point ", + "Remote ref to base the new branch on (env: TASKCORE_WORKTREE_START_POINT)", + ) .option("--instance ", "Explicit isolated instance id") - .option("--home ", `Home root for worktree instances (env: TASKCORE_WORKTREES_DIR, default: ${DEFAULT_WORKTREE_HOME})`) + .option( + "--home ", + `Home root for worktree instances (env: TASKCORE_WORKTREES_DIR, default: ${DEFAULT_WORKTREE_HOME})`, + ) .option("--from-config ", "Source config.json to seed from") - .option("--from-data-dir ", "Source TASKCORE_HOME used when deriving the source config") - .option("--from-instance ", "Source instance id when deriving the source config", "default") - .option("--server-port ", "Preferred server port", (value) => Number(value)) - .option("--db-port ", "Preferred embedded Postgres port", (value) => Number(value)) - .option("--seed-mode ", "Seed profile: minimal or full (default: minimal)", "minimal") + .option( + "--from-data-dir ", + "Source TASKCORE_HOME used when deriving the source config", + ) + .option( + "--from-instance ", + "Source instance id when deriving the source config", + "default", + ) + .option("--server-port ", "Preferred server port", (value) => + Number(value), + ) + .option("--db-port ", "Preferred embedded Postgres port", (value) => + Number(value), + ) + .option( + "--seed-mode ", + "Seed profile: minimal or full (default: minimal)", + "minimal", + ) .option("--no-seed", "Skip database seeding from the source instance") - .option("--force", "Replace existing repo-local config and isolated instance data", false) + .option( + "--force", + "Replace existing repo-local config and isolated instance data", + false, + ) .action(worktreeMakeCommand); worktree .command("init") - .description("Create repo-local config/env and an isolated instance for this worktree") + .description( + "Create repo-local config/env and an isolated instance for this worktree", + ) .option("--name ", "Display name used to derive the instance id") .option("--instance ", "Explicit isolated instance id") - .option("--home ", `Home root for worktree instances (env: TASKCORE_WORKTREES_DIR, default: ${DEFAULT_WORKTREE_HOME})`) + .option( + "--home ", + `Home root for worktree instances (env: TASKCORE_WORKTREES_DIR, default: ${DEFAULT_WORKTREE_HOME})`, + ) .option("--from-config ", "Source config.json to seed from") - .option("--from-data-dir ", "Source TASKCORE_HOME used when deriving the source config") - .option("--from-instance ", "Source instance id when deriving the source config", "default") - .option("--server-port ", "Preferred server port", (value) => Number(value)) - .option("--db-port ", "Preferred embedded Postgres port", (value) => Number(value)) - .option("--seed-mode ", "Seed profile: minimal or full (default: minimal)", "minimal") + .option( + "--from-data-dir ", + "Source TASKCORE_HOME used when deriving the source config", + ) + .option( + "--from-instance ", + "Source instance id when deriving the source config", + "default", + ) + .option("--server-port ", "Preferred server port", (value) => + Number(value), + ) + .option("--db-port ", "Preferred embedded Postgres port", (value) => + Number(value), + ) + .option( + "--seed-mode ", + "Seed profile: minimal or full (default: minimal)", + "minimal", + ) .option("--no-seed", "Skip database seeding from the source instance") - .option("--force", "Replace existing repo-local config and isolated instance data", false) + .option( + "--force", + "Replace existing repo-local config and isolated instance data", + false, + ) .action(worktreeInitCommand); worktree .command("env") - .description("Print shell exports for the current worktree-local Taskcore instance") + .description( + "Print shell exports for the current worktree-local Taskcore instance", + ) .option("-c, --config ", "Path to config file") .option("--json", "Print JSON instead of shell exports") .action(worktreeEnvCommand); program .command("worktree:list") - .description("List git worktrees visible from this repo and whether they look like Taskcore worktrees") + .description( + "List git worktrees visible from this repo and whether they look like Taskcore worktrees", + ) .option("--json", "Print JSON instead of text output") .action(worktreeListCommand); program .command("worktree:merge-history") - .description("Preview or import issue/comment history from another worktree into the current instance") - .argument("[source]", "Optional source worktree path, directory name, or branch name (back-compat alias for --from)") - .option("--from ", "Source worktree path, directory name, branch name, or current") - .option("--to ", "Target worktree path, directory name, branch name, or current (defaults to current)") - .option("--company ", "Shared company id or issue prefix inside the chosen source/target instances") - .option("--scope ", "Comma-separated scopes to import (issues, comments)", "issues,comments") + .description( + "Preview or import issue/comment history from another worktree into the current instance", + ) + .argument( + "[source]", + "Optional source worktree path, directory name, or branch name (back-compat alias for --from)", + ) + .option( + "--from ", + "Source worktree path, directory name, branch name, or current", + ) + .option( + "--to ", + "Target worktree path, directory name, branch name, or current (defaults to current)", + ) + .option( + "--company ", + "Shared company id or issue prefix inside the chosen source/target instances", + ) + .option( + "--scope ", + "Comma-separated scopes to import (issues, comments)", + "issues,comments", + ) .option("--apply", "Apply the import after previewing the plan", false) .option("--dry", "Preview only and do not import anything", false) - .option("--yes", "Skip the interactive confirmation prompt when applying", false) + .option( + "--yes", + "Skip the interactive confirmation prompt when applying", + false, + ) .action(worktreeMergeHistoryCommand); worktree .command("reseed") - .description("Re-seed an existing worktree-local instance from another Taskcore instance or worktree") - .option("--from ", "Source worktree path, directory name, branch name, or current") - .option("--to ", "Target worktree path, directory name, branch name, or current (defaults to current)") + .description( + "Re-seed an existing worktree-local instance from another Taskcore instance or worktree", + ) + .option( + "--from ", + "Source worktree path, directory name, branch name, or current", + ) + .option( + "--to ", + "Target worktree path, directory name, branch name, or current (defaults to current)", + ) .option("--from-config ", "Source config.json to seed from") - .option("--from-data-dir ", "Source TASKCORE_HOME used when deriving the source config") - .option("--from-instance ", "Source instance id when deriving the source config") - .option("--seed-mode ", "Seed profile: minimal or full (default: full)", "full") + .option( + "--from-data-dir ", + "Source TASKCORE_HOME used when deriving the source config", + ) + .option( + "--from-instance ", + "Source instance id when deriving the source config", + ) + .option( + "--seed-mode ", + "Seed profile: minimal or full (default: full)", + "full", + ) .option("--yes", "Skip the destructive confirmation prompt", false) - .option("--allow-live-target", "Override the guard that requires the target worktree DB to be stopped first", false) + .option( + "--allow-live-target", + "Override the guard that requires the target worktree DB to be stopped first", + false, + ) .action(worktreeReseedCommand); worktree .command("repair") - .description("Create or repair a linked worktree-local Taskcore instance without touching the primary checkout") - .option("--branch ", "Existing branch/worktree selector to repair, or a branch name to create under .taskcore/worktrees") - .option("--home ", `Home root for worktree instances (env: TASKCORE_WORKTREES_DIR, default: ${DEFAULT_WORKTREE_HOME})`) + .description( + "Create or repair a linked worktree-local Taskcore instance without touching the primary checkout", + ) + .option( + "--branch ", + "Existing branch/worktree selector to repair, or a branch name to create under .taskcore/worktrees", + ) + .option( + "--home ", + `Home root for worktree instances (env: TASKCORE_WORKTREES_DIR, default: ${DEFAULT_WORKTREE_HOME})`, + ) .option("--from-config ", "Source config.json to seed from") - .option("--from-data-dir ", "Source TASKCORE_HOME used when deriving the source config") - .option("--from-instance ", "Source instance id when deriving the source config (default: default)") - .option("--seed-mode ", "Seed profile: minimal or full (default: minimal)", "minimal") - .option("--no-seed", "Repair metadata only and skip reseeding when bootstrapping a missing worktree config", false) - .option("--allow-live-target", "Override the guard that requires the target worktree DB to be stopped first", false) + .option( + "--from-data-dir ", + "Source TASKCORE_HOME used when deriving the source config", + ) + .option( + "--from-instance ", + "Source instance id when deriving the source config (default: default)", + ) + .option( + "--seed-mode ", + "Seed profile: minimal or full (default: minimal)", + "minimal", + ) + .option( + "--no-seed", + "Repair metadata only and skip reseeding when bootstrapping a missing worktree config", + false, + ) + .option( + "--allow-live-target", + "Override the guard that requires the target worktree DB to be stopped first", + false, + ) .action(worktreeRepairCommand); program .command("worktree:cleanup") - .description("Safely remove a worktree, its branch, and its isolated instance data") - .argument("", "Worktree name β€” auto-prefixed with taskcore- if needed") - .option("--instance ", "Explicit instance id (if different from the worktree name)") - .option("--home ", `Home root for worktree instances (env: TASKCORE_WORKTREES_DIR, default: ${DEFAULT_WORKTREE_HOME})`) - .option("--force", "Bypass safety checks (uncommitted changes, unique commits)", false) + .description( + "Safely remove a worktree, its branch, and its isolated instance data", + ) + .argument( + "", + "Worktree name β€” auto-prefixed with taskcore- if needed", + ) + .option( + "--instance ", + "Explicit instance id (if different from the worktree name)", + ) + .option( + "--home ", + `Home root for worktree instances (env: TASKCORE_WORKTREES_DIR, default: ${DEFAULT_WORKTREE_HOME})`, + ) + .option( + "--force", + "Bypass safety checks (uncommitted changes, unique commits)", + false, + ) .action(worktreeCleanupCommand); } diff --git a/cli/src/prompts/database.ts b/cli/src/prompts/database.ts index 172234c..0da868b 100644 --- a/cli/src/prompts/database.ts +++ b/cli/src/prompts/database.ts @@ -6,7 +6,9 @@ import { resolveTaskcoreInstanceId, } from "../config/home.js"; -export async function promptDatabase(current?: DatabaseConfig): Promise { +export async function promptDatabase( + current?: DatabaseConfig, +): Promise { const instanceId = resolveTaskcoreInstanceId(); const defaultEmbeddedDir = resolveDefaultEmbeddedPostgresDir(instanceId); const defaultBackupDir = resolveDefaultBackupDir(instanceId); @@ -25,7 +27,11 @@ export async function promptDatabase(current?: DatabaseConfig): Promise { if (!val) return "Connection string is required for PostgreSQL mode"; - if (!val.startsWith("postgres")) return "Must be a postgres:// or postgresql:// URL"; + if (!val.startsWith("postgres")) + return "Must be a postgres:// or postgresql:// URL"; }, }); @@ -77,7 +85,8 @@ export async function promptDatabase(current?: DatabaseConfig): Promise { const n = Number(val); - if (!Number.isInteger(n) || n < 1 || n > 65535) return "Port must be an integer between 1 and 65535"; + if (!Number.isInteger(n) || n < 1 || n > 65535) + return "Port must be an integer between 1 and 65535"; }, }); @@ -103,7 +112,10 @@ export async function promptDatabase(current?: DatabaseConfig): Promise (!val || val.trim().length === 0 ? "Backup directory is required" : undefined), + validate: (val) => + !val || val.trim().length === 0 + ? "Backup directory is required" + : undefined, }); if (p.isCancel(backupDirInput)) { p.cancel("Setup cancelled."); @@ -116,7 +128,8 @@ export async function promptDatabase(current?: DatabaseConfig): Promise { const n = Number(val); - if (!Number.isInteger(n) || n < 1) return "Interval must be a positive integer"; + if (!Number.isInteger(n) || n < 1) + return "Interval must be a positive integer"; if (n > 10080) return "Interval must be 10080 minutes (7 days) or less"; return undefined; }, @@ -132,7 +145,8 @@ export async function promptDatabase(current?: DatabaseConfig): Promise { const n = Number(val); - if (!Number.isInteger(n) || n < 1) return "Retention must be a positive integer"; + if (!Number.isInteger(n) || n < 1) + return "Retention must be a positive integer"; if (n > 3650) return "Retention must be 3650 days or less"; return undefined; }, diff --git a/cli/src/prompts/logging.ts b/cli/src/prompts/logging.ts index d5508d9..888aabe 100644 --- a/cli/src/prompts/logging.ts +++ b/cli/src/prompts/logging.ts @@ -1,13 +1,20 @@ import * as p from "@clack/prompts"; import type { LoggingConfig } from "../config/schema.js"; -import { resolveDefaultLogsDir, resolveTaskcoreInstanceId } from "../config/home.js"; +import { + resolveDefaultLogsDir, + resolveTaskcoreInstanceId, +} from "../config/home.js"; export async function promptLogging(): Promise { const defaultLogDir = resolveDefaultLogsDir(resolveTaskcoreInstanceId()); const mode = await p.select({ message: "Logging mode", options: [ - { value: "file" as const, label: "File-based logging", hint: "recommended" }, + { + value: "file" as const, + label: "File-based logging", + hint: "recommended", + }, { value: "cloud" as const, label: "Cloud logging", hint: "coming soon" }, ], }); diff --git a/cli/src/prompts/secrets.ts b/cli/src/prompts/secrets.ts index 2f8622f..0a7d929 100644 --- a/cli/src/prompts/secrets.ts +++ b/cli/src/prompts/secrets.ts @@ -1,7 +1,10 @@ import * as p from "@clack/prompts"; import type { SecretProvider } from "@taskcore/shared"; import type { SecretsConfig } from "../config/schema.js"; -import { resolveDefaultSecretsKeyFilePath, resolveTaskcoreInstanceId } from "../config/home.js"; +import { + resolveDefaultSecretsKeyFilePath, + resolveTaskcoreInstanceId, +} from "../config/home.js"; function defaultKeyFilePath(): string { return resolveDefaultSecretsKeyFilePath(resolveTaskcoreInstanceId()); @@ -18,7 +21,9 @@ export function defaultSecretsConfig(): SecretsConfig { }; } -export async function promptSecrets(current?: SecretsConfig): Promise { +export async function promptSecrets( + current?: SecretsConfig, +): Promise { const base = current ?? defaultSecretsConfig(); const provider = await p.select({ @@ -71,7 +76,8 @@ export async function promptSecrets(current?: SecretsConfig): Promise { - if (!value || value.trim().length === 0) return "Key file path is required"; + if (!value || value.trim().length === 0) + return "Key file path is required"; }, }); diff --git a/cli/src/prompts/server.ts b/cli/src/prompts/server.ts index 9b4439e..7470014 100644 --- a/cli/src/prompts/server.ts +++ b/cli/src/prompts/server.ts @@ -2,7 +2,11 @@ import * as p from "@clack/prompts"; import { isLoopbackHost, type BindMode } from "@taskcore/shared"; import type { AuthConfig, ServerConfig } from "../config/schema.js"; import { parseHostnameCsv } from "../config/hostnames.js"; -import { buildCustomServerConfig, buildPresetServerConfig, inferConfiguredBind } from "../config/server-bind.js"; +import { + buildCustomServerConfig, + buildPresetServerConfig, + inferConfiguredBind, +} from "../config/server-bind.js"; const TAILNET_BIND_WARNING = "No Tailscale address was detected during setup. The saved config will stay on loopback until Tailscale is available or TASKCORE_TAILNET_BIND_HOST is set."; @@ -123,7 +127,8 @@ export async function promptServer(opts?: { }); if (p.isCancel(deploymentModeSelection)) cancelled(); - const deploymentMode = deploymentModeSelection as ServerConfig["deploymentMode"]; + const deploymentMode = + deploymentModeSelection as ServerConfig["deploymentMode"]; let exposure: ServerConfig["exposure"] = "private"; if (deploymentMode === "authenticated") { @@ -193,7 +198,8 @@ export async function promptServer(opts?: { placeholder: "https://taskcore.example.com", validate: (val) => { const candidate = val.trim(); - if (!candidate) return "Public base URL is required for public exposure"; + if (!candidate) + return "Public base URL is required for public exposure"; try { const url = new URL(candidate); if (url.protocol !== "http:" && url.protocol !== "https:") { diff --git a/cli/src/prompts/storage.ts b/cli/src/prompts/storage.ts index a91f422..cfabda5 100644 --- a/cli/src/prompts/storage.ts +++ b/cli/src/prompts/storage.ts @@ -1,6 +1,9 @@ import * as p from "@clack/prompts"; import type { StorageConfig } from "../config/schema.js"; -import { resolveDefaultStorageDir, resolveTaskcoreInstanceId } from "../config/home.js"; +import { + resolveDefaultStorageDir, + resolveTaskcoreInstanceId, +} from "../config/home.js"; function defaultStorageBaseDir(): string { return resolveDefaultStorageDir(resolveTaskcoreInstanceId()); @@ -22,7 +25,9 @@ export function defaultStorageConfig(): StorageConfig { }; } -export async function promptStorage(current?: StorageConfig): Promise { +export async function promptStorage( + current?: StorageConfig, +): Promise { const base = current ?? defaultStorageConfig(); const provider = await p.select({ @@ -53,7 +58,8 @@ export async function promptStorage(current?: StorageConfig): Promise { - if (!value || value.trim().length === 0) return "Storage base directory is required"; + if (!value || value.trim().length === 0) + return "Storage base directory is required"; }, }); @@ -143,4 +149,3 @@ export async function promptStorage(current?: StorageConfig): Promise Date: Fri, 17 Apr 2026 15:30:27 +0600 Subject: [PATCH 3/8] =?UTF-8?q?=F0=9F=94=A7=20Update=20CLI=20config,=20uti?= =?UTF-8?q?ls,=20entry=20points,=20and=20package?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- cli/README.md | 31 +++++++----------- cli/package.json | 2 +- cli/src/config/data-dir.ts | 8 +++-- cli/src/config/env.ts | 31 ++++++++++++++---- cli/src/config/home.ts | 26 ++++++++++++--- cli/src/config/hostnames.ts | 5 +-- cli/src/config/server-bind.ts | 41 ++++++++++++++--------- cli/src/config/store.ts | 54 ++++++++++++++++++++---------- cli/src/index.ts | 60 +++++++++++++++++++++++++++------- cli/src/telemetry.ts | 20 +++++++----- cli/src/utils/net.ts | 4 ++- cli/src/utils/path-resolver.ts | 9 +++-- 12 files changed, 198 insertions(+), 93 deletions(-) diff --git a/cli/README.md b/cli/README.md index b01d63b..8e37f32 100644 --- a/cli/README.md +++ b/cli/README.md @@ -1,6 +1,4 @@ -

- Taskcore β€” runs your business -

+# TaskCore

Quickstart · @@ -129,13 +127,13 @@ Monitor and manage your autonomous businesses from anywhere. ## Problems Taskcore solves -| Without Taskcore | With Taskcore | -| ------------------------------------------------------------------------------------------------------------------------------------- | -------------------------------------------------------------------------------------------------------------------------------------- | -| ❌ You have 20 Claude Code tabs open and can't track which one does what. On reboot you lose everything. | βœ… Tasks are ticket-based, conversations are threaded, sessions persist across reboots. | -| ❌ You manually gather context from several places to remind your bot what you're actually doing. | βœ… Context flows from the task up through the project and company goals β€” your agent always knows what to do and why. | +| Without Taskcore | With Taskcore | +| ------------------------------------------------------------------------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------- | +| ❌ You have 20 Claude Code tabs open and can't track which one does what. On reboot you lose everything. | βœ… Tasks are ticket-based, conversations are threaded, sessions persist across reboots. | +| ❌ You manually gather context from several places to remind your bot what you're actually doing. | βœ… Context flows from the task up through the project and company goals β€” your agent always knows what to do and why. | | ❌ Folders of agent configs are disorganized and you're re-inventing task management, communication, and coordination between agents. | βœ… Taskcore gives you org charts, ticketing, delegation, and governance out of the box β€” so you run a company, not a pile of scripts. | -| ❌ Runaway loops waste hundreds of dollars of tokens and max your quota before you even know what happened. | βœ… Cost tracking surfaces token budgets and throttles agents when they're out. Management prioritizes with budgets. | -| ❌ You have recurring jobs (customer support, social, reports) and have to remember to manually kick them off. | βœ… Heartbeats handle regular work on a schedule. Management supervises. | +| ❌ Runaway loops waste hundreds of dollars of tokens and max your quota before you even know what happened. | βœ… Cost tracking surfaces token budgets and throttles agents when they're out. Management prioritizes with budgets. | +| ❌ You have recurring jobs (customer support, social, reports) and have to remember to manually kick them off. | βœ… Heartbeats handle regular work on a schedule. Management supervises. | | ❌ You have an idea, you have to find your repo, fire up Claude Code, keep a tab open, and babysit it. | βœ… Add a task in Taskcore. Your coding agent works on it until it's done. Management reviews their work. |
@@ -148,7 +146,7 @@ Taskcore handles the hard orchestration details correctly. | --------------------------------- | ------------------------------------------------------------------------------------------------------------- | | **Atomic execution.** | Task checkout and budget enforcement are atomic, so no double-work and no runaway spend. | | **Persistent agent state.** | Agents resume the same task context across heartbeats instead of restarting from scratch. | -| **Runtime skill injection.** | Agents can learn Taskcore workflows and project context at runtime, without retraining. | +| **Runtime skill injection.** | Agents can learn Taskcore workflows and project context at runtime, without retraining. | | **Governance with rollback.** | Approval gates are enforced, config changes are revisioned, and bad changes can be rolled back safely. | | **Goal-aware execution.** | Tasks carry full goal ancestry so agents consistently see the "why," not just a title. | | **Portable company templates.** | Export/import orgs, agents, and skills with secret scrubbing and collision handling. | @@ -158,10 +156,10 @@ Taskcore handles the hard orchestration details correctly. ## What Taskcore is not -| | | -| ---------------------------- | -------------------------------------------------------------------------------------------------------------------- | -| **Not a chatbot.** | Agents have jobs, not chat windows. | -| **Not an agent framework.** | We don't tell you how to build agents. We tell you how to run a company made of them. | +| | | +| ---------------------------- | ------------------------------------------------------------------------------------------------------------------- | +| **Not a chatbot.** | Agents have jobs, not chat windows. | +| **Not an agent framework.** | We don't tell you how to build agents. We tell you how to run a company made of them. | | **Not a workflow builder.** | No drag-and-drop pipelines. Taskcore models companies β€” with org charts, goals, budgets, and governance. | | **Not a prompt manager.** | Agents bring their own prompts, models, and runtimes. Taskcore manages the organization they work in. | | **Not a single-agent tool.** | This is for teams. If you have one agent, you probably don't need Taskcore. If you have twenty β€” you definitely do. | @@ -290,11 +288,6 @@ MIT © 2026 KhulnaSoft, Ltd
--- - -

- -

-

Open source under MIT. Built for people who want to run companies, not babysit agents.

diff --git a/cli/package.json b/cli/package.json index 2b9d802..70f1feb 100644 --- a/cli/package.json +++ b/cli/package.json @@ -59,4 +59,4 @@ "tsx": "^4.19.2", "typescript": "^5.7.3" } -} \ No newline at end of file +} diff --git a/cli/src/config/data-dir.ts b/cli/src/config/data-dir.ts index 2582577..56fc290 100644 --- a/cli/src/config/data-dir.ts +++ b/cli/src/config/data-dir.ts @@ -29,7 +29,9 @@ export function applyDataDirOverride( process.env.TASKCORE_HOME = resolvedDataDir; if (support.hasConfigOption) { - const hasConfigOverride = Boolean(options.config?.trim()) || Boolean(process.env.TASKCORE_CONFIG?.trim()); + const hasConfigOverride = + Boolean(options.config?.trim()) || + Boolean(process.env.TASKCORE_CONFIG?.trim()); if (!hasConfigOverride) { const instanceId = resolveTaskcoreInstanceId(options.instance); process.env.TASKCORE_INSTANCE_ID = instanceId; @@ -38,7 +40,9 @@ export function applyDataDirOverride( } if (support.hasContextOption) { - const hasContextOverride = Boolean(options.context?.trim()) || Boolean(process.env.TASKCORE_CONTEXT?.trim()); + const hasContextOverride = + Boolean(options.context?.trim()) || + Boolean(process.env.TASKCORE_CONTEXT?.trim()); if (!hasContextOverride) { process.env.TASKCORE_CONTEXT = resolveDefaultContextPath(); } diff --git a/cli/src/config/env.ts b/cli/src/config/env.ts index a5ba451..78ad554 100644 --- a/cli/src/config/env.ts +++ b/cli/src/config/env.ts @@ -33,7 +33,9 @@ function renderEnvFile(entries: Record) { const lines = [ "# Taskcore environment variables", "# Generated by Taskcore CLI commands", - ...Object.entries(entries).map(([key, value]) => `${key}=${formatEnvValue(value)}`), + ...Object.entries(entries).map( + ([key, value]) => `${key}=${formatEnvValue(value)}`, + ), "", ]; return lines.join("\n"); @@ -65,7 +67,9 @@ export function readAgentJwtSecretFromEnv(configPath?: string): string | null { return isNonEmpty(raw) ? raw!.trim() : null; } -export function readAgentJwtSecretFromEnvFile(filePath = resolveEnvFilePath()): string | null { +export function readAgentJwtSecretFromEnvFile( + filePath = resolveEnvFilePath(), +): string | null { if (!fs.existsSync(filePath)) return null; const raw = fs.readFileSync(filePath, "utf-8"); @@ -74,7 +78,10 @@ export function readAgentJwtSecretFromEnvFile(filePath = resolveEnvFilePath()): return isNonEmpty(value) ? value!.trim() : null; } -export function ensureAgentJwtSecret(configPath?: string): { secret: string; created: boolean } { +export function ensureAgentJwtSecret(configPath?: string): { + secret: string; + created: boolean; +} { const existingEnv = readAgentJwtSecretFromEnv(configPath); if (existingEnv) { return { secret: existingEnv, created: false }; @@ -92,16 +99,24 @@ export function ensureAgentJwtSecret(configPath?: string): { secret: string; cre return { secret, created }; } -export function writeAgentJwtEnv(secret: string, filePath = resolveEnvFilePath()): void { +export function writeAgentJwtEnv( + secret: string, + filePath = resolveEnvFilePath(), +): void { mergeTaskcoreEnvEntries({ [JWT_SECRET_ENV_KEY]: secret }, filePath); } -export function readTaskcoreEnvEntries(filePath = resolveEnvFilePath()): Record { +export function readTaskcoreEnvEntries( + filePath = resolveEnvFilePath(), +): Record { if (!fs.existsSync(filePath)) return {}; return parseEnvFile(fs.readFileSync(filePath, "utf-8")); } -export function writeTaskcoreEnvEntries(entries: Record, filePath = resolveEnvFilePath()): void { +export function writeTaskcoreEnvEntries( + entries: Record, + filePath = resolveEnvFilePath(), +): void { const dir = path.dirname(filePath); fs.mkdirSync(dir, { recursive: true }); fs.writeFileSync(filePath, renderEnvFile(entries), { @@ -117,7 +132,9 @@ export function mergeTaskcoreEnvEntries( const next = { ...current, ...Object.fromEntries( - Object.entries(entries).filter(([, value]) => typeof value === "string" && value.trim().length > 0), + Object.entries(entries).filter( + ([, value]) => typeof value === "string" && value.trim().length > 0, + ), ), }; writeTaskcoreEnvEntries(next, filePath); diff --git a/cli/src/config/home.ts b/cli/src/config/home.ts index ae46ac5..81f7923 100644 --- a/cli/src/config/home.ts +++ b/cli/src/config/home.ts @@ -11,7 +11,10 @@ export function resolveTaskcoreHomeDir(): string { } export function resolveTaskcoreInstanceId(override?: string): string { - const raw = override?.trim() || process.env.TASKCORE_INSTANCE_ID?.trim() || DEFAULT_INSTANCE_ID; + const raw = + override?.trim() || + process.env.TASKCORE_INSTANCE_ID?.trim() || + DEFAULT_INSTANCE_ID; if (!INSTANCE_ID_RE.test(raw)) { throw new Error( `Invalid instance id '${raw}'. Allowed characters: letters, numbers, '_' and '-'.`, @@ -46,15 +49,27 @@ export function resolveDefaultLogsDir(instanceId?: string): string { } export function resolveDefaultSecretsKeyFilePath(instanceId?: string): string { - return path.resolve(resolveTaskcoreInstanceRoot(instanceId), "secrets", "master.key"); + return path.resolve( + resolveTaskcoreInstanceRoot(instanceId), + "secrets", + "master.key", + ); } export function resolveDefaultStorageDir(instanceId?: string): string { - return path.resolve(resolveTaskcoreInstanceRoot(instanceId), "data", "storage"); + return path.resolve( + resolveTaskcoreInstanceRoot(instanceId), + "data", + "storage", + ); } export function resolveDefaultBackupDir(instanceId?: string): string { - return path.resolve(resolveTaskcoreInstanceRoot(instanceId), "data", "backups"); + return path.resolve( + resolveTaskcoreInstanceRoot(instanceId), + "data", + "backups", + ); } export function expandHomePrefix(value: string): string { @@ -71,7 +86,8 @@ export function describeLocalInstancePaths(instanceId?: string) { instanceId: resolvedInstanceId, instanceRoot, configPath: resolveDefaultConfigPath(resolvedInstanceId), - embeddedPostgresDataDir: resolveDefaultEmbeddedPostgresDir(resolvedInstanceId), + embeddedPostgresDataDir: + resolveDefaultEmbeddedPostgresDir(resolvedInstanceId), backupDir: resolveDefaultBackupDir(resolvedInstanceId), logDir: resolveDefaultLogsDir(resolvedInstanceId), secretsKeyFilePath: resolveDefaultSecretsKeyFilePath(resolvedInstanceId), diff --git a/cli/src/config/hostnames.ts b/cli/src/config/hostnames.ts index b788cbb..76c7b31 100644 --- a/cli/src/config/hostnames.ts +++ b/cli/src/config/hostnames.ts @@ -5,7 +5,9 @@ export function normalizeHostnameInput(raw: string): string { } try { - const url = input.includes("://") ? new URL(input) : new URL(`http://${input}`); + const url = input.includes("://") + ? new URL(input) + : new URL(`http://${input}`); const hostname = url.hostname.trim().toLowerCase(); if (!hostname) throw new Error("Hostname is required"); return hostname; @@ -23,4 +25,3 @@ export function parseHostnameCsv(raw: string): string[] { } return Array.from(unique); } - diff --git a/cli/src/config/server-bind.ts b/cli/src/config/server-bind.ts index b00ae85..82bf155 100644 --- a/cli/src/config/server-bind.ts +++ b/cli/src/config/server-bind.ts @@ -72,12 +72,14 @@ export function buildPresetServerConfig( }; } -export function buildCustomServerConfig(input: BaseServerInput & { - deploymentMode: DeploymentMode; - exposure: DeploymentExposure; - host: string; - publicBaseUrl?: string; -}): { server: ServerConfig; auth: AuthConfig } { +export function buildCustomServerConfig( + input: BaseServerInput & { + deploymentMode: DeploymentMode; + exposure: DeploymentExposure; + host: string; + publicBaseUrl?: string; + }, +): { server: ServerConfig; auth: AuthConfig } { const normalizedHost = input.host.trim(); const bind = isLoopbackHost(normalizedHost) ? "loopback" @@ -88,7 +90,8 @@ export function buildCustomServerConfig(input: BaseServerInput & { return { server: { deploymentMode: input.deploymentMode, - exposure: input.deploymentMode === "local_trusted" ? "private" : input.exposure, + exposure: + input.deploymentMode === "local_trusted" ? "private" : input.exposure, bind, customBindHost: bind === "custom" ? normalizedHost : undefined, host: normalizedHost, @@ -99,14 +102,14 @@ export function buildCustomServerConfig(input: BaseServerInput & { auth: input.deploymentMode === "authenticated" && input.exposure === "public" ? { - baseUrlMode: "explicit", - disableSignUp: false, - publicBaseUrl: input.publicBaseUrl, - } + baseUrlMode: "explicit", + disableSignUp: false, + publicBaseUrl: input.publicBaseUrl, + } : { - baseUrlMode: "auto", - disableSignUp: false, - }, + baseUrlMode: "auto", + disableSignUp: false, + }, }; } @@ -123,7 +126,11 @@ export function resolveQuickstartServerConfig(input: { const trimmedHost = input.host?.trim(); const explicitBind = input.bind ?? null; - if (explicitBind === "loopback" || explicitBind === "lan" || explicitBind === "tailnet") { + if ( + explicitBind === "loopback" || + explicitBind === "lan" || + explicitBind === "tailnet" + ) { return buildPresetServerConfig(explicitBind, { port: input.port, allowedHostnames: input.allowedHostnames, @@ -145,7 +152,9 @@ export function resolveQuickstartServerConfig(input: { if (trimmedHost) { return buildCustomServerConfig({ - deploymentMode: input.deploymentMode ?? (isLoopbackHost(trimmedHost) ? "local_trusted" : "authenticated"), + deploymentMode: + input.deploymentMode ?? + (isLoopbackHost(trimmedHost) ? "local_trusted" : "authenticated"), exposure: input.exposure ?? "private", host: trimmedHost, port: input.port, diff --git a/cli/src/config/store.ts b/cli/src/config/store.ts index 2437da3..d661851 100644 --- a/cli/src/config/store.ts +++ b/cli/src/config/store.ts @@ -1,10 +1,7 @@ import fs from "node:fs"; import path from "node:path"; import { taskcoreConfigSchema, type TaskcoreConfig } from "./schema.js"; -import { - resolveDefaultConfigPath, - resolveTaskcoreInstanceId, -} from "./home.js"; +import { resolveDefaultConfigPath, resolveTaskcoreInstanceId } from "./home.js"; const DEFAULT_CONFIG_BASENAME = "config.json"; @@ -13,7 +10,11 @@ function findConfigFileFromAncestors(startDir: string): string | null { let currentDir = absoluteStartDir; while (true) { - const candidate = path.resolve(currentDir, ".taskcore", DEFAULT_CONFIG_BASENAME); + const candidate = path.resolve( + currentDir, + ".taskcore", + DEFAULT_CONFIG_BASENAME, + ); if (fs.existsSync(candidate)) { return candidate; } @@ -28,15 +29,21 @@ function findConfigFileFromAncestors(startDir: string): string | null { export function resolveConfigPath(overridePath?: string): string { if (overridePath) return path.resolve(overridePath); - if (process.env.TASKCORE_CONFIG) return path.resolve(process.env.TASKCORE_CONFIG); - return findConfigFileFromAncestors(process.cwd()) ?? resolveDefaultConfigPath(resolveTaskcoreInstanceId()); + if (process.env.TASKCORE_CONFIG) + return path.resolve(process.env.TASKCORE_CONFIG); + return ( + findConfigFileFromAncestors(process.cwd()) ?? + resolveDefaultConfigPath(resolveTaskcoreInstanceId()) + ); } function parseJson(filePath: string): unknown { try { return JSON.parse(fs.readFileSync(filePath, "utf-8")); } catch (err) { - throw new Error(`Failed to parse JSON at ${filePath}: ${err instanceof Error ? err.message : String(err)}`); + throw new Error( + `Failed to parse JSON at ${filePath}: ${err instanceof Error ? err.message : String(err)}`, + ); } } @@ -44,7 +51,11 @@ function migrateLegacyConfig(raw: unknown): unknown { if (typeof raw !== "object" || raw === null || Array.isArray(raw)) return raw; const config = { ...(raw as Record) }; const databaseRaw = config.database; - if (typeof databaseRaw !== "object" || databaseRaw === null || Array.isArray(databaseRaw)) { + if ( + typeof databaseRaw !== "object" || + databaseRaw === null || + Array.isArray(databaseRaw) + ) { return config; } @@ -52,7 +63,10 @@ function migrateLegacyConfig(raw: unknown): unknown { if (database.mode === "pglite") { database.mode = "embedded-postgres"; - if (typeof database.embeddedPostgresDataDir !== "string" && typeof database.pgliteDataDir === "string") { + if ( + typeof database.embeddedPostgresDataDir !== "string" && + typeof database.pgliteDataDir === "string" + ) { database.embeddedPostgresDataDir = database.pgliteDataDir; } if ( @@ -69,13 +83,18 @@ function migrateLegacyConfig(raw: unknown): unknown { } function formatValidationError(err: unknown): string { - const issues = (err as { issues?: Array<{ path?: unknown; message?: unknown }> })?.issues; + const issues = ( + err as { issues?: Array<{ path?: unknown; message?: unknown }> } + )?.issues; if (Array.isArray(issues) && issues.length > 0) { return issues .map((issue) => { - const pathParts = Array.isArray(issue.path) ? issue.path.map(String) : []; + const pathParts = Array.isArray(issue.path) + ? issue.path.map(String) + : []; const issuePath = pathParts.length > 0 ? pathParts.join(".") : "config"; - const message = typeof issue.message === "string" ? issue.message : "Invalid value"; + const message = + typeof issue.message === "string" ? issue.message : "Invalid value"; return `${issuePath}: ${message}`; }) .join("; "); @@ -90,15 +109,14 @@ export function readConfig(configPath?: string): TaskcoreConfig | null { const migrated = migrateLegacyConfig(raw); const parsed = taskcoreConfigSchema.safeParse(migrated); if (!parsed.success) { - throw new Error(`Invalid config at ${filePath}: ${formatValidationError(parsed.error)}`); + throw new Error( + `Invalid config at ${filePath}: ${formatValidationError(parsed.error)}`, + ); } return parsed.data; } -export function writeConfig( - config: TaskcoreConfig, - configPath?: string, -): void { +export function writeConfig(config: TaskcoreConfig, configPath?: string): void { const filePath = resolveConfigPath(configPath); const dir = path.dirname(filePath); fs.mkdirSync(dir, { recursive: true }); diff --git a/cli/src/index.ts b/cli/src/index.ts index b1e9f58..2e04d7a 100644 --- a/cli/src/index.ts +++ b/cli/src/index.ts @@ -17,7 +17,10 @@ import { registerActivityCommands } from "./commands/client/activity.js"; import { registerDashboardCommands } from "./commands/client/dashboard.js"; import { registerRoutineCommands } from "./commands/routines.js"; import { registerFeedbackCommands } from "./commands/client/feedback.js"; -import { applyDataDirOverride, type DataDirOptionLike } from "./config/data-dir.js"; +import { + applyDataDirOverride, + type DataDirOptionLike, +} from "./config/data-dir.js"; import { loadTaskcoreEnvFile } from "./config/env.js"; import { initTelemetryFromConfigFile, flushTelemetry } from "./telemetry.js"; import { registerWorktreeCommands } from "./commands/worktree.js"; @@ -36,7 +39,9 @@ program program.hook("preAction", (_thisCommand, actionCommand) => { const options = actionCommand.optsWithGlobals() as DataDirOptionLike; - const optionNames = new Set(actionCommand.options.map((option) => option.attributeName())); + const optionNames = new Set( + actionCommand.options.map((option) => option.attributeName()), + ); applyDataDirOverride(options, { hasConfigOption: optionNames.has("config"), hasContextOption: optionNames.has("context"), @@ -50,8 +55,15 @@ program .description("Interactive first-run setup wizard") .option("-c, --config ", "Path to config file") .option("-d, --data-dir ", DATA_DIR_OPTION_HELP) - .option("--bind ", "Quickstart reachability preset (loopback, lan, tailnet)") - .option("-y, --yes", "Accept quickstart defaults (trusted local loopback unless --bind is set) and start immediately", false) + .option( + "--bind ", + "Quickstart reachability preset (loopback, lan, tailnet)", + ) + .option( + "-y, --yes", + "Accept quickstart defaults (trusted local loopback unless --bind is set) and start immediately", + false, + ) .option("--run", "Start Taskcore immediately after saving config", false) .action(onboard); @@ -79,7 +91,10 @@ program .description("Update configuration sections") .option("-c, --config ", "Path to config file") .option("-d, --data-dir ", DATA_DIR_OPTION_HELP) - .option("-s, --section
", "Section to configure (llm, database, logging, server, storage, secrets)") + .option( + "-s, --section
", + "Section to configure (llm, database, logging, server, storage, secrets)", + ) .action(configure); program @@ -88,7 +103,11 @@ program .option("-c, --config ", "Path to config file") .option("-d, --data-dir ", DATA_DIR_OPTION_HELP) .option("--dir ", "Backup output directory (overrides config)") - .option("--retention-days ", "Retention window used for pruning", (value) => Number(value)) + .option( + "--retention-days ", + "Retention window used for pruning", + (value) => Number(value), + ) .option("--filename-prefix ", "Backup filename prefix", "taskcore") .option("--json", "Print backup metadata as JSON") .action(async (opts) => { @@ -109,12 +128,17 @@ program .option("-c, --config ", "Path to config file") .option("-d, --data-dir ", DATA_DIR_OPTION_HELP) .option("-i, --instance ", "Local instance id (default: default)") - .option("--bind ", "On first run, use onboarding reachability preset (loopback, lan, tailnet)") + .option( + "--bind ", + "On first run, use onboarding reachability preset (loopback, lan, tailnet)", + ) .option("--repair", "Attempt automatic repairs during doctor", true) .option("--no-repair", "Disable automatic repairs during doctor") .action(runCommand); -const heartbeat = program.command("heartbeat").description("Heartbeat utilities"); +const heartbeat = program + .command("heartbeat") + .description("Heartbeat utilities"); heartbeat .command("run") @@ -131,7 +155,11 @@ heartbeat "Invocation source (timer | assignment | on_demand | automation)", "on_demand", ) - .option("--trigger ", "Trigger detail (manual | ping | callback | system)", "manual") + .option( + "--trigger ", + "Trigger detail (manual | ping | callback | system)", + "manual", + ) .option("--timeout-ms ", "Max time to wait before giving up", "0") .option("--json", "Output raw JSON where applicable") .option("--debug", "Show raw adapter stdout/stderr JSON chunks") @@ -149,15 +177,23 @@ registerFeedbackCommands(program); registerWorktreeCommands(program); registerPluginCommands(program); -const auth = program.command("auth").description("Authentication and bootstrap utilities"); +const auth = program + .command("auth") + .description("Authentication and bootstrap utilities"); auth .command("bootstrap-ceo") - .description("Create a one-time bootstrap invite URL for first instance admin") + .description( + "Create a one-time bootstrap invite URL for first instance admin", + ) .option("-c, --config ", "Path to config file") .option("-d, --data-dir ", DATA_DIR_OPTION_HELP) .option("--force", "Create new invite even if admin already exists", false) - .option("--expires-hours ", "Invite expiration window in hours", (value) => Number(value)) + .option( + "--expires-hours ", + "Invite expiration window in hours", + (value) => Number(value), + ) .option("--base-url ", "Public base URL used to print invite link") .action(bootstrapCeoInvite); diff --git a/cli/src/telemetry.ts b/cli/src/telemetry.ts index 034ec57..acac26b 100644 --- a/cli/src/telemetry.ts +++ b/cli/src/telemetry.ts @@ -13,18 +13,26 @@ import { cliVersion } from "./version.js"; let client: TelemetryClient | null = null; -export function initTelemetry(fileConfig?: { enabled?: boolean }): TelemetryClient | null { +export function initTelemetry(fileConfig?: { + enabled?: boolean; +}): TelemetryClient | null { if (client) return client; const config = resolveTelemetryConfig(fileConfig); if (!config.enabled) return null; const stateDir = path.join(resolveTaskcoreInstanceRoot(), "telemetry"); - client = new TelemetryClient(config, () => loadOrCreateState(stateDir, cliVersion), cliVersion); + client = new TelemetryClient( + config, + () => loadOrCreateState(stateDir, cliVersion), + cliVersion, + ); return client; } -export function initTelemetryFromConfigFile(configPath?: string): TelemetryClient | null { +export function initTelemetryFromConfigFile( + configPath?: string, +): TelemetryClient | null { try { return initTelemetry(readConfig(configPath)?.telemetry); } catch { @@ -42,8 +50,4 @@ export async function flushTelemetry(): Promise { } } -export { - trackInstallStarted, - trackInstallCompleted, - trackCompanyImported, -}; +export { trackInstallStarted, trackInstallCompleted, trackCompanyImported }; diff --git a/cli/src/utils/net.ts b/cli/src/utils/net.ts index 0b5597b..03d545e 100644 --- a/cli/src/utils/net.ts +++ b/cli/src/utils/net.ts @@ -1,6 +1,8 @@ import net from "node:net"; -export function checkPort(port: number): Promise<{ available: boolean; error?: string }> { +export function checkPort( + port: number, +): Promise<{ available: boolean; error?: string }> { return new Promise((resolve) => { const server = net.createServer(); server.once("error", (err: NodeJS.ErrnoException) => { diff --git a/cli/src/utils/path-resolver.ts b/cli/src/utils/path-resolver.ts index d8ebbd0..c6cfde2 100644 --- a/cli/src/utils/path-resolver.ts +++ b/cli/src/utils/path-resolver.ts @@ -6,7 +6,10 @@ function unique(items: string[]): string[] { return Array.from(new Set(items)); } -export function resolveRuntimeLikePath(value: string, configPath?: string): string { +export function resolveRuntimeLikePath( + value: string, + configPath?: string, +): string { const expanded = expandHomePrefix(value); if (path.isAbsolute(expanded)) return path.resolve(expanded); @@ -21,5 +24,7 @@ export function resolveRuntimeLikePath(value: string, configPath?: string): stri path.resolve(cwd, expanded), ]); - return candidates.find((candidate) => fs.existsSync(candidate)) ?? candidates[0]; + return ( + candidates.find((candidate) => fs.existsSync(candidate)) ?? candidates[0] + ); } From 258ab01424d9119f76df277f2de57cbc964065a8 Mon Sep 17 00:00:00 2001 From: devenv Date: Fri, 17 Apr 2026 15:30:49 +0600 Subject: [PATCH 4/8] =?UTF-8?q?=F0=9F=93=9A=20Update=20doc/=20directory=20?= =?UTF-8?q?with=20implementation=20specs=20and=20plans?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- doc/AGENTCOMPANIES_SPEC_INVENTORY.md | 114 +++++++------- doc/CLIPHUB.md | 90 +++++------ doc/DATABASE.md | 10 +- doc/DEPLOYMENT-MODES.md | 24 +-- doc/DEVELOPING.md | 100 ++++++------ doc/DOCKER.md | 20 +-- doc/OPENCLAW_ONBOARDING.md | 19 +++ doc/SPEC-implementation.md | 58 +++---- doc/SPEC.md | 44 +++--- doc/TASKS-mcp.md | 10 +- doc/assets/footer.jpg | Bin 484526 -> 0 bytes doc/assets/header.png | Bin 736570 -> 0 bytes doc/memory-landscape.md | 24 +-- doc/plans/2026-02-16-module-system.md | 109 +++++++------ doc/plans/2026-02-18-agent-authentication.md | 4 +- ...026-02-19-ceo-agent-creation-and-hiring.md | 6 +- ...026-02-20-storage-system-implementation.md | 1 - ...1-humans-and-permissions-implementation.md | 26 ++++ doc/plans/2026-02-23-cursor-cloud-adapter.md | 38 ++--- ...2-23-deployment-auth-mode-consolidation.md | 10 ++ ...10-workspace-strategy-and-git-worktrees.md | 17 +- .../2026-03-13-company-import-export-v2.md | 2 + doc/plans/2026-03-13-features.md | 3 - .../2026-03-14-adapter-skill-sync-rollout.md | 1 + .../2026-03-14-skills-ui-product-plan.md | 1 + .../2026-03-17-memory-service-surface-api.md | 22 ++- doc/plans/2026-04-06-smart-model-routing.md | 1 + ...04-06-subissue-creation-on-issue-detail.md | 1 + ...e-detail-speed-and-optimistic-inventory.md | 1 + doc/plugins/PLUGIN_SPEC.md | 110 +++++++++---- doc/plugins/ideas-from-opencode.md | 78 ++++++---- doc/spec/agent-runs.md | 43 ++++-- doc/spec/ui.md | 146 +++++++++++------- 33 files changed, 677 insertions(+), 456 deletions(-) delete mode 100644 doc/assets/footer.jpg delete mode 100644 doc/assets/header.png diff --git a/doc/AGENTCOMPANIES_SPEC_INVENTORY.md b/doc/AGENTCOMPANIES_SPEC_INVENTORY.md index fa1e08a..e118bad 100644 --- a/doc/AGENTCOMPANIES_SPEC_INVENTORY.md +++ b/doc/AGENTCOMPANIES_SPEC_INVENTORY.md @@ -12,104 +12,104 @@ Use it when you need to: ## 1. Specification & Design Documents -| File | Role | -|---|---| -| `docs/companies/companies-spec.md` | **Normative spec** β€” defines the markdown-first package format (COMPANY.md, TEAM.md, AGENTS.md, PROJECT.md, TASK.md, SKILL.md), reserved files, frontmatter schemas, and vendor extension conventions (`.taskcore.yaml`). | -| `doc/plans/2026-03-13-company-import-export-v2.md` | Implementation plan for the markdown-first package model cutover β€” phases, API changes, UI plan, and rollout strategy. | -| `doc/SPEC-implementation.md` | V1 implementation contract; references the portability system and `.taskcore.yaml` sidecar format. | -| `docs/specs/cliphub-plan.md` | Earlier blueprint bundle plan; partially superseded by the markdown-first spec (noted in the v2 plan). | -| `doc/plans/2026-02-16-module-system.md` | Module system plan; JSON-only company template sections superseded by the markdown-first model. | -| `doc/plans/2026-03-14-skills-ui-product-plan.md` | Skills UI plan; references portable skill files and `.taskcore.yaml`. | -| `doc/plans/2026-03-14-adapter-skill-sync-rollout.md` | Adapter skill sync rollout; companion to the v2 import/export plan. | +| File | Role | +| ---------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | +| `docs/companies/companies-spec.md` | **Normative spec** β€” defines the markdown-first package format (COMPANY.md, TEAM.md, AGENTS.md, PROJECT.md, TASK.md, SKILL.md), reserved files, frontmatter schemas, and vendor extension conventions (`.taskcore.yaml`). | +| `doc/plans/2026-03-13-company-import-export-v2.md` | Implementation plan for the markdown-first package model cutover β€” phases, API changes, UI plan, and rollout strategy. | +| `doc/SPEC-implementation.md` | V1 implementation contract; references the portability system and `.taskcore.yaml` sidecar format. | +| `docs/specs/cliphub-plan.md` | Earlier blueprint bundle plan; partially superseded by the markdown-first spec (noted in the v2 plan). | +| `doc/plans/2026-02-16-module-system.md` | Module system plan; JSON-only company template sections superseded by the markdown-first model. | +| `doc/plans/2026-03-14-skills-ui-product-plan.md` | Skills UI plan; references portable skill files and `.taskcore.yaml`. | +| `doc/plans/2026-03-14-adapter-skill-sync-rollout.md` | Adapter skill sync rollout; companion to the v2 import/export plan. | ## 2. Shared Types & Validators These define the contract between server, CLI, and UI. -| File | What it defines | -|---|---| -| `packages/shared/src/types/company-portability.ts` | TypeScript interfaces: `CompanyPortabilityManifest`, `CompanyPortabilityFileEntry`, `CompanyPortabilityEnvInput`, export/import/preview request and result types, manifest entry types for agents, skills, projects, issues, recurring routines, companies. | -| `packages/shared/src/validators/company-portability.ts` | Zod schemas for all portability request/response shapes β€” used by both server routes and CLI. | -| `packages/shared/src/types/index.ts` | Re-exports portability types. | -| `packages/shared/src/validators/index.ts` | Re-exports portability validators. | +| File | What it defines | +| ------------------------------------------------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | +| `packages/shared/src/types/company-portability.ts` | TypeScript interfaces: `CompanyPortabilityManifest`, `CompanyPortabilityFileEntry`, `CompanyPortabilityEnvInput`, export/import/preview request and result types, manifest entry types for agents, skills, projects, issues, recurring routines, companies. | +| `packages/shared/src/validators/company-portability.ts` | Zod schemas for all portability request/response shapes β€” used by both server routes and CLI. | +| `packages/shared/src/types/index.ts` | Re-exports portability types. | +| `packages/shared/src/validators/index.ts` | Re-exports portability validators. | ## 3. Server β€” Services -| File | Responsibility | -|---|---| -| `server/src/services/company-portability.ts` | **Core portability service.** Export (manifest generation, markdown file emission, `.taskcore.yaml` sidecars), import (graph resolution, collision handling, entity creation), preview (planned-action summary). Handles skill key derivation, recurring task <-> routine mapping, legacy recurrence migration, and package README generation. References `agentcompanies/v1` version string. | -| `server/src/services/routines.ts` | Taskcore routine runtime service. Portability now exports routines as recurring `TASK.md` entries and imports recurring tasks back through this service. | -| `server/src/services/company-export-readme.ts` | Generates `README.md` and Mermaid org-chart for exported company packages. | -| `server/src/services/index.ts` | Re-exports `companyPortabilityService`. | +| File | Responsibility | +| ---------------------------------------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | +| `server/src/services/company-portability.ts` | **Core portability service.** Export (manifest generation, markdown file emission, `.taskcore.yaml` sidecars), import (graph resolution, collision handling, entity creation), preview (planned-action summary). Handles skill key derivation, recurring task <-> routine mapping, legacy recurrence migration, and package README generation. References `agentcompanies/v1` version string. | +| `server/src/services/routines.ts` | Taskcore routine runtime service. Portability now exports routines as recurring `TASK.md` entries and imports recurring tasks back through this service. | +| `server/src/services/company-export-readme.ts` | Generates `README.md` and Mermaid org-chart for exported company packages. | +| `server/src/services/index.ts` | Re-exports `companyPortabilityService`. | ## 4. Server β€” Routes -| File | Endpoints | -|---|---| +| File | Endpoints | +| -------------------------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | | `server/src/routes/companies.ts` | `POST /api/companies/:companyId/export` β€” legacy export bundle
`POST /api/companies/:companyId/exports/preview` β€” export preview
`POST /api/companies/:companyId/exports` β€” export package
`POST /api/companies/import/preview` β€” import preview
`POST /api/companies/import` β€” perform import | Route registration lives in `server/src/app.ts` via `companyRoutes(db, storage)`. ## 5. Server β€” Tests -| File | Coverage | -|---|---| -| `server/src/__tests__/company-portability.test.ts` | Unit tests for the portability service (export, import, preview, manifest shape, `agentcompanies/v1` version). | -| `server/src/__tests__/company-portability-routes.test.ts` | Integration tests for the portability HTTP endpoints. | +| File | Coverage | +| --------------------------------------------------------- | -------------------------------------------------------------------------------------------------------------- | +| `server/src/__tests__/company-portability.test.ts` | Unit tests for the portability service (export, import, preview, manifest shape, `agentcompanies/v1` version). | +| `server/src/__tests__/company-portability-routes.test.ts` | Integration tests for the portability HTTP endpoints. | ## 6. CLI -| File | Commands | -|---|---| +| File | Commands | +| ------------------------------------ | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | | `cli/src/commands/client/company.ts` | `company export` β€” exports a company package to disk (flags: `--out`, `--include`, `--projects`, `--issues`, `--projectIssues`).
`company import ` β€” imports a company package from a file or folder (flags: positional source path/URL or GitHub shorthand, `--include`, `--target`, `--companyId`, `--newCompanyName`, `--agents`, `--collision`, `--ref`, `--dryRun`).
Reads/writes portable file entries and handles `.taskcore.yaml` filtering. | ## 7. UI β€” Pages -| File | Role | -|---|---| -| `ui/src/pages/CompanyExport.tsx` | Export UI: preview, manifest display, file tree visualization, ZIP archive creation and download. Filters `.taskcore.yaml` based on selection. Shows manifest and README in editor. | +| File | Role | +| -------------------------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | +| `ui/src/pages/CompanyExport.tsx` | Export UI: preview, manifest display, file tree visualization, ZIP archive creation and download. Filters `.taskcore.yaml` based on selection. Shows manifest and README in editor. | | `ui/src/pages/CompanyImport.tsx` | Import UI: source input (upload/folder/GitHub URL/generic URL), ZIP reading, preview pane with dependency tree, entity selection checkboxes, trust/licensing warnings, secrets requirements, collision strategy, adapter config. | ## 8. UI β€” Components -| File | Role | -|---|---| +| File | Role | +| --------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | | `ui/src/components/PackageFileTree.tsx` | Reusable file tree component for both import and export. Builds tree from `CompanyPortabilityFileEntry` items, parses frontmatter, shows action indicators (create/update/skip), and maps frontmatter field labels. | ## 9. UI β€” Libraries -| File | Role | -|---|---| -| `ui/src/lib/portable-files.ts` | Helpers for portable file entries: `getPortableFileText`, `getPortableFileDataUrl`, `getPortableFileContentType`, `isPortableImageFile`. | -| `ui/src/lib/zip.ts` | ZIP archive creation (`createZipArchive`) and reading (`readZipArchive`) β€” implements ZIP format from scratch for company packages. CRC32, DOS date/time encoding. | -| `ui/src/lib/zip.test.ts` | Tests for ZIP utilities; exercises round-trip with portability file entries and `.taskcore.yaml` content. | +| File | Role | +| ------------------------------ | ------------------------------------------------------------------------------------------------------------------------------------------------------------------ | +| `ui/src/lib/portable-files.ts` | Helpers for portable file entries: `getPortableFileText`, `getPortableFileDataUrl`, `getPortableFileContentType`, `isPortableImageFile`. | +| `ui/src/lib/zip.ts` | ZIP archive creation (`createZipArchive`) and reading (`readZipArchive`) β€” implements ZIP format from scratch for company packages. CRC32, DOS date/time encoding. | +| `ui/src/lib/zip.test.ts` | Tests for ZIP utilities; exercises round-trip with portability file entries and `.taskcore.yaml` content. | ## 10. UI β€” API Client -| File | Functions | -|---|---| +| File | Functions | +| ------------------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | | `ui/src/api/companies.ts` | `companiesApi.exportBundle`, `companiesApi.exportPreview`, `companiesApi.exportPackage`, `companiesApi.importPreview`, `companiesApi.importBundle` β€” typed fetch wrappers for the portability endpoints. | ## 11. Skills & Agent Instructions -| File | Relevance | -|---|---| +| File | Relevance | +| ---------------------------------------------- | --------------------------------------------------------------------------------------------------------------------------------------------- | | `skills/taskcore/references/company-skills.md` | Reference doc for company skill library workflow β€” install, inspect, update, assign. Skill packages are a subset of the agent companies spec. | -| `server/src/services/company-skills.ts` | Company skill management service β€” handles SKILL.md-based imports and company-level skill library. | -| `server/src/services/agent-instructions.ts` | Agent instructions service β€” resolves AGENTS.md paths for agent instruction loading. | +| `server/src/services/company-skills.ts` | Company skill management service β€” handles SKILL.md-based imports and company-level skill library. | +| `server/src/services/agent-instructions.ts` | Agent instructions service β€” resolves AGENTS.md paths for agent instruction loading. | ## 12. Quick Cross-Reference by Spec Concept -| Spec concept | Primary implementation files | -|---|---| -| `COMPANY.md` frontmatter & body | `company-portability.ts` (export emitter + import parser) | -| `AGENTS.md` frontmatter & body | `company-portability.ts`, `agent-instructions.ts` | -| `PROJECT.md` frontmatter & body | `company-portability.ts` | -| `TASK.md` frontmatter & body | `company-portability.ts` | -| `SKILL.md` packages | `company-portability.ts`, `company-skills.ts` | +| Spec concept | Primary implementation files | +| ------------------------------- | -------------------------------------------------------------------------------- | +| `COMPANY.md` frontmatter & body | `company-portability.ts` (export emitter + import parser) | +| `AGENTS.md` frontmatter & body | `company-portability.ts`, `agent-instructions.ts` | +| `PROJECT.md` frontmatter & body | `company-portability.ts` | +| `TASK.md` frontmatter & body | `company-portability.ts` | +| `SKILL.md` packages | `company-portability.ts`, `company-skills.ts` | | `.taskcore.yaml` vendor sidecar | `company-portability.ts`, `routines.ts`, `CompanyExport.tsx`, `company.ts` (CLI) | -| `manifest.json` | `company-portability.ts` (generation), shared types (schema) | -| ZIP package format | `zip.ts` (UI), `company.ts` (CLI file I/O) | -| Collision resolution | `company-portability.ts` (server), `CompanyImport.tsx` (UI) | -| Env/secrets declarations | shared types (`CompanyPortabilityEnvInput`), `CompanyImport.tsx` (UI) | -| README + org chart | `company-export-readme.ts` | +| `manifest.json` | `company-portability.ts` (generation), shared types (schema) | +| ZIP package format | `zip.ts` (UI), `company.ts` (CLI file I/O) | +| Collision resolution | `company-portability.ts` (server), `CompanyImport.tsx` (UI) | +| Env/secrets declarations | shared types (`CompanyPortabilityEnvInput`), `CompanyImport.tsx` (UI) | +| README + org chart | `company-export-readme.ts` | diff --git a/doc/CLIPHUB.md b/doc/CLIPHUB.md index 4cb5a67..75c6749 100644 --- a/doc/CLIPHUB.md +++ b/doc/CLIPHUB.md @@ -20,14 +20,14 @@ The tagline: **you can literally download a company.** A ClipHub package is a **company template export** β€” the portable artifact format defined in the Taskcore spec. It contains: -| Component | Description | -|---|---| -| **Company metadata** | Name, description, intended use case, category | -| **Org chart** | Full reporting hierarchy β€” who reports to whom | -| **Agent definitions** | Every agent: name, role, title, capabilities description | -| **Adapter configs** | Per-agent adapter type and configuration (SOUL.md, HEARTBEAT.md, CLAUDE.md, process commands, webhook URLs β€” whatever the adapter needs) | -| **Seed tasks** | Optional starter tasks and initiatives to bootstrap the company's first run | -| **Budget defaults** | Suggested token/cost budgets per agent and per company | +| Component | Description | +| --------------------- | ---------------------------------------------------------------------------------------------------------------------------------------- | +| **Company metadata** | Name, description, intended use case, category | +| **Org chart** | Full reporting hierarchy β€” who reports to whom | +| **Agent definitions** | Every agent: name, role, title, capabilities description | +| **Adapter configs** | Per-agent adapter type and configuration (SOUL.md, HEARTBEAT.md, CLAUDE.md, process commands, webhook URLs β€” whatever the adapter needs) | +| **Seed tasks** | Optional starter tasks and initiatives to bootstrap the company's first run | +| **Budget defaults** | Suggested token/cost budgets per agent and per company | Templates are **structure, not state.** No in-progress tasks, no historical cost data, no runtime artifacts. Just the blueprint. @@ -84,9 +84,11 @@ Clicking into a company template shows: Two ways to use a template: **Install (fresh start):** + ``` taskcore install cliphub:/ ``` + Downloads the template and creates a new company in your local Taskcore instance. You add your own API keys, set budgets, customize agents, and hit go. **Fork:** @@ -131,17 +133,17 @@ Or use the web UI to upload a template export directly. When publishing, you specify: -| Field | Required | Description | -|---|---|---| -| `slug` | yes | URL-safe identifier (e.g. `lean-dev-shop`) | -| `name` | yes | Display name | -| `description` | yes | What this company does and who it's for | -| `category` | yes | Primary category (see below) | -| `tags` | no | Additional tags for discovery | -| `version` | yes | Semver (e.g. `1.0.0`) | -| `changelog` | no | What changed in this version | -| `readme` | no | Extended documentation (markdown) | -| `license` | no | Usage terms | +| Field | Required | Description | +| ------------- | -------- | ------------------------------------------ | +| `slug` | yes | URL-safe identifier (e.g. `lean-dev-shop`) | +| `name` | yes | Display name | +| `description` | yes | What this company does and who it's for | +| `category` | yes | Primary category (see below) | +| `tags` | no | Additional tags for discovery | +| `version` | yes | Semver (e.g. `1.0.0`) | +| `changelog` | no | What changed in this version | +| `readme` | no | Extended documentation (markdown) | +| `license` | no | Usage terms | ### Versioning @@ -163,17 +165,17 @@ Scans your local exported templates and publishes any that are new or updated. U Company templates are organized by use case: -| Category | Examples | -|---|---| -| **Software Development** | Full-stack dev shop, API development team, mobile app studio | -| **Marketing & Growth** | Performance marketing agency, content marketing team, SEO shop | -| **Content & Media** | Content studio, podcast production, newsletter operation | -| **Research & Analysis** | Market research firm, competitive intelligence, data analysis team | -| **Operations** | Customer support org, internal ops team, QA/testing shop | -| **Sales** | Outbound sales team, lead generation, account management | -| **Finance & Legal** | Bookkeeping service, compliance monitoring, financial analysis | -| **Creative** | Design agency, copywriting studio, brand development | -| **General Purpose** | Starter templates, minimal orgs, single-agent setups | +| Category | Examples | +| ------------------------ | ------------------------------------------------------------------ | +| **Software Development** | Full-stack dev shop, API development team, mobile app studio | +| **Marketing & Growth** | Performance marketing agency, content marketing team, SEO shop | +| **Content & Media** | Content studio, podcast production, newsletter operation | +| **Research & Analysis** | Market research firm, competitive intelligence, data analysis team | +| **Operations** | Customer support org, internal ops team, QA/testing shop | +| **Sales** | Outbound sales team, lead generation, account management | +| **Finance & Legal** | Bookkeeping service, compliance monitoring, financial analysis | +| **Creative** | Design agency, copywriting studio, brand development | +| **General Purpose** | Starter templates, minimal orgs, single-agent setups | Categories are not exclusive β€” a template can have one primary category plus tags for cross-cutting concerns. @@ -205,23 +207,23 @@ ClipHub is a **separate service** from Taskcore itself. Taskcore is self-hosted; ### Integration Points -| Layer | Role | -|---|---| -| **ClipHub Web** | Browse, search, discover, comment, star β€” the website | -| **ClipHub API** | Registry API for publishing, downloading, searching programmatically | -| **Taskcore CLI** | `taskcore install`, `taskcore publish`, `taskcore cliphub sync` β€” built into Taskcore | -| **Taskcore UI** | "Browse ClipHub" panel in the Taskcore web UI for discovering templates without leaving the app | +| Layer | Role | +| ---------------- | ----------------------------------------------------------------------------------------------- | +| **ClipHub Web** | Browse, search, discover, comment, star β€” the website | +| **ClipHub API** | Registry API for publishing, downloading, searching programmatically | +| **Taskcore CLI** | `taskcore install`, `taskcore publish`, `taskcore cliphub sync` β€” built into Taskcore | +| **Taskcore UI** | "Browse ClipHub" panel in the Taskcore web UI for discovering templates without leaving the app | ### Tech Stack -| Layer | Technology | -|---|---| -| Frontend | React + Vite (consistent with Taskcore) | -| Backend | TypeScript + Hono (consistent with Taskcore) | -| Database | PostgreSQL | -| Search | Vector embeddings for semantic search | -| Auth | GitHub OAuth | -| Storage | Template zips stored in object storage (S3 or equivalent) | +| Layer | Technology | +| -------- | --------------------------------------------------------- | +| Frontend | React + Vite (consistent with Taskcore) | +| Backend | TypeScript + Hono (consistent with Taskcore) | +| Database | PostgreSQL | +| Search | Vector embeddings for semantic search | +| Auth | GitHub OAuth | +| Storage | Template zips stored in object storage (S3 or equivalent) | ### Data Model (Sketch) diff --git a/doc/DATABASE.md b/doc/DATABASE.md index 2ab7995..d37ae2d 100644 --- a/doc/DATABASE.md +++ b/doc/DATABASE.md @@ -123,11 +123,11 @@ See [Supabase pricing](https://supabase.com/pricing) for current details. The database mode is controlled by `DATABASE_URL`: -| `DATABASE_URL` | Mode | -|---|---| -| Not set | Embedded PostgreSQL (`~/.taskcore/instances/default/db/`) | -| `postgres://...localhost...` | Local Docker PostgreSQL | -| `postgres://...supabase.com...` | Hosted Supabase | +| `DATABASE_URL` | Mode | +| ------------------------------- | --------------------------------------------------------- | +| Not set | Embedded PostgreSQL (`~/.taskcore/instances/default/db/`) | +| `postgres://...localhost...` | Local Docker PostgreSQL | +| `postgres://...supabase.com...` | Hosted Supabase | Your Drizzle schema (`packages/db/src/schema/`) stays the same regardless of mode. diff --git a/doc/DEPLOYMENT-MODES.md b/doc/DEPLOYMENT-MODES.md index ada4973..7ea3f1b 100644 --- a/doc/DEPLOYMENT-MODES.md +++ b/doc/DEPLOYMENT-MODES.md @@ -24,20 +24,20 @@ Taskcore now treats **bind** as a separate concern from auth: ## 2. Canonical Model -| Runtime Mode | Exposure | Human auth | Primary use | -|---|---|---|---| -| `local_trusted` | n/a | No login required | Single-operator local machine workflow | -| `authenticated` | `private` | Login required | Private-network access (for example Tailscale/VPN/LAN) | -| `authenticated` | `public` | Login required | Internet-facing/cloud deployment | +| Runtime Mode | Exposure | Human auth | Primary use | +| --------------- | --------- | ----------------- | ------------------------------------------------------ | +| `local_trusted` | n/a | No login required | Single-operator local machine workflow | +| `authenticated` | `private` | Login required | Private-network access (for example Tailscale/VPN/LAN) | +| `authenticated` | `public` | Login required | Internet-facing/cloud deployment | ## Reachability Model -| Bind | Meaning | Typical use | -|---|---|---| -| `loopback` | Listen on localhost only | default local usage, reverse-proxy deployments | -| `lan` | Listen on all interfaces (`0.0.0.0`) | LAN/VPN/private-network access | -| `tailnet` | Listen on a detected Tailscale IP | Tailscale-only access | -| `custom` | Listen on an explicit host/IP | advanced interface-specific setups | +| Bind | Meaning | Typical use | +| ---------- | ------------------------------------ | ---------------------------------------------- | +| `loopback` | Listen on localhost only | default local usage, reverse-proxy deployments | +| `lan` | Listen on all interfaces (`0.0.0.0`) | LAN/VPN/private-network access | +| `tailnet` | Listen on a detected Tailscale IP | Tailscale-only access | +| `custom` | Listen on an explicit host/IP | advanced interface-specific setups | ## 3. Security Policy @@ -73,10 +73,12 @@ Server prompt behavior: 1. quickstart `--yes` defaults to `server.bind=loopback` and therefore `local_trusted/private` 2. advanced server setup asks reachability first: + - `Trusted local` β†’ `bind=loopback`, `local_trusted/private` - `Private network` β†’ `bind=lan`, `authenticated/private` - `Tailnet` β†’ `bind=tailnet`, `authenticated/private` - `Custom` β†’ manual mode/exposure/host entry + 3. raw host entry is only required for the `Custom` path 4. explicit public URL is only required for `authenticated + public` diff --git a/doc/DEVELOPING.md b/doc/DEVELOPING.md index f375949..4efba8f 100644 --- a/doc/DEVELOPING.md +++ b/doc/DEVELOPING.md @@ -235,19 +235,19 @@ eval "$(taskcore worktree env)" **`pnpm taskcore worktree init [options]`** β€” Create repo-local config/env and an isolated instance for the current worktree. -| Option | Description | -|---|---| -| `--name ` | Display name used to derive the instance id | -| `--instance ` | Explicit isolated instance id | -| `--home ` | Home root for worktree instances (default: `~/.taskcore-worktrees`) | -| `--from-config ` | Source config.json to seed from | -| `--from-data-dir ` | Source TASKCORE_HOME used when deriving the source config | -| `--from-instance ` | Source instance id (default: `default`) | -| `--server-port ` | Preferred server port | -| `--db-port ` | Preferred embedded Postgres port | -| `--seed-mode ` | Seed profile: `minimal` or `full` (default: `minimal`) | -| `--no-seed` | Skip database seeding from the source instance | -| `--force` | Replace existing repo-local config and isolated instance data | +| Option | Description | +| ------------------------ | ------------------------------------------------------------------- | +| `--name ` | Display name used to derive the instance id | +| `--instance ` | Explicit isolated instance id | +| `--home ` | Home root for worktree instances (default: `~/.taskcore-worktrees`) | +| `--from-config ` | Source config.json to seed from | +| `--from-data-dir ` | Source TASKCORE_HOME used when deriving the source config | +| `--from-instance ` | Source instance id (default: `default`) | +| `--server-port ` | Preferred server port | +| `--db-port ` | Preferred embedded Postgres port | +| `--seed-mode ` | Seed profile: `minimal` or `full` (default: `minimal`) | +| `--no-seed` | Skip database seeding from the source instance | +| `--force` | Replace existing repo-local config and isolated instance data | Examples: @@ -274,16 +274,16 @@ For an already-created worktree where you want the CLI to decide whether to rebu **`pnpm taskcore worktree repair [options]`** β€” Repair the current linked worktree by default, or create/repair a named linked worktree under `.taskcore/worktrees/` when `--branch` is provided. The command never targets the primary checkout unless you explicitly pass `--branch`. -| Option | Description | -|---|---| -| `--branch ` | Existing branch/worktree selector to repair, or a branch name to create under `.taskcore/worktrees` | -| `--home ` | Home root for worktree instances (default: `~/.taskcore-worktrees`) | -| `--from-config ` | Source config.json to seed from | -| `--from-data-dir ` | Source `TASKCORE_HOME` used when deriving the source config | -| `--from-instance ` | Source instance id when deriving the source config (default: `default`) | -| `--seed-mode ` | Seed profile: `minimal` or `full` (default: `minimal`) | -| `--no-seed` | Repair metadata only when bootstrapping a missing worktree config | -| `--allow-live-target` | Override the guard that requires the target worktree DB to be stopped first | +| Option | Description | +| ------------------------ | --------------------------------------------------------------------------------------------------- | +| `--branch ` | Existing branch/worktree selector to repair, or a branch name to create under `.taskcore/worktrees` | +| `--home ` | Home root for worktree instances (default: `~/.taskcore-worktrees`) | +| `--from-config ` | Source config.json to seed from | +| `--from-data-dir ` | Source `TASKCORE_HOME` used when deriving the source config | +| `--from-instance ` | Source instance id when deriving the source config (default: `default`) | +| `--seed-mode ` | Seed profile: `minimal` or `full` (default: `minimal`) | +| `--no-seed` | Repair metadata only when bootstrapping a missing worktree config | +| `--allow-live-target` | Override the guard that requires the target worktree DB to be stopped first | Examples: @@ -301,16 +301,16 @@ For an already-created worktree where you want to keep the existing repo-local c **`pnpm taskcore worktree reseed [options]`** β€” Re-seed an existing worktree-local instance from another Taskcore instance or worktree while preserving the target worktree's current config, ports, and instance identity. -| Option | Description | -|---|---| -| `--from ` | Source worktree path, directory name, branch name, or `current` | -| `--to ` | Target worktree path, directory name, branch name, or `current` (defaults to `current`) | -| `--from-config ` | Source config.json to seed from | -| `--from-data-dir ` | Source `TASKCORE_HOME` used when deriving the source config | -| `--from-instance ` | Source instance id when deriving the source config | -| `--seed-mode ` | Seed profile: `minimal` or `full` (default: `full`) | -| `--yes` | Skip the destructive confirmation prompt | -| `--allow-live-target` | Override the guard that requires the target worktree DB to be stopped first | +| Option | Description | +| ------------------------ | --------------------------------------------------------------------------------------- | +| `--from ` | Source worktree path, directory name, branch name, or `current` | +| `--to ` | Target worktree path, directory name, branch name, or `current` (defaults to `current`) | +| `--from-config ` | Source config.json to seed from | +| `--from-data-dir ` | Source `TASKCORE_HOME` used when deriving the source config | +| `--from-instance ` | Source instance id when deriving the source config | +| `--seed-mode ` | Seed profile: `minimal` or `full` (default: `full`) | +| `--yes` | Skip the destructive confirmation prompt | +| `--allow-live-target` | Override the guard that requires the target worktree DB to be stopped first | Examples: @@ -332,19 +332,19 @@ pnpm taskcore worktree reseed \ **`pnpm taskcore worktree:make [options]`** β€” Create `~/NAME` as a git worktree, then initialize an isolated Taskcore instance inside it. This combines `git worktree add` with `worktree init` in a single step. -| Option | Description | -|---|---| -| `--start-point ` | Remote ref to base the new branch on (e.g. `origin/main`) | -| `--instance ` | Explicit isolated instance id | -| `--home ` | Home root for worktree instances (default: `~/.taskcore-worktrees`) | -| `--from-config ` | Source config.json to seed from | -| `--from-data-dir ` | Source TASKCORE_HOME used when deriving the source config | -| `--from-instance ` | Source instance id (default: `default`) | -| `--server-port ` | Preferred server port | -| `--db-port ` | Preferred embedded Postgres port | -| `--seed-mode ` | Seed profile: `minimal` or `full` (default: `minimal`) | -| `--no-seed` | Skip database seeding from the source instance | -| `--force` | Replace existing repo-local config and isolated instance data | +| Option | Description | +| ------------------------ | ------------------------------------------------------------------- | +| `--start-point ` | Remote ref to base the new branch on (e.g. `origin/main`) | +| `--instance ` | Explicit isolated instance id | +| `--home ` | Home root for worktree instances (default: `~/.taskcore-worktrees`) | +| `--from-config ` | Source config.json to seed from | +| `--from-data-dir ` | Source TASKCORE_HOME used when deriving the source config | +| `--from-instance ` | Source instance id (default: `default`) | +| `--server-port ` | Preferred server port | +| `--db-port ` | Preferred embedded Postgres port | +| `--seed-mode ` | Seed profile: `minimal` or `full` (default: `minimal`) | +| `--no-seed` | Skip database seeding from the source instance | +| `--force` | Replace existing repo-local config and isolated instance data | Examples: @@ -356,10 +356,10 @@ pnpm taskcore worktree:make experiment --no-seed **`pnpm taskcore worktree env [options]`** β€” Print shell exports for the current worktree-local Taskcore instance. -| Option | Description | -|---|---| -| `-c, --config ` | Path to config file | -| `--json` | Print JSON instead of shell exports | +| Option | Description | +| --------------------- | ----------------------------------- | +| `-c, --config ` | Path to config file | +| `--json` | Print JSON instead of shell exports | Examples: diff --git a/doc/DOCKER.md b/doc/DOCKER.md index d471d4f..64f6157 100644 --- a/doc/DOCKER.md +++ b/doc/DOCKER.md @@ -14,10 +14,10 @@ The Dockerfile installs common agent tools (`git`, `gh`, `curl`, `wget`, `ripgre Build arguments: -| Arg | Default | Purpose | -|-----|---------|---------| -| `USER_UID` | `1000` | UID for the container `node` user (match your host UID to avoid permission issues on bind mounts) | -| `USER_GID` | `1000` | GID for the container `node` group | +| Arg | Default | Purpose | +| ---------- | ------- | ------------------------------------------------------------------------------------------------- | +| `USER_UID` | `1000` | UID for the container `node` user (match your host UID to avoid permission issues on bind mounts) | +| `USER_GID` | `1000` | GID for the container `node` group | ```sh docker build -t taskcore-local \ @@ -150,11 +150,11 @@ Notes: The `docker/quadlet/` directory contains unit files to run Taskcore + PostgreSQL as systemd services via Podman Quadlet. -| File | Purpose | -|------|---------| -| `docker/quadlet/taskcore.pod` | Pod definition β€” groups containers into a shared network namespace | -| `docker/quadlet/taskcore.container` | Taskcore server β€” joins the pod, connects to Postgres at `127.0.0.1` | -| `docker/quadlet/taskcore-db.container` | PostgreSQL 17 β€” joins the pod, health-checked | +| File | Purpose | +| -------------------------------------- | -------------------------------------------------------------------- | +| `docker/quadlet/taskcore.pod` | Pod definition β€” groups containers into a shared network namespace | +| `docker/quadlet/taskcore.container` | Taskcore server β€” joins the pod, connects to Postgres at `127.0.0.1` | +| `docker/quadlet/taskcore-db.container` | PostgreSQL 17 β€” joins the pod, health-checked | ### Setup @@ -206,7 +206,7 @@ systemctl --user stop taskcore-pod # Stop all ### Quadlet notes -- **First boot**: Unlike Docker Compose's `condition: service_healthy`, Quadlet's `After=` only waits for the DB unit to *start*, not for PostgreSQL to be ready. On a cold first boot you may see one or two restart attempts in `journalctl --user -u taskcore` while PostgreSQL initialises β€” this is expected and resolves automatically via `Restart=on-failure`. +- **First boot**: Unlike Docker Compose's `condition: service_healthy`, Quadlet's `After=` only waits for the DB unit to _start_, not for PostgreSQL to be ready. On a cold first boot you may see one or two restart attempts in `journalctl --user -u taskcore` while PostgreSQL initialises β€” this is expected and resolves automatically via `Restart=on-failure`. - Containers in a pod share `localhost`, so Taskcore reaches Postgres at `127.0.0.1:5432`. - PostgreSQL data persists in the `taskcore-pgdata` named volume. - Taskcore data persists at `~/.local/share/taskcore`. diff --git a/doc/OPENCLAW_ONBOARDING.md b/doc/OPENCLAW_ONBOARDING.md index 3c01371..19a4e8d 100644 --- a/doc/OPENCLAW_ONBOARDING.md +++ b/doc/OPENCLAW_ONBOARDING.md @@ -1,30 +1,37 @@ Use this exact checklist. 1. Start Taskcore in auth mode. + ```bash cd pnpm dev --bind lan ``` + Then verify: + ```bash curl -sS http://127.0.0.1:3100/api/health | jq ``` 2. Start a clean/stock OpenClaw Docker. + ```bash OPENCLAW_RESET_STATE=1 OPENCLAW_BUILD=1 ./scripts/smoke/openclaw-docker-ui.sh ``` + Open the printed `Dashboard URL` (includes `#token=...`) in your browser. 3. In Taskcore UI, go to `http://127.0.0.1:3100/CLA/company/settings`. 4. Use the OpenClaw invite prompt flow. + - In the Invites section, click `Generate OpenClaw Invite Prompt`. - Copy the generated prompt from `OpenClaw Invite Prompt`. - Paste it into OpenClaw main chat as one message. - If it stalls, send one follow-up: `How is onboarding going? Continue setup now.` Security/control note: + - The OpenClaw invite prompt is created from a controlled endpoint: - `POST /api/companies/{companyId}/openclaw/invite-prompt` - board users with invite permission can call it @@ -33,6 +40,7 @@ Security/control note: 5. Approve the join request in Taskcore UI, then confirm the OpenClaw agent appears in CLA agents. 6. Gateway preflight (required before task tests). + - Confirm the created agent uses `openclaw_gateway` (not `openclaw`). - Confirm gateway URL is `ws://...` or `wss://...`. - Confirm gateway token is non-trivial (not empty / not 1-char placeholder). @@ -41,33 +49,41 @@ Security/control note: - required default: device auth enabled (`adapterConfig.disableDeviceAuth` false/absent) with persisted `adapterConfig.devicePrivateKeyPem` - do not rely on `disableDeviceAuth` for normal onboarding - If you can run API checks with board auth: + ```bash AGENT_ID="" curl -sS -H "Cookie: $TASKCORE_COOKIE" "http://127.0.0.1:3100/api/agents/$AGENT_ID" | jq '{adapterType,adapterConfig:{url:.adapterConfig.url,tokenLen:(.adapterConfig.headers["x-openclaw-token"] // .adapterConfig.headers["x-openclaw-auth"] // "" | length),disableDeviceAuth:(.adapterConfig.disableDeviceAuth // false),hasDeviceKey:(.adapterConfig.devicePrivateKeyPem // "" | length > 0)}}' ``` + - Expected: `adapterType=openclaw_gateway`, `tokenLen >= 16`, `hasDeviceKey=true`, and `disableDeviceAuth=false`. Pairing handshake note: + - Clean run expectation: first task should succeed without manual pairing commands. - The adapter attempts one automatic pairing approval + retry on first `pairing required` (when shared gateway auth token/password is valid). - If auto-pair cannot complete (for example token mismatch or no pending request), the first gateway run may still return `pairing required`. - This is a separate approval from Taskcore invite approval. You must approve the pending device in OpenClaw itself. - Approve it in OpenClaw, then retry the task. - For local docker smoke, you can approve from host: + ```bash docker exec openclaw-docker-openclaw-gateway-1 sh -lc 'openclaw devices approve --latest --json --url "ws://127.0.0.1:18789" --token "$(node -p \"require(process.env.HOME+\\\"/.openclaw/openclaw.json\\\").gateway.auth.token\")"' ``` + - You can inspect pending vs paired devices: + ```bash docker exec openclaw-docker-openclaw-gateway-1 sh -lc 'TOK="$(node -e \"const fs=require(\\\"fs\\\");const c=JSON.parse(fs.readFileSync(\\\"/home/node/.openclaw/openclaw.json\\\",\\\"utf8\\\"));process.stdout.write(c.gateway?.auth?.token||\\\"\\\");\")\"; openclaw devices list --json --url \"ws://127.0.0.1:18789\" --token \"$TOK\"' ``` 7. Case A (manual issue test). + - Create an issue assigned to the OpenClaw agent. - Put instructions: β€œpost comment `OPENCLAW_CASE_A_OK_` and mark done.” - Verify in UI: issue status becomes `done` and comment exists. 8. Case B (message tool test). + - Create another issue assigned to OpenClaw. - Instructions: β€œsend `OPENCLAW_CASE_B_OK_` to main webchat via message tool, then comment same marker on issue, then mark done.” - Verify both: @@ -75,16 +91,19 @@ docker exec openclaw-docker-openclaw-gateway-1 sh -lc 'TOK="$(node -e \"const fs - marker text appears in OpenClaw main chat 9. Case C (new session memory/skills test). + - In OpenClaw, start `/new` session. - Ask it to create a new CLA issue in Taskcore with unique title `OPENCLAW_CASE_C_CREATED_`. - Verify in Taskcore UI that new issue exists. 10. Watch logs during test (optional but helpful): + ```bash docker compose -f /tmp/openclaw-docker/docker-compose.yml -f /tmp/openclaw-docker/.taskcore-openclaw.override.yml logs -f openclaw-gateway ``` 11. Expected pass criteria. + - Preflight: `openclaw_gateway` + non-placeholder token (`tokenLen >= 16`). - Pairing mode: stable `devicePrivateKeyPem` configured with device auth enabled (default path). - Case A: `done` + marker comment. diff --git a/doc/SPEC-implementation.md b/doc/SPEC-implementation.md index 741a946..7184d41 100644 --- a/doc/SPEC-implementation.md +++ b/doc/SPEC-implementation.md @@ -28,21 +28,21 @@ Success means one operator can run a small AI-native company end-to-end with cle These decisions close open questions from `SPEC.md` for V1. -| Topic | V1 Decision | -|---|---| -| Tenancy | Single-tenant deployment, multi-company data model | -| Company model | Company is first-order; all business entities are company-scoped | -| Board | Single human board operator per deployment | -| Org graph | Strict tree (`reports_to` nullable root); no multi-manager reporting | -| Visibility | Full visibility to board and all agents in same company | -| Communication | Tasks + comments only (no separate chat system) | -| Task ownership | Single assignee; atomic checkout required for `in_progress` transition | -| Recovery | No automatic reassignment; work recovery stays manual/explicit | -| Agent adapters | Built-in `process` and `http` adapters | -| Auth | Mode-dependent human auth (`local_trusted` implicit board in current code; authenticated mode uses sessions), API keys for agents | -| Budget period | Monthly UTC calendar window | -| Budget enforcement | Soft alerts + hard limit auto-pause | -| Deployment modes | Canonical model is `local_trusted` + `authenticated` with `private/public` exposure policy (see `doc/DEPLOYMENT-MODES.md`) | +| Topic | V1 Decision | +| ------------------ | --------------------------------------------------------------------------------------------------------------------------------- | +| Tenancy | Single-tenant deployment, multi-company data model | +| Company model | Company is first-order; all business entities are company-scoped | +| Board | Single human board operator per deployment | +| Org graph | Strict tree (`reports_to` nullable root); no multi-manager reporting | +| Visibility | Full visibility to board and all agents in same company | +| Communication | Tasks + comments only (no separate chat system) | +| Task ownership | Single assignee; atomic checkout required for `in_progress` transition | +| Recovery | No automatic reassignment; work recovery stays manual/explicit | +| Agent adapters | Built-in `process` and `http` adapters | +| Auth | Mode-dependent human auth (`local_trusted` implicit board in current code; authenticated mode uses sessions), API keys for agents | +| Budget period | Monthly UTC calendar window | +| Budget enforcement | Soft alerts + hard limit auto-pause | +| Deployment modes | Canonical model is `local_trusted` + `authenticated` with `private/public` exposure policy (see `doc/DEPLOYMENT-MODES.md`) | ## 4. Current Baseline (Repo Snapshot) @@ -426,17 +426,17 @@ Detailed ownership, execution, blocker, and crash-recovery semantics are documen ## 9.3 Permission Matrix (V1) -| Action | Board | Agent | -|---|---|---| -| Create company | yes | no | -| Hire/create agent | yes (direct) | request via approval | -| Pause/resume agent | yes | no | -| Create/update task | yes | yes | -| Force reassign task | yes | limited | -| Approve strategy/hire requests | yes | no | -| Report cost | yes | yes | -| Set company budget | yes | no | -| Set subordinate budget | yes | yes (manager subtree only) | +| Action | Board | Agent | +| ------------------------------ | ------------ | -------------------------- | +| Create company | yes | no | +| Hire/create agent | yes (direct) | request via approval | +| Pause/resume agent | yes | no | +| Create/update task | yes | yes | +| Force reassign task | yes | limited | +| Approve strategy/hire requests | yes | no | +| Report cost | yes | yes | +| Set company budget | yes | no | +| Set subordinate budget | yes | yes (manager subtree only) | ## 10. API Contract (REST) @@ -574,7 +574,7 @@ Config shape: "command": "string", "args": ["string"], "cwd": "string", - "env": {"KEY": "VALUE"}, + "env": { "KEY": "VALUE" }, "timeoutSec": 900, "graceSec": 15 } @@ -595,9 +595,9 @@ Config shape: { "url": "https://...", "method": "POST", - "headers": {"Authorization": "Bearer ..."}, + "headers": { "Authorization": "Bearer ..." }, "timeoutMs": 15000, - "payloadTemplate": {"agentId": "{{agent.id}}", "runId": "{{run.id}}"} + "payloadTemplate": { "agentId": "{{agent.id}}", "runId": "{{run.id}}" } } ``` diff --git a/doc/SPEC.md b/doc/SPEC.md index b830313..9d58221 100644 --- a/doc/SPEC.md +++ b/doc/SPEC.md @@ -10,12 +10,12 @@ A Company is a first-order object. One Taskcore instance runs multiple Companies ### Fields (Draft) -| Field | Type | Notes | -| ----------- | ------------- | --------------------------------- | -| `id` | uuid | Primary key | -| `name` | string | Company name | -| `createdAt` | timestamp | | -| `updatedAt` | timestamp | | +| Field | Type | Notes | +| ----------- | --------- | ------------ | +| `id` | uuid | Primary key | +| `name` | string | Company name | +| `createdAt` | timestamp | | +| `updatedAt` | timestamp | | ### Board Governance [DRAFT] @@ -188,17 +188,17 @@ The heartbeat is a protocol, not a runtime. Taskcore defines how to initiate an Agent configuration includes an **adapter** that defines how Taskcore invokes the agent. Built-in adapters include: -| Adapter | Mechanism | Example | -| ---------------- | -------------------------- | -------------------------------------------------- | -| `process` | Execute a child process | `python run_agent.py --agent-id {id}` | -| `http` | Send an HTTP request | `POST https://openclaw.example.com/hook/{id}` | -| `claude_local` | Local Claude Code process | Claude Code heartbeat worker | -| `codex_local` | Local Codex process | Codex CLI heartbeat worker | -| `opencode_local` | Local OpenCode process | OpenCode heartbeat worker | -| `pi_local` | Local Pi process | Pi CLI heartbeat worker | -| `cursor` | Cursor API/CLI bridge | Cursor-integrated heartbeat worker | -| `openclaw_gateway` | OpenClaw gateway API | Managed OpenClaw agent via gateway | -| `hermes_local` | Local Hermes process | Hermes agent heartbeat worker | +| Adapter | Mechanism | Example | +| ------------------ | ------------------------- | --------------------------------------------- | +| `process` | Execute a child process | `python run_agent.py --agent-id {id}` | +| `http` | Send an HTTP request | `POST https://openclaw.example.com/hook/{id}` | +| `claude_local` | Local Claude Code process | Claude Code heartbeat worker | +| `codex_local` | Local Codex process | Codex CLI heartbeat worker | +| `opencode_local` | Local OpenCode process | OpenCode heartbeat worker | +| `pi_local` | Local Pi process | Pi CLI heartbeat worker | +| `cursor` | Cursor API/CLI bridge | Cursor-integrated heartbeat worker | +| `openclaw_gateway` | OpenClaw gateway API | Managed OpenClaw agent via gateway | +| `hermes_local` | Local Hermes process | Hermes agent heartbeat worker | The `process` and `http` adapters ship as generic defaults. Additional built-in adapters cover common local coding runtimes (see list above), and new adapter types can be registered via the plugin system (see Plugin / Extension Architecture). @@ -377,12 +377,12 @@ Flow: ### Tech Stack -| Layer | Technology | -| -------- | ------------------------------------------------------------ | -| Frontend | React + Vite | -| Backend | TypeScript + Express (REST API, not tRPC β€” need non-TS clients) | +| Layer | Technology | +| -------- | ------------------------------------------------------------------------------------------------------------------------------------- | +| Frontend | React + Vite | +| Backend | TypeScript + Express (REST API, not tRPC β€” need non-TS clients) | | Database | PostgreSQL (see [doc/DATABASE.md](./doc/DATABASE.md) for details β€” PGlite embedded for dev, Docker or hosted Supabase for production) | -| Auth | [Better Auth](https://www.better-auth.com/) | +| Auth | [Better Auth](https://www.better-auth.com/) | ### Concurrency Model: Atomic Task Checkout diff --git a/doc/TASKS-mcp.md b/doc/TASKS-mcp.md index 48590a8..1476998 100644 --- a/doc/TASKS-mcp.md +++ b/doc/TASKS-mcp.md @@ -20,7 +20,7 @@ List and filter issues in the workspace. | ----------------- | -------- | -------- | ----------------------------------------------------------------------------------------------- | | `query` | string | no | Free-text search across title and description | | `teamId` | string | no | Filter by team | -| `status` | string | no | Filter by specific workflow state | +| `status` | string | no | Filter by specific workflow state | | `stateType` | string | no | Filter by state category: `triage`, `backlog`, `unstarted`, `started`, `completed`, `cancelled` | | `assigneeId` | string | no | Filter by assignee (agent id) | | `projectId` | string | no | Filter by project | @@ -66,7 +66,7 @@ Create a new issue. | `title` | string | yes | | | `teamId` | string | yes | Team the issue belongs to | | `description` | string | no | Markdown | -| `status` | string | no | Workflow state. Default: team's default state | +| `status` | string | no | Workflow state. Default: team's default state | | `priority` | number | no | 0-4. Default: 0 (none) | | `estimate` | number | no | Point estimate | | `dueDate` | string | no | ISO date | @@ -96,7 +96,7 @@ Update an existing issue. | `id` | string | yes | UUID or identifier | | `title` | string | no | | | `description` | string | no | | -| `status` | string | no | Transition to a new workflow state | +| `status` | string | no | Transition to a new workflow state | | `priority` | number | no | 0-4 | | `estimate` | number | no | | | `dueDate` | string | no | ISO date, or `null` to clear | @@ -303,7 +303,7 @@ Get a milestone by ID. ### `create_milestone` | Parameter | Type | Required | -| ------------- | ------ | -------- | +| ------------- | ------ | -------- | --------------------------- | | `projectId` | string | yes | | `name` | string | yes | | `description` | string | no | @@ -317,7 +317,7 @@ Get a milestone by ID. ### `update_milestone` | Parameter | Type | Required | -| ------------- | ------ | -------- | +| ------------- | ------ | -------- | --------------------------- | | `id` | string | yes | | `name` | string | no | | `description` | string | no | diff --git a/doc/assets/footer.jpg b/doc/assets/footer.jpg deleted file mode 100644 index b1e3586023e9f41500a794330d08b621b9f43253..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 484526 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�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

5B@?H`tC19sFE>2H+j71|x7_tU_oU^V$iY-m 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%0DV`;`XjpKvrCor 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

ka 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