@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 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 fs3 } from "fs";
55
- import { join as join6 } from "path";
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 join2 } from "path";
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(join2(cwd, ".git"));
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 = join2(cwd, submodulePath);
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 fs2 } from "fs";
124
- import { join as join4 } from "path";
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 join3 } from "path";
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 = join3(PACKAGE_ROOT, "src", "templates");
145
- var HOOKS_ROOT = join3(PACKAGE_ROOT, "src", "hooks");
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(join3(dir, "package.json"))) return dir;
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(join3(TEMPLATES_ROOT, `${name}.tpl`));
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(join3(HOOKS_ROOT, `${name}.sh.tpl`));
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 fs2.rename(path, backupPath);
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 = join4(projectRoot, ".claude");
858
+ const claudeRoot = join8(projectRoot, ".claude");
200
859
  await ensureDir(claudeRoot);
201
860
  for (const sub of CLAUDE_SUBDIRS) {
202
- const dir = join4(claudeRoot, sub);
861
+ const dir = join8(claudeRoot, sub);
203
862
  await ensureDir(dir);
204
- await writeTextAtomic(join4(dir, ".gitkeep"), "");
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 = join4(projectRoot, ".claude", "project", relative4);
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(join4(projectRoot, "CLAUDE.md"), content);
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(join4(projectRoot, ".claude", "settings.json"), content);
906
+ return await writeWithBackup(join8(projectRoot, ".claude", "settings.json"), content);
248
907
  }
249
908
  async function appendGitignoreEntries(projectRoot) {
250
- const path = join4(projectRoot, ".gitignore");
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 fs2.readFile(path, "utf8");
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 = join4(gitDir, "hooks");
923
+ const hooksDir = join8(gitDir, "hooks");
265
924
  await ensureDir(hooksDir);
266
- const dest = join4(hooksDir, hookName);
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 = join6(cwd, ".claude", "pack");
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 = join6(cwd, "CLAUDE.md");
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 = join6(cwd, ".git", "hooks", "post-merge");
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(join6(cwd, ".git"), "post-merge");
1008
+ await installGitHook(join9(cwd, ".git"), "post-merge");
416
1009
  }
417
1010
  });
418
1011
  }
419
- const gitignorePath = join6(cwd, ".gitignore");
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 fs3.readFile(gitignorePath, "utf8");
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 = spawnSync("which", ["claude"]);
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 join13, relative as relative2, resolve } from "path";
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 spawnSync2 } from "child_process";
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(input2) {
569
- const fullName = `${input2.org}/${input2.name}`;
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
- `--${input2.visibility}`,
1153
+ `--${input3.visibility}`,
575
1154
  "--source",
576
- input2.folder,
1155
+ input3.folder,
577
1156
  "--remote",
578
1157
  "origin",
579
1158
  "--push"
580
1159
  ];
