@noir-ai/create 1.2.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/LICENSE +21 -0
- package/dist/index.d.ts +448 -0
- package/dist/index.js +712 -0
- package/dist/index.js.map +1 -0
- package/package.json +59 -0
- package/templates/claude-context-block.md.tmpl +1 -0
- package/templates/claude-rules-block.md.tmpl +1 -0
- package/templates/config.yml.tmpl +2 -0
- package/templates/dockerignore.tmpl +1 -0
- package/templates/gitignore.tmpl +4 -0
- package/templates/mcp.http.json.tmpl +8 -0
- package/templates/mcp.stdio.json.tmpl +12 -0
- package/templates/noir.md.tmpl +5 -0
- package/templates/npmignore.tmpl +1 -0
- package/templates/prettierignore.tmpl +1 -0
- package/templates/rules-seed.md.tmpl +34 -0
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 agaaaptr
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
package/dist/index.d.ts
ADDED
|
@@ -0,0 +1,448 @@
|
|
|
1
|
+
import { HostId, HostAdapter } from '@noir-ai/adapters';
|
|
2
|
+
import { ManagedBlock } from '@noir-ai/core';
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* The three-mode writer — generalizes keystone-K's `writeManagedRegion` into
|
|
6
|
+
* the declarative dispatch the scaffold manifest drives. Each mode maps 1:1 to
|
|
7
|
+
* an artifact class in the spec §4.5 matrix:
|
|
8
|
+
*
|
|
9
|
+
* - {@link regenerate} — pure pointers (`.mcp.json`, `NOIR.md` brief, …).
|
|
10
|
+
* Always overwritten, atomically.
|
|
11
|
+
* - {@link managedBlock} — co-owned files (`CLAUDE.md` context/rules,
|
|
12
|
+
* `.gitignore` noir block, …). DELEGATES to
|
|
13
|
+
* keystone-K's `writeManagedRegion` so user content
|
|
14
|
+
* outside the markers is preserved byte-for-byte and
|
|
15
|
+
* re-runs are idempotent. Never duplicate the
|
|
16
|
+
* managed-region logic.
|
|
17
|
+
* - {@link skipIfExists} — user-owned seeds (`RULES.md`, `config.yml`,
|
|
18
|
+
* `project.id`). Write once; never clobber.
|
|
19
|
+
*
|
|
20
|
+
* The orchestrator (`scaffold.ts`) is the only intended caller; the per-mode
|
|
21
|
+
* functions are exported so the cli (S-T2) and tests can drive them directly
|
|
22
|
+
* when a one-off write is needed outside the manifest.
|
|
23
|
+
*/
|
|
24
|
+
type WriteMode = 'regenerate' | 'managedBlock' | 'skipIfExists';
|
|
25
|
+
interface WriteOutcome {
|
|
26
|
+
/** The absolute path that was written. */
|
|
27
|
+
path: string;
|
|
28
|
+
mode: WriteMode;
|
|
29
|
+
/** true when bytes hit disk; false for skipIfExists no-ops. regenerate and
|
|
30
|
+
* managedBlock always write (managedBlock may write identical bytes — that
|
|
31
|
+
* still counts as a write for telemetry purposes; the file IS up to date). */
|
|
32
|
+
written: boolean;
|
|
33
|
+
}
|
|
34
|
+
/** Atomic overwrite. Writes to `<file>.tmp.<pid>.<rnd>` in the same directory,
|
|
35
|
+
* fsyncs, then renames over the target so a crash never leaves a half-written
|
|
36
|
+
* file (the pointer files this is used for are read by the host agent first —
|
|
37
|
+
* a truncated CLAUDE.md/.mcp.json would break the very startup Noir serves).
|
|
38
|
+
*
|
|
39
|
+
* Parent directories are NOT created here — the orchestrator does that once
|
|
40
|
+
* for the whole manifest so a missing dir is a single, attributable failure
|
|
41
|
+
* rather than N silent ones inside the writer. */
|
|
42
|
+
declare function regenerate(absPath: string, content: string): WriteOutcome;
|
|
43
|
+
/** Re-emit a managed region, delegating to keystone-K's `writeManagedRegion`.
|
|
44
|
+
* `regionText` MUST already include the begin/end markers (matches the shape
|
|
45
|
+
* `writeManagedRegion` expects and that `IGNORE_BLOCK`/`CONTEXT_BLOCK`
|
|
46
|
+
* callers build in core/cli). Use {@link buildRegion} to assemble it from a
|
|
47
|
+
* block + body. */
|
|
48
|
+
declare function managedBlock(absPath: string, block: ManagedBlock, regionText: string): WriteOutcome;
|
|
49
|
+
/** Assemble `<begin>\n<body>\n<end>\n` for a managed block. Centralized here so
|
|
50
|
+
* every caller (manifest rendering, tests, future migrations) produces the
|
|
51
|
+
* exact byte shape `writeManagedRegion` strips/expects. The trailing newline
|
|
52
|
+
* is part of the contract — `stripManagedBlock`'s regex eats a trailing `\n`
|
|
53
|
+
* so re-runs stay idempotent instead of accumulating blank lines.
|
|
54
|
+
*
|
|
55
|
+
* `body` is `trimEnd()`-ed before wrapping so template authors can keep the
|
|
56
|
+
* conventional trailing newline in `.tmpl` files without producing a
|
|
57
|
+
* double-newline before the end marker. This keeps the output byte-identical
|
|
58
|
+
* to `claudeAdapter.emitContext`/`emitRules` and core's `syncIgnores`, which
|
|
59
|
+
* S-T2 relies on for a diff-free refactor. */
|
|
60
|
+
declare function buildRegion(block: ManagedBlock, body: string): string;
|
|
61
|
+
/** Write `content` to `absPath` only if no file exists there. Returns whether
|
|
62
|
+
* bytes were written. Parent dirs are NOT created (orchestrator's job). */
|
|
63
|
+
declare function skipIfExists(absPath: string, content: string): WriteOutcome;
|
|
64
|
+
|
|
65
|
+
/**
|
|
66
|
+
* Declarative scaffold manifest — the single source of truth for what
|
|
67
|
+
* `init` / `create` / `sync` emit. Each entry is one artifact, tagged with its
|
|
68
|
+
* write {@link WriteMode} so the orchestrator can dispatch without knowing
|
|
69
|
+
* what's inside.
|
|
70
|
+
*
|
|
71
|
+
* FAITHFULNESS CONTRACT (S-T1 → S-T2 → S10): this table is a strict superset of
|
|
72
|
+
* the artifacts `packages/cli/src/{init,sync}.ts` wrote pre-Slice-S. The cli
|
|
73
|
+
* refactor (S-T2) replaced those ad-hoc writers with a call into `scaffold()`;
|
|
74
|
+
* the byte-for-byte output MUST stay equivalent for first-run init. S10 makes
|
|
75
|
+
* the manifest HOST-PARAMETRIC: {@link buildManifest} now returns host-agnostic
|
|
76
|
+
* entries + a {@link buildHostArtifacts} call that materializes per-host files
|
|
77
|
+
* (CLAUDE.md/GEMINI.md for claude/gemini; AGENTS.md + .cursor/.../opencode.json
|
|
78
|
+
* for agents-md/cursor/opencode) via the resolved adapter. The claude default
|
|
79
|
+
* `noir init` stays BYTE-IDENTICAL to v1.1 — fix-wave I1 REMOVED the additive
|
|
80
|
+
* root `AGENTS.md` (it was double-importing `.noir/NOIR.md` + RULES.md via
|
|
81
|
+
* CLAUDE.md's existing @-imports; claude's native surface is CLAUDE.md alone).
|
|
82
|
+
*
|
|
83
|
+
* Path-derivation: repo-relative POSIX strings that mirror
|
|
84
|
+
* `@noir-ai/core/layout.ts` (`paths.*`). The test suite asserts
|
|
85
|
+
* `join(root, entry.path) === paths.X(root)` for every entry layout knows
|
|
86
|
+
* about, so a layout rename is caught here instead of silently drifting.
|
|
87
|
+
*/
|
|
88
|
+
/** S10: `HostTag` is now the SAME `HostId` enum the adapter registry uses
|
|
89
|
+
* (re-exported so existing imports keep working). Pre-S10 this was the
|
|
90
|
+
* literal `'claude'`; widening to `HostId` lets one manifest serve every host
|
|
91
|
+
* via the orchestrator's host filter + {@link buildHostArtifacts}. */
|
|
92
|
+
type HostTag = HostId;
|
|
93
|
+
interface ManifestEntry {
|
|
94
|
+
/** Repo-relative POSIX path (forward slashes). Orchestrator joins with root. */
|
|
95
|
+
path: string;
|
|
96
|
+
mode: WriteMode;
|
|
97
|
+
/** Host tag; entry is skipped when opts.host !== entry.host.
|
|
98
|
+
* Undefined = host-agnostic (every host emits it). */
|
|
99
|
+
host?: HostTag;
|
|
100
|
+
/** Required for `managedBlock` mode: the named block to re-emit. */
|
|
101
|
+
block?: ManagedBlock;
|
|
102
|
+
/** Literal content (`regenerate`/`skipIfExists`) or literal region BODY
|
|
103
|
+
* (`managedBlock` — the orchestrator wraps it with the block markers).
|
|
104
|
+
* Mutually exclusive with {@link template}. */
|
|
105
|
+
content?: string;
|
|
106
|
+
/** Template name (resolved by `template-loader`) for content/body.
|
|
107
|
+
* Mutually exclusive with {@link content}. */
|
|
108
|
+
template?: string;
|
|
109
|
+
/** One-line human description for `noir doctor` + logs. */
|
|
110
|
+
description?: string;
|
|
111
|
+
}
|
|
112
|
+
type BuildManifestContext = {
|
|
113
|
+
/** Absolute repo root. Added in S10 so {@link buildHostArtifacts} can resolve
|
|
114
|
+
* absolute adapter paths (`adapter.mcpConfigPath({root})`, etc.) to the
|
|
115
|
+
* manifest's repo-relative POSIX shape. */
|
|
116
|
+
root: string;
|
|
117
|
+
/** Canonical project id (already created/read by the orchestrator). */
|
|
118
|
+
projectId: string;
|
|
119
|
+
/** Target host. Drives {@link buildHostArtifacts} via `resolveAdapter(host)`. */
|
|
120
|
+
host: HostTag;
|
|
121
|
+
/** MCP transport the host should use to reach Noir. */
|
|
122
|
+
transport: 'stdio' | 'streamable-http';
|
|
123
|
+
/** Required when transport is `streamable-http`. */
|
|
124
|
+
url?: string;
|
|
125
|
+
};
|
|
126
|
+
/** Co-owned NOIR.md auto-brief region. Defined locally (not exported from
|
|
127
|
+
* core) because core's keystone-K named instances cover only the three
|
|
128
|
+
* regions core itself writes (context/rules/ignore); the brief is the
|
|
129
|
+
* scaffold engine's own. Uses the SAME `managedBlock()` factory so marker
|
|
130
|
+
* shape stays consistent with the rest of the family. */
|
|
131
|
+
declare const BRIEF_BLOCK: ManagedBlock;
|
|
132
|
+
declare const MANIFEST_PATH_PARITY: ReadonlyArray<[
|
|
133
|
+
entryPath: string,
|
|
134
|
+
layoutFn: (root: string) => string
|
|
135
|
+
]>;
|
|
136
|
+
/**
|
|
137
|
+
* Build the manifest for a given ctx. Pure (no I/O). The orchestrator calls
|
|
138
|
+
* this once per scaffold run; tests assert the shape is stable.
|
|
139
|
+
*
|
|
140
|
+
* S10 structure: the manifest is now `[...hostAgnosticEntries(ctx), ...hostSpecificEntries(ctx)]`
|
|
141
|
+
* where the host-specific half comes from {@link buildHostArtifacts} (driven by
|
|
142
|
+
* `resolveAdapter(ctx.host)`). The host-agnostic half is unchanged from v1.1
|
|
143
|
+
* (canonical `.noir/` store + ignore files). Fix-wave I1: {@link buildHostArtifacts}
|
|
144
|
+
* emits AGENTS.md ONLY for agents-md/cursor/opencode (claude/gemini use their
|
|
145
|
+
* own CLAUDE.md/GEMINI.md — emitting AGENTS.md too would double-import .noir/).
|
|
146
|
+
*
|
|
147
|
+
* Mode-tagging rationale per artifact (see S-T1 report for the full table):
|
|
148
|
+
* - `project.id` → skipIfExists. First init writes a fresh id; re-init MUST
|
|
149
|
+
* NOT overwrite — that would orphan the indexed store DB named after it.
|
|
150
|
+
* - `config.yml` → skipIfExists. User-owned; the seed is written once.
|
|
151
|
+
* (The seed renders `host: {{host}}` so a `--host gemini` init persists the
|
|
152
|
+
* chosen host for `noir sync` to read back.)
|
|
153
|
+
* - `NOIR.md` → managedBlock (BRIEF_BLOCK). Auto-brief is co-owned.
|
|
154
|
+
* - `RULES.md` → skipIfExists. User-owned working-contract seed.
|
|
155
|
+
* - ignore files → managedBlock (IGNORE_BLOCK). Matches syncIgnores.
|
|
156
|
+
* - host entries → SEE {@link buildHostArtifacts} (regenerate / managedBlock).
|
|
157
|
+
*/
|
|
158
|
+
declare function buildManifest(ctx: BuildManifestContext): ManifestEntry[];
|
|
159
|
+
/** Context shape passed to {@link buildHostArtifacts}. A strict subset of
|
|
160
|
+
* {@link BuildManifestContext} (no `projectId`/`host` — the adapter IS the
|
|
161
|
+
* resolved host, and host artifacts never need the project id). Exported
|
|
162
|
+
* separately so callers + tests can name the narrower contract. */
|
|
163
|
+
interface BuildHostArtifactsContext {
|
|
164
|
+
root: string;
|
|
165
|
+
transport: 'stdio' | 'streamable-http';
|
|
166
|
+
url?: string;
|
|
167
|
+
}
|
|
168
|
+
/**
|
|
169
|
+
* Materialize the host-specific manifest entries from a resolved adapter.
|
|
170
|
+
* SINGLE entry point — no scattered `if (host === '…')` conditionals in the
|
|
171
|
+
* orchestrator. Returns entries in emission order:
|
|
172
|
+
*
|
|
173
|
+
* 1. **AGENTS.md** (universal baseline) — `regenerate` at
|
|
174
|
+
* `adapter.agentsMdPath(ctx)` (default `<root>/AGENTS.md`), content from
|
|
175
|
+
* the shared `emitAgentsMd(ctx)` helper. Emitted ONLY for hosts whose
|
|
176
|
+
* `emitContext` IS the AGENTS.md content (agents-md, cursor, opencode) —
|
|
177
|
+
* for them AGENTS.md is the SINGLE native context surface AND carries the
|
|
178
|
+
* Noir working rules via its `@.noir/rules/RULES.md` import. claude and
|
|
179
|
+
* gemini have their OWN native context file (CLAUDE.md / GEMINI.md) that
|
|
180
|
+
* `@`-imports the canonical `.noir/` sources; emitting AGENTS.md too
|
|
181
|
+
* would IMPORT THOSE FILES TWICE into the host's context (2× tokens +
|
|
182
|
+
* drift risk), so for those two hosts AGENTS.md is SKIPPED. (Claude Code
|
|
183
|
+
* still discovers AGENTS.md at the repo root when present — users who
|
|
184
|
+
* want the universal file can drop one in by hand; Noir's auto-emission
|
|
185
|
+
* stays single-source per host.)
|
|
186
|
+
* 2. **Host-native context file** — emitted ONLY for hosts whose `emitContext`
|
|
187
|
+
* is NOT the AGENTS.md content (i.e. the host has its OWN context file
|
|
188
|
+
* with a distinct syntax). Concretely: claude → `CLAUDE.md` (CONTEXT +
|
|
189
|
+
* RULES managed blocks, byte-identical to v1.1 via templates); gemini →
|
|
190
|
+
* `GEMINI.md` (CONTEXT + RULES managed blocks with Gemini's bare `@`
|
|
191
|
+
* import syntax). For `agents-md`/`cursor`/`opencode` the context IS the
|
|
192
|
+
* AGENTS.md (already emitted in step 1) → SKIP to avoid a duplicate.
|
|
193
|
+
* Rules live INSIDE the host's context file: claude's in CLAUDE.md,
|
|
194
|
+
* gemini's in GEMINI.md, agents-md/cursor/opencode's in AGENTS.md — NO
|
|
195
|
+
* host emits a separate rules file. (The prior cursor
|
|
196
|
+
* `.cursor/rules/noir-contract.mdc` host-rules pointer was REMOVED: it
|
|
197
|
+
* collided with the C3 cursor flat-skill prune of `noir-*.mdc` under
|
|
198
|
+
* `.cursor/rules/`, and cursor's rules are already delivered via
|
|
199
|
+
* AGENTS.md's `@.noir/rules/RULES.md` import.)
|
|
200
|
+
* 3. **Host MCP config** — `regenerate` at `adapter.mcpConfigPath(ctx)`
|
|
201
|
+
* (default `<root>/.mcp.json` for claude), content from
|
|
202
|
+
* `adapter.emitMcpConfig(ctx, {transport,url})`. Claude KEEPS the template
|
|
203
|
+
* path (byte-identical parity with v1.1 + the .mcp.json parity test that
|
|
204
|
+
* compares against `claudeAdapter.emitMcpConfig`); other hosts use the
|
|
205
|
+
* adapter directly.
|
|
206
|
+
*
|
|
207
|
+
* Skills are OUT OF SCOPE here — the cli composes `emitSkillsToDir` with
|
|
208
|
+
* `adapter.skillsDir` + the host's `CompileTarget` (claude → `.claude/skills/`
|
|
209
|
+
* as SKILL.md; cursor → `.cursor/rules/<skill>.mdc` FLAT per C3; gemini/
|
|
210
|
+
* agents-md/opencode have no skill dir → skip).
|
|
211
|
+
*/
|
|
212
|
+
declare function buildHostArtifacts(adapter: HostAdapter, ctx: BuildHostArtifactsContext): ManifestEntry[];
|
|
213
|
+
|
|
214
|
+
/**
|
|
215
|
+
* Migration model — Nx-style declarative upgrade scripts, hand-rolled.
|
|
216
|
+
*
|
|
217
|
+
* Per the Slice S design (S-OQ2 resolved → inline conflict markers, never
|
|
218
|
+
* interactive prompts), every migration script:
|
|
219
|
+
* - is a small, IDEMPOTENT TypeScript function (`run(root, opts) → result`).
|
|
220
|
+
* Re-running it on an already-migrated tree is a no-op (or produces the
|
|
221
|
+
* same conflict markers again — also safe).
|
|
222
|
+
* - NEVER prompts. `--no-input` CI must survive. When a migration cannot
|
|
223
|
+
* reconcile a co-edited region, it writes git-style conflict markers
|
|
224
|
+
* (`<<<<<<<` / `=======` / `>>>>>>>`) into the file and records the path in
|
|
225
|
+
* `result.conflicts` so the caller (doctor/CI) can fail loudly with a
|
|
226
|
+
* reviewable artifact rather than hang on a prompt.
|
|
227
|
+
*
|
|
228
|
+
* The registry ({@link MIGRATIONS}) is the deliverable at v1.0.0 — there are no
|
|
229
|
+
* real version-to-version migrations yet. The synthetic `1.0.0 → 1.0.0`
|
|
230
|
+
* migration exists to exercise the runner end-to-end and to give a template
|
|
231
|
+
* for the first real migration.
|
|
232
|
+
*/
|
|
233
|
+
interface MigrationContext {
|
|
234
|
+
/** Absolute repo root the migration operates on. */
|
|
235
|
+
root: string;
|
|
236
|
+
/** When true, the runner performs all reads and the would-be writes, returns
|
|
237
|
+
* the same `changed`/`conflicts` shape, but does NOT touch disk. Used by
|
|
238
|
+
* `noir doctor`/CI to preview. */
|
|
239
|
+
dryRun?: boolean;
|
|
240
|
+
}
|
|
241
|
+
interface MigrationResult {
|
|
242
|
+
/** Repo-relative paths the migration modified (or would modify, in dryRun). */
|
|
243
|
+
changed: string[];
|
|
244
|
+
/** Repo-relative paths left with inline conflict markers. Always non-throwing. */
|
|
245
|
+
conflicts: string[];
|
|
246
|
+
/** Free-form notes for doctor/CI logs (e.g. "rewrote CLAUDE.md import"). */
|
|
247
|
+
notes: string[];
|
|
248
|
+
}
|
|
249
|
+
/** A single version-to-version step. `from`/`to` are inclusive endpoints on a
|
|
250
|
+
* linear history; the runner composes a chain by matching `from` of the next
|
|
251
|
+
* script to `to` of the previous. */
|
|
252
|
+
interface MigrationScript {
|
|
253
|
+
from: string;
|
|
254
|
+
to: string;
|
|
255
|
+
description: string;
|
|
256
|
+
run: (ctx: MigrationContext) => MigrationResult;
|
|
257
|
+
}
|
|
258
|
+
|
|
259
|
+
/**
|
|
260
|
+
* Migration runner. Given a `from` version present on disk and a target `to`
|
|
261
|
+
* version (usually {@link CURRENT_SCAFFOLD_VERSION}), walks the
|
|
262
|
+
* {@link MIGRATIONS} registry forward in version order, executing each step.
|
|
263
|
+
*
|
|
264
|
+
* Selection rule: a script runs when its `from` is `>= fromArg` (semver-ish
|
|
265
|
+
* string compare is enough at Noir's scale; we don't pull in a semver dep for
|
|
266
|
+
* this) AND its `to` is `<= toArg`. Scripts strictly outside the window are
|
|
267
|
+
* skipped. Within the window, scripts run sorted by `to` ascending so a
|
|
268
|
+
* multi-step upgrade (1.0.0 → 1.1.0 → 1.2.0) composes in order.
|
|
269
|
+
*
|
|
270
|
+
* The runner NEVER throws on a per-script failure — it captures the error,
|
|
271
|
+
* records a synthetic conflict entry (`<path>__error`), and continues so a
|
|
272
|
+
* single broken migration doesn't block the rest of the chain. The orchestrator
|
|
273
|
+
* decides whether non-empty `conflicts` is a hard failure (init --upgrade) or a
|
|
274
|
+
* warning (doctor).
|
|
275
|
+
*
|
|
276
|
+
* Returns the aggregate of every step's `changed`/`conflicts`/`notes`.
|
|
277
|
+
*/
|
|
278
|
+
declare function runMigrations(root: string, from: string | null, to: string, opts?: {
|
|
279
|
+
dryRun?: boolean;
|
|
280
|
+
}): MigrationResult & {
|
|
281
|
+
from: string | null;
|
|
282
|
+
to: string;
|
|
283
|
+
ran: string[];
|
|
284
|
+
};
|
|
285
|
+
|
|
286
|
+
declare const MIGRATIONS: readonly MigrationScript[];
|
|
287
|
+
/** Write git-style inline conflict markers around `theirs`/`ours` so a human
|
|
288
|
+
* or AI agent can resolve later. This is the CI-safe fallback the spec locks
|
|
289
|
+
* in (S-OQ2) — no interactive prompts, ever. */
|
|
290
|
+
declare function applyInlineConflict(ours: string, theirs: string, oursLabel?: string, theirsLabel?: string): string;
|
|
291
|
+
/** Apply `(ours, theirs)` to a region: if they're equal, return `ours` (no
|
|
292
|
+
* conflict); otherwise emit inline markers. Migration authors should prefer
|
|
293
|
+
* this over {@link applyInlineConflict} when the "no change needed" case is
|
|
294
|
+
* common — it keeps re-runs truly idempotent (no spurious markers on a clean
|
|
295
|
+
* tree). */
|
|
296
|
+
declare function applyWithConflict(ours: string, theirs: string, path: string): {
|
|
297
|
+
text: string;
|
|
298
|
+
conflicted: boolean;
|
|
299
|
+
};
|
|
300
|
+
|
|
301
|
+
/**
|
|
302
|
+
* READ-ONLY stack detection. Probes for well-known marker files under `root`
|
|
303
|
+
* and reports ONLY what is present — never assumes. The result feeds path
|
|
304
|
+
* adaptation (where to drop `.claude/` vs `.cursor/` etc., future), ignore-file
|
|
305
|
+
* selection, and the onboarding TUI's confirm step.
|
|
306
|
+
*
|
|
307
|
+
* Design rules (spec §4.5):
|
|
308
|
+
* - Never throws. A foreign/empty dir returns `{ languages: [], monorepo:
|
|
309
|
+
* false, frameworks: [], packageManager: null }`.
|
|
310
|
+
* - Never opens network, never parses code beyond a `package.json`/`pyproject`
|
|
311
|
+
* dependency list. Marker files + top-level manifests only.
|
|
312
|
+
* - Frameworks are reported only when both the marker file is present AND the
|
|
313
|
+
* framework's dependency is listed (avoids false positives from a stale
|
|
314
|
+
* `package.json`).
|
|
315
|
+
*/
|
|
316
|
+
interface StackInfo {
|
|
317
|
+
/** Lower-cased language ids found via marker files:
|
|
318
|
+
* `typescript` | `javascript` | `python` | `go` | `rust`. */
|
|
319
|
+
languages: string[];
|
|
320
|
+
/** True when a workspace manifest is present (pnpm-workspace, npm/yarn
|
|
321
|
+
* workspaces in package.json, turbo.json, nx.json). */
|
|
322
|
+
monorepo: boolean;
|
|
323
|
+
/** Lower-cased framework ids (e.g. `next`, `vite`, `express`, `fastapi`,
|
|
324
|
+
* `actix`). Empty when none detected. */
|
|
325
|
+
frameworks: string[];
|
|
326
|
+
/** `pnpm` | `npm` | `yarn` when a lockfile is present, else null. */
|
|
327
|
+
packageManager: string | null;
|
|
328
|
+
}
|
|
329
|
+
declare function detectStack(root: string): StackInfo;
|
|
330
|
+
|
|
331
|
+
/**
|
|
332
|
+
* Scaffold orchestrator. One function the cli (S-T2) calls for `noir init`,
|
|
333
|
+
* `noir create`, and `noir sync`; mode selects the manifest subset + how
|
|
334
|
+
* project identity is resolved.
|
|
335
|
+
*
|
|
336
|
+
* High-level flow:
|
|
337
|
+
* 1. Resolve project id (provided > existing > generated; sync requires existing).
|
|
338
|
+
* 2. Detect stack (READ-ONLY; always runs; never throws).
|
|
339
|
+
* 3. If {@link ScaffoldOptions.upgrade}, run migrations from the on-disk
|
|
340
|
+
* scaffold-version → {@link CURRENT_SCAFFOLD_VERSION}.
|
|
341
|
+
* 4. Build the manifest, filter by host + mode.
|
|
342
|
+
* 5. For each entry: mkdir -p, render template/content, dispatch to the
|
|
343
|
+
* matching writer.
|
|
344
|
+
* 6. On `init`/`create`, stamp `.noir/scaffold-version` LAST so a crash leaves
|
|
345
|
+
* an old/absent stamp rather than a misleading fresh one.
|
|
346
|
+
*
|
|
347
|
+
* The orchestrator owns dir creation (writers refuse to so that a missing dir
|
|
348
|
+
* is one attributable failure, not N silent ones).
|
|
349
|
+
*/
|
|
350
|
+
type ScaffoldMode = 'init' | 'create' | 'sync';
|
|
351
|
+
interface ScaffoldOptions {
|
|
352
|
+
/** Absolute repo root. For `create`, the new dir (created if absent). */
|
|
353
|
+
root: string;
|
|
354
|
+
mode: ScaffoldMode;
|
|
355
|
+
/** Target host. Defaults to `'claude'` (the only shipped host). */
|
|
356
|
+
host?: HostTag;
|
|
357
|
+
/** MCP transport. Defaults to `'stdio'`. */
|
|
358
|
+
transport?: 'stdio' | 'streamable-http';
|
|
359
|
+
/** Required when transport is `streamable-http`. */
|
|
360
|
+
url?: string;
|
|
361
|
+
/** Explicit project id; bypasses generate/read. Mainly for tests + `create`
|
|
362
|
+
* flows that want deterministic ids. */
|
|
363
|
+
projectId?: string;
|
|
364
|
+
/** `noir init --upgrade`: run migrations before re-emitting, and emit only
|
|
365
|
+
* regenerate + managedBlock (skipIfExists left alone). Only meaningful
|
|
366
|
+
* with mode `'init'`. */
|
|
367
|
+
upgrade?: boolean;
|
|
368
|
+
/** Preview: compute the same written/skipped/migrated lists without touching
|
|
369
|
+
* disk. `noir doctor`/CI use this to report drift. */
|
|
370
|
+
dryRun?: boolean;
|
|
371
|
+
}
|
|
372
|
+
interface ScaffoldResult {
|
|
373
|
+
/** Repo-relative paths actually written. */
|
|
374
|
+
written: string[];
|
|
375
|
+
/** Repo-relative paths skipIfExists'd (already present). */
|
|
376
|
+
skipped: string[];
|
|
377
|
+
/** Migration steps executed (`<from>→<to>`), when upgrade ran. */
|
|
378
|
+
migrationsRan: string[];
|
|
379
|
+
/** Migration conflicts (repo-relative or `<runner>:…`), when upgrade ran. */
|
|
380
|
+
migrationConflicts: string[];
|
|
381
|
+
stack: StackInfo;
|
|
382
|
+
projectId: string;
|
|
383
|
+
fromVersion: string | null;
|
|
384
|
+
toVersion: string;
|
|
385
|
+
/** The host actually emitted (post-default). */
|
|
386
|
+
host: HostTag;
|
|
387
|
+
}
|
|
388
|
+
declare function scaffold(opts: ScaffoldOptions): Promise<ScaffoldResult>;
|
|
389
|
+
|
|
390
|
+
/**
|
|
391
|
+
* Scaffold version stamp — Noir's equivalent of Copier's `last-applied`.
|
|
392
|
+
* Stored at `.noir/scaffold-version` as a single `noir-scaffold=<semver>` line
|
|
393
|
+
* so `noir init --upgrade` / `noir doctor` can diff against
|
|
394
|
+
* {@link CURRENT_SCAFFOLD_VERSION} and decide which migrations to run.
|
|
395
|
+
*
|
|
396
|
+
* Format is line-oriented (not YAML) on purpose: it must be readable before
|
|
397
|
+
* `config.yml` is parsed (doctor runs even with a broken config), and the
|
|
398
|
+
* `key=value` shape is trivial to grep from a shell.
|
|
399
|
+
*/
|
|
400
|
+
/** The scaffold version this build of @noir-ai/create ships. Bumped atomically
|
|
401
|
+
* whenever a manifest entry, template, or migration changes shape. */
|
|
402
|
+
declare const CURRENT_SCAFFOLD_VERSION = "1.0.0";
|
|
403
|
+
/** Path to the stamp file under `root`. Exposed for tests + doctor. */
|
|
404
|
+
declare function scaffoldVersionPath(root: string): string;
|
|
405
|
+
/** Read the applied scaffold version, or `null` if the stamp is absent/unparseable.
|
|
406
|
+
* Never throws — doctor must keep reporting even on a malformed stamp. */
|
|
407
|
+
declare function readScaffoldVersion(root: string): string | null;
|
|
408
|
+
/** Write the stamp, creating `.noir/` if needed. The orchestrator writes it
|
|
409
|
+
* LAST so a crash mid-scaffold leaves an old/absent stamp rather than a
|
|
410
|
+
* misleading fresh one.
|
|
411
|
+
*
|
|
412
|
+
* N2: routed through the package's atomic `regenerate()` writer (tmp+rename in
|
|
413
|
+
* the same dir) for consistency with the rest of the engine's durable writes —
|
|
414
|
+
* a half-written stamp would mislead `noir doctor`/`init --upgrade`, so the
|
|
415
|
+
* stamp deserves the same crash-atomicity as `.mcp.json` and the NOIR.md brief. */
|
|
416
|
+
declare function writeScaffoldVersion(root: string, version: string): void;
|
|
417
|
+
|
|
418
|
+
/**
|
|
419
|
+
* Hand-rolled `{{var}}` interpolation. No mustache/handlebars dependency —
|
|
420
|
+
* Noir's markdown/yaml/json templates are simple enough that a tokenizer +
|
|
421
|
+
* map lookup is sufficient and keeps `@noir-ai/create` dependency-light.
|
|
422
|
+
*
|
|
423
|
+
* Semantics (documented):
|
|
424
|
+
* - Tokens are `{{ name }}` / `{{name}}` — any amount of ASCII whitespace
|
|
425
|
+
* inside the braces is permitted. The name is trimmed.
|
|
426
|
+
* - Known vars (value is a string) are substituted verbatim. NO escaping is
|
|
427
|
+
* applied — callers must pre-escape for the target format (JSON/YAML/MD).
|
|
428
|
+
* This is intentional: the engine renders into multiple formats and a
|
|
429
|
+
* single universal escaper would be wrong for at least one of them.
|
|
430
|
+
* - Unknown vars (not in `vars`, or value `undefined`) → the original token
|
|
431
|
+
* is LEFT IN PLACE. Rationale: drift between template and ctx becomes
|
|
432
|
+
* visible (an unrendered `{{projectId}}` in CLAUDE.md is immediately
|
|
433
|
+
* obvious), rather than silently swallowed to empty. The spec called this
|
|
434
|
+
* out as the preferred failure mode.
|
|
435
|
+
* - Non-string var values are stringified via `String(value)` so a stray
|
|
436
|
+
* number/boolean still interpolates rather than printing `[object Object]`.
|
|
437
|
+
* - A lone `{{` with no closing `}}` is left untouched (not a token).
|
|
438
|
+
*/
|
|
439
|
+
declare function render(template: string, vars: Record<string, unknown>): string;
|
|
440
|
+
|
|
441
|
+
/** Read a template's raw text by name (e.g. `noir.md.tmpl`). Throws on missing
|
|
442
|
+
* — an unknown template is a manifest bug and should fail loudly, not render
|
|
443
|
+
* an empty string silently. */
|
|
444
|
+
declare function loadTemplate(name: string): string;
|
|
445
|
+
/** Resolve a template name to its absolute path (for diagnostics + tests). */
|
|
446
|
+
declare function templatesDir(): string;
|
|
447
|
+
|
|
448
|
+
export { BRIEF_BLOCK, type BuildHostArtifactsContext, type BuildManifestContext, CURRENT_SCAFFOLD_VERSION, type HostTag, MANIFEST_PATH_PARITY, MIGRATIONS, type ManifestEntry, type MigrationContext, type MigrationResult, type MigrationScript, type ScaffoldMode, type ScaffoldOptions, type ScaffoldResult, type StackInfo, type WriteMode, type WriteOutcome, applyInlineConflict, applyWithConflict, buildHostArtifacts, buildManifest, buildRegion, detectStack, loadTemplate, managedBlock, readScaffoldVersion, regenerate, render, runMigrations, scaffold, scaffoldVersionPath, skipIfExists, templatesDir, writeScaffoldVersion };
|