@noir-ai/create 1.19.2-beta.1 → 1.19.3-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 +71 -71
- package/dist/index.js +29 -29
- package/dist/index.js.map +1 -1
- package/package.json +3 -3
- package/templates/gitignore.tmpl +1 -0
- package/templates/noir-readme.md.tmpl +4 -3
package/dist/index.d.ts
CHANGED
|
@@ -9,13 +9,13 @@ declare function ancestorsPath(root: string): string;
|
|
|
9
9
|
declare function readAncestors(root: string): Record<string, string>;
|
|
10
10
|
/** Write the ancestor map. Ensures `.noir/` exists (a standalone caller may
|
|
11
11
|
* invoke this before the scaffold has created the dir). Derived state, so the
|
|
12
|
-
* plain write (no tmp) is fine. Content-hash dedup
|
|
12
|
+
* plain write (no tmp) is fine. Content-hash dedup: when the serialized
|
|
13
13
|
* bytes equal what is already on disk, the rewrite is skipped so a no-op sync
|
|
14
14
|
* leaves ancestors.json untouched (no mtime/git churn). */
|
|
15
15
|
declare function writeAncestors(root: string, map: Record<string, string>): void;
|
|
16
16
|
|
|
17
17
|
/**
|
|
18
|
-
* Emit a host's lifecycle-hook config on demand
|
|
18
|
+
* Emit a host's lifecycle-hook config on demand: the write-once `mergeJson`
|
|
19
19
|
* hook the scaffold deliberately does NOT re-emit on `noir init --upgrade` or
|
|
20
20
|
* `noir sync`. This is the explicit restore path (`noir hooks sync`): it runs
|
|
21
21
|
* the same `adapter.emitHooks` seam the scaffold uses and dispatches each entry
|
|
@@ -25,7 +25,7 @@ declare function writeAncestors(root: string, map: Record<string, string>): void
|
|
|
25
25
|
* command instead of being left as-is; the user's other keys and non-Noir hooks
|
|
26
26
|
* are never touched.
|
|
27
27
|
*
|
|
28
|
-
* Pure filesystem work
|
|
28
|
+
* Pure filesystem work: no daemon, no store, no prompt. A host with no hook
|
|
29
29
|
* surface (agents-md, opencode) returns an empty outcome list.
|
|
30
30
|
*/
|
|
31
31
|
/** One host hook-config write performed by {@link emitHooksToFile}. */
|
|
@@ -47,7 +47,7 @@ declare function emitHooksToFile(root: string, host: HostId, opts?: {
|
|
|
47
47
|
|
|
48
48
|
/**
|
|
49
49
|
* READ-ONLY stack detection. Probes for well-known marker files under `root`
|
|
50
|
-
* and reports ONLY what is present
|
|
50
|
+
* and reports ONLY what is present: never assumes. The result feeds path
|
|
51
51
|
* adaptation (where to drop `.claude/` vs `.cursor/` etc., future), ignore-file
|
|
52
52
|
* selection, and the onboarding TUI's confirm step.
|
|
53
53
|
*
|
|
@@ -85,7 +85,7 @@ interface StackInfo {
|
|
|
85
85
|
ci: string | null;
|
|
86
86
|
/**
|
|
87
87
|
* Existing AI instruction files found under root.
|
|
88
|
-
* Never clobbered
|
|
88
|
+
* Never clobbered: the scaffold confirms the write strategy (skip/add-section/
|
|
89
89
|
* standalone).
|
|
90
90
|
*/
|
|
91
91
|
existingAiFiles: {
|
|
@@ -233,19 +233,19 @@ declare const SEED_TEMPLATE_HISTORY: readonly SeedTemplateHistoryEntry[];
|
|
|
233
233
|
declare function isStaleSeed(seed: SeedKind, fileBytes: string, currentRender: string): boolean;
|
|
234
234
|
|
|
235
235
|
/**
|
|
236
|
-
* The three-mode writer
|
|
236
|
+
* The three-mode writer: generalizes the earlier `writeManagedRegion` into
|
|
237
237
|
* the declarative dispatch the scaffold manifest drives. Each mode maps 1:1 to
|
|
238
238
|
* an artifact class in the artifact matrix:
|
|
239
239
|
*
|
|
240
|
-
* - {@link regenerate}
|
|
240
|
+
* - {@link regenerate} : pure pointers (`.mcp.json`, `NOIR.md` brief, …).
|
|
241
241
|
* Always overwritten, atomically.
|
|
242
|
-
* - {@link managedBlock}
|
|
242
|
+
* - {@link managedBlock} : co-owned files (`CLAUDE.md` context/rules,
|
|
243
243
|
* `.gitignore` noir block, …). DELEGATES to
|
|
244
244
|
* core's `writeManagedRegion` so user content
|
|
245
245
|
* outside the markers is preserved byte-for-byte and
|
|
246
246
|
* re-runs are idempotent. Never duplicate the
|
|
247
247
|
* managed-region logic.
|
|
248
|
-
* - {@link skipIfExists}
|
|
248
|
+
* - {@link skipIfExists} : user-owned seeds (`RULES.md`, `config.yml`,
|
|
249
249
|
* `project.id`). Write once; never clobber.
|
|
250
250
|
*
|
|
251
251
|
* The orchestrator (`scaffold.ts`) is the only intended caller; the per-mode
|
|
@@ -258,7 +258,7 @@ interface WriteOutcome {
|
|
|
258
258
|
path: string;
|
|
259
259
|
mode: WriteMode;
|
|
260
260
|
/** true when bytes hit disk; false for skipIfExists no-ops. regenerate and
|
|
261
|
-
* managedBlock always write (managedBlock may write identical bytes
|
|
261
|
+
* managedBlock always write (managedBlock may write identical bytes: that
|
|
262
262
|
* still counts as a write for telemetry purposes; the file IS up to date). */
|
|
263
263
|
written: boolean;
|
|
264
264
|
}
|
|
@@ -267,7 +267,7 @@ interface WriteOutcome {
|
|
|
267
267
|
* file (the pointer files this is used for are read by the host agent first —
|
|
268
268
|
* a truncated CLAUDE.md/.mcp.json would break the very startup Noir serves).
|
|
269
269
|
*
|
|
270
|
-
* Parent directories are NOT created here
|
|
270
|
+
* Parent directories are NOT created here: the orchestrator does that once
|
|
271
271
|
* for the whole manifest so a missing dir is a single, attributable failure
|
|
272
272
|
* rather than N silent ones inside the writer.
|
|
273
273
|
*/
|
|
@@ -281,7 +281,7 @@ declare function managedBlock(absPath: string, block: ManagedBlock, regionText:
|
|
|
281
281
|
/** Assemble `<begin>\n<body>\n<end>\n` for a managed block. Centralized here so
|
|
282
282
|
* every caller (manifest rendering, tests, future migrations) produces the
|
|
283
283
|
* exact byte shape `writeManagedRegion` strips/expects. The trailing newline
|
|
284
|
-
* is part of the contract
|
|
284
|
+
* is part of the contract: `stripManagedBlock`'s regex eats a trailing `\n`
|
|
285
285
|
* so re-runs stay idempotent instead of accumulating blank lines.
|
|
286
286
|
*
|
|
287
287
|
* `body` is `trimEnd()`-ed before wrapping so template authors can keep the
|
|
@@ -298,7 +298,7 @@ declare function buildRegion(block: ManagedBlock, body: string): string;
|
|
|
298
298
|
* BEFORE the rename (see `AtomicWriteOptions.mode`), so the file never exists
|
|
299
299
|
* at its final path with a laxer mode. It is deliberately ignored when the
|
|
300
300
|
* target already exists: this writer's whole contract is "never touch an
|
|
301
|
-
* existing file", and that includes its permissions. POSIX-only
|
|
301
|
+
* existing file", and that includes its permissions. POSIX-only: Windows
|
|
302
302
|
* permissions are ACL-based and the mode is dropped there.
|
|
303
303
|
*
|
|
304
304
|
* The write goes through `atomicWriteFile` (tmp + rename) rather than a bare
|
|
@@ -312,11 +312,11 @@ declare function buildRegion(block: ManagedBlock, body: string): string;
|
|
|
312
312
|
* emit, which tightens a group/world-readable credential to 0600. */
|
|
313
313
|
declare function skipIfExists(absPath: string, content: string, fileMode?: number): WriteOutcome;
|
|
314
314
|
/** What happened when the owner-only mode was re-asserted on a credential
|
|
315
|
-
* file
|
|
315
|
+
* file: see {@link ensureOwnerOnly}. */
|
|
316
316
|
type EnvMode = FileModeOutcome;
|
|
317
317
|
|
|
318
318
|
/**
|
|
319
|
-
* Declarative scaffold manifest
|
|
319
|
+
* Declarative scaffold manifest: the single source of truth for what
|
|
320
320
|
* `init` / `create` / `sync` emit. Each entry is one artifact, tagged with its
|
|
321
321
|
* write {@link WriteMode} so the orchestrator can dispatch without knowing
|
|
322
322
|
* what's inside.
|
|
@@ -330,7 +330,7 @@ type EnvMode = FileModeOutcome;
|
|
|
330
330
|
* per-host files
|
|
331
331
|
* (CLAUDE.md/GEMINI.md for claude/gemini; AGENTS.md + .cursor/.../opencode.json
|
|
332
332
|
* for agents-md/cursor/opencode) via the resolved adapter. The claude default
|
|
333
|
-
* `noir init` stays BYTE-IDENTICAL to v1.1
|
|
333
|
+
* `noir init` stays BYTE-IDENTICAL to v1.1: REMOVED the additive
|
|
334
334
|
* root `AGENTS.md` (it was double-importing `.noir/NOIR.md` + RULES.md via
|
|
335
335
|
* CLAUDE.md's existing @-imports; claude's native surface is CLAUDE.md alone).
|
|
336
336
|
*
|
|
@@ -348,7 +348,7 @@ interface ManifestEntry {
|
|
|
348
348
|
/** Repo-relative POSIX path (forward slashes). Orchestrator joins with root. */
|
|
349
349
|
path: string;
|
|
350
350
|
mode: WriteMode;
|
|
351
|
-
/** Opt-in flag for a DOC-ONLY seed
|
|
351
|
+
/** Opt-in flag for a DOC-ONLY seed: a file whose only job is to be read by a
|
|
352
352
|
* human. On an upgrade, a seed still byte-identical to what an older Noir
|
|
353
353
|
* shipped is refreshed in place rather than staying frozen at that older
|
|
354
354
|
* text forever; one the user edited is never refreshed (it goes to the
|
|
@@ -356,14 +356,14 @@ interface ManifestEntry {
|
|
|
356
356
|
*
|
|
357
357
|
* Honored on the upgrade emit only. A fresh `init`/`create` either creates
|
|
358
358
|
* the file or leaves it alone, and `sync` does not emit seeds at all, so
|
|
359
|
-
* neither can observe the flag
|
|
359
|
+
* neither can observe the flag: the behavior change is confined to the one
|
|
360
360
|
* command a user runs to bring an initialized project up to date.
|
|
361
361
|
*
|
|
362
362
|
* DOC-ONLY SEEDS ONLY. A file the user owns the contents of must never carry
|
|
363
363
|
* this flag: `.noir/.env`, `.noir/config.yml` and `.noir/project.id` are
|
|
364
364
|
* theirs to shape, and an upgrade that rewrote one of them would destroy
|
|
365
365
|
* exactly the work this engine exists to protect. Co-owned files (managed
|
|
366
|
-
* blocks) must not set it either
|
|
366
|
+
* blocks) must not set it either: they have a three-way merge that
|
|
367
367
|
* preserves user edits, which is the better tool for a shared file.
|
|
368
368
|
*
|
|
369
369
|
* Which recorded seed the bytes are compared against is declared separately
|
|
@@ -375,7 +375,7 @@ interface ManifestEntry {
|
|
|
375
375
|
/** Required for `managedBlock` mode: the named block to re-emit. */
|
|
376
376
|
block?: ManagedBlock;
|
|
377
377
|
/** Literal content (`regenerate`/`skipIfExists`) or literal region BODY
|
|
378
|
-
* (`managedBlock
|
|
378
|
+
* (`managedBlock`: the orchestrator wraps it with the block markers).
|
|
379
379
|
* Mutually exclusive with {@link template}. */
|
|
380
380
|
content?: string;
|
|
381
381
|
/** Template name (resolved by `template-loader`) for content/body.
|
|
@@ -384,7 +384,7 @@ interface ManifestEntry {
|
|
|
384
384
|
/** One-line human description for `noir doctor` + logs. */
|
|
385
385
|
description?: string;
|
|
386
386
|
/** Permission for a NEWLY created file (e.g. `0o600` for a credential seed).
|
|
387
|
-
* Applied only on creation
|
|
387
|
+
* Applied only on creation: the `skipIfExists` contract means an existing
|
|
388
388
|
* file is not even opened. The orchestrator forwards this to the writer and
|
|
389
389
|
* reports it on the result as {@link ScaffoldResult.fileModes}; the mode of
|
|
390
390
|
* an existing credential file is re-asserted separately (`ensureOwnerOnly`).
|
|
@@ -395,7 +395,7 @@ interface ManifestEntry {
|
|
|
395
395
|
/** Required for `mergeJson` mode: the JSON patch object (or a template that
|
|
396
396
|
* renders to one). Merged into the existing file, preserving user keys. */
|
|
397
397
|
patch?: Record<string, unknown>;
|
|
398
|
-
/** Required for `mergeJson` mode: dedup substring for `hooks.*` entries
|
|
398
|
+
/** Required for `mergeJson` mode: dedup substring for `hooks.*` entries: an
|
|
399
399
|
* existing entry whose command contains this is NOT re-added. */
|
|
400
400
|
dedupSubstring?: string;
|
|
401
401
|
}
|
|
@@ -418,7 +418,7 @@ type BuildManifestContext = {
|
|
|
418
418
|
* MCP clients that don't read shell profiles can spawn the server. See
|
|
419
419
|
* `resolveNoirCommand()` in @noir-ai/core. */
|
|
420
420
|
command: string;
|
|
421
|
-
/** Detected stack
|
|
421
|
+
/** Detected stack: drives stack-aware ignore emission (.npmignore /
|
|
422
422
|
* .prettierignore only for JS; .dockerignore only when a Dockerfile is
|
|
423
423
|
* present; an unknown/empty stack ⇒ all four, for backward compat). */
|
|
424
424
|
stack?: StackInfo;
|
|
@@ -429,7 +429,7 @@ type BuildManifestContext = {
|
|
|
429
429
|
* drops the working-rules seed from the manifest, so no command emits or
|
|
430
430
|
* backfills `.noir/rules/RULES.md` for a project that opted out. Undefined
|
|
431
431
|
* (the schema's default state, and every caller that predates the switch)
|
|
432
|
-
* means enabled
|
|
432
|
+
* means enabled: a project with no `rules:` block emits the seed exactly
|
|
433
433
|
* as before. A file already on disk is never removed: dropping the entry
|
|
434
434
|
* removes the emission, not the file. */
|
|
435
435
|
rulesEnabled?: boolean;
|
|
@@ -453,11 +453,11 @@ declare const MANIFEST_PATH_PARITY: ReadonlyArray<[
|
|
|
453
453
|
* `resolveAdapter(ctx.host)`). The host-agnostic half is unchanged from v1.1
|
|
454
454
|
* (canonical `.noir/` store + ignore files). {@link buildHostArtifacts}
|
|
455
455
|
* emits AGENTS.md ONLY for agents-md/cursor/opencode (claude/gemini use their
|
|
456
|
-
* own CLAUDE.md/GEMINI.md
|
|
456
|
+
* own CLAUDE.md/GEMINI.md: emitting AGENTS.md too would double-import .noir/).
|
|
457
457
|
*
|
|
458
458
|
* Mode-tagging rationale per artifact:
|
|
459
459
|
* - `project.id` → skipIfExists. First init writes a fresh id; re-init MUST
|
|
460
|
-
* NOT overwrite
|
|
460
|
+
* NOT overwrite: that would orphan the indexed store DB named after it.
|
|
461
461
|
* - `config.yml` → skipIfExists. User-owned; the seed is written once.
|
|
462
462
|
* (The seed renders `host: {{host}}` so a `--host gemini` init persists the
|
|
463
463
|
* chosen host for `noir sync` to read back.)
|
|
@@ -470,7 +470,7 @@ declare const MANIFEST_PATH_PARITY: ReadonlyArray<[
|
|
|
470
470
|
*/
|
|
471
471
|
declare function buildManifest(ctx: BuildManifestContext): ManifestEntry[];
|
|
472
472
|
/** Context shape passed to {@link buildHostArtifacts}. A strict subset of
|
|
473
|
-
* {@link BuildManifestContext} (no `projectId`/`host
|
|
473
|
+
* {@link BuildManifestContext} (no `projectId`/`host`: the adapter IS the
|
|
474
474
|
* resolved host, and host artifacts never need the project id). Exported
|
|
475
475
|
* separately so callers + tests can name the narrower contract. */
|
|
476
476
|
interface BuildHostArtifactsContext {
|
|
@@ -490,10 +490,10 @@ interface BuildHostArtifactsContext {
|
|
|
490
490
|
}
|
|
491
491
|
/**
|
|
492
492
|
* Materialize the host-specific manifest entries from a resolved adapter.
|
|
493
|
-
* SINGLE entry point
|
|
493
|
+
* SINGLE entry point: no scattered `if (host === '…')` conditionals in the
|
|
494
494
|
* orchestrator. Returns entries in emission order:
|
|
495
495
|
*
|
|
496
|
-
* 1. **AGENTS.md** (universal baseline)
|
|
496
|
+
* 1. **AGENTS.md** (universal baseline): `regenerate` at
|
|
497
497
|
* `adapter.agentsMdPath(ctx)` (default `<root>/AGENTS.md`), content from
|
|
498
498
|
* the shared `emitAgentsMd(ctx)` helper. Emitted ONLY for hosts whose
|
|
499
499
|
* `emitContext` IS the AGENTS.md content (agents-md, cursor, opencode) —
|
|
@@ -503,10 +503,10 @@ interface BuildHostArtifactsContext {
|
|
|
503
503
|
* `@`-imports the canonical `.noir/` sources; emitting AGENTS.md too
|
|
504
504
|
* would IMPORT THOSE FILES TWICE into the host's context (2× tokens +
|
|
505
505
|
* drift risk), so for those two hosts AGENTS.md is SKIPPED. (Claude Code
|
|
506
|
-
* still discovers AGENTS.md at the repo root when present
|
|
506
|
+
* still discovers AGENTS.md at the repo root when present: users who
|
|
507
507
|
* want the universal file can drop one in by hand; Noir's auto-emission
|
|
508
508
|
* stays single-source per host.)
|
|
509
|
-
* 2. **Host-native context file
|
|
509
|
+
* 2. **Host-native context file**: emitted ONLY for hosts whose `emitContext`
|
|
510
510
|
* is NOT the AGENTS.md content (i.e. the host has its OWN context file
|
|
511
511
|
* with a distinct syntax). Concretely: claude → `CLAUDE.md` (CONTEXT +
|
|
512
512
|
* RULES managed blocks, byte-identical to v1.1 via templates); gemini →
|
|
@@ -514,7 +514,7 @@ interface BuildHostArtifactsContext {
|
|
|
514
514
|
* import syntax). For `agents-md`/`cursor`/`opencode` the context IS the
|
|
515
515
|
* AGENTS.md (already emitted in step 1) → SKIP to avoid a duplicate.
|
|
516
516
|
* Rules live INSIDE the host's context file: claude's in CLAUDE.md,
|
|
517
|
-
* gemini's in GEMINI.md, agents-md/cursor/opencode's in AGENTS.md
|
|
517
|
+
* gemini's in GEMINI.md, agents-md/cursor/opencode's in AGENTS.md: NO
|
|
518
518
|
* host emits a separate rules file. (The prior cursor
|
|
519
519
|
* `.cursor/rules/noir-contract.mdc` host-rules pointer was REMOVED: it
|
|
520
520
|
* collided with the cursor flat-skill prune of `noir-*.mdc` under
|
|
@@ -524,20 +524,20 @@ interface BuildHostArtifactsContext {
|
|
|
524
524
|
* That rules import/block is dropped for every host when
|
|
525
525
|
* `ctx.rulesEnabled` is false: the switch withholds the seed, so pointing
|
|
526
526
|
* a host at it would resolve to nothing. The context import is unaffected
|
|
527
|
-
*
|
|
527
|
+
* : the switch gates the rules, not the brief. A host file that already
|
|
528
528
|
* carries the rules region from when the switch was on keeps it: dropping
|
|
529
529
|
* the entry stops the emission, and nothing here rewrites a region out of
|
|
530
530
|
* an existing file.
|
|
531
|
-
* 3. **Host MCP config
|
|
531
|
+
* 3. **Host MCP config**: `regenerate` at `adapter.mcpConfigPath(ctx)`
|
|
532
532
|
* (default `<root>/.mcp.json` for claude), content from
|
|
533
533
|
* `adapter.emitMcpConfig(ctx, {transport,url})`. Claude KEEPS the template
|
|
534
534
|
* path (byte-identical parity with v1.1 + the .mcp.json parity test that
|
|
535
535
|
* compares against `claudeAdapter.emitMcpConfig`) for a repo that has not
|
|
536
|
-
* joined a workspace; a joined repo
|
|
537
|
-
* marker
|
|
536
|
+
* joined a workspace; a joined repo: one carrying a `.noir/workspace.json`
|
|
537
|
+
* marker: gets the workspace entry instead, on every host, whatever
|
|
538
538
|
* transport the run asked for (see the entry's own comment below).
|
|
539
539
|
*
|
|
540
|
-
* Skills are OUT OF SCOPE here
|
|
540
|
+
* Skills are OUT OF SCOPE here: the cli composes `emitSkillsToDir` with
|
|
541
541
|
* `adapter.skillsDir` + the host's `CompileTarget` (claude → `.claude/skills/`
|
|
542
542
|
* as SKILL.md; cursor → `.cursor/rules/<skill>.mdc` FLAT; gemini/
|
|
543
543
|
* agents-md/opencode have no skill dir → skip).
|
|
@@ -558,7 +558,7 @@ interface MergeResult {
|
|
|
558
558
|
* `-` red, context dim). Reuses the SAME LCS machinery {@link mergeThreeWay}
|
|
559
559
|
* does so there is exactly one line-diff algorithm in the package.
|
|
560
560
|
*
|
|
561
|
-
* `context` (default 3)
|
|
561
|
+
* `context` (default 3): number of unchanged lines to keep around each hunk
|
|
562
562
|
* (git-style). 0 → every non-equal line stands alone; large → whole-file blob.
|
|
563
563
|
* The resolver uses the default; tests pin to a specific shape.
|
|
564
564
|
*/
|
|
@@ -577,7 +577,7 @@ declare function lineDiff(base: string, head: string, context?: number): DiffLin
|
|
|
577
577
|
* <theirs lines>
|
|
578
578
|
* `>>>>>>> theirs`
|
|
579
579
|
*
|
|
580
|
-
* `diff3(zdiff3)` writes the SAME shape
|
|
580
|
+
* `diff3(zdiff3)` writes the SAME shape: included here so the engine + clack
|
|
581
581
|
* resolver have a single source of truth for marker layout. Returns the marked
|
|
582
582
|
* region body (no outer delimiters); the caller wraps it with surrounding
|
|
583
583
|
* context.
|
|
@@ -588,22 +588,22 @@ declare function zdiff3Region(ours: readonly string[], base: readonly string[],
|
|
|
588
588
|
* new template) with `base` (the last-emitted ancestor). Line-level diff3:
|
|
589
589
|
* changes in disjoint line ranges merge; overlapping changes become inline
|
|
590
590
|
* conflict markers. Default marker shape is the minimal diff3 form
|
|
591
|
-
* (`<<<<<<< ours` / `=======` / `>>>>>>> theirs`)
|
|
591
|
+
* (`<<<<<<< ours` / `=======` / `>>>>>>> theirs`): byte-identical to v1.2 so
|
|
592
592
|
* {@link mergeManagedRegion} stays regression-anchored. Pass `'zdiff3'` to
|
|
593
|
-
* include the base section (`||||||| base`)
|
|
593
|
+
* include the base section (`||||||| base`): used by the conflict resolver's
|
|
594
594
|
* 6th "merge (with conflict markers)" option. Pure + deterministic
|
|
595
595
|
* (no IO) so it's unit-testable.
|
|
596
596
|
*/
|
|
597
597
|
declare function mergeThreeWay(base: string, ours: string, theirs: string, style?: 'minimal' | 'zdiff3'): MergeResult;
|
|
598
598
|
|
|
599
599
|
/**
|
|
600
|
-
* Migration model
|
|
600
|
+
* Migration model: Nx-style declarative upgrade scripts, hand-rolled.
|
|
601
601
|
*
|
|
602
602
|
* Per the migration design (inline conflict markers, never
|
|
603
603
|
* interactive prompts), every migration script:
|
|
604
604
|
* - is a small, IDEMPOTENT TypeScript function (`run(root, opts) → result`).
|
|
605
605
|
* Re-running it on an already-migrated tree is a no-op (or produces the
|
|
606
|
-
* same conflict markers again
|
|
606
|
+
* same conflict markers again: also safe).
|
|
607
607
|
* - NEVER prompts. `--no-input` CI must survive. When a migration cannot
|
|
608
608
|
* reconcile a co-edited region, it writes git-style conflict markers
|
|
609
609
|
* (`<<<<<<<` / `=======` / `>>>>>>>`) into the file and records the path in
|
|
@@ -625,7 +625,7 @@ interface MigrationContext {
|
|
|
625
625
|
dryRun?: boolean;
|
|
626
626
|
/** The project's `rules.enabled` switch, when the caller knows it. False
|
|
627
627
|
* tells a migration that `.noir/rules/RULES.md` is not part of this project
|
|
628
|
-
*
|
|
628
|
+
* : the scaffold withholds the seed: so a migration must not write that
|
|
629
629
|
* file back, however convincing its evidence for a refresh. Undefined means
|
|
630
630
|
* enabled, which is the state of every project that predates the switch. */
|
|
631
631
|
rulesEnabled?: boolean;
|
|
@@ -659,7 +659,7 @@ interface MigrationScript {
|
|
|
659
659
|
* skipped. Within the window, scripts run sorted by `to` ascending so a
|
|
660
660
|
* multi-step upgrade (1.0.0 → 1.1.0 → 1.2.0) composes in order.
|
|
661
661
|
*
|
|
662
|
-
* The runner NEVER throws on a per-script failure
|
|
662
|
+
* The runner NEVER throws on a per-script failure: it captures the error,
|
|
663
663
|
* records a synthetic conflict entry (`<path>__error`), and continues so a
|
|
664
664
|
* single broken migration doesn't block the rest of the chain. The orchestrator
|
|
665
665
|
* decides whether non-empty `conflicts` is a hard failure (init --upgrade) or a
|
|
@@ -677,7 +677,7 @@ declare function runMigrations(root: string, from: string | null, to: string, op
|
|
|
677
677
|
};
|
|
678
678
|
|
|
679
679
|
/** The registry. The runner sorts the selected window by `to`, so declaration
|
|
680
|
-
* order is documentation only
|
|
680
|
+
* order is documentation only: oldest step first. */
|
|
681
681
|
declare const MIGRATIONS: readonly MigrationScript[];
|
|
682
682
|
/** Write git-style inline conflict markers around `theirs`/`ours` so a human
|
|
683
683
|
* or AI agent can resolve later. This is the CI-safe fallback —
|
|
@@ -686,7 +686,7 @@ declare function applyInlineConflict(ours: string, theirs: string, oursLabel?: s
|
|
|
686
686
|
/** Apply `(ours, theirs)` to a region: if they're equal, return `ours` (no
|
|
687
687
|
* conflict); otherwise emit inline markers. Migration authors should prefer
|
|
688
688
|
* this over {@link applyInlineConflict} when the "no change needed" case is
|
|
689
|
-
* common
|
|
689
|
+
* common: it keeps re-runs truly idempotent (no spurious markers on a clean
|
|
690
690
|
* tree). */
|
|
691
691
|
declare function applyWithConflict(ours: string, theirs: string, path: string): {
|
|
692
692
|
text: string;
|
|
@@ -735,7 +735,7 @@ interface ScaffoldOptions {
|
|
|
735
735
|
upgrade?: boolean;
|
|
736
736
|
/** Re-scaffold even when the target is already initialized (bypasses
|
|
737
737
|
* the already-initialized no-op guard). Does NOT bypass `assertSafeRoot`
|
|
738
|
-
*
|
|
738
|
+
* : root-safety is hard, never bypassable. */
|
|
739
739
|
force?: boolean;
|
|
740
740
|
/** Preview: compute the same written/skipped/migrated lists without touching
|
|
741
741
|
* disk. `noir doctor`/CI use this to report drift. */
|
|
@@ -748,10 +748,10 @@ interface ScaffoldOptions {
|
|
|
748
748
|
* `'overwrite'` is byte-backward-compatible. `'preserve'` keeps the user's
|
|
749
749
|
* file (the non-TTY / CI default in the cli). */
|
|
750
750
|
conflictPolicy?: 'overwrite' | 'preserve';
|
|
751
|
-
/** Per-file conflict resolver
|
|
751
|
+
/** Per-file conflict resolver: the UI seam (the engine stays UI-free;
|
|
752
752
|
* the cli injects a @clack-based resolver). Called when a `regenerate`
|
|
753
753
|
* file exists and differs from the template. The return type widens to
|
|
754
|
-
* accept a rich shape carrying `applyToAll
|
|
754
|
+
* accept a rich shape carrying `applyToAll`: the engine then reuses the
|
|
755
755
|
* decision for the rest of the run (per artifact CLASS). */
|
|
756
756
|
onConflict?: (ctx: ConflictContext) => Promise<ConflictResolverReturn> | ConflictResolverReturn;
|
|
757
757
|
/** Three-way merge managed regions (base/ours/theirs) using a persisted
|
|
@@ -760,7 +760,7 @@ interface ScaffoldOptions {
|
|
|
760
760
|
* DEFAULT TRUE: ancestor capture is unconditional, so the first merge
|
|
761
761
|
* run always has a base. Set to `false` (CLI `--no-merge-regions`) to
|
|
762
762
|
* restore strip-replace. Supports BOTH single-region (NOIR.md, ignores) AND
|
|
763
|
-
* multi-region (CLAUDE.md CONTEXT+RULES) managed files
|
|
763
|
+
* multi-region (CLAUDE.md CONTEXT+RULES) managed files: the multi-region
|
|
764
764
|
* path merges managed regions per block (`managedBlocks` +
|
|
765
765
|
* `mergeManagedRegion`). */
|
|
766
766
|
mergeManagedRegions?: boolean;
|
|
@@ -777,7 +777,7 @@ interface ScaffoldResult {
|
|
|
777
777
|
/** Repo-relative paths skipIfExists'd (already present). */
|
|
778
778
|
skipped: string[];
|
|
779
779
|
/** Repo-relative `regenerate` paths skipped because byte-identical to
|
|
780
|
-
* the template (content-hash dedup
|
|
780
|
+
* the template (content-hash dedup: no rewrite). */
|
|
781
781
|
identical: string[];
|
|
782
782
|
/** Repo-relative doc-only seed paths REPLACED on the upgrade path because the
|
|
783
783
|
* copy on disk was still byte-identical to an older Noir's shipped text —
|
|
@@ -787,7 +787,7 @@ interface ScaffoldResult {
|
|
|
787
787
|
* "Noir updated documentation you never touched" apart from "Noir created
|
|
788
788
|
* something new".
|
|
789
789
|
*
|
|
790
|
-
* A seed the user DID edit is never listed here
|
|
790
|
+
* A seed the user DID edit is never listed here: it goes through the
|
|
791
791
|
* ordinary conflict flow, so it appears in {@link written} only when a
|
|
792
792
|
* resolver explicitly chose to replace it (or `--force` did). Empty on every
|
|
793
793
|
* other run: a fresh `init`/`create` has nothing to compare against and
|
|
@@ -795,7 +795,7 @@ interface ScaffoldResult {
|
|
|
795
795
|
refreshed: string[];
|
|
796
796
|
/** True when the already-initialized guard short-circuited (a bare
|
|
797
797
|
* `noir init`/`create` on an initialized project). Callers (init.ts/create.ts)
|
|
798
|
-
* gate skills emission + the "initialized" message on this
|
|
798
|
+
* gate skills emission + the "initialized" message on this: a no-op must NOT
|
|
799
799
|
* re-emit skills or claim it initialized. */
|
|
800
800
|
noop: boolean;
|
|
801
801
|
/** Migration steps executed (`<from>→<to>`), when upgrade ran. */
|
|
@@ -803,7 +803,7 @@ interface ScaffoldResult {
|
|
|
803
803
|
/** Migration conflicts (repo-relative or `<runner>:…`), when upgrade ran. */
|
|
804
804
|
migrationConflicts: string[];
|
|
805
805
|
/** Repo-relative paths a migration CHANGED (wrote or refreshed), when upgrade
|
|
806
|
-
* ran
|
|
806
|
+
* ran: reported separately from {@link refreshed} so callers can name
|
|
807
807
|
* "migrated" files (e.g. `.noir/config.yml`) apart from doc seeds. */
|
|
808
808
|
migrationChanged: string[];
|
|
809
809
|
stack: StackInfo;
|
|
@@ -812,7 +812,7 @@ interface ScaffoldResult {
|
|
|
812
812
|
toVersion: string;
|
|
813
813
|
/** The host actually emitted (post-default). */
|
|
814
814
|
host: HostTag;
|
|
815
|
-
/** Structured conflict report
|
|
815
|
+
/** Structured conflict report: one record per file that existed AND
|
|
816
816
|
* differed from the proposed bytes (regenerate conflict path). Populated in
|
|
817
817
|
* `--json`/`--no-input`/non-TTY runs (no prompt fires) so a CI/JSON caller
|
|
818
818
|
* can see exactly which files diverged + how they were resolved, without
|
|
@@ -823,19 +823,19 @@ interface ScaffoldResult {
|
|
|
823
823
|
* (repo-relative path → mode), i.e. the {@link ManifestEntry.fileMode} of a
|
|
824
824
|
* `skipIfExists` entry that had no file on disk. A real run reports the mode
|
|
825
825
|
* it applied; a `dryRun` reports the mode it WOULD apply (the predictor's
|
|
826
|
-
* job
|
|
826
|
+
* job: the caller can preview `.noir/.env` at 0600 without writing it).
|
|
827
827
|
* Paths the entry skipped (already present) are absent: an existing file's
|
|
828
828
|
* mode is never changed. Optional so existing external constructors of this
|
|
829
829
|
* type (test stubs) stay valid; `scaffold()` always populates it. */
|
|
830
830
|
fileModes?: Record<string, number>;
|
|
831
|
-
/** What the run did to the permissions of `.noir/.env
|
|
831
|
+
/** What the run did to the permissions of `.noir/.env`: the outcome of
|
|
832
832
|
* {@link ensureOwnerOnly}, which is called on every run that may write, in
|
|
833
833
|
* every mode, because the seeded file's 0600 mode is applied only when it is
|
|
834
834
|
* created.
|
|
835
835
|
* `'healed'` means a group/world-readable file was tightened to 0600, so a
|
|
836
836
|
* caller can report the fix instead of leaving the user to notice it in a
|
|
837
837
|
* later diagnostic; `'unchanged'` and `'unsupported'` mean nothing needed
|
|
838
|
-
* doing (or the platform has no POSIX mode bits)
|
|
838
|
+
* doing (or the platform has no POSIX mode bits): the `'unchanged'` a fresh
|
|
839
839
|
* `create` reports, whose seed was written 0600 in the same run. Absent on a
|
|
840
840
|
* dry run, which must not touch disk. Optional so existing external
|
|
841
841
|
* constructors of this type stay valid. */
|
|
@@ -845,7 +845,7 @@ interface ScaffoldResult {
|
|
|
845
845
|
interface ConflictRecord {
|
|
846
846
|
/** Repo-relative path of the conflicting file. */
|
|
847
847
|
path: string;
|
|
848
|
-
/** Artifact class
|
|
848
|
+
/** Artifact class: drives apply-to-all memory scope + report grouping. */
|
|
849
849
|
mode: ConflictContext['mode'];
|
|
850
850
|
/** LCS similarity (0-1) between existing and proposed. */
|
|
851
851
|
similarity?: number;
|
|
@@ -865,7 +865,7 @@ interface ConflictContext {
|
|
|
865
865
|
existing: string;
|
|
866
866
|
/** The content the scaffold would write. */
|
|
867
867
|
proposed: string;
|
|
868
|
-
/** Artifact class
|
|
868
|
+
/** Artifact class: drives apply-to-all memory scope (`regenerate` shares
|
|
869
869
|
* one decision across a run; `managedBlock`/`managedBlocks` stay per-file).
|
|
870
870
|
* Defaults to `'regenerate'` for backward compatibility with older resolvers
|
|
871
871
|
* that pre-date the field (the engine always sets it). */
|
|
@@ -883,32 +883,32 @@ type ConflictResolution = 'replace' | 'preserve' | 'rename' | 'duplicate' | 'can
|
|
|
883
883
|
* `'one'`) OR a rich shape carrying `applyToAll`. The engine unwraps both; when
|
|
884
884
|
* `applyToAll` is true it stores the choice in its per-run memory keyed by
|
|
885
885
|
* artifact CLASS (`regenerate` shares one decision across a run; managedBlock
|
|
886
|
-
* stays per-file regardless
|
|
886
|
+
* stays per-file regardless: user edits there need individual review). */
|
|
887
887
|
type ConflictResolverReturn = ConflictResolution | {
|
|
888
888
|
resolution: ConflictResolution;
|
|
889
889
|
applyToAll?: boolean;
|
|
890
890
|
};
|
|
891
891
|
/**
|
|
892
|
-
* Refuse to scaffold when `root` is
|
|
892
|
+
* Refuse to scaffold when `root` is: or is inside: a `.noir/` directory.
|
|
893
893
|
* Running `noir init`/`create`/`sync` while cwd = `.noir/` would
|
|
894
894
|
* otherwise mint a FRESH project id (because `<root>/.noir/project.id` is
|
|
895
895
|
* absent) and build a NESTED second project (`.noir/.noir/`, `.noir/CLAUDE.md`,
|
|
896
|
-
* `.noir/.claude/skills/`, …)
|
|
896
|
+
* `.noir/.claude/skills/`, …): the duplicate-`.noir` bug. The walk ascends the
|
|
897
897
|
* ancestor chain only, so a legitimate project root that merely CONTAINS a
|
|
898
898
|
* `.noir/` store is never flagged. String-based: works whether or not `root`
|
|
899
899
|
* exists on disk yet (so `noir create <new-dir>` is still guarded).
|
|
900
900
|
*
|
|
901
|
-
* Hard against literal `.noir` path segments
|
|
901
|
+
* Hard against literal `.noir` path segments: NOT bypassable by `--force`
|
|
902
902
|
* (which is reserved for the already-initialized no-op). NOTE: the walk is
|
|
903
903
|
* string-based, so a symlink whose TARGET is inside `.noir/` is not resolved
|
|
904
|
-
* (a local self-foot-gun only
|
|
904
|
+
* (a local self-foot-gun only: the caller controls `root`: not externally
|
|
905
905
|
* exploitable); in practice `--cwd`/positional roots are literal paths.
|
|
906
906
|
*/
|
|
907
907
|
declare function assertSafeRoot(root: string): void;
|
|
908
908
|
declare function scaffold(opts: ScaffoldOptions): Promise<ScaffoldResult>;
|
|
909
909
|
|
|
910
910
|
/**
|
|
911
|
-
* Scaffold version stamp
|
|
911
|
+
* Scaffold version stamp: Noir's equivalent of Copier's `last-applied`.
|
|
912
912
|
* Stored at `.noir/scaffold-version` as a single `noir-scaffold=<semver>` line
|
|
913
913
|
* so `noir init --upgrade` / `noir doctor` can diff against
|
|
914
914
|
* {@link CURRENT_SCAFFOLD_VERSION} and decide which migrations to run.
|
|
@@ -928,7 +928,7 @@ declare const CURRENT_SCAFFOLD_VERSION = "1.5.0";
|
|
|
928
928
|
/** Path to the stamp file under `root`. Exposed for tests + doctor. */
|
|
929
929
|
declare function scaffoldVersionPath(root: string): string;
|
|
930
930
|
/** Read the applied scaffold version, or `null` if the stamp is absent/unparseable.
|
|
931
|
-
* Never throws
|
|
931
|
+
* Never throws: doctor must keep reporting even on a malformed stamp. */
|
|
932
932
|
declare function readScaffoldVersion(root: string): string | null;
|
|
933
933
|
/** Write the stamp, creating `.noir/` if needed. The orchestrator writes it
|
|
934
934
|
* LAST so a crash mid-scaffold leaves an old/absent stamp rather than a
|
|
@@ -946,10 +946,10 @@ declare function writeScaffoldVersion(root: string, version: string): void;
|
|
|
946
946
|
* map lookup is sufficient and keeps `@noir-ai/create` dependency-light.
|
|
947
947
|
*
|
|
948
948
|
* Semantics (documented):
|
|
949
|
-
* - Tokens are `{{ name }}` / `{{name}}
|
|
949
|
+
* - Tokens are `{{ name }}` / `{{name}}`: any amount of ASCII whitespace
|
|
950
950
|
* inside the braces is permitted. The name is trimmed.
|
|
951
951
|
* - Known vars (value is a string) are substituted verbatim. NO escaping is
|
|
952
|
-
* applied
|
|
952
|
+
* applied: callers must pre-escape for the target format (JSON/YAML/MD).
|
|
953
953
|
* This is intentional: the engine renders into multiple formats and a
|
|
954
954
|
* single universal escaper would be wrong for at least one of them.
|
|
955
955
|
* - Unknown vars (not in `vars`, or value `undefined`) → the original token
|
|
@@ -964,7 +964,7 @@ declare function writeScaffoldVersion(root: string, version: string): void;
|
|
|
964
964
|
declare function render(template: string, vars: Record<string, unknown>): string;
|
|
965
965
|
|
|
966
966
|
/** Read a template's raw text by name (e.g. `noir.md.tmpl`). Throws on missing
|
|
967
|
-
*
|
|
967
|
+
* : an unknown template is a manifest bug and should fail loudly, not render
|
|
968
968
|
* an empty string silently. */
|
|
969
969
|
declare function loadTemplate(name: string): string;
|
|
970
970
|
/** Resolve a template name to its absolute path (for diagnostics + tests). */
|