@esneiderbravo/speclaw 0.1.11 → 0.1.12
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 +11 -3
- package/dist/cli/commands/init.js +4 -4
- package/dist/cli/commands/update.js +57 -11
- package/dist/modules/foundation/ownership.js +32 -0
- package/dist/modules/foundation/scaffold.js +21 -8
- package/dist/modules/lawbook/register.js +4 -4
- package/dist/modules/tools/packs.js +5 -3
- package/dist/shared/install.js +42 -10
- package/dist/shared/manifest.js +7 -2
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -185,9 +185,17 @@ speclaw update
|
|
|
185
185
|
```
|
|
186
186
|
|
|
187
187
|
`update` upgrades the global package **and** brings the current project up to date
|
|
188
|
-
without a re-init
|
|
189
|
-
|
|
190
|
-
|
|
188
|
+
without a re-init, splitting files by who owns them:
|
|
189
|
+
|
|
190
|
+
- **Managed files** (speclaw's workflow machinery — the skills, commands, rules,
|
|
191
|
+
and agent packs under `ai-specs/`) are **refreshed** to the new version, so
|
|
192
|
+
improvements actually reach your project. If you edited one locally, your copy
|
|
193
|
+
is saved as `<file>.bak` before the refresh — nothing is lost.
|
|
194
|
+
- **Personalized files** (your constitution and standards — `CLAUDE.md`,
|
|
195
|
+
`AGENTS.md`, `LAWS.md`, `docs/standards/*`, `docs/compass.md`,
|
|
196
|
+
`lawbook/config.yaml`) are **never auto-edited**. When a release changes their
|
|
197
|
+
speclaw-authored content, `update` prints a prompt for **the agent you're
|
|
198
|
+
using** to apply the change while preserving your project's specifics.
|
|
191
199
|
|
|
192
200
|
- `speclaw update --check` — report whether an update exists, change nothing.
|
|
193
201
|
- `NO_UPDATE_NOTIFIER=1` — silence the reminder.
|
|
@@ -113,12 +113,12 @@ export async function runInit(flags) {
|
|
|
113
113
|
c.bold(c.cream(String(stats.embeddings))) +
|
|
114
114
|
c.muted(" embeddings"));
|
|
115
115
|
}
|
|
116
|
-
// 3. Handoff prompt
|
|
117
|
-
//
|
|
118
|
-
|
|
116
|
+
// 3. Handoff prompt — printed as a single flush-left line so it copy-pastes
|
|
117
|
+
// cleanly (no borders, no wrapping artifacts). Agent-generic on purpose:
|
|
118
|
+
// speclaw supports any agent, so it addresses "the agent you're using".
|
|
119
119
|
ui.step("You're set — one last step");
|
|
120
120
|
ui.plain();
|
|
121
|
-
ui.info(`Copy this and paste it into ${c.cyan(
|
|
121
|
+
ui.info(`Copy this and paste it into ${c.cyan("the agent you're using")}:`);
|
|
122
122
|
ui.plain();
|
|
123
123
|
console.log(c.cream("Complete speclaw's foundation: analyze this repo and fill LAWS.md and " +
|
|
124
124
|
"docs/standards/* with its real architecture, quality gates and conventions. " +
|
|
@@ -5,12 +5,29 @@ import { ui, c } from "../lib/ui.js";
|
|
|
5
5
|
import { checkForUpdates, isNewer } from "../lib/update-check.js";
|
|
6
6
|
import { pkgName, pkgVersion } from "../../shared/version.js";
|
|
7
7
|
import { scaffold } from "../../modules/foundation/scaffold.js";
|
|
8
|
+
import { PERSONALIZED } from "../../modules/foundation/ownership.js";
|
|
8
9
|
import { detectConfiguredAgents } from "../../shared/agents.js";
|
|
9
10
|
import { readManifest } from "../../shared/manifest.js";
|
|
10
11
|
import { loadPacks } from "../../modules/tools/packs.js";
|
|
11
12
|
import { detectProjectName } from "./init.js";
|
|
13
|
+
// The first migration must be tagged at the version that introduces this
|
|
14
|
+
// mechanism (0.1.12): `isNewer` is strict, so an entry tagged at an already-
|
|
15
|
+
// shipped version (e.g. 0.1.11) would never fire for projects already on it.
|
|
12
16
|
const MIGRATIONS = [
|
|
13
|
-
|
|
17
|
+
{
|
|
18
|
+
version: "0.1.12",
|
|
19
|
+
describe: "Compass-first rule + reports mandatory step in personalized files",
|
|
20
|
+
agentPrompt: "- In CLAUDE.md and AGENTS.md, change the Compass rule to 'Compass first, always': " +
|
|
21
|
+
"call Compass (compass_explore/search/recall) before grep/sed/cat/Read for any code " +
|
|
22
|
+
"question — including files you already know by name — and fall back to manual file " +
|
|
23
|
+
"tools only after a Compass call returns nothing useful, the graph is missing, or the " +
|
|
24
|
+
"target isn't indexed code (CSS/JSON/logs). Update the matching Compass row in LAWS.md " +
|
|
25
|
+
"and the intro of docs/compass.md the same way.\n" +
|
|
26
|
+
"- In lawbook/config.yaml, add this mandatory task step before the docs/archive steps: " +
|
|
27
|
+
'"Produce the discipline reports under reports/ (unit/integration/e2e results for what ' +
|
|
28
|
+
'the feature touched)."\n' +
|
|
29
|
+
"- Preserve all project-specific wording; only apply these speclaw-authored changes.",
|
|
30
|
+
},
|
|
14
31
|
];
|
|
15
32
|
/**
|
|
16
33
|
* Update speclaw and bring the current project up to date without a full re-init:
|
|
@@ -84,23 +101,52 @@ function applyProjectMigrations(cwd) {
|
|
|
84
101
|
const agents = detectConfiguredAgents(cwd);
|
|
85
102
|
const known = loadPacks();
|
|
86
103
|
const packs = (readManifest(cwd)?.packs ?? []).filter((p) => p in known);
|
|
87
|
-
//
|
|
88
|
-
//
|
|
89
|
-
|
|
90
|
-
const
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
104
|
+
// Managed files (skills/commands/rules/agents) are refreshed to the current
|
|
105
|
+
// version; personalized files (constitution/standards/config) are never
|
|
106
|
+
// rewritten — those changes are handed to the user's agent below.
|
|
107
|
+
const report = scaffold(cwd, { project_name: detectProjectName(cwd) }, packs, agents, {
|
|
108
|
+
refreshManaged: true,
|
|
109
|
+
});
|
|
110
|
+
const changed = report.written.filter((w) => !w.includes(".gitignore"));
|
|
111
|
+
if (changed.length) {
|
|
112
|
+
for (const w of changed)
|
|
113
|
+
ui.ok(c.cream(path.relative(cwd, w)));
|
|
114
|
+
ui.info(`${changed.length} file(s) added/refreshed · ${report.skipped.length} left untouched.`);
|
|
95
115
|
}
|
|
96
116
|
else {
|
|
97
|
-
ui.ok("
|
|
117
|
+
ui.ok("Managed content already up to date — nothing to refresh.");
|
|
98
118
|
}
|
|
99
|
-
const
|
|
119
|
+
for (const b of report.backedUp) {
|
|
120
|
+
const rel = path.relative(cwd, b);
|
|
121
|
+
ui.warn(`${c.cream(rel)} had local edits — saved as ${rel}.bak before refreshing.`);
|
|
122
|
+
}
|
|
123
|
+
// A project several releases behind jumps straight to @latest, so apply EVERY
|
|
124
|
+
// migration newer than its recorded version — not just the next one — oldest
|
|
125
|
+
// first (sorted, so array order can't matter). Entries are cumulative: never
|
|
126
|
+
// delete a shipped migration, or a laggard crossing it later would miss it.
|
|
127
|
+
const pending = MIGRATIONS.filter((m) => isNewer(m.version, fromVersion)).sort((a, b) => isNewer(a.version, b.version) ? 1 : isNewer(b.version, a.version) ? -1 : 0);
|
|
100
128
|
for (const m of pending) {
|
|
129
|
+
if (!m.run)
|
|
130
|
+
continue;
|
|
101
131
|
ui.step(`Step for ${m.version}: ${m.describe}`);
|
|
102
132
|
m.run(cwd, report);
|
|
103
133
|
}
|
|
134
|
+
// Personalized files can't be auto-edited (they hold project specifics), so
|
|
135
|
+
// hand their changes to whatever agent the user runs — never a hardcoded one.
|
|
136
|
+
const prompt = pending
|
|
137
|
+
.map((m) => m.agentPrompt)
|
|
138
|
+
.filter(Boolean)
|
|
139
|
+
.join("\n");
|
|
140
|
+
if (prompt) {
|
|
141
|
+
ui.plain();
|
|
142
|
+
ui.step("One step for the agent you're using");
|
|
143
|
+
ui.info(`Personalized files (${PERSONALIZED.join(", ")}) hold your project specifics, so ` +
|
|
144
|
+
`speclaw won't edit them. Paste this into the agent you're using to apply this ` +
|
|
145
|
+
`release's changes while keeping your content:`);
|
|
146
|
+
ui.plain();
|
|
147
|
+
console.log(c.cream(prompt));
|
|
148
|
+
ui.plain();
|
|
149
|
+
}
|
|
104
150
|
ui.plain();
|
|
105
151
|
ui.ok(`On ${c.cyan(pkgVersion())}. No re-init needed.`);
|
|
106
152
|
}
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
// Which scaffolded files speclaw owns vs the user owns. `speclaw update` uses
|
|
2
|
+
// this split to decide what it may overwrite automatically (managed) and what it
|
|
3
|
+
// must leave to the user's agent via a prompt (personalized).
|
|
4
|
+
/**
|
|
5
|
+
* Project-relative trees that hold speclaw's workflow machinery. The user is not
|
|
6
|
+
* meant to edit these, so `speclaw update` overwrites them with the current
|
|
7
|
+
* version (backing up any local edits to `<file>.bak` first).
|
|
8
|
+
*/
|
|
9
|
+
export const MANAGED_TREES = [
|
|
10
|
+
"ai-specs/skills",
|
|
11
|
+
"ai-specs/commands",
|
|
12
|
+
"ai-specs/rules",
|
|
13
|
+
"ai-specs/agents",
|
|
14
|
+
];
|
|
15
|
+
/**
|
|
16
|
+
* Files filled with project specifics at init. `speclaw update` never rewrites
|
|
17
|
+
* these; when a release changes their speclaw-authored content, update emits an
|
|
18
|
+
* agent prompt to apply the change while preserving the user's content.
|
|
19
|
+
*/
|
|
20
|
+
export const PERSONALIZED = [
|
|
21
|
+
"CLAUDE.md",
|
|
22
|
+
"AGENTS.md",
|
|
23
|
+
"LAWS.md",
|
|
24
|
+
"docs/standards",
|
|
25
|
+
"docs/compass.md",
|
|
26
|
+
"lawbook/config.yaml",
|
|
27
|
+
];
|
|
28
|
+
/** True if a project-relative path sits under a managed tree. */
|
|
29
|
+
export function isManaged(relPath) {
|
|
30
|
+
const norm = relPath.split("\\").join("/");
|
|
31
|
+
return MANAGED_TREES.some((t) => norm === t || norm.startsWith(t + "/"));
|
|
32
|
+
}
|
|
@@ -6,7 +6,7 @@ import { emptyReport, ensureGitignore } from "../../shared/install.js";
|
|
|
6
6
|
import { configureAgent } from "../../shared/agents.js";
|
|
7
7
|
import { installWorkflow } from "../lawbook/register.js";
|
|
8
8
|
import { installPack, loadPacks } from "../tools/packs.js";
|
|
9
|
-
import { writeManifest } from "../../shared/manifest.js";
|
|
9
|
+
import { readManifest, writeManifest } from "../../shared/manifest.js";
|
|
10
10
|
import { pkgVersion } from "../../shared/version.js";
|
|
11
11
|
const ASSETS = assetsDir(import.meta.url);
|
|
12
12
|
// Every {{var}} the foundation templates may reference. Ones the agent didn't
|
|
@@ -70,10 +70,13 @@ function renderFoundation(projectPath, vars, report) {
|
|
|
70
70
|
* @param profile - Project identity and conventions for template rendering.
|
|
71
71
|
* @param packNames - Tool pack names to install (the spec workflow is always installed).
|
|
72
72
|
* @param agents - Agent ids to configure with symlinks + MCP; empty writes content only.
|
|
73
|
+
* @param opts - `refreshManaged: true` overwrites the managed trees (skills,
|
|
74
|
+
* commands, rules, agents) with the current version, backing up local edits to
|
|
75
|
+
* `<file>.bak`. Default (init) is additive — existing files are kept.
|
|
73
76
|
* @returns The install report augmented with the ordered next steps to run.
|
|
74
77
|
* @throws If `projectPath` does not exist, or any pack name is unknown.
|
|
75
78
|
*/
|
|
76
|
-
export function scaffold(projectPath, profile, packNames, agents = []) {
|
|
79
|
+
export function scaffold(projectPath, profile, packNames, agents = [], opts = {}) {
|
|
77
80
|
if (!fs.existsSync(projectPath)) {
|
|
78
81
|
throw new Error(`projectPath does not exist: ${projectPath}`);
|
|
79
82
|
}
|
|
@@ -83,16 +86,26 @@ export function scaffold(projectPath, profile, packNames, agents = []) {
|
|
|
83
86
|
throw new Error(`Unknown packs: ${unknown.join(", ")}`);
|
|
84
87
|
const report = { ...emptyReport(), nextSteps: [] };
|
|
85
88
|
const vars = { ...FOUNDATION_DEFAULTS, ...profile };
|
|
86
|
-
|
|
87
|
-
|
|
89
|
+
// Managed trees (MANAGED_TREES) carry speclaw's workflow logic and may be
|
|
90
|
+
// overwritten on update; the foundation (personalized) is always additive.
|
|
91
|
+
const record = {};
|
|
92
|
+
const managedOpts = {
|
|
93
|
+
overwrite: Boolean(opts.refreshManaged),
|
|
94
|
+
projectPath,
|
|
95
|
+
baselines: readManifest(projectPath)?.baselines ?? {},
|
|
96
|
+
record,
|
|
97
|
+
};
|
|
98
|
+
renderFoundation(projectPath, vars, report); // personalized — never overwritten
|
|
99
|
+
installWorkflow(projectPath, vars, report, managedOpts); // managed
|
|
88
100
|
for (const name of packNames)
|
|
89
|
-
installPack(projectPath, name, vars, report); //
|
|
101
|
+
installPack(projectPath, name, vars, report, managedOpts); // managed
|
|
90
102
|
ensureGitignore(projectPath, ".speclaw/", "speclaw local code Compass (never commit)", report);
|
|
91
103
|
for (const id of agents)
|
|
92
104
|
configureAgent(projectPath, id, report); // only the chosen agents
|
|
93
|
-
// Record what was installed so `speclaw update` can re-apply
|
|
94
|
-
//
|
|
95
|
-
|
|
105
|
+
// Record what was installed so `speclaw update` can re-apply these packs and
|
|
106
|
+
// gate feature migrations by version, plus the managed-file baselines that let
|
|
107
|
+
// a later update tell user edits from stale files.
|
|
108
|
+
writeManifest(projectPath, pkgVersion(), packNames, record);
|
|
96
109
|
report.nextSteps = [
|
|
97
110
|
"Run the `lawbook_init` tool to set up the spec-driven workflow (creates lawbook/). No external CLI needed — it's built into speclaw.",
|
|
98
111
|
"Run the `compass_index` tool to build the local code graph (.speclaw/). No install, no LLM — it's built into speclaw. Re-run it after significant edits.",
|
|
@@ -10,11 +10,11 @@ const ASSETS = assetsDir(import.meta.url);
|
|
|
10
10
|
* the draft/build/sync/archive/explore skills, the /spec commands, and the
|
|
11
11
|
* mandatory-task-steps rule. Always installed — it's the core workflow.
|
|
12
12
|
*/
|
|
13
|
-
export function installWorkflow(projectPath, vars, report) {
|
|
13
|
+
export function installWorkflow(projectPath, vars, report, opts) {
|
|
14
14
|
const aiSpecs = path.join(projectPath, "ai-specs");
|
|
15
|
-
copyRendered(path.join(ASSETS, "skills"), path.join(aiSpecs, "skills"), vars, report);
|
|
16
|
-
copyRendered(path.join(ASSETS, "commands"), path.join(aiSpecs, "commands", "lawbook"), vars, report);
|
|
17
|
-
copyRendered(path.join(ASSETS, "rules"), path.join(aiSpecs, "rules"), vars, report);
|
|
15
|
+
copyRendered(path.join(ASSETS, "skills"), path.join(aiSpecs, "skills"), vars, report, opts);
|
|
16
|
+
copyRendered(path.join(ASSETS, "commands"), path.join(aiSpecs, "commands", "lawbook"), vars, report, opts);
|
|
17
|
+
copyRendered(path.join(ASSETS, "rules"), path.join(aiSpecs, "rules"), vars, report, opts);
|
|
18
18
|
}
|
|
19
19
|
// ─── The spec module: speclaw's own spec-driven workflow (no external OpenSpec) ───
|
|
20
20
|
// Mechanical operations behind the draft/build/sync/archive/explore commands.
|
|
@@ -21,9 +21,11 @@ export function loadPacks() {
|
|
|
21
21
|
* @param name - Pack name to install (key in the manifest).
|
|
22
22
|
* @param vars - Template variables applied while copying the pack's assets.
|
|
23
23
|
* @param report - Mutated in place with the copy results.
|
|
24
|
+
* @param opts - Overwrite/baseline behavior forwarded to {@link copyRendered}
|
|
25
|
+
* (packs write into managed trees, so update refreshes them).
|
|
24
26
|
* @throws If no pack with the given name exists in the manifest.
|
|
25
27
|
*/
|
|
26
|
-
export function installPack(projectPath, name, vars, report) {
|
|
28
|
+
export function installPack(projectPath, name, vars, report, opts) {
|
|
27
29
|
const packs = loadPacks();
|
|
28
30
|
const def = packs[name];
|
|
29
31
|
if (!def) {
|
|
@@ -33,11 +35,11 @@ export function installPack(projectPath, name, vars, report) {
|
|
|
33
35
|
const aiSpecs = path.join(projectPath, "ai-specs");
|
|
34
36
|
for (const sub of fs.readdirSync(packRoot, { withFileTypes: true })) {
|
|
35
37
|
if (sub.isDirectory()) {
|
|
36
|
-
copyRendered(path.join(packRoot, sub.name), path.join(aiSpecs, sub.name), vars, report);
|
|
38
|
+
copyRendered(path.join(packRoot, sub.name), path.join(aiSpecs, sub.name), vars, report, opts);
|
|
37
39
|
}
|
|
38
40
|
else if (sub.name.endsWith(".md")) {
|
|
39
41
|
// loose .md at pack root = agent definitions
|
|
40
|
-
copyRendered(packRoot, path.join(aiSpecs, "agents"), vars, report);
|
|
42
|
+
copyRendered(packRoot, path.join(aiSpecs, "agents"), vars, report, opts);
|
|
41
43
|
break;
|
|
42
44
|
}
|
|
43
45
|
}
|
package/dist/shared/install.js
CHANGED
|
@@ -1,9 +1,14 @@
|
|
|
1
|
+
import crypto from "node:crypto";
|
|
1
2
|
import fs from "node:fs";
|
|
2
3
|
import path from "node:path";
|
|
3
4
|
import { render } from "./render.js";
|
|
4
5
|
/** Create a fresh, empty {@link InstallReport} to accumulate results into. */
|
|
5
6
|
export function emptyReport() {
|
|
6
|
-
return { written: [], skipped: [], symlinks: [], unresolvedVars: [] };
|
|
7
|
+
return { written: [], skipped: [], backedUp: [], symlinks: [], unresolvedVars: [] };
|
|
8
|
+
}
|
|
9
|
+
/** SHA-256 of a file's intended content, used to track managed-file baselines. */
|
|
10
|
+
export function sha256(content) {
|
|
11
|
+
return crypto.createHash("sha256").update(content).digest("hex");
|
|
7
12
|
}
|
|
8
13
|
/**
|
|
9
14
|
* Recursively copy a source tree into a destination, rendering {{var}}
|
|
@@ -13,33 +18,60 @@ export function emptyReport() {
|
|
|
13
18
|
* @param srcDir - Source directory tree to copy from.
|
|
14
19
|
* @param destDir - Destination directory (created if missing).
|
|
15
20
|
* @param vars - Placeholder values used to render `.md`/`.mdc` files.
|
|
16
|
-
* @param report - Report mutated in place with written, skipped,
|
|
21
|
+
* @param report - Report mutated in place with written, skipped, backed-up, and
|
|
22
|
+
* unresolved-var entries.
|
|
23
|
+
* @param opts - Overwrite/baseline behavior; omitted means additive (skip existing).
|
|
17
24
|
*/
|
|
18
|
-
export function copyRendered(srcDir, destDir, vars, report) {
|
|
25
|
+
export function copyRendered(srcDir, destDir, vars, report, opts) {
|
|
19
26
|
fs.mkdirSync(destDir, { recursive: true });
|
|
20
27
|
for (const entry of fs.readdirSync(srcDir, { withFileTypes: true })) {
|
|
21
28
|
const src = path.join(srcDir, entry.name);
|
|
22
29
|
const dest = path.join(destDir, entry.name);
|
|
23
30
|
if (entry.isDirectory()) {
|
|
24
|
-
copyRendered(src, dest, vars, report);
|
|
25
|
-
continue;
|
|
26
|
-
}
|
|
27
|
-
if (fs.existsSync(dest)) {
|
|
28
|
-
report.skipped.push(dest);
|
|
31
|
+
copyRendered(src, dest, vars, report, opts);
|
|
29
32
|
continue;
|
|
30
33
|
}
|
|
34
|
+
// Compute the content speclaw wants at dest (rendered for md/mdc, raw otherwise).
|
|
35
|
+
let content;
|
|
31
36
|
if (entry.name.endsWith(".md") || entry.name.endsWith(".mdc")) {
|
|
32
37
|
const { output, unresolved } = render(fs.readFileSync(src, "utf8"), vars);
|
|
33
38
|
unresolved.forEach((v) => {
|
|
34
39
|
if (!report.unresolvedVars.includes(v))
|
|
35
40
|
report.unresolvedVars.push(v);
|
|
36
41
|
});
|
|
37
|
-
|
|
42
|
+
content = output;
|
|
43
|
+
}
|
|
44
|
+
else {
|
|
45
|
+
content = fs.readFileSync(src);
|
|
46
|
+
}
|
|
47
|
+
const rel = opts?.projectPath ? path.relative(opts.projectPath, dest) : dest;
|
|
48
|
+
const newSha = sha256(content);
|
|
49
|
+
if (fs.existsSync(dest)) {
|
|
50
|
+
if (!opts?.overwrite) {
|
|
51
|
+
report.skipped.push(dest);
|
|
52
|
+
continue;
|
|
53
|
+
}
|
|
54
|
+
const current = fs.readFileSync(dest);
|
|
55
|
+
if (sha256(current) === newSha) {
|
|
56
|
+
// Already the current version — nothing to write, but keep the baseline.
|
|
57
|
+
if (opts.record)
|
|
58
|
+
opts.record[rel] = newSha;
|
|
59
|
+
continue;
|
|
60
|
+
}
|
|
61
|
+
const baseline = opts.baselines?.[rel];
|
|
62
|
+
if (!baseline || sha256(current) !== baseline) {
|
|
63
|
+
// Diverged from what we last wrote (or unknown) — preserve the user's copy.
|
|
64
|
+
fs.copyFileSync(dest, dest + ".bak");
|
|
65
|
+
report.backedUp.push(dest);
|
|
66
|
+
}
|
|
67
|
+
fs.writeFileSync(dest, content);
|
|
38
68
|
}
|
|
39
69
|
else {
|
|
40
|
-
fs.
|
|
70
|
+
fs.writeFileSync(dest, content);
|
|
41
71
|
}
|
|
42
72
|
report.written.push(dest);
|
|
73
|
+
if (opts?.record)
|
|
74
|
+
opts.record[rel] = newSha;
|
|
43
75
|
}
|
|
44
76
|
}
|
|
45
77
|
/**
|
package/dist/shared/manifest.js
CHANGED
|
@@ -15,6 +15,9 @@ export function readManifest(projectPath) {
|
|
|
15
15
|
return {
|
|
16
16
|
version: String(m.version ?? "0.0.0"),
|
|
17
17
|
packs: Array.isArray(m.packs) ? m.packs.map(String) : [],
|
|
18
|
+
baselines: m.baselines && typeof m.baselines === "object"
|
|
19
|
+
? m.baselines
|
|
20
|
+
: {},
|
|
18
21
|
};
|
|
19
22
|
}
|
|
20
23
|
catch {
|
|
@@ -28,11 +31,13 @@ export function readManifest(projectPath) {
|
|
|
28
31
|
* @param projectPath - Project root to write into.
|
|
29
32
|
* @param version - The speclaw version doing the write.
|
|
30
33
|
* @param packs - Pack names installed in this run.
|
|
34
|
+
* @param baselines - Managed-file hashes to merge over the recorded ones.
|
|
31
35
|
*/
|
|
32
|
-
export function writeManifest(projectPath, version, packs) {
|
|
36
|
+
export function writeManifest(projectPath, version, packs, baselines = {}) {
|
|
33
37
|
const prev = readManifest(projectPath);
|
|
34
38
|
const merged = Array.from(new Set([...(prev?.packs ?? []), ...packs]));
|
|
39
|
+
const mergedBaselines = { ...(prev?.baselines ?? {}), ...baselines };
|
|
35
40
|
const p = manifestPath(projectPath);
|
|
36
41
|
fs.mkdirSync(path.dirname(p), { recursive: true });
|
|
37
|
-
fs.writeFileSync(p, JSON.stringify({ version, packs: merged }, null, 2) + "\n");
|
|
42
|
+
fs.writeFileSync(p, JSON.stringify({ version, packs: merged, baselines: mergedBaselines }, null, 2) + "\n");
|
|
38
43
|
}
|