carrick 0.3.96 → 0.3.98
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 +42 -282
- package/bin/carrick.mjs +19 -14
- package/dist/auth/run.d.ts +1 -1
- package/dist/auth/run.js +1 -1
- package/dist/contract.d.ts +15 -0
- package/dist/contract.js.map +1 -1
- package/dist/global-install.js +6 -5
- package/dist/global-install.js.map +1 -1
- package/dist/hook/reuse.d.ts +8 -3
- package/dist/hook/reuse.js +15 -3
- package/dist/hook/reuse.js.map +1 -1
- package/dist/init/connect.js +1 -1
- package/dist/init/connect.js.map +1 -1
- package/dist/init/doctor.js +5 -2
- package/dist/init/doctor.js.map +1 -1
- package/dist/init/hosted.d.ts +18 -1
- package/dist/init/hosted.js +84 -16
- package/dist/init/hosted.js.map +1 -1
- package/dist/init/install-id.d.ts +2 -2
- package/dist/init/install-id.js +3 -3
- package/dist/init/install-id.js.map +1 -1
- package/dist/init/mcp.d.ts +2 -2
- package/dist/init/mcp.js +2 -2
- package/dist/init/mcp.js.map +1 -1
- package/dist/init/outdated.d.ts +1 -1
- package/dist/init/outdated.js +1 -1
- package/dist/init/outdated.js.map +1 -1
- package/dist/init/output.d.ts +18 -1
- package/dist/init/output.js +40 -1
- package/dist/init/output.js.map +1 -1
- package/dist/init/remove.d.ts +71 -2
- package/dist/init/remove.js +474 -145
- package/dist/init/remove.js.map +1 -1
- package/dist/init/repo-copies.d.ts +16 -4
- package/dist/init/repo-copies.js +20 -6
- package/dist/init/repo-copies.js.map +1 -1
- package/dist/init/run.d.ts +12 -16
- package/dist/init/run.js +25 -42
- package/dist/init/run.js.map +1 -1
- package/dist/update.js +1 -0
- package/dist/update.js.map +1 -1
- package/package.json +7 -7
- package/plugin/.claude-plugin/plugin.json +1 -1
- package/sidecar/dist/src/client-semantics.d.ts +36 -0
- package/sidecar/dist/src/client-semantics.js +1008 -0
- package/sidecar/dist/src/index.js +35 -0
- package/sidecar/dist/src/types.d.ts +83 -2
- package/sidecar/dist/src/validators.d.ts +463 -0
- package/sidecar/dist/src/validators.js +50 -0
package/dist/init/remove.js
CHANGED
|
@@ -1,16 +1,25 @@
|
|
|
1
|
-
// Undoing `carrick init`,
|
|
1
|
+
// Undoing `carrick init`, in two halves (carrick#1573).
|
|
2
2
|
//
|
|
3
|
-
//
|
|
4
|
-
//
|
|
5
|
-
//
|
|
6
|
-
//
|
|
7
|
-
//
|
|
8
|
-
//
|
|
9
|
-
//
|
|
10
|
-
//
|
|
11
|
-
// server entry in each agent client's own
|
|
12
|
-
//
|
|
13
|
-
// configuration directory.
|
|
3
|
+
// `carrick remove` undoes what init wrote in one folder: the hook entries in
|
|
4
|
+
// that folder's `.claude` settings and in `.codex/hooks.json`, the task skills
|
|
5
|
+
// under `.claude/skills` and `.agents/skills`, a copy of those hooks and skills
|
|
6
|
+
// inside each repo of a folder with the `.git/info/exclude` lines that keep it
|
|
7
|
+
// out of git (`writeRepoCopy` / `removeRepoCopy` in `repo-copies.ts`), the repo
|
|
8
|
+
// selection in `carrick-workspace.json`, and the `.carrick` directory.
|
|
9
|
+
//
|
|
10
|
+
// `carrick uninstall` undoes what init wrote on this machine, which every
|
|
11
|
+
// folder set up on it shares: an MCP server entry in each agent client's own
|
|
12
|
+
// configuration, the install id, the session records and the refresh notice in
|
|
13
|
+
// `~/.carrick`, and the credential in the user's configuration directory.
|
|
14
|
+
//
|
|
15
|
+
// They were one command, and running it to reset one folder disconnected the
|
|
16
|
+
// agent in every other folder on the machine and signed the machine out. Each
|
|
17
|
+
// now prints the whole list of what it will delete, as `init` does before "Go
|
|
18
|
+
// ahead?", and deletes nothing until it has a typed answer: the folder's name
|
|
19
|
+
// for `remove`, the word `uninstall` for `uninstall`, as the dashboard's
|
|
20
|
+
// project delete asks for the slug. Without a terminal the answer is
|
|
21
|
+
// `--confirm <answer>`, never `--yes`: to `init`, `--yes` means "take the
|
|
22
|
+
// proposal", and an agent adds it by reflex.
|
|
14
23
|
//
|
|
15
24
|
// Every step here is the inverse of a writer in this folder, and each pair
|
|
16
25
|
// lives in one file so the two cannot drift: `mergeCarrickHooks` /
|
|
@@ -19,9 +28,12 @@
|
|
|
19
28
|
// `removeServerEntry` and `connectMcpClients` / `disconnectMcpClients` in
|
|
20
29
|
// `mcp.ts`, `ensureInstallId` / `removeInstallId` in `install-id.ts`,
|
|
21
30
|
// `writeProposal` / the `.carrick` removal below, `saveCredential` /
|
|
22
|
-
// `removeCredential` in `../auth/credentials.ts`.
|
|
31
|
+
// `removeCredential` in `../auth/credentials.ts`. The list each command prints
|
|
32
|
+
// is made by the read half of the same pairs (`recordedRepoCopy`,
|
|
33
|
+
// `inspectTaskSkills`, `withoutOurExclusions`, `inspectMcpClients`), and the
|
|
34
|
+
// removal then acts on what the list named and nothing else.
|
|
23
35
|
//
|
|
24
|
-
// What
|
|
36
|
+
// What neither does is edit a file whose content it cannot reproduce. The
|
|
25
37
|
// scaffold pull request put files in the repository and a repository is
|
|
26
38
|
// version controlled: those are listed with the `git rm` line that removes
|
|
27
39
|
// them, and the sections that were merged into files someone else owns are
|
|
@@ -34,30 +46,37 @@
|
|
|
34
46
|
// nothing of anyone's is in it. One whose stamp no longer matches, and one
|
|
35
47
|
// carrying no stamp, are left where they are and named.
|
|
36
48
|
//
|
|
37
|
-
//
|
|
38
|
-
//
|
|
49
|
+
// Neither runs a scan or asks for the scanner binary: everything they touch is
|
|
50
|
+
// a file this package wrote.
|
|
39
51
|
import fs from "node:fs";
|
|
40
52
|
import path from "node:path";
|
|
41
53
|
import { removeCredential, credentialPath } from "../auth/credentials.js";
|
|
42
54
|
import { installIdPath, removeInstallId } from "./install-id.js";
|
|
43
|
-
import { removeSessions, sessionsDir } from "../hook/reuse.js";
|
|
44
|
-
import { disconnectMcpClients } from "./mcp.js";
|
|
55
|
+
import { removeSessions, sessionRecords, sessionsDir } from "../hook/reuse.js";
|
|
56
|
+
import { disconnectMcpClients, inspectMcpClients } from "./mcp.js";
|
|
45
57
|
import { repoRoots } from "./repos.js";
|
|
46
58
|
import { removeCarrickHooks, SETTINGS_FILES } from "./settings.js";
|
|
47
|
-
import { CODEX_HOOKS_FILE, uninstallCodexHooks } from "./codex.js";
|
|
48
|
-
import { removeTaskSkills, SKILL_ROOTS } from "./task-skills.js";
|
|
49
|
-
import { removeNotice } from "./outdated.js";
|
|
50
|
-
import { removeSelection, WORKSPACE_FILE } from "./workspace-file.js";
|
|
51
|
-
import { removeRepoCopy } from "./repo-copies.js";
|
|
52
|
-
import { createOutput, DOCS_INIT_FILES } from "./output.js";
|
|
53
|
-
|
|
54
|
-
|
|
59
|
+
import { CODEX_HOOKS_FILE, isEmptyHooksDocument, readHooksFile, uninstallCodexHooks } from "./codex.js";
|
|
60
|
+
import { inspectTaskSkills, removeTaskSkills, SKILL_ROOTS } from "./task-skills.js";
|
|
61
|
+
import { noticeFile, removeNotice } from "./outdated.js";
|
|
62
|
+
import { readWorkspaceFile, removeSelection, withoutOurExclusions, WORKSPACE_FILE } from "./workspace-file.js";
|
|
63
|
+
import { recordedRepoCopy, removeRepoCopy } from "./repo-copies.js";
|
|
64
|
+
import { createOutput, DOCS_INIT_FILES, listBlock, PromptCancelled, tilde } from "./output.js";
|
|
65
|
+
/** The word `carrick uninstall` asks to have typed. */
|
|
66
|
+
export const UNINSTALL_ANSWER = "uninstall";
|
|
67
|
+
export function parseRemoveArgs(argv, cwd = process.cwd()) {
|
|
68
|
+
const options = { workspace: cwd, confirm: null };
|
|
55
69
|
for (let index = 0; index < argv.length; index += 1) {
|
|
56
70
|
const argument = argv[index];
|
|
57
71
|
switch (argument) {
|
|
58
|
-
case "--
|
|
59
|
-
|
|
72
|
+
case "--confirm": {
|
|
73
|
+
const value = argv[index + 1];
|
|
74
|
+
if (value === undefined)
|
|
75
|
+
return "--confirm needs this folder's name";
|
|
76
|
+
options.confirm = value;
|
|
77
|
+
index += 1;
|
|
60
78
|
break;
|
|
79
|
+
}
|
|
61
80
|
case "--workspace":
|
|
62
81
|
case "-w": {
|
|
63
82
|
const value = argv[index + 1];
|
|
@@ -69,7 +88,7 @@ export function parseArgs(argv, cwd = process.cwd()) {
|
|
|
69
88
|
}
|
|
70
89
|
case "--help":
|
|
71
90
|
case "-h":
|
|
72
|
-
return
|
|
91
|
+
return removeHelp();
|
|
73
92
|
default:
|
|
74
93
|
if (argument?.startsWith("-"))
|
|
75
94
|
return `unknown option for \`carrick remove\`: ${argument}`;
|
|
@@ -78,24 +97,74 @@ export function parseArgs(argv, cwd = process.cwd()) {
|
|
|
78
97
|
}
|
|
79
98
|
return options;
|
|
80
99
|
}
|
|
81
|
-
function
|
|
100
|
+
export function parseUninstallArgs(argv) {
|
|
101
|
+
const options = { keepLogin: false, confirm: null };
|
|
102
|
+
for (let index = 0; index < argv.length; index += 1) {
|
|
103
|
+
const argument = argv[index];
|
|
104
|
+
switch (argument) {
|
|
105
|
+
case "--keep-login":
|
|
106
|
+
options.keepLogin = true;
|
|
107
|
+
break;
|
|
108
|
+
case "--confirm": {
|
|
109
|
+
const value = argv[index + 1];
|
|
110
|
+
if (value === undefined)
|
|
111
|
+
return `--confirm needs the word ${UNINSTALL_ANSWER}`;
|
|
112
|
+
options.confirm = value;
|
|
113
|
+
index += 1;
|
|
114
|
+
break;
|
|
115
|
+
}
|
|
116
|
+
case "--help":
|
|
117
|
+
case "-h":
|
|
118
|
+
return uninstallHelp();
|
|
119
|
+
default:
|
|
120
|
+
if (argument?.startsWith("-"))
|
|
121
|
+
return `unknown option for \`carrick uninstall\`: ${argument}`;
|
|
122
|
+
return `\`carrick uninstall\` takes no directory: it acts on this machine. \`carrick remove ${argument}\` acts on one folder.`;
|
|
123
|
+
}
|
|
124
|
+
}
|
|
125
|
+
return options;
|
|
126
|
+
}
|
|
127
|
+
function removeHelp() {
|
|
82
128
|
return [
|
|
83
|
-
"carrick remove [DIRECTORY] [--
|
|
129
|
+
"carrick remove [DIRECTORY] [--confirm NAME]",
|
|
84
130
|
"",
|
|
85
|
-
"Undo what carrick init wrote
|
|
86
|
-
"
|
|
87
|
-
"skills it copied into each repo here with their .git/info/exclude lines,",
|
|
88
|
-
"
|
|
89
|
-
"
|
|
90
|
-
"
|
|
91
|
-
"
|
|
92
|
-
"
|
|
93
|
-
"
|
|
94
|
-
"
|
|
95
|
-
"
|
|
131
|
+
"Undo what carrick init wrote in this folder, and nowhere else: the Carrick",
|
|
132
|
+
"hook entries in its .claude settings and in .codex/hooks.json, the hooks and",
|
|
133
|
+
"skills it copied into each repo here with their .git/info/exclude lines, the",
|
|
134
|
+
"repo selection in carrick-workspace.json, and the .carrick directory.",
|
|
135
|
+
"Other hooks and the settings files themselves are left as they are, and so",
|
|
136
|
+
"is anything in carrick-workspace.json that init did not put there. The task",
|
|
137
|
+
"skills it wrote are removed where they still match the stamp it wrote them",
|
|
138
|
+
"with, and named where they do not. Files the scaffold added to the",
|
|
139
|
+
"repository are listed with the git rm line that removes them.",
|
|
140
|
+
"",
|
|
141
|
+
"It lists what it will delete, then asks for this folder's name. The MCP",
|
|
142
|
+
"server, the install id and the sign-in belong to this machine and stay;",
|
|
143
|
+
"carrick uninstall removes them.",
|
|
96
144
|
"",
|
|
97
145
|
" -w, --workspace DIR The folder init was run in (default: this one)",
|
|
146
|
+
" --confirm NAME This folder's name, as the answer to the question.",
|
|
147
|
+
" Required where there is no terminal to ask in",
|
|
148
|
+
"",
|
|
149
|
+
`What each of those things is, and what it does: ${DOCS_INIT_FILES}`,
|
|
150
|
+
].join("\n");
|
|
151
|
+
}
|
|
152
|
+
function uninstallHelp() {
|
|
153
|
+
return [
|
|
154
|
+
`carrick uninstall [--keep-login] [--confirm ${UNINSTALL_ANSWER}]`,
|
|
155
|
+
"",
|
|
156
|
+
"Undo what carrick init wrote on this machine, which every folder set up on",
|
|
157
|
+
"it shares: the carrick MCP server in each agent client's configuration, this",
|
|
158
|
+
"machine's install id, the session records and the refresh notice in",
|
|
159
|
+
"~/.carrick, and the saved credential. Other MCP servers are left as they",
|
|
160
|
+
"are. Each folder keeps its hooks and skills; carrick remove, run in that",
|
|
161
|
+
"folder, takes them out.",
|
|
162
|
+
"",
|
|
163
|
+
`It lists what it will delete, then asks you to type ${UNINSTALL_ANSWER}.`,
|
|
164
|
+
"",
|
|
98
165
|
" --keep-login Leave the saved credential; remove everything else",
|
|
166
|
+
` --confirm WORD ${UNINSTALL_ANSWER}, as the answer to the question. Required`,
|
|
167
|
+
" where there is no terminal to ask in",
|
|
99
168
|
"",
|
|
100
169
|
`What each of those things is, and what it does: ${DOCS_INIT_FILES}`,
|
|
101
170
|
].join("\n");
|
|
@@ -182,6 +251,121 @@ export function mcpRemovalLines(removals) {
|
|
|
182
251
|
}
|
|
183
252
|
return { done, warn };
|
|
184
253
|
}
|
|
254
|
+
/** A repo under the folder as the reader knows it: its path from the folder, or its own name. */
|
|
255
|
+
function repoName(workspace, repo) {
|
|
256
|
+
return path.relative(workspace, repo) || path.basename(repo);
|
|
257
|
+
}
|
|
258
|
+
/** The answer `carrick remove` asks for: the folder's own name. */
|
|
259
|
+
export function folderName(workspace) {
|
|
260
|
+
return path.basename(workspace) || workspace;
|
|
261
|
+
}
|
|
262
|
+
/** A word as it would be typed in a shell, quoted only where it has to be. */
|
|
263
|
+
function typed(word) {
|
|
264
|
+
return /^[\w.@%+=:,/-]+$/.test(word) ? word : `'${word.replaceAll("'", `'\\''`)}'`;
|
|
265
|
+
}
|
|
266
|
+
export function folderPlan(workspace) {
|
|
267
|
+
const plan = {
|
|
268
|
+
workspace,
|
|
269
|
+
copies: [],
|
|
270
|
+
settings: [],
|
|
271
|
+
codex: null,
|
|
272
|
+
skills: [],
|
|
273
|
+
selection: null,
|
|
274
|
+
carrick: false,
|
|
275
|
+
leaves: [],
|
|
276
|
+
};
|
|
277
|
+
// A copy is taken back whole, and first: this folder may itself be one of
|
|
278
|
+
// the repos, and its copy's files are then not listed a second time below
|
|
279
|
+
// (carrick#1512).
|
|
280
|
+
const covered = new Set();
|
|
281
|
+
for (const repo of repoRoots(workspace)) {
|
|
282
|
+
try {
|
|
283
|
+
const copy = recordedRepoCopy(repo);
|
|
284
|
+
if (copy === null)
|
|
285
|
+
continue;
|
|
286
|
+
plan.copies.push(repo);
|
|
287
|
+
for (const relative of copy.recorded)
|
|
288
|
+
covered.add(path.join(repo, relative));
|
|
289
|
+
}
|
|
290
|
+
catch (error) {
|
|
291
|
+
plan.leaves.push(`Could not read the .git/info/exclude of ${repoName(workspace, repo)}: ${error.message}. Its copy of Carrick's hooks and skills stays.`);
|
|
292
|
+
}
|
|
293
|
+
}
|
|
294
|
+
for (const relative of SETTINGS_FILES) {
|
|
295
|
+
const file = path.join(workspace, relative);
|
|
296
|
+
if (covered.has(file) || !fs.existsSync(file))
|
|
297
|
+
continue;
|
|
298
|
+
try {
|
|
299
|
+
if (removeCarrickHooks(fs.readFileSync(file, "utf8")).changed)
|
|
300
|
+
plan.settings.push(relative);
|
|
301
|
+
}
|
|
302
|
+
catch (error) {
|
|
303
|
+
plan.leaves.push(`Could not read ${relative}: ${error.message}. Remove the carrick hook entries there by hand.`);
|
|
304
|
+
}
|
|
305
|
+
}
|
|
306
|
+
const codex = covered.has(path.join(workspace, CODEX_HOOKS_FILE)) ? null : readHooksFile(workspace);
|
|
307
|
+
if (codex !== null) {
|
|
308
|
+
try {
|
|
309
|
+
const cleaned = removeCarrickHooks(codex);
|
|
310
|
+
if (cleaned.changed)
|
|
311
|
+
plan.codex = isEmptyHooksDocument(cleaned.body) ? "file" : "entries";
|
|
312
|
+
}
|
|
313
|
+
catch (error) {
|
|
314
|
+
plan.leaves.push(`Could not read ${CODEX_HOOKS_FILE}: ${error.message}. Remove the carrick hook entries there by hand.`);
|
|
315
|
+
}
|
|
316
|
+
}
|
|
317
|
+
// Only the stamped, unchanged copies. Anything a user has made their own is
|
|
318
|
+
// named rather than deleted, which is the same rule the install follows.
|
|
319
|
+
for (const skill of inspectTaskSkills(workspace)) {
|
|
320
|
+
if (skill.state === "ours") {
|
|
321
|
+
if (!covered.has(path.join(workspace, skill.path)))
|
|
322
|
+
plan.skills.push(skill.path);
|
|
323
|
+
}
|
|
324
|
+
else if (skill.state === "edited") {
|
|
325
|
+
plan.leaves.push(`${skill.path} has been edited since Carrick wrote it, so it stays. Delete it by hand to finish removing it.`);
|
|
326
|
+
}
|
|
327
|
+
else if (skill.state === "theirs") {
|
|
328
|
+
plan.leaves.push(`${skill.path} was not written by Carrick, so it stays.`);
|
|
329
|
+
}
|
|
330
|
+
}
|
|
331
|
+
// The repo selection, which is the one thing init writes into a file a user
|
|
332
|
+
// can also write to (carrick#1344). Only the names init put in the exclude
|
|
333
|
+
// list come out; a repo somebody excluded by hand stays excluded, and the
|
|
334
|
+
// file stays unless init is the whole reason it is there.
|
|
335
|
+
const selection = readWorkspaceFile(workspace);
|
|
336
|
+
if (selection !== null) {
|
|
337
|
+
try {
|
|
338
|
+
const cleaned = withoutOurExclusions(selection);
|
|
339
|
+
if (cleaned.removed.length > 0)
|
|
340
|
+
plan.selection = { names: cleaned.removed, file: cleaned.empty };
|
|
341
|
+
}
|
|
342
|
+
catch (error) {
|
|
343
|
+
plan.leaves.push(`Could not read ${WORKSPACE_FILE}: ${error.message}. Take the carrick exclusions out by hand.`);
|
|
344
|
+
}
|
|
345
|
+
}
|
|
346
|
+
plan.carrick = fs.existsSync(path.join(workspace, ".carrick"));
|
|
347
|
+
return plan;
|
|
348
|
+
}
|
|
349
|
+
/** The list `carrick remove` prints before it asks: one line per thing it deletes. */
|
|
350
|
+
export function folderLines(plan) {
|
|
351
|
+
const lines = plan.copies.map((repo) => `Carrick's hooks and skills in ${repoName(plan.workspace, repo)}, with its .git/info/exclude lines`);
|
|
352
|
+
for (const relative of plan.settings)
|
|
353
|
+
lines.push(`Carrick's hook entries in ${relative}`);
|
|
354
|
+
if (plan.codex === "file")
|
|
355
|
+
lines.push(`${CODEX_HOOKS_FILE}, which holds only Carrick's hook entries`);
|
|
356
|
+
if (plan.codex === "entries")
|
|
357
|
+
lines.push(`Carrick's hook entries in ${CODEX_HOOKS_FILE}`);
|
|
358
|
+
if (plan.skills.length > 0)
|
|
359
|
+
lines.push(`${plan.skills.length} task skill file(s) in ${SKILL_ROOTS.join(" and ")}`);
|
|
360
|
+
if (plan.selection !== null) {
|
|
361
|
+
lines.push(plan.selection.file
|
|
362
|
+
? `${WORKSPACE_FILE}, which holds only the repo selection init wrote`
|
|
363
|
+
: `${plan.selection.names.join(", ")} from the exclude list in ${WORKSPACE_FILE}`);
|
|
364
|
+
}
|
|
365
|
+
if (plan.carrick)
|
|
366
|
+
lines.push(".carrick, with the proposal and the index in it");
|
|
367
|
+
return lines;
|
|
368
|
+
}
|
|
185
369
|
/** Strip our hook entries from one settings file. Null when there was nothing. */
|
|
186
370
|
function removeHooks(file) {
|
|
187
371
|
if (!fs.existsSync(file))
|
|
@@ -193,46 +377,30 @@ function removeHooks(file) {
|
|
|
193
377
|
fs.writeFileSync(file, cleaned.body);
|
|
194
378
|
return file;
|
|
195
379
|
}
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
}
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
// Each step reports its own failure and the run carries on: a settings file
|
|
208
|
-
// someone hand-edited into invalid JSON must not leave the credential and
|
|
209
|
-
// the MCP entries behind.
|
|
210
|
-
let removed = 0;
|
|
211
|
-
// The copies init put inside each repo of a folder (carrick#1512): exactly
|
|
212
|
-
// the files its lines in that repo's `.git/info/exclude` name, and then the
|
|
213
|
-
// lines. A repo with no such lines is one init never copied into. First,
|
|
214
|
-
// because this folder may itself be one of those repos: the cleanup below
|
|
215
|
-
// would empty its copy's settings file and leave it, and then take away the
|
|
216
|
-
// line that kept it out of git.
|
|
217
|
-
for (const repo of repoRoots(workspace)) {
|
|
218
|
-
const name = path.relative(workspace, repo) || path.basename(repo);
|
|
380
|
+
/**
|
|
381
|
+
* Delete what the list named. Each step reports its own failure and the run
|
|
382
|
+
* carries on: a file that cannot be written must not leave the rest behind.
|
|
383
|
+
*/
|
|
384
|
+
function removeFolder(plan, out) {
|
|
385
|
+
const { workspace } = plan;
|
|
386
|
+
// The copies first, for the reason `folderPlan` gives: the cleanup below
|
|
387
|
+
// would otherwise empty a copy's settings file and leave it, and then the
|
|
388
|
+
// copy's removal would take away the line that kept it out of git.
|
|
389
|
+
for (const repo of plan.copies) {
|
|
390
|
+
const name = repoName(workspace, repo);
|
|
219
391
|
try {
|
|
220
392
|
if (removeRepoCopy(repo) !== null) {
|
|
221
393
|
out.done(`Carrick's hooks and skills removed from ${name}, with its .git/info/exclude lines`);
|
|
222
|
-
removed += 1;
|
|
223
394
|
}
|
|
224
395
|
}
|
|
225
396
|
catch (error) {
|
|
226
397
|
out.refuse(`Could not remove Carrick's hooks and skills from ${name}: ${error.message}`);
|
|
227
398
|
}
|
|
228
399
|
}
|
|
229
|
-
for (const relative of
|
|
230
|
-
const file = path.join(workspace, relative);
|
|
400
|
+
for (const relative of plan.settings) {
|
|
231
401
|
try {
|
|
232
|
-
if (removeHooks(
|
|
402
|
+
if (removeHooks(path.join(workspace, relative)) !== null)
|
|
233
403
|
out.done(`Carrick hook entries removed from ${relative}`);
|
|
234
|
-
removed += 1;
|
|
235
|
-
}
|
|
236
404
|
}
|
|
237
405
|
catch (error) {
|
|
238
406
|
out.refuse(`Could not read ${relative}: ${error.message}. Remove the carrick hook entries there by hand.`);
|
|
@@ -241,110 +409,232 @@ export async function remove(argv, out = createOutput()) {
|
|
|
241
409
|
// Codex's half of the same pair. Unlike a settings file, this one exists
|
|
242
410
|
// because init wrote it, so a file left holding no hooks at all goes with the
|
|
243
411
|
// entries (carrick#1335).
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
412
|
+
if (plan.codex !== null) {
|
|
413
|
+
try {
|
|
414
|
+
const codex = uninstallCodexHooks(workspace);
|
|
415
|
+
if (codex !== null) {
|
|
416
|
+
out.done(codex === "file removed"
|
|
417
|
+
? `${CODEX_HOOKS_FILE} removed, with the Carrick hook entries that were the whole of it`
|
|
418
|
+
: `Carrick hook entries removed from ${CODEX_HOOKS_FILE}`);
|
|
419
|
+
}
|
|
420
|
+
}
|
|
421
|
+
catch (error) {
|
|
422
|
+
out.refuse(`Could not read ${CODEX_HOOKS_FILE}: ${error.message}. Remove the carrick hook entries there by hand.`);
|
|
251
423
|
}
|
|
252
424
|
}
|
|
253
|
-
|
|
254
|
-
|
|
425
|
+
if (plan.skills.length > 0) {
|
|
426
|
+
try {
|
|
427
|
+
const skills = removeTaskSkills(workspace, (relative) => plan.skills.includes(relative));
|
|
428
|
+
if (skills.deleted.length > 0) {
|
|
429
|
+
out.done(`${skills.deleted.length} task skill file(s) removed from ${SKILL_ROOTS.join(" and ")}`);
|
|
430
|
+
}
|
|
431
|
+
}
|
|
432
|
+
catch (error) {
|
|
433
|
+
out.refuse(`Could not remove the task skills: ${error.message}`);
|
|
434
|
+
}
|
|
435
|
+
}
|
|
436
|
+
if (plan.selection !== null) {
|
|
437
|
+
try {
|
|
438
|
+
const selection = removeSelection(workspace);
|
|
439
|
+
if (selection !== null) {
|
|
440
|
+
out.done(selection.deleted
|
|
441
|
+
? `${selection.file} removed, with the repo selection init wrote in it`
|
|
442
|
+
: `${selection.removed.join(", ")} taken out of the exclude list in ${selection.file}; the rest of the file is yours and was left`);
|
|
443
|
+
}
|
|
444
|
+
}
|
|
445
|
+
catch (error) {
|
|
446
|
+
out.refuse(`Could not read ${WORKSPACE_FILE}: ${error.message}. Take the carrick exclusions out by hand.`);
|
|
447
|
+
}
|
|
255
448
|
}
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
449
|
+
if (plan.carrick) {
|
|
450
|
+
const carrick = path.join(workspace, ".carrick");
|
|
451
|
+
try {
|
|
452
|
+
fs.rmSync(carrick, { recursive: true, force: true });
|
|
453
|
+
out.done(".carrick removed, with the proposal and the index in it");
|
|
454
|
+
}
|
|
455
|
+
catch (error) {
|
|
456
|
+
out.refuse(`Could not remove ${carrick}: ${error.message}`);
|
|
457
|
+
}
|
|
458
|
+
}
|
|
459
|
+
}
|
|
460
|
+
export function machinePlan(keepLogin) {
|
|
461
|
+
const plan = {
|
|
462
|
+
mcp: [],
|
|
463
|
+
sessions: sessionRecords(),
|
|
464
|
+
notice: fs.existsSync(noticeFile()),
|
|
465
|
+
installId: fs.existsSync(installIdPath()),
|
|
466
|
+
credential: null,
|
|
467
|
+
leaves: [],
|
|
468
|
+
};
|
|
469
|
+
// Gated on the URL, never the name: a server called `carrick` pointing at
|
|
470
|
+
// another host was not written by this package, and stays.
|
|
471
|
+
for (const client of inspectMcpClients()) {
|
|
472
|
+
if (client.state === "connected")
|
|
473
|
+
plan.mcp.push(client.client);
|
|
474
|
+
else if (client.state === "elsewhere")
|
|
475
|
+
plan.leaves.push(`${client.client}: ${client.detail}, so it stays.`);
|
|
476
|
+
else if (client.state === "unreadable")
|
|
477
|
+
plan.leaves.push(`${client.client}: ${client.detail}. Remove "carrick" there by hand.`);
|
|
478
|
+
}
|
|
479
|
+
if (!keepLogin) {
|
|
480
|
+
try {
|
|
481
|
+
const file = credentialPath();
|
|
482
|
+
if (fs.existsSync(file))
|
|
483
|
+
plan.credential = file;
|
|
484
|
+
}
|
|
485
|
+
catch (error) {
|
|
486
|
+
plan.leaves.push(error.message);
|
|
487
|
+
}
|
|
488
|
+
if (process.env["CARRICK_TOKEN"] !== undefined) {
|
|
489
|
+
plan.leaves.push("CARRICK_TOKEN still signs this shell in; unset it to finish signing out.");
|
|
490
|
+
}
|
|
491
|
+
}
|
|
492
|
+
return plan;
|
|
493
|
+
}
|
|
494
|
+
/** The list `carrick uninstall` prints before it asks. */
|
|
495
|
+
export function machineLines(plan) {
|
|
496
|
+
const lines = plan.mcp.map((client) => `Carrick's MCP server in ${client}`);
|
|
497
|
+
if (plan.sessions > 0)
|
|
498
|
+
lines.push(`${plan.sessions} session record(s) in ${tilde(sessionsDir())}`);
|
|
499
|
+
if (plan.notice)
|
|
500
|
+
lines.push(`The refresh notice's last-shown date, in ${tilde(noticeFile())}`);
|
|
501
|
+
if (plan.installId)
|
|
502
|
+
lines.push(`This machine's install id, in ${tilde(installIdPath())}`);
|
|
503
|
+
if (plan.credential !== null) {
|
|
504
|
+
lines.push(`The saved credential, in ${tilde(plan.credential)}, which signs this machine out`);
|
|
505
|
+
}
|
|
506
|
+
return lines;
|
|
507
|
+
}
|
|
508
|
+
function removeMachine(plan, out) {
|
|
509
|
+
if (plan.mcp.length > 0) {
|
|
510
|
+
// A `kept` entry was named above the question already.
|
|
511
|
+
const { done, warn } = mcpRemovalLines(disconnectMcpClients().filter((removal) => removal.state !== "kept"));
|
|
512
|
+
for (const line of done)
|
|
513
|
+
out.done(line);
|
|
514
|
+
for (const line of warn)
|
|
515
|
+
out.warn(line);
|
|
260
516
|
}
|
|
261
|
-
for (const line of warn)
|
|
262
|
-
out.warn(line);
|
|
263
517
|
// Before the install id, so the directory both live in can go with it: what
|
|
264
518
|
// the Stop hook speaks from is a scratch record of conversations that have
|
|
265
519
|
// ended, and it is no more use than the hook entry that fed it
|
|
266
520
|
// (carrick#1330).
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
521
|
+
if (plan.sessions > 0) {
|
|
522
|
+
const sessions = removeSessions();
|
|
523
|
+
if (sessions > 0)
|
|
524
|
+
out.done(`${sessions} session record(s) removed from ${tilde(sessionsDir())}`);
|
|
271
525
|
}
|
|
272
526
|
// Same directory, same reason, and the same ordering it imposes: the day the
|
|
273
527
|
// refresh notice was last said means nothing once there is no install to
|
|
274
528
|
// refresh (carrick#1333).
|
|
275
|
-
removeNotice()
|
|
529
|
+
if (plan.notice && removeNotice())
|
|
530
|
+
out.done("The refresh notice's last-shown date removed");
|
|
276
531
|
// The install id goes with the entries that carried it: what the header
|
|
277
532
|
// named is this machine's setup, and the setup is what just came off it. A
|
|
278
533
|
// later `carrick init` mints a new one, which is the reset the file exists
|
|
279
534
|
// to give (carrick-cloud#890).
|
|
280
|
-
|
|
281
|
-
if (removeInstallId()) {
|
|
282
|
-
out.done("This machine's install id removed");
|
|
283
|
-
removed += 1;
|
|
284
|
-
}
|
|
285
|
-
}
|
|
286
|
-
catch (error) {
|
|
287
|
-
out.refuse(`${error.message}. Delete ${installIdPath()} by hand.`);
|
|
288
|
-
}
|
|
289
|
-
// Only the stamped, unchanged copies. Anything a user has made their own is
|
|
290
|
-
// named rather than deleted, which is the same rule the install follows.
|
|
291
|
-
const skills = removeTaskSkills(workspace);
|
|
292
|
-
if (skills.deleted.length > 0) {
|
|
293
|
-
out.done(`${skills.deleted.length} task skill file(s) removed from ${SKILL_ROOTS.join(" and ")}`);
|
|
294
|
-
removed += 1;
|
|
295
|
-
}
|
|
296
|
-
for (const row of skills.kept) {
|
|
297
|
-
out.warn(row.state === "edited"
|
|
298
|
-
? `${row.path} has been edited since Carrick wrote it, so it was left in place. Delete it by hand to finish removing it.`
|
|
299
|
-
: `${row.path} was not written by Carrick, so it was left in place.`);
|
|
300
|
-
}
|
|
301
|
-
// The repo selection, which is the one thing init writes into a file a user
|
|
302
|
-
// can also write to (carrick#1344). Only the names init put in the exclude
|
|
303
|
-
// list come out; a repo somebody excluded by hand stays excluded, and the
|
|
304
|
-
// file stays unless init is the whole reason it is there.
|
|
305
|
-
try {
|
|
306
|
-
const selection = removeSelection(workspace);
|
|
307
|
-
if (selection !== null) {
|
|
308
|
-
out.done(selection.deleted
|
|
309
|
-
? `${selection.file} removed, with the repo selection init wrote in it`
|
|
310
|
-
: `${selection.removed.join(", ")} taken out of the exclude list in ${selection.file}; the rest of the file is yours and was left`);
|
|
311
|
-
removed += 1;
|
|
312
|
-
}
|
|
313
|
-
}
|
|
314
|
-
catch (error) {
|
|
315
|
-
out.refuse(`Could not read ${WORKSPACE_FILE}: ${error.message}. Take the carrick exclusions out by hand.`);
|
|
316
|
-
}
|
|
317
|
-
const carrick = path.join(workspace, ".carrick");
|
|
318
|
-
if (fs.existsSync(carrick)) {
|
|
535
|
+
if (plan.installId) {
|
|
319
536
|
try {
|
|
320
|
-
|
|
321
|
-
|
|
322
|
-
removed += 1;
|
|
537
|
+
if (removeInstallId())
|
|
538
|
+
out.done("This machine's install id removed");
|
|
323
539
|
}
|
|
324
540
|
catch (error) {
|
|
325
|
-
out.refuse(
|
|
541
|
+
out.refuse(`${error.message}. Delete ${installIdPath()} by hand.`);
|
|
326
542
|
}
|
|
327
543
|
}
|
|
328
|
-
if (
|
|
329
|
-
out.say(`This machine stays signed in; the credential is ${credentialPath()}.`);
|
|
330
|
-
}
|
|
331
|
-
else {
|
|
544
|
+
if (plan.credential !== null) {
|
|
332
545
|
try {
|
|
333
546
|
if (removeCredential()) {
|
|
334
547
|
out.done("Signed out: the saved credential is gone");
|
|
335
|
-
removed += 1;
|
|
336
548
|
out.say("Revoke the key itself at https://app.carrick.tools/account.");
|
|
337
549
|
}
|
|
338
|
-
if (process.env["CARRICK_TOKEN"] !== undefined) {
|
|
339
|
-
out.warn("CARRICK_TOKEN still signs this shell in; unset it to finish signing out.");
|
|
340
|
-
}
|
|
341
550
|
}
|
|
342
551
|
catch (error) {
|
|
343
552
|
out.refuse(error.message);
|
|
344
553
|
}
|
|
345
554
|
}
|
|
346
|
-
|
|
347
|
-
|
|
555
|
+
}
|
|
556
|
+
/** The line a run that was stopped at its question ends on. */
|
|
557
|
+
export const NOTHING_REMOVED = "Nothing was removed.";
|
|
558
|
+
/**
|
|
559
|
+
* Print the list, then take the typed answer (carrick#1573).
|
|
560
|
+
*
|
|
561
|
+
* Null to go ahead; otherwise the code the run ends with, having deleted
|
|
562
|
+
* nothing. The answer is compared exactly, case included, as the dashboard
|
|
563
|
+
* compares the slug a project delete is confirmed with. `--confirm` answers
|
|
564
|
+
* the question whether or not there is a terminal to ask it in.
|
|
565
|
+
*/
|
|
566
|
+
async function confirmed(ask) {
|
|
567
|
+
const { out } = ask;
|
|
568
|
+
out.say(listBlock(ask.heading, ask.deletes));
|
|
569
|
+
for (const line of ask.leaves)
|
|
570
|
+
out.warn(line);
|
|
571
|
+
let answer = ask.given;
|
|
572
|
+
if (answer === null) {
|
|
573
|
+
if (!ask.interactive) {
|
|
574
|
+
process.stderr.write(`carrick ${ask.command}: use --confirm ${typed(ask.expected)} to remove these without a terminal.\n`);
|
|
575
|
+
return 1;
|
|
576
|
+
}
|
|
577
|
+
try {
|
|
578
|
+
answer = await out.ask(ask.question);
|
|
579
|
+
}
|
|
580
|
+
catch (error) {
|
|
581
|
+
if (!(error instanceof PromptCancelled))
|
|
582
|
+
throw error;
|
|
583
|
+
out.refuse(`Cancelled. ${NOTHING_REMOVED}`);
|
|
584
|
+
return 1;
|
|
585
|
+
}
|
|
586
|
+
}
|
|
587
|
+
if (answer !== ask.expected) {
|
|
588
|
+
out.refuse(`"${answer}" is not ${ask.expected}. ${NOTHING_REMOVED}`);
|
|
589
|
+
return 1;
|
|
590
|
+
}
|
|
591
|
+
return null;
|
|
592
|
+
}
|
|
593
|
+
/** A usage answer: the help text exits 0, anything else it had to say exits 2. */
|
|
594
|
+
function usage(text, help) {
|
|
595
|
+
process.stdout.write(`${text}\n`);
|
|
596
|
+
return text.startsWith(help) ? 0 : 2;
|
|
597
|
+
}
|
|
598
|
+
export async function remove(argv) {
|
|
599
|
+
return removeWith(argv, createOutput(), process.stdin.isTTY === true);
|
|
600
|
+
}
|
|
601
|
+
/**
|
|
602
|
+
* `remove` with the terminal stated rather than sniffed, as `initWith` does,
|
|
603
|
+
* so a test can be the terminal that answers the question.
|
|
604
|
+
*/
|
|
605
|
+
export async function removeWith(argv, out, interactive) {
|
|
606
|
+
const parsed = parseRemoveArgs(argv);
|
|
607
|
+
if (typeof parsed === "string")
|
|
608
|
+
return usage(parsed, "carrick remove");
|
|
609
|
+
const { workspace } = parsed;
|
|
610
|
+
if (!fs.existsSync(workspace)) {
|
|
611
|
+
process.stderr.write(`carrick remove: ${workspace} is not a directory on this machine\n`);
|
|
612
|
+
return 1;
|
|
613
|
+
}
|
|
614
|
+
const plan = folderPlan(workspace);
|
|
615
|
+
const deletes = folderLines(plan);
|
|
616
|
+
if (deletes.length === 0) {
|
|
617
|
+
for (const line of plan.leaves)
|
|
618
|
+
out.warn(line);
|
|
619
|
+
out.say("Nothing left to remove in this folder.");
|
|
620
|
+
}
|
|
621
|
+
else {
|
|
622
|
+
const name = folderName(workspace);
|
|
623
|
+
const stop = await confirmed({
|
|
624
|
+
command: "remove",
|
|
625
|
+
heading: `Remove from ${tilde(workspace)}:`,
|
|
626
|
+
deletes,
|
|
627
|
+
leaves: plan.leaves,
|
|
628
|
+
expected: name,
|
|
629
|
+
question: `Type this folder's name, ${name}, to remove these`,
|
|
630
|
+
given: parsed.confirm,
|
|
631
|
+
interactive,
|
|
632
|
+
out,
|
|
633
|
+
});
|
|
634
|
+
if (stop !== null)
|
|
635
|
+
return stop;
|
|
636
|
+
removeFolder(plan, out);
|
|
637
|
+
}
|
|
348
638
|
const leftovers = repoLeftovers(workspace);
|
|
349
639
|
if (leftovers.files.length > 0 || leftovers.sections.length > 0) {
|
|
350
640
|
out.say("");
|
|
@@ -354,7 +644,46 @@ export async function remove(argv, out = createOutput()) {
|
|
|
354
644
|
for (const section of leftovers.sections)
|
|
355
645
|
out.say(` by hand — ${section}`);
|
|
356
646
|
}
|
|
357
|
-
out.
|
|
647
|
+
out.say("The MCP server, the install id and the sign-in belong to this machine and stay: carrick uninstall removes them.");
|
|
648
|
+
return 0;
|
|
649
|
+
}
|
|
650
|
+
export async function uninstall(argv) {
|
|
651
|
+
return uninstallWith(argv, createOutput(), process.stdin.isTTY === true);
|
|
652
|
+
}
|
|
653
|
+
/** `uninstall` with the terminal stated, as `removeWith`. */
|
|
654
|
+
export async function uninstallWith(argv, out, interactive) {
|
|
655
|
+
const parsed = parseUninstallArgs(argv);
|
|
656
|
+
if (typeof parsed === "string")
|
|
657
|
+
return usage(parsed, "carrick uninstall");
|
|
658
|
+
const plan = machinePlan(parsed.keepLogin);
|
|
659
|
+
const deletes = machineLines(plan);
|
|
660
|
+
if (deletes.length === 0) {
|
|
661
|
+
for (const line of plan.leaves)
|
|
662
|
+
out.warn(line);
|
|
663
|
+
out.say("Nothing left to remove on this machine.");
|
|
664
|
+
}
|
|
665
|
+
else {
|
|
666
|
+
const stop = await confirmed({
|
|
667
|
+
command: "uninstall",
|
|
668
|
+
heading: "Remove from this machine:",
|
|
669
|
+
deletes,
|
|
670
|
+
leaves: plan.leaves,
|
|
671
|
+
expected: UNINSTALL_ANSWER,
|
|
672
|
+
question: `Type ${UNINSTALL_ANSWER} to remove these`,
|
|
673
|
+
given: parsed.confirm,
|
|
674
|
+
interactive,
|
|
675
|
+
out,
|
|
676
|
+
});
|
|
677
|
+
if (stop !== null)
|
|
678
|
+
return stop;
|
|
679
|
+
removeMachine(plan, out);
|
|
680
|
+
}
|
|
681
|
+
if (parsed.keepLogin)
|
|
682
|
+
out.say(`This machine stays signed in; the credential is ${tilde(credentialPath())}.`);
|
|
683
|
+
out.note("Last steps, when nothing else on this machine needs it", [
|
|
684
|
+
"carrick remove, in each folder carrick init set up",
|
|
685
|
+
"npm uninstall -g carrick",
|
|
686
|
+
]);
|
|
358
687
|
return 0;
|
|
359
688
|
}
|
|
360
689
|
//# sourceMappingURL=remove.js.map
|