great-cto 3.40.1 → 3.42.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.
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "great-cto",
3
3
  "description": "You already have the agent. This is everything around it. great_cto runs Claude Code as a pipeline of 71 specialist agents \u2014 an independent model checks each stage before the next builds on it, spending caps refuse rather than warn, and three decisions stay yours: what gets built, how, and whether it ships.",
4
- "version": "3.40.1",
4
+ "version": "3.42.0",
5
5
  "author": {
6
6
  "name": "Alexander Velikiy",
7
7
  "url": "https://hashnode.com/@Greatcto"
@@ -162,6 +162,12 @@
162
162
  "command": "_gd=\"$PWD\"; while [ -n \"$_gd\" ] && [ ! -f \"$_gd/.great_cto/PROJECT.md\" ]; do _gd=\"${_gd%/*}\"; done; [ -n \"$_gd\" ] && cd \"$_gd\"; PLUGIN_DIR=${CLAUDE_PLUGIN_ROOT:-$(ls -d ~/.claude/plugins/cache/*/great_cto/*/ 2>/dev/null | awk -F'/plugins/cache/' '{split($NF,p,\"/\"); print p[3], $0}' | sort -V | tail -1 | cut -d' ' -f2- | sed 's|/$||')}; node \"${PLUGIN_DIR}/scripts/hooks/destructive-guard.mjs\"; EXIT=$?; if [ $EXIT -eq 2 ]; then exit 2; fi; exit 0",
163
163
  "timeout": 5,
164
164
  "statusMessage": "Destructive-command check..."
165
+ },
166
+ {
167
+ "type": "command",
168
+ "command": "_gd=\"$PWD\"; while [ -n \"$_gd\" ] && [ ! -f \"$_gd/.great_cto/PROJECT.md\" ]; do _gd=\"${_gd%/*}\"; done; [ -n \"$_gd\" ] && cd \"$_gd\"; PLUGIN_DIR=${CLAUDE_PLUGIN_ROOT:-$(ls -d ~/.claude/plugins/cache/*/great_cto/*/ 2>/dev/null | awk -F'/plugins/cache/' '{split($NF,p,\"/\"); print p[3], $0}' | sort -V | tail -1 | cut -d' ' -f2- | sed 's|/$||')}; node \"${PLUGIN_DIR}/scripts/hooks/frozen-gates-guard.mjs\"; EXIT=$?; if [ $EXIT -eq 2 ]; then exit 2; fi; exit 0",
169
+ "timeout": 5,
170
+ "statusMessage": "Checking for writes to frozen gates..."
165
171
  }
166
172
  ]
167
173
  },
@@ -191,6 +197,12 @@
191
197
  "command": "_gd=\"$PWD\"; while [ -n \"$_gd\" ] && [ ! -f \"$_gd/.great_cto/PROJECT.md\" ]; do _gd=\"${_gd%/*}\"; done; [ -n \"$_gd\" ] && cd \"$_gd\"; PLUGIN_DIR=${CLAUDE_PLUGIN_ROOT:-$(ls -d ~/.claude/plugins/cache/*/great_cto/*/ 2>/dev/null | awk -F'/plugins/cache/' '{split($NF,p,\"/\"); print p[3], $0}' | sort -V | tail -1 | cut -d' ' -f2- | sed 's|/$||')}; node \"${PLUGIN_DIR}/scripts/hooks/edit-scope-guard.mjs\" 2>&1; EXIT=$?; if [ $EXIT -eq 2 ]; then exit 2; fi; exit 0",
192
198
  "timeout": 5,
193
199
  "statusMessage": "Checking edit scope..."
