craftar 0.21.0 → 0.22.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/README.md +13 -1
- package/dist/cli.js +345 -41
- package/dist/cli.js.map +1 -1
- package/dist/core/drift.js +29 -0
- package/dist/core/drift.js.map +1 -0
- package/dist/core/forge.js +34 -3
- package/dist/core/forge.js.map +1 -1
- package/dist/core/frontmatter.js +2 -2
- package/dist/core/frontmatter.js.map +1 -1
- package/dist/core/impact.js +14 -11
- package/dist/core/impact.js.map +1 -1
- package/dist/core/manifest-edit.js +4 -5
- package/dist/core/manifest-edit.js.map +1 -1
- package/dist/core/param-writes.js +6 -7
- package/dist/core/param-writes.js.map +1 -1
- package/dist/core/sync.js +11 -3
- package/dist/core/sync.js.map +1 -1
- package/dist/core/unify.js +6 -5
- package/dist/core/unify.js.map +1 -1
- package/dist/core/workspace-yaml.js +2 -10
- package/dist/core/workspace-yaml.js.map +1 -1
- package/dist/core/yaml-read.js +32 -0
- package/dist/core/yaml-read.js.map +1 -0
- package/dist/importers/claude-code.js +191 -94
- package/dist/importers/claude-code.js.map +1 -1
- package/dist/importers/decide.js +4 -4
- package/dist/importers/decide.js.map +1 -1
- package/dist/importers/drift-promote.js +1218 -0
- package/dist/importers/drift-promote.js.map +1 -0
- package/package.json +1 -1
|
@@ -21,6 +21,8 @@ import { bodyFile } from "../core/extract.js";
|
|
|
21
21
|
import { GENERATED_BANNER, kiroReport } from "./kiro-report.js";
|
|
22
22
|
import { deepMerge } from "../core/merge.js";
|
|
23
23
|
import { outName } from "../emitters/shared.js";
|
|
24
|
+
import { parseYamlText } from "../core/yaml-read.js";
|
|
25
|
+
import { note } from "../core/unify.js";
|
|
24
26
|
/** Rules that every workspace shares by intent — they seed the `base` recipe. */
|
|
25
27
|
const BASE_RULES = new Set([
|
|
26
28
|
"README",
|
|
@@ -118,22 +120,14 @@ async function planImport(opts, stage) {
|
|
|
118
120
|
if (!f.endsWith(".md"))
|
|
119
121
|
continue;
|
|
120
122
|
const name = f.replace(/\.md$/, "");
|
|
121
|
-
const src = await readSource(path.join(claudeDir, "rules", f));
|
|
122
|
-
const text = toLf(stripBom(src.text));
|
|
123
123
|
const sm = steeringMeta.get(name);
|
|
124
|
-
const
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
file: "rule.md",
|
|
130
|
-
targets: "*",
|
|
131
|
-
tags: [],
|
|
132
|
-
origin: origin(`.claude/rules/${f}`),
|
|
133
|
-
};
|
|
134
|
-
read(meta, { "rule.md": text }, scanOf("rule.md", src), (ref) => {
|
|
124
|
+
const emitted = await readEmitted(claudeDir, "rule", f, origin, sm ? { inclusion: sm.inclusion, fileMatchPattern: sm.fileMatchPattern } : undefined);
|
|
125
|
+
if (!emitted)
|
|
126
|
+
continue;
|
|
127
|
+
const ruleMeta = emitted.meta;
|
|
128
|
+
read(emitted.meta, emitted.files, emitted.scan, (ref) => {
|
|
135
129
|
ruleNames.push(ref.split("/")[1]);
|
|
136
|
-
if (
|
|
130
|
+
if (ruleMeta.inclusion === "fileMatch")
|
|
137
131
|
scopedRules.set(ref, sm?.fileMatchPattern ?? "");
|
|
138
132
|
else
|
|
139
133
|
refs.base.push(ref);
|
|
@@ -144,24 +138,12 @@ async function planImport(opts, stage) {
|
|
|
144
138
|
for (const f of await safeList(path.join(claudeDir, "agents"))) {
|
|
145
139
|
if (!f.endsWith(".md"))
|
|
146
140
|
continue;
|
|
147
|
-
const
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
const
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
name,
|
|
154
|
-
description: data.description?.trim(),
|
|
155
|
-
tools: splitList(data.tools),
|
|
156
|
-
model: data.model?.trim() || undefined,
|
|
157
|
-
file: "agent.md",
|
|
158
|
-
frontmatterRaw: raw ?? undefined,
|
|
159
|
-
targets: "*",
|
|
160
|
-
tags: [],
|
|
161
|
-
origin: origin(`.claude/agents/${f}`),
|
|
162
|
-
};
|
|
163
|
-
read(meta, { "agent.md": body }, scanOf("agent.md", src), (ref) => {
|
|
164
|
-
agentBodies.set(ref, (data.description ?? "") + "\n" + body);
|
|
141
|
+
const emitted = await readEmitted(claudeDir, "agent", f, origin);
|
|
142
|
+
if (!emitted)
|
|
143
|
+
continue;
|
|
144
|
+
const agentMeta = emitted.meta;
|
|
145
|
+
read(emitted.meta, emitted.files, emitted.scan, (ref) => {
|
|
146
|
+
agentBodies.set(ref, (agentMeta.description ?? "") + "\n" + emitted.files["agent.md"]);
|
|
165
147
|
refs.base.push(ref);
|
|
166
148
|
});
|
|
167
149
|
}
|
|
@@ -169,22 +151,10 @@ async function planImport(opts, stage) {
|
|
|
169
151
|
for (const f of await safeList(path.join(claudeDir, "commands"))) {
|
|
170
152
|
if (!f.endsWith(".md"))
|
|
171
153
|
continue;
|
|
172
|
-
const
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
type: "command",
|
|
177
|
-
name: f.replace(/\.md$/, ""),
|
|
178
|
-
description: data.description?.trim(),
|
|
179
|
-
argumentHint: data["argument-hint"]?.trim() || undefined,
|
|
180
|
-
allowedTools: data["allowed-tools"]?.trim() || undefined,
|
|
181
|
-
file: "command.md",
|
|
182
|
-
frontmatterRaw: raw ?? undefined,
|
|
183
|
-
targets: "*",
|
|
184
|
-
tags: [],
|
|
185
|
-
origin: origin(`.claude/commands/${f}`),
|
|
186
|
-
};
|
|
187
|
-
read(meta, { "command.md": body }, scanOf("command.md", src), (ref) => addRef(refs.base, ref));
|
|
154
|
+
const emitted = await readEmitted(claudeDir, "command", f, origin);
|
|
155
|
+
if (!emitted)
|
|
156
|
+
continue;
|
|
157
|
+
read(emitted.meta, emitted.files, emitted.scan, (ref) => addRef(refs.base, ref));
|
|
188
158
|
}
|
|
189
159
|
/* ---- skills ---- */
|
|
190
160
|
const skillsDir = path.join(claudeDir, "skills");
|
|
@@ -193,32 +163,13 @@ async function planImport(opts, stage) {
|
|
|
193
163
|
for (const e of (await fs.readdir(skillsDir, { withFileTypes: true })).sort((x, y) => x.name.localeCompare(y.name))) {
|
|
194
164
|
if (e.name === ".gitkeep")
|
|
195
165
|
continue;
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
for (const rel of await listFiles(path.join(skillsDir, e.name))) {
|
|
200
|
-
const abs = path.join(skillsDir, e.name, rel);
|
|
201
|
-
if (/\.(md|txt|json|ya?ml|ps1|py|sh|js|ts)$/i.test(rel)) {
|
|
202
|
-
const src = await readSource(abs);
|
|
203
|
-
files[rel] = toLf(stripBom(src.text));
|
|
204
|
-
Object.assign(scan, scanOf(rel, src));
|
|
205
|
-
}
|
|
206
|
-
else
|
|
207
|
-
files[rel] = await fs.readFile(abs);
|
|
208
|
-
}
|
|
209
|
-
if (!files["SKILL.md"]) {
|
|
166
|
+
const emitted = await readEmitted(claudeDir, "skill", e.name, origin);
|
|
167
|
+
if (!emitted) {
|
|
168
|
+
if (e.isDirectory())
|
|
210
169
|
report.warnings.push(`skill dir ${e.name} has no SKILL.md; skipped`);
|
|
211
|
-
|
|
212
|
-
}
|
|
213
|
-
const meta = { type: "skill", name: e.name, layout: "dir", targets: "*", tags: [], origin: origin(`.claude/skills/${e.name}/`) };
|
|
214
|
-
read(meta, files, scan, (ref) => addRef(refs.base, ref));
|
|
215
|
-
}
|
|
216
|
-
else if (e.name.endsWith(".md")) {
|
|
217
|
-
const src = await readSource(path.join(skillsDir, e.name));
|
|
218
|
-
const text = toLf(stripBom(src.text));
|
|
219
|
-
const meta = { type: "skill", name: e.name.replace(/\.md$/, ""), layout: "file", targets: "*", tags: [], origin: origin(`.claude/skills/${e.name}`) };
|
|
220
|
-
read(meta, { "SKILL.md": text }, scanOf("SKILL.md", src), (ref) => addRef(refs.base, ref));
|
|
170
|
+
continue;
|
|
221
171
|
}
|
|
172
|
+
read(emitted.meta, emitted.files, emitted.scan, (ref) => addRef(refs.base, ref));
|
|
222
173
|
}
|
|
223
174
|
}
|
|
224
175
|
/* ---- scripts & hooks ---- */
|
|
@@ -226,20 +177,10 @@ async function planImport(opts, stage) {
|
|
|
226
177
|
for (const f of await safeList(path.join(claudeDir, kind))) {
|
|
227
178
|
if (f === ".gitkeep" || f.startsWith("__pycache__") || f.endsWith(".pyc"))
|
|
228
179
|
continue;
|
|
229
|
-
const
|
|
230
|
-
if (!
|
|
180
|
+
const emitted = await readEmitted(claudeDir, kind === "scripts" ? "script" : "hook", f, origin);
|
|
181
|
+
if (!emitted)
|
|
231
182
|
continue;
|
|
232
|
-
|
|
233
|
-
const content = src ? toLf(stripBom(src.text)) : await fs.readFile(abs);
|
|
234
|
-
const meta = {
|
|
235
|
-
type: kind === "scripts" ? "script" : "hook",
|
|
236
|
-
name: f.replace(/\.[^.]+$/, "").toLowerCase(),
|
|
237
|
-
files: [f],
|
|
238
|
-
targets: ["claude-code"],
|
|
239
|
-
tags: [],
|
|
240
|
-
origin: origin(`.claude/${kind}/${f}`),
|
|
241
|
-
};
|
|
242
|
-
read(meta, { [f]: content }, src ? scanOf(f, src) : {}, (ref) => addRef(refs.base, ref));
|
|
183
|
+
read(emitted.meta, emitted.files, emitted.scan, (ref) => addRef(refs.base, ref));
|
|
243
184
|
}
|
|
244
185
|
}
|
|
245
186
|
/* ---- MCP servers ---- */
|
|
@@ -470,6 +411,162 @@ export class ForgeStage {
|
|
|
470
411
|
throw new Error(`${e instanceof Error ? e.message : String(e)}\nThe import failed while writing into the Forge (${already}); restore or remove those paths before re-running.`, { cause: e });
|
|
471
412
|
}
|
|
472
413
|
}
|
|
414
|
+
/** Staged paths in staging order, Forge-relative with `/`; `created` is false when the path exists on disk under `root`. */
|
|
415
|
+
async entries() {
|
|
416
|
+
const result = [];
|
|
417
|
+
for (const [abs] of this.files) {
|
|
418
|
+
const rel = path.relative(this.root, abs).split(path.sep).join("/");
|
|
419
|
+
const created = !(await exists(abs));
|
|
420
|
+
result.push({ rel, abs, created });
|
|
421
|
+
}
|
|
422
|
+
return result;
|
|
423
|
+
}
|
|
424
|
+
/** Write every staged file under another root (same relative paths), in the order given by `order` (a comparator over `rel`), noting each in `journal` before it is touched. */
|
|
425
|
+
async flushTo(root, opts) {
|
|
426
|
+
// Build entries in staging order
|
|
427
|
+
const entries = [];
|
|
428
|
+
for (const [abs, content] of this.files) {
|
|
429
|
+
const rel = path.relative(this.root, abs).split(path.sep).join("/");
|
|
430
|
+
entries.push({ rel, content });
|
|
431
|
+
}
|
|
432
|
+
// Sort if order given
|
|
433
|
+
if (opts?.order) {
|
|
434
|
+
entries.sort((a, b) => opts.order(a.rel, b.rel));
|
|
435
|
+
}
|
|
436
|
+
// Write each entry to the target root
|
|
437
|
+
for (const { rel, content } of entries) {
|
|
438
|
+
const targetAbs = path.join(root, rel);
|
|
439
|
+
await note(opts?.journal, targetAbs);
|
|
440
|
+
await fs.mkdir(path.dirname(targetAbs), { recursive: true });
|
|
441
|
+
await fs.writeFile(targetAbs, content);
|
|
442
|
+
}
|
|
443
|
+
}
|
|
444
|
+
}
|
|
445
|
+
/**
|
|
446
|
+
* One ingredient as import reads it from a workspace's `.claude/` tree. `rel` is the path under
|
|
447
|
+
* `.claude/<folder>/` (a file, or a skill's directory name). Returns `null` where `planImport`
|
|
448
|
+
* skips: a skill directory without `SKILL.md`, a non-file under scripts/hooks.
|
|
449
|
+
*/
|
|
450
|
+
export async function readEmitted(claudeDir, type, rel, origin, steering) {
|
|
451
|
+
if (type === "rule") {
|
|
452
|
+
const name = rel.replace(/\.md$/, "");
|
|
453
|
+
const src = await readSource(path.join(claudeDir, "rules", rel));
|
|
454
|
+
const text = toLf(stripBom(src.text));
|
|
455
|
+
const meta = {
|
|
456
|
+
type: "rule",
|
|
457
|
+
name,
|
|
458
|
+
inclusion: steering?.inclusion ?? "always",
|
|
459
|
+
fileMatchPattern: steering?.fileMatchPattern,
|
|
460
|
+
file: "rule.md",
|
|
461
|
+
targets: "*",
|
|
462
|
+
tags: [],
|
|
463
|
+
origin: origin(`.claude/rules/${rel}`),
|
|
464
|
+
};
|
|
465
|
+
return { meta, files: { "rule.md": text }, scan: scanOf("rule.md", src) };
|
|
466
|
+
}
|
|
467
|
+
if (type === "agent") {
|
|
468
|
+
const src = await readSource(path.join(claudeDir, "agents", rel));
|
|
469
|
+
const text = src.text;
|
|
470
|
+
const { data, body, raw } = parseFrontmatter(text, { loose: true });
|
|
471
|
+
const name = (data.name || rel.replace(/\.md$/, "")).trim();
|
|
472
|
+
const meta = {
|
|
473
|
+
type: "agent",
|
|
474
|
+
name,
|
|
475
|
+
description: data.description?.trim(),
|
|
476
|
+
tools: splitList(data.tools),
|
|
477
|
+
model: data.model?.trim() || undefined,
|
|
478
|
+
file: "agent.md",
|
|
479
|
+
frontmatterRaw: raw ?? undefined,
|
|
480
|
+
targets: "*",
|
|
481
|
+
tags: [],
|
|
482
|
+
origin: origin(`.claude/agents/${rel}`),
|
|
483
|
+
};
|
|
484
|
+
return { meta, files: { "agent.md": body }, scan: scanOf("agent.md", src) };
|
|
485
|
+
}
|
|
486
|
+
if (type === "command") {
|
|
487
|
+
const src = await readSource(path.join(claudeDir, "commands", rel));
|
|
488
|
+
const text = src.text;
|
|
489
|
+
const { data, body, raw } = parseFrontmatter(text, { loose: true });
|
|
490
|
+
const meta = {
|
|
491
|
+
type: "command",
|
|
492
|
+
name: rel.replace(/\.md$/, ""),
|
|
493
|
+
description: data.description?.trim(),
|
|
494
|
+
argumentHint: data["argument-hint"]?.trim() || undefined,
|
|
495
|
+
allowedTools: data["allowed-tools"]?.trim() || undefined,
|
|
496
|
+
file: "command.md",
|
|
497
|
+
frontmatterRaw: raw ?? undefined,
|
|
498
|
+
targets: "*",
|
|
499
|
+
tags: [],
|
|
500
|
+
origin: origin(`.claude/commands/${rel}`),
|
|
501
|
+
};
|
|
502
|
+
return { meta, files: { "command.md": body }, scan: scanOf("command.md", src) };
|
|
503
|
+
}
|
|
504
|
+
if (type === "skill") {
|
|
505
|
+
const skillDir = path.join(claudeDir, "skills", rel);
|
|
506
|
+
const stat = await fs.lstat(skillDir).catch(() => null);
|
|
507
|
+
if (!stat)
|
|
508
|
+
return null;
|
|
509
|
+
if (stat.isDirectory()) {
|
|
510
|
+
const files = {};
|
|
511
|
+
const scan = {};
|
|
512
|
+
for (const fileRel of await listFiles(skillDir)) {
|
|
513
|
+
const abs = path.join(skillDir, fileRel);
|
|
514
|
+
if (/\.(md|txt|json|ya?ml|ps1|py|sh|js|ts)$/i.test(fileRel)) {
|
|
515
|
+
const src = await readSource(abs);
|
|
516
|
+
files[fileRel] = toLf(stripBom(src.text));
|
|
517
|
+
Object.assign(scan, scanOf(fileRel, src));
|
|
518
|
+
}
|
|
519
|
+
else {
|
|
520
|
+
files[fileRel] = await fs.readFile(abs);
|
|
521
|
+
}
|
|
522
|
+
}
|
|
523
|
+
if (!files["SKILL.md"])
|
|
524
|
+
return null;
|
|
525
|
+
const meta = {
|
|
526
|
+
type: "skill",
|
|
527
|
+
name: rel,
|
|
528
|
+
layout: "dir",
|
|
529
|
+
targets: "*",
|
|
530
|
+
tags: [],
|
|
531
|
+
origin: origin(`.claude/skills/${rel}/`),
|
|
532
|
+
};
|
|
533
|
+
return { meta, files, scan };
|
|
534
|
+
}
|
|
535
|
+
else if (rel.endsWith(".md")) {
|
|
536
|
+
const src = await readSource(skillDir);
|
|
537
|
+
const text = toLf(stripBom(src.text));
|
|
538
|
+
const meta = {
|
|
539
|
+
type: "skill",
|
|
540
|
+
name: rel.replace(/\.md$/, ""),
|
|
541
|
+
layout: "file",
|
|
542
|
+
targets: "*",
|
|
543
|
+
tags: [],
|
|
544
|
+
origin: origin(`.claude/skills/${rel}`),
|
|
545
|
+
};
|
|
546
|
+
return { meta, files: { "SKILL.md": text }, scan: scanOf("SKILL.md", src) };
|
|
547
|
+
}
|
|
548
|
+
return null;
|
|
549
|
+
}
|
|
550
|
+
if (type === "script" || type === "hook") {
|
|
551
|
+
const kind = type === "script" ? "scripts" : "hooks";
|
|
552
|
+
const abs = path.join(claudeDir, kind, rel);
|
|
553
|
+
// fs.stat throws on a dangling link (ENOENT) — spec 30r1 §3.4 says we must let it throw
|
|
554
|
+
const stat = await fs.stat(abs);
|
|
555
|
+
if (!stat.isFile())
|
|
556
|
+
return null;
|
|
557
|
+
const src = /\.(ps1|py|sh|js|ts|cjs|mjs|json|md|txt|ya?ml)$/i.test(rel) ? await readSource(abs) : null;
|
|
558
|
+
const content = src ? toLf(stripBom(src.text)) : await fs.readFile(abs);
|
|
559
|
+
const meta = {
|
|
560
|
+
type,
|
|
561
|
+
name: rel.replace(/\.[^.]+$/, "").toLowerCase(),
|
|
562
|
+
files: [rel],
|
|
563
|
+
targets: ["claude-code"],
|
|
564
|
+
tags: [],
|
|
565
|
+
origin: origin(`.claude/${kind}/${rel}`),
|
|
566
|
+
};
|
|
567
|
+
return { meta, files: { [rel]: content }, scan: src ? scanOf(rel, src) : {} };
|
|
568
|
+
}
|
|
569
|
+
return null;
|
|
473
570
|
}
|
|
474
571
|
/**
|
|
475
572
|
* A reviewer agent belongs to the stack of a scoped rule when its name is derived from the rule name
|
|
@@ -489,7 +586,7 @@ function addRef(list, ref) {
|
|
|
489
586
|
list.push(ref);
|
|
490
587
|
}
|
|
491
588
|
/** First secret-like value in an ingredient about to be imported, described by location only. */
|
|
492
|
-
function secretIn(meta, files, scan = {}) {
|
|
589
|
+
export function secretIn(meta, files, scan = {}) {
|
|
493
590
|
const origin = meta.origin?.path ?? `${meta.type}/${meta.name}`;
|
|
494
591
|
const raw = "frontmatterRaw" in meta ? meta.frontmatterRaw : undefined;
|
|
495
592
|
// Bodies of agents and commands start after `---`, the raw block and the closing `---`.
|
|
@@ -697,7 +794,7 @@ async function writeIngredient(stage, meta, files, profile, report, scan = {}, r
|
|
|
697
794
|
* `env` value, a name that is not slug-like) fails the whole import, naming its source, before the
|
|
698
795
|
* first write — 0.2.4 wrote a Forge no command could load (spec 07, Ruling 6).
|
|
699
796
|
*/
|
|
700
|
-
function validateImported(meta) {
|
|
797
|
+
export function validateImported(meta) {
|
|
701
798
|
const r = IngredientSchema.safeParse(meta);
|
|
702
799
|
if (r.success)
|
|
703
800
|
return r.data;
|
|
@@ -820,7 +917,7 @@ async function writeOwnedRecipe(o, name, list, description) {
|
|
|
820
917
|
for (const x of next.slice(kept.length))
|
|
821
918
|
seq.items.push(doc.createNode(x));
|
|
822
919
|
});
|
|
823
|
-
const after = RecipeSchema.safeParse(
|
|
920
|
+
const after = RecipeSchema.safeParse(parseYamlText(label, content) ?? {});
|
|
824
921
|
if (!after.success || !isDeepStrictEqual(after.data, { ...before, ingredients: next })) {
|
|
825
922
|
throw new Error(`import: cannot edit ${label} in place (the edit does not read back as exactly the new ingredients) — reformat it by hand, commit, and re-run`);
|
|
826
923
|
}
|
|
@@ -833,7 +930,7 @@ async function writeOwnedRecipe(o, name, list, description) {
|
|
|
833
930
|
* declaring another name, would leave two recipes under one name and loadForge would keep one.
|
|
834
931
|
* Refused before the first write, as I4 refuses the same split for a profile.
|
|
835
932
|
*/
|
|
836
|
-
async function recipeFile(o, name) {
|
|
933
|
+
export async function recipeFile(o, name, command = "import") {
|
|
837
934
|
const file = path.join(o.dir, `${name}.yaml`);
|
|
838
935
|
for (const f of await o.stage.reader().list(o.dir)) {
|
|
839
936
|
if (f.includes("/") || !/\.ya?ml$/.test(f))
|
|
@@ -841,14 +938,14 @@ async function recipeFile(o, name) {
|
|
|
841
938
|
const abs = path.join(o.dir, f);
|
|
842
939
|
const declared = parseYaml(abs, stripBom(await o.stage.readText(abs)), RecipeSchema).name;
|
|
843
940
|
if (abs === file && declared !== name)
|
|
844
|
-
throw new Error(
|
|
941
|
+
throw new Error(`${command}: recipes/${f} is recipe ${declared} — ${command} writes recipe ${name} there`);
|
|
845
942
|
if (abs !== file && declared === name)
|
|
846
|
-
throw new Error(
|
|
943
|
+
throw new Error(`${command}: recipe ${name} is recipes/${f} — ${command} writes recipes/${name}.yaml`);
|
|
847
944
|
}
|
|
848
945
|
return file;
|
|
849
946
|
}
|
|
850
947
|
/** The recipes a profile resolves inside the Forge; fails closed — a profile that does not resolve counts as using everything. */
|
|
851
|
-
function recipesOf(forge, profile) {
|
|
948
|
+
export function recipesOf(forge, profile) {
|
|
852
949
|
try {
|
|
853
950
|
return new Set(resolve(forge, WorkspaceConfigSchema.parse({ forge: ".", profile })).recipes);
|
|
854
951
|
}
|
|
@@ -884,7 +981,7 @@ async function existingProfile(forge, profile, loaded) {
|
|
|
884
981
|
* created or variant source, which sync would read as structure. The line counts from the top of
|
|
885
982
|
* the workspace file, as the secret scan's does: an agent or command body starts after its frontmatter.
|
|
886
983
|
*/
|
|
887
|
-
function markerIn(meta, files, source) {
|
|
984
|
+
export function markerIn(meta, files, source) {
|
|
888
985
|
const raw = "frontmatterRaw" in meta ? meta.frontmatterRaw : undefined;
|
|
889
986
|
const offset = raw ? raw.split("\n").length + 2 : 0;
|
|
890
987
|
for (const [rel, content] of Object.entries(files)) {
|
|
@@ -964,7 +1061,7 @@ async function writeProfile(stage, forge, name, computed, ctx, report) {
|
|
|
964
1061
|
doc.set("targets", targets);
|
|
965
1062
|
}
|
|
966
1063
|
});
|
|
967
|
-
const after = ProfileSchema.safeParse(
|
|
1064
|
+
const after = ProfileSchema.safeParse(parseYamlText(label, content) ?? {});
|
|
968
1065
|
const expected = { ...before, recipes, targets, params: { ...before.params, ...values }, sections: deepMerge(before.sections, sectionValues) };
|
|
969
1066
|
if (!after.success || !isDeepStrictEqual(after.data, expected)) {
|
|
970
1067
|
throw new Error(`import: cannot edit ${label} in place (the edit does not read back as exactly the intended change) — reformat it by hand, commit, and re-run`);
|