@noir-ai/create 1.14.0 → 1.15.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/index.d.ts +226 -46
- package/dist/index.js +604 -224
- package/dist/index.js.map +1 -1
- package/package.json +3 -3
- package/templates/config.env.tmpl +29 -66
- package/templates/env.example.tmpl +73 -47
- package/templates/noir-readme.md.tmpl +1 -1
package/dist/index.d.ts
CHANGED
|
@@ -19,7 +19,7 @@ declare function writeAncestors(root: string, map: Record<string, string>): void
|
|
|
19
19
|
* adaptation (where to drop `.claude/` vs `.cursor/` etc., future), ignore-file
|
|
20
20
|
* selection, and the onboarding TUI's confirm step.
|
|
21
21
|
*
|
|
22
|
-
* Design rules
|
|
22
|
+
* Design rules:
|
|
23
23
|
* - Never throws. A foreign/empty dir returns `{ languages: [], monorepo:
|
|
24
24
|
* false, frameworks: [], packageManager: null }`.
|
|
25
25
|
* - Never opens network, never parses code beyond a `package.json`/`pyproject`
|
|
@@ -41,18 +41,18 @@ interface StackInfo {
|
|
|
41
41
|
/** `pnpm` | `npm` | `yarn` when a lockfile is present, else null. */
|
|
42
42
|
packageManager: string | null;
|
|
43
43
|
/**
|
|
44
|
-
*
|
|
44
|
+
* Source of the resolved PM. `packageManager-field`
|
|
45
45
|
* (package.json#packageManager), `lockfile`, `user-agent` (invoke-time
|
|
46
46
|
* npm_config_user_agent), or `unknown`.
|
|
47
47
|
*/
|
|
48
48
|
pmSource: 'packageManager-field' | 'lockfile' | 'user-agent' | 'unknown';
|
|
49
49
|
/**
|
|
50
|
-
*
|
|
50
|
+
* Detected CI. `github`/`gitlab`/`circleci`/
|
|
51
51
|
* `jenkins`/`none`.
|
|
52
52
|
*/
|
|
53
53
|
ci: string | null;
|
|
54
54
|
/**
|
|
55
|
-
*
|
|
55
|
+
* Existing AI instruction files found under root.
|
|
56
56
|
* Never clobbered — the scaffold confirms the write strategy (skip/add-section/
|
|
57
57
|
* standalone).
|
|
58
58
|
*/
|
|
@@ -66,15 +66,154 @@ interface StackInfo {
|
|
|
66
66
|
declare function detectStack(root: string): StackInfo;
|
|
67
67
|
|
|
68
68
|
/**
|
|
69
|
-
*
|
|
69
|
+
* Template history — the evidence needed to decide whether a shipped seed
|
|
70
|
+
* file may be refreshed during an upgrade.
|
|
71
|
+
*
|
|
72
|
+
* WHY THIS EXISTS
|
|
73
|
+
* ---------------
|
|
74
|
+
* `noir init` writes two files whose only job is to be read by a human:
|
|
75
|
+
* `.noir/.env.example` (documents the variable set) and `.noir/rules/RULES.md`
|
|
76
|
+
* (the AI working-contract seed). Both are emitted create-only-if-absent, and
|
|
77
|
+
* that is normally the right contract: once a file exists, the user owns it and
|
|
78
|
+
* Noir must never overwrite their edits.
|
|
79
|
+
*
|
|
80
|
+
* It has one bad consequence. When a later release adds a variable to
|
|
81
|
+
* `.noir/.env.example`, an already-initialized project keeps the old text
|
|
82
|
+
* forever — the upgrade path cannot tell "the user rewrote this file" apart
|
|
83
|
+
* from "the user never touched it, and the shipped text has moved on". Those
|
|
84
|
+
* two need opposite handling: leave the first alone, refresh the second. A
|
|
85
|
+
* plain-content check cannot distinguish them, and nothing on disk records the
|
|
86
|
+
* bytes that were originally written.
|
|
87
|
+
*
|
|
88
|
+
* This module supplies the missing evidence. It holds the exact bytes of the
|
|
89
|
+
* seed templates as they shipped at each past scaffold version, so a caller can
|
|
90
|
+
* ask: "are the bytes on disk identical to some version Noir once wrote, and
|
|
91
|
+
* different from what Noir writes today?" If yes, the file is an unedited
|
|
92
|
+
* leftover and refreshing it loses nothing.
|
|
93
|
+
*
|
|
94
|
+
* THE CONTRACT
|
|
95
|
+
* ------------
|
|
96
|
+
* {@link isStaleSeed} answers exactly that question. It is deliberately
|
|
97
|
+
* conservative in one direction: it returns `true` only on an exact match
|
|
98
|
+
* against a recorded past version, so ANY other byte difference — a user edit,
|
|
99
|
+
* a re-wrapped line, even a CRLF line-ending conversion from a Windows
|
|
100
|
+
* checkout — reads as "user-owned" and the file is left untouched. A missed
|
|
101
|
+
* refresh is a documentation gap; a wrong refresh destroys user work. The
|
|
102
|
+
* asymmetry is intentional and should not be "fixed" into a fuzzy match.
|
|
103
|
+
*
|
|
104
|
+
* Each entry holds the text of BOTH seeds, so the comparison takes the seed it
|
|
105
|
+
* is about as an argument and scans only that field. Scanning all of them would
|
|
106
|
+
* let a `.env.example` match the recorded working-rules text (or the reverse),
|
|
107
|
+
* and since the caller's response to `true` is an overwrite, a mixed-up pairing
|
|
108
|
+
* would discard an edit the user is entitled to keep. The caller knows which
|
|
109
|
+
* file it just read; the argument makes it say so.
|
|
110
|
+
*
|
|
111
|
+
* RENDERING
|
|
112
|
+
* ---------
|
|
113
|
+
* Seed templates are normally interpolated through `render(template, vars)`
|
|
114
|
+
* before writing. That step is a no-op for the two templates recorded here:
|
|
115
|
+
* neither contains a `{{...}}` token, so `render(text, vars) === text` for
|
|
116
|
+
* every possible `vars`. The registry therefore stores raw template text and
|
|
117
|
+
* {@link isStaleSeed} compares it against raw file bytes — no `vars` argument
|
|
118
|
+
* is needed, and callers do not have to reconstruct the context that wrote the
|
|
119
|
+
* file years ago.
|
|
120
|
+
*
|
|
121
|
+
* That equivalence is load-bearing, so it is asserted rather than merely
|
|
122
|
+
* documented: `template-history.test.ts` fails if any recorded template
|
|
123
|
+
* contains a `{{` token. A future seed template that DOES interpolate needs
|
|
124
|
+
* `render` applied to the history entry (and a `vars` parameter on the
|
|
125
|
+
* comparison) before it is added here — the test exists to make that
|
|
126
|
+
* requirement impossible to overlook.
|
|
127
|
+
*
|
|
128
|
+
* APPENDING TO THE HISTORY
|
|
129
|
+
* ------------------------
|
|
130
|
+
* Record the template bytes as they shipped, keyed by the scaffold version
|
|
131
|
+
* whose `init` wrote them, and never edit an existing entry — a user's disk
|
|
132
|
+
* still holds those old bytes, and rewriting history would silently reclassify
|
|
133
|
+
* their file as "current" (no refresh) or "unknown" (no refresh). The same
|
|
134
|
+
* reasoning that keeps npm versions immutable applies here.
|
|
135
|
+
*/
|
|
136
|
+
/**
|
|
137
|
+
* Which recorded seed a comparison is about. The values mirror the field names
|
|
138
|
+
* of {@link SeedTemplateHistoryEntry}: every entry carries the text of both
|
|
139
|
+
* seeds, and a caller must name the one whose bytes it read (see
|
|
140
|
+
* {@link isStaleSeed} on why the pairing is load-bearing).
|
|
141
|
+
*/
|
|
142
|
+
type SeedKind = 'envExample' | 'rulesSeed';
|
|
143
|
+
/**
|
|
144
|
+
* The seed templates as they shipped at one past scaffold version.
|
|
145
|
+
*
|
|
146
|
+
* Only the doc-only seeds are tracked. Files the user is expected to edit
|
|
147
|
+
* heavily (`.noir/config.yml`, `.noir/project.id`) are not candidates for
|
|
148
|
+
* refresh at all, and co-owned files that carry a managed region already have a
|
|
149
|
+
* three-way merge path of their own.
|
|
150
|
+
*/
|
|
151
|
+
interface SeedTemplateHistoryEntry {
|
|
152
|
+
/**
|
|
153
|
+
* Bare `x.y.z` scaffold version whose `init` wrote these bytes — the value
|
|
154
|
+
* stamped into `.noir/scaffold-version` at the time, with no `v` prefix and
|
|
155
|
+
* no pre-release suffix.
|
|
156
|
+
*
|
|
157
|
+
* Spelled out literally rather than read from `CURRENT_SCAFFOLD_VERSION`:
|
|
158
|
+
* the version recorded here is a historical fact about these bytes, so it
|
|
159
|
+
* must not advance when the current version does.
|
|
160
|
+
*/
|
|
161
|
+
readonly scaffoldVersion: string;
|
|
162
|
+
/** Raw text of the `env.example` template at this version (see the module
|
|
163
|
+
* header on why raw text equals the rendered file for this template). */
|
|
164
|
+
readonly envExample: string;
|
|
165
|
+
/** Raw text of the `rules-seed.md` template at this version. */
|
|
166
|
+
readonly rulesSeed: string;
|
|
167
|
+
}
|
|
168
|
+
/**
|
|
169
|
+
* The recorded seed-template bytes, oldest first.
|
|
170
|
+
*
|
|
171
|
+
* Read-only by intent: this is an append-only ledger of shipped bytes, not
|
|
172
|
+
* mutable configuration. Entries are never edited or removed (see the module
|
|
173
|
+
* header on why rewriting a past entry would silently misclassify a user's
|
|
174
|
+
* file), so a version that changed a seed template appends rather than
|
|
175
|
+
* updates.
|
|
176
|
+
*/
|
|
177
|
+
declare const SEED_TEMPLATE_HISTORY: readonly SeedTemplateHistoryEntry[];
|
|
178
|
+
/**
|
|
179
|
+
* Does `fileBytes` look like an unedited `seed` left over from an older scaffold
|
|
180
|
+
* version?
|
|
181
|
+
*
|
|
182
|
+
* `true` iff the bytes are an exact match for that seed's text in a recorded
|
|
183
|
+
* history entry AND differ from `currentRender` (what Noir writes for that seed
|
|
184
|
+
* today). Callers pass `currentRender` rather than this module deriving it,
|
|
185
|
+
* because the current template text is the business of the manifest and the
|
|
186
|
+
* template loader — and because the compile-time constant that names the
|
|
187
|
+
* current scaffold version advances independently of the bytes recorded here.
|
|
188
|
+
*
|
|
189
|
+
* `seed` names the file the bytes came from. It is a safety argument, not
|
|
190
|
+
* bookkeeping: because each entry records both seeds, a comparison that scanned
|
|
191
|
+
* every field would report the recorded working-rules bytes as a stale
|
|
192
|
+
* `.env.example`, and the caller's response to `true` is to overwrite the file.
|
|
193
|
+
* Naming the seed keeps the comparison inside its own column, so a mixed-up
|
|
194
|
+
* pairing cannot silently destroy an edit the user is entitled to keep.
|
|
195
|
+
*
|
|
196
|
+
* The `currentRender` guard is checked FIRST: it is what keeps a no-op from
|
|
197
|
+
* being reported as a refresh. A project initialized at the current version
|
|
198
|
+
* already holds the current bytes; those bytes also match their own history
|
|
199
|
+
* entry, so without the second condition every up-to-date project would be told
|
|
200
|
+
* to rewrite its seeds.
|
|
201
|
+
*
|
|
202
|
+
* Callers own the other half of the decision: a file that does not exist has no
|
|
203
|
+
* bytes to compare and is a plain "create the seed", not a refresh.
|
|
204
|
+
*/
|
|
205
|
+
declare function isStaleSeed(seed: SeedKind, fileBytes: string, currentRender: string): boolean;
|
|
206
|
+
|
|
207
|
+
/**
|
|
208
|
+
* The three-mode writer — generalizes the earlier `writeManagedRegion` into
|
|
70
209
|
* the declarative dispatch the scaffold manifest drives. Each mode maps 1:1 to
|
|
71
|
-
* an artifact class in the
|
|
210
|
+
* an artifact class in the artifact matrix:
|
|
72
211
|
*
|
|
73
212
|
* - {@link regenerate} — pure pointers (`.mcp.json`, `NOIR.md` brief, …).
|
|
74
213
|
* Always overwritten, atomically.
|
|
75
214
|
* - {@link managedBlock} — co-owned files (`CLAUDE.md` context/rules,
|
|
76
215
|
* `.gitignore` noir block, …). DELEGATES to
|
|
77
|
-
*
|
|
216
|
+
* core's `writeManagedRegion` so user content
|
|
78
217
|
* outside the markers is preserved byte-for-byte and
|
|
79
218
|
* re-runs are idempotent. Never duplicate the
|
|
80
219
|
* managed-region logic.
|
|
@@ -82,7 +221,7 @@ declare function detectStack(root: string): StackInfo;
|
|
|
82
221
|
* `project.id`). Write once; never clobber.
|
|
83
222
|
*
|
|
84
223
|
* The orchestrator (`scaffold.ts`) is the only intended caller; the per-mode
|
|
85
|
-
* functions are exported so the cli
|
|
224
|
+
* functions are exported so the cli and tests can drive them directly
|
|
86
225
|
* when a one-off write is needed outside the manifest.
|
|
87
226
|
*/
|
|
88
227
|
type WriteMode = 'regenerate' | 'managedBlock' | 'skipIfExists' | 'mergeJson';
|
|
@@ -104,7 +243,7 @@ interface WriteOutcome {
|
|
|
104
243
|
* for the whole manifest so a missing dir is a single, attributable failure
|
|
105
244
|
* rather than N silent ones inside the writer. */
|
|
106
245
|
declare function regenerate(absPath: string, content: string): WriteOutcome;
|
|
107
|
-
/** Re-emit a managed region, delegating to
|
|
246
|
+
/** Re-emit a managed region, delegating to core's `writeManagedRegion`.
|
|
108
247
|
* `regionText` MUST already include the begin/end markers (matches the shape
|
|
109
248
|
* `writeManagedRegion` expects and that `IGNORE_BLOCK`/`CONTEXT_BLOCK`
|
|
110
249
|
* callers build in core/cli). Use {@link buildRegion} to assemble it from a
|
|
@@ -120,12 +259,12 @@ declare function managedBlock(absPath: string, block: ManagedBlock, regionText:
|
|
|
120
259
|
* conventional trailing newline in `.tmpl` files without producing a
|
|
121
260
|
* double-newline before the end marker. This keeps the output byte-identical
|
|
122
261
|
* to `claudeAdapter.emitContext`/`emitRules` and core's `syncIgnores`, which
|
|
123
|
-
*
|
|
262
|
+
* the cli refactor relies on for diff-free output. */
|
|
124
263
|
declare function buildRegion(block: ManagedBlock, body: string): string;
|
|
125
264
|
/** Write `content` to `absPath` only if no file exists there. Returns whether
|
|
126
265
|
* bytes were written. Parent dirs are created by {@link atomicWriteFile}.
|
|
127
266
|
*
|
|
128
|
-
* `fileMode`
|
|
267
|
+
* `fileMode` is the permission for the NEWLY created file —
|
|
129
268
|
* `0o600` for the `.noir/.env` credential seed. It is applied to the temp file
|
|
130
269
|
* BEFORE the rename (see `AtomicWriteOptions.mode`), so the file never exists
|
|
131
270
|
* at its final path with a laxer mode. It is deliberately ignored when the
|
|
@@ -146,12 +285,13 @@ declare function skipIfExists(absPath: string, content: string, fileMode?: numbe
|
|
|
146
285
|
* write {@link WriteMode} so the orchestrator can dispatch without knowing
|
|
147
286
|
* what's inside.
|
|
148
287
|
*
|
|
149
|
-
* FAITHFULNESS CONTRACT
|
|
150
|
-
* the artifacts `packages/cli/src/{init,sync}.ts` wrote
|
|
151
|
-
* refactor
|
|
152
|
-
* the byte-for-byte output MUST stay equivalent for first-run
|
|
153
|
-
*
|
|
154
|
-
* entries + a {@link buildHostArtifacts} call that materializes
|
|
288
|
+
* FAITHFULNESS CONTRACT: this table is a strict superset of
|
|
289
|
+
* the artifacts `packages/cli/src/{init,sync}.ts` wrote before the manifest
|
|
290
|
+
* refactor. The cli refactor replaced those ad-hoc writers with a call into
|
|
291
|
+
* `scaffold()`; the byte-for-byte output MUST stay equivalent for first-run
|
|
292
|
+
* init. The manifest is HOST-PARAMETRIC: {@link buildManifest} returns
|
|
293
|
+
* host-agnostic entries + a {@link buildHostArtifacts} call that materializes
|
|
294
|
+
* per-host files
|
|
155
295
|
* (CLAUDE.md/GEMINI.md for claude/gemini; AGENTS.md + .cursor/.../opencode.json
|
|
156
296
|
* for agents-md/cursor/opencode) via the resolved adapter. The claude default
|
|
157
297
|
* `noir init` stays BYTE-IDENTICAL to v1.1 — REMOVED the additive
|
|
@@ -163,15 +303,36 @@ declare function skipIfExists(absPath: string, content: string, fileMode?: numbe
|
|
|
163
303
|
* `join(root, entry.path) === paths.X(root)` for every entry layout knows
|
|
164
304
|
* about, so a layout rename is caught here instead of silently drifting.
|
|
165
305
|
*/
|
|
166
|
-
/**
|
|
167
|
-
* (re-exported so existing imports keep working)
|
|
168
|
-
* literal `'claude'
|
|
169
|
-
* via the orchestrator's host filter + {@link buildHostArtifacts}. */
|
|
306
|
+
/** `HostTag` is the SAME `HostId` enum the adapter registry uses
|
|
307
|
+
* (re-exported so existing imports keep working); it used to be the
|
|
308
|
+
* literal `'claude'`. Widening it to `HostId` lets one manifest serve every
|
|
309
|
+
* host via the orchestrator's host filter + {@link buildHostArtifacts}. */
|
|
170
310
|
type HostTag = HostId;
|
|
171
311
|
interface ManifestEntry {
|
|
172
312
|
/** Repo-relative POSIX path (forward slashes). Orchestrator joins with root. */
|
|
173
313
|
path: string;
|
|
174
314
|
mode: WriteMode;
|
|
315
|
+
/** Opt-in flag for a DOC-ONLY seed — a file whose only job is to be read by a
|
|
316
|
+
* human. On an upgrade, a seed still byte-identical to what an older Noir
|
|
317
|
+
* shipped is refreshed in place rather than staying frozen at that older
|
|
318
|
+
* text forever; one the user edited is never refreshed (it goes to the
|
|
319
|
+
* ordinary conflict flow, which keeps the user's bytes by default).
|
|
320
|
+
*
|
|
321
|
+
* Honored on the upgrade emit only. A fresh `init`/`create` either creates
|
|
322
|
+
* the file or leaves it alone, and `sync` does not emit seeds at all, so
|
|
323
|
+
* neither can observe the flag — the behavior change is confined to the one
|
|
324
|
+
* command a user runs to bring an initialized project up to date.
|
|
325
|
+
*
|
|
326
|
+
* DOC-ONLY SEEDS ONLY. A file the user owns the contents of must never carry
|
|
327
|
+
* this flag: `.noir/.env`, `.noir/config.yml` and `.noir/project.id` are
|
|
328
|
+
* theirs to shape, and an upgrade that rewrote one of them would destroy
|
|
329
|
+
* exactly the work this engine exists to protect. Co-owned files (managed
|
|
330
|
+
* blocks) must not set it either — they have a three-way merge that
|
|
331
|
+
* preserves user edits, which is the better tool for a shared file.
|
|
332
|
+
*
|
|
333
|
+
* Which recorded seed the bytes are compared against is declared separately
|
|
334
|
+
* in {@link REFRESHABLE_SEED_KIND}. */
|
|
335
|
+
refreshIfStale?: true;
|
|
175
336
|
/** Host tag; entry is skipped when opts.host !== entry.host.
|
|
176
337
|
* Undefined = host-agnostic (every host emits it). */
|
|
177
338
|
host?: HostTag;
|
|
@@ -202,7 +363,7 @@ interface ManifestEntry {
|
|
|
202
363
|
dedupSubstring?: string;
|
|
203
364
|
}
|
|
204
365
|
type BuildManifestContext = {
|
|
205
|
-
/** Absolute repo root.
|
|
366
|
+
/** Absolute repo root. Needed so {@link buildHostArtifacts} can resolve
|
|
206
367
|
* absolute adapter paths (`adapter.mcpConfigPath({root})`, etc.) to the
|
|
207
368
|
* manifest's repo-relative POSIX shape. */
|
|
208
369
|
root: string;
|
|
@@ -226,7 +387,7 @@ type BuildManifestContext = {
|
|
|
226
387
|
stack?: StackInfo;
|
|
227
388
|
};
|
|
228
389
|
/** Co-owned NOIR.md auto-brief region. Defined locally (not exported from
|
|
229
|
-
* core) because core's
|
|
390
|
+
* core) because core's own named block instances cover only the three
|
|
230
391
|
* regions core itself writes (context/rules/ignore); the brief is the
|
|
231
392
|
* scaffold engine's own. Uses the SAME `managedBlock()` factory so marker
|
|
232
393
|
* shape stays consistent with the rest of the family. */
|
|
@@ -239,14 +400,14 @@ declare const MANIFEST_PATH_PARITY: ReadonlyArray<[
|
|
|
239
400
|
* Build the manifest for a given ctx. Pure (no I/O). The orchestrator calls
|
|
240
401
|
* this once per scaffold run; tests assert the shape is stable.
|
|
241
402
|
*
|
|
242
|
-
*
|
|
403
|
+
* The manifest is `[...hostAgnosticEntries(ctx), ...hostSpecificEntries(ctx)]`
|
|
243
404
|
* where the host-specific half comes from {@link buildHostArtifacts} (driven by
|
|
244
405
|
* `resolveAdapter(ctx.host)`). The host-agnostic half is unchanged from v1.1
|
|
245
406
|
* (canonical `.noir/` store + ignore files). {@link buildHostArtifacts}
|
|
246
407
|
* emits AGENTS.md ONLY for agents-md/cursor/opencode (claude/gemini use their
|
|
247
408
|
* own CLAUDE.md/GEMINI.md — emitting AGENTS.md too would double-import .noir/).
|
|
248
409
|
*
|
|
249
|
-
* Mode-tagging rationale per artifact
|
|
410
|
+
* Mode-tagging rationale per artifact:
|
|
250
411
|
* - `project.id` → skipIfExists. First init writes a fresh id; re-init MUST
|
|
251
412
|
* NOT overwrite — that would orphan the indexed store DB named after it.
|
|
252
413
|
* - `config.yml` → skipIfExists. User-owned; the seed is written once.
|
|
@@ -374,7 +535,7 @@ declare function mergeThreeWay(base: string, ours: string, theirs: string, style
|
|
|
374
535
|
/**
|
|
375
536
|
* Migration model — Nx-style declarative upgrade scripts, hand-rolled.
|
|
376
537
|
*
|
|
377
|
-
* Per the
|
|
538
|
+
* Per the migration design (inline conflict markers, never
|
|
378
539
|
* interactive prompts), every migration script:
|
|
379
540
|
* - is a small, IDEMPOTENT TypeScript function (`run(root, opts) → result`).
|
|
380
541
|
* Re-running it on an already-migrated tree is a no-op (or produces the
|
|
@@ -385,10 +546,10 @@ declare function mergeThreeWay(base: string, ours: string, theirs: string, style
|
|
|
385
546
|
* `result.conflicts` so the caller (doctor/CI) can fail loudly with a
|
|
386
547
|
* reviewable artifact rather than hang on a prompt.
|
|
387
548
|
*
|
|
388
|
-
* The registry ({@link MIGRATIONS}) carries
|
|
389
|
-
* `1.0.0 → 1.0.0` smoke test that exercises the runner end-to-end, and
|
|
390
|
-
*
|
|
391
|
-
* is the copy-pasteable template for the next contributor.
|
|
549
|
+
* The registry ({@link MIGRATIONS}) carries three entries: the synthetic
|
|
550
|
+
* `1.0.0 → 1.0.0` smoke test that exercises the runner end-to-end, and two real
|
|
551
|
+
* version-to-version migrations, `1.0.0 → 1.1.0` and `1.1.0 → 1.2.0`. The
|
|
552
|
+
* synthetic one is the copy-pasteable template for the next contributor.
|
|
392
553
|
*/
|
|
393
554
|
interface MigrationContext {
|
|
394
555
|
/** Absolute repo root the migration operates on. */
|
|
@@ -447,8 +608,8 @@ declare function runMigrations(root: string, from: string | null, to: string, op
|
|
|
447
608
|
* order is documentation only — oldest step first. */
|
|
448
609
|
declare const MIGRATIONS: readonly MigrationScript[];
|
|
449
610
|
/** Write git-style inline conflict markers around `theirs`/`ours` so a human
|
|
450
|
-
* or AI agent can resolve later. This is the CI-safe fallback
|
|
451
|
-
*
|
|
611
|
+
* or AI agent can resolve later. This is the CI-safe fallback —
|
|
612
|
+
* no interactive prompts, ever. */
|
|
452
613
|
declare function applyInlineConflict(ours: string, theirs: string, oursLabel?: string, theirsLabel?: string): string;
|
|
453
614
|
/** Apply `(ours, theirs)` to a region: if they're equal, return `ours` (no
|
|
454
615
|
* conflict); otherwise emit inline markers. Migration authors should prefer
|
|
@@ -461,7 +622,7 @@ declare function applyWithConflict(ours: string, theirs: string, path: string):
|
|
|
461
622
|
};
|
|
462
623
|
|
|
463
624
|
/**
|
|
464
|
-
* Scaffold orchestrator. One function the cli
|
|
625
|
+
* Scaffold orchestrator. One function the cli calls for `noir init`,
|
|
465
626
|
* `noir create`, and `noir sync`; mode selects the manifest subset + how
|
|
466
627
|
* project identity is resolved.
|
|
467
628
|
*
|
|
@@ -500,19 +661,19 @@ interface ScaffoldOptions {
|
|
|
500
661
|
* initialization without ever touching a file the user owns. Only meaningful
|
|
501
662
|
* with mode `'init'`. */
|
|
502
663
|
upgrade?: boolean;
|
|
503
|
-
/**
|
|
664
|
+
/** Re-scaffold even when the target is already initialized (bypasses
|
|
504
665
|
* the already-initialized no-op guard). Does NOT bypass `assertSafeRoot`
|
|
505
666
|
* — root-safety is hard, never bypassable. */
|
|
506
667
|
force?: boolean;
|
|
507
668
|
/** Preview: compute the same written/skipped/migrated lists without touching
|
|
508
669
|
* disk. `noir doctor`/CI use this to report drift. */
|
|
509
670
|
dryRun?: boolean;
|
|
510
|
-
/**
|
|
671
|
+
/** Policy for a `regenerate` file that exists and DIFFERS from the
|
|
511
672
|
* template, when no {@link onConflict} callback is provided. Default
|
|
512
673
|
* `'overwrite'` is byte-backward-compatible. `'preserve'` keeps the user's
|
|
513
674
|
* file (the non-TTY / CI default in the cli). */
|
|
514
675
|
conflictPolicy?: 'overwrite' | 'preserve';
|
|
515
|
-
/**
|
|
676
|
+
/** Per-file conflict resolver — the UI seam (the engine stays UI-free;
|
|
516
677
|
* the cli injects a @clack-based resolver). Called when a `regenerate`
|
|
517
678
|
* file exists and differs from the template. The return type widens to
|
|
518
679
|
* accept a rich shape carrying `applyToAll` — the engine then reuses the
|
|
@@ -525,7 +686,8 @@ interface ScaffoldOptions {
|
|
|
525
686
|
* run always has a base. Set to `false` (CLI `--no-merge-regions`) to
|
|
526
687
|
* restore strip-replace. Supports BOTH single-region (NOIR.md, ignores) AND
|
|
527
688
|
* multi-region (CLAUDE.md CONTEXT+RULES) managed files — the multi-region
|
|
528
|
-
* path
|
|
689
|
+
* path merges managed regions per block (`managedBlocks` +
|
|
690
|
+
* `mergeManagedRegion`). */
|
|
529
691
|
mergeManagedRegions?: boolean;
|
|
530
692
|
/** Explicit interactivity signal. The engine reads THIS (not
|
|
531
693
|
* `process.env`), so a direct API/embedded caller in a TTY that does NOT
|
|
@@ -539,10 +701,24 @@ interface ScaffoldResult {
|
|
|
539
701
|
written: string[];
|
|
540
702
|
/** Repo-relative paths skipIfExists'd (already present). */
|
|
541
703
|
skipped: string[];
|
|
542
|
-
/**
|
|
704
|
+
/** Repo-relative `regenerate` paths skipped because byte-identical to
|
|
543
705
|
* the template (content-hash dedup — no rewrite). */
|
|
544
706
|
identical: string[];
|
|
545
|
-
/**
|
|
707
|
+
/** Repo-relative doc-only seed paths REPLACED on the upgrade path because the
|
|
708
|
+
* copy on disk was still byte-identical to an older Noir's shipped text —
|
|
709
|
+
* i.e. the user never edited it, so a newer seed costs nothing. Reported
|
|
710
|
+
* separately from {@link written} because the distinction is the whole point
|
|
711
|
+
* of the refresh: a caller (and the user reading the summary) can tell
|
|
712
|
+
* "Noir updated documentation you never touched" apart from "Noir created
|
|
713
|
+
* something new".
|
|
714
|
+
*
|
|
715
|
+
* A seed the user DID edit is never listed here — it goes through the
|
|
716
|
+
* ordinary conflict flow, so it appears in {@link written} only when a
|
|
717
|
+
* resolver explicitly chose to replace it (or `--force` did). Empty on every
|
|
718
|
+
* other run: a fresh `init`/`create` has nothing to compare against and
|
|
719
|
+
* `sync` does not emit seeds. */
|
|
720
|
+
refreshed: string[];
|
|
721
|
+
/** True when the already-initialized guard short-circuited (a bare
|
|
546
722
|
* `noir init`/`create` on an initialized project). Callers (init.ts/create.ts)
|
|
547
723
|
* gate skills emission + the "initialized" message on this — a no-op must NOT
|
|
548
724
|
* re-emit skills or claim it initialized. */
|
|
@@ -551,6 +727,10 @@ interface ScaffoldResult {
|
|
|
551
727
|
migrationsRan: string[];
|
|
552
728
|
/** Migration conflicts (repo-relative or `<runner>:…`), when upgrade ran. */
|
|
553
729
|
migrationConflicts: string[];
|
|
730
|
+
/** Repo-relative paths a migration CHANGED (wrote or refreshed), when upgrade
|
|
731
|
+
* ran — reported separately from {@link refreshed} so callers can name
|
|
732
|
+
* "migrated" files (e.g. `.noir/config.yml`) apart from doc seeds. */
|
|
733
|
+
migrationChanged: string[];
|
|
554
734
|
stack: StackInfo;
|
|
555
735
|
projectId: string;
|
|
556
736
|
fromVersion: string | null;
|
|
@@ -589,7 +769,7 @@ interface ConflictRecord {
|
|
|
589
769
|
/** Resolution the engine applied (`replace`/`preserve`/`rename`/…). */
|
|
590
770
|
resolution: ConflictResolution;
|
|
591
771
|
}
|
|
592
|
-
/**
|
|
772
|
+
/** Context passed to {@link ScaffoldOptions.onConflict} when a
|
|
593
773
|
* `regenerate` file exists and differs from the template. */
|
|
594
774
|
interface ConflictContext {
|
|
595
775
|
/** Repo-relative path of the conflicting file. */
|
|
@@ -600,7 +780,7 @@ interface ConflictContext {
|
|
|
600
780
|
proposed: string;
|
|
601
781
|
/** Artifact class — drives apply-to-all memory scope (`regenerate` shares
|
|
602
782
|
* one decision across a run; `managedBlock`/`managedBlocks` stay per-file).
|
|
603
|
-
* Defaults to `'regenerate'` for backward compatibility with
|
|
783
|
+
* Defaults to `'regenerate'` for backward compatibility with older resolvers
|
|
604
784
|
* that pre-date the field (the engine always sets it). */
|
|
605
785
|
mode?: 'regenerate' | 'managedBlock' | 'managedBlocks' | 'skipIfExists' | 'skill' | 'markdown' | 'artifact';
|
|
606
786
|
/** LCS similarity (0-1) between existing and proposed. Cheap signal for
|
|
@@ -610,7 +790,7 @@ interface ConflictContext {
|
|
|
610
790
|
* bytes WITH zdiff3 conflict markers (selecting `'merge'` writes this). */
|
|
611
791
|
mergedWithMarkers?: string;
|
|
612
792
|
}
|
|
613
|
-
/**
|
|
793
|
+
/** How to resolve a `regenerate` conflict. */
|
|
614
794
|
type ConflictResolution = 'replace' | 'preserve' | 'rename' | 'duplicate' | 'cancel' | 'merge';
|
|
615
795
|
/** The resolver may return a bare {@link ConflictResolution} (scope
|
|
616
796
|
* `'one'`) OR a rich shape carrying `applyToAll`. The engine unwraps both; when
|
|
@@ -623,7 +803,7 @@ type ConflictResolverReturn = ConflictResolution | {
|
|
|
623
803
|
};
|
|
624
804
|
/**
|
|
625
805
|
* Refuse to scaffold when `root` is — or is inside — a `.noir/` directory.
|
|
626
|
-
*
|
|
806
|
+
* Running `noir init`/`create`/`sync` while cwd = `.noir/` would
|
|
627
807
|
* otherwise mint a FRESH project id (because `<root>/.noir/project.id` is
|
|
628
808
|
* absent) and build a NESTED second project (`.noir/.noir/`, `.noir/CLAUDE.md`,
|
|
629
809
|
* `.noir/.claude/skills/`, …) — the duplicate-`.noir` bug. The walk ascends the
|
|
@@ -652,7 +832,7 @@ declare function scaffold(opts: ScaffoldOptions): Promise<ScaffoldResult>;
|
|
|
652
832
|
*/
|
|
653
833
|
/** The scaffold version this build of @noir-ai/create ships. Bumped atomically
|
|
654
834
|
* whenever a manifest entry, template, or migration changes shape. */
|
|
655
|
-
declare const CURRENT_SCAFFOLD_VERSION = "1.
|
|
835
|
+
declare const CURRENT_SCAFFOLD_VERSION = "1.2.0";
|
|
656
836
|
/** Path to the stamp file under `root`. Exposed for tests + doctor. */
|
|
657
837
|
declare function scaffoldVersionPath(root: string): string;
|
|
658
838
|
/** Read the applied scaffold version, or `null` if the stamp is absent/unparseable.
|
|
@@ -662,7 +842,7 @@ declare function readScaffoldVersion(root: string): string | null;
|
|
|
662
842
|
* LAST so a crash mid-scaffold leaves an old/absent stamp rather than a
|
|
663
843
|
* misleading fresh one.
|
|
664
844
|
*
|
|
665
|
-
*
|
|
845
|
+
* Routed through the package's atomic `regenerate()` writer (tmp+rename in
|
|
666
846
|
* the same dir) for consistency with the rest of the engine's durable writes —
|
|
667
847
|
* a half-written stamp would mislead `noir doctor`/`init --upgrade`, so the
|
|
668
848
|
* stamp deserves the same crash-atomicity as `.mcp.json` and the NOIR.md brief. */
|
|
@@ -698,4 +878,4 @@ declare function loadTemplate(name: string): string;
|
|
|
698
878
|
/** Resolve a template name to its absolute path (for diagnostics + tests). */
|
|
699
879
|
declare function templatesDir(): string;
|
|
700
880
|
|
|
701
|
-
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 };
|
|
881
|
+
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, SEED_TEMPLATE_HISTORY, type ScaffoldMode, type ScaffoldOptions, type ScaffoldResult, type SeedTemplateHistoryEntry, type StackInfo, type WriteMode, type WriteOutcome, ancestorsPath, applyInlineConflict, applyWithConflict, assertSafeRoot, buildHostArtifacts, buildManifest, buildRegion, detectStack, isStaleSeed, lineDiff, loadTemplate, managedBlock, mergeThreeWay, readAncestors, readScaffoldVersion, regenerate, render, runMigrations, scaffold, scaffoldVersionPath, skipIfExists, templatesDir, writeAncestors, writeScaffoldVersion, zdiff3Region };
|