@nalvietnam/avatar-cli 1.1.6 → 1.2.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/index.js +1045 -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,574 @@ 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"], { stdio: "ignore" });
|
|
161
|
+
if (result.error || result.status !== 0) return "not-authenticated";
|
|
162
|
+
return "authenticated";
|
|
163
|
+
}
|
|
164
|
+
function triggerClaudeCodeAuthLogin() {
|
|
165
|
+
log.info("Kh\u1EDFi \u0111\u1ED9ng \u0111\u0103ng nh\u1EADp Claude Code (browser s\u1EBD m\u1EDF)...");
|
|
166
|
+
const result = spawnSync("claude", ["auth", "login"], { stdio: "inherit" });
|
|
167
|
+
if (result.status !== 0) {
|
|
168
|
+
throw new Error(
|
|
169
|
+
`claude auth login th\u1EA5t b\u1EA1i (exit ${result.status}). Th\u1EED 'claude auth login' tay r\u1ED3i ch\u1EA1y l\u1EA1i.`
|
|
170
|
+
);
|
|
171
|
+
}
|
|
172
|
+
log.success("\u0110\xE3 \u0111\u0103ng nh\u1EADp Claude Code");
|
|
173
|
+
}
|
|
174
|
+
function classifyQuotaError(combinedOutput) {
|
|
175
|
+
const text = combinedOutput.toLowerCase();
|
|
176
|
+
if (text.includes("credit_balance_too_low") || text.includes("credit balance too low")) {
|
|
177
|
+
return "credit_balance_too_low";
|
|
178
|
+
}
|
|
179
|
+
if (text.includes("insufficient_quota") || text.includes("insufficient quota")) {
|
|
180
|
+
return "insufficient_quota";
|
|
181
|
+
}
|
|
182
|
+
if (text.includes("invalid_api_key") || text.includes("invalid api key")) {
|
|
183
|
+
return "invalid_api_key";
|
|
184
|
+
}
|
|
185
|
+
if (text.includes("rate_limit") || text.includes("rate limit")) {
|
|
186
|
+
return "rate_limit";
|
|
187
|
+
}
|
|
188
|
+
return "unknown";
|
|
189
|
+
}
|
|
190
|
+
function verifyClaudeCodeQuota() {
|
|
191
|
+
const result = spawnSync("claude", ["--print", QUOTA_VERIFY_PROMPT], {
|
|
192
|
+
encoding: "utf8",
|
|
193
|
+
timeout: QUOTA_VERIFY_TIMEOUT_MS
|
|
194
|
+
});
|
|
195
|
+
if (result.signal === "SIGTERM") {
|
|
196
|
+
return { ok: false, reason: "timeout", detail: "claude --print > 30s" };
|
|
197
|
+
}
|
|
198
|
+
const stderr = result.stderr || "";
|
|
199
|
+
const stdout = result.stdout || "";
|
|
200
|
+
if (result.status === 0) {
|
|
201
|
+
return { ok: true };
|
|
202
|
+
}
|
|
203
|
+
const reason = classifyQuotaError(`${stderr}
|
|
204
|
+
${stdout}`);
|
|
205
|
+
return { ok: false, reason, detail: stderr.slice(0, 500) };
|
|
206
|
+
}
|
|
207
|
+
|
|
208
|
+
// src/lib/detect-claude-code-installation.ts
|
|
209
|
+
import { spawnSync as spawnSync2 } from "child_process";
|
|
210
|
+
|
|
211
|
+
// src/lib/detect-host-platform.ts
|
|
212
|
+
import { platform } from "os";
|
|
213
|
+
function detectHostPlatform() {
|
|
214
|
+
const p = platform();
|
|
215
|
+
if (p === "darwin" || p === "linux" || p === "win32") return p;
|
|
216
|
+
return "unsupported";
|
|
217
|
+
}
|
|
218
|
+
|
|
219
|
+
// src/lib/detect-claude-code-installation.ts
|
|
220
|
+
var VERSION_PROBE_TIMEOUT_MS = 5e3;
|
|
221
|
+
var SEMVER_REGEX = /(\d+\.\d+\.\d+)/;
|
|
222
|
+
function probeClaudeBinaryPath() {
|
|
223
|
+
const isWindows = detectHostPlatform() === "win32";
|
|
224
|
+
const probeCmd = isWindows ? "where" : "command";
|
|
225
|
+
const probeArgs = isWindows ? ["claude"] : ["-v", "claude"];
|
|
226
|
+
const result = spawnSync2(probeCmd, probeArgs, {
|
|
227
|
+
encoding: "utf8",
|
|
228
|
+
shell: !isWindows
|
|
229
|
+
});
|
|
230
|
+
if (result.error || result.status !== 0) return null;
|
|
231
|
+
const out = (result.stdout || "").trim();
|
|
232
|
+
if (!out) return null;
|
|
233
|
+
return out.split(/\r?\n/)[0].trim();
|
|
234
|
+
}
|
|
235
|
+
function probeClaudeVersion() {
|
|
236
|
+
const result = spawnSync2("claude", ["--version"], {
|
|
237
|
+
encoding: "utf8",
|
|
238
|
+
timeout: VERSION_PROBE_TIMEOUT_MS
|
|
239
|
+
});
|
|
240
|
+
if (result.error || result.status !== 0) return null;
|
|
241
|
+
const out = (result.stdout || "").trim();
|
|
242
|
+
const match = SEMVER_REGEX.exec(out);
|
|
243
|
+
return match ? match[1] : null;
|
|
244
|
+
}
|
|
245
|
+
function detectClaudeCodeInstallation() {
|
|
246
|
+
const path = probeClaudeBinaryPath();
|
|
247
|
+
if (!path) {
|
|
248
|
+
return { installed: false, version: null, path: null };
|
|
249
|
+
}
|
|
250
|
+
const version = probeClaudeVersion();
|
|
251
|
+
return { installed: true, version, path };
|
|
252
|
+
}
|
|
253
|
+
|
|
254
|
+
// src/lib/install-claude-code-via-npm.ts
|
|
255
|
+
import { spawnSync as spawnSync3 } from "child_process";
|
|
256
|
+
var NPM_INSTALL_TIMEOUT_MS = 5 * 60 * 1e3;
|
|
257
|
+
var CLAUDE_CODE_PACKAGE = "@anthropic-ai/claude-code";
|
|
258
|
+
var InstallClaudeCodeError = class extends Error {
|
|
259
|
+
reason;
|
|
260
|
+
exitCode;
|
|
261
|
+
constructor(reason, message, exitCode = null) {
|
|
262
|
+
super(message);
|
|
263
|
+
this.name = "InstallClaudeCodeError";
|
|
264
|
+
this.reason = reason;
|
|
265
|
+
this.exitCode = exitCode;
|
|
266
|
+
}
|
|
267
|
+
};
|
|
268
|
+
function classifyNpmFailure(exitCode, stderrSample) {
|
|
269
|
+
const stderr = stderrSample.toLowerCase();
|
|
270
|
+
if (stderr.includes("eacces") || stderr.includes("permission denied")) {
|
|
271
|
+
return new InstallClaudeCodeError(
|
|
272
|
+
"permission-denied",
|
|
273
|
+
`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).`,
|
|
274
|
+
exitCode
|
|
275
|
+
);
|
|
276
|
+
}
|
|
277
|
+
if (stderr.includes("enospc") || stderr.includes("no space")) {
|
|
278
|
+
return new InstallClaudeCodeError(
|
|
279
|
+
"disk-full",
|
|
280
|
+
"\u0110\u0129a \u0111\u1EA7y. Free disk space r\u1ED3i th\u1EED l\u1EA1i.",
|
|
281
|
+
exitCode
|
|
282
|
+
);
|
|
283
|
+
}
|
|
284
|
+
return new InstallClaudeCodeError(
|
|
285
|
+
"generic",
|
|
286
|
+
`npm install th\u1EA5t b\u1EA1i (exit ${exitCode ?? "null"}). Xem log npm ph\xEDa tr\xEAn.`,
|
|
287
|
+
exitCode
|
|
288
|
+
);
|
|
289
|
+
}
|
|
290
|
+
function installClaudeCodeViaNpm() {
|
|
291
|
+
log.info("\u0110ang c\xE0i Claude Code qua npm (c\xF3 th\u1EC3 m\u1EA5t 1-2 ph\xFAt)...");
|
|
292
|
+
const result = spawnSync3("npm", ["install", "-g", CLAUDE_CODE_PACKAGE], {
|
|
293
|
+
stdio: ["inherit", "inherit", "pipe"],
|
|
294
|
+
timeout: NPM_INSTALL_TIMEOUT_MS,
|
|
295
|
+
encoding: "utf8"
|
|
296
|
+
});
|
|
297
|
+
if (result.signal === "SIGTERM") {
|
|
298
|
+
throw new InstallClaudeCodeError(
|
|
299
|
+
"timeout",
|
|
300
|
+
`npm install timeout sau ${NPM_INSTALL_TIMEOUT_MS / 1e3}s. Check m\u1EA1ng r\u1ED3i th\u1EED l\u1EA1i.`,
|
|
301
|
+
null
|
|
302
|
+
);
|
|
303
|
+
}
|
|
304
|
+
if (result.status !== 0) {
|
|
305
|
+
if (result.stderr) process.stderr.write(result.stderr);
|
|
306
|
+
throw classifyNpmFailure(result.status, result.stderr || "");
|
|
307
|
+
}
|
|
308
|
+
const probe = detectClaudeCodeInstallation();
|
|
309
|
+
if (!probe.installed || !probe.path) {
|
|
310
|
+
throw new InstallClaudeCodeError(
|
|
311
|
+
"binary-not-in-path",
|
|
312
|
+
"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.",
|
|
313
|
+
null
|
|
314
|
+
);
|
|
315
|
+
}
|
|
316
|
+
log.success(`\u0110\xE3 c\xE0i Claude Code${probe.version ? ` v${probe.version}` : ""} t\u1EA1i ${probe.path}`);
|
|
317
|
+
return { version: probe.version, path: probe.path };
|
|
318
|
+
}
|
|
319
|
+
|
|
320
|
+
// src/lib/prompt-ai-provider-choice.ts
|
|
321
|
+
import { readFileSync } from "fs";
|
|
322
|
+
import { homedir as homedir2 } from "os";
|
|
323
|
+
import { join as join3 } from "path";
|
|
324
|
+
import { select } from "@inquirer/prompts";
|
|
325
|
+
function getGlobalSettingsPath() {
|
|
326
|
+
return join3(homedir2(), ".claude", "settings.json");
|
|
327
|
+
}
|
|
328
|
+
function detectGlobalClaudeSettings() {
|
|
329
|
+
const path = getGlobalSettingsPath();
|
|
330
|
+
let raw;
|
|
331
|
+
try {
|
|
332
|
+
raw = readFileSync(path, "utf8");
|
|
333
|
+
} catch {
|
|
334
|
+
return { exists: false, hasBaseUrl: false, hasToken: false };
|
|
335
|
+
}
|
|
336
|
+
let parsed;
|
|
337
|
+
try {
|
|
338
|
+
parsed = JSON.parse(raw);
|
|
339
|
+
} catch {
|
|
340
|
+
return { exists: true, hasBaseUrl: false, hasToken: false };
|
|
341
|
+
}
|
|
342
|
+
const env = parsed.env || {};
|
|
343
|
+
const baseUrl = typeof env.ANTHROPIC_BASE_URL === "string" ? env.ANTHROPIC_BASE_URL : void 0;
|
|
344
|
+
const hasToken = typeof env.ANTHROPIC_AUTH_TOKEN === "string" && env.ANTHROPIC_AUTH_TOKEN.length > 0;
|
|
345
|
+
const model = typeof parsed.model === "string" ? parsed.model : void 0;
|
|
346
|
+
return {
|
|
347
|
+
exists: true,
|
|
348
|
+
hasBaseUrl: !!baseUrl,
|
|
349
|
+
baseUrl,
|
|
350
|
+
hasToken,
|
|
351
|
+
model,
|
|
352
|
+
rawSettings: parsed
|
|
353
|
+
};
|
|
354
|
+
}
|
|
355
|
+
async function promptAiProviderChoice(globalInfo = detectGlobalClaudeSettings()) {
|
|
356
|
+
if (globalInfo.exists && globalInfo.hasBaseUrl && globalInfo.hasToken) {
|
|
357
|
+
const choice = await select({
|
|
358
|
+
message: `Ph\xE1t hi\u1EC7n AI config global (base URL: ${globalInfo.baseUrl}). D\xF9ng cho project n\xE0y?`,
|
|
359
|
+
choices: [
|
|
360
|
+
{
|
|
361
|
+
name: "a. Yes \u2014 copy config global v\xE0o .claude/settings.json (per-project)",
|
|
362
|
+
value: "use-global"
|
|
363
|
+
},
|
|
364
|
+
{
|
|
365
|
+
name: "b. No \u2014 setup ri\xEAng (ch\u1ECDn provider kh\xE1c)",
|
|
366
|
+
value: "setup-fresh"
|
|
367
|
+
}
|
|
368
|
+
]
|
|
369
|
+
});
|
|
370
|
+
if (choice === "use-global") return "use-global";
|
|
371
|
+
}
|
|
372
|
+
return await select({
|
|
373
|
+
message: "Ch\u1ECDn provider cho AI features:",
|
|
374
|
+
choices: [
|
|
375
|
+
{
|
|
376
|
+
name: "1. Claude Code Subscription (d\xF9ng quota c\xE1 nh\xE2n Anthropic)",
|
|
377
|
+
value: "subscription"
|
|
378
|
+
},
|
|
379
|
+
{
|
|
380
|
+
name: "2. LLMLite API key (llm.nal.vn \u2014 NAL c\u1EA5p)",
|
|
381
|
+
value: "llmlite"
|
|
382
|
+
}
|
|
383
|
+
]
|
|
384
|
+
});
|
|
385
|
+
}
|
|
386
|
+
|
|
387
|
+
// src/lib/setup-llmlite-api-key-and-model.ts
|
|
388
|
+
import { input, password, select as select2 } from "@inquirer/prompts";
|
|
389
|
+
var DEFAULT_BASE_URL = "https://llm.nal.vn";
|
|
390
|
+
var FETCH_TIMEOUT_MS = 1e4;
|
|
391
|
+
function maskApiKey(key) {
|
|
392
|
+
if (key.length <= 8) return "sk-***";
|
|
393
|
+
return `${key.slice(0, 3)}...${key.slice(-4)}`;
|
|
394
|
+
}
|
|
395
|
+
async function promptApiKeyHidden() {
|
|
396
|
+
return await password({
|
|
397
|
+
message: "LLMLite API key (\u1EA9n input):",
|
|
398
|
+
mask: "*",
|
|
399
|
+
validate: (v) => v.trim().length > 0 ? true : "API key b\u1EAFt bu\u1ED9c"
|
|
400
|
+
});
|
|
401
|
+
}
|
|
402
|
+
async function promptBaseUrl(defaultUrl = DEFAULT_BASE_URL) {
|
|
403
|
+
const value = await input({
|
|
404
|
+
message: "LLMLite base URL:",
|
|
405
|
+
default: defaultUrl,
|
|
406
|
+
validate: (v) => /^https?:\/\//.test(v) ? true : "Ph\u1EA3i l\xE0 URL h\u1EE3p l\u1EC7 (http/https)"
|
|
407
|
+
});
|
|
408
|
+
return value.replace(/\/+$/, "");
|
|
409
|
+
}
|
|
410
|
+
async function fetchAvailableModels(baseUrl, apiKey) {
|
|
411
|
+
const controller = new AbortController();
|
|
412
|
+
const timer = setTimeout(() => controller.abort(), FETCH_TIMEOUT_MS);
|
|
413
|
+
try {
|
|
414
|
+
const res = await fetch(`${baseUrl}/v1/models`, {
|
|
415
|
+
method: "GET",
|
|
416
|
+
headers: {
|
|
417
|
+
Authorization: `Bearer ${apiKey}`,
|
|
418
|
+
Accept: "application/json"
|
|
419
|
+
},
|
|
420
|
+
signal: controller.signal
|
|
421
|
+
});
|
|
422
|
+
if (res.status === 401 || res.status === 403) {
|
|
423
|
+
throw new Error(`API key invalid (HTTP ${res.status}).`);
|
|
424
|
+
}
|
|
425
|
+
if (res.status === 404) {
|
|
426
|
+
throw new Error(`Endpoint /v1/models kh\xF4ng t\u1ED3n t\u1EA1i tr\xEAn ${baseUrl}.`);
|
|
427
|
+
}
|
|
428
|
+
if (!res.ok) {
|
|
429
|
+
throw new Error(`Fetch models th\u1EA5t b\u1EA1i (HTTP ${res.status}).`);
|
|
430
|
+
}
|
|
431
|
+
const json = await res.json();
|
|
432
|
+
const models = (json.data || []).map((m) => typeof m.id === "string" ? m.id : null).filter((id) => id !== null);
|
|
433
|
+
if (models.length === 0) {
|
|
434
|
+
throw new Error("LLMLite tr\u1EA3 v\u1EC1 list r\u1ED7ng. Li\xEAn h\u1EC7 admin NAL.");
|
|
435
|
+
}
|
|
436
|
+
return models;
|
|
437
|
+
} catch (err) {
|
|
438
|
+
if (err.name === "AbortError") {
|
|
439
|
+
throw new Error(`Connect ${baseUrl} timeout sau ${FETCH_TIMEOUT_MS / 1e3}s.`);
|
|
440
|
+
}
|
|
441
|
+
throw err;
|
|
442
|
+
} finally {
|
|
443
|
+
clearTimeout(timer);
|
|
444
|
+
}
|
|
445
|
+
}
|
|
446
|
+
async function promptModelChoice(models) {
|
|
447
|
+
const claudeAliases = models.filter((m) => m.toLowerCase().includes("claude"));
|
|
448
|
+
if (claudeAliases.length === 1) {
|
|
449
|
+
log.info(`Auto-pick model: ${claudeAliases[0]} (ch\u1EC9 1 claude alias tr\xEAn endpoint)`);
|
|
450
|
+
return claudeAliases[0];
|
|
451
|
+
}
|
|
452
|
+
const choiceList = claudeAliases.length > 0 ? claudeAliases : models;
|
|
453
|
+
return await select2({
|
|
454
|
+
message: "Ch\u1ECDn model m\u1EB7c \u0111\u1ECBnh cho project:",
|
|
455
|
+
choices: choiceList.map((m) => ({ name: m, value: m }))
|
|
456
|
+
});
|
|
457
|
+
}
|
|
458
|
+
async function setupLLMLiteApiKeyAndModel() {
|
|
459
|
+
const apiKey = await promptApiKeyHidden();
|
|
460
|
+
const baseUrl = await promptBaseUrl();
|
|
461
|
+
log.info(`Verify key (${maskApiKey(apiKey)}) qua ${baseUrl}/v1/models...`);
|
|
462
|
+
const models = await fetchAvailableModels(baseUrl, apiKey);
|
|
463
|
+
log.success(`Endpoint OK \u2014 ${models.length} models available`);
|
|
464
|
+
const model = await promptModelChoice(models);
|
|
465
|
+
return { apiKey, baseUrl, model };
|
|
466
|
+
}
|
|
467
|
+
|
|
468
|
+
// src/lib/write-claude-settings-json-per-project.ts
|
|
469
|
+
import { promises as fs3 } from "fs";
|
|
470
|
+
import { join as join4 } from "path";
|
|
471
|
+
var SECRET_FILE_MODE2 = 384;
|
|
472
|
+
function getClaudeSettingsPath(workspacePath) {
|
|
473
|
+
return join4(workspacePath, ".claude", "settings.json");
|
|
474
|
+
}
|
|
475
|
+
async function readExistingSettings(path) {
|
|
476
|
+
if (!await pathExists(path)) return {};
|
|
477
|
+
try {
|
|
478
|
+
return await readJson(path);
|
|
479
|
+
} catch (err) {
|
|
480
|
+
throw new Error(
|
|
481
|
+
`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.`
|
|
482
|
+
);
|
|
483
|
+
}
|
|
484
|
+
}
|
|
485
|
+
function applySubscription(existing, model) {
|
|
486
|
+
const { env: existingEnv, ...rest } = existing;
|
|
487
|
+
const merged = { ...rest, model };
|
|
488
|
+
if (existingEnv) {
|
|
489
|
+
const { ANTHROPIC_BASE_URL: _b, ANTHROPIC_AUTH_TOKEN: _t, ...envRest } = existingEnv;
|
|
490
|
+
if (Object.keys(envRest).length > 0) {
|
|
491
|
+
merged.env = envRest;
|
|
492
|
+
}
|
|
493
|
+
}
|
|
494
|
+
return merged;
|
|
495
|
+
}
|
|
496
|
+
function applyLLMLite(existing, apiKey, baseUrl, model) {
|
|
497
|
+
return {
|
|
498
|
+
...existing,
|
|
499
|
+
env: {
|
|
500
|
+
...existing.env || {},
|
|
501
|
+
ANTHROPIC_BASE_URL: baseUrl,
|
|
502
|
+
ANTHROPIC_AUTH_TOKEN: apiKey
|
|
503
|
+
},
|
|
504
|
+
model
|
|
505
|
+
};
|
|
506
|
+
}
|
|
507
|
+
function applyUseGlobal(existing, source) {
|
|
508
|
+
const sourceEnv = source.env || {};
|
|
509
|
+
const sourceModel = typeof source.model === "string" ? source.model : void 0;
|
|
510
|
+
return {
|
|
511
|
+
...existing,
|
|
512
|
+
env: {
|
|
513
|
+
...existing.env || {},
|
|
514
|
+
...sourceEnv
|
|
515
|
+
},
|
|
516
|
+
...sourceModel ? { model: sourceModel } : {}
|
|
517
|
+
};
|
|
518
|
+
}
|
|
519
|
+
async function writeClaudeSettings(workspacePath, input3) {
|
|
520
|
+
const path = getClaudeSettingsPath(workspacePath);
|
|
521
|
+
const existing = await readExistingSettings(path);
|
|
522
|
+
let merged;
|
|
523
|
+
switch (input3.provider) {
|
|
524
|
+
case "subscription":
|
|
525
|
+
merged = applySubscription(existing, input3.model);
|
|
526
|
+
break;
|
|
527
|
+
case "llmlite":
|
|
528
|
+
merged = applyLLMLite(existing, input3.apiKey, input3.baseUrl, input3.model);
|
|
529
|
+
break;
|
|
530
|
+
case "use-global":
|
|
531
|
+
merged = applyUseGlobal(existing, input3.sourceSettings);
|
|
532
|
+
break;
|
|
533
|
+
}
|
|
534
|
+
await writeJsonAtomic(path, merged, SECRET_FILE_MODE2);
|
|
535
|
+
try {
|
|
536
|
+
await fs3.chmod(path, SECRET_FILE_MODE2);
|
|
537
|
+
} catch {
|
|
538
|
+
}
|
|
539
|
+
return { path, mode: SECRET_FILE_MODE2 };
|
|
540
|
+
}
|
|
541
|
+
|
|
542
|
+
// src/lib/run-ai-setup-phase.ts
|
|
543
|
+
var SUBSCRIPTION_DEFAULT_MODEL = "sonnet";
|
|
544
|
+
async function runAiSetupPhase(args) {
|
|
545
|
+
try {
|
|
546
|
+
log.info("Setup AI provider cho workspace...");
|
|
547
|
+
let info = detectClaudeCodeInstallation();
|
|
548
|
+
if (!info.installed) {
|
|
549
|
+
log.info("Ch\u01B0a c\xF3 Claude Code \u2014 s\u1EBD t\u1EF1 c\xE0i qua npm.");
|
|
550
|
+
installClaudeCodeViaNpm();
|
|
551
|
+
info = detectClaudeCodeInstallation();
|
|
552
|
+
if (!info.installed) {
|
|
553
|
+
throw new Error("C\xE0i Claude Code xong nh\u01B0ng v\u1EABn kh\xF4ng detect \u0111\u01B0\u1EE3c binary.");
|
|
554
|
+
}
|
|
555
|
+
} else {
|
|
556
|
+
log.success(`Claude Code \u0111\xE3 c\xF3${info.version ? ` v${info.version}` : ""}`);
|
|
557
|
+
}
|
|
558
|
+
const globalInfo = detectGlobalClaudeSettings();
|
|
559
|
+
const choice = await promptAiProviderChoice(globalInfo);
|
|
560
|
+
switch (choice) {
|
|
561
|
+
case "subscription": {
|
|
562
|
+
if (checkClaudeCodeSubscriptionAuth() !== "authenticated") {
|
|
563
|
+
triggerClaudeCodeAuthLogin();
|
|
564
|
+
}
|
|
565
|
+
const quota = verifyClaudeCodeQuota();
|
|
566
|
+
if (!quota.ok) {
|
|
567
|
+
await appendAuditEntry(
|
|
568
|
+
"ai_setup",
|
|
569
|
+
`provider=subscription,result=no-quota,reason=${quota.reason ?? "unknown"}`
|
|
570
|
+
);
|
|
571
|
+
log.warn(
|
|
572
|
+
`Subscription verify th\u1EA5t b\u1EA1i (${quota.reason ?? "unknown"}). Suggest LLMLite ho\u1EB7c upgrade plan.`
|
|
573
|
+
);
|
|
574
|
+
return { ok: false, reason: `subscription-${quota.reason ?? "unknown"}`, phase: "quota" };
|
|
575
|
+
}
|
|
576
|
+
await writeClaudeSettings(args.workspacePath, {
|
|
577
|
+
provider: "subscription",
|
|
578
|
+
model: SUBSCRIPTION_DEFAULT_MODEL
|
|
579
|
+
});
|
|
580
|
+
await appendAuditEntry("ai_setup", "provider=subscription,result=ok");
|
|
581
|
+
log.success(`AI ready \xB7 Subscription \xB7 model=${SUBSCRIPTION_DEFAULT_MODEL}`);
|
|
582
|
+
return { ok: true, provider: "subscription", model: SUBSCRIPTION_DEFAULT_MODEL };
|
|
583
|
+
}
|
|
584
|
+
case "llmlite": {
|
|
585
|
+
const llmConfig = await setupLLMLiteApiKeyAndModel();
|
|
586
|
+
await writeClaudeSettings(args.workspacePath, {
|
|
587
|
+
provider: "llmlite",
|
|
588
|
+
apiKey: llmConfig.apiKey,
|
|
589
|
+
baseUrl: llmConfig.baseUrl,
|
|
590
|
+
model: llmConfig.model
|
|
591
|
+
});
|
|
592
|
+
await appendAuditEntry(
|
|
593
|
+
"ai_setup",
|
|
594
|
+
`provider=llmlite,result=ok,model=${llmConfig.model},base=${llmConfig.baseUrl}`
|
|
595
|
+
);
|
|
596
|
+
log.success(`AI ready \xB7 LLMLite \xB7 model=${llmConfig.model} \xB7 ${llmConfig.baseUrl}`);
|
|
597
|
+
return { ok: true, provider: "llmlite", model: llmConfig.model };
|
|
598
|
+
}
|
|
599
|
+
case "use-global": {
|
|
600
|
+
if (!globalInfo.rawSettings) {
|
|
601
|
+
throw new Error("use-global ch\u1ECDn nh\u01B0ng kh\xF4ng \u0111\u1ECDc \u0111\u01B0\u1EE3c global settings.");
|
|
602
|
+
}
|
|
603
|
+
await writeClaudeSettings(args.workspacePath, {
|
|
604
|
+
provider: "use-global",
|
|
605
|
+
sourceSettings: globalInfo.rawSettings
|
|
606
|
+
});
|
|
607
|
+
await appendAuditEntry("ai_setup", "provider=use-global,result=ok");
|
|
608
|
+
log.success(`AI ready \xB7 Copy t\u1EEB global config (${globalInfo.baseUrl ?? "subscription"})`);
|
|
609
|
+
return { ok: true, provider: "use-global", model: globalInfo.model };
|
|
610
|
+
}
|
|
611
|
+
}
|
|
612
|
+
} catch (err) {
|
|
613
|
+
const message = err instanceof Error ? err.message : String(err);
|
|
614
|
+
log.warn(`AI setup th\u1EA5t b\u1EA1i: ${message}`);
|
|
615
|
+
log.dim("Workspace v\u1EABn s\u1EB5n s\xE0ng. Setup AI sau qua: avatar ai setup");
|
|
616
|
+
await appendAuditEntry("ai_setup", `result=failed,error=${message.slice(0, 200)}`);
|
|
617
|
+
return { ok: false, reason: message };
|
|
618
|
+
}
|
|
619
|
+
}
|
|
620
|
+
|
|
621
|
+
// src/commands/ai.ts
|
|
622
|
+
async function ensureWorkspaceCwd() {
|
|
623
|
+
const cwd = process.cwd();
|
|
624
|
+
const claudeDir = join5(cwd, ".claude");
|
|
625
|
+
if (!await pathExists(claudeDir)) {
|
|
626
|
+
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.");
|
|
627
|
+
process.exit(1);
|
|
628
|
+
}
|
|
629
|
+
return cwd;
|
|
630
|
+
}
|
|
631
|
+
async function readWorkspaceSettings(workspacePath) {
|
|
632
|
+
const settingsPath = join5(workspacePath, ".claude", "settings.json");
|
|
633
|
+
if (!await pathExists(settingsPath)) return {};
|
|
634
|
+
try {
|
|
635
|
+
return await readJson(settingsPath);
|
|
636
|
+
} catch {
|
|
637
|
+
return {};
|
|
638
|
+
}
|
|
639
|
+
}
|
|
640
|
+
async function runAiSetup() {
|
|
641
|
+
const workspacePath = await ensureWorkspaceCwd();
|
|
642
|
+
await runAiSetupPhase({ workspacePath });
|
|
643
|
+
}
|
|
644
|
+
async function runAiStatus() {
|
|
645
|
+
const workspacePath = await ensureWorkspaceCwd();
|
|
646
|
+
const settings = await readWorkspaceSettings(workspacePath);
|
|
647
|
+
const env = settings.env || {};
|
|
648
|
+
const baseUrl = typeof env.ANTHROPIC_BASE_URL === "string" ? env.ANTHROPIC_BASE_URL : void 0;
|
|
649
|
+
const token = typeof env.ANTHROPIC_AUTH_TOKEN === "string" ? env.ANTHROPIC_AUTH_TOKEN : void 0;
|
|
650
|
+
const model = typeof settings.model === "string" ? settings.model : void 0;
|
|
651
|
+
const provider = baseUrl ? "LLMLite" : token ? "Custom" : "Subscription (default)";
|
|
652
|
+
log.info(`Project: ${workspacePath}`);
|
|
653
|
+
log.info(`Provider: ${provider}${baseUrl ? ` (${baseUrl})` : ""}`);
|
|
654
|
+
log.info(`Model: ${model ?? "(default \u2014 Claude Code ch\u1ECDn)"}`);
|
|
655
|
+
log.info(`Token: ${token ? maskApiKey(token) : "(kh\xF4ng set \u2014 d\xF9ng subscription auth)"}`);
|
|
656
|
+
}
|
|
657
|
+
async function runAiTest() {
|
|
658
|
+
await ensureWorkspaceCwd();
|
|
659
|
+
log.info("Calling provider v\u1EDBi prompt 'say ok'...");
|
|
660
|
+
const result = spawnSync4("claude", ["--print", "say ok"], {
|
|
661
|
+
encoding: "utf8",
|
|
662
|
+
timeout: 3e4
|
|
663
|
+
});
|
|
664
|
+
if (result.signal === "SIGTERM") {
|
|
665
|
+
log.error("Timeout sau 30s. Check m\u1EA1ng / endpoint.");
|
|
666
|
+
process.exit(1);
|
|
667
|
+
}
|
|
668
|
+
if (result.status !== 0) {
|
|
669
|
+
log.error(`Test th\u1EA5t b\u1EA1i (exit ${result.status}).`);
|
|
670
|
+
if (result.stderr) process.stderr.write(`${result.stderr}
|
|
671
|
+
`);
|
|
672
|
+
process.exit(1);
|
|
673
|
+
}
|
|
674
|
+
log.success(`Response: ${(result.stdout || "").trim().slice(0, 200)}`);
|
|
675
|
+
log.success("AI provider working");
|
|
676
|
+
}
|
|
677
|
+
async function runAiReset(opts) {
|
|
678
|
+
const workspacePath = await ensureWorkspaceCwd();
|
|
679
|
+
const settingsPath = join5(workspacePath, ".claude", "settings.json");
|
|
680
|
+
const settings = await readWorkspaceSettings(workspacePath);
|
|
681
|
+
if (!opts.yes) {
|
|
682
|
+
const ok = await confirm({
|
|
683
|
+
message: "X\xF3a AI config (v\u1EC1 d\xF9ng Claude Code Subscription default)?",
|
|
684
|
+
default: false
|
|
685
|
+
});
|
|
686
|
+
if (!ok) {
|
|
687
|
+
log.dim("\u0110\xE3 h\u1EE7y.");
|
|
688
|
+
return;
|
|
689
|
+
}
|
|
690
|
+
}
|
|
691
|
+
const { env: existingEnv, ...rest } = settings;
|
|
692
|
+
const reset = { ...rest };
|
|
693
|
+
if (existingEnv) {
|
|
694
|
+
const { ANTHROPIC_BASE_URL: _b, ANTHROPIC_AUTH_TOKEN: _t, ...envRest } = existingEnv;
|
|
695
|
+
if (Object.keys(envRest).length > 0) {
|
|
696
|
+
reset.env = envRest;
|
|
697
|
+
}
|
|
698
|
+
}
|
|
699
|
+
if (Object.keys(reset).length === 0) {
|
|
700
|
+
await fs4.unlink(settingsPath).catch(() => {
|
|
701
|
+
});
|
|
702
|
+
log.success("\u0110\xE3 x\xF3a .claude/settings.json (clean state)");
|
|
703
|
+
} else {
|
|
704
|
+
await writeJsonAtomic(settingsPath, reset, 384);
|
|
705
|
+
log.success("\u0110\xE3 reset env block trong .claude/settings.json");
|
|
706
|
+
}
|
|
707
|
+
}
|
|
708
|
+
function registerAiCommand(program2) {
|
|
709
|
+
const ai = program2.command("ai").description("Qu\u1EA3n l\xFD AI provider config (M12)");
|
|
710
|
+
ai.command("setup").description("Wizard setup/re-config AI provider cho workspace hi\u1EC7n t\u1EA1i").action(async () => {
|
|
711
|
+
await runAiSetup();
|
|
712
|
+
});
|
|
713
|
+
ai.command("status").description("Show AI config hi\u1EC7n t\u1EA1i (mask token)").action(async () => {
|
|
714
|
+
await runAiStatus();
|
|
715
|
+
});
|
|
716
|
+
ai.command("test").description("Verify AI provider qua cheap prompt").action(async () => {
|
|
717
|
+
await runAiTest();
|
|
718
|
+
});
|
|
719
|
+
ai.command("reset").description("X\xF3a env.ANTHROPIC_* kh\u1ECFi settings.json (v\u1EC1 Subscription default)").option("--yes", "Skip confirm").action(async (opts) => {
|
|
720
|
+
await runAiReset(opts);
|
|
721
|
+
});
|
|
722
|
+
}
|
|
723
|
+
|
|
31
724
|
// src/lib/not-implemented-stub.ts
|
|
32
725
|
function notImplementedYet(commandName, milestone) {
|
|
33
726
|
return () => {
|
|
@@ -50,59 +743,25 @@ function registerCommitCommand(program2) {
|
|
|
50
743
|
}
|
|
51
744
|
|
|
52
745
|
// src/commands/doctor.ts
|
|
53
|
-
import { spawnSync } from "child_process";
|
|
54
|
-
import { promises as
|
|
55
|
-
import { join as
|
|
746
|
+
import { spawnSync as spawnSync5 } from "child_process";
|
|
747
|
+
import { promises as fs6 } from "fs";
|
|
748
|
+
import { join as join9 } from "path";
|
|
56
749
|
import boxen from "boxen";
|
|
57
750
|
|
|
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
751
|
// src/lib/git-operations.ts
|
|
93
|
-
import { join as
|
|
752
|
+
import { join as join6 } from "path";
|
|
94
753
|
import { simpleGit } from "simple-git";
|
|
95
754
|
function git(cwd = process.cwd()) {
|
|
96
755
|
return simpleGit({ baseDir: cwd, binary: "git" });
|
|
97
756
|
}
|
|
98
757
|
async function isGitRepo(cwd = process.cwd()) {
|
|
99
|
-
return await pathExists(
|
|
758
|
+
return await pathExists(join6(cwd, ".git"));
|
|
100
759
|
}
|
|
101
760
|
async function addSubmodule(repoUrl, destPath, cwd = process.cwd()) {
|
|
102
761
|
await git(cwd).subModule(["add", repoUrl, destPath]);
|
|
103
762
|
}
|
|
104
763
|
async function checkoutTagInSubmodule(submodulePath, tag, cwd = process.cwd()) {
|
|
105
|
-
const submoduleCwd =
|
|
764
|
+
const submoduleCwd = join6(cwd, submodulePath);
|
|
106
765
|
await git(submoduleCwd).fetch(["--tags"]);
|
|
107
766
|
await git(submoduleCwd).checkout(tag);
|
|
108
767
|
}
|
|
@@ -120,12 +779,12 @@ async function currentCommitSha(cwd = process.cwd()) {
|
|
|
120
779
|
}
|
|
121
780
|
|
|
122
781
|
// src/lib/project-tree-scaffolder.ts
|
|
123
|
-
import { promises as
|
|
124
|
-
import { join as
|
|
782
|
+
import { promises as fs5 } from "fs";
|
|
783
|
+
import { join as join8 } from "path";
|
|
125
784
|
|
|
126
785
|
// src/lib/template-bundle-loader.ts
|
|
127
786
|
import { existsSync } from "fs";
|
|
128
|
-
import { dirname as dirname2, join as
|
|
787
|
+
import { dirname as dirname2, join as join7 } from "path";
|
|
129
788
|
import { fileURLToPath } from "url";
|
|
130
789
|
|
|
131
790
|
// src/lib/mustache-template-engine.ts
|
|
@@ -141,12 +800,12 @@ function renderTemplate(source, variables) {
|
|
|
141
800
|
// src/lib/template-bundle-loader.ts
|
|
142
801
|
var HERE = dirname2(fileURLToPath(import.meta.url));
|
|
143
802
|
var PACKAGE_ROOT = findPackageRoot(HERE);
|
|
144
|
-
var TEMPLATES_ROOT =
|
|
145
|
-
var HOOKS_ROOT =
|
|
803
|
+
var TEMPLATES_ROOT = join7(PACKAGE_ROOT, "src", "templates");
|
|
804
|
+
var HOOKS_ROOT = join7(PACKAGE_ROOT, "src", "hooks");
|
|
146
805
|
function findPackageRoot(startDir) {
|
|
147
806
|
let dir = startDir;
|
|
148
807
|
while (true) {
|
|
149
|
-
if (existsSync(
|
|
808
|
+
if (existsSync(join7(dir, "package.json"))) return dir;
|
|
150
809
|
const parent = dirname2(dir);
|
|
151
810
|
if (parent === dir) {
|
|
152
811
|
throw new Error(`Cannot locate package root from ${startDir}`);
|
|
@@ -155,14 +814,14 @@ function findPackageRoot(startDir) {
|
|
|
155
814
|
}
|
|
156
815
|
}
|
|
157
816
|
async function loadTemplate(name) {
|
|
158
|
-
return await readText(
|
|
817
|
+
return await readText(join7(TEMPLATES_ROOT, `${name}.tpl`));
|
|
159
818
|
}
|
|
160
819
|
async function renderTemplateByName(name, variables) {
|
|
161
820
|
const source = await loadTemplate(name);
|
|
162
821
|
return renderTemplate(source, variables);
|
|
163
822
|
}
|
|
164
823
|
async function loadHook(name) {
|
|
165
|
-
return await readText(
|
|
824
|
+
return await readText(join7(HOOKS_ROOT, `${name}.sh.tpl`));
|
|
166
825
|
}
|
|
167
826
|
|
|
168
827
|
// src/lib/project-tree-scaffolder.ts
|
|
@@ -179,7 +838,7 @@ async function backupIfExists(path) {
|
|
|
179
838
|
throw new Error(`Could not find free backup name for ${path}`);
|
|
180
839
|
}
|
|
181
840
|
}
|
|
182
|
-
await
|
|
841
|
+
await fs5.rename(path, backupPath);
|
|
183
842
|
return backupPath;
|
|
184
843
|
}
|
|
185
844
|
async function writeWithBackup(path, content, mode) {
|
|
@@ -196,12 +855,12 @@ var PROJECT_KNOWLEDGE_TEMPLATES = [
|
|
|
196
855
|
"project/gotchas.md"
|
|
197
856
|
];
|
|
198
857
|
async function createClaudeDirTree(projectRoot) {
|
|
199
|
-
const claudeRoot =
|
|
858
|
+
const claudeRoot = join8(projectRoot, ".claude");
|
|
200
859
|
await ensureDir(claudeRoot);
|
|
201
860
|
for (const sub of CLAUDE_SUBDIRS) {
|
|
202
|
-
const dir =
|
|
861
|
+
const dir = join8(claudeRoot, sub);
|
|
203
862
|
await ensureDir(dir);
|
|
204
|
-
await writeTextAtomic(
|
|
863
|
+
await writeTextAtomic(join8(dir, ".gitkeep"), "");
|
|
205
864
|
}
|
|
206
865
|
}
|
|
207
866
|
async function writeProjectKnowledgeFiles(projectRoot, vars) {
|
|
@@ -232,7 +891,7 @@ async function writeProjectKnowledgeFiles(projectRoot, vars) {
|
|
|
232
891
|
for (const tpl of PROJECT_KNOWLEDGE_TEMPLATES) {
|
|
233
892
|
const content = await renderTemplateByName(tpl, baseVars);
|
|
234
893
|
const relative4 = tpl.replace(/^project\//, "");
|
|
235
|
-
const outPath =
|
|
894
|
+
const outPath = join8(projectRoot, ".claude", "project", relative4);
|
|
236
895
|
const backup = await writeWithBackup(outPath, content);
|
|
237
896
|
if (backup) backups.push(backup);
|
|
238
897
|
}
|
|
@@ -240,19 +899,19 @@ async function writeProjectKnowledgeFiles(projectRoot, vars) {
|
|
|
240
899
|
}
|
|
241
900
|
async function writeRootClaudeMd(projectRoot, vars) {
|
|
242
901
|
const content = await renderTemplateByName("CLAUDE.md", vars);
|
|
243
|
-
return await writeWithBackup(
|
|
902
|
+
return await writeWithBackup(join8(projectRoot, "CLAUDE.md"), content);
|
|
244
903
|
}
|
|
245
904
|
async function writeProjectSettings(projectRoot, vars) {
|
|
246
905
|
const content = await renderTemplateByName("settings.json", vars);
|
|
247
|
-
return await writeWithBackup(
|
|
906
|
+
return await writeWithBackup(join8(projectRoot, ".claude", "settings.json"), content);
|
|
248
907
|
}
|
|
249
908
|
async function appendGitignoreEntries(projectRoot) {
|
|
250
|
-
const path =
|
|
909
|
+
const path = join8(projectRoot, ".gitignore");
|
|
251
910
|
const tpl = await renderTemplateByName("gitignore", {});
|
|
252
911
|
const marker = "# Avatar \u2014 git-ignored entries injected on `avatar init`";
|
|
253
912
|
let existing = "";
|
|
254
913
|
if (await pathExists(path)) {
|
|
255
|
-
existing = await
|
|
914
|
+
existing = await fs5.readFile(path, "utf8");
|
|
256
915
|
if (existing.includes(marker)) return;
|
|
257
916
|
}
|
|
258
917
|
const separator = existing.endsWith("\n") || existing.length === 0 ? "" : "\n";
|
|
@@ -261,78 +920,12 @@ ${tpl}`);
|
|
|
261
920
|
}
|
|
262
921
|
async function installGitHook(gitDir, hookName) {
|
|
263
922
|
const content = await loadHook(hookName);
|
|
264
|
-
const hooksDir =
|
|
923
|
+
const hooksDir = join8(gitDir, "hooks");
|
|
265
924
|
await ensureDir(hooksDir);
|
|
266
|
-
const dest =
|
|
925
|
+
const dest = join8(hooksDir, hookName);
|
|
267
926
|
await writeTextAtomic(dest, content, 493);
|
|
268
927
|
}
|
|
269
928
|
|
|
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
929
|
// src/commands/doctor.ts
|
|
337
930
|
function registerDoctorCommand(program2) {
|
|
338
931
|
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 +980,7 @@ async function runChecks(cwd) {
|
|
|
387
980
|
detail: gitRepo ? cwd : "Kh\xF4ng ph\u1EA3i git repo (c\u1EA7n cho 'avatar init')",
|
|
388
981
|
fixable: false
|
|
389
982
|
});
|
|
390
|
-
const packPath =
|
|
983
|
+
const packPath = join9(cwd, ".claude", "pack");
|
|
391
984
|
const hasPack = await pathExists(packPath);
|
|
392
985
|
checks.push({
|
|
393
986
|
name: "team-ai-pack submodule",
|
|
@@ -395,7 +988,7 @@ async function runChecks(cwd) {
|
|
|
395
988
|
detail: hasPack ? packPath : "Avatar ch\u01B0a init \u2014 ch\u1EA1y 'avatar init'",
|
|
396
989
|
fixable: false
|
|
397
990
|
});
|
|
398
|
-
const claudeMdPath =
|
|
991
|
+
const claudeMdPath = join9(cwd, "CLAUDE.md");
|
|
399
992
|
const hasClaudeMd = await pathExists(claudeMdPath);
|
|
400
993
|
checks.push({
|
|
401
994
|
name: "CLAUDE.md",
|
|
@@ -403,7 +996,7 @@ async function runChecks(cwd) {
|
|
|
403
996
|
detail: hasClaudeMd ? "t\u1ED3n t\u1EA1i \u1EDF project root" : "thi\u1EBFu \u2014 ch\u1EA1y 'avatar init'",
|
|
404
997
|
fixable: false
|
|
405
998
|
});
|
|
406
|
-
const hookPath =
|
|
999
|
+
const hookPath = join9(cwd, ".git", "hooks", "post-merge");
|
|
407
1000
|
const hasHook = await pathExists(hookPath);
|
|
408
1001
|
if (gitRepo && hasPack) {
|
|
409
1002
|
checks.push({
|
|
@@ -412,15 +1005,15 @@ async function runChecks(cwd) {
|
|
|
412
1005
|
detail: hasHook ? "installed" : "missing \u2014 fixable",
|
|
413
1006
|
fixable: !hasHook,
|
|
414
1007
|
fix: hasHook ? void 0 : async () => {
|
|
415
|
-
await installGitHook(
|
|
1008
|
+
await installGitHook(join9(cwd, ".git"), "post-merge");
|
|
416
1009
|
}
|
|
417
1010
|
});
|
|
418
1011
|
}
|
|
419
|
-
const gitignorePath =
|
|
1012
|
+
const gitignorePath = join9(cwd, ".gitignore");
|
|
420
1013
|
if (gitRepo) {
|
|
421
1014
|
let gitignoreOk = false;
|
|
422
1015
|
if (await pathExists(gitignorePath)) {
|
|
423
|
-
const content = await
|
|
1016
|
+
const content = await fs6.readFile(gitignorePath, "utf8");
|
|
424
1017
|
gitignoreOk = content.includes(".claude/_pending/");
|
|
425
1018
|
}
|
|
426
1019
|
checks.push({
|
|
@@ -430,7 +1023,7 @@ async function runChecks(cwd) {
|
|
|
430
1023
|
fixable: false
|
|
431
1024
|
});
|
|
432
1025
|
}
|
|
433
|
-
const which =
|
|
1026
|
+
const which = spawnSync5("which", ["claude"]);
|
|
434
1027
|
const hasClaudeCli = which.status === 0;
|
|
435
1028
|
checks.push({
|
|
436
1029
|
name: "Claude Code CLI",
|
|
@@ -478,24 +1071,10 @@ async function applyFixes(checks) {
|
|
|
478
1071
|
}
|
|
479
1072
|
|
|
480
1073
|
// src/commands/init.ts
|
|
481
|
-
import { basename, join as
|
|
482
|
-
import { confirm, input, select } from "@inquirer/prompts";
|
|
1074
|
+
import { basename, join as join16, relative as relative2, resolve } from "path";
|
|
1075
|
+
import { confirm as confirm2, input as input2, select as select4 } from "@inquirer/prompts";
|
|
483
1076
|
import boxen2 from "boxen";
|
|
484
1077
|
|
|
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
1078
|
// src/lib/avatar-ascii-banner.ts
|
|
500
1079
|
import chalk2 from "chalk";
|
|
501
1080
|
var BANNER_LINES = [
|
|
@@ -558,27 +1137,27 @@ ${renderAvatarBanner(opts)}
|
|
|
558
1137
|
}
|
|
559
1138
|
|
|
560
1139
|
// src/lib/execute-gh-repo-create.ts
|
|
561
|
-
import { spawnSync as
|
|
1140
|
+
import { spawnSync as spawnSync6 } from "child_process";
|
|
562
1141
|
var RepoAlreadyExistsError = class extends Error {
|
|
563
1142
|
constructor(fullName) {
|
|
564
1143
|
super(`Repo "${fullName}" \u0111\xE3 t\u1ED3n t\u1EA1i tr\xEAn GitHub. \u0110\u1ED5i t\xEAn ho\u1EB7c x\xF3a repo c\u0169.`);
|
|
565
1144
|
this.name = "RepoAlreadyExistsError";
|
|
566
1145
|
}
|
|
567
1146
|
};
|
|
568
|
-
function executeGhRepoCreate(
|
|
569
|
-
const fullName = `${
|
|
1147
|
+
function executeGhRepoCreate(input3) {
|
|
1148
|
+
const fullName = `${input3.org}/${input3.name}`;
|
|
570
1149
|
const args = [
|
|
571
1150
|
"repo",
|
|
572
1151
|
"create",
|
|
573
1152
|
fullName,
|
|
574
|
-
`--${
|
|
1153
|
+
`--${input3.visibility}`,
|
|
575
1154
|
"--source",
|
|
576
|
-
|
|
1155
|
+
input3.folder,
|
|
577
1156
|
"--remote",
|
|
578
1157
|
"origin",
|
|
579
1158
|
"--push"
|
|
580
1159
|
];
|
|
581
|
-
const r =
|
|
1160
|
+
const r = spawnSync6("gh", args, { stdio: "inherit" });
|
|
582
1161
|
if (r.status !== 0) {
|
|
583
1162
|
if (r.status === 1) {
|
|
584
1163
|
throw new RepoAlreadyExistsError(fullName);
|
|
@@ -592,9 +1171,9 @@ function executeGhRepoCreate(input2) {
|
|
|
592
1171
|
}
|
|
593
1172
|
|
|
594
1173
|
// src/lib/resolve-github-username-default.ts
|
|
595
|
-
import { spawnSync as
|
|
1174
|
+
import { spawnSync as spawnSync7 } from "child_process";
|
|
596
1175
|
function resolveGithubUsernameDefault() {
|
|
597
|
-
const r =
|
|
1176
|
+
const r = spawnSync7("gh", ["api", "user", "--jq", ".login"], {
|
|
598
1177
|
encoding: "utf8",
|
|
599
1178
|
stdio: ["ignore", "pipe", "pipe"]
|
|
600
1179
|
});
|
|
@@ -626,28 +1205,28 @@ function validateRepoVisibility(v) {
|
|
|
626
1205
|
}
|
|
627
1206
|
|
|
628
1207
|
// 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}/${
|
|
1208
|
+
function createGithubRemoteFromFolder(input3) {
|
|
1209
|
+
validateRepoName(input3.name);
|
|
1210
|
+
validateRepoVisibility(input3.visibility);
|
|
1211
|
+
const org = input3.org ?? resolveGithubUsernameDefault();
|
|
1212
|
+
log.info(`T\u1EA1o GitHub repo ${org}/${input3.name} (${input3.visibility})...`);
|
|
634
1213
|
const urls = executeGhRepoCreate({
|
|
635
|
-
folder:
|
|
1214
|
+
folder: input3.folder,
|
|
636
1215
|
org,
|
|
637
|
-
name:
|
|
638
|
-
visibility:
|
|
1216
|
+
name: input3.name,
|
|
1217
|
+
visibility: input3.visibility
|
|
639
1218
|
});
|
|
640
1219
|
log.success(`\u0110\xE3 t\u1EA1o: ${urls.sshUrl}`);
|
|
641
1220
|
return urls;
|
|
642
1221
|
}
|
|
643
1222
|
|
|
644
1223
|
// src/lib/create-workspace-remote-via-gh.ts
|
|
645
|
-
import { spawnSync as
|
|
1224
|
+
import { spawnSync as spawnSync14 } from "child_process";
|
|
646
1225
|
|
|
647
1226
|
// src/lib/check-gh-cli-auth-status.ts
|
|
648
|
-
import { spawnSync as
|
|
1227
|
+
import { spawnSync as spawnSync8 } from "child_process";
|
|
649
1228
|
function checkGhCliAuthStatus() {
|
|
650
|
-
const r =
|
|
1229
|
+
const r = spawnSync8("gh", ["auth", "status"], { stdio: "ignore" });
|
|
651
1230
|
if (r.error && r.error.code === "ENOENT") {
|
|
652
1231
|
return "not-installed";
|
|
653
1232
|
}
|
|
@@ -655,22 +1234,12 @@ function checkGhCliAuthStatus() {
|
|
|
655
1234
|
}
|
|
656
1235
|
|
|
657
1236
|
// 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
|
|
1237
|
+
import { spawnSync as spawnSync9 } from "child_process";
|
|
669
1238
|
function hasBinary(name) {
|
|
670
1239
|
const platform2 = detectHostPlatform();
|
|
671
1240
|
const probe = platform2 === "win32" ? "where" : "command";
|
|
672
1241
|
const args = platform2 === "win32" ? [name] : ["-v", name];
|
|
673
|
-
const r =
|
|
1242
|
+
const r = spawnSync9(probe, args, {
|
|
674
1243
|
shell: platform2 !== "win32",
|
|
675
1244
|
stdio: "ignore"
|
|
676
1245
|
});
|
|
@@ -686,7 +1255,7 @@ function detectPackageManager() {
|
|
|
686
1255
|
}
|
|
687
1256
|
|
|
688
1257
|
// src/lib/install-gh-cli-via-package-manager.ts
|
|
689
|
-
import { spawnSync as
|
|
1258
|
+
import { spawnSync as spawnSync10 } from "child_process";
|
|
690
1259
|
var INSTALL_COMMANDS = {
|
|
691
1260
|
brew: { cmd: "brew", args: ["install", "gh"] },
|
|
692
1261
|
apt: { cmd: "sudo", args: ["apt-get", "install", "-y", "gh"] },
|
|
@@ -697,7 +1266,7 @@ var INSTALL_COMMANDS = {
|
|
|
697
1266
|
function installGhCliViaPackageManager(pm) {
|
|
698
1267
|
const spec = INSTALL_COMMANDS[pm];
|
|
699
1268
|
log.info(`\u0110ang c\xE0i gh CLI qua ${pm}...`);
|
|
700
|
-
const r =
|
|
1269
|
+
const r = spawnSync10(spec.cmd, spec.args, { stdio: "inherit" });
|
|
701
1270
|
if (r.status !== 0) {
|
|
702
1271
|
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
1272
|
}
|
|
@@ -705,9 +1274,9 @@ function installGhCliViaPackageManager(pm) {
|
|
|
705
1274
|
}
|
|
706
1275
|
|
|
707
1276
|
// src/lib/setup-git-credential-via-gh.ts
|
|
708
|
-
import { spawnSync as
|
|
1277
|
+
import { spawnSync as spawnSync11 } from "child_process";
|
|
709
1278
|
function setupGitCredentialViaGh() {
|
|
710
|
-
const r =
|
|
1279
|
+
const r = spawnSync11("gh", ["auth", "setup-git"], { stdio: "ignore" });
|
|
711
1280
|
if (r.status !== 0) {
|
|
712
1281
|
log.warn("gh auth setup-git fail (non-fatal). N\u1EBFu git clone l\u1ED7i 128 \u2192 ch\u1EA1y th\u1EE7 c\xF4ng.");
|
|
713
1282
|
return;
|
|
@@ -716,10 +1285,10 @@ function setupGitCredentialViaGh() {
|
|
|
716
1285
|
}
|
|
717
1286
|
|
|
718
1287
|
// src/lib/trigger-gh-cli-auth-login.ts
|
|
719
|
-
import { spawnSync as
|
|
1288
|
+
import { spawnSync as spawnSync12 } from "child_process";
|
|
720
1289
|
function triggerGhCliAuthLogin() {
|
|
721
1290
|
log.info("Kh\u1EDFi \u0111\u1ED9ng \u0111\u0103ng nh\u1EADp GitHub qua gh CLI (browser s\u1EBD m\u1EDF)...");
|
|
722
|
-
const r =
|
|
1291
|
+
const r = spawnSync12(
|
|
723
1292
|
"gh",
|
|
724
1293
|
["auth", "login", "--hostname", "github.com", "--web", "--git-protocol", "ssh"],
|
|
725
1294
|
{ stdio: "inherit" }
|
|
@@ -731,7 +1300,7 @@ function triggerGhCliAuthLogin() {
|
|
|
731
1300
|
}
|
|
732
1301
|
|
|
733
1302
|
// src/lib/verify-git-remote-accessible.ts
|
|
734
|
-
import { spawnSync as
|
|
1303
|
+
import { spawnSync as spawnSync13 } from "child_process";
|
|
735
1304
|
var TIMEOUT_MS = 5e3;
|
|
736
1305
|
var RemoteNotAccessibleError = class extends Error {
|
|
737
1306
|
constructor(url, reason) {
|
|
@@ -740,7 +1309,7 @@ var RemoteNotAccessibleError = class extends Error {
|
|
|
740
1309
|
}
|
|
741
1310
|
};
|
|
742
1311
|
function verifyGitRemoteAccessible(url) {
|
|
743
|
-
const r =
|
|
1312
|
+
const r = spawnSync13("git", ["ls-remote", "--exit-code", url, "HEAD"], {
|
|
744
1313
|
stdio: "ignore",
|
|
745
1314
|
timeout: TIMEOUT_MS
|
|
746
1315
|
});
|
|
@@ -780,22 +1349,22 @@ async function ensureGitHubReady(remoteUrl) {
|
|
|
780
1349
|
}
|
|
781
1350
|
|
|
782
1351
|
// src/lib/create-workspace-remote-via-gh.ts
|
|
783
|
-
async function createWorkspaceRemoteViaGh(
|
|
784
|
-
validateRepoName(
|
|
785
|
-
validateRepoVisibility(
|
|
1352
|
+
async function createWorkspaceRemoteViaGh(input3) {
|
|
1353
|
+
validateRepoName(input3.workspaceName);
|
|
1354
|
+
validateRepoVisibility(input3.visibility);
|
|
786
1355
|
await ensureGitHubReady();
|
|
787
|
-
const org =
|
|
788
|
-
const fullName = `${org}/${
|
|
789
|
-
log.info(`T\u1EA1o GitHub repo cho workspace: ${fullName} (${
|
|
790
|
-
const r =
|
|
1356
|
+
const org = input3.org ?? resolveGithubUsernameDefault();
|
|
1357
|
+
const fullName = `${org}/${input3.workspaceName}`;
|
|
1358
|
+
log.info(`T\u1EA1o GitHub repo cho workspace: ${fullName} (${input3.visibility})...`);
|
|
1359
|
+
const r = spawnSync14(
|
|
791
1360
|
"gh",
|
|
792
1361
|
[
|
|
793
1362
|
"repo",
|
|
794
1363
|
"create",
|
|
795
1364
|
fullName,
|
|
796
|
-
`--${
|
|
1365
|
+
`--${input3.visibility}`,
|
|
797
1366
|
"--source",
|
|
798
|
-
|
|
1367
|
+
input3.workspacePath,
|
|
799
1368
|
"--remote",
|
|
800
1369
|
"origin",
|
|
801
1370
|
"--push"
|
|
@@ -804,7 +1373,7 @@ async function createWorkspaceRemoteViaGh(input2) {
|
|
|
804
1373
|
);
|
|
805
1374
|
if (r.status !== 0) {
|
|
806
1375
|
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} --${
|
|
1376
|
+
`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
1377
|
);
|
|
809
1378
|
}
|
|
810
1379
|
const sshUrl = `git@github.com:${fullName}.git`;
|
|
@@ -813,11 +1382,16 @@ async function createWorkspaceRemoteViaGh(input2) {
|
|
|
813
1382
|
return { sshUrl, httpsUrl };
|
|
814
1383
|
}
|
|
815
1384
|
|
|
1385
|
+
// src/lib/safe-bootstrap-for-dirty-folder.ts
|
|
1386
|
+
import { readdirSync } from "fs";
|
|
1387
|
+
import { select as select3 } from "@inquirer/prompts";
|
|
1388
|
+
import { simpleGit as simpleGit3 } from "simple-git";
|
|
1389
|
+
|
|
816
1390
|
// src/lib/check-folder-has-git.ts
|
|
817
1391
|
import { existsSync as existsSync2, statSync } from "fs";
|
|
818
|
-
import { join as
|
|
1392
|
+
import { join as join10 } from "path";
|
|
819
1393
|
function checkFolderHasGit(folderPath) {
|
|
820
|
-
const gitPath =
|
|
1394
|
+
const gitPath = join10(folderPath, ".git");
|
|
821
1395
|
if (!existsSync2(gitPath)) return false;
|
|
822
1396
|
const stat = statSync(gitPath);
|
|
823
1397
|
return stat.isDirectory() || stat.isFile();
|
|
@@ -849,7 +1423,7 @@ async function createInitialGitCommit(folderPath) {
|
|
|
849
1423
|
|
|
850
1424
|
// src/lib/detect-folder-tech-stack.ts
|
|
851
1425
|
import { existsSync as existsSync3 } from "fs";
|
|
852
|
-
import { join as
|
|
1426
|
+
import { join as join11 } from "path";
|
|
853
1427
|
var SIGNATURES = {
|
|
854
1428
|
node: ["package.json"],
|
|
855
1429
|
python: ["pyproject.toml", "requirements.txt", "setup.py", "Pipfile"],
|
|
@@ -861,7 +1435,7 @@ var SIGNATURES = {
|
|
|
861
1435
|
function detectFolderTechStack(folderPath) {
|
|
862
1436
|
const matched = [];
|
|
863
1437
|
for (const [stack, files] of Object.entries(SIGNATURES)) {
|
|
864
|
-
if (files.some((f) => existsSync3(
|
|
1438
|
+
if (files.some((f) => existsSync3(join11(folderPath, f)))) {
|
|
865
1439
|
matched.push(stack);
|
|
866
1440
|
}
|
|
867
1441
|
}
|
|
@@ -869,20 +1443,20 @@ function detectFolderTechStack(folderPath) {
|
|
|
869
1443
|
}
|
|
870
1444
|
|
|
871
1445
|
// src/lib/gitignore-template-loader.ts
|
|
872
|
-
import { readFileSync } from "fs";
|
|
873
|
-
import { dirname as dirname3, join as
|
|
1446
|
+
import { readFileSync as readFileSync2 } from "fs";
|
|
1447
|
+
import { dirname as dirname3, join as join12 } from "path";
|
|
874
1448
|
import { fileURLToPath as fileURLToPath2 } from "url";
|
|
875
1449
|
var __dirname = dirname3(fileURLToPath2(import.meta.url));
|
|
876
1450
|
var CANDIDATE_DIRS = [
|
|
877
|
-
|
|
878
|
-
|
|
1451
|
+
join12(__dirname, "..", "templates", "gitignore"),
|
|
1452
|
+
join12(__dirname, "..", "..", "src", "templates", "gitignore")
|
|
879
1453
|
];
|
|
880
1454
|
var AVATAR_MARKER_START = "# === avatar ===";
|
|
881
1455
|
var AVATAR_MARKER_END = "# === /avatar ===";
|
|
882
1456
|
function readTemplate(stack) {
|
|
883
1457
|
for (const dir of CANDIDATE_DIRS) {
|
|
884
1458
|
try {
|
|
885
|
-
return
|
|
1459
|
+
return readFileSync2(join12(dir, `${stack}.txt`), "utf8");
|
|
886
1460
|
} catch {
|
|
887
1461
|
}
|
|
888
1462
|
}
|
|
@@ -896,15 +1470,15 @@ ${readTemplate(s).trim()}`);
|
|
|
896
1470
|
}
|
|
897
1471
|
|
|
898
1472
|
// src/lib/write-or-merge-gitignore.ts
|
|
899
|
-
import { existsSync as existsSync4, readFileSync as
|
|
900
|
-
import { join as
|
|
1473
|
+
import { existsSync as existsSync4, readFileSync as readFileSync3, writeFileSync } from "fs";
|
|
1474
|
+
import { join as join13 } from "path";
|
|
901
1475
|
function writeOrMergeGitignore(folderPath, avatarBlock) {
|
|
902
|
-
const path =
|
|
1476
|
+
const path = join13(folderPath, ".gitignore");
|
|
903
1477
|
if (!existsSync4(path)) {
|
|
904
1478
|
writeFileSync(path, avatarBlock, "utf8");
|
|
905
1479
|
return;
|
|
906
1480
|
}
|
|
907
|
-
const existing =
|
|
1481
|
+
const existing = readFileSync3(path, "utf8");
|
|
908
1482
|
const startIdx = existing.indexOf(AVATAR_MARKER_START);
|
|
909
1483
|
const endIdx = existing.indexOf(AVATAR_MARKER_END);
|
|
910
1484
|
if (startIdx !== -1 && endIdx !== -1 && endIdx > startIdx) {
|
|
@@ -920,24 +1494,162 @@ ${avatarBlock}${after.trimStart()}`, "utf8");
|
|
|
920
1494
|
${avatarBlock}`, "utf8");
|
|
921
1495
|
}
|
|
922
1496
|
|
|
923
|
-
// src/lib/
|
|
924
|
-
|
|
925
|
-
|
|
1497
|
+
// src/lib/safe-bootstrap-for-dirty-folder.ts
|
|
1498
|
+
var InitAbortedByUserError = class extends Error {
|
|
1499
|
+
constructor(message) {
|
|
1500
|
+
super(message);
|
|
1501
|
+
this.name = "InitAbortedByUserError";
|
|
1502
|
+
}
|
|
1503
|
+
};
|
|
1504
|
+
async function detectFolderGitState(folderPath) {
|
|
1505
|
+
const hasGit = checkFolderHasGit(folderPath);
|
|
1506
|
+
if (!hasGit) {
|
|
1507
|
+
const entries = readdirSync(folderPath).filter((e) => e !== ".git");
|
|
1508
|
+
return entries.length === 0 ? "empty" : "untracked-only";
|
|
1509
|
+
}
|
|
1510
|
+
const g = simpleGit3({ baseDir: folderPath });
|
|
1511
|
+
const status = await g.status();
|
|
1512
|
+
return status.isClean() ? "clean" : "dirty";
|
|
1513
|
+
}
|
|
1514
|
+
async function promptBootstrapStrategy(state, opts) {
|
|
1515
|
+
if (opts.presetStrategy) return opts.presetStrategy;
|
|
1516
|
+
if (opts.autoYes) return "stash";
|
|
1517
|
+
if (state === "empty" || state === "clean") return "commit-all";
|
|
1518
|
+
return await select3({
|
|
1519
|
+
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:",
|
|
1520
|
+
choices: [
|
|
1521
|
+
{
|
|
1522
|
+
value: "stash",
|
|
1523
|
+
name: "1. Stash changes \u2192 bootstrap \u2192 restore (KHUY\u1EBEN NGH\u1ECA)"
|
|
1524
|
+
},
|
|
1525
|
+
{
|
|
1526
|
+
value: "commit-all",
|
|
1527
|
+
name: "2. Commit to\xE0n b\u1ED9 v\xE0o initial commit (legacy v1.1.6)"
|
|
1528
|
+
},
|
|
1529
|
+
{
|
|
1530
|
+
value: "skip",
|
|
1531
|
+
name: "3. Skip \u2014 t\xF4i commit th\u1EE7 c\xF4ng r\u1ED3i ch\u1EA1y l\u1EA1i"
|
|
1532
|
+
},
|
|
1533
|
+
{
|
|
1534
|
+
value: "branch",
|
|
1535
|
+
name: "4. Commit v\xE0o branch ri\xEAng `avatar/init` (main gi\u1EEF s\u1EA1ch)"
|
|
1536
|
+
}
|
|
1537
|
+
],
|
|
1538
|
+
default: "stash"
|
|
1539
|
+
});
|
|
1540
|
+
}
|
|
1541
|
+
async function stashUserChanges(g, stashName) {
|
|
1542
|
+
const status = await g.status();
|
|
1543
|
+
if (status.isClean() && status.not_added.length === 0) return false;
|
|
1544
|
+
await g.stash(["push", "--include-untracked", "-m", stashName]);
|
|
1545
|
+
log.info(`Stashed changes: ${stashName}`);
|
|
1546
|
+
return true;
|
|
1547
|
+
}
|
|
1548
|
+
async function restoreStash(g, stashName) {
|
|
1549
|
+
try {
|
|
1550
|
+
await g.stash(["pop"]);
|
|
1551
|
+
log.success(`Restored stash: ${stashName}`);
|
|
1552
|
+
} catch (err) {
|
|
1553
|
+
log.warn(
|
|
1554
|
+
"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}."
|
|
1555
|
+
);
|
|
1556
|
+
log.warn("Resolve: git stash show -p stash@{0} \u2192 fix conflict \u2192 git stash drop");
|
|
1557
|
+
log.dim(`Detail: ${err.message}`);
|
|
1558
|
+
}
|
|
1559
|
+
}
|
|
1560
|
+
async function getCurrentBranch(g) {
|
|
1561
|
+
try {
|
|
1562
|
+
const result = await g.revparse(["--abbrev-ref", "HEAD"]);
|
|
1563
|
+
const branch = result.trim();
|
|
1564
|
+
return branch === "HEAD" ? "main" : branch;
|
|
1565
|
+
} catch {
|
|
1566
|
+
return "main";
|
|
1567
|
+
}
|
|
1568
|
+
}
|
|
1569
|
+
async function writeAvatarGitignore(folderPath) {
|
|
926
1570
|
const stacks = detectFolderTechStack(folderPath);
|
|
927
|
-
log.info(`Tech stack
|
|
1571
|
+
log.info(`Tech stack: ${stacks.join(", ")}`);
|
|
928
1572
|
writeOrMergeGitignore(folderPath, composeGitignoreContent(stacks));
|
|
929
1573
|
log.success(".gitignore \u0111\xE3 ghi (Avatar block)");
|
|
930
|
-
|
|
931
|
-
|
|
932
|
-
|
|
933
|
-
|
|
934
|
-
|
|
935
|
-
|
|
1574
|
+
}
|
|
1575
|
+
async function executeBootstrapWithStrategy(folderPath, strategy) {
|
|
1576
|
+
const g = simpleGit3({ baseDir: folderPath });
|
|
1577
|
+
switch (strategy) {
|
|
1578
|
+
case "skip":
|
|
1579
|
+
throw new InitAbortedByUserError(
|
|
1580
|
+
"Init aborted. Commit th\u1EE7 c\xF4ng changes hi\u1EC7n t\u1EA1i r\u1ED3i ch\u1EA1y l\u1EA1i `avatar init`."
|
|
1581
|
+
);
|
|
1582
|
+
case "stash": {
|
|
1583
|
+
const stashName = `avatar-init-backup-${Date.now()}`;
|
|
1584
|
+
const hadGit = checkFolderHasGit(folderPath);
|
|
1585
|
+
if (!hadGit) {
|
|
1586
|
+
await g.init();
|
|
1587
|
+
await g.branch(["-M", "main"]).catch(() => void 0);
|
|
1588
|
+
}
|
|
1589
|
+
const hasCommit = (await g.raw(["rev-list", "-n", "1", "--all"]).catch(() => "")).trim();
|
|
1590
|
+
if (!hasCommit) {
|
|
1591
|
+
await g.commit("chore: avatar baseline (pre-stash)", void 0, { "--allow-empty": null });
|
|
1592
|
+
}
|
|
1593
|
+
const stashed = await stashUserChanges(g, stashName);
|
|
1594
|
+
try {
|
|
1595
|
+
await writeAvatarGitignore(folderPath);
|
|
1596
|
+
await createInitialGitCommit(folderPath);
|
|
1597
|
+
} finally {
|
|
1598
|
+
if (stashed) await restoreStash(g, stashName);
|
|
1599
|
+
}
|
|
1600
|
+
break;
|
|
1601
|
+
}
|
|
1602
|
+
case "commit-all": {
|
|
1603
|
+
await writeAvatarGitignore(folderPath);
|
|
1604
|
+
await createInitialGitCommit(folderPath);
|
|
1605
|
+
break;
|
|
1606
|
+
}
|
|
1607
|
+
case "branch": {
|
|
1608
|
+
const hadGit = checkFolderHasGit(folderPath);
|
|
1609
|
+
if (!hadGit) {
|
|
1610
|
+
await g.init();
|
|
1611
|
+
await g.branch(["-M", "main"]);
|
|
1612
|
+
}
|
|
1613
|
+
const originalBranch = await getCurrentBranch(g);
|
|
1614
|
+
try {
|
|
1615
|
+
await g.checkoutLocalBranch("avatar/init");
|
|
1616
|
+
} catch {
|
|
1617
|
+
await g.checkout("avatar/init");
|
|
1618
|
+
}
|
|
1619
|
+
await writeAvatarGitignore(folderPath);
|
|
1620
|
+
await createInitialGitCommit(folderPath);
|
|
1621
|
+
try {
|
|
1622
|
+
await g.checkout(originalBranch);
|
|
1623
|
+
log.info(
|
|
1624
|
+
`Avatar init committed \u1EDF branch 'avatar/init'. Switch back v\u1EC1 '${originalBranch}'. Merge khi s\u1EB5n s\xE0ng: git merge avatar/init`
|
|
1625
|
+
);
|
|
1626
|
+
} catch {
|
|
1627
|
+
log.warn(
|
|
1628
|
+
`Kh\xF4ng switch v\u1EC1 '${originalBranch}' \u0111\u01B0\u1EE3c \u2014 \u1EDF l\u1EA1i branch 'avatar/init'. Switch tay sau.`
|
|
1629
|
+
);
|
|
1630
|
+
}
|
|
1631
|
+
break;
|
|
1632
|
+
}
|
|
936
1633
|
}
|
|
937
1634
|
}
|
|
1635
|
+
async function safeBootstrapGitInFolder(folderPath, opts = {}) {
|
|
1636
|
+
const state = await detectFolderGitState(folderPath);
|
|
1637
|
+
log.info(`Folder state: ${state}`);
|
|
1638
|
+
if (state === "empty" || state === "clean") {
|
|
1639
|
+
await writeAvatarGitignore(folderPath);
|
|
1640
|
+
if (state === "empty") {
|
|
1641
|
+
await createInitialGitCommit(folderPath);
|
|
1642
|
+
}
|
|
1643
|
+
await appendAuditEntry("bootstrap", `state=${state},strategy=auto`);
|
|
1644
|
+
return;
|
|
1645
|
+
}
|
|
1646
|
+
const strategy = await promptBootstrapStrategy(state, opts);
|
|
1647
|
+
await executeBootstrapWithStrategy(folderPath, strategy);
|
|
1648
|
+
await appendAuditEntry("bootstrap", `state=${state},strategy=${strategy}`);
|
|
1649
|
+
}
|
|
938
1650
|
|
|
939
1651
|
// src/lib/team-pack-submodule-manager.ts
|
|
940
|
-
import { join as
|
|
1652
|
+
import { join as join14 } from "path";
|
|
941
1653
|
|
|
942
1654
|
// src/lib/resolve-team-pack-repo-url.ts
|
|
943
1655
|
var LEGACY_FALLBACK = "https://github.com/LukeNALS/team-ai-pack.git";
|
|
@@ -975,7 +1687,7 @@ async function addTeamPackSubmodule(projectRoot, tag) {
|
|
|
975
1687
|
}
|
|
976
1688
|
let target = tag ?? null;
|
|
977
1689
|
if (!target) {
|
|
978
|
-
target = await latestTag(
|
|
1690
|
+
target = await latestTag(join14(projectRoot, TEAM_PACK_RELATIVE_PATH));
|
|
979
1691
|
}
|
|
980
1692
|
if (target) {
|
|
981
1693
|
await checkoutTagInSubmodule(TEAM_PACK_RELATIVE_PATH, target, projectRoot);
|
|
@@ -983,7 +1695,7 @@ async function addTeamPackSubmodule(projectRoot, tag) {
|
|
|
983
1695
|
return { pinnedTag: target };
|
|
984
1696
|
}
|
|
985
1697
|
async function readPinnedPackVersion(projectRoot) {
|
|
986
|
-
const submoduleRoot =
|
|
1698
|
+
const submoduleRoot = join14(projectRoot, TEAM_PACK_RELATIVE_PATH);
|
|
987
1699
|
const tag = await latestTag(submoduleRoot);
|
|
988
1700
|
if (tag) return tag;
|
|
989
1701
|
const sha = await currentCommitSha(submoduleRoot);
|
|
@@ -992,7 +1704,7 @@ async function readPinnedPackVersion(projectRoot) {
|
|
|
992
1704
|
|
|
993
1705
|
// src/commands/init-conflict-detection-helpers.ts
|
|
994
1706
|
import { readdir } from "fs/promises";
|
|
995
|
-
import { join as
|
|
1707
|
+
import { join as join15 } from "path";
|
|
996
1708
|
async function isEmptyOrMissing(path) {
|
|
997
1709
|
if (!await pathExists(path)) return true;
|
|
998
1710
|
try {
|
|
@@ -1005,7 +1717,7 @@ async function isEmptyOrMissing(path) {
|
|
|
1005
1717
|
}
|
|
1006
1718
|
async function findAlternativeWorkspaceName(parent, desiredName, maxAttempts = 10) {
|
|
1007
1719
|
for (let i = 2; i < maxAttempts; i++) {
|
|
1008
|
-
const candidate =
|
|
1720
|
+
const candidate = join15(parent, `${desiredName}-${i}`);
|
|
1009
1721
|
if (await isEmptyOrMissing(candidate)) return candidate;
|
|
1010
1722
|
}
|
|
1011
1723
|
return null;
|
|
@@ -1031,11 +1743,29 @@ function buildScaffoldVariables(args) {
|
|
|
1031
1743
|
}
|
|
1032
1744
|
|
|
1033
1745
|
// src/commands/init.ts
|
|
1746
|
+
function parseBootstrapStrategyOpts(opts) {
|
|
1747
|
+
if (opts.preserveUncommitted) return "stash";
|
|
1748
|
+
if (!opts.bootstrapStrategy) return void 0;
|
|
1749
|
+
const valid = ["stash", "commit-all", "skip", "branch"];
|
|
1750
|
+
if (valid.includes(opts.bootstrapStrategy)) {
|
|
1751
|
+
return opts.bootstrapStrategy;
|
|
1752
|
+
}
|
|
1753
|
+
throw new Error(
|
|
1754
|
+
`--bootstrap-strategy kh\xF4ng h\u1EE3p l\u1EC7: ${opts.bootstrapStrategy}. Ch\u1ECDn: ${valid.join(" | ")}`
|
|
1755
|
+
);
|
|
1756
|
+
}
|
|
1034
1757
|
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("--
|
|
1758
|
+
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(
|
|
1759
|
+
"--bootstrap-strategy <s>",
|
|
1760
|
+
"X\u1EED l\xFD folder dirty: stash | commit-all | skip | branch (default: prompt)"
|
|
1761
|
+
).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
1762
|
try {
|
|
1037
1763
|
await runInit(opts);
|
|
1038
1764
|
} catch (err) {
|
|
1765
|
+
if (err instanceof InitAbortedByUserError) {
|
|
1766
|
+
log.dim(err.message);
|
|
1767
|
+
process.exit(0);
|
|
1768
|
+
}
|
|
1039
1769
|
log.error(err instanceof Error ? err.message : String(err));
|
|
1040
1770
|
process.exit(1);
|
|
1041
1771
|
}
|
|
@@ -1065,7 +1795,7 @@ async function runInit(opts) {
|
|
|
1065
1795
|
}
|
|
1066
1796
|
}
|
|
1067
1797
|
async function promptProjectStatus() {
|
|
1068
|
-
return await
|
|
1798
|
+
return await select4({
|
|
1069
1799
|
message: "T\xECnh tr\u1EA1ng d\u1EF1 \xE1n c\u1EE7a b\u1EA1n?",
|
|
1070
1800
|
choices: [
|
|
1071
1801
|
{ name: "1. \u0110\xE3 c\xF3 repo git remote (URL c\xF3 s\u1EB5n)", value: "existing-remote" },
|
|
@@ -1075,14 +1805,14 @@ async function promptProjectStatus() {
|
|
|
1075
1805
|
});
|
|
1076
1806
|
}
|
|
1077
1807
|
async function runInitFromExistingRemote(opts, ownerEmail) {
|
|
1078
|
-
const remoteUrl = opts.clientRepo ?? await
|
|
1808
|
+
const remoteUrl = opts.clientRepo ?? await input2({
|
|
1079
1809
|
message: "URL git c\u1EE7a repo:",
|
|
1080
1810
|
validate: (v) => v.length > 0 ? true : "URL b\u1EAFt bu\u1ED9c"
|
|
1081
1811
|
});
|
|
1082
1812
|
await ensureGitHubReady(remoteUrl);
|
|
1083
1813
|
const teamOwner = opts.teamOwner ?? await promptTeamOwner(ownerEmail);
|
|
1084
1814
|
const inferredName = inferWorkspaceName(remoteUrl);
|
|
1085
|
-
const workspaceName = opts.workspaceName ?? await
|
|
1815
|
+
const workspaceName = opts.workspaceName ?? await input2({ message: "T\xEAn workspace:", default: inferredName });
|
|
1086
1816
|
const workspaceParent = resolve(opts.workspaceParent ?? ".");
|
|
1087
1817
|
const workspacePath = await resolveWorkspacePath(workspaceParent, workspaceName, opts.force);
|
|
1088
1818
|
await scaffoldWorkspaceWithSrcSubmodule({
|
|
@@ -1098,21 +1828,25 @@ async function runInitFromExistingRemote(opts, ownerEmail) {
|
|
|
1098
1828
|
createWorkspaceRemote: opts.workspaceRemote,
|
|
1099
1829
|
repoVisibility: opts.repoVisibility,
|
|
1100
1830
|
repoOrg: opts.repoOrg,
|
|
1101
|
-
flow: "existing-remote"
|
|
1831
|
+
flow: "existing-remote",
|
|
1832
|
+
aiSkip: opts.aiSkip
|
|
1102
1833
|
});
|
|
1103
1834
|
}
|
|
1104
1835
|
async function runInitFromExistingFolder(opts, ownerEmail) {
|
|
1105
1836
|
const folderPath = resolve(
|
|
1106
|
-
opts.folderPath ?? await
|
|
1837
|
+
opts.folderPath ?? await input2({
|
|
1107
1838
|
message: "\u0110\u01B0\u1EDDng d\u1EABn folder hi\u1EC7n c\xF3:",
|
|
1108
1839
|
validate: (v) => v.length > 0 ? true : "Path b\u1EAFt bu\u1ED9c"
|
|
1109
1840
|
})
|
|
1110
1841
|
);
|
|
1111
|
-
await
|
|
1842
|
+
await safeBootstrapGitInFolder(folderPath, {
|
|
1843
|
+
presetStrategy: parseBootstrapStrategyOpts(opts),
|
|
1844
|
+
autoYes: opts.yes
|
|
1845
|
+
});
|
|
1112
1846
|
const remoteUrl = await getOrCreateOriginRemote(folderPath, opts);
|
|
1113
1847
|
const teamOwner = opts.teamOwner ?? await promptTeamOwner(ownerEmail);
|
|
1114
1848
|
const inferredName = opts.workspaceName ?? `${basename(folderPath)}-avatar-workspace`;
|
|
1115
|
-
const workspaceName = opts.workspaceName ?? await
|
|
1849
|
+
const workspaceName = opts.workspaceName ?? await input2({ message: "T\xEAn workspace:", default: inferredName });
|
|
1116
1850
|
const workspaceParent = resolve(opts.workspaceParent ?? ".");
|
|
1117
1851
|
const workspacePath = await resolveWorkspacePath(workspaceParent, workspaceName, opts.force);
|
|
1118
1852
|
await scaffoldWorkspaceWithSrcSubmodule({
|
|
@@ -1129,16 +1863,17 @@ async function runInitFromExistingFolder(opts, ownerEmail) {
|
|
|
1129
1863
|
createWorkspaceRemote: opts.workspaceRemote,
|
|
1130
1864
|
repoVisibility: opts.repoVisibility,
|
|
1131
1865
|
repoOrg: opts.repoOrg,
|
|
1132
|
-
flow: "existing-folder"
|
|
1866
|
+
flow: "existing-folder",
|
|
1867
|
+
aiSkip: opts.aiSkip
|
|
1133
1868
|
});
|
|
1134
1869
|
}
|
|
1135
1870
|
async function runInitFromScratch(opts, ownerEmail) {
|
|
1136
1871
|
await ensureGitHubReady();
|
|
1137
|
-
const projectName = opts.workspaceName ?? await
|
|
1872
|
+
const projectName = opts.workspaceName ?? await input2({
|
|
1138
1873
|
message: "T\xEAn d\u1EF1 \xE1n:",
|
|
1139
1874
|
validate: (v) => v.length > 0 ? true : "T\xEAn b\u1EAFt bu\u1ED9c"
|
|
1140
1875
|
});
|
|
1141
|
-
const visibility = opts.repoVisibility ?? await
|
|
1876
|
+
const visibility = opts.repoVisibility ?? await select4({
|
|
1142
1877
|
message: "Visibility?",
|
|
1143
1878
|
choices: [
|
|
1144
1879
|
{ name: "private (m\u1EB7c \u0111\u1ECBnh)", value: "private" },
|
|
@@ -1148,10 +1883,10 @@ async function runInitFromScratch(opts, ownerEmail) {
|
|
|
1148
1883
|
const teamOwner = opts.teamOwner ?? await promptTeamOwner(ownerEmail);
|
|
1149
1884
|
const workspaceParent = resolve(opts.workspaceParent ?? ".");
|
|
1150
1885
|
const workspacePath = await resolveWorkspacePath(workspaceParent, projectName, opts.force);
|
|
1151
|
-
const srcPath =
|
|
1886
|
+
const srcPath = join16(workspacePath, "src");
|
|
1152
1887
|
await ensureDir(workspacePath);
|
|
1153
1888
|
await ensureDir(srcPath);
|
|
1154
|
-
await
|
|
1889
|
+
await safeBootstrapGitInFolder(srcPath, { autoYes: true });
|
|
1155
1890
|
const urls = createGithubRemoteFromFolder({
|
|
1156
1891
|
folder: srcPath,
|
|
1157
1892
|
name: projectName,
|
|
@@ -1183,7 +1918,8 @@ async function runInitFromScratch(opts, ownerEmail) {
|
|
|
1183
1918
|
createWorkspaceRemote: opts.workspaceRemote,
|
|
1184
1919
|
repoVisibility: opts.repoVisibility,
|
|
1185
1920
|
repoOrg: opts.repoOrg,
|
|
1186
|
-
flow: "new-project"
|
|
1921
|
+
flow: "new-project",
|
|
1922
|
+
aiSkip: opts.aiSkip
|
|
1187
1923
|
});
|
|
1188
1924
|
} catch (err) {
|
|
1189
1925
|
sp.fail("Init workspace th\u1EA5t b\u1EA1i");
|
|
@@ -1197,7 +1933,7 @@ async function getOrCreateOriginRemote(folderPath, opts) {
|
|
|
1197
1933
|
log.success(`Folder \u0111\xE3 c\xF3 remote origin: ${origin.refs.push}`);
|
|
1198
1934
|
return origin.refs.push;
|
|
1199
1935
|
}
|
|
1200
|
-
const shouldCreate = opts.createRemote ?? await
|
|
1936
|
+
const shouldCreate = opts.createRemote ?? await confirm2({
|
|
1201
1937
|
message: "Folder ch\u01B0a c\xF3 remote. T\u1EA1o GitHub repo ngay \u0111\u1EC3 share team?",
|
|
1202
1938
|
default: true
|
|
1203
1939
|
});
|
|
@@ -1206,14 +1942,14 @@ async function getOrCreateOriginRemote(folderPath, opts) {
|
|
|
1206
1942
|
return void 0;
|
|
1207
1943
|
}
|
|
1208
1944
|
await ensureGitHubReady();
|
|
1209
|
-
const visibility = opts.repoVisibility ?? await
|
|
1945
|
+
const visibility = opts.repoVisibility ?? await select4({
|
|
1210
1946
|
message: "Visibility?",
|
|
1211
1947
|
choices: [
|
|
1212
1948
|
{ name: "private (m\u1EB7c \u0111\u1ECBnh)", value: "private" },
|
|
1213
1949
|
{ name: "public", value: "public" }
|
|
1214
1950
|
]
|
|
1215
1951
|
});
|
|
1216
|
-
const repoName = await
|
|
1952
|
+
const repoName = await input2({
|
|
1217
1953
|
message: "T\xEAn repo:",
|
|
1218
1954
|
default: basename(folderPath)
|
|
1219
1955
|
});
|
|
@@ -1252,7 +1988,8 @@ async function scaffoldWorkspaceWithSrcSubmodule(args) {
|
|
|
1252
1988
|
createWorkspaceRemote: args.createWorkspaceRemote,
|
|
1253
1989
|
repoVisibility: args.repoVisibility,
|
|
1254
1990
|
repoOrg: args.repoOrg,
|
|
1255
|
-
flow: args.flow
|
|
1991
|
+
flow: args.flow,
|
|
1992
|
+
aiSkip: args.aiSkip
|
|
1256
1993
|
});
|
|
1257
1994
|
} catch (err) {
|
|
1258
1995
|
sp.fail("Init workspace th\u1EA5t b\u1EA1i");
|
|
@@ -1272,15 +2009,21 @@ async function finalizeWorkspaceScaffold(args) {
|
|
|
1272
2009
|
await writeRootClaudeMd(args.workspacePath, vars);
|
|
1273
2010
|
await writeProjectSettings(args.workspacePath, vars);
|
|
1274
2011
|
await appendGitignoreEntries(args.workspacePath);
|
|
1275
|
-
await ensureDir(
|
|
1276
|
-
await ensureDir(
|
|
1277
|
-
await installGitHook(
|
|
1278
|
-
await installGitHook(
|
|
2012
|
+
await ensureDir(join16(args.workspacePath, "notes"));
|
|
2013
|
+
await ensureDir(join16(args.workspacePath, "scripts"));
|
|
2014
|
+
await installGitHook(join16(args.workspacePath, ".git"), "post-merge");
|
|
2015
|
+
await installGitHook(join16(args.workspacePath, ".git", "modules", "src"), "pre-push");
|
|
1279
2016
|
log.success("C\xE0i post-merge (workspace) + pre-push (src/)");
|
|
1280
2017
|
await appendAuditEntry("init", `flow=${args.flow},workspace=${args.workspaceName}`);
|
|
1281
2018
|
await maybeCommitWorkspace(args.workspacePath, args.skipCommit);
|
|
1282
2019
|
await maybeCreateWorkspaceRemote(args);
|
|
1283
|
-
|
|
2020
|
+
let aiResult = null;
|
|
2021
|
+
if (args.aiSkip) {
|
|
2022
|
+
log.dim("B\u1ECF qua AI setup (--ai-skip). Setup sau qua: avatar ai setup");
|
|
2023
|
+
} else {
|
|
2024
|
+
aiResult = await runAiSetupPhase({ workspacePath: args.workspacePath });
|
|
2025
|
+
}
|
|
2026
|
+
printInitSuccessBox(args.workspacePath, args.flow, aiResult);
|
|
1284
2027
|
}
|
|
1285
2028
|
async function maybeCreateWorkspaceRemote(args) {
|
|
1286
2029
|
if (args.skipCommit) {
|
|
@@ -1290,13 +2033,13 @@ async function maybeCreateWorkspaceRemote(args) {
|
|
|
1290
2033
|
let shouldCreate = args.createWorkspaceRemote;
|
|
1291
2034
|
if (shouldCreate === void 0) {
|
|
1292
2035
|
if (args.autoYes) return;
|
|
1293
|
-
shouldCreate = await
|
|
2036
|
+
shouldCreate = await confirm2({
|
|
1294
2037
|
message: "T\u1EA1o remote GitHub cho workspace \u0111\u1EC3 share team? (Avatar state)",
|
|
1295
2038
|
default: false
|
|
1296
2039
|
});
|
|
1297
2040
|
}
|
|
1298
2041
|
if (!shouldCreate) return;
|
|
1299
|
-
const visibility = args.repoVisibility ?? (args.autoYes ? "private" : await
|
|
2042
|
+
const visibility = args.repoVisibility ?? (args.autoYes ? "private" : await select4({
|
|
1300
2043
|
message: "Workspace visibility?",
|
|
1301
2044
|
choices: [
|
|
1302
2045
|
{ name: "private (m\u1EB7c \u0111\u1ECBnh, an to\xE0n)", value: "private" },
|
|
@@ -1316,7 +2059,7 @@ async function maybeCreateWorkspaceRemote(args) {
|
|
|
1316
2059
|
}
|
|
1317
2060
|
}
|
|
1318
2061
|
async function resolveWorkspacePath(parent, desiredName, force) {
|
|
1319
|
-
const desired =
|
|
2062
|
+
const desired = join16(parent, desiredName);
|
|
1320
2063
|
if (await isEmptyOrMissing(desired)) return desired;
|
|
1321
2064
|
const alternative = await findAlternativeWorkspaceName(parent, desiredName);
|
|
1322
2065
|
if (!alternative) {
|
|
@@ -1327,12 +2070,12 @@ async function resolveWorkspacePath(parent, desiredName, force) {
|
|
|
1327
2070
|
log.info(`--force: d\xF9ng ${alternative}`);
|
|
1328
2071
|
return alternative;
|
|
1329
2072
|
}
|
|
1330
|
-
const useAlt = await
|
|
2073
|
+
const useAlt = await confirm2({ message: `D\xF9ng "${alternative}" thay th\u1EBF?`, default: true });
|
|
1331
2074
|
if (!useAlt) throw new Error("H\u1EE7y init. Ch\u1EA1y l\u1EA1i v\u1EDBi --workspace-name kh\xE1c.");
|
|
1332
2075
|
return alternative;
|
|
1333
2076
|
}
|
|
1334
2077
|
async function promptTeamOwner(currentUserEmail) {
|
|
1335
|
-
return await
|
|
2078
|
+
return await input2({ message: "Team owner email:", default: currentUserEmail });
|
|
1336
2079
|
}
|
|
1337
2080
|
async function maybeCommitWorkspace(workspacePath, skipCommit) {
|
|
1338
2081
|
if (skipCommit) {
|
|
@@ -1344,10 +2087,21 @@ async function maybeCommitWorkspace(workspacePath, skipCommit) {
|
|
|
1344
2087
|
await g.commit("chore: initialize Avatar workspace");
|
|
1345
2088
|
log.success("\u0110\xE3 commit workspace");
|
|
1346
2089
|
}
|
|
1347
|
-
function
|
|
2090
|
+
function formatAiStatusLine(aiResult) {
|
|
2091
|
+
if (aiResult === null) {
|
|
2092
|
+
return ` ${chalk.yellow("AI:")} skipped \xB7 ${chalk.cyan("avatar ai setup")} \u0111\u1EC3 config sau`;
|
|
2093
|
+
}
|
|
2094
|
+
if (aiResult.ok) {
|
|
2095
|
+
const modelPart = aiResult.model ? ` \xB7 model=${aiResult.model}` : "";
|
|
2096
|
+
return ` ${chalk.green("AI:")} ready \xB7 ${aiResult.provider}${modelPart}`;
|
|
2097
|
+
}
|
|
2098
|
+
return ` ${chalk.yellow("AI:")} failed (${aiResult.reason.slice(0, 60)}) \xB7 th\u1EED ${chalk.cyan("avatar ai setup")}`;
|
|
2099
|
+
}
|
|
2100
|
+
function printInitSuccessBox(rootPath, flow, aiResult = null) {
|
|
1348
2101
|
const lines = [
|
|
1349
2102
|
`${chalk.green("\u2713")} Workspace s\u1EB5n s\xE0ng: ${relative2(process.cwd(), rootPath) || rootPath}`,
|
|
1350
2103
|
` ${chalk.dim(`(flow: ${flow})`)}`,
|
|
2104
|
+
formatAiStatusLine(aiResult),
|
|
1351
2105
|
"",
|
|
1352
2106
|
` ${chalk.cyan(`cd ${rootPath}`)}`,
|
|
1353
2107
|
` ${chalk.cyan("claude")} M\u1EDF Claude Code \u1EDF workspace root`,
|
|
@@ -1582,18 +2336,18 @@ function registerSecretsCommand(program2) {
|
|
|
1582
2336
|
}
|
|
1583
2337
|
|
|
1584
2338
|
// src/commands/status.ts
|
|
1585
|
-
import { promises as
|
|
1586
|
-
import { join as
|
|
2339
|
+
import { promises as fs8 } from "fs";
|
|
2340
|
+
import { join as join18 } from "path";
|
|
1587
2341
|
import boxen4 from "boxen";
|
|
1588
2342
|
|
|
1589
2343
|
// src/lib/pack-backup-manager.ts
|
|
1590
|
-
import { promises as
|
|
1591
|
-
import { join as
|
|
2344
|
+
import { promises as fs7 } from "fs";
|
|
2345
|
+
import { join as join17 } from "path";
|
|
1592
2346
|
var BACKUP_DIR_NAME = "_backup";
|
|
1593
2347
|
async function listBackups(projectRoot) {
|
|
1594
|
-
const dir =
|
|
2348
|
+
const dir = join17(projectRoot, ".claude", BACKUP_DIR_NAME);
|
|
1595
2349
|
if (!await pathExists(dir)) return [];
|
|
1596
|
-
const entries = await
|
|
2350
|
+
const entries = await fs7.readdir(dir, { withFileTypes: true });
|
|
1597
2351
|
return entries.filter((e) => e.isDirectory()).map((e) => e.name).sort().reverse();
|
|
1598
2352
|
}
|
|
1599
2353
|
|
|
@@ -1617,7 +2371,7 @@ function registerStatusCommand(program2) {
|
|
|
1617
2371
|
}
|
|
1618
2372
|
async function gatherStatus(cwd) {
|
|
1619
2373
|
const projectName = cwd.split("/").filter(Boolean).pop() ?? "unknown";
|
|
1620
|
-
const claudeRoot =
|
|
2374
|
+
const claudeRoot = join18(cwd, ".claude");
|
|
1621
2375
|
const hasAvatar = await pathExists(claudeRoot);
|
|
1622
2376
|
if (!hasAvatar) {
|
|
1623
2377
|
return {
|
|
@@ -1630,9 +2384,9 @@ async function gatherStatus(cwd) {
|
|
|
1630
2384
|
hasAvatar: false
|
|
1631
2385
|
};
|
|
1632
2386
|
}
|
|
1633
|
-
const packVersion = await isGitRepo(
|
|
1634
|
-
const pendingDir =
|
|
1635
|
-
const pendingCount = await pathExists(pendingDir) ? (await
|
|
2387
|
+
const packVersion = await isGitRepo(join18(claudeRoot, "pack")) ? await readPinnedPackVersion(cwd).catch(() => null) : null;
|
|
2388
|
+
const pendingDir = join18(claudeRoot, "_pending");
|
|
2389
|
+
const pendingCount = await pathExists(pendingDir) ? (await fs8.readdir(pendingDir)).filter((n) => n.endsWith(".diff.md")).length : 0;
|
|
1636
2390
|
const backupCount = (await listBackups(cwd)).length;
|
|
1637
2391
|
const techStackSummary = await readTechStackFirstLine(claudeRoot);
|
|
1638
2392
|
return {
|
|
@@ -1646,7 +2400,7 @@ async function gatherStatus(cwd) {
|
|
|
1646
2400
|
};
|
|
1647
2401
|
}
|
|
1648
2402
|
async function readTechStackFirstLine(claudeRoot) {
|
|
1649
|
-
const techStackPath =
|
|
2403
|
+
const techStackPath = join18(claudeRoot, "project", "tech-stack.md");
|
|
1650
2404
|
if (!await pathExists(techStackPath)) return "(no tech-stack.md)";
|
|
1651
2405
|
const content = await readText(techStackPath);
|
|
1652
2406
|
const firstNonHeaderLine = content.split("\n").find((l) => l.trim() && !l.startsWith("#") && !l.startsWith(">"));
|
|
@@ -1681,33 +2435,33 @@ function registerToolsCommand(program2) {
|
|
|
1681
2435
|
|
|
1682
2436
|
// src/commands/uninstall.ts
|
|
1683
2437
|
import { relative as relative3 } from "path";
|
|
1684
|
-
import { confirm as
|
|
2438
|
+
import { confirm as confirm3 } from "@inquirer/prompts";
|
|
1685
2439
|
import boxen5 from "boxen";
|
|
1686
2440
|
|
|
1687
2441
|
// src/lib/create-uninstall-backup-snapshot.ts
|
|
1688
2442
|
import { cp, mkdir, writeFile } from "fs/promises";
|
|
1689
|
-
import { homedir as
|
|
1690
|
-
import { basename as basename2, join as
|
|
1691
|
-
var UNINSTALL_BACKUPS_DIR =
|
|
2443
|
+
import { homedir as homedir3 } from "os";
|
|
2444
|
+
import { basename as basename2, join as join19 } from "path";
|
|
2445
|
+
var UNINSTALL_BACKUPS_DIR = join19(homedir3(), ".avatar", "uninstall-backups");
|
|
1692
2446
|
async function createUninstallBackupSnapshot(projectRoot, artifacts, avatarVersion) {
|
|
1693
2447
|
const projectName = basename2(projectRoot);
|
|
1694
2448
|
const timestamp = (/* @__PURE__ */ new Date()).toISOString().replace(/[:.]/g, "-");
|
|
1695
|
-
const backupDir =
|
|
2449
|
+
const backupDir = join19(UNINSTALL_BACKUPS_DIR, `${projectName}-${timestamp}`);
|
|
1696
2450
|
await mkdir(backupDir, { recursive: true, mode: 448 });
|
|
1697
2451
|
if (artifacts.claudeDir) {
|
|
1698
|
-
await cp(artifacts.claudeDir,
|
|
2452
|
+
await cp(artifacts.claudeDir, join19(backupDir, ".claude"), { recursive: true });
|
|
1699
2453
|
}
|
|
1700
2454
|
if (artifacts.claudeMd) {
|
|
1701
|
-
await cp(artifacts.claudeMd,
|
|
2455
|
+
await cp(artifacts.claudeMd, join19(backupDir, "CLAUDE.md"));
|
|
1702
2456
|
}
|
|
1703
2457
|
if (artifacts.postMergeHook || artifacts.prePushHook) {
|
|
1704
|
-
const hooksBackupDir =
|
|
2458
|
+
const hooksBackupDir = join19(backupDir, "hooks");
|
|
1705
2459
|
await mkdir(hooksBackupDir, { recursive: true });
|
|
1706
2460
|
if (artifacts.postMergeHook) {
|
|
1707
|
-
await cp(artifacts.postMergeHook,
|
|
2461
|
+
await cp(artifacts.postMergeHook, join19(hooksBackupDir, "post-merge"));
|
|
1708
2462
|
}
|
|
1709
2463
|
if (artifacts.prePushHook) {
|
|
1710
|
-
await cp(artifacts.prePushHook,
|
|
2464
|
+
await cp(artifacts.prePushHook, join19(hooksBackupDir, "pre-push"));
|
|
1711
2465
|
}
|
|
1712
2466
|
}
|
|
1713
2467
|
const manifest = {
|
|
@@ -1722,27 +2476,27 @@ async function createUninstallBackupSnapshot(projectRoot, artifacts, avatarVersi
|
|
|
1722
2476
|
prePushHook: !!artifacts.prePushHook
|
|
1723
2477
|
}
|
|
1724
2478
|
};
|
|
1725
|
-
await writeFile(
|
|
2479
|
+
await writeFile(join19(backupDir, "manifest.json"), JSON.stringify(manifest, null, 2), "utf8");
|
|
1726
2480
|
return backupDir;
|
|
1727
2481
|
}
|
|
1728
2482
|
|
|
1729
2483
|
// src/lib/detect-avatar-project-artifacts.ts
|
|
1730
2484
|
import { existsSync as existsSync5 } from "fs";
|
|
1731
|
-
import { join as
|
|
2485
|
+
import { join as join20 } from "path";
|
|
1732
2486
|
function existsOrNull(path) {
|
|
1733
2487
|
return existsSync5(path) ? path : null;
|
|
1734
2488
|
}
|
|
1735
2489
|
function detectAvatarProjectArtifacts(projectRoot) {
|
|
1736
|
-
const claudeDir = existsOrNull(
|
|
1737
|
-
const claudeMd = existsOrNull(
|
|
1738
|
-
const postMergeHook = existsOrNull(
|
|
2490
|
+
const claudeDir = existsOrNull(join20(projectRoot, ".claude"));
|
|
2491
|
+
const claudeMd = existsOrNull(join20(projectRoot, "CLAUDE.md"));
|
|
2492
|
+
const postMergeHook = existsOrNull(join20(projectRoot, ".git", "hooks", "post-merge"));
|
|
1739
2493
|
const prePushHook = existsOrNull(
|
|
1740
|
-
|
|
2494
|
+
join20(projectRoot, ".git", "modules", "src", "hooks", "pre-push")
|
|
1741
2495
|
);
|
|
1742
|
-
const gitignorePath = existsOrNull(
|
|
1743
|
-
const gitmodulesPath = existsOrNull(
|
|
1744
|
-
const notesDir = existsOrNull(
|
|
1745
|
-
const scriptsDir = existsOrNull(
|
|
2496
|
+
const gitignorePath = existsOrNull(join20(projectRoot, ".gitignore"));
|
|
2497
|
+
const gitmodulesPath = existsOrNull(join20(projectRoot, ".gitmodules"));
|
|
2498
|
+
const notesDir = existsOrNull(join20(projectRoot, "notes"));
|
|
2499
|
+
const scriptsDir = existsOrNull(join20(projectRoot, "scripts"));
|
|
1746
2500
|
const hasAnyArtifact = !!(claudeDir || claudeMd || postMergeHook || prePushHook);
|
|
1747
2501
|
return {
|
|
1748
2502
|
hasAnyArtifact,
|
|
@@ -1763,11 +2517,11 @@ async function executeUninstallDeletion(artifacts, flags) {
|
|
|
1763
2517
|
if (artifacts.claudeDir) {
|
|
1764
2518
|
if (flags.keepSubmodule) {
|
|
1765
2519
|
const { readdir: readdir2 } = await import("fs/promises");
|
|
1766
|
-
const { join:
|
|
2520
|
+
const { join: join21 } = await import("path");
|
|
1767
2521
|
const entries = await readdir2(artifacts.claudeDir);
|
|
1768
2522
|
for (const entry of entries) {
|
|
1769
2523
|
if (entry === "pack") continue;
|
|
1770
|
-
await rm(
|
|
2524
|
+
await rm(join21(artifacts.claudeDir, entry), { recursive: true, force: true });
|
|
1771
2525
|
}
|
|
1772
2526
|
} else {
|
|
1773
2527
|
await rm(artifacts.claudeDir, { recursive: true, force: true });
|
|
@@ -1836,7 +2590,7 @@ async function removeSubmoduleEntry(gitmodulesPath, submodulePath) {
|
|
|
1836
2590
|
}
|
|
1837
2591
|
|
|
1838
2592
|
// src/commands/uninstall.ts
|
|
1839
|
-
var CLI_VERSION = "1.
|
|
2593
|
+
var CLI_VERSION = "1.2.0";
|
|
1840
2594
|
function registerUninstallCommand(program2) {
|
|
1841
2595
|
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
2596
|
try {
|
|
@@ -1860,7 +2614,7 @@ async function runUninstall(opts) {
|
|
|
1860
2614
|
return;
|
|
1861
2615
|
}
|
|
1862
2616
|
if (!opts.yes) {
|
|
1863
|
-
const ok = await
|
|
2617
|
+
const ok = await confirm3({
|
|
1864
2618
|
message: "Ti\u1EBFp t\u1EE5c g\u1EE1 Avatar?",
|
|
1865
2619
|
default: false
|
|
1866
2620
|
});
|
|
@@ -1918,7 +2672,7 @@ function printUninstallSuccessBox(backupPath) {
|
|
|
1918
2672
|
}
|
|
1919
2673
|
|
|
1920
2674
|
// src/index.ts
|
|
1921
|
-
var CLI_VERSION2 = "1.
|
|
2675
|
+
var CLI_VERSION2 = "1.2.0";
|
|
1922
2676
|
var program = new Command();
|
|
1923
2677
|
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
2678
|
"beforeAll",
|
|
@@ -1944,6 +2698,7 @@ registerCommitCommand(program);
|
|
|
1944
2698
|
registerToolsCommand(program);
|
|
1945
2699
|
registerSecretsCommand(program);
|
|
1946
2700
|
registerMcpRunCommand(program);
|
|
2701
|
+
registerAiCommand(program);
|
|
1947
2702
|
registerUninstallCommand(program);
|
|
1948
2703
|
program.parseAsync(process.argv).catch((err) => {
|
|
1949
2704
|
const msg = err instanceof Error ? err.message : String(err);
|