@apicircle/cli 1.0.1 → 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.cjs CHANGED
@@ -113,7 +113,6 @@ function installShutdown(handle) {
113
113
  }
114
114
 
115
115
  // src/commands/mcp.ts
116
- var path3 = __toESM(require("path"), 1);
117
116
  var import_kleur2 = __toESM(require("kleur"), 1);
118
117
  var import_mcp_server = require("@apicircle/mcp-server");
119
118
 
@@ -176,10 +175,252 @@ async function ensureWorkspace(dir) {
176
175
  return fresh;
177
176
  }
178
177
 
178
+ // src/util/resolveWorkspace.ts
179
+ var os = __toESM(require("os"), 1);
180
+ var path3 = __toESM(require("path"), 1);
181
+ var import_node_fs3 = require("fs");
182
+ var import_registry = require("@apicircle/core/workspace/registry");
183
+ var import_file_backed2 = require("@apicircle/core/workspace/file-backed");
184
+ var import_shared3 = require("@apicircle/shared");
185
+ var APP_NAME = "@apicircle";
186
+ var APP_SUBDIR = "desktop";
187
+ var WORKSPACES_DIRNAME = "workspaces";
188
+ function defaultWorkspacesRoot() {
189
+ const override = process.env.APICIRCLE_WORKSPACES_ROOT;
190
+ if (override && override.length > 0) return path3.resolve(override);
191
+ return path3.join(electronUserDataDir(), WORKSPACES_DIRNAME);
192
+ }
193
+ function electronUserDataDir() {
194
+ const home = os.homedir();
195
+ switch (process.platform) {
196
+ case "win32": {
197
+ const appdata = process.env.APPDATA ?? path3.join(home, "AppData", "Roaming");
198
+ return path3.join(appdata, APP_NAME, APP_SUBDIR);
199
+ }
200
+ case "darwin":
201
+ return path3.join(home, "Library", "Application Support", APP_NAME, APP_SUBDIR);
202
+ default:
203
+ return path3.join(
204
+ process.env.XDG_CONFIG_HOME ?? path3.join(home, ".config"),
205
+ APP_NAME,
206
+ APP_SUBDIR
207
+ );
208
+ }
209
+ }
210
+ async function resolveWorkspace(opts = {}) {
211
+ const root = opts.workspacesRoot ?? defaultWorkspacesRoot();
212
+ const nameSelector = opts.name?.trim();
213
+ const pathSelector = opts.path?.trim();
214
+ const expectExists = opts.expectExists ?? true;
215
+ if (nameSelector && pathSelector) {
216
+ throw new WorkspaceResolutionError(
217
+ "--workspace-name and --workspace-path are mutually exclusive. Pass one or neither.",
218
+ "both-flags"
219
+ );
220
+ }
221
+ if (pathSelector) {
222
+ const expanded = expandTilde(pathSelector);
223
+ const dir = path3.resolve(expanded);
224
+ if (expectExists && !await dirExists(dir)) {
225
+ throw new WorkspaceResolutionError(`Workspace directory not found: ${dir}`, "path-missing");
226
+ }
227
+ return { dir, id: null, name: null, fromRegistry: false, registryRoot: null };
228
+ }
229
+ const registry = await (0, import_registry.loadRegistry)(root);
230
+ if (nameSelector) {
231
+ if (!registry) {
232
+ throw new WorkspaceResolutionError(
233
+ `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.`,
234
+ "no-registry"
235
+ );
236
+ }
237
+ const entry2 = (0, import_registry.findWorkspaceEntry)(registry, nameSelector);
238
+ if (!entry2) {
239
+ throw new WorkspaceResolutionError(
240
+ `No workspace named "${nameSelector}" in the registry at ${root}. Run \`apicircle workspaces list\` to see what's available.`,
241
+ "not-found"
242
+ );
243
+ }
244
+ return {
245
+ dir: (0, import_registry.workspaceDirFor)(root, entry2.id),
246
+ id: entry2.id,
247
+ name: entry2.name,
248
+ fromRegistry: true,
249
+ registryRoot: root
250
+ };
251
+ }
252
+ if (registry && registry.activeWorkspaceId) {
253
+ const active = registry.workspaces.find((w) => w.id === registry.activeWorkspaceId);
254
+ if (active) {
255
+ return {
256
+ dir: (0, import_registry.workspaceDirFor)(root, active.id),
257
+ id: active.id,
258
+ name: active.name,
259
+ fromRegistry: true,
260
+ registryRoot: root
261
+ };
262
+ }
263
+ }
264
+ return {
265
+ dir: path3.resolve(process.cwd()),
266
+ id: null,
267
+ name: null,
268
+ fromRegistry: false,
269
+ registryRoot: null
270
+ };
271
+ }
272
+ async function createWorkspaceOnDisk(args) {
273
+ const root = args.workspacesRoot ?? defaultWorkspacesRoot();
274
+ const trimmed = args.name.trim();
275
+ if (!trimmed) throw new Error("Workspace name is required");
276
+ const existing = await (0, import_registry.loadRegistry)(root);
277
+ if (existing && existing.workspaces.some((w) => w.name.toLowerCase() === trimmed.toLowerCase())) {
278
+ throw new Error(`A workspace named "${trimmed}" already exists`);
279
+ }
280
+ const workspaceId = (0, import_shared3.generateId)();
281
+ const now = (/* @__PURE__ */ new Date()).toISOString();
282
+ const state = buildEmptyState(workspaceId, now, args.sampleRequest ?? false);
283
+ const dir = (0, import_registry.workspaceDirFor)(root, workspaceId);
284
+ await (0, import_file_backed2.saveToFile)(dir, state);
285
+ const entry2 = {
286
+ id: workspaceId,
287
+ name: trimmed,
288
+ createdAt: now,
289
+ lastOpenedAt: now
290
+ };
291
+ const registry = await (0, import_registry.registerWorkspace)(root, entry2);
292
+ return { registry, entry: entry2, state, dir };
293
+ }
294
+ async function listWorkspacesOnDisk(args = {}) {
295
+ const root = args.workspacesRoot ?? defaultWorkspacesRoot();
296
+ const registry = await (0, import_registry.loadRegistry)(root) ?? {
297
+ schemaVersion: 1,
298
+ activeWorkspaceId: null,
299
+ workspaces: []
300
+ };
301
+ return { registry, root };
302
+ }
303
+ async function saveRegistryToDisk(registry, workspacesRoot) {
304
+ await (0, import_registry.saveRegistry)(workspacesRoot ?? defaultWorkspacesRoot(), registry);
305
+ }
306
+ var WorkspaceResolutionError = class extends Error {
307
+ code;
308
+ constructor(message, code) {
309
+ super(message);
310
+ this.name = "WorkspaceResolutionError";
311
+ this.code = code;
312
+ }
313
+ };
314
+ function expandTilde(value) {
315
+ if (value === "~") return os.homedir();
316
+ if (value.startsWith("~/") || value.startsWith("~\\")) {
317
+ return path3.join(os.homedir(), value.slice(2));
318
+ }
319
+ return value;
320
+ }
321
+ async function dirExists(p) {
322
+ try {
323
+ const st = await import_node_fs3.promises.stat(p);
324
+ return st.isDirectory();
325
+ } catch {
326
+ return false;
327
+ }
328
+ }
329
+ function buildEmptyState(workspaceId, now, withSample) {
330
+ const sample = withSample ? {
331
+ id: (0, import_shared3.generateId)(),
332
+ name: "Sample: GET /anything",
333
+ folderId: null,
334
+ method: "GET",
335
+ url: "https://httpbin.org/anything",
336
+ headers: [{ key: "Accept", value: "application/json", enabled: true }],
337
+ query: [],
338
+ body: { type: "none", content: "" },
339
+ auth: { type: "inherit" },
340
+ contextVars: [],
341
+ extractions: [],
342
+ assertions: [],
343
+ createdAt: now,
344
+ updatedAt: now
345
+ } : null;
346
+ const folders = {};
347
+ const requests = sample ? { [sample.id]: sample } : {};
348
+ return {
349
+ synced: {
350
+ schemaVersion: 1,
351
+ workspaceId,
352
+ collections: {
353
+ tree: {
354
+ id: (0, import_shared3.generateId)(),
355
+ type: "root",
356
+ children: sample ? [{ kind: "request", id: sample.id }] : []
357
+ },
358
+ requests,
359
+ folders
360
+ },
361
+ environments: { items: {}, activeName: null, priorityOrder: [] },
362
+ linkedWorkspaces: {},
363
+ linkedOverrides: { requests: {}, environmentVars: {} },
364
+ releases: { self: null, perLink: {} },
365
+ globalAssets: { schemas: {}, graphql: {} },
366
+ mockServers: {},
367
+ meta: { createdAt: now, updatedAt: now, appVersion: "1.0.0" }
368
+ },
369
+ local: {
370
+ schemaVersion: 1,
371
+ workspaceId,
372
+ executionPlans: {},
373
+ history: { requestRuns: [], planRuns: [] },
374
+ secretIndex: { entries: {} },
375
+ sessions: { github: { workspace: null, links: {} } },
376
+ connectedRepo: null,
377
+ workingBranch: null,
378
+ seededWorkspaceSha: null,
379
+ retiredBranch: null,
380
+ sync: { lastPulledSnapshot: null, lastPulledSha: null, lastPulledAt: null, dirtyKeys: [] },
381
+ linkedCollections: {},
382
+ globalContext: {},
383
+ mockRuntime: { active: {} },
384
+ ui: {
385
+ activeRequestId: sample?.id ?? null,
386
+ sidebarExpandedSections: [],
387
+ themeId: "studio-dark",
388
+ fontId: "system-mono",
389
+ fontSizePercent: 100
390
+ },
391
+ settings: { validateOnSend: true, monacoConsumesWheel: false },
392
+ snapshots: { entries: [], maxBytes: 50 * 1024 * 1024 }
393
+ }
394
+ };
395
+ }
396
+
179
397
  // src/commands/mcp.ts
