devflow-kit 2.5.0 → 3.0.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 +73 -0
- package/README.md +44 -19
- package/dist/agents/git.md +13 -15
- package/dist/cli/commands/ambient.js +160 -145
- package/dist/cli/commands/capture.js +29 -55
- package/dist/cli/commands/compliance.js +32 -61
- package/dist/cli/commands/context.js +17 -32
- package/dist/cli/commands/debug.js +65 -26
- package/dist/cli/commands/flags.js +3 -3
- package/dist/cli/commands/hud.js +34 -10
- package/dist/cli/commands/init-seed.js +40 -4
- package/dist/cli/commands/init.js +249 -271
- package/dist/cli/commands/install-report.js +10 -15
- package/dist/cli/commands/knowledge/index.js +1 -1
- package/dist/cli/commands/knowledge/toggle.js +11 -3
- package/dist/cli/commands/learning.js +52 -37
- package/dist/cli/commands/legacy-hooks.js +11 -14
- package/dist/cli/commands/memory.js +67 -78
- package/dist/cli/commands/proxy.js +23 -41
- package/dist/cli/commands/security.js +5 -13
- package/dist/cli/commands/skills.js +21 -3
- package/dist/cli/commands/tracker.js +100 -228
- package/dist/cli/commands/uninstall.js +343 -138
- package/dist/commands/bug-analysis.md +38 -12
- package/dist/commands/code-review.md +70 -21
- package/dist/commands/debug.md +37 -7
- package/dist/commands/dynamic-build.md +66 -17
- package/dist/commands/dynamic-plan.md +19 -8
- package/dist/commands/dynamic-profile.md +24 -10
- package/dist/commands/dynamic-tickets.md +22 -11
- package/dist/commands/explore.md +37 -7
- package/dist/commands/implement.md +96 -32
- package/dist/commands/plan.md +62 -19
- package/dist/commands/release.md +2 -2
- package/dist/commands/research.md +34 -8
- package/dist/commands/resolve.md +65 -17
- package/dist/commands/self-review.md +45 -9
- package/dist/core/compliance-compose.js +27 -27
- package/dist/core/evidence-policy.js +240 -24
- package/dist/core/feature-config.js +94 -25
- package/dist/core/feature-switch.js +1 -1
- package/dist/core/flags.js +30 -2
- package/dist/core/fs-atomic.js +27 -0
- package/dist/core/hook-log-dirs.js +104 -0
- package/dist/core/learning-tuning-config.js +5 -3
- package/dist/core/ledger-root.js +102 -0
- package/dist/core/manifest.js +6 -4
- package/dist/core/mds-variants.js +34 -97
- package/dist/core/migrations.js +49 -23
- package/dist/core/plugins.js +5 -4
- package/dist/core/project-paths.js +0 -17
- package/dist/core/same-location.js +25 -0
- package/dist/core/tracker.js +226 -139
- package/dist/hud/components/config-counts.js +15 -4
- package/dist/hud/components/learning-counts.js +14 -0
- package/dist/hud/config.js +2 -1
- package/dist/hud/cost-history.js +2 -4
- package/dist/hud/git.js +52 -7
- package/dist/hud/index.js +7 -9
- package/dist/skills/git/references/pr/check-merge-readiness.md +1 -1
- package/dist/skills/git/references/pr/ensure-pr-ready.md +1 -1
- package/dist/skills/git/references/pr/update-pr-evidence.md +1 -1
- package/dist/skills/git/references/tracker/_mcp.md +1 -1
- package/dist/skills/git/references/tracker/github/backlink-shipped-issues.md +1 -1
- package/dist/skills/git/references/tracker/github/ensure-traceable-issue.md +1 -1
- package/dist/skills/git/references/tracker/github/gather-release-evidence.md +2 -2
- package/dist/skills/git/references/tracker/github/manage-debt.md +3 -3
- package/dist/skills/git/references/tracker/jira/backlink-shipped-issues.md +1 -1
- package/dist/skills/git/references/tracker/jira/ensure-traceable-issue.md +1 -1
- package/dist/skills/git/references/tracker/jira/gather-release-evidence.md +2 -2
- package/dist/skills/git/references/tracker/jira/manage-debt.md +1 -1
- package/dist/skills/git/references/tracker/jira/post-wave-report.md +1 -1
- package/dist/skills/git/references/tracker/jira/setup-task.md +1 -1
- package/dist/skills/git/references/tracker/linear/backlink-shipped-issues.md +1 -1
- package/dist/skills/git/references/tracker/linear/ensure-traceable-issue.md +1 -1
- package/dist/skills/git/references/tracker/linear/gather-release-evidence.md +2 -2
- package/dist/skills/git/references/tracker/linear/manage-debt.md +1 -1
- package/dist/skills/git/references/tracker/linear/post-wave-report.md +1 -1
- package/dist/skills/git/references/tracker/linear/setup-task.md +1 -1
- package/dist/targets/claude-code/claude-paths.js +59 -57
- package/dist/targets/claude-code/compliance-install.js +49 -65
- package/dist/targets/claude-code/hooks.js +108 -3
- package/dist/targets/claude-code/installer.js +30 -57
- package/dist/targets/claude-code/post-install.js +232 -139
- package/dist/targets/claude-code/tracker-install.js +38 -65
- package/package.json +5 -4
- package/src/assets/agents/code.md +4 -3
- package/src/assets/agents/design.md +1 -0
- package/src/assets/agents/git.mds +55 -57
- package/src/assets/agents/knowledge.md +2 -2
- package/src/assets/agents/review.md +3 -1
- package/src/assets/agents/tracker.md +37 -30
- package/src/assets/commands/_partials/_compliance.mds +19 -1
- package/src/assets/commands/_partials/_decisions.mds +15 -3
- package/src/assets/commands/_partials/_docs_root.mds +35 -0
- package/src/assets/commands/_partials/_engine.mds +2 -2
- package/src/assets/commands/_partials/_evidence_policy.mds +3 -3
- package/src/assets/commands/_partials/_factory.mds +1 -1
- package/src/assets/commands/_partials/_knowledge.mds +27 -9
- package/src/assets/commands/_partials/_plan_contract.mds +2 -2
- package/src/assets/commands/_partials/_preamble.mds +1 -1
- package/src/assets/commands/_partials/_publication.mds +6 -2
- package/src/assets/commands/_partials/_settings.mds +28 -0
- package/src/assets/commands/_partials/_ticket_template.mds +3 -3
- package/src/assets/commands/_partials/_tracker.mds +4 -4
- package/src/assets/commands/_partials/_wave.mds +4 -4
- package/src/assets/commands/bug-analysis.mds +19 -17
- package/src/assets/commands/code-review.mds +39 -33
- package/src/assets/commands/debug.mds +4 -5
- package/src/assets/commands/dynamic-build.mds +75 -53
- package/src/assets/commands/dynamic-plan.mds +20 -15
- package/src/assets/commands/dynamic-profile.mds +24 -11
- package/src/assets/commands/dynamic-tickets.mds +25 -20
- package/src/assets/commands/explore.mds +4 -5
- package/src/assets/commands/implement.mds +58 -45
- package/src/assets/commands/plan.mds +34 -29
- package/src/assets/commands/release.md +2 -2
- package/src/assets/commands/research.mds +11 -9
- package/src/assets/commands/resolve.mds +41 -39
- package/src/assets/commands/self-review.mds +24 -25
- package/src/assets/mds/git/_pr.mds +61 -61
- package/src/assets/mds/git/_references.mds +19 -19
- package/src/assets/mds/tracker/_common.mds +8 -8
- package/src/assets/mds/tracker/_github.mds +71 -71
- package/src/assets/mds/tracker/_jira.mds +74 -74
- package/src/assets/mds/tracker/_linear.mds +75 -75
- package/src/assets/mds/tracker/_mcp.mds +23 -17
- package/src/assets/scripts/hooks/background-memory-update +35 -19
- package/src/assets/scripts/hooks/capture-prompt +18 -12
- package/src/assets/scripts/hooks/capture-question +18 -12
- package/src/assets/scripts/hooks/capture-turn +27 -17
- package/src/assets/scripts/hooks/debug-trace +11 -6
- package/src/assets/scripts/hooks/ensure-devflow-init +33 -6
- package/src/assets/scripts/hooks/ensure-proxy +9 -8
- package/src/assets/scripts/hooks/ensure-root-gitignore +111 -36
- package/src/assets/scripts/hooks/git-marker +48 -0
- package/src/assets/scripts/hooks/json-helper.cjs +6 -1
- package/src/assets/scripts/hooks/lib/project-paths.cjs +0 -19
- package/src/assets/scripts/hooks/log-paths +80 -0
- package/src/assets/scripts/hooks/memory-worker +17 -15
- package/src/assets/scripts/hooks/pre-compact-memory +41 -16
- package/src/assets/scripts/hooks/queue-append +104 -30
- package/src/assets/scripts/hooks/resolve-project-root +101 -7
- package/src/assets/scripts/hooks/session-start-context +289 -122
- package/src/assets/scripts/hooks/session-start-memory +35 -16
- package/src/assets/scripts/lib/project-config.cjs +633 -0
- package/src/assets/scripts/resolve-evidence-policy.cjs +300 -220
- package/src/assets/scripts/resolve-settings.cjs +1054 -0
- package/src/assets/scripts/verify-evidence.cjs +1 -1
- package/src/assets/skills/compliance/SKILL.md +2 -2
- package/src/assets/skills/docs-framework/SKILL.md +6 -7
- package/src/assets/skills/docs-framework/references/patterns.md +10 -17
- package/src/assets/skills/gap-analysis/SKILL.md +2 -2
- package/src/assets/skills/git/references/github-api.md +9 -9
- package/src/assets/skills/git/references/patterns.md +1 -1
- package/src/assets/skills/worktree-support/SKILL.md +1 -1
- package/src/assets/skills/worktree-support/references/roots.md +29 -0
- package/src/targets/claude-code/templates/managed-settings.json +25 -9
|
@@ -4,11 +4,11 @@ import * as os from 'os';
|
|
|
4
4
|
import * as path from 'path';
|
|
5
5
|
import * as p from '@clack/prompts';
|
|
6
6
|
import color from 'picocolors';
|
|
7
|
-
import { getInstallationPaths, getClaudeDirectory, getManagedSettingsPath } from '../../targets/claude-code/claude-paths.js';
|
|
7
|
+
import { getInstallationPaths, getClaudeDirectory, getHomeDirectory, getManagedSettingsPath } from '../../targets/claude-code/claude-paths.js';
|
|
8
8
|
import { getGitRoot } from '../../core/git.js';
|
|
9
|
+
import { isSameLocation } from '../../core/same-location.js';
|
|
9
10
|
import { DEVFLOW_PLUGINS, SKILL_NAMESPACE, getAllSkillNames, getAllAgentNames, getAllCommandNames, parsePluginSelection, resolveFeatureRedirect, prefixSkillName, unprefixSkillName, skillsOf, FEATURE_OWNED_SKILLS } from '../../core/plugins.js';
|
|
10
11
|
import { readManifest } from '../../core/manifest.js';
|
|
11
|
-
import { TRACKER_ATTEMPTS_FILE, TRACKER_CLAIM_FILE, TRACKER_ENABLED_FILE } from '../../core/tracker.js';
|
|
12
12
|
import { sweepOrphanedAssets, mdFileName, mdEntryName } from '../../core/orphan-sweep.js';
|
|
13
13
|
import { LEGACY_SKILL_NAMES } from '../../targets/claude-code/legacy.js';
|
|
14
14
|
import { removeAmbientHook } from './ambient.js';
|
|
@@ -20,15 +20,54 @@ import { removeContextHook } from './context.js';
|
|
|
20
20
|
import { applyProxyTeardownToSettings } from './proxy.js';
|
|
21
21
|
import { readProxyState, proxyJsonExists } from '../../core/proxy-state.js';
|
|
22
22
|
import { hudCacheDir } from '../../core/cache.js';
|
|
23
|
-
import {
|
|
23
|
+
import { TRACKER_ATTEMPTS_NAMES, TRACKER_CLAIM_FILE, TRACKER_CONVENTIONS_DIR, TRACKER_ENABLED_FILE, TRACKER_LEGACY_ATTEMPTS_FILE, TRACKER_LEGACY_CONVENTIONS_FILE, TRACKER_PROVIDER_IDS, TRACKER_STAGED_PREFIX, } from '../../core/tracker.js';
|
|
24
24
|
import { revertExternalAgents } from '../../core/agent-models.js';
|
|
25
25
|
import { detectShell, getProfilePath } from '../../core/safe-delete.js';
|
|
26
26
|
import { isAlreadyInstalled, removeFromProfile } from '../../core/safe-delete-install.js';
|
|
27
|
-
import { removeManagedSettings, stripUserDenyList, detectDenyState, DEVFLOW_HISTORICAL_DENY } from '../../targets/claude-code/post-install.js';
|
|
28
|
-
import {
|
|
27
|
+
import { removeManagedSettings, stripUserDenyList, detectDenyState, DEVFLOW_HISTORICAL_DENY, DEVFLOW_TRACKED_PATHS } from '../../targets/claude-code/post-install.js';
|
|
28
|
+
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
|
+
/**
|
|
33
|
+
* Where a retired repo-local install lives: `<gitRoot>/.claude` and `<gitRoot>/.devflow`.
|
|
34
|
+
*
|
|
35
|
+
* D-LEGACY-LOCAL-CLEANUP: `init --scope local` no longer exists (D-SCOPE-RETIRED
|
|
36
|
+
* in claude-paths.ts), but repos installed by it still carry its assets, so
|
|
37
|
+
* `uninstall` keeps detecting and removing them. This is the ONLY place a
|
|
38
|
+
* repo-local install path is derived, and it is private to uninstall: nothing
|
|
39
|
+
* installs there any more. A local-only uninstall never reaches the user's
|
|
40
|
+
* settings.json or anything else under HOME — the settings strip edits
|
|
41
|
+
* `<gitRoot>/.claude/settings.json`, the legacy commands-rule purge is skipped,
|
|
42
|
+
* and the machine-wide steps (security deny list, safe-delete) run only when
|
|
43
|
+
* the user scope is being uninstalled too.
|
|
44
|
+
*
|
|
45
|
+
* Returns null when either repo-local directory IS a machine-wide one — a
|
|
46
|
+
* repository rooted at HOME (a dotfiles repo) puts `<gitRoot>/.claude` and
|
|
47
|
+
* `<gitRoot>/.devflow` on `~/.claude` and `~/.devflow`, and a "local" removal
|
|
48
|
+
* there would be a machine-wide uninstall under another name.
|
|
49
|
+
*/
|
|
50
|
+
async function legacyLocalInstallPaths(gitRoot) {
|
|
51
|
+
const legacy = {
|
|
52
|
+
claudeDir: path.join(gitRoot, '.claude'),
|
|
53
|
+
devflowDir: path.join(gitRoot, '.devflow'),
|
|
54
|
+
};
|
|
55
|
+
const machine = getInstallationPaths();
|
|
56
|
+
if (await isSameLocation(legacy.claudeDir, machine.claudeDir))
|
|
57
|
+
return null;
|
|
58
|
+
if (await isSameLocation(legacy.devflowDir, machine.devflowDir))
|
|
59
|
+
return null;
|
|
60
|
+
return legacy;
|
|
61
|
+
}
|
|
62
|
+
/**
|
|
63
|
+
* The directories for `scope`, or null for a local scope with no repo-local
|
|
64
|
+
* install to act on: outside a git repository, or in one rooted at HOME.
|
|
65
|
+
*/
|
|
66
|
+
async function scopeInstallPaths(scope, gitRoot) {
|
|
67
|
+
if (scope === 'user')
|
|
68
|
+
return getInstallationPaths();
|
|
69
|
+
return gitRoot === null ? null : legacyLocalInstallPaths(gitRoot);
|
|
70
|
+
}
|
|
32
71
|
/**
|
|
33
72
|
* The plugins the manifest records as installed, as registry definitions.
|
|
34
73
|
*
|
|
@@ -164,6 +203,103 @@ export function resolveSecurityRemovalDecision(opts) {
|
|
|
164
203
|
export function resolveProjectDataCleanup(answer) {
|
|
165
204
|
return answer === true;
|
|
166
205
|
}
|
|
206
|
+
/** Tracked entries by name and kind: `features/` is a directory, the rest are files. */
|
|
207
|
+
const TRACKED_ENTRIES = DEVFLOW_TRACKED_PATHS.map((tracked) => ({
|
|
208
|
+
name: tracked.replace(/\/$/, ''),
|
|
209
|
+
isDir: tracked.endsWith('/'),
|
|
210
|
+
}));
|
|
211
|
+
/**
|
|
212
|
+
* Split the entries under a repository's `.devflow/` into what a confirmed cleanup
|
|
213
|
+
* removes and what it keeps (D-UNINSTALL-CARVE-OUT). An entry is kept only when both
|
|
214
|
+
* its name and its kind match a tracked path, so a stray FILE named `features` is
|
|
215
|
+
* not mistaken for the tracked directory. PURE — both lists sorted by name.
|
|
216
|
+
*/
|
|
217
|
+
export function partitionProjectData(entries) {
|
|
218
|
+
const isTracked = (entry) => TRACKED_ENTRIES.some((t) => t.name === entry.name && t.isDir === entry.isDir);
|
|
219
|
+
const byName = (a, b) => a.name.localeCompare(b.name);
|
|
220
|
+
return {
|
|
221
|
+
remove: entries.filter((e) => !isTracked(e)).sort(byName),
|
|
222
|
+
keep: entries.filter(isTracked).sort(byName),
|
|
223
|
+
};
|
|
224
|
+
}
|
|
225
|
+
/**
|
|
226
|
+
* Resolve the project-data step's target: `<gitRoot>/.devflow`, partitioned.
|
|
227
|
+
*
|
|
228
|
+
* D-UNINSTALL-CARVE-OUT: the step acts on the repository's `.devflow` at its git
|
|
229
|
+
* root — the directory devflow's hooks write — never on whatever `.devflow` the
|
|
230
|
+
* cwd happens to hold. It is skipped with no git root, and when the git root is
|
|
231
|
+
* HOME or its `.devflow` is the machine-wide devflow directory (a dotfiles repo):
|
|
232
|
+
* there `.devflow` is the install itself, not project data. Both comparisons use
|
|
233
|
+
* realpaths, so macOS's `/var` → `/private/var` and a symlinked HOME still match.
|
|
234
|
+
*
|
|
235
|
+
* 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 (applies
|
|
237
|
+
* ADR-024), so a confirmed cleanup must not empty it — nor unlink a link the user made.
|
|
238
|
+
*/
|
|
239
|
+
export async function resolveProjectDataPlan(opts) {
|
|
240
|
+
if (opts.gitRoot === null)
|
|
241
|
+
return { kind: 'skip', reason: 'no-git-root' };
|
|
242
|
+
if (await isSameLocation(opts.gitRoot, opts.homeDir))
|
|
243
|
+
return { kind: 'skip', reason: 'home-root' };
|
|
244
|
+
const dir = path.join(opts.gitRoot, '.devflow');
|
|
245
|
+
if (await isSameLocation(dir, opts.machineDevflowDir))
|
|
246
|
+
return { kind: 'skip', reason: 'machine-dir' };
|
|
247
|
+
try {
|
|
248
|
+
if ((await fs.lstat(dir)).isSymbolicLink())
|
|
249
|
+
return { kind: 'skip', reason: 'symlink' };
|
|
250
|
+
}
|
|
251
|
+
catch {
|
|
252
|
+
return { kind: 'skip', reason: 'absent' };
|
|
253
|
+
}
|
|
254
|
+
let dirents;
|
|
255
|
+
try {
|
|
256
|
+
dirents = await fs.readdir(dir, { withFileTypes: true });
|
|
257
|
+
}
|
|
258
|
+
catch {
|
|
259
|
+
return { kind: 'skip', reason: 'absent' };
|
|
260
|
+
}
|
|
261
|
+
const { remove, keep } = partitionProjectData(dirents.map((d) => ({ name: d.name, isDir: d.isDirectory() })));
|
|
262
|
+
return { kind: 'plan', plan: { dir, remove, keep } };
|
|
263
|
+
}
|
|
264
|
+
/**
|
|
265
|
+
* The line that tells the user why the project-data step was skipped, or null
|
|
266
|
+
* when there is nothing worth saying (no repository, no `.devflow`). Shared by
|
|
267
|
+
* the real cleanup and the dry run so both say the same. PURE.
|
|
268
|
+
*/
|
|
269
|
+
export function formatProjectDataSkip(reason, gitRoot) {
|
|
270
|
+
if (gitRoot === null)
|
|
271
|
+
return null;
|
|
272
|
+
const dir = path.join(gitRoot, '.devflow');
|
|
273
|
+
switch (reason) {
|
|
274
|
+
case 'home-root':
|
|
275
|
+
case 'machine-dir':
|
|
276
|
+
return `Project data step skipped: ${dir}/ is the machine-wide devflow directory`;
|
|
277
|
+
case 'symlink':
|
|
278
|
+
return `Project data step skipped: ${dir} is a symbolic link — devflow does not follow it; remove it yourself if nothing there is needed`;
|
|
279
|
+
case 'no-git-root':
|
|
280
|
+
case 'absent':
|
|
281
|
+
return null;
|
|
282
|
+
}
|
|
283
|
+
}
|
|
284
|
+
/** An entry as the prompt shows it: directories carry a trailing `/`. */
|
|
285
|
+
function entryLabel(entry) {
|
|
286
|
+
return entry.isDir ? `${entry.name}/` : entry.name;
|
|
287
|
+
}
|
|
288
|
+
/** Comma-separated labels, or `(none)`. */
|
|
289
|
+
function entryList(entries) {
|
|
290
|
+
return entries.length === 0 ? '(none)' : entries.map(entryLabel).join(', ');
|
|
291
|
+
}
|
|
292
|
+
/**
|
|
293
|
+
* The lines shown before the project-data confirm: what is removed and what is
|
|
294
|
+
* kept. PURE.
|
|
295
|
+
*/
|
|
296
|
+
export function formatProjectDataPlan(plan) {
|
|
297
|
+
return [
|
|
298
|
+
`Project data in ${plan.dir}/:`,
|
|
299
|
+
` Remove: ${entryList(plan.remove)}`,
|
|
300
|
+
` Keep (shared via git): ${entryList(plan.keep)}`,
|
|
301
|
+
];
|
|
302
|
+
}
|
|
167
303
|
/**
|
|
168
304
|
* Determine the appropriate cleanup action for the user-scope devflow directory on
|
|
169
305
|
* full uninstall. Mirrors the resolveSecurityRemovalDecision pattern.
|
|
@@ -175,7 +311,7 @@ export function resolveProjectDataCleanup(answer) {
|
|
|
175
311
|
* 1. basename(devflowDir) must be '.devflow'
|
|
176
312
|
* 2. devflowDir must not equal homeDir
|
|
177
313
|
* 3. devflowDir must not be the filesystem root '/'
|
|
178
|
-
* 4. devflowDir must reside inside $HOME
|
|
314
|
+
* 4. devflowDir must reside inside $HOME
|
|
179
315
|
*
|
|
180
316
|
* Returns:
|
|
181
317
|
* - 'artifacts-only' — remove only manifest.json; leave the directory intact
|
|
@@ -192,14 +328,11 @@ export function resolveProjectDataCleanup(answer) {
|
|
|
192
328
|
* --keep-docs from triggering prompts about skill shadows or preference-profile.md.
|
|
193
329
|
*/
|
|
194
330
|
export function resolveDevflowDirCleanup(opts) {
|
|
195
|
-
// Local scope never removes project data — only install artifacts.
|
|
196
|
-
if (opts.scope !== 'user')
|
|
197
|
-
return 'artifacts-only';
|
|
198
331
|
// --keep-docs: suppress the full cleanup prompt entirely; artifacts-only.
|
|
199
332
|
if (opts.keepDocs)
|
|
200
333
|
return 'artifacts-only';
|
|
201
334
|
// Precondition guard: devflowDir must be a well-known, safe-to-rm path.
|
|
202
|
-
// Any anomalous value (
|
|
335
|
+
// Any anomalous value (bare homedir, filesystem root, a path outside HOME)
|
|
203
336
|
// resolves to artifacts-only — never throw in business logic (engineering rule).
|
|
204
337
|
const isBasenameValid = path.basename(opts.devflowDir) === '.devflow';
|
|
205
338
|
const isNotHomeDir = opts.devflowDir !== opts.homeDir;
|
|
@@ -239,35 +372,48 @@ export function userContentPaths(devflowDir) {
|
|
|
239
372
|
{ relPath: 'rules', isDir: true, label: `rule shadows (${path.join(devflowDir, 'rules')})` },
|
|
240
373
|
// preference-profile.md — user-curated decision-preference profile
|
|
241
374
|
{ relPath: 'preference-profile.md', label: 'preference-profile.md' },
|
|
242
|
-
// tracker
|
|
375
|
+
// tracker/ — the inferred, hand-editable issue-tracker conventions, one file
|
|
376
|
+
// per provider (D-TRACKER-PER-PROVIDER-CONVENTIONS).
|
|
243
377
|
//
|
|
244
378
|
// USER CONTENT (OD-15), classified the same way as preference-profile.md above
|
|
245
|
-
// rather than as an install artifact like agent-models.json, because
|
|
246
|
-
// inferred ONCE per machine and then hand-editable: absence is the trigger
|
|
247
|
-
// re-runs inference, so deleting
|
|
248
|
-
// would silently discard work the user may have corrected by hand.
|
|
379
|
+
// rather than as an install artifact like agent-models.json, because each file
|
|
380
|
+
// is inferred ONCE per machine and then hand-editable: absence is the trigger
|
|
381
|
+
// that re-runs inference, so deleting one on every decline/cancel/--keep-docs
|
|
382
|
+
// path would silently discard work the user may have corrected by hand.
|
|
249
383
|
//
|
|
250
384
|
// REVERSAL CONDITION, recorded: this classification is CONDITIONAL on the
|
|
251
385
|
// provider-mismatch guard shipping. agent-models.json was reclassified to an
|
|
252
386
|
// artifact precisely because stale overrides re-apply *silently*; "silently" is
|
|
253
|
-
// the load-bearing word. A
|
|
387
|
+
// the load-bearing word. A conventions file whose frontmatter provider
|
|
254
388
|
// disagrees with the resolved provider produces
|
|
255
389
|
// `TRACEABILITY: DEGRADED (tracker configuration mismatch (conventions file))`
|
|
256
390
|
// and no tracker call — that is what removes the silence, and the reason names
|
|
257
|
-
//
|
|
258
|
-
// two to edit. If that guard is ever dropped,
|
|
259
|
-
//
|
|
260
|
-
//
|
|
261
|
-
{
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
//
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
391
|
+
// THE FILE rather than the per-repo override so the user is told which of the
|
|
392
|
+
// two to edit. If that guard is ever dropped, reclassify these to install
|
|
393
|
+
// artifacts IN THE SAME CHANGE, otherwise a silently-authoritative stale file
|
|
394
|
+
// survives uninstall.
|
|
395
|
+
{
|
|
396
|
+
relPath: TRACKER_CONVENTIONS_DIR,
|
|
397
|
+
isDir: true,
|
|
398
|
+
label: `issue tracker conventions (${path.join(devflowDir, TRACKER_CONVENTIONS_DIR)})`,
|
|
399
|
+
},
|
|
400
|
+
// tracker.md and tracker.md.{provider}.bak — the conventions earlier releases
|
|
401
|
+
// kept in one machine-wide file, and the copies they moved aside on a provider
|
|
402
|
+
// change. The per-provider migration moves tracker.md when its frontmatter
|
|
403
|
+
// names a provider whose file does not exist yet, and leaves it otherwise;
|
|
404
|
+
// nothing moves or deletes a backup. Both hold what the conventions files
|
|
405
|
+
// hold — the user's site and project key — so the same classification: named
|
|
406
|
+
// by the confirm prompt, kept by every artifacts-only pass. The backup names
|
|
407
|
+
// come from the registry, so every provider an earlier release could have
|
|
408
|
+
// backed up is covered.
|
|
409
|
+
{
|
|
410
|
+
relPath: TRACKER_LEGACY_CONVENTIONS_FILE,
|
|
411
|
+
label: `${TRACKER_LEGACY_CONVENTIONS_FILE} (issue tracker conventions from an earlier release)`,
|
|
412
|
+
},
|
|
413
|
+
...TRACKER_PROVIDER_IDS.map(id => {
|
|
414
|
+
const name = `${TRACKER_LEGACY_CONVENTIONS_FILE}.${id}.bak`;
|
|
415
|
+
return { relPath: name, label: `${name} (previous issue tracker conventions)` };
|
|
416
|
+
}),
|
|
271
417
|
// learning.json — global learning agent tuning config
|
|
272
418
|
{ relPath: 'learning.json', label: 'learning.json' },
|
|
273
419
|
// hud.json — user HUD enable/disable preference and display config
|
|
@@ -378,14 +524,16 @@ export function installArtifactPaths(devflowDir) {
|
|
|
378
524
|
{ relPath: 'proxy-routing.json' },
|
|
379
525
|
{ relPath: 'proxy.pid' },
|
|
380
526
|
{ relPath: '.proxy-spawn.lock', isDir: true },
|
|
381
|
-
// tracker runtime artifacts — the Tracker agent's atomic claim file,
|
|
382
|
-
// inference attempt
|
|
383
|
-
//
|
|
384
|
-
//
|
|
385
|
-
// `tracker
|
|
386
|
-
// (OD-15) and are deliberately
|
|
527
|
+
// tracker runtime artifacts — the Tracker agent's atomic claim file, the
|
|
528
|
+
// per-provider inference attempt counters (and the single counter earlier
|
|
529
|
+
// releases kept), and the machine provider sentinel the SessionStart hook
|
|
530
|
+
// reads. All are machine state with no user-authored content, so they go on
|
|
531
|
+
// this list; the conventions under `tracker/` beside them, and the legacy
|
|
532
|
+
// `tracker.md` and its backups, are USER CONTENT (OD-15) and are deliberately
|
|
533
|
+
// NOT here (@D8: the two lists stay disjoint).
|
|
387
534
|
{ relPath: TRACKER_CLAIM_FILE },
|
|
388
|
-
{ relPath:
|
|
535
|
+
...TRACKER_ATTEMPTS_NAMES.map(name => ({ relPath: name })),
|
|
536
|
+
{ relPath: TRACKER_LEGACY_ATTEMPTS_FILE },
|
|
389
537
|
{ relPath: TRACKER_ENABLED_FILE },
|
|
390
538
|
// The agent's scrubbed staging file, one per invocation under a mktemp name
|
|
391
539
|
// it removes from a trap — a SIGKILL outruns the trap and leaves it behind.
|
|
@@ -414,8 +562,8 @@ export function installArtifactPaths(devflowDir) {
|
|
|
414
562
|
* This function runs on the decline, cancel, non-interactive AND --keep-docs paths,
|
|
415
563
|
* so an entry here is deleted even when the user answers "no" to the full wipe.
|
|
416
564
|
* User-authored state (everything in `userContentPaths`: skill/rule shadows,
|
|
417
|
-
* preference-profile.md, tracker
|
|
418
|
-
* hud.json) is removed only by the confirmed full-dir rm.
|
|
565
|
+
* preference-profile.md, the tracker conventions with the legacy tracker.md and
|
|
566
|
+
* its backups, learning.json, hud.json) is removed only by the confirmed full-dir rm.
|
|
419
567
|
* agent-models.json is an INSTALL ARTIFACT (stale per-agent overrides silently
|
|
420
568
|
* re-apply to renamed/deleted agents on reinstall — AC-P1-F4) and therefore
|
|
421
569
|
* belongs in this list, not in enumerateUserDevFlowContent.
|
|
@@ -619,22 +767,31 @@ export async function runDryRunPhase(opts) {
|
|
|
619
767
|
// skills sweep. Enumerate what is actually on disk for each detected scope
|
|
620
768
|
// rather than computing from the registry (which misses legacy/orphaned assets).
|
|
621
769
|
const extras = [];
|
|
770
|
+
const gitRoot = scopesToUninstall.includes('local') ? await getGitRoot() : null;
|
|
622
771
|
for (const scope of [...scopesToUninstall]) {
|
|
623
|
-
|
|
624
|
-
|
|
625
|
-
|
|
626
|
-
|
|
627
|
-
|
|
772
|
+
const paths = await scopeInstallPaths(scope, gitRoot);
|
|
773
|
+
if (paths === null)
|
|
774
|
+
continue;
|
|
775
|
+
extras.push(...await enumerateDryRunExtras(paths.claudeDir, paths.devflowDir));
|
|
776
|
+
}
|
|
777
|
+
// Project data under <gitRoot>/.devflow — the same plan the real cleanup phase
|
|
778
|
+
// resolves, so the preview lists exactly what a confirmed removal deletes.
|
|
779
|
+
const projectRoot = await getGitRoot(process.cwd());
|
|
780
|
+
const projectData = await resolveProjectDataPlan({
|
|
781
|
+
gitRoot: projectRoot,
|
|
782
|
+
homeDir: getHomeDirectory(),
|
|
783
|
+
machineDevflowDir: getInstallationPaths().devflowDir,
|
|
784
|
+
});
|
|
785
|
+
if (projectData.kind === 'plan') {
|
|
786
|
+
for (const entry of projectData.plan.remove) {
|
|
787
|
+
extras.push(`${path.join(projectData.plan.dir, entryLabel(entry))} (if confirmed)`);
|
|
628
788
|
}
|
|
629
|
-
catch { /* scope path resolution failed */ }
|
|
630
789
|
}
|
|
631
|
-
|
|
632
|
-
|
|
633
|
-
|
|
634
|
-
|
|
635
|
-
extras.push(`${devflowDataDir} (if confirmed)`);
|
|
790
|
+
else {
|
|
791
|
+
const skipped = formatProjectDataSkip(projectData.reason, projectRoot);
|
|
792
|
+
if (skipped !== null)
|
|
793
|
+
extras.push(skipped);
|
|
636
794
|
}
|
|
637
|
-
catch { /* noop */ }
|
|
638
795
|
extras.push('hooks removed from settings.json');
|
|
639
796
|
for (const line of extras) {
|
|
640
797
|
p.log.info(` ${line}`);
|
|
@@ -653,6 +810,7 @@ export async function runDryRunPhase(opts) {
|
|
|
653
810
|
*/
|
|
654
811
|
export async function runSelectivePhaseForScope(opts) {
|
|
655
812
|
const { claudeDir, devflowDir, selectedPlugins, verbose } = opts;
|
|
813
|
+
const scope = opts.scope ?? 'user';
|
|
656
814
|
const installedPlugins = opts.installedPlugins ?? DEVFLOW_PLUGINS;
|
|
657
815
|
// Revert GPT agent frontmatter BEFORE removing agent files — strips GPT model
|
|
658
816
|
// lines from installed agent frontmatter while the files are still present.
|
|
@@ -676,9 +834,9 @@ export async function runSelectivePhaseForScope(opts) {
|
|
|
676
834
|
const settingsPath = path.join(claudeDir, 'settings.json');
|
|
677
835
|
try {
|
|
678
836
|
const settings = await fs.readFile(settingsPath, 'utf-8');
|
|
679
|
-
const updated = await removeAmbientHook(settings);
|
|
837
|
+
const updated = await removeAmbientHook(settings, { purgeLegacyRule: scope === 'user' });
|
|
680
838
|
if (updated !== settings) {
|
|
681
|
-
await
|
|
839
|
+
await writeSettingsFileAtomic(settingsPath, updated);
|
|
682
840
|
if (verbose) {
|
|
683
841
|
p.log.success('Ambient mode hooks removed from settings.json');
|
|
684
842
|
}
|
|
@@ -696,7 +854,7 @@ export async function runSelectivePhaseForScope(opts) {
|
|
|
696
854
|
* User scope: interactive TTY with user-authored content → confirm before wiping
|
|
697
855
|
* ~/.devflow/; non-interactive or no user content → artifacts-only.
|
|
698
856
|
*
|
|
699
|
-
* @param opts.scope - 'user' or 'local'.
|
|
857
|
+
* @param opts.scope - 'user' or a legacy 'local' install.
|
|
700
858
|
* @param opts.claudeDir - Target Claude Code directory for this scope.
|
|
701
859
|
* @param opts.devflowDir - Devflow data directory for this scope.
|
|
702
860
|
* @param opts.devflowScriptsDir - scripts/ sub-directory removed by removeAllDevFlow.
|
|
@@ -741,7 +899,6 @@ export async function runFullPhaseForScope(opts) {
|
|
|
741
899
|
// Non-interactive, no user content, or precondition guard failure → artifacts-only.
|
|
742
900
|
const userContent = await enumerateUserDevFlowContent(devflowDir);
|
|
743
901
|
const cleanupDecision = resolveDevflowDirCleanup({
|
|
744
|
-
scope: 'user',
|
|
745
902
|
isTTY,
|
|
746
903
|
userContent,
|
|
747
904
|
devflowDir,
|
|
@@ -782,11 +939,73 @@ export async function runFullPhaseForScope(opts) {
|
|
|
782
939
|
}
|
|
783
940
|
}
|
|
784
941
|
}
|
|
942
|
+
/**
|
|
943
|
+
* The project-data step of the cleanup phase: list what a confirmed cleanup removes
|
|
944
|
+
* from `<gitRoot>/.devflow` and what it keeps, then remove on an explicit yes.
|
|
945
|
+
*
|
|
946
|
+
* D-UNINSTALL-CARVE-OUT: DEVFLOW_TRACKED_PATHS are never removed. `--keep-docs`,
|
|
947
|
+
* a non-interactive run, a decline and a cancel all leave the directory untouched,
|
|
948
|
+
* and a cancel continues the uninstall rather than exiting (avoids PF-014). The
|
|
949
|
+
* directory itself is removed only when nothing tracked was in it.
|
|
950
|
+
*/
|
|
951
|
+
async function runProjectDataStep(plan, gates) {
|
|
952
|
+
const shown = `${plan.dir}/`;
|
|
953
|
+
if (plan.remove.length === 0) {
|
|
954
|
+
p.log.info(`${shown} preserved (holds only files shared via git)`);
|
|
955
|
+
return;
|
|
956
|
+
}
|
|
957
|
+
if (gates.keepDocs) {
|
|
958
|
+
p.log.info(`${shown} preserved (--keep-docs)`);
|
|
959
|
+
return;
|
|
960
|
+
}
|
|
961
|
+
if (!gates.isTTY) {
|
|
962
|
+
p.log.info(`${shown} preserved (non-interactive mode)`);
|
|
963
|
+
return;
|
|
964
|
+
}
|
|
965
|
+
for (const line of formatProjectDataPlan(plan))
|
|
966
|
+
p.log.info(line);
|
|
967
|
+
const answer = await gates.confirm({
|
|
968
|
+
message: `Remove the project data listed above from ${shown}?`,
|
|
969
|
+
initialValue: false,
|
|
970
|
+
});
|
|
971
|
+
if (!resolveProjectDataCleanup(answer)) {
|
|
972
|
+
p.log.info(p.isCancel(answer)
|
|
973
|
+
? `${shown} preserved (prompt cancelled — continuing cleanup)`
|
|
974
|
+
: `${shown} preserved`);
|
|
975
|
+
return;
|
|
976
|
+
}
|
|
977
|
+
// Each removal is non-fatal: a failure is reported and the remaining cleanup
|
|
978
|
+
// steps (settings.json hooks above all) still run.
|
|
979
|
+
const removed = [];
|
|
980
|
+
const failed = [];
|
|
981
|
+
for (const entry of plan.remove) {
|
|
982
|
+
try {
|
|
983
|
+
await fs.rm(path.join(plan.dir, entry.name), { recursive: true, force: true });
|
|
984
|
+
removed.push(entry);
|
|
985
|
+
}
|
|
986
|
+
catch (err) {
|
|
987
|
+
failed.push(`${entryLabel(entry)} (${err.code ?? err.message})`);
|
|
988
|
+
}
|
|
989
|
+
}
|
|
990
|
+
if (plan.keep.length === 0 && failed.length === 0) {
|
|
991
|
+
// Nothing tracked was there: drop the now-empty directory. rmdir refuses a
|
|
992
|
+
// directory something wrote into meanwhile, which is then left in place.
|
|
993
|
+
await fs.rmdir(plan.dir).catch(() => undefined);
|
|
994
|
+
}
|
|
995
|
+
if (removed.length > 0)
|
|
996
|
+
p.log.success(`Removed from ${shown}: ${entryList(removed)}`);
|
|
997
|
+
if (failed.length > 0)
|
|
998
|
+
p.log.warn(`Could not remove from ${shown}: ${failed.join(', ')}`);
|
|
999
|
+
if (plan.keep.length > 0)
|
|
1000
|
+
p.log.info(`Kept: ${entryList(plan.keep)}`);
|
|
1001
|
+
}
|
|
785
1002
|
/**
|
|
786
1003
|
* CLEANUP PHASE: post-loop extras run only on full uninstall.
|
|
787
1004
|
*
|
|
788
1005
|
* Steps (all non-fatal, every interactive path gated on opts.isTTY):
|
|
789
|
-
* 1.
|
|
1006
|
+
* 1. Project data under `<gitRoot>/.devflow`, minus DEVFLOW_TRACKED_PATHS
|
|
1007
|
+
* (resolveProjectDataPlan; skipped outside a repo, in one rooted at HOME, and
|
|
1008
|
+
* when `.devflow` is a symbolic link)
|
|
790
1009
|
* 2. .claudeignore
|
|
791
1010
|
* 3. settings.json — remove all Devflow hooks and flags
|
|
792
1011
|
* 4. Security deny list
|
|
@@ -798,58 +1017,39 @@ export async function runFullPhaseForScope(opts) {
|
|
|
798
1017
|
* would leave every destructive prompt gated on a global the caller cannot set,
|
|
799
1018
|
* which under a TTY test runner points the prompts at the developer's real files.
|
|
800
1019
|
*
|
|
801
|
-
* @param opts.scopesToUninstall - Scopes processed by the scope loop.
|
|
802
|
-
*
|
|
1020
|
+
* @param opts.scopesToUninstall - Scopes processed by the scope loop. Steps 4 and 5
|
|
1021
|
+
* edit machine-wide files under HOME, so they run
|
|
1022
|
+
* only when this includes `user` — a legacy
|
|
1023
|
+
* local-only uninstall never touches HOME
|
|
1024
|
+
* (D-LEGACY-LOCAL-CLEANUP).
|
|
1025
|
+
* @param opts.keepDocs - When true, the project-data and security prompts are suppressed.
|
|
803
1026
|
* @param opts.verbose - Whether to emit verbose log lines.
|
|
804
|
-
* @param opts.cwd - Working directory
|
|
805
|
-
*
|
|
1027
|
+
* @param opts.cwd - Working directory the git root is resolved from; it
|
|
1028
|
+
* locates `<gitRoot>/.devflow` and `.claudeignore`
|
|
1029
|
+
* (the cwd itself is the `.claudeignore` fallback).
|
|
806
1030
|
* @param opts.isTTY - Whether the session is interactive. Every confirm
|
|
807
1031
|
* prompt in this phase is gated on it.
|
|
808
1032
|
*/
|
|
809
1033
|
export async function runCleanupPhase(opts) {
|
|
810
1034
|
const { scopesToUninstall, keepDocs, verbose, cwd, isTTY } = opts;
|
|
1035
|
+
const confirm = opts.confirm ?? p.confirm;
|
|
811
1036
|
// Resolve the git root from the injected cwd, not process.cwd(): otherwise
|
|
812
1037
|
// .devflow/ resolves under `cwd` while .claudeignore resolves under the process
|
|
813
1038
|
// directory, and the two halves of this phase act on different repositories.
|
|
814
1039
|
const gitRoot = await getGitRoot(cwd);
|
|
815
|
-
// 1.
|
|
816
|
-
const
|
|
817
|
-
|
|
818
|
-
|
|
819
|
-
|
|
820
|
-
|
|
821
|
-
|
|
822
|
-
|
|
823
|
-
|
|
824
|
-
|
|
825
|
-
|
|
826
|
-
|
|
827
|
-
|
|
828
|
-
if (keepDocs) {
|
|
829
|
-
shouldRemoveDevflow = false;
|
|
830
|
-
}
|
|
831
|
-
else if (isTTY) {
|
|
832
|
-
const removeDevflow = await p.confirm({
|
|
833
|
-
message: '.devflow/ directory found. Remove project data (docs, memory, learning)?',
|
|
834
|
-
initialValue: false,
|
|
835
|
-
});
|
|
836
|
-
if (p.isCancel(removeDevflow)) {
|
|
837
|
-
// Treat cancel as decline: preserve .devflow/ and continue cleanup.
|
|
838
|
-
// avoids PF-014: process.exit() here would skip claudeignore, hooks,
|
|
839
|
-
// and safe-delete removal — removeAllDevFlow has already run.
|
|
840
|
-
// applies ADR-003: clean end-state on every path.
|
|
841
|
-
p.log.info('.devflow/ preserved (prompt cancelled — continuing cleanup)');
|
|
842
|
-
preservedLogged = true;
|
|
843
|
-
}
|
|
844
|
-
shouldRemoveDevflow = resolveProjectDataCleanup(removeDevflow);
|
|
845
|
-
}
|
|
846
|
-
if (shouldRemoveDevflow) {
|
|
847
|
-
await fs.rm(devflowDataDir, { recursive: true, force: true });
|
|
848
|
-
p.log.success('.devflow/ removed');
|
|
849
|
-
}
|
|
850
|
-
else if (!preservedLogged) {
|
|
851
|
-
p.log.info('.devflow/ preserved');
|
|
852
|
-
}
|
|
1040
|
+
// 1. Project data under <gitRoot>/.devflow (D-UNINSTALL-CARVE-OUT)
|
|
1041
|
+
const projectData = await resolveProjectDataPlan({
|
|
1042
|
+
gitRoot,
|
|
1043
|
+
homeDir: getHomeDirectory(),
|
|
1044
|
+
machineDevflowDir: getInstallationPaths().devflowDir,
|
|
1045
|
+
});
|
|
1046
|
+
if (projectData.kind === 'plan') {
|
|
1047
|
+
await runProjectDataStep(projectData.plan, { keepDocs, isTTY, confirm });
|
|
1048
|
+
}
|
|
1049
|
+
else {
|
|
1050
|
+
const skipped = formatProjectDataSkip(projectData.reason, gitRoot);
|
|
1051
|
+
if (skipped !== null)
|
|
1052
|
+
p.log.info(skipped);
|
|
853
1053
|
}
|
|
854
1054
|
// 2. .claudeignore
|
|
855
1055
|
const claudeignorePath = gitRoot
|
|
@@ -863,7 +1063,7 @@ export async function runCleanupPhase(opts) {
|
|
|
863
1063
|
catch { /* doesn't exist */ }
|
|
864
1064
|
if (claudeignoreExists) {
|
|
865
1065
|
if (isTTY) {
|
|
866
|
-
const removeClaudeignore = await
|
|
1066
|
+
const removeClaudeignore = await confirm({
|
|
867
1067
|
message: '.claudeignore found. Remove it? (may contain custom rules)',
|
|
868
1068
|
initialValue: false,
|
|
869
1069
|
});
|
|
@@ -882,11 +1082,13 @@ export async function runCleanupPhase(opts) {
|
|
|
882
1082
|
// 3. settings.json (Devflow hooks)
|
|
883
1083
|
for (const scope of [...scopesToUninstall]) {
|
|
884
1084
|
try {
|
|
885
|
-
const paths = await
|
|
1085
|
+
const paths = await scopeInstallPaths(scope, gitRoot);
|
|
1086
|
+
if (paths === null)
|
|
1087
|
+
continue;
|
|
886
1088
|
const settingsPath = path.join(paths.claudeDir, 'settings.json');
|
|
887
1089
|
const originalContent = await fs.readFile(settingsPath, 'utf-8');
|
|
888
1090
|
// Remove all Devflow hooks and flags in one pass (idempotent)
|
|
889
|
-
let settingsContent = await removeAmbientHook(originalContent);
|
|
1091
|
+
let settingsContent = await removeAmbientHook(originalContent, { purgeLegacyRule: scope === 'user' });
|
|
890
1092
|
settingsContent = removeMemoryHooks(settingsContent);
|
|
891
1093
|
settingsContent = removeCaptureHooks(settingsContent);
|
|
892
1094
|
settingsContent = removeDreamHook(settingsContent);
|
|
@@ -908,7 +1110,7 @@ export async function runCleanupPhase(opts) {
|
|
|
908
1110
|
settingsContent = JSON.stringify(parsedSettings, null, 2) + '\n';
|
|
909
1111
|
}
|
|
910
1112
|
if (settingsContent !== originalContent) {
|
|
911
|
-
await
|
|
1113
|
+
await writeSettingsFileAtomic(settingsPath, settingsContent);
|
|
912
1114
|
if (verbose) {
|
|
913
1115
|
p.log.success(`Devflow hooks removed from settings.json (${scope})`);
|
|
914
1116
|
}
|
|
@@ -923,6 +1125,10 @@ export async function runCleanupPhase(opts) {
|
|
|
923
1125
|
// settings.json doesn't exist or can't be parsed — skip
|
|
924
1126
|
}
|
|
925
1127
|
}
|
|
1128
|
+
// Steps 4 and 5 edit the user's settings.json and shell profile: a legacy
|
|
1129
|
+
// local-only uninstall stops here (D-LEGACY-LOCAL-CLEANUP).
|
|
1130
|
+
if (!scopesToUninstall.includes('user'))
|
|
1131
|
+
return;
|
|
926
1132
|
// 4. Security deny list
|
|
927
1133
|
// Detect what's installed
|
|
928
1134
|
let userSettingsJsonForSecurity = null;
|
|
@@ -952,7 +1158,7 @@ export async function runCleanupPhase(opts) {
|
|
|
952
1158
|
});
|
|
953
1159
|
let shouldRemoveSecurity = false;
|
|
954
1160
|
if (securityDecision === 'prompt') {
|
|
955
|
-
const removeDenyConfirm = await
|
|
1161
|
+
const removeDenyConfirm = await confirm({
|
|
956
1162
|
message: `Remove Devflow security deny list from ${locationLabel}?`,
|
|
957
1163
|
initialValue: false,
|
|
958
1164
|
});
|
|
@@ -977,7 +1183,7 @@ export async function runCleanupPhase(opts) {
|
|
|
977
1183
|
if (detectedSecurity.user && userSettingsJsonForSecurity !== null) {
|
|
978
1184
|
const { json: stripped, removed } = stripUserDenyList(userSettingsJsonForSecurity, DEVFLOW_HISTORICAL_DENY);
|
|
979
1185
|
if (removed.length > 0) {
|
|
980
|
-
await
|
|
1186
|
+
await writeSettingsFileAtomic(userSettingsPathForSecurity, stripped);
|
|
981
1187
|
p.log.success(`Security deny list removed from user settings (${removed.length} entries)`);
|
|
982
1188
|
}
|
|
983
1189
|
}
|
|
@@ -992,7 +1198,7 @@ export async function runCleanupPhase(opts) {
|
|
|
992
1198
|
const profilePath = getProfilePath(shell);
|
|
993
1199
|
if (profilePath && await isAlreadyInstalled(profilePath)) {
|
|
994
1200
|
if (isTTY) {
|
|
995
|
-
const removeSafeDelete = await
|
|
1201
|
+
const removeSafeDelete = await confirm({
|
|
996
1202
|
message: `Remove safe-delete function from ${profilePath}?`,
|
|
997
1203
|
initialValue: false,
|
|
998
1204
|
});
|
|
@@ -1017,7 +1223,7 @@ export async function runCleanupPhase(opts) {
|
|
|
1017
1223
|
export const uninstallCommand = new Command('uninstall')
|
|
1018
1224
|
.description('Uninstall Devflow from Claude Code')
|
|
1019
1225
|
.option('--keep-docs', 'Keep .devflow/ directory and project data')
|
|
1020
|
-
.option('--scope <type>', 'Uninstall from
|
|
1226
|
+
.option('--scope <type>', 'Uninstall from one scope only: user, or local to remove a legacy project-local install (default: auto-detect both)', /^(user|local)$/i)
|
|
1021
1227
|
.option('--plugin <names>', 'Uninstall specific plugin(s), comma-separated (e.g., implement,code-review)')
|
|
1022
1228
|
.option('--verbose', 'Show detailed uninstall output')
|
|
1023
1229
|
.option('--dry-run', 'Show what would be removed without actually removing anything')
|
|
@@ -1054,24 +1260,27 @@ export const uninstallCommand = new Command('uninstall')
|
|
|
1054
1260
|
: [];
|
|
1055
1261
|
// Determine which scopes to uninstall
|
|
1056
1262
|
let scopesToUninstall = [];
|
|
1263
|
+
const gitRoot = await getGitRoot();
|
|
1057
1264
|
if (options.scope) {
|
|
1058
1265
|
scopesToUninstall = [options.scope.toLowerCase()];
|
|
1266
|
+
// A local-only uninstall with no repo-local install to act on stops here,
|
|
1267
|
+
// before its cleanup phase can reach the cwd — HOME, in a repo rooted there.
|
|
1268
|
+
if (scopesToUninstall[0] === 'local' && await scopeInstallPaths('local', gitRoot) === null) {
|
|
1269
|
+
p.log.error('No legacy project-local install here: not in a git repository, or the repository root holds the machine-wide install');
|
|
1270
|
+
process.exit(1);
|
|
1271
|
+
}
|
|
1059
1272
|
}
|
|
1060
1273
|
else {
|
|
1061
|
-
|
|
1062
|
-
const gitRoot = await getGitRoot();
|
|
1063
|
-
if (await isDevFlowInstalled(userClaudeDir)) {
|
|
1274
|
+
if (await isDevFlowInstalled(getClaudeDirectory())) {
|
|
1064
1275
|
scopesToUninstall.push('user');
|
|
1065
1276
|
}
|
|
1066
|
-
|
|
1067
|
-
|
|
1068
|
-
|
|
1069
|
-
scopesToUninstall.push('local');
|
|
1070
|
-
}
|
|
1277
|
+
const legacyPaths = gitRoot === null ? null : await legacyLocalInstallPaths(gitRoot);
|
|
1278
|
+
if (legacyPaths !== null && await isDevFlowInstalled(legacyPaths.claudeDir)) {
|
|
1279
|
+
scopesToUninstall.push('local');
|
|
1071
1280
|
}
|
|
1072
1281
|
if (scopesToUninstall.length === 0) {
|
|
1073
1282
|
p.log.error('No Devflow installation found');
|
|
1074
|
-
p.log.info(
|
|
1283
|
+
p.log.info(`Checked user scope (${getClaudeDirectory()}/) and legacy local scope (git-root/.claude/)`);
|
|
1075
1284
|
process.exit(1);
|
|
1076
1285
|
}
|
|
1077
1286
|
if (scopesToUninstall.length > 1 && !dryRun) {
|
|
@@ -1080,7 +1289,7 @@ export const uninstallCommand = new Command('uninstall')
|
|
|
1080
1289
|
message: 'Found Devflow in multiple scopes. Uninstall from:',
|
|
1081
1290
|
options: [
|
|
1082
1291
|
{ value: 'both', label: 'Both', hint: 'user + local' },
|
|
1083
|
-
{ value: 'user', label: 'User scope', hint:
|
|
1292
|
+
{ value: 'user', label: 'User scope', hint: `${getClaudeDirectory()}/` },
|
|
1084
1293
|
{ value: 'local', label: 'Local scope', hint: 'git-root/.claude/' },
|
|
1085
1294
|
],
|
|
1086
1295
|
});
|
|
@@ -1102,11 +1311,10 @@ export const uninstallCommand = new Command('uninstall')
|
|
|
1102
1311
|
// One resolution, handed to the dry-run and (below) to the real removal,
|
|
1103
1312
|
// so the preview and the outcome are computed from the same list.
|
|
1104
1313
|
let dryRunInstalled = DEVFLOW_PLUGINS;
|
|
1105
|
-
|
|
1106
|
-
|
|
1107
|
-
dryRunInstalled = await resolveInstalledPlugins(
|
|
1314
|
+
const dryRunPaths = await scopeInstallPaths(scopesToUninstall[0], gitRoot);
|
|
1315
|
+
if (dryRunPaths !== null) {
|
|
1316
|
+
dryRunInstalled = await resolveInstalledPlugins(dryRunPaths.devflowDir);
|
|
1108
1317
|
}
|
|
1109
|
-
catch { /* scope path resolution failed — fall back to the registry */ }
|
|
1110
1318
|
await runDryRunPhase({
|
|
1111
1319
|
scopesToUninstall,
|
|
1112
1320
|
isSelectiveUninstall,
|
|
@@ -1129,7 +1337,9 @@ export const uninstallCommand = new Command('uninstall')
|
|
|
1129
1337
|
if (!isSelectiveUninstall) {
|
|
1130
1338
|
for (const scope of scopesToUninstall) {
|
|
1131
1339
|
try {
|
|
1132
|
-
const paths = await
|
|
1340
|
+
const paths = await scopeInstallPaths(scope, gitRoot);
|
|
1341
|
+
if (paths === null)
|
|
1342
|
+
continue;
|
|
1133
1343
|
if (await proxyJsonExists(paths.devflowDir)) {
|
|
1134
1344
|
const proxyState = await readProxyState(paths.devflowDir);
|
|
1135
1345
|
if (proxyState.ok)
|
|
@@ -1141,25 +1351,19 @@ export const uninstallCommand = new Command('uninstall')
|
|
|
1141
1351
|
}
|
|
1142
1352
|
// Uninstall from each scope
|
|
1143
1353
|
for (const scope of scopesToUninstall) {
|
|
1144
|
-
|
|
1145
|
-
|
|
1146
|
-
|
|
1147
|
-
try {
|
|
1148
|
-
const paths = await getInstallationPaths(scope);
|
|
1149
|
-
claudeDir = paths.claudeDir;
|
|
1150
|
-
devflowDir = paths.devflowDir;
|
|
1151
|
-
devflowScriptsDir = path.join(paths.devflowDir, 'scripts');
|
|
1152
|
-
if (scope === 'user') {
|
|
1153
|
-
p.log.step('Uninstalling user scope (~/.claude/)');
|
|
1154
|
-
}
|
|
1155
|
-
else {
|
|
1156
|
-
p.log.step('Uninstalling local scope (git-root/.claude/)');
|
|
1157
|
-
}
|
|
1158
|
-
}
|
|
1159
|
-
catch (error) {
|
|
1160
|
-
p.log.warn(`Cannot uninstall ${scope} scope: ${error instanceof Error ? error.message : error}`);
|
|
1354
|
+
const paths = await scopeInstallPaths(scope, gitRoot);
|
|
1355
|
+
if (paths === null) {
|
|
1356
|
+
p.log.warn(`Cannot uninstall ${scope} scope: no repo-local install here`);
|
|
1161
1357
|
continue;
|
|
1162
1358
|
}
|
|
1359
|
+
const { claudeDir, devflowDir } = paths;
|
|
1360
|
+
const devflowScriptsDir = path.join(devflowDir, 'scripts');
|
|
1361
|
+
if (scope === 'user') {
|
|
1362
|
+
p.log.step(`Uninstalling user scope (${claudeDir})`);
|
|
1363
|
+
}
|
|
1364
|
+
else {
|
|
1365
|
+
p.log.step('Uninstalling legacy local scope (git-root/.claude/)');
|
|
1366
|
+
}
|
|
1163
1367
|
if (isSelectiveUninstall) {
|
|
1164
1368
|
await runSelectivePhaseForScope({
|
|
1165
1369
|
claudeDir,
|
|
@@ -1167,6 +1371,7 @@ export const uninstallCommand = new Command('uninstall')
|
|
|
1167
1371
|
selectedPlugins,
|
|
1168
1372
|
verbose,
|
|
1169
1373
|
installedPlugins: await resolveInstalledPlugins(devflowDir),
|
|
1374
|
+
scope,
|
|
1170
1375
|
});
|
|
1171
1376
|
}
|
|
1172
1377
|
else {
|