@warnyin/sdlc 0.13.0 → 0.14.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/CHANGELOG.md +27 -0
- package/bin/cli.mjs +50 -8
- package/bin/detect.mjs +3 -0
- package/lib/manifest.mjs +1 -0
- package/package.json +1 -1
- package/payload/adapters/kimi.md +4 -0
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,32 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
+
## 0.14.1 (2026-09-19)
|
|
4
|
+
|
|
5
|
+
- **Fix (init)**: adding a tool to a project that already has `sdlc/` no longer loses it on the
|
|
6
|
+
next update. `init --tool <newtool>` installed the tool's files but never recorded it in
|
|
7
|
+
`sdlc/config.yaml`'s `tools:` line, so the next plain `update` read that stale list and
|
|
8
|
+
**silently pruned the files it had just installed**. `init` now records what it installs,
|
|
9
|
+
added to whatever was already listed — never removing an entry, because `init` has no prune
|
|
10
|
+
capability and this does not give it one. Only `update --tool <list>` still replaces the list
|
|
11
|
+
wholesale and prunes what is left out. The install summary says `(recorded <tools>)` when the
|
|
12
|
+
list actually grew, and only then. A `config.yaml` carrying no `tools:` line at all is still
|
|
13
|
+
left alone, exactly as `update` already leaves it.
|
|
14
|
+
- **Fix (config)**: the `tools:` line rewrite — now shared by `init` and `update` instead of
|
|
15
|
+
living inline in one of them — passes its replacement as a function rather than a string, so
|
|
16
|
+
a tool name containing `$&`, `` $` `` or `$'` in a hand-edited config is written literally
|
|
17
|
+
instead of being interpreted by `String.replace` and mangling the surrounding file.
|
|
18
|
+
|
|
19
|
+
## 0.14.0 (2026-09-19)
|
|
20
|
+
|
|
21
|
+
- **Feature (init)**: `kimi` (Kimi Code CLI) is now a supported tool. Selecting or detecting it
|
|
22
|
+
installs a dedicated `.kimi-code/AGENTS.md` — the shared rules-card plus a pointer to
|
|
23
|
+
`sdlc/.playbook/` — the same lite-tier treatment every non-Claude tool gets: no hooks, no
|
|
24
|
+
skills, no agents, just the rules-card and the validator as the enforcement floor. Detection
|
|
25
|
+
looks for an existing `.kimi-code/` directory, mirroring `claude`/`cursor`/`windsurf`; its
|
|
26
|
+
absence never blocks picking `kimi` explicitly with `--tool kimi` or `--tool all`. The file
|
|
27
|
+
is manifest-owned like `.cursor/rules/sdlc.mdc`: a hand-written file at that path is left
|
|
28
|
+
untouched and never claimed, and it is correctly pruned if a project later deselects the tool.
|
|
29
|
+
|
|
3
30
|
## 0.13.0 (2026-09-19)
|
|
4
31
|
|
|
5
32
|
- **Feature (update notice)**: the notice that a newer version exists now **asks** instead of
|
package/bin/cli.mjs
CHANGED
|
@@ -34,7 +34,7 @@ const MARKER = '<!-- sdlc:start -->';
|
|
|
34
34
|
const OWN_PACKAGE_NAME = '@warnyin/sdlc';
|
|
35
35
|
|
|
36
36
|
export const TOOLS = Object.freeze([
|
|
37
|
-
'claude', 'cursor', 'windsurf', 'copilot', 'cline', 'gemini', 'agents-md',
|
|
37
|
+
'claude', 'cursor', 'windsurf', 'copilot', 'cline', 'gemini', 'agents-md', 'kimi',
|
|
38
38
|
]);
|
|
39
39
|
|
|
40
40
|
const TEXT_EXT = new Set(['.md', '.mdc', '.mjs', '.json', '.yaml', '.yml', '.txt']);
|
|
@@ -203,6 +203,9 @@ function installToolAdapters(projectRoot, tools, ctx) {
|
|
|
203
203
|
if (tools.includes('agents-md')) {
|
|
204
204
|
appendWithMarker(projectRoot, 'AGENTS.md', renderAdapter('adapters/agents-md.md'));
|
|
205
205
|
}
|
|
206
|
+
if (tools.includes('kimi')) {
|
|
207
|
+
installFile(projectRoot, path.join('.kimi-code', 'AGENTS.md'), renderAdapter('adapters/kimi.md'), ctx);
|
|
208
|
+
}
|
|
206
209
|
}
|
|
207
210
|
|
|
208
211
|
// ---------- scaffold ----------
|
|
@@ -309,7 +312,7 @@ export async function resolveTools(args, {
|
|
|
309
312
|
return picked;
|
|
310
313
|
}
|
|
311
314
|
|
|
312
|
-
function printInitSummary(tools, ctx, style, symbols, { configExisted }) {
|
|
315
|
+
function printInitSummary(tools, ctx, style, symbols, { configExisted, toolsAdded }) {
|
|
313
316
|
const s = summarizeInstall(ctx.manifest.keys(), tools);
|
|
314
317
|
const stats = ctx.stats;
|
|
315
318
|
const line = (text) => console.log(` ${text}`);
|
|
@@ -326,7 +329,10 @@ function printInitSummary(tools, ctx, style, symbols, { configExisted }) {
|
|
|
326
329
|
}
|
|
327
330
|
line(`${s.hooks} hooks in sdlc/.hooks/`);
|
|
328
331
|
line(`Playbook: sdlc/.playbook/ (${s.playbook} stages + ${s.templates} templates)`);
|
|
329
|
-
|
|
332
|
+
const configNote = toolsAdded?.length
|
|
333
|
+
? ` (recorded ${toolsAdded.map(toolName).join(', ')})`
|
|
334
|
+
: configExisted ? ' (kept)' : '';
|
|
335
|
+
line(`Config: sdlc/config.yaml${configNote}`);
|
|
330
336
|
line(style.dim(`Files: ${stats.written} written · ${stats.current} unchanged · ${stats.updated} refreshed · ${stats.kept} kept (yours)`));
|
|
331
337
|
console.log('');
|
|
332
338
|
console.log(` ${style.bold('Getting started:')}`);
|
|
@@ -338,7 +344,26 @@ export async function cmdInit(projectRoot, args) {
|
|
|
338
344
|
const style = createStyle(colorEnabled());
|
|
339
345
|
const symbols = symbolsFor();
|
|
340
346
|
const tools = await resolveTools(args, { projectRoot, style });
|
|
341
|
-
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
|
+
|
|
342
367
|
const ctx = {
|
|
343
368
|
mode: 'install',
|
|
344
369
|
manifest: new Map(),
|
|
@@ -351,7 +376,7 @@ export async function cmdInit(projectRoot, args) {
|
|
|
351
376
|
writeManifestFile(projectRoot, ctx.manifest);
|
|
352
377
|
ensureGitignore(projectRoot);
|
|
353
378
|
for (const w of ctx.warnings) console.warn(` ${style.yellow(symbols.warn)} ${w}`);
|
|
354
|
-
printInitSummary(tools, ctx, style, symbols, { configExisted });
|
|
379
|
+
printInitSummary(tools, ctx, style, symbols, { configExisted, toolsAdded });
|
|
355
380
|
return { tools };
|
|
356
381
|
}
|
|
357
382
|
|
|
@@ -392,6 +417,25 @@ export function forceNeedsAPerson(args, env = process.env, stdin = process.stdin
|
|
|
392
417
|
+ ' Run it yourself, or set WARNYIN_SDLC_FORCE=1 if this really is automation that meant it.';
|
|
393
418
|
}
|
|
394
419
|
|
|
420
|
+
// Rewrites just the `tools:` line of `config.yaml` in place, leaving every other line
|
|
421
|
+
// untouched — a no-op when the file carries no `tools:` line at all (a config predating the
|
|
422
|
+
// key; matched, not fixed). Shared by `cmdInit` and `cmdUpdate` so the two paths can't drift.
|
|
423
|
+
// Reproduces the existing behavior exactly, trailing inline comment included: the line is
|
|
424
|
+
// replaced wholesale, so a hand-added comment after `tools: [...]` does not survive a rewrite.
|
|
425
|
+
// Returns whether it actually wrote — callers that report "recorded X" to the user must not
|
|
426
|
+
// claim it when this was a no-op (no `tools:` line to rewrite).
|
|
427
|
+
export function persistToolsLine(configPath, tools) {
|
|
428
|
+
const raw = fs.readFileSync(configPath, 'utf8');
|
|
429
|
+
if (!/^tools:/m.test(raw)) return false;
|
|
430
|
+
// A replacer FUNCTION, not a string: `recorded` (unioned in by cmdInit) is free text read
|
|
431
|
+
// back from the project's own config.yaml, not validated against the TOOLS registry the way
|
|
432
|
+
// CLI-sourced tool names are — a string replacement would let `$&`/`$'`/`` $` `` in a
|
|
433
|
+
// hand-edited or corrupted config get interpreted as replacement patterns instead of literal
|
|
434
|
+
// text, silently duplicating or mangling the surrounding file.
|
|
435
|
+
fs.writeFileSync(configPath, raw.replace(/^tools:.*$/m, () => `tools: [${tools.join(', ')}]`));
|
|
436
|
+
return true;
|
|
437
|
+
}
|
|
438
|
+
|
|
395
439
|
export function cmdUpdate(projectRoot, args) {
|
|
396
440
|
const refusal = refuseSelfUpdate(projectRoot, PKG_ROOT);
|
|
397
441
|
if (refusal) throw new Error(refusal);
|
|
@@ -411,9 +455,7 @@ export function cmdUpdate(projectRoot, args) {
|
|
|
411
455
|
// Persist an explicit --tool override so declared and installed state never
|
|
412
456
|
// diverge (otherwise pruning tool-specific files leaves config.yaml stale).
|
|
413
457
|
if (args.toolProvided) {
|
|
414
|
-
|
|
415
|
-
const raw = fs.readFileSync(configPath, 'utf8');
|
|
416
|
-
fs.writeFileSync(configPath, raw.replace(/^tools:.*$/m, `tools: [${tools.join(', ')}]`));
|
|
458
|
+
persistToolsLine(path.join(sdlcRoot, 'config.yaml'), tools);
|
|
417
459
|
}
|
|
418
460
|
|
|
419
461
|
// Read before scaffolding: recordPayloadVersion overwrites version.json with our own.
|
package/bin/detect.mjs
CHANGED
|
@@ -15,6 +15,7 @@ export const ADAPTER_PATHS = Object.freeze({
|
|
|
15
15
|
cline: '.clinerules',
|
|
16
16
|
gemini: 'GEMINI.md',
|
|
17
17
|
'agents-md': 'AGENTS.md',
|
|
18
|
+
kimi: '.kimi-code/AGENTS.md',
|
|
18
19
|
});
|
|
19
20
|
|
|
20
21
|
export const TOOL_NAMES = Object.freeze({
|
|
@@ -25,6 +26,7 @@ export const TOOL_NAMES = Object.freeze({
|
|
|
25
26
|
cline: 'Cline',
|
|
26
27
|
gemini: 'Gemini CLI',
|
|
27
28
|
'agents-md': 'AGENTS.md',
|
|
29
|
+
kimi: 'Kimi Code',
|
|
28
30
|
});
|
|
29
31
|
|
|
30
32
|
// A tool counts as present when the project already carries its home directory
|
|
@@ -38,6 +40,7 @@ const MARKERS = Object.freeze({
|
|
|
38
40
|
cline: ['.clinerules'],
|
|
39
41
|
gemini: ['GEMINI.md', '.gemini'],
|
|
40
42
|
'agents-md': ['AGENTS.md'],
|
|
43
|
+
kimi: ['.kimi-code'],
|
|
41
44
|
});
|
|
42
45
|
|
|
43
46
|
export function detectTools(projectRoot) {
|
package/lib/manifest.mjs
CHANGED
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@warnyin/sdlc",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.14.1",
|
|
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": {
|