freeharness 0.4.1 → 0.4.2
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/README.md +8 -7
- package/canonical/HARVEST.yaml +25 -14
- package/canonical/PROVENANCE.yaml +1 -1
- package/canonical/agents/README.md +77 -36
- package/canonical/agents/agentic-architect.md +2 -1
- package/canonical/agents/database-reviewer.md +6 -8
- package/canonical/agents/explorer-ai-engineer-analyst.md +2 -6
- package/canonical/agents/implementer.md +30 -0
- package/canonical/agents/pr-reviewer.md +9 -11
- package/canonical/agents/python-reviewer.md +7 -9
- package/canonical/agents/report-formatter.md +7 -3
- package/canonical/agents/security-reviewer.md +7 -8
- package/canonical/agents/test-writer.md +5 -2
- package/canonical/hooks/cost-prices.json +11 -9
- package/canonical/hooks/freeharness-config-guard.mjs +8072 -87
- package/canonical/hooks/freeharness-context-monitor.mjs +88 -54
- package/canonical/hooks/freeharness-converge.mjs +78 -23
- package/canonical/hooks/freeharness-cost-tracker.mjs +1129 -218
- package/canonical/hooks/freeharness-memory-guard.mjs +20 -23
- package/canonical/hooks/freeharness-precompact.mjs +217 -87
- package/canonical/hooks/freeharness-quota-tap.mjs +173 -0
- package/canonical/hooks/freeharness-reload-context.mjs +197 -0
- package/canonical/hooks/freeharness-repeat-tool.mjs +1 -1
- package/canonical/hooks/freeharness-sdd-context.mjs +970 -0
- package/canonical/hooks/freeharness-session-start +297 -59
- package/canonical/hooks/hooks.claude-code.json +1 -1
- package/canonical/hooks/hooks.config-guard.claude-code.json +3 -2
- package/canonical/hooks/hooks.context-monitor.claude-code.json +1 -1
- package/canonical/hooks/hooks.converge.claude-code.json +1 -1
- package/canonical/hooks/hooks.memory-guard.claude-code.json +3 -2
- package/canonical/hooks/hooks.reload-context.claude-code.json +15 -0
- package/canonical/openspec/schema.spec-driven.yaml +29 -8
- package/canonical/openspec/templates/follow-ups.md +3 -2
- package/canonical/openspec/templates/proposal.md +1 -1
- package/canonical/openspec/templates/tasks.md +8 -0
- package/canonical/rules/CLAUDE.md +5 -4
- package/canonical/rules/routing.md +10 -9
- package/canonical/skills/agent-evaluation/SKILL.md +17 -6
- package/canonical/skills/agent-evaluation/evals/cases.yaml +38 -23
- package/canonical/skills/agentic-architecture/SKILL.md +4 -0
- package/canonical/skills/agentic-architecture/evals/cases.yaml +27 -0
- package/canonical/skills/code-review/SKILL.md +9 -2
- package/canonical/skills/code-review/evals/cases.yaml +1118 -27
- package/canonical/skills/git-workflow/SKILL.md +2 -0
- package/canonical/skills/local-code/SKILL.md +2 -2
- package/canonical/skills/local-code/evals/cases.yaml +1 -1
- package/canonical/skills/report-formatting/SKILL.md +10 -2
- package/canonical/skills/report-formatting/reference/ARCHITECTURE.md +3 -3
- package/canonical/skills/report-formatting/reference/HELPERS.md +10 -5
- package/canonical/skills/report-formatting/reference/RELEASE.md +1 -2
- package/canonical/skills/report-formatting/reference/TEMPLATES.md +3 -2
- package/canonical/skills/report-formatting/reference/authoring.md +25 -37
- package/canonical/skills/report-formatting/scripts/build-architecture.js +1 -1
- package/canonical/skills/report-formatting/scripts/build-md-claro.js +5 -8
- package/canonical/skills/report-formatting/scripts/check-env.js +0 -4
- package/canonical/skills/report-formatting/scripts/docmodel.js +3 -11
- package/canonical/skills/report-formatting/scripts/example-build.js +8 -10
- package/canonical/skills/report-formatting/scripts/example_build_ArchitectureReview_docx.js +8 -10
- package/canonical/skills/report-formatting/scripts/example_build_ExecutiveReport_docx.js +8 -10
- package/canonical/skills/report-formatting/scripts/theme-claro.js +39 -10
- package/canonical/skills/subagent-driven-development/SKILL.md +35 -28
- package/canonical/skills/subagent-driven-development/evals/cases.yaml +3496 -19
- package/canonical/skills/subagent-driven-development/reference/brief.md +185 -35
- package/canonical/skills/subagent-driven-development/reference/handoff.md +82 -2
- package/canonical/skills/subagent-driven-development/reference/loop.md +57 -0
- package/canonical/skills/subagent-driven-development/reference/trees.md +40 -0
- package/canonical/skills/subagent-driven-development/reference/workspace.md +31 -0
- package/canonical/skills/subagent-driven-development/scripts/validate-handoff.mjs +4771 -326
- package/dist/adapters/harness/claude-code.d.ts +19 -3
- package/dist/adapters/harness/claude-code.js +204 -39
- package/dist/adapters/harness/opencode.js +16 -2
- package/dist/adapters/harness/plugin.js +28 -17
- package/dist/adapters/memory/doctor.d.ts +174 -29
- package/dist/adapters/memory/doctor.js +758 -134
- package/dist/adapters/memory/migrate.d.ts +95 -0
- package/dist/adapters/memory/migrate.js +497 -0
- package/dist/adapters/memory/paths.d.ts +22 -0
- package/dist/adapters/memory/paths.js +85 -0
- package/dist/adapters/memory/vendored.d.ts +56 -29
- package/dist/adapters/memory/vendored.js +191 -154
- package/dist/adapters/sync/diff.js +4 -1
- package/dist/cli/cleanup-prompt.d.ts +35 -0
- package/dist/cli/cleanup-prompt.js +68 -0
- package/dist/cli/cleanup.d.ts +22 -24
- package/dist/cli/cleanup.js +182 -68
- package/dist/cli/cost-quota.d.ts +158 -0
- package/dist/cli/cost-quota.js +645 -0
- package/dist/cli/cost.d.ts +408 -46
- package/dist/cli/cost.js +1731 -57
- package/dist/cli/doctor.d.ts +52 -13
- package/dist/cli/doctor.js +193 -44
- package/dist/cli/hard-cleanup.d.ts +93 -0
- package/dist/cli/hard-cleanup.js +552 -0
- package/dist/cli/install-facts.d.ts +28 -0
- package/dist/cli/install-facts.js +66 -0
- package/dist/cli/json-key-edit.d.ts +18 -0
- package/dist/cli/json-key-edit.js +118 -0
- package/dist/cli/memory-import.d.ts +74 -0
- package/dist/cli/memory-import.js +469 -0
- package/dist/cli/plugin-validate.d.ts +12 -0
- package/dist/cli/plugin-validate.js +43 -0
- package/dist/cli/uninstall.d.ts +1 -1
- package/dist/cli/uninstall.js +2 -2
- package/dist/cli/upstream-launchers.d.ts +49 -0
- package/dist/cli/upstream-launchers.js +287 -0
- package/dist/index.js +247 -70
- package/dist/infra/fsutil.d.ts +31 -3
- package/dist/infra/fsutil.js +69 -6
- package/dist/infra/manifest.d.ts +2 -1
- package/dist/infra/manifest.js +3 -2
- package/dist/infra/prices.d.ts +18 -0
- package/dist/infra/prices.js +64 -0
- package/node_modules/@freeharness/core/dist/tiers.d.ts +10 -1
- package/node_modules/@freeharness/core/dist/tiers.js +24 -2
- package/node_modules/@freeharness/core/package.json +1 -1
- package/node_modules/@freeharness/sdd/PROVENANCE.yaml +279 -2
- package/node_modules/@freeharness/sdd/dist/src/bin/openspec.js +34 -8
- package/node_modules/@freeharness/sdd/dist/src/shim/archive.d.ts +7 -6
- package/node_modules/@freeharness/sdd/dist/src/shim/archive.js +72 -64
- package/node_modules/@freeharness/sdd/dist/src/shim/follow-up.d.ts +48 -16
- package/node_modules/@freeharness/sdd/dist/src/shim/follow-up.js +168 -70
- package/node_modules/@freeharness/sdd/dist/src/shim/instructions.d.ts +36 -6
- package/node_modules/@freeharness/sdd/dist/src/shim/instructions.js +110 -49
- package/node_modules/@freeharness/sdd/dist/src/shim/list.d.ts +1 -1
- package/node_modules/@freeharness/sdd/dist/src/shim/list.js +57 -14
- package/node_modules/@freeharness/sdd/dist/src/shim/nested-change.d.ts +7 -0
- package/node_modules/@freeharness/sdd/dist/src/shim/nested-change.js +118 -0
- package/node_modules/@freeharness/sdd/dist/src/shim/new-change.js +3 -1
- package/node_modules/@freeharness/sdd/dist/src/shim/proposal-capabilities.d.ts +12 -0
- package/node_modules/@freeharness/sdd/dist/src/shim/proposal-capabilities.js +94 -0
- package/node_modules/@freeharness/sdd/dist/src/shim/shared.d.ts +17 -0
- package/node_modules/@freeharness/sdd/dist/src/shim/shared.js +182 -1
- package/node_modules/@freeharness/sdd/dist/src/shim/status.d.ts +2 -2
- package/node_modules/@freeharness/sdd/dist/src/shim/status.js +34 -5
- package/node_modules/@freeharness/sdd/dist/src/shim/validate.d.ts +9 -0
- package/node_modules/@freeharness/sdd/dist/src/shim/validate.js +49 -41
- package/node_modules/@freeharness/sdd/dist/src/shim/warnings.d.ts +21 -0
- package/node_modules/@freeharness/sdd/dist/src/shim/warnings.js +66 -0
- package/node_modules/@freeharness/sdd/dist/vendor/openspec/apply/line-endings.d.ts +4 -0
- package/node_modules/@freeharness/sdd/dist/vendor/openspec/apply/line-endings.js +17 -0
- package/node_modules/@freeharness/sdd/dist/vendor/openspec/apply/specs-apply.js +95 -22
- package/node_modules/@freeharness/sdd/dist/vendor/openspec/engine/change-status-policy.d.ts +6 -0
- package/node_modules/@freeharness/sdd/dist/vendor/openspec/engine/change-status-policy.js +14 -6
- package/node_modules/@freeharness/sdd/dist/vendor/openspec/engine/index.d.ts +1 -1
- package/node_modules/@freeharness/sdd/dist/vendor/openspec/engine/index.js +1 -1
- package/node_modules/@freeharness/sdd/dist/vendor/openspec/engine/outputs.d.ts +3 -3
- package/node_modules/@freeharness/sdd/dist/vendor/openspec/engine/outputs.js +24 -4
- package/node_modules/@freeharness/sdd/dist/vendor/openspec/engine/task-progress.js +4 -7
- package/node_modules/@freeharness/sdd/dist/vendor/openspec/engine/types.js +13 -1
- package/node_modules/@freeharness/sdd/dist/vendor/openspec/generation/templates/workflows/apply-change.js +21 -6
- package/node_modules/@freeharness/sdd/dist/vendor/openspec/generation/templates/workflows/archive-change.js +80 -19
- package/node_modules/@freeharness/sdd/dist/vendor/openspec/generation/templates/workflows/explore.js +42 -22
- package/node_modules/@freeharness/sdd/dist/vendor/openspec/generation/templates/workflows/project-root.d.ts +32 -0
- package/node_modules/@freeharness/sdd/dist/vendor/openspec/generation/templates/workflows/project-root.js +39 -0
- package/node_modules/@freeharness/sdd/dist/vendor/openspec/generation/templates/workflows/propose.js +16 -9
- package/node_modules/@freeharness/sdd/dist/vendor/openspec/generation/templates/workflows/sync-specs.js +36 -1
- package/node_modules/@freeharness/sdd/dist/vendor/openspec/generation/templates/workflows/update-change.js +65 -30
- package/node_modules/@freeharness/sdd/dist/vendor/openspec/validation/constants.d.ts +1 -1
- package/node_modules/@freeharness/sdd/dist/vendor/openspec/validation/constants.js +1 -1
- package/node_modules/@freeharness/sdd/dist/vendor/openspec/validation/parsers/change-parser.d.ts +8 -13
- package/node_modules/@freeharness/sdd/dist/vendor/openspec/validation/parsers/change-parser.js +79 -102
- package/node_modules/@freeharness/sdd/dist/vendor/openspec/validation/parsers/markdown-parser.js +4 -9
- package/node_modules/@freeharness/sdd/dist/vendor/openspec/validation/parsers/requirement-blocks.d.ts +50 -10
- package/node_modules/@freeharness/sdd/dist/vendor/openspec/validation/parsers/requirement-blocks.js +165 -37
- package/node_modules/@freeharness/sdd/dist/vendor/openspec/validation/parsers/requirement-text.d.ts +24 -3
- package/node_modules/@freeharness/sdd/dist/vendor/openspec/validation/parsers/requirement-text.js +55 -8
- package/node_modules/@freeharness/sdd/dist/vendor/openspec/validation/parsers/spec-structure.js +4 -1
- package/node_modules/@freeharness/sdd/dist/vendor/openspec/validation/purpose-placeholder.js +19 -9
- package/node_modules/@freeharness/sdd/dist/vendor/openspec/validation/spec-discovery.d.ts +10 -0
- package/node_modules/@freeharness/sdd/dist/vendor/openspec/validation/spec-discovery.js +115 -0
- package/node_modules/@freeharness/sdd/dist/vendor/openspec/validation/task-lines.d.ts +3 -3
- package/node_modules/@freeharness/sdd/dist/vendor/openspec/validation/task-lines.js +20 -20
- package/node_modules/@freeharness/sdd/dist/vendor/openspec/validation/validator.d.ts +6 -0
- package/node_modules/@freeharness/sdd/dist/vendor/openspec/validation/validator.js +75 -14
- package/node_modules/@freeharness/sdd/package.json +1 -1
- package/node_modules/@freeharness/sdd/vendor/openspec/assets/schema.upstream.yaml +47 -7
- package/node_modules/@freeharness/sdd/vendor/openspec/assets/skills/openspec-apply-change/SKILL.md +25 -7
- package/node_modules/@freeharness/sdd/vendor/openspec/assets/skills/openspec-archive-change/SKILL.md +50 -11
- package/node_modules/@freeharness/sdd/vendor/openspec/assets/skills/openspec-bulk-archive-change/SKILL.md +48 -10
- package/node_modules/@freeharness/sdd/vendor/openspec/assets/skills/openspec-continue-change/SKILL.md +27 -7
- package/node_modules/@freeharness/sdd/vendor/openspec/assets/skills/openspec-explore/SKILL.md +25 -12
- package/node_modules/@freeharness/sdd/vendor/openspec/assets/skills/openspec-ff-change/SKILL.md +17 -5
- package/node_modules/@freeharness/sdd/vendor/openspec/assets/skills/openspec-new-change/SKILL.md +12 -1
- package/node_modules/@freeharness/sdd/vendor/openspec/assets/skills/openspec-onboard/SKILL.md +47 -33
- package/node_modules/@freeharness/sdd/vendor/openspec/assets/skills/openspec-propose/SKILL.md +17 -5
- package/node_modules/@freeharness/sdd/vendor/openspec/assets/skills/openspec-sync-specs/SKILL.md +29 -1
- package/node_modules/@freeharness/sdd/vendor/openspec/assets/skills/openspec-update-change/SKILL.md +31 -13
- package/node_modules/@freeharness/sdd/vendor/openspec/assets/skills/openspec-verify-change/SKILL.md +75 -27
- package/package.json +6 -4
- package/provenance/canonical.PROVENANCE.yaml +1 -1
- package/provenance/memory.PROVENANCE.yaml +458 -6
- package/provenance/sdd.PROVENANCE.yaml +279 -2
- package/canonical/hooks/freeharness-auto-memory.mjs +0 -158
- package/canonical/hooks/hooks.auto-memory.claude-code.json +0 -16
|
@@ -0,0 +1,68 @@
|
|
|
1
|
+
import { applyCleanup } from "./cleanup.js";
|
|
2
|
+
import { applyHard, printable } from "./hard-cleanup.js";
|
|
3
|
+
/**
|
|
4
|
+
* Maps the answers to pass-2 actions; the hard answer counts only with --hard.
|
|
5
|
+
* @param offersMoves The cleanup plan holds an archive item, so the abort line names moves too.
|
|
6
|
+
*/
|
|
7
|
+
export function cleanupDecision(answers, withHard, offersMoves = false) {
|
|
8
|
+
const moveHard = withHard && answers.hard;
|
|
9
|
+
const nothing = !answers.main && !answers.backups && !moveHard;
|
|
10
|
+
let abort;
|
|
11
|
+
if (nothing)
|
|
12
|
+
abort = withHard || offersMoves ? "Aborted; nothing was removed or moved." : "Aborted; nothing was removed.";
|
|
13
|
+
return { abort, remove: (i) => (i.group === "backups" ? answers.backups : answers.main), moveHard };
|
|
14
|
+
}
|
|
15
|
+
/** The first cleanup confirmation; a zero remove or archive count is left out. */
|
|
16
|
+
function mainPrompt(removeCount, archiveCount) {
|
|
17
|
+
if (archiveCount === 0)
|
|
18
|
+
return `\nRemove the ${removeCount} item(s) above (backups excluded)?`;
|
|
19
|
+
if (removeCount === 0)
|
|
20
|
+
return `\nArchive the ${archiveCount} item(s) above (backups excluded)?`;
|
|
21
|
+
return `\nRemove ${removeCount} and archive ${archiveCount} item(s) above (backups excluded)?`;
|
|
22
|
+
}
|
|
23
|
+
/** The third cleanup confirmation, for the hard items. */
|
|
24
|
+
export function hardPrompt(count, archiveRoot) {
|
|
25
|
+
return `Move the ${count} item(s) to ${printable(archiveRoot)}?`;
|
|
26
|
+
}
|
|
27
|
+
/** Asks the three confirmations, then applies what was confirmed; undefined when all were no. */
|
|
28
|
+
export async function runCleanupPass2(input) {
|
|
29
|
+
const { plan, hard, yes, prompt, log } = input;
|
|
30
|
+
const removable = plan.items.filter((i) => !i.refused);
|
|
31
|
+
const main = removable.filter((i) => i.group !== "backups");
|
|
32
|
+
const backups = removable.filter((i) => i.group === "backups");
|
|
33
|
+
const movable = hard?.items.filter((i) => !i.refused) ?? [];
|
|
34
|
+
const archive = main.filter((i) => i.archiveDest !== undefined).length;
|
|
35
|
+
let confirmMain = yes;
|
|
36
|
+
let confirmBackups = yes;
|
|
37
|
+
let confirmHard = yes;
|
|
38
|
+
if (!yes) {
|
|
39
|
+
if (main.length > 0)
|
|
40
|
+
confirmMain = await prompt.confirm(mainPrompt(main.length - archive, archive));
|
|
41
|
+
if (backups.length > 0) {
|
|
42
|
+
const total = backups.reduce((n, b) => n + (b.bytes ?? 0), 0);
|
|
43
|
+
confirmBackups = await prompt.confirm(`Also delete the ${backups.length} memory backup dir(s) (${total >= 1048576 ? `${(total / 1048576).toFixed(1)} MB` : `${total} B`})? They cannot be recovered.`);
|
|
44
|
+
}
|
|
45
|
+
if (hard && movable.length > 0)
|
|
46
|
+
confirmHard = await prompt.confirm(hardPrompt(movable.length, hard.archiveRoot));
|
|
47
|
+
}
|
|
48
|
+
const decision = cleanupDecision({ main: confirmMain, backups: confirmBackups, hard: confirmHard }, hard !== undefined, archive > 0);
|
|
49
|
+
if (decision.abort) {
|
|
50
|
+
log(decision.abort);
|
|
51
|
+
return undefined;
|
|
52
|
+
}
|
|
53
|
+
const done = applyCleanup(plan, decision.remove);
|
|
54
|
+
for (const r of done.removed)
|
|
55
|
+
log(` removed: ${printable(r)}`);
|
|
56
|
+
for (const m of done.moved)
|
|
57
|
+
log(` moved: ${printable(m)}`);
|
|
58
|
+
for (const k of done.skipped)
|
|
59
|
+
log(` kept: ${printable(k)}`);
|
|
60
|
+
for (const w of done.warnings)
|
|
61
|
+
log(` WARN ${printable(w)}`);
|
|
62
|
+
const archived = hard && decision.moveHard ? applyHard(hard) : undefined;
|
|
63
|
+
for (const m of archived?.moved ?? [])
|
|
64
|
+
log(` moved: ${printable(m)}`);
|
|
65
|
+
for (const w of archived?.warnings ?? [])
|
|
66
|
+
log(` WARN ${printable(w)}`);
|
|
67
|
+
return { done, archived };
|
|
68
|
+
}
|
package/dist/cli/cleanup.d.ts
CHANGED
|
@@ -1,6 +1,5 @@
|
|
|
1
1
|
import type { Scope } from "@freeharness/core";
|
|
2
2
|
import { UPSTREAM_PLUGIN_NAME } from "../adapters/memory/vendored.js";
|
|
3
|
-
import { type InstallManifest } from "../infra/manifest.js";
|
|
4
3
|
import { type MemoryDiagnosis } from "../adapters/memory/doctor.js";
|
|
5
4
|
/** The vendored `openspec` bin replaces it. */
|
|
6
5
|
export declare const LEGACY_OPENSPEC_PACKAGE = "@fission-ai/openspec";
|
|
@@ -8,25 +7,11 @@ export declare const LEGACY_OPENSPEC_PACKAGE = "@fission-ai/openspec";
|
|
|
8
7
|
export declare const CLI_PACKAGE = "freeharness";
|
|
9
8
|
/** Backups confirm separately. */
|
|
10
9
|
export type CleanupGroup = "memory" | "backups" | "legacy" | "duplicates" | "npm" | "retired";
|
|
11
|
-
/**
|
|
12
|
-
export
|
|
13
|
-
readonly skills: readonly ["local-code-intel"];
|
|
14
|
-
readonly agents: readonly ["mle-reviewer", "mle-evaluator"];
|
|
15
|
-
};
|
|
16
|
-
/** One retired name found on disk, with its manifest-recorded flag. */
|
|
17
|
-
export interface RetiredFinding {
|
|
18
|
-
kind: "skill" | "agent";
|
|
19
|
-
name: string;
|
|
20
|
-
/** Skill dir or agent file. */
|
|
21
|
-
path: string;
|
|
22
|
-
/** Manifest lists `path` (agent) or a file under it (skill dir). */
|
|
23
|
-
recorded: boolean;
|
|
24
|
-
}
|
|
25
|
-
/** Whose entries a `hooks` item prunes. */
|
|
26
|
-
export type HookOwner = "freeharness" | typeof UPSTREAM_PLUGIN_NAME;
|
|
10
|
+
/** Whose entries a `hooks` item prunes; `retired` is only the entries running a retired hook script. */
|
|
11
|
+
export type HookOwner = "freeharness" | typeof UPSTREAM_PLUGIN_NAME | "retired";
|
|
27
12
|
/** One leftover the cleanup knows how to remove. */
|
|
28
13
|
export interface CleanupItem {
|
|
29
|
-
kind: "dir" | "file" | "hooks" | "npm-global" | "
|
|
14
|
+
kind: "dir" | "file" | "hooks" | "npm-global" | "json-key";
|
|
30
15
|
/** Filesystem path, or the package name for `npm-global`. */
|
|
31
16
|
target: string;
|
|
32
17
|
reason: string;
|
|
@@ -39,8 +24,14 @@ export interface CleanupItem {
|
|
|
39
24
|
hookOwner?: HookOwner;
|
|
40
25
|
/** `npm-global` items: try `npm unlink -g` first, then `npm uninstall -g`. */
|
|
41
26
|
unlink?: boolean;
|
|
42
|
-
/** `
|
|
43
|
-
|
|
27
|
+
/** `json-key` items: the key removed from the JSON object in `target`, outermost first. */
|
|
28
|
+
keyPath?: string[];
|
|
29
|
+
/** Plugin-cache and IDE-rule items: the archive path the item moves to instead of being removed; `json-key` items: where the pre-edit copy goes. */
|
|
30
|
+
archiveDest?: string;
|
|
31
|
+
/** Marks an IDE rule file, whose archive check differs from a plugin-cache item's. */
|
|
32
|
+
ideRule?: boolean;
|
|
33
|
+
/** Upstream launcher items, each kind with its own pass-2 check. */
|
|
34
|
+
launcher?: "mcp" | "marketplace";
|
|
44
35
|
}
|
|
45
36
|
/** What a cleanup pass would (or did) do. */
|
|
46
37
|
export interface CleanupPlan {
|
|
@@ -48,10 +39,14 @@ export interface CleanupPlan {
|
|
|
48
39
|
items: CleanupItem[];
|
|
49
40
|
/** Informational lines: seen, but out of the fixed list. */
|
|
50
41
|
notes: string[];
|
|
42
|
+
/** User scope: the dated archive every `archiveDest` lies in. */
|
|
43
|
+
archiveRoot?: string;
|
|
51
44
|
}
|
|
52
45
|
/** Outcome of `applyCleanup`. */
|
|
53
46
|
export interface CleanupResult {
|
|
54
47
|
removed: string[];
|
|
48
|
+
/** One `source -> destination` line per plugin-cache item moved into the archive. */
|
|
49
|
+
moved: string[];
|
|
55
50
|
/** Refused, or not confirmed. */
|
|
56
51
|
skipped: string[];
|
|
57
52
|
/** Non-fatal failures (an npm uninstall that exited non-zero). */
|
|
@@ -63,6 +58,8 @@ export interface CleanupDeps {
|
|
|
63
58
|
memory?: MemoryDiagnosis;
|
|
64
59
|
/** Names the agents/skills a project may duplicate. */
|
|
65
60
|
canonicalDir?: string;
|
|
61
|
+
/** The run's dated archive, shared with the hard plan (default: today's under the home dir). */
|
|
62
|
+
archiveRoot?: string;
|
|
66
63
|
}
|
|
67
64
|
/** Sums the size (bytes) of every file under a directory. */
|
|
68
65
|
export declare function dirBytes(dir: string): number;
|
|
@@ -72,12 +69,13 @@ export declare function humanBytes(n: number): string;
|
|
|
72
69
|
export declare function globalNpmPackages(): Record<string, string> | null;
|
|
73
70
|
/** The `<npm prefix -g>/bin/freeharness` shadowing ours on PATH (an old `npm link -g`); undefined when absent, identical to ours, or npm is unavailable. */
|
|
74
71
|
export declare function globalNpmFreeharnessBin(): string | undefined;
|
|
75
|
-
/**
|
|
76
|
-
|
|
72
|
+
/**
|
|
73
|
+
* Memory data backups written by `freeharness memory backup` (`<dataDir>-backup-<stamp>`), sorted.
|
|
74
|
+
* Backups of either default directory name count, whichever directory is in use now.
|
|
75
|
+
*/
|
|
76
|
+
export declare function memoryBackupDirs(env?: Record<string, string | undefined>, home?: string): string[];
|
|
77
77
|
/** Drops install-manifest entries whose file no longer exists (else every removal is "drift"); 0 without a manifest. */
|
|
78
78
|
export declare function pruneManifest(scope: Scope, projectRoot: string): number;
|
|
79
|
-
/** Retired skill/agent names still present under a `.claude` dir: `update`'s additive merge never deletes them; an absent path is skipped. */
|
|
80
|
-
export declare function retiredFindings(claudeDir: string, manifest: InstallManifest | undefined): RetiredFinding[];
|
|
81
79
|
/** Builds the cleanup plan for a scope without touching anything; `items` is empty when there is nothing to do. */
|
|
82
80
|
export declare function planCleanup(scope: Scope, projectRoot: string, deps?: CleanupDeps): Promise<CleanupPlan>;
|
|
83
81
|
/** Removes the confirmed items of a plan (`confirmed` defaults to all); refused items are always skipped. */
|
package/dist/cli/cleanup.js
CHANGED
|
@@ -2,30 +2,48 @@
|
|
|
2
2
|
// discovered by heuristics. User scope: parked upstream claude-mem + data backups, upstream
|
|
3
3
|
// claude-mem hooks in settings.json (once the plugin is registered), legacy /openspec commands,
|
|
4
4
|
// the global @fission-ai/openspec package, a global npm `freeharness` link, retired names
|
|
5
|
-
// (RETIRED) still on disk after a rename or a fold when the manifest records them
|
|
5
|
+
// (RETIRED in install-facts.ts) still on disk after a rename or a fold when the manifest records them,
|
|
6
|
+
// retired hook scripts and the settings entries that run them. Project
|
|
6
7
|
// scope: legacy /openspec commands, project copies of canonical agents/skills, duplicate
|
|
7
8
|
// FreeHarness hook entries. Never listed: the schema override, non-canonical skills/agents,
|
|
8
9
|
// `openspec-*` skills, `.claude/commands/opsx/` (the user's or ours).
|
|
9
10
|
//
|
|
10
11
|
// Two-pass: `planCleanup` computes the list untouched; `applyCleanup` removes the confirmed items.
|
|
12
|
+
// Items under the Claude Code plugin cache are never deleted: they move into the dated archive
|
|
13
|
+
// through the hard-cleanup mover.
|
|
11
14
|
import fs from "node:fs";
|
|
12
15
|
import os from "node:os";
|
|
13
16
|
import path from "node:path";
|
|
14
17
|
import { spawnSync } from "node:child_process";
|
|
15
|
-
import { PLUGIN_KEY,
|
|
18
|
+
import { PLUGIN_KEY, UPSTREAM_MARKETPLACE_OWNER, UPSTREAM_PLUGIN_KEY, UPSTREAM_PLUGIN_NAME } from "../adapters/memory/vendored.js";
|
|
16
19
|
import { manifestPath, readManifest } from "../infra/manifest.js";
|
|
17
|
-
import { configDir,
|
|
18
|
-
import {
|
|
20
|
+
import { configDir, diagnoseMemory } from "../adapters/memory/doctor.js";
|
|
21
|
+
import { FREE_DIR_NAME, LEGACY_DIR_NAME, resolveMemoryPaths } from "../adapters/memory/paths.js";
|
|
22
|
+
import { canonicalNames, memoryPluginInstalled, RETIRED, retiredFindings, runsRetiredHook } from "./install-facts.js";
|
|
19
23
|
import { isClaudeMemHook, isOurHook, pruneHooksMatching } from "./uninstall.js";
|
|
24
|
+
import { archiveMove, archiveRootFor, isOrHolds, moveWarning, pluginCacheArchive, pluginCacheOverlap, pluginCacheRefusal } from "./hard-cleanup.js";
|
|
25
|
+
import { removeJsonKey } from "./json-key-edit.js";
|
|
26
|
+
import { configCopyArchive, launcherStillMatches, launcherText, marketplaceRefusal, planUpstreamLaunchers } from "./upstream-launchers.js";
|
|
20
27
|
/** The vendored `openspec` bin replaces it. */
|
|
21
28
|
export const LEGACY_OPENSPEC_PACKAGE = "@fission-ai/openspec";
|
|
22
29
|
/** A global link of it shadows the ~/.local/bin symlink. */
|
|
23
30
|
export const CLI_PACKAGE = "freeharness";
|
|
24
|
-
/**
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
}
|
|
31
|
+
/** An entry mixing a retired script with another command is not one we emitted, so it stays. */
|
|
32
|
+
function isRetiredHook(e) {
|
|
33
|
+
const commands = (e.hooks ?? []).map((h) => h.command ?? "");
|
|
34
|
+
return commands.length > 0 && commands.every(runsRetiredHook);
|
|
35
|
+
}
|
|
36
|
+
function hookPredicate(owner) {
|
|
37
|
+
if (owner === UPSTREAM_PLUGIN_NAME)
|
|
38
|
+
return isClaudeMemHook;
|
|
39
|
+
return owner === "retired" ? isRetiredHook : isOurHook;
|
|
40
|
+
}
|
|
41
|
+
/** Upstream IDE rule files under the home dir, relative; fixed list. */
|
|
42
|
+
const IDE_RULE_FILES = [
|
|
43
|
+
path.join(".agent", "rules", `${UPSTREAM_PLUGIN_NAME}-context.md`),
|
|
44
|
+
path.join(".agents", "rules", `${UPSTREAM_PLUGIN_NAME}-context.md`),
|
|
45
|
+
path.join(".agent", "rules", "free-mem-context.md"),
|
|
46
|
+
];
|
|
29
47
|
/** Sums the size (bytes) of every file under a directory. */
|
|
30
48
|
export function dirBytes(dir) {
|
|
31
49
|
let total = 0;
|
|
@@ -93,18 +111,26 @@ export function globalNpmFreeharnessBin() {
|
|
|
93
111
|
}
|
|
94
112
|
return bin;
|
|
95
113
|
}
|
|
96
|
-
/**
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
.
|
|
114
|
+
/**
|
|
115
|
+
* Memory data backups written by `freeharness memory backup` (`<dataDir>-backup-<stamp>`), sorted.
|
|
116
|
+
* Backups of either default directory name count, whichever directory is in use now.
|
|
117
|
+
*/
|
|
118
|
+
export function memoryBackupDirs(env = process.env, home = os.homedir()) {
|
|
119
|
+
const live = resolveMemoryPaths(env, home).dataDir;
|
|
120
|
+
const dirs = [live, path.join(home, FREE_DIR_NAME), path.join(home, LEGACY_DIR_NAME)];
|
|
121
|
+
const found = new Set();
|
|
122
|
+
for (const data of dirs) {
|
|
123
|
+
const parent = path.dirname(data);
|
|
124
|
+
const prefix = `${path.basename(data)}-backup-`;
|
|
125
|
+
if (!fs.existsSync(parent))
|
|
126
|
+
continue;
|
|
127
|
+
for (const e of fs.readdirSync(parent, { withFileTypes: true })) {
|
|
128
|
+
const candidate = path.join(parent, e.name);
|
|
129
|
+
if (e.isDirectory() && e.name.startsWith(prefix) && !isOrHolds(candidate, live))
|
|
130
|
+
found.add(candidate);
|
|
131
|
+
}
|
|
132
|
+
}
|
|
133
|
+
return [...found].sort();
|
|
108
134
|
}
|
|
109
135
|
/** Plugin keys registered in Claude Code's installed_plugins.json (e.g. "name@marketplace"); empty when absent or unreadable. */
|
|
110
136
|
function installedPluginKeys(cfg) {
|
|
@@ -128,29 +154,16 @@ export function pruneManifest(scope, projectRoot) {
|
|
|
128
154
|
fs.writeFileSync(file, JSON.stringify({ ...m, files: kept, updatedAt: new Date().toISOString() }, null, 2) + "\n");
|
|
129
155
|
return dropped;
|
|
130
156
|
}
|
|
131
|
-
/**
|
|
132
|
-
|
|
133
|
-
const
|
|
134
|
-
|
|
135
|
-
{ kind: "agent", dir: "agents", names: RETIRED.agents },
|
|
136
|
-
];
|
|
137
|
-
const found = [];
|
|
138
|
-
for (const { kind, dir, names } of groups) {
|
|
139
|
-
for (const name of names) {
|
|
140
|
-
const fileName = kind === "agent" ? `${name}.md` : name;
|
|
141
|
-
const p = path.join(claudeDir, dir, fileName);
|
|
142
|
-
if (!fs.existsSync(p))
|
|
143
|
-
continue;
|
|
144
|
-
const under = `${p}${path.sep}`;
|
|
145
|
-
const recorded = manifest?.files.some((f) => f.path === p || f.path.startsWith(under)) ?? false;
|
|
146
|
-
found.push({ kind, name, path: p, recorded });
|
|
147
|
-
}
|
|
148
|
-
}
|
|
149
|
-
return found;
|
|
157
|
+
/** Routes a plugin-cache item into the dated archive; an earlier refusal wins over the containment one. */
|
|
158
|
+
function toArchive(item, cfg, archiveRoot) {
|
|
159
|
+
const archive = pluginCacheArchive(item.target, cfg, archiveRoot);
|
|
160
|
+
return { ...item, archiveDest: archive.destination, refused: item.refused ?? archive.refused };
|
|
150
161
|
}
|
|
162
|
+
const UPSTREAM_ENABLED_REFUSAL = `the ${UPSTREAM_PLUGIN_KEY} key is still enabled — disable it first: \`claude plugin disable ${UPSTREAM_PLUGIN_KEY}\``;
|
|
151
163
|
/** Computes the user-scope cleanup list; nothing is touched. */
|
|
152
164
|
async function planUser(deps) {
|
|
153
|
-
const
|
|
165
|
+
const archiveRoot = deps.archiveRoot ?? archiveRootFor(os.homedir(), new Date());
|
|
166
|
+
const plan = { scope: "user", items: [], notes: [], archiveRoot };
|
|
154
167
|
const cfg = configDir();
|
|
155
168
|
// `memory activate` parks foreign versions under the upstream root (doctor.ts's
|
|
156
169
|
// disabledRoot): an upstream install or a pre-rename FreeHarness build, never ours.
|
|
@@ -162,7 +175,7 @@ async function planUser(deps) {
|
|
|
162
175
|
const verdictLine = d.lines.find((l) => l.includes("VERDICT")) ?? `verdict ${d.verdict}`;
|
|
163
176
|
item.refused = `the FreeHarness build is not the only active memory plugin yet (${verdictLine.replace(/^(OK|WARN)\s+/, "")}); run \`freeharness memory activate\` first`;
|
|
164
177
|
}
|
|
165
|
-
plan.items.push(item);
|
|
178
|
+
plan.items.push(toArchive(item, cfg, archiveRoot));
|
|
166
179
|
}
|
|
167
180
|
// Superseded FreeHarness builds: our own cache versions (.install-version present) that are
|
|
168
181
|
// not the registered installPath, left behind by a version bump. A legacy-root build that
|
|
@@ -171,7 +184,7 @@ async function planUser(deps) {
|
|
|
171
184
|
const d = deps.memory ?? (await diagnoseMemory());
|
|
172
185
|
if (d.activeInstallPath) {
|
|
173
186
|
for (const c of d.cache.filter((c) => c.ours && !c.legacy && path.resolve(c.dir) !== path.resolve(d.activeInstallPath))) {
|
|
174
|
-
plan.items.push({ kind: "dir", target: c.dir, reason: `superseded FreeHarness memory build ${c.version} (registered: ${d.activeVersion ?? "?"})`, group: "memory", bytes: dirBytes(c.dir) });
|
|
187
|
+
plan.items.push(toArchive({ kind: "dir", target: c.dir, reason: `superseded FreeHarness memory build ${c.version} (registered: ${d.activeVersion ?? "?"})`, group: "memory", bytes: dirBytes(c.dir) }, cfg, archiveRoot));
|
|
175
188
|
}
|
|
176
189
|
}
|
|
177
190
|
}
|
|
@@ -184,7 +197,21 @@ async function planUser(deps) {
|
|
|
184
197
|
if (fs.existsSync(userSettings) && memoryPluginInstalled(cfg) && pruneHooksMatching(userSettings, isClaudeMemHook, true) > 0) {
|
|
185
198
|
plan.items.push({ kind: "hooks", target: userSettings, reason: `upstream claude-mem hook entries (the ${PLUGIN_KEY} plugin ships its own hooks.json; these run every hook twice)`, group: "memory", hookOwner: UPSTREAM_PLUGIN_NAME });
|
|
186
199
|
}
|
|
187
|
-
//
|
|
200
|
+
// Rule stubs the upstream Gemini/Antigravity installer wrote into IDE rule dirs; no engine
|
|
201
|
+
// updates them. Fixed list, no glob; a directory at the path is not ours to move.
|
|
202
|
+
for (const rel of IDE_RULE_FILES) {
|
|
203
|
+
const file = path.join(os.homedir(), rel);
|
|
204
|
+
try {
|
|
205
|
+
const st = fs.lstatSync(file);
|
|
206
|
+
if (!st.isFile() && !st.isSymbolicLink())
|
|
207
|
+
continue;
|
|
208
|
+
}
|
|
209
|
+
catch {
|
|
210
|
+
continue;
|
|
211
|
+
}
|
|
212
|
+
plan.items.push({ kind: "file", target: file, reason: "upstream IDE rule file (no engine updates it)", group: "memory", ideRule: true, archiveDest: path.join(archiveRoot, "ide-rules", rel) });
|
|
213
|
+
}
|
|
214
|
+
// Leftovers of the old key: the pre-rename cache dir (gated on its own
|
|
188
215
|
// existence, not the key's) and the disabled key (listed only while present). Both stay
|
|
189
216
|
// refused while the old key is enabled, or (same VERDICT check as above) while the build is
|
|
190
217
|
// not verified as sole active plugin — installed_plugins.json alone can register the
|
|
@@ -202,18 +229,23 @@ async function planUser(deps) {
|
|
|
202
229
|
}
|
|
203
230
|
let refused;
|
|
204
231
|
if (oldKeyValue === true) {
|
|
205
|
-
refused =
|
|
232
|
+
refused = UPSTREAM_ENABLED_REFUSAL;
|
|
206
233
|
}
|
|
207
234
|
else if (d.verdict !== "ours") {
|
|
208
235
|
const verdictLine = d.lines.find((l) => l.includes("VERDICT")) ?? `verdict ${d.verdict}`;
|
|
209
236
|
refused = `the FreeHarness build is not the only active memory plugin yet (${verdictLine.replace(/^(OK|WARN)\s+/, "")}); run \`freeharness memory activate\` first`;
|
|
210
237
|
}
|
|
211
238
|
if (fs.existsSync(oldCacheDir)) {
|
|
212
|
-
plan.items.push({ kind: "dir", target: oldCacheDir, reason:
|
|
239
|
+
plan.items.push(toArchive({ kind: "dir", target: oldCacheDir, reason: "previous registration, left in the plugin cache", group: "memory", bytes: dirBytes(oldCacheDir), refused }, cfg, archiveRoot));
|
|
213
240
|
}
|
|
214
241
|
if (oldKeyValue !== undefined) {
|
|
215
|
-
|
|
242
|
+
const copy = configCopyArchive(userSettings, os.homedir(), archiveRoot);
|
|
243
|
+
plan.items.push({ kind: "json-key", target: userSettings, reason: `disabled ${UPSTREAM_PLUGIN_KEY} key left in enabledPlugins`, group: "memory", keyPath: ["enabledPlugins", UPSTREAM_PLUGIN_KEY], archiveDest: copy.destination, refused: refused ?? copy.refused });
|
|
216
244
|
}
|
|
245
|
+
// An enabled upstream plugin still installs from its marketplace; other clients' launchers are not that plugin.
|
|
246
|
+
const launchers = planUpstreamLaunchers({ home: os.homedir(), cfg, archiveRoot, marketplaceRefusal: oldKeyValue === true ? UPSTREAM_ENABLED_REFUSAL : undefined });
|
|
247
|
+
plan.items.push(...launchers.items);
|
|
248
|
+
plan.notes.push(...launchers.notes);
|
|
217
249
|
}
|
|
218
250
|
const legacy = path.join(cfg, "commands", "openspec");
|
|
219
251
|
if (fs.existsSync(legacy)) {
|
|
@@ -250,20 +282,24 @@ async function planUser(deps) {
|
|
|
250
282
|
plan.items.push({ kind: "npm-global", target: CLI_PACKAGE, reason: `npm -g ${CLI_PACKAGE} (old npm link; shadows ~/.local/bin)${linkedBin ? ` at ${linkedBin}` : ""}`, group: "npm", unlink: true });
|
|
251
283
|
}
|
|
252
284
|
}
|
|
253
|
-
// Retired names: listed only when the manifest records the path, so a
|
|
254
|
-
// user-owned
|
|
285
|
+
// Retired names: a skill or agent is listed only when the manifest records the path, so a
|
|
286
|
+
// same-named user-owned one stays untouched (doctor reports that case as INFO, never here).
|
|
287
|
+
// A hook script lives in FreeHarness's own hooks dir, so its presence is enough.
|
|
255
288
|
const manifest = readManifest(manifestPath("user", ""));
|
|
256
289
|
for (const f of retiredFindings(path.join(os.homedir(), ".claude"), manifest)) {
|
|
257
|
-
if (!f.recorded)
|
|
290
|
+
if (!f.recorded && f.kind !== "hook")
|
|
258
291
|
continue;
|
|
259
292
|
plan.items.push({
|
|
260
293
|
kind: f.kind === "skill" ? "dir" : "file",
|
|
261
294
|
target: f.path,
|
|
262
|
-
reason: `retired ${f.kind} (renamed or folded elsewhere; kept on disk by update's additive manifest merge)`,
|
|
295
|
+
reason: f.kind === "hook" ? `retired hook ${f.name} (removed from FreeHarness; update never deletes the installed copy)` : `retired ${f.kind} (renamed or folded elsewhere; kept on disk by update's additive manifest merge)`,
|
|
263
296
|
group: "retired",
|
|
264
297
|
bytes: f.kind === "skill" ? dirBytes(f.path) : undefined,
|
|
265
298
|
});
|
|
266
299
|
}
|
|
300
|
+
if (fs.existsSync(userSettings) && pruneHooksMatching(userSettings, isRetiredHook, true) > 0) {
|
|
301
|
+
plan.items.push({ kind: "hooks", target: userSettings, reason: `settings hook entries running a retired hook script (${RETIRED.hooks.join(", ")})`, group: "retired", hookOwner: "retired" });
|
|
302
|
+
}
|
|
267
303
|
plan.notes.push(`kept: ${path.join(os.homedir(), ".local", "share", "openspec", "schemas", "spec-driven")} (the FreeHarness schema override)`);
|
|
268
304
|
return plan;
|
|
269
305
|
}
|
|
@@ -319,9 +355,61 @@ function planProject(projectRoot, deps) {
|
|
|
319
355
|
export async function planCleanup(scope, projectRoot, deps = {}) {
|
|
320
356
|
return scope === "user" ? planUser(deps) : planProject(projectRoot, deps);
|
|
321
357
|
}
|
|
358
|
+
function inInbox(dest, inbox) {
|
|
359
|
+
const rel = path.relative(inbox, dest);
|
|
360
|
+
return !(rel === "" || rel === ".." || rel.startsWith(`..${path.sep}`) || path.isAbsolute(rel));
|
|
361
|
+
}
|
|
362
|
+
/**
|
|
363
|
+
* The destination stays in the `ide-rules` inbox so no plan item lands over other archive content;
|
|
364
|
+
* a source that became a directory since pass 1 is no longer the stub it listed, and moving it would take what the user put there.
|
|
365
|
+
*/
|
|
366
|
+
function ideRuleRefusal(source, dest, archiveRoot) {
|
|
367
|
+
if (archiveRoot === undefined)
|
|
368
|
+
return "no archive root in the plan";
|
|
369
|
+
const inbox = path.join(archiveRoot, "ide-rules");
|
|
370
|
+
if (!inInbox(dest, inbox))
|
|
371
|
+
return `destination ${dest} is outside ${inbox}`;
|
|
372
|
+
try {
|
|
373
|
+
if (fs.lstatSync(source).isDirectory())
|
|
374
|
+
return "is a directory now";
|
|
375
|
+
}
|
|
376
|
+
catch {
|
|
377
|
+
// gone: the move reports it
|
|
378
|
+
}
|
|
379
|
+
return undefined;
|
|
380
|
+
}
|
|
381
|
+
/** A pre-edit copy outside the `configs` inbox could land over other archive content, so pass 2 refuses it. */
|
|
382
|
+
function jsonKeyRefusal(item, archiveRoot) {
|
|
383
|
+
if (archiveRoot === undefined)
|
|
384
|
+
return "no archive root in the plan";
|
|
385
|
+
if (!item.keyPath?.length || item.archiveDest === undefined)
|
|
386
|
+
return "no key path or copy destination in the plan";
|
|
387
|
+
const inbox = path.join(archiveRoot, "configs");
|
|
388
|
+
return inInbox(item.archiveDest, inbox) ? undefined : `destination ${item.archiveDest} is outside ${inbox}`;
|
|
389
|
+
}
|
|
390
|
+
/** Moving rather than deleting keeps a wrong match recoverable; each kind has its own pass-2 check, as what makes its move unsafe differs. */
|
|
391
|
+
function archiveItem(item, dest, cfg, archiveRoot, out) {
|
|
392
|
+
const refused = item.ideRule
|
|
393
|
+
? ideRuleRefusal(item.target, dest, archiveRoot)
|
|
394
|
+
: item.launcher === "marketplace"
|
|
395
|
+
? marketplaceRefusal(item.target, dest, archiveRoot)
|
|
396
|
+
: pluginCacheRefusal(item.target, dest, cfg, archiveRoot);
|
|
397
|
+
if (refused) {
|
|
398
|
+
out.skipped.push(item.target);
|
|
399
|
+
out.warnings.push(`${item.target}: refused (${refused})`);
|
|
400
|
+
return;
|
|
401
|
+
}
|
|
402
|
+
try {
|
|
403
|
+
out.moved.push(`${item.target} -> ${archiveMove(item.target, dest)}`);
|
|
404
|
+
}
|
|
405
|
+
catch (e) {
|
|
406
|
+
out.warnings.push(moveWarning(item.target, e));
|
|
407
|
+
}
|
|
408
|
+
}
|
|
322
409
|
/** Removes the confirmed items of a plan (`confirmed` defaults to all); refused items are always skipped. */
|
|
323
410
|
export function applyCleanup(plan, confirmed = () => true) {
|
|
324
|
-
const out = { removed: [], skipped: [], warnings: [] };
|
|
411
|
+
const out = { removed: [], moved: [], skipped: [], warnings: [] };
|
|
412
|
+
const cfg = configDir();
|
|
325
413
|
for (const item of plan.items) {
|
|
326
414
|
if (item.refused || !confirmed(item)) {
|
|
327
415
|
out.skipped.push(item.target);
|
|
@@ -329,14 +417,26 @@ export function applyCleanup(plan, confirmed = () => true) {
|
|
|
329
417
|
}
|
|
330
418
|
switch (item.kind) {
|
|
331
419
|
case "dir":
|
|
332
|
-
case "file":
|
|
420
|
+
case "file": {
|
|
421
|
+
if (item.archiveDest !== undefined) {
|
|
422
|
+
archiveItem(item, item.archiveDest, cfg, plan.archiveRoot, out);
|
|
423
|
+
break;
|
|
424
|
+
}
|
|
425
|
+
// Nothing in, at or above the plugin cache is ever deleted; a link to it is unlinked only.
|
|
426
|
+
const cached = pluginCacheOverlap(item.target, cfg);
|
|
427
|
+
if (cached) {
|
|
428
|
+
out.skipped.push(item.target);
|
|
429
|
+
out.warnings.push(`${item.target}: refused (${cached})`);
|
|
430
|
+
break;
|
|
431
|
+
}
|
|
333
432
|
fs.rmSync(item.target, { recursive: true, force: true });
|
|
334
433
|
out.removed.push(item.target);
|
|
335
434
|
break;
|
|
435
|
+
}
|
|
336
436
|
case "hooks": {
|
|
337
|
-
|
|
338
|
-
|
|
339
|
-
out.removed.push(`${item.target} (${
|
|
437
|
+
pruneHooksMatching(item.target, hookPredicate(item.hookOwner), false);
|
|
438
|
+
const whose = item.hookOwner === UPSTREAM_PLUGIN_NAME ? `upstream ${UPSTREAM_PLUGIN_NAME}` : item.hookOwner === "retired" ? "retired FreeHarness" : "FreeHarness";
|
|
439
|
+
out.removed.push(`${item.target} (${whose} hook entries)`);
|
|
340
440
|
break;
|
|
341
441
|
}
|
|
342
442
|
case "npm-global": {
|
|
@@ -358,12 +458,16 @@ export function applyCleanup(plan, confirmed = () => true) {
|
|
|
358
458
|
out.warnings.push(`npm ${verb} -g ${item.target} failed (exit ${r.status ?? "?"}): ${(r.stderr || r.stdout || "").trim().split("\n")[0]}`);
|
|
359
459
|
break;
|
|
360
460
|
}
|
|
361
|
-
case "
|
|
362
|
-
const
|
|
363
|
-
|
|
364
|
-
|
|
365
|
-
|
|
366
|
-
|
|
461
|
+
case "json-key": {
|
|
462
|
+
const refused = jsonKeyRefusal(item, plan.archiveRoot);
|
|
463
|
+
const r = refused ? { ok: false, reason: refused } : removeJsonKey(item.target, item.keyPath ?? [], item.archiveDest ?? "", { stillMatches: launcherStillMatches(item) });
|
|
464
|
+
if (r.ok) {
|
|
465
|
+
out.removed.push(`${item.target} (removed ${keyLabel(item)}; copy at ${r.copy})`);
|
|
466
|
+
}
|
|
467
|
+
else {
|
|
468
|
+
out.skipped.push(item.target);
|
|
469
|
+
out.warnings.push(`${item.target}: refused (${r.reason})`);
|
|
470
|
+
}
|
|
367
471
|
break;
|
|
368
472
|
}
|
|
369
473
|
}
|
|
@@ -373,10 +477,20 @@ export function applyCleanup(plan, confirmed = () => true) {
|
|
|
373
477
|
/** Renders a plan item for the preview / doctor output, one line, without the OK/WARN prefix. */
|
|
374
478
|
export function describeItem(item) {
|
|
375
479
|
const size = item.bytes !== undefined ? ` (${humanBytes(item.bytes)})` : "";
|
|
376
|
-
|
|
377
|
-
|
|
378
|
-
|
|
379
|
-
|
|
380
|
-
|
|
381
|
-
|
|
480
|
+
return `${itemAction(item)}${size} — ${item.reason}${item.refused ? ` — REFUSED: ${item.refused}` : ""}`;
|
|
481
|
+
}
|
|
482
|
+
/** Doctor prints these lines unescaped, and a launcher's key is a server name another program wrote. */
|
|
483
|
+
function keyLabel(item) {
|
|
484
|
+
return item.keyPath?.map(launcherText).join(".") ?? "?";
|
|
485
|
+
}
|
|
486
|
+
function itemAction(item) {
|
|
487
|
+
if (item.kind === "npm-global")
|
|
488
|
+
return `npm ${item.unlink ? "unlink" : "uninstall"} -g ${item.target}`;
|
|
489
|
+
if (item.kind === "json-key")
|
|
490
|
+
return `edit: ${item.target} (remove ${keyLabel(item)}; original to ${item.archiveDest ?? "?"})`;
|
|
491
|
+
if (item.kind === "hooks")
|
|
492
|
+
return `edit: ${item.target}`;
|
|
493
|
+
if (item.archiveDest !== undefined)
|
|
494
|
+
return `mv: ${item.target} -> ${item.archiveDest}`;
|
|
495
|
+
return `rm: ${item.target}`;
|
|
382
496
|
}
|