@nalvietnam/avatar-cli 1.0.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 ADDED
@@ -0,0 +1,1025 @@
1
+ // @nalvietnam/avatar-cli — built with tsup
2
+
3
+ // src/index.ts
4
+ import { Command } from "commander";
5
+
6
+ // src/lib/terminal-logger.ts
7
+ import chalk from "chalk";
8
+ import ora from "ora";
9
+ var log = {
10
+ info: (m) => process.stdout.write(`${chalk.blue("\u2139")} ${m}
11
+ `),
12
+ success: (m) => process.stdout.write(`${chalk.green("\u2713")} ${m}
13
+ `),
14
+ warn: (m) => process.stdout.write(`${chalk.yellow("\u26A0")} ${m}
15
+ `),
16
+ error: (m) => process.stderr.write(`${chalk.red("\u2717")} ${m}
17
+ `),
18
+ dim: (m) => process.stdout.write(`${chalk.dim(m)}
19
+ `),
20
+ plain: (m) => process.stdout.write(`${m}
21
+ `)
22
+ };
23
+ function spinner(text) {
24
+ return ora({
25
+ text,
26
+ spinner: "dots",
27
+ isEnabled: process.stdout.isTTY ?? false
28
+ }).start();
29
+ }
30
+
31
+ // src/lib/not-implemented-stub.ts
32
+ function notImplementedYet(commandName, milestone) {
33
+ return () => {
34
+ process.stdout.write(
35
+ `${chalk.yellow("\u23F3")} ${chalk.bold(`avatar ${commandName}`)} \u2014 ch\u01B0a implement \u1EDF milestone hi\u1EC7n t\u1EA1i.
36
+ `
37
+ );
38
+ if (milestone) {
39
+ process.stdout.write(` D\u1EF1 ki\u1EBFn: ${chalk.cyan(milestone)}
40
+ `);
41
+ }
42
+ process.stdout.write(" Spec \u0111\xE3 c\xF3 trong avatar-cli-implementation_4.html.\n");
43
+ process.exit(0);
44
+ };
45
+ }
46
+
47
+ // src/commands/commit.ts
48
+ function registerCommitCommand(program2) {
49
+ program2.command("commit").description("Commit code kh\xE1ch (src/) ho\u1EB7c Avatar state ri\xEAng \u2014 ch\u1EC9 client mode (M07)").option("--src", "Commit src/ \u2192 client remote").option("--avatar", "Commit Avatar state \u2192 workspace remote").option("--both", "Commit c\u1EA3 hai (src tr\u01B0\u1EDBc, avatar sau)").option("-m, --message <msg>", "Commit message").option("--push", "T\u1EF1 \u0111\u1ED9ng push sau khi commit").action(notImplementedYet("commit", "Milestone 07"));
50
+ }
51
+
52
+ // src/commands/doctor.ts
53
+ import { spawnSync } from "child_process";
54
+ import { promises as fs3 } from "fs";
55
+ import { join as join6 } from "path";
56
+ import boxen from "boxen";
57
+
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
+ // src/lib/git-operations.ts
93
+ import { join as join2 } from "path";
94
+ import { simpleGit } from "simple-git";
95
+ function git(cwd = process.cwd()) {
96
+ return simpleGit({ baseDir: cwd, binary: "git" });
97
+ }
98
+ async function isGitRepo(cwd = process.cwd()) {
99
+ return await pathExists(join2(cwd, ".git"));
100
+ }
101
+ async function addSubmodule(repoUrl, destPath, cwd = process.cwd()) {
102
+ await git(cwd).subModule(["add", repoUrl, destPath]);
103
+ }
104
+ async function checkoutTagInSubmodule(submodulePath, tag, cwd = process.cwd()) {
105
+ const submoduleCwd = join2(cwd, submodulePath);
106
+ await git(submoduleCwd).fetch(["--tags"]);
107
+ await git(submoduleCwd).checkout(tag);
108
+ }
109
+ async function listTags(cwd = process.cwd()) {
110
+ const result = await git(cwd).tags();
111
+ return result.all;
112
+ }
113
+ async function latestTag(cwd = process.cwd()) {
114
+ const tags = await listTags(cwd);
115
+ return tags.length > 0 ? tags[tags.length - 1] ?? null : null;
116
+ }
117
+ async function currentCommitSha(cwd = process.cwd()) {
118
+ const result = await git(cwd).revparse(["HEAD"]);
119
+ return result.trim();
120
+ }
121
+
122
+ // src/lib/project-tree-scaffolder.ts
123
+ import { promises as fs2 } from "fs";
124
+ import { join as join4 } from "path";
125
+
126
+ // src/lib/template-bundle-loader.ts
127
+ import { dirname as dirname2, join as join3 } from "path";
128
+ import { fileURLToPath } from "url";
129
+
130
+ // src/lib/mustache-template-engine.ts
131
+ var TEMPLATE_PATTERN = /\{\{\s*([a-zA-Z_][a-zA-Z0-9_]*)\s*\}\}/g;
132
+ function renderTemplate(source, variables) {
133
+ return source.replace(TEMPLATE_PATTERN, (match, key) => {
134
+ const value = variables[key];
135
+ if (value === void 0) return match;
136
+ return String(value);
137
+ });
138
+ }
139
+
140
+ // src/lib/template-bundle-loader.ts
141
+ var HERE = dirname2(fileURLToPath(import.meta.url));
142
+ var TEMPLATES_ROOT = join3(HERE, "..", "src", "templates");
143
+ var HOOKS_ROOT = join3(HERE, "..", "src", "hooks");
144
+ async function loadTemplate(name) {
145
+ return await readText(join3(TEMPLATES_ROOT, `${name}.tpl`));
146
+ }
147
+ async function renderTemplateByName(name, variables) {
148
+ const source = await loadTemplate(name);
149
+ return renderTemplate(source, variables);
150
+ }
151
+ async function loadHook(name) {
152
+ return await readText(join3(HOOKS_ROOT, `${name}.sh.tpl`));
153
+ }
154
+
155
+ // src/lib/project-tree-scaffolder.ts
156
+ var CLAUDE_SUBDIRS = ["project", "state", "_pending", "_backup"];
157
+ var PROJECT_KNOWLEDGE_TEMPLATES = [
158
+ "project/tech-stack.md",
159
+ "project/conventions.md",
160
+ "project/architecture.md",
161
+ "project/domain.md",
162
+ "project/gotchas.md"
163
+ ];
164
+ async function createClaudeDirTree(projectRoot) {
165
+ const claudeRoot = join4(projectRoot, ".claude");
166
+ await ensureDir(claudeRoot);
167
+ for (const sub of CLAUDE_SUBDIRS) {
168
+ const dir = join4(claudeRoot, sub);
169
+ await ensureDir(dir);
170
+ await writeTextAtomic(join4(dir, ".gitkeep"), "");
171
+ }
172
+ }
173
+ async function writeProjectKnowledgeFiles(projectRoot, vars) {
174
+ const baseVars = {
175
+ ...vars,
176
+ primaryLanguage: "(ch\u01B0a scan)",
177
+ frameworks: "(ch\u01B0a scan)",
178
+ databases: "(ch\u01B0a scan)",
179
+ testStack: "(ch\u01B0a scan)",
180
+ buildStack: "(ch\u01B0a scan)",
181
+ toolVersions: "(ch\u01B0a scan)",
182
+ codeStyle: "(ch\u01B0a scan)",
183
+ namingConvention: "(ch\u01B0a scan)",
184
+ folderStructure: "(ch\u01B0a scan)",
185
+ commitConvention: "(ch\u01B0a scan)",
186
+ linterConfig: "(ch\u01B0a scan)",
187
+ architectureOverview: "(ch\u01B0a scan)",
188
+ moduleLayout: "(ch\u01B0a scan)",
189
+ dataFlow: "(ch\u01B0a scan)",
190
+ externalIntegrations: "(ch\u01B0a scan)",
191
+ deploymentTopology: "(ch\u01B0a scan)",
192
+ domainDescription: "(ch\u01B0a scan)",
193
+ coreEntities: "(ch\u01B0a scan)",
194
+ primaryUseCases: "(ch\u01B0a scan)",
195
+ domainGlossary: "(ch\u01B0a scan)"
196
+ };
197
+ for (const tpl of PROJECT_KNOWLEDGE_TEMPLATES) {
198
+ const content = await renderTemplateByName(tpl, baseVars);
199
+ const relative2 = tpl.replace(/^project\//, "");
200
+ const outPath = join4(projectRoot, ".claude", "project", relative2);
201
+ await writeTextAtomic(outPath, content);
202
+ }
203
+ }
204
+ async function writeRootClaudeMd(projectRoot, vars) {
205
+ const content = await renderTemplateByName("CLAUDE.md", vars);
206
+ await writeTextAtomic(join4(projectRoot, "CLAUDE.md"), content);
207
+ }
208
+ async function writeProjectSettings(projectRoot, vars) {
209
+ const content = await renderTemplateByName("settings.json", vars);
210
+ await writeTextAtomic(join4(projectRoot, ".claude", "settings.json"), content);
211
+ }
212
+ async function appendGitignoreEntries(projectRoot) {
213
+ const path = join4(projectRoot, ".gitignore");
214
+ const tpl = await renderTemplateByName("gitignore", {});
215
+ const marker = "# Avatar \u2014 git-ignored entries injected on `avatar init`";
216
+ let existing = "";
217
+ if (await pathExists(path)) {
218
+ existing = await fs2.readFile(path, "utf8");
219
+ if (existing.includes(marker)) return;
220
+ }
221
+ const separator = existing.endsWith("\n") || existing.length === 0 ? "" : "\n";
222
+ await writeTextAtomic(path, `${existing}${separator}
223
+ ${tpl}`);
224
+ }
225
+ async function installGitHook(gitDir, hookName) {
226
+ const content = await loadHook(hookName);
227
+ const hooksDir = join4(gitDir, "hooks");
228
+ await ensureDir(hooksDir);
229
+ const dest = join4(hooksDir, hookName);
230
+ await writeTextAtomic(dest, content, 493);
231
+ }
232
+
233
+ // src/lib/user-config-store.ts
234
+ import { homedir } from "os";
235
+ import { join as join5 } from "path";
236
+
237
+ // src/types/config-schema.ts
238
+ import { z } from "zod";
239
+ var userConfigSchema = z.object({
240
+ email: z.string().email(),
241
+ name: z.string(),
242
+ access_token: z.string().min(1),
243
+ refresh_token: z.string().min(1),
244
+ expires_at: z.string().datetime(),
245
+ id_token: z.string().min(1)
246
+ });
247
+ var userStateSchema = z.object({
248
+ installed_tools: z.record(
249
+ z.string(),
250
+ z.object({
251
+ version: z.string().optional(),
252
+ installed_at: z.string().datetime(),
253
+ install_method: z.string()
254
+ })
255
+ ).default({}),
256
+ tool_inputs: z.record(z.string(), z.unknown()).default({})
257
+ });
258
+ var projectSettingsSchema = z.object({
259
+ allowedTools: z.array(z.string()),
260
+ hooks: z.object({
261
+ PostToolUse: z.array(z.unknown()).optional()
262
+ }).partial().optional(),
263
+ env: z.record(z.string(), z.string()).default({})
264
+ });
265
+ var initModeSchema = z.enum(["internal", "client", "library"]);
266
+
267
+ // src/lib/user-config-store.ts
268
+ var AVATAR_HOME = join5(homedir(), ".avatar");
269
+ var USER_CONFIG_PATH = join5(AVATAR_HOME, "config.json");
270
+ var USER_STATE_PATH = join5(AVATAR_HOME, "state.json");
271
+ var AUDIT_LOG_PATH = join5(AVATAR_HOME, "audit.log");
272
+ var BACKUPS_DIR = join5(AVATAR_HOME, "backups");
273
+ var SECRET_FILE_MODE = 384;
274
+ async function ensureAvatarHome() {
275
+ await ensureDir(AVATAR_HOME);
276
+ }
277
+ async function readUserConfig() {
278
+ if (!await pathExists(USER_CONFIG_PATH)) return null;
279
+ const raw = await readJson(USER_CONFIG_PATH);
280
+ const parsed = userConfigSchema.safeParse(raw);
281
+ if (!parsed.success) return null;
282
+ return parsed.data;
283
+ }
284
+ async function writeUserConfig(config) {
285
+ await ensureAvatarHome();
286
+ await writeJsonAtomic(USER_CONFIG_PATH, config, SECRET_FILE_MODE);
287
+ }
288
+ async function clearUserConfig() {
289
+ if (await pathExists(USER_CONFIG_PATH)) {
290
+ const { promises: fs7 } = await import("fs");
291
+ await fs7.unlink(USER_CONFIG_PATH);
292
+ }
293
+ }
294
+ function isTokenExpired(config) {
295
+ const expiresAt = Date.parse(config.expires_at);
296
+ return Number.isNaN(expiresAt) || expiresAt - Date.now() < 6e4;
297
+ }
298
+
299
+ // src/commands/doctor.ts
300
+ function registerDoctorCommand(program2) {
301
+ 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) => {
302
+ try {
303
+ const checks = await runChecks(process.cwd());
304
+ renderChecks(checks);
305
+ if (opts.fix) await applyFixes(checks);
306
+ } catch (err) {
307
+ log.error(err instanceof Error ? err.message : String(err));
308
+ process.exit(1);
309
+ }
310
+ });
311
+ }
312
+ async function runChecks(cwd) {
313
+ const checks = [];
314
+ const nodeVer = process.versions.node;
315
+ const [major, minor] = nodeVer.split(".").map((n) => Number.parseInt(n, 10));
316
+ const nodeOk = (major ?? 0) > 18 || (major ?? 0) === 18 && (minor ?? 0) >= 17;
317
+ checks.push({
318
+ name: "Node.js version",
319
+ status: nodeOk ? "ok" : "fail",
320
+ detail: `v${nodeVer}${nodeOk ? "" : " (c\u1EA7n >= 18.17)"}`,
321
+ fixable: false
322
+ });
323
+ const config = await readUserConfig();
324
+ if (!config) {
325
+ checks.push({
326
+ name: "Login status",
327
+ status: "fail",
328
+ detail: "Ch\u01B0a \u0111\u0103ng nh\u1EADp \u2014 ch\u1EA1y 'avatar login'",
329
+ fixable: false
330
+ });
331
+ } else if (isTokenExpired(config)) {
332
+ checks.push({
333
+ name: "Login status",
334
+ status: "warn",
335
+ detail: `Token h\u1EBFt h\u1EA1n (${config.email}) \u2014 ch\u1EA1y 'avatar login'`,
336
+ fixable: false
337
+ });
338
+ } else {
339
+ checks.push({
340
+ name: "Login status",
341
+ status: "ok",
342
+ detail: `Logged in: ${config.email}`,
343
+ fixable: false
344
+ });
345
+ }
346
+ const gitRepo = await isGitRepo(cwd);
347
+ checks.push({
348
+ name: "Git repository",
349
+ status: gitRepo ? "ok" : "warn",
350
+ detail: gitRepo ? cwd : "Kh\xF4ng ph\u1EA3i git repo (c\u1EA7n cho 'avatar init')",
351
+ fixable: false
352
+ });
353
+ const packPath = join6(cwd, ".claude", "pack");
354
+ const hasPack = await pathExists(packPath);
355
+ checks.push({
356
+ name: "team-ai-pack submodule",
357
+ status: hasPack ? "ok" : "warn",
358
+ detail: hasPack ? packPath : "Avatar ch\u01B0a init \u2014 ch\u1EA1y 'avatar init'",
359
+ fixable: false
360
+ });
361
+ const claudeMdPath = join6(cwd, "CLAUDE.md");
362
+ const hasClaudeMd = await pathExists(claudeMdPath);
363
+ checks.push({
364
+ name: "CLAUDE.md",
365
+ status: hasClaudeMd ? "ok" : "warn",
366
+ detail: hasClaudeMd ? "t\u1ED3n t\u1EA1i \u1EDF project root" : "thi\u1EBFu \u2014 ch\u1EA1y 'avatar init'",
367
+ fixable: false
368
+ });
369
+ const hookPath = join6(cwd, ".git", "hooks", "post-merge");
370
+ const hasHook = await pathExists(hookPath);
371
+ if (gitRepo && hasPack) {
372
+ checks.push({
373
+ name: "Git hook post-merge",
374
+ status: hasHook ? "ok" : "fail",
375
+ detail: hasHook ? "installed" : "missing \u2014 fixable",
376
+ fixable: !hasHook,
377
+ fix: hasHook ? void 0 : async () => {
378
+ await installGitHook(join6(cwd, ".git"), "post-merge");
379
+ }
380
+ });
381
+ }
382
+ const gitignorePath = join6(cwd, ".gitignore");
383
+ if (gitRepo) {
384
+ let gitignoreOk = false;
385
+ if (await pathExists(gitignorePath)) {
386
+ const content = await fs3.readFile(gitignorePath, "utf8");
387
+ gitignoreOk = content.includes(".claude/_pending/");
388
+ }
389
+ checks.push({
390
+ name: ".gitignore Avatar entries",
391
+ status: gitignoreOk ? "ok" : hasPack ? "fail" : "warn",
392
+ detail: gitignoreOk ? "c\xF3 .claude/_pending/, .claude/_backup/" : "thi\u1EBFu entries",
393
+ fixable: false
394
+ });
395
+ }
396
+ const which = spawnSync("which", ["claude"]);
397
+ const hasClaudeCli = which.status === 0;
398
+ checks.push({
399
+ name: "Claude Code CLI",
400
+ status: hasClaudeCli ? "ok" : "warn",
401
+ detail: hasClaudeCli ? which.stdout.toString().trim() : "kh\xF4ng t\xECm th\u1EA5y 'claude' tr\xEAn PATH",
402
+ fixable: false
403
+ });
404
+ return checks;
405
+ }
406
+ function renderChecks(checks) {
407
+ const lines = [chalk.bold("Avatar Doctor"), "\u2500".repeat(48)];
408
+ let passed = 0;
409
+ let issues = 0;
410
+ let fixable = 0;
411
+ for (const c of checks) {
412
+ const icon = c.status === "ok" ? chalk.green("\u2713") : c.status === "warn" ? chalk.yellow("\u26A0") : chalk.red("\u2717");
413
+ lines.push(`${icon} ${c.name.padEnd(28)} ${chalk.dim(c.detail)}`);
414
+ if (c.status === "ok") passed += 1;
415
+ else {
416
+ issues += 1;
417
+ if (c.fixable) fixable += 1;
418
+ }
419
+ }
420
+ lines.push("\u2500".repeat(48));
421
+ lines.push(
422
+ `${passed} checks passed, ${issues} issue${issues === 1 ? "" : "s"}${fixable > 0 ? ` (${fixable} fixable \u2014 ch\u1EA1y 'avatar doctor --fix')` : ""}`
423
+ );
424
+ process.stdout.write(`${boxen(lines.join("\n"), { padding: 1, borderStyle: "round" })}
425
+ `);
426
+ }
427
+ async function applyFixes(checks) {
428
+ let count = 0;
429
+ for (const c of checks) {
430
+ if (c.fixable && c.fix) {
431
+ try {
432
+ await c.fix();
433
+ log.success(`Fixed: ${c.name}`);
434
+ count += 1;
435
+ } catch (err) {
436
+ log.error(`Failed to fix ${c.name}: ${err instanceof Error ? err.message : String(err)}`);
437
+ }
438
+ }
439
+ }
440
+ if (count === 0) log.dim("Kh\xF4ng c\xF3 g\xEC \u0111\u1EC3 fix t\u1EF1 \u0111\u1ED9ng.");
441
+ }
442
+
443
+ // src/commands/init.ts
444
+ import { join as join8, resolve } from "path";
445
+ import { confirm, input, select } from "@inquirer/prompts";
446
+ import boxen2 from "boxen";
447
+
448
+ // src/lib/audit-log-appender.ts
449
+ import { promises as fs4 } from "fs";
450
+ async function appendAuditEntry(action, detail) {
451
+ await ensureAvatarHome();
452
+ const entry = {
453
+ timestamp: (/* @__PURE__ */ new Date()).toISOString(),
454
+ action,
455
+ ...detail ? { detail } : {}
456
+ };
457
+ const line = `${JSON.stringify(entry)}
458
+ `;
459
+ await fs4.appendFile(AUDIT_LOG_PATH, line, "utf8");
460
+ }
461
+
462
+ // src/lib/team-pack-submodule-manager.ts
463
+ import { join as join7 } from "path";
464
+ var TEAM_PACK_REPO_URL = "https://github.com/LukeNALS/team-ai-pack.git";
465
+ var TEAM_PACK_RELATIVE_PATH = ".claude/pack";
466
+ async function addTeamPackSubmodule(projectRoot, tag) {
467
+ await addSubmodule(TEAM_PACK_REPO_URL, TEAM_PACK_RELATIVE_PATH, projectRoot);
468
+ let target = tag ?? null;
469
+ if (!target) {
470
+ target = await latestTag(join7(projectRoot, TEAM_PACK_RELATIVE_PATH));
471
+ }
472
+ if (target) {
473
+ await checkoutTagInSubmodule(TEAM_PACK_RELATIVE_PATH, target, projectRoot);
474
+ }
475
+ return { pinnedTag: target };
476
+ }
477
+ async function readPinnedPackVersion(projectRoot) {
478
+ const submoduleRoot = join7(projectRoot, TEAM_PACK_RELATIVE_PATH);
479
+ const tag = await latestTag(submoduleRoot);
480
+ if (tag) return tag;
481
+ const sha = await currentCommitSha(submoduleRoot);
482
+ return sha.slice(0, 7);
483
+ }
484
+
485
+ // src/commands/init.ts
486
+ var AVATAR_CLI_VERSION = "1.0.0";
487
+ function registerInitCommand(program2) {
488
+ program2.command("init").description("Kh\u1EDFi t\u1EA1o Avatar trong d\u1EF1 \xE1n (3 mode: internal/client/library)").option("--mode <mode>", "internal | client | library").option("--skip-scan", "B\u1ECF qua b\u01B0\u1EDBc project-scanner").option("--pack-version <tag>", "Pin team-ai-pack v\xE0o version c\u1EE5 th\u1EC3").option("--client-repo <url>", "URL git c\u1EE7a client repo (mode=client)").option("--workspace-name <name>", "T\xEAn workspace (mode=client)").option("--workspace-parent <path>", "Th\u01B0 m\u1EE5c cha t\u1EA1o workspace (mode=client)").action(async (opts) => {
489
+ try {
490
+ await runInit(opts);
491
+ } catch (err) {
492
+ log.error(err instanceof Error ? err.message : String(err));
493
+ process.exit(1);
494
+ }
495
+ });
496
+ }
497
+ async function runInit(opts) {
498
+ const userConfig = await readUserConfig();
499
+ if (!userConfig || isTokenExpired(userConfig)) {
500
+ log.error("Ch\u01B0a \u0111\u0103ng nh\u1EADp ho\u1EB7c token \u0111\xE3 h\u1EBFt h\u1EA1n. Ch\u1EA1y 'avatar login' tr\u01B0\u1EDBc.");
501
+ process.exit(1);
502
+ }
503
+ const mode = opts.mode ?? await promptMode();
504
+ if (mode === "internal") {
505
+ await runInitInternal(opts, userConfig.email);
506
+ } else {
507
+ await runInitClientOrLibrary(opts, mode, userConfig.email);
508
+ }
509
+ }
510
+ async function promptMode() {
511
+ return await select({
512
+ message: "\u0110\xE2y l\xE0 lo\u1EA1i d\u1EF1 \xE1n g\xEC?",
513
+ choices: [
514
+ {
515
+ name: "N\u1ED9i b\u1ED9 NAL (Avatar files commit c\xF9ng code)",
516
+ value: "internal"
517
+ },
518
+ { name: "Client (Pattern A \u2014 t\xE1ch workspace)", value: "client" },
519
+ { name: "Library/SDK public (t\xE1ch workspace)", value: "library" }
520
+ ]
521
+ });
522
+ }
523
+ async function runInitInternal(opts, ownerEmail) {
524
+ const projectRoot = process.cwd();
525
+ if (!await isGitRepo(projectRoot)) {
526
+ throw new Error("Mode internal c\u1EA7n d\u1EF1 \xE1n \u0111\xE3 l\xE0 git repo. Ch\u1EA1y 'git init' tr\u01B0\u1EDBc r\u1ED3i th\u1EED l\u1EA1i.");
527
+ }
528
+ if (await pathExists(join8(projectRoot, ".claude"))) {
529
+ throw new Error(
530
+ ".claude/ \u0111\xE3 t\u1ED3n t\u1EA1i. Avatar kh\xF4ng override. X\xF3a th\u1EE7 c\xF4ng ho\u1EB7c d\xF9ng mode kh\xE1c."
531
+ );
532
+ }
533
+ const teamOwner = await promptTeamOwner(ownerEmail);
534
+ const projectName = projectNameOf(projectRoot);
535
+ const projectDescription = await input({
536
+ message: "M\xF4 t\u1EA3 ng\u1EAFn 1 d\xF2ng c\u1EE7a d\u1EF1 \xE1n:",
537
+ default: `Avatar-managed project: ${projectName}`
538
+ });
539
+ const sp = spinner(`Th\xEAm submodule team-ai-pack t\u1EEB ${TEAM_PACK_REPO_URL}...`);
540
+ let pinnedTag = null;
541
+ try {
542
+ const result = await addTeamPackSubmodule(projectRoot, opts.packVersion);
543
+ pinnedTag = result.pinnedTag;
544
+ sp.succeed(`\u0110\xE3 pin team-ai-pack v\xE0o ${pinnedTag ?? "HEAD"}`);
545
+ } catch (err) {
546
+ sp.fail("Add submodule th\u1EA5t b\u1EA1i");
547
+ throw err;
548
+ }
549
+ const vars = buildScaffoldVariables({
550
+ projectName,
551
+ projectDescription,
552
+ teamOwner,
553
+ packVersion: pinnedTag ?? "HEAD",
554
+ mode: "internal"
555
+ });
556
+ await createClaudeDirTree(projectRoot);
557
+ await writeProjectKnowledgeFiles(projectRoot, vars);
558
+ await writeRootClaudeMd(projectRoot, vars);
559
+ await writeProjectSettings(projectRoot, vars);
560
+ await appendGitignoreEntries(projectRoot);
561
+ await installGitHook(join8(projectRoot, ".git"), "post-merge");
562
+ log.success("C\xE0i git hook post-merge");
563
+ await appendAuditEntry("init", `mode=internal,project=${projectName}`);
564
+ await maybeCommit(projectRoot, "internal");
565
+ printInitSuccessBox(projectRoot, "internal");
566
+ }
567
+ async function runInitClientOrLibrary(opts, mode, ownerEmail) {
568
+ const teamOwner = await promptTeamOwner(ownerEmail);
569
+ const clientRepoUrl = opts.clientRepo ?? await input({
570
+ message: "URL git c\u1EE7a client repo:",
571
+ validate: (v) => v.length > 0 ? true : "URL b\u1EAFt bu\u1ED9c"
572
+ });
573
+ const inferredName = inferWorkspaceName(clientRepoUrl);
574
+ const workspaceName = opts.workspaceName ?? await input({
575
+ message: "T\xEAn workspace:",
576
+ default: inferredName
577
+ });
578
+ const workspaceParent = resolve(opts.workspaceParent ?? "..");
579
+ const workspacePath = join8(workspaceParent, workspaceName);
580
+ if (await pathExists(workspacePath)) {
581
+ throw new Error(`Workspace path \u0111\xE3 t\u1ED3n t\u1EA1i: ${workspacePath}`);
582
+ }
583
+ await ensureDir(workspacePath);
584
+ await git(workspacePath).init();
585
+ const sp = spinner("Add submodule client repo + team-ai-pack...");
586
+ try {
587
+ await git(workspacePath).subModule(["add", clientRepoUrl, "src"]);
588
+ const result = await addTeamPackSubmodule(workspacePath, opts.packVersion);
589
+ sp.succeed(`Workspace pin team-ai-pack v\xE0o ${result.pinnedTag ?? "HEAD"}`);
590
+ const vars = buildScaffoldVariables({
591
+ projectName: workspaceName,
592
+ projectDescription: `Avatar ${mode} workspace for ${clientRepoUrl}`,
593
+ teamOwner,
594
+ packVersion: result.pinnedTag ?? "HEAD",
595
+ mode
596
+ });
597
+ await createClaudeDirTree(workspacePath);
598
+ await writeProjectKnowledgeFiles(workspacePath, vars);
599
+ await writeRootClaudeMd(workspacePath, vars);
600
+ await writeProjectSettings(workspacePath, vars);
601
+ await appendGitignoreEntries(workspacePath);
602
+ await ensureDir(join8(workspacePath, "notes"));
603
+ await ensureDir(join8(workspacePath, "scripts"));
604
+ await installGitHook(join8(workspacePath, ".git"), "post-merge");
605
+ await installGitHook(join8(workspacePath, "src", ".git"), "pre-push");
606
+ log.success("C\xE0i post-merge (workspace) + pre-push (src/)");
607
+ await appendAuditEntry("init", `mode=${mode},workspace=${workspaceName}`);
608
+ await maybeCommitWorkspace(workspacePath);
609
+ printInitSuccessBox(workspacePath, mode);
610
+ } catch (err) {
611
+ sp.fail("Init workspace th\u1EA5t b\u1EA1i");
612
+ throw err;
613
+ }
614
+ }
615
+ function projectNameOf(projectRoot) {
616
+ return projectRoot.split("/").filter(Boolean).pop() ?? "avatar-project";
617
+ }
618
+ function inferWorkspaceName(repoUrl) {
619
+ const m = repoUrl.match(/[/:]([^/]+?)(\.git)?$/);
620
+ const base = m?.[1] ?? "client";
621
+ return `avatar-${base}-workspace`;
622
+ }
623
+ async function promptTeamOwner(currentUserEmail) {
624
+ return await input({
625
+ message: "Team owner email:",
626
+ default: currentUserEmail
627
+ });
628
+ }
629
+ function buildScaffoldVariables(args) {
630
+ return {
631
+ projectName: args.projectName,
632
+ projectDescription: args.projectDescription,
633
+ teamOwner: args.teamOwner,
634
+ avatarVersion: AVATAR_CLI_VERSION,
635
+ packVersion: args.packVersion,
636
+ lastScan: (/* @__PURE__ */ new Date()).toISOString(),
637
+ mode: args.mode
638
+ };
639
+ }
640
+ async function maybeCommit(projectRoot, mode) {
641
+ const wantCommit = await confirm({
642
+ message: "Commit ngay c\xE1c file Avatar \u0111\xE3 t\u1EA1o?",
643
+ default: true
644
+ });
645
+ if (!wantCommit) return;
646
+ const g = git(projectRoot);
647
+ await g.add([".claude/", "CLAUDE.md", ".gitignore", ".gitmodules"]);
648
+ await g.commit(`chore: initialize Avatar in ${mode} mode`);
649
+ log.success("\u0110\xE3 commit");
650
+ }
651
+ async function maybeCommitWorkspace(workspacePath) {
652
+ const wantCommit = await confirm({
653
+ message: "Commit workspace ngay?",
654
+ default: true
655
+ });
656
+ if (!wantCommit) return;
657
+ const g = git(workspacePath);
658
+ await g.add(["CLAUDE.md", ".claude/", ".gitignore", ".gitmodules", "notes/", "scripts/"]);
659
+ await g.commit("chore: initialize Avatar workspace for client");
660
+ log.success("\u0110\xE3 commit workspace");
661
+ }
662
+ function printInitSuccessBox(rootPath, mode) {
663
+ const lines = [];
664
+ if (mode === "internal") {
665
+ lines.push(`${chalk.green("\u2713")} Avatar \u0111\xE3 s\u1EB5n s\xE0ng trong d\u1EF1 \xE1n`);
666
+ lines.push("");
667
+ lines.push(` ${chalk.cyan("claude")} M\u1EDF Claude Code`);
668
+ lines.push(` ${chalk.cyan("avatar status")} Xem snapshot`);
669
+ lines.push(` ${chalk.cyan("avatar sync")} Pull team-ai-pack m\u1EDBi`);
670
+ } else {
671
+ lines.push(`${chalk.green("\u2713")} Workspace s\u1EB5n s\xE0ng: ${rootPath}`);
672
+ lines.push("");
673
+ lines.push(` ${chalk.cyan(`cd ${rootPath}`)}`);
674
+ lines.push(` ${chalk.cyan("claude")} M\u1EDF Claude Code \u1EDF workspace root`);
675
+ lines.push("");
676
+ lines.push(` ${chalk.cyan("avatar commit --src")} Commit code kh\xE1ch l\xEAn client remote`);
677
+ lines.push(
678
+ ` ${chalk.cyan("avatar commit --avatar")} Commit Avatar state l\xEAn workspace remote`
679
+ );
680
+ lines.push(` ${chalk.cyan("avatar sync")} Sync team pack`);
681
+ }
682
+ process.stdout.write(`${boxen2(lines.join("\n"), { padding: 1, borderStyle: "round" })}
683
+ `);
684
+ }
685
+
686
+ // src/commands/login.ts
687
+ import boxen3 from "boxen";
688
+ import open from "open";
689
+
690
+ // src/lib/google-oauth-device-flow.ts
691
+ var GOOGLE_CLIENT_ID = "1014766441755-i4jimivh5rd7vt8phuhmepmt45sovtph.apps.googleusercontent.com";
692
+ var GOOGLE_CLIENT_SECRET = "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";
693
+ var HOSTED_DOMAIN = "nal.vn";
694
+ var SCOPES = ["openid", "email", "profile"];
695
+ var DEVICE_CODE_URL = "https://oauth2.googleapis.com/device/code";
696
+ var TOKEN_URL = "https://oauth2.googleapis.com/token";
697
+ var REVOKE_URL = "https://oauth2.googleapis.com/revoke";
698
+ async function requestDeviceCode() {
699
+ const body = new URLSearchParams({
700
+ client_id: GOOGLE_CLIENT_ID,
701
+ scope: SCOPES.join(" ")
702
+ });
703
+ const res = await fetch(DEVICE_CODE_URL, {
704
+ method: "POST",
705
+ headers: { "Content-Type": "application/x-www-form-urlencoded" },
706
+ body
707
+ });
708
+ if (!res.ok) {
709
+ const text = await res.text();
710
+ throw new Error(`Device code request failed (${res.status}): ${text}`);
711
+ }
712
+ return await res.json();
713
+ }
714
+ async function pollForToken(deviceCode) {
715
+ const body = new URLSearchParams({
716
+ client_id: GOOGLE_CLIENT_ID,
717
+ client_secret: GOOGLE_CLIENT_SECRET,
718
+ device_code: deviceCode,
719
+ grant_type: "urn:ietf:params:oauth:grant-type:device_code"
720
+ });
721
+ const res = await fetch(TOKEN_URL, {
722
+ method: "POST",
723
+ headers: { "Content-Type": "application/x-www-form-urlencoded" },
724
+ body
725
+ });
726
+ if (res.ok) {
727
+ return await res.json();
728
+ }
729
+ let errorCode = "";
730
+ try {
731
+ const data = await res.json();
732
+ errorCode = data.error ?? "";
733
+ } catch {
734
+ errorCode = "";
735
+ }
736
+ if (errorCode === "authorization_pending" || errorCode === "slow_down") {
737
+ return null;
738
+ }
739
+ if (errorCode === "access_denied") {
740
+ throw new Error("User t\u1EEB ch\u1ED1i quy\u1EC1n truy c\u1EADp");
741
+ }
742
+ if (errorCode === "expired_token") {
743
+ throw new Error("H\u1EBFt h\u1EA1n ch\u1EDD (5 ph\xFAt). Ch\u1EA1y l\u1EA1i 'avatar login'.");
744
+ }
745
+ throw new Error(`OAuth token endpoint tr\u1EA3 l\u1ED7i: ${errorCode || res.status}`);
746
+ }
747
+ function decodeIdToken(idToken) {
748
+ const parts = idToken.split(".");
749
+ if (parts.length !== 3) {
750
+ throw new Error("id_token format kh\xF4ng h\u1EE3p l\u1EC7");
751
+ }
752
+ const payload = parts[1];
753
+ if (!payload) throw new Error("id_token thi\u1EBFu payload");
754
+ const base64 = payload.replace(/-/g, "+").replace(/_/g, "/");
755
+ const json = Buffer.from(base64, "base64").toString("utf8");
756
+ return JSON.parse(json);
757
+ }
758
+ function verifyHostedDomain(claims) {
759
+ if (claims.hd !== HOSTED_DOMAIN) {
760
+ throw new Error(
761
+ `Email kh\xF4ng thu\u1ED9c workspace NAL (y\xEAu c\u1EA7u @${HOSTED_DOMAIN}). Nh\u1EADn: ${claims.email}`
762
+ );
763
+ }
764
+ if (!claims.email_verified) {
765
+ throw new Error("Email ch\u01B0a \u0111\u01B0\u1EE3c Google verify");
766
+ }
767
+ }
768
+ function buildUserConfig(token, claims) {
769
+ const expiresAt = new Date(Date.now() + token.expires_in * 1e3).toISOString();
770
+ return {
771
+ email: claims.email,
772
+ name: claims.name ?? claims.email,
773
+ access_token: token.access_token,
774
+ refresh_token: token.refresh_token,
775
+ expires_at: expiresAt,
776
+ id_token: token.id_token
777
+ };
778
+ }
779
+ async function revokeToken(token) {
780
+ const body = new URLSearchParams({ token });
781
+ await fetch(REVOKE_URL, {
782
+ method: "POST",
783
+ headers: { "Content-Type": "application/x-www-form-urlencoded" },
784
+ body
785
+ }).catch(() => {
786
+ });
787
+ }
788
+ function buildVerificationUrl(response) {
789
+ const url = new URL(response.verification_url);
790
+ url.searchParams.set("user_code", response.user_code);
791
+ url.searchParams.set("hd", HOSTED_DOMAIN);
792
+ return url.toString();
793
+ }
794
+
795
+ // src/commands/login.ts
796
+ function registerLoginCommand(program2) {
797
+ program2.command("login").description("\u0110\u0103ng nh\u1EADp Google SSO (workspace @nal.vn)").option("--reset", "X\xF3a credential c\u0169 v\xE0 \u0111\u0103ng nh\u1EADp l\u1EA1i").action(async (opts) => {
798
+ try {
799
+ await runLogin(opts);
800
+ } catch (err) {
801
+ log.error(err instanceof Error ? err.message : String(err));
802
+ process.exit(1);
803
+ }
804
+ });
805
+ }
806
+ async function runLogin(opts) {
807
+ if (opts.reset) {
808
+ await clearUserConfig();
809
+ await appendAuditEntry("login_reset");
810
+ } else {
811
+ const existing = await readUserConfig();
812
+ if (existing && !isTokenExpired(existing)) {
813
+ log.success(`\u0110\xE3 \u0111\u0103ng nh\u1EADp: ${existing.email}`);
814
+ return;
815
+ }
816
+ }
817
+ const deviceSpinner = spinner("\u0110ang y\xEAu c\u1EA7u device code t\u1EEB Google...");
818
+ let deviceCode;
819
+ try {
820
+ deviceCode = await requestDeviceCode();
821
+ deviceSpinner.succeed("Nh\u1EADn device code");
822
+ } catch (err) {
823
+ deviceSpinner.fail("Kh\xF4ng k\u1EBFt n\u1ED1i \u0111\u01B0\u1EE3c Google");
824
+ throw err;
825
+ }
826
+ const verificationUrl = buildVerificationUrl(deviceCode);
827
+ const instructions = [
828
+ `1. Truy c\u1EADp: ${chalk.cyan(deviceCode.verification_url)}`,
829
+ `2. Nh\u1EADp code: ${chalk.bold.yellow(deviceCode.user_code)}`,
830
+ "",
831
+ `Ho\u1EB7c Avatar t\u1EF1 m\u1EDF browser, click ${chalk.green("Allow")}...`
832
+ ].join("\n");
833
+ process.stdout.write(`${boxen3(instructions, { padding: 1, borderStyle: "round" })}
834
+ `);
835
+ void open(verificationUrl).catch(() => {
836
+ log.dim("(Kh\xF4ng m\u1EDF \u0111\u01B0\u1EE3c browser t\u1EF1 \u0111\u1ED9ng \u2014 copy URL \u1EDF tr\xEAn)");
837
+ });
838
+ const waitSpinner = spinner("\u0110ang ch\u1EDD x\xE1c nh\u1EADn trong browser...");
839
+ const intervalMs = deviceCode.interval * 1e3;
840
+ const deadline = Date.now() + deviceCode.expires_in * 1e3;
841
+ let token = null;
842
+ while (Date.now() < deadline) {
843
+ await sleep(intervalMs);
844
+ try {
845
+ token = await pollForToken(deviceCode.device_code);
846
+ if (token) break;
847
+ } catch (err) {
848
+ waitSpinner.fail("X\xE1c th\u1EF1c th\u1EA5t b\u1EA1i");
849
+ throw err;
850
+ }
851
+ }
852
+ if (!token) {
853
+ waitSpinner.fail("H\u1EBFt h\u1EA1n ch\u1EDD (5 ph\xFAt). Ch\u1EA1y l\u1EA1i 'avatar login'.");
854
+ process.exit(1);
855
+ }
856
+ waitSpinner.succeed("\u0110\xE3 nh\u1EADn token t\u1EEB Google");
857
+ const claims = decodeIdToken(token.id_token);
858
+ try {
859
+ verifyHostedDomain(claims);
860
+ } catch (err) {
861
+ await revokeToken(token.access_token);
862
+ throw err;
863
+ }
864
+ const userConfig = buildUserConfig(token, claims);
865
+ await writeUserConfig(userConfig);
866
+ await appendAuditEntry("login", userConfig.email);
867
+ log.success(`X\xE1c th\u1EF1c th\xE0nh c\xF4ng: ${userConfig.email}`);
868
+ log.success(`Verify hosted domain: ${claims.hd} \u2713`);
869
+ log.success(`L\u01B0u credential v\xE0o ${USER_CONFIG_PATH} (chmod 600)`);
870
+ }
871
+ function sleep(ms) {
872
+ return new Promise((resolve2) => setTimeout(resolve2, ms));
873
+ }
874
+
875
+ // src/commands/mcp-run.ts
876
+ function registerMcpRunCommand(program2) {
877
+ program2.command("mcp-run <tool-id>", { hidden: true }).description("[internal] Spawn MCP v\u1EDBi secrets injected (M09)").action(notImplementedYet("mcp-run", "Milestone 09"));
878
+ }
879
+
880
+ // src/commands/restore.ts
881
+ function registerRestoreCommand(program2) {
882
+ program2.command("restore").description("Kh\xF4i ph\u1EE5c .claude/pack/ t\u1EEB backup (M08)").option("--backup <name>", "T\xEAn backup folder trong .claude/_backup/").option("--list", "Li\u1EC7t k\xEA c\xE1c backup hi\u1EC7n c\xF3").action(notImplementedYet("restore", "Milestone 08"));
883
+ }
884
+
885
+ // src/commands/review.ts
886
+ function registerReviewCommand(program2) {
887
+ program2.command("review").description("Review pending proposals t\u1EEB avatar scan (M08)").option("--accept-all", "Approve m\u1ECDi pending kh\xF4ng h\u1ECFi (CI mode)").option("--reject-all", "X\xF3a m\u1ECDi pending kh\xF4ng h\u1ECFi").action(notImplementedYet("review", "Milestone 08"));
888
+ }
889
+
890
+ // src/commands/scan.ts
891
+ function registerScanCommand(program2) {
892
+ program2.command("scan").description("Ch\u1EA1y project scanner v\xE0 \u0111\u1EC1 xu\u1EA5t knowledge update (M06)").option("--incremental", "Ch\u1EC9 scan c\xE1c file thay \u0111\u1ED5i t\u1EEB commit cu\u1ED1i").option("--full", "Scan to\xE0n b\u1ED9 d\u1EF1 \xE1n (default)").option("--scanners <list>", "tech-stack,conventions,architecture,domain,git-pattern").option("--quiet", "Ch\u1EA1y ng\u1EA7m, \xEDt output (d\xF9ng cho git hook)").action(notImplementedYet("scan", "Milestone 06"));
893
+ }
894
+
895
+ // src/commands/secrets.ts
896
+ function registerSecretsCommand(program2) {
897
+ const secrets = program2.command("secrets").description("Qu\u1EA3n l\xFD secrets trong OS keychain (M09)");
898
+ secrets.command("list").description("Li\u1EC7t k\xEA secrets \u0111\xE3 set (ch\u1EC9 t\xEAn, kh\xF4ng value)").action(notImplementedYet("secrets list", "Milestone 09"));
899
+ secrets.command("set <service> <name>").description("Set/update secret (prompt \u1EA9n)").action(notImplementedYet("secrets set", "Milestone 09"));
900
+ secrets.command("get <service> <name>").description("L\u1EA5y secret, copy clipboard, auto-x\xF3a sau 30s").action(notImplementedYet("secrets get", "Milestone 09"));
901
+ secrets.command("rm <service> <name>").description("X\xF3a secret kh\u1ECFi keychain").action(notImplementedYet("secrets rm", "Milestone 09"));
902
+ secrets.command("check").description("Verify m\u1ECDi secret required b\u1EDFi MCP \u0111\xE3 enabled").action(notImplementedYet("secrets check", "Milestone 09"));
903
+ }
904
+
905
+ // src/commands/status.ts
906
+ import { promises as fs6 } from "fs";
907
+ import { join as join10 } from "path";
908
+ import boxen4 from "boxen";
909
+
910
+ // src/lib/pack-backup-manager.ts
911
+ import { promises as fs5 } from "fs";
912
+ import { join as join9 } from "path";
913
+ var BACKUP_DIR_NAME = "_backup";
914
+ async function listBackups(projectRoot) {
915
+ const dir = join9(projectRoot, ".claude", BACKUP_DIR_NAME);
916
+ if (!await pathExists(dir)) return [];
917
+ const entries = await fs5.readdir(dir, { withFileTypes: true });
918
+ return entries.filter((e) => e.isDirectory()).map((e) => e.name).sort().reverse();
919
+ }
920
+
921
+ // src/commands/status.ts
922
+ var AVATAR_CLI_VERSION2 = "1.0.0";
923
+ function registerStatusCommand(program2) {
924
+ program2.command("status").description("Snapshot t\u1EE9c th\xEC: project, pack version, pending, backup").option("--json", "Output JSON cho script").action(async (opts) => {
925
+ try {
926
+ const snapshot = await gatherStatus(process.cwd());
927
+ if (opts.json) {
928
+ process.stdout.write(`${JSON.stringify(snapshot, null, 2)}
929
+ `);
930
+ } else {
931
+ renderStatusBox(snapshot);
932
+ }
933
+ } catch (err) {
934
+ log.error(err instanceof Error ? err.message : String(err));
935
+ process.exit(1);
936
+ }
937
+ });
938
+ }
939
+ async function gatherStatus(cwd) {
940
+ const projectName = cwd.split("/").filter(Boolean).pop() ?? "unknown";
941
+ const claudeRoot = join10(cwd, ".claude");
942
+ const hasAvatar = await pathExists(claudeRoot);
943
+ if (!hasAvatar) {
944
+ return {
945
+ projectName,
946
+ cliVersion: AVATAR_CLI_VERSION2,
947
+ packVersion: null,
948
+ pendingCount: 0,
949
+ backupCount: 0,
950
+ techStackSummary: "(Avatar ch\u01B0a init)",
951
+ hasAvatar: false
952
+ };
953
+ }
954
+ const packVersion = await isGitRepo(join10(claudeRoot, "pack")) ? await readPinnedPackVersion(cwd).catch(() => null) : null;
955
+ const pendingDir = join10(claudeRoot, "_pending");
956
+ const pendingCount = await pathExists(pendingDir) ? (await fs6.readdir(pendingDir)).filter((n) => n.endsWith(".diff.md")).length : 0;
957
+ const backupCount = (await listBackups(cwd)).length;
958
+ const techStackSummary = await readTechStackFirstLine(claudeRoot);
959
+ return {
960
+ projectName,
961
+ cliVersion: AVATAR_CLI_VERSION2,
962
+ packVersion,
963
+ pendingCount,
964
+ backupCount,
965
+ techStackSummary,
966
+ hasAvatar: true
967
+ };
968
+ }
969
+ async function readTechStackFirstLine(claudeRoot) {
970
+ const techStackPath = join10(claudeRoot, "project", "tech-stack.md");
971
+ if (!await pathExists(techStackPath)) return "(no tech-stack.md)";
972
+ const content = await readText(techStackPath);
973
+ const firstNonHeaderLine = content.split("\n").find((l) => l.trim() && !l.startsWith("#") && !l.startsWith(">"));
974
+ return firstNonHeaderLine?.trim() ?? "(empty)";
975
+ }
976
+ function renderStatusBox(s) {
977
+ const lines = [
978
+ `${chalk.bold("Avatar Status")} \xB7 ${chalk.cyan(s.projectName)}`,
979
+ "\u2500".repeat(48),
980
+ `${chalk.dim("CLI version:")} ${s.cliVersion}`,
981
+ `${chalk.dim("Pack version:")} ${s.packVersion ?? chalk.yellow("not installed")}`,
982
+ `${chalk.dim("Pending changes:")} ${s.pendingCount}${s.pendingCount > 0 ? chalk.dim(" (avatar review)") : ""}`,
983
+ `${chalk.dim("Backups:")} ${s.backupCount}`,
984
+ `${chalk.dim("Tech stack:")} ${s.techStackSummary}`
985
+ ];
986
+ process.stdout.write(`${boxen4(lines.join("\n"), { padding: 1, borderStyle: "round" })}
987
+ `);
988
+ }
989
+
990
+ // src/commands/sync.ts
991
+ function registerSyncCommand(program2) {
992
+ program2.command("sync").description("Pull team-ai-pack m\u1EDBi nh\u1EA5t (M08)").option("--force", "Override .claude/pack/, backup tr\u01B0\u1EDBc").option("--version <tag>", "Pin v\xE0o version c\u1EE5 th\u1EC3").option("--dry-run", "Hi\u1EC3n th\u1ECB changes, kh\xF4ng apply").action(notImplementedYet("sync", "Milestone 08"));
993
+ }
994
+
995
+ // src/commands/tools.ts
996
+ function registerToolsCommand(program2) {
997
+ const tools = program2.command("tools").description("Qu\u1EA3n l\xFD system tools + MCP servers (M09)");
998
+ tools.command("list").description("Li\u1EC7t k\xEA tool \u0111\xE3 c\xE0i / c\xF2n thi\u1EBFu").option("--installed", "Ch\u1EC9 li\u1EC7t k\xEA tool \u0111\xE3 c\xE0i").option("--missing", "Ch\u1EC9 li\u1EC7t k\xEA tool c\xF2n thi\u1EBFu").option("--json", "Output JSON cho script").action(notImplementedYet("tools list", "Milestone 09"));
999
+ tools.command("install [tool-ids...]").description("C\xE0i tool v\xE0 \u0111\u0103ng k\xFD v\xE0o ~/.claude.json").option("--all-recommended", "C\xE0i m\u1ECDi MCP \u0111\u01B0\u1EE3c recommend cho project type").option("--verify", "Ch\u1EA1y MCP th\u1EED \u0111\u1EC3 verify (m\u1EA5t ~30s/tool)").option("--no-secrets", "Skip prompt secrets, set sau qua 'avatar secrets'").action(notImplementedYet("tools install", "Milestone 09"));
1000
+ tools.command("remove <tool-id>").description("G\u1EE1 tool kh\u1ECFi ~/.claude.json (optional uninstall binary)").option("--keep-secrets", "Kh\xF4ng x\xF3a secrets kh\u1ECFi keychain").option("--keep-binary", "Kh\xF4ng uninstall npm global binary").action(notImplementedYet("tools remove", "Milestone 09"));
1001
+ }
1002
+
1003
+ // src/index.ts
1004
+ var CLI_VERSION = "1.0.0";
1005
+ var program = new Command();
1006
+ program.name("avatar").description("AI harness CLI for NAL Vietnam engineering").version(CLI_VERSION, "-v, --version", "Hi\u1EC3n th\u1ECB phi\xEAn b\u1EA3n Avatar CLI");
1007
+ registerLoginCommand(program);
1008
+ registerInitCommand(program);
1009
+ registerSyncCommand(program);
1010
+ registerScanCommand(program);
1011
+ registerReviewCommand(program);
1012
+ registerStatusCommand(program);
1013
+ registerDoctorCommand(program);
1014
+ registerRestoreCommand(program);
1015
+ registerCommitCommand(program);
1016
+ registerToolsCommand(program);
1017
+ registerSecretsCommand(program);
1018
+ registerMcpRunCommand(program);
1019
+ program.parseAsync(process.argv).catch((err) => {
1020
+ const msg = err instanceof Error ? err.message : String(err);
1021
+ process.stderr.write(`\u2717 L\u1ED7i kh\xF4ng x\u1EED l\xFD \u0111\u01B0\u1EE3c: ${msg}
1022
+ `);
1023
+ process.exit(1);
1024
+ });
1025
+ //# sourceMappingURL=index.js.map