@noir-ai/create 1.2.0-beta.2 → 1.3.0-beta.2

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/dist/index.d.ts CHANGED
@@ -1,6 +1,16 @@
1
1
  import { HostId, HostAdapter } from '@noir-ai/adapters';
2
2
  import { ManagedBlock } from '@noir-ai/core';
3
3
 
4
+ /** Absolute path of the ancestor store under `root`. */
5
+ declare function ancestorsPath(root: string): string;
6
+ /** Read the ancestor map (`{ "${relPath}::${blockBegin}": regionText }`).
7
+ * Returns `{}` for a missing/corrupt file (never throws). */
8
+ declare function readAncestors(root: string): Record<string, string>;
9
+ /** Write the ancestor map. Ensures `.noir/` exists (a standalone caller may
10
+ * invoke this before the scaffold has created the dir). Derived state, so the
11
+ * plain write (no tmp) is fine. */
12
+ declare function writeAncestors(root: string, map: Record<string, string>): void;
13
+
4
14
  /**
5
15
  * The three-mode writer — generalizes keystone-K's `writeManagedRegion` into
6
16
  * the declarative dispatch the scaffold manifest drives. Each mode maps 1:1 to
@@ -211,6 +221,21 @@ interface BuildHostArtifactsContext {
211
221
  */
212
222
  declare function buildHostArtifacts(adapter: HostAdapter, ctx: BuildHostArtifactsContext): ManifestEntry[];
213
223
 
224
+ interface MergeResult {
225
+ /** The merged text. When `conflict` is true, contains inline markers. */
226
+ merged: string;
227
+ /** True when ours and theirs diverged in the same region (markers emitted). */
228
+ conflict: boolean;
229
+ }
230
+ /**
231
+ * Three-way merge of `ours` (the user's current region) against `theirs` (the
232
+ * new template) with `base` (the last-emitted ancestor). Line-level diff3:
233
+ * changes in disjoint line ranges merge; overlapping changes become inline
234
+ * conflict markers (`<<<<<<< ours` / `=======` / `>>>>>>> theirs`). Pure +
235
+ * deterministic (no IO) so it's unit-testable.
236
+ */
237
+ declare function mergeThreeWay(base: string, ours: string, theirs: string): MergeResult;
238
+
214
239
  /**
215
240
  * Migration model — Nx-style declarative upgrade scripts, hand-rolled.
216
241
  *
@@ -365,15 +390,43 @@ interface ScaffoldOptions {
365
390
  * regenerate + managedBlock (skipIfExists left alone). Only meaningful
366
391
  * with mode `'init'`. */
367
392
  upgrade?: boolean;
393
+ /** SP-A: re-scaffold even when the target is already initialized (bypasses
394
+ * the already-initialized no-op guard). Does NOT bypass `assertSafeRoot`
395
+ * — root-safety is hard, never bypassable. */
396
+ force?: boolean;
368
397
  /** Preview: compute the same written/skipped/migrated lists without touching
369
398
  * disk. `noir doctor`/CI use this to report drift. */
370
399
  dryRun?: boolean;
400
+ /** SP-C: policy for a `regenerate` file that exists and DIFFERS from the
401
+ * template, when no {@link onConflict} callback is provided. Default
402
+ * `'overwrite'` is byte-backward-compatible. `'preserve'` keeps the user's
403
+ * file (the non-TTY / CI default in the cli). */
404
+ conflictPolicy?: 'overwrite' | 'preserve';
405
+ /** SP-C: per-file conflict resolver — the UI seam (the engine stays UI-free;
406
+ * the cli injects a @clack-based resolver). Called when a `regenerate`
407
+ * file exists and differs from the template. */
408
+ onConflict?: (ctx: ConflictContext) => Promise<ConflictResolution> | ConflictResolution;
409
+ /** SP-D follow-up: three-way merge managed regions (base/ours/theirs) using a
410
+ * persisted ancestor snapshot (`.noir/ancestors.json`) instead of
411
+ * strip-replace, so a hand-edit inside a `<!-- noir:* -->` region survives a
412
+ * template update. Opt-in (default false ⇒ current strip-replace, no
413
+ * ancestor file). Single-region managed files only (NOIR.md, ignores);
414
+ * multi-region (CLAUDE.md) is a follow-up. */
415
+ mergeManagedRegions?: boolean;
371
416
  }
