@noir-ai/cli 1.6.0-beta.1 → 1.7.0-beta.1

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 (45) hide show
  1. package/README.md +1 -1
  2. package/dist/bin.js +81 -8
  3. package/dist/bin.js.map +1 -1
  4. package/dist/{chunk-SVFQGAOF.js → chunk-3E7ZT5OG.js} +5 -3
  5. package/dist/{chunk-SVFQGAOF.js.map → chunk-3E7ZT5OG.js.map} +1 -1
  6. package/dist/chunk-47ITYQ3A.js +203 -0
  7. package/dist/chunk-47ITYQ3A.js.map +1 -0
  8. package/dist/chunk-DQXQQDJN.js +310 -0
  9. package/dist/chunk-DQXQQDJN.js.map +1 -0
  10. package/dist/chunk-RYE3BUGP.js +174 -0
  11. package/dist/chunk-RYE3BUGP.js.map +1 -0
  12. package/dist/{chunk-YES5UCU5.js → chunk-WS7K4NKA.js} +59 -311
  13. package/dist/chunk-WS7K4NKA.js.map +1 -0
  14. package/dist/{create-7ERBL42K.js → create-P4M52LZZ.js} +6 -4
  15. package/dist/{create-7ERBL42K.js.map → create-P4M52LZZ.js.map} +1 -1
  16. package/dist/index.js +3 -2
  17. package/dist/install-XACGZ235.js +13 -0
  18. package/dist/install-XACGZ235.js.map +1 -0
  19. package/dist/{sync-SCAF6Z5H.js → sync-35DR6DNN.js} +5 -3
  20. package/dist/{sync-SCAF6Z5H.js.map → sync-35DR6DNN.js.map} +1 -1
  21. package/dist/tui/chunk-63KFZH6T.js +202 -0
  22. package/dist/tui/chunk-63KFZH6T.js.map +1 -0
  23. package/dist/tui/{chunk-YNEBQJXA.js → chunk-BDFQ5S3K.js} +59 -311
  24. package/dist/tui/chunk-BDFQ5S3K.js.map +1 -0
  25. package/dist/tui/chunk-CMGOOU7L.js +173 -0
  26. package/dist/tui/chunk-CMGOOU7L.js.map +1 -0
  27. package/dist/tui/chunk-L5ZKEOQV.js +309 -0
  28. package/dist/tui/chunk-L5ZKEOQV.js.map +1 -0
  29. package/dist/tui/{chunk-OMF7XZMN.js → chunk-LHT7M6DO.js} +5 -3
  30. package/dist/tui/{chunk-OMF7XZMN.js.map → chunk-LHT7M6DO.js.map} +1 -1
  31. package/dist/tui/{create-U2YJH5QC.js → create-IZSOFSO7.js} +6 -4
  32. package/dist/tui/{create-U2YJH5QC.js.map → create-IZSOFSO7.js.map} +1 -1
  33. package/dist/tui/index.js +86 -13
  34. package/dist/tui/index.js.map +1 -1
  35. package/dist/tui/install-357OEHOZ.js +12 -0
  36. package/dist/tui/install-357OEHOZ.js.map +1 -0
  37. package/dist/tui/{sync-VYRXCGK4.js → sync-32TP7XB6.js} +5 -3
  38. package/dist/tui/{sync-VYRXCGK4.js.map → sync-32TP7XB6.js.map} +1 -1
  39. package/dist/tui/update-7HWOY55V.js +15 -0
  40. package/dist/tui/update-7HWOY55V.js.map +1 -0
  41. package/dist/update-J3TAORFX.js +16 -0
  42. package/dist/update-J3TAORFX.js.map +1 -0
  43. package/package.json +9 -9
  44. package/dist/chunk-YES5UCU5.js.map +0 -1
  45. package/dist/tui/chunk-YNEBQJXA.js.map +0 -1
@@ -1,9 +1,11 @@
1
1
  #!/usr/bin/env node
2
2
  import {
3
3
  buildConflictOpts,
4
- checkWritePathDedup,
4
+ checkWritePathDedup
5
+ } from "./chunk-WS7K4NKA.js";
6
+ import {
5
7
  resolveInteractive
6
- } from "./chunk-YES5UCU5.js";
8
+ } from "./chunk-DQXQQDJN.js";
7
9
 
8
10
  // src/init.ts
9
11
  import { resolveAdapter } from "@noir-ai/adapters";
@@ -97,4 +99,4 @@ export {
97
99
  init,
98
100
  assertTransportUrl
99
101
  };
