@noir-ai/create 1.3.0-beta.6 → 1.4.0-beta.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/index.d.ts +109 -18
- package/dist/index.js +282 -31
- package/dist/index.js.map +1 -1
- package/package.json +4 -4
- package/templates/gitignore.tmpl +1 -0
package/dist/index.d.ts
CHANGED
|
@@ -8,7 +8,9 @@ declare function ancestorsPath(root: string): string;
|
|
|
8
8
|
declare function readAncestors(root: string): Record<string, string>;
|
|
9
9
|
/** Write the ancestor map. Ensures `.noir/` exists (a standalone caller may
|
|
10
10
|
* invoke this before the scaffold has created the dir). Derived state, so the
|
|
11
|
-
* plain write (no tmp) is fine.
|
|
11
|
+
* plain write (no tmp) is fine. Content-hash dedup — when the serialized
|
|
12
|
+
* bytes equal what is already on disk, the rewrite is skipped so a no-op sync
|
|
13
|
+
* leaves ancestors.json untouched (no mtime/git churn). */
|
|
12
14
|
declare function writeAncestors(root: string, map: Record<string, string>): void;
|
|
13
15
|
|
|
14
16
|
/**
|
|
@@ -116,7 +118,7 @@ declare function skipIfExists(absPath: string, content: string): WriteOutcome;
|
|
|
116
118
|
* entries + a {@link buildHostArtifacts} call that materializes per-host files
|
|
117
119
|
* (CLAUDE.md/GEMINI.md for claude/gemini; AGENTS.md + .cursor/.../opencode.json
|
|
118
120
|
* for agents-md/cursor/opencode) via the resolved adapter. The claude default
|
|
119
|
-
* `noir init` stays BYTE-IDENTICAL to v1.1 —
|
|
121
|
+
* `noir init` stays BYTE-IDENTICAL to v1.1 — REMOVED the additive
|
|
120
122
|
* root `AGENTS.md` (it was double-importing `.noir/NOIR.md` + RULES.md via
|
|
121
123
|
* CLAUDE.md's existing @-imports; claude's native surface is CLAUDE.md alone).
|
|
122
124
|
*
|
|
@@ -184,7 +186,7 @@ declare const MANIFEST_PATH_PARITY: ReadonlyArray<[
|
|
|
184
186
|
* S10 structure: the manifest is now `[...hostAgnosticEntries(ctx), ...hostSpecificEntries(ctx)]`
|
|
185
187
|
* where the host-specific half comes from {@link buildHostArtifacts} (driven by
|
|
186
188
|
* `resolveAdapter(ctx.host)`). The host-agnostic half is unchanged from v1.1
|
|
187
|
-
* (canonical `.noir/` store + ignore files).
|
|
189
|
+
* (canonical `.noir/` store + ignore files). {@link buildHostArtifacts}
|
|
188
190
|
* emits AGENTS.md ONLY for agents-md/cursor/opencode (claude/gemini use their
|
|
189
191
|
* own CLAUDE.md/GEMINI.md — emitting AGENTS.md too would double-import .noir/).
|
|
190
192
|
*
|
|
@@ -238,7 +240,7 @@ interface BuildHostArtifactsContext {
|
|
|
238
240
|
* gemini's in GEMINI.md, agents-md/cursor/opencode's in AGENTS.md — NO
|
|
239
241
|
* host emits a separate rules file. (The prior cursor
|
|
240
242
|
* `.cursor/rules/noir-contract.mdc` host-rules pointer was REMOVED: it
|
|
241
|
-
* collided with the
|
|
243
|
+
* collided with the cursor flat-skill prune of `noir-*.mdc` under
|
|
242
244
|
* `.cursor/rules/`, and cursor's rules are already delivered via
|
|
243
245
|
* AGENTS.md's `@.noir/rules/RULES.md` import.)
|
|
244
246
|
* 3. **Host MCP config** — `regenerate` at `adapter.mcpConfigPath(ctx)`
|
|
@@ -250,7 +252,7 @@ interface BuildHostArtifactsContext {
|
|
|
250
252
|
*
|
|
251
253
|
* Skills are OUT OF SCOPE here — the cli composes `emitSkillsToDir` with
|
|
252
254
|
* `adapter.skillsDir` + the host's `CompileTarget` (claude → `.claude/skills/`
|
|
253
|
-
* as SKILL.md; cursor → `.cursor/rules/<skill>.mdc` FLAT
|
|
255
|
+
* as SKILL.md; cursor → `.cursor/rules/<skill>.mdc` FLAT; gemini/
|
|
254
256
|
* agents-md/opencode have no skill dir → skip).
|
|
255
257
|
*/
|
|
256
258
|
declare function buildHostArtifacts(adapter: HostAdapter, ctx: BuildHostArtifactsContext): ManifestEntry[];
|
|
@@ -261,14 +263,51 @@ interface MergeResult {
|
|
|
261
263
|
/** True when ours and theirs diverged in the same region (markers emitted). */
|
|
262
264
|
conflict: boolean;
|
|
263
265
|
}
|
|
266
|
+
/**
|
|
267
|
+
* Unified line diff for the conflict resolver's stderr preview. Returns a
|
|
268
|
+
* sequence of {type:'eq'|'del'|'add', line} records, LCS-based, equivalent in
|
|
269
|
+
* shape to `git diff --no-color`. Pure (no IO) so it unit-tests cleanly and the
|
|
270
|
+
* resolver can render it through {@link packages/cli/src/theme.ts} (`+` green,
|
|
271
|
+
* `-` red, context dim). Reuses the SAME LCS machinery {@link mergeThreeWay}
|
|
272
|
+
* does so there is exactly one line-diff algorithm in the package.
|
|
273
|
+
*
|
|
274
|
+
* `context` (default 3) — number of unchanged lines to keep around each hunk
|
|
275
|
+
* (git-style). 0 → every non-equal line stands alone; large → whole-file blob.
|
|
276
|
+
* The resolver uses the default; tests pin to a specific shape.
|
|
277
|
+
*/
|
|
278
|
+
interface DiffLine {
|
|
279
|
+
type: 'eq' | 'del' | 'add';
|
|
280
|
+
line: string;
|
|
281
|
+
}
|
|
282
|
+
declare function lineDiff(base: string, head: string, context?: number): DiffLine[];
|
|
283
|
+
/**
|
|
284
|
+
* zdiff3-style conflict markers for an unresolved overlap. Shape:
|
|
285
|
+
* `<<<<<<< ours`
|
|
286
|
+
* <ours lines>
|
|
287
|
+
* `||||||| base`
|
|
288
|
+
* <base lines>
|
|
289
|
+
* `=======`
|
|
290
|
+
* <theirs lines>
|
|
291
|
+
* `>>>>>>> theirs`
|
|
292
|
+
*
|
|
293
|
+
* `diff3(zdiff3)` writes the SAME shape — included here so the engine + clack
|
|
294
|
+
* resolver have a single source of truth for marker layout. Returns the marked
|
|
295
|
+
* region body (no outer delimiters); the caller wraps it with surrounding
|
|
296
|
+
* context.
|
|
297
|
+
*/
|
|
298
|
+
declare function zdiff3Region(ours: readonly string[], base: readonly string[], theirs: readonly string[]): string[];
|
|
264
299
|
/**
|
|
265
300
|
* Three-way merge of `ours` (the user's current region) against `theirs` (the
|
|
266
301
|
* new template) with `base` (the last-emitted ancestor). Line-level diff3:
|
|
267
302
|
* changes in disjoint line ranges merge; overlapping changes become inline
|
|
268
|
-
* conflict markers
|
|
269
|
-
*
|
|
303
|
+
* conflict markers. Default marker shape is the minimal diff3 form
|
|
304
|
+
* (`<<<<<<< ours` / `=======` / `>>>>>>> theirs`) — byte-identical to v1.2 so
|
|
305
|
+
* {@link mergeManagedRegion} stays regression-anchored. Pass `'zdiff3'` to
|
|
306
|
+
* include the base section (`||||||| base`) — used by the conflict resolver's
|
|
307
|
+
* 6th "merge (with conflict markers)" option. Pure + deterministic
|
|
308
|
+
* (no IO) so it's unit-testable.
|
|
270
309
|
*/
|
|
271
|
-
declare function mergeThreeWay(base: string, ours: string, theirs: string): MergeResult;
|
|
310
|
+
declare function mergeThreeWay(base: string, ours: string, theirs: string, style?: 'minimal' | 'zdiff3'): MergeResult;
|
|
272
311
|
|
|
273
312
|
/**
|
|
274
313
|
* Migration model — Nx-style declarative upgrade scripts, hand-rolled.
|
|
@@ -408,15 +447,25 @@ interface ScaffoldOptions {
|
|
|
408
447
|
conflictPolicy?: 'overwrite' | 'preserve';
|
|
409
448
|
/** SP-C: per-file conflict resolver — the UI seam (the engine stays UI-free;
|
|
410
449
|
* the cli injects a @clack-based resolver). Called when a `regenerate`
|
|
411
|
-
* file exists and differs from the template.
|
|
412
|
-
|
|
413
|
-
|
|
414
|
-
|
|
415
|
-
|
|
416
|
-
*
|
|
417
|
-
*
|
|
418
|
-
*
|
|
450
|
+
* file exists and differs from the template. The return type widens to
|
|
451
|
+
* accept a rich shape carrying `applyToAll` — the engine then reuses the
|
|
452
|
+
* decision for the rest of the run (per artifact CLASS). */
|
|
453
|
+
onConflict?: (ctx: ConflictContext) => Promise<ConflictResolverReturn> | ConflictResolverReturn;
|
|
454
|
+
/** Three-way merge managed regions (base/ours/theirs) using a persisted
|
|
455
|
+
* ancestor snapshot (`.noir/ancestors.json`) instead of strip-replace, so a
|
|
456
|
+
* hand-edit inside a `<!-- noir:* -->` region survives a template update.
|
|
457
|
+
* DEFAULT TRUE: ancestor capture is unconditional, so the first merge
|
|
458
|
+
* run always has a base. Set to `false` (CLI `--no-merge-regions`) to
|
|
459
|
+
* restore strip-replace. Supports BOTH single-region (NOIR.md, ignores) AND
|
|
460
|
+
* multi-region (CLAUDE.md CONTEXT+RULES) managed files — the multi-region
|
|
461
|
+
* path shipped with SP-H (`managedBlocks` + `mergeManagedRegion` per block). */
|
|
419
462
|
mergeManagedRegions?: boolean;
|
|
463
|
+
/** Explicit interactivity signal. The engine reads THIS (not
|
|
464
|
+
* `process.env`), so a direct API/embedded caller in a TTY that does NOT
|
|
465
|
+
* inject {@link onConflict} never hits a @clack prompt. The CLI sets it from
|
|
466
|
+
* its global flags (via the `NOIR_NON_INTERACTIVE` bridge bin.ts owns);
|
|
467
|
+
* `process.env` remains only a fallback bridge for the CLI layer. */
|
|
468
|
+
interactive?: boolean;
|
|
420
469
|
}
|
|
421
470
|
interface ScaffoldResult {
|
|
422
471
|
/** Repo-relative paths actually written. */
|
|
@@ -441,6 +490,28 @@ interface ScaffoldResult {
|
|
|
441
490
|
toVersion: string;
|
|
442
491
|
/** The host actually emitted (post-default). */
|
|
443
492
|
host: HostTag;
|
|
493
|
+
/** Structured conflict report — one record per file that existed AND
|
|
494
|
+
* differed from the proposed bytes (regenerate conflict path). Populated in
|
|
495
|
+
* `--json`/`--no-input`/non-TTY runs (no prompt fires) so a CI/JSON caller
|
|
496
|
+
* can see exactly which files diverged + how they were resolved, without
|
|
497
|
+
* grepping stderr. Empty when no conflicts occurred (also on first-run +
|
|
498
|
+
* no-op). */
|
|
499
|
+
conflicts: ConflictRecord[];
|
|
500
|
+
}
|
|
501
|
+
/** One entry in {@link ScaffoldResult.conflicts}. */
|
|
502
|
+
interface ConflictRecord {
|
|
503
|
+
/** Repo-relative path of the conflicting file. */
|
|
504
|
+
path: string;
|
|
505
|
+
/** Artifact class — drives apply-to-all memory scope + report grouping. */
|
|
506
|
+
mode: ConflictContext['mode'];
|
|
507
|
+
/** LCS similarity (0-1) between existing and proposed. */
|
|
508
|
+
similarity?: number;
|
|
509
|
+
/** sha256 (hex, first 12 chars) of the on-disk bytes at conflict time. */
|
|
510
|
+
existingSha: string;
|
|
511
|
+
/** sha256 (hex, first 12 chars) of the proposed bytes. */
|
|
512
|
+
proposedSha: string;
|
|
513
|
+
/** Resolution the engine applied (`replace`/`preserve`/`rename`/…). */
|
|
514
|
+
resolution: ConflictResolution;
|
|
444
515
|
}
|
|
445
516
|
/** SP-C — context passed to {@link ScaffoldOptions.onConflict} when a
|
|
446
517
|
* `regenerate` file exists and differs from the template. */
|
|
@@ -451,9 +522,29 @@ interface ConflictContext {
|
|
|
451
522
|
existing: string;
|
|
452
523
|
/** The content the scaffold would write. */
|
|
453
524
|
proposed: string;
|
|
525
|
+
/** Artifact class — drives apply-to-all memory scope (`regenerate` shares
|
|
526
|
+
* one decision across a run; `managedBlock`/`managedBlocks` stay per-file).
|
|
527
|
+
* Defaults to `'regenerate'` for backward compatibility with SP-C resolvers
|
|
528
|
+
* that pre-date the field (the engine always sets it). */
|
|
529
|
+
mode?: 'regenerate' | 'managedBlock' | 'managedBlocks' | 'skipIfExists' | 'skill' | 'markdown' | 'artifact';
|
|
530
|
+
/** LCS similarity (0-1) between existing and proposed. Cheap signal for
|
|
531
|
+
* the resolver to bias toward `replace` when ~1.0 or `preserve` when ~0. */
|
|
532
|
+
similarity?: number;
|
|
533
|
+
/** When the 3-way merge of a managed region hit an overlap, the merged
|
|
534
|
+
* bytes WITH zdiff3 conflict markers (selecting `'merge'` writes this). */
|
|
535
|
+
mergedWithMarkers?: string;
|
|
454
536
|
}
|
|
455
537
|
/** SP-C — how to resolve a `regenerate` conflict. */
|
|
456
|
-
type ConflictResolution = 'replace' | 'preserve' | 'rename' | 'duplicate' | 'cancel';
|
|
538
|
+
type ConflictResolution = 'replace' | 'preserve' | 'rename' | 'duplicate' | 'cancel' | 'merge';
|
|
539
|
+
/** The resolver may return a bare {@link ConflictResolution} (scope
|
|
540
|
+
* `'one'`) OR a rich shape carrying `applyToAll`. The engine unwraps both; when
|
|
541
|
+
* `applyToAll` is true it stores the choice in its per-run memory keyed by
|
|
542
|
+
* artifact CLASS (`regenerate` shares one decision across a run; managedBlock
|
|
543
|
+
* stays per-file regardless — user edits there need individual review). */
|
|
544
|
+
type ConflictResolverReturn = ConflictResolution | {
|
|
545
|
+
resolution: ConflictResolution;
|
|
546
|
+
applyToAll?: boolean;
|
|
547
|
+
};
|
|
457
548
|
/**
|
|
458
549
|
* Refuse to scaffold when `root` is — or is inside — a `.noir/` directory.
|
|
459
550
|
* (SP-A) Running `noir init`/`create`/`sync` while cwd = `.noir/` would
|
|
@@ -531,4 +622,4 @@ declare function loadTemplate(name: string): string;
|
|
|
531
622
|
/** Resolve a template name to its absolute path (for diagnostics + tests). */
|
|
532
623
|
declare function templatesDir(): string;
|
|
533
624
|
|
|
534
|
-
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 };
|
|
625
|
+
export { BRIEF_BLOCK, type BuildHostArtifactsContext, type BuildManifestContext, CURRENT_SCAFFOLD_VERSION, type ConflictContext, type ConflictRecord, type ConflictResolution, type ConflictResolverReturn, type DiffLine, 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, lineDiff, loadTemplate, managedBlock, mergeThreeWay, readAncestors, readScaffoldVersion, regenerate, render, runMigrations, scaffold, scaffoldVersionPath, skipIfExists, templatesDir, writeAncestors, writeScaffoldVersion, zdiff3Region };
|
package/dist/index.js
CHANGED
|
@@ -1,3 +1,10 @@
|
|
|
1
|
+
var __require = /* @__PURE__ */ ((x) => typeof require !== "undefined" ? require : typeof Proxy !== "undefined" ? new Proxy(x, {
|
|
2
|
+
get: (a, b) => (typeof require !== "undefined" ? require : a)[b]
|
|
3
|
+
}) : x)(function(x) {
|
|
4
|
+
if (typeof require !== "undefined") return require.apply(this, arguments);
|
|
5
|
+
throw Error('Dynamic require of "' + x + '" is not supported');
|
|
6
|
+
});
|
|
7
|
+
|
|
1
8
|
// src/ancestors.ts
|
|
2
9
|
import { existsSync, mkdirSync, readFileSync, writeFileSync } from "fs";
|
|
3
10
|
import { dirname, join } from "path";
|
|
@@ -19,9 +26,15 @@ function readAncestors(root) {
|
|
|
19
26
|
}
|
|
20
27
|
}
|
|
21
28
|
function writeAncestors(root, map) {
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
29
|
+
const path = ancestorsPath(root);
|
|
30
|
+
const next = `${JSON.stringify(map, null, 2)}
|
|
31
|
+
`;
|
|
32
|
+
try {
|
|
33
|
+
if (readFileSync(path, "utf8") === next) return;
|
|
34
|
+
} catch {
|
|
35
|
+
}
|
|
36
|
+
mkdirSync(dirname(path), { recursive: true });
|
|
37
|
+
writeFileSync(path, next, "utf8");
|
|
25
38
|
}
|
|
26
39
|
|
|
27
40
|
// src/manifest.ts
|
|
@@ -253,8 +266,71 @@ function lcsMatch(a, b) {
|
|
|
253
266
|
}
|
|
254
267
|
return out;
|
|
255
268
|
}
|
|
269
|
+
function lineDiff(base, head, context = 3) {
|
|
270
|
+
const A = base.split("\n");
|
|
271
|
+
const B = head.split("\n");
|
|
272
|
+
const pairs = lcsMatch(A, B);
|
|
273
|
+
const inA = new Set(pairs.map((p) => p[0]));
|
|
274
|
+
const inB = new Set(pairs.map((p) => p[1]));
|
|
275
|
+
const out = [];
|
|
276
|
+
let i = 0;
|
|
277
|
+
let j = 0;
|
|
278
|
+
const bToH = /* @__PURE__ */ new Map();
|
|
279
|
+
for (const [bi, hi] of pairs) bToH.set(bi, hi);
|
|
280
|
+
while (i < A.length || j < B.length) {
|
|
281
|
+
const matched = bToH.get(i);
|
|
282
|
+
if (matched !== void 0 && matched === j) {
|
|
283
|
+
out.push({ type: "eq", line: A[i] ?? "" });
|
|
284
|
+
i++;
|
|
285
|
+
j++;
|
|
286
|
+
} else if (matched !== void 0) {
|
|
287
|
+
out.push({ type: "add", line: B[j] ?? "" });
|
|
288
|
+
j++;
|
|
289
|
+
} else if (i < A.length && !inA.has(i)) {
|
|
290
|
+
out.push({ type: "del", line: A[i] ?? "" });
|
|
291
|
+
i++;
|
|
292
|
+
} else if (j < B.length && !inB.has(j)) {
|
|
293
|
+
out.push({ type: "add", line: B[j] ?? "" });
|
|
294
|
+
j++;
|
|
295
|
+
} else {
|
|
296
|
+
if (i < A.length) i++;
|
|
297
|
+
else j++;
|
|
298
|
+
}
|
|
299
|
+
}
|
|
300
|
+
return context > 0 ? collapseContext(out, context) : out;
|
|
301
|
+
}
|
|
302
|
+
function collapseContext(lines, context) {
|
|
303
|
+
const out = [];
|
|
304
|
+
const keep = new Array(lines.length).fill(false);
|
|
305
|
+
for (let i = 0; i < lines.length; i++) {
|
|
306
|
+
const dl = lines[i];
|
|
307
|
+
if (dl && dl.type !== "eq") {
|
|
308
|
+
for (let k = Math.max(0, i - context); k <= Math.min(lines.length - 1, i + context); k++) {
|
|
309
|
+
keep[k] = true;
|
|
310
|
+
}
|
|
311
|
+
}
|
|
312
|
+
}
|
|
313
|
+
let inRun = false;
|
|
314
|
+
for (let i = 0; i < lines.length; i++) {
|
|
315
|
+
const dl = lines[i];
|
|
316
|
+
if (!dl) continue;
|
|
317
|
+
if (keep[i]) {
|
|
318
|
+
out.push(dl);
|
|
319
|
+
inRun = false;
|
|
320
|
+
} else {
|
|
321
|
+
if (!inRun) {
|
|
322
|
+
out.push({ type: "eq", line: "\u2026" });
|
|
323
|
+
inRun = true;
|
|
324
|
+
}
|
|
325
|
+
}
|
|
326
|
+
}
|
|
327
|
+
return out;
|
|
328
|
+
}
|
|
329
|
+
function zdiff3Region(ours, base, theirs) {
|
|
330
|
+
return ["<<<<<<< ours", ...ours, "||||||| base", ...base, "=======", ...theirs, ">>>>>>> theirs"];
|
|
331
|
+
}
|
|
256
332
|
var eq = (a, b) => a.length === b.length && a.every((v, k) => v === b[k]);
|
|
257
|
-
function mergeThreeWay(base, ours, theirs) {
|
|
333
|
+
function mergeThreeWay(base, ours, theirs, style = "minimal") {
|
|
258
334
|
const t = trivial(base, ours, theirs);
|
|
259
335
|
if (t) return t;
|
|
260
336
|
const B = base.split("\n");
|
|
@@ -287,7 +363,11 @@ function mergeThreeWay(base, ours, theirs) {
|
|
|
287
363
|
else if (eq(oSeg, tSeg)) out.push(...oSeg);
|
|
288
364
|
else {
|
|
289
365
|
conflict = true;
|
|
290
|
-
|
|
366
|
+
if (style === "zdiff3") {
|
|
367
|
+
out.push(...zdiff3Region(oSeg, baseSeg, tSeg));
|
|
368
|
+
} else {
|
|
369
|
+
out.push("<<<<<<< ours", ...oSeg, "=======", ...tSeg, ">>>>>>> theirs");
|
|
370
|
+
}
|
|
291
371
|
}
|
|
292
372
|
if (bBase < B.length) out.push(B[bBase] ?? "");
|
|
293
373
|
}
|
|
@@ -458,6 +538,30 @@ ${body.trimEnd()}
|
|
|
458
538
|
${block.end}
|
|
459
539
|
`;
|
|
460
540
|
}
|
|
541
|
+
function predictManagedBlock(currentContent, block, regionText) {
|
|
542
|
+
const stripped = stripManagedBlock(currentContent, block);
|
|
543
|
+
return stripped ? `${stripped.trimEnd()}
|
|
544
|
+
|
|
545
|
+
${regionText}` : regionText;
|
|
546
|
+
}
|
|
547
|
+
function predictManagedBlocks(currentContent, regions) {
|
|
548
|
+
if (regions.length === 0) {
|
|
549
|
+
throw new Error("predictManagedBlocks requires at least one region");
|
|
550
|
+
}
|
|
551
|
+
if (regions.length === 1) {
|
|
552
|
+
const only = regions[0];
|
|
553
|
+
if (!only) throw new Error("predictManagedBlocks: undefined region");
|
|
554
|
+
return predictManagedBlock(currentContent, only.block, only.regionText);
|
|
555
|
+
}
|
|
556
|
+
let stripped = currentContent;
|
|
557
|
+
for (const r of regions) {
|
|
558
|
+
stripped = stripManagedBlock(stripped, r.block);
|
|
559
|
+
}
|
|
560
|
+
const regionsJoined = regions.map((r) => r.regionText).join("\n");
|
|
561
|
+
return stripped.trim().length > 0 ? `${stripped.trimEnd()}
|
|
562
|
+
|
|
563
|
+
${regionsJoined}` : regionsJoined;
|
|
564
|
+
}
|
|
461
565
|
function skipIfExists(absPath, content) {
|
|
462
566
|
if (existsSync4(absPath)) {
|
|
463
567
|
return { path: absPath, mode: "skipIfExists", written: false };
|
|
@@ -684,11 +788,14 @@ async function scaffold(opts) {
|
|
|
684
788
|
}
|
|
685
789
|
const fromVersion = readScaffoldVersion(opts.root);
|
|
686
790
|
const stack = detectStack(opts.root);
|
|
687
|
-
const ancestors =
|
|
688
|
-
|
|
791
|
+
const ancestors = readAncestors(opts.root);
|
|
792
|
+
const mergeManagedRegions = opts.mergeManagedRegions !== false;
|
|
793
|
+
const hasIdentity = fromVersion !== null || idFile.state === "valid";
|
|
794
|
+
if (hasIdentity && opts.upgrade !== true && opts.force !== true && (opts.mode === "init" || opts.mode === "create")) {
|
|
689
795
|
if (opts.dryRun !== true) {
|
|
796
|
+
const where = fromVersion !== null ? `at scaffold ${fromVersion}` : "(pre-1.3.0 project; no scaffold-version stamp)";
|
|
690
797
|
process.stderr.write(
|
|
691
|
-
`Noir
|
|
798
|
+
`Noir already initialized in ${opts.root} ${where}; run \`noir init --upgrade\` to migrate.
|
|
692
799
|
`
|
|
693
800
|
);
|
|
694
801
|
}
|
|
@@ -696,6 +803,7 @@ async function scaffold(opts) {
|
|
|
696
803
|
written: [],
|
|
697
804
|
skipped: [],
|
|
698
805
|
identical: [],
|
|
806
|
+
conflicts: [],
|
|
699
807
|
noop: true,
|
|
700
808
|
migrationsRan: [],
|
|
701
809
|
migrationConflicts: [],
|
|
@@ -734,6 +842,18 @@ async function scaffold(opts) {
|
|
|
734
842
|
const written = [];
|
|
735
843
|
const skipped = [];
|
|
736
844
|
const identical = [];
|
|
845
|
+
const conflictMemory = /* @__PURE__ */ new Map();
|
|
846
|
+
const conflictRecords = [];
|
|
847
|
+
const recordConflict = (relPath, mode, existing, proposed, resolution, similarity2) => {
|
|
848
|
+
conflictRecords.push({
|
|
849
|
+
path: relPath,
|
|
850
|
+
mode,
|
|
851
|
+
similarity: similarity2,
|
|
852
|
+
existingSha: sha256Hex12(existing),
|
|
853
|
+
proposedSha: sha256Hex12(proposed),
|
|
854
|
+
resolution
|
|
855
|
+
});
|
|
856
|
+
};
|
|
737
857
|
const groups = groupApplicableByPath(manifest, host, emitRuntimeOnly);
|
|
738
858
|
for (const [relPath, entries] of groups) {
|
|
739
859
|
const abs = join8(opts.root, relPath);
|
|
@@ -744,18 +864,44 @@ async function scaffold(opts) {
|
|
|
744
864
|
continue;
|
|
745
865
|
}
|
|
746
866
|
mkdirSync3(dirname5(abs), { recursive: true });
|
|
867
|
+
const onDisk = readOptional(abs);
|
|
868
|
+
let hadMergeConflict = false;
|
|
747
869
|
const regions = managed.map((e) => {
|
|
748
870
|
const block = e.block;
|
|
749
871
|
if (!block) {
|
|
750
872
|
throw new Error(`manifest entry ${e.path}: managedBlock mode missing 'block'`);
|
|
751
873
|
}
|
|
752
874
|
const theirs = buildRegion(block, renderEntry(e, vars));
|
|
753
|
-
const
|
|
754
|
-
if (
|
|
755
|
-
|
|
875
|
+
const merged = mergeManagedRegions ? mergeManagedRegion(abs, e.path, block, theirs, ancestors) : { text: theirs, conflict: false, cleanTheirs: theirs };
|
|
876
|
+
if (merged.conflict) hadMergeConflict = true;
|
|
877
|
+
ancestors[`${e.path}::${block.begin}`] = theirs;
|
|
878
|
+
return { block, regionText: merged.text };
|
|
756
879
|
});
|
|
757
|
-
|
|
758
|
-
|
|
880
|
+
if (hadMergeConflict) {
|
|
881
|
+
const ctxMode = "managedBlocks";
|
|
882
|
+
const resolution = await resolveManagedConflict(relPath, ctxMode, opts);
|
|
883
|
+
if (resolution === "cancel") {
|
|
884
|
+
throw new Error(`scaffold cancelled by user at conflicting file ${relPath}`);
|
|
885
|
+
}
|
|
886
|
+
if (resolution !== void 0 && resolution !== "merge") {
|
|
887
|
+
process.stderr.write(
|
|
888
|
+
`noir: managed-region conflict in ${relPath} \u2014 wrote inline markers; resolve manually.
|
|
889
|
+
`
|
|
890
|
+
);
|
|
891
|
+
} else {
|
|
892
|
+
process.stderr.write(
|
|
893
|
+
`noir: managed-region conflict in ${relPath} \u2014 wrote inline markers; resolve manually.
|
|
894
|
+
`
|
|
895
|
+
);
|
|
896
|
+
}
|
|
897
|
+
}
|
|
898
|
+
const predicted = predictManagedBlocks(onDisk ?? "", regions);
|
|
899
|
+
if (onDisk !== void 0 && predicted === onDisk) {
|
|
900
|
+
identical.push(relPath);
|
|
901
|
+
} else {
|
|
902
|
+
managedBlocks(abs, regions);
|
|
903
|
+
written.push(relPath);
|
|
904
|
+
}
|
|
759
905
|
continue;
|
|
760
906
|
}
|
|
761
907
|
if (!opts.dryRun) mkdirSync3(dirname5(abs), { recursive: true });
|
|
@@ -767,7 +913,10 @@ async function scaffold(opts) {
|
|
|
767
913
|
}
|
|
768
914
|
const body = renderEntry(entry, vars);
|
|
769
915
|
if (entry.mode === "regenerate") {
|
|
770
|
-
const out = await writeRegenerateWithConflict(abs, entry.path, body, opts
|
|
916
|
+
const out = await writeRegenerateWithConflict(abs, entry.path, body, opts, {
|
|
917
|
+
memory: conflictMemory,
|
|
918
|
+
record: recordConflict
|
|
919
|
+
});
|
|
771
920
|
written.push(...out.written);
|
|
772
921
|
skipped.push(...out.skipped);
|
|
773
922
|
identical.push(...out.identical);
|
|
@@ -778,10 +927,41 @@ async function scaffold(opts) {
|
|
|
778
927
|
}
|
|
779
928
|
if (isNoirMdPath(entry.path)) healLegacyNoirMd(abs);
|
|
780
929
|
const theirs = buildRegion(block, body);
|
|
781
|
-
const
|
|
782
|
-
|
|
783
|
-
|
|
784
|
-
|
|
930
|
+
const merged = mergeManagedRegions ? mergeManagedRegion(abs, entry.path, block, theirs, ancestors) : { text: theirs, conflict: false, cleanTheirs: theirs };
|
|
931
|
+
const regionText = merged.text;
|
|
932
|
+
ancestors[`${entry.path}::${block.begin}`] = theirs;
|
|
933
|
+
if (merged.conflict) {
|
|
934
|
+
const ctxMode = "managedBlock";
|
|
935
|
+
const onDiskRegion = readOptional(abs);
|
|
936
|
+
const ctx = {
|
|
937
|
+
relPath: entry.path,
|
|
938
|
+
existing: onDiskRegion ?? "",
|
|
939
|
+
proposed: theirs,
|
|
940
|
+
mode: ctxMode,
|
|
941
|
+
similarity: similarity(onDiskRegion ?? "", merged.text),
|
|
942
|
+
mergedWithMarkers: merged.text
|
|
943
|
+
};
|
|
944
|
+
const resolution = await resolveManagedConflictCtx(ctx, opts);
|
|
945
|
+
if (resolution === "cancel") {
|
|
946
|
+
throw new Error(`scaffold cancelled by user at conflicting file ${entry.path}`);
|
|
947
|
+
}
|
|
948
|
+
if (resolution === "preserve") {
|
|
949
|
+
identical.push(entry.path);
|
|
950
|
+
continue;
|
|
951
|
+
}
|
|
952
|
+
process.stderr.write(
|
|
953
|
+
`noir: managed-region conflict in ${entry.path} \u2014 wrote inline markers; resolve manually.
|
|
954
|
+
`
|
|
955
|
+
);
|
|
956
|
+
}
|
|
957
|
+
const onDisk = readOptional(abs);
|
|
958
|
+
const predicted = predictManagedBlock(onDisk ?? "", block, regionText);
|
|
959
|
+
if (onDisk !== void 0 && predicted === onDisk) {
|
|
960
|
+
identical.push(entry.path);
|
|
961
|
+
} else {
|
|
962
|
+
managedBlock2(abs, block, regionText);
|
|
963
|
+
written.push(entry.path);
|
|
964
|
+
}
|
|
785
965
|
} else {
|
|
786
966
|
const out = skipIfExists(abs, body);
|
|
787
967
|
if (out.written) written.push(entry.path);
|
|
@@ -792,13 +972,14 @@ async function scaffold(opts) {
|
|
|
792
972
|
if ((opts.mode === "init" || opts.mode === "create") && !opts.dryRun) {
|
|
793
973
|
writeScaffoldVersion(opts.root, CURRENT_SCAFFOLD_VERSION);
|
|
794
974
|
}
|
|
795
|
-
if (
|
|
975
|
+
if (!opts.dryRun) {
|
|
796
976
|
writeAncestors(opts.root, ancestors);
|
|
797
977
|
}
|
|
798
978
|
return {
|
|
799
979
|
written,
|
|
800
980
|
skipped,
|
|
801
981
|
identical,
|
|
982
|
+
conflicts: conflictRecords,
|
|
802
983
|
noop: false,
|
|
803
984
|
migrationsRan,
|
|
804
985
|
migrationConflicts,
|
|
@@ -809,19 +990,57 @@ async function scaffold(opts) {
|
|
|
809
990
|
host
|
|
810
991
|
};
|
|
811
992
|
}
|
|
993
|
+
function sha256Hex12(s) {
|
|
994
|
+
const { createHash } = __require("crypto");
|
|
995
|
+
return createHash("sha256").update(s, "utf8").digest("hex").slice(0, 12);
|
|
996
|
+
}
|
|
997
|
+
function similarity(a, b) {
|
|
998
|
+
if (a === b) return 1;
|
|
999
|
+
const A = a.split("\n");
|
|
1000
|
+
const B = b.split("\n");
|
|
1001
|
+
if (A.length === 0 && B.length === 0) return 1;
|
|
1002
|
+
const n = A.length;
|
|
1003
|
+
const m = B.length;
|
|
1004
|
+
const dp = Array.from({ length: n + 1 }, () => new Array(m + 1).fill(0));
|
|
1005
|
+
for (let i = n - 1; i >= 0; i--) {
|
|
1006
|
+
const dpi = dp[i] ?? [];
|
|
1007
|
+
const dpi1 = dp[i + 1] ?? [];
|
|
1008
|
+
const ai = A[i] ?? "";
|
|
1009
|
+
for (let j = m - 1; j >= 0; j--) {
|
|
1010
|
+
const bj = B[j] ?? "";
|
|
1011
|
+
dpi[j] = ai === bj ? (dpi1[j + 1] ?? 0) + 1 : Math.max(dpi1[j] ?? 0, dpi[j + 1] ?? 0);
|
|
1012
|
+
}
|
|
1013
|
+
}
|
|
1014
|
+
const lcs = dp[0]?.[0] ?? 0;
|
|
1015
|
+
return n + m === 0 ? 1 : 2 * lcs / (n + m);
|
|
1016
|
+
}
|
|
812
1017
|
function mergeManagedRegion(abs, relPath, block, theirs, ancestors) {
|
|
813
1018
|
const base = ancestors[`${relPath}::${block.begin}`];
|
|
814
|
-
if (base === void 0) return theirs;
|
|
1019
|
+
if (base === void 0) return { text: theirs, conflict: false, cleanTheirs: theirs };
|
|
815
1020
|
const ours = readManagedBlock(abs, block);
|
|
816
|
-
if (ours === null) return theirs;
|
|
817
|
-
const
|
|
818
|
-
|
|
819
|
-
|
|
820
|
-
|
|
821
|
-
|
|
822
|
-
|
|
1021
|
+
if (ours === null) return { text: theirs, conflict: false, cleanTheirs: theirs };
|
|
1022
|
+
const oursNormalized = ours.endsWith("\n") ? ours : `${ours}
|
|
1023
|
+
`;
|
|
1024
|
+
const res = mergeThreeWay(base, oursNormalized, theirs, "zdiff3");
|
|
1025
|
+
return { text: res.merged, conflict: res.conflict, cleanTheirs: theirs };
|
|
1026
|
+
}
|
|
1027
|
+
async function resolveManagedConflictCtx(ctx, opts) {
|
|
1028
|
+
if (opts.onConflict === void 0) return void 0;
|
|
1029
|
+
const ret = await opts.onConflict(ctx);
|
|
1030
|
+
const resolution = typeof ret === "string" ? ret : ret.resolution;
|
|
1031
|
+
if (resolution === "rename" || resolution === "duplicate") {
|
|
1032
|
+
return "merge";
|
|
1033
|
+
}
|
|
1034
|
+
if (resolution === "replace") {
|
|
1035
|
+
return "replace";
|
|
823
1036
|
}
|
|
824
|
-
return
|
|
1037
|
+
return resolution;
|
|
1038
|
+
}
|
|
1039
|
+
async function resolveManagedConflict(relPath, mode, opts) {
|
|
1040
|
+
if (opts.onConflict === void 0) return void 0;
|
|
1041
|
+
const ret = await opts.onConflict({ relPath, existing: "", proposed: "", mode });
|
|
1042
|
+
const resolution = typeof ret === "string" ? ret : ret.resolution;
|
|
1043
|
+
return resolution;
|
|
825
1044
|
}
|
|
826
1045
|
function readProjectIdFile(root) {
|
|
827
1046
|
let raw;
|
|
@@ -841,7 +1060,7 @@ function resolveProjectId(opts, idFile) {
|
|
|
841
1060
|
}
|
|
842
1061
|
return createProjectId();
|
|
843
1062
|
}
|
|
844
|
-
async function writeRegenerateWithConflict(abs, relPath, proposed, opts) {
|
|
1063
|
+
async function writeRegenerateWithConflict(abs, relPath, proposed, opts, internals) {
|
|
845
1064
|
let existing;
|
|
846
1065
|
try {
|
|
847
1066
|
existing = readFileSync7(abs, "utf8");
|
|
@@ -855,11 +1074,31 @@ async function writeRegenerateWithConflict(abs, relPath, proposed, opts) {
|
|
|
855
1074
|
if (existing === proposed) {
|
|
856
1075
|
return { written: [], skipped: [], identical: [relPath] };
|
|
857
1076
|
}
|
|
858
|
-
const
|
|
1077
|
+
const MODE = "regenerate";
|
|
1078
|
+
const sim = similarity(existing, proposed);
|
|
1079
|
+
let resolution;
|
|
1080
|
+
const remembered = internals.memory.get(MODE);
|
|
1081
|
+
if (remembered !== void 0) {
|
|
1082
|
+
resolution = remembered;
|
|
1083
|
+
} else if (opts.onConflict !== void 0) {
|
|
1084
|
+
const ret = await opts.onConflict({ relPath, existing, proposed, mode: MODE, similarity: sim });
|
|
1085
|
+
const unwrapped = typeof ret === "string" ? ret : ret.resolution;
|
|
1086
|
+
if (typeof ret !== "string" && ret.applyToAll === true) {
|
|
1087
|
+
internals.memory.set(MODE, unwrapped);
|
|
1088
|
+
}
|
|
1089
|
+
resolution = unwrapped;
|
|
1090
|
+
} else {
|
|
1091
|
+
resolution = opts.conflictPolicy === "preserve" ? "preserve" : "replace";
|
|
1092
|
+
}
|
|
1093
|
+
internals.record(relPath, MODE, existing, proposed, resolution, sim);
|
|
859
1094
|
switch (resolution) {
|
|
860
1095
|
case "replace":
|
|
861
1096
|
regenerate(abs, proposed);
|
|
862
1097
|
return { written: [relPath], skipped: [], identical: [] };
|
|
1098
|
+
case "merge": {
|
|
1099
|
+
regenerate(abs, proposed);
|
|
1100
|
+
return { written: [relPath], skipped: [], identical: [] };
|
|
1101
|
+
}
|
|
863
1102
|
case "rename": {
|
|
864
1103
|
const aside = uniqueAside(abs, relPath, ".local");
|
|
865
1104
|
renameSync2(abs, aside.abs);
|
|
@@ -919,6 +1158,16 @@ function renderEntry(entry, vars) {
|
|
|
919
1158
|
if (entry.content !== void 0) return entry.content;
|
|
920
1159
|
throw new Error(`manifest entry ${entry.path}: must define 'content' or 'template'`);
|
|
921
1160
|
}
|
|
1161
|
+
function readOptional(absPath) {
|
|
1162
|
+
try {
|
|
1163
|
+
return readFileSync7(absPath, "utf8");
|
|
1164
|
+
} catch (err) {
|
|
1165
|
+
if (err instanceof Error && err.code === "ENOENT") {
|
|
1166
|
+
return void 0;
|
|
1167
|
+
}
|
|
1168
|
+
throw err;
|
|
1169
|
+
}
|
|
1170
|
+
}
|
|
922
1171
|
export {
|
|
923
1172
|
BRIEF_BLOCK,
|
|
924
1173
|
CURRENT_SCAFFOLD_VERSION,
|
|
@@ -932,6 +1181,7 @@ export {
|
|
|
932
1181
|
buildManifest,
|
|
933
1182
|
buildRegion,
|
|
934
1183
|
detectStack,
|
|
1184
|
+
lineDiff,
|
|
935
1185
|
loadTemplate,
|
|
936
1186
|
managedBlock2 as managedBlock,
|
|
937
1187
|
mergeThreeWay,
|
|
@@ -945,6 +1195,7 @@ export {
|
|
|
945
1195
|
skipIfExists,
|
|
946
1196
|
templatesDir,
|
|
947
1197
|
writeAncestors,
|
|
948
|
-
writeScaffoldVersion
|
|
1198
|
+
writeScaffoldVersion,
|
|
1199
|
+
zdiff3Region
|
|
949
1200
|
};
|
|
950
1201
|
//# sourceMappingURL=index.js.map
|