200
+ },
201
+ {
202
+ "type": "command",
203
+ "command": "_gd=\"$PWD\"; while [ -n \"$_gd\" ] && [ ! -f \"$_gd/.great_cto/PROJECT.md\" ]; do _gd=\"${_gd%/*}\"; done; [ -n \"$_gd\" ] && cd \"$_gd\"; PLUGIN_DIR=${CLAUDE_PLUGIN_ROOT:-$(ls -d ~/.claude/plugins/cache/*/great_cto/*/ 2>/dev/null | awk -F'/plugins/cache/' '{split($NF,p,\"/\"); print p[3], $0}' | sort -V | tail -1 | cut -d' ' -f2- | sed 's|/$||')}; node \"${PLUGIN_DIR}/scripts/hooks/edit-impact.mjs\" 2>/dev/null; exit 0",
204
+ "timeout": 5,
205
+ "statusMessage": "Finding what depends on this file..."
194
206
  }
195
207
  ]
196
208
  }
package/dist/codex.js CHANGED
@@ -48,12 +48,13 @@ export function codexInstallPlan({ repoDir, codexOnPath }) {
48
48
  "skills — all of them, the same tree Claude Code reads",
49
49
  "the MCP server (great_cto), resolved from npm",
50
50
  "the controlled role pipeline — run `npx --yes great-cto codex-host doctor` then `npx --yes great-cto codex-host start`",
51
+ "six safety guards as Codex plugin hooks (destructive commands, gate bypasses, a neighbour session's work, secrets in files, frozen gates, weakened checks) — Codex asks you to review them once on the next interactive `codex` start; they do not run until you do",
51
52
  ],
52
- // Checked across every shipped Codex plugin: no manifest declares any of
53
- // these. Saying "installed" without saying this would promise a pipeline the
54
- // host cannot run.
53
+ // Codex runs plugin hooks (verified 2026-09-28, codex-cli 0.153.4) but has no
54
+ // plugin surface for commands or role agents. Saying "installed" without saying
55
+ // this would promise a pipeline the host cannot run.
55
56
  notSupported: [
56
- "native hooks — no plugin surface; ordinary Codex sessions do not inherit controller gates or secret-scan",
57
+ "the rest of the Claude Code hooks — pipeline dispatch, cost guard, write log, format and type checks — only the safety guards are ported",
57
58
  "slash commands — no plugin surface",
58
59
  "native role agents — Codex plugins carry interface metadata; controlled roles run only through `npx --yes great-cto codex-host`",
59
60
  ],
package/dist/main.js CHANGED
@@ -68,6 +68,8 @@ function parseArgs(argv) {
68
68
  noLlm: false,
69
69
  host: null,
70
70
  upgradeSelf: false,
71
+ purgeData: false,
72
+ projects: false,
71
73
  positional: [],
72
74
  };
73
75
  const rest = [];
