@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 +1116 -278
- package/dist/index.js.map +1 -1
- package/dist/lib/print-welcome-screen.js +1 -1
- package/dist/lib/print-welcome-screen.js.map +1 -1
- package/package.json +1 -1
package/dist/index.js
CHANGED
|
@@ -3,6 +3,131 @@
|
|
|
3
3
|
// src/index.ts
|
|
4
4
|
import { Command } from "commander";
|
|
5
5
|
|
|
6
|
+
// src/commands/ai.ts
|
|
7
|
+
import { spawnSync as spawnSync4 } from "child_process";
|
|
8
|
+
import { promises as fs4 } from "fs";
|
|
9
|
+
import { join as join5 } from "path";
|
|
10
|
+
import { confirm } from "@inquirer/prompts";
|
|
11
|
+
|
|
12
|
+
// src/lib/filesystem-helpers.ts
|
|
13
|
+
import { constants, promises as fs } from "fs";
|
|
14
|
+
import { dirname, join, relative } from "path";
|
|
15
|
+
async function pathExists(path) {
|
|
16
|
+
try {
|
|
17
|
+
await fs.access(path, constants.F_OK);
|
|
18
|
+
return true;
|
|
19
|
+
} catch {
|
|
20
|
+
return false;
|
|
21
|
+
}
|
|
22
|
+
}
|
|
23
|
+
async function ensureDir(path) {
|
|
24
|
+
await fs.mkdir(path, { recursive: true });
|
|
25
|
+
}
|
|
26
|
+
async function readText(path) {
|
|
27
|
+
return await fs.readFile(path, "utf8");
|
|
28
|
+
}
|
|
29
|
+
async function readJson(path) {
|
|
30
|
+
return JSON.parse(await readText(path));
|
|
31
|
+
}
|
|
32
|
+
async function writeTextAtomic(path, content, mode) {
|
|
33
|
+
await ensureDir(dirname(path));
|
|
34
|
+
const tmp = `${path}.tmp-${process.pid}-${Date.now()}`;
|
|
35
|
+
await fs.writeFile(tmp, content, "utf8");
|
|
36
|
+
if (mode !== void 0) {
|
|
37
|
+
await fs.chmod(tmp, mode);
|
|
38
|
+
}
|
|
39
|
+
await fs.rename(tmp, path);
|
|
40
|
+
}
|
|
41
|
+
async function writeJsonAtomic(path, data, mode) {
|
|
42
|
+
await writeTextAtomic(path, `${JSON.stringify(data, null, 2)}
|
|
43
|
+
`, mode);
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
// src/lib/audit-log-appender.ts
|
|
47
|
+
import { promises as fs2 } from "fs";
|
|
48
|
+
|
|
49
|
+
// src/lib/user-config-store.ts
|
|
50
|
+
import { homedir } from "os";
|
|
51
|
+
import { join as join2 } from "path";
|
|
52
|
+
|
|
53
|
+
// src/types/config-schema.ts
|
|
54
|
+
import { z } from "zod";
|
|
55
|
+
var userConfigSchema = z.object({
|
|
56
|
+
email: z.string().email(),
|
|
57
|
+
name: z.string(),
|
|
58
|
+
access_token: z.string().min(1),
|
|
59
|
+
refresh_token: z.string().min(1),
|
|
60
|
+
expires_at: z.string().datetime(),
|
|
61
|
+
id_token: z.string().min(1)
|
|
62
|
+
});
|
|
63
|
+
var userStateSchema = z.object({
|
|
64
|
+
installed_tools: z.record(
|
|
65
|
+
z.string(),
|
|
66
|
+
z.object({
|
|
67
|
+
version: z.string().optional(),
|
|
68
|
+
installed_at: z.string().datetime(),
|
|
69
|
+
install_method: z.string()
|
|
70
|
+
})
|
|
71
|
+
).default({}),
|
|
72
|
+
tool_inputs: z.record(z.string(), z.unknown()).default({})
|
|
73
|
+
});
|
|
74
|
+
var projectSettingsSchema = z.object({
|
|
75
|
+
allowedTools: z.array(z.string()),
|
|
76
|
+
hooks: z.object({
|
|
77
|
+
PostToolUse: z.array(z.unknown()).optional()
|
|
78
|
+
}).partial().optional(),
|
|
79
|
+
env: z.record(z.string(), z.string()).default({})
|
|
80
|
+
});
|
|
81
|
+
var initModeSchema = z.enum(["internal", "client", "library"]);
|
|
82
|
+
|
|
83
|
+
// src/lib/user-config-store.ts
|
|
84
|
+
var AVATAR_HOME = join2(homedir(), ".avatar");
|
|
85
|
+
var USER_CONFIG_PATH = join2(AVATAR_HOME, "config.json");
|
|
86
|
+
var USER_STATE_PATH = join2(AVATAR_HOME, "state.json");
|
|
87
|
+
var AUDIT_LOG_PATH = join2(AVATAR_HOME, "audit.log");
|
|
88
|
+
var BACKUPS_DIR = join2(AVATAR_HOME, "backups");
|
|
89
|
+
var SECRET_FILE_MODE = 384;
|
|
90
|
+
async function ensureAvatarHome() {
|
|
91
|
+
await ensureDir(AVATAR_HOME);
|
|
92
|
+
}
|
|
93
|
+
async function readUserConfig() {
|
|
94
|
+
if (!await pathExists(USER_CONFIG_PATH)) return null;
|
|
95
|
+
const raw = await readJson(USER_CONFIG_PATH);
|
|
96
|
+
const parsed = userConfigSchema.safeParse(raw);
|
|
97
|
+
if (!parsed.success) return null;
|
|
98
|
+
return parsed.data;
|
|
99
|
+
}
|
|
100
|
+
async function writeUserConfig(config) {
|
|
101
|
+
await ensureAvatarHome();
|
|
102
|
+
await writeJsonAtomic(USER_CONFIG_PATH, config, SECRET_FILE_MODE);
|
|
103
|
+
}
|
|
104
|
+
async function clearUserConfig() {
|
|
105
|
+
if (await pathExists(USER_CONFIG_PATH)) {
|
|
106
|
+
const { promises: fs9 } = await import("fs");
|
|
107
|
+
await fs9.unlink(USER_CONFIG_PATH);
|
|
108
|
+
}
|
|
109
|
+
}
|
|
110
|
+
function isTokenExpired(config) {
|
|
111
|
+
const expiresAt = Date.parse(config.expires_at);
|
|
112
|
+
return Number.isNaN(expiresAt) || expiresAt - Date.now() < 6e4;
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
// src/lib/audit-log-appender.ts
|
|
116
|
+
async function appendAuditEntry(action, detail) {
|
|
117
|
+
await ensureAvatarHome();
|
|
118
|
+
const entry = {
|
|
119
|
+
timestamp: (/* @__PURE__ */ new Date()).toISOString(),
|
|
120
|
+
action,
|
|
121
|
+
...detail ? { detail } : {}
|
|
122
|
+
};
|
|
123
|
+
const line = `${JSON.stringify(entry)}
|
|
124
|
+
`;
|
|
125
|
+
await fs2.appendFile(AUDIT_LOG_PATH, line, "utf8");
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
// src/lib/check-claude-code-subscription-and-quota.ts
|
|
129
|
+
import { spawnSync } from "child_process";
|
|
130
|
+
|
|
6
131
|
// src/lib/terminal-logger.ts
|
|
7
132
|
import chalk from "chalk";
|
|
8
133
|
import ora from "ora";
|
|
@@ -28,6 +153,574 @@ function spinner(text) {
|
|
|
28
153
|
}).start();
|
|
29
154
|
}
|
|
30
155
|
|
|
156
|
+
// src/lib/check-claude-code-subscription-and-quota.ts
|
|
157
|
+
var QUOTA_VERIFY_TIMEOUT_MS = 3e4;
|
|
158
|
+
var QUOTA_VERIFY_PROMPT = "ok";
|
|
159
|
+
function checkClaudeCodeSubscriptionAuth() {
|
|
160
|
+
const result = spawnSync("claude", ["auth", "status"], { stdio: "ignore" });
|
|
161
|
+
if (result.error || result.status !== 0) return "not-authenticated";
|
|
162
|
+
return "authenticated";
|
|
163
|
+
}
|
|
164
|
+
function triggerClaudeCodeAuthLogin() {
|
|
165
|
+
log.info("Kh\u1EDFi \u0111\u1ED9ng \u0111\u0103ng nh\u1EADp Claude Code (browser s\u1EBD m\u1EDF)...");
|
|
166
|
+
const result = spawnSync("claude", ["auth", "login"], { stdio: "inherit" });
|
|
167
|
+
if (result.status !== 0) {
|
|
168
|
+
throw new Error(
|
|
169
|
+
`claude auth login th\u1EA5t b\u1EA1i (exit ${result.status}). Th\u1EED 'claude auth login' tay r\u1ED3i ch\u1EA1y l\u1EA1i.`
|
|
170
|
+
);
|
|
171
|
+
}
|
|
172
|
+
log.success("\u0110\xE3 \u0111\u0103ng nh\u1EADp Claude Code");
|
|
173
|
+
}
|
|
174
|
+
function classifyQuotaError(combinedOutput) {
|
|
175
|
+
const text = combinedOutput.toLowerCase();
|
|
176
|
+
if (text.includes("credit_balance_too_low") || text.includes("credit balance too low")) {
|
|
177
|
+
return "credit_balance_too_low";
|
|
178
|
+
}
|
|
179
|
+
if (text.includes("insufficient_quota") || text.includes("insufficient quota")) {
|
|
180
|
+
return "insufficient_quota";
|
|
181
|
+
}
|
|
182
|
+
if (text.includes("invalid_api_key") || text.includes("invalid api key")) {
|
|
183
|
+
return "invalid_api_key";
|
|
184
|
+
}
|
|
185
|
+
if (text.includes("rate_limit") || text.includes("rate limit")) {
|
|
186
|
+
return "rate_limit";
|
|
187
|
+
}
|
|
188
|
+
return "unknown";
|
|
189
|
+
}
|
|
190
|
+
function verifyClaudeCodeQuota() {
|
|
191
|
+
const result = spawnSync("claude", ["--print", QUOTA_VERIFY_PROMPT], {
|
|
192
|
+
encoding: "utf8",
|
|
193
|
+
timeout: QUOTA_VERIFY_TIMEOUT_MS
|
|
194
|
+
});
|
|
195
|
+
if (result.signal === "SIGTERM") {
|
|
196
|
+
return { ok: false, reason: "timeout", detail: "claude --print > 30s" };
|
|
197
|
+
}
|
|
198
|
+
const stderr = result.stderr || "";
|
|
199
|
+
const stdout = result.stdout || "";
|
|
200
|
+
if (result.status === 0) {
|
|
201
|
+
return { ok: true };
|
|
202
|
+
}
|
|
203
|
+
const reason = classifyQuotaError(`${stderr}
|
|
204
|
+
${stdout}`);
|
|
205
|
+
return { ok: false, reason, detail: stderr.slice(0, 500) };
|
|
206
|
+
}
|
|
207
|
+
|
|
208
|
+
// src/lib/detect-claude-code-installation.ts
|
|
209
|
+
import { spawnSync as spawnSync2 } from "child_process";
|
|
210
|
+
|
|
211
|
+
// src/lib/detect-host-platform.ts
|
|
212
|
+
import { platform } from "os";
|
|
213
|
+
function detectHostPlatform() {
|
|
214
|
+
const p = platform();
|
|
215
|
+
if (p === "darwin" || p === "linux" || p === "win32") return p;
|
|
216
|
+
return "unsupported";
|
|
217
|
+
}
|
|
218
|
+
|
|
219
|
+
// src/lib/detect-claude-code-installation.ts
|
|
220
|
+
var VERSION_PROBE_TIMEOUT_MS = 5e3;
|
|
221
|
+
var SEMVER_REGEX = /(\d+\.\d+\.\d+)/;
|
|
222
|
+
function probeClaudeBinaryPath() {
|
|
223
|
+
const isWindows = detectHostPlatform() === "win32";
|
|
224
|
+
const probeCmd = isWindows ? "where" : "command";
|
|
225
|
+
const probeArgs = isWindows ? ["claude"] : ["-v", "claude"];
|
|
226
|
+
const result = spawnSync2(probeCmd, probeArgs, {
|
|
227
|
+
encoding: "utf8",
|
|
228
|
+
shell: !isWindows
|
|
229
|
+
});
|
|
230
|
+
if (result.error || result.status !== 0) return null;
|
|
231
|
+
const out = (result.stdout || "").trim();
|
|
232
|
+
if (!out) return null;
|
|
233
|
+
return out.split(/\r?\n/)[0].trim();
|
|
234
|
+
}
|
|
235
|
+
function probeClaudeVersion() {
|
|
236
|
+
const result = spawnSync2("claude", ["--version"], {
|
|
237
|
+
encoding: "utf8",
|
|
238
|
+
timeout: VERSION_PROBE_TIMEOUT_MS
|
|
239
|
+
});
|
|
240
|
+
if (result.error || result.status !== 0) return null;
|
|
241
|
+
const out = (result.stdout || "").trim();
|
|
242
|
+
const match = SEMVER_REGEX.exec(out);
|
|
243
|
+
return match ? match[1] : null;
|
|
244
|
+
}
|
|
245
|
+
function detectClaudeCodeInstallation() {
|
|
246
|
+
const path = probeClaudeBinaryPath();
|
|
247
|
+
if (!path) {
|
|
248
|
+
return { installed: false, version: null, path: null };
|
|
249
|
+
}
|
|
250
|
+
const version = probeClaudeVersion();
|
|
251
|
+
return { installed: true, version, path };
|
|
252
|
+
}
|
|
253
|
+
|
|
254
|
+
// src/lib/install-claude-code-via-npm.ts
|
|
255
|
+
import { spawnSync as spawnSync3 } from "child_process";
|
|
256
|
+
var NPM_INSTALL_TIMEOUT_MS = 5 * 60 * 1e3;
|
|
257
|
+
var CLAUDE_CODE_PACKAGE = "@anthropic-ai/claude-code";
|
|
258
|
+
var InstallClaudeCodeError = class extends Error {
|
|
259
|
+
reason;
|
|
260
|
+
exitCode;
|
|
261
|
+
constructor(reason, message, exitCode = null) {
|
|
262
|
+
super(message);
|
|
263
|
+
this.name = "InstallClaudeCodeError";
|
|
264
|
+
this.reason = reason;
|
|
265
|
+
this.exitCode = exitCode;
|
|
266
|
+
}
|
|
267
|
+
};
|
|
268
|
+
function classifyNpmFailure(exitCode, stderrSample) {
|
|
269
|
+
const stderr = stderrSample.toLowerCase();
|
|
270
|
+
if (stderr.includes("eacces") || stderr.includes("permission denied")) {
|
|
271
|
+
return new InstallClaudeCodeError(
|
|
272
|
+
"permission-denied",
|
|
273
|
+
`npm install -g c\u1EA7n quy\u1EC1n. Th\u1EED: sudo npm install -g ${CLAUDE_CODE_PACKAGE} ho\u1EB7c fix npm prefix (npm config set prefix ~/.npm-global).`,
|
|
274
|
+
exitCode
|
|
275
|
+
);
|
|
276
|
+
}
|
|
277
|
+
if (stderr.includes("enospc") || stderr.includes("no space")) {
|
|
278
|
+
return new InstallClaudeCodeError(
|
|
279
|
+
"disk-full",
|
|
280
|
+
"\u0110\u0129a \u0111\u1EA7y. Free disk space r\u1ED3i th\u1EED l\u1EA1i.",
|
|
281
|
+
exitCode
|
|
282
|
+
);
|
|
283
|
+
}
|
|
284
|
+
return new InstallClaudeCodeError(
|
|
285
|
+
"generic",
|
|
286
|
+
`npm install th\u1EA5t b\u1EA1i (exit ${exitCode ?? "null"}). Xem log npm ph\xEDa tr\xEAn.`,
|
|
287
|
+
exitCode
|
|
288
|
+
);
|
|
289
|
+
}
|
|
290
|
+
function installClaudeCodeViaNpm() {
|
|
291
|
+
log.info("\u0110ang c\xE0i Claude Code qua npm (c\xF3 th\u1EC3 m\u1EA5t 1-2 ph\xFAt)...");
|
|
292
|
+
const result = spawnSync3("npm", ["install", "-g", CLAUDE_CODE_PACKAGE], {
|
|
293
|
+
stdio: ["inherit", "inherit", "pipe"],
|
|
294
|
+
timeout: NPM_INSTALL_TIMEOUT_MS,
|
|
295
|
+
encoding: "utf8"
|
|
296
|
+
});
|
|
297
|
+
if (result.signal === "SIGTERM") {
|
|
298
|
+
throw new InstallClaudeCodeError(
|
|
299
|
+
"timeout",
|
|
300
|
+
`npm install timeout sau ${NPM_INSTALL_TIMEOUT_MS / 1e3}s. Check m\u1EA1ng r\u1ED3i th\u1EED l\u1EA1i.`,
|
|
301
|
+
null
|
|
302
|
+
);
|
|
303
|
+
}
|
|
304
|
+
if (result.status !== 0) {
|
|
305
|
+
if (result.stderr) process.stderr.write(result.stderr);
|
|
306
|
+
throw classifyNpmFailure(result.status, result.stderr || "");
|
|
307
|
+
}
|
|
308
|
+
const probe = detectClaudeCodeInstallation();
|
|
309
|
+
if (!probe.installed || !probe.path) {
|
|
310
|
+
throw new InstallClaudeCodeError(
|
|
311
|
+
"binary-not-in-path",
|
|
312
|
+
"npm c\xE0i xong nh\u01B0ng `claude` kh\xF4ng trong PATH. Reload shell (source ~/.zshrc) ho\u1EB7c th\xEAm npm global bin v\xE0o PATH.",
|
|
313
|
+
null
|
|
314
|
+
);
|
|
315
|
+
}
|
|
316
|
+
log.success(`\u0110\xE3 c\xE0i Claude Code${probe.version ? ` v${probe.version}` : ""} t\u1EA1i ${probe.path}`);
|
|
317
|
+
return { version: probe.version, path: probe.path };
|
|
318
|
+
}
|
|
319
|
+
|
|
320
|
+
// src/lib/prompt-ai-provider-choice.ts
|
|
321
|
+
import { readFileSync } from "fs";
|
|
322
|
+
import { homedir as homedir2 } from "os";
|
|
323
|
+
import { join as join3 } from "path";
|
|
324
|
+
import { select } from "@inquirer/prompts";
|
|
325
|
+
function getGlobalSettingsPath() {
|
|
326
|
+
return join3(homedir2(), ".claude", "settings.json");
|
|
327
|
+
}
|
|
328
|
+
function detectGlobalClaudeSettings() {
|
|
329
|
+
const path = getGlobalSettingsPath();
|
|
330
|
+
let raw;
|
|
331
|
+
try {
|
|
332
|
+
raw = readFileSync(path, "utf8");
|
|
333
|
+
} catch {
|
|
334
|
+
return { exists: false, hasBaseUrl: false, hasToken: false };
|
|
335
|
+
}
|
|
336
|
+
let parsed;
|
|
337
|
+
try {
|
|
338
|
+
parsed = JSON.parse(raw);
|
|
339
|
+
} catch {
|
|
340
|
+
return { exists: true, hasBaseUrl: false, hasToken: false };
|
|
341
|
+
}
|
|
342
|
+
const env = parsed.env || {};
|
|
343
|
+
const baseUrl = typeof env.ANTHROPIC_BASE_URL === "string" ? env.ANTHROPIC_BASE_URL : void 0;
|
|
344
|
+
const hasToken = typeof env.ANTHROPIC_AUTH_TOKEN === "string" && env.ANTHROPIC_AUTH_TOKEN.length > 0;
|
|
345
|
+
const model = typeof parsed.model === "string" ? parsed.model : void 0;
|
|
346
|
+
return {
|
|
347
|
+
exists: true,
|
|
348
|
+
hasBaseUrl: !!baseUrl,
|
|
349
|
+
baseUrl,
|
|
350
|
+
hasToken,
|
|
351
|
+
model,
|
|
352
|
+
rawSettings: parsed
|
|
353
|
+
};
|
|
354
|
+
}
|
|
355
|
+
async function promptAiProviderChoice(globalInfo = detectGlobalClaudeSettings()) {
|
|
356
|
+
if (globalInfo.exists && globalInfo.hasBaseUrl && globalInfo.hasToken) {
|
|
357
|
+
const choice = await select({
|
|
358
|
+
message: `Ph\xE1t hi\u1EC7n AI config global (base URL: ${globalInfo.baseUrl}). D\xF9ng cho project n\xE0y?`,
|
|
359
|
+
choices: [
|
|
360
|
+
{
|
|
361
|
+
name: "a. Yes \u2014 copy config global v\xE0o .claude/settings.json (per-project)",
|
|
362
|
+
value: "use-global"
|
|
363
|
+
},
|
|
364
|
+
{
|
|
365
|
+
name: "b. No \u2014 setup ri\xEAng (ch\u1ECDn provider kh\xE1c)",
|
|
366
|
+
value: "setup-fresh"
|
|
367
|
+
}
|
|
368
|
+
]
|
|
369
|
+
});
|
|
370
|
+
if (choice === "use-global") return "use-global";
|
|
371
|
+
}
|
|
372
|
+
return await select({
|
|
373
|
+
message: "Ch\u1ECDn provider cho AI features:",
|
|
374
|
+
choices: [
|
|
375
|
+
{
|
|
376
|
+
name: "1. Claude Code Subscription (d\xF9ng quota c\xE1 nh\xE2n Anthropic)",
|
|
377
|
+
value: "subscription"
|
|
378
|
+
},
|
|
379
|
+
{
|
|
380
|
+
name: "2. LLMLite API key (llm.nal.vn \u2014 NAL c\u1EA5p)",
|
|
381
|
+
value: "llmlite"
|
|
382
|
+
}
|
|
383
|
+
]
|
|
384
|
+
});
|
|
385
|
+
}
|
|
386
|
+
|
|
387
|
+
// src/lib/setup-llmlite-api-key-and-model.ts
|
|
388
|
+
import { input, password, select as select2 } from "@inquirer/prompts";
|
|
389
|
+
var DEFAULT_BASE_URL = "https://llm.nal.vn";
|
|
390
|
+
var FETCH_TIMEOUT_MS = 1e4;
|
|
391
|
+
function maskApiKey(key) {
|
|
392
|
+
if (key.length <= 8) return "sk-***";
|
|
393
|
+
return `${key.slice(0, 3)}...${key.slice(-4)}`;
|
|
394
|
+
}
|
|
395
|
+
async function promptApiKeyHidden() {
|
|
396
|
+
return await password({
|
|
397
|
+
message: "LLMLite API key (\u1EA9n input):",
|
|
398
|
+
mask: "*",
|
|
399
|
+
validate: (v) => v.trim().length > 0 ? true : "API key b\u1EAFt bu\u1ED9c"
|
|
400
|
+
});
|
|
401
|
+
}
|
|
402
|
+
async function promptBaseUrl(defaultUrl = DEFAULT_BASE_URL) {
|
|
403
|
+
const value = await input({
|
|
404
|
+
message: "LLMLite base URL:",
|
|
405
|
+
default: defaultUrl,
|
|
406
|
+
validate: (v) => /^https?:\/\//.test(v) ? true : "Ph\u1EA3i l\xE0 URL h\u1EE3p l\u1EC7 (http/https)"
|
|
407
|
+
});
|
|
408
|
+
return value.replace(/\/+$/, "");
|
|
409
|
+
}
|
|
410
|
+
async function fetchAvailableModels(baseUrl, apiKey) {
|
|
411
|
+
const controller = new AbortController();
|
|
412
|
+
const timer = setTimeout(() => controller.abort(), FETCH_TIMEOUT_MS);
|
|
413
|
+
try {
|
|
414
|
+
const res = await fetch(`${baseUrl}/v1/models`, {
|
|
415
|
+
method: "GET",
|
|
416
|
+
headers: {
|
|
417
|
+
Authorization: `Bearer ${apiKey}`,
|
|
418
|
+
Accept: "application/json"
|
|
419
|
+
},
|
|
420
|
+
signal: controller.signal
|
|
421
|
+
});
|
|
422
|
+
if (res.status === 401 || res.status === 403) {
|
|
423
|
+
throw new Error(`API key invalid (HTTP ${res.status}).`);
|
|
424
|
+
}
|
|
425
|
+
if (res.status === 404) {
|
|
426
|
+
throw new Error(`Endpoint /v1/models kh\xF4ng t\u1ED3n t\u1EA1i tr\xEAn ${baseUrl}.`);
|
|
427
|
+
}
|
|
428
|
+
if (!res.ok) {
|
|
429
|
+
throw new Error(`Fetch models th\u1EA5t b\u1EA1i (HTTP ${res.status}).`);
|
|
430
|
+
}
|
|
431
|
+
const json = await res.json();
|
|
432
|
+
const models = (json.data || []).map((m) => typeof m.id === "string" ? m.id : null).filter((id) => id !== null);
|
|
433
|
+
if (models.length === 0) {
|
|
434
|
+
throw new Error("LLMLite tr\u1EA3 v\u1EC1 list r\u1ED7ng. Li\xEAn h\u1EC7 admin NAL.");
|
|
435
|
+
}
|
|
436
|
+
return models;
|
|
437
|
+
} catch (err) {
|
|
438
|
+
if (err.name === "AbortError") {
|
|
439
|
+
throw new Error(`Connect ${baseUrl} timeout sau ${FETCH_TIMEOUT_MS / 1e3}s.`);
|
|
440
|
+
}
|
|
441
|
+
throw err;
|
|
442
|
+
} finally {
|
|
443
|
+
clearTimeout(timer);
|
|
444
|
+
}
|
|
445
|
+
}
|
|
446
|
+
async function promptModelChoice(models) {
|
|
447
|
+
const claudeAliases = models.filter((m) => m.toLowerCase().includes("claude"));
|
|
448
|
+
if (claudeAliases.length === 1) {
|
|
449
|
+
log.info(`Auto-pick model: ${claudeAliases[0]} (ch\u1EC9 1 claude alias tr\xEAn endpoint)`);
|
|
450
|
+
return claudeAliases[0];
|
|
451
|
+
}
|
|
452
|
+
const choiceList = claudeAliases.length > 0 ? claudeAliases : models;
|
|
453
|
+
return await select2({
|
|
454
|
+
message: "Ch\u1ECDn model m\u1EB7c \u0111\u1ECBnh cho project:",
|
|
455
|
+
choices: choiceList.map((m) => ({ name: m, value: m }))
|
|
456
|
+
});
|
|
457
|
+
}
|
|
458
|
+
async function setupLLMLiteApiKeyAndModel() {
|
|
459
|
+
const apiKey = await promptApiKeyHidden();
|
|
460
|
+
const baseUrl = await promptBaseUrl();
|
|
461
|
+
log.info(`Verify key (${maskApiKey(apiKey)}) qua ${baseUrl}/v1/models...`);
|
|
462
|
+
const models = await fetchAvailableModels(baseUrl, apiKey);
|
|
463
|
+
log.success(`Endpoint OK \u2014 ${models.length} models available`);
|
|
464
|
+
const model = await promptModelChoice(models);
|
|
465
|
+
return { apiKey, baseUrl, model };
|
|
466
|
+
}
|
|
467
|
+
|
|
468
|
+
// src/lib/write-claude-settings-json-per-project.ts
|
|
469
|
+
import { promises as fs3 } from "fs";
|
|
470
|
+
import { join as join4 } from "path";
|
|
471
|
+
var SECRET_FILE_MODE2 = 384;
|
|
472
|
+
function getClaudeSettingsPath(workspacePath) {
|
|
473
|
+
return join4(workspacePath, ".claude", "settings.json");
|
|
474
|
+
}
|
|
475
|
+
async function readExistingSettings(path) {
|
|
476
|
+
if (!await pathExists(path)) return {};
|
|
477
|
+
try {
|
|
478
|
+
return await readJson(path);
|
|
479
|
+
} catch (err) {
|
|
480
|
+
throw new Error(
|
|
481
|
+
`Kh\xF4ng parse \u0111\u01B0\u1EE3c ${path} (JSON l\u1ED7i): ${err.message}. Backup file r\u1ED3i x\xF3a \u0111\u1EC3 Avatar t\u1EA1o l\u1EA1i.`
|
|
482
|
+
);
|
|
483
|
+
}
|
|
484
|
+
}
|
|
485
|
+
function applySubscription(existing, model) {
|
|
486
|
+
const { env: existingEnv, ...rest } = existing;
|
|
487
|
+
const merged = { ...rest, model };
|
|
488
|
+
if (existingEnv) {
|
|
489
|
+
const { ANTHROPIC_BASE_URL: _b, ANTHROPIC_AUTH_TOKEN: _t, ...envRest } = existingEnv;
|
|
490
|
+
if (Object.keys(envRest).length > 0) {
|
|
491
|
+
merged.env = envRest;
|
|
492
|
+
}
|
|
493
|
+
}
|
|
494
|
+
return merged;
|
|
495
|
+
}
|
|
496
|
+
function applyLLMLite(existing, apiKey, baseUrl, model) {
|
|
497
|
+
return {
|
|
498
|
+
...existing,
|
|
499
|
+
env: {
|
|
500
|
+
...existing.env || {},
|
|
501
|
+
ANTHROPIC_BASE_URL: baseUrl,
|
|
502
|
+
ANTHROPIC_AUTH_TOKEN: apiKey
|
|
503
|
+
},
|
|
504
|
+
model
|
|
505
|
+
};
|
|
506
|
+
}
|
|
507
|
+
function applyUseGlobal(existing, source) {
|
|
508
|
+
const sourceEnv = source.env || {};
|
|
509
|
+
const sourceModel = typeof source.model === "string" ? source.model : void 0;
|
|
510
|
+
return {
|
|
511
|
+
...existing,
|
|
512
|
+
env: {
|
|
513
|
+
...existing.env || {},
|
|
514
|
+
...sourceEnv
|
|
515
|
+
},
|
|
516
|
+
...sourceModel ? { model: sourceModel } : {}
|
|
517
|
+
};
|
|
518
|
+
}
|
|
519
|
+
async function writeClaudeSettings(workspacePath, input3) {
|
|
520
|
+
const path = getClaudeSettingsPath(workspacePath);
|
|
521
|
+
const existing = await readExistingSettings(path);
|
|
522
|
+
let merged;
|
|
523
|
+
switch (input3.provider) {
|
|
524
|
+
case "subscription":
|
|
525
|
+
merged = applySubscription(existing, input3.model);
|
|
526
|
+
break;
|
|
527
|
+
case "llmlite":
|
|
528
|
+
merged = applyLLMLite(existing, input3.apiKey, input3.baseUrl, input3.model);
|
|
529
|
+
break;
|
|
530
|
+
case "use-global":
|
|
531
|
+
merged = applyUseGlobal(existing, input3.sourceSettings);
|
|
532
|
+
break;
|
|
533
|
+
}
|
|
534
|
+
await writeJsonAtomic(path, merged, SECRET_FILE_MODE2);
|
|
535
|
+
try {
|
|
536
|
+
await fs3.chmod(path, SECRET_FILE_MODE2);
|
|
537
|
+
} catch {
|
|
538
|
+
}
|
|
539
|
+
return { path, mode: SECRET_FILE_MODE2 };
|
|
540
|
+
}
|
|
541
|
+
|
|
542
|
+
// src/lib/run-ai-setup-phase.ts
|
|
543
|
+
var SUBSCRIPTION_DEFAULT_MODEL = "sonnet";
|
|
544
|
+
async function runAiSetupPhase(args) {
|
|
545
|
+
try {
|
|
546
|
+
log.info("Setup AI provider cho workspace...");
|
|
547
|
+
let info = detectClaudeCodeInstallation();
|
|
548
|
+
if (!info.installed) {
|
|
549
|
+
log.info("Ch\u01B0a c\xF3 Claude Code \u2014 s\u1EBD t\u1EF1 c\xE0i qua npm.");
|
|
550
|
+
installClaudeCodeViaNpm();
|
|
551
|
+
info = detectClaudeCodeInstallation();
|
|
552
|
+
if (!info.installed) {
|
|
553
|
+
throw new Error("C\xE0i Claude Code xong nh\u01B0ng v\u1EABn kh\xF4ng detect \u0111\u01B0\u1EE3c binary.");
|
|
554
|
+
}
|
|
555
|
+
} else {
|
|
556
|
+
log.success(`Claude Code \u0111\xE3 c\xF3${info.version ? ` v${info.version}` : ""}`);
|
|
557
|
+
}
|
|
558
|
+
const globalInfo = detectGlobalClaudeSettings();
|
|
559
|
+
const choice = await promptAiProviderChoice(globalInfo);
|
|
560
|
+
switch (choice) {
|
|
561
|
+
case "subscription": {
|
|
562
|
+
if (checkClaudeCodeSubscriptionAuth() !== "authenticated") {
|
|
563
|
+
triggerClaudeCodeAuthLogin();
|
|
564
|
+
}
|
|
565
|
+
const quota = verifyClaudeCodeQuota();
|
|
566
|
+
if (!quota.ok) {
|
|
567
|
+
await appendAuditEntry(
|
|
568
|
+
"ai_setup",
|
|
569
|
+
`provider=subscription,result=no-quota,reason=${quota.reason ?? "unknown"}`
|
|
570
|
+
);
|
|
571
|
+
log.warn(
|
|
572
|
+
`Subscription verify th\u1EA5t b\u1EA1i (${quota.reason ?? "unknown"}). Suggest LLMLite ho\u1EB7c upgrade plan.`
|
|
573
|
+
);
|
|
574
|
+
return { ok: false, reason: `subscription-${quota.reason ?? "unknown"}`, phase: "quota" };
|
|
575
|
+
}
|
|
576
|
+
await writeClaudeSettings(args.workspacePath, {
|
|
577
|
+
provider: "subscription",
|
|
578
|
+
model: SUBSCRIPTION_DEFAULT_MODEL
|
|
579
|
+
});
|
|
580
|
+
await appendAuditEntry("ai_setup", "provider=subscription,result=ok");
|
|
581
|
+
log.success(`AI ready \xB7 Subscription \xB7 model=${SUBSCRIPTION_DEFAULT_MODEL}`);
|
|
582
|
+
return { ok: true, provider: "subscription", model: SUBSCRIPTION_DEFAULT_MODEL };
|
|
583
|
+
}
|
|
584
|
+
case "llmlite": {
|
|
585
|
+
const llmConfig = await setupLLMLiteApiKeyAndModel();
|
|
586
|
+
await writeClaudeSettings(args.workspacePath, {
|
|
587
|
+
provider: "llmlite",
|
|
588
|
+
apiKey: llmConfig.apiKey,
|
|
589
|
+
baseUrl: llmConfig.baseUrl,
|
|
590
|
+
model: llmConfig.model
|
|
591
|
+
});
|
|
592
|
+
await appendAuditEntry(
|
|
593
|
+
"ai_setup",
|
|
594
|
+
`provider=llmlite,result=ok,model=${llmConfig.model},base=${llmConfig.baseUrl}`
|
|
595
|
+
);
|
|
596
|
+
log.success(`AI ready \xB7 LLMLite \xB7 model=${llmConfig.model} \xB7 ${llmConfig.baseUrl}`);
|
|
597
|
+
return { ok: true, provider: "llmlite", model: llmConfig.model };
|
|
598
|
+
}
|
|
599
|
+
case "use-global": {
|
|
600
|
+
if (!globalInfo.rawSettings) {
|
|
601
|
+
throw new Error("use-global ch\u1ECDn nh\u01B0ng kh\xF4ng \u0111\u1ECDc \u0111\u01B0\u1EE3c global settings.");
|
|
602
|
+
}
|
|
603
|
+
await writeClaudeSettings(args.workspacePath, {
|
|
604
|
+
provider: "use-global",
|
|
605
|
+
sourceSettings: globalInfo.rawSettings
|
|
606
|
+
});
|
|
607
|
+
await appendAuditEntry("ai_setup", "provider=use-global,result=ok");
|
|
608
|
+
log.success(`AI ready \xB7 Copy t\u1EEB global config (${globalInfo.baseUrl ?? "subscription"})`);
|
|
609
|
+
return { ok: true, provider: "use-global", model: globalInfo.model };
|
|
610
|
+
}
|
|
611
|
+
}
|
|
612
|
+
} catch (err) {
|
|
613
|
+
const message = err instanceof Error ? err.message : String(err);
|
|
614
|
+
log.warn(`AI setup th\u1EA5t b\u1EA1i: ${message}`);
|
|
615
|
+
log.dim("Workspace v\u1EABn s\u1EB5n s\xE0ng. Setup AI sau qua: avatar ai setup");
|
|
616
|
+
await appendAuditEntry("ai_setup", `result=failed,error=${message.slice(0, 200)}`);
|
|
617
|
+
return { ok: false, reason: message };
|
|
618
|
+
}
|
|
619
|
+
}
|
|
620
|
+
|
|
621
|
+
// src/commands/ai.ts
|
|
622
|
+
async function ensureWorkspaceCwd() {
|
|
623
|
+
const cwd = process.cwd();
|
|
624
|
+
const claudeDir = join5(cwd, ".claude");
|
|
625
|
+
if (!await pathExists(claudeDir)) {
|
|
626
|
+
log.error("Kh\xF4ng th\u1EA5y .claude/ trong th\u01B0 m\u1EE5c hi\u1EC7n t\u1EA1i. Ch\u1EA1y l\u1EC7nh n\xE0y trong workspace Avatar.");
|
|
627
|
+
process.exit(1);
|
|
628
|
+
}
|
|
629
|
+
return cwd;
|
|
630
|
+
}
|
|
631
|
+
async function readWorkspaceSettings(workspacePath) {
|
|
632
|
+
const settingsPath = join5(workspacePath, ".claude", "settings.json");
|
|
633
|
+
if (!await pathExists(settingsPath)) return {};
|
|
634
|
+
try {
|
|
635
|
+
return await readJson(settingsPath);
|
|
636
|
+
} catch {
|
|
637
|
+
return {};
|
|
638
|
+
}
|
|
639
|
+
}
|
|
640
|
+
async function runAiSetup() {
|
|
641
|
+
const workspacePath = await ensureWorkspaceCwd();
|
|
642
|
+
await runAiSetupPhase({ workspacePath });
|
|
643
|
+
}
|
|
644
|
+
async function runAiStatus() {
|
|
645
|
+
const workspacePath = await ensureWorkspaceCwd();
|
|
646
|
+
const settings = await readWorkspaceSettings(workspacePath);
|
|
647
|
+
const env = settings.env || {};
|
|
648
|
+
const baseUrl = typeof env.ANTHROPIC_BASE_URL === "string" ? env.ANTHROPIC_BASE_URL : void 0;
|
|
649
|
+
const token = typeof env.ANTHROPIC_AUTH_TOKEN === "string" ? env.ANTHROPIC_AUTH_TOKEN : void 0;
|
|
650
|
+
const model = typeof settings.model === "string" ? settings.model : void 0;
|
|
651
|
+
const provider = baseUrl ? "LLMLite" : token ? "Custom" : "Subscription (default)";
|
|
652
|
+
log.info(`Project: ${workspacePath}`);
|
|
653
|
+
log.info(`Provider: ${provider}${baseUrl ? ` (${baseUrl})` : ""}`);
|
|
654
|
+
log.info(`Model: ${model ?? "(default \u2014 Claude Code ch\u1ECDn)"}`);
|
|
655
|
+
log.info(`Token: ${token ? maskApiKey(token) : "(kh\xF4ng set \u2014 d\xF9ng subscription auth)"}`);
|
|
656
|
+
}
|
|
657
|
+
async function runAiTest() {
|
|
658
|
+
await ensureWorkspaceCwd();
|
|
659
|
+
log.info("Calling provider v\u1EDBi prompt 'say ok'...");
|
|
660
|
+
const result = spawnSync4("claude", ["--print", "say ok"], {
|
|
661
|
+
encoding: "utf8",
|
|
662
|
+
timeout: 3e4
|
|
663
|
+
});
|
|
664
|
+
if (result.signal === "SIGTERM") {
|
|
665
|
+
log.error("Timeout sau 30s. Check m\u1EA1ng / endpoint.");
|
|
666
|
+
process.exit(1);
|
|
667
|
+
}
|
|
668
|
+
if (result.status !== 0) {
|
|
669
|
+
log.error(`Test th\u1EA5t b\u1EA1i (exit ${result.status}).`);
|
|
670
|
+
if (result.stderr) process.stderr.write(`${result.stderr}
|
|
671
|
+
`);
|
|
672
|
+
process.exit(1);
|
|
673
|
+
}
|
|
674
|
+
log.success(`Response: ${(result.stdout || "").trim().slice(0, 200)}`);
|
|
675
|
+
log.success("AI provider working");
|
|
676
|
+
}
|
|
677
|
+
async function runAiReset(opts) {
|
|
678
|
+
const workspacePath = await ensureWorkspaceCwd();
|
|
679
|
+
const settingsPath = join5(workspacePath, ".claude", "settings.json");
|
|
680
|
+
const settings = await readWorkspaceSettings(workspacePath);
|
|
681
|
+
if (!opts.yes) {
|
|
682
|
+
const ok = await confirm({
|
|
683
|
+
message: "X\xF3a AI config (v\u1EC1 d\xF9ng Claude Code Subscription default)?",
|
|
684
|
+
default: false
|
|
685
|
+
});
|
|
686
|
+
if (!ok) {
|
|
687
|
+
log.dim("\u0110\xE3 h\u1EE7y.");
|
|
688
|
+
return;
|
|
689
|
+
}
|
|
690
|
+
}
|
|
691
|
+
const { env: existingEnv, ...rest } = settings;
|
|
692
|
+
const reset = { ...rest };
|
|
693
|
+
if (existingEnv) {
|
|
694
|
+
const { ANTHROPIC_BASE_URL: _b, ANTHROPIC_AUTH_TOKEN: _t, ...envRest } = existingEnv;
|
|
695
|
+
if (Object.keys(envRest).length > 0) {
|
|
696
|
+
reset.env = envRest;
|
|
697
|
+
}
|
|
698
|
+
}
|
|
699
|
+
if (Object.keys(reset).length === 0) {
|
|
700
|
+
await fs4.unlink(settingsPath).catch(() => {
|
|
701
|
+
});
|
|
702
|
+
log.success("\u0110\xE3 x\xF3a .claude/settings.json (clean state)");
|
|
703
|
+
} else {
|
|
704
|
+
await writeJsonAtomic(settingsPath, reset, 384);
|
|
705
|
+
log.success("\u0110\xE3 reset env block trong .claude/settings.json");
|
|
706
|
+
}
|
|
707
|
+
}
|
|
708
|
+
function registerAiCommand(program2) {
|
|
709
|
+
const ai = program2.command("ai").description("Qu\u1EA3n l\xFD AI provider config (M12)");
|
|
710
|
+
ai.command("setup").description("Wizard setup/re-config AI provider cho workspace hi\u1EC7n t\u1EA1i").action(async () => {
|
|
711
|
+
await runAiSetup();
|
|
712
|
+
});
|
|
713
|
+
ai.command("status").description("Show AI config hi\u1EC7n t\u1EA1i (mask token)").action(async () => {
|
|
714
|
+
await runAiStatus();
|
|
715
|
+
});
|
|
716
|
+
ai.command("test").description("Verify AI provider qua cheap prompt").action(async () => {
|
|
717
|
+
await runAiTest();
|
|
718
|
+
});
|
|
719
|
+
ai.command("reset").description("X\xF3a env.ANTHROPIC_* kh\u1ECFi settings.json (v\u1EC1 Subscription default)").option("--yes", "Skip confirm").action(async (opts) => {
|
|
720
|
+
await runAiReset(opts);
|
|
721
|
+
});
|
|
722
|
+
}
|
|
723
|
+
|
|
31
724
|
// src/lib/not-implemented-stub.ts
|
|
32
725
|
function notImplementedYet(commandName, milestone) {
|
|
33
726
|
return () => {
|
|
@@ -50,59 +743,25 @@ function registerCommitCommand(program2) {
|
|
|
50
743
|
}
|
|
51
744
|
|
|
52
745
|
// src/commands/doctor.ts
|
|
53
|
-
import { spawnSync } from "child_process";
|
|
54
|
-
import { promises as
|
|
55
|
-
import { join as
|
|
746
|
+
import { spawnSync as spawnSync5 } from "child_process";
|
|
747
|
+
import { promises as fs6 } from "fs";
|
|
748
|
+
import { join as join9 } from "path";
|
|
56
749
|
import boxen from "boxen";
|
|
57
750
|
|
|
58
|
-
// src/lib/filesystem-helpers.ts
|
|
59
|
-
import { constants, promises as fs } from "fs";
|
|
60
|
-
import { dirname, join, relative } from "path";
|
|
61
|
-
async function pathExists(path) {
|
|
62
|
-
try {
|
|
63
|
-
await fs.access(path, constants.F_OK);
|
|
64
|
-
return true;
|
|
65
|
-
} catch {
|
|
66
|
-
return false;
|
|
67
|
-
}
|
|
68
|
-
}
|
|
69
|
-
async function ensureDir(path) {
|
|
70
|
-
await fs.mkdir(path, { recursive: true });
|
|
71
|
-
}
|
|
72
|
-
async function readText(path) {
|
|
73
|
-
return await fs.readFile(path, "utf8");
|
|
74
|
-
}
|
|
75
|
-
async function readJson(path) {
|
|
76
|
-
return JSON.parse(await readText(path));
|
|
77
|
-
}
|
|
78
|
-
async function writeTextAtomic(path, content, mode) {
|
|
79
|
-
await ensureDir(dirname(path));
|
|
80
|
-
const tmp = `${path}.tmp-${process.pid}-${Date.now()}`;
|
|
81
|
-
await fs.writeFile(tmp, content, "utf8");
|
|
82
|
-
if (mode !== void 0) {
|
|
83
|
-
await fs.chmod(tmp, mode);
|
|
84
|
-
}
|
|
85
|
-
await fs.rename(tmp, path);
|
|
86
|
-
}
|
|
87
|
-
async function writeJsonAtomic(path, data, mode) {
|
|
88
|
-
await writeTextAtomic(path, `${JSON.stringify(data, null, 2)}
|
|
89
|
-
`, mode);
|
|
90
|
-
}
|
|
91
|
-
|
|
92
751
|
// src/lib/git-operations.ts
|
|
93
|
-
import { join as
|
|
752
|
+
import { join as join6 } from "path";
|
|
94
753
|
import { simpleGit } from "simple-git";
|
|
95
754
|
function git(cwd = process.cwd()) {
|
|
96
755
|
return simpleGit({ baseDir: cwd, binary: "git" });
|
|
97
756
|
}
|
|
98
757
|
async function isGitRepo(cwd = process.cwd()) {
|
|
99
|
-
return await pathExists(
|
|
758
|
+
return await pathExists(join6(cwd, ".git"));
|
|
100
759
|
}
|
|
101
760
|
async function addSubmodule(repoUrl, destPath, cwd = process.cwd()) {
|
|
102
761
|
await git(cwd).subModule(["add", repoUrl, destPath]);
|
|
103
762
|
}
|
|
104
763
|
async function checkoutTagInSubmodule(submodulePath, tag, cwd = process.cwd()) {
|
|
105
|
-
const submoduleCwd =
|
|
764
|
+
const submoduleCwd = join6(cwd, submodulePath);
|
|
106
765
|
await git(submoduleCwd).fetch(["--tags"]);
|
|
107
766
|
await git(submoduleCwd).checkout(tag);
|
|
108
767
|
}
|
|
@@ -120,12 +779,12 @@ async function currentCommitSha(cwd = process.cwd()) {
|
|
|
120
779
|
}
|
|
121
780
|
|
|
122
781
|
// src/lib/project-tree-scaffolder.ts
|
|
123
|
-
import { promises as
|
|
124
|
-
import { join as
|
|
782
|
+
import { promises as fs5 } from "fs";
|
|
783
|
+
import { join as join8 } from "path";
|
|
125
784
|
|
|
126
785
|
// src/lib/template-bundle-loader.ts
|
|
127
786
|
import { existsSync } from "fs";
|
|
128
|
-
import { dirname as dirname2, join as
|
|
787
|
+
import { dirname as dirname2, join as join7 } from "path";
|
|
129
788
|
import { fileURLToPath } from "url";
|
|
130
789
|
|
|
131
790
|
// src/lib/mustache-template-engine.ts
|
|
@@ -141,12 +800,12 @@ function renderTemplate(source, variables) {
|
|
|
141
800
|
// src/lib/template-bundle-loader.ts
|
|
142
801
|
var HERE = dirname2(fileURLToPath(import.meta.url));
|
|
143
802
|
var PACKAGE_ROOT = findPackageRoot(HERE);
|
|
144
|
-
var TEMPLATES_ROOT =
|
|
145
|
-
var HOOKS_ROOT =
|
|
803
|
+
var TEMPLATES_ROOT = join7(PACKAGE_ROOT, "src", "templates");
|
|
804
|
+
var HOOKS_ROOT = join7(PACKAGE_ROOT, "src", "hooks");
|
|
146
805
|
function findPackageRoot(startDir) {
|
|
147
806
|
let dir = startDir;
|
|
148
807
|
while (true) {
|
|
149
|
-
if (existsSync(
|
|
808
|
+
if (existsSync(join7(dir, "package.json"))) return dir;
|
|
150
809
|
const parent = dirname2(dir);
|
|
151
810
|
if (parent === dir) {
|
|
152
811
|
throw new Error(`Cannot locate package root from ${startDir}`);
|
|
@@ -155,14 +814,14 @@ function findPackageRoot(startDir) {
|
|
|
155
814
|
}
|
|
156
815
|
}
|
|
157
816
|
async function loadTemplate(name) {
|
|
158
|
-
return await readText(
|
|
817
|
+
return await readText(join7(TEMPLATES_ROOT, `${name}.tpl`));
|
|
159
818
|
}
|
|
160
819
|
async function renderTemplateByName(name, variables) {
|
|
161
820
|
const source = await loadTemplate(name);
|
|
162
821
|
return renderTemplate(source, variables);
|
|
163
822
|
}
|
|
164
823
|
async function loadHook(name) {
|
|
165
|
-
return await readText(
|
|
824
|
+
return await readText(join7(HOOKS_ROOT, `${name}.sh.tpl`));
|
|
166
825
|
}
|
|
167
826
|
|
|
168
827
|
// src/lib/project-tree-scaffolder.ts
|
|
@@ -179,7 +838,7 @@ async function backupIfExists(path) {
|
|
|
179
838
|
throw new Error(`Could not find free backup name for ${path}`);
|
|
180
839
|
}
|
|
181
840
|
}
|
|
182
|
-
await
|
|
841
|
+
await fs5.rename(path, backupPath);
|
|
183
842
|
return backupPath;
|
|
184
843
|
}
|
|
185
844
|
async function writeWithBackup(path, content, mode) {
|
|
@@ -196,12 +855,12 @@ var PROJECT_KNOWLEDGE_TEMPLATES = [
|
|
|
196
855
|
"project/gotchas.md"
|
|
197
856
|
];
|
|
198
857
|
async function createClaudeDirTree(projectRoot) {
|
|
199
|
-
const claudeRoot =
|
|
858
|
+
const claudeRoot = join8(projectRoot, ".claude");
|
|
200
859
|
await ensureDir(claudeRoot);
|
|
201
860
|
for (const sub of CLAUDE_SUBDIRS) {
|
|
202
|
-
const dir =
|
|
861
|
+
const dir = join8(claudeRoot, sub);
|
|
203
862
|
await ensureDir(dir);
|
|
204
|
-
await writeTextAtomic(
|
|
863
|
+
await writeTextAtomic(join8(dir, ".gitkeep"), "");
|
|
205
864
|
}
|
|
206
865
|
}
|
|
207
866
|
async function writeProjectKnowledgeFiles(projectRoot, vars) {
|
|
@@ -232,7 +891,7 @@ async function writeProjectKnowledgeFiles(projectRoot, vars) {
|
|
|
232
891
|
for (const tpl of PROJECT_KNOWLEDGE_TEMPLATES) {
|
|
233
892
|
const content = await renderTemplateByName(tpl, baseVars);
|
|
234
893
|
const relative4 = tpl.replace(/^project\//, "");
|
|
235
|
-
const outPath =
|
|
894
|
+
const outPath = join8(projectRoot, ".claude", "project", relative4);
|
|
236
895
|
const backup = await writeWithBackup(outPath, content);
|
|
237
896
|
if (backup) backups.push(backup);
|
|
238
897
|
}
|
|
@@ -240,19 +899,19 @@ async function writeProjectKnowledgeFiles(projectRoot, vars) {
|
|
|
240
899
|
}
|
|
241
900
|
async function writeRootClaudeMd(projectRoot, vars) {
|
|
242
901
|
const content = await renderTemplateByName("CLAUDE.md", vars);
|
|
243
|
-
return await writeWithBackup(
|
|
902
|
+
return await writeWithBackup(join8(projectRoot, "CLAUDE.md"), content);
|
|
244
903
|
}
|
|
245
904
|
async function writeProjectSettings(projectRoot, vars) {
|
|
246
905
|
const content = await renderTemplateByName("settings.json", vars);
|
|
247
|
-
return await writeWithBackup(
|
|
906
|
+
return await writeWithBackup(join8(projectRoot, ".claude", "settings.json"), content);
|
|
248
907
|
}
|
|
249
908
|
async function appendGitignoreEntries(projectRoot) {
|
|
250
|
-
const path =
|
|
909
|
+
const path = join8(projectRoot, ".gitignore");
|
|
251
910
|
const tpl = await renderTemplateByName("gitignore", {});
|
|
252
911
|
const marker = "# Avatar \u2014 git-ignored entries injected on `avatar init`";
|
|
253
912
|
let existing = "";
|
|
254
913
|
if (await pathExists(path)) {
|
|
255
|
-
existing = await
|
|
914
|
+
existing = await fs5.readFile(path, "utf8");
|
|
256
915
|
if (existing.includes(marker)) return;
|
|
257
916
|
}
|
|
258
917
|
const separator = existing.endsWith("\n") || existing.length === 0 ? "" : "\n";
|
|
@@ -261,78 +920,12 @@ ${tpl}`);
|
|
|
261
920
|
}
|
|
262
921
|
async function installGitHook(gitDir, hookName) {
|
|
263
922
|
const content = await loadHook(hookName);
|
|
264
|
-
const hooksDir =
|
|
923
|
+
const hooksDir = join8(gitDir, "hooks");
|
|
265
924
|
await ensureDir(hooksDir);
|
|
266
|
-
const dest =
|
|
925
|
+
const dest = join8(hooksDir, hookName);
|
|
267
926
|
await writeTextAtomic(dest, content, 493);
|
|
268
927
|
}
|
|
269
928
|
|
|
270
|
-
// src/lib/user-config-store.ts
|
|
271
|
-
import { homedir } from "os";
|
|
272
|
-
import { join as join5 } from "path";
|
|
273
|
-
|
|
274
|
-
// src/types/config-schema.ts
|
|
275
|
-
import { z } from "zod";
|
|
276
|
-
var userConfigSchema = z.object({
|
|
277
|
-
email: z.string().email(),
|
|
278
|
-
name: z.string(),
|
|
279
|
-
access_token: z.string().min(1),
|
|
280
|
-
refresh_token: z.string().min(1),
|
|
281
|
-
expires_at: z.string().datetime(),
|
|
282
|
-
id_token: z.string().min(1)
|
|
283
|
-
});
|
|
284
|
-
var userStateSchema = z.object({
|
|
285
|
-
installed_tools: z.record(
|
|
286
|
-
z.string(),
|
|
287
|
-
z.object({
|
|
288
|
-
version: z.string().optional(),
|
|
289
|
-
installed_at: z.string().datetime(),
|
|
290
|
-
install_method: z.string()
|
|
291
|
-
})
|
|
292
|
-
).default({}),
|
|
293
|
-
tool_inputs: z.record(z.string(), z.unknown()).default({})
|
|
294
|
-
});
|
|
295
|
-
var projectSettingsSchema = z.object({
|
|
296
|
-
allowedTools: z.array(z.string()),
|
|
297
|
-
hooks: z.object({
|
|
298
|
-
PostToolUse: z.array(z.unknown()).optional()
|
|
299
|
-
}).partial().optional(),
|
|
300
|
-
env: z.record(z.string(), z.string()).default({})
|
|
301
|
-
});
|
|
302
|
-
var initModeSchema = z.enum(["internal", "client", "library"]);
|
|
303
|
-
|
|
304
|
-
// src/lib/user-config-store.ts
|
|
305
|
-
var AVATAR_HOME = join5(homedir(), ".avatar");
|
|
306
|
-
var USER_CONFIG_PATH = join5(AVATAR_HOME, "config.json");
|
|
307
|
-
var USER_STATE_PATH = join5(AVATAR_HOME, "state.json");
|
|
308
|
-
var AUDIT_LOG_PATH = join5(AVATAR_HOME, "audit.log");
|
|
309
|
-
var BACKUPS_DIR = join5(AVATAR_HOME, "backups");
|
|
310
|
-
var SECRET_FILE_MODE = 384;
|
|
311
|
-
async function ensureAvatarHome() {
|
|
312
|
-
await ensureDir(AVATAR_HOME);
|
|
313
|
-
}
|
|
314
|
-
async function readUserConfig() {
|
|
315
|
-
if (!await pathExists(USER_CONFIG_PATH)) return null;
|
|
316
|
-
const raw = await readJson(USER_CONFIG_PATH);
|
|
317
|
-
const parsed = userConfigSchema.safeParse(raw);
|
|
318
|
-
if (!parsed.success) return null;
|
|
319
|
-
return parsed.data;
|
|
320
|
-
}
|
|
321
|
-
async function writeUserConfig(config) {
|
|
322
|
-
await ensureAvatarHome();
|
|
323
|
-
await writeJsonAtomic(USER_CONFIG_PATH, config, SECRET_FILE_MODE);
|
|
324
|
-
}
|
|
325
|
-
async function clearUserConfig() {
|
|
326
|
-
if (await pathExists(USER_CONFIG_PATH)) {
|
|
327
|
-
const { promises: fs7 } = await import("fs");
|
|
328
|
-
await fs7.unlink(USER_CONFIG_PATH);
|
|
329
|
-
}
|
|
330
|
-
}
|
|
331
|
-
function isTokenExpired(config) {
|
|
332
|
-
const expiresAt = Date.parse(config.expires_at);
|
|
333
|
-
return Number.isNaN(expiresAt) || expiresAt - Date.now() < 6e4;
|
|
334
|
-
}
|
|
335
|
-
|
|
336
929
|
// src/commands/doctor.ts
|
|
337
930
|
function registerDoctorCommand(program2) {
|
|
338
931
|
program2.command("doctor").description("Ch\u1EA9n \u0111o\xE1n c\xE0i \u0111\u1EB7t Avatar: hooks, MCP, login, submodule, ...").option("--fix", "T\u1EF1 \u0111\u1ED9ng fix c\xE1c issue c\xF3 th\u1EC3 fix t\u1EF1 \u0111\u1ED9ng").action(async (opts) => {
|
|
@@ -387,7 +980,7 @@ async function runChecks(cwd) {
|
|
|
387
980
|
detail: gitRepo ? cwd : "Kh\xF4ng ph\u1EA3i git repo (c\u1EA7n cho 'avatar init')",
|
|
388
981
|
fixable: false
|
|
389
982
|
});
|
|
390
|
-
const packPath =
|
|
983
|
+
const packPath = join9(cwd, ".claude", "pack");
|
|
391
984
|
const hasPack = await pathExists(packPath);
|
|
392
985
|
checks.push({
|
|
393
986
|
name: "team-ai-pack submodule",
|
|
@@ -395,7 +988,7 @@ async function runChecks(cwd) {
|
|
|
395
988
|
detail: hasPack ? packPath : "Avatar ch\u01B0a init \u2014 ch\u1EA1y 'avatar init'",
|
|
396
989
|
fixable: false
|
|
397
990
|
});
|
|
398
|
-
const claudeMdPath =
|
|
991
|
+
const claudeMdPath = join9(cwd, "CLAUDE.md");
|
|
399
992
|
const hasClaudeMd = await pathExists(claudeMdPath);
|
|
400
993
|
checks.push({
|
|
401
994
|
name: "CLAUDE.md",
|
|
@@ -403,7 +996,7 @@ async function runChecks(cwd) {
|
|
|
403
996
|
detail: hasClaudeMd ? "t\u1ED3n t\u1EA1i \u1EDF project root" : "thi\u1EBFu \u2014 ch\u1EA1y 'avatar init'",
|
|
404
997
|
fixable: false
|
|
405
998
|
});
|
|
406
|
-
const hookPath =
|
|
999
|
+
const hookPath = join9(cwd, ".git", "hooks", "post-merge");
|
|
407
1000
|
const hasHook = await pathExists(hookPath);
|
|
408
1001
|
if (gitRepo && hasPack) {
|
|
409
1002
|
checks.push({
|
|
@@ -412,15 +1005,15 @@ async function runChecks(cwd) {
|
|
|
412
1005
|
detail: hasHook ? "installed" : "missing \u2014 fixable",
|
|
413
1006
|
fixable: !hasHook,
|
|
414
1007
|
fix: hasHook ? void 0 : async () => {
|
|
415
|
-
await installGitHook(
|
|
1008
|
+
await installGitHook(join9(cwd, ".git"), "post-merge");
|
|
416
1009
|
}
|
|
417
1010
|
});
|
|
418
1011
|
}
|
|
419
|
-
const gitignorePath =
|
|
1012
|
+
const gitignorePath = join9(cwd, ".gitignore");
|
|
420
1013
|
if (gitRepo) {
|
|
421
1014
|
let gitignoreOk = false;
|
|
422
1015
|
if (await pathExists(gitignorePath)) {
|
|
423
|
-
const content = await
|
|
1016
|
+
const content = await fs6.readFile(gitignorePath, "utf8");
|
|
424
1017
|
gitignoreOk = content.includes(".claude/_pending/");
|
|
425
1018
|
}
|
|
426
1019
|
checks.push({
|
|
@@ -430,7 +1023,7 @@ async function runChecks(cwd) {
|
|
|
430
1023
|
fixable: false
|
|
431
1024
|
});
|
|
432
1025
|
}
|
|
433
|
-
const which =
|
|
1026
|
+
const which = spawnSync5("which", ["claude"]);
|
|
434
1027
|
const hasClaudeCli = which.status === 0;
|
|
435
1028
|
checks.push({
|
|
436
1029
|
name: "Claude Code CLI",
|
|
@@ -478,24 +1071,10 @@ async function applyFixes(checks) {
|
|
|
478
1071
|
}
|
|
479
1072
|
|
|
480
1073
|
// src/commands/init.ts
|
|
481
|
-
import { basename, join as
|
|
482
|
-
import { confirm, input, select } from "@inquirer/prompts";
|
|
1074
|
+
import { basename, join as join16, relative as relative2, resolve } from "path";
|
|
1075
|
+
import { confirm as confirm2, input as input2, select as select4 } from "@inquirer/prompts";
|
|
483
1076
|
import boxen2 from "boxen";
|
|
484
1077
|
|
|
485
|
-
// src/lib/audit-log-appender.ts
|
|
486
|
-
import { promises as fs4 } from "fs";
|
|
487
|
-
async function appendAuditEntry(action, detail) {
|
|
488
|
-
await ensureAvatarHome();
|
|
489
|
-
const entry = {
|
|
490
|
-
timestamp: (/* @__PURE__ */ new Date()).toISOString(),
|
|
491
|
-
action,
|
|
492
|
-
...detail ? { detail } : {}
|
|
493
|
-
};
|
|
494
|
-
const line = `${JSON.stringify(entry)}
|
|
495
|
-
`;
|
|
496
|
-
await fs4.appendFile(AUDIT_LOG_PATH, line, "utf8");
|
|
497
|
-
}
|
|
498
|
-
|
|
499
1078
|
// src/lib/avatar-ascii-banner.ts
|
|
500
1079
|
import chalk2 from "chalk";
|
|
501
1080
|
var BANNER_LINES = [
|
|
@@ -558,27 +1137,27 @@ ${renderAvatarBanner(opts)}
|
|
|
558
1137
|
}
|
|
559
1138
|
|
|
560
1139
|
// src/lib/execute-gh-repo-create.ts
|
|
561
|
-
import { spawnSync as
|
|
1140
|
+
import { spawnSync as spawnSync6 } from "child_process";
|
|
562
1141
|
var RepoAlreadyExistsError = class extends Error {
|
|
563
1142
|
constructor(fullName) {
|
|
564
1143
|
super(`Repo "${fullName}" \u0111\xE3 t\u1ED3n t\u1EA1i tr\xEAn GitHub. \u0110\u1ED5i t\xEAn ho\u1EB7c x\xF3a repo c\u0169.`);
|
|
565
1144
|
this.name = "RepoAlreadyExistsError";
|
|
566
1145
|
}
|
|
567
1146
|
};
|
|
568
|
-
function executeGhRepoCreate(
|
|
569
|
-
const fullName = `${
|
|
1147
|
+
function executeGhRepoCreate(input3) {
|
|
1148
|
+
const fullName = `${input3.org}/${input3.name}`;
|
|
570
1149
|
const args = [
|
|
571
1150
|
"repo",
|
|
572
1151
|
"create",
|
|
573
1152
|
fullName,
|
|
574
|
-
`--${
|
|
1153
|
+
`--${input3.visibility}`,
|
|
575
1154
|
"--source",
|
|
576
|
-
|
|
1155
|
+
input3.folder,
|
|
577
1156
|
"--remote",
|
|
578
1157
|
"origin",
|
|
579
1158
|
"--push"
|
|
580
1159
|
];
|
|
581
|
-
const r =
|
|
1160
|
+
const r = spawnSync6("gh", args, { stdio: "inherit" });
|
|
582
1161
|
if (r.status !== 0) {
|
|
583
1162
|
if (r.status === 1) {
|
|
584
1163
|
throw new RepoAlreadyExistsError(fullName);
|
|
@@ -592,9 +1171,9 @@ function executeGhRepoCreate(input2) {
|
|
|
592
1171
|
}
|
|
593
1172
|
|
|
594
1173
|
// src/lib/resolve-github-username-default.ts
|
|
595
|
-
import { spawnSync as
|
|
1174
|
+
import { spawnSync as spawnSync7 } from "child_process";
|
|
596
1175
|
function resolveGithubUsernameDefault() {
|
|
597
|
-
const r =
|
|
1176
|
+
const r = spawnSync7("gh", ["api", "user", "--jq", ".login"], {
|
|
598
1177
|
encoding: "utf8",
|
|
599
1178
|
stdio: ["ignore", "pipe", "pipe"]
|
|
600
1179
|
});
|
|
@@ -626,25 +1205,28 @@ function validateRepoVisibility(v) {
|
|
|
626
1205
|
}
|
|
627
1206
|
|
|
628
1207
|
// src/lib/create-github-remote-from-folder.ts
|
|
629
|
-
function createGithubRemoteFromFolder(
|
|
630
|
-
validateRepoName(
|
|
631
|
-
validateRepoVisibility(
|
|
632
|
-
const org =
|
|
633
|
-
log.info(`T\u1EA1o GitHub repo ${org}/${
|
|
1208
|
+
function createGithubRemoteFromFolder(input3) {
|
|
1209
|
+
validateRepoName(input3.name);
|
|
1210
|
+
validateRepoVisibility(input3.visibility);
|
|
1211
|
+
const org = input3.org ?? resolveGithubUsernameDefault();
|
|
1212
|
+
log.info(`T\u1EA1o GitHub repo ${org}/${input3.name} (${input3.visibility})...`);
|
|
634
1213
|
const urls = executeGhRepoCreate({
|
|
635
|
-
folder:
|
|
1214
|
+
folder: input3.folder,
|
|
636
1215
|
org,
|
|
637
|
-
name:
|
|
638
|
-
visibility:
|
|
1216
|
+
name: input3.name,
|
|
1217
|
+
visibility: input3.visibility
|
|
639
1218
|
});
|
|
640
1219
|
log.success(`\u0110\xE3 t\u1EA1o: ${urls.sshUrl}`);
|
|
641
1220
|
return urls;
|
|
642
1221
|
}
|
|
643
1222
|
|
|
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
|
|
1227
|
+
import { spawnSync as spawnSync8 } from "child_process";
|
|
646
1228
|
function checkGhCliAuthStatus() {
|
|
647
|
-
const r =
|
|
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
|
|
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 =
|
|
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
|
|
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 =
|
|
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
|
|
1277
|
+
import { spawnSync as spawnSync11 } from "child_process";
|
|
706
1278
|
function setupGitCredentialViaGh() {
|
|
707
|
-
const r =
|
|
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
|
|
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 =
|
|
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
|
|
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 =
|
|
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
|
|
1392
|
+
import { join as join10 } from "path";
|
|
782
1393
|
function checkFolderHasGit(folderPath) {
|
|
783
|
-
const gitPath =
|
|
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
|
|
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(
|
|
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
|
|
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
|
-
|
|
841
|
-
|
|
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
|
|
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
|
|
863
|
-
import { join as
|
|
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 =
|
|
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 =
|
|
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/
|
|
887
|
-
|
|
888
|
-
|
|
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
|
|
1571
|
+
log.info(`Tech stack: ${stacks.join(", ")}`);
|
|
891
1572
|
writeOrMergeGitignore(folderPath, composeGitignoreContent(stacks));
|
|
892
1573
|
log.success(".gitignore \u0111\xE3 ghi (Avatar block)");
|
|
893
|
-
|
|
894
|
-
|
|
895
|
-
|
|
896
|
-
|
|
897
|
-
|
|
898
|
-
|
|
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
|
|
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
|
|
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(
|
|
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 =
|
|
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
|
|
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 =
|
|
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("--
|
|
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
|
|
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
|
|
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
|
|
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
|
-
|
|
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
|
|
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
|
|
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
|
|
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
|
-
|
|
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
|
|
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
|
|
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 =
|
|
1886
|
+
const srcPath = join16(workspacePath, "src");
|
|
1109
1887
|
await ensureDir(workspacePath);
|
|
1110
1888
|
await ensureDir(srcPath);
|
|
1111
|
-
await
|
|
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
|
-
|
|
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
|
|
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
|
|
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
|
|
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
|
-
|
|
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(
|
|
1227
|
-
await ensureDir(
|
|
1228
|
-
await installGitHook(
|
|
1229
|
-
await installGitHook(
|
|
2012
|
+
await ensureDir(join16(args.workspacePath, "notes"));
|
|
2013
|
+
await ensureDir(join16(args.workspacePath, "scripts"));
|
|
2014
|
+
await installGitHook(join16(args.workspacePath, ".git"), "post-merge");
|
|
2015
|
+
await installGitHook(join16(args.workspacePath, ".git", "modules", "src"), "pre-push");
|
|
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
|
-
|
|
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 =
|
|
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
|
|
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
|
|
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
|
|
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
|
|
1503
|
-
import { join as
|
|
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
|
|
1508
|
-
import { join as
|
|
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 =
|
|
2348
|
+
const dir = join17(projectRoot, ".claude", BACKUP_DIR_NAME);
|
|
1512
2349
|
if (!await pathExists(dir)) return [];
|
|
1513
|
-
const entries = await
|
|
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 =
|
|
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(
|
|
1551
|
-
const pendingDir =
|
|
1552
|
-
const pendingCount = await pathExists(pendingDir) ? (await
|
|
2387
|
+
const packVersion = await isGitRepo(join18(claudeRoot, "pack")) ? await readPinnedPackVersion(cwd).catch(() => null) : null;
|
|
2388
|
+
const pendingDir = join18(claudeRoot, "_pending");
|
|
2389
|
+
const pendingCount = await pathExists(pendingDir) ? (await fs8.readdir(pendingDir)).filter((n) => n.endsWith(".diff.md")).length : 0;
|
|
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 =
|
|
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
|
|
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
|
|
1607
|
-
import { basename as basename2, join as
|
|
1608
|
-
var UNINSTALL_BACKUPS_DIR =
|
|
2443
|
+
import { homedir as homedir3 } from "os";
|
|
2444
|
+
import { basename as basename2, join as join19 } from "path";
|
|
2445
|
+
var UNINSTALL_BACKUPS_DIR = join19(homedir3(), ".avatar", "uninstall-backups");
|
|
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 =
|
|
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,
|
|
2452
|
+
await cp(artifacts.claudeDir, join19(backupDir, ".claude"), { recursive: true });
|
|
1616
2453
|
}
|
|
1617
2454
|
if (artifacts.claudeMd) {
|
|
1618
|
-
await cp(artifacts.claudeMd,
|
|
2455
|
+
await cp(artifacts.claudeMd, join19(backupDir, "CLAUDE.md"));
|
|
1619
2456
|
}
|
|
1620
2457
|
if (artifacts.postMergeHook || artifacts.prePushHook) {
|
|
1621
|
-
const hooksBackupDir =
|
|
2458
|
+
const hooksBackupDir = join19(backupDir, "hooks");
|
|
1622
2459
|
await mkdir(hooksBackupDir, { recursive: true });
|
|
1623
2460
|
if (artifacts.postMergeHook) {
|
|
1624
|
-
await cp(artifacts.postMergeHook,
|
|
2461
|
+
await cp(artifacts.postMergeHook, join19(hooksBackupDir, "post-merge"));
|
|
1625
2462
|
}
|
|
1626
2463
|
if (artifacts.prePushHook) {
|
|
1627
|
-
await cp(artifacts.prePushHook,
|
|
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(
|
|
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
|
|
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(
|
|
1654
|
-
const claudeMd = existsOrNull(
|
|
1655
|
-
const postMergeHook = existsOrNull(
|
|
2490
|
+
const claudeDir = existsOrNull(join20(projectRoot, ".claude"));
|
|
2491
|
+
const claudeMd = existsOrNull(join20(projectRoot, "CLAUDE.md"));
|
|
2492
|
+
const postMergeHook = existsOrNull(join20(projectRoot, ".git", "hooks", "post-merge"));
|
|
1656
2493
|
const prePushHook = existsOrNull(
|
|
1657
|
-
|
|
2494
|
+
join20(projectRoot, ".git", "modules", "src", "hooks", "pre-push")
|
|
1658
2495
|
);
|
|
1659
|
-
const gitignorePath = existsOrNull(
|
|
1660
|
-
const gitmodulesPath = existsOrNull(
|
|
1661
|
-
const notesDir = existsOrNull(
|
|
1662
|
-
const scriptsDir = existsOrNull(
|
|
2496
|
+
const gitignorePath = existsOrNull(join20(projectRoot, ".gitignore"));
|
|
2497
|
+
const gitmodulesPath = existsOrNull(join20(projectRoot, ".gitmodules"));
|
|
2498
|
+
const notesDir = existsOrNull(join20(projectRoot, "notes"));
|
|
2499
|
+
const scriptsDir = existsOrNull(join20(projectRoot, "scripts"));
|
|
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:
|
|
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(
|
|
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.
|
|
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
|
|
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.
|
|
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);
|