devflow-kit 3.0.1 → 3.2.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 +49 -0
- package/README.md +1 -1
- package/dist/agents/git.md +2 -2
- package/dist/cli/agents-view/index.js +1 -1
- package/dist/cli/agents-view/render.js +71 -17
- package/dist/cli/agents-view/state.js +42 -16
- package/dist/cli/agents-view/terminal.js +5 -5
- package/dist/cli/commands/agents.js +142 -51
- package/dist/cli/commands/ambient.js +1 -1
- package/dist/cli/commands/attribution-prompts.js +8 -8
- package/dist/cli/commands/capture.js +1 -1
- package/dist/cli/commands/compliance-prompts.js +8 -8
- package/dist/cli/commands/compliance.js +8 -7
- package/dist/cli/commands/flags.js +33 -31
- package/dist/cli/commands/hud.js +1 -1
- package/dist/cli/commands/init-seed.js +9 -9
- package/dist/cli/commands/init.js +162 -85
- package/dist/cli/commands/install-report.js +10 -10
- package/dist/cli/commands/learning.js +302 -136
- package/dist/cli/commands/memory.js +36 -15
- package/dist/cli/commands/proxy.js +23 -23
- package/dist/cli/commands/rules.js +6 -5
- package/dist/cli/commands/tracker-prompts.js +6 -6
- package/dist/cli/commands/tracker.js +9 -9
- package/dist/cli/commands/uninstall.js +183 -59
- package/dist/cli/flags-view/render.js +5 -5
- package/dist/cli/flags-view/state.js +9 -9
- package/dist/cli/flags-view/terminal.js +4 -4
- package/dist/cli/tui/cells.js +1 -1
- package/dist/cli/tui/terminal.js +6 -6
- package/dist/commands/code-review.md +0 -2
- package/dist/commands/debug.md +14 -11
- package/dist/commands/dynamic-build.md +51 -47
- package/dist/commands/dynamic-plan.md +27 -7
- package/dist/commands/dynamic-profile.md +17 -3
- package/dist/commands/dynamic-tickets.md +18 -4
- package/dist/commands/explore.md +9 -3
- package/dist/commands/implement.md +20 -16
- package/dist/commands/plan.md +13 -9
- package/dist/commands/release.md +23 -3
- package/dist/commands/research.md +9 -3
- package/dist/commands/resolve.md +9 -12
- package/dist/commands/self-review.md +0 -2
- package/dist/core/agent-frontmatter.js +28 -3
- package/dist/core/agent-models.js +204 -42
- package/dist/core/agent-state.js +28 -6
- package/dist/core/ansi.js +2 -2
- package/dist/core/assets.js +1 -1
- package/dist/core/cache.js +7 -8
- package/dist/core/codex-auth-inspect.js +4 -4
- package/dist/core/compliance-compose.js +3 -3
- package/dist/core/compliance.js +3 -4
- package/dist/core/evidence-policy.js +14 -13
- package/dist/core/external-models.js +1 -1
- package/dist/core/feature-config.js +71 -13
- package/dist/core/feature-switch.js +3 -3
- package/dist/core/flags.js +49 -25
- package/dist/core/fs-atomic.js +6 -7
- package/dist/core/learning-queue-cleanup.js +16 -81
- package/dist/core/learning-store.js +61 -0
- package/dist/core/linked-path.js +46 -0
- package/dist/core/manifest.js +5 -5
- package/dist/core/mds-variants.js +13 -13
- package/dist/core/model-discovery.js +8 -8
- package/dist/core/observations.js +17 -101
- package/dist/core/orphan-sweep.js +4 -4
- package/dist/core/plugins.js +13 -8
- package/dist/core/project-paths.js +9 -13
- package/dist/core/proxy-log.js +8 -8
- package/dist/core/proxy-state.js +3 -3
- package/dist/core/queue-drain.js +31 -0
- package/dist/core/reference-sweep.js +6 -6
- package/dist/core/teammate-mode-cleanup.js +1 -1
- package/dist/core/tracker.js +14 -14
- package/dist/hud/colors.js +2 -2
- package/dist/hud/components/learning-counts.js +54 -22
- package/dist/hud/components/version-badge.js +1 -1
- package/dist/skills/git/references/pr/resolve-review-threads.md +2 -2
- package/dist/skills/git/references/tracker/github/create-release.md +2 -2
- package/dist/skills/git/references/tracker/jira/create-release.md +2 -2
- package/dist/skills/git/references/tracker/linear/create-release.md +2 -2
- package/dist/targets/claude-code/compliance-install.js +17 -15
- package/dist/targets/claude-code/hooks.js +2 -2
- package/dist/targets/claude-code/installer.js +59 -32
- package/dist/targets/claude-code/legacy.js +1 -1
- package/dist/targets/claude-code/post-install.js +135 -45
- package/dist/targets/claude-code/tracker-install.js +2 -2
- package/package.json +1 -1
- package/src/assets/agents/code.md +15 -21
- package/src/assets/agents/design.md +4 -2
- package/src/assets/agents/diagnose.md +3 -1
- package/src/assets/agents/evaluate.md +4 -0
- package/src/assets/agents/git.mds +2 -2
- package/src/assets/agents/knowledge.md +5 -3
- package/src/assets/agents/learning.md +281 -196
- package/src/assets/agents/research.md +3 -1
- package/src/assets/agents/review.md +5 -3
- package/src/assets/agents/scrutinize.md +5 -1
- package/src/assets/agents/simplify.md +4 -0
- package/src/assets/agents/skim.md +4 -2
- package/src/assets/agents/synthesize.md +6 -0
- package/src/assets/agents/test.md +18 -10
- package/src/assets/agents/triage.md +11 -9
- package/src/assets/agents/validate.md +14 -10
- package/src/assets/commands/_partials/_decisions.mds +8 -3
- package/src/assets/commands/_partials/_docs_root.mds +3 -3
- package/src/assets/commands/_partials/_engine.mds +16 -32
- package/src/assets/commands/_partials/_knowledge.mds +0 -2
- package/src/assets/commands/_partials/_preamble.mds +6 -2
- package/src/assets/commands/_partials/_settings.mds +2 -2
- package/src/assets/commands/_partials/_tracker.mds +1 -1
- package/src/assets/commands/code-review.mds +0 -2
- package/src/assets/commands/debug.mds +13 -8
- package/src/assets/commands/dynamic-build.mds +18 -12
- package/src/assets/commands/dynamic-plan.mds +10 -4
- package/src/assets/commands/dynamic-profile.mds +1 -1
- package/src/assets/commands/dynamic-tickets.mds +2 -2
- package/src/assets/commands/explore.mds +9 -1
- package/src/assets/commands/implement.mds +19 -13
- package/src/assets/commands/plan.mds +12 -8
- package/src/assets/commands/release.md +23 -3
- package/src/assets/commands/research.mds +9 -3
- package/src/assets/commands/resolve.mds +9 -10
- package/src/assets/mds/git/_pr.mds +3 -3
- package/src/assets/mds/tracker/_common.mds +1 -1
- package/src/assets/mds/tracker/_github.mds +3 -3
- package/src/assets/mds/tracker/_jira.mds +3 -3
- package/src/assets/mds/tracker/_linear.mds +3 -3
- package/src/assets/mds/tracker/_mcp.mds +6 -5
- package/src/assets/scripts/hooks/assets/orchestrator-charter.md +4 -2
- package/src/assets/scripts/hooks/background-memory-update +97 -33
- package/src/assets/scripts/hooks/capture-prompt +4 -3
- package/src/assets/scripts/hooks/capture-question +4 -3
- package/src/assets/scripts/hooks/capture-turn +5 -20
- package/src/assets/scripts/hooks/ensure-devflow-init +14 -2
- package/src/assets/scripts/hooks/ensure-proxy +5 -6
- package/src/assets/scripts/hooks/ensure-root-gitignore +123 -11
- package/src/assets/scripts/hooks/git-marker +71 -0
- package/src/assets/scripts/hooks/is-hex-sha +1 -1
- package/src/assets/scripts/hooks/json-helper.cjs +345 -944
- package/src/assets/scripts/hooks/json-parse +25 -129
- package/src/assets/scripts/hooks/lib/decisions-format.cjs +205 -156
- package/src/assets/scripts/hooks/lib/learning-store.cjs +3207 -0
- package/src/assets/scripts/hooks/lib/mkdir-lock.cjs +7 -5
- package/src/assets/scripts/hooks/lib/project-paths.cjs +13 -19
- package/src/assets/scripts/hooks/lib/render-decisions.cjs +253 -226
- package/src/assets/scripts/hooks/memory-worker +10 -0
- package/src/assets/scripts/hooks/pre-compact-memory +66 -14
- package/src/assets/scripts/hooks/preamble +9 -1
- package/src/assets/scripts/hooks/queue-append +55 -23
- package/src/assets/scripts/hooks/resolve-project-root +3 -4
- package/src/assets/scripts/hooks/session-start-context +146 -45
- package/src/assets/scripts/hooks/session-start-memory +33 -11
- package/src/assets/scripts/lib/project-config.cjs +2 -2
- package/src/assets/scripts/pr-evidence.cjs +3 -3
- package/src/assets/scripts/redact-secrets.cjs +20 -20
- package/src/assets/scripts/release-trace.cjs +1 -1
- package/src/assets/scripts/resolve-evidence-policy.cjs +3 -3
- package/src/assets/scripts/resolve-settings.cjs +3 -3
- package/src/assets/scripts/verify-evidence.cjs +2 -2
- package/src/assets/skills/apply-decisions/SKILL.md +37 -17
- package/src/assets/skills/docs-framework/SKILL.md +2 -2
- package/src/assets/skills/feature-knowledge/SKILL.md +6 -5
- package/src/assets/skills/test-driven-development/SKILL.md +6 -4
- package/dist/core/observation-io.js +0 -50
- package/src/assets/scripts/hooks/decisions-usage-scan.cjs +0 -131
|
@@ -29,6 +29,7 @@ import { writeSettingsFileAtomic } from '../../core/fs-atomic.js';
|
|
|
29
29
|
import { stripFlags } from '../../core/flags.js';
|
|
30
30
|
import { stripDevflowTeammateModeFromJson } from '../../core/teammate-mode-cleanup.js';
|
|
31
31
|
import { getPackageRoot, isContainedIn } from '../../core/paths.js';
|
|
32
|
+
import { firstSymbolicLink } from '../../core/linked-path.js';
|
|
32
33
|
/**
|
|
33
34
|
* Where a retired repo-local install lives: `<gitRoot>/.claude` and `<gitRoot>/.devflow`.
|
|
34
35
|
*
|
|
@@ -68,6 +69,77 @@ async function scopeInstallPaths(scope, gitRoot) {
|
|
|
68
69
|
return getInstallationPaths();
|
|
69
70
|
return gitRoot === null ? null : legacyLocalInstallPaths(gitRoot);
|
|
70
71
|
}
|
|
72
|
+
/**
|
|
73
|
+
* The user scope's guard, which passes everything: `~/.claude` and `~/.devflow` are
|
|
74
|
+
* the user's own, so a symbolic link there is the user's choice.
|
|
75
|
+
*/
|
|
76
|
+
const changeAnything = async () => true;
|
|
77
|
+
/**
|
|
78
|
+
* `folder` and every path below it down to `target`, the target included, outermost
|
|
79
|
+
* first, for whichever of the two folders holds `target`; null for a target in
|
|
80
|
+
* neither. PURE.
|
|
81
|
+
*/
|
|
82
|
+
function pathsDownTo(folders, target) {
|
|
83
|
+
for (const folder of [folders.claudeDir, folders.devflowDir]) {
|
|
84
|
+
const rel = path.relative(folder, target);
|
|
85
|
+
if (rel === '..' || rel.startsWith(`..${path.sep}`) || path.isAbsolute(rel))
|
|
86
|
+
continue;
|
|
87
|
+
const chain = [folder];
|
|
88
|
+
for (const part of rel === '' ? [] : rel.split(path.sep)) {
|
|
89
|
+
chain.push(path.join(chain[chain.length - 1], part));
|
|
90
|
+
}
|
|
91
|
+
return chain;
|
|
92
|
+
}
|
|
93
|
+
return null;
|
|
94
|
+
}
|
|
95
|
+
/**
|
|
96
|
+
* The guard for a legacy local install's removals and settings rewrites.
|
|
97
|
+
*
|
|
98
|
+
* D-CLI-NO-SYMLINK (firstSymbolicLink): a repository can commit `.claude`, `.devflow`
|
|
99
|
+
* or any folder or file in them as a symbolic link, and enough of `.claude` for a
|
|
100
|
+
* plain `uninstall` to detect a legacy install there. `fs.rm` and the settings
|
|
101
|
+
* rewrite act inside whatever folder a link on the way names, so a target is acted
|
|
102
|
+
* on only where neither `<gitRoot>/.claude` or `<gitRoot>/.devflow` nor anything
|
|
103
|
+
* below it on the way to the target, the target included, is a link. Otherwise the
|
|
104
|
+
* target is skipped, the link is left as it was, and one warning per link, opened
|
|
105
|
+
* by `skipped`, names it. A target that cannot be checked, or lies in neither
|
|
106
|
+
* folder, is skipped and reported the same way. The rest of the install is still
|
|
107
|
+
* removed, so a real, unlinked legacy install comes out whole.
|
|
108
|
+
*/
|
|
109
|
+
function legacyLocalChangeGuard(folders, skipped) {
|
|
110
|
+
const reported = new Set();
|
|
111
|
+
const report = (subject, reason) => {
|
|
112
|
+
if (reported.has(subject))
|
|
113
|
+
return;
|
|
114
|
+
reported.add(subject);
|
|
115
|
+
p.log.warn(`${skipped}: ${reason}`);
|
|
116
|
+
};
|
|
117
|
+
return async (target) => {
|
|
118
|
+
const chain = pathsDownTo(folders, target);
|
|
119
|
+
if (chain === null) {
|
|
120
|
+
report(target, `${target} is outside the legacy install`);
|
|
121
|
+
return false;
|
|
122
|
+
}
|
|
123
|
+
let link;
|
|
124
|
+
try {
|
|
125
|
+
link = await firstSymbolicLink(chain);
|
|
126
|
+
}
|
|
127
|
+
catch (error) {
|
|
128
|
+
report(target, `${target} could not be checked for a symbolic link (${error instanceof Error ? error.message : String(error)})`);
|
|
129
|
+
return false;
|
|
130
|
+
}
|
|
131
|
+
if (link === null)
|
|
132
|
+
return true;
|
|
133
|
+
report(link, `${link} is a symbolic link, and devflow removes or changes nothing through one`);
|
|
134
|
+
return false;
|
|
135
|
+
};
|
|
136
|
+
}
|
|
137
|
+
/** The guard for one scope's full or selective phase: checked for a legacy local install, open for the user's. */
|
|
138
|
+
function scopeChangeGuard(scope, folders) {
|
|
139
|
+
return scope === 'local'
|
|
140
|
+
? legacyLocalChangeGuard(folders, 'Legacy local install not fully removed')
|
|
141
|
+
: changeAnything;
|
|
142
|
+
}
|
|
71
143
|
/**
|
|
72
144
|
* The plugins the manifest records as installed, as registry definitions.
|
|
73
145
|
*
|
|
@@ -178,7 +250,7 @@ export function formatDryRunPlan(assets) {
|
|
|
178
250
|
* - "prompt" — interactive (isTTY=true); caller should ask the user
|
|
179
251
|
*
|
|
180
252
|
* @D1 Non-interactive-preserve invariant: when isTTY is false the result is
|
|
181
|
-
* always "preserve", never "prompt" — avoids
|
|
253
|
+
* always "preserve", never "prompt" — avoids a half-applied-state hazard.
|
|
182
254
|
*/
|
|
183
255
|
export function resolveSecurityRemovalDecision(opts) {
|
|
184
256
|
if (!opts.anySecurityPresent || opts.keepDocs)
|
|
@@ -193,7 +265,7 @@ export function resolveSecurityRemovalDecision(opts) {
|
|
|
193
265
|
*
|
|
194
266
|
* A cancel (user presses Ctrl-C on this prompt) is treated as decline: the
|
|
195
267
|
* .devflow/ directory is preserved and the uninstall continues with the remaining
|
|
196
|
-
* steps rather than aborting via process.exit()
|
|
268
|
+
* steps rather than aborting via process.exit().
|
|
197
269
|
*
|
|
198
270
|
* PURE — no I/O, fully testable.
|
|
199
271
|
*
|
|
@@ -233,8 +305,8 @@ export function partitionProjectData(entries) {
|
|
|
233
305
|
* realpaths, so macOS's `/var` → `/private/var` and a symlinked HOME still match.
|
|
234
306
|
*
|
|
235
307
|
* A `.devflow` that is a symbolic link to anywhere else is skipped too, and never
|
|
236
|
-
* followed: its target is a directory devflow cannot prove it wrote
|
|
237
|
-
*
|
|
308
|
+
* followed: its target is a directory devflow cannot prove it wrote, so a
|
|
309
|
+
* confirmed cleanup must not empty it — nor unlink a link the user made.
|
|
238
310
|
*/
|
|
239
311
|
export async function resolveProjectDataPlan(opts) {
|
|
240
312
|
if (opts.gitRoot === null)
|
|
@@ -320,9 +392,9 @@ export function formatProjectDataPlan(plan) {
|
|
|
320
392
|
* @D5 Precondition guard: any anomalous devflowDir falls back to 'artifacts-only' rather
|
|
321
393
|
* than throwing — business logic must not throw (engineering rule). The explicit guard
|
|
322
394
|
* makes the invariant present in production code, not only in tests (reliability rule).
|
|
323
|
-
* @D6
|
|
395
|
+
* @D6 The caller must NOT process.exit() after a cancel/decline response;
|
|
324
396
|
* removeAllDevFlow has already run by the time the prompt fires, so removeDevFlowInstallArtifacts
|
|
325
|
-
* must execute on every non-confirm path to leave a clean end-state
|
|
397
|
+
* must execute on every non-confirm path to leave a clean end-state.
|
|
326
398
|
* @D7 keepDocs gate: when the caller passes --keep-docs, the entire ~/.devflow dir cleanup
|
|
327
399
|
* prompt is suppressed — artifacts-only regardless of isTTY or userContent. This prevents
|
|
328
400
|
* --keep-docs from triggering prompts about skill shadows or preference-profile.md.
|
|
@@ -517,7 +589,7 @@ export function installArtifactPaths(devflowDir) {
|
|
|
517
589
|
{ relPath: 'agent-models.json' },
|
|
518
590
|
// cost history — auto-generated session telemetry, not user-authored.
|
|
519
591
|
// The whole costs/ tree is removed so sessions/ and archive.jsonl cannot
|
|
520
|
-
// drift from their write sites in src/hud/cost-history.ts
|
|
592
|
+
// drift from their write sites in src/hud/cost-history.ts.
|
|
521
593
|
{ relPath: 'costs', isDir: true },
|
|
522
594
|
// proxy artifacts
|
|
523
595
|
{ relPath: 'proxy.json' },
|
|
@@ -545,7 +617,7 @@ export function installArtifactPaths(devflowDir) {
|
|
|
545
617
|
{ relPath: 'logs', isDir: true },
|
|
546
618
|
// Model-discovery + HUD component caches. hudCacheDir() is the authoritative
|
|
547
619
|
// accessor for the cache/ parent (modelCacheDir() resolves beneath it), so the
|
|
548
|
-
// removal site stays byte-locked to the write sites
|
|
620
|
+
// removal site stays byte-locked to the write sites.
|
|
549
621
|
{ relPath: path.relative(devflowDir, hudCacheDir(devflowDir)), isDir: true },
|
|
550
622
|
];
|
|
551
623
|
}
|
|
@@ -567,19 +639,24 @@ export function installArtifactPaths(devflowDir) {
|
|
|
567
639
|
* agent-models.json is an INSTALL ARTIFACT (stale per-agent overrides silently
|
|
568
640
|
* re-apply to renamed/deleted agents on reinstall — AC-P1-F4) and therefore
|
|
569
641
|
* belongs in this list, not in enumerateUserDevFlowContent.
|
|
642
|
+
*
|
|
643
|
+
* Each removal passes `mayChange` first, which skips one a symbolic link would
|
|
644
|
+
* redirect in a legacy local install (D-CLI-NO-SYMLINK, legacyLocalChangeGuard).
|
|
570
645
|
*/
|
|
571
|
-
export async function removeDevFlowInstallArtifacts(devflowDir, verbose) {
|
|
646
|
+
export async function removeDevFlowInstallArtifacts(devflowDir, verbose, mayChange = changeAnything) {
|
|
572
647
|
const manifestPath = path.join(devflowDir, 'manifest.json');
|
|
573
|
-
|
|
574
|
-
|
|
575
|
-
|
|
576
|
-
|
|
648
|
+
if (await mayChange(manifestPath)) {
|
|
649
|
+
try {
|
|
650
|
+
await fs.rm(manifestPath, { force: true });
|
|
651
|
+
if (verbose) {
|
|
652
|
+
p.log.success('Removed manifest.json');
|
|
653
|
+
}
|
|
654
|
+
}
|
|
655
|
+
catch (error) {
|
|
656
|
+
p.log.warn(`Could not remove manifest.json: ${error}`);
|
|
577
657
|
}
|
|
578
658
|
}
|
|
579
|
-
|
|
580
|
-
p.log.warn(`Could not remove manifest.json: ${error}`);
|
|
581
|
-
}
|
|
582
|
-
// Proxy install artifacts — remove non-fatally (per-item failure isolation, avoids PF-009)
|
|
659
|
+
// Proxy install artifacts — remove non-fatally (per-item failure isolation)
|
|
583
660
|
// Inform the user if a proxy relay process is still running (never kill — informational only).
|
|
584
661
|
try {
|
|
585
662
|
const pidContent = await fs.readFile(path.join(devflowDir, 'proxy.pid'), 'utf-8');
|
|
@@ -594,7 +671,7 @@ export async function removeDevFlowInstallArtifacts(devflowDir, verbose) {
|
|
|
594
671
|
}
|
|
595
672
|
}
|
|
596
673
|
catch { /* proxy.pid absent or unreadable — non-fatal */ }
|
|
597
|
-
// All install artifacts removed non-fatally
|
|
674
|
+
// All install artifacts removed non-fatally. Resolved against
|
|
598
675
|
// disk first, so a per-run staging basename is a real path by the time the
|
|
599
676
|
// containment guard below sees it.
|
|
600
677
|
for (const artifact of await resolveInstallArtifactPaths(devflowDir)) {
|
|
@@ -607,6 +684,8 @@ export async function removeDevFlowInstallArtifacts(devflowDir, verbose) {
|
|
|
607
684
|
if (!isContainedIn(devflowDir, artifact.relPath)) {
|
|
608
685
|
continue;
|
|
609
686
|
}
|
|
687
|
+
if (!(await mayChange(fullPath)))
|
|
688
|
+
continue;
|
|
610
689
|
try {
|
|
611
690
|
await fs.rm(fullPath, { force: true, recursive: artifact.isDir === true });
|
|
612
691
|
if (verbose)
|
|
@@ -654,7 +733,7 @@ export async function isDevFlowInstalled(claudeDir) {
|
|
|
654
733
|
* scope directories, intersected with what actually exists on disk.
|
|
655
734
|
*
|
|
656
735
|
* Extracted from the dry-run loop body so the enumeration logic is
|
|
657
|
-
* independently testable (
|
|
736
|
+
* independently testable (tests must exercise the production
|
|
658
737
|
* path, not only the pure helper functions).
|
|
659
738
|
*
|
|
660
739
|
* Coverage matches removeAllDevFlow exactly:
|
|
@@ -664,7 +743,7 @@ export async function isDevFlowInstalled(claudeDir) {
|
|
|
664
743
|
* - Prefixed (devflow:name): live registry ∪ LEGACY_SKILL_NAMES.
|
|
665
744
|
* - Bare (name or devflow-name legacy): LEGACY_SKILL_NAMES ONLY.
|
|
666
745
|
* ~/.claude/skills/ is shared; bare dirs for live-registry skill names
|
|
667
|
-
* are by construction foreign to Devflow
|
|
746
|
+
* are by construction foreign to Devflow.
|
|
668
747
|
* 3. manifest.json (removed separately in removeDevFlowInstallArtifacts).
|
|
669
748
|
* 4. Every artifact path from installArtifactPaths(devflowDir) — the single
|
|
670
749
|
* source of truth shared with the real removal loop.
|
|
@@ -688,7 +767,7 @@ export async function enumerateDryRunExtras(claudeDir, devflowDir) {
|
|
|
688
767
|
catch { /* absent */ }
|
|
689
768
|
}
|
|
690
769
|
// 2. Skills: enumerate ALL removal candidates that exist on disk.
|
|
691
|
-
// Mirrors removeAllDevFlow's split-pass approach
|
|
770
|
+
// Mirrors removeAllDevFlow's split-pass approach:
|
|
692
771
|
// Prefixed: live registry ∪ LEGACY_SKILL_NAMES ∪ FEATURE_OWNED_SKILLS
|
|
693
772
|
// Bare: LEGACY_SKILL_NAMES only — shared skills/ dir; live-registry bare
|
|
694
773
|
// dirs are by construction foreign to Devflow.
|
|
@@ -812,6 +891,8 @@ export async function runSelectivePhaseForScope(opts) {
|
|
|
812
891
|
const { claudeDir, devflowDir, selectedPlugins, verbose } = opts;
|
|
813
892
|
const scope = opts.scope ?? 'user';
|
|
814
893
|
const installedPlugins = opts.installedPlugins ?? DEVFLOW_PLUGINS;
|
|
894
|
+
// D-CLI-NO-SYMLINK: a legacy local install's removals and rewrites pass this first.
|
|
895
|
+
const mayChange = scopeChangeGuard(scope, { claudeDir, devflowDir });
|
|
815
896
|
// Revert GPT agent frontmatter BEFORE removing agent files — strips GPT model
|
|
816
897
|
// lines from installed agent frontmatter while the files are still present.
|
|
817
898
|
// Non-fatal: tolerate missing agents dir or revert errors.
|
|
@@ -819,23 +900,25 @@ export async function runSelectivePhaseForScope(opts) {
|
|
|
819
900
|
const agentsInstallDir = path.join(claudeDir, 'agents', 'devflow');
|
|
820
901
|
try {
|
|
821
902
|
await fs.access(agentsInstallDir);
|
|
822
|
-
await
|
|
823
|
-
|
|
824
|
-
|
|
825
|
-
|
|
826
|
-
|
|
827
|
-
|
|
903
|
+
if (await mayChange(agentsInstallDir)) {
|
|
904
|
+
await revertExternalAgents({
|
|
905
|
+
installDir: agentsInstallDir,
|
|
906
|
+
devflowDir,
|
|
907
|
+
onWarning: (msg) => { if (verbose)
|
|
908
|
+
p.log.warn(msg); },
|
|
909
|
+
});
|
|
910
|
+
}
|
|
828
911
|
}
|
|
829
912
|
catch { /* agents dir absent or revert failed — non-fatal */ }
|
|
830
913
|
}
|
|
831
|
-
await removeSelectedPlugins(claudeDir, selectedPlugins, verbose, installedPlugins);
|
|
914
|
+
await removeSelectedPlugins(claudeDir, selectedPlugins, verbose, installedPlugins, mayChange);
|
|
832
915
|
// Clean up ambient hook if ambient plugin is being removed
|
|
833
916
|
if (selectedPlugins.some(sp => sp.name === 'devflow-ambient')) {
|
|
834
917
|
const settingsPath = path.join(claudeDir, 'settings.json');
|
|
835
918
|
try {
|
|
836
919
|
const settings = await fs.readFile(settingsPath, 'utf-8');
|
|
837
920
|
const updated = await removeAmbientHook(settings, { purgeLegacyRule: scope === 'user' });
|
|
838
|
-
if (updated !== settings) {
|
|
921
|
+
if (updated !== settings && await mayChange(settingsPath)) {
|
|
839
922
|
await writeSettingsFileAtomic(settingsPath, updated);
|
|
840
923
|
if (verbose) {
|
|
841
924
|
p.log.success('Ambient mode hooks removed from settings.json');
|
|
@@ -866,6 +949,8 @@ export async function runSelectivePhaseForScope(opts) {
|
|
|
866
949
|
*/
|
|
867
950
|
export async function runFullPhaseForScope(opts) {
|
|
868
951
|
const { scope, claudeDir, devflowDir, devflowScriptsDir, verbose, keepDocs, isTTY } = opts;
|
|
952
|
+
// D-CLI-NO-SYMLINK: a legacy local install's removals and rewrites pass this first.
|
|
953
|
+
const mayChange = scopeChangeGuard(scope, { claudeDir, devflowDir });
|
|
869
954
|
// Revert GPT agent frontmatter before removing agents — ensures no orphaned
|
|
870
955
|
// GPT model lines remain if agents dir is preserved by a later partial flow.
|
|
871
956
|
// Non-fatal: tolerate missing agents dir or revert errors.
|
|
@@ -873,24 +958,26 @@ export async function runFullPhaseForScope(opts) {
|
|
|
873
958
|
const agentsInstallDir = path.join(claudeDir, 'agents', 'devflow');
|
|
874
959
|
try {
|
|
875
960
|
await fs.access(agentsInstallDir);
|
|
876
|
-
await
|
|
877
|
-
|
|
878
|
-
|
|
879
|
-
|
|
880
|
-
|
|
881
|
-
|
|
961
|
+
if (await mayChange(agentsInstallDir)) {
|
|
962
|
+
await revertExternalAgents({
|
|
963
|
+
installDir: agentsInstallDir,
|
|
964
|
+
devflowDir,
|
|
965
|
+
onWarning: (msg) => { if (verbose)
|
|
966
|
+
p.log.warn(msg); },
|
|
967
|
+
});
|
|
968
|
+
}
|
|
882
969
|
}
|
|
883
970
|
catch { /* agents dir absent or revert failed — non-fatal */ }
|
|
884
971
|
}
|
|
885
972
|
// removeAllDevFlow removes Claude Code assets (commands, agents, rules, skills)
|
|
886
973
|
// and devflowDir/scripts/. Scope-aware cleanup handles the rest of devflowDir.
|
|
887
|
-
await removeAllDevFlow(claudeDir, devflowScriptsDir, verbose);
|
|
974
|
+
await removeAllDevFlow(claudeDir, devflowScriptsDir, verbose, mayChange);
|
|
888
975
|
if (scope === 'local') {
|
|
889
976
|
// Local scope: devflowDir is gitRoot/.devflow/ which holds project data
|
|
890
977
|
// (memory, learning, features, docs, config.json). Never remove those —
|
|
891
978
|
// only remove install artifacts: scripts/ (done above) + manifest.json and
|
|
892
979
|
// the other Devflow-generated artifacts (see removeDevFlowInstallArtifacts).
|
|
893
|
-
await removeDevFlowInstallArtifacts(devflowDir, verbose);
|
|
980
|
+
await removeDevFlowInstallArtifacts(devflowDir, verbose, mayChange);
|
|
894
981
|
p.log.info('Local project data (memory, learning, features, docs) preserved');
|
|
895
982
|
}
|
|
896
983
|
else {
|
|
@@ -923,7 +1010,7 @@ export async function runFullPhaseForScope(opts) {
|
|
|
923
1010
|
});
|
|
924
1011
|
if (p.isCancel(confirmFullCleanup)) {
|
|
925
1012
|
// removeAllDevFlow already ran — clean up the manifest to leave a consistent
|
|
926
|
-
// state.
|
|
1013
|
+
// state. process.exit() here would skip removeDevFlowInstallArtifacts,
|
|
927
1014
|
// leaving a stale manifest.json that points to assets no longer on disk.
|
|
928
1015
|
await removeDevFlowInstallArtifacts(devflowDir, verbose);
|
|
929
1016
|
p.log.info(`${devflowDir} preserved (full removal cancelled; removing scripts and install artifacts only)`);
|
|
@@ -945,7 +1032,7 @@ export async function runFullPhaseForScope(opts) {
|
|
|
945
1032
|
*
|
|
946
1033
|
* D-UNINSTALL-CARVE-OUT: DEVFLOW_TRACKED_PATHS are never removed. `--keep-docs`,
|
|
947
1034
|
* a non-interactive run, a decline and a cancel all leave the directory untouched,
|
|
948
|
-
* and a cancel continues the uninstall rather than exiting
|
|
1035
|
+
* and a cancel continues the uninstall rather than exiting. The
|
|
949
1036
|
* directory itself is removed only when nothing tracked was in it.
|
|
950
1037
|
*/
|
|
951
1038
|
async function runProjectDataStep(plan, gates) {
|
|
@@ -1109,7 +1196,12 @@ export async function runCleanupPhase(opts) {
|
|
|
1109
1196
|
applyProxyTeardownToSettings(parsedSettings, opts.managedProxyPorts?.get(scope));
|
|
1110
1197
|
settingsContent = JSON.stringify(parsedSettings, null, 2) + '\n';
|
|
1111
1198
|
}
|
|
1112
|
-
|
|
1199
|
+
// D-CLI-NO-SYMLINK: a legacy local install's settings.json is rewritten only
|
|
1200
|
+
// where no symbolic link leads it out of `<gitRoot>/.claude`.
|
|
1201
|
+
const mayChange = scope === 'local'
|
|
1202
|
+
? legacyLocalChangeGuard(paths, 'settings.json not edited')
|
|
1203
|
+
: changeAnything;
|
|
1204
|
+
if (settingsContent !== originalContent && await mayChange(settingsPath)) {
|
|
1113
1205
|
await writeSettingsFileAtomic(settingsPath, settingsContent);
|
|
1114
1206
|
if (verbose) {
|
|
1115
1207
|
p.log.success(`Devflow hooks removed from settings.json (${scope})`);
|
|
@@ -1391,8 +1483,11 @@ export const uninstallCommand = new Command('uninstall')
|
|
|
1391
1483
|
});
|
|
1392
1484
|
/**
|
|
1393
1485
|
* Remove all Devflow assets (full uninstall).
|
|
1486
|
+
*
|
|
1487
|
+
* Each removal passes `mayChange` first, which skips one a symbolic link would
|
|
1488
|
+
* redirect in a legacy local install (D-CLI-NO-SYMLINK, legacyLocalChangeGuard).
|
|
1394
1489
|
*/
|
|
1395
|
-
export async function removeAllDevFlow(claudeDir, devflowScriptsDir, verbose) {
|
|
1490
|
+
export async function removeAllDevFlow(claudeDir, devflowScriptsDir, verbose, mayChange = changeAnything) {
|
|
1396
1491
|
const devflowDirectories = [
|
|
1397
1492
|
{ path: path.join(claudeDir, 'commands', 'devflow'), name: 'commands' },
|
|
1398
1493
|
{ path: path.join(claudeDir, 'agents', 'devflow'), name: 'agents' },
|
|
@@ -1400,6 +1495,8 @@ export async function removeAllDevFlow(claudeDir, devflowScriptsDir, verbose) {
|
|
|
1400
1495
|
{ path: devflowScriptsDir, name: 'scripts' }
|
|
1401
1496
|
];
|
|
1402
1497
|
for (const dir of devflowDirectories) {
|
|
1498
|
+
if (!(await mayChange(dir.path)))
|
|
1499
|
+
continue;
|
|
1403
1500
|
try {
|
|
1404
1501
|
await fs.rm(dir.path, { recursive: true, force: true });
|
|
1405
1502
|
if (verbose) {
|
|
@@ -1412,7 +1509,7 @@ export async function removeAllDevFlow(claudeDir, devflowScriptsDir, verbose) {
|
|
|
1412
1509
|
}
|
|
1413
1510
|
// Remove Devflow skill directories.
|
|
1414
1511
|
// ~/.claude/skills/ is shared with other tools; the two passes use different
|
|
1415
|
-
// name sets to avoid deleting foreign dirs
|
|
1512
|
+
// name sets to avoid deleting foreign dirs:
|
|
1416
1513
|
//
|
|
1417
1514
|
// Prefixed pass (devflow:name): live registry ∪ LEGACY_SKILL_NAMES ∪ FEATURE_OWNED_SKILLS.
|
|
1418
1515
|
// prefixSkillName() is idempotent for already-prefixed legacy entries.
|
|
@@ -1421,7 +1518,7 @@ export async function removeAllDevFlow(claudeDir, devflowScriptsDir, verbose) {
|
|
|
1421
1518
|
// Bare pass (name or devflow-name legacy): LEGACY_SKILL_NAMES ONLY.
|
|
1422
1519
|
// A bare dir whose name matches a live-registry skill is by construction
|
|
1423
1520
|
// foreign to Devflow (the devflow: namespace shipped in dcecda3, 2026-03-30).
|
|
1424
|
-
// A bare 'compliance/' dir is also foreign (not in LEGACY_SKILL_NAMES)
|
|
1521
|
+
// A bare 'compliance/' dir is also foreign (not in LEGACY_SKILL_NAMES).
|
|
1425
1522
|
const prefixedSkillNames = new Set([...getAllSkillNames(), ...LEGACY_SKILL_NAMES, ...FEATURE_OWNED_SKILLS]);
|
|
1426
1523
|
const skillsDir = path.join(claudeDir, 'skills');
|
|
1427
1524
|
let skillsRemoved = 0;
|
|
@@ -1430,6 +1527,8 @@ export async function removeAllDevFlow(claudeDir, devflowScriptsDir, verbose) {
|
|
|
1430
1527
|
const prefixedPath = path.join(skillsDir, prefixSkillName(skillName));
|
|
1431
1528
|
try {
|
|
1432
1529
|
await fs.stat(prefixedPath);
|
|
1530
|
+
if (!(await mayChange(prefixedPath)))
|
|
1531
|
+
continue;
|
|
1433
1532
|
await fs.rm(prefixedPath, { recursive: true, force: true });
|
|
1434
1533
|
skillsRemoved++;
|
|
1435
1534
|
}
|
|
@@ -1440,6 +1539,8 @@ export async function removeAllDevFlow(claudeDir, devflowScriptsDir, verbose) {
|
|
|
1440
1539
|
const barePath = path.join(skillsDir, skillName);
|
|
1441
1540
|
try {
|
|
1442
1541
|
await fs.stat(barePath);
|
|
1542
|
+
if (!(await mayChange(barePath)))
|
|
1543
|
+
continue;
|
|
1443
1544
|
await fs.rm(barePath, { recursive: true, force: true });
|
|
1444
1545
|
skillsRemoved++;
|
|
1445
1546
|
}
|
|
@@ -1449,8 +1550,12 @@ export async function removeAllDevFlow(claudeDir, devflowScriptsDir, verbose) {
|
|
|
1449
1550
|
p.log.success(`Removed ${skillsRemoved} Devflow skill directories`);
|
|
1450
1551
|
}
|
|
1451
1552
|
// Also remove old nested skills structure if it exists
|
|
1553
|
+
const nestedSkillsDir = path.join(claudeDir, 'skills', 'devflow');
|
|
1452
1554
|
try {
|
|
1453
|
-
await fs.
|
|
1555
|
+
await fs.stat(nestedSkillsDir);
|
|
1556
|
+
if (await mayChange(nestedSkillsDir)) {
|
|
1557
|
+
await fs.rm(nestedSkillsDir, { recursive: true, force: true });
|
|
1558
|
+
}
|
|
1454
1559
|
}
|
|
1455
1560
|
catch {
|
|
1456
1561
|
// Old structure doesn't exist
|
|
@@ -1459,7 +1564,7 @@ export async function removeAllDevFlow(claudeDir, devflowScriptsDir, verbose) {
|
|
|
1459
1564
|
// registry (retired, renamed, or deleted). The loop above walks the static
|
|
1460
1565
|
// registry + LEGACY list; this sweep walks the actual directory, so orphaned
|
|
1461
1566
|
// dirs from older versions are also cleaned up. (F9)
|
|
1462
|
-
await sweepDevflowNamespaces(claudeDir, verbose);
|
|
1567
|
+
await sweepDevflowNamespaces(claudeDir, verbose, mayChange);
|
|
1463
1568
|
}
|
|
1464
1569
|
/**
|
|
1465
1570
|
* Registry-diff sweep of all Devflow-owned namespaces in `claudeDir`:
|
|
@@ -1472,23 +1577,29 @@ export async function removeAllDevFlow(claudeDir, devflowScriptsDir, verbose) {
|
|
|
1472
1577
|
* cleaned up promptly rather than accumulating on disk.
|
|
1473
1578
|
*
|
|
1474
1579
|
* knownNames spans ALL plugins for each asset type so assets belonging to
|
|
1475
|
-
* plugins NOT being uninstalled are never swept
|
|
1580
|
+
* plugins NOT being uninstalled are never swept.
|
|
1476
1581
|
*
|
|
1477
1582
|
* Removals are announced only under `verbose`, but removal FAILURES always warn:
|
|
1478
1583
|
* a swept-but-not-actually-removed agent or command keeps loading in Claude Code,
|
|
1479
1584
|
* and the user has no other signal that it is still there.
|
|
1585
|
+
*
|
|
1586
|
+
* A folder `mayChange` refuses is not swept at all (D-CLI-NO-SYMLINK,
|
|
1587
|
+
* legacyLocalChangeGuard). An entry in a swept folder that is itself a link is
|
|
1588
|
+
* only unlinked: `fs.rm` never follows the link it is given.
|
|
1480
1589
|
*/
|
|
1481
|
-
export async function sweepDevflowNamespaces(claudeDir, verbose) {
|
|
1590
|
+
export async function sweepDevflowNamespaces(claudeDir, verbose, mayChange = changeAnything) {
|
|
1482
1591
|
const agentsDir = path.join(claudeDir, 'agents', 'devflow');
|
|
1483
1592
|
const commandsDir = path.join(claudeDir, 'commands', 'devflow');
|
|
1484
1593
|
const skillsDir = path.join(claudeDir, 'skills');
|
|
1485
|
-
const
|
|
1486
|
-
const
|
|
1594
|
+
const noSweep = { scanned: 0, removed: [], failed: [] };
|
|
1595
|
+
const sweep = async (...args) => (await mayChange(args[0])) ? sweepOrphanedAssets(...args) : noSweep;
|
|
1596
|
+
const agentsSweep = await sweep(agentsDir, new Set(getAllAgentNames()), mdEntryName);
|
|
1597
|
+
const commandsSweep = await sweep(commandsDir, new Set(getAllCommandNames()), mdEntryName);
|
|
1487
1598
|
// FEATURE_OWNED_SKILLS (compliance) unioned into knownNames so a selective
|
|
1488
1599
|
// uninstall of another plugin does not sweep devflow:compliance — nothing
|
|
1489
1600
|
// converges after selective uninstall (only installViaFileCopy + convergeComplianceArtifacts
|
|
1490
1601
|
// on full/partial init re-materialize it). D-FO-1 applies.
|
|
1491
|
-
const skillsSweep = await
|
|
1602
|
+
const skillsSweep = await sweep(skillsDir, new Set([...getAllSkillNames(), ...FEATURE_OWNED_SKILLS]), (entry) => entry.startsWith(SKILL_NAMESPACE) ? unprefixSkillName(entry) : null);
|
|
1492
1603
|
const byKind = [
|
|
1493
1604
|
{ kind: 'agent', sweep: agentsSweep },
|
|
1494
1605
|
{ kind: 'command', sweep: commandsSweep },
|
|
@@ -1511,14 +1622,18 @@ export async function sweepDevflowNamespaces(claudeDir, verbose) {
|
|
|
1511
1622
|
* sweep the install directory for any orphaned assets whose names left the
|
|
1512
1623
|
* registry (retired agents/commands survive indefinitely without the sweep).
|
|
1513
1624
|
* For skills: only remove skills that are NOT used by any remaining plugin.
|
|
1625
|
+
* Each removal passes `mayChange` first, which skips one a symbolic link would
|
|
1626
|
+
* redirect in a legacy local install (D-CLI-NO-SYMLINK, legacyLocalChangeGuard).
|
|
1514
1627
|
*/
|
|
1515
|
-
export async function removeSelectedPlugins(claudeDir, plugins, verbose, installedPlugins = DEVFLOW_PLUGINS) {
|
|
1628
|
+
export async function removeSelectedPlugins(claudeDir, plugins, verbose, installedPlugins = DEVFLOW_PLUGINS, mayChange = changeAnything) {
|
|
1516
1629
|
const { skills, agents, commands, rules } = computeAssetsToRemove(plugins, installedPlugins);
|
|
1517
1630
|
const commandsDir = path.join(claudeDir, 'commands', 'devflow');
|
|
1518
1631
|
for (const cmd of commands) {
|
|
1519
|
-
const
|
|
1632
|
+
const cmdPath = path.join(commandsDir, mdFileName(cmd.replace(/^\//, '')));
|
|
1633
|
+
if (!(await mayChange(cmdPath)))
|
|
1634
|
+
continue;
|
|
1520
1635
|
try {
|
|
1521
|
-
await fs.rm(
|
|
1636
|
+
await fs.rm(cmdPath, { force: true });
|
|
1522
1637
|
if (verbose) {
|
|
1523
1638
|
p.log.success(`Removed command ${cmd}`);
|
|
1524
1639
|
}
|
|
@@ -1529,8 +1644,11 @@ export async function removeSelectedPlugins(claudeDir, plugins, verbose, install
|
|
|
1529
1644
|
}
|
|
1530
1645
|
const agentsDir = path.join(claudeDir, 'agents', 'devflow');
|
|
1531
1646
|
for (const agent of agents) {
|
|
1647
|
+
const agentPath = path.join(agentsDir, mdFileName(agent));
|
|
1648
|
+
if (!(await mayChange(agentPath)))
|
|
1649
|
+
continue;
|
|
1532
1650
|
try {
|
|
1533
|
-
await fs.rm(
|
|
1651
|
+
await fs.rm(agentPath, { force: true });
|
|
1534
1652
|
if (verbose) {
|
|
1535
1653
|
p.log.success(`Removed agent ${agent}`);
|
|
1536
1654
|
}
|
|
@@ -1546,9 +1664,12 @@ export async function removeSelectedPlugins(claudeDir, plugins, verbose, install
|
|
|
1546
1664
|
// exclusively by the frozen LEGACY_SKILL_NAMES pass in removeAllDevFlow and
|
|
1547
1665
|
// in init.ts; live-registry skills never had bare installs (the devflow:
|
|
1548
1666
|
// namespace shipped in dcecda3, 2026-03-30), so a bare dir for a current
|
|
1549
|
-
// registry name is by construction foreign
|
|
1667
|
+
// registry name is by construction foreign.
|
|
1668
|
+
const skillPath = path.join(skillsDir, prefixSkillName(skill));
|
|
1669
|
+
if (!(await mayChange(skillPath)))
|
|
1670
|
+
continue;
|
|
1550
1671
|
try {
|
|
1551
|
-
await fs.rm(
|
|
1672
|
+
await fs.rm(skillPath, { recursive: true, force: true });
|
|
1552
1673
|
}
|
|
1553
1674
|
catch { /* Skill might not exist */ }
|
|
1554
1675
|
if (verbose) {
|
|
@@ -1557,8 +1678,11 @@ export async function removeSelectedPlugins(claudeDir, plugins, verbose, install
|
|
|
1557
1678
|
}
|
|
1558
1679
|
const rulesDir = path.join(claudeDir, 'rules', 'devflow');
|
|
1559
1680
|
for (const rule of rules) {
|
|
1681
|
+
const rulePath = path.join(rulesDir, mdFileName(rule));
|
|
1682
|
+
if (!(await mayChange(rulePath)))
|
|
1683
|
+
continue;
|
|
1560
1684
|
try {
|
|
1561
|
-
await fs.rm(
|
|
1685
|
+
await fs.rm(rulePath, { force: true });
|
|
1562
1686
|
if (verbose) {
|
|
1563
1687
|
p.log.success(`Removed rule ${rule}`);
|
|
1564
1688
|
}
|
|
@@ -1568,7 +1692,7 @@ export async function removeSelectedPlugins(claudeDir, plugins, verbose, install
|
|
|
1568
1692
|
// Registry-diff sweep: agents, commands, AND skills — named step (A6).
|
|
1569
1693
|
// Removes any orphaned files whose names left the registry (retired, renamed,
|
|
1570
1694
|
// or deleted from all plugins). Spans ALL plugins so assets belonging to
|
|
1571
|
-
// non-selected plugins are never swept
|
|
1572
|
-
await sweepDevflowNamespaces(claudeDir, verbose);
|
|
1695
|
+
// non-selected plugins are never swept.
|
|
1696
|
+
await sweepDevflowNamespaces(claudeDir, verbose, mayChange);
|
|
1573
1697
|
}
|
|
1574
1698
|
//# sourceMappingURL=uninstall.js.map
|
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* Pure TUI frame renderer for the devflow flags view.
|
|
3
3
|
*
|
|
4
|
-
*
|
|
5
|
-
*
|
|
4
|
+
* CLI-layer view module; zero fs/tty imports.
|
|
5
|
+
* Pure function, no process.exit(), no I/O.
|
|
6
6
|
*
|
|
7
7
|
* Layout (FIXED_ROWS = 10, viewport = state.viewportHeight — single owner):
|
|
8
8
|
* 1 Title " Devflow Flags"
|
|
@@ -59,14 +59,14 @@ export function computeViewportHeight(termRows) {
|
|
|
59
59
|
* non-boolean at devflow default → plain string
|
|
60
60
|
* non-boolean deviating from devflow default → bold string
|
|
61
61
|
*
|
|
62
|
-
* Colour vocabulary (one colour, one semantic
|
|
62
|
+
* Colour vocabulary (one colour, one semantic):
|
|
63
63
|
* cyan = focus indicator (chevron wrapper ‹ › on the cursor row only)
|
|
64
64
|
* yellow = dirty indicator (unconditional ●) and boolean 'off'
|
|
65
65
|
* green = boolean 'on'
|
|
66
66
|
* bold = non-boolean value deviating from devflow default
|
|
67
67
|
*
|
|
68
68
|
* disk-sourced values are routed through sanitizeCell to prevent TAB/LF
|
|
69
|
-
* layout breaks inside the fixed-width TUI cell
|
|
69
|
+
* layout breaks inside the fixed-width TUI cell.
|
|
70
70
|
*/
|
|
71
71
|
function formatValue(row) {
|
|
72
72
|
const v = row.configuredValue;
|
|
@@ -151,7 +151,7 @@ function renderRow(row, isCursor, isEditing, editBuffer, editCaret, labelW, valu
|
|
|
151
151
|
// so an inner RESET (e.g. from green('on')) does not kill the outer cyan.
|
|
152
152
|
// cyan('‹ ') + <styled-or-plain content> + cyan(' ›')
|
|
153
153
|
// rather than cyan(`‹ ${content} ›`), which terminates the outer cyan at the
|
|
154
|
-
// inner RESET, leaving the closing chevron unstyled
|
|
154
|
+
// inner RESET, leaving the closing chevron unstyled.
|
|
155
155
|
const chevronBudget = valueW - 4;
|
|
156
156
|
let valueCell;
|
|
157
157
|
if (isCursor && isEditing) {
|
|
@@ -1,20 +1,20 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* Pure keypress reducer for the devflow flags TUI.
|
|
3
3
|
*
|
|
4
|
-
*
|
|
5
|
-
*
|
|
6
|
-
*
|
|
7
|
-
*
|
|
4
|
+
* CLI-layer view module; consumes src/core/ imports only.
|
|
5
|
+
* One syntax, one semantic — value vocabulary.
|
|
6
|
+
* Pure functions only — no process.exit(), no I/O.
|
|
7
|
+
* Generic shell in tui/terminal.ts; this module is pure logic.
|
|
8
8
|
*
|
|
9
9
|
* viewMode GLUE RULE: view-mode's neutralValue ('default') maps to null in the TUI.
|
|
10
10
|
* `buildFlagRows` maps record value 'default' → null via `recordToTui` (core/flags.ts);
|
|
11
11
|
* `collectFlagRecord` maps null → 'default' via `tuiToRecord` (core/flags.ts).
|
|
12
12
|
* Number 0 is ACTIVE — null ≠ 0. Both functions live next to neutralValueOf, their
|
|
13
|
-
* definition dependency
|
|
13
|
+
* definition dependency, so there is one shared definition rather than per-consumer copies.
|
|
14
14
|
*
|
|
15
15
|
* Strict number parsing: leading/trailing whitespace and leading zeros are
|
|
16
16
|
* invalid ('007' → error, ' 8' → error). This rejects pathological inputs
|
|
17
|
-
* before they reach coerceFlagValue
|
|
17
|
+
* before they reach coerceFlagValue.
|
|
18
18
|
*
|
|
19
19
|
* Buffer hard cap: BUFFER_MAX_LEN = 64 chars (paste-flood guard).
|
|
20
20
|
*
|
|
@@ -198,8 +198,8 @@ function enterEdit(state) {
|
|
|
198
198
|
* Contract:
|
|
199
199
|
* - Empty buffer + allowUnset → commit null (unset)
|
|
200
200
|
* - Empty buffer + !allowUnset → error "Value is required"
|
|
201
|
-
* - For number/string flags: delegate to parseFlagValueInput (
|
|
202
|
-
*
|
|
201
|
+
* - For number/string flags: delegate to parseFlagValueInput (strict grammar
|
|
202
|
+
* enforced at the core sink, not per-caller). Error messages
|
|
203
203
|
* distinguish format failures (padded/hex/leading-zeros) from bounds failures.
|
|
204
204
|
* - parseFlagValueInput returns null on invalid input → stay editing + error
|
|
205
205
|
*/
|
|
@@ -230,7 +230,7 @@ function commitEdit(state) {
|
|
|
230
230
|
};
|
|
231
231
|
}
|
|
232
232
|
}
|
|
233
|
-
// Number flag: parseFlagValueInput enforces strict decimal grammar
|
|
233
|
+
// Number flag: parseFlagValueInput enforces strict decimal grammar.
|
|
234
234
|
// Provide specific error messages to distinguish format from bounds failures.
|
|
235
235
|
if (flagDef.kind === 'number') {
|
|
236
236
|
if (buf !== buf.trim()) {
|
|
@@ -1,10 +1,10 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* Thin adapter — devflow flags TUI shell over the generic runTui driver.
|
|
3
3
|
*
|
|
4
|
-
*
|
|
5
|
-
*
|
|
6
|
-
* a finally-guarded scope.
|
|
7
|
-
*
|
|
4
|
+
* Impure I/O shell in CLI layer; pure logic lives in state.ts/render.ts.
|
|
5
|
+
* Cleanup wired via Promise resolve — never process.exit() inside
|
|
6
|
+
* a finally-guarded scope (it would skip the finally).
|
|
7
|
+
* Thin adapter over the generic shell (src/cli/tui/terminal.ts).
|
|
8
8
|
*
|
|
9
9
|
* Public API:
|
|
10
10
|
* - runFlagsTui(initialRows, io?) → Promise<FlagsTuiResult>
|
package/dist/cli/tui/cells.js
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* Shared TUI cell helpers — shared by agents-view and flags-view.
|
|
3
3
|
*
|
|
4
|
-
*
|
|
4
|
+
* Generified into a shared module rather than copy-adapted per consumer.
|
|
5
5
|
* Pure functions, no I/O.
|
|
6
6
|
*/
|
|
7
7
|
import { stripAnsi, truncate } from '../../core/ansi.js';
|