infernoflow 0.33.0 → 0.34.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 (84) hide show
  1. package/README.md +208 -120
  2. package/dist/bin/infernoflow.mjs +271 -85
  3. package/dist/lib/adopters/angular.mjs +128 -1
  4. package/dist/lib/adopters/css.mjs +111 -1
  5. package/dist/lib/adopters/react.mjs +104 -1
  6. package/dist/lib/ai/ideDetection.mjs +31 -1
  7. package/dist/lib/ai/localProvider.mjs +88 -1
  8. package/dist/lib/ai/providerRouter.mjs +295 -2
  9. package/dist/lib/commands/adopt.mjs +869 -20
  10. package/dist/lib/commands/adoptWizard.mjs +320 -9
  11. package/dist/lib/commands/agent.mjs +191 -5
  12. package/dist/lib/commands/ai.mjs +407 -2
  13. package/dist/lib/commands/ask.mjs +299 -0
  14. package/dist/lib/commands/audit.mjs +300 -13
  15. package/dist/lib/commands/changelog.mjs +594 -26
  16. package/dist/lib/commands/check.mjs +184 -3
  17. package/dist/lib/commands/ci.mjs +208 -3
  18. package/dist/lib/commands/claudeMd.mjs +139 -28
  19. package/dist/lib/commands/cloud.mjs +521 -5
  20. package/dist/lib/commands/context.mjs +346 -34
  21. package/dist/lib/commands/coverage.mjs +282 -2
  22. package/dist/lib/commands/dashboard.mjs +635 -123
  23. package/dist/lib/commands/demo.mjs +465 -8
  24. package/dist/lib/commands/diff.mjs +274 -5
  25. package/dist/lib/commands/docGate.mjs +81 -2
  26. package/dist/lib/commands/doctor.mjs +321 -3
  27. package/dist/lib/commands/explain.mjs +438 -8
  28. package/dist/lib/commands/export.mjs +239 -10
  29. package/dist/lib/commands/generateSkills.mjs +163 -38
  30. package/dist/lib/commands/graph.mjs +378 -11
  31. package/dist/lib/commands/health.mjs +309 -2
  32. package/dist/lib/commands/impact.mjs +325 -2
  33. package/dist/lib/commands/implement.mjs +103 -7
  34. package/dist/lib/commands/init.mjs +545 -23
  35. package/dist/lib/commands/installCursorHooks.mjs +36 -1
  36. package/dist/lib/commands/installVsCodeCopilotHooks.mjs +37 -1
  37. package/dist/lib/commands/link.mjs +342 -2
  38. package/dist/lib/commands/log.mjs +164 -16
  39. package/dist/lib/commands/monorepo.mjs +428 -4
  40. package/dist/lib/commands/notify.mjs +258 -4
  41. package/dist/lib/commands/onboard.mjs +296 -4
  42. package/dist/lib/commands/prComment.mjs +361 -2
  43. package/dist/lib/commands/prImpact.mjs +157 -2
  44. package/dist/lib/commands/publish.mjs +316 -15
  45. package/dist/lib/commands/recap.mjs +359 -0
  46. package/dist/lib/commands/report.mjs +272 -28
  47. package/dist/lib/commands/review.mjs +223 -9
  48. package/dist/lib/commands/run.mjs +336 -8
  49. package/dist/lib/commands/scaffold.mjs +419 -54
  50. package/dist/lib/commands/scan.mjs +1118 -5
  51. package/dist/lib/commands/scout.mjs +291 -2
  52. package/dist/lib/commands/setup.mjs +310 -5
  53. package/dist/lib/commands/share.mjs +196 -13
  54. package/dist/lib/commands/snapshot.mjs +383 -3
  55. package/dist/lib/commands/stability.mjs +293 -2
  56. package/dist/lib/commands/stats.mjs +402 -0
  57. package/dist/lib/commands/status.mjs +172 -4
  58. package/dist/lib/commands/suggest.mjs +563 -21
  59. package/dist/lib/commands/switch.mjs +310 -0
  60. package/dist/lib/commands/syncAuto.mjs +96 -1
  61. package/dist/lib/commands/synthesize.mjs +228 -10
  62. package/dist/lib/commands/teamSync.mjs +388 -2
  63. package/dist/lib/commands/test.mjs +363 -6
  64. package/dist/lib/commands/theme.mjs +195 -18
  65. package/dist/lib/commands/upgrade.mjs +153 -0
  66. package/dist/lib/commands/version.mjs +282 -2
  67. package/dist/lib/commands/vibe.mjs +357 -7
  68. package/dist/lib/commands/watch.mjs +203 -4
  69. package/dist/lib/commands/why.mjs +358 -4
  70. package/dist/lib/cursorHooksInstall.mjs +60 -1
  71. package/dist/lib/draftToolingInstall.mjs +68 -7
  72. package/dist/lib/git/detect-drift.mjs +208 -4
  73. package/dist/lib/learning/adapt.mjs +101 -6
  74. package/dist/lib/learning/observe.mjs +119 -1
  75. package/dist/lib/learning/patternDetector.mjs +298 -1
  76. package/dist/lib/learning/profile.mjs +279 -2
  77. package/dist/lib/learning/skillSynthesizer.mjs +145 -24
  78. package/dist/lib/templates/index.mjs +131 -1
  79. package/dist/lib/theme/scanner.mjs +343 -4
  80. package/dist/lib/ui/errors.mjs +142 -1
  81. package/dist/lib/ui/output.mjs +72 -6
  82. package/dist/lib/ui/prompts.mjs +147 -6
  83. package/dist/lib/vsCodeCopilotHooksInstall.mjs +42 -1
  84. package/package.json +1 -1