372
417
  interface ScaffoldResult {
373
418
  /** Repo-relative paths actually written. */
374
419
  written: string[];
375
420
  /** Repo-relative paths skipIfExists'd (already present). */
376
421
  skipped: string[];
422
+ /** SP-D: repo-relative `regenerate` paths skipped because byte-identical to
423
+ * the template (content-hash dedup — no rewrite). */
424
+ identical: string[];
425
+ /** SP-A: true when the already-initialized guard short-circuited (a bare
426
+ * `noir init`/`create` on an initialized project). Callers (init.ts/create.ts)
427
+ * gate skills emission + the "initialized" message on this — a no-op must NOT
428
+ * re-emit skills or claim it initialized. */
429
+ noop: boolean;
377
430
  /** Migration steps executed (`<from>→<to>`), when upgrade ran. */
378
431
  migrationsRan: string[];
379
432
  /** Migration conflicts (repo-relative or `<runner>:…`), when upgrade ran. */
@@ -385,6 +438,35 @@ interface ScaffoldResult {
385
438
  /** The host actually emitted (post-default). */
386
439
  host: HostTag;
387
440
  }
441
+ /** SP-C — context passed to {@link ScaffoldOptions.onConflict} when a
442
+ * `regenerate` file exists and differs from the template. */
443
+ interface ConflictContext {
444
+ /** Repo-relative path of the conflicting file. */
445
+ relPath: string;
446
+ /** The file's current on-disk content. */
447
+ existing: string;
448
+ /** The content the scaffold would write. */
449
+ proposed: string;
450
+ }
451
+ /** SP-C — how to resolve a `regenerate` conflict. */
452
+ type ConflictResolution = 'replace' | 'preserve' | 'rename' | 'duplicate' | 'cancel';
453
+ /**
454
+ * Refuse to scaffold when `root` is — or is inside — a `.noir/` directory.
455
+ * (SP-A) Running `noir init`/`create`/`sync` while cwd = `.noir/` would
456
+ * otherwise mint a FRESH project id (because `<root>/.noir/project.id` is
457
+ * absent) and build a NESTED second project (`.noir/.noir/`, `.noir/CLAUDE.md`,
458
+ * `.noir/.claude/skills/`, …) — the duplicate-`.noir` bug. The walk ascends the
459
+ * ancestor chain only, so a legitimate project root that merely CONTAINS a
460
+ * `.noir/` store is never flagged. String-based: works whether or not `root`
461
+ * exists on disk yet (so `noir create <new-dir>` is still guarded).
462
+ *
463
+ * Hard against literal `.noir` path segments — NOT bypassable by `--force`
464
+ * (which is reserved for the already-initialized no-op). NOTE: the walk is
465
+ * string-based, so a symlink whose TARGET is inside `.noir/` is not resolved
466
+ * (a local self-foot-gun only — the caller controls `root` — not externally
467
+ * exploitable); in practice `--cwd`/positional roots are literal paths.
468
+ */
469
+ declare function assertSafeRoot(root: string): void;
388
470
  declare function scaffold(opts: ScaffoldOptions): Promise<ScaffoldResult>;
389
471
 
390
472
  /**
@@ -445,4 +527,4 @@ declare function loadTemplate(name: string): string;
445
527
  /** Resolve a template name to its absolute path (for diagnostics + tests). */
446
528
  declare function templatesDir(): string;
447
529
 
448
- export { BRIEF_BLOCK, type BuildHostArtifactsContext, type BuildManifestContext, CURRENT_SCAFFOLD_VERSION, type HostTag, MANIFEST_PATH_PARITY, MIGRATIONS, type ManifestEntry, type MigrationContext, type MigrationResult, type MigrationScript, type ScaffoldMode, type ScaffoldOptions, type ScaffoldResult, type StackInfo, type WriteMode, type WriteOutcome, applyInlineConflict, applyWithConflict, buildHostArtifacts, buildManifest, buildRegion, detectStack, loadTemplate, managedBlock, readScaffoldVersion, regenerate, render, runMigrations, scaffold, scaffoldVersionPath, skipIfExists, templatesDir, writeScaffoldVersion };
530
+ export { BRIEF_BLOCK, type BuildHostArtifactsContext, type BuildManifestContext, CURRENT_SCAFFOLD_VERSION, type ConflictContext, type ConflictResolution, type HostTag, MANIFEST_PATH_PARITY, MIGRATIONS, type ManifestEntry, type MergeResult, type MigrationContext, type MigrationResult, type MigrationScript, type ScaffoldMode, type ScaffoldOptions, type ScaffoldResult, type StackInfo, type WriteMode, type WriteOutcome, ancestorsPath, applyInlineConflict, applyWithConflict, assertSafeRoot, buildHostArtifacts, buildManifest, buildRegion, detectStack, loadTemplate, managedBlock, mergeThreeWay, readAncestors, readScaffoldVersion, regenerate, render, runMigrations, scaffold, scaffoldVersionPath, skipIfExists, templatesDir, writeAncestors, writeScaffoldVersion };
package/dist/index.js CHANGED
@@ -1,5 +1,31 @@
1
+ // src/ancestors.ts
2
+ import { existsSync, mkdirSync, readFileSync, writeFileSync } from "fs";
3
+ import { dirname, join } from "path";
4
+ var ANCESTORS_REL = ".noir/ancestors.json";
5
+ function ancestorsPath(root) {
6
+ return join(root, ANCESTORS_REL);
7
+ }
8
+ function readAncestors(root) {
9
+ if (!existsSync(ancestorsPath(root))) return {};
10
+ try {
11
+ const raw = readFileSync(ancestorsPath(root), "utf8");
12
+ const obj = JSON.parse(raw);
13
+ if (obj && typeof obj === "object" && !Array.isArray(obj)) {
14
+ return obj;
15
+ }
16
+ return {};
17
+ } catch {
18
+ return {};
19
+ }
20
+ }
21
+ function writeAncestors(root, map) {
22
+ mkdirSync(dirname(ancestorsPath(root)), { recursive: true });
23
+ writeFileSync(ancestorsPath(root), `${JSON.stringify(map, null, 2)}
24
+ `, "utf8");
25
+ }
26
+
1
27
  // src/manifest.ts
2
- import { join, relative } from "path";
28
+ import { join as join2, relative } from "path";
3
29
  import {
4
30
  AGENTS_MD_FILENAME,
5
31
  emitAgentsMd,
@@ -95,7 +121,7 @@ function buildHostArtifacts(adapter, ctx) {
95
121
  const emitsAgentsMd = host === "agents-md" || host === "cursor" || host === "opencode";
96
122
  if (emitsAgentsMd) {
97
123
  entries.push({
98
- path: hostRel(adapter.agentsMdPath?.(ectx) ?? join(ctx.root, AGENTS_MD_FILENAME), ctx.root),
124
+ path: hostRel(adapter.agentsMdPath?.(ectx) ?? join2(ctx.root, AGENTS_MD_FILENAME), ctx.root),
99
125
  mode: "regenerate",
100
126
  host,
101
127
  content: emitAgentsMd(ectx),
@@ -144,7 +170,7 @@ function buildHostArtifacts(adapter, ctx) {
144
170
  case "opencode":
145
171
  break;
146
172
  }
147
- const mcpAbs = adapter.mcpConfigPath?.(ectx) ?? join(ctx.root, ".mcp.json");
173
+ const mcpAbs = adapter.mcpConfigPath?.(ectx) ?? join2(ctx.root, ".mcp.json");
148
174
  const mcpRel = hostRel(mcpAbs, ctx.root);
149
175
  if (host === "claude") {
150
176
  const mcpTemplate = ctx.transport === "streamable-http" ? "mcp.http.json.tmpl" : "mcp.stdio.json.tmpl";
@@ -179,9 +205,89 @@ function hostRel(abs, root) {
179
205
  return rel.replace(/\\/g, "/");
180
206
  }
181
207
 
208
+ // src/merge.ts
209
+ function trivial(base, ours, theirs) {
210
+ if (ours === base) return { merged: theirs, conflict: false };
211
+ if (theirs === base) return { merged: ours, conflict: false };
212
+ if (ours === theirs) return { merged: ours, conflict: false };
213
+ return null;
214
+ }
215
+ function numAt(arr, i) {
216
+ return arr[i] ?? 0;
217
+ }
218
+ function lcsMatch(a, b) {
219
+ const n = a.length;
220
+ const m = b.length;
221
+ const dp = Array.from({ length: n + 1 }, () => new Array(m + 1).fill(0));
222
+ for (let i2 = n - 1; i2 >= 0; i2--) {
223
+ const dpi = dp[i2] ?? [];
224
+ const dpi1 = dp[i2 + 1] ?? [];
225
+ const ai = a[i2] ?? "";
226
+ for (let j2 = m - 1; j2 >= 0; j2--) {
227
+ const bj = b[j2] ?? "";
228
+ dpi[j2] = ai === bj ? numAt(dpi1, j2 + 1) + 1 : Math.max(numAt(dpi1, j2), numAt(dpi, j2 + 1));
229
+ }
230
+ }
231
+ const out = [];
232
+ let i = 0;
233
+ let j = 0;
234
+ while (i < n && j < m) {
235
+ if ((a[i] ?? "") === (b[j] ?? "")) {
236
+ out.push([i, j]);
237
+ i++;
238
+ j++;
239
+ } else if (numAt(dp[i + 1] ?? [], j) >= numAt(dp[i] ?? [], j + 1)) {
240
+ i++;
241
+ } else {
242
+ j++;
243
+ }
244
+ }
245
+ return out;
246
+ }
247
+ var eq = (a, b) => a.length === b.length && a.every((v, k) => v === b[k]);
248
+ function mergeThreeWay(base, ours, theirs) {
249
+ const t = trivial(base, ours, theirs);
250
+ if (t) return t;
251
+ const B = base.split("\n");
252
+ const O = ours.split("\n");
253
+ const T = theirs.split("\n");
254
+ const oMap = /* @__PURE__ */ new Map();
255
+ for (const [bi, oi] of lcsMatch(B, O)) oMap.set(bi, oi);
256
+ const tMap = /* @__PURE__ */ new Map();
257
+ for (const [bi, ti] of lcsMatch(B, T)) tMap.set(bi, ti);
258
+ const isAnchor = (bi) => oMap.has(bi) && tMap.has(bi);
259
+ const anchors = [];
260
+ for (let b = 0; b < B.length; b++) if (isAnchor(b)) anchors.push(b);
261
+ const pts = [-1, ...anchors, B.length];
262
+ const out = [];
263
+ let conflict = false;
264
+ for (let k = 0; k < pts.length - 1; k++) {
265
+ const aBase = pts[k];
266
+ const bBase = pts[k + 1];
267
+ if (aBase === void 0 || bBase === void 0) break;
268
+ const baseSeg = B.slice(aBase + 1, bBase);
269
+ const oStart = aBase >= 0 ? (oMap.get(aBase) ?? -1) + 1 : 0;
270
+ const oEnd = bBase < B.length ? oMap.get(bBase) ?? 0 : O.length;
271
+ const tStart = aBase >= 0 ? (tMap.get(aBase) ?? -1) + 1 : 0;
272
+ const tEnd = bBase < B.length ? tMap.get(bBase) ?? 0 : T.length;
273
+ const oSeg = O.slice(oStart, oEnd);
274
+ const tSeg = T.slice(tStart, tEnd);
275
+ if (eq(oSeg, baseSeg) && eq(tSeg, baseSeg)) out.push(...baseSeg);
276
+ else if (eq(oSeg, baseSeg)) out.push(...tSeg);
277
+ else if (eq(tSeg, baseSeg)) out.push(...oSeg);
278
+ else if (eq(oSeg, tSeg)) out.push(...oSeg);
279
+ else {
280
+ conflict = true;
281
+ out.push("<<<<<<< ours", ...oSeg, "=======", ...tSeg, ">>>>>>> theirs");
282
+ }
283
+ if (bBase < B.length) out.push(B[bBase] ?? "");
284
+ }
285
+ return { merged: out.join("\n"), conflict };
286
+ }
287
+
182
288
  // src/migrations/index.ts
183
- import { existsSync, readFileSync, writeFileSync } from "fs";
184
- import { join as join2 } from "path";
289
+ import { existsSync as existsSync2, readFileSync as readFileSync2, writeFileSync as writeFileSync2 } from "fs";
290
+ import { join as join3 } from "path";
185
291
 
186
292
  // src/migrations/runner.ts
187
293
  function runMigrations(root, from, to, opts = {}) {
@@ -231,11 +337,11 @@ var synthetic = {
231
337
  run: (ctx) => {
232
338
  const result = { changed: [], conflicts: [], notes: [] };
233
339
  if (process.env.NOIR_TEST_FORCE_CONFLICT === "1" && !ctx.dryRun) {
234
- const file = join2(ctx.root, ".noir", "scaffold-version");
235
- if (existsSync(file)) {
236
- const prev = readFileSync(file, "utf8");
340
+ const file = join3(ctx.root, ".noir", "scaffold-version");
341
+ if (existsSync2(file)) {
342
+ const prev = readFileSync2(file, "utf8");
237
343
  const merged = applyInlineConflict(prev, "noir-scaffold=1.0.0\n", "ours", "theirs");
238
- writeFileSync(file, merged, "utf8");
344
+ writeFileSync2(file, merged, "utf8");
239
345
  result.conflicts.push(".noir/scaffold-version");
240
346
  }
241
347
  }
@@ -259,31 +365,31 @@ function applyWithConflict(ours, theirs, path) {
259
365
  }
260
366
 
261
367
  // src/scaffold.ts
262
- import { existsSync as existsSync4, mkdirSync as mkdirSync2, readFileSync as readFileSync6, rmSync as rmSync2 } from "fs";
263
- import { dirname as dirname4, join as join7 } from "path";
264
- import { createProjectId, paths as paths2 } from "@noir-ai/core";
368
+ import { existsSync as existsSync5, mkdirSync as mkdirSync3, readFileSync as readFileSync7, renameSync as renameSync2, rmSync as rmSync2 } from "fs";
369
+ import { basename as basename2, dirname as dirname5, join as join8 } from "path";
370
+ import { createProjectId, paths as paths2, readManagedBlock } from "@noir-ai/core";
265
371
 
266
372
  // src/scaffold-version.ts
267
- import { mkdirSync, readFileSync as readFileSync3 } from "fs";
268
- import { dirname as dirname2, join as join4 } from "path";
373
+ import { mkdirSync as mkdirSync2, readFileSync as readFileSync4 } from "fs";
374
+ import { dirname as dirname3, join as join5 } from "path";
269
375
  import { NOIR_DIR as NOIR_DIR2 } from "@noir-ai/core";
270
376
 
271
377
  // src/writers.ts
272
378
  import {
273
379
  closeSync,
274
- existsSync as existsSync2,
380
+ existsSync as existsSync3,
275
381
  openSync,
276
- readFileSync as readFileSync2,
382
+ readFileSync as readFileSync3,
277
383
  renameSync,
278
384
  rmSync,
279
- writeFileSync as writeFileSync2,
385
+ writeFileSync as writeFileSync3,
280
386
  writeSync
281
387
  } from "fs";
282
- import { basename, dirname, join as join3 } from "path";
388
+ import { basename, dirname as dirname2, join as join4 } from "path";
283
389
  import { stripManagedBlock, writeManagedRegion } from "@noir-ai/core";
284
390
  function regenerate(absPath, content) {
285
- const dir = dirname(absPath);
286
- const tmp = join3(
391
+ const dir = dirname2(absPath);
392
+ const tmp = join4(
287
393
  dir,
288
394
  `.${basename(absPath)}.tmp.${process.pid}.${Math.random().toString(36).slice(2)}`
289
395
  );
@@ -323,7 +429,7 @@ function managedBlocks(absPath, regions) {
323
429
  }
324
430
  let content = "";
325
431
  try {
326
- content = readFileSync2(absPath, "utf8");
432
+ content = readFileSync3(absPath, "utf8");
327
433
  } catch {
328
434
  }
329
435
  let stripped = content;
@@ -334,7 +440,7 @@ function managedBlocks(absPath, regions) {
334
440
  const next = stripped.trim().length > 0 ? `${stripped.trimEnd()}
335
441
 
336
442
  ${regionsJoined}` : regionsJoined;
337
- writeFileSync2(absPath, next, "utf8");
443
+ writeFileSync3(absPath, next, "utf8");
338
444
  return { path: absPath, mode: "managedBlock", written: true };
339
445
  }
340
446
  function buildRegion(block, body) {
@@ -344,10 +450,10 @@ ${block.end}
344
450
  `;
345
451
  }
346
452
  function skipIfExists(absPath, content) {
347
- if (existsSync2(absPath)) {
453
+ if (existsSync3(absPath)) {
348
454
  return { path: absPath, mode: "skipIfExists", written: false };
349
455
  }
350
- writeFileSync2(absPath, content, "utf8");
456
+ writeFileSync3(absPath, content, "utf8");
351
457
  return { path: absPath, mode: "skipIfExists", written: true };
352
458
  }
353
459
 
@@ -355,12 +461,12 @@ function skipIfExists(absPath, content) {
355
461
  var CURRENT_SCAFFOLD_VERSION = "1.0.0";
356
462
  var PREFIX = "noir-scaffold=";
357
463
  function scaffoldVersionPath(root) {
358
- return join4(root, NOIR_DIR2, "scaffold-version");
464
+ return join5(root, NOIR_DIR2, "scaffold-version");
359
465
  }
360
466
  function readScaffoldVersion(root) {
361
467
  let raw;
362
468
  try {
363
- raw = readFileSync3(scaffoldVersionPath(root), "utf8");
469
+ raw = readFileSync4(scaffoldVersionPath(root), "utf8");
364
470
  } catch {
365
471
  return null;
366
472
  }
@@ -375,14 +481,14 @@ function readScaffoldVersion(root) {
375
481
  }
376
482
  function writeScaffoldVersion(root, version) {
377
483
  const file = scaffoldVersionPath(root);
378
- mkdirSync(dirname2(file), { recursive: true });
484
+ mkdirSync2(dirname3(file), { recursive: true });
379
485
  regenerate(file, `${PREFIX}${version}
380
486
  `);
381
487
  }
382
488
 
383
489
  // src/stack-detect.ts
384
- import { existsSync as existsSync3, readFileSync as readFileSync4 } from "fs";
385
- import { join as join5 } from "path";
490
+ import { existsSync as existsSync4, readFileSync as readFileSync5 } from "fs";
491
+ import { join as join6 } from "path";
386
492
  var NODE_FRAMEWORKS = [
387
493
  ["next", "next"],
388
494
  ["vite", "vite"],
@@ -409,7 +515,7 @@ var PYTHON_FRAMEWORKS = [
409
515
  ];
410
516
  function readJson(file) {
411
517
  try {
412
- return JSON.parse(readFileSync4(file, "utf8"));
518
+ return JSON.parse(readFileSync5(file, "utf8"));
413
519
  } catch {
414
520
  return void 0;
415
521
  }
@@ -419,11 +525,11 @@ function detectStack(root) {
419
525
  const frameworks = /* @__PURE__ */ new Set();
420
526
  let monorepo = false;
421
527
  let packageManager = null;
422
- const pjPath = join5(root, "package.json");
423
- if (existsSync3(pjPath)) {
528
+ const pjPath = join6(root, "package.json");
529
+ if (existsSync4(pjPath)) {
424
530
  const pj = readJson(pjPath);
425
531
  if (pj) {
426
- const hasTs = Boolean(pj.devDependencies?.typescript) || existsSync3(join5(root, "tsconfig.json"));
532
+ const hasTs = Boolean(pj.devDependencies?.typescript) || existsSync4(join6(root, "tsconfig.json"));
427
533
  languages.add(hasTs ? "typescript" : "javascript");
428
534
  const deps = /* @__PURE__ */ new Set([
429
535
  ...Object.keys(pj.dependencies ?? {}),
@@ -442,22 +548,22 @@ function detectStack(root) {
442
548
  }
443
549
  }
444
550
  }
445
- if (existsSync3(join5(root, "pnpm-workspace.yaml"))) {
551
+ if (existsSync4(join6(root, "pnpm-workspace.yaml"))) {
446
552
  monorepo = true;
447
553
  packageManager = packageManager ?? "pnpm";
448
554
  }
449
- if (existsSync3(join5(root, "turbo.json")) || existsSync3(join5(root, "nx.json"))) {
555
+ if (existsSync4(join6(root, "turbo.json")) || existsSync4(join6(root, "nx.json"))) {
450
556
  monorepo = true;
451
557
  }
452
558
  if (!packageManager) {
453
- if (existsSync3(join5(root, "pnpm-lock.yaml"))) packageManager = "pnpm";
454
- else if (existsSync3(join5(root, "yarn.lock"))) packageManager = "yarn";
455
- else if (existsSync3(join5(root, "package-lock.json"))) packageManager = "npm";
559
+ if (existsSync4(join6(root, "pnpm-lock.yaml"))) packageManager = "pnpm";
560
+ else if (existsSync4(join6(root, "yarn.lock"))) packageManager = "yarn";
561
+ else if (existsSync4(join6(root, "package-lock.json"))) packageManager = "npm";
456
562
  }
457
- if (existsSync3(join5(root, "pyproject.toml")) || existsSync3(join5(root, "requirements.txt")) || existsSync3(join5(root, "Pipfile")) || existsSync3(join5(root, "setup.py"))) {
563
+ if (existsSync4(join6(root, "pyproject.toml")) || existsSync4(join6(root, "requirements.txt")) || existsSync4(join6(root, "Pipfile")) || existsSync4(join6(root, "setup.py"))) {
458
564
  languages.add("python");
459
- if (existsSync3(join5(root, "pyproject.toml"))) {
460
- const raw = safeRead(join5(root, "pyproject.toml"));
565
+ if (existsSync4(join6(root, "pyproject.toml"))) {
566
+ const raw = safeRead(join6(root, "pyproject.toml"));
461
567
  if (raw) {
462
568
  for (const [dep, id] of PYTHON_FRAMEWORKS) {
463
569
  if (pyprojectHasDep(raw, dep)) frameworks.add(id);
@@ -465,13 +571,13 @@ function detectStack(root) {
465
571
  }
466
572
  }
467
573
  }
468
- if (existsSync3(join5(root, "go.mod"))) {
574
+ if (existsSync4(join6(root, "go.mod"))) {
469
575
  languages.add("go");
470
576
  packageManager = packageManager ?? "go-modules";
471
577
  }
472
- if (existsSync3(join5(root, "Cargo.toml"))) {
578
+ if (existsSync4(join6(root, "Cargo.toml"))) {
473
579
  languages.add("rust");
474
- const raw = safeRead(join5(root, "Cargo.toml"));
580
+ const raw = safeRead(join6(root, "Cargo.toml"));
475
581
  if (raw) {
476
582
  if (/^\s*actix-web\b/m.test(raw)) frameworks.add("actix-web");
477
583
  if (/^\s*actix\s*=/m.test(raw)) frameworks.add("actix");
@@ -489,7 +595,7 @@ function detectStack(root) {
489
595
  }
490
596
  function safeRead(file) {
491
597
  try {
492
- return readFileSync4(file, "utf8");
598
+ return readFileSync5(file, "utf8");
493
599
  } catch {
494
600
  return void 0;
495
601
  }
@@ -513,18 +619,18 @@ function render(template, vars) {
513
619
  }
514
620
 
515
621
  // src/template-loader.ts
516
- import { readFileSync as readFileSync5 } from "fs";
517
- import { dirname as dirname3, join as join6, resolve } from "path";
622
+ import { readFileSync as readFileSync6 } from "fs";
623
+ import { dirname as dirname4, join as join7, resolve } from "path";
518
624
  import { fileURLToPath } from "url";
519
625
  var DEFAULT_TEMPLATES_DIR = resolveTemplatesDir();
520
626
  function resolveTemplatesDir() {
521
627
  const override = process.env.NOIR_TEMPLATES_DIR;
522
628
  if (override && override.length > 0) return resolve(override);
523
- const here = dirname3(fileURLToPath(import.meta.url));
524
- return join6(here, "..", "templates");
629
+ const here = dirname4(fileURLToPath(import.meta.url));
630
+ return join7(here, "..", "templates");
525
631
  }
526
632
  function loadTemplate(name) {
527
- return readFileSync5(join6(DEFAULT_TEMPLATES_DIR, name), "utf8");
633
+ return readFileSync6(join7(DEFAULT_TEMPLATES_DIR, name), "utf8");
528
634
  }
529
635
  function templatesDir() {
530
636
  return DEFAULT_TEMPLATES_DIR;
@@ -539,14 +645,28 @@ var WRITER_BY_MODE = {
539
645
  managedBlock: "runtime",
540
646
  skipIfExists: "all"
541
647
  };
648
+ function assertSafeRoot(root) {
649
+ let cur = root;
650
+ for (let i = 0; i < 64; i++) {
651
+ if (basename2(cur) === ".noir") {
652
+ throw new Error(
653
+ `Refusing to scaffold inside a .noir/ directory (${root}). Run \`noir init\` from the project root, not from inside .noir/.`
654
+ );
655
+ }
656
+ const parent = dirname5(cur);
657
+ if (parent === cur) break;
658
+ cur = parent;
659
+ }
660
+ }
542
661
  async function scaffold(opts) {
662
+ assertSafeRoot(opts.root);
543
663
  const host = opts.host ?? "claude";
544
664
  const transport = opts.transport ?? "stdio";
545
665
  if (transport === "streamable-http" && !opts.url) {
546
666
  throw new Error("transport 'streamable-http' requires opts.url");
547
667
  }
548
668
  if (opts.mode === "create" && !opts.dryRun) {
549
- mkdirSync2(opts.root, { recursive: true });
669
+ mkdirSync3(opts.root, { recursive: true });
550
670
  }
551
671
  const idFile = readProjectIdFile(opts.root);
552
672
  const projectId = resolveProjectId(opts, idFile);
@@ -555,6 +675,28 @@ async function scaffold(opts) {
555
675
  }
556
676
  const fromVersion = readScaffoldVersion(opts.root);
557
677
  const stack = detectStack(opts.root);
678
+ const ancestors = opts.mergeManagedRegions ? readAncestors(opts.root) : {};
679
+ if (fromVersion !== null && opts.upgrade !== true && opts.force !== true && (opts.mode === "init" || opts.mode === "create")) {
680
+ if (opts.dryRun !== true) {
681
+ process.stderr.write(
682
+ `Noir is already initialized in ${opts.root} (scaffold ${fromVersion}). No-op. Use \`noir init --upgrade\` to migrate, or \`--force\` to re-scaffold.
683
+ `
684
+ );
685
+ }
686
+ return {
687
+ written: [],
688
+ skipped: [],
689
+ identical: [],
690
+ noop: true,
691
+ migrationsRan: [],
692
+ migrationConflicts: [],
693
+ stack,
694
+ projectId,
695
+ fromVersion,
696
+ toVersion: CURRENT_SCAFFOLD_VERSION,
697
+ host
698
+ };
699
+ }
558
700
  const migrationsRan = [];
559
701
  const migrationConflicts = [];
560
702
  if (opts.mode === "init" && opts.upgrade === true && fromVersion !== null) {
@@ -575,45 +717,54 @@ async function scaffold(opts) {
575
717
  };
576
718
  const written = [];
577
719
  const skipped = [];
720
+ const identical = [];
578
721
  const groups = groupApplicableByPath(manifest, host, emitRuntimeOnly);
579
722
  for (const [relPath, entries] of groups) {
580
- const abs = join7(opts.root, relPath);
723
+ const abs = join8(opts.root, relPath);
581
724
  const managed = entries.filter((e) => e.mode === "managedBlock");
582
725
  if (managed.length >= 2) {
583
726
  if (opts.dryRun) {
584
727
  written.push(relPath);
585
728
  continue;
586
729
  }
587
- mkdirSync2(dirname4(abs), { recursive: true });
730
+ mkdirSync3(dirname5(abs), { recursive: true });
588
731
  const regions = managed.map((e) => {
589
732
  const block = e.block;
590
733
  if (!block) {
591
734
  throw new Error(`manifest entry ${e.path}: managedBlock mode missing 'block'`);
592
735
  }
593
- return { block, regionText: buildRegion(block, renderEntry(e, vars)) };
736
+ const theirs = buildRegion(block, renderEntry(e, vars));
737
+ const regionText = opts.mergeManagedRegions ? mergeManagedRegion(abs, e.path, block, theirs, ancestors) : theirs;
738
+ if (opts.mergeManagedRegions) ancestors[`${e.path}::${block.begin}`] = theirs;
739
+ return { block, regionText };
594
740
  });
595
741
  managedBlocks(abs, regions);
596
742
  written.push(relPath);
597
743
  continue;
598
744
  }
599
- if (!opts.dryRun) mkdirSync2(dirname4(abs), { recursive: true });
745
+ if (!opts.dryRun) mkdirSync3(dirname5(abs), { recursive: true });
600
746
  for (const entry of entries) {
601
747
  if (opts.dryRun) {
602
- if (entry.mode === "skipIfExists" && existsSync4(abs)) skipped.push(entry.path);
748
+ if (entry.mode === "skipIfExists" && existsSync5(abs)) skipped.push(entry.path);
603
749
  else written.push(entry.path);
604
750
  continue;
605
751
  }
606
752
  const body = renderEntry(entry, vars);
607
753
  if (entry.mode === "regenerate") {
608
- regenerate(abs, body);
609
- written.push(entry.path);
754
+ const out = await writeRegenerateWithConflict(abs, entry.path, body, opts);
755
+ written.push(...out.written);
756
+ skipped.push(...out.skipped);
757
+ identical.push(...out.identical);
610
758
  } else if (entry.mode === "managedBlock") {
611
759
  const block = entry.block;
612
760
  if (!block) {
613
761
  throw new Error(`manifest entry ${entry.path}: managedBlock mode missing 'block'`);
614
762
  }
615
763
  if (isNoirMdPath(entry.path)) healLegacyNoirMd(abs);
616
- managedBlock2(abs, block, buildRegion(block, body));
764
+ const theirs = buildRegion(block, body);
765
+ const regionText = opts.mergeManagedRegions ? mergeManagedRegion(abs, entry.path, block, theirs, ancestors) : theirs;
766
+ managedBlock2(abs, block, regionText);
767
+ if (opts.mergeManagedRegions) ancestors[`${entry.path}::${block.begin}`] = theirs;
617
768
  written.push(entry.path);
618
769
  } else {
619
770
  const out = skipIfExists(abs, body);
@@ -625,9 +776,14 @@ async function scaffold(opts) {
625
776
  if ((opts.mode === "init" || opts.mode === "create") && !opts.dryRun) {
626
777
  writeScaffoldVersion(opts.root, CURRENT_SCAFFOLD_VERSION);
627
778
  }
779
+ if (opts.mergeManagedRegions && !opts.dryRun) {
780
+ writeAncestors(opts.root, ancestors);
781
+ }
628
782
  return {
629
783
  written,
630
784
  skipped,
785
+ identical,
786
+ noop: false,
631
787
  migrationsRan,
632
788
  migrationConflicts,
633
789
  stack,
@@ -637,10 +793,24 @@ async function scaffold(opts) {
637
793
  host
638
794
  };
639
795
  }
796
+ function mergeManagedRegion(abs, relPath, block, theirs, ancestors) {
797
+ const base = ancestors[`${relPath}::${block.begin}`];
798
+ if (base === void 0) return theirs;
799
+ const ours = readManagedBlock(abs, block);
800
+ if (ours === null) return theirs;
801
+ const res = mergeThreeWay(base, ours, theirs);
802
+ if (res.conflict) {
803
+ process.stderr.write(
804
+ `noir: managed-region conflict in ${relPath} \u2014 wrote inline markers; resolve manually.
805
+ `
806
+ );
807
+ }
808
+ return res.merged;
809
+ }
640
810
  function readProjectIdFile(root) {
641
811
  let raw;
642
812
  try {
643
- raw = readFileSync6(paths2.projectId(root), "utf8");
813
+ raw = readFileSync7(paths2.projectId(root), "utf8");
644
814
  } catch {
645
815
  return { state: "absent", id: null };
646
816
  }
@@ -655,6 +825,53 @@ function resolveProjectId(opts, idFile) {
655
825
  }
656
826
  return createProjectId();
657
827
  }
828
+ async function writeRegenerateWithConflict(abs, relPath, proposed, opts) {
829
+ let existing;
830
+ try {
831
+ existing = readFileSync7(abs, "utf8");
832
+ } catch {
833
+ existing = void 0;
834
+ }
835
+ if (existing === void 0) {
836
+ regenerate(abs, proposed);
837
+ return { written: [relPath], skipped: [], identical: [] };
838
+ }
839
+ if (existing === proposed) {
840
+ return { written: [], skipped: [], identical: [relPath] };
841
+ }
842
+ const resolution = opts.onConflict !== void 0 ? await opts.onConflict({ relPath, existing, proposed }) : opts.conflictPolicy === "preserve" ? "preserve" : "replace";
843
+ switch (resolution) {
844
+ case "replace":
845
+ regenerate(abs, proposed);
846
+ return { written: [relPath], skipped: [], identical: [] };
847
+ case "rename": {
848
+ const aside = uniqueAside(abs, relPath, ".local");
849
+ renameSync2(abs, aside.abs);
850
+ regenerate(abs, proposed);
851
+ return { written: [relPath], skipped: [aside.rel], identical: [] };
852
+ }
853
+ case "duplicate": {
854
+ const aside = uniqueAside(abs, relPath, ".noir");
855
+ regenerate(aside.abs, proposed);
856
+ return { written: [aside.rel], skipped: [relPath], identical: [] };
857
+ }
858
+ case "preserve":
859
+ return { written: [], skipped: [relPath], identical: [] };
860
+ case "cancel":
861
+ throw new Error(`scaffold cancelled by user at conflicting file ${relPath}`);
862
+ default:
863
+ return { written: [], skipped: [relPath], identical: [] };
864
+ }
865
+ }
866
+ function uniqueAside(abs, relPath, suffix) {
867
+ const make = (s) => ({
868
+ abs: `${abs}${s}`,
869
+ rel: `${relPath}${s}`
870
+ });
871
+ let candidate = make(suffix);
872
+ for (let n = 1; existsSync5(candidate.abs); n++) candidate = make(`${suffix}.${n}`);
873
+ return candidate;
874
+ }
658
875
  function groupApplicableByPath(manifest, host, emitRuntimeOnly) {
659
876
  const groups = /* @__PURE__ */ new Map();
660
877
  for (const entry of manifest) {
@@ -672,7 +889,7 @@ function isNoirMdPath(relPath) {
672
889
  function healLegacyNoirMd(absPath) {
673
890
  let content;
674
891
  try {
675
- content = readFileSync6(absPath, "utf8");
892
+ content = readFileSync7(absPath, "utf8");
676
893
  } catch {
677
894
  return;
678
895
  }
@@ -691,14 +908,18 @@ export {
691
908
  CURRENT_SCAFFOLD_VERSION,
692
909
  MANIFEST_PATH_PARITY,
693
910
  MIGRATIONS,
911
+ ancestorsPath,
694
912
  applyInlineConflict,
695
913
  applyWithConflict,
914
+ assertSafeRoot,
696
915
  buildHostArtifacts,
697
916
  buildManifest,
698
917
  buildRegion,
699
918
  detectStack,
700
919
  loadTemplate,
701
920
  managedBlock2 as managedBlock,
921
+ mergeThreeWay,
922
+ readAncestors,
702
923
  readScaffoldVersion,
703
924
  regenerate,
704
925
  render,
@@ -707,6 +928,7 @@ export {
707
928
  scaffoldVersionPath,
708
929
  skipIfExists,
709
930
  templatesDir,
931
+ writeAncestors,
710
932
  writeScaffoldVersion
711
933
  };
712
934
  //# sourceMappingURL=index.js.map
package/dist/index.js.map CHANGED
@@ -1 +1 @@
1
- {"version":3,"sources":["../src/manifest.ts","../src/migrations/index.ts","../src/migrations/runner.ts","../src/scaffold.ts","../src/scaffold-version.ts","../src/writers.ts","../src/stack-detect.ts","../src/template.ts","../src/template-loader.ts"],"sourcesContent":["import { join, relative } from 'node:path';\nimport {\n AGENTS_MD_FILENAME,\n type EmitContext,\n emitAgentsMd,\n type HostAdapter,\n type HostId,\n resolveAdapter,\n} from '@noir-ai/adapters';\nimport {\n CONTEXT_BLOCK,\n IGNORE_BLOCK,\n type ManagedBlock,\n managedBlock,\n NOIR_DIR,\n paths,\n RULES_BLOCK,\n} from '@noir-ai/core';\nimport type { WriteMode } from './writers.js';\n\n/**\n * Declarative scaffold manifest — the single source of truth for what\n * `init` / `create` / `sync` emit. Each entry is one artifact, tagged with its\n * write {@link WriteMode} so the orchestrator can dispatch without knowing\n * what's inside.\n *\n * FAITHFULNESS CONTRACT (S-T1 → S-T2 → S10): this table is a strict superset of\n * the artifacts `packages/cli/src/{init,sync}.ts` wrote pre-Slice-S. The cli\n * refactor (S-T2) replaced those ad-hoc writers with a call into `scaffold()`;\n * the byte-for-byte output MUST stay equivalent for first-run init. S10 makes\n * the manifest HOST-PARAMETRIC: {@link buildManifest} now returns host-agnostic\n * entries + a {@link buildHostArtifacts} call that materializes per-host files\n * (CLAUDE.md/GEMINI.md for claude/gemini; AGENTS.md + .cursor/.../opencode.json\n * for agents-md/cursor/opencode) via the resolved adapter. The claude default\n * `noir init` stays BYTE-IDENTICAL to v1.1 — fix-wave I1 REMOVED the additive\n * root `AGENTS.md` (it was double-importing `.noir/NOIR.md` + RULES.md via\n * CLAUDE.md's existing @-imports; claude's native surface is CLAUDE.md alone).\n *\n * Path-derivation: repo-relative POSIX strings that mirror\n * `@noir-ai/core/layout.ts` (`paths.*`). The test suite asserts\n * `join(root, entry.path) === paths.X(root)` for every entry layout knows\n * about, so a layout rename is caught here instead of silently drifting.\n */\n\n/** S10: `HostTag` is now the SAME `HostId` enum the adapter registry uses\n * (re-exported so existing imports keep working). Pre-S10 this was the\n * literal `'claude'`; widening to `HostId` lets one manifest serve every host\n * via the orchestrator's host filter + {@link buildHostArtifacts}. */\nexport type HostTag = HostId;\n\nexport interface ManifestEntry {\n /** Repo-relative POSIX path (forward slashes). Orchestrator joins with root. */\n path: string;\n mode: WriteMode;\n /** Host tag; entry is skipped when opts.host !== entry.host.\n * Undefined = host-agnostic (every host emits it). */\n host?: HostTag;\n /** Required for `managedBlock` mode: the named block to re-emit. */\n block?: ManagedBlock;\n /** Literal content (`regenerate`/`skipIfExists`) or literal region BODY\n * (`managedBlock` — the orchestrator wraps it with the block markers).\n * Mutually exclusive with {@link template}. */\n content?: string;\n /** Template name (resolved by `template-loader`) for content/body.\n * Mutually exclusive with {@link content}. */\n template?: string;\n /** One-line human description for `noir doctor` + logs. */\n description?: string;\n}\n\nexport type BuildManifestContext = {\n /** Absolute repo root. Added in S10 so {@link buildHostArtifacts} can resolve\n * absolute adapter paths (`adapter.mcpConfigPath({root})`, etc.) to the\n * manifest's repo-relative POSIX shape. */\n root: string;\n /** Canonical project id (already created/read by the orchestrator). */\n projectId: string;\n /** Target host. Drives {@link buildHostArtifacts} via `resolveAdapter(host)`. */\n host: HostTag;\n /** MCP transport the host should use to reach Noir. */\n transport: 'stdio' | 'streamable-http';\n /** Required when transport is `streamable-http`. */\n url?: string;\n};\n\n// --- named managed blocks ----------------------------------------------------\n\n/** Co-owned NOIR.md auto-brief region. Defined locally (not exported from\n * core) because core's keystone-K named instances cover only the three\n * regions core itself writes (context/rules/ignore); the brief is the\n * scaffold engine's own. Uses the SAME `managedBlock()` factory so marker\n * shape stays consistent with the rest of the family. */\nexport const BRIEF_BLOCK: ManagedBlock = managedBlock('brief', 'html');\n\n// --- repo-relative path constants (mirror @noir-ai/core/layout.ts) -----------\n// Inlined as string literals so the manifest has zero runtime dep on layout\n// for path strings; the test suite cross-checks against `paths.*`.\n\nconst P = {\n projectId: `${NOIR_DIR}/project.id`,\n config: `${NOIR_DIR}/config.yml`,\n noirMd: `${NOIR_DIR}/NOIR.md`,\n rulesMd: `${NOIR_DIR}/rules/RULES.md`,\n} as const;\n\n// Aliases for the parity test (kept here so a layout rename breaks the test\n// at the same site the literal lives, not in a far-off helper).\nexport const MANIFEST_PATH_PARITY: ReadonlyArray<\n [entryPath: string, layoutFn: (root: string) => string]\n> = [\n [P.projectId, paths.projectId],\n [P.config, paths.config],\n [P.noirMd, paths.noirMd],\n [P.rulesMd, paths.rulesMd],\n];\n\n/**\n * Build the manifest for a given ctx. Pure (no I/O). The orchestrator calls\n * this once per scaffold run; tests assert the shape is stable.\n *\n * S10 structure: the manifest is now `[...hostAgnosticEntries(ctx), ...hostSpecificEntries(ctx)]`\n * where the host-specific half comes from {@link buildHostArtifacts} (driven by\n * `resolveAdapter(ctx.host)`). The host-agnostic half is unchanged from v1.1\n * (canonical `.noir/` store + ignore files). Fix-wave I1: {@link buildHostArtifacts}\n * emits AGENTS.md ONLY for agents-md/cursor/opencode (claude/gemini use their\n * own CLAUDE.md/GEMINI.md — emitting AGENTS.md too would double-import .noir/).\n *\n * Mode-tagging rationale per artifact (see S-T1 report for the full table):\n * - `project.id` → skipIfExists. First init writes a fresh id; re-init MUST\n * NOT overwrite — that would orphan the indexed store DB named after it.\n * - `config.yml` → skipIfExists. User-owned; the seed is written once.\n * (The seed renders `host: {{host}}` so a `--host gemini` init persists the\n * chosen host for `noir sync` to read back.)\n * - `NOIR.md` → managedBlock (BRIEF_BLOCK). Auto-brief is co-owned.\n * - `RULES.md` → skipIfExists. User-owned working-contract seed.\n * - ignore files → managedBlock (IGNORE_BLOCK). Matches syncIgnores.\n * - host entries → SEE {@link buildHostArtifacts} (regenerate / managedBlock).\n */\nexport function buildManifest(ctx: BuildManifestContext): ManifestEntry[] {\n return [...hostAgnosticEntries(ctx), ...buildHostArtifacts(resolveAdapter(ctx.host), ctx)];\n}\n\n/** The host-agnostic canonical-store + ignore entries — identical bytes for\n * every host. Split out so {@link buildHostArtifacts} can be unit-tested in\n * isolation and so the doctor's host-artifacts check can reason about the\n * host-specific half alone. */\nfunction hostAgnosticEntries(ctx: BuildManifestContext): ManifestEntry[] {\n return [\n {\n path: P.projectId,\n mode: 'skipIfExists',\n content: `${ctx.projectId}\\n`,\n description: 'canonical project id (store DB is named after it)',\n },\n {\n path: P.config,\n mode: 'skipIfExists',\n template: 'config.yml.tmpl',\n description: 'user config seed (host + mode)',\n },\n {\n path: P.noirMd,\n mode: 'managedBlock',\n block: BRIEF_BLOCK,\n template: 'noir.md.tmpl',\n description: 'NOIR.md auto-brief (project id pointer)',\n },\n {\n path: P.rulesMd,\n mode: 'skipIfExists',\n template: 'rules-seed.md.tmpl',\n description: 'AI working-rules seed',\n },\n\n // --- ignore files (host-agnostic; co-owned via IGNORE_BLOCK) ------------\n {\n path: '.gitignore',\n mode: 'managedBlock',\n block: IGNORE_BLOCK,\n template: 'gitignore.tmpl',\n description: '.gitignore noir managed block',\n },\n {\n path: '.dockerignore',\n mode: 'managedBlock',\n block: IGNORE_BLOCK,\n template: 'dockerignore.tmpl',\n description: '.dockerignore noir managed block',\n },\n {\n path: '.npmignore',\n mode: 'managedBlock',\n block: IGNORE_BLOCK,\n template: 'npmignore.tmpl',\n description: '.npmignore noir managed block',\n },\n {\n path: '.prettierignore',\n mode: 'managedBlock',\n block: IGNORE_BLOCK,\n template: 'prettierignore.tmpl',\n description: '.prettierignore noir managed block',\n },\n ];\n}\n\n// ---------------------------------------------------------------------------\n// S10 — host-specific artifact generation. One entry point: `buildHostArtifacts`.\n// ---------------------------------------------------------------------------\n\n/** Context shape passed to {@link buildHostArtifacts}. A strict subset of\n * {@link BuildManifestContext} (no `projectId`/`host` — the adapter IS the\n * resolved host, and host artifacts never need the project id). Exported\n * separately so callers + tests can name the narrower contract. */\nexport interface BuildHostArtifactsContext {\n root: string;\n transport: 'stdio' | 'streamable-http';\n url?: string;\n}\n\n/**\n * Materialize the host-specific manifest entries from a resolved adapter.\n * SINGLE entry point — no scattered `if (host === '…')` conditionals in the\n * orchestrator. Returns entries in emission order:\n *\n * 1. **AGENTS.md** (universal baseline) — `regenerate` at\n * `adapter.agentsMdPath(ctx)` (default `<root>/AGENTS.md`), content from\n * the shared `emitAgentsMd(ctx)` helper. Emitted ONLY for hosts whose\n * `emitContext` IS the AGENTS.md content (agents-md, cursor, opencode) —\n * for them AGENTS.md is the SINGLE native context surface AND carries the\n * Noir working rules via its `@.noir/rules/RULES.md` import. claude and\n * gemini have their OWN native context file (CLAUDE.md / GEMINI.md) that\n * `@`-imports the canonical `.noir/` sources; emitting AGENTS.md too\n * would IMPORT THOSE FILES TWICE into the host's context (2× tokens +\n * drift risk), so for those two hosts AGENTS.md is SKIPPED. (Claude Code\n * still discovers AGENTS.md at the repo root when present — users who\n * want the universal file can drop one in by hand; Noir's auto-emission\n * stays single-source per host.)\n * 2. **Host-native context file** — emitted ONLY for hosts whose `emitContext`\n * is NOT the AGENTS.md content (i.e. the host has its OWN context file\n * with a distinct syntax). Concretely: claude → `CLAUDE.md` (CONTEXT +\n * RULES managed blocks, byte-identical to v1.1 via templates); gemini →\n * `GEMINI.md` (CONTEXT + RULES managed blocks with Gemini's bare `@`\n * import syntax). For `agents-md`/`cursor`/`opencode` the context IS the\n * AGENTS.md (already emitted in step 1) → SKIP to avoid a duplicate.\n * Rules live INSIDE the host's context file: claude's in CLAUDE.md,\n * gemini's in GEMINI.md, agents-md/cursor/opencode's in AGENTS.md — NO\n * host emits a separate rules file. (The prior cursor\n * `.cursor/rules/noir-contract.mdc` host-rules pointer was REMOVED: it\n * collided with the C3 cursor flat-skill prune of `noir-*.mdc` under\n * `.cursor/rules/`, and cursor's rules are already delivered via\n * AGENTS.md's `@.noir/rules/RULES.md` import.)\n * 3. **Host MCP config** — `regenerate` at `adapter.mcpConfigPath(ctx)`\n * (default `<root>/.mcp.json` for claude), content from\n * `adapter.emitMcpConfig(ctx, {transport,url})`. Claude KEEPS the template\n * path (byte-identical parity with v1.1 + the .mcp.json parity test that\n * compares against `claudeAdapter.emitMcpConfig`); other hosts use the\n * adapter directly.\n *\n * Skills are OUT OF SCOPE here — the cli composes `emitSkillsToDir` with\n * `adapter.skillsDir` + the host's `CompileTarget` (claude → `.claude/skills/`\n * as SKILL.md; cursor → `.cursor/rules/<skill>.mdc` FLAT per C3; gemini/\n * agents-md/opencode have no skill dir → skip).\n */\nexport function buildHostArtifacts(\n adapter: HostAdapter,\n ctx: BuildHostArtifactsContext,\n): ManifestEntry[] {\n const ectx: EmitContext = { root: ctx.root };\n const host = adapter.id;\n const entries: ManifestEntry[] = [];\n\n // 1. AGENTS.md — emitted for hosts whose emitContext IS the AGENTS.md content\n // (agents-md, cursor, opencode). SKIPPED for claude/gemini: their native\n // CLAUDE.md / GEMINI.md already @-import the canonical .noir/ sources, so\n // a root AGENTS.md would double-import (2× context tokens, drift risk).\n // This also restores the claude default `noir init` to byte-identity with\n // v1.1 (the prior additive AGENTS.md delta is removed).\n const emitsAgentsMd = host === 'agents-md' || host === 'cursor' || host === 'opencode';\n if (emitsAgentsMd) {\n entries.push({\n path: hostRel(adapter.agentsMdPath?.(ectx) ?? join(ctx.root, AGENTS_MD_FILENAME), ctx.root),\n mode: 'regenerate',\n host,\n content: emitAgentsMd(ectx),\n description: `AGENTS.md (${host}'s native context surface; @-imports .noir/)`,\n });\n }\n\n // 2. Host-native context file (when distinct from AGENTS.md) + folded rules.\n switch (host) {\n case 'claude':\n // CLAUDE.md keeps template-based bodies — byte-identical to v1.1 (the\n // scaffold.test.ts parity gates compare against claudeAdapter.emitContext\n // + emitRules; the templates render to the same body bytes).\n entries.push({\n path: 'CLAUDE.md',\n mode: 'managedBlock',\n host,\n block: CONTEXT_BLOCK,\n template: 'claude-context-block.md.tmpl',\n description: 'CLAUDE.md context @import block',\n });\n entries.push({\n path: 'CLAUDE.md',\n mode: 'managedBlock',\n host,\n block: RULES_BLOCK,\n template: 'claude-rules-block.md.tmpl',\n description: 'CLAUDE.md rules @import block',\n });\n break;\n case 'gemini':\n // GEMINI.md carries CONTEXT_BLOCK + RULES_BLOCK with Gemini's bare\n // `@file` import syntax (no `@import` keyword, no quotes — distinct from\n // Claude's form). Emitted as TWO managed regions so user content outside\n // the markers survives `noir sync` (same write path as CLAUDE.md — the\n // multi-region atomic `managedBlocks` writer).\n entries.push({\n path: 'GEMINI.md',\n mode: 'managedBlock',\n host,\n block: CONTEXT_BLOCK,\n content: '@.noir/NOIR.md',\n description: 'GEMINI.md context @-import block',\n });\n entries.push({\n path: 'GEMINI.md',\n mode: 'managedBlock',\n host,\n block: RULES_BLOCK,\n content: '@.noir/rules/RULES.md',\n description: 'GEMINI.md rules @-import block',\n });\n break;\n case 'agents-md':\n case 'cursor':\n case 'opencode':\n // emitContext IS the AGENTS.md content (already emitted in step 1) →\n // no separate context file. Rules are carried by AGENTS.md's\n // `@.noir/rules/RULES.md` import (agents-md/cursor/opencode share that\n // universal surface — NO host emits a separate rules file).\n break;\n }\n\n // 3. Host MCP config. Claude keeps the template path (byte-identical parity\n // gate); other hosts use adapter.emitMcpConfig directly.\n const mcpAbs = adapter.mcpConfigPath?.(ectx) ?? join(ctx.root, '.mcp.json');\n const mcpRel = hostRel(mcpAbs, ctx.root);\n if (host === 'claude') {\n const mcpTemplate =\n ctx.transport === 'streamable-http' ? 'mcp.http.json.tmpl' : 'mcp.stdio.json.tmpl';\n entries.push({\n path: mcpRel,\n mode: 'regenerate',\n host,\n template: mcpTemplate,\n description: 'host MCP server pointer',\n });\n } else {\n const mcpContent = `${adapter.emitMcpConfig(ectx, {\n transport: ctx.transport,\n ...(ctx.url !== undefined ? { url: ctx.url } : {}),\n })}\\n`;\n entries.push({\n path: mcpRel,\n mode: 'regenerate',\n host,\n content: mcpContent,\n description: `${host} MCP server pointer`,\n });\n }\n\n return entries;\n}\n\n/** Convert an absolute path under `root` to a repo-relative POSIX string (the\n * manifest's path shape). Throws if `abs` is NOT under `root` so a future\n * adapter that returns a stray path fails loudly instead of producing a\n * malformed manifest entry. */\nfunction hostRel(abs: string, root: string): string {\n const rel = relative(root, abs);\n if (rel.length === 0 || rel.startsWith('..') || rel.startsWith('/')) {\n throw new Error(`buildHostArtifacts: path '${abs}' is not under root '${root}'`);\n }\n // Normalize any platform separators to POSIX (manifest paths are POSIX).\n return rel.replace(/\\\\/g, '/');\n}\n","import { existsSync, readFileSync, writeFileSync } from 'node:fs';\nimport { join } from 'node:path';\nimport type { MigrationResult, MigrationScript } from './types.js';\n\nexport { runMigrations } from './runner.js';\nexport type { MigrationContext, MigrationResult, MigrationScript } from './types.js';\n\n/**\n * Migration registry — the linear history of scaffold-version upgrades.\n *\n * At v1.0.0 there are no real migrations (the package ships fresh, so every\n * install starts at `CURRENT_SCAFFOLD_VERSION`). The registry is the\n * deliverable: it proves the runner end-to-end and gives the next contributor\n * a copy-pasteable template. Add the first real entry here when a template or\n * manifest change merits a `1.0.0 → 1.1.0` step.\n *\n * Convention:\n * - `from`/`to` are bare `x.y.z` (no `v` prefix, no pre-release); the runner\n * compares them numerically.\n * - Every `run` MUST be idempotent and non-throwing (capture failures into\n * `result.conflicts`). See {@link types.ts}.\n * - Conflict resolution writes git-style markers inline — see\n * {@link applyWithConflict} for the canonical helper.\n */\n\n/** Synthetic 1.0.0 → 1.0.0 migration. Proves the runner wires up; also\n * demonstrates the conflict-marker path with a guarded, idempotent touch on\n * `.noir/scaffold-version` only when explicitly asked via\n * `NOIR_TEST_FORCE_CONFLICT`. Safe to remove once a real migration lands. */\nconst synthetic: MigrationScript = {\n from: '1.0.0',\n to: '1.0.0',\n description: 'no-op synthetic migration (runner smoke test)',\n run: (ctx) => {\n const result: MigrationResult = { changed: [], conflicts: [], notes: [] };\n // The only \"real\" thing it does: when the env var is set, write a conflict\n // marker into `.noir/scaffold-version` so the runner's conflict plumbing is\n // exercised by tests. In normal operation this branch never fires and the\n // script is a true no-op.\n if (process.env.NOIR_TEST_FORCE_CONFLICT === '1' && !ctx.dryRun) {\n const file = join(ctx.root, '.noir', 'scaffold-version');\n if (existsSync(file)) {\n const prev = readFileSync(file, 'utf8');\n const merged = applyInlineConflict(prev, 'noir-scaffold=1.0.0\\n', 'ours', 'theirs');\n writeFileSync(file, merged, 'utf8');\n result.conflicts.push('.noir/scaffold-version');\n }\n }\n result.notes.push('synthetic 1.0.0→1.0.0 migration ran');\n return result;\n },\n};\n\nexport const MIGRATIONS: readonly MigrationScript[] = [synthetic];\n\n// --- conflict-marker helpers (exported for migration authors) ---------------\n\n/** Write git-style inline conflict markers around `theirs`/`ours` so a human\n * or AI agent can resolve later. This is the CI-safe fallback the spec locks\n * in (S-OQ2) — no interactive prompts, ever. */\nexport function applyInlineConflict(\n ours: string,\n theirs: string,\n oursLabel = 'ours',\n theirsLabel = 'theirs',\n): string {\n return `<<<<<<< ${oursLabel}\\n${ours}=======\\n${theirs}>>>>>>> ${theirsLabel}\\n`;\n}\n\n/** Apply `(ours, theirs)` to a region: if they're equal, return `ours` (no\n * conflict); otherwise emit inline markers. Migration authors should prefer\n * this over {@link applyInlineConflict} when the \"no change needed\" case is\n * common — it keeps re-runs truly idempotent (no spurious markers on a clean\n * tree). */\nexport function applyWithConflict(\n ours: string,\n theirs: string,\n path: string,\n): {\n text: string;\n conflicted: boolean;\n} {\n if (ours === theirs) return { text: ours, conflicted: false };\n return {\n text: applyInlineConflict(ours, theirs, path, path),\n conflicted: true,\n };\n}\n","import { MIGRATIONS } from './index.js';\nimport type { MigrationContext, MigrationResult, MigrationScript } from './types.js';\n\n/**\n * Migration runner. Given a `from` version present on disk and a target `to`\n * version (usually {@link CURRENT_SCAFFOLD_VERSION}), walks the\n * {@link MIGRATIONS} registry forward in version order, executing each step.\n *\n * Selection rule: a script runs when its `from` is `>= fromArg` (semver-ish\n * string compare is enough at Noir's scale; we don't pull in a semver dep for\n * this) AND its `to` is `<= toArg`. Scripts strictly outside the window are\n * skipped. Within the window, scripts run sorted by `to` ascending so a\n * multi-step upgrade (1.0.0 → 1.1.0 → 1.2.0) composes in order.\n *\n * The runner NEVER throws on a per-script failure — it captures the error,\n * records a synthetic conflict entry (`<path>__error`), and continues so a\n * single broken migration doesn't block the rest of the chain. The orchestrator\n * decides whether non-empty `conflicts` is a hard failure (init --upgrade) or a\n * warning (doctor).\n *\n * Returns the aggregate of every step's `changed`/`conflicts`/`notes`.\n */\nexport function runMigrations(\n root: string,\n from: string | null,\n to: string,\n opts: { dryRun?: boolean } = {},\n): MigrationResult & { from: string | null; to: string; ran: string[] } {\n const window = pickWindow(from ?? '0.0.0', to, MIGRATIONS);\n const ctx: MigrationContext = { root, dryRun: opts.dryRun === true };\n const aggregate: MigrationResult = { changed: [], conflicts: [], notes: [] };\n const ran: string[] = [];\n\n for (const script of window) {\n ran.push(`${script.from}→${script.to}`);\n let res: MigrationResult;\n try {\n res = script.run(ctx);\n } catch (err) {\n // Non-fatal at the runner level: record and continue. The caller turns\n // non-empty conflicts into a CI failure with a real exit code.\n const msg = err instanceof Error ? err.message : String(err);\n aggregate.conflicts.push(`<runner>:${script.from}→${script.to} threw: ${msg}`);\n continue;\n }\n aggregate.changed.push(...res.changed);\n aggregate.conflicts.push(...res.conflicts);\n aggregate.notes.push(...res.notes);\n }\n\n return {\n ...aggregate,\n from,\n to,\n ran,\n };\n}\n\n/** Pick the ordered subset of `scripts` forming the chain `[from, to]`. */\nfunction pickWindow(\n from: string,\n to: string,\n scripts: readonly MigrationScript[],\n): MigrationScript[] {\n return scripts\n .filter((s) => compareVer(s.from) >= compareVer(from) && compareVer(s.to) <= compareVer(to))\n .sort((a, b) => compareVer(a.to) - compareVer(b.to));\n}\n\n/** Tiny numeric tuple compare: `'1.10.3'` → `[1,10,3]`, compared element-wise.\n * Pre-release suffixes (e.g. `-beta.1`) are ignored — Noir ships `x.y.z`\n * scaffold versions and the runner only needs a deterministic order. */\nfunction compareVer(v: string): number {\n const parts = v.split('-')[0]?.split('.') ?? [];\n let n = 0;\n for (let i = 0; i < 3; i++) {\n const p = Number(parts[i] ?? '0');\n n = n * 1000 + (Number.isFinite(p) ? p : 0);\n }\n return n;\n}\n","import { existsSync, mkdirSync, readFileSync, rmSync } from 'node:fs';\nimport { dirname, join } from 'node:path';\nimport { createProjectId, paths } from '@noir-ai/core';\nimport {\n type BuildManifestContext,\n buildManifest,\n type HostTag,\n type ManifestEntry,\n} from './manifest.js';\nimport { runMigrations } from './migrations/index.js';\nimport {\n CURRENT_SCAFFOLD_VERSION,\n readScaffoldVersion,\n writeScaffoldVersion,\n} from './scaffold-version.js';\nimport { detectStack, type StackInfo } from './stack-detect.js';\nimport { render } from './template.js';\nimport { loadTemplate } from './template-loader.js';\nimport {\n buildRegion,\n managedBlock,\n managedBlocks,\n regenerate,\n skipIfExists,\n type WriteMode,\n} from './writers.js';\n\n/**\n * Scaffold orchestrator. One function the cli (S-T2) calls for `noir init`,\n * `noir create`, and `noir sync`; mode selects the manifest subset + how\n * project identity is resolved.\n *\n * High-level flow:\n * 1. Resolve project id (provided > existing > generated; sync requires existing).\n * 2. Detect stack (READ-ONLY; always runs; never throws).\n * 3. If {@link ScaffoldOptions.upgrade}, run migrations from the on-disk\n * scaffold-version → {@link CURRENT_SCAFFOLD_VERSION}.\n * 4. Build the manifest, filter by host + mode.\n * 5. For each entry: mkdir -p, render template/content, dispatch to the\n * matching writer.\n * 6. On `init`/`create`, stamp `.noir/scaffold-version` LAST so a crash leaves\n * an old/absent stamp rather than a misleading fresh one.\n *\n * The orchestrator owns dir creation (writers refuse to so that a missing dir\n * is one attributable failure, not N silent ones).\n */\n\nexport type ScaffoldMode = 'init' | 'create' | 'sync';\n\nexport interface ScaffoldOptions {\n /** Absolute repo root. For `create`, the new dir (created if absent). */\n root: string;\n mode: ScaffoldMode;\n /** Target host. Defaults to `'claude'` (the only shipped host). */\n host?: HostTag;\n /** MCP transport. Defaults to `'stdio'`. */\n transport?: 'stdio' | 'streamable-http';\n /** Required when transport is `streamable-http`. */\n url?: string;\n /** Explicit project id; bypasses generate/read. Mainly for tests + `create`\n * flows that want deterministic ids. */\n projectId?: string;\n /** `noir init --upgrade`: run migrations before re-emitting, and emit only\n * regenerate + managedBlock (skipIfExists left alone). Only meaningful\n * with mode `'init'`. */\n upgrade?: boolean;\n /** Preview: compute the same written/skipped/migrated lists without touching\n * disk. `noir doctor`/CI use this to report drift. */\n dryRun?: boolean;\n}\n\nexport interface ScaffoldResult {\n /** Repo-relative paths actually written. */\n written: string[];\n /** Repo-relative paths skipIfExists'd (already present). */\n skipped: string[];\n /** Migration steps executed (`<from>→<to>`), when upgrade ran. */\n migrationsRan: string[];\n /** Migration conflicts (repo-relative or `<runner>:…`), when upgrade ran. */\n migrationConflicts: string[];\n stack: StackInfo;\n projectId: string;\n fromVersion: string | null;\n toVersion: string;\n /** The host actually emitted (post-default). */\n host: HostTag;\n}\n\nconst WRITER_BY_MODE: Record<WriteMode, 'all' | 'runtime'> = {\n // 'runtime' subset = regenerate + managedBlock (the always-safe-to-rewrite\n // entries). sync + init --upgrade emit only this subset; skipIfExists is\n // reserved for first-run init/create so user edits survive.\n regenerate: 'runtime',\n managedBlock: 'runtime',\n skipIfExists: 'all',\n};\n\nexport async function scaffold(opts: ScaffoldOptions): Promise<ScaffoldResult> {\n const host: HostTag = opts.host ?? 'claude';\n const transport: BuildManifestContext['transport'] = opts.transport ?? 'stdio';\n if (transport === 'streamable-http' && !opts.url) {\n throw new Error(\"transport 'streamable-http' requires opts.url\");\n }\n\n // 1. Root for `create` may not exist yet — mirror `init.ts`'s mkdir of .noir/.\n if (opts.mode === 'create' && !opts.dryRun) {\n mkdirSync(opts.root, { recursive: true });\n }\n\n // 2. Resolve project id. Read the on-disk stamp ONCE and reuse the result\n // for the corrupt-file heal below (C1). sync requires a VALID existing id.\n const idFile = readProjectIdFile(opts.root);\n const projectId = resolveProjectId(opts, idFile);\n\n // C1: a `project.id` that EXISTS but is empty/unparseable is CORRUPT, not\n // absent. The manifest writes project.id via `skipIfExists`, which would\n // preserve the empty file while NOIR.md's BRIEF_BLOCK renders the freshly\n // resolved/generated id → silent identity split (NOIR.md states an id the\n // store DB can't open). project.id is Noir-owned canonical; heal a corrupt\n // stamp by removing it so `skipIfExists` writes the resolved id fresh.\n // Absent/valid files and dryRun are left to the manifest writer.\n if (!opts.dryRun && idFile.state === 'corrupt') {\n rmSync(paths.projectId(opts.root), { force: true });\n }\n\n const fromVersion = readScaffoldVersion(opts.root);\n\n // 3. Stack detect (read-only, never throws). Always populated so callers\n // (TUI, doctor) get a single source of truth regardless of mode.\n const stack = detectStack(opts.root);\n\n // 4. Migrations (only when explicitly upgrading). M4: a fresh project\n // (`fromVersion === null`) has NO prior stamp → nothing to migrate. Skip\n // entirely so `noir init --upgrade` on a never-initialized tree doesn't\n // report a synthetic no-op `1.0.0→1.0.0` step.\n const migrationsRan: string[] = [];\n const migrationConflicts: string[] = [];\n if (opts.mode === 'init' && opts.upgrade === true && fromVersion !== null) {\n const m = runMigrations(opts.root, fromVersion, CURRENT_SCAFFOLD_VERSION, {\n dryRun: opts.dryRun === true,\n });\n migrationsRan.push(...m.ran);\n migrationConflicts.push(...m.conflicts);\n }\n\n // 5. Build manifest + filter by host + mode.\n const manifest = buildManifest({ root: opts.root, projectId, host, transport, url: opts.url });\n const emitRuntimeOnly = opts.mode === 'sync' || (opts.mode === 'init' && opts.upgrade === true);\n const vars: BuildManifestContext = {\n root: opts.root,\n projectId,\n host,\n transport,\n url: opts.url,\n };\n\n const written: string[] = [];\n const skipped: string[] = [];\n\n // GROUP applicable entries by target path (manifest order preserved within\n // each group) so files carrying MULTIPLE managed blocks (CLAUDE.md today =\n // CONTEXT + RULES) get ONE atomic multi-region write (I1). Single-entry\n // paths keep the existing per-entry writer — byte-stable for the NOIR.md\n // brief, the ignore files, and the regenerated `.mcp.json`. dryRun uses the\n // SAME grouping so its reported paths match what a real run would write\n // (CLAUDE.md reported once, not twice).\n const groups = groupApplicableByPath(manifest, host, emitRuntimeOnly);\n for (const [relPath, entries] of groups) {\n const abs = join(opts.root, relPath);\n const managed = entries.filter((e) => e.mode === 'managedBlock');\n\n if (managed.length >= 2) {\n // Multi-managed-block file: ONE atomic write of all regions.\n if (opts.dryRun) {\n written.push(relPath);\n continue;\n }\n mkdirSync(dirname(abs), { recursive: true });\n const regions = managed.map((e) => {\n const block = e.block;\n if (!block) {\n throw new Error(`manifest entry ${e.path}: managedBlock mode missing 'block'`);\n }\n return { block, regionText: buildRegion(block, renderEntry(e, vars)) };\n });\n managedBlocks(abs, regions);\n written.push(relPath);\n continue;\n }\n\n // Per-entry path: single-region managed / regenerate / skipIfExists.\n if (!opts.dryRun) mkdirSync(dirname(abs), { recursive: true });\n for (const entry of entries) {\n if (opts.dryRun) {\n if (entry.mode === 'skipIfExists' && existsSync(abs)) skipped.push(entry.path);\n else written.push(entry.path);\n continue;\n }\n const body = renderEntry(entry, vars);\n if (entry.mode === 'regenerate') {\n regenerate(abs, body);\n written.push(entry.path);\n } else if (entry.mode === 'managedBlock') {\n const block = entry.block;\n if (!block) {\n throw new Error(`manifest entry ${entry.path}: managedBlock mode missing 'block'`);\n }\n // I2: a legacy (pre-Slice-S) .noir/NOIR.md is a whole-file auto-brief\n // with NO managed markers. The normal path would treat the old brief\n // as user content and append a SECOND managed brief → two \"Project\n // id:\" lines. Self-heal: when the existing file has NO noir managed\n // marker at all, wipe it first so the managed write emits a clean\n // single brief. (Pre-Slice-S NOIR.md was 100% auto-generated, so\n // there is no user content to preserve in that legacy shape.)\n if (isNoirMdPath(entry.path)) healLegacyNoirMd(abs);\n managedBlock(abs, block, buildRegion(block, body));\n written.push(entry.path);\n } else {\n const out = skipIfExists(abs, body);\n if (out.written) written.push(entry.path);\n else skipped.push(entry.path);\n }\n }\n }\n\n // 6. Stamp scaffold-version on init/create (NOT sync). Written last so a\n // crash leaves the previous stamp. Upgrade rewrites it to current.\n if ((opts.mode === 'init' || opts.mode === 'create') && !opts.dryRun) {\n writeScaffoldVersion(opts.root, CURRENT_SCAFFOLD_VERSION);\n }\n\n return {\n written,\n skipped,\n migrationsRan,\n migrationConflicts,\n stack,\n projectId,\n fromVersion,\n toVersion: CURRENT_SCAFFOLD_VERSION,\n host,\n };\n}\n\n// --- helpers -----------------------------------------------------------------\n\n/** Read the `.noir/project.id` stamp ONCE and classify it for BOTH id\n * resolution and the C1 corrupt-file heal. `absent` (ENOENT) and `valid`\n * (non-empty) are the normal cases; `corrupt` (file exists but trims to empty)\n * is healed by the orchestrator before the manifest loop so `skipIfExists`\n * writes the resolved id fresh instead of preserving the empty file. */\nfunction readProjectIdFile(\n root: string,\n): { state: 'absent'; id: null } | { state: 'valid'; id: string } | { state: 'corrupt'; id: null } {\n let raw: string;\n try {\n raw = readFileSync(paths.projectId(root), 'utf8');\n } catch {\n return { state: 'absent', id: null };\n }\n const trimmed = raw.trim();\n return trimmed.length > 0 ? { state: 'valid', id: trimmed } : { state: 'corrupt', id: null };\n}\n\nfunction resolveProjectId(\n opts: ScaffoldOptions,\n idFile: ReturnType<typeof readProjectIdFile>,\n): string {\n if (opts.projectId !== undefined) return opts.projectId;\n if (idFile.state === 'valid') return idFile.id;\n // absent OR corrupt → resolve a fresh id. sync still requires a valid\n // pre-existing id (a corrupt stamp can't be trusted to name the store DB).\n if (opts.mode === 'sync') {\n throw new Error(`Noir is not initialized in ${opts.root}. Run \\`noir init\\` first.`);\n }\n return createProjectId();\n}\n\n/** Group applicable manifest entries by target path, preserving manifest order\n * within each group. JS `Map` preserves insertion order, so iterating groups\n * visits paths in the same sequence the manifest declares them (CONTEXT before\n * RULES inside the CLAUDE.md group). */\nfunction groupApplicableByPath(\n manifest: readonly ManifestEntry[],\n host: HostTag,\n emitRuntimeOnly: boolean,\n): Map<string, ManifestEntry[]> {\n const groups = new Map<string, ManifestEntry[]>();\n for (const entry of manifest) {\n if (entry.host !== undefined && entry.host !== host) continue; // host filter\n if (emitRuntimeOnly && WRITER_BY_MODE[entry.mode] !== 'runtime') continue;\n const list = groups.get(entry.path);\n if (list) list.push(entry);\n else groups.set(entry.path, [entry]);\n }\n return groups;\n}\n\n/** True for the canonical NOIR.md path the manifest emits (the BRIEF_BLOCK\n * target). Scoped so the I2 legacy-heal only fires for that one file — we must\n * not wipe arbitrary co-owned managed files. */\nfunction isNoirMdPath(relPath: string): boolean {\n return relPath === '.noir/NOIR.md';\n}\n\n/** I2 self-heal: wipe a legacy (pre-Slice-S) NOIR.md before the managed write.\n * Legacy shape = file exists but contains NO `<!-- noir:<name> begin -->`\n * managed marker (the whole file was the auto-brief). Files that already have\n * markers, or are absent, are left untouched (normal managed-block path or\n * fresh write respectively). */\nfunction healLegacyNoirMd(absPath: string): void {\n let content: string;\n try {\n content = readFileSync(absPath, 'utf8');\n } catch {\n return; // absent — fresh write, nothing to heal\n }\n if (/<!-- noir:[a-z]+ begin -->/.test(content)) return; // already managed-shape\n rmSync(absPath, { force: true });\n}\n\nfunction renderEntry(entry: ManifestEntry, vars: BuildManifestContext): string {\n if (entry.template !== undefined) {\n return render(loadTemplate(entry.template), vars);\n }\n if (entry.content !== undefined) return entry.content;\n throw new Error(`manifest entry ${entry.path}: must define 'content' or 'template'`);\n}\n","import { mkdirSync, readFileSync } from 'node:fs';\nimport { dirname, join } from 'node:path';\nimport { NOIR_DIR } from '@noir-ai/core';\nimport { regenerate } from './writers.js';\n\n/**\n * Scaffold version stamp — Noir's equivalent of Copier's `last-applied`.\n * Stored at `.noir/scaffold-version` as a single `noir-scaffold=<semver>` line\n * so `noir init --upgrade` / `noir doctor` can diff against\n * {@link CURRENT_SCAFFOLD_VERSION} and decide which migrations to run.\n *\n * Format is line-oriented (not YAML) on purpose: it must be readable before\n * `config.yml` is parsed (doctor runs even with a broken config), and the\n * `key=value` shape is trivial to grep from a shell.\n */\n\n/** The scaffold version this build of @noir-ai/create ships. Bumped atomically\n * whenever a manifest entry, template, or migration changes shape. */\nexport const CURRENT_SCAFFOLD_VERSION = '1.0.0';\n\nconst PREFIX = 'noir-scaffold=';\n\n/** Path to the stamp file under `root`. Exposed for tests + doctor. */\nexport function scaffoldVersionPath(root: string): string {\n return join(root, NOIR_DIR, 'scaffold-version');\n}\n\n/** Read the applied scaffold version, or `null` if the stamp is absent/unparseable.\n * Never throws — doctor must keep reporting even on a malformed stamp. */\nexport function readScaffoldVersion(root: string): string | null {\n let raw: string;\n try {\n raw = readFileSync(scaffoldVersionPath(root), 'utf8');\n } catch {\n return null;\n }\n for (const line of raw.split('\\n')) {\n const trimmed = line.trim();\n if (trimmed.startsWith(PREFIX)) {\n const v = trimmed.slice(PREFIX.length).trim();\n if (v.length > 0) return v;\n }\n }\n return null;\n}\n\n/** Write the stamp, creating `.noir/` if needed. The orchestrator writes it\n * LAST so a crash mid-scaffold leaves an old/absent stamp rather than a\n * misleading fresh one.\n *\n * N2: routed through the package's atomic `regenerate()` writer (tmp+rename in\n * the same dir) for consistency with the rest of the engine's durable writes —\n * a half-written stamp would mislead `noir doctor`/`init --upgrade`, so the\n * stamp deserves the same crash-atomicity as `.mcp.json` and the NOIR.md brief. */\nexport function writeScaffoldVersion(root: string, version: string): void {\n const file = scaffoldVersionPath(root);\n mkdirSync(dirname(file), { recursive: true });\n regenerate(file, `${PREFIX}${version}\\n`);\n}\n","import {\n closeSync,\n existsSync,\n openSync,\n readFileSync,\n renameSync,\n rmSync,\n writeFileSync,\n writeSync,\n} from 'node:fs';\nimport { basename, dirname, join } from 'node:path';\nimport type { ManagedBlock } from '@noir-ai/core';\nimport { stripManagedBlock, writeManagedRegion } from '@noir-ai/core';\n\n/**\n * The three-mode writer — generalizes keystone-K's `writeManagedRegion` into\n * the declarative dispatch the scaffold manifest drives. Each mode maps 1:1 to\n * an artifact class in the spec §4.5 matrix:\n *\n * - {@link regenerate} — pure pointers (`.mcp.json`, `NOIR.md` brief, …).\n * Always overwritten, atomically.\n * - {@link managedBlock} — co-owned files (`CLAUDE.md` context/rules,\n * `.gitignore` noir block, …). DELEGATES to\n * keystone-K's `writeManagedRegion` so user content\n * outside the markers is preserved byte-for-byte and\n * re-runs are idempotent. Never duplicate the\n * managed-region logic.\n * - {@link skipIfExists} — user-owned seeds (`RULES.md`, `config.yml`,\n * `project.id`). Write once; never clobber.\n *\n * The orchestrator (`scaffold.ts`) is the only intended caller; the per-mode\n * functions are exported so the cli (S-T2) and tests can drive them directly\n * when a one-off write is needed outside the manifest.\n */\n\nexport type WriteMode = 'regenerate' | 'managedBlock' | 'skipIfExists';\n\nexport interface WriteOutcome {\n /** The absolute path that was written. */\n path: string;\n mode: WriteMode;\n /** true when bytes hit disk; false for skipIfExists no-ops. regenerate and\n * managedBlock always write (managedBlock may write identical bytes — that\n * still counts as a write for telemetry purposes; the file IS up to date). */\n written: boolean;\n}\n\n/** Atomic overwrite. Writes to `<file>.tmp.<pid>.<rnd>` in the same directory,\n * fsyncs, then renames over the target so a crash never leaves a half-written\n * file (the pointer files this is used for are read by the host agent first —\n * a truncated CLAUDE.md/.mcp.json would break the very startup Noir serves).\n *\n * Parent directories are NOT created here — the orchestrator does that once\n * for the whole manifest so a missing dir is a single, attributable failure\n * rather than N silent ones inside the writer. */\nexport function regenerate(absPath: string, content: string): WriteOutcome {\n const dir = dirname(absPath);\n const tmp = join(\n dir,\n `.${basename(absPath)}.tmp.${process.pid}.${Math.random().toString(36).slice(2)}`,\n );\n // Open with 'w' truncates; writeSync + closeSync before rename so the bytes\n // are durable pre-swap. O_SYNC would be stronger but is platform-flaky; the\n // rename is the real atomicity guarantee on POSIX (and practical-enough on\n // the win32 targets Noir supports).\n //\n // M1: the tmp MUST be cleaned up on EVERY exit path. The previous shape only\n // ran `rmSync(tmp)` when `renameSync` threw, so a `writeSync` failure (disk\n // full, EPERM, …) left the tmp behind. A single try/finally with `force:true`\n // rmSync (no-op ENOENT after a successful rename consumed the file) covers\n // both.\n let fd: number | undefined;\n try {\n fd = openSync(tmp, 'w');\n writeSync(fd, content, 0, 'utf8');\n closeSync(fd);\n fd = undefined; // closed cleanly — don't re-close in finally\n renameSync(tmp, absPath);\n } finally {\n if (fd !== undefined) {\n try {\n closeSync(fd);\n } catch {\n /* best-effort: the rename/rm below are the meaningful cleanups */\n }\n }\n try {\n rmSync(tmp, { force: true });\n } catch {\n /* best-effort */\n }\n }\n return { path: absPath, mode: 'regenerate', written: true };\n}\n\n/** Re-emit a managed region, delegating to keystone-K's `writeManagedRegion`.\n * `regionText` MUST already include the begin/end markers (matches the shape\n * `writeManagedRegion` expects and that `IGNORE_BLOCK`/`CONTEXT_BLOCK`\n * callers build in core/cli). Use {@link buildRegion} to assemble it from a\n * block + body. */\nexport function managedBlock(\n absPath: string,\n block: ManagedBlock,\n regionText: string,\n): WriteOutcome {\n writeManagedRegion(absPath, block, regionText);\n return { path: absPath, mode: 'managedBlock', written: true };\n}\n\n/** Atomically (re)emit MULTIPLE managed regions into the SAME file in one\n * pass. Used when a co-owned target carries more than one managed block —\n * today only `CLAUDE.md` (CONTEXT + RULES).\n *\n * WHY this exists (I1): calling {@link managedBlock} twice on the same file is\n * NOT byte-idempotent. The 2nd call strips ONLY its own block, treats the 1st\n * region (and the `\\n\\n` separator) as user content, `trimEnd`s it, and\n * re-appends a fresh `\\n\\n` separator. Re-runs therefore accumulate ~2 leading\n * `\\n` bytes per init (verified: 158→168 over 5 runs). Doing both regions in a\n * SINGLE read → strip-all → append-all pass removes the interleaving: after\n * stripping BOTH blocks the only thing left is real user content, so re-runs\n * produce identical bytes.\n *\n * Strategy:\n * 1. Read the file (missing → empty).\n * 2. Strip EVERY named block (via core's `stripManagedBlock`, in the given\n * order) — what remains is user content + any managed blocks outside this\n * group.\n * 3. Append all `regionText`s in the GIVEN ORDER, joined by a single `\\n`.\n * Each `regionText` already ends with `\\n` (buildRegion appends the end\n * marker's trailing newline), so `\\n` between regions yields exactly one\n * blank-line separator (`END\\n` + `\\n` + `BEGIN`) — byte-identical to what\n * the single-block path emits on a first run, so the CONTEXT/RULES parity\n * gates against `claudeAdapter.emitContext/emitRules` keep passing.\n *\n * Single-region files (NOIR.md brief, ignore files) do NOT route through here\n * — the orchestrator only calls this for groups of ≥2 managed blocks, so\n * single-region byte-stability (delegated to keystone-K `writeManagedRegion`)\n * is unchanged. */\nexport function managedBlocks(\n absPath: string,\n regions: ReadonlyArray<{ block: ManagedBlock; regionText: string }>,\n): WriteOutcome {\n if (regions.length === 0) {\n throw new Error('managedBlocks requires at least one region');\n }\n if (regions.length === 1) {\n const only = regions[0];\n if (!only) throw new Error('managedBlocks: undefined region');\n return managedBlock(absPath, only.block, only.regionText);\n }\n let content = '';\n try {\n content = readFileSync(absPath, 'utf8');\n } catch {\n /* missing → treat as empty */\n }\n let stripped = content;\n for (const r of regions) {\n stripped = stripManagedBlock(stripped, r.block);\n }\n const regionsJoined = regions.map((r) => r.regionText).join('\\n');\n // Whitespace-only remainder (typical on re-run after both blocks are\n // stripped) → emit just the regions, no leading separator.\n const next =\n stripped.trim().length > 0 ? `${stripped.trimEnd()}\\n\\n${regionsJoined}` : regionsJoined;\n writeFileSync(absPath, next, 'utf8');\n return { path: absPath, mode: 'managedBlock', written: true };\n}\n\n/** Assemble `<begin>\\n<body>\\n<end>\\n` for a managed block. Centralized here so\n * every caller (manifest rendering, tests, future migrations) produces the\n * exact byte shape `writeManagedRegion` strips/expects. The trailing newline\n * is part of the contract — `stripManagedBlock`'s regex eats a trailing `\\n`\n * so re-runs stay idempotent instead of accumulating blank lines.\n *\n * `body` is `trimEnd()`-ed before wrapping so template authors can keep the\n * conventional trailing newline in `.tmpl` files without producing a\n * double-newline before the end marker. This keeps the output byte-identical\n * to `claudeAdapter.emitContext`/`emitRules` and core's `syncIgnores`, which\n * S-T2 relies on for a diff-free refactor. */\nexport function buildRegion(block: ManagedBlock, body: string): string {\n return `${block.begin}\\n${body.trimEnd()}\\n${block.end}\\n`;\n}\n\n/** Write `content` to `absPath` only if no file exists there. Returns whether\n * bytes were written. Parent dirs are NOT created (orchestrator's job). */\nexport function skipIfExists(absPath: string, content: string): WriteOutcome {\n if (existsSync(absPath)) {\n return { path: absPath, mode: 'skipIfExists', written: false };\n }\n writeFileSync(absPath, content, 'utf8');\n return { path: absPath, mode: 'skipIfExists', written: true };\n}\n","import { existsSync, readFileSync } from 'node:fs';\nimport { join } from 'node:path';\n\n/**\n * READ-ONLY stack detection. Probes for well-known marker files under `root`\n * and reports ONLY what is present — never assumes. The result feeds path\n * adaptation (where to drop `.claude/` vs `.cursor/` etc., future), ignore-file\n * selection, and the onboarding TUI's confirm step.\n *\n * Design rules (spec §4.5):\n * - Never throws. A foreign/empty dir returns `{ languages: [], monorepo:\n * false, frameworks: [], packageManager: null }`.\n * - Never opens network, never parses code beyond a `package.json`/`pyproject`\n * dependency list. Marker files + top-level manifests only.\n * - Frameworks are reported only when both the marker file is present AND the\n * framework's dependency is listed (avoids false positives from a stale\n * `package.json`).\n */\n\nexport interface StackInfo {\n /** Lower-cased language ids found via marker files:\n * `typescript` | `javascript` | `python` | `go` | `rust`. */\n languages: string[];\n /** True when a workspace manifest is present (pnpm-workspace, npm/yarn\n * workspaces in package.json, turbo.json, nx.json). */\n monorepo: boolean;\n /** Lower-cased framework ids (e.g. `next`, `vite`, `express`, `fastapi`,\n * `actix`). Empty when none detected. */\n frameworks: string[];\n /** `pnpm` | `npm` | `yarn` when a lockfile is present, else null. */\n packageManager: string | null;\n}\n\n/** Frameworks looked up against `package.json#dependencies`+`devDependencies`.\n * Keyed by the dependency name as published on npm. */\nconst NODE_FRAMEWORKS: ReadonlyArray<[dep: string, id: string]> = [\n ['next', 'next'],\n ['vite', 'vite'],\n ['express', 'express'],\n ['fastify', 'fastify'],\n ['nuxt', 'nuxt'],\n ['remix', 'remix'],\n ['@sveltejs/kit', 'sveltekit'],\n ['@angular/core', 'angular'],\n ['react', 'react'],\n ['vue', 'vue'],\n];\n\n/** Frameworks looked up against `[project] dependencies` /\n * `[project.optional-dependencies]` / `[tool.poetry.dependencies]` in\n * `pyproject.toml`. Keyed by the PyPI package name. */\nconst PYTHON_FRAMEWORKS: ReadonlyArray<[dep: string, id: string]> = [\n ['fastapi', 'fastapi'],\n ['flask', 'flask'],\n ['django', 'django'],\n ['sanic', 'sanic'],\n ['starlette', 'starlette'],\n ['tornado', 'tornado'],\n ['aiohttp', 'aiohttp'],\n ['bottle', 'bottle'],\n ['pyramid', 'pyramid'],\n ['falcon', 'falcon'],\n];\n\n/** Read+parse JSON without throwing; returns undefined on any error. JSON5/ESM\n * `package.json` with comments would land here too — `package.json` is plain\n * JSON in practice so a failed `JSON.parse` genuinely means \"not a node\n * project\" or \"broken file\", both of which we report as \"absent\". */\nfunction readJson<T = unknown>(file: string): T | undefined {\n try {\n return JSON.parse(readFileSync(file, 'utf8')) as T;\n } catch {\n return undefined;\n }\n}\n\ninterface PackageJson {\n dependencies?: Record<string, string>;\n devDependencies?: Record<string, string>;\n workspaces?: string[] | { packages?: string[] };\n packageManager?: string;\n}\n\nexport function detectStack(root: string): StackInfo {\n const languages = new Set<string>();\n const frameworks = new Set<string>();\n let monorepo = false;\n let packageManager: string | null = null;\n\n // Node / JS / TS — gated on package.json presence.\n const pjPath = join(root, 'package.json');\n if (existsSync(pjPath)) {\n const pj = readJson<PackageJson>(pjPath);\n if (pj) {\n const hasTs =\n Boolean(pj.devDependencies?.typescript) || existsSync(join(root, 'tsconfig.json'));\n languages.add(hasTs ? 'typescript' : 'javascript');\n\n const deps = new Set([\n ...Object.keys(pj.dependencies ?? {}),\n ...Object.keys(pj.devDependencies ?? {}),\n ]);\n for (const [dep, id] of NODE_FRAMEWORKS) {\n if (deps.has(dep)) frameworks.add(id);\n }\n\n const ws = pj.workspaces;\n if (\n Array.isArray(ws) ||\n (typeof ws === 'object' && ws !== null && Array.isArray(ws.packages))\n ) {\n monorepo = true;\n }\n if (typeof pj.packageManager === 'string' && pj.packageManager.length > 0) {\n // `packageManager: pnpm@10.12.4` → `pnpm`. Yarn/npm similarly.\n const m = pj.packageManager.split('@')[0];\n if (m) packageManager = m;\n }\n }\n }\n\n // Workspace manifests (these override/confirm monorepo independent of pj).\n if (existsSync(join(root, 'pnpm-workspace.yaml'))) {\n monorepo = true;\n packageManager = packageManager ?? 'pnpm';\n }\n if (existsSync(join(root, 'turbo.json')) || existsSync(join(root, 'nx.json'))) {\n monorepo = true;\n }\n\n // Lockfiles pin packageManager when `package.json#packageManager` didn't.\n if (!packageManager) {\n if (existsSync(join(root, 'pnpm-lock.yaml'))) packageManager = 'pnpm';\n else if (existsSync(join(root, 'yarn.lock'))) packageManager = 'yarn';\n else if (existsSync(join(root, 'package-lock.json'))) packageManager = 'npm';\n }\n\n // Python — pyproject.toml / requirements.txt / Pipfile / setup.py.\n if (\n existsSync(join(root, 'pyproject.toml')) ||\n existsSync(join(root, 'requirements.txt')) ||\n existsSync(join(root, 'Pipfile')) ||\n existsSync(join(root, 'setup.py'))\n ) {\n languages.add('python');\n // Framework detection scans pyproject.toml for PEP 508 dependency names\n // under `[project] dependencies` / `[project.optional-dependencies]` /\n // `[tool.poetry.dependencies]`. A full section-aware TOML parse is overkill\n // at v1 (and would need a dep we don't ship); the boundary-aware text scan\n // in `pyprojectHasDep` is robust to the three formats users actually write\n // and never throws on malformed TOML (degrades to \"no match\").\n if (existsSync(join(root, 'pyproject.toml'))) {\n const raw = safeRead(join(root, 'pyproject.toml'));\n if (raw) {\n for (const [dep, id] of PYTHON_FRAMEWORKS) {\n if (pyprojectHasDep(raw, dep)) frameworks.add(id);\n }\n }\n }\n }\n\n // Go — go.mod.\n if (existsSync(join(root, 'go.mod'))) {\n languages.add('go');\n packageManager = packageManager ?? 'go-modules';\n }\n\n // Rust — Cargo.toml.\n if (existsSync(join(root, 'Cargo.toml'))) {\n languages.add('rust');\n const raw = safeRead(join(root, 'Cargo.toml'));\n if (raw) {\n // M3: Cargo.toml deps are always `name = \"ver\"` or `name = { … }`, so\n // require the `=` after the crate name. The old `/^\\s*actix\\b/m` matched\n // `actix-web` (word boundary between `x` and `-`) and falsely reported\n // `actix`. Treat the hyphenated runtime (`actix-web`) as its OWN id and\n // gate bare `actix` on the equals form. Same equals-form tightening is\n // applied to `axum`/`rocket` so `axum-extra`-style crates don't trip the\n // same bug. The `/m` flag makes `^` match any line (the previous\n // `/^actix\\s*=/` had no `/m` and only matched a string starting with\n // `actix`, i.e. effectively never — dead code).\n if (/^\\s*actix-web\\b/m.test(raw)) frameworks.add('actix-web');\n if (/^\\s*actix\\s*=/m.test(raw)) frameworks.add('actix');\n if (/^\\s*axum\\s*=/m.test(raw)) frameworks.add('axum');\n if (/^\\s*rocket\\s*=/m.test(raw)) frameworks.add('rocket');\n }\n packageManager = packageManager ?? 'cargo';\n }\n\n return {\n languages: [...languages].sort(),\n monorepo,\n frameworks: [...frameworks].sort(),\n packageManager,\n };\n}\n\nfunction safeRead(file: string): string | undefined {\n try {\n return readFileSync(file, 'utf8');\n } catch {\n return undefined;\n }\n}\n\n/** True iff `dep` appears as a PEP 508 dependency name in `pyproject.toml`\n * text — handles PEP 621 `dependencies`/`optional-dependencies` list entries\n * (`\"fastapi\"`, `\"fastapi[all]>=0.100\"`) AND Poetry `[tool.poetry.dependencies]`\n * bare keys (`fastapi = \"^0.100\"`).\n *\n * The `before` boundary (line-start, quote, or whitespace) plus the `after`\n * PEP 508 boundary (whitespace, quote, version specifier `<>=!~`, extras `[`,\n * marker `;`, or end-of-string) prevent substring mismatches — `flask` will\n * NOT match `flask-restful`, and `fastapi` will NOT match `x-fastapi`.\n *\n * Never throws: `dep` is escaped, the regex is constructed from a controlled\n * template, and `String.prototype.test` is total on string input. Malformed\n * TOML simply yields no matches. */\nfunction pyprojectHasDep(raw: string, dep: string): boolean {\n const esc = dep.replace(/[.*+?^${}()|[\\]\\\\]/g, '\\\\$&');\n const before = `(?:^|[\"'\\\\s])`;\n const after = `(?=\\\\s|[\"'<>=!~;]|\\\\[|$)`;\n return new RegExp(`${before}${esc}${after}`, 'm').test(raw);\n}\n","/**\n * Hand-rolled `{{var}}` interpolation. No mustache/handlebars dependency —\n * Noir's markdown/yaml/json templates are simple enough that a tokenizer +\n * map lookup is sufficient and keeps `@noir-ai/create` dependency-light.\n *\n * Semantics (documented):\n * - Tokens are `{{ name }}` / `{{name}}` — any amount of ASCII whitespace\n * inside the braces is permitted. The name is trimmed.\n * - Known vars (value is a string) are substituted verbatim. NO escaping is\n * applied — callers must pre-escape for the target format (JSON/YAML/MD).\n * This is intentional: the engine renders into multiple formats and a\n * single universal escaper would be wrong for at least one of them.\n * - Unknown vars (not in `vars`, or value `undefined`) → the original token\n * is LEFT IN PLACE. Rationale: drift between template and ctx becomes\n * visible (an unrendered `{{projectId}}` in CLAUDE.md is immediately\n * obvious), rather than silently swallowed to empty. The spec called this\n * out as the preferred failure mode.\n * - Non-string var values are stringified via `String(value)` so a stray\n * number/boolean still interpolates rather than printing `[object Object]`.\n * - A lone `{{` with no closing `}}` is left untouched (not a token).\n */\n\nconst TOKEN = /\\{\\{\\s*([^}{}]+?)\\s*\\}\\}/g;\n\nexport function render(template: string, vars: Record<string, unknown>): string {\n return template.replace(TOKEN, (whole, name: string) => {\n if (!Object.hasOwn(vars, name)) return whole; // unknown → leave token\n const v = vars[name];\n if (v === undefined || v === null) return whole; // treat as unknown → leave token\n return String(v);\n });\n}\n","import { readFileSync } from 'node:fs';\nimport { dirname, join, resolve } from 'node:path';\nimport { fileURLToPath } from 'node:url';\n\n/**\n * Template loader. Templates ship at `<pkg>/templates/` (see `files` in\n * package.json) and are read at runtime so non-code changes (copy tweaks, new\n * pointers) don't require a rebuild.\n *\n * Path resolution must work identically in three layouts:\n * - source (vitest, tsx): this file at `packages/create/src/template-loader.ts`\n * → `../templates/<name>`.\n * - built (tsup): this file at `packages/create/dist/template-loader.js` →\n * `../templates/<name>` (same relative offset — `dist/` and `src/` are both\n * direct children of the package root, so `../templates` lands correctly).\n * - packed (npm tarball): `templates/` is included per `files`, dist/ layout\n * preserved → same as built.\n *\n * `NOIR_TEMPLATES_DIR` overrides everything (used by tests + downstream packs\n * that want to substitute their own template set without forking the engine).\n */\n\nconst DEFAULT_TEMPLATES_DIR = resolveTemplatesDir();\n\nfunction resolveTemplatesDir(): string {\n const override = process.env.NOIR_TEMPLATES_DIR;\n if (override && override.length > 0) return resolve(override);\n // `import.meta.url` is this module's URL. Going up one level reaches the\n // package root in both source and built layouts (see file header).\n const here = dirname(fileURLToPath(import.meta.url));\n return join(here, '..', 'templates');\n}\n\n/** Read a template's raw text by name (e.g. `noir.md.tmpl`). Throws on missing\n * — an unknown template is a manifest bug and should fail loudly, not render\n * an empty string silently. */\nexport function loadTemplate(name: string): string {\n return readFileSync(join(DEFAULT_TEMPLATES_DIR, name), 'utf8');\n}\n\n/** Resolve a template name to its absolute path (for diagnostics + tests). */\nexport function templatesDir(): string {\n return DEFAULT_TEMPLATES_DIR;\n}\n"],"mappings":"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","names":["join","join","existsSync","mkdirSync","readFileSync","rmSync","dirname","join","paths","readFileSync","dirname","join","NOIR_DIR","existsSync","readFileSync","writeFileSync","join","managedBlock","join","NOIR_DIR","readFileSync","dirname","existsSync","readFileSync","join","readFileSync","dirname","join","mkdirSync","rmSync","paths","join","dirname","existsSync","managedBlock","readFileSync"]}
1
+ {"version":3,"sources":["../src/ancestors.ts","../src/manifest.ts","../src/merge.ts","../src/migrations/index.ts","../src/migrations/runner.ts","../src/scaffold.ts","../src/scaffold-version.ts","../src/writers.ts","../src/stack-detect.ts","../src/template.ts","../src/template-loader.ts"],"sourcesContent":["// SP-D follow-up — ancestor store for three-way managed-region merge.\n//\n// Persists the last-emitted managed-region text per (file, block) so a later\n// `noir init`/`sync --merge` can three-way merge (base/ours/theirs) instead of\n// strip-replacing. Stored at `.noir/ancestors.json` as a flat map. Only written\n// when `ScaffoldOptions.mergeManagedRegions` is set (opt-in), so a default\n// scaffold run never creates it.\n\nimport { existsSync, mkdirSync, readFileSync, writeFileSync } from 'node:fs';\nimport { dirname, join } from 'node:path';\n\nconst ANCESTORS_REL = '.noir/ancestors.json';\n\n/** Absolute path of the ancestor store under `root`. */\nexport function ancestorsPath(root: string): string {\n return join(root, ANCESTORS_REL);\n}\n\n/** Read the ancestor map (`{ \"${relPath}::${blockBegin}\": regionText }`).\n * Returns `{}` for a missing/corrupt file (never throws). */\nexport function readAncestors(root: string): Record<string, string> {\n if (!existsSync(ancestorsPath(root))) return {};\n try {\n const raw = readFileSync(ancestorsPath(root), 'utf8');\n const obj: unknown = JSON.parse(raw);\n if (obj && typeof obj === 'object' && !Array.isArray(obj)) {\n return obj as Record<string, string>;\n }\n return {};\n } catch {\n return {};\n }\n}\n\n/** Write the ancestor map. Ensures `.noir/` exists (a standalone caller may\n * invoke this before the scaffold has created the dir). Derived state, so the\n * plain write (no tmp) is fine. */\nexport function writeAncestors(root: string, map: Record<string, string>): void {\n mkdirSync(dirname(ancestorsPath(root)), { recursive: true });\n writeFileSync(ancestorsPath(root), `${JSON.stringify(map, null, 2)}\\n`, 'utf8');\n}\n","import { join, relative } from 'node:path';\nimport {\n AGENTS_MD_FILENAME,\n type EmitContext,\n emitAgentsMd,\n type HostAdapter,\n type HostId,\n resolveAdapter,\n} from '@noir-ai/adapters';\nimport {\n CONTEXT_BLOCK,\n IGNORE_BLOCK,\n type ManagedBlock,\n managedBlock,\n NOIR_DIR,\n paths,\n RULES_BLOCK,\n} from '@noir-ai/core';\nimport type { WriteMode } from './writers.js';\n\n/**\n * Declarative scaffold manifest — the single source of truth for what\n * `init` / `create` / `sync` emit. Each entry is one artifact, tagged with its\n * write {@link WriteMode} so the orchestrator can dispatch without knowing\n * what's inside.\n *\n * FAITHFULNESS CONTRACT (S-T1 → S-T2 → S10): this table is a strict superset of\n * the artifacts `packages/cli/src/{init,sync}.ts` wrote pre-Slice-S. The cli\n * refactor (S-T2) replaced those ad-hoc writers with a call into `scaffold()`;\n * the byte-for-byte output MUST stay equivalent for first-run init. S10 makes\n * the manifest HOST-PARAMETRIC: {@link buildManifest} now returns host-agnostic\n * entries + a {@link buildHostArtifacts} call that materializes per-host files\n * (CLAUDE.md/GEMINI.md for claude/gemini; AGENTS.md + .cursor/.../opencode.json\n * for agents-md/cursor/opencode) via the resolved adapter. The claude default\n * `noir init` stays BYTE-IDENTICAL to v1.1 — fix-wave I1 REMOVED the additive\n * root `AGENTS.md` (it was double-importing `.noir/NOIR.md` + RULES.md via\n * CLAUDE.md's existing @-imports; claude's native surface is CLAUDE.md alone).\n *\n * Path-derivation: repo-relative POSIX strings that mirror\n * `@noir-ai/core/layout.ts` (`paths.*`). The test suite asserts\n * `join(root, entry.path) === paths.X(root)` for every entry layout knows\n * about, so a layout rename is caught here instead of silently drifting.\n */\n\n/** S10: `HostTag` is now the SAME `HostId` enum the adapter registry uses\n * (re-exported so existing imports keep working). Pre-S10 this was the\n * literal `'claude'`; widening to `HostId` lets one manifest serve every host\n * via the orchestrator's host filter + {@link buildHostArtifacts}. */\nexport type HostTag = HostId;\n\nexport interface ManifestEntry {\n /** Repo-relative POSIX path (forward slashes). Orchestrator joins with root. */\n path: string;\n mode: WriteMode;\n /** Host tag; entry is skipped when opts.host !== entry.host.\n * Undefined = host-agnostic (every host emits it). */\n host?: HostTag;\n /** Required for `managedBlock` mode: the named block to re-emit. */\n block?: ManagedBlock;\n /** Literal content (`regenerate`/`skipIfExists`) or literal region BODY\n * (`managedBlock` — the orchestrator wraps it with the block markers).\n * Mutually exclusive with {@link template}. */\n content?: string;\n /** Template name (resolved by `template-loader`) for content/body.\n * Mutually exclusive with {@link content}. */\n template?: string;\n /** One-line human description for `noir doctor` + logs. */\n description?: string;\n}\n\nexport type BuildManifestContext = {\n /** Absolute repo root. Added in S10 so {@link buildHostArtifacts} can resolve\n * absolute adapter paths (`adapter.mcpConfigPath({root})`, etc.) to the\n * manifest's repo-relative POSIX shape. */\n root: string;\n /** Canonical project id (already created/read by the orchestrator). */\n projectId: string;\n /** Target host. Drives {@link buildHostArtifacts} via `resolveAdapter(host)`. */\n host: HostTag;\n /** MCP transport the host should use to reach Noir. */\n transport: 'stdio' | 'streamable-http';\n /** Required when transport is `streamable-http`. */\n url?: string;\n};\n\n// --- named managed blocks ----------------------------------------------------\n\n/** Co-owned NOIR.md auto-brief region. Defined locally (not exported from\n * core) because core's keystone-K named instances cover only the three\n * regions core itself writes (context/rules/ignore); the brief is the\n * scaffold engine's own. Uses the SAME `managedBlock()` factory so marker\n * shape stays consistent with the rest of the family. */\nexport const BRIEF_BLOCK: ManagedBlock = managedBlock('brief', 'html');\n\n// --- repo-relative path constants (mirror @noir-ai/core/layout.ts) -----------\n// Inlined as string literals so the manifest has zero runtime dep on layout\n// for path strings; the test suite cross-checks against `paths.*`.\n\nconst P = {\n projectId: `${NOIR_DIR}/project.id`,\n config: `${NOIR_DIR}/config.yml`,\n noirMd: `${NOIR_DIR}/NOIR.md`,\n rulesMd: `${NOIR_DIR}/rules/RULES.md`,\n} as const;\n\n// Aliases for the parity test (kept here so a layout rename breaks the test\n// at the same site the literal lives, not in a far-off helper).\nexport const MANIFEST_PATH_PARITY: ReadonlyArray<\n [entryPath: string, layoutFn: (root: string) => string]\n> = [\n [P.projectId, paths.projectId],\n [P.config, paths.config],\n [P.noirMd, paths.noirMd],\n [P.rulesMd, paths.rulesMd],\n];\n\n/**\n * Build the manifest for a given ctx. Pure (no I/O). The orchestrator calls\n * this once per scaffold run; tests assert the shape is stable.\n *\n * S10 structure: the manifest is now `[...hostAgnosticEntries(ctx), ...hostSpecificEntries(ctx)]`\n * where the host-specific half comes from {@link buildHostArtifacts} (driven by\n * `resolveAdapter(ctx.host)`). The host-agnostic half is unchanged from v1.1\n * (canonical `.noir/` store + ignore files). Fix-wave I1: {@link buildHostArtifacts}\n * emits AGENTS.md ONLY for agents-md/cursor/opencode (claude/gemini use their\n * own CLAUDE.md/GEMINI.md — emitting AGENTS.md too would double-import .noir/).\n *\n * Mode-tagging rationale per artifact (see S-T1 report for the full table):\n * - `project.id` → skipIfExists. First init writes a fresh id; re-init MUST\n * NOT overwrite — that would orphan the indexed store DB named after it.\n * - `config.yml` → skipIfExists. User-owned; the seed is written once.\n * (The seed renders `host: {{host}}` so a `--host gemini` init persists the\n * chosen host for `noir sync` to read back.)\n * - `NOIR.md` → managedBlock (BRIEF_BLOCK). Auto-brief is co-owned.\n * - `RULES.md` → skipIfExists. User-owned working-contract seed.\n * - ignore files → managedBlock (IGNORE_BLOCK). Matches syncIgnores.\n * - host entries → SEE {@link buildHostArtifacts} (regenerate / managedBlock).\n */\nexport function buildManifest(ctx: BuildManifestContext): ManifestEntry[] {\n return [...hostAgnosticEntries(ctx), ...buildHostArtifacts(resolveAdapter(ctx.host), ctx)];\n}\n\n/** The host-agnostic canonical-store + ignore entries — identical bytes for\n * every host. Split out so {@link buildHostArtifacts} can be unit-tested in\n * isolation and so the doctor's host-artifacts check can reason about the\n * host-specific half alone. */\nfunction hostAgnosticEntries(ctx: BuildManifestContext): ManifestEntry[] {\n return [\n {\n path: P.projectId,\n mode: 'skipIfExists',\n content: `${ctx.projectId}\\n`,\n description: 'canonical project id (store DB is named after it)',\n },\n {\n path: P.config,\n mode: 'skipIfExists',\n template: 'config.yml.tmpl',\n description: 'user config seed (host + mode)',\n },\n {\n path: P.noirMd,\n mode: 'managedBlock',\n block: BRIEF_BLOCK,\n template: 'noir.md.tmpl',\n description: 'NOIR.md auto-brief (project id pointer)',\n },\n {\n path: P.rulesMd,\n mode: 'skipIfExists',\n template: 'rules-seed.md.tmpl',\n description: 'AI working-rules seed',\n },\n\n // --- ignore files (host-agnostic; co-owned via IGNORE_BLOCK) ------------\n {\n path: '.gitignore',\n mode: 'managedBlock',\n block: IGNORE_BLOCK,\n template: 'gitignore.tmpl',\n description: '.gitignore noir managed block',\n },\n {\n path: '.dockerignore',\n mode: 'managedBlock',\n block: IGNORE_BLOCK,\n template: 'dockerignore.tmpl',\n description: '.dockerignore noir managed block',\n },\n {\n path: '.npmignore',\n mode: 'managedBlock',\n block: IGNORE_BLOCK,\n template: 'npmignore.tmpl',\n description: '.npmignore noir managed block',\n },\n {\n path: '.prettierignore',\n mode: 'managedBlock',\n block: IGNORE_BLOCK,\n template: 'prettierignore.tmpl',\n description: '.prettierignore noir managed block',\n },\n ];\n}\n\n// ---------------------------------------------------------------------------\n// S10 — host-specific artifact generation. One entry point: `buildHostArtifacts`.\n// ---------------------------------------------------------------------------\n\n/** Context shape passed to {@link buildHostArtifacts}. A strict subset of\n * {@link BuildManifestContext} (no `projectId`/`host` — the adapter IS the\n * resolved host, and host artifacts never need the project id). Exported\n * separately so callers + tests can name the narrower contract. */\nexport interface BuildHostArtifactsContext {\n root: string;\n transport: 'stdio' | 'streamable-http';\n url?: string;\n}\n\n/**\n * Materialize the host-specific manifest entries from a resolved adapter.\n * SINGLE entry point — no scattered `if (host === '…')` conditionals in the\n * orchestrator. Returns entries in emission order:\n *\n * 1. **AGENTS.md** (universal baseline) — `regenerate` at\n * `adapter.agentsMdPath(ctx)` (default `<root>/AGENTS.md`), content from\n * the shared `emitAgentsMd(ctx)` helper. Emitted ONLY for hosts whose\n * `emitContext` IS the AGENTS.md content (agents-md, cursor, opencode) —\n * for them AGENTS.md is the SINGLE native context surface AND carries the\n * Noir working rules via its `@.noir/rules/RULES.md` import. claude and\n * gemini have their OWN native context file (CLAUDE.md / GEMINI.md) that\n * `@`-imports the canonical `.noir/` sources; emitting AGENTS.md too\n * would IMPORT THOSE FILES TWICE into the host's context (2× tokens +\n * drift risk), so for those two hosts AGENTS.md is SKIPPED. (Claude Code\n * still discovers AGENTS.md at the repo root when present — users who\n * want the universal file can drop one in by hand; Noir's auto-emission\n * stays single-source per host.)\n * 2. **Host-native context file** — emitted ONLY for hosts whose `emitContext`\n * is NOT the AGENTS.md content (i.e. the host has its OWN context file\n * with a distinct syntax). Concretely: claude → `CLAUDE.md` (CONTEXT +\n * RULES managed blocks, byte-identical to v1.1 via templates); gemini →\n * `GEMINI.md` (CONTEXT + RULES managed blocks with Gemini's bare `@`\n * import syntax). For `agents-md`/`cursor`/`opencode` the context IS the\n * AGENTS.md (already emitted in step 1) → SKIP to avoid a duplicate.\n * Rules live INSIDE the host's context file: claude's in CLAUDE.md,\n * gemini's in GEMINI.md, agents-md/cursor/opencode's in AGENTS.md — NO\n * host emits a separate rules file. (The prior cursor\n * `.cursor/rules/noir-contract.mdc` host-rules pointer was REMOVED: it\n * collided with the C3 cursor flat-skill prune of `noir-*.mdc` under\n * `.cursor/rules/`, and cursor's rules are already delivered via\n * AGENTS.md's `@.noir/rules/RULES.md` import.)\n * 3. **Host MCP config** — `regenerate` at `adapter.mcpConfigPath(ctx)`\n * (default `<root>/.mcp.json` for claude), content from\n * `adapter.emitMcpConfig(ctx, {transport,url})`. Claude KEEPS the template\n * path (byte-identical parity with v1.1 + the .mcp.json parity test that\n * compares against `claudeAdapter.emitMcpConfig`); other hosts use the\n * adapter directly.\n *\n * Skills are OUT OF SCOPE here — the cli composes `emitSkillsToDir` with\n * `adapter.skillsDir` + the host's `CompileTarget` (claude → `.claude/skills/`\n * as SKILL.md; cursor → `.cursor/rules/<skill>.mdc` FLAT per C3; gemini/\n * agents-md/opencode have no skill dir → skip).\n */\nexport function buildHostArtifacts(\n adapter: HostAdapter,\n ctx: BuildHostArtifactsContext,\n): ManifestEntry[] {\n const ectx: EmitContext = { root: ctx.root };\n const host = adapter.id;\n const entries: ManifestEntry[] = [];\n\n // 1. AGENTS.md — emitted for hosts whose emitContext IS the AGENTS.md content\n // (agents-md, cursor, opencode). SKIPPED for claude/gemini: their native\n // CLAUDE.md / GEMINI.md already @-import the canonical .noir/ sources, so\n // a root AGENTS.md would double-import (2× context tokens, drift risk).\n // This also restores the claude default `noir init` to byte-identity with\n // v1.1 (the prior additive AGENTS.md delta is removed).\n const emitsAgentsMd = host === 'agents-md' || host === 'cursor' || host === 'opencode';\n if (emitsAgentsMd) {\n entries.push({\n path: hostRel(adapter.agentsMdPath?.(ectx) ?? join(ctx.root, AGENTS_MD_FILENAME), ctx.root),\n mode: 'regenerate',\n host,\n content: emitAgentsMd(ectx),\n description: `AGENTS.md (${host}'s native context surface; @-imports .noir/)`,\n });\n }\n\n // 2. Host-native context file (when distinct from AGENTS.md) + folded rules.\n switch (host) {\n case 'claude':\n // CLAUDE.md keeps template-based bodies — byte-identical to v1.1 (the\n // scaffold.test.ts parity gates compare against claudeAdapter.emitContext\n // + emitRules; the templates render to the same body bytes).\n entries.push({\n path: 'CLAUDE.md',\n mode: 'managedBlock',\n host,\n block: CONTEXT_BLOCK,\n template: 'claude-context-block.md.tmpl',\n description: 'CLAUDE.md context @import block',\n });\n entries.push({\n path: 'CLAUDE.md',\n mode: 'managedBlock',\n host,\n block: RULES_BLOCK,\n template: 'claude-rules-block.md.tmpl',\n description: 'CLAUDE.md rules @import block',\n });\n break;\n case 'gemini':\n // GEMINI.md carries CONTEXT_BLOCK + RULES_BLOCK with Gemini's bare\n // `@file` import syntax (no `@import` keyword, no quotes — distinct from\n // Claude's form). Emitted as TWO managed regions so user content outside\n // the markers survives `noir sync` (same write path as CLAUDE.md — the\n // multi-region atomic `managedBlocks` writer).\n entries.push({\n path: 'GEMINI.md',\n mode: 'managedBlock',\n host,\n block: CONTEXT_BLOCK,\n content: '@.noir/NOIR.md',\n description: 'GEMINI.md context @-import block',\n });\n entries.push({\n path: 'GEMINI.md',\n mode: 'managedBlock',\n host,\n block: RULES_BLOCK,\n content: '@.noir/rules/RULES.md',\n description: 'GEMINI.md rules @-import block',\n });\n break;\n case 'agents-md':\n case 'cursor':\n case 'opencode':\n // emitContext IS the AGENTS.md content (already emitted in step 1) →\n // no separate context file. Rules are carried by AGENTS.md's\n // `@.noir/rules/RULES.md` import (agents-md/cursor/opencode share that\n // universal surface — NO host emits a separate rules file).\n break;\n }\n\n // 3. Host MCP config. Claude keeps the template path (byte-identical parity\n // gate); other hosts use adapter.emitMcpConfig directly.\n const mcpAbs = adapter.mcpConfigPath?.(ectx) ?? join(ctx.root, '.mcp.json');\n const mcpRel = hostRel(mcpAbs, ctx.root);\n if (host === 'claude') {\n const mcpTemplate =\n ctx.transport === 'streamable-http' ? 'mcp.http.json.tmpl' : 'mcp.stdio.json.tmpl';\n entries.push({\n path: mcpRel,\n mode: 'regenerate',\n host,\n template: mcpTemplate,\n description: 'host MCP server pointer',\n });\n } else {\n const mcpContent = `${adapter.emitMcpConfig(ectx, {\n transport: ctx.transport,\n ...(ctx.url !== undefined ? { url: ctx.url } : {}),\n })}\\n`;\n entries.push({\n path: mcpRel,\n mode: 'regenerate',\n host,\n content: mcpContent,\n description: `${host} MCP server pointer`,\n });\n }\n\n return entries;\n}\n\n/** Convert an absolute path under `root` to a repo-relative POSIX string (the\n * manifest's path shape). Throws if `abs` is NOT under `root` so a future\n * adapter that returns a stray path fails loudly instead of producing a\n * malformed manifest entry. */\nfunction hostRel(abs: string, root: string): string {\n const rel = relative(root, abs);\n if (rel.length === 0 || rel.startsWith('..') || rel.startsWith('/')) {\n throw new Error(`buildHostArtifacts: path '${abs}' is not under root '${root}'`);\n }\n // Normalize any platform separators to POSIX (manifest paths are POSIX).\n return rel.replace(/\\\\/g, '/');\n}\n","// SP-D follow-up — three-way merge for managed-block regions.\n//\n// When a user hand-edits the INSIDE of a `<!-- noir:* -->` managed region and a\n// later `noir init`/`sync` updates the template, this merges (base/ours/theirs)\n// instead of strip-replacing (which would silently clobber the user's edit).\n// Line-level diff3: disjoint line changes merge cleanly; overlapping changes\n// surface as inline `<<<<<<< / ======= / >>>>>>>` markers for manual resolution.\n// Never silently drops either side.\n\nexport interface MergeResult {\n /** The merged text. When `conflict` is true, contains inline markers. */\n merged: string;\n /** True when ours and theirs diverged in the same region (markers emitted). */\n conflict: boolean;\n}\n\n/** Whole-string short-circuits for the trivial cases (also covers empty). */\nfunction trivial(base: string, ours: string, theirs: string): MergeResult | null {\n if (ours === base) return { merged: theirs, conflict: false };\n if (theirs === base) return { merged: ours, conflict: false };\n if (ours === theirs) return { merged: ours, conflict: false };\n return null;\n}\n\n/** Read index `i` of `arr` as a number (0 when out of range) — sidesteps\n * noUncheckedIndexedAccess without non-null assertions. */\nfunction numAt(arr: readonly number[], i: number): number {\n return arr[i] ?? 0;\n}\n\n/** Longest-common-subsequence match pairs between two line arrays. */\nfunction lcsMatch(a: readonly string[], b: readonly string[]): Array<[number, number]> {\n const n = a.length;\n const m = b.length;\n const dp: number[][] = Array.from({ length: n + 1 }, () => new Array<number>(m + 1).fill(0));\n for (let i = n - 1; i >= 0; i--) {\n const dpi = dp[i] ?? [];\n const dpi1 = dp[i + 1] ?? [];\n const ai = a[i] ?? '';\n for (let j = m - 1; j >= 0; j--) {\n const bj = b[j] ?? '';\n dpi[j] = ai === bj ? numAt(dpi1, j + 1) + 1 : Math.max(numAt(dpi1, j), numAt(dpi, j + 1));\n }\n }\n const out: Array<[number, number]> = [];\n let i = 0;\n let j = 0;\n while (i < n && j < m) {\n if ((a[i] ?? '') === (b[j] ?? '')) {\n out.push([i, j]);\n i++;\n j++;\n } else if (numAt(dp[i + 1] ?? [], j) >= numAt(dp[i] ?? [], j + 1)) {\n i++;\n } else {\n j++;\n }\n }\n return out;\n}\n\nconst eq = (a: readonly string[], b: readonly string[]): boolean =>\n a.length === b.length && a.every((v, k) => v === b[k]);\n\n/**\n * Three-way merge of `ours` (the user's current region) against `theirs` (the\n * new template) with `base` (the last-emitted ancestor). Line-level diff3:\n * changes in disjoint line ranges merge; overlapping changes become inline\n * conflict markers (`<<<<<<< ours` / `=======` / `>>>>>>> theirs`). Pure +\n * deterministic (no IO) so it's unit-testable.\n */\nexport function mergeThreeWay(base: string, ours: string, theirs: string): MergeResult {\n const t = trivial(base, ours, theirs);\n if (t) return t;\n\n const B = base.split('\\n');\n const O = ours.split('\\n');\n const T = theirs.split('\\n');\n const oMap = new Map<number, number>(); // baseIdx → oursIdx\n for (const [bi, oi] of lcsMatch(B, O)) oMap.set(bi, oi);\n const tMap = new Map<number, number>(); // baseIdx → theirsIdx\n for (const [bi, ti] of lcsMatch(B, T)) tMap.set(bi, ti);\n const isAnchor = (bi: number): boolean => oMap.has(bi) && tMap.has(bi);\n\n const anchors: number[] = [];\n for (let b = 0; b < B.length; b++) if (isAnchor(b)) anchors.push(b);\n const pts: number[] = [-1, ...anchors, B.length];\n\n const out: string[] = [];\n let conflict = false;\n for (let k = 0; k < pts.length - 1; k++) {\n const aBase = pts[k];\n const bBase = pts[k + 1];\n if (aBase === undefined || bBase === undefined) break;\n const baseSeg = B.slice(aBase + 1, bBase);\n const oStart = aBase >= 0 ? (oMap.get(aBase) ?? -1) + 1 : 0;\n const oEnd = bBase < B.length ? (oMap.get(bBase) ?? 0) : O.length;\n const tStart = aBase >= 0 ? (tMap.get(aBase) ?? -1) + 1 : 0;\n const tEnd = bBase < B.length ? (tMap.get(bBase) ?? 0) : T.length;\n const oSeg = O.slice(oStart, oEnd);\n const tSeg = T.slice(tStart, tEnd);\n if (eq(oSeg, baseSeg) && eq(tSeg, baseSeg)) out.push(...baseSeg);\n else if (eq(oSeg, baseSeg)) out.push(...tSeg);\n else if (eq(tSeg, baseSeg)) out.push(...oSeg);\n else if (eq(oSeg, tSeg)) out.push(...oSeg);\n else {\n conflict = true;\n out.push('<<<<<<< ours', ...oSeg, '=======', ...tSeg, '>>>>>>> theirs');\n }\n if (bBase < B.length) out.push(B[bBase] ?? '');\n }\n return { merged: out.join('\\n'), conflict };\n}\n","import { existsSync, readFileSync, writeFileSync } from 'node:fs';\nimport { join } from 'node:path';\nimport type { MigrationResult, MigrationScript } from './types.js';\n\nexport { runMigrations } from './runner.js';\nexport type { MigrationContext, MigrationResult, MigrationScript } from './types.js';\n\n/**\n * Migration registry — the linear history of scaffold-version upgrades.\n *\n * At v1.0.0 there are no real migrations (the package ships fresh, so every\n * install starts at `CURRENT_SCAFFOLD_VERSION`). The registry is the\n * deliverable: it proves the runner end-to-end and gives the next contributor\n * a copy-pasteable template. Add the first real entry here when a template or\n * manifest change merits a `1.0.0 → 1.1.0` step.\n *\n * Convention:\n * - `from`/`to` are bare `x.y.z` (no `v` prefix, no pre-release); the runner\n * compares them numerically.\n * - Every `run` MUST be idempotent and non-throwing (capture failures into\n * `result.conflicts`). See {@link types.ts}.\n * - Conflict resolution writes git-style markers inline — see\n * {@link applyWithConflict} for the canonical helper.\n */\n\n/** Synthetic 1.0.0 → 1.0.0 migration. Proves the runner wires up; also\n * demonstrates the conflict-marker path with a guarded, idempotent touch on\n * `.noir/scaffold-version` only when explicitly asked via\n * `NOIR_TEST_FORCE_CONFLICT`. Safe to remove once a real migration lands. */\nconst synthetic: MigrationScript = {\n from: '1.0.0',\n to: '1.0.0',\n description: 'no-op synthetic migration (runner smoke test)',\n run: (ctx) => {\n const result: MigrationResult = { changed: [], conflicts: [], notes: [] };\n // The only \"real\" thing it does: when the env var is set, write a conflict\n // marker into `.noir/scaffold-version` so the runner's conflict plumbing is\n // exercised by tests. In normal operation this branch never fires and the\n // script is a true no-op.\n if (process.env.NOIR_TEST_FORCE_CONFLICT === '1' && !ctx.dryRun) {\n const file = join(ctx.root, '.noir', 'scaffold-version');\n if (existsSync(file)) {\n const prev = readFileSync(file, 'utf8');\n const merged = applyInlineConflict(prev, 'noir-scaffold=1.0.0\\n', 'ours', 'theirs');\n writeFileSync(file, merged, 'utf8');\n result.conflicts.push('.noir/scaffold-version');\n }\n }\n result.notes.push('synthetic 1.0.0→1.0.0 migration ran');\n return result;\n },\n};\n\nexport const MIGRATIONS: readonly MigrationScript[] = [synthetic];\n\n// --- conflict-marker helpers (exported for migration authors) ---------------\n\n/** Write git-style inline conflict markers around `theirs`/`ours` so a human\n * or AI agent can resolve later. This is the CI-safe fallback the spec locks\n * in (S-OQ2) — no interactive prompts, ever. */\nexport function applyInlineConflict(\n ours: string,\n theirs: string,\n oursLabel = 'ours',\n theirsLabel = 'theirs',\n): string {\n return `<<<<<<< ${oursLabel}\\n${ours}=======\\n${theirs}>>>>>>> ${theirsLabel}\\n`;\n}\n\n/** Apply `(ours, theirs)` to a region: if they're equal, return `ours` (no\n * conflict); otherwise emit inline markers. Migration authors should prefer\n * this over {@link applyInlineConflict} when the \"no change needed\" case is\n * common — it keeps re-runs truly idempotent (no spurious markers on a clean\n * tree). */\nexport function applyWithConflict(\n ours: string,\n theirs: string,\n path: string,\n): {\n text: string;\n conflicted: boolean;\n} {\n if (ours === theirs) return { text: ours, conflicted: false };\n return {\n text: applyInlineConflict(ours, theirs, path, path),\n conflicted: true,\n };\n}\n","import { MIGRATIONS } from './index.js';\nimport type { MigrationContext, MigrationResult, MigrationScript } from './types.js';\n\n/**\n * Migration runner. Given a `from` version present on disk and a target `to`\n * version (usually {@link CURRENT_SCAFFOLD_VERSION}), walks the\n * {@link MIGRATIONS} registry forward in version order, executing each step.\n *\n * Selection rule: a script runs when its `from` is `>= fromArg` (semver-ish\n * string compare is enough at Noir's scale; we don't pull in a semver dep for\n * this) AND its `to` is `<= toArg`. Scripts strictly outside the window are\n * skipped. Within the window, scripts run sorted by `to` ascending so a\n * multi-step upgrade (1.0.0 → 1.1.0 → 1.2.0) composes in order.\n *\n * The runner NEVER throws on a per-script failure — it captures the error,\n * records a synthetic conflict entry (`<path>__error`), and continues so a\n * single broken migration doesn't block the rest of the chain. The orchestrator\n * decides whether non-empty `conflicts` is a hard failure (init --upgrade) or a\n * warning (doctor).\n *\n * Returns the aggregate of every step's `changed`/`conflicts`/`notes`.\n */\nexport function runMigrations(\n root: string,\n from: string | null,\n to: string,\n opts: { dryRun?: boolean } = {},\n): MigrationResult & { from: string | null; to: string; ran: string[] } {\n const window = pickWindow(from ?? '0.0.0', to, MIGRATIONS);\n const ctx: MigrationContext = { root, dryRun: opts.dryRun === true };\n const aggregate: MigrationResult = { changed: [], conflicts: [], notes: [] };\n const ran: string[] = [];\n\n for (const script of window) {\n ran.push(`${script.from}→${script.to}`);\n let res: MigrationResult;\n try {\n res = script.run(ctx);\n } catch (err) {\n // Non-fatal at the runner level: record and continue. The caller turns\n // non-empty conflicts into a CI failure with a real exit code.\n const msg = err instanceof Error ? err.message : String(err);\n aggregate.conflicts.push(`<runner>:${script.from}→${script.to} threw: ${msg}`);\n continue;\n }\n aggregate.changed.push(...res.changed);\n aggregate.conflicts.push(...res.conflicts);\n aggregate.notes.push(...res.notes);\n }\n\n return {\n ...aggregate,\n from,\n to,\n ran,\n };\n}\n\n/** Pick the ordered subset of `scripts` forming the chain `[from, to]`. */\nfunction pickWindow(\n from: string,\n to: string,\n scripts: readonly MigrationScript[],\n): MigrationScript[] {\n return scripts\n .filter((s) => compareVer(s.from) >= compareVer(from) && compareVer(s.to) <= compareVer(to))\n .sort((a, b) => compareVer(a.to) - compareVer(b.to));\n}\n\n/** Tiny numeric tuple compare: `'1.10.3'` → `[1,10,3]`, compared element-wise.\n * Pre-release suffixes (e.g. `-beta.1`) are ignored — Noir ships `x.y.z`\n * scaffold versions and the runner only needs a deterministic order. */\nfunction compareVer(v: string): number {\n const parts = v.split('-')[0]?.split('.') ?? [];\n let n = 0;\n for (let i = 0; i < 3; i++) {\n const p = Number(parts[i] ?? '0');\n n = n * 1000 + (Number.isFinite(p) ? p : 0);\n }\n return n;\n}\n","import { existsSync, mkdirSync, readFileSync, renameSync, rmSync } from 'node:fs';\nimport { basename, dirname, join } from 'node:path';\nimport { createProjectId, type ManagedBlock, paths, readManagedBlock } from '@noir-ai/core';\nimport { readAncestors, writeAncestors } from './ancestors.js';\nimport {\n type BuildManifestContext,\n buildManifest,\n type HostTag,\n type ManifestEntry,\n} from './manifest.js';\nimport { mergeThreeWay } from './merge.js';\nimport { runMigrations } from './migrations/index.js';\nimport {\n CURRENT_SCAFFOLD_VERSION,\n readScaffoldVersion,\n writeScaffoldVersion,\n} from './scaffold-version.js';\nimport { detectStack, type StackInfo } from './stack-detect.js';\nimport { render } from './template.js';\nimport { loadTemplate } from './template-loader.js';\nimport {\n buildRegion,\n managedBlock,\n managedBlocks,\n regenerate,\n skipIfExists,\n type WriteMode,\n} from './writers.js';\n\n/**\n * Scaffold orchestrator. One function the cli (S-T2) calls for `noir init`,\n * `noir create`, and `noir sync`; mode selects the manifest subset + how\n * project identity is resolved.\n *\n * High-level flow:\n * 1. Resolve project id (provided > existing > generated; sync requires existing).\n * 2. Detect stack (READ-ONLY; always runs; never throws).\n * 3. If {@link ScaffoldOptions.upgrade}, run migrations from the on-disk\n * scaffold-version → {@link CURRENT_SCAFFOLD_VERSION}.\n * 4. Build the manifest, filter by host + mode.\n * 5. For each entry: mkdir -p, render template/content, dispatch to the\n * matching writer.\n * 6. On `init`/`create`, stamp `.noir/scaffold-version` LAST so a crash leaves\n * an old/absent stamp rather than a misleading fresh one.\n *\n * The orchestrator owns dir creation (writers refuse to so that a missing dir\n * is one attributable failure, not N silent ones).\n */\n\nexport type ScaffoldMode = 'init' | 'create' | 'sync';\n\nexport interface ScaffoldOptions {\n /** Absolute repo root. For `create`, the new dir (created if absent). */\n root: string;\n mode: ScaffoldMode;\n /** Target host. Defaults to `'claude'` (the only shipped host). */\n host?: HostTag;\n /** MCP transport. Defaults to `'stdio'`. */\n transport?: 'stdio' | 'streamable-http';\n /** Required when transport is `streamable-http`. */\n url?: string;\n /** Explicit project id; bypasses generate/read. Mainly for tests + `create`\n * flows that want deterministic ids. */\n projectId?: string;\n /** `noir init --upgrade`: run migrations before re-emitting, and emit only\n * regenerate + managedBlock (skipIfExists left alone). Only meaningful\n * with mode `'init'`. */\n upgrade?: boolean;\n /** SP-A: re-scaffold even when the target is already initialized (bypasses\n * the already-initialized no-op guard). Does NOT bypass `assertSafeRoot`\n * — root-safety is hard, never bypassable. */\n force?: boolean;\n /** Preview: compute the same written/skipped/migrated lists without touching\n * disk. `noir doctor`/CI use this to report drift. */\n dryRun?: boolean;\n /** SP-C: policy for a `regenerate` file that exists and DIFFERS from the\n * template, when no {@link onConflict} callback is provided. Default\n * `'overwrite'` is byte-backward-compatible. `'preserve'` keeps the user's\n * file (the non-TTY / CI default in the cli). */\n conflictPolicy?: 'overwrite' | 'preserve';\n /** SP-C: per-file conflict resolver — the UI seam (the engine stays UI-free;\n * the cli injects a @clack-based resolver). Called when a `regenerate`\n * file exists and differs from the template. */\n onConflict?: (ctx: ConflictContext) => Promise<ConflictResolution> | ConflictResolution;\n /** SP-D follow-up: three-way merge managed regions (base/ours/theirs) using a\n * persisted ancestor snapshot (`.noir/ancestors.json`) instead of\n * strip-replace, so a hand-edit inside a `<!-- noir:* -->` region survives a\n * template update. Opt-in (default false ⇒ current strip-replace, no\n * ancestor file). Single-region managed files only (NOIR.md, ignores);\n * multi-region (CLAUDE.md) is a follow-up. */\n mergeManagedRegions?: boolean;\n}\n\nexport interface ScaffoldResult {\n /** Repo-relative paths actually written. */\n written: string[];\n /** Repo-relative paths skipIfExists'd (already present). */\n skipped: string[];\n /** SP-D: repo-relative `regenerate` paths skipped because byte-identical to\n * the template (content-hash dedup — no rewrite). */\n identical: string[];\n /** SP-A: true when the already-initialized guard short-circuited (a bare\n * `noir init`/`create` on an initialized project). Callers (init.ts/create.ts)\n * gate skills emission + the \"initialized\" message on this — a no-op must NOT\n * re-emit skills or claim it initialized. */\n noop: boolean;\n /** Migration steps executed (`<from>→<to>`), when upgrade ran. */\n migrationsRan: string[];\n /** Migration conflicts (repo-relative or `<runner>:…`), when upgrade ran. */\n migrationConflicts: string[];\n stack: StackInfo;\n projectId: string;\n fromVersion: string | null;\n toVersion: string;\n /** The host actually emitted (post-default). */\n host: HostTag;\n}\n\n/** SP-C — context passed to {@link ScaffoldOptions.onConflict} when a\n * `regenerate` file exists and differs from the template. */\nexport interface ConflictContext {\n /** Repo-relative path of the conflicting file. */\n relPath: string;\n /** The file's current on-disk content. */\n existing: string;\n /** The content the scaffold would write. */\n proposed: string;\n}\n\n/** SP-C — how to resolve a `regenerate` conflict. */\nexport type ConflictResolution = 'replace' | 'preserve' | 'rename' | 'duplicate' | 'cancel';\n\nconst WRITER_BY_MODE: Record<WriteMode, 'all' | 'runtime'> = {\n // 'runtime' subset = regenerate + managedBlock (the always-safe-to-rewrite\n // entries). sync + init --upgrade emit only this subset; skipIfExists is\n // reserved for first-run init/create so user edits survive.\n regenerate: 'runtime',\n managedBlock: 'runtime',\n skipIfExists: 'all',\n};\n\n/**\n * Refuse to scaffold when `root` is — or is inside — a `.noir/` directory.\n * (SP-A) Running `noir init`/`create`/`sync` while cwd = `.noir/` would\n * otherwise mint a FRESH project id (because `<root>/.noir/project.id` is\n * absent) and build a NESTED second project (`.noir/.noir/`, `.noir/CLAUDE.md`,\n * `.noir/.claude/skills/`, …) — the duplicate-`.noir` bug. The walk ascends the\n * ancestor chain only, so a legitimate project root that merely CONTAINS a\n * `.noir/` store is never flagged. String-based: works whether or not `root`\n * exists on disk yet (so `noir create <new-dir>` is still guarded).\n *\n * Hard against literal `.noir` path segments — NOT bypassable by `--force`\n * (which is reserved for the already-initialized no-op). NOTE: the walk is\n * string-based, so a symlink whose TARGET is inside `.noir/` is not resolved\n * (a local self-foot-gun only — the caller controls `root` — not externally\n * exploitable); in practice `--cwd`/positional roots are literal paths.\n */\nexport function assertSafeRoot(root: string): void {\n let cur = root;\n for (let i = 0; i < 64; i++) {\n if (basename(cur) === '.noir') {\n throw new Error(\n `Refusing to scaffold inside a .noir/ directory (${root}). Run \\`noir init\\` from the project root, not from inside .noir/.`,\n );\n }\n const parent = dirname(cur);\n if (parent === cur) break; // reached the filesystem root\n cur = parent;\n }\n}\n\nexport async function scaffold(opts: ScaffoldOptions): Promise<ScaffoldResult> {\n // Root-safety (SP-A): refuse to scaffold at/inside a .noir/ directory BEFORE\n // any write (incl. `create`'s target mkdir). Prevents the nested .noir/.noir/\n // re-init bug. Hard guard; not bypassable.\n assertSafeRoot(opts.root);\n\n const host: HostTag = opts.host ?? 'claude';\n const transport: BuildManifestContext['transport'] = opts.transport ?? 'stdio';\n if (transport === 'streamable-http' && !opts.url) {\n throw new Error(\"transport 'streamable-http' requires opts.url\");\n }\n\n // 1. Root for `create` may not exist yet — mirror `init.ts`'s mkdir of .noir/.\n if (opts.mode === 'create' && !opts.dryRun) {\n mkdirSync(opts.root, { recursive: true });\n }\n\n // 2. Resolve project id. Read the on-disk stamp ONCE and reuse the result\n // for the corrupt-file heal below (C1). sync requires a VALID existing id.\n const idFile = readProjectIdFile(opts.root);\n const projectId = resolveProjectId(opts, idFile);\n\n // C1: a `project.id` that EXISTS but is empty/unparseable is CORRUPT, not\n // absent. The manifest writes project.id via `skipIfExists`, which would\n // preserve the empty file while NOIR.md's BRIEF_BLOCK renders the freshly\n // resolved/generated id → silent identity split (NOIR.md states an id the\n // store DB can't open). project.id is Noir-owned canonical; heal a corrupt\n // stamp by removing it so `skipIfExists` writes the resolved id fresh.\n // Absent/valid files and dryRun are left to the manifest writer.\n if (!opts.dryRun && idFile.state === 'corrupt') {\n rmSync(paths.projectId(opts.root), { force: true });\n }\n\n const fromVersion = readScaffoldVersion(opts.root);\n\n // 3. Stack detect (read-only, never throws). Always populated so callers\n // (TUI, doctor) get a single source of truth regardless of mode.\n const stack = detectStack(opts.root);\n\n // SP-D: ancestor map for three-way managed-region merge (opt-in). Read once;\n // written back at the end only when mergeManagedRegions is set.\n const ancestors = opts.mergeManagedRegions ? readAncestors(opts.root) : {};\n\n // SP-A — already-initialized guard: a bare `noir init`/`noir create` on a\n // project that already carries a .noir/scaffold-version stamp is a NO-OP,\n // not a silent re-emit (re-running init looked like it re-scaffolded, which\n // is what made the nested-`.noir` bug feel like \"init duplicates things\").\n // `--upgrade` is the explicit migrate+re-emit path; `--force` re-scaffolds\n // without migrating. Both bypass this guard. sync is unaffected (it requires\n // a valid project.id and emits the runtime subset only). dryRun returns the\n // no-op shape silently (no stderr) so `noir doctor`/CI previews stay clean.\n if (\n fromVersion !== null &&\n opts.upgrade !== true &&\n opts.force !== true &&\n (opts.mode === 'init' || opts.mode === 'create')\n ) {\n if (opts.dryRun !== true) {\n process.stderr.write(\n `Noir is already initialized in ${opts.root} (scaffold ${fromVersion}). No-op. Use \\`noir init --upgrade\\` to migrate, or \\`--force\\` to re-scaffold.\\n`,\n );\n }\n return {\n written: [],\n skipped: [],\n identical: [],\n noop: true,\n migrationsRan: [],\n migrationConflicts: [],\n stack,\n projectId,\n fromVersion,\n toVersion: CURRENT_SCAFFOLD_VERSION,\n host,\n };\n }\n\n // 4. Migrations (only when explicitly upgrading). M4: a fresh project\n // (`fromVersion === null`) has NO prior stamp → nothing to migrate. Skip\n // entirely so `noir init --upgrade` on a never-initialized tree doesn't\n // report a synthetic no-op `1.0.0→1.0.0` step.\n const migrationsRan: string[] = [];\n const migrationConflicts: string[] = [];\n if (opts.mode === 'init' && opts.upgrade === true && fromVersion !== null) {\n const m = runMigrations(opts.root, fromVersion, CURRENT_SCAFFOLD_VERSION, {\n dryRun: opts.dryRun === true,\n });\n migrationsRan.push(...m.ran);\n migrationConflicts.push(...m.conflicts);\n }\n\n // 5. Build manifest + filter by host + mode.\n const manifest = buildManifest({ root: opts.root, projectId, host, transport, url: opts.url });\n const emitRuntimeOnly = opts.mode === 'sync' || (opts.mode === 'init' && opts.upgrade === true);\n const vars: BuildManifestContext = {\n root: opts.root,\n projectId,\n host,\n transport,\n url: opts.url,\n };\n\n const written: string[] = [];\n const skipped: string[] = [];\n const identical: string[] = [];\n\n // GROUP applicable entries by target path (manifest order preserved within\n // each group) so files carrying MULTIPLE managed blocks (CLAUDE.md today =\n // CONTEXT + RULES) get ONE atomic multi-region write (I1). Single-entry\n // paths keep the existing per-entry writer — byte-stable for the NOIR.md\n // brief, the ignore files, and the regenerated `.mcp.json`. dryRun uses the\n // SAME grouping so its reported paths match what a real run would write\n // (CLAUDE.md reported once, not twice).\n const groups = groupApplicableByPath(manifest, host, emitRuntimeOnly);\n for (const [relPath, entries] of groups) {\n const abs = join(opts.root, relPath);\n const managed = entries.filter((e) => e.mode === 'managedBlock');\n\n if (managed.length >= 2) {\n // Multi-managed-block file: ONE atomic write of all regions.\n if (opts.dryRun) {\n written.push(relPath);\n continue;\n }\n mkdirSync(dirname(abs), { recursive: true });\n const regions = managed.map((e) => {\n const block = e.block;\n if (!block) {\n throw new Error(`manifest entry ${e.path}: managedBlock mode missing 'block'`);\n }\n const theirs = buildRegion(block, renderEntry(e, vars));\n const regionText = opts.mergeManagedRegions\n ? mergeManagedRegion(abs, e.path, block, theirs, ancestors)\n : theirs;\n if (opts.mergeManagedRegions) ancestors[`${e.path}::${block.begin}`] = theirs;\n return { block, regionText };\n });\n managedBlocks(abs, regions);\n written.push(relPath);\n continue;\n }\n\n // Per-entry path: single-region managed / regenerate / skipIfExists.\n if (!opts.dryRun) mkdirSync(dirname(abs), { recursive: true });\n for (const entry of entries) {\n if (opts.dryRun) {\n if (entry.mode === 'skipIfExists' && existsSync(abs)) skipped.push(entry.path);\n else written.push(entry.path);\n continue;\n }\n const body = renderEntry(entry, vars);\n if (entry.mode === 'regenerate') {\n const out = await writeRegenerateWithConflict(abs, entry.path, body, opts);\n written.push(...out.written);\n skipped.push(...out.skipped);\n identical.push(...out.identical);\n } else if (entry.mode === 'managedBlock') {\n const block = entry.block;\n if (!block) {\n throw new Error(`manifest entry ${entry.path}: managedBlock mode missing 'block'`);\n }\n // I2: a legacy (pre-Slice-S) .noir/NOIR.md is a whole-file auto-brief\n // with NO managed markers. The normal path would treat the old brief\n // as user content and append a SECOND managed brief → two \"Project\n // id:\" lines. Self-heal: when the existing file has NO noir managed\n // marker at all, wipe it first so the managed write emits a clean\n // single brief. (Pre-Slice-S NOIR.md was 100% auto-generated, so\n // there is no user content to preserve in that legacy shape.)\n if (isNoirMdPath(entry.path)) healLegacyNoirMd(abs);\n const theirs = buildRegion(block, body);\n const regionText = opts.mergeManagedRegions\n ? mergeManagedRegion(abs, entry.path, block, theirs, ancestors)\n : theirs;\n managedBlock(abs, block, regionText);\n if (opts.mergeManagedRegions) ancestors[`${entry.path}::${block.begin}`] = theirs;\n written.push(entry.path);\n } else {\n const out = skipIfExists(abs, body);\n if (out.written) written.push(entry.path);\n else skipped.push(entry.path);\n }\n }\n }\n\n // 6. Stamp scaffold-version on init/create (NOT sync). Written last so a\n // crash leaves the previous stamp. Upgrade rewrites it to current.\n if ((opts.mode === 'init' || opts.mode === 'create') && !opts.dryRun) {\n writeScaffoldVersion(opts.root, CURRENT_SCAFFOLD_VERSION);\n }\n\n // SP-D: persist the ancestor map (only when three-way merge is opted in).\n if (opts.mergeManagedRegions && !opts.dryRun) {\n writeAncestors(opts.root, ancestors);\n }\n\n return {\n written,\n skipped,\n identical,\n noop: false,\n migrationsRan,\n migrationConflicts,\n stack,\n projectId,\n fromVersion,\n toVersion: CURRENT_SCAFFOLD_VERSION,\n host,\n };\n}\n\n// --- helpers -----------------------------------------------------------------\n\n/** SP-D — three-way merge a managed region against the persisted ancestor\n * (`base`). `theirs` is the freshly-rendered template region (with markers);\n * `ours` is the region currently on disk. With no ancestor / no existing\n * region this is a no-op (returns `theirs`). On conflict, inline markers are\n * written + a stderr note (never silently drops either side). */\nfunction mergeManagedRegion(\n abs: string,\n relPath: string,\n block: ManagedBlock,\n theirs: string,\n ancestors: Record<string, string>,\n): string {\n const base = ancestors[`${relPath}::${block.begin}`];\n if (base === undefined) return theirs; // no ancestor yet → strip-replace (first merge run)\n const ours = readManagedBlock(abs, block);\n if (ours === null) return theirs; // no existing region → fresh\n const res = mergeThreeWay(base, ours, theirs);\n if (res.conflict) {\n process.stderr.write(\n `noir: managed-region conflict in ${relPath} — wrote inline markers; resolve manually.\\n`,\n );\n }\n return res.merged;\n}\n\n/** Read the `.noir/project.id` stamp ONCE and classify it for BOTH id\n * resolution and the C1 corrupt-file heal. `absent` (ENOENT) and `valid`\n * (non-empty) are the normal cases; `corrupt` (file exists but trims to empty)\n * is healed by the orchestrator before the manifest loop so `skipIfExists`\n * writes the resolved id fresh instead of preserving the empty file. */\nfunction readProjectIdFile(\n root: string,\n): { state: 'absent'; id: null } | { state: 'valid'; id: string } | { state: 'corrupt'; id: null } {\n let raw: string;\n try {\n raw = readFileSync(paths.projectId(root), 'utf8');\n } catch {\n return { state: 'absent', id: null };\n }\n const trimmed = raw.trim();\n return trimmed.length > 0 ? { state: 'valid', id: trimmed } : { state: 'corrupt', id: null };\n}\n\nfunction resolveProjectId(\n opts: ScaffoldOptions,\n idFile: ReturnType<typeof readProjectIdFile>,\n): string {\n if (opts.projectId !== undefined) return opts.projectId;\n if (idFile.state === 'valid') return idFile.id;\n // absent OR corrupt → resolve a fresh id. sync still requires a valid\n // pre-existing id (a corrupt stamp can't be trusted to name the store DB).\n if (opts.mode === 'sync') {\n throw new Error(`Noir is not initialized in ${opts.root}. Run \\`noir init\\` first.`);\n }\n return createProjectId();\n}\n\n/**\n * SP-C — write a `regenerate` file, honoring conflict resolution when the\n * target already exists and DIFFERS from the proposed bytes. Identical bytes\n * (or a missing file) write straight through (content-hash dedup is a deferred\n * slice — identical still \"writes\" today to keep sync/--upgrade byte-stable).\n * Resolution:\n * - `replace` — overwrite (the historical default).\n * - `preserve`/`cancel` — keep the user's file; report skipped.\n * - `rename` — move the user's file to `<path>.local`, write template.\n * - `duplicate` — write the template to `<path>.noir`, keep the user's.\n * Returns the repo-relative paths to record as written / skipped.\n */\nasync function writeRegenerateWithConflict(\n abs: string,\n relPath: string,\n proposed: string,\n opts: ScaffoldOptions,\n): Promise<{ written: string[]; skipped: string[]; identical: string[] }> {\n let existing: string | undefined;\n try {\n existing = readFileSync(abs, 'utf8');\n } catch {\n existing = undefined;\n }\n if (existing === undefined) {\n regenerate(abs, proposed);\n return { written: [relPath], skipped: [], identical: [] };\n }\n if (existing === proposed) {\n // content-hash dedup: byte-identical → skip the rewrite entirely (no disk IO).\n return { written: [], skipped: [], identical: [relPath] };\n }\n const resolution: ConflictResolution =\n opts.onConflict !== undefined\n ? await opts.onConflict({ relPath, existing, proposed })\n : opts.conflictPolicy === 'preserve'\n ? 'preserve'\n : 'replace';\n switch (resolution) {\n case 'replace':\n regenerate(abs, proposed);\n return { written: [relPath], skipped: [], identical: [] };\n case 'rename': {\n // Preserve the user's file aside at a UNIQUE path. Review fix: a bare\n // `renameSync(abs, abs.local)` would silently clobber a pre-existing\n // `.local` (POSIX rename replaces) or throw EEXIST mid-scaffold (win32) —\n // the very data-loss SP-C exists to prevent. uniqueAside picks a fresh\n // `.local` (then `.local.1`, …) so the move is always safe.\n const aside = uniqueAside(abs, relPath, '.local');\n renameSync(abs, aside.abs);\n regenerate(abs, proposed);\n return { written: [relPath], skipped: [aside.rel], identical: [] };\n }\n case 'duplicate': {\n // Write the template ALONGSIDE at a unique path; keep the user's file\n // untouched. (Same unique-suffix safeguard as `rename`.)\n const aside = uniqueAside(abs, relPath, '.noir');\n regenerate(aside.abs, proposed);\n return { written: [aside.rel], skipped: [relPath], identical: [] };\n }\n case 'preserve':\n return { written: [], skipped: [relPath], identical: [] };\n case 'cancel':\n // Review fix: Cancel ABORTS the whole scaffold. It used to fall through\n // to \"skip this file\" and keep writing the remaining entries — a contract\n // violation (Cancel/Escape must stop the run). Throwing propagates out of\n // scaffold(); the cli reports it. Entries written before this conflict\n // remain on disk, as with any cancelled operation.\n throw new Error(`scaffold cancelled by user at conflicting file ${relPath}`);\n default:\n return { written: [], skipped: [relPath], identical: [] };\n }\n}\n\n/** Pick a fresh `<abs><suffix>` aside path (plus its repo-relative form) that\n * does NOT exist: tries `<suffix>`, then `<suffix>.1`, `<suffix>.2`, … so the\n * `rename`/`duplicate` resolutions never silently overwrite a prior backup\n * (data-loss) and never hit win32 EEXIST. */\nfunction uniqueAside(abs: string, relPath: string, suffix: string): { abs: string; rel: string } {\n const make = (s: string): { abs: string; rel: string } => ({\n abs: `${abs}${s}`,\n rel: `${relPath}${s}`,\n });\n let candidate = make(suffix);\n for (let n = 1; existsSync(candidate.abs); n++) candidate = make(`${suffix}.${n}`);\n return candidate;\n}\n\n/** Group applicable manifest entries by target path, preserving manifest order\n * within each group. JS `Map` preserves insertion order, so iterating groups\n * visits paths in the same sequence the manifest declares them (CONTEXT before\n * RULES inside the CLAUDE.md group). */\nfunction groupApplicableByPath(\n manifest: readonly ManifestEntry[],\n host: HostTag,\n emitRuntimeOnly: boolean,\n): Map<string, ManifestEntry[]> {\n const groups = new Map<string, ManifestEntry[]>();\n for (const entry of manifest) {\n if (entry.host !== undefined && entry.host !== host) continue; // host filter\n if (emitRuntimeOnly && WRITER_BY_MODE[entry.mode] !== 'runtime') continue;\n const list = groups.get(entry.path);\n if (list) list.push(entry);\n else groups.set(entry.path, [entry]);\n }\n return groups;\n}\n\n/** True for the canonical NOIR.md path the manifest emits (the BRIEF_BLOCK\n * target). Scoped so the I2 legacy-heal only fires for that one file — we must\n * not wipe arbitrary co-owned managed files. */\nfunction isNoirMdPath(relPath: string): boolean {\n return relPath === '.noir/NOIR.md';\n}\n\n/** I2 self-heal: wipe a legacy (pre-Slice-S) NOIR.md before the managed write.\n * Legacy shape = file exists but contains NO `<!-- noir:<name> begin -->`\n * managed marker (the whole file was the auto-brief). Files that already have\n * markers, or are absent, are left untouched (normal managed-block path or\n * fresh write respectively). */\nfunction healLegacyNoirMd(absPath: string): void {\n let content: string;\n try {\n content = readFileSync(absPath, 'utf8');\n } catch {\n return; // absent — fresh write, nothing to heal\n }\n if (/<!-- noir:[a-z]+ begin -->/.test(content)) return; // already managed-shape\n rmSync(absPath, { force: true });\n}\n\nfunction renderEntry(entry: ManifestEntry, vars: BuildManifestContext): string {\n if (entry.template !== undefined) {\n return render(loadTemplate(entry.template), vars);\n }\n if (entry.content !== undefined) return entry.content;\n throw new Error(`manifest entry ${entry.path}: must define 'content' or 'template'`);\n}\n","import { mkdirSync, readFileSync } from 'node:fs';\nimport { dirname, join } from 'node:path';\nimport { NOIR_DIR } from '@noir-ai/core';\nimport { regenerate } from './writers.js';\n\n/**\n * Scaffold version stamp — Noir's equivalent of Copier's `last-applied`.\n * Stored at `.noir/scaffold-version` as a single `noir-scaffold=<semver>` line\n * so `noir init --upgrade` / `noir doctor` can diff against\n * {@link CURRENT_SCAFFOLD_VERSION} and decide which migrations to run.\n *\n * Format is line-oriented (not YAML) on purpose: it must be readable before\n * `config.yml` is parsed (doctor runs even with a broken config), and the\n * `key=value` shape is trivial to grep from a shell.\n */\n\n/** The scaffold version this build of @noir-ai/create ships. Bumped atomically\n * whenever a manifest entry, template, or migration changes shape. */\nexport const CURRENT_SCAFFOLD_VERSION = '1.0.0';\n\nconst PREFIX = 'noir-scaffold=';\n\n/** Path to the stamp file under `root`. Exposed for tests + doctor. */\nexport function scaffoldVersionPath(root: string): string {\n return join(root, NOIR_DIR, 'scaffold-version');\n}\n\n/** Read the applied scaffold version, or `null` if the stamp is absent/unparseable.\n * Never throws — doctor must keep reporting even on a malformed stamp. */\nexport function readScaffoldVersion(root: string): string | null {\n let raw: string;\n try {\n raw = readFileSync(scaffoldVersionPath(root), 'utf8');\n } catch {\n return null;\n }\n for (const line of raw.split('\\n')) {\n const trimmed = line.trim();\n if (trimmed.startsWith(PREFIX)) {\n const v = trimmed.slice(PREFIX.length).trim();\n if (v.length > 0) return v;\n }\n }\n return null;\n}\n\n/** Write the stamp, creating `.noir/` if needed. The orchestrator writes it\n * LAST so a crash mid-scaffold leaves an old/absent stamp rather than a\n * misleading fresh one.\n *\n * N2: routed through the package's atomic `regenerate()` writer (tmp+rename in\n * the same dir) for consistency with the rest of the engine's durable writes —\n * a half-written stamp would mislead `noir doctor`/`init --upgrade`, so the\n * stamp deserves the same crash-atomicity as `.mcp.json` and the NOIR.md brief. */\nexport function writeScaffoldVersion(root: string, version: string): void {\n const file = scaffoldVersionPath(root);\n mkdirSync(dirname(file), { recursive: true });\n regenerate(file, `${PREFIX}${version}\\n`);\n}\n","import {\n closeSync,\n existsSync,\n openSync,\n readFileSync,\n renameSync,\n rmSync,\n writeFileSync,\n writeSync,\n} from 'node:fs';\nimport { basename, dirname, join } from 'node:path';\nimport type { ManagedBlock } from '@noir-ai/core';\nimport { stripManagedBlock, writeManagedRegion } from '@noir-ai/core';\n\n/**\n * The three-mode writer — generalizes keystone-K's `writeManagedRegion` into\n * the declarative dispatch the scaffold manifest drives. Each mode maps 1:1 to\n * an artifact class in the spec §4.5 matrix:\n *\n * - {@link regenerate} — pure pointers (`.mcp.json`, `NOIR.md` brief, …).\n * Always overwritten, atomically.\n * - {@link managedBlock} — co-owned files (`CLAUDE.md` context/rules,\n * `.gitignore` noir block, …). DELEGATES to\n * keystone-K's `writeManagedRegion` so user content\n * outside the markers is preserved byte-for-byte and\n * re-runs are idempotent. Never duplicate the\n * managed-region logic.\n * - {@link skipIfExists} — user-owned seeds (`RULES.md`, `config.yml`,\n * `project.id`). Write once; never clobber.\n *\n * The orchestrator (`scaffold.ts`) is the only intended caller; the per-mode\n * functions are exported so the cli (S-T2) and tests can drive them directly\n * when a one-off write is needed outside the manifest.\n */\n\nexport type WriteMode = 'regenerate' | 'managedBlock' | 'skipIfExists';\n\nexport interface WriteOutcome {\n /** The absolute path that was written. */\n path: string;\n mode: WriteMode;\n /** true when bytes hit disk; false for skipIfExists no-ops. regenerate and\n * managedBlock always write (managedBlock may write identical bytes — that\n * still counts as a write for telemetry purposes; the file IS up to date). */\n written: boolean;\n}\n\n/** Atomic overwrite. Writes to `<file>.tmp.<pid>.<rnd>` in the same directory,\n * fsyncs, then renames over the target so a crash never leaves a half-written\n * file (the pointer files this is used for are read by the host agent first —\n * a truncated CLAUDE.md/.mcp.json would break the very startup Noir serves).\n *\n * Parent directories are NOT created here — the orchestrator does that once\n * for the whole manifest so a missing dir is a single, attributable failure\n * rather than N silent ones inside the writer. */\nexport function regenerate(absPath: string, content: string): WriteOutcome {\n const dir = dirname(absPath);\n const tmp = join(\n dir,\n `.${basename(absPath)}.tmp.${process.pid}.${Math.random().toString(36).slice(2)}`,\n );\n // Open with 'w' truncates; writeSync + closeSync before rename so the bytes\n // are durable pre-swap. O_SYNC would be stronger but is platform-flaky; the\n // rename is the real atomicity guarantee on POSIX (and practical-enough on\n // the win32 targets Noir supports).\n //\n // M1: the tmp MUST be cleaned up on EVERY exit path. The previous shape only\n // ran `rmSync(tmp)` when `renameSync` threw, so a `writeSync` failure (disk\n // full, EPERM, …) left the tmp behind. A single try/finally with `force:true`\n // rmSync (no-op ENOENT after a successful rename consumed the file) covers\n // both.\n let fd: number | undefined;\n try {\n fd = openSync(tmp, 'w');\n writeSync(fd, content, 0, 'utf8');\n closeSync(fd);\n fd = undefined; // closed cleanly — don't re-close in finally\n renameSync(tmp, absPath);\n } finally {\n if (fd !== undefined) {\n try {\n closeSync(fd);\n } catch {\n /* best-effort: the rename/rm below are the meaningful cleanups */\n }\n }\n try {\n rmSync(tmp, { force: true });\n } catch {\n /* best-effort */\n }\n }\n return { path: absPath, mode: 'regenerate', written: true };\n}\n\n/** Re-emit a managed region, delegating to keystone-K's `writeManagedRegion`.\n * `regionText` MUST already include the begin/end markers (matches the shape\n * `writeManagedRegion` expects and that `IGNORE_BLOCK`/`CONTEXT_BLOCK`\n * callers build in core/cli). Use {@link buildRegion} to assemble it from a\n * block + body. */\nexport function managedBlock(\n absPath: string,\n block: ManagedBlock,\n regionText: string,\n): WriteOutcome {\n writeManagedRegion(absPath, block, regionText);\n return { path: absPath, mode: 'managedBlock', written: true };\n}\n\n/** Atomically (re)emit MULTIPLE managed regions into the SAME file in one\n * pass. Used when a co-owned target carries more than one managed block —\n * today only `CLAUDE.md` (CONTEXT + RULES).\n *\n * WHY this exists (I1): calling {@link managedBlock} twice on the same file is\n * NOT byte-idempotent. The 2nd call strips ONLY its own block, treats the 1st\n * region (and the `\\n\\n` separator) as user content, `trimEnd`s it, and\n * re-appends a fresh `\\n\\n` separator. Re-runs therefore accumulate ~2 leading\n * `\\n` bytes per init (verified: 158→168 over 5 runs). Doing both regions in a\n * SINGLE read → strip-all → append-all pass removes the interleaving: after\n * stripping BOTH blocks the only thing left is real user content, so re-runs\n * produce identical bytes.\n *\n * Strategy:\n * 1. Read the file (missing → empty).\n * 2. Strip EVERY named block (via core's `stripManagedBlock`, in the given\n * order) — what remains is user content + any managed blocks outside this\n * group.\n * 3. Append all `regionText`s in the GIVEN ORDER, joined by a single `\\n`.\n * Each `regionText` already ends with `\\n` (buildRegion appends the end\n * marker's trailing newline), so `\\n` between regions yields exactly one\n * blank-line separator (`END\\n` + `\\n` + `BEGIN`) — byte-identical to what\n * the single-block path emits on a first run, so the CONTEXT/RULES parity\n * gates against `claudeAdapter.emitContext/emitRules` keep passing.\n *\n * Single-region files (NOIR.md brief, ignore files) do NOT route through here\n * — the orchestrator only calls this for groups of ≥2 managed blocks, so\n * single-region byte-stability (delegated to keystone-K `writeManagedRegion`)\n * is unchanged. */\nexport function managedBlocks(\n absPath: string,\n regions: ReadonlyArray<{ block: ManagedBlock; regionText: string }>,\n): WriteOutcome {\n if (regions.length === 0) {\n throw new Error('managedBlocks requires at least one region');\n }\n if (regions.length === 1) {\n const only = regions[0];\n if (!only) throw new Error('managedBlocks: undefined region');\n return managedBlock(absPath, only.block, only.regionText);\n }\n let content = '';\n try {\n content = readFileSync(absPath, 'utf8');\n } catch {\n /* missing → treat as empty */\n }\n let stripped = content;\n for (const r of regions) {\n stripped = stripManagedBlock(stripped, r.block);\n }\n const regionsJoined = regions.map((r) => r.regionText).join('\\n');\n // Whitespace-only remainder (typical on re-run after both blocks are\n // stripped) → emit just the regions, no leading separator.\n const next =\n stripped.trim().length > 0 ? `${stripped.trimEnd()}\\n\\n${regionsJoined}` : regionsJoined;\n writeFileSync(absPath, next, 'utf8');\n return { path: absPath, mode: 'managedBlock', written: true };\n}\n\n/** Assemble `<begin>\\n<body>\\n<end>\\n` for a managed block. Centralized here so\n * every caller (manifest rendering, tests, future migrations) produces the\n * exact byte shape `writeManagedRegion` strips/expects. The trailing newline\n * is part of the contract — `stripManagedBlock`'s regex eats a trailing `\\n`\n * so re-runs stay idempotent instead of accumulating blank lines.\n *\n * `body` is `trimEnd()`-ed before wrapping so template authors can keep the\n * conventional trailing newline in `.tmpl` files without producing a\n * double-newline before the end marker. This keeps the output byte-identical\n * to `claudeAdapter.emitContext`/`emitRules` and core's `syncIgnores`, which\n * S-T2 relies on for a diff-free refactor. */\nexport function buildRegion(block: ManagedBlock, body: string): string {\n return `${block.begin}\\n${body.trimEnd()}\\n${block.end}\\n`;\n}\n\n/** Write `content` to `absPath` only if no file exists there. Returns whether\n * bytes were written. Parent dirs are NOT created (orchestrator's job). */\nexport function skipIfExists(absPath: string, content: string): WriteOutcome {\n if (existsSync(absPath)) {\n return { path: absPath, mode: 'skipIfExists', written: false };\n }\n writeFileSync(absPath, content, 'utf8');\n return { path: absPath, mode: 'skipIfExists', written: true };\n}\n","import { existsSync, readFileSync } from 'node:fs';\nimport { join } from 'node:path';\n\n/**\n * READ-ONLY stack detection. Probes for well-known marker files under `root`\n * and reports ONLY what is present — never assumes. The result feeds path\n * adaptation (where to drop `.claude/` vs `.cursor/` etc., future), ignore-file\n * selection, and the onboarding TUI's confirm step.\n *\n * Design rules (spec §4.5):\n * - Never throws. A foreign/empty dir returns `{ languages: [], monorepo:\n * false, frameworks: [], packageManager: null }`.\n * - Never opens network, never parses code beyond a `package.json`/`pyproject`\n * dependency list. Marker files + top-level manifests only.\n * - Frameworks are reported only when both the marker file is present AND the\n * framework's dependency is listed (avoids false positives from a stale\n * `package.json`).\n */\n\nexport interface StackInfo {\n /** Lower-cased language ids found via marker files:\n * `typescript` | `javascript` | `python` | `go` | `rust`. */\n languages: string[];\n /** True when a workspace manifest is present (pnpm-workspace, npm/yarn\n * workspaces in package.json, turbo.json, nx.json). */\n monorepo: boolean;\n /** Lower-cased framework ids (e.g. `next`, `vite`, `express`, `fastapi`,\n * `actix`). Empty when none detected. */\n frameworks: string[];\n /** `pnpm` | `npm` | `yarn` when a lockfile is present, else null. */\n packageManager: string | null;\n}\n\n/** Frameworks looked up against `package.json#dependencies`+`devDependencies`.\n * Keyed by the dependency name as published on npm. */\nconst NODE_FRAMEWORKS: ReadonlyArray<[dep: string, id: string]> = [\n ['next', 'next'],\n ['vite', 'vite'],\n ['express', 'express'],\n ['fastify', 'fastify'],\n ['nuxt', 'nuxt'],\n ['remix', 'remix'],\n ['@sveltejs/kit', 'sveltekit'],\n ['@angular/core', 'angular'],\n ['react', 'react'],\n ['vue', 'vue'],\n];\n\n/** Frameworks looked up against `[project] dependencies` /\n * `[project.optional-dependencies]` / `[tool.poetry.dependencies]` in\n * `pyproject.toml`. Keyed by the PyPI package name. */\nconst PYTHON_FRAMEWORKS: ReadonlyArray<[dep: string, id: string]> = [\n ['fastapi', 'fastapi'],\n ['flask', 'flask'],\n ['django', 'django'],\n ['sanic', 'sanic'],\n ['starlette', 'starlette'],\n ['tornado', 'tornado'],\n ['aiohttp', 'aiohttp'],\n ['bottle', 'bottle'],\n ['pyramid', 'pyramid'],\n ['falcon', 'falcon'],\n];\n\n/** Read+parse JSON without throwing; returns undefined on any error. JSON5/ESM\n * `package.json` with comments would land here too — `package.json` is plain\n * JSON in practice so a failed `JSON.parse` genuinely means \"not a node\n * project\" or \"broken file\", both of which we report as \"absent\". */\nfunction readJson<T = unknown>(file: string): T | undefined {\n try {\n return JSON.parse(readFileSync(file, 'utf8')) as T;\n } catch {\n return undefined;\n }\n}\n\ninterface PackageJson {\n dependencies?: Record<string, string>;\n devDependencies?: Record<string, string>;\n workspaces?: string[] | { packages?: string[] };\n packageManager?: string;\n}\n\nexport function detectStack(root: string): StackInfo {\n const languages = new Set<string>();\n const frameworks = new Set<string>();\n let monorepo = false;\n let packageManager: string | null = null;\n\n // Node / JS / TS — gated on package.json presence.\n const pjPath = join(root, 'package.json');\n if (existsSync(pjPath)) {\n const pj = readJson<PackageJson>(pjPath);\n if (pj) {\n const hasTs =\n Boolean(pj.devDependencies?.typescript) || existsSync(join(root, 'tsconfig.json'));\n languages.add(hasTs ? 'typescript' : 'javascript');\n\n const deps = new Set([\n ...Object.keys(pj.dependencies ?? {}),\n ...Object.keys(pj.devDependencies ?? {}),\n ]);\n for (const [dep, id] of NODE_FRAMEWORKS) {\n if (deps.has(dep)) frameworks.add(id);\n }\n\n const ws = pj.workspaces;\n if (\n Array.isArray(ws) ||\n (typeof ws === 'object' && ws !== null && Array.isArray(ws.packages))\n ) {\n monorepo = true;\n }\n if (typeof pj.packageManager === 'string' && pj.packageManager.length > 0) {\n // `packageManager: pnpm@10.12.4` → `pnpm`. Yarn/npm similarly.\n const m = pj.packageManager.split('@')[0];\n if (m) packageManager = m;\n }\n }\n }\n\n // Workspace manifests (these override/confirm monorepo independent of pj).\n if (existsSync(join(root, 'pnpm-workspace.yaml'))) {\n monorepo = true;\n packageManager = packageManager ?? 'pnpm';\n }\n if (existsSync(join(root, 'turbo.json')) || existsSync(join(root, 'nx.json'))) {\n monorepo = true;\n }\n\n // Lockfiles pin packageManager when `package.json#packageManager` didn't.\n if (!packageManager) {\n if (existsSync(join(root, 'pnpm-lock.yaml'))) packageManager = 'pnpm';\n else if (existsSync(join(root, 'yarn.lock'))) packageManager = 'yarn';\n else if (existsSync(join(root, 'package-lock.json'))) packageManager = 'npm';\n }\n\n // Python — pyproject.toml / requirements.txt / Pipfile / setup.py.\n if (\n existsSync(join(root, 'pyproject.toml')) ||\n existsSync(join(root, 'requirements.txt')) ||\n existsSync(join(root, 'Pipfile')) ||\n existsSync(join(root, 'setup.py'))\n ) {\n languages.add('python');\n // Framework detection scans pyproject.toml for PEP 508 dependency names\n // under `[project] dependencies` / `[project.optional-dependencies]` /\n // `[tool.poetry.dependencies]`. A full section-aware TOML parse is overkill\n // at v1 (and would need a dep we don't ship); the boundary-aware text scan\n // in `pyprojectHasDep` is robust to the three formats users actually write\n // and never throws on malformed TOML (degrades to \"no match\").\n if (existsSync(join(root, 'pyproject.toml'))) {\n const raw = safeRead(join(root, 'pyproject.toml'));\n if (raw) {\n for (const [dep, id] of PYTHON_FRAMEWORKS) {\n if (pyprojectHasDep(raw, dep)) frameworks.add(id);\n }\n }\n }\n }\n\n // Go — go.mod.\n if (existsSync(join(root, 'go.mod'))) {\n languages.add('go');\n packageManager = packageManager ?? 'go-modules';\n }\n\n // Rust — Cargo.toml.\n if (existsSync(join(root, 'Cargo.toml'))) {\n languages.add('rust');\n const raw = safeRead(join(root, 'Cargo.toml'));\n if (raw) {\n // M3: Cargo.toml deps are always `name = \"ver\"` or `name = { … }`, so\n // require the `=` after the crate name. The old `/^\\s*actix\\b/m` matched\n // `actix-web` (word boundary between `x` and `-`) and falsely reported\n // `actix`. Treat the hyphenated runtime (`actix-web`) as its OWN id and\n // gate bare `actix` on the equals form. Same equals-form tightening is\n // applied to `axum`/`rocket` so `axum-extra`-style crates don't trip the\n // same bug. The `/m` flag makes `^` match any line (the previous\n // `/^actix\\s*=/` had no `/m` and only matched a string starting with\n // `actix`, i.e. effectively never — dead code).\n if (/^\\s*actix-web\\b/m.test(raw)) frameworks.add('actix-web');\n if (/^\\s*actix\\s*=/m.test(raw)) frameworks.add('actix');\n if (/^\\s*axum\\s*=/m.test(raw)) frameworks.add('axum');\n if (/^\\s*rocket\\s*=/m.test(raw)) frameworks.add('rocket');\n }\n packageManager = packageManager ?? 'cargo';\n }\n\n return {\n languages: [...languages].sort(),\n monorepo,\n frameworks: [...frameworks].sort(),\n packageManager,\n };\n}\n\nfunction safeRead(file: string): string | undefined {\n try {\n return readFileSync(file, 'utf8');\n } catch {\n return undefined;\n }\n}\n\n/** True iff `dep` appears as a PEP 508 dependency name in `pyproject.toml`\n * text — handles PEP 621 `dependencies`/`optional-dependencies` list entries\n * (`\"fastapi\"`, `\"fastapi[all]>=0.100\"`) AND Poetry `[tool.poetry.dependencies]`\n * bare keys (`fastapi = \"^0.100\"`).\n *\n * The `before` boundary (line-start, quote, or whitespace) plus the `after`\n * PEP 508 boundary (whitespace, quote, version specifier `<>=!~`, extras `[`,\n * marker `;`, or end-of-string) prevent substring mismatches — `flask` will\n * NOT match `flask-restful`, and `fastapi` will NOT match `x-fastapi`.\n *\n * Never throws: `dep` is escaped, the regex is constructed from a controlled\n * template, and `String.prototype.test` is total on string input. Malformed\n * TOML simply yields no matches. */\nfunction pyprojectHasDep(raw: string, dep: string): boolean {\n const esc = dep.replace(/[.*+?^${}()|[\\]\\\\]/g, '\\\\$&');\n const before = `(?:^|[\"'\\\\s])`;\n const after = `(?=\\\\s|[\"'<>=!~;]|\\\\[|$)`;\n return new RegExp(`${before}${esc}${after}`, 'm').test(raw);\n}\n","/**\n * Hand-rolled `{{var}}` interpolation. No mustache/handlebars dependency —\n * Noir's markdown/yaml/json templates are simple enough that a tokenizer +\n * map lookup is sufficient and keeps `@noir-ai/create` dependency-light.\n *\n * Semantics (documented):\n * - Tokens are `{{ name }}` / `{{name}}` — any amount of ASCII whitespace\n * inside the braces is permitted. The name is trimmed.\n * - Known vars (value is a string) are substituted verbatim. NO escaping is\n * applied — callers must pre-escape for the target format (JSON/YAML/MD).\n * This is intentional: the engine renders into multiple formats and a\n * single universal escaper would be wrong for at least one of them.\n * - Unknown vars (not in `vars`, or value `undefined`) → the original token\n * is LEFT IN PLACE. Rationale: drift between template and ctx becomes\n * visible (an unrendered `{{projectId}}` in CLAUDE.md is immediately\n * obvious), rather than silently swallowed to empty. The spec called this\n * out as the preferred failure mode.\n * - Non-string var values are stringified via `String(value)` so a stray\n * number/boolean still interpolates rather than printing `[object Object]`.\n * - A lone `{{` with no closing `}}` is left untouched (not a token).\n */\n\nconst TOKEN = /\\{\\{\\s*([^}{}]+?)\\s*\\}\\}/g;\n\nexport function render(template: string, vars: Record<string, unknown>): string {\n return template.replace(TOKEN, (whole, name: string) => {\n if (!Object.hasOwn(vars, name)) return whole; // unknown → leave token\n const v = vars[name];\n if (v === undefined || v === null) return whole; // treat as unknown → leave token\n return String(v);\n });\n}\n","import { readFileSync } from 'node:fs';\nimport { dirname, join, resolve } from 'node:path';\nimport { fileURLToPath } from 'node:url';\n\n/**\n * Template loader. Templates ship at `<pkg>/templates/` (see `files` in\n * package.json) and are read at runtime so non-code changes (copy tweaks, new\n * pointers) don't require a rebuild.\n *\n * Path resolution must work identically in three layouts:\n * - source (vitest, tsx): this file at `packages/create/src/template-loader.ts`\n * → `../templates/<name>`.\n * - built (tsup): this file at `packages/create/dist/template-loader.js` →\n * `../templates/<name>` (same relative offset — `dist/` and `src/` are both\n * direct children of the package root, so `../templates` lands correctly).\n * - packed (npm tarball): `templates/` is included per `files`, dist/ layout\n * preserved → same as built.\n *\n * `NOIR_TEMPLATES_DIR` overrides everything (used by tests + downstream packs\n * that want to substitute their own template set without forking the engine).\n */\n\nconst DEFAULT_TEMPLATES_DIR = resolveTemplatesDir();\n\nfunction resolveTemplatesDir(): string {\n const override = process.env.NOIR_TEMPLATES_DIR;\n if (override && override.length > 0) return resolve(override);\n // `import.meta.url` is this module's URL. Going up one level reaches the\n // package root in both source and built layouts (see file header).\n const here = dirname(fileURLToPath(import.meta.url));\n return join(here, '..', 'templates');\n}\n\n/** Read a template's raw text by name (e.g. `noir.md.tmpl`). Throws on missing\n * — an unknown template is a manifest bug and should fail loudly, not render\n * an empty string silently. */\nexport function loadTemplate(name: string): string {\n return readFileSync(join(DEFAULT_TEMPLATES_DIR, name), 'utf8');\n}\n\n/** Resolve a template name to its absolute path (for diagnostics + tests). */\nexport function templatesDir(): string {\n return DEFAULT_TEMPLATES_DIR;\n}\n"],"mappings":"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","names":["join","i","j","existsSync","readFileSync","writeFileSync","join","join","existsSync","readFileSync","writeFileSync","existsSync","mkdirSync","readFileSync","renameSync","rmSync","basename","dirname","join","paths","mkdirSync","readFileSync","dirname","join","NOIR_DIR","existsSync","readFileSync","writeFileSync","dirname","join","managedBlock","join","NOIR_DIR","readFileSync","mkdirSync","dirname","existsSync","readFileSync","join","readFileSync","dirname","join","basename","dirname","mkdirSync","rmSync","paths","join","existsSync","managedBlock","readFileSync","renameSync"]}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@noir-ai/create",
3
- "version": "1.2.0-beta.2",
3
+ "version": "1.3.0-beta.2",
4
4
  "description": "Noir scaffold engine — the declarative three-mode writer, manifest, templates, stack-detect, and migrations that power `noir init`/`create`/`sync`.",
5
5
  "license": "MIT",
6
6
  "author": "agaaaptr",
@@ -46,8 +46,8 @@
46
46
  "templates"
47
47
  ],
48
48
  "dependencies": {
49
- "@noir-ai/core": "1.2.0-beta.2",
50
- "@noir-ai/adapters": "1.2.0-beta.2"
49
+ "@noir-ai/adapters": "1.3.0-beta.2",
50
+ "@noir-ai/core": "1.3.0-beta.2"
51
51
  },
52
52
  "devDependencies": {
53
53
  "@types/node": "^26.1.1"