@xwm111/ccs 0.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +21 -0
- package/README.md +65 -0
- package/bin/ccs.mjs +2 -0
- package/dist/chunks/auto-updater.mjs +1708 -0
- package/dist/chunks/claude-code-incremental-manager.mjs +576 -0
- package/dist/chunks/installer.mjs +610 -0
- package/dist/cli.d.mts +1 -0
- package/dist/cli.d.ts +1 -0
- package/dist/cli.mjs +2407 -0
- package/dist/i18n/locales/en/api.json +51 -0
- package/dist/i18n/locales/en/cli.json +58 -0
- package/dist/i18n/locales/en/common.json +19 -0
- package/dist/i18n/locales/en/configuration.json +81 -0
- package/dist/i18n/locales/en/errors.json +26 -0
- package/dist/i18n/locales/en/installation.json +80 -0
- package/dist/i18n/locales/en/language.json +19 -0
- package/dist/i18n/locales/en/menu.json +31 -0
- package/dist/i18n/locales/en/multi-config.json +79 -0
- package/dist/i18n/locales/en/uninstall.json +56 -0
- package/dist/i18n/locales/en/updater.json +26 -0
- package/dist/i18n/locales/zh-CN/api.json +51 -0
- package/dist/i18n/locales/zh-CN/cli.json +58 -0
- package/dist/i18n/locales/zh-CN/common.json +19 -0
- package/dist/i18n/locales/zh-CN/configuration.json +81 -0
- package/dist/i18n/locales/zh-CN/errors.json +26 -0
- package/dist/i18n/locales/zh-CN/installation.json +80 -0
- package/dist/i18n/locales/zh-CN/language.json +19 -0
- package/dist/i18n/locales/zh-CN/menu.json +31 -0
- package/dist/i18n/locales/zh-CN/multi-config.json +79 -0
- package/dist/i18n/locales/zh-CN/uninstall.json +56 -0
- package/dist/i18n/locales/zh-CN/updater.json +26 -0
- package/dist/index.d.mts +222 -0
- package/dist/index.d.ts +222 -0
- package/dist/index.mjs +18 -0
- package/package.json +109 -0
|
@@ -0,0 +1,610 @@
|
|
|
1
|
+
import * as nodeFs from 'node:fs';
|
|
2
|
+
import { homedir } from 'node:os';
|
|
3
|
+
import ansis from 'ansis';
|
|
4
|
+
import inquirer from 'inquirer';
|
|
5
|
+
import ora from 'ora';
|
|
6
|
+
import { join } from 'pathe';
|
|
7
|
+
import { exec } from 'tinyexec';
|
|
8
|
+
import { c as commandExists, G as ensureI18nInitialized, H as i18n, I as updateClaudeCode, J as isTermux, K as getTermuxPrefix, M as isWSL, N as getWSLInfo, O as wrapCommandWithSudo, P as exists, Q as isExecutable, R as remove, g as getPlatform, T as getRecommendedInstallMethods, U as findCommandPath, V as getHomebrewCommandPaths } from './auto-updater.mjs';
|
|
9
|
+
import 'node:url';
|
|
10
|
+
import 'dayjs';
|
|
11
|
+
import 'node:process';
|
|
12
|
+
import 'semver';
|
|
13
|
+
import 'i18next';
|
|
14
|
+
import 'i18next-fs-backend';
|
|
15
|
+
import 'inquirer-toggle';
|
|
16
|
+
import 'node:child_process';
|
|
17
|
+
import 'node:util';
|
|
18
|
+
|
|
19
|
+
async function isClaudeCodeInstalled() {
|
|
20
|
+
return await commandExists("claude");
|
|
21
|
+
}
|
|
22
|
+
async function installClaudeCode(skipMethodSelection = false) {
|
|
23
|
+
ensureI18nInitialized();
|
|
24
|
+
const codeType = "claude-code";
|
|
25
|
+
const installed = await isClaudeCodeInstalled();
|
|
26
|
+
if (installed) {
|
|
27
|
+
console.log(ansis.green(`\u2714 ${i18n.t("installation:alreadyInstalled")}`));
|
|
28
|
+
const version = await detectInstalledVersion(codeType);
|
|
29
|
+
if (version) {
|
|
30
|
+
console.log(ansis.gray(` ${i18n.t("installation:detectedVersion", { version })}`));
|
|
31
|
+
}
|
|
32
|
+
const verification = await verifyInstallation(codeType);
|
|
33
|
+
if (verification.symlinkCreated) {
|
|
34
|
+
displayVerificationResult(verification);
|
|
35
|
+
}
|
|
36
|
+
await updateClaudeCode();
|
|
37
|
+
return;
|
|
38
|
+
}
|
|
39
|
+
if (isTermux()) {
|
|
40
|
+
console.log(ansis.yellow(`\u2139 ${i18n.t("installation:termuxDetected")}`));
|
|
41
|
+
const termuxPrefix = getTermuxPrefix();
|
|
42
|
+
console.log(ansis.gray(i18n.t("installation:termuxPathInfo", { path: termuxPrefix })));
|
|
43
|
+
console.log(ansis.gray(`Node.js: ${termuxPrefix}/bin/node`));
|
|
44
|
+
console.log(ansis.gray(`npm: ${termuxPrefix}/bin/npm`));
|
|
45
|
+
}
|
|
46
|
+
if (isWSL()) {
|
|
47
|
+
const wslInfo = getWSLInfo();
|
|
48
|
+
if (wslInfo?.distro) {
|
|
49
|
+
console.log(ansis.yellow(`\u2139 ${i18n.t("installation:wslDetected", { distro: wslInfo.distro })}`));
|
|
50
|
+
} else {
|
|
51
|
+
console.log(ansis.yellow(`\u2139 ${i18n.t("installation:wslDetectedGeneric")}`));
|
|
52
|
+
}
|
|
53
|
+
console.log(ansis.gray(i18n.t("installation:wslPathInfo", { path: `${homedir()}/.claude/` })));
|
|
54
|
+
}
|
|
55
|
+
if (skipMethodSelection) {
|
|
56
|
+
console.log(i18n.t("installation:installing"));
|
|
57
|
+
try {
|
|
58
|
+
const { command, args, usedSudo } = wrapCommandWithSudo("npm", ["install", "-g", "@anthropic-ai/claude-code", "--force"]);
|
|
59
|
+
if (usedSudo) {
|
|
60
|
+
console.log(ansis.yellow(`\u2139 ${i18n.t("installation:usingSudo")}`));
|
|
61
|
+
}
|
|
62
|
+
await exec(command, args);
|
|
63
|
+
console.log(`\u2714 ${i18n.t("installation:installSuccess")}`);
|
|
64
|
+
await setInstallMethod("npm");
|
|
65
|
+
const verification = await verifyInstallation(codeType);
|
|
66
|
+
displayVerificationResult(verification, codeType);
|
|
67
|
+
if (isTermux()) {
|
|
68
|
+
console.log(ansis.gray(`
|
|
69
|
+
Claude Code installed to: ${getTermuxPrefix()}/bin/claude`));
|
|
70
|
+
}
|
|
71
|
+
if (isWSL()) {
|
|
72
|
+
console.log(ansis.gray(`
|
|
73
|
+
${i18n.t("installation:wslInstallSuccess")}`));
|
|
74
|
+
}
|
|
75
|
+
} catch (error) {
|
|
76
|
+
console.error(`\u2716 ${i18n.t("installation:installFailed")}`);
|
|
77
|
+
if (isTermux()) {
|
|
78
|
+
console.error(ansis.yellow(`
|
|
79
|
+
${i18n.t("installation:termuxInstallHint")}
|
|
80
|
+
`));
|
|
81
|
+
}
|
|
82
|
+
throw error;
|
|
83
|
+
}
|
|
84
|
+
return;
|
|
85
|
+
}
|
|
86
|
+
const method = await selectInstallMethod(codeType);
|
|
87
|
+
if (!method) {
|
|
88
|
+
console.log(ansis.yellow(i18n.t("common:cancelled")));
|
|
89
|
+
return;
|
|
90
|
+
}
|
|
91
|
+
const success = await executeInstallMethod(method, codeType);
|
|
92
|
+
if (!success) {
|
|
93
|
+
const retrySuccess = await handleInstallFailure(codeType, [method]);
|
|
94
|
+
if (!retrySuccess) {
|
|
95
|
+
console.error(ansis.red(`\u2716 ${i18n.t("installation:installFailed")}`));
|
|
96
|
+
throw new Error(i18n.t("installation:installFailed"));
|
|
97
|
+
}
|
|
98
|
+
}
|
|
99
|
+
if (isTermux()) {
|
|
100
|
+
console.log(ansis.gray(`
|
|
101
|
+
Claude Code installed to: ${getTermuxPrefix()}/bin/claude`));
|
|
102
|
+
}
|
|
103
|
+
if (isWSL()) {
|
|
104
|
+
console.log(ansis.gray(`
|
|
105
|
+
${i18n.t("installation:wslInstallSuccess")}`));
|
|
106
|
+
}
|
|
107
|
+
}
|
|
108
|
+
async function isLocalClaudeCodeInstalled() {
|
|
109
|
+
const localClaudePath = join(homedir(), ".claude", "local", "claude");
|
|
110
|
+
if (!exists(localClaudePath)) {
|
|
111
|
+
return false;
|
|
112
|
+
}
|
|
113
|
+
return await isExecutable(localClaudePath);
|
|
114
|
+
}
|
|
115
|
+
async function getInstallationStatus() {
|
|
116
|
+
const localPath = join(homedir(), ".claude", "local", "claude");
|
|
117
|
+
const [hasGlobal, hasLocal] = await Promise.all([
|
|
118
|
+
isClaudeCodeInstalled(),
|
|
119
|
+
isLocalClaudeCodeInstalled()
|
|
120
|
+
]);
|
|
121
|
+
return {
|
|
122
|
+
hasGlobal,
|
|
123
|
+
hasLocal,
|
|
124
|
+
localPath
|
|
125
|
+
};
|
|
126
|
+
}
|
|
127
|
+
async function removeLocalClaudeCode() {
|
|
128
|
+
const localDir = join(homedir(), ".claude", "local");
|
|
129
|
+
if (!exists(localDir)) {
|
|
130
|
+
return;
|
|
131
|
+
}
|
|
132
|
+
try {
|
|
133
|
+
await remove(localDir);
|
|
134
|
+
} catch (error) {
|
|
135
|
+
ensureI18nInitialized();
|
|
136
|
+
throw new Error(`${i18n.t("installation:failedToRemoveLocalInstallation")}: ${error}`);
|
|
137
|
+
}
|
|
138
|
+
}
|
|
139
|
+
async function getInstallMethodFromConfig(codeType) {
|
|
140
|
+
try {
|
|
141
|
+
if (codeType === "claude-code") {
|
|
142
|
+
const { readMcpConfig } = await import('./auto-updater.mjs').then(function (n) { return n.aa; });
|
|
143
|
+
const config = readMcpConfig();
|
|
144
|
+
return config?.installMethod || null;
|
|
145
|
+
}
|
|
146
|
+
} catch {
|
|
147
|
+
}
|
|
148
|
+
return null;
|
|
149
|
+
}
|
|
150
|
+
async function uninstallCodeTool(codeType) {
|
|
151
|
+
ensureI18nInitialized();
|
|
152
|
+
const codeTypeName = i18n.t("common:claudeCode");
|
|
153
|
+
let method = await getInstallMethodFromConfig(codeType);
|
|
154
|
+
if (!method) {
|
|
155
|
+
if (codeType === "claude-code") {
|
|
156
|
+
try {
|
|
157
|
+
const result = await exec("brew", ["list", "--cask", "claude-code"]);
|
|
158
|
+
if (result.exitCode === 0) {
|
|
159
|
+
method = "homebrew";
|
|
160
|
+
}
|
|
161
|
+
} catch {
|
|
162
|
+
}
|
|
163
|
+
} else if (codeType === "codex") {
|
|
164
|
+
try {
|
|
165
|
+
const result = await exec("brew", ["list", "--cask", "codex"]);
|
|
166
|
+
if (result.exitCode === 0) {
|
|
167
|
+
method = "homebrew";
|
|
168
|
+
}
|
|
169
|
+
} catch {
|
|
170
|
+
}
|
|
171
|
+
}
|
|
172
|
+
if (!method) {
|
|
173
|
+
method = "npm";
|
|
174
|
+
}
|
|
175
|
+
}
|
|
176
|
+
if (method === "native") {
|
|
177
|
+
const platform = getPlatform();
|
|
178
|
+
if (platform === "macos" || platform === "linux") {
|
|
179
|
+
try {
|
|
180
|
+
const testResult = codeType === "claude-code" ? await exec("brew", ["list", "--cask", "claude-code"]) : await exec("brew", ["list", "--cask", "codex"]);
|
|
181
|
+
if (testResult.exitCode === 0) {
|
|
182
|
+
method = "homebrew";
|
|
183
|
+
}
|
|
184
|
+
} catch {
|
|
185
|
+
method = "manual";
|
|
186
|
+
}
|
|
187
|
+
} else {
|
|
188
|
+
method = "manual";
|
|
189
|
+
}
|
|
190
|
+
}
|
|
191
|
+
const spinner = ora(i18n.t("installation:uninstallingWith", { method, codeType: codeTypeName })).start();
|
|
192
|
+
try {
|
|
193
|
+
switch (method) {
|
|
194
|
+
case "npm":
|
|
195
|
+
case "npm-global": {
|
|
196
|
+
const packageName = codeType === "claude-code" ? "@anthropic-ai/claude-code" : "@openai/codex";
|
|
197
|
+
const { command, args, usedSudo } = wrapCommandWithSudo("npm", ["uninstall", "-g", packageName]);
|
|
198
|
+
if (usedSudo) {
|
|
199
|
+
spinner.info(i18n.t("installation:usingSudo"));
|
|
200
|
+
spinner.start();
|
|
201
|
+
}
|
|
202
|
+
await exec(command, args);
|
|
203
|
+
break;
|
|
204
|
+
}
|
|
205
|
+
case "homebrew": {
|
|
206
|
+
if (codeType === "claude-code") {
|
|
207
|
+
await exec("brew", ["uninstall", "--cask", "claude-code"]);
|
|
208
|
+
} else {
|
|
209
|
+
await exec("brew", ["uninstall", "--cask", "codex"]);
|
|
210
|
+
}
|
|
211
|
+
break;
|
|
212
|
+
}
|
|
213
|
+
case "manual":
|
|
214
|
+
default: {
|
|
215
|
+
spinner.warn(i18n.t("installation:manualUninstallRequired", { codeType: codeTypeName }));
|
|
216
|
+
const command = codeType === "claude-code" ? "claude" : "codex";
|
|
217
|
+
try {
|
|
218
|
+
const whichCmd = getPlatform() === "windows" ? "where" : "which";
|
|
219
|
+
const result = await exec(whichCmd, [command]);
|
|
220
|
+
if (result.stdout) {
|
|
221
|
+
const binaryPath = result.stdout.trim().split("\n")[0];
|
|
222
|
+
spinner.info(i18n.t("installation:binaryLocation", { path: binaryPath }));
|
|
223
|
+
const platform = getPlatform();
|
|
224
|
+
if (platform === "windows") {
|
|
225
|
+
const quotedBinaryPath = `"${binaryPath}"`;
|
|
226
|
+
await exec("cmd", ["/c", "del", "/f", "/q", quotedBinaryPath]);
|
|
227
|
+
} else {
|
|
228
|
+
const { command: rmCmd, args: rmArgs } = wrapCommandWithSudo("rm", ["-f", binaryPath]);
|
|
229
|
+
if (rmCmd === "sudo") {
|
|
230
|
+
spinner.info(i18n.t("installation:usingSudo"));
|
|
231
|
+
spinner.start();
|
|
232
|
+
}
|
|
233
|
+
await exec(rmCmd, rmArgs);
|
|
234
|
+
}
|
|
235
|
+
}
|
|
236
|
+
} catch {
|
|
237
|
+
spinner.fail(i18n.t("installation:failedToLocateBinary", { command }));
|
|
238
|
+
return false;
|
|
239
|
+
}
|
|
240
|
+
break;
|
|
241
|
+
}
|
|
242
|
+
}
|
|
243
|
+
spinner.succeed(i18n.t("installation:uninstallSuccess", { method, codeType: codeTypeName }));
|
|
244
|
+
return true;
|
|
245
|
+
} catch (error) {
|
|
246
|
+
spinner.fail(i18n.t("installation:uninstallFailed", { method, codeType: codeTypeName }));
|
|
247
|
+
if (error instanceof Error) {
|
|
248
|
+
console.error(ansis.gray(error.message));
|
|
249
|
+
}
|
|
250
|
+
return false;
|
|
251
|
+
}
|
|
252
|
+
}
|
|
253
|
+
async function setInstallMethod(method, codeType = "claude-code") {
|
|
254
|
+
try {
|
|
255
|
+
if (codeType === "claude-code") {
|
|
256
|
+
const { readMcpConfig, writeMcpConfig } = await import('./auto-updater.mjs').then(function (n) { return n.aa; });
|
|
257
|
+
let config = readMcpConfig();
|
|
258
|
+
if (!config) {
|
|
259
|
+
config = { mcpServers: {} };
|
|
260
|
+
}
|
|
261
|
+
config.installMethod = method === "npm" ? "npm-global" : method;
|
|
262
|
+
writeMcpConfig(config);
|
|
263
|
+
}
|
|
264
|
+
} catch (error) {
|
|
265
|
+
console.error("Failed to set installMethod:", error);
|
|
266
|
+
}
|
|
267
|
+
}
|
|
268
|
+
async function detectInstalledVersion(codeType) {
|
|
269
|
+
try {
|
|
270
|
+
const command = codeType === "claude-code" ? "claude" : "codex";
|
|
271
|
+
const result = await exec(command, ["--version"]);
|
|
272
|
+
if (result.exitCode === 0 && result.stdout) {
|
|
273
|
+
const versionMatch = result.stdout.match(/(\d+\.\d+\.\d+)/);
|
|
274
|
+
return versionMatch ? versionMatch[1] : result.stdout.trim();
|
|
275
|
+
}
|
|
276
|
+
} catch {
|
|
277
|
+
}
|
|
278
|
+
return null;
|
|
279
|
+
}
|
|
280
|
+
function getInstallMethodLabel(method) {
|
|
281
|
+
switch (method) {
|
|
282
|
+
case "npm":
|
|
283
|
+
return i18n.t("installation:installMethodNpm");
|
|
284
|
+
case "homebrew":
|
|
285
|
+
return i18n.t("installation:installMethodHomebrew");
|
|
286
|
+
case "curl":
|
|
287
|
+
return i18n.t("installation:installMethodCurl");
|
|
288
|
+
case "powershell":
|
|
289
|
+
return i18n.t("installation:installMethodPowershell");
|
|
290
|
+
case "cmd":
|
|
291
|
+
return i18n.t("installation:installMethodCmd");
|
|
292
|
+
default:
|
|
293
|
+
return method;
|
|
294
|
+
}
|
|
295
|
+
}
|
|
296
|
+
function getInstallMethodOptions(codeType, recommendedMethods) {
|
|
297
|
+
const allMethods = ["npm", "homebrew", "curl", "powershell", "cmd"];
|
|
298
|
+
const platform = getPlatform();
|
|
299
|
+
const availableMethods = allMethods.filter((method) => {
|
|
300
|
+
if (codeType === "codex" && !["npm", "homebrew"].includes(method)) {
|
|
301
|
+
return false;
|
|
302
|
+
}
|
|
303
|
+
if (method === "homebrew")
|
|
304
|
+
return platform === "macos" || platform === "linux";
|
|
305
|
+
if (method === "curl")
|
|
306
|
+
return platform !== "windows" || isWSL();
|
|
307
|
+
if (method === "powershell" || method === "cmd")
|
|
308
|
+
return platform === "windows";
|
|
309
|
+
return true;
|
|
310
|
+
});
|
|
311
|
+
const topRecommended = recommendedMethods.length > 0 ? recommendedMethods[0] : null;
|
|
312
|
+
return availableMethods.map((method) => {
|
|
313
|
+
const isTopRecommended = method === topRecommended;
|
|
314
|
+
const methodLabel = getInstallMethodLabel(method);
|
|
315
|
+
const title = isTopRecommended ? `${methodLabel} ${ansis.green(`[${i18n.t("installation:recommendedMethod")}]`)}` : methodLabel;
|
|
316
|
+
return {
|
|
317
|
+
title,
|
|
318
|
+
value: method
|
|
319
|
+
};
|
|
320
|
+
});
|
|
321
|
+
}
|
|
322
|
+
async function selectInstallMethod(codeType, excludeMethods = []) {
|
|
323
|
+
ensureI18nInitialized();
|
|
324
|
+
const codeTypeName = i18n.t("common:claudeCode");
|
|
325
|
+
const recommendedMethods = getRecommendedInstallMethods(codeType);
|
|
326
|
+
const methodOptions = getInstallMethodOptions(codeType, recommendedMethods).filter((option) => !excludeMethods.includes(option.value));
|
|
327
|
+
if (methodOptions.length === 0) {
|
|
328
|
+
console.log(ansis.yellow(i18n.t("installation:noMoreMethods")));
|
|
329
|
+
return null;
|
|
330
|
+
}
|
|
331
|
+
const response = await inquirer.prompt({
|
|
332
|
+
type: "list",
|
|
333
|
+
name: "method",
|
|
334
|
+
message: i18n.t("installation:selectInstallMethod", { codeType: codeTypeName }),
|
|
335
|
+
choices: methodOptions.map((opt) => ({
|
|
336
|
+
name: opt.title,
|
|
337
|
+
value: opt.value
|
|
338
|
+
}))
|
|
339
|
+
});
|
|
340
|
+
return response.method || null;
|
|
341
|
+
}
|
|
342
|
+
async function executeInstallMethod(method, codeType) {
|
|
343
|
+
ensureI18nInitialized();
|
|
344
|
+
const codeTypeName = i18n.t("common:claudeCode");
|
|
345
|
+
const spinner = ora(i18n.t("installation:installingWith", { method, codeType: codeTypeName })).start();
|
|
346
|
+
try {
|
|
347
|
+
switch (method) {
|
|
348
|
+
case "npm": {
|
|
349
|
+
const packageName = codeType === "claude-code" ? "@anthropic-ai/claude-code" : "@openai/codex";
|
|
350
|
+
const { command, args, usedSudo } = wrapCommandWithSudo("npm", ["install", "-g", packageName, "--force"]);
|
|
351
|
+
if (usedSudo) {
|
|
352
|
+
spinner.info(i18n.t("installation:usingSudo"));
|
|
353
|
+
spinner.start();
|
|
354
|
+
}
|
|
355
|
+
await exec(command, args);
|
|
356
|
+
await setInstallMethod("npm", codeType);
|
|
357
|
+
break;
|
|
358
|
+
}
|
|
359
|
+
case "homebrew": {
|
|
360
|
+
if (codeType === "claude-code") {
|
|
361
|
+
await exec("brew", ["install", "--cask", "claude-code"]);
|
|
362
|
+
} else {
|
|
363
|
+
await exec("brew", ["install", "--cask", "codex"]);
|
|
364
|
+
}
|
|
365
|
+
await setInstallMethod("homebrew", codeType);
|
|
366
|
+
break;
|
|
367
|
+
}
|
|
368
|
+
case "curl": {
|
|
369
|
+
if (codeType === "claude-code") {
|
|
370
|
+
await exec("bash", ["-c", "curl -fsSL https://claude.ai/install.sh | bash"]);
|
|
371
|
+
} else {
|
|
372
|
+
spinner.stop();
|
|
373
|
+
return await executeInstallMethod("npm", codeType);
|
|
374
|
+
}
|
|
375
|
+
await setInstallMethod("curl", codeType);
|
|
376
|
+
break;
|
|
377
|
+
}
|
|
378
|
+
case "powershell": {
|
|
379
|
+
if (codeType === "claude-code") {
|
|
380
|
+
await exec("powershell", ["-Command", "irm https://claude.ai/install.ps1 | iex"]);
|
|
381
|
+
} else {
|
|
382
|
+
spinner.stop();
|
|
383
|
+
return await executeInstallMethod("npm", codeType);
|
|
384
|
+
}
|
|
385
|
+
await setInstallMethod("powershell", codeType);
|
|
386
|
+
break;
|
|
387
|
+
}
|
|
388
|
+
case "cmd": {
|
|
389
|
+
if (codeType === "claude-code") {
|
|
390
|
+
await exec("cmd", ["/c", "curl -fsSL https://claude.ai/install.cmd -o install.cmd && install.cmd && del install.cmd"]);
|
|
391
|
+
} else {
|
|
392
|
+
spinner.stop();
|
|
393
|
+
return await executeInstallMethod("npm", codeType);
|
|
394
|
+
}
|
|
395
|
+
await setInstallMethod("cmd", codeType);
|
|
396
|
+
break;
|
|
397
|
+
}
|
|
398
|
+
default:
|
|
399
|
+
throw new Error(`Unsupported install method: ${method}`);
|
|
400
|
+
}
|
|
401
|
+
spinner.succeed(i18n.t("installation:installMethodSuccess", { method }));
|
|
402
|
+
const verification = await verifyInstallation(codeType);
|
|
403
|
+
displayVerificationResult(verification, codeType);
|
|
404
|
+
return true;
|
|
405
|
+
} catch (error) {
|
|
406
|
+
spinner.fail(i18n.t("installation:installMethodFailed", { method }));
|
|
407
|
+
if (error instanceof Error) {
|
|
408
|
+
console.error(ansis.gray(error.message));
|
|
409
|
+
}
|
|
410
|
+
return false;
|
|
411
|
+
}
|
|
412
|
+
}
|
|
413
|
+
async function handleInstallFailure(codeType, failedMethods) {
|
|
414
|
+
ensureI18nInitialized();
|
|
415
|
+
const response = await inquirer.prompt({
|
|
416
|
+
type: "confirm",
|
|
417
|
+
name: "retry",
|
|
418
|
+
message: i18n.t("installation:tryAnotherMethod"),
|
|
419
|
+
default: true
|
|
420
|
+
});
|
|
421
|
+
if (!response.retry) {
|
|
422
|
+
return false;
|
|
423
|
+
}
|
|
424
|
+
const newMethod = await selectInstallMethod(codeType, failedMethods);
|
|
425
|
+
if (!newMethod) {
|
|
426
|
+
return false;
|
|
427
|
+
}
|
|
428
|
+
const success = await executeInstallMethod(newMethod, codeType);
|
|
429
|
+
if (success) {
|
|
430
|
+
return true;
|
|
431
|
+
}
|
|
432
|
+
return await handleInstallFailure(codeType, [...failedMethods, newMethod]);
|
|
433
|
+
}
|
|
434
|
+
async function isCommandInPath(command) {
|
|
435
|
+
try {
|
|
436
|
+
const cmd = getPlatform() === "windows" ? "where" : "which";
|
|
437
|
+
const res = await exec(cmd, [command]);
|
|
438
|
+
return res.exitCode === 0;
|
|
439
|
+
} catch {
|
|
440
|
+
return false;
|
|
441
|
+
}
|
|
442
|
+
}
|
|
443
|
+
async function verifyInstallation(codeType) {
|
|
444
|
+
const command = codeType === "claude-code" ? "claude" : "codex";
|
|
445
|
+
const commandInPath = await isCommandInPath(command);
|
|
446
|
+
if (commandInPath) {
|
|
447
|
+
const version = await detectInstalledVersion(codeType);
|
|
448
|
+
return {
|
|
449
|
+
success: true,
|
|
450
|
+
commandPath: await findCommandPath(command),
|
|
451
|
+
version,
|
|
452
|
+
needsSymlink: false,
|
|
453
|
+
symlinkCreated: false
|
|
454
|
+
};
|
|
455
|
+
}
|
|
456
|
+
if (getPlatform() === "macos") {
|
|
457
|
+
const homebrewPaths = await getHomebrewCommandPaths(command);
|
|
458
|
+
let foundPath = null;
|
|
459
|
+
for (const path of homebrewPaths) {
|
|
460
|
+
if (exists(path)) {
|
|
461
|
+
foundPath = path;
|
|
462
|
+
break;
|
|
463
|
+
}
|
|
464
|
+
}
|
|
465
|
+
if (foundPath) {
|
|
466
|
+
const symlinkResult = await createHomebrewSymlink(command, foundPath);
|
|
467
|
+
if (symlinkResult.success) {
|
|
468
|
+
const version = await detectInstalledVersion(codeType);
|
|
469
|
+
return {
|
|
470
|
+
success: true,
|
|
471
|
+
commandPath: symlinkResult.symlinkPath,
|
|
472
|
+
version,
|
|
473
|
+
needsSymlink: true,
|
|
474
|
+
symlinkCreated: true
|
|
475
|
+
};
|
|
476
|
+
}
|
|
477
|
+
return {
|
|
478
|
+
success: false,
|
|
479
|
+
commandPath: foundPath,
|
|
480
|
+
version: null,
|
|
481
|
+
needsSymlink: true,
|
|
482
|
+
symlinkCreated: false,
|
|
483
|
+
error: symlinkResult.error
|
|
484
|
+
};
|
|
485
|
+
}
|
|
486
|
+
}
|
|
487
|
+
if (isTermux()) {
|
|
488
|
+
const termuxPrefix = getTermuxPrefix();
|
|
489
|
+
const termuxPaths = [
|
|
490
|
+
`${termuxPrefix}/bin/${command}`,
|
|
491
|
+
`${termuxPrefix}/usr/bin/${command}`
|
|
492
|
+
];
|
|
493
|
+
for (const path of termuxPaths) {
|
|
494
|
+
if (exists(path)) {
|
|
495
|
+
const version = await detectInstalledVersion(codeType);
|
|
496
|
+
return {
|
|
497
|
+
success: true,
|
|
498
|
+
commandPath: path,
|
|
499
|
+
version,
|
|
500
|
+
needsSymlink: false,
|
|
501
|
+
symlinkCreated: false
|
|
502
|
+
};
|
|
503
|
+
}
|
|
504
|
+
}
|
|
505
|
+
}
|
|
506
|
+
return {
|
|
507
|
+
success: false,
|
|
508
|
+
commandPath: null,
|
|
509
|
+
version: null,
|
|
510
|
+
needsSymlink: false,
|
|
511
|
+
symlinkCreated: false,
|
|
512
|
+
error: "Command not found in any known location"
|
|
513
|
+
};
|
|
514
|
+
}
|
|
515
|
+
async function createHomebrewSymlink(command, sourcePath) {
|
|
516
|
+
const homebrewBinPaths = [
|
|
517
|
+
"/opt/homebrew/bin",
|
|
518
|
+
// Apple Silicon (M1/M2)
|
|
519
|
+
"/usr/local/bin"
|
|
520
|
+
// Intel Mac
|
|
521
|
+
];
|
|
522
|
+
let targetDir = null;
|
|
523
|
+
for (const binPath of homebrewBinPaths) {
|
|
524
|
+
if (nodeFs.existsSync(binPath)) {
|
|
525
|
+
targetDir = binPath;
|
|
526
|
+
break;
|
|
527
|
+
}
|
|
528
|
+
}
|
|
529
|
+
if (!targetDir) {
|
|
530
|
+
return {
|
|
531
|
+
success: false,
|
|
532
|
+
symlinkPath: null,
|
|
533
|
+
error: "No suitable Homebrew bin directory found"
|
|
534
|
+
};
|
|
535
|
+
}
|
|
536
|
+
const symlinkPath = join(targetDir, command);
|
|
537
|
+
try {
|
|
538
|
+
const stats = nodeFs.lstatSync(symlinkPath);
|
|
539
|
+
if (stats.isSymbolicLink()) {
|
|
540
|
+
const existingTarget = nodeFs.readlinkSync(symlinkPath);
|
|
541
|
+
if (existingTarget === sourcePath) {
|
|
542
|
+
return {
|
|
543
|
+
success: true,
|
|
544
|
+
symlinkPath
|
|
545
|
+
};
|
|
546
|
+
}
|
|
547
|
+
nodeFs.unlinkSync(symlinkPath);
|
|
548
|
+
} else {
|
|
549
|
+
return {
|
|
550
|
+
success: false,
|
|
551
|
+
symlinkPath: null,
|
|
552
|
+
error: `File already exists at ${symlinkPath} and is not a symlink`
|
|
553
|
+
};
|
|
554
|
+
}
|
|
555
|
+
} catch (error) {
|
|
556
|
+
if (error.code !== "ENOENT") {
|
|
557
|
+
return {
|
|
558
|
+
success: false,
|
|
559
|
+
symlinkPath: null,
|
|
560
|
+
error: `Failed to check existing file: ${error}`
|
|
561
|
+
};
|
|
562
|
+
}
|
|
563
|
+
}
|
|
564
|
+
try {
|
|
565
|
+
nodeFs.symlinkSync(sourcePath, symlinkPath);
|
|
566
|
+
return {
|
|
567
|
+
success: true,
|
|
568
|
+
symlinkPath
|
|
569
|
+
};
|
|
570
|
+
} catch (error) {
|
|
571
|
+
if (error.code === "EACCES") {
|
|
572
|
+
return {
|
|
573
|
+
success: false,
|
|
574
|
+
symlinkPath: null,
|
|
575
|
+
error: `Permission denied. Try running: sudo ln -sf ${sourcePath} ${symlinkPath}`
|
|
576
|
+
};
|
|
577
|
+
}
|
|
578
|
+
return {
|
|
579
|
+
success: false,
|
|
580
|
+
symlinkPath: null,
|
|
581
|
+
error: `Failed to create symlink: ${error.message}`
|
|
582
|
+
};
|
|
583
|
+
}
|
|
584
|
+
}
|
|
585
|
+
function displayVerificationResult(result, _codeType) {
|
|
586
|
+
ensureI18nInitialized();
|
|
587
|
+
const codeTypeName = i18n.t("common:claudeCode");
|
|
588
|
+
if (result.success) {
|
|
589
|
+
if (result.symlinkCreated) {
|
|
590
|
+
console.log(ansis.green(`\u2714 ${codeTypeName} ${i18n.t("installation:verificationSuccess")}`));
|
|
591
|
+
console.log(ansis.gray(` ${i18n.t("installation:symlinkCreated", { path: result.commandPath })}`));
|
|
592
|
+
}
|
|
593
|
+
if (result.version) {
|
|
594
|
+
console.log(ansis.gray(` ${i18n.t("installation:detectedVersion", { version: result.version })}`));
|
|
595
|
+
}
|
|
596
|
+
} else {
|
|
597
|
+
console.log(ansis.yellow(`\u26A0 ${codeTypeName} ${i18n.t("installation:verificationFailed")}`));
|
|
598
|
+
if (result.commandPath) {
|
|
599
|
+
console.log(ansis.gray(` ${i18n.t("installation:foundAtPath", { path: result.commandPath })}`));
|
|
600
|
+
}
|
|
601
|
+
if (result.error) {
|
|
602
|
+
console.log(ansis.gray(` ${result.error}`));
|
|
603
|
+
}
|
|
604
|
+
if (result.needsSymlink && !result.symlinkCreated) {
|
|
605
|
+
console.log(ansis.yellow(` ${i18n.t("installation:manualSymlinkHint")}`));
|
|
606
|
+
}
|
|
607
|
+
}
|
|
608
|
+
}
|
|
609
|
+
|
|
610
|
+
export { createHomebrewSymlink, detectInstalledVersion, displayVerificationResult, executeInstallMethod, getInstallationStatus, handleInstallFailure, installClaudeCode, isClaudeCodeInstalled, isLocalClaudeCodeInstalled, removeLocalClaudeCode, selectInstallMethod, setInstallMethod, uninstallCodeTool, verifyInstallation };
|
package/dist/cli.d.mts
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
|
package/dist/cli.d.ts
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
|