@nalvietnam/avatar-cli 1.1.6 → 1.2.1
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/dist/index.js +1081 -290
- package/dist/index.js.map +1 -1
- package/dist/lib/print-welcome-screen.js +1 -1
- package/dist/lib/print-welcome-screen.js.map +1 -1
- package/package.json +1 -1
package/dist/index.js
CHANGED
|
@@ -3,6 +3,131 @@
|
|
|
3
3
|
// src/index.ts
|
|
4
4
|
import { Command } from "commander";
|
|
5
5
|
|
|
6
|
+
// src/commands/ai.ts
|
|
7
|
+
import { spawnSync as spawnSync4 } from "child_process";
|
|
8
|
+
import { promises as fs4 } from "fs";
|
|
9
|
+
import { join as join5 } from "path";
|
|
10
|
+
import { confirm } from "@inquirer/prompts";
|
|
11
|
+
|
|
12
|
+
// src/lib/filesystem-helpers.ts
|
|
13
|
+
import { constants, promises as fs } from "fs";
|
|
14
|
+
import { dirname, join, relative } from "path";
|
|
15
|
+
async function pathExists(path) {
|
|
16
|
+
try {
|
|
17
|
+
await fs.access(path, constants.F_OK);
|
|
18
|
+
return true;
|
|
19
|
+
} catch {
|
|
20
|
+
return false;
|
|
21
|
+
}
|
|
22
|
+
}
|
|
23
|
+
async function ensureDir(path) {
|
|
24
|
+
await fs.mkdir(path, { recursive: true });
|
|
25
|
+
}
|
|
26
|
+
async function readText(path) {
|
|
27
|
+
return await fs.readFile(path, "utf8");
|
|
28
|
+
}
|
|
29
|
+
async function readJson(path) {
|
|
30
|
+
return JSON.parse(await readText(path));
|
|
31
|
+
}
|
|
32
|
+
async function writeTextAtomic(path, content, mode) {
|
|
33
|
+
await ensureDir(dirname(path));
|
|
34
|
+
const tmp = `${path}.tmp-${process.pid}-${Date.now()}`;
|
|
35
|
+
await fs.writeFile(tmp, content, "utf8");
|
|
36
|
+
if (mode !== void 0) {
|
|
37
|
+
await fs.chmod(tmp, mode);
|
|
38
|
+
}
|
|
39
|
+
await fs.rename(tmp, path);
|
|
40
|
+
}
|
|
41
|
+
async function writeJsonAtomic(path, data, mode) {
|
|
42
|
+
await writeTextAtomic(path, `${JSON.stringify(data, null, 2)}
|
|
43
|
+
`, mode);
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
// src/lib/audit-log-appender.ts
|
|
47
|
+
import { promises as fs2 } from "fs";
|
|
48
|
+
|
|
49
|
+
// src/lib/user-config-store.ts
|
|
50
|
+
import { homedir } from "os";
|
|
51
|
+
import { join as join2 } from "path";
|
|
52
|
+
|
|
53
|
+
// src/types/config-schema.ts
|
|
54
|
+
import { z } from "zod";
|
|
55
|
+
var userConfigSchema = z.object({
|
|
56
|
+
email: z.string().email(),
|
|
57
|
+
name: z.string(),
|
|
58
|
+
access_token: z.string().min(1),
|
|
59
|
+
refresh_token: z.string().min(1),
|
|
60
|
+
expires_at: z.string().datetime(),
|
|
61
|
+
id_token: z.string().min(1)
|
|
62
|
+
});
|
|
63
|
+
var userStateSchema = z.object({
|
|
64
|
+
installed_tools: z.record(
|
|
65
|
+
z.string(),
|
|
66
|
+
z.object({
|
|
67
|
+
version: z.string().optional(),
|
|
68
|
+
installed_at: z.string().datetime(),
|
|
69
|
+
install_method: z.string()
|
|
70
|
+
})
|
|
71
|
+
).default({}),
|
|
72
|
+
tool_inputs: z.record(z.string(), z.unknown()).default({})
|
|
73
|
+
});
|
|
74
|
+
var projectSettingsSchema = z.object({
|
|
75
|
+
allowedTools: z.array(z.string()),
|
|
76
|
+
hooks: z.object({
|
|
77
|
+
PostToolUse: z.array(z.unknown()).optional()
|
|
78
|
+
}).partial().optional(),
|
|
79
|
+
env: z.record(z.string(), z.string()).default({})
|
|
80
|
+
});
|
|
81
|
+
var initModeSchema = z.enum(["internal", "client", "library"]);
|
|
82
|
+
|
|
83
|
+
// src/lib/user-config-store.ts
|
|
84
|
+
var AVATAR_HOME = join2(homedir(), ".avatar");
|
|
85
|
+
var USER_CONFIG_PATH = join2(AVATAR_HOME, "config.json");
|
|
86
|
+
var USER_STATE_PATH = join2(AVATAR_HOME, "state.json");
|
|
87
|
+
var AUDIT_LOG_PATH = join2(AVATAR_HOME, "audit.log");
|
|
88
|
+
var BACKUPS_DIR = join2(AVATAR_HOME, "backups");
|
|
89
|
+
var SECRET_FILE_MODE = 384;
|
|
90
|
+
async function ensureAvatarHome() {
|
|
91
|
+
await ensureDir(AVATAR_HOME);
|
|
92
|
+
}
|
|
93
|
+
async function readUserConfig() {
|
|
94
|
+
if (!await pathExists(USER_CONFIG_PATH)) return null;
|
|
95
|
+
const raw = await readJson(USER_CONFIG_PATH);
|
|
96
|
+
const parsed = userConfigSchema.safeParse(raw);
|
|
97
|
+
if (!parsed.success) return null;
|
|
98
|
+
return parsed.data;
|
|
99
|
+
}
|
|
100
|
+
async function writeUserConfig(config) {
|
|
101
|
+
await ensureAvatarHome();
|
|
102
|
+
await writeJsonAtomic(USER_CONFIG_PATH, config, SECRET_FILE_MODE);
|
|
103
|
+
}
|
|
104
|
+
async function clearUserConfig() {
|
|
105
|
+
if (await pathExists(USER_CONFIG_PATH)) {
|
|
106
|
+
const { promises: fs9 } = await import("fs");
|
|
107
|
+
await fs9.unlink(USER_CONFIG_PATH);
|
|
108
|
+
}
|
|
109
|
+
}
|
|
110
|
+
function isTokenExpired(config) {
|
|
111
|
+
const expiresAt = Date.parse(config.expires_at);
|
|
112
|
+
return Number.isNaN(expiresAt) || expiresAt - Date.now() < 6e4;
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
// src/lib/audit-log-appender.ts
|
|
116
|
+
async function appendAuditEntry(action, detail) {
|
|
117
|
+
await ensureAvatarHome();
|
|
118
|
+
const entry = {
|
|
119
|
+
timestamp: (/* @__PURE__ */ new Date()).toISOString(),
|
|
120
|
+
action,
|
|
121
|
+
...detail ? { detail } : {}
|
|
122
|
+
};
|
|
123
|
+
const line = `${JSON.stringify(entry)}
|
|
124
|
+
`;
|
|
125
|
+
await fs2.appendFile(AUDIT_LOG_PATH, line, "utf8");
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
// src/lib/check-claude-code-subscription-and-quota.ts
|
|
129
|
+
import { spawnSync } from "child_process";
|
|
130
|
+
|
|
6
131
|
// src/lib/terminal-logger.ts
|
|
7
132
|
import chalk from "chalk";
|
|
8
133
|
import ora from "ora";
|
|
@@ -28,6 +153,610 @@ function spinner(text) {
|
|
|
28
153
|
}).start();
|
|
29
154
|
}
|
|
30
155
|
|
|
156
|
+
// src/lib/check-claude-code-subscription-and-quota.ts
|
|
157
|
+
var QUOTA_VERIFY_TIMEOUT_MS = 3e4;
|
|
158
|
+
var QUOTA_VERIFY_PROMPT = "ok";
|
|
159
|
+
function checkClaudeCodeSubscriptionAuth() {
|
|
160
|
+
const result = spawnSync("claude", ["auth", "status"], { encoding: "utf8" });
|
|
161
|
+
if (result.error || result.status !== 0) return "not-authenticated";
|
|
162
|
+
const stdout = (result.stdout || "").trim();
|
|
163
|
+
if (stdout.startsWith("{")) {
|
|
164
|
+
try {
|
|
165
|
+
const parsed = JSON.parse(stdout);
|
|
166
|
+
return parsed.loggedIn === true ? "authenticated" : "not-authenticated";
|
|
167
|
+
} catch {
|
|
168
|
+
return "authenticated";
|
|
169
|
+
}
|
|
170
|
+
}
|
|
171
|
+
return "authenticated";
|
|
172
|
+
}
|
|
173
|
+
function triggerClaudeCodeAuthLogin() {
|
|
174
|
+
log.info("Kh\u1EDFi \u0111\u1ED9ng \u0111\u0103ng nh\u1EADp Claude Code (browser s\u1EBD m\u1EDF)...");
|
|
175
|
+
const result = spawnSync("claude", ["auth", "login"], { stdio: "inherit" });
|
|
176
|
+
if (result.status !== 0) {
|
|
177
|
+
throw new Error(
|
|
178
|
+
`claude auth login th\u1EA5t b\u1EA1i (exit ${result.status}). Th\u1EED 'claude auth login' tay r\u1ED3i ch\u1EA1y l\u1EA1i.`
|
|
179
|
+
);
|
|
180
|
+
}
|
|
181
|
+
log.success("\u0110\xE3 \u0111\u0103ng nh\u1EADp Claude Code");
|
|
182
|
+
}
|
|
183
|
+
function classifyQuotaError(combinedOutput) {
|
|
184
|
+
const text = combinedOutput.toLowerCase();
|
|
185
|
+
if (text.includes("credit_balance_too_low") || text.includes("credit balance too low")) {
|
|
186
|
+
return "credit_balance_too_low";
|
|
187
|
+
}
|
|
188
|
+
if (text.includes("insufficient_quota") || text.includes("insufficient quota")) {
|
|
189
|
+
return "insufficient_quota";
|
|
190
|
+
}
|
|
191
|
+
if (text.includes("quota_exceeded") || text.includes("quota exceeded") || text.includes("usage limit") || text.includes("you've used all")) {
|
|
192
|
+
return "insufficient_quota";
|
|
193
|
+
}
|
|
194
|
+
if (text.includes("401") || text.includes("invalid authentication") || text.includes("authentication credentials") || text.includes("failed to authenticate") || text.includes("authentication failed") || text.includes("unauthorized")) {
|
|
195
|
+
return "auth-expired";
|
|
196
|
+
}
|
|
197
|
+
if (text.includes("invalid_api_key") || text.includes("invalid api key")) {
|
|
198
|
+
return "invalid_api_key";
|
|
199
|
+
}
|
|
200
|
+
if (text.includes("rate_limit") || text.includes("rate limit") || text.includes("429")) {
|
|
201
|
+
return "rate_limit";
|
|
202
|
+
}
|
|
203
|
+
return "unknown";
|
|
204
|
+
}
|
|
205
|
+
function getQuotaErrorHint(reason) {
|
|
206
|
+
switch (reason) {
|
|
207
|
+
case "auth-expired":
|
|
208
|
+
return "Token Claude Code \u0111\xE3 h\u1EBFt h\u1EA1n/b\u1ECB revoke. Ch\u1EA1y: `claude auth logout && claude auth login`.";
|
|
209
|
+
case "credit_balance_too_low":
|
|
210
|
+
case "insufficient_quota":
|
|
211
|
+
return "H\u1EBFt quota subscription. Upgrade plan ho\u1EB7c d\xF9ng LLMLite (avatar ai setup \u2192 ch\u1ECDn LLMLite).";
|
|
212
|
+
case "invalid_api_key":
|
|
213
|
+
return "API key invalid. Re-login: `claude auth login`.";
|
|
214
|
+
case "rate_limit":
|
|
215
|
+
return "B\u1ECB rate limit t\u1EA1m th\u1EDDi. Ch\u1EDD v\xE0i ph\xFAt r\u1ED3i ch\u1EA1y `avatar ai setup`.";
|
|
216
|
+
case "timeout":
|
|
217
|
+
return "Network ch\u1EADm ho\u1EB7c Anthropic API down. Check m\u1EA1ng r\u1ED3i ch\u1EA1y l\u1EA1i.";
|
|
218
|
+
default:
|
|
219
|
+
return "L\u1ED7i ch\u01B0a bi\u1EBFt. Xem stderr \u1EDF tr\xEAn + ch\u1EA1y `claude --print ok` tay \u0111\u1EC3 debug.";
|
|
220
|
+
}
|
|
221
|
+
}
|
|
222
|
+
function verifyClaudeCodeQuota() {
|
|
223
|
+
const result = spawnSync("claude", ["--print", QUOTA_VERIFY_PROMPT], {
|
|
224
|
+
encoding: "utf8",
|
|
225
|
+
timeout: QUOTA_VERIFY_TIMEOUT_MS
|
|
226
|
+
});
|
|
227
|
+
if (result.signal === "SIGTERM") {
|
|
228
|
+
return { ok: false, reason: "timeout", detail: "claude --print > 30s" };
|
|
229
|
+
}
|
|
230
|
+
const stderr = result.stderr || "";
|
|
231
|
+
const stdout = result.stdout || "";
|
|
232
|
+
if (result.status === 0) {
|
|
233
|
+
return { ok: true };
|
|
234
|
+
}
|
|
235
|
+
const reason = classifyQuotaError(`${stderr}
|
|
236
|
+
${stdout}`);
|
|
237
|
+
if (reason === "unknown" && stderr.trim()) {
|
|
238
|
+
log.dim(`[debug] claude --print stderr: ${stderr.slice(0, 500)}`);
|
|
239
|
+
}
|
|
240
|
+
return { ok: false, reason, detail: stderr.slice(0, 500) };
|
|
241
|
+
}
|
|
242
|
+
|
|
243
|
+
// src/lib/detect-claude-code-installation.ts
|
|
244
|
+
import { spawnSync as spawnSync2 } from "child_process";
|
|
245
|
+
|
|
246
|
+
// src/lib/detect-host-platform.ts
|
|
247
|
+
import { platform } from "os";
|
|
248
|
+
function detectHostPlatform() {
|
|
249
|
+
const p = platform();
|
|
250
|
+
if (p === "darwin" || p === "linux" || p === "win32") return p;
|
|
251
|
+
return "unsupported";
|
|
252
|
+
}
|
|
253
|
+
|
|
254
|
+
// src/lib/detect-claude-code-installation.ts
|
|
255
|
+
var VERSION_PROBE_TIMEOUT_MS = 5e3;
|
|
256
|
+
var SEMVER_REGEX = /(\d+\.\d+\.\d+)/;
|
|
257
|
+
function probeClaudeBinaryPath() {
|
|
258
|
+
const isWindows = detectHostPlatform() === "win32";
|
|
259
|
+
const probeCmd = isWindows ? "where" : "which";
|
|
260
|
+
const result = spawnSync2(probeCmd, ["claude"], { encoding: "utf8" });
|
|
261
|
+
if (result.error || result.status !== 0) return null;
|
|
262
|
+
const out = (result.stdout || "").trim();
|
|
263
|
+
if (!out) return null;
|
|
264
|
+
return out.split(/\r?\n/)[0].trim();
|
|
265
|
+
}
|
|
266
|
+
function probeClaudeVersion() {
|
|
267
|
+
const result = spawnSync2("claude", ["--version"], {
|
|
268
|
+
encoding: "utf8",
|
|
269
|
+
timeout: VERSION_PROBE_TIMEOUT_MS
|
|
270
|
+
});
|
|
271
|
+
if (result.error || result.status !== 0) return null;
|
|
272
|
+
const out = (result.stdout || "").trim();
|
|
273
|
+
const match = SEMVER_REGEX.exec(out);
|
|
274
|
+
return match ? match[1] : null;
|
|
275
|
+
}
|
|
276
|
+
function detectClaudeCodeInstallation() {
|
|
277
|
+
const path = probeClaudeBinaryPath();
|
|
278
|
+
if (!path) {
|
|
279
|
+
return { installed: false, version: null, path: null };
|
|
280
|
+
}
|
|
281
|
+
const version = probeClaudeVersion();
|
|
282
|
+
return { installed: true, version, path };
|
|
283
|
+
}
|
|
284
|
+
|
|
285
|
+
// src/lib/install-claude-code-via-npm.ts
|
|
286
|
+
import { spawnSync as spawnSync3 } from "child_process";
|
|
287
|
+
var NPM_INSTALL_TIMEOUT_MS = 5 * 60 * 1e3;
|
|
288
|
+
var CLAUDE_CODE_PACKAGE = "@anthropic-ai/claude-code";
|
|
289
|
+
var InstallClaudeCodeError = class extends Error {
|
|
290
|
+
reason;
|
|
291
|
+
exitCode;
|
|
292
|
+
constructor(reason, message, exitCode = null) {
|
|
293
|
+
super(message);
|
|
294
|
+
this.name = "InstallClaudeCodeError";
|
|
295
|
+
this.reason = reason;
|
|
296
|
+
this.exitCode = exitCode;
|
|
297
|
+
}
|
|
298
|
+
};
|
|
299
|
+
function classifyNpmFailure(exitCode, stderrSample) {
|
|
300
|
+
const stderr = stderrSample.toLowerCase();
|
|
301
|
+
if (stderr.includes("eacces") || stderr.includes("permission denied")) {
|
|
302
|
+
return new InstallClaudeCodeError(
|
|
303
|
+
"permission-denied",
|
|
304
|
+
`npm install -g c\u1EA7n quy\u1EC1n. Th\u1EED: sudo npm install -g ${CLAUDE_CODE_PACKAGE} ho\u1EB7c fix npm prefix (npm config set prefix ~/.npm-global).`,
|
|
305
|
+
exitCode
|
|
306
|
+
);
|
|
307
|
+
}
|
|
308
|
+
if (stderr.includes("enospc") || stderr.includes("no space")) {
|
|
309
|
+
return new InstallClaudeCodeError(
|
|
310
|
+
"disk-full",
|
|
311
|
+
"\u0110\u0129a \u0111\u1EA7y. Free disk space r\u1ED3i th\u1EED l\u1EA1i.",
|
|
312
|
+
exitCode
|
|
313
|
+
);
|
|
314
|
+
}
|
|
315
|
+
return new InstallClaudeCodeError(
|
|
316
|
+
"generic",
|
|
317
|
+
`npm install th\u1EA5t b\u1EA1i (exit ${exitCode ?? "null"}). Xem log npm ph\xEDa tr\xEAn.`,
|
|
318
|
+
exitCode
|
|
319
|
+
);
|
|
320
|
+
}
|
|
321
|
+
function installClaudeCodeViaNpm() {
|
|
322
|
+
log.info("\u0110ang c\xE0i Claude Code qua npm (c\xF3 th\u1EC3 m\u1EA5t 1-2 ph\xFAt)...");
|
|
323
|
+
const result = spawnSync3("npm", ["install", "-g", CLAUDE_CODE_PACKAGE], {
|
|
324
|
+
stdio: ["inherit", "inherit", "pipe"],
|
|
325
|
+
timeout: NPM_INSTALL_TIMEOUT_MS,
|
|
326
|
+
encoding: "utf8"
|
|
327
|
+
});
|
|
328
|
+
if (result.signal === "SIGTERM") {
|
|
329
|
+
throw new InstallClaudeCodeError(
|
|
330
|
+
"timeout",
|
|
331
|
+
`npm install timeout sau ${NPM_INSTALL_TIMEOUT_MS / 1e3}s. Check m\u1EA1ng r\u1ED3i th\u1EED l\u1EA1i.`,
|
|
332
|
+
null
|
|
333
|
+
);
|
|
334
|
+
}
|
|
335
|
+
if (result.status !== 0) {
|
|
336
|
+
if (result.stderr) process.stderr.write(result.stderr);
|
|
337
|
+
throw classifyNpmFailure(result.status, result.stderr || "");
|
|
338
|
+
}
|
|
339
|
+
const probe = detectClaudeCodeInstallation();
|
|
340
|
+
if (!probe.installed || !probe.path) {
|
|
341
|
+
throw new InstallClaudeCodeError(
|
|
342
|
+
"binary-not-in-path",
|
|
343
|
+
"npm c\xE0i xong nh\u01B0ng `claude` kh\xF4ng trong PATH. Reload shell (source ~/.zshrc) ho\u1EB7c th\xEAm npm global bin v\xE0o PATH.",
|
|
344
|
+
null
|
|
345
|
+
);
|
|
346
|
+
}
|
|
347
|
+
log.success(`\u0110\xE3 c\xE0i Claude Code${probe.version ? ` v${probe.version}` : ""} t\u1EA1i ${probe.path}`);
|
|
348
|
+
return { version: probe.version, path: probe.path };
|
|
349
|
+
}
|
|
350
|
+
|
|
351
|
+
// src/lib/prompt-ai-provider-choice.ts
|
|
352
|
+
import { readFileSync } from "fs";
|
|
353
|
+
import { homedir as homedir2 } from "os";
|
|
354
|
+
import { join as join3 } from "path";
|
|
355
|
+
import { select } from "@inquirer/prompts";
|
|
356
|
+
function getGlobalSettingsPath() {
|
|
357
|
+
return join3(homedir2(), ".claude", "settings.json");
|
|
358
|
+
}
|
|
359
|
+
function detectGlobalClaudeSettings() {
|
|
360
|
+
const path = getGlobalSettingsPath();
|
|
361
|
+
let raw;
|
|
362
|
+
try {
|
|
363
|
+
raw = readFileSync(path, "utf8");
|
|
364
|
+
} catch {
|
|
365
|
+
return { exists: false, hasBaseUrl: false, hasToken: false };
|
|
366
|
+
}
|
|
367
|
+
let parsed;
|
|
368
|
+
try {
|
|
369
|
+
parsed = JSON.parse(raw);
|
|
370
|
+
} catch {
|
|
371
|
+
return { exists: true, hasBaseUrl: false, hasToken: false };
|
|
372
|
+
}
|
|
373
|
+
const env = parsed.env || {};
|
|
374
|
+
const baseUrl = typeof env.ANTHROPIC_BASE_URL === "string" ? env.ANTHROPIC_BASE_URL : void 0;
|
|
375
|
+
const hasToken = typeof env.ANTHROPIC_AUTH_TOKEN === "string" && env.ANTHROPIC_AUTH_TOKEN.length > 0;
|
|
376
|
+
const model = typeof parsed.model === "string" ? parsed.model : void 0;
|
|
377
|
+
return {
|
|
378
|
+
exists: true,
|
|
379
|
+
hasBaseUrl: !!baseUrl,
|
|
380
|
+
baseUrl,
|
|
381
|
+
hasToken,
|
|
382
|
+
model,
|
|
383
|
+
rawSettings: parsed
|
|
384
|
+
};
|
|
385
|
+
}
|
|
386
|
+
async function promptAiProviderChoice(globalInfo = detectGlobalClaudeSettings()) {
|
|
387
|
+
if (globalInfo.exists && globalInfo.hasBaseUrl && globalInfo.hasToken) {
|
|
388
|
+
const choice = await select({
|
|
389
|
+
message: `Ph\xE1t hi\u1EC7n AI config global (base URL: ${globalInfo.baseUrl}). D\xF9ng cho project n\xE0y?`,
|
|
390
|
+
choices: [
|
|
391
|
+
{
|
|
392
|
+
name: "a. Yes \u2014 copy config global v\xE0o .claude/settings.json (per-project)",
|
|
393
|
+
value: "use-global"
|
|
394
|
+
},
|
|
395
|
+
{
|
|
396
|
+
name: "b. No \u2014 setup ri\xEAng (ch\u1ECDn provider kh\xE1c)",
|
|
397
|
+
value: "setup-fresh"
|
|
398
|
+
}
|
|
399
|
+
]
|
|
400
|
+
});
|
|
401
|
+
if (choice === "use-global") return "use-global";
|
|
402
|
+
}
|
|
403
|
+
return await select({
|
|
404
|
+
message: "Ch\u1ECDn provider cho AI features:",
|
|
405
|
+
choices: [
|
|
406
|
+
{
|
|
407
|
+
name: "1. Claude Code Subscription (d\xF9ng quota c\xE1 nh\xE2n Anthropic)",
|
|
408
|
+
value: "subscription"
|
|
409
|
+
},
|
|
410
|
+
{
|
|
411
|
+
name: "2. LLMLite API key (llm.nal.vn \u2014 NAL c\u1EA5p)",
|
|
412
|
+
value: "llmlite"
|
|
413
|
+
}
|
|
414
|
+
]
|
|
415
|
+
});
|
|
416
|
+
}
|
|
417
|
+
|
|
418
|
+
// src/lib/setup-llmlite-api-key-and-model.ts
|
|
419
|
+
import { input, password, select as select2 } from "@inquirer/prompts";
|
|
420
|
+
var DEFAULT_BASE_URL = "https://llm.nal.vn";
|
|
421
|
+
var FETCH_TIMEOUT_MS = 1e4;
|
|
422
|
+
function maskApiKey(key) {
|
|
423
|
+
if (key.length <= 8) return "sk-***";
|
|
424
|
+
return `${key.slice(0, 3)}...${key.slice(-4)}`;
|
|
425
|
+
}
|
|
426
|
+
async function promptApiKeyHidden() {
|
|
427
|
+
return await password({
|
|
428
|
+
message: "LLMLite API key (\u1EA9n input):",
|
|
429
|
+
mask: "*",
|
|
430
|
+
validate: (v) => v.trim().length > 0 ? true : "API key b\u1EAFt bu\u1ED9c"
|
|
431
|
+
});
|
|
432
|
+
}
|
|
433
|
+
async function promptBaseUrl(defaultUrl = DEFAULT_BASE_URL) {
|
|
434
|
+
const value = await input({
|
|
435
|
+
message: "LLMLite base URL:",
|
|
436
|
+
default: defaultUrl,
|
|
437
|
+
validate: (v) => /^https?:\/\//.test(v) ? true : "Ph\u1EA3i l\xE0 URL h\u1EE3p l\u1EC7 (http/https)"
|
|
438
|
+
});
|
|
439
|
+
return value.replace(/\/+$/, "");
|
|
440
|
+
}
|
|
441
|
+
async function fetchAvailableModels(baseUrl, apiKey) {
|
|
442
|
+
const controller = new AbortController();
|
|
443
|
+
const timer = setTimeout(() => controller.abort(), FETCH_TIMEOUT_MS);
|
|
444
|
+
try {
|
|
445
|
+
const res = await fetch(`${baseUrl}/v1/models`, {
|
|
446
|
+
method: "GET",
|
|
447
|
+
headers: {
|
|
448
|
+
Authorization: `Bearer ${apiKey}`,
|
|
449
|
+
Accept: "application/json"
|
|
450
|
+
},
|
|
451
|
+
signal: controller.signal
|
|
452
|
+
});
|
|
453
|
+
if (res.status === 401 || res.status === 403) {
|
|
454
|
+
throw new Error(`API key invalid (HTTP ${res.status}).`);
|
|
455
|
+
}
|
|
456
|
+
if (res.status === 404) {
|
|
457
|
+
throw new Error(`Endpoint /v1/models kh\xF4ng t\u1ED3n t\u1EA1i tr\xEAn ${baseUrl}.`);
|
|
458
|
+
}
|
|
459
|
+
if (!res.ok) {
|
|
460
|
+
throw new Error(`Fetch models th\u1EA5t b\u1EA1i (HTTP ${res.status}).`);
|
|
461
|
+
}
|
|
462
|
+
const json = await res.json();
|
|
463
|
+
const models = (json.data || []).map((m) => typeof m.id === "string" ? m.id : null).filter((id) => id !== null);
|
|
464
|
+
if (models.length === 0) {
|
|
465
|
+
throw new Error("LLMLite tr\u1EA3 v\u1EC1 list r\u1ED7ng. Li\xEAn h\u1EC7 admin NAL.");
|
|
466
|
+
}
|
|
467
|
+
return models;
|
|
468
|
+
} catch (err) {
|
|
469
|
+
if (err.name === "AbortError") {
|
|
470
|
+
throw new Error(`Connect ${baseUrl} timeout sau ${FETCH_TIMEOUT_MS / 1e3}s.`);
|
|
471
|
+
}
|
|
472
|
+
throw err;
|
|
473
|
+
} finally {
|
|
474
|
+
clearTimeout(timer);
|
|
475
|
+
}
|
|
476
|
+
}
|
|
477
|
+
async function promptModelChoice(models) {
|
|
478
|
+
const claudeAliases = models.filter((m) => m.toLowerCase().includes("claude"));
|
|
479
|
+
if (claudeAliases.length === 1) {
|
|
480
|
+
log.info(`Auto-pick model: ${claudeAliases[0]} (ch\u1EC9 1 claude alias tr\xEAn endpoint)`);
|
|
481
|
+
return claudeAliases[0];
|
|
482
|
+
}
|
|
483
|
+
const choiceList = claudeAliases.length > 0 ? claudeAliases : models;
|
|
484
|
+
return await select2({
|
|
485
|
+
message: "Ch\u1ECDn model m\u1EB7c \u0111\u1ECBnh cho project:",
|
|
486
|
+
choices: choiceList.map((m) => ({ name: m, value: m }))
|
|
487
|
+
});
|
|
488
|
+
}
|
|
489
|
+
async function setupLLMLiteApiKeyAndModel() {
|
|
490
|
+
const apiKey = await promptApiKeyHidden();
|
|
491
|
+
const baseUrl = await promptBaseUrl();
|
|
492
|
+
log.info(`Verify key (${maskApiKey(apiKey)}) qua ${baseUrl}/v1/models...`);
|
|
493
|
+
const models = await fetchAvailableModels(baseUrl, apiKey);
|
|
494
|
+
log.success(`Endpoint OK \u2014 ${models.length} models available`);
|
|
495
|
+
const model = await promptModelChoice(models);
|
|
496
|
+
return { apiKey, baseUrl, model };
|
|
497
|
+
}
|
|
498
|
+
|
|
499
|
+
// src/lib/write-claude-settings-json-per-project.ts
|
|
500
|
+
import { promises as fs3 } from "fs";
|
|
501
|
+
import { join as join4 } from "path";
|
|
502
|
+
var SECRET_FILE_MODE2 = 384;
|
|
503
|
+
function getClaudeSettingsPath(workspacePath) {
|
|
504
|
+
return join4(workspacePath, ".claude", "settings.json");
|
|
505
|
+
}
|
|
506
|
+
async function readExistingSettings(path) {
|
|
507
|
+
if (!await pathExists(path)) return {};
|
|
508
|
+
try {
|
|
509
|
+
return await readJson(path);
|
|
510
|
+
} catch (err) {
|
|
511
|
+
throw new Error(
|
|
512
|
+
`Kh\xF4ng parse \u0111\u01B0\u1EE3c ${path} (JSON l\u1ED7i): ${err.message}. Backup file r\u1ED3i x\xF3a \u0111\u1EC3 Avatar t\u1EA1o l\u1EA1i.`
|
|
513
|
+
);
|
|
514
|
+
}
|
|
515
|
+
}
|
|
516
|
+
function applySubscription(existing, model) {
|
|
517
|
+
const { env: existingEnv, ...rest } = existing;
|
|
518
|
+
const merged = { ...rest, model };
|
|
519
|
+
if (existingEnv) {
|
|
520
|
+
const { ANTHROPIC_BASE_URL: _b, ANTHROPIC_AUTH_TOKEN: _t, ...envRest } = existingEnv;
|
|
521
|
+
if (Object.keys(envRest).length > 0) {
|
|
522
|
+
merged.env = envRest;
|
|
523
|
+
}
|
|
524
|
+
}
|
|
525
|
+
return merged;
|
|
526
|
+
}
|
|
527
|
+
function applyLLMLite(existing, apiKey, baseUrl, model) {
|
|
528
|
+
return {
|
|
529
|
+
...existing,
|
|
530
|
+
env: {
|
|
531
|
+
...existing.env || {},
|
|
532
|
+
ANTHROPIC_BASE_URL: baseUrl,
|
|
533
|
+
ANTHROPIC_AUTH_TOKEN: apiKey
|
|
534
|
+
},
|
|
535
|
+
model
|
|
536
|
+
};
|
|
537
|
+
}
|
|
538
|
+
function applyUseGlobal(existing, source) {
|
|
539
|
+
const sourceEnv = source.env || {};
|
|
540
|
+
const sourceModel = typeof source.model === "string" ? source.model : void 0;
|
|
541
|
+
return {
|
|
542
|
+
...existing,
|
|
543
|
+
env: {
|
|
544
|
+
...existing.env || {},
|
|
545
|
+
...sourceEnv
|
|
546
|
+
},
|
|
547
|
+
...sourceModel ? { model: sourceModel } : {}
|
|
548
|
+
};
|
|
549
|
+
}
|
|
550
|
+
async function writeClaudeSettings(workspacePath, input3) {
|
|
551
|
+
const path = getClaudeSettingsPath(workspacePath);
|
|
552
|
+
const existing = await readExistingSettings(path);
|
|
553
|
+
let merged;
|
|
554
|
+
switch (input3.provider) {
|
|
555
|
+
case "subscription":
|
|
556
|
+
merged = applySubscription(existing, input3.model);
|
|
557
|
+
break;
|
|
558
|
+
case "llmlite":
|
|
559
|
+
merged = applyLLMLite(existing, input3.apiKey, input3.baseUrl, input3.model);
|
|
560
|
+
break;
|
|
561
|
+
case "use-global":
|
|
562
|
+
merged = applyUseGlobal(existing, input3.sourceSettings);
|
|
563
|
+
break;
|
|
564
|
+
}
|
|
565
|
+
await writeJsonAtomic(path, merged, SECRET_FILE_MODE2);
|
|
566
|
+
try {
|
|
567
|
+
await fs3.chmod(path, SECRET_FILE_MODE2);
|
|
568
|
+
} catch {
|
|
569
|
+
}
|
|
570
|
+
return { path, mode: SECRET_FILE_MODE2 };
|
|
571
|
+
}
|
|
572
|
+
|
|
573
|
+
// src/lib/run-ai-setup-phase.ts
|
|
574
|
+
var SUBSCRIPTION_DEFAULT_MODEL = "sonnet";
|
|
575
|
+
async function runAiSetupPhase(args) {
|
|
576
|
+
try {
|
|
577
|
+
log.info("Setup AI provider cho workspace...");
|
|
578
|
+
let info = detectClaudeCodeInstallation();
|
|
579
|
+
if (!info.installed) {
|
|
580
|
+
log.info("Ch\u01B0a c\xF3 Claude Code \u2014 s\u1EBD t\u1EF1 c\xE0i qua npm.");
|
|
581
|
+
installClaudeCodeViaNpm();
|
|
582
|
+
info = detectClaudeCodeInstallation();
|
|
583
|
+
if (!info.installed) {
|
|
584
|
+
throw new Error("C\xE0i Claude Code xong nh\u01B0ng v\u1EABn kh\xF4ng detect \u0111\u01B0\u1EE3c binary.");
|
|
585
|
+
}
|
|
586
|
+
} else {
|
|
587
|
+
log.success(`Claude Code \u0111\xE3 c\xF3${info.version ? ` v${info.version}` : ""}`);
|
|
588
|
+
}
|
|
589
|
+
const globalInfo = detectGlobalClaudeSettings();
|
|
590
|
+
const choice = await promptAiProviderChoice(globalInfo);
|
|
591
|
+
switch (choice) {
|
|
592
|
+
case "subscription": {
|
|
593
|
+
if (checkClaudeCodeSubscriptionAuth() !== "authenticated") {
|
|
594
|
+
triggerClaudeCodeAuthLogin();
|
|
595
|
+
}
|
|
596
|
+
let quota = verifyClaudeCodeQuota();
|
|
597
|
+
if (!quota.ok && quota.reason === "auth-expired") {
|
|
598
|
+
log.warn("Token Claude Code \u0111\xE3 h\u1EBFt h\u1EA1n. T\u1EF1 \u0111\u1ED9ng re-login...");
|
|
599
|
+
triggerClaudeCodeAuthLogin();
|
|
600
|
+
quota = verifyClaudeCodeQuota();
|
|
601
|
+
}
|
|
602
|
+
if (!quota.ok) {
|
|
603
|
+
const reason = quota.reason ?? "unknown";
|
|
604
|
+
await appendAuditEntry(
|
|
605
|
+
"ai_setup",
|
|
606
|
+
`provider=subscription,result=no-quota,reason=${reason}`
|
|
607
|
+
);
|
|
608
|
+
log.warn(`Subscription verify th\u1EA5t b\u1EA1i (${reason}).`);
|
|
609
|
+
log.dim(`\u2192 ${getQuotaErrorHint(reason)}`);
|
|
610
|
+
return { ok: false, reason: `subscription-${reason}`, phase: "quota" };
|
|
611
|
+
}
|
|
612
|
+
await writeClaudeSettings(args.workspacePath, {
|
|
613
|
+
provider: "subscription",
|
|
614
|
+
model: SUBSCRIPTION_DEFAULT_MODEL
|
|
615
|
+
});
|
|
616
|
+
await appendAuditEntry("ai_setup", "provider=subscription,result=ok");
|
|
617
|
+
log.success(`AI ready \xB7 Subscription \xB7 model=${SUBSCRIPTION_DEFAULT_MODEL}`);
|
|
618
|
+
return { ok: true, provider: "subscription", model: SUBSCRIPTION_DEFAULT_MODEL };
|
|
619
|
+
}
|
|
620
|
+
case "llmlite": {
|
|
621
|
+
const llmConfig = await setupLLMLiteApiKeyAndModel();
|
|
622
|
+
await writeClaudeSettings(args.workspacePath, {
|
|
623
|
+
provider: "llmlite",
|
|
624
|
+
apiKey: llmConfig.apiKey,
|
|
625
|
+
baseUrl: llmConfig.baseUrl,
|
|
626
|
+
model: llmConfig.model
|
|
627
|
+
});
|
|
628
|
+
await appendAuditEntry(
|
|
629
|
+
"ai_setup",
|
|
630
|
+
`provider=llmlite,result=ok,model=${llmConfig.model},base=${llmConfig.baseUrl}`
|
|
631
|
+
);
|
|
632
|
+
log.success(`AI ready \xB7 LLMLite \xB7 model=${llmConfig.model} \xB7 ${llmConfig.baseUrl}`);
|
|
633
|
+
return { ok: true, provider: "llmlite", model: llmConfig.model };
|
|
634
|
+
}
|
|
635
|
+
case "use-global": {
|
|
636
|
+
if (!globalInfo.rawSettings) {
|
|
637
|
+
throw new Error("use-global ch\u1ECDn nh\u01B0ng kh\xF4ng \u0111\u1ECDc \u0111\u01B0\u1EE3c global settings.");
|
|
638
|
+
}
|
|
639
|
+
await writeClaudeSettings(args.workspacePath, {
|
|
640
|
+
provider: "use-global",
|
|
641
|
+
sourceSettings: globalInfo.rawSettings
|
|
642
|
+
});
|
|
643
|
+
await appendAuditEntry("ai_setup", "provider=use-global,result=ok");
|
|
644
|
+
log.success(`AI ready \xB7 Copy t\u1EEB global config (${globalInfo.baseUrl ?? "subscription"})`);
|
|
645
|
+
return { ok: true, provider: "use-global", model: globalInfo.model };
|
|
646
|
+
}
|
|
647
|
+
}
|
|
648
|
+
} catch (err) {
|
|
649
|
+
const message = err instanceof Error ? err.message : String(err);
|
|
650
|
+
log.warn(`AI setup th\u1EA5t b\u1EA1i: ${message}`);
|
|
651
|
+
log.dim("Workspace v\u1EABn s\u1EB5n s\xE0ng. Setup AI sau qua: avatar ai setup");
|
|
652
|
+
await appendAuditEntry("ai_setup", `result=failed,error=${message.slice(0, 200)}`);
|
|
653
|
+
return { ok: false, reason: message };
|
|
654
|
+
}
|
|
655
|
+
}
|
|
656
|
+
|
|
657
|
+
// src/commands/ai.ts
|
|
658
|
+
async function ensureWorkspaceCwd() {
|
|
659
|
+
const cwd = process.cwd();
|
|
660
|
+
const claudeDir = join5(cwd, ".claude");
|
|
661
|
+
if (!await pathExists(claudeDir)) {
|
|
662
|
+
log.error("Kh\xF4ng th\u1EA5y .claude/ trong th\u01B0 m\u1EE5c hi\u1EC7n t\u1EA1i. Ch\u1EA1y l\u1EC7nh n\xE0y trong workspace Avatar.");
|
|
663
|
+
process.exit(1);
|
|
664
|
+
}
|
|
665
|
+
return cwd;
|
|
666
|
+
}
|
|
667
|
+
async function readWorkspaceSettings(workspacePath) {
|
|
668
|
+
const settingsPath = join5(workspacePath, ".claude", "settings.json");
|
|
669
|
+
if (!await pathExists(settingsPath)) return {};
|
|
670
|
+
try {
|
|
671
|
+
return await readJson(settingsPath);
|
|
672
|
+
} catch {
|
|
673
|
+
return {};
|
|
674
|
+
}
|
|
675
|
+
}
|
|
676
|
+
async function runAiSetup() {
|
|
677
|
+
const workspacePath = await ensureWorkspaceCwd();
|
|
678
|
+
await runAiSetupPhase({ workspacePath });
|
|
679
|
+
}
|
|
680
|
+
async function runAiStatus() {
|
|
681
|
+
const workspacePath = await ensureWorkspaceCwd();
|
|
682
|
+
const settings = await readWorkspaceSettings(workspacePath);
|
|
683
|
+
const env = settings.env || {};
|
|
684
|
+
const baseUrl = typeof env.ANTHROPIC_BASE_URL === "string" ? env.ANTHROPIC_BASE_URL : void 0;
|
|
685
|
+
const token = typeof env.ANTHROPIC_AUTH_TOKEN === "string" ? env.ANTHROPIC_AUTH_TOKEN : void 0;
|
|
686
|
+
const model = typeof settings.model === "string" ? settings.model : void 0;
|
|
687
|
+
const provider = baseUrl ? "LLMLite" : token ? "Custom" : "Subscription (default)";
|
|
688
|
+
log.info(`Project: ${workspacePath}`);
|
|
689
|
+
log.info(`Provider: ${provider}${baseUrl ? ` (${baseUrl})` : ""}`);
|
|
690
|
+
log.info(`Model: ${model ?? "(default \u2014 Claude Code ch\u1ECDn)"}`);
|
|
691
|
+
log.info(`Token: ${token ? maskApiKey(token) : "(kh\xF4ng set \u2014 d\xF9ng subscription auth)"}`);
|
|
692
|
+
}
|
|
693
|
+
async function runAiTest() {
|
|
694
|
+
await ensureWorkspaceCwd();
|
|
695
|
+
log.info("Calling provider v\u1EDBi prompt 'say ok'...");
|
|
696
|
+
const result = spawnSync4("claude", ["--print", "say ok"], {
|
|
697
|
+
encoding: "utf8",
|
|
698
|
+
timeout: 3e4
|
|
699
|
+
});
|
|
700
|
+
if (result.signal === "SIGTERM") {
|
|
701
|
+
log.error("Timeout sau 30s. Check m\u1EA1ng / endpoint.");
|
|
702
|
+
process.exit(1);
|
|
703
|
+
}
|
|
704
|
+
if (result.status !== 0) {
|
|
705
|
+
log.error(`Test th\u1EA5t b\u1EA1i (exit ${result.status}).`);
|
|
706
|
+
if (result.stderr) process.stderr.write(`${result.stderr}
|
|
707
|
+
`);
|
|
708
|
+
process.exit(1);
|
|
709
|
+
}
|
|
710
|
+
log.success(`Response: ${(result.stdout || "").trim().slice(0, 200)}`);
|
|
711
|
+
log.success("AI provider working");
|
|
712
|
+
}
|
|
713
|
+
async function runAiReset(opts) {
|
|
714
|
+
const workspacePath = await ensureWorkspaceCwd();
|
|
715
|
+
const settingsPath = join5(workspacePath, ".claude", "settings.json");
|
|
716
|
+
const settings = await readWorkspaceSettings(workspacePath);
|
|
717
|
+
if (!opts.yes) {
|
|
718
|
+
const ok = await confirm({
|
|
719
|
+
message: "X\xF3a AI config (v\u1EC1 d\xF9ng Claude Code Subscription default)?",
|
|
720
|
+
default: false
|
|
721
|
+
});
|
|
722
|
+
if (!ok) {
|
|
723
|
+
log.dim("\u0110\xE3 h\u1EE7y.");
|
|
724
|
+
return;
|
|
725
|
+
}
|
|
726
|
+
}
|
|
727
|
+
const { env: existingEnv, ...rest } = settings;
|
|
728
|
+
const reset = { ...rest };
|
|
729
|
+
if (existingEnv) {
|
|
730
|
+
const { ANTHROPIC_BASE_URL: _b, ANTHROPIC_AUTH_TOKEN: _t, ...envRest } = existingEnv;
|
|
731
|
+
if (Object.keys(envRest).length > 0) {
|
|
732
|
+
reset.env = envRest;
|
|
733
|
+
}
|
|
734
|
+
}
|
|
735
|
+
if (Object.keys(reset).length === 0) {
|
|
736
|
+
await fs4.unlink(settingsPath).catch(() => {
|
|
737
|
+
});
|
|
738
|
+
log.success("\u0110\xE3 x\xF3a .claude/settings.json (clean state)");
|
|
739
|
+
} else {
|
|
740
|
+
await writeJsonAtomic(settingsPath, reset, 384);
|
|
741
|
+
log.success("\u0110\xE3 reset env block trong .claude/settings.json");
|
|
742
|
+
}
|
|
743
|
+
}
|
|
744
|
+
function registerAiCommand(program2) {
|
|
745
|
+
const ai = program2.command("ai").description("Qu\u1EA3n l\xFD AI provider config (M12)");
|
|
746
|
+
ai.command("setup").description("Wizard setup/re-config AI provider cho workspace hi\u1EC7n t\u1EA1i").action(async () => {
|
|
747
|
+
await runAiSetup();
|
|
748
|
+
});
|
|
749
|
+
ai.command("status").description("Show AI config hi\u1EC7n t\u1EA1i (mask token)").action(async () => {
|
|
750
|
+
await runAiStatus();
|
|
751
|
+
});
|
|
752
|
+
ai.command("test").description("Verify AI provider qua cheap prompt").action(async () => {
|
|
753
|
+
await runAiTest();
|
|
754
|
+
});
|
|
755
|
+
ai.command("reset").description("X\xF3a env.ANTHROPIC_* kh\u1ECFi settings.json (v\u1EC1 Subscription default)").option("--yes", "Skip confirm").action(async (opts) => {
|
|
756
|
+
await runAiReset(opts);
|
|
757
|
+
});
|
|
758
|
+
}
|
|
759
|
+
|
|
31
760
|
// src/lib/not-implemented-stub.ts
|
|
32
761
|
function notImplementedYet(commandName, milestone) {
|
|
33
762
|
return () => {
|
|
@@ -50,59 +779,25 @@ function registerCommitCommand(program2) {
|
|
|
50
779
|
}
|
|
51
780
|
|
|
52
781
|
// src/commands/doctor.ts
|
|
53
|
-
import { spawnSync } from "child_process";
|
|
54
|
-
import { promises as
|
|
55
|
-
import { join as
|
|
782
|
+
import { spawnSync as spawnSync5 } from "child_process";
|
|
783
|
+
import { promises as fs6 } from "fs";
|
|
784
|
+
import { join as join9 } from "path";
|
|
56
785
|
import boxen from "boxen";
|
|
57
786
|
|
|
58
|
-
// src/lib/filesystem-helpers.ts
|
|
59
|
-
import { constants, promises as fs } from "fs";
|
|
60
|
-
import { dirname, join, relative } from "path";
|
|
61
|
-
async function pathExists(path) {
|
|
62
|
-
try {
|
|
63
|
-
await fs.access(path, constants.F_OK);
|
|
64
|
-
return true;
|
|
65
|
-
} catch {
|
|
66
|
-
return false;
|
|
67
|
-
}
|
|
68
|
-
}
|
|
69
|
-
async function ensureDir(path) {
|
|
70
|
-
await fs.mkdir(path, { recursive: true });
|
|
71
|
-
}
|
|
72
|
-
async function readText(path) {
|
|
73
|
-
return await fs.readFile(path, "utf8");
|
|
74
|
-
}
|
|
75
|
-
async function readJson(path) {
|
|
76
|
-
return JSON.parse(await readText(path));
|
|
77
|
-
}
|
|
78
|
-
async function writeTextAtomic(path, content, mode) {
|
|
79
|
-
await ensureDir(dirname(path));
|
|
80
|
-
const tmp = `${path}.tmp-${process.pid}-${Date.now()}`;
|
|
81
|
-
await fs.writeFile(tmp, content, "utf8");
|
|
82
|
-
if (mode !== void 0) {
|
|
83
|
-
await fs.chmod(tmp, mode);
|
|
84
|
-
}
|
|
85
|
-
await fs.rename(tmp, path);
|
|
86
|
-
}
|
|
87
|
-
async function writeJsonAtomic(path, data, mode) {
|
|
88
|
-
await writeTextAtomic(path, `${JSON.stringify(data, null, 2)}
|
|
89
|
-
`, mode);
|
|
90
|
-
}
|
|
91
|
-
|
|
92
787
|
// src/lib/git-operations.ts
|
|
93
|
-
import { join as
|
|
788
|
+
import { join as join6 } from "path";
|
|
94
789
|
import { simpleGit } from "simple-git";
|
|
95
790
|
function git(cwd = process.cwd()) {
|
|
96
791
|
return simpleGit({ baseDir: cwd, binary: "git" });
|
|
97
792
|
}
|
|
98
793
|
async function isGitRepo(cwd = process.cwd()) {
|
|
99
|
-
return await pathExists(
|
|
794
|
+
return await pathExists(join6(cwd, ".git"));
|
|
100
795
|
}
|
|
101
796
|
async function addSubmodule(repoUrl, destPath, cwd = process.cwd()) {
|
|
102
797
|
await git(cwd).subModule(["add", repoUrl, destPath]);
|
|
103
798
|
}
|
|
104
799
|
async function checkoutTagInSubmodule(submodulePath, tag, cwd = process.cwd()) {
|
|
105
|
-
const submoduleCwd =
|
|
800
|
+
const submoduleCwd = join6(cwd, submodulePath);
|
|
106
801
|
await git(submoduleCwd).fetch(["--tags"]);
|
|
107
802
|
await git(submoduleCwd).checkout(tag);
|
|
108
803
|
}
|
|
@@ -120,12 +815,12 @@ async function currentCommitSha(cwd = process.cwd()) {
|
|
|
120
815
|
}
|
|
121
816
|
|
|
122
817
|
// src/lib/project-tree-scaffolder.ts
|
|
123
|
-
import { promises as
|
|
124
|
-
import { join as
|
|
818
|
+
import { promises as fs5 } from "fs";
|
|
819
|
+
import { join as join8 } from "path";
|
|
125
820
|
|
|
126
821
|
// src/lib/template-bundle-loader.ts
|
|
127
822
|
import { existsSync } from "fs";
|
|
128
|
-
import { dirname as dirname2, join as
|
|
823
|
+
import { dirname as dirname2, join as join7 } from "path";
|
|
129
824
|
import { fileURLToPath } from "url";
|
|
130
825
|
|
|
131
826
|
// src/lib/mustache-template-engine.ts
|
|
@@ -141,12 +836,12 @@ function renderTemplate(source, variables) {
|
|
|
141
836
|
// src/lib/template-bundle-loader.ts
|
|
142
837
|
var HERE = dirname2(fileURLToPath(import.meta.url));
|
|
143
838
|
var PACKAGE_ROOT = findPackageRoot(HERE);
|
|
144
|
-
var TEMPLATES_ROOT =
|
|
145
|
-
var HOOKS_ROOT =
|
|
839
|
+
var TEMPLATES_ROOT = join7(PACKAGE_ROOT, "src", "templates");
|
|
840
|
+
var HOOKS_ROOT = join7(PACKAGE_ROOT, "src", "hooks");
|
|
146
841
|
function findPackageRoot(startDir) {
|
|
147
842
|
let dir = startDir;
|
|
148
843
|
while (true) {
|
|
149
|
-
if (existsSync(
|
|
844
|
+
if (existsSync(join7(dir, "package.json"))) return dir;
|
|
150
845
|
const parent = dirname2(dir);
|
|
151
846
|
if (parent === dir) {
|
|
152
847
|
throw new Error(`Cannot locate package root from ${startDir}`);
|
|
@@ -155,14 +850,14 @@ function findPackageRoot(startDir) {
|
|
|
155
850
|
}
|
|
156
851
|
}
|
|
157
852
|
async function loadTemplate(name) {
|
|
158
|
-
return await readText(
|
|
853
|
+
return await readText(join7(TEMPLATES_ROOT, `${name}.tpl`));
|
|
159
854
|
}
|
|
160
855
|
async function renderTemplateByName(name, variables) {
|
|
161
856
|
const source = await loadTemplate(name);
|
|
162
857
|
return renderTemplate(source, variables);
|
|
163
858
|
}
|
|
164
859
|
async function loadHook(name) {
|
|
165
|
-
return await readText(
|
|
860
|
+
return await readText(join7(HOOKS_ROOT, `${name}.sh.tpl`));
|
|
166
861
|
}
|
|
167
862
|
|
|
168
863
|
// src/lib/project-tree-scaffolder.ts
|
|
@@ -179,7 +874,7 @@ async function backupIfExists(path) {
|
|
|
179
874
|
throw new Error(`Could not find free backup name for ${path}`);
|
|
180
875
|
}
|
|
181
876
|
}
|
|
182
|
-
await
|
|
877
|
+
await fs5.rename(path, backupPath);
|
|
183
878
|
return backupPath;
|
|
184
879
|
}
|
|
185
880
|
async function writeWithBackup(path, content, mode) {
|
|
@@ -196,12 +891,12 @@ var PROJECT_KNOWLEDGE_TEMPLATES = [
|
|
|
196
891
|
"project/gotchas.md"
|
|
197
892
|
];
|
|
198
893
|
async function createClaudeDirTree(projectRoot) {
|
|
199
|
-
const claudeRoot =
|
|
894
|
+
const claudeRoot = join8(projectRoot, ".claude");
|
|
200
895
|
await ensureDir(claudeRoot);
|
|
201
896
|
for (const sub of CLAUDE_SUBDIRS) {
|
|
202
|
-
const dir =
|
|
897
|
+
const dir = join8(claudeRoot, sub);
|
|
203
898
|
await ensureDir(dir);
|
|
204
|
-
await writeTextAtomic(
|
|
899
|
+
await writeTextAtomic(join8(dir, ".gitkeep"), "");
|
|
205
900
|
}
|
|
206
901
|
}
|
|
207
902
|
async function writeProjectKnowledgeFiles(projectRoot, vars) {
|
|
@@ -232,7 +927,7 @@ async function writeProjectKnowledgeFiles(projectRoot, vars) {
|
|
|
232
927
|
for (const tpl of PROJECT_KNOWLEDGE_TEMPLATES) {
|
|
233
928
|
const content = await renderTemplateByName(tpl, baseVars);
|
|
234
929
|
const relative4 = tpl.replace(/^project\//, "");
|
|
235
|
-
const outPath =
|
|
930
|
+
const outPath = join8(projectRoot, ".claude", "project", relative4);
|
|
236
931
|
const backup = await writeWithBackup(outPath, content);
|
|
237
932
|
if (backup) backups.push(backup);
|
|
238
933
|
}
|
|
@@ -240,19 +935,19 @@ async function writeProjectKnowledgeFiles(projectRoot, vars) {
|
|
|
240
935
|
}
|
|
241
936
|
async function writeRootClaudeMd(projectRoot, vars) {
|
|
242
937
|
const content = await renderTemplateByName("CLAUDE.md", vars);
|
|
243
|
-
return await writeWithBackup(
|
|
938
|
+
return await writeWithBackup(join8(projectRoot, "CLAUDE.md"), content);
|
|
244
939
|
}
|
|
245
940
|
async function writeProjectSettings(projectRoot, vars) {
|
|
246
941
|
const content = await renderTemplateByName("settings.json", vars);
|
|
247
|
-
return await writeWithBackup(
|
|
942
|
+
return await writeWithBackup(join8(projectRoot, ".claude", "settings.json"), content);
|
|
248
943
|
}
|
|
249
944
|
async function appendGitignoreEntries(projectRoot) {
|
|
250
|
-
const path =
|
|
945
|
+
const path = join8(projectRoot, ".gitignore");
|
|
251
946
|
const tpl = await renderTemplateByName("gitignore", {});
|
|
252
947
|
const marker = "# Avatar \u2014 git-ignored entries injected on `avatar init`";
|
|
253
948
|
let existing = "";
|
|
254
949
|
if (await pathExists(path)) {
|
|
255
|
-
existing = await
|
|
950
|
+
existing = await fs5.readFile(path, "utf8");
|
|
256
951
|
if (existing.includes(marker)) return;
|
|
257
952
|
}
|
|
258
953
|
const separator = existing.endsWith("\n") || existing.length === 0 ? "" : "\n";
|
|
@@ -261,78 +956,12 @@ ${tpl}`);
|
|
|
261
956
|
}
|
|
262
957
|
async function installGitHook(gitDir, hookName) {
|
|
263
958
|
const content = await loadHook(hookName);
|
|
264
|
-
const hooksDir =
|
|
959
|
+
const hooksDir = join8(gitDir, "hooks");
|
|
265
960
|
await ensureDir(hooksDir);
|
|
266
|
-
const dest =
|
|
961
|
+
const dest = join8(hooksDir, hookName);
|
|
267
962
|
await writeTextAtomic(dest, content, 493);
|
|
268
963
|
}
|
|
269
964
|
|
|
270
|
-
// src/lib/user-config-store.ts
|
|
271
|
-
import { homedir } from "os";
|
|
272
|
-
import { join as join5 } from "path";
|
|
273
|
-
|
|
274
|
-
// src/types/config-schema.ts
|
|
275
|
-
import { z } from "zod";
|
|
276
|
-
var userConfigSchema = z.object({
|
|
277
|
-
email: z.string().email(),
|
|
278
|
-
name: z.string(),
|
|
279
|
-
access_token: z.string().min(1),
|
|
280
|
-
refresh_token: z.string().min(1),
|
|
281
|
-
expires_at: z.string().datetime(),
|
|
282
|
-
id_token: z.string().min(1)
|
|
283
|
-
});
|
|
284
|
-
var userStateSchema = z.object({
|
|
285
|
-
installed_tools: z.record(
|
|
286
|
-
z.string(),
|
|
287
|
-
z.object({
|
|
288
|
-
version: z.string().optional(),
|
|
289
|
-
installed_at: z.string().datetime(),
|
|
290
|
-
install_method: z.string()
|
|
291
|
-
})
|
|
292
|
-
).default({}),
|
|
293
|
-
tool_inputs: z.record(z.string(), z.unknown()).default({})
|
|
294
|
-
});
|
|
295
|
-
var projectSettingsSchema = z.object({
|
|
296
|
-
allowedTools: z.array(z.string()),
|
|
297
|
-
hooks: z.object({
|
|
298
|
-
PostToolUse: z.array(z.unknown()).optional()
|
|
299
|
-
}).partial().optional(),
|
|
300
|
-
env: z.record(z.string(), z.string()).default({})
|
|
301
|
-
});
|
|
302
|
-
var initModeSchema = z.enum(["internal", "client", "library"]);
|
|
303
|
-
|
|
304
|
-
// src/lib/user-config-store.ts
|
|
305
|
-
var AVATAR_HOME = join5(homedir(), ".avatar");
|
|
306
|
-
var USER_CONFIG_PATH = join5(AVATAR_HOME, "config.json");
|
|
307
|
-
var USER_STATE_PATH = join5(AVATAR_HOME, "state.json");
|
|
308
|
-
var AUDIT_LOG_PATH = join5(AVATAR_HOME, "audit.log");
|
|
309
|
-
var BACKUPS_DIR = join5(AVATAR_HOME, "backups");
|
|
310
|
-
var SECRET_FILE_MODE = 384;
|
|
311
|
-
async function ensureAvatarHome() {
|
|
312
|
-
await ensureDir(AVATAR_HOME);
|
|
313
|
-
}
|
|
314
|
-
async function readUserConfig() {
|
|
315
|
-
if (!await pathExists(USER_CONFIG_PATH)) return null;
|
|
316
|
-
const raw = await readJson(USER_CONFIG_PATH);
|
|
317
|
-
const parsed = userConfigSchema.safeParse(raw);
|
|
318
|
-
if (!parsed.success) return null;
|
|
319
|
-
return parsed.data;
|
|
320
|
-
}
|
|
321
|
-
async function writeUserConfig(config) {
|
|
322
|
-
await ensureAvatarHome();
|
|
323
|
-
await writeJsonAtomic(USER_CONFIG_PATH, config, SECRET_FILE_MODE);
|
|
324
|
-
}
|
|
325
|
-
async function clearUserConfig() {
|
|
326
|
-
if (await pathExists(USER_CONFIG_PATH)) {
|
|
327
|
-
const { promises: fs7 } = await import("fs");
|
|
328
|
-
await fs7.unlink(USER_CONFIG_PATH);
|
|
329
|
-
}
|
|
330
|
-
}
|
|
331
|
-
function isTokenExpired(config) {
|
|
332
|
-
const expiresAt = Date.parse(config.expires_at);
|
|
333
|
-
return Number.isNaN(expiresAt) || expiresAt - Date.now() < 6e4;
|
|
334
|
-
}
|
|
335
|
-
|
|
336
965
|
// src/commands/doctor.ts
|
|
337
966
|
function registerDoctorCommand(program2) {
|
|
338
967
|
program2.command("doctor").description("Ch\u1EA9n \u0111o\xE1n c\xE0i \u0111\u1EB7t Avatar: hooks, MCP, login, submodule, ...").option("--fix", "T\u1EF1 \u0111\u1ED9ng fix c\xE1c issue c\xF3 th\u1EC3 fix t\u1EF1 \u0111\u1ED9ng").action(async (opts) => {
|
|
@@ -387,7 +1016,7 @@ async function runChecks(cwd) {
|
|
|
387
1016
|
detail: gitRepo ? cwd : "Kh\xF4ng ph\u1EA3i git repo (c\u1EA7n cho 'avatar init')",
|
|
388
1017
|
fixable: false
|
|
389
1018
|
});
|
|
390
|
-
const packPath =
|
|
1019
|
+
const packPath = join9(cwd, ".claude", "pack");
|
|
391
1020
|
const hasPack = await pathExists(packPath);
|
|
392
1021
|
checks.push({
|
|
393
1022
|
name: "team-ai-pack submodule",
|
|
@@ -395,7 +1024,7 @@ async function runChecks(cwd) {
|
|
|
395
1024
|
detail: hasPack ? packPath : "Avatar ch\u01B0a init \u2014 ch\u1EA1y 'avatar init'",
|
|
396
1025
|
fixable: false
|
|
397
1026
|
});
|
|
398
|
-
const claudeMdPath =
|
|
1027
|
+
const claudeMdPath = join9(cwd, "CLAUDE.md");
|
|
399
1028
|
const hasClaudeMd = await pathExists(claudeMdPath);
|
|
400
1029
|
checks.push({
|
|
401
1030
|
name: "CLAUDE.md",
|
|
@@ -403,7 +1032,7 @@ async function runChecks(cwd) {
|
|
|
403
1032
|
detail: hasClaudeMd ? "t\u1ED3n t\u1EA1i \u1EDF project root" : "thi\u1EBFu \u2014 ch\u1EA1y 'avatar init'",
|
|
404
1033
|
fixable: false
|
|
405
1034
|
});
|
|
406
|
-
const hookPath =
|
|
1035
|
+
const hookPath = join9(cwd, ".git", "hooks", "post-merge");
|
|
407
1036
|
const hasHook = await pathExists(hookPath);
|
|
408
1037
|
if (gitRepo && hasPack) {
|
|
409
1038
|
checks.push({
|
|
@@ -412,15 +1041,15 @@ async function runChecks(cwd) {
|
|
|
412
1041
|
detail: hasHook ? "installed" : "missing \u2014 fixable",
|
|
413
1042
|
fixable: !hasHook,
|
|
414
1043
|
fix: hasHook ? void 0 : async () => {
|
|
415
|
-
await installGitHook(
|
|
1044
|
+
await installGitHook(join9(cwd, ".git"), "post-merge");
|
|
416
1045
|
}
|
|
417
1046
|
});
|
|
418
1047
|
}
|
|
419
|
-
const gitignorePath =
|
|
1048
|
+
const gitignorePath = join9(cwd, ".gitignore");
|
|
420
1049
|
if (gitRepo) {
|
|
421
1050
|
let gitignoreOk = false;
|
|
422
1051
|
if (await pathExists(gitignorePath)) {
|
|
423
|
-
const content = await
|
|
1052
|
+
const content = await fs6.readFile(gitignorePath, "utf8");
|
|
424
1053
|
gitignoreOk = content.includes(".claude/_pending/");
|
|
425
1054
|
}
|
|
426
1055
|
checks.push({
|
|
@@ -430,7 +1059,7 @@ async function runChecks(cwd) {
|
|
|
430
1059
|
fixable: false
|
|
431
1060
|
});
|
|
432
1061
|
}
|
|
433
|
-
const which =
|
|
1062
|
+
const which = spawnSync5("which", ["claude"]);
|
|
434
1063
|
const hasClaudeCli = which.status === 0;
|
|
435
1064
|
checks.push({
|
|
436
1065
|
name: "Claude Code CLI",
|
|
@@ -478,24 +1107,10 @@ async function applyFixes(checks) {
|
|
|
478
1107
|
}
|
|
479
1108
|
|
|
480
1109
|
// src/commands/init.ts
|
|
481
|
-
import { basename, join as
|
|
482
|
-
import { confirm, input, select } from "@inquirer/prompts";
|
|
1110
|
+
import { basename, join as join16, relative as relative2, resolve } from "path";
|
|
1111
|
+
import { confirm as confirm2, input as input2, select as select4 } from "@inquirer/prompts";
|
|
483
1112
|
import boxen2 from "boxen";
|
|
484
1113
|
|
|
485
|
-
// src/lib/audit-log-appender.ts
|
|
486
|
-
import { promises as fs4 } from "fs";
|
|
487
|
-
async function appendAuditEntry(action, detail) {
|
|
488
|
-
await ensureAvatarHome();
|
|
489
|
-
const entry = {
|
|
490
|
-
timestamp: (/* @__PURE__ */ new Date()).toISOString(),
|
|
491
|
-
action,
|
|
492
|
-
...detail ? { detail } : {}
|
|
493
|
-
};
|
|
494
|
-
const line = `${JSON.stringify(entry)}
|
|
495
|
-
`;
|
|
496
|
-
await fs4.appendFile(AUDIT_LOG_PATH, line, "utf8");
|
|
497
|
-
}
|
|
498
|
-
|
|
499
1114
|
// src/lib/avatar-ascii-banner.ts
|
|
500
1115
|
import chalk2 from "chalk";
|
|
501
1116
|
var BANNER_LINES = [
|
|
@@ -558,27 +1173,27 @@ ${renderAvatarBanner(opts)}
|
|
|
558
1173
|
}
|
|
559
1174
|
|
|
560
1175
|
// src/lib/execute-gh-repo-create.ts
|
|
561
|
-
import { spawnSync as
|
|
1176
|
+
import { spawnSync as spawnSync6 } from "child_process";
|
|
562
1177
|
var RepoAlreadyExistsError = class extends Error {
|
|
563
1178
|
constructor(fullName) {
|
|
564
1179
|
super(`Repo "${fullName}" \u0111\xE3 t\u1ED3n t\u1EA1i tr\xEAn GitHub. \u0110\u1ED5i t\xEAn ho\u1EB7c x\xF3a repo c\u0169.`);
|
|
565
1180
|
this.name = "RepoAlreadyExistsError";
|
|
566
1181
|
}
|
|
567
1182
|
};
|
|
568
|
-
function executeGhRepoCreate(
|
|
569
|
-
const fullName = `${
|
|
1183
|
+
function executeGhRepoCreate(input3) {
|
|
1184
|
+
const fullName = `${input3.org}/${input3.name}`;
|
|
570
1185
|
const args = [
|
|
571
1186
|
"repo",
|
|
572
1187
|
"create",
|
|
573
1188
|
fullName,
|
|
574
|
-
`--${
|
|
1189
|
+
`--${input3.visibility}`,
|
|
575
1190
|
"--source",
|
|
576
|
-
|
|
1191
|
+
input3.folder,
|
|
577
1192
|
"--remote",
|
|
578
1193
|
"origin",
|
|
579
1194
|
"--push"
|
|
580
1195
|
];
|
|
581
|
-
const r =
|
|
1196
|
+
const r = spawnSync6("gh", args, { stdio: "inherit" });
|
|
582
1197
|
if (r.status !== 0) {
|
|
583
1198
|
if (r.status === 1) {
|
|
584
1199
|
throw new RepoAlreadyExistsError(fullName);
|
|
@@ -592,9 +1207,9 @@ function executeGhRepoCreate(input2) {
|
|
|
592
1207
|
}
|
|
593
1208
|
|
|
594
1209
|
// src/lib/resolve-github-username-default.ts
|
|
595
|
-
import { spawnSync as
|
|
1210
|
+
import { spawnSync as spawnSync7 } from "child_process";
|
|
596
1211
|
function resolveGithubUsernameDefault() {
|
|
597
|
-
const r =
|
|
1212
|
+
const r = spawnSync7("gh", ["api", "user", "--jq", ".login"], {
|
|
598
1213
|
encoding: "utf8",
|
|
599
1214
|
stdio: ["ignore", "pipe", "pipe"]
|
|
600
1215
|
});
|
|
@@ -626,28 +1241,28 @@ function validateRepoVisibility(v) {
|
|
|
626
1241
|
}
|
|
627
1242
|
|
|
628
1243
|
// src/lib/create-github-remote-from-folder.ts
|
|
629
|
-
function createGithubRemoteFromFolder(
|
|
630
|
-
validateRepoName(
|
|
631
|
-
validateRepoVisibility(
|
|
632
|
-
const org =
|
|
633
|
-
log.info(`T\u1EA1o GitHub repo ${org}/${
|
|
1244
|
+
function createGithubRemoteFromFolder(input3) {
|
|
1245
|
+
validateRepoName(input3.name);
|
|
1246
|
+
validateRepoVisibility(input3.visibility);
|
|
1247
|
+
const org = input3.org ?? resolveGithubUsernameDefault();
|
|
1248
|
+
log.info(`T\u1EA1o GitHub repo ${org}/${input3.name} (${input3.visibility})...`);
|
|
634
1249
|
const urls = executeGhRepoCreate({
|
|
635
|
-
folder:
|
|
1250
|
+
folder: input3.folder,
|
|
636
1251
|
org,
|
|
637
|
-
name:
|
|
638
|
-
visibility:
|
|
1252
|
+
name: input3.name,
|
|
1253
|
+
visibility: input3.visibility
|
|
639
1254
|
});
|
|
640
1255
|
log.success(`\u0110\xE3 t\u1EA1o: ${urls.sshUrl}`);
|
|
641
1256
|
return urls;
|
|
642
1257
|
}
|
|
643
1258
|
|
|
644
1259
|
// src/lib/create-workspace-remote-via-gh.ts
|
|
645
|
-
import { spawnSync as
|
|
1260
|
+
import { spawnSync as spawnSync14 } from "child_process";
|
|
646
1261
|
|
|
647
1262
|
// src/lib/check-gh-cli-auth-status.ts
|
|
648
|
-
import { spawnSync as
|
|
1263
|
+
import { spawnSync as spawnSync8 } from "child_process";
|
|
649
1264
|
function checkGhCliAuthStatus() {
|
|
650
|
-
const r =
|
|
1265
|
+
const r = spawnSync8("gh", ["auth", "status"], { stdio: "ignore" });
|
|
651
1266
|
if (r.error && r.error.code === "ENOENT") {
|
|
652
1267
|
return "not-installed";
|
|
653
1268
|
}
|
|
@@ -655,22 +1270,12 @@ function checkGhCliAuthStatus() {
|
|
|
655
1270
|
}
|
|
656
1271
|
|
|
657
1272
|
// src/lib/detect-package-manager.ts
|
|
658
|
-
import { spawnSync as
|
|
659
|
-
|
|
660
|
-
// src/lib/detect-host-platform.ts
|
|
661
|
-
import { platform } from "os";
|
|
662
|
-
function detectHostPlatform() {
|
|
663
|
-
const p = platform();
|
|
664
|
-
if (p === "darwin" || p === "linux" || p === "win32") return p;
|
|
665
|
-
return "unsupported";
|
|
666
|
-
}
|
|
667
|
-
|
|
668
|
-
// src/lib/detect-package-manager.ts
|
|
1273
|
+
import { spawnSync as spawnSync9 } from "child_process";
|
|
669
1274
|
function hasBinary(name) {
|
|
670
1275
|
const platform2 = detectHostPlatform();
|
|
671
1276
|
const probe = platform2 === "win32" ? "where" : "command";
|
|
672
1277
|
const args = platform2 === "win32" ? [name] : ["-v", name];
|
|
673
|
-
const r =
|
|
1278
|
+
const r = spawnSync9(probe, args, {
|
|
674
1279
|
shell: platform2 !== "win32",
|
|
675
1280
|
stdio: "ignore"
|
|
676
1281
|
});
|
|
@@ -686,7 +1291,7 @@ function detectPackageManager() {
|
|
|
686
1291
|
}
|
|
687
1292
|
|
|
688
1293
|
// src/lib/install-gh-cli-via-package-manager.ts
|
|
689
|
-
import { spawnSync as
|
|
1294
|
+
import { spawnSync as spawnSync10 } from "child_process";
|
|
690
1295
|
var INSTALL_COMMANDS = {
|
|
691
1296
|
brew: { cmd: "brew", args: ["install", "gh"] },
|
|
692
1297
|
apt: { cmd: "sudo", args: ["apt-get", "install", "-y", "gh"] },
|
|
@@ -697,7 +1302,7 @@ var INSTALL_COMMANDS = {
|
|
|
697
1302
|
function installGhCliViaPackageManager(pm) {
|
|
698
1303
|
const spec = INSTALL_COMMANDS[pm];
|
|
699
1304
|
log.info(`\u0110ang c\xE0i gh CLI qua ${pm}...`);
|
|
700
|
-
const r =
|
|
1305
|
+
const r = spawnSync10(spec.cmd, spec.args, { stdio: "inherit" });
|
|
701
1306
|
if (r.status !== 0) {
|
|
702
1307
|
throw new Error(`C\xE0i gh CLI th\u1EA5t b\u1EA1i qua ${pm} (exit ${r.status}). C\xE0i tay r\u1ED3i ch\u1EA1y l\u1EA1i.`);
|
|
703
1308
|
}
|
|
@@ -705,9 +1310,9 @@ function installGhCliViaPackageManager(pm) {
|
|
|
705
1310
|
}
|
|
706
1311
|
|
|
707
1312
|
// src/lib/setup-git-credential-via-gh.ts
|
|
708
|
-
import { spawnSync as
|
|
1313
|
+
import { spawnSync as spawnSync11 } from "child_process";
|
|
709
1314
|
function setupGitCredentialViaGh() {
|
|
710
|
-
const r =
|
|
1315
|
+
const r = spawnSync11("gh", ["auth", "setup-git"], { stdio: "ignore" });
|
|
711
1316
|
if (r.status !== 0) {
|
|
712
1317
|
log.warn("gh auth setup-git fail (non-fatal). N\u1EBFu git clone l\u1ED7i 128 \u2192 ch\u1EA1y th\u1EE7 c\xF4ng.");
|
|
713
1318
|
return;
|
|
@@ -716,10 +1321,10 @@ function setupGitCredentialViaGh() {
|
|
|
716
1321
|
}
|
|
717
1322
|
|
|
718
1323
|
// src/lib/trigger-gh-cli-auth-login.ts
|
|
719
|
-
import { spawnSync as
|
|
1324
|
+
import { spawnSync as spawnSync12 } from "child_process";
|
|
720
1325
|
function triggerGhCliAuthLogin() {
|
|
721
1326
|
log.info("Kh\u1EDFi \u0111\u1ED9ng \u0111\u0103ng nh\u1EADp GitHub qua gh CLI (browser s\u1EBD m\u1EDF)...");
|
|
722
|
-
const r =
|
|
1327
|
+
const r = spawnSync12(
|
|
723
1328
|
"gh",
|
|
724
1329
|
["auth", "login", "--hostname", "github.com", "--web", "--git-protocol", "ssh"],
|
|
725
1330
|
{ stdio: "inherit" }
|
|
@@ -731,7 +1336,7 @@ function triggerGhCliAuthLogin() {
|
|
|
731
1336
|
}
|
|
732
1337
|
|
|
733
1338
|
// src/lib/verify-git-remote-accessible.ts
|
|
734
|
-
import { spawnSync as
|
|
1339
|
+
import { spawnSync as spawnSync13 } from "child_process";
|
|
735
1340
|
var TIMEOUT_MS = 5e3;
|
|
736
1341
|
var RemoteNotAccessibleError = class extends Error {
|
|
737
1342
|
constructor(url, reason) {
|
|
@@ -740,7 +1345,7 @@ var RemoteNotAccessibleError = class extends Error {
|
|
|
740
1345
|
}
|
|
741
1346
|
};
|
|
742
1347
|
function verifyGitRemoteAccessible(url) {
|
|
743
|
-
const r =
|
|
1348
|
+
const r = spawnSync13("git", ["ls-remote", "--exit-code", url, "HEAD"], {
|
|
744
1349
|
stdio: "ignore",
|
|
745
1350
|
timeout: TIMEOUT_MS
|
|
746
1351
|
});
|
|
@@ -780,22 +1385,22 @@ async function ensureGitHubReady(remoteUrl) {
|
|
|
780
1385
|
}
|
|
781
1386
|
|
|
782
1387
|
// src/lib/create-workspace-remote-via-gh.ts
|
|
783
|
-
async function createWorkspaceRemoteViaGh(
|
|
784
|
-
validateRepoName(
|
|
785
|
-
validateRepoVisibility(
|
|
1388
|
+
async function createWorkspaceRemoteViaGh(input3) {
|
|
1389
|
+
validateRepoName(input3.workspaceName);
|
|
1390
|
+
validateRepoVisibility(input3.visibility);
|
|
786
1391
|
await ensureGitHubReady();
|
|
787
|
-
const org =
|
|
788
|
-
const fullName = `${org}/${
|
|
789
|
-
log.info(`T\u1EA1o GitHub repo cho workspace: ${fullName} (${
|
|
790
|
-
const r =
|
|
1392
|
+
const org = input3.org ?? resolveGithubUsernameDefault();
|
|
1393
|
+
const fullName = `${org}/${input3.workspaceName}`;
|
|
1394
|
+
log.info(`T\u1EA1o GitHub repo cho workspace: ${fullName} (${input3.visibility})...`);
|
|
1395
|
+
const r = spawnSync14(
|
|
791
1396
|
"gh",
|
|
792
1397
|
[
|
|
793
1398
|
"repo",
|
|
794
1399
|
"create",
|
|
795
1400
|
fullName,
|
|
796
|
-
`--${
|
|
1401
|
+
`--${input3.visibility}`,
|
|
797
1402
|
"--source",
|
|
798
|
-
|
|
1403
|
+
input3.workspacePath,
|
|
799
1404
|
"--remote",
|
|
800
1405
|
"origin",
|
|
801
1406
|
"--push"
|
|
@@ -804,7 +1409,7 @@ async function createWorkspaceRemoteViaGh(input2) {
|
|
|
804
1409
|
);
|
|
805
1410
|
if (r.status !== 0) {
|
|
806
1411
|
throw new Error(
|
|
807
|
-
`T\u1EA1o workspace remote th\u1EA5t b\u1EA1i (exit ${r.status}). Workspace v\u1EABn d\xF9ng \u0111\u01B0\u1EE3c local. Setup remote sau qua: gh repo create ${fullName} --${
|
|
1412
|
+
`T\u1EA1o workspace remote th\u1EA5t b\u1EA1i (exit ${r.status}). Workspace v\u1EABn d\xF9ng \u0111\u01B0\u1EE3c local. Setup remote sau qua: gh repo create ${fullName} --${input3.visibility} --source=. --remote=origin --push`
|
|
808
1413
|
);
|
|
809
1414
|
}
|
|
810
1415
|
const sshUrl = `git@github.com:${fullName}.git`;
|
|
@@ -813,11 +1418,16 @@ async function createWorkspaceRemoteViaGh(input2) {
|
|
|
813
1418
|
return { sshUrl, httpsUrl };
|
|
814
1419
|
}
|
|
815
1420
|
|
|
1421
|
+
// src/lib/safe-bootstrap-for-dirty-folder.ts
|
|
1422
|
+
import { readdirSync } from "fs";
|
|
1423
|
+
import { select as select3 } from "@inquirer/prompts";
|
|
1424
|
+
import { simpleGit as simpleGit3 } from "simple-git";
|
|
1425
|
+
|
|
816
1426
|
// src/lib/check-folder-has-git.ts
|
|
817
1427
|
import { existsSync as existsSync2, statSync } from "fs";
|
|
818
|
-
import { join as
|
|
1428
|
+
import { join as join10 } from "path";
|
|
819
1429
|
function checkFolderHasGit(folderPath) {
|
|
820
|
-
const gitPath =
|
|
1430
|
+
const gitPath = join10(folderPath, ".git");
|
|
821
1431
|
if (!existsSync2(gitPath)) return false;
|
|
822
1432
|
const stat = statSync(gitPath);
|
|
823
1433
|
return stat.isDirectory() || stat.isFile();
|
|
@@ -849,7 +1459,7 @@ async function createInitialGitCommit(folderPath) {
|
|
|
849
1459
|
|
|
850
1460
|
// src/lib/detect-folder-tech-stack.ts
|
|
851
1461
|
import { existsSync as existsSync3 } from "fs";
|
|
852
|
-
import { join as
|
|
1462
|
+
import { join as join11 } from "path";
|
|
853
1463
|
var SIGNATURES = {
|
|
854
1464
|
node: ["package.json"],
|
|
855
1465
|
python: ["pyproject.toml", "requirements.txt", "setup.py", "Pipfile"],
|
|
@@ -861,7 +1471,7 @@ var SIGNATURES = {
|
|
|
861
1471
|
function detectFolderTechStack(folderPath) {
|
|
862
1472
|
const matched = [];
|
|
863
1473
|
for (const [stack, files] of Object.entries(SIGNATURES)) {
|
|
864
|
-
if (files.some((f) => existsSync3(
|
|
1474
|
+
if (files.some((f) => existsSync3(join11(folderPath, f)))) {
|
|
865
1475
|
matched.push(stack);
|
|
866
1476
|
}
|
|
867
1477
|
}
|
|
@@ -869,20 +1479,20 @@ function detectFolderTechStack(folderPath) {
|
|
|
869
1479
|
}
|
|
870
1480
|
|
|
871
1481
|
// src/lib/gitignore-template-loader.ts
|
|
872
|
-
import { readFileSync } from "fs";
|
|
873
|
-
import { dirname as dirname3, join as
|
|
1482
|
+
import { readFileSync as readFileSync2 } from "fs";
|
|
1483
|
+
import { dirname as dirname3, join as join12 } from "path";
|
|
874
1484
|
import { fileURLToPath as fileURLToPath2 } from "url";
|
|
875
1485
|
var __dirname = dirname3(fileURLToPath2(import.meta.url));
|
|
876
1486
|
var CANDIDATE_DIRS = [
|
|
877
|
-
|
|
878
|
-
|
|
1487
|
+
join12(__dirname, "..", "templates", "gitignore"),
|
|
1488
|
+
join12(__dirname, "..", "..", "src", "templates", "gitignore")
|
|
879
1489
|
];
|
|
880
1490
|
var AVATAR_MARKER_START = "# === avatar ===";
|
|
881
1491
|
var AVATAR_MARKER_END = "# === /avatar ===";
|
|
882
1492
|
function readTemplate(stack) {
|
|
883
1493
|
for (const dir of CANDIDATE_DIRS) {
|
|
884
1494
|
try {
|
|
885
|
-
return
|
|
1495
|
+
return readFileSync2(join12(dir, `${stack}.txt`), "utf8");
|
|
886
1496
|
} catch {
|
|
887
1497
|
}
|
|
888
1498
|
}
|
|
@@ -896,15 +1506,15 @@ ${readTemplate(s).trim()}`);
|
|
|
896
1506
|
}
|
|
897
1507
|
|
|
898
1508
|
// src/lib/write-or-merge-gitignore.ts
|
|
899
|
-
import { existsSync as existsSync4, readFileSync as
|
|
900
|
-
import { join as
|
|
1509
|
+
import { existsSync as existsSync4, readFileSync as readFileSync3, writeFileSync } from "fs";
|
|
1510
|
+
import { join as join13 } from "path";
|
|
901
1511
|
function writeOrMergeGitignore(folderPath, avatarBlock) {
|
|
902
|
-
const path =
|
|
1512
|
+
const path = join13(folderPath, ".gitignore");
|
|
903
1513
|
if (!existsSync4(path)) {
|
|
904
1514
|
writeFileSync(path, avatarBlock, "utf8");
|
|
905
1515
|
return;
|
|
906
1516
|
}
|
|
907
|
-
const existing =
|
|
1517
|
+
const existing = readFileSync3(path, "utf8");
|
|
908
1518
|
const startIdx = existing.indexOf(AVATAR_MARKER_START);
|
|
909
1519
|
const endIdx = existing.indexOf(AVATAR_MARKER_END);
|
|
910
1520
|
if (startIdx !== -1 && endIdx !== -1 && endIdx > startIdx) {
|
|
@@ -920,24 +1530,162 @@ ${avatarBlock}${after.trimStart()}`, "utf8");
|
|
|
920
1530
|
${avatarBlock}`, "utf8");
|
|
921
1531
|
}
|
|
922
1532
|
|
|
923
|
-
// src/lib/
|
|
924
|
-
|
|
925
|
-
|
|
1533
|
+
// src/lib/safe-bootstrap-for-dirty-folder.ts
|
|
1534
|
+
var InitAbortedByUserError = class extends Error {
|
|
1535
|
+
constructor(message) {
|
|
1536
|
+
super(message);
|
|
1537
|
+
this.name = "InitAbortedByUserError";
|
|
1538
|
+
}
|
|
1539
|
+
};
|
|
1540
|
+
async function detectFolderGitState(folderPath) {
|
|
1541
|
+
const hasGit = checkFolderHasGit(folderPath);
|
|
1542
|
+
if (!hasGit) {
|
|
1543
|
+
const entries = readdirSync(folderPath).filter((e) => e !== ".git");
|
|
1544
|
+
return entries.length === 0 ? "empty" : "untracked-only";
|
|
1545
|
+
}
|
|
1546
|
+
const g = simpleGit3({ baseDir: folderPath });
|
|
1547
|
+
const status = await g.status();
|
|
1548
|
+
return status.isClean() ? "clean" : "dirty";
|
|
1549
|
+
}
|
|
1550
|
+
async function promptBootstrapStrategy(state, opts) {
|
|
1551
|
+
if (opts.presetStrategy) return opts.presetStrategy;
|
|
1552
|
+
if (opts.autoYes) return "stash";
|
|
1553
|
+
if (state === "empty" || state === "clean") return "commit-all";
|
|
1554
|
+
return await select3({
|
|
1555
|
+
message: state === "dirty" ? "Folder c\xF3 changes ch\u01B0a commit. C\xE1ch x\u1EED l\xFD:" : "Folder c\xF3 file ch\u01B0a version. C\xE1ch x\u1EED l\xFD:",
|
|
1556
|
+
choices: [
|
|
1557
|
+
{
|
|
1558
|
+
value: "stash",
|
|
1559
|
+
name: "1. Stash changes \u2192 bootstrap \u2192 restore (KHUY\u1EBEN NGH\u1ECA)"
|
|
1560
|
+
},
|
|
1561
|
+
{
|
|
1562
|
+
value: "commit-all",
|
|
1563
|
+
name: "2. Commit to\xE0n b\u1ED9 v\xE0o initial commit (legacy v1.1.6)"
|
|
1564
|
+
},
|
|
1565
|
+
{
|
|
1566
|
+
value: "skip",
|
|
1567
|
+
name: "3. Skip \u2014 t\xF4i commit th\u1EE7 c\xF4ng r\u1ED3i ch\u1EA1y l\u1EA1i"
|
|
1568
|
+
},
|
|
1569
|
+
{
|
|
1570
|
+
value: "branch",
|
|
1571
|
+
name: "4. Commit v\xE0o branch ri\xEAng `avatar/init` (main gi\u1EEF s\u1EA1ch)"
|
|
1572
|
+
}
|
|
1573
|
+
],
|
|
1574
|
+
default: "stash"
|
|
1575
|
+
});
|
|
1576
|
+
}
|
|
1577
|
+
async function stashUserChanges(g, stashName) {
|
|
1578
|
+
const status = await g.status();
|
|
1579
|
+
if (status.isClean() && status.not_added.length === 0) return false;
|
|
1580
|
+
await g.stash(["push", "--include-untracked", "-m", stashName]);
|
|
1581
|
+
log.info(`Stashed changes: ${stashName}`);
|
|
1582
|
+
return true;
|
|
1583
|
+
}
|
|
1584
|
+
async function restoreStash(g, stashName) {
|
|
1585
|
+
try {
|
|
1586
|
+
await g.stash(["pop"]);
|
|
1587
|
+
log.success(`Restored stash: ${stashName}`);
|
|
1588
|
+
} catch (err) {
|
|
1589
|
+
log.warn(
|
|
1590
|
+
"Restore stash conflict \u2014 files c\xF3 Avatar t\u1EA1o v\xE0 stash c\u1EE7a user xung \u0111\u1ED9t. Stash gi\u1EEF t\u1EA1i ref stash@{0}."
|
|
1591
|
+
);
|
|
1592
|
+
log.warn("Resolve: git stash show -p stash@{0} \u2192 fix conflict \u2192 git stash drop");
|
|
1593
|
+
log.dim(`Detail: ${err.message}`);
|
|
1594
|
+
}
|
|
1595
|
+
}
|
|
1596
|
+
async function getCurrentBranch(g) {
|
|
1597
|
+
try {
|
|
1598
|
+
const result = await g.revparse(["--abbrev-ref", "HEAD"]);
|
|
1599
|
+
const branch = result.trim();
|
|
1600
|
+
return branch === "HEAD" ? "main" : branch;
|
|
1601
|
+
} catch {
|
|
1602
|
+
return "main";
|
|
1603
|
+
}
|
|
1604
|
+
}
|
|
1605
|
+
async function writeAvatarGitignore(folderPath) {
|
|
926
1606
|
const stacks = detectFolderTechStack(folderPath);
|
|
927
|
-
log.info(`Tech stack
|
|
1607
|
+
log.info(`Tech stack: ${stacks.join(", ")}`);
|
|
928
1608
|
writeOrMergeGitignore(folderPath, composeGitignoreContent(stacks));
|
|
929
1609
|
log.success(".gitignore \u0111\xE3 ghi (Avatar block)");
|
|
930
|
-
|
|
931
|
-
|
|
932
|
-
|
|
933
|
-
|
|
934
|
-
|
|
935
|
-
|
|
1610
|
+
}
|
|
1611
|
+
async function executeBootstrapWithStrategy(folderPath, strategy) {
|
|
1612
|
+
const g = simpleGit3({ baseDir: folderPath });
|
|
1613
|
+
switch (strategy) {
|
|
1614
|
+
case "skip":
|
|
1615
|
+
throw new InitAbortedByUserError(
|
|
1616
|
+
"Init aborted. Commit th\u1EE7 c\xF4ng changes hi\u1EC7n t\u1EA1i r\u1ED3i ch\u1EA1y l\u1EA1i `avatar init`."
|
|
1617
|
+
);
|
|
1618
|
+
case "stash": {
|
|
1619
|
+
const stashName = `avatar-init-backup-${Date.now()}`;
|
|
1620
|
+
const hadGit = checkFolderHasGit(folderPath);
|
|
1621
|
+
if (!hadGit) {
|
|
1622
|
+
await g.init();
|
|
1623
|
+
await g.branch(["-M", "main"]).catch(() => void 0);
|
|
1624
|
+
}
|
|
1625
|
+
const hasCommit = (await g.raw(["rev-list", "-n", "1", "--all"]).catch(() => "")).trim();
|
|
1626
|
+
if (!hasCommit) {
|
|
1627
|
+
await g.commit("chore: avatar baseline (pre-stash)", void 0, { "--allow-empty": null });
|
|
1628
|
+
}
|
|
1629
|
+
const stashed = await stashUserChanges(g, stashName);
|
|
1630
|
+
try {
|
|
1631
|
+
await writeAvatarGitignore(folderPath);
|
|
1632
|
+
await createInitialGitCommit(folderPath);
|
|
1633
|
+
} finally {
|
|
1634
|
+
if (stashed) await restoreStash(g, stashName);
|
|
1635
|
+
}
|
|
1636
|
+
break;
|
|
1637
|
+
}
|
|
1638
|
+
case "commit-all": {
|
|
1639
|
+
await writeAvatarGitignore(folderPath);
|
|
1640
|
+
await createInitialGitCommit(folderPath);
|
|
1641
|
+
break;
|
|
1642
|
+
}
|
|
1643
|
+
case "branch": {
|
|
1644
|
+
const hadGit = checkFolderHasGit(folderPath);
|
|
1645
|
+
if (!hadGit) {
|
|
1646
|
+
await g.init();
|
|
1647
|
+
await g.branch(["-M", "main"]);
|
|
1648
|
+
}
|
|
1649
|
+
const originalBranch = await getCurrentBranch(g);
|
|
1650
|
+
try {
|
|
1651
|
+
await g.checkoutLocalBranch("avatar/init");
|
|
1652
|
+
} catch {
|
|
1653
|
+
await g.checkout("avatar/init");
|
|
1654
|
+
}
|
|
1655
|
+
await writeAvatarGitignore(folderPath);
|
|
1656
|
+
await createInitialGitCommit(folderPath);
|
|
1657
|
+
try {
|
|
1658
|
+
await g.checkout(originalBranch);
|
|
1659
|
+
log.info(
|
|
1660
|
+
`Avatar init committed \u1EDF branch 'avatar/init'. Switch back v\u1EC1 '${originalBranch}'. Merge khi s\u1EB5n s\xE0ng: git merge avatar/init`
|
|
1661
|
+
);
|
|
1662
|
+
} catch {
|
|
1663
|
+
log.warn(
|
|
1664
|
+
`Kh\xF4ng switch v\u1EC1 '${originalBranch}' \u0111\u01B0\u1EE3c \u2014 \u1EDF l\u1EA1i branch 'avatar/init'. Switch tay sau.`
|
|
1665
|
+
);
|
|
1666
|
+
}
|
|
1667
|
+
break;
|
|
1668
|
+
}
|
|
936
1669
|
}
|
|
937
1670
|
}
|
|
1671
|
+
async function safeBootstrapGitInFolder(folderPath, opts = {}) {
|
|
1672
|
+
const state = await detectFolderGitState(folderPath);
|
|
1673
|
+
log.info(`Folder state: ${state}`);
|
|
1674
|
+
if (state === "empty" || state === "clean") {
|
|
1675
|
+
await writeAvatarGitignore(folderPath);
|
|
1676
|
+
if (state === "empty") {
|
|
1677
|
+
await createInitialGitCommit(folderPath);
|
|
1678
|
+
}
|
|
1679
|
+
await appendAuditEntry("bootstrap", `state=${state},strategy=auto`);
|
|
1680
|
+
return;
|
|
1681
|
+
}
|
|
1682
|
+
const strategy = await promptBootstrapStrategy(state, opts);
|
|
1683
|
+
await executeBootstrapWithStrategy(folderPath, strategy);
|
|
1684
|
+
await appendAuditEntry("bootstrap", `state=${state},strategy=${strategy}`);
|
|
1685
|
+
}
|
|
938
1686
|
|
|
939
1687
|
// src/lib/team-pack-submodule-manager.ts
|
|
940
|
-
import { join as
|
|
1688
|
+
import { join as join14 } from "path";
|
|
941
1689
|
|
|
942
1690
|
// src/lib/resolve-team-pack-repo-url.ts
|
|
943
1691
|
var LEGACY_FALLBACK = "https://github.com/LukeNALS/team-ai-pack.git";
|
|
@@ -975,7 +1723,7 @@ async function addTeamPackSubmodule(projectRoot, tag) {
|
|
|
975
1723
|
}
|
|
976
1724
|
let target = tag ?? null;
|
|
977
1725
|
if (!target) {
|
|
978
|
-
target = await latestTag(
|
|
1726
|
+
target = await latestTag(join14(projectRoot, TEAM_PACK_RELATIVE_PATH));
|
|
979
1727
|
}
|
|
980
1728
|
if (target) {
|
|
981
1729
|
await checkoutTagInSubmodule(TEAM_PACK_RELATIVE_PATH, target, projectRoot);
|
|
@@ -983,7 +1731,7 @@ async function addTeamPackSubmodule(projectRoot, tag) {
|
|
|
983
1731
|
return { pinnedTag: target };
|
|
984
1732
|
}
|
|
985
1733
|
async function readPinnedPackVersion(projectRoot) {
|
|
986
|
-
const submoduleRoot =
|
|
1734
|
+
const submoduleRoot = join14(projectRoot, TEAM_PACK_RELATIVE_PATH);
|
|
987
1735
|
const tag = await latestTag(submoduleRoot);
|
|
988
1736
|
if (tag) return tag;
|
|
989
1737
|
const sha = await currentCommitSha(submoduleRoot);
|
|
@@ -992,7 +1740,7 @@ async function readPinnedPackVersion(projectRoot) {
|
|
|
992
1740
|
|
|
993
1741
|
// src/commands/init-conflict-detection-helpers.ts
|
|
994
1742
|
import { readdir } from "fs/promises";
|
|
995
|
-
import { join as
|
|
1743
|
+
import { join as join15 } from "path";
|
|
996
1744
|
async function isEmptyOrMissing(path) {
|
|
997
1745
|
if (!await pathExists(path)) return true;
|
|
998
1746
|
try {
|
|
@@ -1005,7 +1753,7 @@ async function isEmptyOrMissing(path) {
|
|
|
1005
1753
|
}
|
|
1006
1754
|
async function findAlternativeWorkspaceName(parent, desiredName, maxAttempts = 10) {
|
|
1007
1755
|
for (let i = 2; i < maxAttempts; i++) {
|
|
1008
|
-
const candidate =
|
|
1756
|
+
const candidate = join15(parent, `${desiredName}-${i}`);
|
|
1009
1757
|
if (await isEmptyOrMissing(candidate)) return candidate;
|
|
1010
1758
|
}
|
|
1011
1759
|
return null;
|
|
@@ -1031,11 +1779,29 @@ function buildScaffoldVariables(args) {
|
|
|
1031
1779
|
}
|
|
1032
1780
|
|
|
1033
1781
|
// src/commands/init.ts
|
|
1782
|
+
function parseBootstrapStrategyOpts(opts) {
|
|
1783
|
+
if (opts.preserveUncommitted) return "stash";
|
|
1784
|
+
if (!opts.bootstrapStrategy) return void 0;
|
|
1785
|
+
const valid = ["stash", "commit-all", "skip", "branch"];
|
|
1786
|
+
if (valid.includes(opts.bootstrapStrategy)) {
|
|
1787
|
+
return opts.bootstrapStrategy;
|
|
1788
|
+
}
|
|
1789
|
+
throw new Error(
|
|
1790
|
+
`--bootstrap-strategy kh\xF4ng h\u1EE3p l\u1EC7: ${opts.bootstrapStrategy}. Ch\u1ECDn: ${valid.join(" | ")}`
|
|
1791
|
+
);
|
|
1792
|
+
}
|
|
1034
1793
|
function registerInitCommand(program2) {
|
|
1035
|
-
program2.command("init").description("Kh\u1EDFi t\u1EA1o Avatar \u2014 3 flow t\u1EF1 nh\u1EADn di\u1EC7n (repo / folder / new)").option("--project-status <val>", "existing-remote | existing-folder | new-project").option("--folder-path <path>", "\u0110\u01B0\u1EDDng d\u1EABn folder hi\u1EC7n c\xF3 (flow existing-folder)").option("--create-remote", "Force t\u1EA1o remote qua gh (flow existing-folder ho\u1EB7c new-project)").option("--repo-visibility <val>", "private (m\u1EB7c \u0111\u1ECBnh) | public").option("--repo-org <name>", "GitHub org/owner cho repo m\u1EDBi").option("--client-repo <url>", "URL git remote (flow existing-remote)").option("--workspace-name <name>", "T\xEAn workspace").option("--workspace-parent <path>", "Th\u01B0 m\u1EE5c cha t\u1EA1o workspace (m\u1EB7c \u0111\u1ECBnh . \u2014 CWD)").option("--pack-version <tag>", "Pin team-ai-pack v\xE0o tag c\u1EE5 th\u1EC3").option("--team-owner <email>", "Email team owner (b\u1ECF qua prompt)").option("--description <text>", "M\xF4 t\u1EA3 1 d\xF2ng c\u1EE7a d\u1EF1 \xE1n").option("--skip-scan", "B\u1ECF qua project-scanner sau scaffold").option("--skip-team-pack", "B\u1ECF qua submodule team-ai-pack (test mode)").option("--force", "B\u1ECF qua prompt khi workspace path \u0111\xE3 t\u1ED3n t\u1EA1i").option("--yes", "Auto-confirm t\u1EA5t c\u1EA3 prompt").option("--no-commit", "Skip commit workspace initial state (m\u1EB7c \u0111\u1ECBnh LU\xD4N commit)").option("--workspace-remote", "T\u1EA1o GitHub remote cho workspace root (default: prompt)").option("--
|
|
1794
|
+
program2.command("init").description("Kh\u1EDFi t\u1EA1o Avatar \u2014 3 flow t\u1EF1 nh\u1EADn di\u1EC7n (repo / folder / new)").option("--project-status <val>", "existing-remote | existing-folder | new-project").option("--folder-path <path>", "\u0110\u01B0\u1EDDng d\u1EABn folder hi\u1EC7n c\xF3 (flow existing-folder)").option("--create-remote", "Force t\u1EA1o remote qua gh (flow existing-folder ho\u1EB7c new-project)").option("--repo-visibility <val>", "private (m\u1EB7c \u0111\u1ECBnh) | public").option("--repo-org <name>", "GitHub org/owner cho repo m\u1EDBi").option("--client-repo <url>", "URL git remote (flow existing-remote)").option("--workspace-name <name>", "T\xEAn workspace").option("--workspace-parent <path>", "Th\u01B0 m\u1EE5c cha t\u1EA1o workspace (m\u1EB7c \u0111\u1ECBnh . \u2014 CWD)").option("--pack-version <tag>", "Pin team-ai-pack v\xE0o tag c\u1EE5 th\u1EC3").option("--team-owner <email>", "Email team owner (b\u1ECF qua prompt)").option("--description <text>", "M\xF4 t\u1EA3 1 d\xF2ng c\u1EE7a d\u1EF1 \xE1n").option("--skip-scan", "B\u1ECF qua project-scanner sau scaffold").option("--skip-team-pack", "B\u1ECF qua submodule team-ai-pack (test mode)").option("--force", "B\u1ECF qua prompt khi workspace path \u0111\xE3 t\u1ED3n t\u1EA1i").option("--yes", "Auto-confirm t\u1EA5t c\u1EA3 prompt").option("--no-commit", "Skip commit workspace initial state (m\u1EB7c \u0111\u1ECBnh LU\xD4N commit)").option("--workspace-remote", "T\u1EA1o GitHub remote cho workspace root (default: prompt)").option("--ai-skip", "B\u1ECF qua phase AI setup (CI/test mode \u2014 ch\u1EA1y `avatar ai setup` sau)").option(
|
|
1795
|
+
"--bootstrap-strategy <s>",
|
|
1796
|
+
"X\u1EED l\xFD folder dirty: stash | commit-all | skip | branch (default: prompt)"
|
|
1797
|
+
).option("--preserve-uncommitted", "Alias cho --bootstrap-strategy=stash (gi\u1EEF changes user)").option("--mode <mode>", "[DEPRECATED] D\xF9ng --project-status thay th\u1EBF").action(async (opts) => {
|
|
1036
1798
|
try {
|
|
1037
1799
|
await runInit(opts);
|
|
1038
1800
|
} catch (err) {
|
|
1801
|
+
if (err instanceof InitAbortedByUserError) {
|
|
1802
|
+
log.dim(err.message);
|
|
1803
|
+
process.exit(0);
|
|
1804
|
+
}
|
|
1039
1805
|
log.error(err instanceof Error ? err.message : String(err));
|
|
1040
1806
|
process.exit(1);
|
|
1041
1807
|
}
|
|
@@ -1065,7 +1831,7 @@ async function runInit(opts) {
|
|
|
1065
1831
|
}
|
|
1066
1832
|
}
|
|
1067
1833
|
async function promptProjectStatus() {
|
|
1068
|
-
return await
|
|
1834
|
+
return await select4({
|
|
1069
1835
|
message: "T\xECnh tr\u1EA1ng d\u1EF1 \xE1n c\u1EE7a b\u1EA1n?",
|
|
1070
1836
|
choices: [
|
|
1071
1837
|
{ name: "1. \u0110\xE3 c\xF3 repo git remote (URL c\xF3 s\u1EB5n)", value: "existing-remote" },
|
|
@@ -1075,14 +1841,14 @@ async function promptProjectStatus() {
|
|
|
1075
1841
|
});
|
|
1076
1842
|
}
|
|
1077
1843
|
async function runInitFromExistingRemote(opts, ownerEmail) {
|
|
1078
|
-
const remoteUrl = opts.clientRepo ?? await
|
|
1844
|
+
const remoteUrl = opts.clientRepo ?? await input2({
|
|
1079
1845
|
message: "URL git c\u1EE7a repo:",
|
|
1080
1846
|
validate: (v) => v.length > 0 ? true : "URL b\u1EAFt bu\u1ED9c"
|
|
1081
1847
|
});
|
|
1082
1848
|
await ensureGitHubReady(remoteUrl);
|
|
1083
1849
|
const teamOwner = opts.teamOwner ?? await promptTeamOwner(ownerEmail);
|
|
1084
1850
|
const inferredName = inferWorkspaceName(remoteUrl);
|
|
1085
|
-
const workspaceName = opts.workspaceName ?? await
|
|
1851
|
+
const workspaceName = opts.workspaceName ?? await input2({ message: "T\xEAn workspace:", default: inferredName });
|
|
1086
1852
|
const workspaceParent = resolve(opts.workspaceParent ?? ".");
|
|
1087
1853
|
const workspacePath = await resolveWorkspacePath(workspaceParent, workspaceName, opts.force);
|
|
1088
1854
|
await scaffoldWorkspaceWithSrcSubmodule({
|
|
@@ -1098,21 +1864,25 @@ async function runInitFromExistingRemote(opts, ownerEmail) {
|
|
|
1098
1864
|
createWorkspaceRemote: opts.workspaceRemote,
|
|
1099
1865
|
repoVisibility: opts.repoVisibility,
|
|
1100
1866
|
repoOrg: opts.repoOrg,
|
|
1101
|
-
flow: "existing-remote"
|
|
1867
|
+
flow: "existing-remote",
|
|
1868
|
+
aiSkip: opts.aiSkip
|
|
1102
1869
|
});
|
|
1103
1870
|
}
|
|
1104
1871
|
async function runInitFromExistingFolder(opts, ownerEmail) {
|
|
1105
1872
|
const folderPath = resolve(
|
|
1106
|
-
opts.folderPath ?? await
|
|
1873
|
+
opts.folderPath ?? await input2({
|
|
1107
1874
|
message: "\u0110\u01B0\u1EDDng d\u1EABn folder hi\u1EC7n c\xF3:",
|
|
1108
1875
|
validate: (v) => v.length > 0 ? true : "Path b\u1EAFt bu\u1ED9c"
|
|
1109
1876
|
})
|
|
1110
1877
|
);
|
|
1111
|
-
await
|
|
1878
|
+
await safeBootstrapGitInFolder(folderPath, {
|
|
1879
|
+
presetStrategy: parseBootstrapStrategyOpts(opts),
|
|
1880
|
+
autoYes: opts.yes
|
|
1881
|
+
});
|
|
1112
1882
|
const remoteUrl = await getOrCreateOriginRemote(folderPath, opts);
|
|
1113
1883
|
const teamOwner = opts.teamOwner ?? await promptTeamOwner(ownerEmail);
|
|
1114
1884
|
const inferredName = opts.workspaceName ?? `${basename(folderPath)}-avatar-workspace`;
|
|
1115
|
-
const workspaceName = opts.workspaceName ?? await
|
|
1885
|
+
const workspaceName = opts.workspaceName ?? await input2({ message: "T\xEAn workspace:", default: inferredName });
|
|
1116
1886
|
const workspaceParent = resolve(opts.workspaceParent ?? ".");
|
|
1117
1887
|
const workspacePath = await resolveWorkspacePath(workspaceParent, workspaceName, opts.force);
|
|
1118
1888
|
await scaffoldWorkspaceWithSrcSubmodule({
|
|
@@ -1129,16 +1899,17 @@ async function runInitFromExistingFolder(opts, ownerEmail) {
|
|
|
1129
1899
|
createWorkspaceRemote: opts.workspaceRemote,
|
|
1130
1900
|
repoVisibility: opts.repoVisibility,
|
|
1131
1901
|
repoOrg: opts.repoOrg,
|
|
1132
|
-
flow: "existing-folder"
|
|
1902
|
+
flow: "existing-folder",
|
|
1903
|
+
aiSkip: opts.aiSkip
|
|
1133
1904
|
});
|
|
1134
1905
|
}
|
|
1135
1906
|
async function runInitFromScratch(opts, ownerEmail) {
|
|
1136
1907
|
await ensureGitHubReady();
|
|
1137
|
-
const projectName = opts.workspaceName ?? await
|
|
1908
|
+
const projectName = opts.workspaceName ?? await input2({
|
|
1138
1909
|
message: "T\xEAn d\u1EF1 \xE1n:",
|
|
1139
1910
|
validate: (v) => v.length > 0 ? true : "T\xEAn b\u1EAFt bu\u1ED9c"
|
|
1140
1911
|
});
|
|
1141
|
-
const visibility = opts.repoVisibility ?? await
|
|
1912
|
+
const visibility = opts.repoVisibility ?? await select4({
|
|
1142
1913
|
message: "Visibility?",
|
|
1143
1914
|
choices: [
|
|
1144
1915
|
{ name: "private (m\u1EB7c \u0111\u1ECBnh)", value: "private" },
|
|
@@ -1148,10 +1919,10 @@ async function runInitFromScratch(opts, ownerEmail) {
|
|
|
1148
1919
|
const teamOwner = opts.teamOwner ?? await promptTeamOwner(ownerEmail);
|
|
1149
1920
|
const workspaceParent = resolve(opts.workspaceParent ?? ".");
|
|
1150
1921
|
const workspacePath = await resolveWorkspacePath(workspaceParent, projectName, opts.force);
|
|
1151
|
-
const srcPath =
|
|
1922
|
+
const srcPath = join16(workspacePath, "src");
|
|
1152
1923
|
await ensureDir(workspacePath);
|
|
1153
1924
|
await ensureDir(srcPath);
|
|
1154
|
-
await
|
|
1925
|
+
await safeBootstrapGitInFolder(srcPath, { autoYes: true });
|
|
1155
1926
|
const urls = createGithubRemoteFromFolder({
|
|
1156
1927
|
folder: srcPath,
|
|
1157
1928
|
name: projectName,
|
|
@@ -1183,7 +1954,8 @@ async function runInitFromScratch(opts, ownerEmail) {
|
|
|
1183
1954
|
createWorkspaceRemote: opts.workspaceRemote,
|
|
1184
1955
|
repoVisibility: opts.repoVisibility,
|
|
1185
1956
|
repoOrg: opts.repoOrg,
|
|
1186
|
-
flow: "new-project"
|
|
1957
|
+
flow: "new-project",
|
|
1958
|
+
aiSkip: opts.aiSkip
|
|
1187
1959
|
});
|
|
1188
1960
|
} catch (err) {
|
|
1189
1961
|
sp.fail("Init workspace th\u1EA5t b\u1EA1i");
|
|
@@ -1197,7 +1969,7 @@ async function getOrCreateOriginRemote(folderPath, opts) {
|
|
|
1197
1969
|
log.success(`Folder \u0111\xE3 c\xF3 remote origin: ${origin.refs.push}`);
|
|
1198
1970
|
return origin.refs.push;
|
|
1199
1971
|
}
|
|
1200
|
-
const shouldCreate = opts.createRemote ?? await
|
|
1972
|
+
const shouldCreate = opts.createRemote ?? await confirm2({
|
|
1201
1973
|
message: "Folder ch\u01B0a c\xF3 remote. T\u1EA1o GitHub repo ngay \u0111\u1EC3 share team?",
|
|
1202
1974
|
default: true
|
|
1203
1975
|
});
|
|
@@ -1206,14 +1978,14 @@ async function getOrCreateOriginRemote(folderPath, opts) {
|
|
|
1206
1978
|
return void 0;
|
|
1207
1979
|
}
|
|
1208
1980
|
await ensureGitHubReady();
|
|
1209
|
-
const visibility = opts.repoVisibility ?? await
|
|
1981
|
+
const visibility = opts.repoVisibility ?? await select4({
|
|
1210
1982
|
message: "Visibility?",
|
|
1211
1983
|
choices: [
|
|
1212
1984
|
{ name: "private (m\u1EB7c \u0111\u1ECBnh)", value: "private" },
|
|
1213
1985
|
{ name: "public", value: "public" }
|
|
1214
1986
|
]
|
|
1215
1987
|
});
|
|
1216
|
-
const repoName = await
|
|
1988
|
+
const repoName = await input2({
|
|
1217
1989
|
message: "T\xEAn repo:",
|
|
1218
1990
|
default: basename(folderPath)
|
|
1219
1991
|
});
|
|
@@ -1252,7 +2024,8 @@ async function scaffoldWorkspaceWithSrcSubmodule(args) {
|
|
|
1252
2024
|
createWorkspaceRemote: args.createWorkspaceRemote,
|
|
1253
2025
|
repoVisibility: args.repoVisibility,
|
|
1254
2026
|
repoOrg: args.repoOrg,
|
|
1255
|
-
flow: args.flow
|
|
2027
|
+
flow: args.flow,
|
|
2028
|
+
aiSkip: args.aiSkip
|
|
1256
2029
|
});
|
|
1257
2030
|
} catch (err) {
|
|
1258
2031
|
sp.fail("Init workspace th\u1EA5t b\u1EA1i");
|
|
@@ -1272,15 +2045,21 @@ async function finalizeWorkspaceScaffold(args) {
|
|
|
1272
2045
|
await writeRootClaudeMd(args.workspacePath, vars);
|
|
1273
2046
|
await writeProjectSettings(args.workspacePath, vars);
|
|
1274
2047
|
await appendGitignoreEntries(args.workspacePath);
|
|
1275
|
-
await ensureDir(
|
|
1276
|
-
await ensureDir(
|
|
1277
|
-
await installGitHook(
|
|
1278
|
-
await installGitHook(
|
|
2048
|
+
await ensureDir(join16(args.workspacePath, "notes"));
|
|
2049
|
+
await ensureDir(join16(args.workspacePath, "scripts"));
|
|
2050
|
+
await installGitHook(join16(args.workspacePath, ".git"), "post-merge");
|
|
2051
|
+
await installGitHook(join16(args.workspacePath, ".git", "modules", "src"), "pre-push");
|
|
1279
2052
|
log.success("C\xE0i post-merge (workspace) + pre-push (src/)");
|
|
1280
2053
|
await appendAuditEntry("init", `flow=${args.flow},workspace=${args.workspaceName}`);
|
|
1281
2054
|
await maybeCommitWorkspace(args.workspacePath, args.skipCommit);
|
|
1282
2055
|
await maybeCreateWorkspaceRemote(args);
|
|
1283
|
-
|
|
2056
|
+
let aiResult = null;
|
|
2057
|
+
if (args.aiSkip) {
|
|
2058
|
+
log.dim("B\u1ECF qua AI setup (--ai-skip). Setup sau qua: avatar ai setup");
|
|
2059
|
+
} else {
|
|
2060
|
+
aiResult = await runAiSetupPhase({ workspacePath: args.workspacePath });
|
|
2061
|
+
}
|
|
2062
|
+
printInitSuccessBox(args.workspacePath, args.flow, aiResult);
|
|
1284
2063
|
}
|
|
1285
2064
|
async function maybeCreateWorkspaceRemote(args) {
|
|
1286
2065
|
if (args.skipCommit) {
|
|
@@ -1290,13 +2069,13 @@ async function maybeCreateWorkspaceRemote(args) {
|
|
|
1290
2069
|
let shouldCreate = args.createWorkspaceRemote;
|
|
1291
2070
|
if (shouldCreate === void 0) {
|
|
1292
2071
|
if (args.autoYes) return;
|
|
1293
|
-
shouldCreate = await
|
|
2072
|
+
shouldCreate = await confirm2({
|
|
1294
2073
|
message: "T\u1EA1o remote GitHub cho workspace \u0111\u1EC3 share team? (Avatar state)",
|
|
1295
2074
|
default: false
|
|
1296
2075
|
});
|
|
1297
2076
|
}
|
|
1298
2077
|
if (!shouldCreate) return;
|
|
1299
|
-
const visibility = args.repoVisibility ?? (args.autoYes ? "private" : await
|
|
2078
|
+
const visibility = args.repoVisibility ?? (args.autoYes ? "private" : await select4({
|
|
1300
2079
|
message: "Workspace visibility?",
|
|
1301
2080
|
choices: [
|
|
1302
2081
|
{ name: "private (m\u1EB7c \u0111\u1ECBnh, an to\xE0n)", value: "private" },
|
|
@@ -1316,7 +2095,7 @@ async function maybeCreateWorkspaceRemote(args) {
|
|
|
1316
2095
|
}
|
|
1317
2096
|
}
|
|
1318
2097
|
async function resolveWorkspacePath(parent, desiredName, force) {
|
|
1319
|
-
const desired =
|
|
2098
|
+
const desired = join16(parent, desiredName);
|
|
1320
2099
|
if (await isEmptyOrMissing(desired)) return desired;
|
|
1321
2100
|
const alternative = await findAlternativeWorkspaceName(parent, desiredName);
|
|
1322
2101
|
if (!alternative) {
|
|
@@ -1327,12 +2106,12 @@ async function resolveWorkspacePath(parent, desiredName, force) {
|
|
|
1327
2106
|
log.info(`--force: d\xF9ng ${alternative}`);
|
|
1328
2107
|
return alternative;
|
|
1329
2108
|
}
|
|
1330
|
-
const useAlt = await
|
|
2109
|
+
const useAlt = await confirm2({ message: `D\xF9ng "${alternative}" thay th\u1EBF?`, default: true });
|
|
1331
2110
|
if (!useAlt) throw new Error("H\u1EE7y init. Ch\u1EA1y l\u1EA1i v\u1EDBi --workspace-name kh\xE1c.");
|
|
1332
2111
|
return alternative;
|
|
1333
2112
|
}
|
|
1334
2113
|
async function promptTeamOwner(currentUserEmail) {
|
|
1335
|
-
return await
|
|
2114
|
+
return await input2({ message: "Team owner email:", default: currentUserEmail });
|
|
1336
2115
|
}
|
|
1337
2116
|
async function maybeCommitWorkspace(workspacePath, skipCommit) {
|
|
1338
2117
|
if (skipCommit) {
|
|
@@ -1344,10 +2123,21 @@ async function maybeCommitWorkspace(workspacePath, skipCommit) {
|
|
|
1344
2123
|
await g.commit("chore: initialize Avatar workspace");
|
|
1345
2124
|
log.success("\u0110\xE3 commit workspace");
|
|
1346
2125
|
}
|
|
1347
|
-
function
|
|
2126
|
+
function formatAiStatusLine(aiResult) {
|
|
2127
|
+
if (aiResult === null) {
|
|
2128
|
+
return ` ${chalk.yellow("AI:")} skipped \xB7 ${chalk.cyan("avatar ai setup")} \u0111\u1EC3 config sau`;
|
|
2129
|
+
}
|
|
2130
|
+
if (aiResult.ok) {
|
|
2131
|
+
const modelPart = aiResult.model ? ` \xB7 model=${aiResult.model}` : "";
|
|
2132
|
+
return ` ${chalk.green("AI:")} ready \xB7 ${aiResult.provider}${modelPart}`;
|
|
2133
|
+
}
|
|
2134
|
+
return ` ${chalk.yellow("AI:")} failed (${aiResult.reason.slice(0, 60)}) \xB7 th\u1EED ${chalk.cyan("avatar ai setup")}`;
|
|
2135
|
+
}
|
|
2136
|
+
function printInitSuccessBox(rootPath, flow, aiResult = null) {
|
|
1348
2137
|
const lines = [
|
|
1349
2138
|
`${chalk.green("\u2713")} Workspace s\u1EB5n s\xE0ng: ${relative2(process.cwd(), rootPath) || rootPath}`,
|
|
1350
2139
|
` ${chalk.dim(`(flow: ${flow})`)}`,
|
|
2140
|
+
formatAiStatusLine(aiResult),
|
|
1351
2141
|
"",
|
|
1352
2142
|
` ${chalk.cyan(`cd ${rootPath}`)}`,
|
|
1353
2143
|
` ${chalk.cyan("claude")} M\u1EDF Claude Code \u1EDF workspace root`,
|
|
@@ -1582,18 +2372,18 @@ function registerSecretsCommand(program2) {
|
|
|
1582
2372
|
}
|
|
1583
2373
|
|
|
1584
2374
|
// src/commands/status.ts
|
|
1585
|
-
import { promises as
|
|
1586
|
-
import { join as
|
|
2375
|
+
import { promises as fs8 } from "fs";
|
|
2376
|
+
import { join as join18 } from "path";
|
|
1587
2377
|
import boxen4 from "boxen";
|
|
1588
2378
|
|
|
1589
2379
|
// src/lib/pack-backup-manager.ts
|
|
1590
|
-
import { promises as
|
|
1591
|
-
import { join as
|
|
2380
|
+
import { promises as fs7 } from "fs";
|
|
2381
|
+
import { join as join17 } from "path";
|
|
1592
2382
|
var BACKUP_DIR_NAME = "_backup";
|
|
1593
2383
|
async function listBackups(projectRoot) {
|
|
1594
|
-
const dir =
|
|
2384
|
+
const dir = join17(projectRoot, ".claude", BACKUP_DIR_NAME);
|
|
1595
2385
|
if (!await pathExists(dir)) return [];
|
|
1596
|
-
const entries = await
|
|
2386
|
+
const entries = await fs7.readdir(dir, { withFileTypes: true });
|
|
1597
2387
|
return entries.filter((e) => e.isDirectory()).map((e) => e.name).sort().reverse();
|
|
1598
2388
|
}
|
|
1599
2389
|
|
|
@@ -1617,7 +2407,7 @@ function registerStatusCommand(program2) {
|
|
|
1617
2407
|
}
|
|
1618
2408
|
async function gatherStatus(cwd) {
|
|
1619
2409
|
const projectName = cwd.split("/").filter(Boolean).pop() ?? "unknown";
|
|
1620
|
-
const claudeRoot =
|
|
2410
|
+
const claudeRoot = join18(cwd, ".claude");
|
|
1621
2411
|
const hasAvatar = await pathExists(claudeRoot);
|
|
1622
2412
|
if (!hasAvatar) {
|
|
1623
2413
|
return {
|
|
@@ -1630,9 +2420,9 @@ async function gatherStatus(cwd) {
|
|
|
1630
2420
|
hasAvatar: false
|
|
1631
2421
|
};
|
|
1632
2422
|
}
|
|
1633
|
-
const packVersion = await isGitRepo(
|
|
1634
|
-
const pendingDir =
|
|
1635
|
-
const pendingCount = await pathExists(pendingDir) ? (await
|
|
2423
|
+
const packVersion = await isGitRepo(join18(claudeRoot, "pack")) ? await readPinnedPackVersion(cwd).catch(() => null) : null;
|
|
2424
|
+
const pendingDir = join18(claudeRoot, "_pending");
|
|
2425
|
+
const pendingCount = await pathExists(pendingDir) ? (await fs8.readdir(pendingDir)).filter((n) => n.endsWith(".diff.md")).length : 0;
|
|
1636
2426
|
const backupCount = (await listBackups(cwd)).length;
|
|
1637
2427
|
const techStackSummary = await readTechStackFirstLine(claudeRoot);
|
|
1638
2428
|
return {
|
|
@@ -1646,7 +2436,7 @@ async function gatherStatus(cwd) {
|
|
|
1646
2436
|
};
|
|
1647
2437
|
}
|
|
1648
2438
|
async function readTechStackFirstLine(claudeRoot) {
|
|
1649
|
-
const techStackPath =
|
|
2439
|
+
const techStackPath = join18(claudeRoot, "project", "tech-stack.md");
|
|
1650
2440
|
if (!await pathExists(techStackPath)) return "(no tech-stack.md)";
|
|
1651
2441
|
const content = await readText(techStackPath);
|
|
1652
2442
|
const firstNonHeaderLine = content.split("\n").find((l) => l.trim() && !l.startsWith("#") && !l.startsWith(">"));
|
|
@@ -1681,33 +2471,33 @@ function registerToolsCommand(program2) {
|
|
|
1681
2471
|
|
|
1682
2472
|
// src/commands/uninstall.ts
|
|
1683
2473
|
import { relative as relative3 } from "path";
|
|
1684
|
-
import { confirm as
|
|
2474
|
+
import { confirm as confirm3 } from "@inquirer/prompts";
|
|
1685
2475
|
import boxen5 from "boxen";
|
|
1686
2476
|
|
|
1687
2477
|
// src/lib/create-uninstall-backup-snapshot.ts
|
|
1688
2478
|
import { cp, mkdir, writeFile } from "fs/promises";
|
|
1689
|
-
import { homedir as
|
|
1690
|
-
import { basename as basename2, join as
|
|
1691
|
-
var UNINSTALL_BACKUPS_DIR =
|
|
2479
|
+
import { homedir as homedir3 } from "os";
|
|
2480
|
+
import { basename as basename2, join as join19 } from "path";
|
|
2481
|
+
var UNINSTALL_BACKUPS_DIR = join19(homedir3(), ".avatar", "uninstall-backups");
|
|
1692
2482
|
async function createUninstallBackupSnapshot(projectRoot, artifacts, avatarVersion) {
|
|
1693
2483
|
const projectName = basename2(projectRoot);
|
|
1694
2484
|
const timestamp = (/* @__PURE__ */ new Date()).toISOString().replace(/[:.]/g, "-");
|
|
1695
|
-
const backupDir =
|
|
2485
|
+
const backupDir = join19(UNINSTALL_BACKUPS_DIR, `${projectName}-${timestamp}`);
|
|
1696
2486
|
await mkdir(backupDir, { recursive: true, mode: 448 });
|
|
1697
2487
|
if (artifacts.claudeDir) {
|
|
1698
|
-
await cp(artifacts.claudeDir,
|
|
2488
|
+
await cp(artifacts.claudeDir, join19(backupDir, ".claude"), { recursive: true });
|
|
1699
2489
|
}
|
|
1700
2490
|
if (artifacts.claudeMd) {
|
|
1701
|
-
await cp(artifacts.claudeMd,
|
|
2491
|
+
await cp(artifacts.claudeMd, join19(backupDir, "CLAUDE.md"));
|
|
1702
2492
|
}
|
|
1703
2493
|
if (artifacts.postMergeHook || artifacts.prePushHook) {
|
|
1704
|
-
const hooksBackupDir =
|
|
2494
|
+
const hooksBackupDir = join19(backupDir, "hooks");
|
|
1705
2495
|
await mkdir(hooksBackupDir, { recursive: true });
|
|
1706
2496
|
if (artifacts.postMergeHook) {
|
|
1707
|
-
await cp(artifacts.postMergeHook,
|
|
2497
|
+
await cp(artifacts.postMergeHook, join19(hooksBackupDir, "post-merge"));
|
|
1708
2498
|
}
|
|
1709
2499
|
if (artifacts.prePushHook) {
|
|
1710
|
-
await cp(artifacts.prePushHook,
|
|
2500
|
+
await cp(artifacts.prePushHook, join19(hooksBackupDir, "pre-push"));
|
|
1711
2501
|
}
|
|
1712
2502
|
}
|
|
1713
2503
|
const manifest = {
|
|
@@ -1722,27 +2512,27 @@ async function createUninstallBackupSnapshot(projectRoot, artifacts, avatarVersi
|
|
|
1722
2512
|
prePushHook: !!artifacts.prePushHook
|
|
1723
2513
|
}
|
|
1724
2514
|
};
|
|
1725
|
-
await writeFile(
|
|
2515
|
+
await writeFile(join19(backupDir, "manifest.json"), JSON.stringify(manifest, null, 2), "utf8");
|
|
1726
2516
|
return backupDir;
|
|
1727
2517
|
}
|
|
1728
2518
|
|
|
1729
2519
|
// src/lib/detect-avatar-project-artifacts.ts
|
|
1730
2520
|
import { existsSync as existsSync5 } from "fs";
|
|
1731
|
-
import { join as
|
|
2521
|
+
import { join as join20 } from "path";
|
|
1732
2522
|
function existsOrNull(path) {
|
|
1733
2523
|
return existsSync5(path) ? path : null;
|
|
1734
2524
|
}
|
|
1735
2525
|
function detectAvatarProjectArtifacts(projectRoot) {
|
|
1736
|
-
const claudeDir = existsOrNull(
|
|
1737
|
-
const claudeMd = existsOrNull(
|
|
1738
|
-
const postMergeHook = existsOrNull(
|
|
2526
|
+
const claudeDir = existsOrNull(join20(projectRoot, ".claude"));
|
|
2527
|
+
const claudeMd = existsOrNull(join20(projectRoot, "CLAUDE.md"));
|
|
2528
|
+
const postMergeHook = existsOrNull(join20(projectRoot, ".git", "hooks", "post-merge"));
|
|
1739
2529
|
const prePushHook = existsOrNull(
|
|
1740
|
-
|
|
2530
|
+
join20(projectRoot, ".git", "modules", "src", "hooks", "pre-push")
|
|
1741
2531
|
);
|
|
1742
|
-
const gitignorePath = existsOrNull(
|
|
1743
|
-
const gitmodulesPath = existsOrNull(
|
|
1744
|
-
const notesDir = existsOrNull(
|
|
1745
|
-
const scriptsDir = existsOrNull(
|
|
2532
|
+
const gitignorePath = existsOrNull(join20(projectRoot, ".gitignore"));
|
|
2533
|
+
const gitmodulesPath = existsOrNull(join20(projectRoot, ".gitmodules"));
|
|
2534
|
+
const notesDir = existsOrNull(join20(projectRoot, "notes"));
|
|
2535
|
+
const scriptsDir = existsOrNull(join20(projectRoot, "scripts"));
|
|
1746
2536
|
const hasAnyArtifact = !!(claudeDir || claudeMd || postMergeHook || prePushHook);
|
|
1747
2537
|
return {
|
|
1748
2538
|
hasAnyArtifact,
|
|
@@ -1763,11 +2553,11 @@ async function executeUninstallDeletion(artifacts, flags) {
|
|
|
1763
2553
|
if (artifacts.claudeDir) {
|
|
1764
2554
|
if (flags.keepSubmodule) {
|
|
1765
2555
|
const { readdir: readdir2 } = await import("fs/promises");
|
|
1766
|
-
const { join:
|
|
2556
|
+
const { join: join21 } = await import("path");
|
|
1767
2557
|
const entries = await readdir2(artifacts.claudeDir);
|
|
1768
2558
|
for (const entry of entries) {
|
|
1769
2559
|
if (entry === "pack") continue;
|
|
1770
|
-
await rm(
|
|
2560
|
+
await rm(join21(artifacts.claudeDir, entry), { recursive: true, force: true });
|
|
1771
2561
|
}
|
|
1772
2562
|
} else {
|
|
1773
2563
|
await rm(artifacts.claudeDir, { recursive: true, force: true });
|
|
@@ -1836,7 +2626,7 @@ async function removeSubmoduleEntry(gitmodulesPath, submodulePath) {
|
|
|
1836
2626
|
}
|
|
1837
2627
|
|
|
1838
2628
|
// src/commands/uninstall.ts
|
|
1839
|
-
var CLI_VERSION = "1.1
|
|
2629
|
+
var CLI_VERSION = "1.2.1";
|
|
1840
2630
|
function registerUninstallCommand(program2) {
|
|
1841
2631
|
program2.command("uninstall").description("G\u1EE1 Avatar kh\u1ECFi project \u2014 backup t\u1EF1 \u0111\u1ED9ng (M11)").option("--yes", "Skip confirm prompt").option("--no-backup", "Kh\xF4ng t\u1EA1o backup tr\u01B0\u1EDBc khi x\xF3a (nguy hi\u1EC3m)").option("--keep-submodule", "Gi\u1EEF submodule .claude/pack/").option("--keep-hooks", "Gi\u1EEF git hooks post-merge, pre-push").option("--dry-run", "Hi\u1EC3n th\u1ECB danh s\xE1ch s\u1EBD x\xF3a, kh\xF4ng th\u1EF1c thi").action(async (opts) => {
|
|
1842
2632
|
try {
|
|
@@ -1860,7 +2650,7 @@ async function runUninstall(opts) {
|
|
|
1860
2650
|
return;
|
|
1861
2651
|
}
|
|
1862
2652
|
if (!opts.yes) {
|
|
1863
|
-
const ok = await
|
|
2653
|
+
const ok = await confirm3({
|
|
1864
2654
|
message: "Ti\u1EBFp t\u1EE5c g\u1EE1 Avatar?",
|
|
1865
2655
|
default: false
|
|
1866
2656
|
});
|
|
@@ -1918,7 +2708,7 @@ function printUninstallSuccessBox(backupPath) {
|
|
|
1918
2708
|
}
|
|
1919
2709
|
|
|
1920
2710
|
// src/index.ts
|
|
1921
|
-
var CLI_VERSION2 = "1.1
|
|
2711
|
+
var CLI_VERSION2 = "1.2.1";
|
|
1922
2712
|
var program = new Command();
|
|
1923
2713
|
program.name("avatar").description("AI harness CLI for NAL Vietnam engineering").version(CLI_VERSION2, "-v, --version", "Hi\u1EC3n th\u1ECB phi\xEAn b\u1EA3n Avatar CLI").addHelpText(
|
|
1924
2714
|
"beforeAll",
|
|
@@ -1944,6 +2734,7 @@ registerCommitCommand(program);
|
|
|
1944
2734
|
registerToolsCommand(program);
|
|
1945
2735
|
registerSecretsCommand(program);
|
|
1946
2736
|
registerMcpRunCommand(program);
|
|
2737
|
+
registerAiCommand(program);
|
|
1947
2738
|
registerUninstallCommand(program);
|
|
1948
2739
|
program.parseAsync(process.argv).catch((err) => {
|
|
1949
2740
|
const msg = err instanceof Error ? err.message : String(err);
|