infernoflow 0.37.1 → 0.37.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.
Files changed (88) hide show
  1. package/CHANGELOG.md +64 -0
  2. package/dist/bin/infernoflow.mjs +29 -277
  3. package/dist/lib/adopters/angular.mjs +1 -128
  4. package/dist/lib/adopters/css.mjs +1 -111
  5. package/dist/lib/adopters/react.mjs +1 -104
  6. package/dist/lib/ai/ideDetection.mjs +1 -31
  7. package/dist/lib/ai/localProvider.mjs +1 -88
  8. package/dist/lib/ai/providerRouter.mjs +2 -295
  9. package/dist/lib/commands/adopt.mjs +20 -869
  10. package/dist/lib/commands/adoptWizard.mjs +9 -320
  11. package/dist/lib/commands/agent.mjs +5 -191
  12. package/dist/lib/commands/ai.mjs +2 -407
  13. package/dist/lib/commands/ask.mjs +4 -299
  14. package/dist/lib/commands/audit.mjs +13 -300
  15. package/dist/lib/commands/changelog.mjs +26 -594
  16. package/dist/lib/commands/check.mjs +3 -184
  17. package/dist/lib/commands/ci.mjs +3 -208
  18. package/dist/lib/commands/claudeMd.mjs +30 -135
  19. package/dist/lib/commands/cloud.mjs +10 -773
  20. package/dist/lib/commands/context.mjs +34 -346
  21. package/dist/lib/commands/coverage.mjs +2 -282
  22. package/dist/lib/commands/dashboard.mjs +123 -635
  23. package/dist/lib/commands/demo.mjs +8 -465
  24. package/dist/lib/commands/diff.mjs +5 -274
  25. package/dist/lib/commands/docGate.mjs +2 -81
  26. package/dist/lib/commands/doctor.mjs +3 -321
  27. package/dist/lib/commands/explain.mjs +8 -438
  28. package/dist/lib/commands/export.mjs +10 -239
  29. package/dist/lib/commands/feedback.mjs +12 -216
  30. package/dist/lib/commands/generateSkills.mjs +38 -163
  31. package/dist/lib/commands/graph.mjs +11 -378
  32. package/dist/lib/commands/health.mjs +2 -309
  33. package/dist/lib/commands/impact.mjs +2 -325
  34. package/dist/lib/commands/implement.mjs +7 -103
  35. package/dist/lib/commands/init.mjs +45 -631
  36. package/dist/lib/commands/installCursorHooks.mjs +1 -36
  37. package/dist/lib/commands/installVsCodeCopilotHooks.mjs +1 -37
  38. package/dist/lib/commands/link.mjs +2 -342
  39. package/dist/lib/commands/log.mjs +18 -248
  40. package/dist/lib/commands/monorepo.mjs +4 -428
  41. package/dist/lib/commands/notify.mjs +4 -258
  42. package/dist/lib/commands/onboard.mjs +4 -296
  43. package/dist/lib/commands/prComment.mjs +2 -361
  44. package/dist/lib/commands/prImpact.mjs +2 -157
  45. package/dist/lib/commands/publish.mjs +15 -316
  46. package/dist/lib/commands/recap.mjs +6 -380
  47. package/dist/lib/commands/report.mjs +28 -272
  48. package/dist/lib/commands/review.mjs +9 -223
  49. package/dist/lib/commands/run.mjs +8 -336
  50. package/dist/lib/commands/scaffold.mjs +54 -419
  51. package/dist/lib/commands/scan.mjs +11 -1118
  52. package/dist/lib/commands/scout.mjs +2 -291
  53. package/dist/lib/commands/setup.mjs +5 -310
  54. package/dist/lib/commands/share.mjs +13 -196
  55. package/dist/lib/commands/snapshot.mjs +3 -383
  56. package/dist/lib/commands/stability.mjs +2 -293
  57. package/dist/lib/commands/stats.mjs +5 -402
  58. package/dist/lib/commands/status.mjs +4 -172
  59. package/dist/lib/commands/suggest.mjs +21 -563
  60. package/dist/lib/commands/switch.mjs +13 -520
  61. package/dist/lib/commands/syncAuto.mjs +1 -96
  62. package/dist/lib/commands/synthesize.mjs +10 -228
  63. package/dist/lib/commands/teamSync.mjs +2 -388
  64. package/dist/lib/commands/test.mjs +6 -363
  65. package/dist/lib/commands/theme.mjs +18 -195
  66. package/dist/lib/commands/uninstall.mjs +13 -406
  67. package/dist/lib/commands/upgrade.mjs +20 -153
  68. package/dist/lib/commands/version.mjs +2 -282
  69. package/dist/lib/commands/vibe.mjs +7 -357
  70. package/dist/lib/commands/watch.mjs +4 -203
  71. package/dist/lib/commands/why.mjs +4 -358
  72. package/dist/lib/cursorHooksInstall.mjs +1 -60
  73. package/dist/lib/draftToolingInstall.mjs +7 -68
  74. package/dist/lib/git/detect-drift.mjs +4 -208
  75. package/dist/lib/learning/adapt.mjs +6 -101
  76. package/dist/lib/learning/observe.mjs +1 -119
  77. package/dist/lib/learning/patternDetector.mjs +1 -298
  78. package/dist/lib/learning/profile.mjs +2 -279
  79. package/dist/lib/learning/skillSynthesizer.mjs +24 -145
  80. package/dist/lib/telemetry.mjs +19 -269
  81. package/dist/lib/templates/index.mjs +1 -131
  82. package/dist/lib/theme/scanner.mjs +4 -343
  83. package/dist/lib/ui/errors.mjs +1 -142
  84. package/dist/lib/ui/output.mjs +6 -95
  85. package/dist/lib/ui/prompts.mjs +6 -147
  86. package/dist/lib/vsCodeCopilotHooksInstall.mjs +1 -42
  87. package/package.json +2 -4
  88. package/scripts/postinstall.js +2 -2
