@lmzhen/dsh-evolution-commands 0.3.27 → 0.3.29
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/lib/index.js +46 -8
- package/lib/types/index.d.ts +11 -1
- package/package.json +4 -4
package/lib/index.js
CHANGED
|
@@ -2,7 +2,7 @@ import { createRequire } from "node:module";
|
|
|
2
2
|
import { createUserMessage } from "@deepseek-ai/dsh-llm";
|
|
3
3
|
import { SkillLibrary, appendEvolutionEvent, buildLearnPrompt, composePresetComposition, eventsFile, evolutionRoot, resolveSkillsRoot } from "@lmzhen/dsh-evolution-core";
|
|
4
4
|
import { buildEnrichment, buildMaintainFacts, runMaintain, snapshotFromLibrary } from "@lmzhen/dsh-evolution-maintenance";
|
|
5
|
-
import { copyFileSync, existsSync, mkdirSync, readFileSync, renameSync, rmSync, writeFileSync } from "node:fs";
|
|
5
|
+
import { copyFileSync, existsSync, mkdirSync, readFileSync, renameSync, rmSync, statSync, writeFileSync } from "node:fs";
|
|
6
6
|
import { dirname, join } from "node:path";
|
|
7
7
|
import { fileURLToPath } from "node:url";
|
|
8
8
|
//#region lib/types/index.js
|
|
@@ -342,6 +342,13 @@ const defaultFs = {
|
|
|
342
342
|
},
|
|
343
343
|
rmSync: (path, options) => {
|
|
344
344
|
rmSync(path, options);
|
|
345
|
+
},
|
|
346
|
+
mtime: (path) => {
|
|
347
|
+
try {
|
|
348
|
+
return statSync(path).mtimeMs;
|
|
349
|
+
} catch {
|
|
350
|
+
return null;
|
|
351
|
+
}
|
|
345
352
|
}
|
|
346
353
|
};
|
|
347
354
|
/**
|
|
@@ -358,6 +365,13 @@ const defaultFs = {
|
|
|
358
365
|
* single `.bak` created above still holds the previous content, so the target
|
|
359
366
|
* is restored from it (best-effort) before the original rename error is
|
|
360
367
|
* rethrown, so a failed commit never leaves the file missing.
|
|
368
|
+
*
|
|
369
|
+
* V4-17: the `.bak` is refreshed to the committed content after every
|
|
370
|
+
* successful commit, so a later failed commit always recovers to the most
|
|
371
|
+
* recent good generation (a once-at-first-install `.bak` could otherwise
|
|
372
|
+
* restore content several installs old). The recovery error names the `.bak`
|
|
373
|
+
* source (and mtime when available) so the caller knows the generation it
|
|
374
|
+
* restored from.
|
|
361
375
|
*/
|
|
362
376
|
function atomicWriteFiles(targetDir, writes, fs = defaultFs) {
|
|
363
377
|
const stage = (name) => join(targetDir, `${name}.tmp`);
|
|
@@ -379,15 +393,24 @@ function atomicWriteFiles(targetDir, writes, fs = defaultFs) {
|
|
|
379
393
|
try {
|
|
380
394
|
fs.renameSync(tmp, finalPath);
|
|
381
395
|
} catch (renameError) {
|
|
382
|
-
if (fs.existsSync(bakPath))
|
|
383
|
-
|
|
384
|
-
|
|
385
|
-
|
|
396
|
+
if (fs.existsSync(bakPath)) {
|
|
397
|
+
try {
|
|
398
|
+
fs.copyFileSync(bakPath, finalPath);
|
|
399
|
+
} catch {
|
|
400
|
+
throw new Error(`atomicWriteFiles: "${name}" was removed but recovery from ${bakPath} failed (${renameError instanceof Error ? renameError.message : String(renameError)}); verify the file manually`);
|
|
401
|
+
}
|
|
402
|
+
const bakMtime = fs.mtime?.(bakPath);
|
|
403
|
+
const when = bakMtime !== null && bakMtime !== void 0 && Number.isFinite(bakMtime) ? ` (bak mtime ${new Date(bakMtime).toISOString()})` : "";
|
|
404
|
+
throw new Error(`atomicWriteFiles: "${name}" was removed during commit and restored from the last good generation ${bakPath}${when}; original error: ${renameError instanceof Error ? renameError.message : String(renameError)}`);
|
|
386
405
|
}
|
|
387
406
|
throw renameError;
|
|
388
407
|
}
|
|
389
408
|
}
|
|
390
409
|
}
|
|
410
|
+
for (const { name } of writes) {
|
|
411
|
+
const finalPath = join(targetDir, name);
|
|
412
|
+
if (fs.existsSync(finalPath)) fs.copyFileSync(finalPath, join(targetDir, `${name}.bak`));
|
|
413
|
+
}
|
|
391
414
|
} catch (error) {
|
|
392
415
|
for (const { name } of writes) fs.rmSync(stage(name), {
|
|
393
416
|
force: true,
|
|
@@ -410,13 +433,28 @@ function atomicWriteFiles(targetDir, writes, fs = defaultFs) {
|
|
|
410
433
|
*/
|
|
411
434
|
function countMaintainRecommendations(text) {
|
|
412
435
|
if (!text) return 0;
|
|
413
|
-
return (text
|
|
436
|
+
return beforeNotesHeader(text).match(/^- \[/gm)?.length ?? 0;
|
|
437
|
+
}
|
|
438
|
+
/** Locate the maintain-plan `Notes:` section header as a standalone line.
|
|
439
|
+
* Returns everything before it (the plan section), or the whole text when the
|
|
440
|
+
* header is absent. formatPlan emits the header via `lines.push('Notes:')`, so
|
|
441
|
+
* it is always a line that is exactly `Notes:` (never an indented/embedded
|
|
442
|
+
* continuation), letting us ignore a `Notes:` that appears inside a field. */
|
|
443
|
+
function beforeNotesHeader(text) {
|
|
444
|
+
const header = /(^|\n)Notes:(?=\n)/.exec(text);
|
|
445
|
+
if (!header) return text;
|
|
446
|
+
return text.slice(0, header.index);
|
|
414
447
|
}
|
|
415
448
|
/** F-328: render staged args for `pending --detail`, truncated to 500 chars.
|
|
416
|
-
* args may be non-JSON (a tool produced garbage), so the render is fail-safe.
|
|
449
|
+
* args may be non-JSON (a tool produced garbage), so the render is fail-safe.
|
|
450
|
+
* V4-19: a truncation is marked with `…(truncated N chars)` so the operator is
|
|
451
|
+
* never silently shown a partial JSON payload that could even split an escape
|
|
452
|
+
* sequence — the marker makes the cut explicit rather than looking complete. */
|
|
417
453
|
function safeStagedArgs(args) {
|
|
418
454
|
try {
|
|
419
|
-
|
|
455
|
+
const json = JSON.stringify(args);
|
|
456
|
+
if (json.length > 500) return `${json.slice(0, 500)}…(truncated ${json.length - 500} chars)`;
|
|
457
|
+
return json;
|
|
420
458
|
} catch {
|
|
421
459
|
return "(unserializable)";
|
|
422
460
|
}
|
package/lib/types/index.d.ts
CHANGED
|
@@ -25,7 +25,9 @@ export interface Config {
|
|
|
25
25
|
}
|
|
26
26
|
export declare function apply(ctx: Context, rawConfig?: Config): void;
|
|
27
27
|
/** F-211: the fs surface atomicWriteFiles uses — injectable so the commit-phase
|
|
28
|
-
* rename-failure path is deterministically testable. Defaults to node:fs.
|
|
28
|
+
* rename-failure path is deterministically testable. Defaults to node:fs.
|
|
29
|
+
* `mtime` (optional) probes a path's last-write time so the failed-commit
|
|
30
|
+
* recovery message can name the generation it restored from. */
|
|
29
31
|
interface FsOps {
|
|
30
32
|
writeFileSync(path: string, data: string | Uint8Array): void;
|
|
31
33
|
existsSync(path: string): boolean;
|
|
@@ -35,6 +37,7 @@ interface FsOps {
|
|
|
35
37
|
force?: boolean;
|
|
36
38
|
recursive?: boolean;
|
|
37
39
|
}): void;
|
|
40
|
+
mtime?(path: string): number | null;
|
|
38
41
|
}
|
|
39
42
|
/**
|
|
40
43
|
* S6.3 (E-40): atomically replace a set of files within one directory.
|
|
@@ -50,6 +53,13 @@ interface FsOps {
|
|
|
50
53
|
* single `.bak` created above still holds the previous content, so the target
|
|
51
54
|
* is restored from it (best-effort) before the original rename error is
|
|
52
55
|
* rethrown, so a failed commit never leaves the file missing.
|
|
56
|
+
*
|
|
57
|
+
* V4-17: the `.bak` is refreshed to the committed content after every
|
|
58
|
+
* successful commit, so a later failed commit always recovers to the most
|
|
59
|
+
* recent good generation (a once-at-first-install `.bak` could otherwise
|
|
60
|
+
* restore content several installs old). The recovery error names the `.bak`
|
|
61
|
+
* source (and mtime when available) so the caller knows the generation it
|
|
62
|
+
* restored from.
|
|
53
63
|
*/
|
|
54
64
|
export declare function atomicWriteFiles(targetDir: string, writes: Array<{
|
|
55
65
|
name: string;
|
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@lmzhen/dsh-evolution-commands",
|
|
3
3
|
"description": "Human commands for the evolution family (community build)",
|
|
4
|
-
"version": "0.3.
|
|
4
|
+
"version": "0.3.29",
|
|
5
5
|
"publishConfig": {
|
|
6
6
|
"access": "public"
|
|
7
7
|
},
|
|
@@ -30,9 +30,9 @@
|
|
|
30
30
|
],
|
|
31
31
|
"license": "MIT",
|
|
32
32
|
"dependencies": {
|
|
33
|
-
"@lmzhen/dsh-evolution-approval": "^0.3.
|
|
34
|
-
"@lmzhen/dsh-evolution-core": "^0.3.
|
|
35
|
-
"@lmzhen/dsh-evolution-maintenance": "^0.3.
|
|
33
|
+
"@lmzhen/dsh-evolution-approval": "^0.3.29",
|
|
34
|
+
"@lmzhen/dsh-evolution-core": "^0.3.29",
|
|
35
|
+
"@lmzhen/dsh-evolution-maintenance": "^0.3.29"
|
|
36
36
|
},
|
|
37
37
|
"peerDependencies": {
|
|
38
38
|
"@deepseek-ai/cordis": "^4.0.1",
|