@warnyin/sdlc 0.14.0 → 0.14.2
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/CHANGELOG.md +32 -0
- package/bin/cli.mjs +56 -8
- package/package.json +1 -1
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,37 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
+
## 0.14.2 (2026-09-19)
|
|
4
|
+
|
|
5
|
+
- **Fix (init)**: `init` no longer disowns the tools it did not install that run. It rebuilt the
|
|
6
|
+
ownership manifest from scratch and wrote it wholesale, so running `init` a second time for a
|
|
7
|
+
different tool dropped every entry belonging to the first — the files stayed on disk, but
|
|
8
|
+
`update` could no longer recognise them. The damage showed up later: such a file was **never
|
|
9
|
+
refreshed again** (refresh needs the recorded hash to match what is on disk) and was reported
|
|
10
|
+
as `kept (user-modified)`, blaming you for an edit you never made. `init` now carries forward
|
|
11
|
+
every entry it does not rewrite. It still never prunes — only `update --tool <list>` removes
|
|
12
|
+
a tool, and now that a tool installed by its own `init` run is properly owned, deselecting it
|
|
13
|
+
there prunes its files as it always should have.
|
|
14
|
+
- **If a project already hit this**, the fix cannot recover a hash nothing recorded. A disowned
|
|
15
|
+
file that still matches the current payload is re-claimed silently on your next `update`; one
|
|
16
|
+
that has already drifted stays frozen and keeps reporting `kept (user-modified)`. To recover
|
|
17
|
+
it, delete that file and run `update` — it is rewritten from the payload and owned again.
|
|
18
|
+
|
|
19
|
+
## 0.14.1 (2026-09-19)
|
|
20
|
+
|
|
21
|
+
- **Fix (init)**: adding a tool to a project that already has `sdlc/` no longer loses it on the
|
|
22
|
+
next update. `init --tool <newtool>` installed the tool's files but never recorded it in
|
|
23
|
+
`sdlc/config.yaml`'s `tools:` line, so the next plain `update` read that stale list and
|
|
24
|
+
**silently pruned the files it had just installed**. `init` now records what it installs,
|
|
25
|
+
added to whatever was already listed — never removing an entry, because `init` has no prune
|
|
26
|
+
capability and this does not give it one. Only `update --tool <list>` still replaces the list
|
|
27
|
+
wholesale and prunes what is left out. The install summary says `(recorded <tools>)` when the
|
|
28
|
+
list actually grew, and only then. A `config.yaml` carrying no `tools:` line at all is still
|
|
29
|
+
left alone, exactly as `update` already leaves it.
|
|
30
|
+
- **Fix (config)**: the `tools:` line rewrite — now shared by `init` and `update` instead of
|
|
31
|
+
living inline in one of them — passes its replacement as a function rather than a string, so
|
|
32
|
+
a tool name containing `$&`, `` $` `` or `$'` in a hand-edited config is written literally
|
|
33
|
+
instead of being interpreted by `String.replace` and mangling the surrounding file.
|
|
34
|
+
|
|
3
35
|
## 0.14.0 (2026-09-19)
|
|
4
36
|
|
|
5
37
|
- **Feature (init)**: `kimi` (Kimi Code CLI) is now a supported tool. Selecting or detecting it
|
package/bin/cli.mjs
CHANGED
|
@@ -312,7 +312,7 @@ export async function resolveTools(args, {
|
|
|
312
312
|
return picked;
|
|
313
313
|
}
|
|
314
314
|
|
|
315
|
-
function printInitSummary(tools, ctx, style, symbols, { configExisted }) {
|
|
315
|
+
function printInitSummary(tools, ctx, style, symbols, { configExisted, toolsAdded }) {
|
|
316
316
|
const s = summarizeInstall(ctx.manifest.keys(), tools);
|
|
317
317
|
const stats = ctx.stats;
|
|
318
318
|
const line = (text) => console.log(` ${text}`);
|
|
@@ -329,7 +329,10 @@ function printInitSummary(tools, ctx, style, symbols, { configExisted }) {
|
|
|
329
329
|
}
|
|
330
330
|
line(`${s.hooks} hooks in sdlc/.hooks/`);
|
|
331
331
|
line(`Playbook: sdlc/.playbook/ (${s.playbook} stages + ${s.templates} templates)`);
|
|
332
|
-
|
|
332
|
+
const configNote = toolsAdded?.length
|
|
333
|
+
? ` (recorded ${toolsAdded.map(toolName).join(', ')})`
|
|
334
|
+
: configExisted ? ' (kept)' : '';
|
|
335
|
+
line(`Config: sdlc/config.yaml${configNote}`);
|
|
333
336
|
line(style.dim(`Files: ${stats.written} written · ${stats.current} unchanged · ${stats.updated} refreshed · ${stats.kept} kept (yours)`));
|
|
334
337
|
console.log('');
|
|
335
338
|
console.log(` ${style.bold('Getting started:')}`);
|
|
@@ -341,7 +344,26 @@ export async function cmdInit(projectRoot, args) {
|
|
|
341
344
|
const style = createStyle(colorEnabled());
|
|
342
345
|
const symbols = symbolsFor();
|
|
343
346
|
const tools = await resolveTools(args, { projectRoot, style });
|
|
344
|
-
const
|
|
347
|
+
const configPath = path.join(projectRoot, 'sdlc', 'config.yaml');
|
|
348
|
+
const configExisted = fs.existsSync(configPath);
|
|
349
|
+
|
|
350
|
+
// config.yaml is a seed — written once, never rewritten wholesale. But its `tools:` line
|
|
351
|
+
// is the one field this command promises to keep current ("filled by `warnyin-sdlc init`"
|
|
352
|
+
// in the template), so a second init adding a tool must not leave it stale: union what's
|
|
353
|
+
// already recorded with what this run installs, never removing an entry (init has no prune
|
|
354
|
+
// capability, and this doesn't give it one — only `update --tool <list>` can shrink the set).
|
|
355
|
+
let toolsAdded = [];
|
|
356
|
+
if (configExisted) {
|
|
357
|
+
const recorded = parseConfig(fs.readFileSync(configPath, 'utf8')).tools;
|
|
358
|
+
const newlySelected = tools.filter((t) => !recorded.includes(t));
|
|
359
|
+
// Only claim "recorded" in the summary when a write actually happened — a config
|
|
360
|
+
// predating the `tools:` key makes persistToolsLine a no-op, and the message must not
|
|
361
|
+
// say otherwise.
|
|
362
|
+
if (newlySelected.length && persistToolsLine(configPath, [...recorded, ...newlySelected])) {
|
|
363
|
+
toolsAdded = newlySelected;
|
|
364
|
+
}
|
|
365
|
+
}
|
|
366
|
+
|
|
345
367
|
const ctx = {
|
|
346
368
|
mode: 'install',
|
|
347
369
|
manifest: new Map(),
|
|
@@ -351,10 +373,19 @@ export async function cmdInit(projectRoot, args) {
|
|
|
351
373
|
};
|
|
352
374
|
scaffoldSdlc(projectRoot, tools, ctx);
|
|
353
375
|
installToolAdapters(projectRoot, tools, ctx);
|
|
354
|
-
|
|
376
|
+
// Carry forward every entry this run did not rewrite. `init` installs only the tools it was
|
|
377
|
+
// given, so writing `ctx.manifest` alone would disown the files of every OTHER tool already
|
|
378
|
+
// installed here — they stay on disk, but `update` then sees files it has no record of,
|
|
379
|
+
// refuses to refresh them (its refresh branch needs disk hash === recorded hash), freezes
|
|
380
|
+
// them at their old payload version and reports them as user-modified. Same failure
|
|
381
|
+
// `installFile` documents fixing per-file, reached instead by never visiting the entry.
|
|
382
|
+
// The merge happens HERE, not by seeding `ctx.manifest`: that Map is also what the install
|
|
383
|
+
// summary counts, and it must keep describing only what this run installed. `cmdUpdate`
|
|
384
|
+
// deliberately does NOT do this — its wholesale replace is what tells prune a tool is gone.
|
|
385
|
+
writeManifestFile(projectRoot, new Map([...ctx.oldManifest, ...ctx.manifest]));
|
|
355
386
|
ensureGitignore(projectRoot);
|
|
356
387
|
for (const w of ctx.warnings) console.warn(` ${style.yellow(symbols.warn)} ${w}`);
|
|
357
|
-
printInitSummary(tools, ctx, style, symbols, { configExisted });
|
|
388
|
+
printInitSummary(tools, ctx, style, symbols, { configExisted, toolsAdded });
|
|
358
389
|
return { tools };
|
|
359
390
|
}
|
|
360
391
|
|
|
@@ -395,6 +426,25 @@ export function forceNeedsAPerson(args, env = process.env, stdin = process.stdin
|
|
|
395
426
|
+ ' Run it yourself, or set WARNYIN_SDLC_FORCE=1 if this really is automation that meant it.';
|
|
396
427
|
}
|
|
397
428
|
|
|
429
|
+
// Rewrites just the `tools:` line of `config.yaml` in place, leaving every other line
|
|
430
|
+
// untouched — a no-op when the file carries no `tools:` line at all (a config predating the
|
|
431
|
+
// key; matched, not fixed). Shared by `cmdInit` and `cmdUpdate` so the two paths can't drift.
|
|
432
|
+
// Reproduces the existing behavior exactly, trailing inline comment included: the line is
|
|
433
|
+
// replaced wholesale, so a hand-added comment after `tools: [...]` does not survive a rewrite.
|
|
434
|
+
// Returns whether it actually wrote — callers that report "recorded X" to the user must not
|
|
435
|
+
// claim it when this was a no-op (no `tools:` line to rewrite).
|
|
436
|
+
export function persistToolsLine(configPath, tools) {
|
|
437
|
+
const raw = fs.readFileSync(configPath, 'utf8');
|
|
438
|
+
if (!/^tools:/m.test(raw)) return false;
|
|
439
|
+
// A replacer FUNCTION, not a string: `recorded` (unioned in by cmdInit) is free text read
|
|
440
|
+
// back from the project's own config.yaml, not validated against the TOOLS registry the way
|
|
441
|
+
// CLI-sourced tool names are — a string replacement would let `$&`/`$'`/`` $` `` in a
|
|
442
|
+
// hand-edited or corrupted config get interpreted as replacement patterns instead of literal
|
|
443
|
+
// text, silently duplicating or mangling the surrounding file.
|
|
444
|
+
fs.writeFileSync(configPath, raw.replace(/^tools:.*$/m, () => `tools: [${tools.join(', ')}]`));
|
|
445
|
+
return true;
|
|
446
|
+
}
|
|
447
|
+
|
|
398
448
|
export function cmdUpdate(projectRoot, args) {
|
|
399
449
|
const refusal = refuseSelfUpdate(projectRoot, PKG_ROOT);
|
|
400
450
|
if (refusal) throw new Error(refusal);
|
|
@@ -414,9 +464,7 @@ export function cmdUpdate(projectRoot, args) {
|
|
|
414
464
|
// Persist an explicit --tool override so declared and installed state never
|
|
415
465
|
// diverge (otherwise pruning tool-specific files leaves config.yaml stale).
|
|
416
466
|
if (args.toolProvided) {
|
|
417
|
-
|
|
418
|
-
const raw = fs.readFileSync(configPath, 'utf8');
|
|
419
|
-
fs.writeFileSync(configPath, raw.replace(/^tools:.*$/m, `tools: [${tools.join(', ')}]`));
|
|
467
|
+
persistToolsLine(path.join(sdlcRoot, 'config.yaml'), tools);
|
|
420
468
|
}
|
|
421
469
|
|
|
422
470
|
// Read before scaffolding: recordPayloadVersion overwrites version.json with our own.
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@warnyin/sdlc",
|
|
3
|
-
"version": "0.14.
|
|
3
|
+
"version": "0.14.2",
|
|
4
4
|
"description": "Spec-driven, AI-driven SDLC framework — token-lean specs, contract-first changes, autonomous pipeline with managed hooks. Operationalizes the Day-1 'New SDLC with Vibe Coding' work process.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"bin": {
|