@@ -1,36 +1 @@
1
- import * as path from "node:path";
2
- import { fileURLToPath } from "node:url";
3
- import { header, ok, warn, done, nextSteps, cyan, yellow } from "../ui/output.mjs";
4
- import { installCursorHooksArtifacts } from "../cursorHooksInstall.mjs";
5
-
6
- const __dirname = path.dirname(fileURLToPath(import.meta.url));
7
-
8
- function getTemplatesRoot() {
9
- return path.resolve(__dirname, "../../templates");
10
- }
11
-
12
- export async function installCursorHooksCommand(args) {
13
- const cwd = process.cwd();
14
- const force = args.includes("--force") || args.includes("-f");
15
-
16
- header("install-cursor-hooks");
17
-
18
- installCursorHooksArtifacts({
19
- cwd,
20
- templatesRoot: getTemplatesRoot(),
21
- force,
22
- silent: false,
23
- logOk: (msg) => ok(msg),
24
- logWarn: (msg) => warn(msg),
25
- });
26
-
27
- done("Cursor draft hooks installed");
28
-
29
- nextSteps([
30
- "Restart Cursor (or reload window) so " + yellow(".cursor/hooks.json") + " is picked up",
31
- "Use Agent chat — each assistant reply appends to " + yellow("inferno/CONTEXT.draft.md") + " (gitignored)",
32
- cyan("npm run inferno:promote-draft") + " — preview draft",
33
- cyan("npm run inferno:promote-draft -- --append-notes") + " — merge into inferno/CONTEXT.md under Decisions",
34
- cyan("npm run inferno:promote-draft -- --clear") + " — discard draft",
35
- ]);
36
- }
1
+ import*as t from"node:path";import{fileURLToPath as i}from"node:url";import{header as d,ok as m,warn as p,done as l,nextSteps as f,cyan as r,yellow as n}from"../ui/output.mjs";import{installCursorHooksArtifacts as c}from"../cursorHooksInstall.mjs";const u=t.dirname(i(import.meta.url));function h(){return t.resolve(u,"../../templates")}async function g(e){const s=process.cwd(),a=e.includes("--force")||e.includes("-f");d("install-cursor-hooks"),c({cwd:s,templatesRoot:h(),force:a,silent:!1,logOk:o=>m(o),logWarn:o=>p(o)}),l("Cursor draft hooks installed"),f(["Restart Cursor (or reload window) so "+n(".cursor/hooks.json")+" is picked up","Use Agent chat \u2014 each assistant reply appends to "+n("inferno/CONTEXT.draft.md")+" (gitignored)",r("npm run inferno:promote-draft")+" \u2014 preview draft",r("npm run inferno:promote-draft -- --append-notes")+" \u2014 merge into inferno/CONTEXT.md under Decisions",r("npm run inferno:promote-draft -- --clear")+" \u2014 discard draft"])}export{g as installCursorHooksCommand};
@@ -1,37 +1 @@
1
- import * as path from "node:path";
2
- import { fileURLToPath } from "node:url";
3
- import { header, ok, warn, done, nextSteps, cyan, yellow } from "../ui/output.mjs";
4
- import { installVsCodeCopilotHooksArtifacts } from "../vsCodeCopilotHooksInstall.mjs";
5
-
6
- const __dirname = path.dirname(fileURLToPath(import.meta.url));
7
-
8
- function getTemplatesRoot() {
9
- return path.resolve(__dirname, "../../templates");
10
- }
11
-
12
- export async function installVsCodeCopilotHooksCommand(args) {
13
- const cwd = process.cwd();
14
- const force = args.includes("--force") || args.includes("-f");
15
-
16
- header("install-vscode-copilot-hooks");
17
-
18
- installVsCodeCopilotHooksArtifacts({
19
- cwd,
20
- templatesRoot: getTemplatesRoot(),
21
- force,
22
- silent: false,
23
- logOk: (msg) => ok(msg),
24
- logWarn: (msg) => warn(msg),
25
- });
26
-
27
- done("VS Code / Copilot draft hooks installed");
28
-
29
- nextSteps([
30
- "Requires VS Code + GitHub Copilot and **Agent hooks (Preview)** — see " +
31
- yellow("https://code.visualstudio.com/docs/copilot/customization/hooks"),
32
- "Hooks load from " + yellow(".github/hooks/*.json") + " — restart VS Code or reload window after first install",
33
- "Check the **GitHub Copilot Chat Hooks** output channel if nothing runs",
34
- cyan("npm run inferno:promote-draft") + " — preview draft",
35
- cyan("npm run inferno:promote-draft -- --append-notes") + " — merge into inferno/CONTEXT.md",
36
- ]);
37
- }
1
+ import*as e from"node:path";import{fileURLToPath as a}from"node:url";import{header as l,ok as d,warn as p,done as m,nextSteps as c,cyan as n,yellow as r}from"../ui/output.mjs";import{installVsCodeCopilotHooksArtifacts as f}from"../vsCodeCopilotHooksInstall.mjs";const u=e.dirname(a(import.meta.url));function h(){return e.resolve(u,"../../templates")}async function g(t){const s=process.cwd(),i=t.includes("--force")||t.includes("-f");l("install-vscode-copilot-hooks"),f({cwd:s,templatesRoot:h(),force:i,silent:!1,logOk:o=>d(o),logWarn:o=>p(o)}),m("VS Code / Copilot draft hooks installed"),c(["Requires VS Code + GitHub Copilot and **Agent hooks (Preview)** \u2014 see "+r("https://code.visualstudio.com/docs/copilot/customization/hooks"),"Hooks load from "+r(".github/hooks/*.json")+" \u2014 restart VS Code or reload window after first install","Check the **GitHub Copilot Chat Hooks** output channel if nothing runs",n("npm run inferno:promote-draft")+" \u2014 preview draft",n("npm run inferno:promote-draft -- --append-notes")+" \u2014 merge into inferno/CONTEXT.md"])}export{g as installVsCodeCopilotHooksCommand};
@@ -1,342 +1,2 @@
1
- /**
2
- * infernoflow link
3
- *
4
- * Link capabilities to tickets in Jira, Linear, or GitHub Issues.
5
- * Stored in inferno/links.json — travels with the repo.
6
- *
7
- * Usage:
8
- * infernoflow link --jira PROJ-123 --capability CreateTask
9
- * infernoflow link --linear LIN-456 --capability FilterByTag
10
- * infernoflow link --github 78 --capability ExportToCsv
11
- * infernoflow link list Show all links
12
- * infernoflow link status Show which caps have open tickets
13
- * infernoflow link remove --capability CreateTask
14
- * infernoflow link --json Machine-readable
15
- *
16
- * Config (inferno/integrations.json):
17
- * {
18
- * "jira": { "baseUrl": "https://myorg.atlassian.net", "token": "...", "email": "..." },
19
- * "linear": { "apiKey": "lin_api_..." },
20
- * "github": { "repo": "owner/repo", "token": "ghp_..." }
21
- * }
22
- *
23
- * Env vars (override config):
24
- * JIRA_BASE_URL, JIRA_TOKEN, JIRA_EMAIL
25
- * LINEAR_API_KEY
26
- * GITHUB_TOKEN, GITHUB_REPOSITORY
27
- */
28
-
29
- import * as fs from "node:fs";
30
- import * as path from "node:path";
31
- import * as https from "node:https";
32
- import { done, warn, info, bold, cyan, gray, green, yellow, red } from "../ui/output.mjs";
33
-
34
- const LINKS_FILE = "links.json";
35
- const CONFIG_FILE = "integrations.json";
36
-
37
- // ── Storage ───────────────────────────────────────────────────────────────────
38
-
39
- function readLinks(infernoDir) {
40
- const p = path.join(infernoDir, LINKS_FILE);
41
- if (!fs.existsSync(p)) return [];
42
- try { return JSON.parse(fs.readFileSync(p, "utf8")); } catch { return []; }
43
- }
44
-
45
- function writeLinks(infernoDir, links) {
46
- fs.writeFileSync(path.join(infernoDir, LINKS_FILE), JSON.stringify(links, null, 2) + "\n");
47
- }
48
-
49
- function readIntegrationConfig(infernoDir) {
50
- const p = path.join(infernoDir, CONFIG_FILE);
51
- if (!fs.existsSync(p)) return {};
52
- try { return JSON.parse(fs.readFileSync(p, "utf8")); } catch { return {}; }
53
- }
54
-
55
- // ── HTTP helper ───────────────────────────────────────────────────────────────
56
-
57
- function httpsGet(url, headers = {}) {
58
- return new Promise((resolve, reject) => {
59
- const parsed = new URL(url);
60
- https.get({
61
- hostname: parsed.hostname,
62
- path: parsed.pathname + (parsed.search || ""),
63
- headers: { "User-Agent": "infernoflow-cli", "Accept": "application/json", ...headers },
64
- }, (res) => {
65
- let data = "";
66
- res.on("data", d => (data += d));
67
- res.on("end", () => {
68
- try { resolve({ status: res.statusCode, body: JSON.parse(data) }); }
69
- catch { resolve({ status: res.statusCode, body: data }); }
70
- });
71
- }).on("error", reject);
72
- });
73
- }
74
-
75
- // ── Ticket fetchers ───────────────────────────────────────────────────────────
76
-
77
- async function fetchJiraTicket(ticketId, config) {
78
- const base = process.env.JIRA_BASE_URL || config.jira?.baseUrl;
79
- const token = process.env.JIRA_TOKEN || config.jira?.token;
80
- const email = process.env.JIRA_EMAIL || config.jira?.email;
81
- if (!base || !token) return null;
82
-
83
- try {
84
- const creds = Buffer.from(`${email}:${token}`).toString("base64");
85
- const resp = await httpsGet(`${base}/rest/api/3/issue/${ticketId}`, {
86
- "Authorization": `Basic ${creds}`,
87
- });
88
- if (resp.status === 200) {
89
- return {
90
- id: ticketId,
91
- title: resp.body.fields?.summary || ticketId,
92
- status: resp.body.fields?.status?.name || "unknown",
93
- url: `${base}/browse/${ticketId}`,
94
- };
95
- }
96
- } catch {}
97
- return { id: ticketId, title: ticketId, status: "unknown", url: null };
98
- }
99
-
100
- async function fetchLinearTicket(ticketId, config) {
101
- const apiKey = process.env.LINEAR_API_KEY || config.linear?.apiKey;
102
- if (!apiKey) return null;
103
-
104
- try {
105
- const query = JSON.stringify({
106
- query: `{ issue(id: "${ticketId}") { title state { name } url } }`
107
- });
108
- const resp = await new Promise((resolve, reject) => {
109
- const body = query;
110
- const req = https.request({
111
- hostname: "api.linear.app",
112
- path: "/graphql",
113
- method: "POST",
114
- headers: { "Content-Type": "application/json", "Authorization": apiKey, "Content-Length": Buffer.byteLength(body) },
115
- }, (res) => {
116
- let data = "";
117
- res.on("data", d => (data += d));
118
- res.on("end", () => resolve(JSON.parse(data)));
119
- });
120
- req.on("error", reject);
121
- req.write(body);
122
- req.end();
123
- });
124
- const issue = resp.data?.issue;
125
- if (issue) return { id: ticketId, title: issue.title, status: issue.state?.name || "unknown", url: issue.url };
126
- } catch {}
127
- return { id: ticketId, title: ticketId, status: "unknown", url: null };
128
- }
129
-
130
- async function fetchGithubIssue(issueNum, config) {
131
- const token = process.env.GITHUB_TOKEN || config.github?.token;
132
- const repo = process.env.GITHUB_REPOSITORY || config.github?.repo;
133
- if (!repo) return null;
134
-
135
- try {
136
- const headers = { "Authorization": token ? `Bearer ${token}` : undefined };
137
- const resp = await httpsGet(`https://api.github.com/repos/${repo}/issues/${issueNum}`, headers);
138
- if (resp.status === 200) {
139
- return {
140
- id: `#${issueNum}`,
141
- title: resp.body.title || `Issue #${issueNum}`,
142
- status: resp.body.state || "unknown",
143
- url: resp.body.html_url,
144
- };
145
- }
146
- } catch {}
147
- return { id: `#${issueNum}`, title: `Issue #${issueNum}`, status: "unknown", url: null };
148
- }
149
-
150
- // ── Sub-commands ──────────────────────────────────────────────────────────────
151
-
152
- async function subcmdAdd(args, infernoDir, config) {
153
- const jsonMode = args.includes("--json");
154
- const capIdx = args.indexOf("--capability");
155
- const jiraIdx = args.indexOf("--jira");
156
- const linearIdx = args.indexOf("--linear");
157
- const githubIdx = args.indexOf("--github");
158
-
159
- const capability = capIdx !== -1 ? args[capIdx + 1] : null;
160
- const jiraId = jiraIdx !== -1 ? args[jiraIdx + 1] : null;
161
- const linearId = linearIdx !== -1 ? args[linearIdx + 1] : null;
162
- const githubNum = githubIdx !== -1 ? args[githubIdx + 1] : null;
163
-
164
- if (!capability) {
165
- const msg = "Usage: infernoflow link --capability <id> --jira <TICKET> | --linear <ID> | --github <NUM>";
166
- if (jsonMode) { console.log(JSON.stringify({ ok: false, error: msg })); } else { warn(msg); }
167
- return;
168
- }
169
-
170
- let ticket = null;
171
- let platform = null;
172
-
173
- if (jiraId) {
174
- if (!jsonMode) process.stdout.write(` Fetching Jira ${jiraId}… `);
175
- ticket = await fetchJiraTicket(jiraId, config);
176
- platform = "jira";
177
- } else if (linearId) {
178
- if (!jsonMode) process.stdout.write(` Fetching Linear ${linearId}… `);
179
- ticket = await fetchLinearTicket(linearId, config);
180
- platform = "linear";
181
- } else if (githubNum) {
182
- if (!jsonMode) process.stdout.write(` Fetching GitHub #${githubNum}… `);
183
- ticket = await fetchGithubIssue(githubNum, config);
184
- platform = "github";
185
- } else {
186
- const msg = "Specify --jira, --linear, or --github";
187
- if (jsonMode) { console.log(JSON.stringify({ ok: false, error: msg })); } else { warn(msg); }
188
- return;
189
- }
190
-
191
- const links = readLinks(infernoDir);
192
- const existing = links.findIndex(l => l.capability === capability);
193
-
194
- const link = {
195
- capability,
196
- platform,
197
- ticketId: ticket?.id || jiraId || linearId || `#${githubNum}`,
198
- title: ticket?.title || "",
199
- status: ticket?.status || "unknown",
200
- url: ticket?.url || null,
201
- linkedAt: new Date().toISOString(),
202
- };
203
-
204
- if (existing !== -1) {
205
- links[existing] = link;
206
- } else {
207
- links.push(link);
208
- }
209
-
210
- writeLinks(infernoDir, links);
211
-
212
- if (!jsonMode) console.log(green("done"));
213
-
214
- if (jsonMode) {
215
- console.log(JSON.stringify({ ok: true, link }));
216
- } else {
217
- done(`Linked: ${bold(capability)} → ${cyan(link.ticketId)} (${link.status})`);
218
- if (link.url) console.log(` ${gray(link.url)}`);
219
- console.log();
220
- }
221
- }
222
-
223
- async function subcmdList(args, infernoDir) {
224
- const jsonMode = args.includes("--json");
225
- const links = readLinks(infernoDir);
226
-
227
- if (jsonMode) {
228
- console.log(JSON.stringify({ ok: true, links }));
229
- return;
230
- }
231
-
232
- if (!links.length) {
233
- info("No links yet. Use: infernoflow link --capability <id> --jira <TICKET>");
234
- return;
235
- }
236
-
237
- console.log();
238
- console.log(` ${bold(`${links.length} capability link${links.length !== 1 ? "s" : ""}`)}`);
239
- console.log();
240
-
241
- const w = Math.max(...links.map(l => l.capability.length), 10) + 2;
242
- for (const l of links) {
243
- const statusColor = l.status?.toLowerCase() === "done" || l.status?.toLowerCase() === "closed"
244
- ? green : l.status?.toLowerCase() === "in progress" ? yellow : gray;
245
- console.log(` ${bold(l.capability.padEnd(w))} ${cyan(l.ticketId.padEnd(14))} ${statusColor(l.status || "unknown")}`);
246
- if (l.title && l.title !== l.ticketId) console.log(` ${" ".repeat(w + 2)}${gray(l.title)}`);
247
- }
248
- console.log();
249
- }
250
-
251
- async function subcmdStatus(args, infernoDir) {
252
- const jsonMode = args.includes("--json");
253
- const links = readLinks(infernoDir);
254
-
255
- // Load contract to find unlinked capabilities
256
- let contract = null;
257
- for (const f of ["contract.json", "capabilities.json"]) {
258
- const p = path.join(infernoDir, f);
259
- if (fs.existsSync(p)) { try { contract = JSON.parse(fs.readFileSync(p, "utf8")); break; } catch {} }
260
- }
261
- const allCaps = (contract?.capabilities || []).map(c => typeof c === "string" ? c : c.id);
262
- const linkedIds = new Set(links.map(l => l.capability));
263
- const unlinked = allCaps.filter(id => !linkedIds.has(id));
264
-
265
- if (jsonMode) {
266
- console.log(JSON.stringify({ ok: true, linked: links.length, unlinked: unlinked.length, links, unlinkedCapabilities: unlinked }));
267
- return;
268
- }
269
-
270
- console.log();
271
- console.log(` ${bold("Capability link status")}`);
272
- console.log();
273
-
274
- if (links.length) {
275
- console.log(` ${gray("Linked:")}`);
276
- for (const l of links) {
277
- const icon = l.status?.toLowerCase() === "done" ? green("✔") : l.status?.toLowerCase() === "in progress" ? yellow("⟳") : gray("○");
278
- console.log(` ${icon} ${bold(l.capability)} ${cyan(l.ticketId)} ${gray(l.status || "")}`);
279
- }
280
- console.log();
281
- }
282
-
283
- if (unlinked.length) {
284
- console.log(` ${gray("Unlinked capabilities:")}`);
285
- unlinked.forEach(id => console.log(` ${gray("·")} ${id}`));
286
- console.log();
287
- }
288
-
289
- console.log(` ${green(String(links.length))} linked · ${gray(String(unlinked.length))} unlinked`);
290
- console.log();
291
- }
292
-
293
- async function subcmdRemove(args, infernoDir) {
294
- const jsonMode = args.includes("--json");
295
- const capIdx = args.indexOf("--capability");
296
- const capId = capIdx !== -1 ? args[capIdx + 1] : null;
297
-
298
- if (!capId) {
299
- if (jsonMode) { console.log(JSON.stringify({ ok: false, error: "Usage: infernoflow link remove --capability <id>" })); }
300
- else { warn("Usage: infernoflow link remove --capability <id>"); }
301
- return;
302
- }
303
-
304
- const links = readLinks(infernoDir);
305
- const before = links.length;
306
- const updated = links.filter(l => l.capability !== capId);
307
-
308
- if (updated.length === before) {
309
- if (jsonMode) { console.log(JSON.stringify({ ok: false, error: `No link found for: ${capId}` })); }
310
- else { warn(`No link found for: ${capId}`); }
311
- return;
312
- }
313
-
314
- writeLinks(infernoDir, updated);
315
- if (jsonMode) { console.log(JSON.stringify({ ok: true, removed: capId })); }
316
- else { done(`Removed link for ${bold(capId)}`); console.log(); }
317
- }
318
-
319
- // ── Entry ─────────────────────────────────────────────────────────────────────
320
-
321
- export async function linkCommand(rawArgs) {
322
- const args = rawArgs.slice(1);
323
- const cwd = process.cwd();
324
- const infernoDir = path.join(cwd, "inferno");
325
-
326
- if (!fs.existsSync(infernoDir)) {
327
- const msg = "inferno/ not found. Run: infernoflow init";
328
- if (args.includes("--json")) { console.log(JSON.stringify({ ok: false, error: msg })); }
329
- else { warn(msg); }
330
- process.exit(1);
331
- }
332
-
333
- const config = readIntegrationConfig(infernoDir);
334
- const subcmd = args[0];
335
-
336
- if (subcmd === "list") return subcmdList(args.slice(1), infernoDir);
337
- if (subcmd === "status") return subcmdStatus(args.slice(1), infernoDir);
338
- if (subcmd === "remove") return subcmdRemove(args.slice(1), infernoDir);
339
-
340
- // Default: add a link
341
- return subcmdAdd(args, infernoDir, config);
342
- }
1
+ import*as d from"node:fs";import*as b from"node:path";import*as N from"node:https";import{done as J,warn as $,info as E,bold as k,cyan as I,gray as f,green as S,yellow as L}from"../ui/output.mjs";const x="links.json",T="integrations.json";function j(n){const o=b.join(n,x);if(!d.existsSync(o))return[];try{return JSON.parse(d.readFileSync(o,"utf8"))}catch{return[]}}function C(n,o){d.writeFileSync(b.join(n,x),JSON.stringify(o,null,2)+`
2
+ `)}function U(n){const o=b.join(n,T);if(!d.existsSync(o))return{};try{return JSON.parse(d.readFileSync(o,"utf8"))}catch{return{}}}function A(n,o={}){return new Promise((i,t)=>{const l=new URL(n);N.get({hostname:l.hostname,path:l.pathname+(l.search||""),headers:{"User-Agent":"infernoflow-cli",Accept:"application/json",...o}},e=>{let r="";e.on("data",a=>r+=a),e.on("end",()=>{try{i({status:e.statusCode,body:JSON.parse(r)})}catch{i({status:e.statusCode,body:r})}})}).on("error",t)})}async function v(n,o){const i=process.env.JIRA_BASE_URL||o.jira?.baseUrl,t=process.env.JIRA_TOKEN||o.jira?.token,l=process.env.JIRA_EMAIL||o.jira?.email;if(!i||!t)return null;try{const e=Buffer.from(`${l}:${t}`).toString("base64"),r=await A(`${i}/rest/api/3/issue/${n}`,{Authorization:`Basic ${e}`});if(r.status===200)return{id:n,title:r.body.fields?.summary||n,status:r.body.fields?.status?.name||"unknown",url:`${i}/browse/${n}`}}catch{}return{id:n,title:n,status:"unknown",url:null}}async function R(n,o){const i=process.env.LINEAR_API_KEY||o.linear?.apiKey;if(!i)return null;try{const t=JSON.stringify({query:`{ issue(id: "${n}") { title state { name } url } }`}),e=(await new Promise((r,a)=>{const s=t,c=N.request({hostname:"api.linear.app",path:"/graphql",method:"POST",headers:{"Content-Type":"application/json",Authorization:i,"Content-Length":Buffer.byteLength(s)}},p=>{let g="";p.on("data",u=>g+=u),p.on("end",()=>r(JSON.parse(g)))});c.on("error",a),c.write(s),c.end()})).data?.issue;if(e)return{id:n,title:e.title,status:e.state?.name||"unknown",url:e.url}}catch{}return{id:n,title:n,status:"unknown",url:null}}async function _(n,o){const i=process.env.GITHUB_TOKEN||o.github?.token,t=process.env.GITHUB_REPOSITORY||o.github?.repo;if(!t)return null;try{const l={Authorization:i?`Bearer ${i}`:void 0},e=await A(`https://api.github.com/repos/${t}/issues/${n}`,l);if(e.status===200)return{id:`#${n}`,title:e.body.title||`Issue #${n}`,status:e.body.state||"unknown",url:e.body.html_url}}catch{}return{id:`#${n}`,title:`Issue #${n}`,status:"unknown",url:null}}async function F(n,o,i){const t=n.includes("--json"),l=n.indexOf("--capability"),e=n.indexOf("--jira"),r=n.indexOf("--linear"),a=n.indexOf("--github"),s=l!==-1?n[l+1]:null,c=e!==-1?n[e+1]:null,p=r!==-1?n[r+1]:null,g=a!==-1?n[a+1]:null;if(!s){const h="Usage: infernoflow link --capability <id> --jira <TICKET> | --linear <ID> | --github <NUM>";t?console.log(JSON.stringify({ok:!1,error:h})):$(h);return}let u=null,m=null;if(c)t||process.stdout.write(` Fetching Jira ${c}\u2026 `),u=await v(c,i),m="jira";else if(p)t||process.stdout.write(` Fetching Linear ${p}\u2026 `),u=await R(p,i),m="linear";else if(g)t||process.stdout.write(` Fetching GitHub #${g}\u2026 `),u=await _(g,i),m="github";else{const h="Specify --jira, --linear, or --github";t?console.log(JSON.stringify({ok:!1,error:h})):$(h);return}const w=j(o),O=w.findIndex(h=>h.capability===s),y={capability:s,platform:m,ticketId:u?.id||c||p||`#${g}`,title:u?.title||"",status:u?.status||"unknown",url:u?.url||null,linkedAt:new Date().toISOString()};O!==-1?w[O]=y:w.push(y),C(o,w),t||console.log(S("done")),t?console.log(JSON.stringify({ok:!0,link:y})):(J(`Linked: ${k(s)} \u2192 ${I(y.ticketId)} (${y.status})`),y.url&&console.log(` ${f(y.url)}`),console.log())}async function K(n,o){const i=n.includes("--json"),t=j(o);if(i){console.log(JSON.stringify({ok:!0,links:t}));return}if(!t.length){E("No links yet. Use: infernoflow link --capability <id> --jira <TICKET>");return}console.log(),console.log(` ${k(`${t.length} capability link${t.length!==1?"s":""}`)}`),console.log();const l=Math.max(...t.map(e=>e.capability.length),10)+2;for(const e of t){const r=e.status?.toLowerCase()==="done"||e.status?.toLowerCase()==="closed"?S:e.status?.toLowerCase()==="in progress"?L:f;console.log(` ${k(e.capability.padEnd(l))} ${I(e.ticketId.padEnd(14))} ${r(e.status||"unknown")}`),e.title&&e.title!==e.ticketId&&console.log(` ${" ".repeat(l+2)}${f(e.title)}`)}console.log()}async function B(n,o){const i=n.includes("--json"),t=j(o);let l=null;for(const s of["contract.json","capabilities.json"]){const c=b.join(o,s);if(d.existsSync(c))try{l=JSON.parse(d.readFileSync(c,"utf8"));break}catch{}}const e=(l?.capabilities||[]).map(s=>typeof s=="string"?s:s.id),r=new Set(t.map(s=>s.capability)),a=e.filter(s=>!r.has(s));if(i){console.log(JSON.stringify({ok:!0,linked:t.length,unlinked:a.length,links:t,unlinkedCapabilities:a}));return}if(console.log(),console.log(` ${k("Capability link status")}`),console.log(),t.length){console.log(` ${f("Linked:")}`);for(const s of t){const c=s.status?.toLowerCase()==="done"?S("\u2714"):s.status?.toLowerCase()==="in progress"?L("\u27F3"):f("\u25CB");console.log(` ${c} ${k(s.capability)} ${I(s.ticketId)} ${f(s.status||"")}`)}console.log()}a.length&&(console.log(` ${f("Unlinked capabilities:")}`),a.forEach(s=>console.log(` ${f("\xB7")} ${s}`)),console.log()),console.log(` ${S(String(t.length))} linked \xB7 ${f(String(a.length))} unlinked`),console.log()}async function M(n,o){const i=n.includes("--json"),t=n.indexOf("--capability"),l=t!==-1?n[t+1]:null;if(!l){i?console.log(JSON.stringify({ok:!1,error:"Usage: infernoflow link remove --capability <id>"})):$("Usage: infernoflow link remove --capability <id>");return}const e=j(o),r=e.length,a=e.filter(s=>s.capability!==l);if(a.length===r){i?console.log(JSON.stringify({ok:!1,error:`No link found for: ${l}`})):$(`No link found for: ${l}`);return}C(o,a),i?console.log(JSON.stringify({ok:!0,removed:l})):(J(`Removed link for ${k(l)}`),console.log())}async function P(n){const o=n.slice(1),i=process.cwd(),t=b.join(i,"inferno");if(!d.existsSync(t)){const r="inferno/ not found. Run: infernoflow init";o.includes("--json")?console.log(JSON.stringify({ok:!1,error:r})):$(r),process.exit(1)}const l=U(t),e=o[0];return e==="list"?K(o.slice(1),t):e==="status"?B(o.slice(1),t):e==="remove"?M(o.slice(1),t):F(o,t,l)}export{P as linkCommand};