@apicircle/cli 1.0.0 → 1.0.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/index.js CHANGED
@@ -82,7 +82,6 @@ function installShutdown(handle) {
82
82
  }
83
83
 
84
84
  // src/commands/mcp.ts
85
- import * as path3 from "path";
86
85
  import kleur2 from "kleur";
87
86
  import {
88
87
  createMcpServer,
@@ -149,10 +148,258 @@ async function ensureWorkspace(dir) {
149
148
  return fresh;
150
149
  }
151
150
 
151
+ // src/util/resolveWorkspace.ts
152
+ import * as os from "os";
153
+ import * as path3 from "path";
154
+ import { promises as fs3 } from "fs";
155
+ import {
156
+ findWorkspaceEntry,
157
+ loadRegistry,
158
+ registerWorkspace,
159
+ saveRegistry,
160
+ workspaceDirFor
161
+ } from "@apicircle/core/workspace/registry";
162
+ import { saveToFile as saveToFile2 } from "@apicircle/core/workspace/file-backed";
163
+ import { generateId as generateId3 } from "@apicircle/shared";
164
+ var APP_NAME = "@apicircle";
165
+ var APP_SUBDIR = "desktop";
166
+ var WORKSPACES_DIRNAME = "workspaces";
167
+ function defaultWorkspacesRoot() {
168
+ const override = process.env.APICIRCLE_WORKSPACES_ROOT;
169
+ if (override && override.length > 0) return path3.resolve(override);
170
+ return path3.join(electronUserDataDir(), WORKSPACES_DIRNAME);
171
+ }
172
+ function electronUserDataDir() {
173
+ const home = os.homedir();
174
+ switch (process.platform) {
175
+ case "win32": {
176
+ const appdata = process.env.APPDATA ?? path3.join(home, "AppData", "Roaming");
177
+ return path3.join(appdata, APP_NAME, APP_SUBDIR);
178
+ }
179
+ case "darwin":
180
+ return path3.join(home, "Library", "Application Support", APP_NAME, APP_SUBDIR);
181
+ default:
182
+ return path3.join(
183
+ process.env.XDG_CONFIG_HOME ?? path3.join(home, ".config"),
184
+ APP_NAME,
185
+ APP_SUBDIR
186
+ );
187
+ }
188
+ }
189
+ async function resolveWorkspace(opts = {}) {
190
+ const root = opts.workspacesRoot ?? defaultWorkspacesRoot();
191
+ const nameSelector = opts.name?.trim();
192
+ const pathSelector = opts.path?.trim();
193
+ const expectExists = opts.expectExists ?? true;
194
+ if (nameSelector && pathSelector) {
195
+ throw new WorkspaceResolutionError(
196
+ "--workspace-name and --workspace-path are mutually exclusive. Pass one or neither.",
197
+ "both-flags"
198
+ );
199
+ }
200
+ if (pathSelector) {
201
+ const expanded = expandTilde(pathSelector);
202
+ const dir = path3.resolve(expanded);
203
+ if (expectExists && !await dirExists(dir)) {
204
+ throw new WorkspaceResolutionError(`Workspace directory not found: ${dir}`, "path-missing");
205
+ }
206
+ return { dir, id: null, name: null, fromRegistry: false, registryRoot: null };
207
+ }
208
+ const registry = await loadRegistry(root);
209
+ if (nameSelector) {
210
+ if (!registry) {
211
+ throw new WorkspaceResolutionError(
212
+ `No workspaces are registered at ${root}. Open the desktop app once to seed the registry, or pass --workspace-path <dir> to point at a workspace directory directly.`,
213
+ "no-registry"
214
+ );
215
+ }
216
+ const entry2 = findWorkspaceEntry(registry, nameSelector);
217
+ if (!entry2) {
218
+ throw new WorkspaceResolutionError(
219
+ `No workspace named "${nameSelector}" in the registry at ${root}. Run \`apicircle workspaces list\` to see what's available.`,
220
+ "not-found"
221
+ );
222
+ }
223
+ return {
224
+ dir: workspaceDirFor(root, entry2.id),
225
+ id: entry2.id,
226
+ name: entry2.name,
227
+ fromRegistry: true,
228
+ registryRoot: root
229
+ };
230
+ }
231
+ if (registry && registry.activeWorkspaceId) {
232
+ const active = registry.workspaces.find((w) => w.id === registry.activeWorkspaceId);
233
+ if (active) {
234
+ return {
235
+ dir: workspaceDirFor(root, active.id),
236
+ id: active.id,
237
+ name: active.name,
238
+ fromRegistry: true,
239
+ registryRoot: root
240
+ };
241
+ }
242
+ }
243
+ return {
244
+ dir: path3.resolve(process.cwd()),
245
+ id: null,
246
+ name: null,
247
+ fromRegistry: false,
248
+ registryRoot: null
249
+ };
250
+ }
251
+ async function createWorkspaceOnDisk(args) {
252
+ const root = args.workspacesRoot ?? defaultWorkspacesRoot();
253
+ const trimmed = args.name.trim();
254
+ if (!trimmed) throw new Error("Workspace name is required");
255
+ const existing = await loadRegistry(root);
256
+ if (existing && existing.workspaces.some((w) => w.name.toLowerCase() === trimmed.toLowerCase())) {
257
+ throw new Error(`A workspace named "${trimmed}" already exists`);
258
+ }
259
+ const workspaceId = generateId3();
260
+ const now = (/* @__PURE__ */ new Date()).toISOString();
261
+ const state = buildEmptyState(workspaceId, now, args.sampleRequest ?? false);
262
+ const dir = workspaceDirFor(root, workspaceId);
263
+ await saveToFile2(dir, state);
264
+ const entry2 = {
265
+ id: workspaceId,
266
+ name: trimmed,
267
+ createdAt: now,
268
+ lastOpenedAt: now
269
+ };
270
+ const registry = await registerWorkspace(root, entry2);
271
+ return { registry, entry: entry2, state, dir };
272
+ }
273
+ async function listWorkspacesOnDisk(args = {}) {
274
+ const root = args.workspacesRoot ?? defaultWorkspacesRoot();
275
+ const registry = await loadRegistry(root) ?? {
276
+ schemaVersion: 1,
277
+ activeWorkspaceId: null,
278
+ workspaces: []
279
+ };
280
+ return { registry, root };
281
+ }
282
+ async function saveRegistryToDisk(registry, workspacesRoot) {
283
+ await saveRegistry(workspacesRoot ?? defaultWorkspacesRoot(), registry);
284
+ }
285
+ var WorkspaceResolutionError = class extends Error {
286
+ code;
287
+ constructor(message, code) {
288
+ super(message);
289
+ this.name = "WorkspaceResolutionError";
290
+ this.code = code;
291
+ }
292
+ };
293
+ function expandTilde(value) {
294
+ if (value === "~") return os.homedir();
295
+ if (value.startsWith("~/") || value.startsWith("~\\")) {
296
+ return path3.join(os.homedir(), value.slice(2));
297
+ }
298
+ return value;
299
+ }
300
+ async function dirExists(p) {
301
+ try {
302
+ const st = await fs3.stat(p);
303
+ return st.isDirectory();
304
+ } catch {
305
+ return false;
306
+ }
307
+ }
308
+ function buildEmptyState(workspaceId, now, withSample) {
309
+ const sample = withSample ? {
310
+ id: generateId3(),
311
+ name: "Sample: GET /anything",
312
+ folderId: null,
313
+ method: "GET",
314
+ url: "https://httpbin.org/anything",
315
+ headers: [{ key: "Accept", value: "application/json", enabled: true }],
316
+ query: [],
317
+ body: { type: "none", content: "" },
318
+ auth: { type: "inherit" },
319
+ contextVars: [],
320
+ extractions: [],
321
+ assertions: [],
322
+ createdAt: now,
323
+ updatedAt: now
324
+ } : null;
325
+ const folders = {};
326
+ const requests = sample ? { [sample.id]: sample } : {};
327
+ return {
328
+ synced: {
329
+ schemaVersion: 1,
330
+ workspaceId,
331
+ collections: {
332
+ tree: {
333
+ id: generateId3(),
334
+ type: "root",
335
+ children: sample ? [{ kind: "request", id: sample.id }] : []
336
+ },
337
+ requests,
338
+ folders
339
+ },
340
+ environments: { items: {}, activeName: null, priorityOrder: [] },
341
+ linkedWorkspaces: {},
342
+ linkedOverrides: { requests: {}, environmentVars: {} },
343
+ releases: { self: null, perLink: {} },
344
+ globalAssets: { schemas: {}, graphql: {} },
345
+ mockServers: {},
346
+ meta: { createdAt: now, updatedAt: now, appVersion: "1.0.0" }
347
+ },
348
+ local: {
349
+ schemaVersion: 1,
350
+ workspaceId,
351
+ executionPlans: {},
352
+ history: { requestRuns: [], planRuns: [] },
353
+ secretIndex: { entries: {} },
354
+ sessions: { github: { workspace: null, links: {} } },
355
+ connectedRepo: null,
356
+ workingBranch: null,
357
+ seededWorkspaceSha: null,
358
+ retiredBranch: null,
359
+ sync: { lastPulledSnapshot: null, lastPulledSha: null, lastPulledAt: null, dirtyKeys: [] },
360
+ linkedCollections: {},
361
+ globalContext: {},
362
+ mockRuntime: { active: {} },
363
+ ui: {
364
+ activeRequestId: sample?.id ?? null,
365
+ sidebarExpandedSections: [],
366
+ themeId: "studio-dark",
367
+ fontId: "system-mono",
368
+ fontSizePercent: 100
369
+ },
370
+ settings: { validateOnSend: true, monacoConsumesWheel: false },
371
+ snapshots: { entries: [], maxBytes: 50 * 1024 * 1024 }
372
+ }
373
+ };
374
+ }
375
+
152
376
  // src/commands/mcp.ts
153
377
  function registerMcpCommand(program) {
154
- program.command("mcp").description("Run the APICircle MCP server (stdio transport)").option("-w, --workspace <dir>", "Workspace directory (defaults to current directory)").action(async (opts) => {
155
- const dir = path3.resolve(opts.workspace ?? process.cwd());
378
+ program.command("mcp").description("Run the API Circle MCP server (stdio transport)").option(
379
+ "--workspace-name <name-or-id>",
380
+ "Registry workspace name (case-insensitive) or id. Defaults to the active workspace."
381
+ ).option(
382
+ "--workspace-path <dir>",
383
+ "Filesystem directory containing workspace.synced.json (skips the registry)."
384
+ ).action(async (opts) => {
385
+ let dir;
386
+ let label;
387
+ try {
388
+ const resolved = await resolveWorkspace({
389
+ name: opts.workspaceName,
390
+ path: opts.workspacePath,
391
+ expectExists: false
392
+ });
393
+ dir = resolved.dir;
394
+ label = resolved.fromRegistry ? `${resolved.name ?? resolved.id} (${dir})` : dir;
395
+ } catch (err) {
396
+ if (err instanceof WorkspaceResolutionError) {
397
+ process.stderr.write(`${kleur2.red("error")}: ${err.message}
398
+ `);
399
+ process.exit(2);
400
+ }
401
+ throw err;
402
+ }
156
403
  try {
157
404
  await ensureWorkspace(dir);
158
405
  } catch (err) {
@@ -165,27 +412,54 @@ function registerMcpCommand(program) {
165
412
  const workspace = new FileBackedWorkspaceProvider(dir);
166
413
  const mock = new InProcessMockController();
167
414
  const host = createMcpServer({ workspace, mock });
168
- process.stderr.write(`${kleur2.green("apicircle-mcp")} ready \xB7 workspace=${dir}
415
+ process.stderr.write(`${kleur2.green("apicircle-mcp")} ready \xB7 workspace=${label}
169
416
  `);
170
417
  await host.connect();
171
418
  });
172
419
  }
173
420
 
174
421
  // src/commands/import.ts
175
- import { promises as fs3 } from "fs";
422
+ import { promises as fs4 } from "fs";
176
423
  import * as path4 from "path";
177
424
  import kleur3 from "kleur";
178
425
  import { applyMutation } from "@apicircle/core";
179
- import { saveToFile as saveToFile2 } from "@apicircle/core/workspace/file-backed";
426
+ import { saveToFile as saveToFile3 } from "@apicircle/core/workspace/file-backed";
180
427
  import {
181
428
  parseInsomniaToEndpoints,
182
429
  parseOpenApiToEndpoints,
183
430
  parsePostmanToEndpoints
184
431
  } from "@apicircle/mock-server-core";
185
- import { generateId as generateId3 } from "@apicircle/shared";
432
+ import { generateId as generateId4 } from "@apicircle/shared";
186
433
  function registerImportCommand(program) {
187
- program.command("import").description("Import a spec into a workspace folder").argument("<type>", "Source type: openapi | postman | insomnia | curl").argument("<input>", "Path to a spec file, or `-` to read from stdin").option("-w, --workspace <dir>", "Workspace directory (defaults to current directory)").option("-f, --format <format>", "OpenAPI format: json | yaml", "json").action(async (type, input, opts) => {
188
- const dir = path4.resolve(opts.workspace ?? process.cwd());
434
+ program.command("import").description("Import a spec into a workspace folder").argument("<type>", "Source type: openapi | postman | insomnia | curl").argument("<input>", "Path to a spec file, or `-` to read from stdin").option(
435
+ "--workspace-name <name-or-id>",
436
+ "Registry workspace name (case-insensitive) or id. Defaults to the active workspace."
437
+ ).option(
438
+ "--workspace-path <dir>",
439
+ "Filesystem directory containing workspace.synced.json (skips the registry)."
440
+ ).option("-f, --format <format>", "OpenAPI format: json | yaml", "json").action(async (type, input, opts) => {
441
+ let dir;
442
+ try {
443
+ const resolved = await resolveWorkspace({
444
+ name: opts.workspaceName,
445
+ path: opts.workspacePath,
446
+ expectExists: false
447
+ });
448
+ dir = resolved.dir;
449
+ if (resolved.fromRegistry) {
450
+ process.stderr.write(
451
+ `${kleur3.dim("workspace")}: ${kleur3.cyan(resolved.name ?? resolved.id ?? "")} ${kleur3.dim(`(${dir})`)}
452
+ `
453
+ );
454
+ }
455
+ } catch (err) {
456
+ if (err instanceof WorkspaceResolutionError) {
457
+ process.stderr.write(`${kleur3.red("error")}: ${err.message}
458
+ `);
459
+ process.exit(2);
460
+ }
461
+ throw err;
462
+ }
189
463
  const raw = await readInput(input);
190
464
  const state = await ensureWorkspace(dir);
191
465
  let nextSynced = state.synced;
@@ -252,7 +526,7 @@ function registerImportCommand(program) {
252
526
  `);
253
527
  process.exit(2);
254
528
  }
255
- await saveToFile2(dir, { synced: nextSynced, local: nextLocal });
529
+ await saveToFile3(dir, { synced: nextSynced, local: nextLocal });
256
530
  process.stdout.write(
257
531
  `${kleur3.green("imported")} ${created.length} request${created.length === 1 ? "" : "s"} into ${dir}
258
532
  `
@@ -261,22 +535,22 @@ function registerImportCommand(program) {
261
535
  }
262
536
  async function readInput(p) {
263
537
  if (p === "-") {
264
- return new Promise((resolve7, reject) => {
538
+ return new Promise((resolve6, reject) => {
265
539
  let data = "";
266
540
  process.stdin.setEncoding("utf-8");
267
541
  process.stdin.on("data", (chunk) => {
268
542
  data += typeof chunk === "string" ? chunk : chunk.toString("utf-8");
269
543
  });
270
- process.stdin.on("end", () => resolve7(data));
544
+ process.stdin.on("end", () => resolve6(data));
271
545
  process.stdin.on("error", reject);
272
546
  });
273
547
  }
274
- return fs3.readFile(path4.resolve(p), "utf-8");
548
+ return fs4.readFile(path4.resolve(p), "utf-8");
275
549
  }
276
550
  function blankRequest(partial) {
277
551
  const now = (/* @__PURE__ */ new Date()).toISOString();
278
552
  return {
279
- id: generateId3(),
553
+ id: generateId4(),
280
554
  folderId: null,
281
555
  headers: [],
282
556
  query: [],
@@ -292,8 +566,7 @@ function blankRequest(partial) {
292
566
  }
293
567
 
294
568
  // src/commands/run.ts
295
- import * as os from "os";
296
- import * as path6 from "path";
569
+ import * as os2 from "os";
297
570
  import kleur4 from "kleur";
298
571
  import {
299
572
  ANONYMOUS_ACTOR,
@@ -301,11 +574,11 @@ import {
301
574
  resolvePlanRef,
302
575
  runPlan
303
576
  } from "@apicircle/core";
304
- import { loadFromFile as loadFromFile2, saveToFile as saveToFile3 } from "@apicircle/core/workspace/file-backed";
577
+ import { loadFromFile as loadFromFile2, saveToFile as saveToFile4 } from "@apicircle/core/workspace/file-backed";
305
578
 
306
579
  // src/util/secrets.ts
307
580
  import * as path5 from "path";
308
- import { promises as fs4 } from "fs";
581
+ import { promises as fs5 } from "fs";
309
582
  var DEFAULT_PREFIX = "APICIRCLE_SECRET_";
310
583
  async function buildSecretsFromCli(options = {}) {
311
584
  const env = options.env ?? process.env;
@@ -313,7 +586,7 @@ async function buildSecretsFromCli(options = {}) {
313
586
  const byId = {};
314
587
  if (options.secretsFile) {
315
588
  const resolved = path5.resolve(options.secretsFile);
316
- const raw = await fs4.readFile(resolved, "utf8");
589
+ const raw = await fs5.readFile(resolved, "utf8");
317
590
  const parsed = JSON.parse(raw);
318
591
  if (!parsed || typeof parsed !== "object" || Array.isArray(parsed)) {
319
592
  throw new Error(
@@ -338,8 +611,34 @@ async function buildSecretsFromCli(options = {}) {
338
611
  // src/commands/run.ts
339
612
  var REPORTERS = ["text", "json", "junit"];
340
613
  function registerRunCommand(program) {
341
- program.command("run").description("Run a saved execution plan from a workspace and report the result").argument("<plan>", "Plan name or id to run").option("-w, --workspace <dir>", "Workspace directory (defaults to current directory)").option("--no-assertions", "Run requests without evaluating their assertions").option("-s, --secrets <file>", "JSON file mapping secretKeyId \u2192 plaintext value").option("--no-save", "Do not write the plan run to workspace history").option("--reporter <format>", "Report format: text | json | junit", "text").option("--bail", "Stop the run at the first failed step").option("-e, --env <name>", "Layer a local environment on top of the run").option("--as <actor>", "Override the recorded runner identity").action(async (planRef, opts) => {
342
- const dir = path6.resolve(opts.workspace ?? process.cwd());
614
+ program.command("run").description("Run a saved execution plan from a workspace and report the result").argument("<plan>", "Plan name or id to run").option(
615
+ "--workspace-name <name-or-id>",
616
+ "Registry workspace name (case-insensitive) or id. Defaults to the active workspace."
617
+ ).option(
618
+ "--workspace-path <dir>",
619
+ "Filesystem directory containing workspace.synced.json (skips the registry)."
620
+ ).option("--no-assertions", "Run requests without evaluating their assertions").option("-s, --secrets <file>", "JSON file mapping secretKeyId \u2192 plaintext value").option("--no-save", "Do not write the plan run to workspace history").option("--reporter <format>", "Report format: text | json | junit", "text").option("--bail", "Stop the run at the first failed step").option("-e, --env <name>", "Layer a local environment on top of the run").option("--as <actor>", "Override the recorded runner identity").action(async (planRef, opts) => {
621
+ let dir;
622
+ try {
623
+ const resolved = await resolveWorkspace({
624
+ name: opts.workspaceName,
625
+ path: opts.workspacePath,
626
+ expectExists: false
627
+ });
628
+ dir = resolved.dir;
629
+ if (resolved.fromRegistry) {
630
+ process.stderr.write(
631
+ `${kleur4.dim("workspace")}: ${kleur4.cyan(resolved.name ?? resolved.id ?? "")} ${kleur4.dim(`(${dir})`)}
632
+ `
633
+ );
634
+ }
635
+ } catch (err) {
636
+ if (err instanceof WorkspaceResolutionError) {
637
+ fail(err.message);
638
+ return;
639
+ }
640
+ throw err;
641
+ }
343
642
  const reporter = opts.reporter ?? "text";
344
643
  if (!isReporter(reporter)) {
345
644
  fail(`unknown --reporter "${reporter}" (expected: ${REPORTERS.join(", ")})`);
@@ -405,7 +704,7 @@ function registerRunCommand(program) {
405
704
  process.off("SIGINT", onSigint);
406
705
  const aborted = controller.signal.aborted;
407
706
  const saved = opts.save !== false;
408
- if (saved) await saveToFile3(dir, result.nextState);
707
+ if (saved) await saveToFile4(dir, result.nextState);
409
708
  if (reporter === "json") {
410
709
  process.stdout.write(
411
710
  JSON.stringify(
@@ -431,7 +730,7 @@ function resolveActor(local, override) {
431
730
  const login = local.sessions.github.workspace?.accountLogin;
432
731
  if (login) return { kind: "github", name: login };
433
732
  try {
434
- const username = os.userInfo().username;
733
+ const username = os2.userInfo().username;
435
734
  if (username) return { kind: "os", name: username };
436
735
  } catch {
437
736
  }
@@ -597,14 +896,126 @@ function fail(message, code = 2, kind = "error") {
597
896
  process.exitCode = code;
598
897
  }
599
898
 
899
+ // src/commands/workspaces.ts
900
+ import kleur5 from "kleur";
901
+ import { findWorkspaceEntry as findWorkspaceEntry2, setActiveWorkspace } from "@apicircle/core/workspace/registry";
902
+ function registerWorkspacesCommand(program) {
903
+ const ws = program.command("workspaces").description("List, create, or switch the active workspace");
904
+ ws.command("list").description("List every workspace registered on this machine").option("--json", "Emit JSON instead of a formatted table").action(async (opts) => {
905
+ const { registry, root } = await listWorkspacesOnDisk();
906
+ if (opts.json) {
907
+ process.stdout.write(JSON.stringify({ root, registry }, null, 2) + "\n");
908
+ return;
909
+ }
910
+ if (registry.workspaces.length === 0) {
911
+ process.stdout.write(
912
+ `${kleur5.dim("No workspaces registered yet at")} ${root}
913
+ ${kleur5.dim("Run")} ${kleur5.cyan("apicircle workspaces create <name>")} ${kleur5.dim(
914
+ "or open the desktop app to seed one."
915
+ )}
916
+ `
917
+ );
918
+ return;
919
+ }
920
+ process.stdout.write(`${kleur5.dim("registry")}: ${root}
921
+
922
+ `);
923
+ const rows = [...registry.workspaces].sort(
924
+ (a, b) => b.lastOpenedAt.localeCompare(a.lastOpenedAt)
925
+ );
926
+ const nameWidth = Math.max(4, ...rows.map((r) => r.name.length));
927
+ const idWidth = Math.max(2, ...rows.map((r) => r.id.length));
928
+ process.stdout.write(
929
+ kleur5.bold(
930
+ ` ${"".padEnd(1)} ${"NAME".padEnd(nameWidth)} ${"ID".padEnd(idWidth)} LAST OPENED
931
+ `
932
+ )
933
+ );
934
+ for (const w of rows) {
935
+ const mark = w.id === registry.activeWorkspaceId ? kleur5.green("\u25CF") : " ";
936
+ process.stdout.write(
937
+ ` ${mark} ${w.name.padEnd(nameWidth)} ${kleur5.dim(
938
+ w.id.padEnd(idWidth)
939
+ )} ${kleur5.dim(w.lastOpenedAt)}
940
+ `
941
+ );
942
+ }
943
+ process.stdout.write(`
944
+ ${kleur5.dim("\u25CF = active")}
945
+ `);
946
+ });
947
+ ws.command("create").description("Create a new workspace and add it to the registry").argument("<name>", "Human-readable label for the workspace").option("--sample", "Seed the workspace with one sample request", false).action(async (name, opts) => {
948
+ try {
949
+ const { entry: entry2, dir, registry } = await createWorkspaceOnDisk({
950
+ name,
951
+ sampleRequest: opts.sample ?? false
952
+ });
953
+ process.stdout.write(
954
+ `${kleur5.green("created")} workspace ${kleur5.cyan(entry2.name)} ${kleur5.dim(`(${entry2.id})`)}
955
+ at ${dir}
956
+ `
957
+ );
958
+ if (registry.activeWorkspaceId === entry2.id) {
959
+ process.stdout.write(`${kleur5.dim("marked as active")}
960
+ `);
961
+ }
962
+ } catch (err) {
963
+ process.stderr.write(
964
+ `${kleur5.red("error")}: ${err instanceof Error ? err.message : String(err)}
965
+ `
966
+ );
967
+ process.exit(2);
968
+ }
969
+ });
970
+ ws.command("use").description("Set the active workspace by id or name").argument("<selector>", "Workspace id or name").action(async (selector) => {
971
+ const { registry, root } = await listWorkspacesOnDisk();
972
+ const entry2 = findWorkspaceEntry2(registry, selector);
973
+ if (!entry2) {
974
+ process.stderr.write(
975
+ `${kleur5.red("error")}: no workspace named "${selector}" in the registry at ${root}.
976
+ ${kleur5.dim("Run")} ${kleur5.cyan("apicircle workspaces list")} ${kleur5.dim("to see what is available.")}
977
+ `
978
+ );
979
+ process.exit(2);
980
+ return;
981
+ }
982
+ const next = await setActiveWorkspace(root, entry2.id);
983
+ void next;
984
+ process.stdout.write(
985
+ `${kleur5.green("active")} workspace is now ${kleur5.cyan(entry2.name)} ${kleur5.dim(`(${entry2.id})`)}
986
+ `
987
+ );
988
+ });
989
+ ws.command("path").description("Print the on-disk path for a workspace (or the workspaces root)").argument("[selector]", "Optional workspace id or name; prints the root when omitted").action(async (selector) => {
990
+ if (!selector) {
991
+ process.stdout.write(defaultWorkspacesRoot() + "\n");
992
+ return;
993
+ }
994
+ const { registry, root } = await listWorkspacesOnDisk();
995
+ const entry2 = findWorkspaceEntry2(registry, selector);
996
+ if (!entry2) {
997
+ process.stderr.write(
998
+ `${kleur5.red("error")}: no workspace named "${selector}" in the registry at ${root}.
999
+ `
1000
+ );
1001
+ process.exit(2);
1002
+ return;
1003
+ }
1004
+ void saveRegistryToDisk;
1005
+ const { workspaceDirFor: workspaceDirFor2 } = await import("@apicircle/core/workspace/registry");
1006
+ process.stdout.write(workspaceDirFor2(root, entry2.id) + "\n");
1007
+ });
1008
+ }
1009
+
600
1010
  // src/index.ts
601
1011
  function buildProgram() {
602
1012
  const program = new Command();
603
- program.name("apicircle").description("Command-line companion to APICircle Studio.").version("1.0.0");
1013
+ program.name("apicircle").description("Command-line companion to API Circle Studio.").version("1.0.0");
604
1014
  registerMockCommand(program);
605
1015
  registerMcpCommand(program);
606
1016
  registerImportCommand(program);
607
1017
  registerRunCommand(program);
1018
+ registerWorkspacesCommand(program);
608
1019
  return program;
609
1020
  }
610
1021
  async function runCli(argv = process.argv) {