@noir-ai/cli 1.3.0-beta.6 → 1.4.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.
@@ -0,0 +1,33 @@
1
+ #!/usr/bin/env node
2
+
3
+ // src/serve.ts
4
+ import { loadProjectInfo } from "@noir-ai/core";
5
+ import { ensureDaemonRunning, startStdioServer } from "@noir-ai/daemon";
6
+ async function serve(opts) {
7
+ const project = loadProjectInfo(process.cwd());
8
+ if (opts.stdio) {
9
+ await startStdioServer({ project, transport: "stdio", daemon: false });
10
+ return;
11
+ }
12
+ try {
13
+ const { url } = await ensureDaemonRunning({
14
+ project,
15
+ idleTimeoutSec: project.config.daemon.idleTimeoutSec
16
+ });
17
+ process.stderr.write(
18
+ `Noir daemon available at ${url}. (For HTTP clients, use this URL in .mcp.json.)
19
+ `
20
+ );
21
+ } catch (err) {
22
+ process.stderr.write(
23
+ `Noir daemon unavailable (${err instanceof Error ? err.message : String(err)}); falling back to stdio.
24
+ `
25
+ );
26
+ await startStdioServer({ project, transport: "stdio", daemon: false });
27
+ }
28
+ }
29
+
30
+ export {
31
+ serve
32
+ };
33
+ //# sourceMappingURL=chunk-A5W2E3X2.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../src/serve.ts"],"sourcesContent":["import { loadProjectInfo } from '@noir-ai/core';\nimport { ensureDaemonRunning, startStdioServer } from '@noir-ai/daemon';\n\nexport async function serve(opts: { stdio: boolean }): Promise<void> {\n const project = loadProjectInfo(process.cwd());\n if (opts.stdio) {\n await startStdioServer({ project, transport: 'stdio', daemon: false });\n return; // stdio server runs until stdin closes\n }\n // Prefer the shared daemon; on failure fall back to in-process stdio so Noir\n // keeps working even when the daemon cannot start (FS-degradation path).\n try {\n const { url } = await ensureDaemonRunning({\n project,\n idleTimeoutSec: project.config.daemon.idleTimeoutSec,\n });\n process.stderr.write(\n `Noir daemon available at ${url}. (For HTTP clients, use this URL in .mcp.json.)\\n`,\n );\n } catch (err) {\n process.stderr.write(\n `Noir daemon unavailable (${err instanceof Error ? err.message : String(err)}); falling back to stdio.\\n`,\n );\n await startStdioServer({ project, transport: 'stdio', daemon: false });\n }\n}\n"],"mappings":";;;AAAA,SAAS,uBAAuB;AAChC,SAAS,qBAAqB,wBAAwB;AAEtD,eAAsB,MAAM,MAAyC;AACnE,QAAM,UAAU,gBAAgB,QAAQ,IAAI,CAAC;AAC7C,MAAI,KAAK,OAAO;AACd,UAAM,iBAAiB,EAAE,SAAS,WAAW,SAAS,QAAQ,MAAM,CAAC;AACrE;AAAA,EACF;AAGA,MAAI;AACF,UAAM,EAAE,IAAI,IAAI,MAAM,oBAAoB;AAAA,MACxC;AAAA,MACA,gBAAgB,QAAQ,OAAO,OAAO;AAAA,IACxC,CAAC;AACD,YAAQ,OAAO;AAAA,MACb,4BAA4B,GAAG;AAAA;AAAA,IACjC;AAAA,EACF,SAAS,KAAK;AACZ,YAAQ,OAAO;AAAA,MACb,4BAA4B,eAAe,QAAQ,IAAI,UAAU,OAAO,GAAG,CAAC;AAAA;AAAA,IAC9E;AACA,UAAM,iBAAiB,EAAE,SAAS,WAAW,SAAS,QAAQ,MAAM,CAAC;AAAA,EACvE;AACF;","names":[]}
@@ -1,27 +1,41 @@
1
1
  #!/usr/bin/env node
