@nalvietnam/avatar-cli 1.1.6 → 1.2.1

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