@@ -1,30 +1,552 @@
1
- import*as r from"node:fs";import*as t from"node:path";import*as G from"node:readline";import{fileURLToPath as ne}from"node:url";import{header as oe,ok as d,warn as $,done as D,nextSteps as ie,cyan as l,yellow as N,gray as S}from"../ui/output.mjs";import{discoverProjectSignals as I,reviewCapabilitiesInteractive as te,writeAdoptionBaseline as se,buildAdoptionReport as re,summarizeCapabilities as ae,buildSignalsReport as ce}from"./adopt.mjs";import{installCursorHooksArtifacts as le}from"../cursorHooksInstall.mjs";import{installVsCodeCopilotHooksArtifacts as pe}from"../vsCodeCopilotHooksInstall.mjs";const fe=t.dirname(ne(import.meta.url));function de(){return t.resolve(fe,"../../templates")}function P(e,n,i=""){return new Promise(s=>{const u=i?S(` (${i})`):"";e.question(` ${n}${u}: `,y=>{s(y.trim()||i)})})}function J(e,...n){for(const i of n){const s=e.indexOf(i);if(s!==-1&&e[s+1]&&!e[s+1].startsWith("-"))return e[s+1]}return null}function T(e,n,i,s=!1){return r.existsSync(n)&&!i?(s||$("Skipped (exists): "+t.relative(process.cwd(),n)),!1):(r.mkdirSync(t.dirname(n),{recursive:!0}),r.copyFileSync(e,n),s||d("Created: "+l(t.relative(process.cwd(),n))),!0)}function ue(e,n,i){r.mkdirSync(n,{recursive:!0});for(const s of r.readdirSync(e,{withFileTypes:!0})){const u=t.join(e,s.name),y=t.join(n,s.name);s.isDirectory()?ue(u,y,i):T(u,y,i)}}function me(e,n=!1){const i=t.join(e,"package.json");if(!r.existsSync(i))return;const s=JSON.parse(r.readFileSync(i,"utf8"));s.scripts=s.scripts||{};let u=!1;const y={"inferno:check":"infernoflow check","inferno:status":"infernoflow status","inferno:gate":"infernoflow doc-gate","inferno:impact":"infernoflow pr-impact --json","inferno:sync":"infernoflow sync --auto --json","inferno:run":'infernoflow run "sync check" --provider auto --json',"inferno:hooks":"node scripts/inferno-install-hooks.mjs"};for(const[g,v]of Object.entries(y))s.scripts[g]||(s.scripts[g]=v,u=!0);u&&(r.writeFileSync(i,JSON.stringify(s,null,2)+`
2
- `,"utf8"),n||d("Updated "+l("package.json")+" scripts"))}function U(e){const n=t.join(e,"package.json");if(r.existsSync(n))try{const i=JSON.parse(r.readFileSync(n,"utf8"));if(i.name)return i.name.replace(/[^a-z0-9_-]/gi,"_")}catch{}return t.basename(e)}function ye(e,n,i){const s={policyId:n,policyVersion:1,capabilities:i,rules:{docsRequiredOnCapabilityChange:!0,requireScenarioForEachCapability:!0,requireChangelogOnCapabilityChange:!0}};r.writeFileSync(e,JSON.stringify(s,null,2)+`
3
- `)}function ge(e,n){const i={schemaVersion:1,capabilities:n.map(s=>({id:s,title:s.replace(/([A-Z])/g," $1").trim(),since:"0.1.0"}))};r.writeFileSync(e,JSON.stringify(i,null,2)+`
4
- `)}function we(e,n){r.mkdirSync(e,{recursive:!0});const i={scenarioId:"happy_path",description:"Basic happy-path flow covering all capabilities",capabilitiesCovered:n,steps:n.map(s=>({action:s,expect:`${s} works as expected`}))};r.writeFileSync(t.join(e,"happy_path.json"),JSON.stringify(i,null,2)+`
5
- `)}function K(e,n){const i=`# Changelog \u2014 ${n}
1
+ import * as fs from "node:fs";
2
+ import * as path from "node:path";
3
+ import * as readline from "node:readline";
4
+ import { fileURLToPath } from "node:url";
5
+ import { header, ok, warn, done, nextSteps, cyan, yellow, gray } from "../ui/output.mjs";
6
+ import {
7
+ discoverProjectSignals,
8
+ reviewCapabilitiesInteractive,
9
+ writeAdoptionBaseline,
10
+ buildAdoptionReport,
11
+ summarizeCapabilities,
12
+ buildSignalsReport,
13
+ } from "./adopt.mjs";
14
+ import { installCursorHooksArtifacts } from "../cursorHooksInstall.mjs";
15
+ import { installVsCodeCopilotHooksArtifacts } from "../vsCodeCopilotHooksInstall.mjs";
16
+
17
+ const __dirname = path.dirname(fileURLToPath(import.meta.url));
18
+
19
+ function getTemplatesRoot() {
20
+ return path.resolve(__dirname, "../../templates");
21
+ }
22
+
23
+ function ask(rl, question, defaultVal = "") {
24
+ return new Promise(resolve => {
25
+ const hint = defaultVal ? gray(` (${defaultVal})`) : "";
26
+ rl.question(` ${question}${hint}: `, answer => {
27
+ resolve(answer.trim() || defaultVal);
28
+ });
29
+ });
30
+ }
31
+
32
+ function getArgValue(args, ...flags) {
33
+ for (const flag of flags) {
34
+ const i = args.indexOf(flag);
35
+ if (i !== -1 && args[i + 1] && !args[i + 1].startsWith("-")) return args[i + 1];
36
+ }
37
+ return null;
38
+ }
39
+
40
+ function copyFile(src, dst, force, silent = false) {
41
+ if (fs.existsSync(dst) && !force) {
42
+ if (!silent) warn("Skipped (exists): " + path.relative(process.cwd(), dst));
43
+ return false;
44
+ }
45
+ fs.mkdirSync(path.dirname(dst), { recursive: true });
46
+ fs.copyFileSync(src, dst);
47
+ if (!silent) ok("Created: " + cyan(path.relative(process.cwd(), dst)));
48
+ return true;
49
+ }
50
+
51
+ function copyDirDeep(srcDir, dstDir, force) {
52
+ fs.mkdirSync(dstDir, { recursive: true });
53
+ for (const entry of fs.readdirSync(srcDir, { withFileTypes: true })) {
54
+ const src = path.join(srcDir, entry.name);
55
+ const dst = path.join(dstDir, entry.name);
56
+ if (entry.isDirectory()) copyDirDeep(src, dst, force);
57
+ else copyFile(src, dst, force);
58
+ }
59
+ }
60
+
61
+ function upsertScripts(cwd, silent = false) {
62
+ const pkgPath = path.join(cwd, "package.json");
63
+ if (!fs.existsSync(pkgPath)) return;
64
+ const pkg = JSON.parse(fs.readFileSync(pkgPath, "utf8"));
65
+ pkg.scripts = pkg.scripts || {};
66
+ let changed = false;
67
+ const toAdd = {
68
+ "inferno:check": "infernoflow check",
69
+ "inferno:status": "infernoflow status",
70
+ "inferno:gate": "infernoflow doc-gate",
71
+ "inferno:impact": "infernoflow pr-impact --json",
72
+ "inferno:sync": "infernoflow sync --auto --json",
73
+ "inferno:run": "infernoflow run \"sync check\" --provider auto --json",
74
+ "inferno:hooks": "node scripts/inferno-install-hooks.mjs"
75
+ };
76
+ for (const [k, v] of Object.entries(toAdd)) {
77
+ if (!pkg.scripts[k]) { pkg.scripts[k] = v; changed = true; }
78
+ }
79
+ if (changed) {
80
+ fs.writeFileSync(pkgPath, JSON.stringify(pkg, null, 2) + "\n", "utf8");
81
+ if (!silent) ok("Updated " + cyan("package.json") + " scripts");
82
+ }
83
+ }
84
+
85
+ function detectProjectName(cwd) {
86
+ const pkgPath = path.join(cwd, "package.json");
87
+ if (fs.existsSync(pkgPath)) {
88
+ try {
89
+ const pkg = JSON.parse(fs.readFileSync(pkgPath, "utf8"));
90
+ if (pkg.name) return pkg.name.replace(/[^a-z0-9_-]/gi, "_");
91
+ } catch {}
92
+ }
93
+ return path.basename(cwd);
94
+ }
95
+
96
+ function writeContract(contractPath, policyId, capabilities) {
97
+ const contract = {
98
+ policyId,
99
+ policyVersion: 1,
100
+ capabilities,
101
+ rules: {
102
+ docsRequiredOnCapabilityChange: true,
103
+ requireScenarioForEachCapability: true,
104
+ requireChangelogOnCapabilityChange: true
105
+ }
106
+ };
107
+ fs.writeFileSync(contractPath, JSON.stringify(contract, null, 2) + "\n");
108
+ }
109
+
110
+ function writeCapabilities(capsPath, capabilities) {
111
+ const registry = {
112
+ schemaVersion: 1,
113
+ capabilities: capabilities.map(id => ({
114
+ id,
115
+ title: id.replace(/([A-Z])/g, " $1").trim(),
116
+ since: "0.1.0"
117
+ }))
118
+ };
119
+ fs.writeFileSync(capsPath, JSON.stringify(registry, null, 2) + "\n");
120
+ }
121
+
122
+ function writeScenario(scenariosDir, capabilities) {
123
+ fs.mkdirSync(scenariosDir, { recursive: true });
124
+ const scenario = {
125
+ scenarioId: "happy_path",
126
+ description: "Basic happy-path flow covering all capabilities",
127
+ capabilitiesCovered: capabilities,
128
+ steps: capabilities.map(c => ({
129
+ action: c,
130
+ expect: `${c} works as expected`
131
+ }))
132
+ };
133
+ fs.writeFileSync(
134
+ path.join(scenariosDir, "happy_path.json"),
135
+ JSON.stringify(scenario, null, 2) + "\n"
136
+ );
137
+ }
138
+
139
+ function writeChangelog(changelogPath, policyId) {
140
+ const content = `# Changelog — ${policyId}
6
141
 
7
142
  ## Unreleased
8
143
 
9
144
  - Initial capabilities defined
10
145
 
11
- ## 0.1.0 \u2014 Initial release
146
+ ## 0.1.0 Initial release
12
147
 
13
148
  - Project initialized with infernoflow
14
- `;r.writeFileSync(e,i)}async function Ce(e){const n=process.cwd(),i=e.includes("--force")||e.includes("-f"),s=e.includes("--yes")||e.includes("-y"),u=e.includes("--adopt"),y=e.indexOf("--template"),g=y!==-1?e[y+1]:null;if(g){let a;try{a=await import("../templates/index.mjs")}catch{}const o=a?.getTemplate(g);if(!o){const c=a?a.listTemplates().map(j=>j.name).join(", "):"rest-api, nextjs, cli, graphql, monorepo";$(`Unknown template: ${g}. Available: ${c}`),process.exit(1)}const f=t.join(n,"inferno"),h=t.join(f,"scenarios");r.existsSync(f)||r.mkdirSync(f,{recursive:!0}),r.existsSync(h)||r.mkdirSync(h,{recursive:!0});const x=U(n),m=o.capabilities;r.writeFileSync(t.join(f,"contract.json"),JSON.stringify({policyId:x,policyVersion:1,capabilities:m.map(c=>c.id)},null,2)+`
15
- `),r.writeFileSync(t.join(f,"capabilities.json"),JSON.stringify({capabilities:m.map(c=>({id:c.id,description:c.description,since:new Date().toISOString().slice(0,10),source:`template:${g}`}))},null,2)+`
16
- `);for(const c of m)r.writeFileSync(t.join(h,`${c.id}.json`),JSON.stringify({id:`${c.id}-happy-path`,capability:c.id,description:`Happy path for ${c.description||c.id}`,steps:[{action:"invoke",target:c.id,input:{}},{action:"assert",field:"status",value:"success"}],capabilitiesCovered:[c.id]},null,2)+`
17
- `);K(t.join(f,"CHANGELOG.md"),x),r.writeFileSync(t.join(f,"CONTEXT.md"),`# ${x} \u2014 infernoflow context
18
-
19
- > Template: ${g} \u2014 ${o.description}
20
-
21
- ## Hint
22
- ${o.contextHint}
23
-
24
- ## Capabilities (${m.length})
25
- ${m.map(c=>`- \`${c.id}\`: ${c.description}`).join(`
26
- `)}
27
- `),o.scripts&&(info("Suggested package.json scripts for this template:"),Object.entries(o.scripts).forEach(([c,j])=>console.log(` ${bold(c)}: ${S(j)}`)),console.log()),D(`Initialised from template ${bold(l(g))} \u2014 ${bold(String(m.length))} capabilities`),console.log(),info(`Run ${l("infernoflow vibe")} to start vibe coding mode`),console.log();return}const v=e.includes("--cursor-hooks"),E=e.includes("--vscode-copilot-hooks"),q=e.includes("--report-json"),A=e.includes("--report-json-only"),H=e.includes("--report-human-only"),F=J(e,"--lang"),_=J(e,"--framework"),R=J(e,"--project-type"),p=A;A&&H&&(console.error("Error: --report-json-only and --report-human-only cannot be used together."),process.exit(1)),p||oe("init");const w=t.join(n,"inferno"),z=t.join(n,".github","workflows");r.existsSync(w)&&!i&&(p&&(console.log(JSON.stringify({ok:!1,error:"inferno_exists",hint:"Use --force to overwrite"},null,2)),process.exit(1)),$("inferno/ already exists. Use --force to overwrite."),console.log(),process.exit(0));const C=U(n),L="CreateTask, ReadTasks, UpdateTask, ToggleComplete, DeleteTask";let b=C,k=L.split(",").map(a=>a.trim());if(u){let o=I(n,{language:F||void 0,framework:_||void 0,projectType:R||void 0});if(!s&&!A){const m=G.createInterface({input:process.stdin,output:process.stdout}),c=o.developmentProfile||{},j=c.detected||{};console.log(S(` Review inferred development stack (press Enter to accept detected values)
28
- `));const Z=await P(m,"Language",c.language||j.language||"unknown"),Q=await P(m,"Framework",c.framework||j.framework||"unknown"),ee=await P(m,"Project type",c.projectType||j.projectType||"unknown");m.close(),o=I(n,{language:Z,framework:Q,projectType:ee})}const f=o.capabilities,h=ae(f);A?console.log(JSON.stringify({mode:"adopt",policyId:C,inferredCapabilities:h,components:o.components,displayFields:o.displayFields,externalLibraries:o.externalLibraries,uiLayout:o.uiLayout,styling:o.styling,developmentProfile:o.developmentProfile,apiCalls:o.apiCalls},null,2)):(console.log(),console.log(S(re(f))),console.log(),console.log(S(ce(o))),console.log(),q&&!H&&(console.log(JSON.stringify({mode:"adopt",policyId:C,inferredCapabilities:h,components:o.components,displayFields:o.displayFields,externalLibraries:o.externalLibraries,uiLayout:o.uiLayout,styling:o.styling,developmentProfile:o.developmentProfile,apiCalls:o.apiCalls},null,2)),console.log()));const x=await te(f,s);b=C,k=x.map(m=>m.id)}else if(!s){const a=G.createInterface({input:process.stdin,output:process.stdout});console.log(S(` Press Enter to accept defaults
29
- `)),b=await P(a,"Project / policy name",C),k=(await P(a,"Capabilities (comma-separated)",L)).split(",").map(f=>f.trim()).filter(Boolean),a.close(),console.log()}if(r.mkdirSync(w,{recursive:!0}),u){const a=k.map(f=>({id:f,title:f.replace(/([A-Z])/g," $1").trim()})),o=I(n,{language:F||void 0,framework:_||void 0,projectType:R||void 0});se(w,b,a,o),p||(d("Created: "+l("inferno/contract.json")),d("Created: "+l("inferno/capabilities.json")),d("Created: "+l("inferno/scenarios/adoption_baseline.json")),d("Created: "+l("inferno/adoption_profile.json")),d("Created: "+l("inferno/CHANGELOG.md")))}else ye(t.join(w,"contract.json"),b,k),p||d("Created: "+l("inferno/contract.json")),ge(t.join(w,"capabilities.json"),k),p||d("Created: "+l("inferno/capabilities.json")),we(t.join(w,"scenarios"),k),p||d("Created: "+l("inferno/scenarios/happy_path.json")),K(t.join(w,"CHANGELOG.md"),b),p||d("Created: "+l("inferno/CHANGELOG.md"));const O=de(),W=t.join(O,"scripts","inferno-doc-gate.mjs"),Y=t.join(n,"scripts","inferno-doc-gate.mjs");T(W,Y,i,p);const V=t.join(O,"scripts","inferno-install-hooks.mjs"),B=t.join(n,"scripts","inferno-install-hooks.mjs");T(V,B,i,p);const X=t.join(O,"ci","github-inferno-check.yml"),M=t.join(z,"infernoflow-check.yml");if(T(X,M,i,p),me(n,p),v&&le({cwd:n,templatesRoot:O,force:i,silent:p,logOk:a=>{p||d(a)},logWarn:a=>{p||$(a)}}),E&&pe({cwd:n,templatesRoot:O,force:i,silent:p,logOk:a=>{p||d(a)},logWarn:a=>{p||$(a)}}),u){const a=t.join(w,"context-state.json");let o={};try{o=JSON.parse(r.readFileSync(a,"utf8"))}catch{}const f=I(n,{language:F||void 0,framework:_||void 0,projectType:R||void 0});o.stack=f.developmentProfile,r.writeFileSync(a,JSON.stringify(o,null,2)+`
30
- `,"utf8"),p||d("Created: "+l("inferno/context-state.json"))}if(!p){D("infernoflow initialized!");const a=t.join(w,"integrations.json");!(process.env.ANTHROPIC_API_KEY||process.env.OPENAI_API_KEY||process.env.GOOGLE_AI_API_KEY||process.env.OPENROUTER_API_KEY||process.env.GEMINI_API_KEY)&&!r.existsSync(a)&&(console.log(),console.log(` ${N("\u{1F4A1}")} ${bold("Tip:")} connect an AI provider for explain, why, review, and changelog AI.`),console.log(` ${l("infernoflow ai setup")} \u2014 takes 60 seconds`)),ie([l("infernoflow status")+" \u2014 see your contract at a glance",l("infernoflow check")+" \u2014 validate everything",(u?"Review inferred baseline in ":"Edit ")+N("inferno/capabilities.json")+(u?" and refine IDs/titles":" to describe each capability in detail"),"Add more "+N("inferno/scenarios/*.json")+" files for edge cases","Add "+l("inferno:check")+" to your CI pipeline",...v?["Restart Cursor \u2014 hooks write assistant text to "+N("inferno/CONTEXT.draft.md"),"Promote when ready: "+l("npm run inferno:promote-draft -- --append-notes")]:[],...E?["Restart VS Code \u2014 Copilot hooks append prompts + assistant (from transcript) to "+N("inferno/CONTEXT.draft.md"),"Promote when ready: "+l("npm run inferno:promote-draft -- --append-notes")]:[],...!v&&!E?["Optional: "+l("infernoflow install-cursor-hooks")+" or "+l("infernoflow install-vscode-copilot-hooks")]:[]])}}export{Ce as initCommand};
149
+ `;
150
+ fs.writeFileSync(changelogPath, content);
151
+ }
152
+
153
+ // ── Lite init — minimal footprint for small projects ─────────────────────────
154
+
155
+ async function initLite(cwd, force) {
156
+ const { bold, cyan, gray, green, yellow, red } = await import("../ui/output.mjs");
157
+
158
+ console.log("\n " + bold("🔥 infernoflow init --lite"));
159
+ console.log(" " + "─".repeat(50) + "\n");
160
+ console.log(gray(" Lite mode: 3 files, no scripts, no workflows, no hooks."));
161
+ console.log(gray(" Use `infernoflow upgrade` later to expand to the full setup.\n"));
162
+
163
+ const infernoDir = path.join(cwd, "inferno");
164
+
165
+ if (fs.existsSync(infernoDir) && !force) {
166
+ console.log(yellow(" ⚠ inferno/ already exists. Use --force to overwrite.\n"));
167
+ process.exit(0);
168
+ }
169
+
170
+ fs.mkdirSync(infernoDir, { recursive: true });
171
+
172
+ const policyId = detectProjectName(cwd);
173
+
174
+ // Prompt for a one-liner intent (skip if --yes)
175
+ let intent = "";
176
+ if (!process.argv.includes("--yes") && !process.argv.includes("-y") && process.stdin.isTTY) {
177
+ const rl = readline.createInterface({ input: process.stdin, output: process.stdout });
178
+ intent = await new Promise(resolve => {
179
+ rl.question(gray(" What does this project do? (one line, Enter to skip): "), ans => {
180
+ rl.close();
181
+ resolve(ans.trim());
182
+ });
183
+ });
184
+ }
185
+
186
+ // contract.json — minimal, no rules bloat
187
+ const contract = {
188
+ policyId,
189
+ policyVersion: 1,
190
+ lite: true,
191
+ capabilities: [],
192
+ intent: intent || undefined,
193
+ };
194
+ fs.writeFileSync(path.join(infernoDir, "contract.json"), JSON.stringify(contract, null, 2) + "\n");
195
+
196
+ // capabilities.json — bare array (lite projects stay flat)
197
+ fs.writeFileSync(path.join(infernoDir, "capabilities.json"), JSON.stringify([], null, 2) + "\n");
198
+
199
+ // sessions.jsonl — empty, ready to receive log entries
200
+ fs.writeFileSync(path.join(infernoDir, "sessions.jsonl"), "", "utf8");
201
+
202
+ // .lite marker — lets `infernoflow upgrade` know the mode
203
+ fs.writeFileSync(path.join(infernoDir, ".lite"), "1", "utf8");
204
+
205
+ console.log(green(" ✔ Created inferno/contract.json"));
206
+ console.log(green(" ✔ Created inferno/capabilities.json"));
207
+ console.log(green(" ✔ Created inferno/sessions.jsonl"));
208
+ console.log();
209
+ console.log(" " + bold("Ready. Start using it:"));
210
+ console.log(" " + cyan("infernoflow log") + gray(' "what you\'re building" --type note'));
211
+ console.log(" " + cyan("infernoflow theme") + gray(" — scan your fonts + colors"));
212
+ console.log(" " + cyan("infernoflow context") + gray(" — generate AI context to paste"));
213
+ console.log(" " + cyan("infernoflow upgrade") + gray(" — expand to full setup when you need it"));
214
+ console.log();
215
+ }
216
+
217
+ export async function initCommand(args) {
218
+ const cwd = process.cwd();
219
+ const force = args.includes("--force") || args.includes("-f");
220
+ const yes = args.includes("--yes") || args.includes("-y");
221
+ const adopt = args.includes("--adopt");
222
+
223
+ // ── Lite mode — tiny project, single directory, 3 files only ──────────────
224
+ if (args.includes("--lite")) {
225
+ await initLite(cwd, force);
226
+ return;
227
+ }
228
+
229
+ // ── Template shortcut ──────────────────────────────────────────────────────
230
+ const templateIdx = args.indexOf("--template");
231
+ const templateName = templateIdx !== -1 ? args[templateIdx + 1] : null;
232
+
233
+ if (templateName) {
234
+ let tmplMod;
235
+ try { tmplMod = await import("../templates/index.mjs"); } catch {}
236
+ const tmpl = tmplMod?.getTemplate(templateName);
237
+ if (!tmpl) {
238
+ const available = tmplMod ? tmplMod.listTemplates().map(t => t.name).join(", ") : "rest-api, nextjs, cli, graphql, monorepo";
239
+ warn(`Unknown template: ${templateName}. Available: ${available}`);
240
+ process.exit(1);
241
+ }
242
+
243
+ const infernoDir = path.join(cwd, "inferno");
244
+ const scenDir = path.join(infernoDir, "scenarios");
245
+ if (!fs.existsSync(infernoDir)) fs.mkdirSync(infernoDir, { recursive: true });
246
+ if (!fs.existsSync(scenDir)) fs.mkdirSync(scenDir, { recursive: true });
247
+
248
+ const policyId = detectProjectName(cwd);
249
+ const caps = tmpl.capabilities;
250
+
251
+ // Write contract.json
252
+ fs.writeFileSync(path.join(infernoDir, "contract.json"), JSON.stringify({
253
+ policyId, policyVersion: 1,
254
+ capabilities: caps.map(c => c.id),
255
+ }, null, 2) + "\n");
256
+
257
+ // Write capabilities.json
258
+ fs.writeFileSync(path.join(infernoDir, "capabilities.json"), JSON.stringify({
259
+ capabilities: caps.map(c => ({
260
+ id: c.id, description: c.description,
261
+ since: new Date().toISOString().slice(0, 10), source: `template:${templateName}`,
262
+ })),
263
+ }, null, 2) + "\n");
264
+
265
+ // Write one scenario per capability
266
+ for (const cap of caps) {
267
+ fs.writeFileSync(path.join(scenDir, `${cap.id}.json`), JSON.stringify({
268
+ id: `${cap.id}-happy-path`, capability: cap.id,
269
+ description: `Happy path for ${cap.description || cap.id}`,
270
+ steps: [
271
+ { action: "invoke", target: cap.id, input: {} },
272
+ { action: "assert", field: "status", value: "success" },
273
+ ],
274
+ capabilitiesCovered: [cap.id],
275
+ }, null, 2) + "\n");
276
+ }
277
+
278
+ // Write CHANGELOG.md
279
+ writeChangelog(path.join(infernoDir, "CHANGELOG.md"), policyId);
280
+
281
+ // Write CONTEXT.md hint
282
+ fs.writeFileSync(path.join(infernoDir, "CONTEXT.md"),
283
+ `# ${policyId} — infernoflow context\n\n> Template: ${templateName} — ${tmpl.description}\n\n## Hint\n${tmpl.contextHint}\n\n## Capabilities (${caps.length})\n${caps.map(c => `- \`${c.id}\`: ${c.description}`).join("\n")}\n`
284
+ );
285
+
286
+ if (tmpl.scripts) {
287
+ info(`Suggested package.json scripts for this template:`);
288
+ Object.entries(tmpl.scripts).forEach(([k, v]) => console.log(` ${bold(k)}: ${gray(v)}`));
289
+ console.log();
290
+ }
291
+
292
+ done(`Initialised from template ${bold(cyan(templateName))} — ${bold(String(caps.length))} capabilities`);
293
+ console.log();
294
+ info(`Run ${cyan("infernoflow vibe")} to start vibe coding mode`);
295
+ console.log();
296
+ return;
297
+ }
298
+ const cursorHooks = args.includes("--cursor-hooks");
299
+ const vscodeCopilotHooks = args.includes("--vscode-copilot-hooks");
300
+ const reportJson = args.includes("--report-json");
301
+ const reportJsonOnly = args.includes("--report-json-only");
302
+ const reportHumanOnly = args.includes("--report-human-only");
303
+ const langOverride = getArgValue(args, "--lang");
304
+ const frameworkOverride = getArgValue(args, "--framework");
305
+ const projectTypeOverride = getArgValue(args, "--project-type");
306
+ const silent = reportJsonOnly;
307
+
308
+ if (reportJsonOnly && reportHumanOnly) {
309
+ console.error("Error: --report-json-only and --report-human-only cannot be used together.");
310
+ process.exit(1);
311
+ }
312
+
313
+ if (!silent) {
314
+ header("init");
315
+ }
316
+
317
+ const infernoDir = path.join(cwd, "inferno");
318
+ const workflowsDir = path.join(cwd, ".github", "workflows");
319
+ if (fs.existsSync(infernoDir) && !force) {
320
+ if (silent) {
321
+ console.log(JSON.stringify({ ok: false, error: "inferno_exists", hint: "Use --force to overwrite" }, null, 2));
322
+ process.exit(1);
323
+ }
324
+ warn("inferno/ already exists. Use --force to overwrite.");
325
+ console.log();
326
+ process.exit(0);
327
+ }
328
+
329
+ const detectedName = detectProjectName(cwd);
330
+ const defaultCaps = "CreateTask, ReadTasks, UpdateTask, ToggleComplete, DeleteTask";
331
+
332
+ let policyId = detectedName;
333
+ let capabilities = defaultCaps.split(",").map(c => c.trim());
334
+
335
+ if (adopt) {
336
+ const profileOverrides = {
337
+ language: langOverride || undefined,
338
+ framework: frameworkOverride || undefined,
339
+ projectType: projectTypeOverride || undefined,
340
+ };
341
+ let signals = discoverProjectSignals(cwd, profileOverrides);
342
+ if (!yes && !reportJsonOnly) {
343
+ const rl = readline.createInterface({ input: process.stdin, output: process.stdout });
344
+ const profile = signals.developmentProfile || {};
345
+ const detected = profile.detected || {};
346
+ console.log(gray(" Review inferred development stack (press Enter to accept detected values)\n"));
347
+ const language = await ask(rl, "Language", profile.language || detected.language || "unknown");
348
+ const framework = await ask(rl, "Framework", profile.framework || detected.framework || "unknown");
349
+ const projectType = await ask(rl, "Project type", profile.projectType || detected.projectType || "unknown");
350
+ rl.close();
351
+ signals = discoverProjectSignals(cwd, { language, framework, projectType });
352
+ }
353
+ const inferred = signals.capabilities;
354
+ const summarized = summarizeCapabilities(inferred);
355
+ if (reportJsonOnly) {
356
+ console.log(
357
+ JSON.stringify(
358
+ {
359
+ mode: "adopt",
360
+ policyId: detectedName,
361
+ inferredCapabilities: summarized,
362
+ components: signals.components,
363
+ displayFields: signals.displayFields,
364
+ externalLibraries: signals.externalLibraries,
365
+ uiLayout: signals.uiLayout,
366
+ styling: signals.styling,
367
+ developmentProfile: signals.developmentProfile,
368
+ apiCalls: signals.apiCalls,
369
+ },
370
+ null,
371
+ 2
372
+ )
373
+ );
374
+ } else {
375
+ console.log();
376
+ console.log(gray(buildAdoptionReport(inferred)));
377
+ console.log();
378
+ console.log(gray(buildSignalsReport(signals)));
379
+ console.log();
380
+ if (reportJson && !reportHumanOnly) {
381
+ console.log(
382
+ JSON.stringify(
383
+ {
384
+ mode: "adopt",
385
+ policyId: detectedName,
386
+ inferredCapabilities: summarized,
387
+ components: signals.components,
388
+ displayFields: signals.displayFields,
389
+ externalLibraries: signals.externalLibraries,
390
+ uiLayout: signals.uiLayout,
391
+ styling: signals.styling,
392
+ developmentProfile: signals.developmentProfile,
393
+ apiCalls: signals.apiCalls,
394
+ },
395
+ null,
396
+ 2
397
+ )
398
+ );
399
+ console.log();
400
+ }
401
+ }
402
+ const reviewed = await reviewCapabilitiesInteractive(inferred, yes);
403
+ policyId = detectedName;
404
+ capabilities = reviewed.map((c) => c.id);
405
+ } else if (!yes) {
406
+ const rl = readline.createInterface({ input: process.stdin, output: process.stdout });
407
+ console.log(gray(" Press Enter to accept defaults\n"));
408
+ policyId = await ask(rl, "Project / policy name", detectedName);
409
+ const capsRaw = await ask(rl, "Capabilities (comma-separated)", defaultCaps);
410
+ capabilities = capsRaw.split(",").map(c => c.trim()).filter(Boolean);
411
+ rl.close();
412
+ console.log();
413
+ }
414
+
415
+ // Write files
416
+ fs.mkdirSync(infernoDir, { recursive: true });
417
+
418
+ if (adopt) {
419
+ const capDetails = capabilities.map((id) => ({
420
+ id,
421
+ title: id.replace(/([A-Z])/g, " $1").trim(),
422
+ }));
423
+ const signals = discoverProjectSignals(cwd, {
424
+ language: langOverride || undefined,
425
+ framework: frameworkOverride || undefined,
426
+ projectType: projectTypeOverride || undefined,
427
+ });
428
+ writeAdoptionBaseline(infernoDir, policyId, capDetails, signals);
429
+ if (!silent) {
430
+ ok("Created: " + cyan("inferno/contract.json"));
431
+ ok("Created: " + cyan("inferno/capabilities.json"));
432
+ ok("Created: " + cyan("inferno/scenarios/adoption_baseline.json"));
433
+ ok("Created: " + cyan("inferno/adoption_profile.json"));
434
+ ok("Created: " + cyan("inferno/CHANGELOG.md"));
435
+ }
436
+ } else {
437
+ writeContract(path.join(infernoDir, "contract.json"), policyId, capabilities);
438
+ if (!silent) ok("Created: " + cyan("inferno/contract.json"));
439
+
440
+ writeCapabilities(path.join(infernoDir, "capabilities.json"), capabilities);
441
+ if (!silent) ok("Created: " + cyan("inferno/capabilities.json"));
442
+
443
+ writeScenario(path.join(infernoDir, "scenarios"), capabilities);
444
+ if (!silent) ok("Created: " + cyan("inferno/scenarios/happy_path.json"));
445
+
446
+ writeChangelog(path.join(infernoDir, "CHANGELOG.md"), policyId);
447
+ if (!silent) ok("Created: " + cyan("inferno/CHANGELOG.md"));
448
+ }
449
+
450
+ // Copy doc-gate script
451
+ const templates = getTemplatesRoot();
452
+ const srcScript = path.join(templates, "scripts", "inferno-doc-gate.mjs");
453
+ const dstScript = path.join(cwd, "scripts", "inferno-doc-gate.mjs");
454
+ copyFile(srcScript, dstScript, force, silent);
455
+ const srcHookScript = path.join(templates, "scripts", "inferno-install-hooks.mjs");
456
+ const dstHookScript = path.join(cwd, "scripts", "inferno-install-hooks.mjs");
457
+ copyFile(srcHookScript, dstHookScript, force, silent);
458
+ const srcWorkflow = path.join(templates, "ci", "github-inferno-check.yml");
459
+ const dstWorkflow = path.join(workflowsDir, "infernoflow-check.yml");
460
+ copyFile(srcWorkflow, dstWorkflow, force, silent);
461
+
462
+ upsertScripts(cwd, silent);
463
+
464
+ if (cursorHooks) {
465
+ installCursorHooksArtifacts({
466
+ cwd,
467
+ templatesRoot: templates,
468
+ force,
469
+ silent,
470
+ logOk: (msg) => {
471
+ if (!silent) ok(msg);
472
+ },
473
+ logWarn: (msg) => {
474
+ if (!silent) warn(msg);
475
+ },
476
+ });
477
+ }
478
+ if (vscodeCopilotHooks) {
479
+ installVsCodeCopilotHooksArtifacts({
480
+ cwd,
481
+ templatesRoot: templates,
482
+ force,
483
+ silent,
484
+ logOk: (msg) => {
485
+ if (!silent) ok(msg);
486
+ },
487
+ logWarn: (msg) => {
488
+ if (!silent) warn(msg);
489
+ },
490
+ });
491
+ }
492
+
493
+ if (adopt) {
494
+ const statePath = path.join(infernoDir, "context-state.json");
495
+ let state = {};
496
+ try {
497
+ state = JSON.parse(fs.readFileSync(statePath, "utf8"));
498
+ } catch {}
499
+ const signals = discoverProjectSignals(cwd, {
500
+ language: langOverride || undefined,
501
+ framework: frameworkOverride || undefined,
502
+ projectType: projectTypeOverride || undefined,
503
+ });
504
+ state.stack = signals.developmentProfile;
505
+ fs.writeFileSync(statePath, JSON.stringify(state, null, 2) + "\n", "utf8");
506
+ if (!silent) ok("Created: " + cyan("inferno/context-state.json"));
507
+ }
508
+
509
+ if (!silent) {
510
+ done("infernoflow initialized!");
511
+
512
+ // AI provider nudge — show once at init if nothing is configured
513
+ const intPath = path.join(infernoDir, "integrations.json");
514
+ const hasAiKey = process.env.ANTHROPIC_API_KEY || process.env.OPENAI_API_KEY ||
515
+ process.env.GOOGLE_AI_API_KEY || process.env.OPENROUTER_API_KEY ||
516
+ process.env.GEMINI_API_KEY;
517
+ if (!hasAiKey && !fs.existsSync(intPath)) {
518
+ console.log();
519
+ console.log(` ${yellow("💡")} ${bold("Tip:")} connect an AI provider for explain, why, review, and changelog AI.`);
520
+ console.log(` ${cyan("infernoflow ai setup")} — takes 60 seconds`);
521
+ }
522
+
523
+ nextSteps([
524
+ cyan("infernoflow status") + " — see your contract at a glance",
525
+ cyan("infernoflow check") + " — validate everything",
526
+ (adopt ? "Review inferred baseline in " : "Edit ") + yellow("inferno/capabilities.json") + (adopt ? " and refine IDs/titles" : " to describe each capability in detail"),
527
+ "Add more " + yellow("inferno/scenarios/*.json") + " files for edge cases",
528
+ "Add " + cyan("inferno:check") + " to your CI pipeline",
529
+ ...(cursorHooks
530
+ ? [
531
+ "Restart Cursor — hooks write assistant text to " + yellow("inferno/CONTEXT.draft.md"),
532
+ "Promote when ready: " + cyan("npm run inferno:promote-draft -- --append-notes"),
533
+ ]
534
+ : []),
535
+ ...(vscodeCopilotHooks
536
+ ? [
537
+ "Restart VS Code — Copilot hooks append prompts + assistant (from transcript) to " +
538
+ yellow("inferno/CONTEXT.draft.md"),
539
+ "Promote when ready: " + cyan("npm run inferno:promote-draft -- --append-notes"),
540
+ ]
541
+ : []),
542
+ ...(!cursorHooks && !vscodeCopilotHooks
543
+ ? [
544
+ "Optional: " +
545
+ cyan("infernoflow install-cursor-hooks") +
546
+ " or " +
547
+ cyan("infernoflow install-vscode-copilot-hooks"),
548
+ ]
549
+ : []),
550
+ ]);
551
+ }
552
+ }
@@ -1 +1,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
+ 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 +1,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
+ 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
+ }