@aidecisionops/decisionops 0.1.0

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.
Files changed (40) hide show
  1. package/LICENSE +201 -0
  2. package/README.md +161 -0
  3. package/dist/cli.js +950 -0
  4. package/dist/cli.js.map +1 -0
  5. package/dist/core/auth.js +422 -0
  6. package/dist/core/auth.js.map +1 -0
  7. package/dist/core/controlPlane.js +103 -0
  8. package/dist/core/controlPlane.js.map +1 -0
  9. package/dist/core/git.js +45 -0
  10. package/dist/core/git.js.map +1 -0
  11. package/dist/core/installer.js +408 -0
  12. package/dist/core/installer.js.map +1 -0
  13. package/dist/core/manifest.js +34 -0
  14. package/dist/core/manifest.js.map +1 -0
  15. package/dist/core/platformCatalog.js +88 -0
  16. package/dist/core/platformCatalog.js.map +1 -0
  17. package/dist/core/platforms.js +88 -0
  18. package/dist/core/platforms.js.map +1 -0
  19. package/dist/core/runtime.js +34 -0
  20. package/dist/core/runtime.js.map +1 -0
  21. package/dist/generate-platform-catalog.js +10 -0
  22. package/dist/generate-platform-catalog.js.map +1 -0
  23. package/dist/platform-catalog.json +145 -0
  24. package/dist/ui/prompts.js +138 -0
  25. package/dist/ui/prompts.js.map +1 -0
  26. package/package.json +67 -0
  27. package/platforms/README.md +133 -0
  28. package/platforms/antigravity.toml +29 -0
  29. package/platforms/claude-code.toml +30 -0
  30. package/platforms/codex.toml +31 -0
  31. package/platforms/cursor.toml +30 -0
  32. package/platforms/vscode.toml +26 -0
  33. package/skills/decision-ops/SKILL.md +88 -0
  34. package/skills/decision-ops/agents/openai.yaml +4 -0
  35. package/skills/decision-ops/evals/evals.json +87 -0
  36. package/skills/decision-ops/evals/trigger-queries.json +108 -0
  37. package/skills/decision-ops/references/decision-ops-manifest.md +35 -0
  38. package/skills/decision-ops/references/decision-register-format.md +36 -0
  39. package/skills/decision-ops/references/mcp-interface.md +100 -0
  40. package/skills/decision-ops/scripts/read-manifest.sh +65 -0