100
- //# sourceMappingURL=chunk-SVFQGAOF.js.map
102
+ //# sourceMappingURL=chunk-3E7ZT5OG.js.map
@@ -1 +1 @@
1
- {"version":3,"sources":["../src/init.ts"],"sourcesContent":["// `noir init` — first-run + `--upgrade`.\n//\n// Slice S-T2 refactor: the ad-hoc writers (syncIgnores, writeManagedRegion,\n// writeFileSync for .mcp.json/project.id/config.yml/NOIR.md/RULES.md) are\n// replaced by a single call into `@noir-ai/create`'s `scaffold({mode:'init'})`.\n// The manifest + three-mode writer are the source of truth for what init\n// emits; this module is now a thin caller that owns ONLY:\n// - the transport/url precondition (the localhost security gate, whose\n// error strings are locked by url-validation.test.ts), and\n// - skills emission (out-of-manifest by design — composed after scaffold()).\n//\n// S10 multi-host: the 8 direct `claudeAdapter` imports across init/sync/create\n// collapsed to `resolveAdapter(host)` where `host` comes from `--host <id>`\n// (default `'claude'`). The adapter drives (a) the manifest via\n// `scaffold({host})` and (b) skills emission — claude/cursor have a skill dir;\n// gemini/agents-md/opencode have no skill concept and the call is skipped.\n//\n// Deliberate behavior changes vs the predecessor (spec-aligned latent-bug\n// fixes; see S-T1 contract notes + CHANGELOG):\n// - `.noir/project.id` → skipIfExists (predecessor overwrote on every init,\n// orphaning the store DB named after the id). Re-init now preserves it.\n// - `.noir/config.yml` → skipIfExists (predecessor overwrote).\n// - `.noir/NOIR.md` → managedBlock with BRIEF_BLOCK markers (predecessor\n// wrote the whole file with no markers). First-run output GAINS markers;\n// user notes outside the markers survive re-runs.\n// - `.noir/scaffold-version` is now stamped on init/create (engine-owned).\n\nimport { type HostId, resolveAdapter } from '@noir-ai/adapters';\nimport { loadProjectInfo, type ProjectInfo } from '@noir-ai/core';\nimport { type ScaffoldResult, scaffold } from '@noir-ai/create';\nimport { type CompileTarget, emitSkillsToDir } from '@noir-ai/skills';\nimport { buildConflictOpts, type ScaffoldConflictOpts } from './conflict.js';\nimport { checkWritePathDedup } from './dedup-write.js';\nimport { resolveInteractive } from './output.js';\n\nexport interface InitOptions {\n transport: 'stdio' | 'streamable-http';\n url?: string;\n /** `noir init --upgrade`: run scaffold migrations from the on-disk\n * scaffold-version to current, then re-emit ONLY the runtime subset\n * (regenerate + managedBlock). skipIfExists seeds are left alone so user\n * edits survive. */\n upgrade?: boolean;\n /** S10 target host. Defaults to `'claude'` (the regression anchor). Drives\n * both scaffold emission (the manifest's host-specific half) and skills\n * emission (skipped for hosts with no `skillsDir`). */\n host?: HostId;\n /** SP-A: re-scaffold even if already initialized (bypasses the\n * already-initialized no-op guard in scaffold()). */\n force?: boolean;\n}\n\n/**\n * Initialize Noir in `root`. Returns the {@link ScaffoldResult} (with\n * structured `conflicts[]` + any dedup records appended) so `--json`\n * callers can surface conflict detail. `undefined` when the already-initialized\n * guard short-circuited (a no-op).\n */\nexport async function init(root: string, opts: InitOptions): Promise<ScaffoldResult | undefined> {\n assertTransportUrl(opts);\n\n const host: HostId = opts.host ?? 'claude';\n // The engine reads ScaffoldOptions.interactive (hermetic — never\n // process.env). The CLI derives it once from the bridge + TTY/CI/NO_COLOR gate.\n const interactive = resolveInteractive();\n const conflictOpts = buildConflictOpts({ force: opts.force, interactive });\n\n const res = await scaffold({\n root,\n mode: 'init',\n host,\n transport: opts.transport,\n interactive,\n ...(opts.url !== undefined ? { url: opts.url } : {}),\n ...(opts.upgrade === true ? { upgrade: true } : {}),\n ...(opts.force === true ? { force: true } : {}),\n ...conflictOpts,\n });\n // SP-A: if the already-initialized guard no-op'd scaffold, stop — don't re-emit\n // skills or print \"initialized\" (scaffold already printed the no-op message).\n if (res.noop) return res;\n\n // Thread the SAME conflictOpts into skills emission so the\n // skills-emit conflict flow is LIVE in interactive mode (the producer\n // accepts conflict opts; this closes the wiring gap). The safe-default\n // `assertNotUserOwned` runs unconditionally inside the producer.\n await emitHostSkills(root, host, conflictOpts, interactive);\n\n // Write-path semantic dedup. Non-blocking; degrades to a\n // stderr warn-skip when the embedder is unavailable. Records near-dups on\n // `res.conflicts` so `--json` consumers see them without a prompt.\n // Best-effort project read: first-run init has no .noir/config.yml yet →\n // undefined → resolveEmbedder warn-skips. `init --force` on an existing\n // project reads the config and runs the dedup against existing host files.\n let projectInfo: ProjectInfo | undefined;\n try {\n projectInfo = loadProjectInfo(root);\n } catch {\n projectInfo = undefined;\n }\n const dedup = await checkWritePathDedup(root, res, { interactive, project: projectInfo });\n if (dedup.conflicts.length > 0) res.conflicts.push(...dedup.conflicts);\n\n process.stderr.write(\n `Noir initialized in ${root} (host: ${host}, transport: ${opts.transport}).\\n`,\n );\n process.stderr.write(\n 'Next: run `noir` to open the home menu (or `noir status` for a snapshot).\\n',\n );\n return res;\n}\n\n/**\n * Compose skill emission onto the resolved adapter's `skillsDir` (claude →\n * `.claude/skills/`; cursor → `.cursor/rules/` compiled as `.mdc`; the other\n * three hosts have no skill concept and are skipped with a stderr note).\n *\n * The `CompileTarget` matches the host id (S10 foundation widened the enum to\n * the same union) so cursor skills compile to the `.mdc` rule shape via\n * `compileSkill(_, 'cursor')`; the others keep the verbatim SKILL.md format.\n */\nasync function emitHostSkills(\n root: string,\n host: HostId,\n conflictOpts: ScaffoldConflictOpts,\n interactive: boolean,\n): Promise<void> {\n const adapter = resolveAdapter(host);\n const skillsDir = adapter.skillsDir?.({ root });\n if (skillsDir === undefined) {\n // N1: standardized wording — same phrase across init/sync/create so logs\n // grep uniformly. (Pre-N1 each command phrased this differently.)\n process.stderr.write(`host '${host}' has no skill emitter; skipping skills\\n`);\n return;\n }\n const target: CompileTarget = host;\n // Forward conflictPolicy + onConflict + interactive so an\n // interactive `noir init` with a conflicting skill emit consults the\n // resolver (clack menu + diff preview + apply-to-all); --json/--no-input\n // stays prompt-free via the `interactive: false` guard. The resolver shape\n // is structurally compatible with the skill emit seam (see skills/types.ts);\n // the cast isolates the literal-narrowing mismatch on the return type.\n type SkillEmitOpts = NonNullable<Parameters<typeof emitSkillsToDir>[1]>;\n const skillOpts: SkillEmitOpts = {\n includeIntegrations: true,\n target,\n conflictPolicy: conflictOpts.conflictPolicy,\n interactive,\n };\n if (conflictOpts.onConflict !== undefined) {\n skillOpts.onConflict = conflictOpts.onConflict as SkillEmitOpts['onConflict'];\n }\n const summary = await emitSkillsToDir(skillsDir, skillOpts);\n const relDir = skillsDir.replace(`${root}/`, '');\n process.stderr.write(\n `Emitted ${summary.emitted.length} Noir skills to ${relDir}/ (target: ${target}).\\n`,\n );\n}\n\n/**\n * Transport + URL precondition. The error strings here are the SECURITY GATE\n * locked by `url-validation.test.ts` — do not rephrase. Thrown errors are\n * plain `Error`s (bin.ts's `main().catch` maps them to stderr + exitCode 1).\n *\n * Exported so `noir create` (commands/create.ts) reuses the SAME gate instead\n * of duplicating the localhost allowlist.\n */\nexport function assertTransportUrl(opts: {\n transport: 'stdio' | 'streamable-http';\n url?: string;\n}): void {\n if (opts.transport === 'streamable-http' && opts.url === undefined) {\n throw new Error('--transport streamable-http requires --url');\n }\n if (opts.url !== undefined) {\n assertLocalhostUrl(opts.url);\n }\n}\n\n// Validate a streamable-http --url is http(s) and localhost-only. Gate 2's\n// daemon binds 127.0.0.1, so persisting a non-localhost URL is a footgun.\n// Not exported: the single external surface is assertTransportUrl(); thrown\n// errors propagate to bin.ts's main().catch (stderr + exitCode 1).\nfunction assertLocalhostUrl(raw: string): void {\n let url: URL;\n try {\n url = new URL(raw);\n } catch {\n throw new Error(`Invalid URL: ${raw}`);\n }\n if (!['http:', 'https:'].includes(url.protocol)) {\n throw new Error('Only http/https URLs are supported');\n }\n if (!['localhost', '127.0.0.1', '::1'].includes(url.hostname)) {\n throw new Error(`Only localhost URLs are supported (got ${url.hostname})`);\n }\n}\n"],"mappings":";;;;;;;;AA2BA,SAAsB,sBAAsB;AAC5C,SAAS,uBAAyC;AAClD,SAA8B,gBAAgB;AAC9C,SAA6B,uBAAuB;AA4BpD,eAAsB,KAAK,MAAc,MAAwD;AAC/F,qBAAmB,IAAI;AAEvB,QAAM,OAAe,KAAK,QAAQ;AAGlC,QAAM,cAAc,mBAAmB;AACvC,QAAM,eAAe,kBAAkB,EAAE,OAAO,KAAK,OAAO,YAAY,CAAC;AAEzE,QAAM,MAAM,MAAM,SAAS;AAAA,IACzB;AAAA,IACA,MAAM;AAAA,IACN;AAAA,IACA,WAAW,KAAK;AAAA,IAChB;AAAA,IACA,GAAI,KAAK,QAAQ,SAAY,EAAE,KAAK,KAAK,IAAI,IAAI,CAAC;AAAA,IAClD,GAAI,KAAK,YAAY,OAAO,EAAE,SAAS,KAAK,IAAI,CAAC;AAAA,IACjD,GAAI,KAAK,UAAU,OAAO,EAAE,OAAO,KAAK,IAAI,CAAC;AAAA,IAC7C,GAAG;AAAA,EACL,CAAC;AAGD,MAAI,IAAI,KAAM,QAAO;AAMrB,QAAM,eAAe,MAAM,MAAM,cAAc,WAAW;AAQ1D,MAAI;AACJ,MAAI;AACF,kBAAc,gBAAgB,IAAI;AAAA,EACpC,QAAQ;AACN,kBAAc;AAAA,EAChB;AACA,QAAM,QAAQ,MAAM,oBAAoB,MAAM,KAAK,EAAE,aAAa,SAAS,YAAY,CAAC;AACxF,MAAI,MAAM,UAAU,SAAS,EAAG,KAAI,UAAU,KAAK,GAAG,MAAM,SAAS;AAErE,UAAQ,OAAO;AAAA,IACb,uBAAuB,IAAI,WAAW,IAAI,gBAAgB,KAAK,SAAS;AAAA;AAAA,EAC1E;AACA,UAAQ,OAAO;AAAA,IACb;AAAA,EACF;AACA,SAAO;AACT;AAWA,eAAe,eACb,MACA,MACA,cACA,aACe;AACf,QAAM,UAAU,eAAe,IAAI;AACnC,QAAM,YAAY,QAAQ,YAAY,EAAE,KAAK,CAAC;AAC9C,MAAI,cAAc,QAAW;AAG3B,YAAQ,OAAO,MAAM,SAAS,IAAI;AAAA,CAA2C;AAC7E;AAAA,EACF;AACA,QAAM,SAAwB;AAQ9B,QAAM,YAA2B;AAAA,IAC/B,qBAAqB;AAAA,IACrB;AAAA,IACA,gBAAgB,aAAa;AAAA,IAC7B;AAAA,EACF;AACA,MAAI,aAAa,eAAe,QAAW;AACzC,cAAU,aAAa,aAAa;AAAA,EACtC;AACA,QAAM,UAAU,MAAM,gBAAgB,WAAW,SAAS;AAC1D,QAAM,SAAS,UAAU,QAAQ,GAAG,IAAI,KAAK,EAAE;AAC/C,UAAQ,OAAO;AAAA,IACb,WAAW,QAAQ,QAAQ,MAAM,mBAAmB,MAAM,cAAc,MAAM;AAAA;AAAA,EAChF;AACF;AAUO,SAAS,mBAAmB,MAG1B;AACP,MAAI,KAAK,cAAc,qBAAqB,KAAK,QAAQ,QAAW;AAClE,UAAM,IAAI,MAAM,4CAA4C;AAAA,EAC9D;AACA,MAAI,KAAK,QAAQ,QAAW;AAC1B,uBAAmB,KAAK,GAAG;AAAA,EAC7B;AACF;AAMA,SAAS,mBAAmB,KAAmB;AAC7C,MAAI;AACJ,MAAI;AACF,UAAM,IAAI,IAAI,GAAG;AAAA,EACnB,QAAQ;AACN,UAAM,IAAI,MAAM,gBAAgB,GAAG,EAAE;AAAA,EACvC;AACA,MAAI,CAAC,CAAC,SAAS,QAAQ,EAAE,SAAS,IAAI,QAAQ,GAAG;AAC/C,UAAM,IAAI,MAAM,oCAAoC;AAAA,EACtD;AACA,MAAI,CAAC,CAAC,aAAa,aAAa,KAAK,EAAE,SAAS,IAAI,QAAQ,GAAG;AAC7D,UAAM,IAAI,MAAM,0CAA0C,IAAI,QAAQ,GAAG;AAAA,EAC3E;AACF;","names":[]}
1
+ {"version":3,"sources":["../src/init.ts"],"sourcesContent":["// `noir init` — first-run + `--upgrade`.\n//\n// Slice S-T2 refactor: the ad-hoc writers (syncIgnores, writeManagedRegion,\n// writeFileSync for .mcp.json/project.id/config.yml/NOIR.md/RULES.md) are\n// replaced by a single call into `@noir-ai/create`'s `scaffold({mode:'init'})`.\n// The manifest + three-mode writer are the source of truth for what init\n// emits; this module is now a thin caller that owns ONLY:\n// - the transport/url precondition (the localhost security gate, whose\n// error strings are locked by url-validation.test.ts), and\n// - skills emission (out-of-manifest by design — composed after scaffold()).\n//\n// S10 multi-host: the 8 direct `claudeAdapter` imports across init/sync/create\n// collapsed to `resolveAdapter(host)` where `host` comes from `--host <id>`\n// (default `'claude'`). The adapter drives (a) the manifest via\n// `scaffold({host})` and (b) skills emission — claude/cursor have a skill dir;\n// gemini/agents-md/opencode have no skill concept and the call is skipped.\n//\n// Deliberate behavior changes vs the predecessor (spec-aligned latent-bug\n// fixes; see S-T1 contract notes + CHANGELOG):\n// - `.noir/project.id` → skipIfExists (predecessor overwrote on every init,\n// orphaning the store DB named after the id). Re-init now preserves it.\n// - `.noir/config.yml` → skipIfExists (predecessor overwrote).\n// - `.noir/NOIR.md` → managedBlock with BRIEF_BLOCK markers (predecessor\n// wrote the whole file with no markers). First-run output GAINS markers;\n// user notes outside the markers survive re-runs.\n// - `.noir/scaffold-version` is now stamped on init/create (engine-owned).\n\nimport { type HostId, resolveAdapter } from '@noir-ai/adapters';\nimport { loadProjectInfo, type ProjectInfo } from '@noir-ai/core';\nimport { type ScaffoldResult, scaffold } from '@noir-ai/create';\nimport { type CompileTarget, emitSkillsToDir } from '@noir-ai/skills';\nimport { buildConflictOpts, type ScaffoldConflictOpts } from './conflict.js';\nimport { checkWritePathDedup } from './dedup-write.js';\nimport { resolveInteractive } from './output.js';\n\nexport interface InitOptions {\n transport: 'stdio' | 'streamable-http';\n url?: string;\n /** `noir init --upgrade`: run scaffold migrations from the on-disk\n * scaffold-version to current, then re-emit ONLY the runtime subset\n * (regenerate + managedBlock). skipIfExists seeds are left alone so user\n * edits survive. */\n upgrade?: boolean;\n /** S10 target host. Defaults to `'claude'` (the regression anchor). Drives\n * both scaffold emission (the manifest's host-specific half) and skills\n * emission (skipped for hosts with no `skillsDir`). */\n host?: HostId;\n /** SP-A: re-scaffold even if already initialized (bypasses the\n * already-initialized no-op guard in scaffold()). */\n force?: boolean;\n}\n\n/**\n * Initialize Noir in `root`. Returns the {@link ScaffoldResult} (with\n * structured `conflicts[]` + any dedup records appended) so `--json`\n * callers can surface conflict detail. `undefined` when the already-initialized\n * guard short-circuited (a no-op).\n */\nexport async function init(root: string, opts: InitOptions): Promise<ScaffoldResult | undefined> {\n assertTransportUrl(opts);\n\n const host: HostId = opts.host ?? 'claude';\n // The engine reads ScaffoldOptions.interactive (hermetic — never\n // process.env). The CLI derives it once from the bridge + TTY/CI/NO_COLOR gate.\n const interactive = resolveInteractive();\n const conflictOpts = buildConflictOpts({ force: opts.force, interactive });\n\n const res = await scaffold({\n root,\n mode: 'init',\n host,\n transport: opts.transport,\n interactive,\n ...(opts.url !== undefined ? { url: opts.url } : {}),\n ...(opts.upgrade === true ? { upgrade: true } : {}),\n ...(opts.force === true ? { force: true } : {}),\n ...conflictOpts,\n });\n // SP-A: if the already-initialized guard no-op'd scaffold, stop — don't re-emit\n // skills or print \"initialized\" (scaffold already printed the no-op message).\n if (res.noop) return res;\n\n // Thread the SAME conflictOpts into skills emission so the\n // skills-emit conflict flow is LIVE in interactive mode (the producer\n // accepts conflict opts; this closes the wiring gap). The safe-default\n // `assertNotUserOwned` runs unconditionally inside the producer.\n await emitHostSkills(root, host, conflictOpts, interactive);\n\n // Write-path semantic dedup. Non-blocking; degrades to a\n // stderr warn-skip when the embedder is unavailable. Records near-dups on\n // `res.conflicts` so `--json` consumers see them without a prompt.\n // Best-effort project read: first-run init has no .noir/config.yml yet →\n // undefined → resolveEmbedder warn-skips. `init --force` on an existing\n // project reads the config and runs the dedup against existing host files.\n let projectInfo: ProjectInfo | undefined;\n try {\n projectInfo = loadProjectInfo(root);\n } catch {\n projectInfo = undefined;\n }\n const dedup = await checkWritePathDedup(root, res, { interactive, project: projectInfo });\n if (dedup.conflicts.length > 0) res.conflicts.push(...dedup.conflicts);\n\n process.stderr.write(\n `Noir initialized in ${root} (host: ${host}, transport: ${opts.transport}).\\n`,\n );\n process.stderr.write(\n 'Next: run `noir` to open the home menu (or `noir status` for a snapshot).\\n',\n );\n return res;\n}\n\n/**\n * Compose skill emission onto the resolved adapter's `skillsDir` (claude →\n * `.claude/skills/`; cursor → `.cursor/rules/` compiled as `.mdc`; the other\n * three hosts have no skill concept and are skipped with a stderr note).\n *\n * The `CompileTarget` matches the host id (S10 foundation widened the enum to\n * the same union) so cursor skills compile to the `.mdc` rule shape via\n * `compileSkill(_, 'cursor')`; the others keep the verbatim SKILL.md format.\n */\nasync function emitHostSkills(\n root: string,\n host: HostId,\n conflictOpts: ScaffoldConflictOpts,\n interactive: boolean,\n): Promise<void> {\n const adapter = resolveAdapter(host);\n const skillsDir = adapter.skillsDir?.({ root });\n if (skillsDir === undefined) {\n // N1: standardized wording — same phrase across init/sync/create so logs\n // grep uniformly. (Pre-N1 each command phrased this differently.)\n process.stderr.write(`host '${host}' has no skill emitter; skipping skills\\n`);\n return;\n }\n const target: CompileTarget = host;\n // Forward conflictPolicy + onConflict + interactive so an\n // interactive `noir init` with a conflicting skill emit consults the\n // resolver (clack menu + diff preview + apply-to-all); --json/--no-input\n // stays prompt-free via the `interactive: false` guard. The resolver shape\n // is structurally compatible with the skill emit seam (see skills/types.ts);\n // the cast isolates the literal-narrowing mismatch on the return type.\n type SkillEmitOpts = NonNullable<Parameters<typeof emitSkillsToDir>[1]>;\n const skillOpts: SkillEmitOpts = {\n includeIntegrations: true,\n target,\n conflictPolicy: conflictOpts.conflictPolicy,\n interactive,\n };\n if (conflictOpts.onConflict !== undefined) {\n skillOpts.onConflict = conflictOpts.onConflict as SkillEmitOpts['onConflict'];\n }\n const summary = await emitSkillsToDir(skillsDir, skillOpts);\n const relDir = skillsDir.replace(`${root}/`, '');\n process.stderr.write(\n `Emitted ${summary.emitted.length} Noir skills to ${relDir}/ (target: ${target}).\\n`,\n );\n}\n\n/**\n * Transport + URL precondition. The error strings here are the SECURITY GATE\n * locked by `url-validation.test.ts` — do not rephrase. Thrown errors are\n * plain `Error`s (bin.ts's `main().catch` maps them to stderr + exitCode 1).\n *\n * Exported so `noir create` (commands/create.ts) reuses the SAME gate instead\n * of duplicating the localhost allowlist.\n */\nexport function assertTransportUrl(opts: {\n transport: 'stdio' | 'streamable-http';\n url?: string;\n}): void {\n if (opts.transport === 'streamable-http' && opts.url === undefined) {\n throw new Error('--transport streamable-http requires --url');\n }\n if (opts.url !== undefined) {\n assertLocalhostUrl(opts.url);\n }\n}\n\n// Validate a streamable-http --url is http(s) and localhost-only. Gate 2's\n// daemon binds 127.0.0.1, so persisting a non-localhost URL is a footgun.\n// Not exported: the single external surface is assertTransportUrl(); thrown\n// errors propagate to bin.ts's main().catch (stderr + exitCode 1).\nfunction assertLocalhostUrl(raw: string): void {\n let url: URL;\n try {\n url = new URL(raw);\n } catch {\n throw new Error(`Invalid URL: ${raw}`);\n }\n if (!['http:', 'https:'].includes(url.protocol)) {\n throw new Error('Only http/https URLs are supported');\n }\n if (!['localhost', '127.0.0.1', '::1'].includes(url.hostname)) {\n throw new Error(`Only localhost URLs are supported (got ${url.hostname})`);\n }\n}\n"],"mappings":";;;;;;;;;;AA2BA,SAAsB,sBAAsB;AAC5C,SAAS,uBAAyC;AAClD,SAA8B,gBAAgB;AAC9C,SAA6B,uBAAuB;AA4BpD,eAAsB,KAAK,MAAc,MAAwD;AAC/F,qBAAmB,IAAI;AAEvB,QAAM,OAAe,KAAK,QAAQ;AAGlC,QAAM,cAAc,mBAAmB;AACvC,QAAM,eAAe,kBAAkB,EAAE,OAAO,KAAK,OAAO,YAAY,CAAC;AAEzE,QAAM,MAAM,MAAM,SAAS;AAAA,IACzB;AAAA,IACA,MAAM;AAAA,IACN;AAAA,IACA,WAAW,KAAK;AAAA,IAChB;AAAA,IACA,GAAI,KAAK,QAAQ,SAAY,EAAE,KAAK,KAAK,IAAI,IAAI,CAAC;AAAA,IAClD,GAAI,KAAK,YAAY,OAAO,EAAE,SAAS,KAAK,IAAI,CAAC;AAAA,IACjD,GAAI,KAAK,UAAU,OAAO,EAAE,OAAO,KAAK,IAAI,CAAC;AAAA,IAC7C,GAAG;AAAA,EACL,CAAC;AAGD,MAAI,IAAI,KAAM,QAAO;AAMrB,QAAM,eAAe,MAAM,MAAM,cAAc,WAAW;AAQ1D,MAAI;AACJ,MAAI;AACF,kBAAc,gBAAgB,IAAI;AAAA,EACpC,QAAQ;AACN,kBAAc;AAAA,EAChB;AACA,QAAM,QAAQ,MAAM,oBAAoB,MAAM,KAAK,EAAE,aAAa,SAAS,YAAY,CAAC;AACxF,MAAI,MAAM,UAAU,SAAS,EAAG,KAAI,UAAU,KAAK,GAAG,MAAM,SAAS;AAErE,UAAQ,OAAO;AAAA,IACb,uBAAuB,IAAI,WAAW,IAAI,gBAAgB,KAAK,SAAS;AAAA;AAAA,EAC1E;AACA,UAAQ,OAAO;AAAA,IACb;AAAA,EACF;AACA,SAAO;AACT;AAWA,eAAe,eACb,MACA,MACA,cACA,aACe;AACf,QAAM,UAAU,eAAe,IAAI;AACnC,QAAM,YAAY,QAAQ,YAAY,EAAE,KAAK,CAAC;AAC9C,MAAI,cAAc,QAAW;AAG3B,YAAQ,OAAO,MAAM,SAAS,IAAI;AAAA,CAA2C;AAC7E;AAAA,EACF;AACA,QAAM,SAAwB;AAQ9B,QAAM,YAA2B;AAAA,IAC/B,qBAAqB;AAAA,IACrB;AAAA,IACA,gBAAgB,aAAa;AAAA,IAC7B;AAAA,EACF;AACA,MAAI,aAAa,eAAe,QAAW;AACzC,cAAU,aAAa,aAAa;AAAA,EACtC;AACA,QAAM,UAAU,MAAM,gBAAgB,WAAW,SAAS;AAC1D,QAAM,SAAS,UAAU,QAAQ,GAAG,IAAI,KAAK,EAAE;AAC/C,UAAQ,OAAO;AAAA,IACb,WAAW,QAAQ,QAAQ,MAAM,mBAAmB,MAAM,cAAc,MAAM;AAAA;AAAA,EAChF;AACF;AAUO,SAAS,mBAAmB,MAG1B;AACP,MAAI,KAAK,cAAc,qBAAqB,KAAK,QAAQ,QAAW;AAClE,UAAM,IAAI,MAAM,4CAA4C;AAAA,EAC9D;AACA,MAAI,KAAK,QAAQ,QAAW;AAC1B,uBAAmB,KAAK,GAAG;AAAA,EAC7B;AACF;AAMA,SAAS,mBAAmB,KAAmB;AAC7C,MAAI;AACJ,MAAI;AACF,UAAM,IAAI,IAAI,GAAG;AAAA,EACnB,QAAQ;AACN,UAAM,IAAI,MAAM,gBAAgB,GAAG,EAAE;AAAA,EACvC;AACA,MAAI,CAAC,CAAC,SAAS,QAAQ,EAAE,SAAS,IAAI,QAAQ,GAAG;AAC/C,UAAM,IAAI,MAAM,oCAAoC;AAAA,EACtD;AACA,MAAI,CAAC,CAAC,aAAa,aAAa,KAAK,EAAE,SAAS,IAAI,QAAQ,GAAG;AAC7D,UAAM,IAAI,MAAM,0CAA0C,IAAI,QAAQ,GAAG;AAAA,EAC3E;AACF;","names":[]}
@@ -0,0 +1,203 @@
1
+ #!/usr/bin/env node
2
+ import {
3
+ EXIT,
4
+ fail,
5
+ info,
6
+ success,
7
+ warn
8
+ } from "./chunk-DQXQQDJN.js";
9
+
10
+ // src/commands/install.ts
11
+ import { mkdirSync } from "fs";
12
+ import { join } from "path";
13
+ import {
14
+ atomicWriteFile,
15
+ detectActiveMethod,
16
+ detectInstallMethods,
17
+ loadProjectInfo,
18
+ MANAGED_NODE_VERSION,
19
+ NOIR_VERSION,
20
+ noirHome,
21
+ provisionManagedNode,
22
+ readInstallRecord,
23
+ runManagerCmd,
24
+ writeInstallRecord
25
+ } from "@noir-ai/core";
26
+ function semverLt(a, b) {
27
+ const pa = a.split(".").map(Number);
28
+ const pb = b.split(".").map(Number);
29
+ for (let i = 0; i < 3; i++) {
30
+ const x = pa[i] ?? 0;
31
+ const y = pb[i] ?? 0;
32
+ if (x !== y) return x < y;
33
+ }
34
+ return false;
35
+ }
36
+ function buildMigrationPlan(opts) {
37
+ const detected = opts.detected;
38
+ const shouldMigrate = detected.length > 0 && opts.currentMethod !== "native";
39
+ const nativeVersion = opts.targetSpec;
40
+ let isDowngrade = false;
41
+ if (opts.installedVersion && nativeVersion !== "latest" && nativeVersion !== "beta") {
42
+ isDowngrade = semverLt(nativeVersion, opts.installedVersion);
43
+ }
44
+ const floor = opts.minVersion ?? "1.6.0";
45
+ const belowMinVersion = nativeVersion !== "latest" && nativeVersion !== "beta" && semverLt(nativeVersion, floor);
46
+ return {
47
+ detected,
48
+ currentMethod: opts.currentMethod,
49
+ targetSpec: opts.targetSpec,
50
+ installedVersion: opts.installedVersion,
51
+ shouldMigrate,
52
+ isDowngrade,
53
+ belowMinVersion,
54
+ nativeVersion,
55
+ prevUninstallCmd: detected[0]?.uninstallCmd ?? null,
56
+ autoUninstall: false
57
+ };
58
+ }
59
+ async function installManagedNode(opts) {
60
+ const spec = opts.version ?? opts.channel ?? "latest";
61
+ const home = noirHome();
62
+ const cliDir = join(home, "cli");
63
+ const binDir = join(home, "bin");
64
+ mkdirSync(cliDir, { recursive: true });
65
+ mkdirSync(binDir, { recursive: true });
66
+ let runtime;
67
+ try {
68
+ runtime = await provisionManagedNode({
69
+ ...opts.provision ?? {},
70
+ env: opts.env
71
+ });
72
+ } catch (err) {
73
+ const reason = err instanceof Error ? err.message : String(err);
74
+ return { ok: false, version: null, error: `managed-Node provision failed: ${reason}` };
75
+ }
76
+ if (runtime.source === "system") {
77
+ warn(
78
+ `managed-Node download failed; using system Node ${runtime.version} at ${runtime.nodeBin}.`
79
+ );
80
+ }
81
+ const { nodeBin, npmBin } = runtime;
82
+ const { code, stderr } = await runManagerCmd(
83
+ [npmBin, "install", "-g", `@noir-ai/cli@${spec}`, `--prefix=${cliDir}`],
84
+ { env: opts.env, timeoutMs: 12e4 }
85
+ );
86
+ if (code !== 0) {
87
+ return { ok: false, version: null, error: `npm install failed: ${stderr.slice(0, 300)}` };
88
+ }
89
+ const shim = join(binDir, "noir");
90
+ const shimBody = `#!/usr/bin/env bash
91
+ "${nodeBin}" "${join(cliDir, "lib", "node_modules", "@noir-ai", "cli", "dist", "bin.js")}" "$@"
92
+ `;
93
+ atomicWriteFile(shim, shimBody);
94
+ const ver = await runManagerCmd(
95
+ [
96
+ nodeBin,
97
+ join(cliDir, "lib", "node_modules", "@noir-ai", "cli", "dist", "bin.js"),
98
+ "--version"
99
+ ],
100
+ { env: opts.env, timeoutMs: 3e4 }
101
+ );
102
+ const version = ver.code === 0 ? ver.stdout.trim() : null;
103
+ writeInstallRecord({
104
+ method: "native",
105
+ version: version ?? "0.0.0",
106
+ channel: opts.channel ?? "latest",
107
+ installedAt: (/* @__PURE__ */ new Date()).toISOString(),
108
+ // Reflect which runtime backs this install: the pinned managed version
109
+ // (e.g. '22.23.2') for managed, or 'system-<v>' for the fallback so doctor
110
+ // can distinguish a fallback install from a real managed one.
111
+ managedRuntimeVersion: runtime.source === "managed" ? MANAGED_NODE_VERSION : `system-${runtime.version}`
112
+ });
113
+ return { ok: true, version, runtimeSource: runtime.source };
114
+ }
115
+ async function install(opts = {}) {
116
+ if (opts.dismiss === true) {
117
+ const rec = readInstallRecord();
118
+ if (!rec) {
119
+ info("No install record found; nothing to dismiss.");
120
+ return;
121
+ }
122
+ const dismissed = new Set(rec.dismissedVersions ?? []);
123
+ if (!dismissed.has(NOIR_VERSION)) dismissed.add(NOIR_VERSION);
124
+ writeInstallRecord({ ...rec, dismissedVersions: [...dismissed] });
125
+ success(`Migration banner dismissed for ${NOIR_VERSION}.`);
126
+ if (opts.list !== true) return;
127
+ }
128
+ if (opts.list === true) {
129
+ const detected2 = await detectInstallMethods(process.env);
130
+ const jsonOutput = { ok: true, data: { detected: detected2 } };
131
+ if (opts.json === true) {
132
+ process.stdout.write(`${JSON.stringify(jsonOutput)}
133
+ `);
134
+ return;
135
+ }
136
+ info("Detected installs:");
137
+ for (const d of detected2) info(` ${d.method} (${d.version ?? "unknown"})`);
138
+ return;
139
+ }
140
+ const currentMethod = detectActiveMethod();
141
+ const detected = await detectInstallMethods(process.env);
142
+ const installedRecord = readInstallRecord();
143
+ let minVersion = "1.6.0";
144
+ try {
145
+ minVersion = loadProjectInfo(process.cwd()).config.update.minVersion;
146
+ } catch {
147
+ }
148
+ const plan = buildMigrationPlan({
149
+ detected,
150
+ currentMethod,
151
+ targetSpec: opts.spec ?? "latest",
152
+ installedVersion: installedRecord?.version ?? null,
153
+ minVersion
154
+ });
155
+ if (plan.belowMinVersion) {
156
+ warn(`Target ${plan.nativeVersion} is below the minVersion floor (${minVersion}).`);
157
+ if (opts.noInput === true || opts.input === false) {
158
+ fail(
159
+ EXIT.USAGE,
160
+ `refusing install of ${plan.nativeVersion} (below minVersion ${minVersion})`,
161
+ opts
162
+ );
163
+ }
164
+ }
165
+ if (plan.isDowngrade) {
166
+ warn(`Target ${plan.nativeVersion} is OLDER than installed ${plan.installedVersion}.`);
167
+ if (opts.noInput === true || opts.input === false) {
168
+ fail(
169
+ EXIT.USAGE,
170
+ `refusing downgrade to ${plan.nativeVersion} (installed ${plan.installedVersion})`,
171
+ opts
172
+ );
173
+ }
174
+ }
175
+ const result = await installManagedNode({
176
+ channel: plan.nativeVersion === "beta" ? "beta" : void 0,
177
+ version: plan.nativeVersion.startsWith("v") ? plan.nativeVersion : plan.nativeVersion === "latest" || plan.nativeVersion === "beta" ? void 0 : plan.nativeVersion,
178
+ env: process.env
179
+ });
180
+ if (!result.ok) {
181
+ fail(EXIT.ERROR, result.error ?? "native install failed", opts);
182
+ }
183
+ info(`Installed native: ${result.version}.`);
184
+ if (plan.prevUninstallCmd && opts.uninstallPrev !== true) {
185
+ warn(`To finish the migration, uninstall the previous install: ${plan.prevUninstallCmd}`);
186
+ warn(
187
+ " Or re-run with --uninstall-prev to do it now. Rollback: reinstall via the previous manager."
188
+ );
189
+ } else if (plan.prevUninstallCmd && opts.uninstallPrev === true) {
190
+ const { code } = await runManagerCmd(plan.prevUninstallCmd.split(" "), { env: process.env });
191
+ if (code === 0) success("Previous install removed.");
192
+ else warn("Previous install NOT removed (non-zero exit). You can remove it manually.");
193
+ }
194
+ info("If `noir` still points at the old install, run: hash -r && which -a noir");
195
+ success(`noir ${result.version} installed via native. Run \`noir doctor\` to verify.`);
196
+ }
197
+
198
+ export {
199
+ buildMigrationPlan,
200
+ installManagedNode,
201
+ install
202
+ };
203
+ //# sourceMappingURL=chunk-47ITYQ3A.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../src/commands/install.ts"],"sourcesContent":["import { mkdirSync } from 'node:fs';\nimport { join } from 'node:path';\nimport {\n atomicWriteFile,\n type DetectResult,\n detectActiveMethod,\n detectInstallMethods,\n loadProjectInfo,\n MANAGED_NODE_VERSION,\n NOIR_VERSION,\n noirHome,\n type ProvisionedNode,\n type ProvisionOptions,\n provisionManagedNode,\n readInstallRecord,\n runManagerCmd,\n writeInstallRecord,\n} from '@noir-ai/core';\nimport { type CliOptions, EXIT, fail, info, success, warn } from '../output.js';\n\nexport interface InstallOptions extends CliOptions {\n list?: boolean;\n uninstallPrev?: boolean;\n spec?: string;\n /** Dismiss the migration banner for the current CLI version (persists in\n * install.json's `dismissedVersions`). Typically passed with `--list`. */\n dismiss?: boolean;\n}\n\nexport interface MigrationPlan {\n detected: DetectResult[];\n currentMethod: string;\n targetSpec: string;\n installedVersion: string | null;\n shouldMigrate: boolean;\n isDowngrade: boolean;\n /**\n * I2 — `update.minVersion` floor (config.ts:238). True when the resolved\n * target is a concrete version below the configured floor. Channel targets\n * (`latest`/`beta`) resolve to the newest at install time and never trip it.\n * Enforced in {@link install} with the same warn/refuse pattern as the\n * downgrade guard.\n */\n belowMinVersion: boolean;\n nativeVersion: string;\n prevUninstallCmd: string | null;\n autoUninstall: boolean; // always false unless --uninstall-prev\n}\n\n/** Per-segment numeric semver comparison. Returns true when `a < b`. */\nfunction semverLt(a: string, b: string): boolean {\n const pa = a.split('.').map(Number);\n const pb = b.split('.').map(Number);\n for (let i = 0; i < 3; i++) {\n const x = pa[i] ?? 0;\n const y = pb[i] ?? 0;\n if (x !== y) return x < y;\n }\n return false;\n}\n\n/** Pure: build the migration plan from detection + target. No I/O. */\nexport function buildMigrationPlan(opts: {\n detected: DetectResult[];\n currentMethod: string;\n targetSpec: string;\n installedVersion: string | null;\n /**\n * The `update.minVersion` floor from config (default `'1.6.0'`). When\n * omitted, the config default applies. See I2.\n */\n minVersion?: string;\n}): MigrationPlan {\n const detected = opts.detected;\n const shouldMigrate = detected.length > 0 && opts.currentMethod !== 'native';\n const nativeVersion = opts.targetSpec;\n let isDowngrade = false;\n if (opts.installedVersion && nativeVersion !== 'latest' && nativeVersion !== 'beta') {\n isDowngrade = semverLt(nativeVersion, opts.installedVersion);\n }\n // I2 — minVersion floor: only a concrete version below the floor trips it;\n // 'latest'/'beta' resolve to the newest at install time (always >= floor).\n const floor = opts.minVersion ?? '1.6.0';\n const belowMinVersion =\n nativeVersion !== 'latest' && nativeVersion !== 'beta' && semverLt(nativeVersion, floor);\n return {\n detected,\n currentMethod: opts.currentMethod,\n targetSpec: opts.targetSpec,\n installedVersion: opts.installedVersion,\n shouldMigrate,\n isDowngrade,\n belowMinVersion,\n nativeVersion,\n prevUninstallCmd: detected[0]?.uninstallCmd ?? null,\n autoUninstall: false,\n };\n}\n\nexport interface InstallManagedNodeOptions {\n channel?: string;\n version?: string;\n env: NodeJS.ProcessEnv;\n /**\n * Forwarded into {@link provisionManagedNode} (P1). Carries the offline mock\n * seams (`fetch`, `exec`, `target`, `signal`) so the install pipeline is\n * unit-testable without network — same pattern as `provisionManagedNode`'s\n * own suite in `packages/core/test/node-provision.test.ts`.\n */\n provision?: Pick<ProvisionOptions, 'fetch' | 'exec' | 'target' | 'signal'>;\n}\n\n/** Normalized record of which runtime backed an install (P2). */\nexport interface InstallManagedNodeResult {\n ok: boolean;\n version: string | null;\n /** What runtime backed this install: `'managed'` (downloaded into ~/.noir)\n * or `'system'` (used the system-Node fallback). */\n runtimeSource?: ProvisionedNode['source'];\n error?: string;\n}\n\n/**\n * Provision the managed Node runtime (P1) and install `@noir-ai/cli` into an\n * isolated `~/.noir/cli` prefix, shimmed from `~/.noir/bin/noir`.\n *\n * Replaces the prior \"not provisioned\" fail branch: instead of requiring the\n * user to pre-run `install.sh`/`install.ps1`, this now delegates to\n * {@link provisionManagedNode} (download + verify + extract, fail-closed), then\n * installs the CLI with the provisioned npm. On a system-Node fallback the\n * fallback node/npm paths are used and a warning is emitted; the install\n * record's `managedRuntimeVersion` reflects which path was taken so `noir\n * doctor` can report it accurately.\n */\nexport async function installManagedNode(\n opts: InstallManagedNodeOptions,\n): Promise<InstallManagedNodeResult> {\n const spec = opts.version ?? opts.channel ?? 'latest';\n const home = noirHome();\n const cliDir = join(home, 'cli');\n const binDir = join(home, 'bin');\n mkdirSync(cliDir, { recursive: true });\n mkdirSync(binDir, { recursive: true });\n\n // Provision the managed Node runtime (P1). Idempotent — re-runs are no-ops.\n // On any failure, provisionManagedNode either falls back to a system Node\n // >=22 (returns { source: 'system' }) or throws — we surface the throw as an\n // {ok:false} envelope rather than letting it kill the CLI.\n let runtime: ProvisionedNode;\n try {\n runtime = await provisionManagedNode({\n ...(opts.provision ?? {}),\n env: opts.env,\n });\n } catch (err) {\n const reason = err instanceof Error ? err.message : String(err);\n return { ok: false, version: null, error: `managed-Node provision failed: ${reason}` };\n }\n if (runtime.source === 'system') {\n warn(\n `managed-Node download failed; using system Node ${runtime.version} at ${runtime.nodeBin}.`,\n );\n }\n\n const { nodeBin, npmBin } = runtime;\n\n // Install @noir-ai/cli into the isolated prefix using the provisioned npm.\n const { code, stderr } = await runManagerCmd(\n [npmBin, 'install', '-g', `@noir-ai/cli@${spec}`, `--prefix=${cliDir}`],\n { env: opts.env, timeoutMs: 120_000 },\n );\n if (code !== 0) {\n return { ok: false, version: null, error: `npm install failed: ${stderr.slice(0, 300)}` };\n }\n\n // Shim: ~/.noir/bin/noir -> provisioned node + isolated prefix.\n const shim = join(binDir, 'noir');\n const shimBody = `#!/usr/bin/env bash\\n\"${nodeBin}\" \"${join(cliDir, 'lib', 'node_modules', '@noir-ai', 'cli', 'dist', 'bin.js')}\" \"$@\"\\n`;\n atomicWriteFile(shim, shimBody);\n // (POSIX shim; Windows uses a .cmd wrapper -- install.sh/install.ps1, P3.)\n\n // Resolve installed version via the provisioned node.\n const ver = await runManagerCmd(\n [\n nodeBin,\n join(cliDir, 'lib', 'node_modules', '@noir-ai', 'cli', 'dist', 'bin.js'),\n '--version',\n ],\n { env: opts.env, timeoutMs: 30_000 },\n );\n const version = ver.code === 0 ? ver.stdout.trim() : null;\n writeInstallRecord({\n method: 'native',\n version: version ?? '0.0.0',\n channel: opts.channel ?? 'latest',\n installedAt: new Date().toISOString(),\n // Reflect which runtime backs this install: the pinned managed version\n // (e.g. '22.23.2') for managed, or 'system-<v>' for the fallback so doctor\n // can distinguish a fallback install from a real managed one.\n managedRuntimeVersion:\n runtime.source === 'managed' ? MANAGED_NODE_VERSION : `system-${runtime.version}`,\n });\n return { ok: true, version, runtimeSource: runtime.source };\n}\n\nexport async function install(opts: InstallOptions = {}): Promise<void> {\n // --dismiss: append the current CLI version to install.json's\n // `dismissedVersions` so the migration banner stops showing for this\n // version. Idempotent (no duplicate entries). Typically combined with\n // `--list`; works standalone too. Requires an existing install record —\n // if there's none, there's nothing to dismiss (no banner was shown).\n if (opts.dismiss === true) {\n const rec = readInstallRecord();\n if (!rec) {\n info('No install record found; nothing to dismiss.');\n return;\n }\n const dismissed = new Set(rec.dismissedVersions ?? []);\n if (!dismissed.has(NOIR_VERSION)) dismissed.add(NOIR_VERSION);\n writeInstallRecord({ ...rec, dismissedVersions: [...dismissed] });\n success(`Migration banner dismissed for ${NOIR_VERSION}.`);\n if (opts.list !== true) return; // --dismiss alone is done; --list continues below.\n }\n\n if (opts.list === true) {\n const detected = await detectInstallMethods(process.env);\n const jsonOutput = { ok: true, data: { detected } };\n if (opts.json === true) {\n process.stdout.write(`${JSON.stringify(jsonOutput)}\\n`);\n return;\n }\n info('Detected installs:');\n for (const d of detected) info(` ${d.method} (${d.version ?? 'unknown'})`);\n return;\n }\n\n const currentMethod = detectActiveMethod();\n const detected = await detectInstallMethods(process.env);\n const installedRecord = readInstallRecord();\n // I2 — read the `update.minVersion` floor from the project config (falls\n // back to the config default '1.6.0' when the project isn't initialized or\n // the block is absent). Same try/catch pattern as home.ts's update-config read.\n let minVersion = '1.6.0';\n try {\n minVersion = loadProjectInfo(process.cwd()).config.update.minVersion;\n } catch {\n // Not initialized yet — the config default applies.\n }\n const plan = buildMigrationPlan({\n detected,\n currentMethod,\n targetSpec: opts.spec ?? 'latest',\n installedVersion: installedRecord?.version ?? null,\n minVersion,\n });\n\n // I2 — refuse to install a version below the configured minVersion floor\n // (same warn/refuse pattern as the installed-version downgrade guard).\n if (plan.belowMinVersion) {\n warn(`Target ${plan.nativeVersion} is below the minVersion floor (${minVersion}).`);\n if (opts.noInput === true || opts.input === false) {\n fail(\n EXIT.USAGE,\n `refusing install of ${plan.nativeVersion} (below minVersion ${minVersion})`,\n opts,\n );\n }\n }\n\n if (plan.isDowngrade) {\n warn(`Target ${plan.nativeVersion} is OLDER than installed ${plan.installedVersion}.`);\n // (Interactive confirm is skipped under --no-input; we hard-stop to be safe.)\n if (opts.noInput === true || opts.input === false) {\n fail(\n EXIT.USAGE,\n `refusing downgrade to ${plan.nativeVersion} (installed ${plan.installedVersion})`,\n opts,\n );\n }\n // @clack confirm gate (lazy import) -- same pattern as home.ts.\n }\n\n const result = await installManagedNode({\n channel: plan.nativeVersion === 'beta' ? 'beta' : undefined,\n version: plan.nativeVersion.startsWith('v')\n ? plan.nativeVersion\n : plan.nativeVersion === 'latest' || plan.nativeVersion === 'beta'\n ? undefined\n : plan.nativeVersion,\n env: process.env,\n });\n if (!result.ok) {\n fail(EXIT.ERROR, result.error ?? 'native install failed', opts);\n }\n\n info(`Installed native: ${result.version}.`);\n if (plan.prevUninstallCmd && opts.uninstallPrev !== true) {\n warn(`To finish the migration, uninstall the previous install: ${plan.prevUninstallCmd}`);\n warn(\n ' Or re-run with --uninstall-prev to do it now. Rollback: reinstall via the previous manager.',\n );\n } else if (plan.prevUninstallCmd && opts.uninstallPrev === true) {\n const { code } = await runManagerCmd(plan.prevUninstallCmd.split(' '), { env: process.env });\n if (code === 0) success('Previous install removed.');\n else warn('Previous install NOT removed (non-zero exit). You can remove it manually.');\n }\n // Claude Code #41806/#27910 mitigation: the old npm bin may still resolve\n // first in $PATH after migration (the shell hashes command paths, and a\n // stale global install can shadow ~/.noir/bin). Hint the user to refresh.\n info('If `noir` still points at the old install, run: hash -r && which -a noir');\n success(`noir ${result.version} installed via native. Run \\`noir doctor\\` to verify.`);\n}\n"],"mappings":"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","names":["detected"]}
@@ -0,0 +1,310 @@
1
+ #!/usr/bin/env node
2
+ var __require = /* @__PURE__ */ ((x) => typeof require !== "undefined" ? require : typeof Proxy !== "undefined" ? new Proxy(x, {
3
+ get: (a, b) => (typeof require !== "undefined" ? require : a)[b]
4
+ }) : x)(function(x) {
5
+ if (typeof require !== "undefined") return require.apply(this, arguments);
6
+ throw Error('Dynamic require of "' + x + '" is not supported');
7
+ });
8
+
9
+ // src/output.ts
10
+ import Table from "cli-table3";
11
+ import { CommanderError } from "commander";
12
+ import ora from "ora";
13
+
14
+ // src/theme.ts
15
+ import pc from "picocolors";
16
+ var FORCE = pc.createColors(true);
17
+ var c = {
18
+ /** Green — success / healthy. */
19
+ ok: (s) => useColor() ? FORCE.green(s) : s,
20
+ /** Yellow — degraded but usable. */
21
+ warn: (s) => useColor() ? FORCE.yellow(s) : s,
22
+ /** Red — reserved strictly for ERROR. Never a table header. */
23
+ error: (s) => useColor() ? FORCE.red(s) : s,
24
+ /** Cyan — informational / brand-neutral accent for labels. */
25
+ info: (s) => useColor() ? FORCE.cyan(s) : s,
26
+ /** Blue — the Midnight Cobalt brand role (banner / accent). */
27
+ accent: (s) => useColor() ? FORCE.blue(s) : s,
28
+ /** Dim — secondary text, table borders when color is on. */
29
+ dim: (s) => useColor() ? FORCE.dim(s) : s,
30
+ /** Bold — emphasis (table headers compose this with `info`). */
31
+ bold: (s) => useColor() ? FORCE.bold(s) : s
32
+ };
33
+ var BADGE_SYMBOL = {
34
+ ok: "\u2713",
35
+ warn: "\u26A0",
36
+ error: "\u2717",
37
+ info: "\u2139"
38
+ };
39
+ function badge(state, label = state) {
40
+ const text = `${BADGE_SYMBOL[state]} ${label}`;
41
+ switch (state) {
42
+ case "ok":
43
+ return c.ok(text);
44
+ case "warn":
45
+ return c.warn(text);
46
+ case "error":
47
+ return c.error(text);
48
+ case "info":
49
+ return c.info(text);
50
+ }
51
+ }
52
+ function envFlagSet(name) {
53
+ const v = process.env[name];
54
+ return v !== void 0 && v !== "";
55
+ }
56
+ function isCiEnv() {
57
+ const v = process.env.CI;
58
+ if (v === void 0 || v === "") return false;
59
+ return v !== "0" && v !== "false";
60
+ }
61
+ function useColor() {
62
+ if (envFlagSet("NO_COLOR")) return false;
63
+ if (process.env.CLICOLOR_FORCE === "1") return true;
64
+ if (isCiEnv()) return false;
65
+ return pc.isColorSupported;
66
+ }
67
+ function accessibleMode() {
68
+ return envFlagSet("NOIR_ACCESSIBLE");
69
+ }
70
+ function terminalWidth() {
71
+ const fromEnv = process.env.COLUMNS;
72
+ if (fromEnv !== void 0 && fromEnv !== "") {
73
+ const n = Number(fromEnv);
74
+ if (Number.isInteger(n) && n > 0) return Math.max(n, 20);
75
+ }
76
+ const cols = process.stdout.columns;
77
+ if (typeof cols === "number" && cols > 0) return Math.max(cols, 20);
78
+ return 80;
79
+ }
80
+
81
+ // src/output.ts
82
+ var EXIT = {
83
+ OK: 0,
84
+ ERROR: 1,
85
+ USAGE: 2,
86
+ NOT_FOUND: 3,
87
+ DAEMON_DOWN: 4,
88
+ CANCELLED: 5
89
+ };
90
+ var NoirCliError = class extends Error {
91
+ exitCode;
92
+ constructor(exitCode, message) {
93
+ super(message);
94
+ this.name = "NoirCliError";
95
+ this.exitCode = exitCode;
96
+ }
97
+ };
98
+ var NOIR_ERROR_CODE = "noir.error";
99
+ function isJsonMode(opts) {
100
+ return opts.json === true;
101
+ }
102
+ function isQuietMode(opts) {
103
+ return opts.quiet === true;
104
+ }
105
+ function isNoInput(opts = {}) {
106
+ return opts.noInput === true || opts.input === false;
107
+ }
108
+ function envFlagSet2(name) {
109
+ const v = process.env[name];
110
+ return v !== void 0 && v !== "";
111
+ }
112
+ function isInteractive(opts = {}) {
113
+ if (isJsonMode(opts) || isNoInput(opts)) return false;
114
+ if (isCiEnv() || envFlagSet2("NO_COLOR")) return false;
115
+ return process.stdin.isTTY === true && process.stdout.isTTY === true;
116
+ }
117
+ function resolveInteractive() {
118
+ if (flaggedNonInteractiveCli()) return false;
119
+ return isInteractive();
120
+ }
121
+ function flaggedNonInteractiveCli() {
122
+ const v = process.env.NOIR_NON_INTERACTIVE;
123
+ return v !== void 0 && v !== "";
124
+ }
125
+ function json(obj) {
126
+ process.stdout.write(`${JSON.stringify(obj)}
127
+ `);
128
+ }
129
+ function log(msg, opts = {}) {
130
+ if (isJsonMode(opts) || isQuietMode(opts)) return;
131
+ process.stderr.write(`${msg}
132
+ `);
133
+ }
134
+ function info(msg, opts = {}) {
135
+ if (isJsonMode(opts) || isQuietMode(opts)) return;
136
+ process.stderr.write(`${c.info(msg)}
137
+ `);
138
+ }
139
+ function success(msg, opts = {}) {
140
+ if (isJsonMode(opts) || isQuietMode(opts)) return;
141
+ process.stderr.write(`${c.ok(msg)}
142
+ `);
143
+ }
144
+ function warn(msg, opts = {}) {
145
+ if (isJsonMode(opts)) return;
146
+ process.stderr.write(`${c.warn(msg)}
147
+ `);
148
+ }
149
+ function error(msg, opts = {}) {
150
+ if (isJsonMode(opts)) return;
151
+ process.stderr.write(`${c.error(msg)}
152
+ `);
153
+ }
154
+ function tip(msg, opts = {}) {
155
+ if (isJsonMode(opts) || opts.noTips === true) return;
156
+ process.stderr.write(`${c.warn(msg)}
157
+ `);
158
+ }
159
+ function table(rows, cols, opts = {}) {
160
+ if (isJsonMode(opts)) return;
161
+ if (rows.length === 0) {
162
+ info("(no rows)", opts);
163
+ return;
164
+ }
165
+ const t = new Table({
166
+ head: cols.map((col) => c.bold(c.info(col))),
167
+ // Empty arrays ⇒ cli-table3 applies NO @colors/colors wrap (neither the
168
+ // default red header nor a colored border). All color is ours, via theme.
169
+ style: { head: [], border: [] },
170
+ colWidths: computeColWidths(rows, cols),
171
+ wordWrap: true,
172
+ truncate: "\u2026"
173
+ });
174
+ for (const row of rows) {
175
+ t.push(cols.map((col) => formatCell(row[col])));
176
+ }
177
+ process.stderr.write(`${t.toString()}
178
+ `);
179
+ }
180
+ function computeColWidths(rows, cols) {
181
+ const n = cols.length;
182
+ if (n === 0) return [];
183
+ const headerLen = cols.map((col) => col.length);
184
+ const natural = cols.map((col, i) => {
185
+ let max = Math.max(headerLen[i] ?? col.length, 3);
186
+ for (const row of rows) {
187
+ const len = formatCell(row[col]).length;
188
+ if (len > max) max = len;
189
+ }
190
+ return max;
191
+ });
192
+ const contentBudget = Math.max(n * 4, terminalWidth() - 3 * n - 1);
193
+ const naturalSum = natural.reduce((a, b) => a + b, 0);
194
+ if (naturalSum <= contentBudget) {
195
+ return natural.map((w) => w + 2);
196
+ }
197
+ const content = natural.slice();
198
+ let sum = naturalSum;
199
+ const floor = headerLen.map((h) => Math.max(h, 3));
200
+ while (sum > contentBudget) {
201
+ let idx = -1;
202
+ let widest = -1;
203
+ for (let i = 0; i < n; i++) {
204
+ const c2 = content[i] ?? 0;
205
+ const f = floor[i] ?? 3;
206
+ if (c2 > f && c2 > widest) {
207
+ widest = c2;
208
+ idx = i;
209
+ }
210
+ }
211
+ if (idx === -1) break;
212
+ const overshoot = sum - contentBudget;
213
+ const reducible = (content[idx] ?? 0) - (floor[idx] ?? 3);
214
+ const cut = Math.min(overshoot, reducible);
215
+ content[idx] = (content[idx] ?? 0) - cut;
216
+ sum -= cut;
217
+ }
218
+ return content.map((w) => w + 2);
219
+ }
220
+ function formatCell(value) {
221
+ if (value === void 0 || value === null) return "";
222
+ if (typeof value === "string") return value;
223
+ if (typeof value === "number" || typeof value === "boolean" || typeof value === "bigint") {
224
+ return String(value);
225
+ }
226
+ return JSON.stringify(value);
227
+ }
228
+ function definitionList(rows, opts = {}) {
229
+ table(
230
+ rows.map((r) => ({ Field: r.label, Value: formatCell(r.value) })),
231
+ ["Field", "Value"],
232
+ opts
233
+ );
234
+ }
235
+ function fail(exitCode, message, opts = {}) {
236
+ if (isJsonMode(opts)) {
237
+ process.stdout.write(`${JSON.stringify({ ok: false, error: { code: exitCode, message } })}
238
+ `);
239
+ } else if (message.length > 0) {
240
+ process.stderr.write(`${message}
241
+ `);
242
+ }
243
+ throw new CommanderError(exitCode, NOIR_ERROR_CODE, message);
244
+ }
245
+ function requireInteractive(opts, hint) {
246
+ if (!isInteractive(opts)) {
247
+ fail(
248
+ EXIT.USAGE,
249
+ `${hint} needs an interactive terminal, but input is disabled (non-TTY, --no-input, --json, CI, or NO_COLOR). Re-run in a TTY or supply the value as a flag.`,
250
+ opts
251
+ );
252
+ }
253
+ }
254
+ function commanderExitCode(err) {
255
+ switch (err.code) {
256
+ case "commander.helpDisplayed":
257
+ case "commander.versionDisplayed":
258
+ case "commander.version":
259
+ return EXIT.OK;
260
+ case "commander.unknownCommand":
261
+ return EXIT.NOT_FOUND;
262
+ default:
263
+ return EXIT.USAGE;
264
+ }
265
+ }
266
+ function inferExitCode(err) {
267
+ if (err instanceof NoirCliError) return err.exitCode;
268
+ if (err instanceof CommanderError) {
269
+ if (err.code.startsWith("noir.")) return err.exitCode;
270
+ return commanderExitCode(err);
271
+ }
272
+ return EXIT.ERROR;
273
+ }
274
+ function handleError(err) {
275
+ if (err instanceof NoirCliError) {
276
+ if (err.message.length > 0) process.stderr.write(`${err.message}
277
+ `);
278
+ } else if (err instanceof CommanderError) {
279
+ } else {
280
+ process.stderr.write(`noir: ${err instanceof Error ? err.message : String(err)}
281
+ `);
282
+ }
283
+ process.exitCode = inferExitCode(err);
284
+ }
285
+
286
+ export {
287
+ __require,
288
+ c,
289
+ badge,
290
+ useColor,
291
+ accessibleMode,
292
+ EXIT,
293
+ NoirCliError,
294
+ isInteractive,
295
+ resolveInteractive,
296
+ json,
297
+ log,
298
+ info,
299
+ success,
300
+ warn,
301
+ error,
302
+ tip,
303
+ table,
304
+ definitionList,
305
+ fail,
306
+ requireInteractive,
307
+ inferExitCode,
308
+ handleError
309
+ };
310
+ //# sourceMappingURL=chunk-DQXQQDJN.js.map