@@ -133,6 +135,12 @@ function parseArgs(argv) {
133
135
  args.command = "report";
134
136
  else if (a === "upgrade")
135
137
  args.command = "upgrade";
138
+ else if (a === "uninstall")
139
+ args.command = "uninstall";
140
+ else if (a === "--purge-data")
141
+ args.purgeData = true;
142
+ else if (a === "--projects")
143
+ args.projects = true;
136
144
  else if (a === "--self")
137
145
  args.upgradeSelf = true;
138
146
  else if (a === "task") {
@@ -608,6 +616,109 @@ async function runBoardUninstallDaemon(args) {
608
616
  log(` ${green("✓")} board daemon removed. (A running board keeps running — stop it with a new \`board\` or reboot.)`);
609
617
  return 0;
610
618
  }
619
+ /**
620
+ * `great-cto uninstall` — plan by default, act on --yes. See uninstall.ts for what
621
+ * counts as an artefact, as user data and as host-owned; this function adds the two
622
+ * steps that need the platform: stopping the board and unloading its service.
623
+ */
624
+ async function runUninstall(args) {
625
+ const { planUninstall, applyUninstall } = await import("./uninstall.js");
626
+ const { spawnSync } = await import("node:child_process");
627
+ const home = homedir();
628
+ const spec = daemonSpec(process.platform, { nodePath: process.execPath, cliPath: cliEntryPath(), port: args.boardPort, home });
629
+ let psText = "";
630
+ try {
631
+ psText = execFileSync("ps", ["eww", "-A", "-o", "command="], { encoding: "utf8", stdio: ["ignore", "pipe", "ignore"], maxBuffer: 64 * 1024 * 1024 });
632
+ }
633
+ catch { /* no ps: nothing is known live */ }
634
+ // The nightly loop's agent is written by scripts/nightly-loop.sh (a maintainer tool), not by an install — but it is ours.
635
+ const nightly = process.platform === "darwin" ? join(home, "Library", "LaunchAgents", "systems.greatcto.nightly-loop.plist") : "";
636
+ const plan = planUninstall({ home, psText, daemonUnitPaths: [spec.unitPath, nightly].filter(Boolean), includeProjects: args.projects });
637
+ const short = (p) => p.replace(home, "~");
638
+ const group = (title, items) => {
639
+ if (!items.length)
640
+ return;
641
+ log(` ${bold(title)} ${dim(`(${items.length})`)}`);
642
+ for (const i of items.slice(0, 8))
643
+ log(` ${short(i)}`);
644
+ if (items.length > 8)
645
+ log(dim(` … and ${items.length - 8} more`));
646
+ };
647
+ log(`${bold("great-cto uninstall")} ${args.yes ? "" : dim("— plan only; nothing is changed without --yes")}`);
648
+ log("");
649
+ log(green("Removes") + dim(" — what an install wrote"));
650
+ group("plugin versions", plan.pluginVersions);
651
+ group("agents (marked great_cto-managed)", plan.agents);
652
+ group("commands (marked great_cto-managed)", plan.commands);
653
+ if (plan.settingsKeys.length)
654
+ log(` ${bold("~/.claude/settings.json")} enabledPlugins: ${plan.settingsKeys.join(", ")} ${dim("(backed up first)")}`);
655
+ if (plan.installedKeys.length)
656
+ log(` ${bold("installed_plugins.json")}: ${plan.installedKeys.join(", ")} ${dim("(backed up first)")}`);
657
+ group("background services", plan.daemonUnits);
658
+ log(` ${bold("board")} ${dim("— the running board process, if any, is stopped")}`);
659
+ const welcomed = plan.caches.filter((c) => /\/\.welcomed-/.test(c));
660
+ group("caches in ~/.great_cto (clones, registries, runtime files)", [
661
+ ...(welcomed.length ? [`~/.great_cto/.welcomed-* (${welcomed.length} markers)`] : []),
662
+ ...plan.caches.filter((c) => !welcomed.includes(c)),
663
+ ]);
664
+ group("pre-push hooks init wrote into projects", plan.projectHooks);
665
+ const nothing = !plan.pluginVersions.length && !plan.agents.length && !plan.commands.length && !plan.settingsKeys.length
666
+ && !plan.installedKeys.length && !plan.daemonUnits.length && !plan.caches.length && !plan.projectHooks.length;
667
+ if (nothing)
668
+ log(dim(" nothing installed by great_cto was found"));
669
+ log("");
670
+ log(yellow("Keeps") + dim(" — yours, or used by something else"));
671
+ if (plan.keptVersions.length)
672
+ for (const k of plan.keptVersions)
673
+ log(` ${short(k.dir)} ${dim(`— ${k.why}`)}`);
674
+ if (plan.userData.length) {
675
+ const named = ["lessons.md", "decisions.md", "verdicts", "global-patterns", "cost-history.log", "secrets.env"]
676
+ .filter((n) => plan.userData.some((d) => d.endsWith(`/${n}`)));
677
+ log(` ~/.great_cto ${dim(`— ${plan.userData.length} entries of your data${named.length ? `: ${named.join(", ")}` : ""}`)}`);
678
+ log(dim(` --purge-data moves it to ~/.great_cto.removed-<date> (reversible; nothing is deleted)`));
679
+ }
680
+ if (plan.projects.length)
681
+ log(` ${plan.projects.length} project(s)' .great_cto/ ${dim("— project records; delete per project if you want")}`);
682
+ if (!args.projects && plan.projects.length)
683
+ log(dim(` --projects also removes the pre-push hooks great_cto installed in them`));
684
+ if (plan.companions.length)
685
+ log(` ${plan.companions.join(", ")} ${dim("— companion plugins other tools may use; disable with `claude plugin disable <name>`")}`);
686
+ log("");
687
+ log(cyan("Yours to run") + dim(" — owned by the host, not edited from here"));
688
+ if (plan.settingsKeys.includes("great_cto@great-cto") || plan.marketplaces.length)
689
+ log(` claude plugin uninstall great_cto@great-cto`);
690
+ for (const m of plan.marketplaces)
691
+ log(` claude plugin marketplace remove ${m}`);
692
+ try {
693
+ execFileSync("codex", ["--version"], { stdio: "ignore" });
694
+ log(` codex plugin remove great-cto@great-cto ${dim("# if you installed it for Codex")}`);
695
+ }
696
+ catch { /* no codex */ }
697
+ log(` npm uninstall -g great-cto ${dim("# last: this command is running from it")}`);
698
+ if (!args.yes) {
699
+ log("");
700
+ log(`Nothing changed. Run ${bold("great-cto uninstall --yes")} to remove the list above.`);
701
+ return 0;
702
+ }
703
+ await killExistingBoard();
704
+ await killExistingBoard("console");
705
+ for (const cmd of spec.uninstallCmds)
706
+ spawnSync(cmd[0], cmd.slice(1), { stdio: "ignore" });
707
+ if (nightly && plan.daemonUnits.includes(nightly))
708
+ spawnSync("launchctl", ["unload", "-w", nightly], { stdio: "ignore" });
709
+ const r = applyUninstall(plan, { purgeData: args.purgeData });
710
+ log("");
711
+ success(`removed ${r.removed.length} item(s)`);
712
+ for (const b of r.backups)
713
+ log(dim(` backup: ${short(b)}`));
714
+ if (r.movedData)
715
+ log(` ~/.great_cto moved to ${short(r.movedData)} ${dim("— delete it yourself once you are sure")}`);
716
+ for (const f of r.failed)
717
+ warn(`could not remove ${short(f)}`);
718
+ if (plan.keptVersions.length)
719
+ warn(`${plan.keptVersions.length} plugin version(s) kept — a running session uses them; run uninstall again after closing it`);
720
+ return r.failed.length ? 1 : 0;
721
+ }
611
722
  function printHelp() {
612
723
  log(`${bold("great-cto")} — one-command install for the great_cto Claude Code plugin
613
724
 
@@ -624,6 +735,7 @@ ${bold("Usage:")}
624
735
  npx great-cto serve [--port 3142]
625
736
  npx great-cto upgrade [superpowers|beads] Re-clone companions to latest tag + re-apply overlays
626
737
  npx great-cto upgrade --self Upgrade the great-cto CLI itself, in place
738
+ npx great-cto uninstall [--yes] Show what great_cto installed; --yes removes it (your data stays)
627
739
  npx great-cto help
628
740
  npx great-cto version
629
741
 
@@ -660,6 +772,10 @@ ${bold("Upgrade:")}
660
772
  great-cto upgrade superpowers Upgrade superpowers only
661
773
  great-cto upgrade beads Upgrade beads only
662
774
  great-cto upgrade --self Upgrade the great-cto CLI itself (also: upgrade self)
775
+ great-cto uninstall Plan: what would be removed, what is kept (changes nothing)
776
+ great-cto uninstall --yes Remove plugin, agents, commands, board service, caches
777
+ --purge-data Also move ~/.great_cto aside (reversible, never deleted)
778
+ --projects Also remove the pre-push hooks init wrote into your projects
663
779
  ${dim("(Safe to run any time — idempotent if already on latest)")}
664
780
 
665
781
  ${bold("CI gate:")}
@@ -1526,6 +1642,15 @@ async function main() {
1526
1642
  await finish(2);
1527
1643
  }
1528
1644
  }
1645
+ if (args.command === "uninstall") {
1646
+ try {
1647
+ await finish(await runUninstall(args));
1648
+ }
1649
+ catch (e) {
1650
+ error(e.message);
1651
+ await finish(2);
1652
+ }
1653
+ }
1529
1654
  if (args.command === "upgrade") {
1530
1655
  try {
1531
1656
  const code = await runUpgrade(rawArgv, args);
@@ -0,0 +1,214 @@
1
+ // `great-cto uninstall` — remove what great_cto installed, keep what the user made.
2
+ //
3
+ // Plan first, act on --yes (ADR-009: expensive to undo means a human decision,
4
+ // and "delete things under ~" is the textbook case). The plan is a pure function of
5
+ // the filesystem so tests run it against a temporary HOME.
6
+ //
7
+ // Three classes, never mixed:
8
+ // artefacts — what an install wrote: plugin versions, managed agents/commands,
9
+ // plugin keys in settings, the board service, clones and caches.
10
+ // Removed on --yes.
11
+ // user data — ~/.great_cto lessons, decisions, verdicts, cost history, secrets.
12
+ // Kept. --purge-data MOVES the directory aside (reversible), it does
13
+ // not delete it: secrets.env may hold the only copy of a key.
14
+ // host-owned — marketplace registrations, the npm package running this code,
15
+ // companion plugins other tools may use. Listed with the command
16
+ // that removes them; not edited from here.
17
+ import { existsSync, readdirSync, readFileSync, writeFileSync, copyFileSync, renameSync, rmSync, statSync, rmdirSync } from "node:fs";
18
+ import { join, resolve } from "node:path";
19
+ import { execFileSync } from "node:child_process";
20
+ /** Appended to every agent and command great_cto installs (scripts/lib/sync-managed.mjs). */
21
+ export const MANAGED_MARKER = "<!-- great_cto-managed -->";
22
+ /** The first comment line of the pre-push hook `init` copies into a project. */
23
+ export const PRE_PUSH_HEADER = "# pre-push.sh — block pushes that contain private project name leaks";
24
+ export const PLUGIN_KEYS = ["great_cto@local", "great_cto@great-cto"];
25
+ const COMPANION_KEYS = ["superpowers@local", "beads@local"];
26
+ /** Re-creatable: git clones, registries, copies, update/quota caches, runtime files. */
27
+ const CACHE_ENTRIES = [
28
+ "catalog", "anthropic-skills", "personal-skills", "skills-registry.json",
29
+ "update-check.json", ".last-version-check", "quota-cache.json",
30
+ "SKILL.md", "ARCHETYPES.md", "env.sh",
31
+ "board.pid", "console.pid", "board.log", "board.err", "worker.lock", "leash-proxy.pid",
32
+ ];
33
+ const CACHE_PREFIXES = [".welcomed-"];
34
+ const readJson = (p) => {
35
+ try {
36
+ return JSON.parse(readFileSync(p, "utf8"));
37
+ }
38
+ catch {
39
+ return null;
40
+ }
41
+ };
42
+ const ls = (d) => { try {
43
+ return readdirSync(d);
44
+ }
45
+ catch {
46
+ return [];
47
+ } };
48
+ const isDir = (p) => { try {
49
+ return statSync(p).isDirectory();
50
+ }
51
+ catch {
52
+ return false;
53
+ } };
54
+ const norm = (p) => p.replace(/\/+$/, "");
55
+ /** Plugin roots named in process environments (`ps eww` output) — the same rule as prune-versions.mjs. */
56
+ export function liveRootsFromPs(text) {
57
+ const out = [];
58
+ for (const m of String(text ?? "").matchAll(/CLAUDE_PLUGIN_ROOT=(.+?)(?=\s+[A-Za-z_][A-Za-z0-9_]*=|\s*$)/gm)) {
59
+ const root = norm(m[1]);
60
+ if (!out.includes(root))
61
+ out.push(root);
62
+ }
63
+ return out;
64
+ }
65
+ /** Where git reads hooks from: core.hooksPath wins over .git/hooks. Null when not a repository. */
66
+ function gitHooksDir(projectDir) {
67
+ const git = (args) => {
68
+ try {
69
+ return execFileSync("git", args, { cwd: projectDir, encoding: "utf8", stdio: ["ignore", "pipe", "ignore"] }).trim();
70
+ }
71
+ catch {
72
+ return "";
73
+ }
74
+ };
75
+ const top = git(["rev-parse", "--show-toplevel"]);
76
+ if (!top)
77
+ return null;
78
+ const configured = git(["config", "--get", "core.hooksPath"]);
79
+ if (configured)
80
+ return resolve(top, configured);
81
+ const common = git(["rev-parse", "--git-common-dir"]);
82
+ return join(common ? resolve(top, common) : join(top, ".git"), "hooks");
83
+ }
84
+ export function planUninstall(o) {
85
+ const { home } = o;
86
+ const claude = join(home, ".claude");
87
+ const gdir = join(home, ".great_cto");
88
+ const live = liveRootsFromPs(o.psText ?? "");
89
+ const pluginVersions = [];
90
+ const keptVersions = [];
91
+ const cache = join(claude, "plugins", "cache");
92
+ for (const market of ls(cache)) {
93
+ const root = join(cache, market, "great_cto");
94
+ for (const v of ls(root)) {
95
+ const dir = join(root, v);
96
+ if (!isDir(dir))
97
+ continue;
98
+ if (live.some((r) => norm(r) === dir || norm(r).startsWith(dir + "/"))) {
99
+ keptVersions.push({ dir, why: "a running Claude Code session loads it — close that session and run uninstall again" });
100
+ }
101
+ else
102
+ pluginVersions.push(dir);
103
+ }
104
+ }
105
+ const marked = (d, pick) => ls(d).filter((f) => f.endsWith(".md") && pick(f)).map((f) => join(d, f))
106
+ .filter((p) => { try {
107
+ return readFileSync(p, "utf8").includes(MANAGED_MARKER);
108
+ }
109
+ catch {
110
+ return false;
111
+ } });
112
+ const agents = marked(join(claude, "agents"), () => true);
113
+ const commands = marked(join(claude, "commands"), () => true);
114
+ const settings = readJson(join(claude, "settings.json"));
115
+ const enabled = (settings?.enabledPlugins ?? {});
116
+ const settingsKeys = PLUGIN_KEYS.filter((k) => k in enabled);
117
+ const companions = COMPANION_KEYS.filter((k) => k in enabled);
118
+ const installed = readJson(join(claude, "plugins", "installed_plugins.json"));
119
+ const installedPlugins = (installed?.plugins ?? {});
120
+ const installedKeys = PLUGIN_KEYS.filter((k) => k in installedPlugins);
121
+ const known = readJson(join(claude, "plugins", "known_marketplaces.json")) ?? {};
122
+ const marketplaces = Object.keys(known).filter((k) => /great[-_]cto/i.test(k));
123
+ const daemonUnits = (o.daemonUnitPaths ?? []).filter((p) => p && existsSync(p));
124
+ const caches = [];
125
+ const userData = [];
126
+ for (const e of ls(gdir)) {
127
+ const isCache = CACHE_ENTRIES.includes(e) || CACHE_PREFIXES.some((p) => e.startsWith(p));
128
+ (isCache ? caches : userData).push(join(gdir, e));
129
+ }
130
+ const reg = readJson(join(gdir, "projects.json"));
131
+ const projects = Array.isArray(reg?.projects)
132
+ ? reg.projects.map((p) => p.path).filter((p) => typeof p === "string" && existsSync(p))
133
+ : [];
134
+ const projectHooks = [];
135
+ if (o.includeProjects) {
136
+ for (const p of projects) {
137
+ const dir = gitHooksDir(p);
138
+ const hook = dir ? join(dir, "pre-push") : "";
139
+ try {
140
+ if (hook && readFileSync(hook, "utf8").includes(PRE_PUSH_HEADER))
141
+ projectHooks.push(hook);
142
+ }
143
+ catch { /* none */ }
144
+ }
145
+ }
146
+ return { home, pluginVersions, keptVersions, agents, commands, settingsKeys, installedKeys, daemonUnits,
147
+ caches, projectHooks, userData, projects, companions, marketplaces };
148
+ }
149
+ /** Drop keys from one object inside a JSON file: backup, then write to a temp file and rename. */
150
+ function dropKeys(file, field, keys, stamp) {
151
+ if (!keys.length || !existsSync(file))
152
+ return null;
153
+ const data = readJson(file);
154
+ if (!data)
155
+ return null; // unparseable: not ours to rewrite
156
+ const obj = (data[field] ?? {});
157
+ for (const k of keys)
158
+ delete obj[k];
159
+ data[field] = obj;
160
+ const backup = `${file}.bak-${stamp}`;
161
+ copyFileSync(file, backup);
162
+ const tmp = `${file}.tmp-${stamp}`;
163
+ writeFileSync(tmp, JSON.stringify(data, null, 2) + "\n", "utf8");
164
+ renameSync(tmp, file);
165
+ return backup;
166
+ }
167
+ /** Carry out a plan. Board processes and service unloads are the caller's (they need the platform). */
168
+ export function applyUninstall(plan, o = {}) {
169
+ const stamp = o.stamp ?? new Date().toISOString().replace(/[:.]/g, "-");
170
+ const removed = [];
171
+ const failed = [];
172
+ const backups = [];
173
+ const rm = (p) => { try {
174
+ rmSync(p, { recursive: true, force: true });
175
+ removed.push(p);
176
+ }
177
+ catch {
178
+ failed.push(p);
179
+ } };
180
+ for (const p of [...plan.agents, ...plan.commands, ...plan.pluginVersions, ...plan.daemonUnits, ...plan.projectHooks])
181
+ rm(p);
182
+ // `<market>/great_cto/` and then `<market>/` itself, each only when emptied: a market
183
+ // that still holds a live version, or another plugin, is not empty.
184
+ for (const dir of new Set(plan.pluginVersions.map((v) => resolve(v, "..")))) {
185
+ for (const d of [dir, resolve(dir, "..")]) {
186
+ try {
187
+ if (ls(d).length === 0)
188
+ rmdirSync(d);
189
+ }
190
+ catch { /* keep */ }
191
+ }
192
+ }
193
+ const claude = join(plan.home, ".claude");
194
+ const b1 = dropKeys(join(claude, "settings.json"), "enabledPlugins", plan.settingsKeys, stamp);
195
+ const b2 = dropKeys(join(claude, "plugins", "installed_plugins.json"), "plugins", plan.installedKeys, stamp);
196
+ for (const b of [b1, b2])
197
+ if (b)
198
+ backups.push(b);
199
+ for (const p of plan.caches)
200
+ rm(p);
201
+ let movedData = null;
202
+ const gdir = join(plan.home, ".great_cto");
203
+ if (o.purgeData && existsSync(gdir)) {
204
+ const aside = `${gdir}.removed-${stamp}`;
205
+ try {
206
+ renameSync(gdir, aside);
207
+ movedData = aside;
208
+ }
209
+ catch {
210
+ failed.push(gdir);
211
+ }
212
+ }
213
+ return { removed, backups, movedData, failed };
214
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "great-cto",
3
- "version": "3.40.1",
3
+ "version": "3.42.0",
4
4
  "description": "One command install for the great_cto Claude Code plugin. Auto-detects your stack, picks the right archetype, bootstraps PROJECT.md.",
5
5
  "keywords": [
6
6
  "claude-code",