devflow-kit 3.10.0 → 3.12.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/CHANGELOG.md +74 -0
- package/dist/agents/architect.md +87 -0
- package/dist/agents/code.md +21 -25
- package/dist/agents/debug.md +21 -49
- package/dist/agents/design.md +9 -43
- package/dist/agents/fix.md +15 -29
- package/dist/agents/git.md +19 -22
- package/dist/agents/harden.md +14 -19
- package/dist/agents/knowledge.md +11 -19
- package/dist/agents/refactor.md +9 -14
- package/dist/agents/research.md +11 -34
- package/dist/agents/review.md +22 -52
- package/dist/agents/skim.md +7 -17
- package/dist/agents/triage.md +7 -14
- package/dist/cli/commands/backup-session.js +131 -0
- package/dist/cli/commands/compliance.js +16 -3
- package/dist/cli/commands/curation.js +12 -4
- package/dist/cli/commands/init.js +36 -55
- package/dist/cli/commands/install-report.js +95 -0
- package/dist/cli/commands/legacy-sweep.js +140 -45
- package/dist/cli/commands/rules.js +50 -13
- package/dist/cli/commands/uninstall.js +62 -24
- package/dist/commands/bug-analysis.md +31 -62
- package/dist/commands/code-review.md +38 -87
- package/dist/commands/debug.md +32 -82
- package/dist/commands/dynamic-build.md +155 -211
- package/dist/commands/dynamic-plan.md +71 -93
- package/dist/commands/dynamic-profile.md +7 -155
- package/dist/commands/dynamic-tickets.md +65 -186
- package/dist/commands/explore.md +27 -70
- package/dist/commands/implement.md +98 -186
- package/dist/commands/plan.md +60 -107
- package/dist/commands/release.md +25 -91
- package/dist/commands/research.md +34 -103
- package/dist/commands/resolve.md +64 -128
- package/dist/commands/self-review.md +35 -99
- package/dist/compliance-off/agents/git.md +15 -18
- package/dist/compliance-off/agents/triage.md +7 -14
- package/dist/compliance-off/commands/bug-analysis.md +31 -62
- package/dist/compliance-off/commands/code-review.md +34 -82
- package/dist/compliance-off/commands/dynamic-build.md +149 -205
- package/dist/compliance-off/commands/dynamic-tickets.md +65 -186
- package/dist/compliance-off/commands/implement.md +94 -180
- package/dist/compliance-off/commands/plan.md +60 -107
- package/dist/compliance-off/commands/release.md +25 -90
- package/dist/compliance-off/commands/resolve.md +54 -128
- package/dist/compliance-off/skills/git/references/tracker/github/ensure-pr-ready.md +3 -3
- package/dist/compliance-off/skills/git/references/tracker/jira/ensure-pr-ready.md +3 -3
- package/dist/compliance-off/skills/git/references/tracker/linear/ensure-pr-ready.md +3 -3
- package/dist/core/compliance.js +14 -0
- package/dist/core/fs-atomic.js +5 -1
- package/dist/core/orphan-sweep.js +20 -2
- package/dist/core/path-set.js +15 -0
- package/dist/core/plugins.js +6 -5
- package/dist/core/reference-sweep.js +42 -25
- package/dist/core/retired-shipped-names.js +278 -0
- package/dist/curation-compliance-off/agents/triage.md +7 -13
- package/dist/curation-compliance-off/commands/bug-analysis.md +21 -41
- package/dist/curation-compliance-off/commands/code-review.md +25 -62
- package/dist/curation-compliance-off/commands/dynamic-build.md +142 -194
- package/dist/curation-compliance-off/commands/dynamic-tickets.md +54 -173
- package/dist/curation-compliance-off/commands/implement.md +86 -161
- package/dist/curation-compliance-off/commands/plan.md +51 -86
- package/dist/curation-compliance-off/commands/release.md +17 -78
- package/dist/curation-compliance-off/commands/resolve.md +45 -108
- package/dist/curation-off/agents/architect.md +81 -0
- package/dist/curation-off/agents/code.md +20 -23
- package/dist/curation-off/agents/debug.md +19 -47
- package/dist/curation-off/agents/design.md +8 -41
- package/dist/curation-off/agents/fix.md +14 -27
- package/dist/curation-off/agents/harden.md +13 -18
- package/dist/curation-off/agents/knowledge.md +9 -17
- package/dist/curation-off/agents/refactor.md +8 -12
- package/dist/curation-off/agents/research.md +9 -32
- package/dist/curation-off/agents/review.md +11 -40
- package/dist/curation-off/agents/skim.md +7 -17
- package/dist/curation-off/agents/triage.md +7 -13
- package/dist/curation-off/commands/bug-analysis.md +21 -41
- package/dist/curation-off/commands/code-review.md +29 -67
- package/dist/curation-off/commands/debug.md +24 -63
- package/dist/curation-off/commands/dynamic-build.md +148 -200
- package/dist/curation-off/commands/dynamic-plan.md +60 -80
- package/dist/curation-off/commands/dynamic-tickets.md +54 -173
- package/dist/curation-off/commands/explore.md +18 -50
- package/dist/curation-off/commands/implement.md +90 -167
- package/dist/curation-off/commands/plan.md +51 -86
- package/dist/curation-off/commands/release.md +17 -79
- package/dist/curation-off/commands/research.md +23 -81
- package/dist/curation-off/commands/resolve.md +55 -108
- package/dist/curation-off/commands/self-review.md +26 -78
- package/dist/skills/git/references/pr/update-pr-evidence.md +2 -1
- package/dist/skills/git/references/tracker/github/ensure-pr-ready.md +3 -3
- package/dist/skills/git/references/tracker/jira/ensure-pr-ready.md +3 -3
- package/dist/skills/git/references/tracker/linear/ensure-pr-ready.md +3 -3
- package/dist/targets/claude-code/compliance-install.js +38 -13
- package/dist/targets/claude-code/compliance-off-files.js +5 -1
- package/dist/targets/claude-code/curation-install.js +77 -32
- package/dist/targets/claude-code/folder-swap.js +142 -0
- package/dist/targets/claude-code/install-backup.js +780 -0
- package/dist/targets/claude-code/installer.js +383 -192
- package/dist/targets/claude-code/legacy-skill-hashes.js +170 -0
- package/dist/targets/claude-code/linked-folder.js +5 -11
- package/dist/targets/claude-code/reference-converge.js +15 -2
- package/dist/targets/claude-code/rules-folder.js +34 -0
- package/dist/targets/claude-code/shipped-set.js +160 -0
- package/dist/targets/claude-code/workflow-folder-install.js +32 -12
- package/dist/variant-axes.json +5 -1
- package/package.json +1 -1
- package/src/assets/agents/_partials/_code.mds +3 -5
- package/src/assets/agents/_partials/_design.mds +86 -0
- package/src/assets/agents/architect.mds +41 -0
- package/src/assets/agents/code.mds +18 -20
- package/src/assets/agents/curate.md +26 -35
- package/src/assets/agents/debug.mds +21 -49
- package/src/assets/agents/design.mds +11 -114
- package/src/assets/agents/evaluate.md +6 -16
- package/src/assets/agents/explore.md +1 -2
- package/src/assets/agents/fix.mds +12 -24
- package/src/assets/agents/git.mds +20 -23
- package/src/assets/agents/harden.mds +14 -19
- package/src/assets/agents/knowledge.mds +10 -38
- package/src/assets/agents/polish.md +13 -39
- package/src/assets/agents/refactor.mds +6 -9
- package/src/assets/agents/research.mds +11 -34
- package/src/assets/agents/review.mds +12 -84
- package/src/assets/agents/skim.mds +7 -17
- package/src/assets/agents/synthesize.md +3 -21
- package/src/assets/agents/test.md +31 -32
- package/src/assets/agents/tracker.md +116 -229
- package/src/assets/agents/triage.mds +7 -26
- package/src/assets/agents/validate.md +1 -3
- package/src/assets/commands/_partials/_compliance.mds +2 -2
- package/src/assets/commands/_partials/_decisions.mds +17 -24
- package/src/assets/commands/_partials/_docs_root.mds +31 -10
- package/src/assets/commands/_partials/_engine.mds +34 -56
- package/src/assets/commands/_partials/_evidence_policy.mds +1 -1
- package/src/assets/commands/_partials/_factory.mds +10 -104
- package/src/assets/commands/_partials/_knowledge.mds +47 -51
- package/src/assets/commands/_partials/_plan_contract.mds +2 -2
- package/src/assets/commands/_partials/_preamble.mds +21 -21
- package/src/assets/commands/_partials/_publication.mds +3 -3
- package/src/assets/commands/_partials/_roster.mds +12 -13
- package/src/assets/commands/_partials/_settings.mds +6 -3
- package/src/assets/commands/_partials/_ticket_template.mds +3 -7
- package/src/assets/commands/_partials/_tracker.mds +10 -5
- package/src/assets/commands/_partials/_wave.mds +23 -32
- package/src/assets/commands/bug-analysis.mds +3 -7
- package/src/assets/commands/code-review.mds +12 -33
- package/src/assets/commands/debug.mds +5 -47
- package/src/assets/commands/dynamic-build.mds +48 -70
- package/src/assets/commands/dynamic-plan.mds +23 -42
- package/src/assets/commands/dynamic-profile.mds +7 -57
- package/src/assets/commands/dynamic-tickets.mds +9 -29
- package/src/assets/commands/explore.mds +5 -44
- package/src/assets/commands/implement.mds +75 -117
- package/src/assets/commands/plan.mds +27 -57
- package/src/assets/commands/release.mds +4 -62
- package/src/assets/commands/research.mds +7 -53
- package/src/assets/commands/resolve.mds +40 -87
- package/src/assets/commands/self-review.mds +5 -45
- package/src/assets/mds/git/_ops_pr.mds +1 -0
- package/src/assets/mds/git/_ops_steps.mds +4 -4
- package/src/assets/mds/git/_pr.mds +1 -1
- package/src/assets/rules/engineering.md +1 -1
- package/src/assets/rules/quality.md +2 -2
- package/src/assets/scripts/ci-wait.cjs +150 -18
- package/src/assets/scripts/hooks/assets/orchestrator-charter.md +3 -3
- package/src/assets/scripts/hooks/preamble +2 -3
- package/src/assets/scripts/pr-evidence.cjs +1 -0
- package/src/assets/scripts/resolve-evidence-policy.cjs +14 -13
- package/src/assets/scripts/resolve-root.cjs +186 -0
- package/src/assets/scripts/resolve-settings.cjs +14 -9
- package/src/assets/scripts/verify-evidence.cjs +355 -22
- package/src/assets/skills/accessibility/SKILL.md +28 -95
- package/src/assets/skills/agent-references/SKILL.md +2 -4
- package/src/assets/skills/agent-references/references/commands/implement-evidence-asks.md +1 -1
- package/src/assets/skills/agent-references/references/commands/implement-levels.md +4 -7
- package/src/assets/skills/agent-references/references/commands/implement-revalidate.md +1 -1
- package/src/assets/skills/agent-references/references/commands/plan-levels.md +2 -2
- package/src/assets/skills/apply-decisions/SKILL.md +5 -6
- package/src/assets/skills/apply-feature-knowledge/SKILL.md +3 -13
- package/src/assets/skills/architecture/SKILL.md +29 -105
- package/src/assets/skills/boundary-validation/SKILL.md +27 -78
- package/src/assets/skills/complexity/SKILL.md +8 -104
- package/src/assets/skills/compliance/SKILL.md +9 -67
- package/src/assets/skills/consistency/SKILL.md +7 -107
- package/src/assets/skills/database/SKILL.md +7 -72
- package/src/assets/skills/dependencies/SKILL.md +9 -109
- package/src/assets/skills/dependencies/references/detection.md +10 -0
- package/src/assets/skills/dependency-research/SKILL.md +9 -63
- package/src/assets/skills/design-review/SKILL.md +1 -35
- package/src/assets/skills/docs-framework/SKILL.md +4 -47
- package/src/assets/skills/documentation/SKILL.md +7 -71
- package/src/assets/skills/documentation/references/patterns.md +1 -1
- package/src/assets/skills/documentation/references/violations.md +1 -1
- package/src/assets/skills/feature-knowledge/SKILL.md +19 -222
- package/src/assets/skills/feature-knowledge/references/new-kb.md +128 -0
- package/src/assets/skills/gap-analysis/SKILL.md +1 -31
- package/src/assets/skills/git/SKILL.md +2 -22
- package/src/assets/skills/go/SKILL.md +24 -113
- package/src/assets/skills/java/SKILL.md +12 -98
- package/src/assets/skills/patterns/SKILL.md +8 -93
- package/src/assets/skills/performance/SKILL.md +29 -88
- package/src/assets/skills/python/SKILL.md +22 -120
- package/src/assets/skills/qa/SKILL.md +16 -54
- package/src/assets/skills/qa/references/browser-testing.md +0 -24
- package/src/assets/skills/qa/references/sources.md +2 -2
- package/src/assets/skills/quality-gates/SKILL.md +13 -100
- package/src/assets/skills/react/SKILL.md +33 -95
- package/src/assets/skills/regression/SKILL.md +8 -73
- package/src/assets/skills/reliability/SKILL.md +11 -120
- package/src/assets/skills/research-codebase/SKILL.md +7 -49
- package/src/assets/skills/research-competitor/SKILL.md +4 -39
- package/src/assets/skills/research-external/SKILL.md +5 -43
- package/src/assets/skills/research-market/SKILL.md +5 -43
- package/src/assets/skills/research-technology/SKILL.md +8 -68
- package/src/assets/skills/review-methodology/SKILL.md +10 -56
- package/src/assets/skills/rust/SKILL.md +19 -109
- package/src/assets/skills/security/SKILL.md +39 -66
- package/src/assets/skills/software-design/SKILL.md +20 -105
- package/src/assets/skills/software-design/references/patterns.md +84 -2
- package/src/assets/skills/software-design/references/violations.md +1 -1
- package/src/assets/skills/test-driven-development/SKILL.md +32 -107
- package/src/assets/skills/test-driven-development/references/rationalization-prevention.md +33 -1
- package/src/assets/skills/testing/SKILL.md +35 -53
- package/src/assets/skills/typescript/SKILL.md +23 -102
- package/src/assets/skills/ui-design/SKILL.md +26 -74
- package/src/assets/skills/worktree-support/SKILL.md +4 -10
- package/dist/curation-off/commands/dynamic-profile.md +0 -214
|
@@ -91,15 +91,16 @@ import { isProxyEnabled } from '../../core/proxy-state.js';
|
|
|
91
91
|
import { WORKFLOW_FOLDERS } from '../../core/mds-variants.js';
|
|
92
92
|
import { DEVFLOW_PLUGINS, CURATION_GATED_SKILLS, installedLanguageFocuses, prefixSkillName, selectedWorkflowFolders, skillsOf, workflowFolderNames, } from '../../core/plugins.js';
|
|
93
93
|
import { convergeFromManifest } from './compliance-install.js';
|
|
94
|
+
import { swapFolderIn } from './folder-swap.js';
|
|
94
95
|
import { copyDirectory, resolveSkillSource } from './installer.js';
|
|
95
96
|
import { LANGUAGE_STAMPED_COMMANDS, stampForConverge } from './language-stamp.js';
|
|
96
|
-
import {
|
|
97
|
+
import { directoryState, newLinkReport, refuseUnrealFolder, refusedFolderMessage, unlinkLinkAt, warnLinkReport, writeFileReplacingLink } from './linked-folder.js';
|
|
97
98
|
import { convergeReferences } from './reference-converge.js';
|
|
98
99
|
import { installWorkflowFolder, readWorkflowFolder } from './workflow-folder-install.js';
|
|
99
100
|
// ── Bounds ─────────────────────────────────────────────────────────────────
|
|
100
101
|
/**
|
|
101
|
-
* The most variant files one kind may carry. The shipped roster is
|
|
102
|
-
*
|
|
102
|
+
* The most variant files one kind may carry. The shipped roster is 13 commands and
|
|
103
|
+
* 11 agents; the bound exists so a directory that grew without limit cannot turn the
|
|
103
104
|
* converge into an unbounded loop (every loop has an explicit bound).
|
|
104
105
|
*/
|
|
105
106
|
export const MAX_CURATION_ROSTER = 256;
|
|
@@ -380,6 +381,16 @@ async function pathExists(p) {
|
|
|
380
381
|
return false;
|
|
381
382
|
}
|
|
382
383
|
}
|
|
384
|
+
/**
|
|
385
|
+
* The two folders a skill swap uses, both directly under `claudeDir`: the staging copy, and the
|
|
386
|
+
* backup the old skill folder waits in while the copy is renamed into place.
|
|
387
|
+
*
|
|
388
|
+
* @param now - The clock reading that makes the name unique, in milliseconds.
|
|
389
|
+
*/
|
|
390
|
+
export function curationSwapPaths(claudeDir, now) {
|
|
391
|
+
const staging = path.join(claudeDir, `.devflow-curation-${process.pid}-${now.toString(36)}.tmp`);
|
|
392
|
+
return { staging, backup: `${staging}.old` };
|
|
393
|
+
}
|
|
383
394
|
/**
|
|
384
395
|
* Install one skill directory atomically: stage a copy beside the claude dir's
|
|
385
396
|
* skills, then swap it in, restoring the old directory if the swap fails.
|
|
@@ -387,33 +398,56 @@ async function pathExists(p) {
|
|
|
387
398
|
* Staged under `claudeDir` itself, not under `skills/`: a staging directory a
|
|
388
399
|
* crash leaves behind must not look like a second skill to Claude Code.
|
|
389
400
|
*
|
|
401
|
+
* A file in the old directory that devflow did not ship moves to the backup folder first. If one
|
|
402
|
+
* cannot move, nothing is swapped: the shipped files are written into the old directory and the
|
|
403
|
+
* file stays (see {@link swapFolderIn}).
|
|
404
|
+
*
|
|
390
405
|
* A target that is a link is moved aside as the link itself, never followed, so what it points to is
|
|
391
406
|
* left as it was; the link is recorded in `links` once the real folder stands in its place.
|
|
392
407
|
*/
|
|
393
|
-
async function swapInSkillDirectory(claudeDir, sourceDir, target, links) {
|
|
394
|
-
const staging =
|
|
395
|
-
const backup = `${staging}.old`;
|
|
408
|
+
async function swapInSkillDirectory(claudeDir, sourceDir, target, links, backups) {
|
|
409
|
+
const { staging, backup } = curationSwapPaths(claudeDir, Date.now());
|
|
396
410
|
try {
|
|
397
411
|
await copyDirectory(sourceDir, staging);
|
|
398
412
|
await fs.mkdir(path.dirname(target), { recursive: true });
|
|
399
|
-
|
|
400
|
-
|
|
401
|
-
|
|
402
|
-
|
|
403
|
-
|
|
404
|
-
|
|
405
|
-
|
|
406
|
-
|
|
407
|
-
if (hadTarget)
|
|
408
|
-
await fs.rename(backup, target);
|
|
409
|
-
throw err;
|
|
410
|
-
}
|
|
411
|
-
if (wasLink)
|
|
412
|
-
links.replacedLinks.push(target);
|
|
413
|
+
await swapFolderIn({
|
|
414
|
+
backups,
|
|
415
|
+
target,
|
|
416
|
+
staged: staging,
|
|
417
|
+
aside: backup,
|
|
418
|
+
links,
|
|
419
|
+
mergeInto: () => copyDirectory(staging, target, undefined, links),
|
|
420
|
+
});
|
|
413
421
|
}
|
|
414
422
|
finally {
|
|
415
423
|
await fs.rm(staging, { recursive: true, force: true }).catch(() => undefined);
|
|
416
|
-
|
|
424
|
+
}
|
|
425
|
+
}
|
|
426
|
+
/**
|
|
427
|
+
* Remove a curation-gated skill folder. The files devflow shipped go by the session's list, anything
|
|
428
|
+
* else moves to the backup folder first, and the folder goes with a plain `rmdir`: 'kept' means it
|
|
429
|
+
* still stands because a file could not move. A link is unlinked and a file in its place moves aside.
|
|
430
|
+
*/
|
|
431
|
+
async function removeSkillFolder(target, backups) {
|
|
432
|
+
const state = await directoryState(target);
|
|
433
|
+
switch (state) {
|
|
434
|
+
case 'absent':
|
|
435
|
+
return 'absent';
|
|
436
|
+
case 'link':
|
|
437
|
+
return (await unlinkLinkAt(target)) === 'unlinked' ? 'removed' : 'kept';
|
|
438
|
+
case 'unusable': {
|
|
439
|
+
const moved = await backups.moveWhole(target);
|
|
440
|
+
return moved.status === 'moved' ? 'removed' : moved.status === 'absent' ? 'absent' : 'kept';
|
|
441
|
+
}
|
|
442
|
+
case 'real': {
|
|
443
|
+
await backups.prepass([target]);
|
|
444
|
+
return (await backups.removeListed(target)).folderRemoved ? 'removed' : 'kept';
|
|
445
|
+
}
|
|
446
|
+
default: {
|
|
447
|
+
const unreachable = state;
|
|
448
|
+
void unreachable;
|
|
449
|
+
return 'kept';
|
|
450
|
+
}
|
|
417
451
|
}
|
|
418
452
|
}
|
|
419
453
|
/** Converge the curation-gated skill directories. A skill folder that was a link and was swapped is recorded in `links`. Never throws. */
|
|
@@ -421,15 +455,24 @@ async function convergeSkills(opts, links) {
|
|
|
421
455
|
const { claudeDir, devflowDir, curation, plugins, warn } = opts;
|
|
422
456
|
const selected = skillsOf(plugins);
|
|
423
457
|
let state = 'unchanged';
|
|
458
|
+
// A skills folder that is a link or not a folder is refused once. The removal and the swap below
|
|
459
|
+
// would reach the folder the link points to: `directoryState` of a skill folder follows the link
|
|
460
|
+
// above it. The refusal is recorded in `links`, and the converge is not clean.
|
|
461
|
+
if (await refuseUnrealFolder(links, path.join(claudeDir, 'skills')))
|
|
462
|
+
return 'failed';
|
|
424
463
|
// Every gated skill is converged: a failure on one must not skip the next.
|
|
425
464
|
for (const skill of CURATION_GATED_SKILLS) {
|
|
426
465
|
const target = path.join(claudeDir, 'skills', prefixSkillName(skill));
|
|
427
466
|
try {
|
|
428
467
|
if (!curation) {
|
|
429
|
-
|
|
430
|
-
|
|
431
|
-
|
|
432
|
-
|
|
468
|
+
const removal = await removeSkillFolder(target, opts.backups);
|
|
469
|
+
if (removal === 'removed') {
|
|
470
|
+
state = 'removed';
|
|
471
|
+
}
|
|
472
|
+
else if (removal === 'kept') {
|
|
473
|
+
warn(`curation variants: the ${prefixSkillName(skill)} skill folder (${target}) still holds a file that could not be moved aside, so it was not removed`);
|
|
474
|
+
state = 'failed';
|
|
475
|
+
}
|
|
433
476
|
continue;
|
|
434
477
|
}
|
|
435
478
|
if (!selected.has(skill)) {
|
|
@@ -440,7 +483,7 @@ async function convergeSkills(opts, links) {
|
|
|
440
483
|
const resolved = await resolveSkillSource(skill, devflowDir, opts.packageRoot);
|
|
441
484
|
if ((await pathExists(target)) && (await treesIdentical(resolved.dir, target)))
|
|
442
485
|
continue;
|
|
443
|
-
await swapInSkillDirectory(claudeDir, resolved.dir, target, links);
|
|
486
|
+
await swapInSkillDirectory(claudeDir, resolved.dir, target, links, opts.backups);
|
|
444
487
|
state = 'installed';
|
|
445
488
|
}
|
|
446
489
|
catch (err) {
|
|
@@ -590,7 +633,7 @@ async function attempt(label, warn, fallback, run) {
|
|
|
590
633
|
* install owns the removal. Never throws.
|
|
591
634
|
*/
|
|
592
635
|
async function convergeWorkflowFolders(opts) {
|
|
593
|
-
const { claudeDir, switches, plugins, folders, root, warn } = opts;
|
|
636
|
+
const { claudeDir, switches, plugins, folders, root, warn, backups } = opts;
|
|
594
637
|
const installed = [];
|
|
595
638
|
const unchanged = [];
|
|
596
639
|
const failures = [];
|
|
@@ -611,7 +654,7 @@ async function convergeWorkflowFolders(opts) {
|
|
|
611
654
|
continue;
|
|
612
655
|
}
|
|
613
656
|
const links = newLinkReport();
|
|
614
|
-
const result = await installWorkflowFolder({ claudeDir, name: folder.name, files: read.files, links });
|
|
657
|
+
const result = await installWorkflowFolder({ claudeDir, name: folder.name, files: read.files, links, backups });
|
|
615
658
|
switch (result.outcome) {
|
|
616
659
|
case 'installed':
|
|
617
660
|
installed.push(folder.name);
|
|
@@ -668,7 +711,7 @@ export function workflowFoldersPosting(folders, compliance) {
|
|
|
668
711
|
* Never throws.
|
|
669
712
|
*/
|
|
670
713
|
export async function convergeVariantAxes(opts) {
|
|
671
|
-
const { claudeDir, devflowDir, manifest, plugins, warn, packageRoot } = opts;
|
|
714
|
+
const { claudeDir, devflowDir, manifest, plugins, warn, packageRoot, backups } = opts;
|
|
672
715
|
const switches = { curation: manifest.features.curation, compliance: manifest.features.compliance.enabled };
|
|
673
716
|
// Precondition, asserted in production code: a relative claudeDir would resolve
|
|
674
717
|
// every install target somewhere unexpected, the skill removal included.
|
|
@@ -692,9 +735,9 @@ export async function convergeVariantAxes(opts) {
|
|
|
692
735
|
}
|
|
693
736
|
const prompts = await attempt('prompt variants', warn, notRun(switches, 'the prompt converge failed'), () => convergePromptVariants({ claudeDir, switches, plugins, warn, packageRoot }));
|
|
694
737
|
const skillLinks = newLinkReport();
|
|
695
|
-
const skill = await attempt('curation variants', warn, 'failed', () => convergeSkills({ claudeDir, devflowDir, curation: switches.curation, plugins, warn, packageRoot }, skillLinks));
|
|
738
|
+
const skill = await attempt('curation variants', warn, 'failed', () => convergeSkills({ claudeDir, devflowDir, curation: switches.curation, plugins, warn, packageRoot, backups }, skillLinks));
|
|
696
739
|
warnLinkReport(skillLinks, warn);
|
|
697
|
-
const compliance = await attempt('compliance artifacts', warn, { removedPreexisting: false, converged: false, replacedLinks: [], refusedFolders: [] }, () => convergeFromManifest({ claudeDir, devflowDir, manifest, warn }));
|
|
740
|
+
const compliance = await attempt('compliance artifacts', warn, { removedPreexisting: false, converged: false, replacedLinks: [], refusedFolders: [] }, () => convergeFromManifest({ claudeDir, devflowDir, manifest, warn, backups }));
|
|
698
741
|
const references = await attempt('reference converge', warn, {
|
|
699
742
|
converged: false,
|
|
700
743
|
rewritten: [],
|
|
@@ -703,7 +746,7 @@ export async function convergeVariantAxes(opts) {
|
|
|
703
746
|
failures: ['the reference converge failed'],
|
|
704
747
|
stillPosting: [],
|
|
705
748
|
unproven: !switches.compliance,
|
|
706
|
-
}, () => convergeReferences({ claudeDir, devflowDir, compliance: switches.compliance, warn, packageRoot }));
|
|
749
|
+
}, () => convergeReferences({ claudeDir, devflowDir, compliance: switches.compliance, warn, packageRoot, backups }));
|
|
707
750
|
const workflowFolders = await attempt('workflow folders', warn, { converged: false, installed: [], unchanged: [], failures: ['the workflow folder converge failed'], stalled: [] }, () => convergeWorkflowFolders({
|
|
708
751
|
claudeDir,
|
|
709
752
|
switches,
|
|
@@ -711,6 +754,7 @@ export async function convergeVariantAxes(opts) {
|
|
|
711
754
|
folders: opts.workflowFolders ?? WORKFLOW_FOLDERS,
|
|
712
755
|
root: opts.workflowFoldersRoot ?? packageRoot ?? getPackageRoot(),
|
|
713
756
|
warn,
|
|
757
|
+
backups,
|
|
714
758
|
}));
|
|
715
759
|
return {
|
|
716
760
|
converged: prompts.converged && skill !== 'failed' && compliance.converged && references.converged && workflowFolders.converged,
|
|
@@ -975,6 +1019,7 @@ export async function applyVariantToggle(opts) {
|
|
|
975
1019
|
plugins: DEVFLOW_PLUGINS.filter(plugin => selection.has(plugin.name)),
|
|
976
1020
|
warn,
|
|
977
1021
|
packageRoot: opts.packageRoot,
|
|
1022
|
+
backups: opts.backups,
|
|
978
1023
|
});
|
|
979
1024
|
if (result.converged) {
|
|
980
1025
|
await recordInstalledVariants(devflowDir, { curation: state.features.curation, compliance: state.features.compliance.enabled }, warn);
|
|
@@ -0,0 +1,142 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The swap of a folder that devflow owns and that may hold files of the user's: a skill folder the
|
|
3
|
+
* converge replaces. The new folder is built beside the old one and renamed in; the old folder waits
|
|
4
|
+
* in an aside folder while that happens.
|
|
5
|
+
*
|
|
6
|
+
* Nothing here removes a folder with a recursive `rm`. Before the swap, the backup session moves
|
|
7
|
+
* every file devflow did not ship out of the old folder. If any file could not move, the folder is
|
|
8
|
+
* not swapped: the files devflow ships are written into it one by one, and it stays in place. The
|
|
9
|
+
* write comes before any removal, so a copy that fails leaves the old shipped files in the folder;
|
|
10
|
+
* then only the listed entries that the new folder lacks are removed. The same holds when the aside
|
|
11
|
+
* name is taken by a folder that keeps a file. A file at the aside name
|
|
12
|
+
* moves to the backup folder first; if it cannot move, a rename never replaces it, so a folder
|
|
13
|
+
* stays in place and a link makes the swap throw. After
|
|
14
|
+
* a swap, the aside folder is scanned again, because a file can arrive between the first scan and the
|
|
15
|
+
* rename. What the session moves out of it is named, what devflow shipped is removed by the list the
|
|
16
|
+
* scan made, and the folder goes with a plain `rmdir`; a folder that still holds a file stays.
|
|
17
|
+
*
|
|
18
|
+
* This module imports no installer module, so the install and the converge steps can all use it.
|
|
19
|
+
* A swap that cannot finish throws, and the caller reports it.
|
|
20
|
+
*/
|
|
21
|
+
import { promises as fs } from 'fs';
|
|
22
|
+
import * as path from 'path';
|
|
23
|
+
import { DEFAULT_ENTRY_CAP, MAX_WALK_DEPTH } from './install-backup.js';
|
|
24
|
+
import { directoryState, unlinkLinkAt } from './linked-folder.js';
|
|
25
|
+
const errorMessage = (err) => (err instanceof Error ? err.message : String(err));
|
|
26
|
+
/**
|
|
27
|
+
* Remove a folder that was displaced from `stoodAt`: scan it as if it still stood there, move what
|
|
28
|
+
* devflow did not ship, remove what it shipped by the session's list, then `rmdir`. A displaced link
|
|
29
|
+
* is unlinked. A folder that still holds a file stays, and the session's record names the file.
|
|
30
|
+
*/
|
|
31
|
+
export async function discardDisplacedFolder(backups, aside, stoodAt) {
|
|
32
|
+
const state = await directoryState(aside);
|
|
33
|
+
if (state === 'link') {
|
|
34
|
+
await unlinkLinkAt(aside);
|
|
35
|
+
return;
|
|
36
|
+
}
|
|
37
|
+
if (state !== 'real')
|
|
38
|
+
return;
|
|
39
|
+
await backups.prepass([{ folder: aside, displacedFrom: stoodAt }]);
|
|
40
|
+
await backups.removeListed(aside);
|
|
41
|
+
}
|
|
42
|
+
/**
|
|
43
|
+
* The files and links below `staged` as paths relative to it, with `/` separators. Null when the
|
|
44
|
+
* walk reached its depth or entry bound, because a list that stops early would name too few.
|
|
45
|
+
*/
|
|
46
|
+
async function stagedPaths(staged) {
|
|
47
|
+
const found = new Set();
|
|
48
|
+
async function walk(folder, rel, depth) {
|
|
49
|
+
if (depth > MAX_WALK_DEPTH)
|
|
50
|
+
return false;
|
|
51
|
+
const entries = await fs.readdir(folder, { withFileTypes: true });
|
|
52
|
+
for (const entry of entries) {
|
|
53
|
+
const childRel = rel === '' ? entry.name : `${rel}/${entry.name}`;
|
|
54
|
+
if (entry.isDirectory()) {
|
|
55
|
+
if (!(await walk(path.join(folder, entry.name), childRel, depth + 1)))
|
|
56
|
+
return false;
|
|
57
|
+
}
|
|
58
|
+
else {
|
|
59
|
+
found.add(childRel);
|
|
60
|
+
if (found.size > DEFAULT_ENTRY_CAP)
|
|
61
|
+
return false;
|
|
62
|
+
}
|
|
63
|
+
}
|
|
64
|
+
return true;
|
|
65
|
+
}
|
|
66
|
+
return (await walk(staged, '', 0)) ? found : null;
|
|
67
|
+
}
|
|
68
|
+
/**
|
|
69
|
+
* Write the files of `staged` into `target`, then remove what the old folder held and the new one
|
|
70
|
+
* lacks. The write comes first: if the copy fails, the files devflow shipped are still in the old
|
|
71
|
+
* folder. The removal scans `target` again with a filter that leaves every staged path, every kept
|
|
72
|
+
* file and everything below a kept folder alone, so `removeListed` takes only the listed leftovers
|
|
73
|
+
* (retired and sidecar names, `.DS_Store`) and the shipped files that the new release dropped.
|
|
74
|
+
* When the staged list is not complete, nothing is removed.
|
|
75
|
+
*/
|
|
76
|
+
async function mergeThenClear(backups, target, staged, mergeInto) {
|
|
77
|
+
const written = await stagedPaths(staged);
|
|
78
|
+
await mergeInto();
|
|
79
|
+
if (written === null)
|
|
80
|
+
return;
|
|
81
|
+
await backups.prepass([{
|
|
82
|
+
folder: target,
|
|
83
|
+
include: (relative, kind) => !(kind !== 'directory' && written.has(relative)) && !backups.isKept(path.join(target, ...relative.split('/'))),
|
|
84
|
+
}]);
|
|
85
|
+
await backups.removeListed(target);
|
|
86
|
+
}
|
|
87
|
+
/**
|
|
88
|
+
* Put `staged` at `target`. See the module comment for the order of the steps.
|
|
89
|
+
*
|
|
90
|
+
* If the rename of `staged` fails, the old folder is renamed back. If that fails too, the old folder
|
|
91
|
+
* stays at `aside` and the error names the path. Throws on a failure that leaves the swap undone.
|
|
92
|
+
*/
|
|
93
|
+
export async function swapFolderIn(options) {
|
|
94
|
+
const { backups, target, staged, aside, links, mergeInto } = options;
|
|
95
|
+
// What an earlier, interrupted swap left at the aside name is cleared first, so the rename below has a free name.
|
|
96
|
+
// A file there is not a displaced folder: it moves to the backup folder, because a rename replaces it without a trace.
|
|
97
|
+
await discardDisplacedFolder(backups, aside, target);
|
|
98
|
+
if ((await directoryState(aside)) === 'unusable')
|
|
99
|
+
await backups.moveWhole(aside);
|
|
100
|
+
const asideTaken = (await directoryState(aside)) !== 'absent';
|
|
101
|
+
let state = await directoryState(target);
|
|
102
|
+
if (state === 'unusable') {
|
|
103
|
+
const moved = await backups.moveWhole(target);
|
|
104
|
+
if (moved.status === 'kept') {
|
|
105
|
+
throw new Error(`${target} is not a folder and could not be moved aside (${moved.reason})`);
|
|
106
|
+
}
|
|
107
|
+
state = 'absent';
|
|
108
|
+
}
|
|
109
|
+
if (state === 'real') {
|
|
110
|
+
await backups.prepass([target]);
|
|
111
|
+
if (asideTaken || backups.keptUnder(target).length > 0) {
|
|
112
|
+
await mergeThenClear(backups, target, staged, mergeInto);
|
|
113
|
+
return 'merged';
|
|
114
|
+
}
|
|
115
|
+
}
|
|
116
|
+
if (state === 'link' && asideTaken) {
|
|
117
|
+
throw new Error(`${target} is a link and was not replaced: ${aside} is in the way and could not be moved`);
|
|
118
|
+
}
|
|
119
|
+
const displaced = state !== 'absent';
|
|
120
|
+
if (displaced)
|
|
121
|
+
await fs.rename(target, aside);
|
|
122
|
+
try {
|
|
123
|
+
await fs.rename(staged, target);
|
|
124
|
+
}
|
|
125
|
+
catch (err) {
|
|
126
|
+
if (displaced) {
|
|
127
|
+
try {
|
|
128
|
+
await fs.rename(aside, target);
|
|
129
|
+
}
|
|
130
|
+
catch (restoreErr) {
|
|
131
|
+
throw new Error(`${errorMessage(err)}; the previous folder could not be put back (${errorMessage(restoreErr)}) and is kept at ${aside}`);
|
|
132
|
+
}
|
|
133
|
+
}
|
|
134
|
+
throw err;
|
|
135
|
+
}
|
|
136
|
+
if (state === 'link')
|
|
137
|
+
links.replacedLinks.push(target);
|
|
138
|
+
if (displaced)
|
|
139
|
+
await discardDisplacedFolder(backups, aside, target);
|
|
140
|
+
return 'swapped';
|
|
141
|
+
}
|
|
142
|
+
//# sourceMappingURL=folder-swap.js.map
|