@apicircle/cli 1.0.1 → 1.0.3
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/LICENSE +110 -110
- package/README.md +209 -39
- package/dist/index.cjs +429 -24
- package/dist/index.cjs.map +1 -1
- package/dist/index.js +434 -23
- package/dist/index.js.map +1 -1
- package/package.json +5 -5
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(
|
|
182
|
-
|
|
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=${
|
|
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
|
|
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
|
|
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
|
|
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(
|
|
211
|
-
|
|
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,
|
|
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((
|
|
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", () =>
|
|
561
|
+
process.stdin.on("end", () => resolve6(data));
|
|
294
562
|
process.stdin.on("error", reject);
|
|
295
563
|
});
|
|
296
564
|
}
|
|
297
|
-
return
|
|
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,
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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(
|
|
360
|
-
|
|
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,
|
|
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,
|
|
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 =
|
|
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) {
|