package/dist/cli.js ADDED
@@ -0,0 +1,950 @@
1
+ #!/usr/bin/env node
2
+ import fs from "node:fs";
3
+ import path from "node:path";
4
+ import process from "node:process";
5
+ import { Command } from "commander";
6
+ import { z } from "zod";
7
+ import { clearAuthState, defaultApiBaseUrl, defaultClientId, defaultScopes, ensureValidAuthState, isExpired, loginWithPkce, readAuthState, revokeAuthState, saveTokenAuthState, } from "./core/auth.js";
8
+ import { DecisionOpsApiError, loadProjectRepositories, loadUserContext } from "./core/controlPlane.js";
9
+ import { inferDefaultBranch, inferRepoRef, resolveRepoPath } from "./core/git.js";
10
+ import { buildPlatforms, cleanupPlatforms, installPlatforms } from "./core/installer.js";
11
+ import { readManifest, writeManifest } from "./core/manifest.js";
12
+ import { loadPlatforms, resolveInstallPath } from "./core/platforms.js";
13
+ import { DEFAULT_MCP_SERVER_NAME, DEFAULT_MCP_SERVER_URL, DEFAULT_OUTPUT_DIR, DEFAULT_PLATFORMS_DIR, DEFAULT_SKILL_NAME, DEFAULT_SOURCE_DIR, PLACEHOLDER_ORG_ID, PLACEHOLDER_PROJECT_ID, PLACEHOLDER_REPO_REF, } from "./core/runtime.js";
14
+ import { promptConfirm, promptSelect, promptText } from "./ui/prompts.js";
15
+ const program = new Command();
16
+ program
17
+ .name("decisionops")
18
+ .description("Decision Ops CLI")
19
+ .version("0.1.0");
20
+ function isInteractive() {
21
+ return Boolean(process.stdin.isTTY && process.stdout.isTTY);
22
+ }
23
+ function parseScopes(raw) {
24
+ if (!raw) {
25
+ return undefined;
26
+ }
27
+ return raw
28
+ .split(/[,\s]+/)
29
+ .map((value) => value.trim())
30
+ .filter(Boolean);
31
+ }
32
+ function authDisplay(auth) {
33
+ const identity = auth.user?.email || auth.user?.name || auth.user?.id || "unknown";
34
+ const expiry = auth.expiresAt ? `${auth.expiresAt}${isExpired(auth) ? " (expired)" : ""}` : "session";
35
+ return `${identity} via ${auth.method} • ${expiry}`;
36
+ }
37
+ function normalizeRepoRef(value) {
38
+ let normalized = value.trim().replace(/\/+$/, "").replace(/\.git$/i, "");
39
+ for (const prefix of ["git@github.com:", "https://github.com/", "http://github.com/", "ssh://git@github.com/"]) {
40
+ if (normalized.startsWith(prefix)) {
41
+ normalized = normalized.slice(prefix.length);
42
+ break;
43
+ }
44
+ }
45
+ return normalized;
46
+ }
47
+ function inferRepoId(repoRef) {
48
+ if (!repoRef) {
49
+ return undefined;
50
+ }
51
+ const normalized = normalizeRepoRef(repoRef);
52
+ return /^[^/\s]+\/[^/\s]+$/.test(normalized) ? normalized : undefined;
53
+ }
54
+ function detectRepoRef(repoPath) {
55
+ try {
56
+ return normalizeRepoRef(inferRepoRef(repoPath));
57
+ }
58
+ catch {
59
+ return undefined;
60
+ }
61
+ }
62
+ function normalizeApiBaseUrl(value) {
63
+ return String(value ?? process.env.DECISIONOPS_API_BASE_URL ?? defaultApiBaseUrl())
64
+ .trim()
65
+ .replace(/\/+$/, "");
66
+ }
67
+ async function runLogin(flags) {
68
+ const scopes = parseScopes(flags.scopes);
69
+ const authOptions = {
70
+ apiBaseUrl: flags.apiBaseUrl,
71
+ issuerUrl: flags.issuerUrl,
72
+ clientId: flags.clientId,
73
+ audience: flags.audience,
74
+ scopes,
75
+ };
76
+ if (flags.clear) {
77
+ const current = readAuthState();
78
+ if (current) {
79
+ await revokeAuthState(current);
80
+ }
81
+ clearAuthState();
82
+ console.log("Cleared saved auth state.");
83
+ return;
84
+ }
85
+ if (flags.withToken) {
86
+ const token = flags.token ?? (isInteractive()
87
+ ? await promptText({
88
+ title: "Paste your Decision Ops access token",
89
+ chrome: {
90
+ eyebrow: "Auth",
91
+ description: "Use this fallback for automation or environments where browser OAuth login is unavailable.",
92
+ },
93
+ placeholder: "dop_...",
94
+ secret: true,
95
+ validate: (value) => (value.length > 0 ? null : "Token is required."),
96
+ })
97
+ : "");
98
+ if (!token) {
99
+ throw new Error("Token is required. Pass --token in non-interactive mode.");
100
+ }
101
+ const result = saveTokenAuthState({ ...authOptions, token });
102
+ console.log(`Saved auth token -> ${result.storagePath}`);
103
+ return;
104
+ }
105
+ let method;
106
+ if (flags.web) {
107
+ method = "web";
108
+ }
109
+ else if (!isInteractive()) {
110
+ throw new Error("Choose --web or --with-token in non-interactive mode.");
111
+ }
112
+ else {
113
+ method = await promptSelect("Choose a login method", [
114
+ {
115
+ label: "Browser login",
116
+ value: "web",
117
+ description: "Open the DecisionOps sign-in page in your browser and complete OAuth there.",
118
+ },
119
+ {
120
+ label: "Paste token",
121
+ value: "token",
122
+ description: "Fallback for automation or environments without interactive OAuth support.",
123
+ },
124
+ ], {
125
+ eyebrow: "Auth",
126
+ description: "DecisionOps uses OAuth for CLI sessions. Browser login is the primary path.",
127
+ footer: `Client: ${flags.clientId ?? defaultClientId()} • Scopes: ${(scopes ?? defaultScopes()).join(" ")}`,
128
+ });
129
+ if (method === "token") {
130
+ const result = saveTokenAuthState({
131
+ ...authOptions,
132
+ token: await promptText({
133
+ title: "Paste your Decision Ops access token",
134
+ chrome: {
135
+ eyebrow: "Auth",
136
+ description: "Use this fallback for automation or environments where browser OAuth login is unavailable.",
137
+ },
138
+ placeholder: "dop_...",
139
+ secret: true,
140
+ validate: (value) => (value.length > 0 ? null : "Token is required."),
141
+ }),
142
+ });
143
+ console.log(`Saved auth token -> ${result.storagePath}`);
144
+ return;
145
+ }
146
+ }
147
+ if (method === "web") {
148
+ const result = await loginWithPkce({
149
+ ...authOptions,
150
+ openBrowser: !(flags.noBrowser ?? false),
151
+ onAuthorizeUrl: (url) => {
152
+ if (flags.noBrowser) {
153
+ console.log("Open this URL in your browser to continue authentication:");
154
+ console.log(url);
155
+ }
156
+ },
157
+ });
158
+ if (!result.openedBrowser) {
159
+ if (!flags.noBrowser && result.authorizationUrl) {
160
+ console.log("Open this URL in your browser to continue authentication:");
161
+ console.log(result.authorizationUrl);
162
+ }
163
+ }
164
+ else {
165
+ console.log("Browser login started.");
166
+ }
167
+ console.log(`Saved session -> ${result.storagePath}`);
168
+ console.log(`Authenticated as ${authDisplay(result.state)}`);
169
+ return;
170
+ }
171
+ }
172
+ function printInstallResult(result) {
173
+ console.log("");
174
+ console.log("=== Install summary ===");
175
+ console.log("");
176
+ if (result.manifestPath) {
177
+ console.log(result.placeholdersUsed
178
+ ? ` Manifest (placeholder): ${result.manifestPath}`
179
+ : ` Manifest: ${result.manifestPath}`);
180
+ }
181
+ for (const entry of result.installedSkills) {
182
+ console.log(` Skill installed: ${entry.target} (${entry.platformId})`);
183
+ }
184
+ for (const entry of result.installedMcp) {
185
+ console.log(` MCP config written: ${entry.target} (${entry.platformId})`);
186
+ }
187
+ for (const entry of result.skippedMcp) {
188
+ console.log(` MCP config skipped: ${entry.platformId} — ${entry.reason}`);
189
+ }
190
+ if (result.installedMcp.length > 0 || result.installedSkills.length > 0) {
191
+ console.log("");
192
+ console.log("=== Next steps (complete these in your IDE) ===");
193
+ console.log("");
194
+ console.log("The CLI wrote config files to disk, but your IDE needs to pick them up.");
195
+ console.log("");
196
+ console.log(" 1. Open (or restart) your IDE in the target repository.");
197
+ for (const entry of result.installedMcp) {
198
+ console.log(` → ${entry.platformId}: verify MCP server at ${entry.target}`);
199
+ }
200
+ console.log(" 2. Invoke any DecisionOps MCP tool once to trigger the auth handoff.");
201
+ console.log(" 3. Complete the sign-in flow prompted by the MCP server.");
202
+ console.log(" 4. Retry the same tool call — you're live.");
203
+ console.log("");
204
+ console.log("Note: `decisionops login` authenticates CLI commands only.");
205
+ console.log("IDE MCP auth is a separate session completed from inside the IDE.");
206
+ }
207
+ if (result.authHandoffPath) {
208
+ console.log("");
209
+ console.log(`Detailed auth handoff steps: ${result.authHandoffPath}`);
210
+ }
211
+ console.log("");
212
+ }
213
+ function printCleanupResult(result) {
214
+ console.log("");
215
+ console.log("=== Cleanup summary ===");
216
+ console.log("");
217
+ for (const entry of result.removedSkills) {
218
+ console.log(` Skill removed: ${entry.target} (${entry.platformId})`);
219
+ }
220
+ for (const entry of result.skippedSkills) {
221
+ console.log(` Skill skipped: ${entry.platformId} — ${entry.reason}`);
222
+ }
223
+ for (const entry of result.removedMcp) {
224
+ console.log(` MCP config removed: ${entry.target} (${entry.platformId})`);
225
+ }
226
+ for (const entry of result.skippedMcp) {
227
+ console.log(` MCP config skipped: ${entry.platformId} — ${entry.reason}`);
228
+ }
229
+ if (result.removedManifestPath) {
230
+ console.log(` Manifest removed: ${result.removedManifestPath}`);
231
+ }
232
+ if (result.removedAuthHandoffPath) {
233
+ console.log(` Auth handoff removed: ${result.removedAuthHandoffPath}`);
234
+ }
235
+ if (result.removedMcp.length > 0) {
236
+ console.log("");
237
+ console.log("Restart your IDE to stop using the removed MCP server.");
238
+ }
239
+ console.log("");
240
+ }
241
+ function addInstallFlags(command) {
242
+ return command
243
+ .option("-p, --platform <id>", "Select a platform to install", collectValues, [])
244
+ .option("--repo-path <path>")
245
+ .option("--api-base-url <url>", "Decision Ops API base URL", defaultApiBaseUrl())
246
+ .option("--org-id <orgId>")
247
+ .option("--project-id <projectId>")
248
+ .option("--repo-ref <repoRef>")
249
+ .option("--repo-id <repoId>")
250
+ .option("--default-branch <branch>")
251
+ .option("--user-session-token <token>", "Decision Ops user session token for interactive org/project discovery")
252
+ .option("--allow-placeholders", "Allow placeholder manifest values for local prototyping")
253
+ .option("--skip-manifest", "Do not write .decisionops/manifest.toml")
254
+ .option("--skip-skill", "Do not install skill bundles")
255
+ .option("--skip-mcp", "Do not install MCP config")
256
+ .option("--output-dir <path>", "Build output directory", DEFAULT_OUTPUT_DIR)
257
+ .option("--source-dir <path>", "Skill source directory", DEFAULT_SOURCE_DIR)
258
+ .option("--skill-name <name>", "Skill bundle name", DEFAULT_SKILL_NAME)
259
+ .option("--server-name <name>", "MCP server name", DEFAULT_MCP_SERVER_NAME)
260
+ .option("--server-url <url>", "MCP server URL", DEFAULT_MCP_SERVER_URL)
261
+ .option("-y, --yes", "Accept interactive defaults");
262
+ }
263
+ function collectValues(value, previous) {
264
+ previous.push(value);
265
+ return previous;
266
+ }
267
+ async function choosePlatforms(initialPlatformIds, action = "install") {
268
+ if (initialPlatformIds && initialPlatformIds.length > 0) {
269
+ return initialPlatformIds;
270
+ }
271
+ if (!isInteractive()) {
272
+ throw new Error("No platform selected. Use --platform in non-interactive mode.");
273
+ }
274
+ const platforms = Object.values(loadPlatforms(DEFAULT_PLATFORMS_DIR));
275
+ const chosen = new Set();
276
+ let addAnother = true;
277
+ while (addAnother) {
278
+ const platformId = await promptSelect(action === "install" ? "Choose a platform to install" : "Choose a platform to clean up", platforms.map((platform) => ({
279
+ label: platform.display_name,
280
+ value: platform.id,
281
+ description: `Target id: ${platform.id}`,
282
+ })), {
283
+ eyebrow: action === "install" ? "Install" : "Cleanup",
284
+ description: action === "install"
285
+ ? "Select where DecisionOps should register its skill bundle and MCP configuration."
286
+ : "Select which platform integration should be removed from this machine/repository.",
287
+ footer: chosen.size > 0 ? `Already selected: ${[...chosen].join(", ")}` : undefined,
288
+ });
289
+ chosen.add(platformId);
290
+ const remaining = platforms.filter((platform) => !chosen.has(platform.id));
291
+ if (remaining.length === 0) {
292
+ break;
293
+ }
294
+ addAnother = await promptConfirm("Add another platform?", false, {
295
+ eyebrow: action === "install" ? "Install" : "Cleanup",
296
+ description: action === "install"
297
+ ? "Choose more than one platform if you want to register DecisionOps across multiple local agent runtimes."
298
+ : "Choose more than one platform if you want to remove DecisionOps integrations across multiple runtimes.",
299
+ });
300
+ }
301
+ return [...chosen];
302
+ }
303
+ async function chooseManifestSetupMode(allowPlaceholders) {
304
+ const options = [
305
+ {
306
+ label: "Load from DecisionOps",
307
+ value: "remote",
308
+ description: "Browse organizations, projects, and project repositories using a DecisionOps user session token.",
309
+ },
310
+ {
311
+ label: "Enter IDs manually",
312
+ value: "manual",
313
+ description: "Paste org_id, project_id, and repo_ref yourself.",
314
+ },
315
+ ];
316
+ if (allowPlaceholders) {
317
+ options.push({
318
+ label: "Use placeholders",
319
+ value: "placeholder",
320
+ description: "Write sample values for local prototyping only.",
321
+ });
322
+ }
323
+ return await promptSelect("Choose manifest setup mode", options, {
324
+ eyebrow: "Init",
325
+ description: "Select where the repository binding values should come from.",
326
+ });
327
+ }
328
+ async function resolveManualRepoBinding(repoPath, flags, detectedRepoRef) {
329
+ if (flags.repoRef) {
330
+ const repoRef = normalizeRepoRef(flags.repoRef);
331
+ return { repoRef, repoId: flags.repoId ?? inferRepoId(repoRef) };
332
+ }
333
+ if (!isInteractive()) {
334
+ if (detectedRepoRef) {
335
+ return { repoRef: detectedRepoRef, repoId: flags.repoId ?? inferRepoId(detectedRepoRef) };
336
+ }
337
+ if (flags.allowPlaceholders ?? false) {
338
+ return { repoRef: PLACEHOLDER_REPO_REF, repoId: flags.repoId ?? PLACEHOLDER_REPO_REF };
339
+ }
340
+ throw new Error("Could not infer repo_ref from git remote. Pass --repo-ref or use --allow-placeholders for local prototyping.");
341
+ }
342
+ if (detectedRepoRef) {
343
+ const useDetected = await promptConfirm(`Use detected repo_ref ${detectedRepoRef}?`, true, {
344
+ eyebrow: "Init",
345
+ description: `Detected from git remote for ${repoPath}. Choose No to enter a different repository binding.`,
346
+ });
347
+ if (useDetected) {
348
+ return { repoRef: detectedRepoRef, repoId: flags.repoId ?? inferRepoId(detectedRepoRef) };
349
+ }
350
+ }
351
+ const repoRef = normalizeRepoRef(await promptText({
352
+ title: "Decision Ops repo_ref",
353
+ chrome: {
354
+ eyebrow: "Init",
355
+ description: "Choose the repository reference this manifest should bind to.",
356
+ footer: detectedRepoRef ? `Detected from git remote: ${detectedRepoRef}` : undefined,
357
+ },
358
+ defaultValue: detectedRepoRef,
359
+ placeholder: (flags.allowPlaceholders ?? false) ? PLACEHOLDER_REPO_REF : "owner/repo",
360
+ validate: (value) => (value.trim().length > 0 ? null : "repo_ref is required."),
361
+ }));
362
+ return { repoRef, repoId: flags.repoId ?? inferRepoId(repoRef) };
363
+ }
364
+ async function resolveRemoteRepoBinding(options) {
365
+ const { detectedRepoRef, flags, projectName, repositories } = options;
366
+ if (flags.repoRef) {
367
+ const repoRef = normalizeRepoRef(flags.repoRef);
368
+ return { repoRef, repoId: flags.repoId ?? inferRepoId(repoRef) };
369
+ }
370
+ if (!isInteractive()) {
371
+ if (detectedRepoRef) {
372
+ return { repoRef: detectedRepoRef, repoId: flags.repoId ?? inferRepoId(detectedRepoRef) };
373
+ }
374
+ if (repositories.length === 1) {
375
+ const repoRef = normalizeRepoRef(repositories[0] ?? "");
376
+ return { repoRef, repoId: flags.repoId ?? inferRepoId(repoRef) };
377
+ }
378
+ if (flags.allowPlaceholders ?? false) {
379
+ return { repoRef: PLACEHOLDER_REPO_REF, repoId: flags.repoId ?? PLACEHOLDER_REPO_REF };
380
+ }
381
+ throw new Error("--repo-ref is required when no single repository binding can be inferred. Pass --repo-ref or use --allow-placeholders for local prototyping.");
382
+ }
383
+ const optionsList = [];
384
+ const seen = new Set();
385
+ if (detectedRepoRef) {
386
+ seen.add(detectedRepoRef);
387
+ optionsList.push({
388
+ label: `Detected current repo (${detectedRepoRef})`,
389
+ value: { kind: "repo", repoRef: detectedRepoRef },
390
+ description: repositories.includes(detectedRepoRef)
391
+ ? "Detected from git remote and already assigned to the selected project."
392
+ : "Detected from git remote for the current checkout.",
393
+ });
394
+ }
395
+ for (const repository of repositories) {
396
+ const repoRef = normalizeRepoRef(repository);
397
+ if (!repoRef || seen.has(repoRef)) {
398
+ continue;
399
+ }
400
+ seen.add(repoRef);
401
+ optionsList.push({
402
+ label: repoRef,
403
+ value: { kind: "repo", repoRef },
404
+ description: `Already assigned to ${projectName} in DecisionOps.`,
405
+ });
406
+ }
407
+ optionsList.push({
408
+ label: "Enter repo_ref manually",
409
+ value: { kind: "manual" },
410
+ description: "Use a repository binding that is not currently mapped to the selected project.",
411
+ });
412
+ if (flags.allowPlaceholders ?? false) {
413
+ optionsList.push({
414
+ label: "Use placeholder repo_ref",
415
+ value: { kind: "placeholder" },
416
+ description: "For local prototyping only.",
417
+ });
418
+ }
419
+ const choice = await promptSelect("Choose the repository binding", optionsList, {
420
+ eyebrow: "Init",
421
+ description: repositories.length > 0
422
+ ? `${projectName} already has ${repositories.length} repository binding${repositories.length === 1 ? "" : "s"} in DecisionOps.`
423
+ : `${projectName} does not have any repositories assigned in DecisionOps yet.`,
424
+ });
425
+ if (choice.kind === "repo") {
426
+ return { repoRef: choice.repoRef, repoId: flags.repoId ?? inferRepoId(choice.repoRef) };
427
+ }
428
+ if (choice.kind === "placeholder") {
429
+ return { repoRef: PLACEHOLDER_REPO_REF, repoId: flags.repoId ?? PLACEHOLDER_REPO_REF };
430
+ }
431
+ const repoRef = normalizeRepoRef(await promptText({
432
+ title: "Decision Ops repo_ref",
433
+ chrome: {
434
+ eyebrow: "Init",
435
+ description: `Enter the repository reference to bind to ${projectName}.`,
436
+ footer: detectedRepoRef ? `Detected from git remote: ${detectedRepoRef}` : undefined,
437
+ },
438
+ defaultValue: detectedRepoRef,
439
+ placeholder: "owner/repo",
440
+ validate: (value) => (value.trim().length > 0 ? null : "repo_ref is required."),
441
+ }));
442
+ return { repoRef, repoId: flags.repoId ?? inferRepoId(repoRef) };
443
+ }
444
+ async function collectRemoteManifestValues(repoPath, flags, detectedRepoRef, defaultBranch) {
445
+ const apiBaseUrl = normalizeApiBaseUrl(flags.apiBaseUrl);
446
+ const userSessionToken = (flags.userSessionToken ?? process.env.DECISIONOPS_USER_SESSION_TOKEN ?? "").trim() || await promptText({
447
+ title: "Decision Ops user session token",
448
+ chrome: {
449
+ eyebrow: "Init",
450
+ description: "Paste a DecisionOps user session token to browse your organizations, projects, and repositories.",
451
+ footer: "This uses /v1/auth/me and /v1/admin/projects/* on the DecisionOps API.",
452
+ },
453
+ placeholder: "session_...",
454
+ secret: true,
455
+ validate: (value) => (value.trim().length > 0 ? null : "A user session token is required."),
456
+ });
457
+ let context;
458
+ try {
459
+ context = await loadUserContext({ token: userSessionToken, apiBaseUrl });
460
+ }
461
+ catch (error) {
462
+ if (error instanceof DecisionOpsApiError && error.status === 401) {
463
+ throw new Error("DecisionOps rejected the user session token. Sign in via the dashboard and paste a valid session token, or pass --org-id/--project-id manually.");
464
+ }
465
+ throw error;
466
+ }
467
+ if (context.organizations.length === 0) {
468
+ throw new Error("The current DecisionOps user session is not a member of any organizations.");
469
+ }
470
+ const selectedOrganization = flags.orgId
471
+ ? context.organizations.find((organization) => organization.orgId === flags.orgId)
472
+ : context.organizations.length === 1
473
+ ? context.organizations[0]
474
+ : await promptSelect("Choose a DecisionOps organization", context.organizations.map((organization) => ({
475
+ label: organization.orgName || organization.orgId,
476
+ value: organization,
477
+ description: `${organization.orgId} • role: ${organization.role}`,
478
+ })), {
479
+ eyebrow: "Init",
480
+ description: `Bind ${repoPath} to one of your DecisionOps organizations.`,
481
+ });
482
+ if (!selectedOrganization) {
483
+ throw new Error(`Could not find organization ${flags.orgId} in the current DecisionOps session.`);
484
+ }
485
+ const orgContext = await loadUserContext({
486
+ token: userSessionToken,
487
+ orgId: selectedOrganization.orgId,
488
+ apiBaseUrl,
489
+ });
490
+ const activeProjects = orgContext.projects.filter((project) => project.status === "active");
491
+ if (activeProjects.length === 0) {
492
+ throw new Error(`Organization ${selectedOrganization.orgName || selectedOrganization.orgId} does not have any active projects.`);
493
+ }
494
+ const selectedProject = flags.projectId
495
+ ? activeProjects.find((project) => project.id === flags.projectId)
496
+ : activeProjects.length === 1
497
+ ? activeProjects[0]
498
+ : await promptSelect("Choose a DecisionOps project", activeProjects.map((project) => ({
499
+ label: project.name,
500
+ value: project,
501
+ description: `${project.id}${project.repoCount > 0 ? ` • ${project.repoCount} repos` : ""}${project.isDefault ? " • default" : ""}`,
502
+ })), {
503
+ eyebrow: "Init",
504
+ description: `Select the project inside ${selectedOrganization.orgName || selectedOrganization.orgId}.`,
505
+ });
506
+ if (!selectedProject) {
507
+ throw new Error(`Could not find project ${flags.projectId} in organization ${selectedOrganization.orgId}.`);
508
+ }
509
+ const projectRepositories = await loadProjectRepositories({
510
+ token: userSessionToken,
511
+ orgId: selectedOrganization.orgId,
512
+ projectId: selectedProject.id,
513
+ apiBaseUrl,
514
+ });
515
+ const repoBinding = await resolveRemoteRepoBinding({
516
+ repoPath,
517
+ detectedRepoRef,
518
+ flags,
519
+ projectName: selectedProject.name,
520
+ repositories: projectRepositories.repositories,
521
+ });
522
+ return {
523
+ orgId: selectedOrganization.orgId,
524
+ projectId: selectedProject.id,
525
+ repoRef: repoBinding.repoRef,
526
+ repoId: flags.repoId ?? repoBinding.repoId,
527
+ defaultBranch,
528
+ };
529
+ }
530
+ async function collectManifestValues(repoPath, flags) {
531
+ const allowPlaceholders = flags.allowPlaceholders ?? false;
532
+ const detectedRepoRef = flags.repoRef ? normalizeRepoRef(flags.repoRef) : detectRepoRef(repoPath);
533
+ const defaultBranch = flags.defaultBranch ?? inferDefaultBranch(repoPath);
534
+ if (flags.orgId && flags.projectId) {
535
+ const repoBinding = await resolveManualRepoBinding(repoPath, flags, detectedRepoRef);
536
+ return {
537
+ orgId: flags.orgId,
538
+ projectId: flags.projectId,
539
+ repoRef: repoBinding.repoRef,
540
+ repoId: flags.repoId ?? repoBinding.repoId,
541
+ defaultBranch,
542
+ };
543
+ }
544
+ if (!isInteractive()) {
545
+ if (!allowPlaceholders) {
546
+ throw new Error("--org-id and --project-id are required when writing a manifest. Pass explicit values, provide --user-session-token in interactive mode, or use --allow-placeholders for local prototyping.");
547
+ }
548
+ return {
549
+ orgId: flags.orgId ?? PLACEHOLDER_ORG_ID,
550
+ projectId: flags.projectId ?? PLACEHOLDER_PROJECT_ID,
551
+ repoRef: detectedRepoRef ?? PLACEHOLDER_REPO_REF,
552
+ repoId: flags.repoId ?? inferRepoId(detectedRepoRef) ?? PLACEHOLDER_REPO_REF,
553
+ defaultBranch,
554
+ };
555
+ }
556
+ const setupMode = await chooseManifestSetupMode(allowPlaceholders);
557
+ if (setupMode === "remote") {
558
+ return await collectRemoteManifestValues(repoPath, flags, detectedRepoRef, defaultBranch);
559
+ }
560
+ if (setupMode === "placeholder") {
561
+ return {
562
+ orgId: PLACEHOLDER_ORG_ID,
563
+ projectId: PLACEHOLDER_PROJECT_ID,
564
+ repoRef: detectedRepoRef ?? PLACEHOLDER_REPO_REF,
565
+ repoId: flags.repoId ?? inferRepoId(detectedRepoRef) ?? PLACEHOLDER_REPO_REF,
566
+ defaultBranch,
567
+ };
568
+ }
569
+ const repoBinding = await resolveManualRepoBinding(repoPath, flags, detectedRepoRef);
570
+ const orgId = flags.orgId ?? await promptText({
571
+ title: "Decision Ops org_id",
572
+ chrome: {
573
+ eyebrow: "Init",
574
+ description: `Bind repository ${repoPath} to an existing DecisionOps organization.`,
575
+ footer: `repo_ref: ${repoBinding.repoRef} • default branch: ${defaultBranch}`,
576
+ },
577
+ placeholder: allowPlaceholders ? PLACEHOLDER_ORG_ID : undefined,
578
+ validate: (value) => (value.length > 0 ? null : "org_id is required."),
579
+ });
580
+ const projectId = flags.projectId ?? await promptText({
581
+ title: "Decision Ops project_id",
582
+ chrome: {
583
+ eyebrow: "Init",
584
+ description: "Choose the project this repository should publish decisions into.",
585
+ footer: `repo_ref: ${repoBinding.repoRef} • default branch: ${defaultBranch}`,
586
+ },
587
+ placeholder: allowPlaceholders ? PLACEHOLDER_PROJECT_ID : undefined,
588
+ validate: (value) => (value.length > 0 ? null : "project_id is required."),
589
+ });
590
+ return {
591
+ orgId,
592
+ projectId,
593
+ repoRef: repoBinding.repoRef,
594
+ repoId: flags.repoId ?? repoBinding.repoId,
595
+ defaultBranch,
596
+ };
597
+ }
598
+ program
599
+ .command("login")
600
+ .description("Authenticate this machine with DecisionOps")
601
+ .option("--api-base-url <url>", "Decision Ops API base URL", defaultApiBaseUrl())
602
+ .option("--issuer-url <url>", "OAuth issuer base URL")
603
+ .option("--client-id <id>", "OAuth client id", defaultClientId())
604
+ .option("--audience <value>", "OAuth audience")
605
+ .option("--scopes <list>", "Comma or space separated OAuth scopes", defaultScopes().join(" "))
606
+ .option("--web", "Use browser-based PKCE login")
607
+ .option("--with-token", "Save a raw access token instead of using OAuth")
608
+ .option("--token <token>", "Access token value for --with-token")
609
+ .option("--no-browser", "Do not attempt to launch a browser automatically")
610
+ .option("--clear", "Remove saved login state")
611
+ .action(async (flags) => {
612
+ await runLogin(flags);
613
+ });
614
+ program
615
+ .command("logout")
616
+ .description("Revoke and remove the local DecisionOps session")
617
+ .action(async () => {
618
+ const current = readAuthState();
619
+ if (!current) {
620
+ console.log("No DecisionOps session is stored locally.");
621
+ return;
622
+ }
623
+ await revokeAuthState(current);
624
+ clearAuthState();
625
+ console.log("Logged out and removed the local session.");
626
+ });
627
+ const authCommand = program.command("auth").description("Inspect or manage the current DecisionOps auth session");
628
+ authCommand
629
+ .command("status")
630
+ .description("Show the current auth session")
631
+ .action(async () => {
632
+ const current = readAuthState();
633
+ if (!current) {
634
+ console.log("Auth: missing");
635
+ process.exitCode = 1;
636
+ return;
637
+ }
638
+ const auth = await ensureValidAuthState(current);
639
+ console.log(`Auth: configured`);
640
+ console.log(`API base URL: ${auth.apiBaseUrl}`);
641
+ console.log(`Issuer URL: ${auth.issuerUrl}`);
642
+ console.log(`Client ID: ${auth.clientId}`);
643
+ console.log(`Method: ${auth.method}`);
644
+ console.log(`Scopes: ${auth.scopes.join(" ")}`);
645
+ console.log(`Access token: ${"*".repeat(Math.min(8, auth.accessToken.length))}…`);
646
+ console.log(`Expires: ${auth.expiresAt ?? "session"}`);
647
+ if (auth.user?.email || auth.user?.name || auth.user?.id) {
648
+ console.log(`User: ${auth.user?.email ?? auth.user?.name ?? auth.user?.id}`);
649
+ }
650
+ });
651
+ program
652
+ .command("init")
653
+ .description("Bind the current repository to a Decision Ops project")
654
+ .option("--repo-path <path>")
655
+ .option("--api-base-url <url>", "Decision Ops API base URL", defaultApiBaseUrl())
656
+ .option("--org-id <orgId>")
657
+ .option("--project-id <projectId>")
658
+ .option("--repo-ref <repoRef>")
659
+ .option("--repo-id <repoId>")
660
+ .option("--default-branch <branch>")
661
+ .option("--user-session-token <token>", "Decision Ops user session token for interactive org/project discovery")
662
+ .option("--allow-placeholders", "Allow placeholder manifest values for local prototyping")
663
+ .option("--server-name <name>", "MCP server name", DEFAULT_MCP_SERVER_NAME)
664
+ .option("--server-url <url>", "MCP server URL", DEFAULT_MCP_SERVER_URL)
665
+ .action(async (flags) => {
666
+ const repoPath = resolveRepoPath(flags.repoPath);
667
+ if (!repoPath) {
668
+ throw new Error("Could not determine repository path. Use --repo-path.");
669
+ }
670
+ const values = await collectManifestValues(repoPath, flags);
671
+ const manifestPath = writeManifest(repoPath, {
672
+ org_id: values.orgId,
673
+ project_id: values.projectId,
674
+ repo_ref: values.repoRef,
675
+ default_branch: values.defaultBranch,
676
+ mcp_server_name: flags.serverName ?? DEFAULT_MCP_SERVER_NAME,
677
+ mcp_server_url: flags.serverUrl ?? DEFAULT_MCP_SERVER_URL,
678
+ repo_id: values.repoId ?? flags.repoId,
679
+ });
680
+ console.log(`Wrote manifest: ${manifestPath}`);
681
+ });
682
+ addInstallFlags(program
683
+ .command("install")
684
+ .description("Install Decision Ops platform integrations and optionally bind the current repo")).action(async (flags) => {
685
+ const selectedPlatforms = await choosePlatforms(flags.platform);
686
+ const repoPath = resolveRepoPath(flags.repoPath);
687
+ let shouldWriteManifest = !(flags.skipManifest ?? false);
688
+ let orgId = flags.orgId;
689
+ let projectId = flags.projectId;
690
+ let repoRef = flags.repoRef;
691
+ let repoId = flags.repoId;
692
+ let defaultBranch = flags.defaultBranch;
693
+ if (!repoPath) {
694
+ shouldWriteManifest = false;
695
+ }
696
+ else if (!flags.yes && isInteractive() && !flags.repoPath && !flags.orgId && !flags.projectId && !(flags.skipManifest ?? false)) {
697
+ shouldWriteManifest = await promptConfirm("Also configure the current repository with a Decision Ops manifest?", true, {
698
+ eyebrow: "Install",
699
+ description: `Detected repository: ${repoPath}`,
700
+ footer: "This writes .decisionops/manifest.toml and keeps the install bound to the current repo.",
701
+ });
702
+ }
703
+ if (shouldWriteManifest && repoPath) {
704
+ const manifestValues = await collectManifestValues(repoPath, flags);
705
+ orgId = manifestValues.orgId;
706
+ projectId = manifestValues.projectId;
707
+ repoRef = manifestValues.repoRef;
708
+ repoId = manifestValues.repoId ?? repoId;
709
+ defaultBranch = manifestValues.defaultBranch;
710
+ }
711
+ const result = installPlatforms({
712
+ platformsDir: DEFAULT_PLATFORMS_DIR,
713
+ selectedPlatforms,
714
+ repoPath,
715
+ orgId,
716
+ projectId,
717
+ repoRef,
718
+ repoId,
719
+ defaultBranch,
720
+ installSkill: !(flags.skipSkill ?? false),
721
+ installMcp: !(flags.skipMcp ?? false),
722
+ writeManifest: shouldWriteManifest,
723
+ allowPlaceholders: flags.allowPlaceholders,
724
+ outputDir: flags.outputDir,
725
+ sourceDir: flags.sourceDir,
726
+ skillName: flags.skillName,
727
+ serverName: flags.serverName,
728
+ serverUrl: flags.serverUrl,
729
+ });
730
+ printInstallResult(result);
731
+ });
732
+ program
733
+ .command("cleanup")
734
+ .alias("uninstall")
735
+ .description("Remove installed Decision Ops skills, MCP entries, and local auth state")
736
+ .option("-p, --platform <id>", "Select a platform to clean up", collectValues, [])
737
+ .option("--repo-path <path>")
738
+ .option("--skill-name <name>", "Skill bundle name", DEFAULT_SKILL_NAME)
739
+ .option("--server-name <name>", "MCP server name", DEFAULT_MCP_SERVER_NAME)
740
+ .option("--skip-skill", "Do not remove installed skill bundles")
741
+ .option("--skip-mcp", "Do not remove MCP config entries")
742
+ .option("--skip-auth", "Do not remove local auth tokens")
743
+ .option("--remove-manifest", "Also remove .decisionops/manifest.toml from the repository")
744
+ .option("--remove-auth-handoff", "Also remove .decisionops/auth-handoff.toml from the repository")
745
+ .action(async (flags) => {
746
+ const selectedPlatforms = await choosePlatforms(flags.platform, "cleanup");
747
+ const repoPath = resolveRepoPath(flags.repoPath);
748
+ const result = cleanupPlatforms({
749
+ platformsDir: DEFAULT_PLATFORMS_DIR,
750
+ selectedPlatforms,
751
+ repoPath,
752
+ skillName: flags.skillName,
753
+ serverName: flags.serverName,
754
+ removeSkill: !(flags.skipSkill ?? false),
755
+ removeMcp: !(flags.skipMcp ?? false),
756
+ removeManifest: flags.removeManifest ?? false,
757
+ removeAuthHandoff: flags.removeAuthHandoff ?? false,
758
+ });
759
+ printCleanupResult(result);
760
+ if (flags.skipAuth ?? false) {
761
+ console.log("Skipping auth cleanup.");
762
+ return;
763
+ }
764
+ const current = readAuthState();
765
+ if (!current) {
766
+ console.log("No local auth state found.");
767
+ return;
768
+ }
769
+ await revokeAuthState(current);
770
+ clearAuthState();
771
+ console.log("Removed local auth state.");
772
+ });
773
+ program
774
+ .command("doctor")
775
+ .description("Diagnose local Decision Ops setup and suggest fixes")
776
+ .option("--repo-path <path>")
777
+ .action(async (flags) => {
778
+ const repoPath = resolveRepoPath(flags.repoPath);
779
+ const currentAuth = readAuthState();
780
+ const auth = currentAuth ? await ensureValidAuthState(currentAuth) : null;
781
+ const platforms = loadPlatforms(DEFAULT_PLATFORMS_DIR);
782
+ const issues = [];
783
+ console.log("");
784
+ console.log("=== DecisionOps Doctor ===");
785
+ console.log("");
786
+ if (auth) {
787
+ console.log(` CLI auth: configured (${authDisplay(auth)})`);
788
+ }
789
+ else {
790
+ console.log(" CLI auth: not configured");
791
+ console.log(" → Run: npx @aidecisionops/decisionops login");
792
+ issues.push("CLI auth not configured");
793
+ }
794
+ if (repoPath) {
795
+ console.log(` Repository: ${repoPath}`);
796
+ const manifest = readManifest(repoPath);
797
+ if (manifest) {
798
+ console.log(" Manifest: present");
799
+ console.log(` org_id: ${manifest.org_id ?? "(missing)"}`);
800
+ console.log(` project_id: ${manifest.project_id ?? "(missing)"}`);
801
+ console.log(` repo_ref: ${manifest.repo_ref ?? "(missing)"}`);
802
+ if (!manifest.org_id || !manifest.project_id || !manifest.repo_ref) {
803
+ issues.push("Manifest is missing required fields (org_id, project_id, or repo_ref)");
804
+ console.log(" → Run: npx @aidecisionops/decisionops init --repo-path .");
805
+ }
806
+ }
807
+ else {
808
+ console.log(" Manifest: missing");
809
+ console.log(" → Run: npx @aidecisionops/decisionops init --repo-path .");
810
+ issues.push("No .decisionops/manifest.toml found");
811
+ }
812
+ }
813
+ else {
814
+ console.log(" Repository: not detected (run from a git repo or pass --repo-path)");
815
+ issues.push("Not inside a git repository");
816
+ }
817
+ console.log("");
818
+ console.log(" Platforms:");
819
+ const context = {
820
+ skill_name: DEFAULT_SKILL_NAME,
821
+ repo_path: repoPath ?? "",
822
+ };
823
+ for (const platform of Object.values(platforms)) {
824
+ const skillPath = platform.skill?.supported ? resolveInstallPath(platform.skill, context) : null;
825
+ const mcpPath = platform.mcp?.supported ? resolveInstallPath(platform.mcp, context) : null;
826
+ const skillInstalled = skillPath ? fs.existsSync(skillPath) : false;
827
+ const mcpInstalled = mcpPath ? fs.existsSync(mcpPath) : false;
828
+ const skillStatus = !platform.skill?.supported ? "n/a" : skillInstalled ? `installed (${skillPath})` : `not installed (${skillPath})`;
829
+ const mcpStatus = !platform.mcp?.supported ? "n/a" : mcpInstalled ? `configured (${mcpPath})` : `not configured (${mcpPath})`;
830
+ console.log(` ${platform.display_name}:`);
831
+ console.log(` Skill: ${skillStatus}`);
832
+ console.log(` MCP: ${mcpStatus}`);
833
+ }
834
+ console.log("");
835
+ if (issues.length === 0) {
836
+ console.log(" No issues found.");
837
+ }
838
+ else {
839
+ console.log(` ${issues.length} issue${issues.length === 1 ? "" : "s"} found:`);
840
+ for (const issue of issues) {
841
+ console.log(` - ${issue}`);
842
+ }
843
+ }
844
+ console.log("");
845
+ });
846
+ const platformCommand = program.command("platform").description("Low-level platform registry operations");
847
+ platformCommand
848
+ .command("list")
849
+ .description("List supported platforms")
850
+ .action(() => {
851
+ for (const platform of Object.values(loadPlatforms(DEFAULT_PLATFORMS_DIR))) {
852
+ console.log(platform.id);
853
+ }
854
+ });
855
+ platformCommand
856
+ .command("build")
857
+ .description("Build platform bundles into the local build directory")
858
+ .option("-p, --platform <id>", "Select a platform to build", collectValues, [])
859
+ .option("--output-dir <path>", "Build output directory", DEFAULT_OUTPUT_DIR)
860
+ .option("--source-dir <path>", "Skill source directory", DEFAULT_SOURCE_DIR)
861
+ .option("--skill-name <name>", "Skill bundle name", DEFAULT_SKILL_NAME)
862
+ .option("--server-name <name>", "MCP server name", DEFAULT_MCP_SERVER_NAME)
863
+ .option("--server-url <url>", "MCP server URL", DEFAULT_MCP_SERVER_URL)
864
+ .action((flags) => {
865
+ const results = buildPlatforms({
866
+ platformsDir: DEFAULT_PLATFORMS_DIR,
867
+ selectedPlatforms: flags.platform,
868
+ outputDir: flags.outputDir,
869
+ sourceDir: flags.sourceDir,
870
+ skillName: flags.skillName,
871
+ serverName: flags.serverName,
872
+ serverUrl: flags.serverUrl,
873
+ });
874
+ for (const result of results) {
875
+ console.log(`Built ${result.platformId} -> ${result.outputPath}`);
876
+ }
877
+ });
878
+ platformCommand
879
+ .command("install")
880
+ .description("Low-level install path matching the legacy installer")
881
+ .option("-p, --platform <id>", "Select a platform to install", collectValues, [])
882
+ .option("--repo-path <path>")
883
+ .option("--org-id <orgId>")
884
+ .option("--project-id <projectId>")
885
+ .option("--repo-ref <repoRef>")
886
+ .option("--repo-id <repoId>")
887
+ .option("--default-branch <branch>")
888
+ .option("--allow-placeholders", "Allow placeholder manifest values for local prototyping")
889
+ .option("--skip-manifest")
890
+ .option("--skip-skill")
891
+ .option("--skip-mcp")
892
+ .option("--output-dir <path>", "Build output directory", DEFAULT_OUTPUT_DIR)
893
+ .option("--source-dir <path>", "Skill source directory", DEFAULT_SOURCE_DIR)
894
+ .option("--skill-name <name>", "Skill bundle name", DEFAULT_SKILL_NAME)
895
+ .option("--server-name <name>", "MCP server name", DEFAULT_MCP_SERVER_NAME)
896
+ .option("--server-url <url>", "MCP server URL", DEFAULT_MCP_SERVER_URL)
897
+ .action((flags) => {
898
+ const result = installPlatforms({
899
+ platformsDir: DEFAULT_PLATFORMS_DIR,
900
+ selectedPlatforms: flags.platform,
901
+ repoPath: flags.repoPath ? path.resolve(flags.repoPath) : null,
902
+ orgId: flags.orgId,
903
+ projectId: flags.projectId,
904
+ repoRef: flags.repoRef,
905
+ repoId: flags.repoId,
906
+ defaultBranch: flags.defaultBranch,
907
+ installSkill: !(flags.skipSkill ?? false),
908
+ installMcp: !(flags.skipMcp ?? false),
909
+ writeManifest: !(flags.skipManifest ?? false),
910
+ allowPlaceholders: flags.allowPlaceholders,
911
+ outputDir: flags.outputDir,
912
+ sourceDir: flags.sourceDir,
913
+ skillName: flags.skillName,
914
+ serverName: flags.serverName,
915
+ serverUrl: flags.serverUrl,
916
+ });
917
+ printInstallResult(result);
918
+ });
919
+ platformCommand
920
+ .command("install-skill")
921
+ .description("Install only skill bundles for skill-capable platforms")
922
+ .option("-p, --platform <id>", "Select a platform to install", collectValues, [])
923
+ .option("--repo-path <path>")
924
+ .option("--output-dir <path>", "Build output directory", DEFAULT_OUTPUT_DIR)
925
+ .option("--source-dir <path>", "Skill source directory", DEFAULT_SOURCE_DIR)
926
+ .option("--skill-name <name>", "Skill bundle name", DEFAULT_SKILL_NAME)
927
+ .action((flags) => {
928
+ const result = installPlatforms({
929
+ platformsDir: DEFAULT_PLATFORMS_DIR,
930
+ selectedPlatforms: flags.platform,
931
+ repoPath: flags.repoPath ? path.resolve(flags.repoPath) : null,
932
+ installSkill: true,
933
+ installMcp: false,
934
+ writeManifest: false,
935
+ outputDir: flags.outputDir,
936
+ sourceDir: flags.sourceDir,
937
+ skillName: flags.skillName,
938
+ });
939
+ printInstallResult(result);
940
+ });
941
+ program.parseAsync(process.argv).catch((error) => {
942
+ const message = error instanceof z.ZodError
943
+ ? error.issues.map((issue) => issue.message).join(", ")
944
+ : error instanceof Error
945
+ ? error.message
946
+ : String(error);
947
+ console.error(message);
948
+ process.exitCode = 1;
949
+ });
950
+ //# sourceMappingURL=cli.js.map