581
- const r = spawnSync2("gh", args, { stdio: "inherit" });
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 spawnSync3 } from "child_process";
1174
+ import { spawnSync as spawnSync7 } from "child_process";
596
1175
  function resolveGithubUsernameDefault() {
597
- const r = spawnSync3("gh", ["api", "user", "--jq", ".login"], {
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(input2) {
630
- validateRepoName(input2.name);
631
- validateRepoVisibility(input2.visibility);
632
- const org = input2.org ?? resolveGithubUsernameDefault();
633
- log.info(`T\u1EA1o GitHub repo ${org}/${input2.name} (${input2.visibility})...`);
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: input2.folder,
1214
+ folder: input3.folder,
636
1215
  org,
637
- name: input2.name,
638
- visibility: input2.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 spawnSync10 } from "child_process";
1224
+ import { spawnSync as spawnSync14 } from "child_process";
646
1225
 
647
1226
  // src/lib/check-gh-cli-auth-status.ts
648
- import { spawnSync as spawnSync4 } from "child_process";
1227
+ import { spawnSync as spawnSync8 } from "child_process";
649
1228
  function checkGhCliAuthStatus() {
650
- const r = spawnSync4("gh", ["auth", "status"], { stdio: "ignore" });
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 spawnSync5 } from "child_process";
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 = spawnSync5(probe, args, {
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 spawnSync6 } from "child_process";
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 = spawnSync6(spec.cmd, spec.args, { stdio: "inherit" });
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 spawnSync7 } from "child_process";
1277
+ import { spawnSync as spawnSync11 } from "child_process";
709
1278
  function setupGitCredentialViaGh() {
710
- const r = spawnSync7("gh", ["auth", "setup-git"], { stdio: "ignore" });
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 spawnSync8 } from "child_process";
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 = spawnSync8(
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 spawnSync9 } from "child_process";
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 = spawnSync9("git", ["ls-remote", "--exit-code", url, "HEAD"], {
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(input2) {
784
- validateRepoName(input2.workspaceName);
785
- validateRepoVisibility(input2.visibility);
1352
+ async function createWorkspaceRemoteViaGh(input3) {
1353
+ validateRepoName(input3.workspaceName);
1354
+ validateRepoVisibility(input3.visibility);
786
1355
  await ensureGitHubReady();
787
- const org = input2.org ?? resolveGithubUsernameDefault();
788
- const fullName = `${org}/${input2.workspaceName}`;
789
- log.info(`T\u1EA1o GitHub repo cho workspace: ${fullName} (${input2.visibility})...`);
790
- const r = spawnSync10(
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
- `--${input2.visibility}`,
1365
+ `--${input3.visibility}`,
797
1366
  "--source",
798
- input2.workspacePath,
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} --${input2.visibility} --source=. --remote=origin --push`
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 join7 } from "path";
1392
+ import { join as join10 } from "path";
819
1393
  function checkFolderHasGit(folderPath) {
820
- const gitPath = join7(folderPath, ".git");
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 join8 } from "path";
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(join8(folderPath, f)))) {
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 join9 } from "path";
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
- join9(__dirname, "..", "templates", "gitignore"),
878
- join9(__dirname, "..", "..", "src", "templates", "gitignore")
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 readFileSync(join9(dir, `${stack}.txt`), "utf8");
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 readFileSync2, writeFileSync } from "fs";
900
- import { join as join10 } from "path";
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 = join10(folderPath, ".gitignore");
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 = readFileSync2(path, "utf8");
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/git-bootstrap-orchestrator.ts
924
- async function bootstrapGitInFolder(folderPath) {
925
- const hadGit = checkFolderHasGit(folderPath);
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 detected: ${stacks.join(", ")}`);
1571
+ log.info(`Tech stack: ${stacks.join(", ")}`);
928
1572
  writeOrMergeGitignore(folderPath, composeGitignoreContent(stacks));
929
1573
  log.success(".gitignore \u0111\xE3 ghi (Avatar block)");
930
- if (!hadGit) {
931
- log.info(`Bootstrap git cho ${folderPath}...`);
932
- await createInitialGitCommit(folderPath);
933
- log.success("\u0110\xE3 git init + initial commit");
934
- } else {
935
- log.dim("Folder \u0111\xE3 c\xF3 .git \u2014 skip init.");
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 join11 } from "path";
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(join11(projectRoot, TEAM_PACK_RELATIVE_PATH));
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 = join11(projectRoot, TEAM_PACK_RELATIVE_PATH);
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 join12 } from "path";
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 = join12(parent, `${desiredName}-${i}`);
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("--mode <mode>", "[DEPRECATED] D\xF9ng --project-status thay th\u1EBF").action(async (opts) => {
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 select({
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 input({
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 input({ message: "T\xEAn workspace:", default: inferredName });
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 input({
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 bootstrapGitInFolder(folderPath);
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 input({ message: "T\xEAn workspace:", default: inferredName });
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 input({
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 select({
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 = join13(workspacePath, "src");
1886
+ const srcPath = join16(workspacePath, "src");
1152
1887
  await ensureDir(workspacePath);
1153
1888
  await ensureDir(srcPath);
1154
- await bootstrapGitInFolder(srcPath);
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 confirm({
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 select({
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 input({
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(join13(args.workspacePath, "notes"));
1276
- await ensureDir(join13(args.workspacePath, "scripts"));
1277
- await installGitHook(join13(args.workspacePath, ".git"), "post-merge");
1278
- await installGitHook(join13(args.workspacePath, ".git", "modules", "src"), "pre-push");
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
- printInitSuccessBox(args.workspacePath, args.flow);
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 confirm({
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 select({
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 = join13(parent, desiredName);
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 confirm({ message: `D\xF9ng "${alternative}" thay th\u1EBF?`, default: true });
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 input({ message: "Team owner email:", default: currentUserEmail });
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 printInitSuccessBox(rootPath, flow) {
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 fs6 } from "fs";
1586
- import { join as join15 } from "path";
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 fs5 } from "fs";
1591
- import { join as join14 } from "path";
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 = join14(projectRoot, ".claude", BACKUP_DIR_NAME);
2348
+ const dir = join17(projectRoot, ".claude", BACKUP_DIR_NAME);
1595
2349
  if (!await pathExists(dir)) return [];
1596
- const entries = await fs5.readdir(dir, { withFileTypes: true });
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 = join15(cwd, ".claude");
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(join15(claudeRoot, "pack")) ? await readPinnedPackVersion(cwd).catch(() => null) : null;
1634
- const pendingDir = join15(claudeRoot, "_pending");
1635
- const pendingCount = await pathExists(pendingDir) ? (await fs6.readdir(pendingDir)).filter((n) => n.endsWith(".diff.md")).length : 0;
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 = join15(claudeRoot, "project", "tech-stack.md");
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 confirm2 } from "@inquirer/prompts";
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 homedir2 } from "os";
1690
- import { basename as basename2, join as join16 } from "path";
1691
- var UNINSTALL_BACKUPS_DIR = join16(homedir2(), ".avatar", "uninstall-backups");
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 = join16(UNINSTALL_BACKUPS_DIR, `${projectName}-${timestamp}`);
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, join16(backupDir, ".claude"), { recursive: true });
2452
+ await cp(artifacts.claudeDir, join19(backupDir, ".claude"), { recursive: true });
1699
2453
  }
1700
2454
  if (artifacts.claudeMd) {
1701
- await cp(artifacts.claudeMd, join16(backupDir, "CLAUDE.md"));
2455
+ await cp(artifacts.claudeMd, join19(backupDir, "CLAUDE.md"));
1702
2456
  }
1703
2457
  if (artifacts.postMergeHook || artifacts.prePushHook) {
1704
- const hooksBackupDir = join16(backupDir, "hooks");
2458
+ const hooksBackupDir = join19(backupDir, "hooks");
1705
2459
  await mkdir(hooksBackupDir, { recursive: true });
1706
2460
  if (artifacts.postMergeHook) {
1707
- await cp(artifacts.postMergeHook, join16(hooksBackupDir, "post-merge"));
2461
+ await cp(artifacts.postMergeHook, join19(hooksBackupDir, "post-merge"));
1708
2462
  }
1709
2463
  if (artifacts.prePushHook) {
1710
- await cp(artifacts.prePushHook, join16(hooksBackupDir, "pre-push"));
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(join16(backupDir, "manifest.json"), JSON.stringify(manifest, null, 2), "utf8");
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 join17 } from "path";
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(join17(projectRoot, ".claude"));
1737
- const claudeMd = existsOrNull(join17(projectRoot, "CLAUDE.md"));
1738
- const postMergeHook = existsOrNull(join17(projectRoot, ".git", "hooks", "post-merge"));
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
- join17(projectRoot, ".git", "modules", "src", "hooks", "pre-push")
2494
+ join20(projectRoot, ".git", "modules", "src", "hooks", "pre-push")
1741
2495
  );
1742
- const gitignorePath = existsOrNull(join17(projectRoot, ".gitignore"));
1743
- const gitmodulesPath = existsOrNull(join17(projectRoot, ".gitmodules"));
1744
- const notesDir = existsOrNull(join17(projectRoot, "notes"));
1745
- const scriptsDir = existsOrNull(join17(projectRoot, "scripts"));
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: join18 } = await import("path");
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(join18(artifacts.claudeDir, entry), { recursive: true, force: true });
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.1.6";
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 confirm2({
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.1.6";
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);