@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.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 API Circle MCP server (stdio transport)").option(
|
|
155
|
-
|
|
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=${
|
|
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
|
|
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
|
|
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
|
|
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(
|
|
188
|
-
|
|
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
|
|
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((
|
|
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", () =>
|
|
544
|
+
process.stdin.on("end", () => resolve6(data));
|
|
271
545
|
process.stdin.on("error", reject);
|
|
272
546
|
});
|
|
273
547
|
}
|
|
274
|
-
return
|
|
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:
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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(
|
|
342
|
-
|
|
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
|
|
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 =
|
|
733
|
+
const username = os2.userInfo().username;
|
|
435
734
|
if (username) return { kind: "os", name: username };
|
|
436
735
|
} catch {
|
|
437
736
|
}
|
|
@@ -597,6 +896,117 @@ 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();
|
|
@@ -605,6 +1015,7 @@ function buildProgram() {
|
|
|
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) {
|