180
398
  function registerMcpCommand(program) {
181
- program.command("mcp").description("Run the API Circle MCP server (stdio transport)").option("-w, --workspace <dir>", "Workspace directory (defaults to current directory)").action(async (opts) => {
182
- const dir = path3.resolve(opts.workspace ?? process.cwd());
399
+ program.command("mcp").description("Run the API Circle MCP server (stdio transport)").option(
400
+ "--workspace-name <name-or-id>",
401
+ "Registry workspace name (case-insensitive) or id. Defaults to the active workspace."
402
+ ).option(
403
+ "--workspace-path <dir>",
404
+ "Filesystem directory containing workspace.synced.json (skips the registry)."
405
+ ).action(async (opts) => {
406
+ let dir;
407
+ let label;
408
+ try {
409
+ const resolved = await resolveWorkspace({
410
+ name: opts.workspaceName,
411
+ path: opts.workspacePath,
412
+ expectExists: false
413
+ });
414
+ dir = resolved.dir;
415
+ label = resolved.fromRegistry ? `${resolved.name ?? resolved.id} (${dir})` : dir;
416
+ } catch (err) {
417
+ if (err instanceof WorkspaceResolutionError) {
418
+ process.stderr.write(`${import_kleur2.default.red("error")}: ${err.message}
419
+ `);
420
+ process.exit(2);
421
+ }
422
+ throw err;
423
+ }
183
424
  try {
184
425
  await ensureWorkspace(dir);
185
426
  } catch (err) {
@@ -192,23 +433,50 @@ function registerMcpCommand(program) {
192
433
  const workspace = new import_mcp_server.FileBackedWorkspaceProvider(dir);
193
434
  const mock = new import_mcp_server.InProcessMockController();
194
435
  const host = (0, import_mcp_server.createMcpServer)({ workspace, mock });
195
- process.stderr.write(`${import_kleur2.default.green("apicircle-mcp")} ready \xB7 workspace=${dir}
436
+ process.stderr.write(`${import_kleur2.default.green("apicircle-mcp")} ready \xB7 workspace=${label}
196
437
  `);
197
438
  await host.connect();
198
439
  });
199
440
  }
200
441
 
201
442
  // src/commands/import.ts
202
- var import_node_fs3 = require("fs");
443
+ var import_node_fs4 = require("fs");
203
444
  var path4 = __toESM(require("path"), 1);
204
445
  var import_kleur3 = __toESM(require("kleur"), 1);
205
446
  var import_core = require("@apicircle/core");
206
- var import_file_backed2 = require("@apicircle/core/workspace/file-backed");
447
+ var import_file_backed3 = require("@apicircle/core/workspace/file-backed");
207
448
  var import_mock_server_core2 = require("@apicircle/mock-server-core");
208
- var import_shared3 = require("@apicircle/shared");
449
+ var import_shared4 = require("@apicircle/shared");
209
450
  function registerImportCommand(program) {
210
- 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) => {
211
- const dir = path4.resolve(opts.workspace ?? process.cwd());
451
+ 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(
452
+ "--workspace-name <name-or-id>",
453
+ "Registry workspace name (case-insensitive) or id. Defaults to the active workspace."
454
+ ).option(
455
+ "--workspace-path <dir>",
456
+ "Filesystem directory containing workspace.synced.json (skips the registry)."
457
+ ).option("-f, --format <format>", "OpenAPI format: json | yaml", "json").action(async (type, input, opts) => {
458
+ let dir;
459
+ try {
460
+ const resolved = await resolveWorkspace({
461
+ name: opts.workspaceName,
462
+ path: opts.workspacePath,
463
+ expectExists: false
464
+ });
465
+ dir = resolved.dir;
466
+ if (resolved.fromRegistry) {
467
+ process.stderr.write(
468
+ `${import_kleur3.default.dim("workspace")}: ${import_kleur3.default.cyan(resolved.name ?? resolved.id ?? "")} ${import_kleur3.default.dim(`(${dir})`)}
469
+ `
470
+ );
471
+ }
472
+ } catch (err) {
473
+ if (err instanceof WorkspaceResolutionError) {
474
+ process.stderr.write(`${import_kleur3.default.red("error")}: ${err.message}
475
+ `);
476
+ process.exit(2);
477
+ }
478
+ throw err;
479
+ }
212
480
  const raw = await readInput(input);
213
481
  const state = await ensureWorkspace(dir);
214
482
  let nextSynced = state.synced;
@@ -275,7 +543,7 @@ function registerImportCommand(program) {
275
543
  `);
276
544
  process.exit(2);
277
545
  }
278
- await (0, import_file_backed2.saveToFile)(dir, { synced: nextSynced, local: nextLocal });
546
+ await (0, import_file_backed3.saveToFile)(dir, { synced: nextSynced, local: nextLocal });
279
547
  process.stdout.write(
280
548
  `${import_kleur3.default.green("imported")} ${created.length} request${created.length === 1 ? "" : "s"} into ${dir}
281
549
  `
@@ -284,22 +552,22 @@ function registerImportCommand(program) {
284
552
  }
285
553
  async function readInput(p) {
286
554
  if (p === "-") {
287
- return new Promise((resolve7, reject) => {
555
+ return new Promise((resolve6, reject) => {
288
556
  let data = "";
289
557
  process.stdin.setEncoding("utf-8");
290
558
  process.stdin.on("data", (chunk) => {
291
559
  data += typeof chunk === "string" ? chunk : chunk.toString("utf-8");
292
560
  });
293
- process.stdin.on("end", () => resolve7(data));
561
+ process.stdin.on("end", () => resolve6(data));
294
562
  process.stdin.on("error", reject);
295
563
  });
296
564
  }
297
- return import_node_fs3.promises.readFile(path4.resolve(p), "utf-8");
565
+ return import_node_fs4.promises.readFile(path4.resolve(p), "utf-8");
298
566
  }
299
567
  function blankRequest(partial) {
300
568
  const now = (/* @__PURE__ */ new Date()).toISOString();
301
569
  return {
302
- id: (0, import_shared3.generateId)(),
570
+ id: (0, import_shared4.generateId)(),
303
571
  folderId: null,
304
572
  headers: [],
305
573
  query: [],
@@ -315,15 +583,14 @@ function blankRequest(partial) {
315
583
  }
316
584
 
317
585
  // src/commands/run.ts
318
- var os = __toESM(require("os"), 1);
319
- var path6 = __toESM(require("path"), 1);
586
+ var os2 = __toESM(require("os"), 1);
320
587
  var import_kleur4 = __toESM(require("kleur"), 1);
321
588
  var import_core2 = require("@apicircle/core");
322
- var import_file_backed3 = require("@apicircle/core/workspace/file-backed");
589
+ var import_file_backed4 = require("@apicircle/core/workspace/file-backed");
323
590
 
324
591
  // src/util/secrets.ts
325
592
  var path5 = __toESM(require("path"), 1);
326
- var import_node_fs4 = require("fs");
593
+ var import_node_fs5 = require("fs");
327
594
  var DEFAULT_PREFIX = "APICIRCLE_SECRET_";
328
595
  async function buildSecretsFromCli(options = {}) {
329
596
  const env = options.env ?? process.env;
@@ -331,7 +598,7 @@ async function buildSecretsFromCli(options = {}) {
331
598
  const byId = {};
332
599
  if (options.secretsFile) {
333
600
  const resolved = path5.resolve(options.secretsFile);
334
- const raw = await import_node_fs4.promises.readFile(resolved, "utf8");
601
+ const raw = await import_node_fs5.promises.readFile(resolved, "utf8");
335
602
  const parsed = JSON.parse(raw);
336
603
  if (!parsed || typeof parsed !== "object" || Array.isArray(parsed)) {
337
604
  throw new Error(
@@ -356,14 +623,40 @@ async function buildSecretsFromCli(options = {}) {
356
623
  // src/commands/run.ts
357
624
  var REPORTERS = ["text", "json", "junit"];
358
625
  function registerRunCommand(program) {
359
- 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) => {
360
- const dir = path6.resolve(opts.workspace ?? process.cwd());
626
+ 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(
627
+ "--workspace-name <name-or-id>",
628
+ "Registry workspace name (case-insensitive) or id. Defaults to the active workspace."
629
+ ).option(
630
+ "--workspace-path <dir>",
631
+ "Filesystem directory containing workspace.synced.json (skips the registry)."
632
+ ).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) => {
633
+ let dir;
634
+ try {
635
+ const resolved = await resolveWorkspace({
636
+ name: opts.workspaceName,
637
+ path: opts.workspacePath,
638
+ expectExists: false
639
+ });
640
+ dir = resolved.dir;
641
+ if (resolved.fromRegistry) {
642
+ process.stderr.write(
643
+ `${import_kleur4.default.dim("workspace")}: ${import_kleur4.default.cyan(resolved.name ?? resolved.id ?? "")} ${import_kleur4.default.dim(`(${dir})`)}
644
+ `
645
+ );
646
+ }
647
+ } catch (err) {
648
+ if (err instanceof WorkspaceResolutionError) {
649
+ fail(err.message);
650
+ return;
651
+ }
652
+ throw err;
653
+ }
361
654
  const reporter = opts.reporter ?? "text";
362
655
  if (!isReporter(reporter)) {
363
656
  fail(`unknown --reporter "${reporter}" (expected: ${REPORTERS.join(", ")})`);
364
657
  return;
365
658
  }
366
- const state = await (0, import_file_backed3.loadFromFile)(dir, { allowMissing: true });
659
+ const state = await (0, import_file_backed4.loadFromFile)(dir, { allowMissing: true });
367
660
  if (!state) {
368
661
  fail(`no workspace found at ${dir} (expected workspace.synced.json)`);
369
662
  return;
@@ -423,7 +716,7 @@ function registerRunCommand(program) {
423
716
  process.off("SIGINT", onSigint);
424
717
  const aborted = controller.signal.aborted;
425
718
  const saved = opts.save !== false;
426
- if (saved) await (0, import_file_backed3.saveToFile)(dir, result.nextState);
719
+ if (saved) await (0, import_file_backed4.saveToFile)(dir, result.nextState);
427
720
  if (reporter === "json") {
428
721
  process.stdout.write(
429
722
  JSON.stringify(
@@ -449,7 +742,7 @@ function resolveActor(local, override) {
449
742
  const login = local.sessions.github.workspace?.accountLogin;
450
743
  if (login) return { kind: "github", name: login };
451
744
  try {
452
- const username = os.userInfo().username;
745
+ const username = os2.userInfo().username;
453
746
  if (username) return { kind: "os", name: username };
454
747
  } catch {
455
748
  }
@@ -615,6 +908,117 @@ function fail(message, code = 2, kind = "error") {
615
908
  process.exitCode = code;
616
909
  }
617
910
 
911
+ // src/commands/workspaces.ts
912
+ var import_kleur5 = __toESM(require("kleur"), 1);
913
+ var import_registry2 = require("@apicircle/core/workspace/registry");
914
+ function registerWorkspacesCommand(program) {
915
+ const ws = program.command("workspaces").description("List, create, or switch the active workspace");
916
+ ws.command("list").description("List every workspace registered on this machine").option("--json", "Emit JSON instead of a formatted table").action(async (opts) => {
917
+ const { registry, root } = await listWorkspacesOnDisk();
918
+ if (opts.json) {
919
+ process.stdout.write(JSON.stringify({ root, registry }, null, 2) + "\n");
920
+ return;
921
+ }
922
+ if (registry.workspaces.length === 0) {
923
+ process.stdout.write(
924
+ `${import_kleur5.default.dim("No workspaces registered yet at")} ${root}
925
+ ${import_kleur5.default.dim("Run")} ${import_kleur5.default.cyan("apicircle workspaces create <name>")} ${import_kleur5.default.dim(
926
+ "or open the desktop app to seed one."
927
+ )}
928
+ `
929
+ );
930
+ return;
931
+ }
932
+ process.stdout.write(`${import_kleur5.default.dim("registry")}: ${root}
933
+
934
+ `);
935
+ const rows = [...registry.workspaces].sort(
936
+ (a, b) => b.lastOpenedAt.localeCompare(a.lastOpenedAt)
937
+ );
938
+ const nameWidth = Math.max(4, ...rows.map((r) => r.name.length));
939
+ const idWidth = Math.max(2, ...rows.map((r) => r.id.length));
940
+ process.stdout.write(
941
+ import_kleur5.default.bold(
942
+ ` ${"".padEnd(1)} ${"NAME".padEnd(nameWidth)} ${"ID".padEnd(idWidth)} LAST OPENED
943
+ `
944
+ )
945
+ );
946
+ for (const w of rows) {
947
+ const mark = w.id === registry.activeWorkspaceId ? import_kleur5.default.green("\u25CF") : " ";
948
+ process.stdout.write(
949
+ ` ${mark} ${w.name.padEnd(nameWidth)} ${import_kleur5.default.dim(
950
+ w.id.padEnd(idWidth)
951
+ )} ${import_kleur5.default.dim(w.lastOpenedAt)}
952
+ `
953
+ );
954
+ }
955
+ process.stdout.write(`
956
+ ${import_kleur5.default.dim("\u25CF = active")}
957
+ `);
958
+ });
959
+ 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) => {
960
+ try {
961
+ const { entry: entry2, dir, registry } = await createWorkspaceOnDisk({
962
+ name,
963
+ sampleRequest: opts.sample ?? false
964
+ });
965
+ process.stdout.write(
966
+ `${import_kleur5.default.green("created")} workspace ${import_kleur5.default.cyan(entry2.name)} ${import_kleur5.default.dim(`(${entry2.id})`)}
967
+ at ${dir}
968
+ `
969
+ );
970
+ if (registry.activeWorkspaceId === entry2.id) {
971
+ process.stdout.write(`${import_kleur5.default.dim("marked as active")}
972
+ `);
973
+ }
974
+ } catch (err) {
975
+ process.stderr.write(
976
+ `${import_kleur5.default.red("error")}: ${err instanceof Error ? err.message : String(err)}
977
+ `
978
+ );
979
+ process.exit(2);
980
+ }
981
+ });
982
+ ws.command("use").description("Set the active workspace by id or name").argument("<selector>", "Workspace id or name").action(async (selector) => {
983
+ const { registry, root } = await listWorkspacesOnDisk();
984
+ const entry2 = (0, import_registry2.findWorkspaceEntry)(registry, selector);
985
+ if (!entry2) {
986
+ process.stderr.write(
987
+ `${import_kleur5.default.red("error")}: no workspace named "${selector}" in the registry at ${root}.
988
+ ${import_kleur5.default.dim("Run")} ${import_kleur5.default.cyan("apicircle workspaces list")} ${import_kleur5.default.dim("to see what is available.")}
989
+ `
990
+ );
991
+ process.exit(2);
992
+ return;
993
+ }
994
+ const next = await (0, import_registry2.setActiveWorkspace)(root, entry2.id);
995
+ void next;
996
+ process.stdout.write(
997
+ `${import_kleur5.default.green("active")} workspace is now ${import_kleur5.default.cyan(entry2.name)} ${import_kleur5.default.dim(`(${entry2.id})`)}
998
+ `
999
+ );
1000
+ });
1001
+ 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) => {
1002
+ if (!selector) {
1003
+ process.stdout.write(defaultWorkspacesRoot() + "\n");
1004
+ return;
1005
+ }
1006
+ const { registry, root } = await listWorkspacesOnDisk();
1007
+ const entry2 = (0, import_registry2.findWorkspaceEntry)(registry, selector);
1008
+ if (!entry2) {
1009
+ process.stderr.write(
1010
+ `${import_kleur5.default.red("error")}: no workspace named "${selector}" in the registry at ${root}.
1011
+ `
1012
+ );
1013
+ process.exit(2);
1014
+ return;
1015
+ }
1016
+ void saveRegistryToDisk;
1017
+ const { workspaceDirFor: workspaceDirFor2 } = await import("@apicircle/core/workspace/registry");
1018
+ process.stdout.write(workspaceDirFor2(root, entry2.id) + "\n");
1019
+ });
1020
+ }
1021
+
618
1022
  // src/index.ts
619
1023
  function buildProgram() {
620
1024
  const program = new import_commander.Command();
@@ -623,6 +1027,7 @@ function buildProgram() {
623
1027
  registerMcpCommand(program);
624
1028
  registerImportCommand(program);
625
1029
  registerRunCommand(program);
1030
+ registerWorkspacesCommand(program);
626
1031
  return program;
627
1032
  }
628
1033
  async function runCli(argv = process.argv) {