@nalvietnam/avatar-cli 1.1.5 → 1.2.0

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