@noir-ai/create 1.15.0 → 1.16.0-beta.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/index.d.ts +72 -20
- package/dist/index.js +238 -76
- package/dist/index.js.map +1 -1
- package/package.json +3 -3
- package/templates/rules-seed.md.tmpl +15 -7
package/dist/index.js
CHANGED
|
@@ -32,10 +32,11 @@ function writeAncestors(root, map) {
|
|
|
32
32
|
|
|
33
33
|
// src/manifest.ts
|
|
34
34
|
import { existsSync as existsSync2 } from "fs";
|
|
35
|
-
import { join as
|
|
35
|
+
import { join as join3, relative } from "path";
|
|
36
36
|
import {
|
|
37
37
|
AGENTS_MD_FILENAME,
|
|
38
38
|
emitAgentsMd,
|
|
39
|
+
mcpConfigPathForAdapter,
|
|
39
40
|
resolveAdapter
|
|
40
41
|
} from "@noir-ai/adapters";
|
|
41
42
|
import {
|
|
@@ -44,10 +45,48 @@ import {
|
|
|
44
45
|
managedBlock,
|
|
45
46
|
NOIR_DIR,
|
|
46
47
|
paths,
|
|
47
|
-
RULES_BLOCK
|
|
48
|
+
RULES_BLOCK,
|
|
49
|
+
readWorkspaceMarker
|
|
48
50
|
} from "@noir-ai/core";
|
|
51
|
+
|
|
52
|
+
// src/template.ts
|
|
53
|
+
var TOKEN = /\{\{\s*([^}{}]+?)\s*\}\}/g;
|
|
54
|
+
function render(template, vars) {
|
|
55
|
+
return template.replace(TOKEN, (whole, name) => {
|
|
56
|
+
if (!Object.hasOwn(vars, name)) return whole;
|
|
57
|
+
const v = vars[name];
|
|
58
|
+
if (v === void 0 || v === null) return whole;
|
|
59
|
+
return String(v);
|
|
60
|
+
});
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
// src/template-loader.ts
|
|
64
|
+
import { readFileSync as readFileSync2 } from "fs";
|
|
65
|
+
import { dirname as dirname2, join as join2, resolve } from "path";
|
|
66
|
+
import { fileURLToPath } from "url";
|
|
67
|
+
var DEFAULT_TEMPLATES_DIR = resolveTemplatesDir();
|
|
68
|
+
function resolveTemplatesDir() {
|
|
69
|
+
const override = process.env.NOIR_TEMPLATES_DIR;
|
|
70
|
+
if (override && override.length > 0) return resolve(override);
|
|
71
|
+
const here = dirname2(fileURLToPath(import.meta.url));
|
|
72
|
+
return join2(here, "..", "templates");
|
|
73
|
+
}
|
|
74
|
+
function loadTemplate(name) {
|
|
75
|
+
return readFileSync2(join2(DEFAULT_TEMPLATES_DIR, name), "utf8");
|
|
76
|
+
}
|
|
77
|
+
function templatesDir() {
|
|
78
|
+
return DEFAULT_TEMPLATES_DIR;
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
// src/manifest.ts
|
|
49
82
|
var BRIEF_BLOCK = managedBlock("brief", "html");
|
|
50
83
|
var README_BLOCK = managedBlock("readme", "html");
|
|
84
|
+
var RULES_MAP_ROW_PREFIX = "| `rules/RULES.md` |";
|
|
85
|
+
function readmeMapContent(ctx) {
|
|
86
|
+
const rendered = render(loadTemplate("noir-readme.md.tmpl"), ctx);
|
|
87
|
+
if (ctx.rulesEnabled !== false) return rendered;
|
|
88
|
+
return rendered.split("\n").filter((line) => !line.startsWith(RULES_MAP_ROW_PREFIX)).join("\n");
|
|
89
|
+
}
|
|
51
90
|
var P = {
|
|
52
91
|
projectId: `${NOIR_DIR}/project.id`,
|
|
53
92
|
config: `${NOIR_DIR}/config.yml`,
|
|
@@ -100,8 +139,9 @@ function hostAgnosticEntries(ctx) {
|
|
|
100
139
|
// all-comment, so the file parses to an EMPTY overlay and creating it
|
|
101
140
|
// changes no behaviour — it exists so the user never has to copy the
|
|
102
141
|
// example by hand. `fileMode: 0o600` because the file holds tokens the
|
|
103
|
-
// moment the user edits it
|
|
104
|
-
//
|
|
142
|
+
// moment the user edits it. The mode is applied when the file is created;
|
|
143
|
+
// a copy that predates this contract (or that an editor saved by rename)
|
|
144
|
+
// has its mode re-asserted on every init/sync/create run — see `ensureOwnerOnly`.
|
|
105
145
|
path: P.env,
|
|
106
146
|
mode: "skipIfExists",
|
|
107
147
|
template: "config.env.tmpl",
|
|
@@ -130,10 +170,14 @@ function hostAgnosticEntries(ctx) {
|
|
|
130
170
|
// next. Host-agnostic: it describes the canonical store, which every
|
|
131
171
|
// host shares. Co-owned (README_BLOCK) so user notes survive while the
|
|
132
172
|
// map stays current through `noir sync` / `init --upgrade`.
|
|
173
|
+
//
|
|
174
|
+
// Rendered here rather than left to `renderEntry` because one row of the
|
|
175
|
+
// map depends on the project: the working-rules row is dropped when
|
|
176
|
+
// `rules.enabled` is off (see {@link readmeMapContent}).
|
|
133
177
|
path: P.readme,
|
|
134
178
|
mode: "managedBlock",
|
|
135
179
|
block: README_BLOCK,
|
|
136
|
-
|
|
180
|
+
content: readmeMapContent(ctx),
|
|
137
181
|
description: ".noir/ runtime map (what exists now / what appears later)"
|
|
138
182
|
},
|
|
139
183
|
// --- ignore files (host-agnostic; co-owned via IGNORE_BLOCK) ------------
|
|
@@ -168,21 +212,24 @@ function hostAgnosticEntries(ctx) {
|
|
|
168
212
|
];
|
|
169
213
|
const isEmpty = (ctx.stack?.languages?.length ?? 0) === 0 && !ctx.stack?.packageManager;
|
|
170
214
|
const isJs = isEmpty || (ctx.stack?.languages?.some((l) => l === "typescript" || l === "javascript") ?? false) || ["npm", "pnpm", "yarn"].includes(ctx.stack?.packageManager ?? "");
|
|
171
|
-
const hasDocker = isEmpty || existsSync2(
|
|
215
|
+
const hasDocker = isEmpty || existsSync2(join3(ctx.root, "Dockerfile")) || existsSync2(join3(ctx.root, "docker-compose.yml")) || existsSync2(join3(ctx.root, "compose.yaml"));
|
|
216
|
+
const rulesEnabled = ctx.rulesEnabled !== false;
|
|
172
217
|
return entries.filter((e) => {
|
|
218
|
+
if (e.path === P.rulesMd) return rulesEnabled;
|
|
173
219
|
if (e.path === ".npmignore" || e.path === ".prettierignore") return isJs;
|
|
174
220
|
if (e.path === ".dockerignore") return hasDocker;
|
|
175
221
|
return true;
|
|
176
222
|
});
|
|
177
223
|
}
|
|
178
224
|
function buildHostArtifacts(adapter, ctx) {
|
|
179
|
-
const
|
|
225
|
+
const rulesEnabled = ctx.rulesEnabled !== false;
|
|
226
|
+
const ectx = { root: ctx.root, rulesEnabled };
|
|
180
227
|
const host = adapter.id;
|
|
181
228
|
const entries = [];
|
|
182
229
|
const emitsAgentsMd = host === "agents-md" || host === "cursor" || host === "opencode";
|
|
183
230
|
if (emitsAgentsMd) {
|
|
184
231
|
entries.push({
|
|
185
|
-
path: hostRel(adapter.agentsMdPath?.(ectx) ??
|
|
232
|
+
path: hostRel(adapter.agentsMdPath?.(ectx) ?? join3(ctx.root, AGENTS_MD_FILENAME), ctx.root),
|
|
186
233
|
mode: "regenerate",
|
|
187
234
|
host,
|
|
188
235
|
content: emitAgentsMd(ectx),
|
|
@@ -199,14 +246,16 @@ function buildHostArtifacts(adapter, ctx) {
|
|
|
199
246
|
template: "claude-context-block.md.tmpl",
|
|
200
247
|
description: "CLAUDE.md context @import block"
|
|
201
248
|
});
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
249
|
+
if (rulesEnabled) {
|
|
250
|
+
entries.push({
|
|
251
|
+
path: "CLAUDE.md",
|
|
252
|
+
mode: "managedBlock",
|
|
253
|
+
host,
|
|
254
|
+
block: RULES_BLOCK,
|
|
255
|
+
template: "claude-rules-block.md.tmpl",
|
|
256
|
+
description: "CLAUDE.md rules @import block"
|
|
257
|
+
});
|
|
258
|
+
}
|
|
210
259
|
break;
|
|
211
260
|
case "gemini":
|
|
212
261
|
entries.push({
|
|
@@ -217,23 +266,26 @@ function buildHostArtifacts(adapter, ctx) {
|
|
|
217
266
|
content: "@.noir/NOIR.md",
|
|
218
267
|
description: "GEMINI.md context @-import block"
|
|
219
268
|
});
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
269
|
+
if (rulesEnabled) {
|
|
270
|
+
entries.push({
|
|
271
|
+
path: "GEMINI.md",
|
|
272
|
+
mode: "managedBlock",
|
|
273
|
+
host,
|
|
274
|
+
block: RULES_BLOCK,
|
|
275
|
+
content: "@.noir/rules/RULES.md",
|
|
276
|
+
description: "GEMINI.md rules @-import block"
|
|
277
|
+
});
|
|
278
|
+
}
|
|
228
279
|
break;
|
|
229
280
|
case "agents-md":
|
|
230
281
|
case "cursor":
|
|
231
282
|
case "opencode":
|
|
232
283
|
break;
|
|
233
284
|
}
|
|
234
|
-
const mcpAbs = adapter
|
|
285
|
+
const mcpAbs = mcpConfigPathForAdapter(adapter, ctx.root);
|
|
235
286
|
const mcpRel = hostRel(mcpAbs, ctx.root);
|
|
236
|
-
|
|
287
|
+
const workspace = readWorkspaceMarker(ctx.root);
|
|
288
|
+
if (host === "claude" && workspace === null) {
|
|
237
289
|
const mcpTemplate = ctx.transport === "streamable-http" ? "mcp.http.json.tmpl" : "mcp.stdio.json.tmpl";
|
|
238
290
|
entries.push({
|
|
239
291
|
path: mcpRel,
|
|
@@ -243,18 +295,24 @@ function buildHostArtifacts(adapter, ctx) {
|
|
|
243
295
|
description: "host MCP server pointer"
|
|
244
296
|
});
|
|
245
297
|
} else {
|
|
246
|
-
const mcpContent = `${adapter.emitMcpConfig(
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
298
|
+
const mcpContent = `${adapter.emitMcpConfig(
|
|
299
|
+
ectx,
|
|
300
|
+
workspace !== null ? (
|
|
301
|
+
// Joined: stdio + the workspace name, whatever transport was asked for.
|
|
302
|
+
{ transport: "stdio", command: ctx.command, workspace }
|
|
303
|
+
) : {
|
|
304
|
+
transport: ctx.transport,
|
|
305
|
+
command: ctx.command,
|
|
306
|
+
...ctx.url !== void 0 ? { url: ctx.url } : {}
|
|
307
|
+
}
|
|
308
|
+
)}
|
|
251
309
|
`;
|
|
252
310
|
entries.push({
|
|
253
311
|
path: mcpRel,
|
|
254
312
|
mode: "regenerate",
|
|
255
313
|
host,
|
|
256
314
|
content: mcpContent,
|
|
257
|
-
description: `${host} MCP server pointer`
|
|
315
|
+
description: workspace !== null ? `${host} MCP server pointer (workspace "${workspace}")` : `${host} MCP server pointer`
|
|
258
316
|
});
|
|
259
317
|
}
|
|
260
318
|
if (host === "claude") {
|
|
@@ -481,19 +539,15 @@ function mergeThreeWay(base, ours, theirs, style = "minimal") {
|
|
|
481
539
|
}
|
|
482
540
|
|
|
483
541
|
// src/migrations/index.ts
|
|
484
|
-
import { existsSync as existsSync4, readFileSync as readFileSync4, writeFileSync as
|
|
485
|
-
import { join as join5 } from "path";
|
|
486
|
-
|
|
487
|
-
|
|
488
|
-
|
|
489
|
-
|
|
490
|
-
|
|
491
|
-
|
|
492
|
-
|
|
493
|
-
if (v === void 0 || v === null) return whole;
|
|
494
|
-
return String(v);
|
|
495
|
-
});
|
|
496
|
-
}
|
|
542
|
+
import { existsSync as existsSync4, readFileSync as readFileSync4, writeFileSync as writeFileSync2 } from "fs";
|
|
543
|
+
import { join as join5, relative as relative2, sep } from "path";
|
|
544
|
+
import { mcpConfigPathFor, noirStdioArgs, TRANSPORT_KEYS } from "@noir-ai/adapters";
|
|
545
|
+
import {
|
|
546
|
+
atomicWriteFile as atomicWriteFile2,
|
|
547
|
+
loadProjectInfo,
|
|
548
|
+
readWorkspaceMarker as readWorkspaceMarker2,
|
|
549
|
+
resolveNoirCommand
|
|
550
|
+
} from "@noir-ai/core";
|
|
497
551
|
|
|
498
552
|
// src/template-history.ts
|
|
499
553
|
var ENV_EXAMPLE_1_1_0 = `# .noir/.env.example \u2014 committable documentation of the format.
|
|
@@ -734,24 +788,6 @@ function isStaleSeed(seed, fileBytes, currentRender) {
|
|
|
734
788
|
return SEED_TEMPLATE_HISTORY.some((entry) => entry[seed] === fileBytes);
|
|
735
789
|
}
|
|
736
790
|
|
|
737
|
-
// src/template-loader.ts
|
|
738
|
-
import { readFileSync as readFileSync2 } from "fs";
|
|
739
|
-
import { dirname as dirname2, join as join3, resolve } from "path";
|
|
740
|
-
import { fileURLToPath } from "url";
|
|
741
|
-
var DEFAULT_TEMPLATES_DIR = resolveTemplatesDir();
|
|
742
|
-
function resolveTemplatesDir() {
|
|
743
|
-
const override = process.env.NOIR_TEMPLATES_DIR;
|
|
744
|
-
if (override && override.length > 0) return resolve(override);
|
|
745
|
-
const here = dirname2(fileURLToPath(import.meta.url));
|
|
746
|
-
return join3(here, "..", "templates");
|
|
747
|
-
}
|
|
748
|
-
function loadTemplate(name) {
|
|
749
|
-
return readFileSync2(join3(DEFAULT_TEMPLATES_DIR, name), "utf8");
|
|
750
|
-
}
|
|
751
|
-
function templatesDir() {
|
|
752
|
-
return DEFAULT_TEMPLATES_DIR;
|
|
753
|
-
}
|
|
754
|
-
|
|
755
791
|
// src/writers.ts
|
|
756
792
|
import {
|
|
757
793
|
closeSync,
|
|
@@ -760,11 +796,11 @@ import {
|
|
|
760
796
|
readFileSync as readFileSync3,
|
|
761
797
|
renameSync,
|
|
762
798
|
rmSync,
|
|
763
|
-
writeFileSync as writeFileSync2,
|
|
764
799
|
writeSync
|
|
765
800
|
} from "fs";
|
|
766
801
|
import { basename, dirname as dirname3, join as join4 } from "path";
|
|
767
802
|
import { atomicWriteFile, stripManagedBlock, writeManagedRegion } from "@noir-ai/core";
|
|
803
|
+
import { ensureOwnerOnly } from "@noir-ai/core";
|
|
768
804
|
function regenerate(absPath, content) {
|
|
769
805
|
const dir = dirname3(absPath);
|
|
770
806
|
const tmp = join4(
|
|
@@ -818,7 +854,7 @@ function managedBlocks(absPath, regions) {
|
|
|
818
854
|
const next = stripped.trim().length > 0 ? `${stripped.trimEnd()}
|
|
819
855
|
|
|
820
856
|
${regionsJoined}` : regionsJoined;
|
|
821
|
-
|
|
857
|
+
atomicWriteFile(absPath, next);
|
|
822
858
|
return { path: absPath, mode: "managedBlock", written: true };
|
|
823
859
|
}
|
|
824
860
|
function buildRegion(block, body) {
|
|
@@ -911,7 +947,11 @@ function deepMergePreservingHooks(existing, patch, dedupSubstring) {
|
|
|
911
947
|
// src/migrations/runner.ts
|
|
912
948
|
function runMigrations(root, from, to, opts = {}) {
|
|
913
949
|
const window = pickWindow(from ?? "0.0.0", to, MIGRATIONS);
|
|
914
|
-
const ctx = {
|
|
950
|
+
const ctx = {
|
|
951
|
+
root,
|
|
952
|
+
dryRun: opts.dryRun === true,
|
|
953
|
+
rulesEnabled: opts.rulesEnabled
|
|
954
|
+
};
|
|
915
955
|
const aggregate = { changed: [], conflicts: [], notes: [] };
|
|
916
956
|
const ran = [];
|
|
917
957
|
for (const script of window) {
|
|
@@ -960,7 +1000,7 @@ var synthetic = {
|
|
|
960
1000
|
if (existsSync4(file)) {
|
|
961
1001
|
const prev = readFileSync4(file, "utf8");
|
|
962
1002
|
const merged = applyInlineConflict(prev, "noir-scaffold=1.0.0\n", "ours", "theirs");
|
|
963
|
-
|
|
1003
|
+
writeFileSync2(file, merged, "utf8");
|
|
964
1004
|
result.conflicts.push(".noir/scaffold-version");
|
|
965
1005
|
}
|
|
966
1006
|
}
|
|
@@ -1015,7 +1055,7 @@ var envPointer = {
|
|
|
1015
1055
|
return result;
|
|
1016
1056
|
}
|
|
1017
1057
|
try {
|
|
1018
|
-
|
|
1058
|
+
writeFileSync2(abs, appendSeparated(prev, ENV_POINTER_BLOCK), "utf8");
|
|
1019
1059
|
} catch (err) {
|
|
1020
1060
|
result.conflicts.push(rel);
|
|
1021
1061
|
result.notes.push(`${rel}: write failed (${errorMessage(err)}) \u2014 left untouched`);
|
|
@@ -1037,6 +1077,10 @@ var envTemplates = {
|
|
|
1037
1077
|
run: (ctx) => {
|
|
1038
1078
|
const result = { changed: [], conflicts: [], notes: [] };
|
|
1039
1079
|
for (const seed of DOC_SEEDS) {
|
|
1080
|
+
if (seed.kind === "rulesSeed" && ctx.rulesEnabled === false) {
|
|
1081
|
+
result.notes.push(`${seed.rel}: skipped \u2014 the working-rules switch is off`);
|
|
1082
|
+
continue;
|
|
1083
|
+
}
|
|
1040
1084
|
const abs = join5(ctx.root, seed.rel);
|
|
1041
1085
|
if (!existsSync4(abs)) {
|
|
1042
1086
|
result.notes.push(`${seed.rel}: absent \u2014 nothing to refresh`);
|
|
@@ -1073,7 +1117,101 @@ var envTemplates = {
|
|
|
1073
1117
|
return result;
|
|
1074
1118
|
}
|
|
1075
1119
|
};
|
|
1076
|
-
|
|
1120
|
+
function mcpConfigPath(root) {
|
|
1121
|
+
let host = "claude";
|
|
1122
|
+
try {
|
|
1123
|
+
host = loadProjectInfo(root).config.host;
|
|
1124
|
+
} catch {
|
|
1125
|
+
}
|
|
1126
|
+
const abs = mcpConfigPathFor(host, root);
|
|
1127
|
+
if (!existsSync4(abs)) return null;
|
|
1128
|
+
return { rel: relative2(root, abs).split(sep).join("/"), abs };
|
|
1129
|
+
}
|
|
1130
|
+
var workspaceBridge = {
|
|
1131
|
+
from: "1.2.0",
|
|
1132
|
+
to: "1.3.0",
|
|
1133
|
+
description: "point a joined repo's MCP entry at the workspace bridge",
|
|
1134
|
+
run: (ctx) => {
|
|
1135
|
+
const result = { changed: [], conflicts: [], notes: [] };
|
|
1136
|
+
const name = readWorkspaceMarker2(ctx.root);
|
|
1137
|
+
if (name === null) {
|
|
1138
|
+
result.notes.push("not joined to a workspace \u2014 nothing to rewrite");
|
|
1139
|
+
return result;
|
|
1140
|
+
}
|
|
1141
|
+
const target = mcpConfigPath(ctx.root);
|
|
1142
|
+
if (target === null) {
|
|
1143
|
+
result.notes.push(`workspace "${name}": no MCP config file to migrate`);
|
|
1144
|
+
return result;
|
|
1145
|
+
}
|
|
1146
|
+
let config;
|
|
1147
|
+
try {
|
|
1148
|
+
config = JSON.parse(readFileSync4(target.abs, "utf8"));
|
|
1149
|
+
} catch (err) {
|
|
1150
|
+
result.conflicts.push(target.rel);
|
|
1151
|
+
result.notes.push(`${target.rel}: unreadable (${errorMessage(err)}) \u2014 left untouched`);
|
|
1152
|
+
return result;
|
|
1153
|
+
}
|
|
1154
|
+
const servers = config.mcpServers;
|
|
1155
|
+
if (typeof servers !== "object" || servers === null || Array.isArray(servers)) {
|
|
1156
|
+
result.notes.push(`${target.rel}: no mcpServers block \u2014 left untouched`);
|
|
1157
|
+
return result;
|
|
1158
|
+
}
|
|
1159
|
+
const entry = servers.noir;
|
|
1160
|
+
if (typeof entry !== "object" || entry === null || Array.isArray(entry)) {
|
|
1161
|
+
result.notes.push(`${target.rel}: no noir entry \u2014 left untouched`);
|
|
1162
|
+
return result;
|
|
1163
|
+
}
|
|
1164
|
+
const entryObj = entry;
|
|
1165
|
+
const args = entryObj.args;
|
|
1166
|
+
const namesWorkspace = Array.isArray(args) && args.includes("--workspace");
|
|
1167
|
+
const isHttpPointer = entryObj.type === "http";
|
|
1168
|
+
const isRepoScopedStdio = typeof entryObj.command === "string" && !namesWorkspace;
|
|
1169
|
+
if (!isHttpPointer && !isRepoScopedStdio) {
|
|
1170
|
+
result.notes.push(
|
|
1171
|
+
namesWorkspace ? `${target.rel}: already reaches Noir over the bridge \u2014 left untouched` : `${target.rel}: noir entry is not a transport this migration owns \u2014 left untouched`
|
|
1172
|
+
);
|
|
1173
|
+
return result;
|
|
1174
|
+
}
|
|
1175
|
+
const kept = Object.entries(entryObj).filter(([key]) => !TRANSPORT_KEYS.has(key));
|
|
1176
|
+
if (ctx.dryRun) {
|
|
1177
|
+
result.changed.push(target.rel);
|
|
1178
|
+
result.notes.push(`${target.rel}: would point the noir entry at workspace "${name}"`);
|
|
1179
|
+
return result;
|
|
1180
|
+
}
|
|
1181
|
+
const next = {
|
|
1182
|
+
...config,
|
|
1183
|
+
mcpServers: {
|
|
1184
|
+
...servers,
|
|
1185
|
+
noir: {
|
|
1186
|
+
command: resolveNoirCommand(),
|
|
1187
|
+
// The argv comes from the adapters' own helper, so this rewrite lands
|
|
1188
|
+
// on exactly the entry a fresh join writes — the name is the whole
|
|
1189
|
+
// address, and the bridge resolves the daemon (and reads its token)
|
|
1190
|
+
// itself, so none of that is written into a committed config file.
|
|
1191
|
+
args: noirStdioArgs(name),
|
|
1192
|
+
...Object.fromEntries(kept)
|
|
1193
|
+
}
|
|
1194
|
+
}
|
|
1195
|
+
};
|
|
1196
|
+
try {
|
|
1197
|
+
atomicWriteFile2(target.abs, `${JSON.stringify(next, null, 2)}
|
|
1198
|
+
`);
|
|
1199
|
+
} catch (err) {
|
|
1200
|
+
result.conflicts.push(target.rel);
|
|
1201
|
+
result.notes.push(`${target.rel}: write failed (${errorMessage(err)}) \u2014 left untouched`);
|
|
1202
|
+
return result;
|
|
1203
|
+
}
|
|
1204
|
+
result.changed.push(target.rel);
|
|
1205
|
+
result.notes.push(`${target.rel}: noir entry now reaches workspace "${name}" over the bridge`);
|
|
1206
|
+
return result;
|
|
1207
|
+
}
|
|
1208
|
+
};
|
|
1209
|
+
var MIGRATIONS = [
|
|
1210
|
+
synthetic,
|
|
1211
|
+
envPointer,
|
|
1212
|
+
envTemplates,
|
|
1213
|
+
workspaceBridge
|
|
1214
|
+
];
|
|
1077
1215
|
function errorMessage(err) {
|
|
1078
1216
|
return err instanceof Error ? err.message : String(err);
|
|
1079
1217
|
}
|
|
@@ -1096,10 +1234,11 @@ import { existsSync as existsSync6, mkdirSync as mkdirSync3, readFileSync as rea
|
|
|
1096
1234
|
import { basename as basename2, dirname as dirname5, join as join8 } from "path";
|
|
1097
1235
|
import {
|
|
1098
1236
|
createProjectId,
|
|
1099
|
-
loadProjectInfo,
|
|
1237
|
+
loadProjectInfo as loadProjectInfo2,
|
|
1100
1238
|
paths as paths2,
|
|
1101
1239
|
readManagedBlock,
|
|
1102
|
-
|
|
1240
|
+
readProjectConfig,
|
|
1241
|
+
resolveNoirCommand as resolveNoirCommand2,
|
|
1103
1242
|
sha256Hex12
|
|
1104
1243
|
} from "@noir-ai/core";
|
|
1105
1244
|
|
|
@@ -1107,7 +1246,7 @@ import {
|
|
|
1107
1246
|
import { mkdirSync as mkdirSync2, readFileSync as readFileSync5 } from "fs";
|
|
1108
1247
|
import { dirname as dirname4, join as join6 } from "path";
|
|
1109
1248
|
import { NOIR_DIR as NOIR_DIR2 } from "@noir-ai/core";
|
|
1110
|
-
var CURRENT_SCAFFOLD_VERSION = "1.
|
|
1249
|
+
var CURRENT_SCAFFOLD_VERSION = "1.3.0";
|
|
1111
1250
|
var PREFIX = "noir-scaffold=";
|
|
1112
1251
|
function scaffoldVersionPath(root) {
|
|
1113
1252
|
return join6(root, NOIR_DIR2, "scaffold-version");
|
|
@@ -1339,14 +1478,21 @@ function assertSafeRoot(root) {
|
|
|
1339
1478
|
cur = parent;
|
|
1340
1479
|
}
|
|
1341
1480
|
}
|
|
1481
|
+
function envOwnerOnly(opts) {
|
|
1482
|
+
if (opts.dryRun === true) return void 0;
|
|
1483
|
+
return ensureOwnerOnly(join8(paths2.noirDir(opts.root), ".env"));
|
|
1484
|
+
}
|
|
1342
1485
|
function readConfiguredHost(root) {
|
|
1343
1486
|
if (!existsSync6(paths2.config(root))) return "claude";
|
|
1344
1487
|
try {
|
|
1345
|
-
return
|
|
1488
|
+
return loadProjectInfo2(root).config.host;
|
|
1346
1489
|
} catch {
|
|
1347
1490
|
return "claude";
|
|
1348
1491
|
}
|
|
1349
1492
|
}
|
|
1493
|
+
function readConfiguredRulesEnabled(root) {
|
|
1494
|
+
return readProjectConfig(root)?.rules.enabled ?? true;
|
|
1495
|
+
}
|
|
1350
1496
|
async function scaffold(opts) {
|
|
1351
1497
|
assertSafeRoot(opts.root);
|
|
1352
1498
|
const host = opts.host ?? readConfiguredHost(opts.root);
|
|
@@ -1382,6 +1528,7 @@ async function scaffold(opts) {
|
|
|
1382
1528
|
refreshed: [],
|
|
1383
1529
|
conflicts: [],
|
|
1384
1530
|
fileModes: {},
|
|
1531
|
+
envMode: envOwnerOnly(opts),
|
|
1385
1532
|
noop: true,
|
|
1386
1533
|
migrationsRan: [],
|
|
1387
1534
|
migrationConflicts: [],
|
|
@@ -1398,13 +1545,18 @@ async function scaffold(opts) {
|
|
|
1398
1545
|
const migrationChanged = [];
|
|
1399
1546
|
if (opts.mode === "init" && opts.upgrade === true && (fromVersion !== null || idFile.state === "valid")) {
|
|
1400
1547
|
const m = runMigrations(opts.root, fromVersion, CURRENT_SCAFFOLD_VERSION, {
|
|
1401
|
-
dryRun: opts.dryRun === true
|
|
1548
|
+
dryRun: opts.dryRun === true,
|
|
1549
|
+
// The migration runs BEFORE the manifest, so it cannot see the manifest's
|
|
1550
|
+
// own rules-gating. Hand the switch through so a migration that would
|
|
1551
|
+
// rewrite the working-rules seed (the doc-seed refresh keyed on recorded
|
|
1552
|
+
// older bytes) leaves a switch-off project's RULES.md alone.
|
|
1553
|
+
rulesEnabled: readConfiguredRulesEnabled(opts.root)
|
|
1402
1554
|
});
|
|
1403
1555
|
migrationsRan.push(...m.ran);
|
|
1404
1556
|
migrationConflicts.push(...m.conflicts);
|
|
1405
1557
|
migrationChanged.push(...m.changed);
|
|
1406
1558
|
}
|
|
1407
|
-
const command =
|
|
1559
|
+
const command = resolveNoirCommand2();
|
|
1408
1560
|
const manifest = buildManifest({
|
|
1409
1561
|
root: opts.root,
|
|
1410
1562
|
projectId,
|
|
@@ -1412,7 +1564,12 @@ async function scaffold(opts) {
|
|
|
1412
1564
|
transport,
|
|
1413
1565
|
url: opts.url,
|
|
1414
1566
|
command,
|
|
1415
|
-
stack
|
|
1567
|
+
stack,
|
|
1568
|
+
// The project's own working-rules switch: a project that set
|
|
1569
|
+
// `rules.enabled: false` gets no `.noir/rules/RULES.md` — not on the first
|
|
1570
|
+
// scaffold, and not backfilled by an upgrade. Unchanged (enabled) whenever
|
|
1571
|
+
// the config is absent or says nothing about rules.
|
|
1572
|
+
rulesEnabled: readConfiguredRulesEnabled(opts.root)
|
|
1416
1573
|
});
|
|
1417
1574
|
const emitContext = opts.mode === "sync" ? "sync" : opts.mode === "init" && opts.upgrade === true ? "upgrade" : null;
|
|
1418
1575
|
const vars = {
|
|
@@ -1626,6 +1783,10 @@ async function scaffold(opts) {
|
|
|
1626
1783
|
refreshed,
|
|
1627
1784
|
conflicts: conflictRecords,
|
|
1628
1785
|
fileModes,
|
|
1786
|
+
// Re-asserted here, after the emit, so the mode on disk is the one this run
|
|
1787
|
+
// leaves behind: the seed writer only applies 0600 at creation (see
|
|
1788
|
+
// `envOwnerOnly`).
|
|
1789
|
+
envMode: envOwnerOnly(opts),
|
|
1629
1790
|
noop: false,
|
|
1630
1791
|
migrationsRan,
|
|
1631
1792
|
migrationConflicts,
|
|
@@ -1847,6 +2008,7 @@ export {
|
|
|
1847
2008
|
buildManifest,
|
|
1848
2009
|
buildRegion,
|
|
1849
2010
|
detectStack,
|
|
2011
|
+
ensureOwnerOnly,
|
|
1850
2012
|
isStaleSeed,
|
|
1851
2013
|
lineDiff,
|
|
1852
2014
|
loadTemplate,
|