@noir-ai/cli 1.14.0 → 1.15.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.
- package/README.md +2 -2
- package/dist/bin.d.ts +10 -3
- package/dist/bin.js +1240 -365
- package/dist/bin.js.map +1 -1
- package/dist/index.d.ts +39 -12
- package/dist/index.js +1258 -383
- package/dist/index.js.map +1 -1
- package/dist/{output-BNE9htEm.d.ts → output-2T2lGR7a.d.ts} +3 -3
- package/dist/tui/{chunk-6LO4LXEH.js → chunk-5KWETEMF.js} +1191 -352
- package/dist/tui/chunk-5KWETEMF.js.map +1 -0
- package/dist/tui/{chunk-ESZTOQEC.js → chunk-KETMM3BA.js} +25 -3
- package/dist/tui/chunk-KETMM3BA.js.map +1 -0
- package/dist/tui/{chunk-2RK6AFZQ.js → chunk-TPNJEN4B.js} +2 -2
- package/dist/tui/chunk-TPNJEN4B.js.map +1 -0
- package/dist/tui/{create-FD4E626Z.js → create-6KDIFPUL.js} +10 -6
- package/dist/tui/create-6KDIFPUL.js.map +1 -0
- package/dist/tui/index.js +994 -120
- package/dist/tui/index.js.map +1 -1
- package/dist/tui/{install-YGTUSLRO.js → install-KA35ONQP.js} +2 -2
- package/dist/tui/{sync-N633SQSS.js → sync-AII5WCRO.js} +8 -4
- package/dist/tui/sync-AII5WCRO.js.map +1 -0
- package/dist/tui/{update-LET5BWOU.js → update-XGKJ4KBR.js} +3 -3
- package/package.json +11 -11
- package/dist/tui/chunk-2RK6AFZQ.js.map +0 -1
- package/dist/tui/chunk-6LO4LXEH.js.map +0 -1
- package/dist/tui/chunk-ESZTOQEC.js.map +0 -1
- package/dist/tui/create-FD4E626Z.js.map +0 -1
- package/dist/tui/sync-N633SQSS.js.map +0 -1
- /package/dist/tui/{install-YGTUSLRO.js.map → install-KA35ONQP.js.map} +0 -0
- /package/dist/tui/{update-LET5BWOU.js.map → update-XGKJ4KBR.js.map} +0 -0
|
@@ -2,10 +2,10 @@ import {
|
|
|
2
2
|
buildMigrationPlan,
|
|
3
3
|
install,
|
|
4
4
|
installManagedNode
|
|
5
|
-
} from "./chunk-
|
|
5
|
+
} from "./chunk-KETMM3BA.js";
|
|
6
6
|
export {
|
|
7
7
|
buildMigrationPlan,
|
|
8
8
|
install,
|
|
9
9
|
installManagedNode
|
|
10
10
|
};
|
|
11
|
-
//# sourceMappingURL=install-
|
|
11
|
+
//# sourceMappingURL=install-KA35ONQP.js.map
|
|
@@ -1,12 +1,13 @@
|
|
|
1
1
|
import {
|
|
2
2
|
buildConflictOpts,
|
|
3
3
|
checkWritePathDedup,
|
|
4
|
+
preservedStaleLine,
|
|
4
5
|
reportPlannedWrites
|
|
5
|
-
} from "./chunk-
|
|
6
|
-
import "./chunk-
|
|
6
|
+
} from "./chunk-5KWETEMF.js";
|
|
7
|
+
import "./chunk-TPNJEN4B.js";
|
|
7
8
|
import {
|
|
8
9
|
resolveInteractive
|
|
9
|
-
} from "./chunk-
|
|
10
|
+
} from "./chunk-KETMM3BA.js";
|
|
10
11
|
|
|
11
12
|
// src/sync.ts
|
|
12
13
|
import { existsSync } from "fs";
|
|
@@ -71,6 +72,9 @@ async function sync(root, opts = {}) {
|
|
|
71
72
|
`
|
|
72
73
|
);
|
|
73
74
|
}
|
|
75
|
+
const stale = preservedStaleLine(summary.preserved ?? []);
|
|
76
|
+
if (stale !== void 0) process.stderr.write(`${stale}
|
|
77
|
+
`);
|
|
74
78
|
}
|
|
75
79
|
const dedup = await checkWritePathDedup(root, res, { interactive, project: projectInfo });
|
|
76
80
|
if (dedup.conflicts.length > 0) res.conflicts.push(...dedup.conflicts);
|
|
@@ -88,4 +92,4 @@ function resolveSyncHost(root, opts) {
|
|
|
88
92
|
export {
|
|
89
93
|
sync
|
|
90
94
|
};
|
|
91
|
-
//# sourceMappingURL=sync-
|
|
95
|
+
//# sourceMappingURL=sync-AII5WCRO.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../../src/sync.ts"],"sourcesContent":["// `noir sync` — re-emit the runtime subset of the scaffold manifest.\n//\n// Refactor: delegates to `@noir-ai/create`'s `scaffold({mode:'sync'})`,\n// which emits ONLY the regenerate + managedBlock entries (the always-safe-to-\n// rewrite subset). This ADOPTS THE ENGINE'S\n// SEMANTICS and retires the predecessor's ad-hoc behavior:\n//\n// - The engine re-emits `.mcp.json` (regenerate) + NOIR.md brief\n// (managedBlock) + CLAUDE.md context/rules blocks + ignore-file blocks.\n// The predecessor's sync re-emitted only CLAUDE.md blocks + ignores and\n// did NOT refresh `.mcp.json`/NOIR.md — the engine is a strict superset\n// here, so a transport/url change is now picked up by `noir sync`.\n// - The engine does NOT seed `.noir/rules/RULES.md` on sync (RULES.md is\n// skipIfExists, owned by init/create). The predecessor seeded it when\n// missing; init now owns all seeds. (By design: \"sync re-emits\n// generated/managed content; init owns seeds.\")\n//\n// Multi-host: sync resolves the host from `.noir/config.yml` (persisted by\n// `noir init --host <id>` via the config seed's `host: {{host}}` literal) OR\n// from an explicit `--host <id>` override. The resolved adapter drives the\n// manifest + skills emission. Skills emission stays a core sync feature for\n// hosts with a `skillsDir` (claude → `.claude/skills/`; cursor → `.cursor/rules/`\n// as `.mdc`); gemini/agents-md/opencode have no skill concept and the step is\n// skipped. `scaffold({mode:'sync'})` throws when the project isn't initialized\n// (no .noir/project.id), matching the predecessor's `loadProjectInfo` gate.\n\nimport { existsSync } from 'node:fs';\nimport { type HostId, resolveAdapter } from '@noir-ai/adapters';\nimport { loadProjectInfo, paths } from '@noir-ai/core';\nimport { type ScaffoldResult, scaffold } from '@noir-ai/create';\nimport { type CompileTarget, emitSkillsToDir } from '@noir-ai/skills';\nimport { buildConflictOpts } from './conflict.js';\nimport { checkWritePathDedup } from './dedup-write.js';\nimport { preservedStaleLine, reportPlannedWrites } from './init.js';\nimport { resolveInteractive } from './output.js';\n\nexport interface SyncOptions {\n /** `--host <id>` override. When set, takes precedence over the\n * `.noir/config.yml` `host:` field. Useful for re-emitting under a\n * different host without re-init (advanced — the canonical host stays\n * whatever init wrote). */\n host?: HostId;\n /** Overwrite differing regenerated files without prompting (bypasses\n * the conflict menu). */\n force?: boolean;\n /** Three-way merge managed regions (preserve hand-edits inside\n * `<!-- noir:* -->` markers across a template update). DEFAULT TRUE;\n * `--merge` is now a no-op (kept for backward compatibility). */\n merge?: boolean;\n /** Opt OUT of managed-region merge (restore strip-replace). Exposed on\n * the bin as `--no-merge-regions`. When explicitly `false`, hand-edits\n * inside `<!-- noir:* -->` markers are discarded on a template upgrade. */\n mergeManagedRegions?: boolean;\n /** `--dry-run`/`--preview` — report the planned writes to stderr\n * without touching disk. sync still requires a valid `.noir/project.id`\n * (the engine's \"not initialized\" gate fires regardless). */\n dryRun?: boolean;\n /** Alias for `--dry-run`. Kept on the options bag so direct callers can\n * pass either spelling; the bin collapses both flags before dispatch. */\n preview?: boolean;\n}\n\nexport async function sync(root: string, opts: SyncOptions = {}): Promise<ScaffoldResult> {\n const host = resolveSyncHost(root, opts);\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 // Load project info once for both the dedup hook (embedder config)\n // and the existing config-host fallback. Best-effort: a missing/corrupt file\n // is left to scaffold's \"not initialized\" gate below.\n let projectInfo: ReturnType<typeof loadProjectInfo> | undefined;\n try {\n projectInfo = loadProjectInfo(root);\n } catch {\n projectInfo = undefined;\n }\n\n // --dry-run/--preview collapse to a single dryRun boolean.\n const dryRun = opts.dryRun === true || opts.preview === true;\n\n const res = await scaffold({\n root,\n mode: 'sync',\n host,\n interactive,\n ...(dryRun ? { dryRun: true } : {}),\n ...conflictOpts,\n // Merge defaults TRUE inside scaffold; only forward an explicit opt-out.\n ...(opts.mergeManagedRegions === false ? { mergeManagedRegions: false } : {}),\n });\n\n // dry-run reports the planned writes and stops BEFORE skills emission +\n // dedup (both would touch disk / load the embedder). The engine still throws\n // its \"not initialized\" gate when `.noir/project.id` is missing, so a dry-run\n // sync on an uninitialized project is a clean error (not a misleading no-op).\n if (dryRun) {\n reportPlannedWrites(res);\n return res;\n }\n\n // Surface the no-op so users see sync was a true no-op on disk (the\n // scaffold wrote nothing — every runtime file was content-hash identical).\n // Skills emission below still runs (it has its own dedup).\n if (res.written.length === 0 && res.identical.length > 0) {\n process.stderr.write('Nothing to sync (Noir-managed files are up to date).\\n');\n }\n\n const adapter = resolveAdapter(host);\n const skillsDir = adapter.skillsDir?.({ root });\n if (skillsDir === undefined) {\n // Standardized wording — same phrase across init/sync/create so logs\n // grep uniformly. (sync used to phrase this as \"nothing to sync\".)\n process.stderr.write(`host '${host}' has no skill emitter; skipping skills\\n`);\n } else {\n const target: CompileTarget = host;\n // Forward conflictOpts so the skills-emit conflict flow is\n // live in interactive mode (mirrors init.ts). --json/--no-input stays\n // prompt-free via the `interactive: false` guard.\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 `Synced ${summary.emitted.length} Noir skills to ${relDir}/ (target: ${target}).\\n`,\n );\n // Surface stale-dir pruning so a user can see when a previous Noir\n // version's builtin was removed (the dir was deleted from .claude/skills/).\n // Pure hygiene; never affects correctness of the freshly-emitted pack.\n const pruned = summary.pruned ?? [];\n if (pruned.length > 0) {\n process.stderr.write(\n `Pruned ${pruned.length} stale noir-* skill dir${pruned.length === 1 ? '' : 's'}: ${pruned.join(', ')}\\n`,\n );\n }\n // The emitted count above is skills that are fully current; anything the\n // emit preserved is stale on disk and has to be named, exactly as `noir\n // init` names it.\n const stale = preservedStaleLine(summary.preserved ?? []);\n if (stale !== undefined) process.stderr.write(`${stale}\\n`);\n }\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 const dedup = await checkWritePathDedup(root, res, { interactive, project: projectInfo });\n if (dedup.conflicts.length > 0) res.conflicts.push(...dedup.conflicts);\n\n return res;\n}\n\n/** Resolve the sync host: explicit `--host` override > `.noir/config.yml`\n * `host:` field > `'claude'` (default — preserves the v1.1 regression anchor\n * for projects initialized before the multi-host layer). The config read is\n * best-effort: if\n * `.noir/config.yml` is absent OR fails to parse, scaffold's own\n * \"not initialized\" gate (no `.noir/project.id`) fires below with the locked\n * error string. */\nfunction resolveSyncHost(root: string, opts: SyncOptions): HostId {\n if (opts.host !== undefined) return opts.host;\n if (!existsSync(paths.config(root))) return 'claude';\n try {\n return loadProjectInfo(root).config.host;\n } catch {\n return 'claude';\n }\n}\n"],"mappings":";;;;;;;;;;;;AA0BA,SAAS,kBAAkB;AAC3B,SAAsB,sBAAsB;AAC5C,SAAS,iBAAiB,aAAa;AACvC,SAA8B,gBAAgB;AAC9C,SAA6B,uBAAuB;AAgCpD,eAAsB,KAAK,MAAc,OAAoB,CAAC,GAA4B;AACxF,QAAM,OAAO,gBAAgB,MAAM,IAAI;AAGvC,QAAM,cAAc,mBAAmB;AACvC,QAAM,eAAe,kBAAkB,EAAE,OAAO,KAAK,OAAO,YAAY,CAAC;AAKzE,MAAI;AACJ,MAAI;AACF,kBAAc,gBAAgB,IAAI;AAAA,EACpC,QAAQ;AACN,kBAAc;AAAA,EAChB;AAGA,QAAM,SAAS,KAAK,WAAW,QAAQ,KAAK,YAAY;AAExD,QAAM,MAAM,MAAM,SAAS;AAAA,IACzB;AAAA,IACA,MAAM;AAAA,IACN;AAAA,IACA;AAAA,IACA,GAAI,SAAS,EAAE,QAAQ,KAAK,IAAI,CAAC;AAAA,IACjC,GAAG;AAAA;AAAA,IAEH,GAAI,KAAK,wBAAwB,QAAQ,EAAE,qBAAqB,MAAM,IAAI,CAAC;AAAA,EAC7E,CAAC;AAMD,MAAI,QAAQ;AACV,wBAAoB,GAAG;AACvB,WAAO;AAAA,EACT;AAKA,MAAI,IAAI,QAAQ,WAAW,KAAK,IAAI,UAAU,SAAS,GAAG;AACxD,YAAQ,OAAO,MAAM,wDAAwD;AAAA,EAC/E;AAEA,QAAM,UAAU,eAAe,IAAI;AACnC,QAAM,YAAY,QAAQ,YAAY,EAAE,KAAK,CAAC;AAC9C,MAAI,cAAc,QAAW;AAG3B,YAAQ,OAAO,MAAM,SAAS,IAAI;AAAA,CAA2C;AAAA,EAC/E,OAAO;AACL,UAAM,SAAwB;AAK9B,UAAM,YAA2B;AAAA,MAC/B,qBAAqB;AAAA,MACrB;AAAA,MACA,gBAAgB,aAAa;AAAA,MAC7B;AAAA,IACF;AACA,QAAI,aAAa,eAAe,QAAW;AACzC,gBAAU,aAAa,aAAa;AAAA,IACtC;AACA,UAAM,UAAU,MAAM,gBAAgB,WAAW,SAAS;AAC1D,UAAM,SAAS,UAAU,QAAQ,GAAG,IAAI,KAAK,EAAE;AAC/C,YAAQ,OAAO;AAAA,MACb,UAAU,QAAQ,QAAQ,MAAM,mBAAmB,MAAM,cAAc,MAAM;AAAA;AAAA,IAC/E;AAIA,UAAM,SAAS,QAAQ,UAAU,CAAC;AAClC,QAAI,OAAO,SAAS,GAAG;AACrB,cAAQ,OAAO;AAAA,QACb,UAAU,OAAO,MAAM,0BAA0B,OAAO,WAAW,IAAI,KAAK,GAAG,KAAK,OAAO,KAAK,IAAI,CAAC;AAAA;AAAA,MACvG;AAAA,IACF;AAIA,UAAM,QAAQ,mBAAmB,QAAQ,aAAa,CAAC,CAAC;AACxD,QAAI,UAAU,OAAW,SAAQ,OAAO,MAAM,GAAG,KAAK;AAAA,CAAI;AAAA,EAC5D;AAKA,QAAM,QAAQ,MAAM,oBAAoB,MAAM,KAAK,EAAE,aAAa,SAAS,YAAY,CAAC;AACxF,MAAI,MAAM,UAAU,SAAS,EAAG,KAAI,UAAU,KAAK,GAAG,MAAM,SAAS;AAErE,SAAO;AACT;AASA,SAAS,gBAAgB,MAAc,MAA2B;AAChE,MAAI,KAAK,SAAS,OAAW,QAAO,KAAK;AACzC,MAAI,CAAC,WAAW,MAAM,OAAO,IAAI,CAAC,EAAG,QAAO;AAC5C,MAAI;AACF,WAAO,gBAAgB,IAAI,EAAE,OAAO;AAAA,EACtC,QAAQ;AACN,WAAO;AAAA,EACT;AACF;","names":[]}
|
|
@@ -5,8 +5,8 @@ import {
|
|
|
5
5
|
runAsyncUpdateCheck,
|
|
6
6
|
update,
|
|
7
7
|
updateAdvice
|
|
8
|
-
} from "./chunk-
|
|
9
|
-
import "./chunk-
|
|
8
|
+
} from "./chunk-TPNJEN4B.js";
|
|
9
|
+
import "./chunk-KETMM3BA.js";
|
|
10
10
|
export {
|
|
11
11
|
DEFAULT_UPDATE_CONFIG,
|
|
12
12
|
buildUpdateNotice,
|
|
@@ -15,4 +15,4 @@ export {
|
|
|
15
15
|
update,
|
|
16
16
|
updateAdvice
|
|
17
17
|
};
|
|
18
|
-
//# sourceMappingURL=update-
|
|
18
|
+
//# sourceMappingURL=update-XGKJ4KBR.js.map
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@noir-ai/cli",
|
|
3
|
-
"version": "1.
|
|
3
|
+
"version": "1.15.0",
|
|
4
4
|
"description": "Noir CLI — the `noir` command tree (commander + Ink dashboard + @clack home menu), the shell entry point to the Noir AI toolkit.",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"author": "agaaaptr",
|
|
@@ -55,16 +55,16 @@
|
|
|
55
55
|
"ora": "^8.1.0",
|
|
56
56
|
"picocolors": "^1.1.1",
|
|
57
57
|
"react": "^19.2.0",
|
|
58
|
-
"@noir-ai/
|
|
59
|
-
"@noir-ai/
|
|
60
|
-
"@noir-ai/
|
|
61
|
-
"@noir-ai/create": "1.
|
|
62
|
-
"@noir-ai/
|
|
63
|
-
"@noir-ai/
|
|
64
|
-
"@noir-ai/model": "1.
|
|
65
|
-
"@noir-ai/
|
|
66
|
-
"@noir-ai/
|
|
67
|
-
"@noir-ai/workflow": "1.
|
|
58
|
+
"@noir-ai/context": "1.15.0",
|
|
59
|
+
"@noir-ai/adapters": "1.15.0",
|
|
60
|
+
"@noir-ai/core": "1.15.0",
|
|
61
|
+
"@noir-ai/create": "1.15.0",
|
|
62
|
+
"@noir-ai/daemon": "1.15.0",
|
|
63
|
+
"@noir-ai/memory": "1.15.0",
|
|
64
|
+
"@noir-ai/model": "1.15.0",
|
|
65
|
+
"@noir-ai/store": "1.15.0",
|
|
66
|
+
"@noir-ai/skills": "1.15.0",
|
|
67
|
+
"@noir-ai/workflow": "1.15.0"
|
|
68
68
|
},
|
|
69
69
|
"devDependencies": {
|
|
70
70
|
"@types/node": "^22.17.0",
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
{"version":3,"sources":["../../src/commands/update.ts"],"sourcesContent":["import {\n detectActiveMethod,\n ensureShimExecutable,\n fetchLatestVersion,\n loadProjectInfo,\n readInstallRecord,\n readUpdateCache,\n runManagerCmd,\n shouldCheckForUpdate,\n type UpdateConfigLike,\n writeUpdateCache,\n} from '@noir-ai/core';\nimport { type CliOptions, EXIT, fail, info, spinner, success, warn } from '../output.js';\nimport { installManagedNode } from './install.js';\n\nexport const DEFAULT_UPDATE_CONFIG: UpdateConfigLike = {\n checkEnabled: true,\n checkIntervalHours: 24,\n channel: 'latest',\n minVersion: '1.6.0',\n display: 'notice',\n};\n\nexport interface UpdateOptions extends CliOptions {\n check?: boolean;\n spec?: string;\n}\n\nexport interface UpdateTarget {\n method: string;\n targetSpec: string;\n currentVersion: string | null;\n latestKnown: string | null;\n isUpgrade: boolean;\n /**\n * I2 — `update.minVersion` floor (config.ts:238). True when the latest-known\n * version the registry returned is below the configured floor (e.g. a\n * beta/prerelease channel lag, or a yanked dist-tag). Enforced in\n * {@link update} with the same warn/refuse pattern as the downgrade guard.\n */\n belowMinVersion: boolean;\n}\n\nexport function buildUpdateTarget(opts: {\n method: string;\n channel: string;\n spec?: string;\n currentVersion: string | null;\n latestKnown: 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}): UpdateTarget {\n const targetSpec = opts.spec ?? opts.channel;\n // The guard target: a concrete `--spec` (an exact version) WINS over the\n // fetched registry version — the T6/I2 guards must evaluate what we will\n // ACTUALLY install, not what the registry happens to offer. A pinned\n // `--spec 1.9.0` against a 1.12.0 registry is a DOWNGRADE, and a pinned\n // `--spec 1.5.0` can trip the minVersion floor — both were previously missed\n // because the guards only ever looked at `latestKnown`.\n const concrete =\n opts.spec != null && opts.spec !== 'latest' && opts.spec !== 'beta'\n ? opts.spec\n : opts.latestKnown;\n // Semver downgrade guard (T6 hardening): only treat the target as an upgrade\n // when it is STRICTLY NEWER than the current one. The prior inequality check\n // (`latestKnown !== currentVersion`) would treat a registry that returned an\n // OLDER version (e.g. beta/prerelease channel lag) as an \"upgrade\" and\n // silently downgrade the install.\n const isUpgrade =\n concrete != null && opts.currentVersion != null && semverGt(concrete, opts.currentVersion);\n // I2 — minVersion floor: refuse a target version below the floor. null\n // latestKnown is handled separately (registry-unreachable branch).\n const floor = opts.minVersion ?? '1.6.0';\n const belowMinVersion = concrete != null && semverLt(concrete, floor);\n return {\n method: opts.method,\n targetSpec,\n currentVersion: opts.currentVersion,\n latestKnown: opts.latestKnown,\n isUpgrade,\n belowMinVersion,\n };\n}\n\n/**\n * Per-segment numeric semver comparison (major.minor.patch). Returns true when\n * `a` is strictly greater than `b`. Pre-release suffixes are ignored — Noir\n * versioning is `MAJOR.MINOR.PATCH` only, and the registry's dist-tag is the\n * source of truth for \"latest\". Non-numeric segments coerce to 0. Used here\n * instead of a full semver library to keep the CLI's dependency surface\n * unchanged (the same helper lives in install.ts for the downgrade guard).\n */\nfunction semverGt(a: string, b: string): boolean {\n // Strip a leading `v` (npm-idiomatic) — bare Number('v1') is NaN.\n const seg = (s: string): number => Number(s.replace(/^v/, '')) || 0;\n const pa = a.split('.').map(seg);\n const pb = b.split('.').map(seg);\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/**\n * Per-segment numeric semver `<` (I2 — minVersion floor). Same comparison\n * discipline as {@link semverGt} / install.ts's downgrade guard: numeric per\n * segment, non-numeric → 0. Returns true when `a` is strictly less than `b`.\n */\nfunction semverLt(a: string, b: string): boolean {\n // Strip a leading `v` (npm-idiomatic) — bare Number('v1') is NaN.\n const seg = (s: string): number => Number(s.replace(/^v/, '')) || 0;\n const pa = a.split('.').map(seg);\n const pb = b.split('.').map(seg);\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\nexport async function update(opts: UpdateOptions = {}): Promise<void> {\n // NOIR_DISABLE_UPDATES is the hard kill-switch for the SELF-UPDATE surface\n // (distinct from NOIR_DISABLE_UPDATE_CHECK, which only gags the background\n // version check). When set, `noir update` refuses to run at all — enterprises\n // can pin this in the environment to enforce \"updates flow only through the\n // package manager / image rebuild, never from inside the CLI\". `fail` is\n // `: never` (always throws) — the explicit `return` is defensive.\n if (process.env.NOIR_DISABLE_UPDATES !== undefined) {\n fail(\n EXIT.USAGE,\n 'self-update is disabled (NOIR_DISABLE_UPDATES is set); update via your package manager or image rebuild.',\n opts,\n );\n return;\n }\n\n const method = detectActiveMethod();\n const rec = readInstallRecord();\n const currentVersion = rec?.version ?? null;\n\n if (opts.check === true) {\n const checkSpin = spinner('Checking for updates...', opts).start();\n const latest = await fetchLatestVersion('latest');\n if (latest) {\n checkSpin.succeed(`Latest: ${latest} (you have ${currentVersion ?? 'unknown'})`);\n if (opts.json === true) {\n process.stdout.write(`${JSON.stringify({ ok: true, data: { latest, currentVersion } })}\\n`);\n }\n } else {\n checkSpin.fail('Could not reach the registry.');\n // fail() emits the canonical {ok:false,error} envelope under --json AND\n // exits 1 (a manual envelope here would leave the process at exit 0 while\n // the payload claims code 1).\n fail(EXIT.ERROR, 'Could not reach the registry.', opts);\n }\n return;\n }\n\n // Fetch latest (network; timeout-bound).\n const fetchSpin = spinner('Checking for updates...', opts).start();\n const latest = await fetchLatestVersion('latest');\n if (latest === null) {\n fetchSpin.fail('Could not reach the registry.');\n // Registry-unreachable is a FAILURE: route through fail() so --json emits\n // {ok:false,error} on stdout AND the process exits 1 (a silent return would\n // leave stdout empty + exit 0 — indistinguishable from success for scripts).\n fail(EXIT.ERROR, 'Could not reach the registry.', opts);\n }\n fetchSpin.succeed(`Latest: ${latest}`);\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 = DEFAULT_UPDATE_CONFIG.minVersion;\n try {\n minVersion = loadProjectInfo(process.cwd()).config.update.minVersion;\n } catch {\n // Not initialized yet — the config default applies.\n }\n const target = buildUpdateTarget({\n method,\n channel: 'latest',\n spec: opts.spec,\n currentVersion,\n latestKnown: latest,\n minVersion,\n });\n\n // I2 — refuse to install a registry-offered version below the configured\n // minVersion floor (same warn/refuse pattern as install.ts's downgrade guard).\n // Checked before the isUpgrade branch: a below-floor offer is unsafe even if\n // it happens to be newer than the (very old) current install.\n if (target.belowMinVersion) {\n // Name the ACTUAL target (a concrete --spec), not the registry's latest.\n warn(`Target ${target.targetSpec} is below the minVersion floor (${minVersion}).`);\n fail(\n EXIT.USAGE,\n `refusing update to ${target.targetSpec} (below minVersion ${minVersion})`,\n opts,\n );\n }\n\n if (!target.isUpgrade) {\n fetchSpin.succeed(`noir ${currentVersion} is up to date`);\n if (opts.json === true) {\n process.stdout.write(\n `${JSON.stringify({ ok: true, data: { version: currentVersion, upToDate: true } })}\\n`,\n );\n return;\n }\n info(`noir ${currentVersion} is up to date.`, opts);\n return;\n }\n\n if (method === 'native') {\n const updateSpin = spinner(\n `Updating to ${target.latestKnown} via native installer...`,\n opts,\n ).start();\n const res = await installManagedNode({\n version: target.targetSpec === 'latest' ? undefined : target.targetSpec,\n env: process.env,\n });\n if (!res.ok) {\n updateSpin.fail('Update failed');\n fail(EXIT.ERROR, res.error ?? 'update failed', opts);\n }\n // Chicken-egg safety net: the update was ORCHESTRATED by the OLD binary\n // (which may predate the chmod fix). Re-assert the freshly-written shim is\n // executable so the user's NEXT `noir …` doesn't fail with permission\n // denied. This is the ONLY line that closes the regression once 1.7.4+ is\n // installed — it runs in the NEW binary's process.\n ensureShimExecutable();\n updateSpin.succeed(`Updated to noir ${res.version}`);\n if (opts.json === true) {\n process.stdout.write(`${JSON.stringify({ ok: true, data: { version: res.version } })}\\n`);\n return;\n }\n success(`Updated to ${res.version}.`);\n return;\n }\n\n // npm/pnpm/yarn/bun/homebrew/scoop -> reinstall via the same manager.\n const cmd = updateCmdFor(method, target.targetSpec);\n if (!cmd)\n fail(EXIT.USAGE, `cannot auto-update a ${method} install; use the manager directly`, opts);\n const mgrSpin = spinner(`Updating via ${method}...`, opts).start();\n const { code, stderr } = await runManagerCmd(cmd, { env: process.env });\n if (code !== 0) {\n mgrSpin.fail(`Update failed`);\n fail(EXIT.ERROR, `update failed: ${stderr.slice(0, 300)}`, opts);\n }\n mgrSpin.succeed('Updated');\n if (opts.json === true) {\n process.stdout.write(`${JSON.stringify({ ok: true, data: { version: target.targetSpec } })}\\n`);\n return;\n }\n success('Updated.');\n}\n\nfunction updateCmdFor(method: string, spec: string): string[] | null {\n switch (method) {\n case 'npm':\n return ['npm', 'install', '-g', `@noir-ai/cli@${spec}`];\n case 'pnpm':\n return ['pnpm', 'add', '-g', `@noir-ai/cli@${spec}`];\n case 'yarn':\n return ['yarn', 'global', 'add', `@noir-ai/cli@${spec}`];\n case 'bun':\n return ['bun', 'add', '-g', `@noir-ai/cli@${spec}`];\n case 'homebrew':\n return ['brew', 'upgrade', 'noir'];\n case 'scoop':\n return ['scoop', 'update', 'noir'];\n default:\n return null;\n }\n}\n\n/**\n * The human-readable update command for an install method. Mirrors\n * {@link updateCmdFor} (the machine command) with the manager the user should\n * actually run — `noir update` is the universal entry (it re-dispatches to the\n * right manager), but the package-manager spellings are shown for transparency.\n */\nexport function updateAdvice(method: string): string {\n switch (method) {\n case 'npm':\n return 'run `npm install -g @noir-ai/cli@latest`';\n case 'pnpm':\n return 'run `pnpm add -g @noir-ai/cli@latest`';\n case 'yarn':\n return 'run `yarn global add @noir-ai/cli@latest`';\n case 'bun':\n return 'run `bun add -g @noir-ai/cli@latest`';\n case 'homebrew':\n return 'run `brew upgrade noir`';\n case 'scoop':\n return 'run `scoop update noir`';\n case 'native':\n return 'run `noir update`';\n default:\n return 'run `noir update`';\n }\n}\n\n/** A \"new version available\" notice for the home menu (version + advice). */\nexport interface UpdateNotice {\n readonly latestVersion: string;\n readonly currentVersion: string;\n readonly method: string;\n readonly advice: string;\n}\n\n/**\n * Build the home-menu \"update available\" notice from the cached latest version.\n * Returns `null` when there is no cached latest, no current version, or the\n * latest is NOT strictly newer (the semver downgrade guard — a stale/older\n * cache entry is never advertised as an upgrade).\n */\nexport function buildUpdateNotice(opts: {\n method: string;\n currentVersion: string | null;\n latestKnown: string | null;\n}): UpdateNotice | null {\n const { method, currentVersion, latestKnown } = opts;\n if (currentVersion == null || latestKnown == null) return null;\n if (!semverGt(latestKnown, currentVersion)) return null;\n return { latestVersion: latestKnown, currentVersion, method, advice: updateAdvice(method) };\n}\n\n/** Non-blocking, time-boxed startup check. Writes the cache on success; silent on any failure. */\nexport async function runAsyncUpdateCheck(opts: {\n env: NodeJS.ProcessEnv;\n configUpdate: UpdateConfigLike;\n quiet: boolean;\n}): Promise<void> {\n try {\n const cache = readUpdateCache();\n if (!shouldCheckForUpdate({ env: opts.env, configUpdate: opts.configUpdate, cache })) return;\n const controller = new AbortController();\n const timer = setTimeout(() => controller.abort(), 2000);\n const latest = await fetchLatestVersion(opts.configUpdate.channel, controller.signal);\n clearTimeout(timer);\n if (latest != null) {\n writeUpdateCache({\n lastCheckAt: new Date().toISOString(),\n latestVersion: latest,\n channel: opts.configUpdate.channel,\n });\n }\n // Non-blocking: never print in quiet/CI/non-TTY; a mismatch only nudges.\n } catch {\n // silent on any failure — this is a non-blocking background check\n }\n}\n"],"mappings":"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","names":["latest"]}
|