2
2
  import {
3
- buildConflictOpts
4
- } from "./chunk-OF4OO5W6.js";
3
+ buildConflictOpts,
4
+ checkWritePathDedup,
5
+ resolveInteractive
6
+ } from "./chunk-YES5UCU5.js";
5
7
 
6
8
  // src/init.ts
7
9
  import { resolveAdapter } from "@noir-ai/adapters";
10
+ import { loadProjectInfo } from "@noir-ai/core";
8
11
  import { scaffold } from "@noir-ai/create";
9
12
  import { emitSkillsToDir } from "@noir-ai/skills";
10
13
  async function init(root, opts) {
11
14
  assertTransportUrl(opts);
12
15
  const host = opts.host ?? "claude";
16
+ const interactive = resolveInteractive();
17
+ const conflictOpts = buildConflictOpts({ force: opts.force, interactive });
13
18
  const res = await scaffold({
14
19
  root,
15
20
  mode: "init",
16
21
  host,
17
22
  transport: opts.transport,
23
+ interactive,
18
24
  ...opts.url !== void 0 ? { url: opts.url } : {},
19
25
  ...opts.upgrade === true ? { upgrade: true } : {},
20
26
  ...opts.force === true ? { force: true } : {},
21
- ...buildConflictOpts({ force: opts.force })
27
+ ...conflictOpts
22
28
  });
23
- if (res.noop) return;
24
- await emitHostSkills(root, host);
29
+ if (res.noop) return res;
30
+ await emitHostSkills(root, host, conflictOpts, interactive);
31
+ let projectInfo;
32
+ try {
33
+ projectInfo = loadProjectInfo(root);
34
+ } catch {
35
+ projectInfo = void 0;
36
+ }
37
+ const dedup = await checkWritePathDedup(root, res, { interactive, project: projectInfo });
38
+ if (dedup.conflicts.length > 0) res.conflicts.push(...dedup.conflicts);
25
39
  process.stderr.write(
26
40
  `Noir initialized in ${root} (host: ${host}, transport: ${opts.transport}).
27
41
  `
@@ -29,8 +43,9 @@ async function init(root, opts) {
29
43
  process.stderr.write(
30
44
  "Next: run `noir` to open the home menu (or `noir status` for a snapshot).\n"
31
45
  );
46
+ return res;
32
47
  }
33
- async function emitHostSkills(root, host) {
48
+ async function emitHostSkills(root, host, conflictOpts, interactive) {
34
49
  const adapter = resolveAdapter(host);
35
50
  const skillsDir = adapter.skillsDir?.({ root });
36
51
  if (skillsDir === void 0) {
@@ -39,7 +54,16 @@ async function emitHostSkills(root, host) {
39
54
  return;
40
55
  }
41
56
  const target = host;
42
- const summary = await emitSkillsToDir(skillsDir, { includeIntegrations: true, target });
57
+ const skillOpts = {
58
+ includeIntegrations: true,
59
+ target,
60
+ conflictPolicy: conflictOpts.conflictPolicy,
61
+ interactive
62
+ };
63
+ if (conflictOpts.onConflict !== void 0) {
64
+ skillOpts.onConflict = conflictOpts.onConflict;
65
+ }
66
+ const summary = await emitSkillsToDir(skillsDir, skillOpts);
43
67
  const relDir = skillsDir.replace(`${root}/`, "");
44
68
  process.stderr.write(
45
69
  `Emitted ${summary.emitted.length} Noir skills to ${relDir}/ (target: ${target}).
@@ -73,4 +97,4 @@ export {
73
97
  init,
74
98
  assertTransportUrl
75
99
  };
76
- //# sourceMappingURL=chunk-J2Y3BOVK.js.map
100
+ //# sourceMappingURL=chunk-SVFQGAOF.js.map
@@ -0,0 +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":[]}