@nalvietnam/avatar-cli 1.11.1 → 1.12.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 +1956 -1817
- package/dist/index.js.map +1 -1
- package/dist/lib/print-welcome-screen.js +1 -13
- package/dist/lib/print-welcome-screen.js.map +1 -1
- package/package.json +2 -2
package/dist/index.js
CHANGED
|
@@ -145,8 +145,8 @@ async function writeUserConfig(config) {
|
|
|
145
145
|
}
|
|
146
146
|
async function clearUserConfig() {
|
|
147
147
|
if (await pathExists(USER_CONFIG_PATH)) {
|
|
148
|
-
const { promises:
|
|
149
|
-
await
|
|
148
|
+
const { promises: fs15 } = await import("fs");
|
|
149
|
+
await fs15.unlink(USER_CONFIG_PATH);
|
|
150
150
|
}
|
|
151
151
|
}
|
|
152
152
|
function isTokenExpired(config) {
|
|
@@ -291,7 +291,8 @@ function getQuotaErrorHint(reason) {
|
|
|
291
291
|
function verifyClaudeCodeQuota() {
|
|
292
292
|
const result = spawnSync("claude", ["--print", QUOTA_VERIFY_PROMPT], {
|
|
293
293
|
encoding: "utf8",
|
|
294
|
-
timeout: QUOTA_VERIFY_TIMEOUT_MS
|
|
294
|
+
timeout: QUOTA_VERIFY_TIMEOUT_MS,
|
|
295
|
+
stdio: ["ignore", "pipe", "pipe"]
|
|
295
296
|
});
|
|
296
297
|
if (result.signal === "SIGTERM") {
|
|
297
298
|
return { ok: false, reason: "timeout", detail: "claude --print > 30s" };
|
|
@@ -301,12 +302,24 @@ function verifyClaudeCodeQuota() {
|
|
|
301
302
|
if (result.status === 0) {
|
|
302
303
|
return { ok: true };
|
|
303
304
|
}
|
|
305
|
+
const stdoutTrimmed = stdout.trim();
|
|
306
|
+
const stderrLower = stderr.toLowerCase();
|
|
307
|
+
if (stdoutTrimmed.length > 20 && !stderrLower.includes("error") && !stderrLower.includes("limit") && !stderrLower.includes("quota") && !stderrLower.includes("401")) {
|
|
308
|
+
log.warn(
|
|
309
|
+
`claude --print exit=${result.status} nh\u01B0ng c\xF3 response (${stdoutTrimmed.length} chars). Accept v\u1EDBi caution.`
|
|
310
|
+
);
|
|
311
|
+
return { ok: true };
|
|
312
|
+
}
|
|
304
313
|
const reason = classifyQuotaError(`${stderr}
|
|
305
314
|
${stdout}`);
|
|
306
|
-
if (reason === "unknown"
|
|
307
|
-
log.
|
|
315
|
+
if (reason === "unknown") {
|
|
316
|
+
log.warn(
|
|
317
|
+
`[debug] claude --print exit=${result.status} signal=${result.signal ?? "none"}`
|
|
318
|
+
);
|
|
319
|
+
if (stderr.trim()) log.warn(`[debug] stderr: ${stderr.slice(0, 500)}`);
|
|
320
|
+
if (stdout.trim()) log.warn(`[debug] stdout: ${stdout.slice(0, 300)}`);
|
|
308
321
|
}
|
|
309
|
-
return { ok: false, reason, detail: stderr.slice(0, 500) };
|
|
322
|
+
return { ok: false, reason, detail: stderr.slice(0, 500) || stdout.slice(0, 500) };
|
|
310
323
|
}
|
|
311
324
|
|
|
312
325
|
// src/lib/detect-claude-code-installation.ts
|
|
@@ -340,7 +353,7 @@ function probeClaudeVersion() {
|
|
|
340
353
|
if (result.error || result.status !== 0) return null;
|
|
341
354
|
const out = (result.stdout || "").trim();
|
|
342
355
|
const match = SEMVER_REGEX.exec(out);
|
|
343
|
-
return match
|
|
356
|
+
return match?.[1] ?? null;
|
|
344
357
|
}
|
|
345
358
|
function detectClaudeCodeInstallation() {
|
|
346
359
|
const path = probeClaudeBinaryPath();
|
|
@@ -556,8 +569,9 @@ async function fetchAnthropicModels(apiKey) {
|
|
|
556
569
|
}
|
|
557
570
|
async function promptAnthropicModelChoice(models) {
|
|
558
571
|
if (models.length === 1) {
|
|
559
|
-
|
|
560
|
-
|
|
572
|
+
const only = models[0];
|
|
573
|
+
log.info(`Auto-pick model: ${only} (ch\u1EC9 1 model available)`);
|
|
574
|
+
return only;
|
|
561
575
|
}
|
|
562
576
|
const sorted = [...models].sort((a, b) => {
|
|
563
577
|
const score = (m) => {
|
|
@@ -645,8 +659,9 @@ async function fetchAvailableModels(baseUrl, apiKey) {
|
|
|
645
659
|
async function promptModelChoice(models) {
|
|
646
660
|
const claudeAliases = models.filter((m) => m.toLowerCase().includes("claude"));
|
|
647
661
|
if (claudeAliases.length === 1) {
|
|
648
|
-
|
|
649
|
-
|
|
662
|
+
const only = claudeAliases[0];
|
|
663
|
+
log.info(`Auto-pick model: ${only} (ch\u1EC9 1 claude alias tr\xEAn endpoint)`);
|
|
664
|
+
return only;
|
|
650
665
|
}
|
|
651
666
|
const choiceList = claudeAliases.length > 0 ? claudeAliases : models;
|
|
652
667
|
return await select3({
|
|
@@ -733,22 +748,22 @@ function applyUseGlobal(existing, source) {
|
|
|
733
748
|
...sourceModel ? { model: sourceModel } : {}
|
|
734
749
|
};
|
|
735
750
|
}
|
|
736
|
-
async function writeClaudeSettings(workspacePath,
|
|
751
|
+
async function writeClaudeSettings(workspacePath, input8) {
|
|
737
752
|
const path = getClaudeSettingsPath(workspacePath);
|
|
738
753
|
const existing = await readExistingSettings(path);
|
|
739
754
|
let merged;
|
|
740
|
-
switch (
|
|
755
|
+
switch (input8.provider) {
|
|
741
756
|
case "subscription":
|
|
742
|
-
merged = applySubscription(existing,
|
|
757
|
+
merged = applySubscription(existing, input8.model);
|
|
743
758
|
break;
|
|
744
759
|
case "llmlite":
|
|
745
|
-
merged = applyLLMLite(existing,
|
|
760
|
+
merged = applyLLMLite(existing, input8.apiKey, input8.baseUrl, input8.model);
|
|
746
761
|
break;
|
|
747
762
|
case "anthropic":
|
|
748
|
-
merged = applyAnthropic(existing,
|
|
763
|
+
merged = applyAnthropic(existing, input8.apiKey, input8.baseUrl, input8.model);
|
|
749
764
|
break;
|
|
750
765
|
case "use-global":
|
|
751
|
-
merged = applyUseGlobal(existing,
|
|
766
|
+
merged = applyUseGlobal(existing, input8.sourceSettings);
|
|
752
767
|
break;
|
|
753
768
|
}
|
|
754
769
|
await writeJsonAtomic(path, merged, SECRET_FILE_MODE2);
|
|
@@ -803,7 +818,10 @@ async function runAiSetupPhase(args) {
|
|
|
803
818
|
`provider=subscription,result=no-quota,reason=${reason}`
|
|
804
819
|
);
|
|
805
820
|
log.warn(`Subscription verify th\u1EA5t b\u1EA1i (${reason}).`);
|
|
806
|
-
|
|
821
|
+
if (quota.detail?.trim()) {
|
|
822
|
+
log.warn(` Chi ti\u1EBFt: ${quota.detail.slice(0, 200)}`);
|
|
823
|
+
}
|
|
824
|
+
log.warn(` \u2192 ${getQuotaErrorHint(reason)}`);
|
|
807
825
|
return { ok: false, reason: `subscription-${reason}`, phase: "quota" };
|
|
808
826
|
}
|
|
809
827
|
await writeClaudeSettings(args.workspacePath, {
|
|
@@ -1765,7 +1783,7 @@ function probeGitnexusVersion() {
|
|
|
1765
1783
|
if (result.error || result.status !== 0) return null;
|
|
1766
1784
|
const out = (result.stdout || "").trim();
|
|
1767
1785
|
const match = SEMVER_REGEX2.exec(out);
|
|
1768
|
-
return match
|
|
1786
|
+
return match?.[1] ?? null;
|
|
1769
1787
|
}
|
|
1770
1788
|
function detectGitnexusInstallation() {
|
|
1771
1789
|
const path = probeGitnexusBinaryPath();
|
|
@@ -2311,641 +2329,910 @@ function registerGitnexusCommand(program2) {
|
|
|
2311
2329
|
}
|
|
2312
2330
|
|
|
2313
2331
|
// src/commands/init.ts
|
|
2314
|
-
import {
|
|
2315
|
-
import { confirm as confirm5, input as input5, select as select9 } from "@inquirer/prompts";
|
|
2316
|
-
import boxen6 from "boxen";
|
|
2332
|
+
import { select as select13 } from "@inquirer/prompts";
|
|
2317
2333
|
|
|
2318
|
-
// src/lib/
|
|
2319
|
-
import
|
|
2320
|
-
|
|
2334
|
+
// src/lib/avatar-ascii-banner.ts
|
|
2335
|
+
import chalk2 from "chalk";
|
|
2336
|
+
var BANNER_LINES = [
|
|
2337
|
+
" \u2588\u2588\u2588\u2588\u2588\u2557 \u2588\u2588\u2557 \u2588\u2588\u2557 \u2588\u2588\u2588\u2588\u2588\u2557 \u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2557 \u2588\u2588\u2588\u2588\u2588\u2557 \u2588\u2588\u2588\u2588\u2588\u2588\u2557 ",
|
|
2338
|
+
"\u2588\u2588\u2554\u2550\u2550\u2588\u2588\u2557\u2588\u2588\u2551 \u2588\u2588\u2551\u2588\u2588\u2554\u2550\u2550\u2588\u2588\u2557\u255A\u2550\u2550\u2588\u2588\u2554\u2550\u2550\u255D\u2588\u2588\u2554\u2550\u2550\u2588\u2588\u2557\u2588\u2588\u2554\u2550\u2550\u2588\u2588\u2557",
|
|
2339
|
+
"\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2551\u2588\u2588\u2551 \u2588\u2588\u2551\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2551 \u2588\u2588\u2551 \u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2551\u2588\u2588\u2588\u2588\u2588\u2588\u2554\u255D",
|
|
2340
|
+
"\u2588\u2588\u2554\u2550\u2550\u2588\u2588\u2551\u255A\u2588\u2588\u2557 \u2588\u2588\u2554\u255D\u2588\u2588\u2554\u2550\u2550\u2588\u2588\u2551 \u2588\u2588\u2551 \u2588\u2588\u2554\u2550\u2550\u2588\u2588\u2551\u2588\u2588\u2554\u2550\u2550\u2588\u2588\u2557",
|
|
2341
|
+
"\u2588\u2588\u2551 \u2588\u2588\u2551 \u255A\u2588\u2588\u2588\u2588\u2554\u255D \u2588\u2588\u2551 \u2588\u2588\u2551 \u2588\u2588\u2551 \u2588\u2588\u2551 \u2588\u2588\u2551\u2588\u2588\u2551 \u2588\u2588\u2551",
|
|
2342
|
+
"\u255A\u2550\u255D \u255A\u2550\u255D \u255A\u2550\u2550\u2550\u255D \u255A\u2550\u255D \u255A\u2550\u255D \u255A\u2550\u255D \u255A\u2550\u255D \u255A\u2550\u255D\u255A\u2550\u255D \u255A\u2550\u255D"
|
|
2343
|
+
];
|
|
2344
|
+
var GRADIENT_STOPS = [
|
|
2345
|
+
[217, 79, 30],
|
|
2346
|
+
// cam-cháy (#d94f1e)
|
|
2347
|
+
[200, 70, 80],
|
|
2348
|
+
// cam-hồng
|
|
2349
|
+
[170, 70, 140],
|
|
2350
|
+
// hồng-tím
|
|
2351
|
+
[125, 88, 217]
|
|
2352
|
+
// tím (#7d58d9)
|
|
2353
|
+
];
|
|
2354
|
+
function lerpChannel(a, b, t) {
|
|
2355
|
+
return Math.round(a + (b - a) * t);
|
|
2356
|
+
}
|
|
2357
|
+
function gradientAt(t) {
|
|
2358
|
+
const clamped = Math.max(0, Math.min(1, t));
|
|
2359
|
+
const scaled = clamped * (GRADIENT_STOPS.length - 1);
|
|
2360
|
+
const lo = Math.floor(scaled);
|
|
2361
|
+
const hi = Math.min(GRADIENT_STOPS.length - 1, lo + 1);
|
|
2362
|
+
const localT = scaled - lo;
|
|
2363
|
+
const a = GRADIENT_STOPS[lo];
|
|
2364
|
+
const b = GRADIENT_STOPS[hi];
|
|
2365
|
+
return [
|
|
2366
|
+
lerpChannel(a[0], b[0], localT),
|
|
2367
|
+
lerpChannel(a[1], b[1], localT),
|
|
2368
|
+
lerpChannel(a[2], b[2], localT)
|
|
2369
|
+
];
|
|
2370
|
+
}
|
|
2371
|
+
function renderAvatarBanner(opts) {
|
|
2372
|
+
const isTty = process.stdout.isTTY ?? false;
|
|
2373
|
+
const supportsColor = isTty && chalk2.level > 0;
|
|
2374
|
+
if (!supportsColor) {
|
|
2375
|
+
return [...BANNER_LINES, ...opts?.tagline ? ["", opts.tagline] : []].join("\n");
|
|
2376
|
+
}
|
|
2377
|
+
const colored = BANNER_LINES.map((line, idx) => {
|
|
2378
|
+
const t = BANNER_LINES.length === 1 ? 0 : idx / (BANNER_LINES.length - 1);
|
|
2379
|
+
const [r, g, b] = gradientAt(t);
|
|
2380
|
+
return chalk2.rgb(r, g, b).bold(line);
|
|
2381
|
+
});
|
|
2382
|
+
if (opts?.tagline) {
|
|
2383
|
+
colored.push("");
|
|
2384
|
+
colored.push(chalk2.dim(opts.tagline));
|
|
2385
|
+
}
|
|
2386
|
+
return colored.join("\n");
|
|
2387
|
+
}
|
|
2388
|
+
function printAvatarBanner(opts) {
|
|
2389
|
+
process.stdout.write(`
|
|
2390
|
+
${renderAvatarBanner(opts)}
|
|
2321
2391
|
|
|
2322
|
-
|
|
2323
|
-
|
|
2392
|
+
`);
|
|
2393
|
+
}
|
|
2324
2394
|
|
|
2325
|
-
// src/lib/
|
|
2326
|
-
import { spawnSync as
|
|
2395
|
+
// src/lib/handle-remote-access-failure-with-account-switch.ts
|
|
2396
|
+
import { spawnSync as spawnSync16 } from "child_process";
|
|
2397
|
+
import { input as input4, select as select6 } from "@inquirer/prompts";
|
|
2398
|
+
|
|
2399
|
+
// src/lib/reset-folder-git-and-create-new-remote-under-current-user.ts
|
|
2400
|
+
import { spawnSync as spawnSync14 } from "child_process";
|
|
2401
|
+
import { promises as fs9 } from "fs";
|
|
2402
|
+
import { basename, join as join16 } from "path";
|
|
2327
2403
|
import { confirm as confirm4, select as select5 } from "@inquirer/prompts";
|
|
2328
|
-
|
|
2329
|
-
|
|
2330
|
-
|
|
2331
|
-
|
|
2332
|
-
|
|
2333
|
-
}
|
|
2334
|
-
|
|
2335
|
-
|
|
2336
|
-
|
|
2404
|
+
|
|
2405
|
+
// src/lib/execute-gh-repo-create.ts
|
|
2406
|
+
import { spawnSync as spawnSync12 } from "child_process";
|
|
2407
|
+
var RepoAlreadyExistsError = class extends Error {
|
|
2408
|
+
constructor(fullName) {
|
|
2409
|
+
super(`Repo "${fullName}" \u0111\xE3 t\u1ED3n t\u1EA1i tr\xEAn GitHub. \u0110\u1ED5i t\xEAn ho\u1EB7c x\xF3a repo c\u0169.`);
|
|
2410
|
+
this.name = "RepoAlreadyExistsError";
|
|
2411
|
+
}
|
|
2412
|
+
};
|
|
2413
|
+
function executeGhRepoCreate(input8) {
|
|
2414
|
+
const fullName = `${input8.org}/${input8.name}`;
|
|
2415
|
+
const args = [
|
|
2416
|
+
"repo",
|
|
2417
|
+
"create",
|
|
2418
|
+
fullName,
|
|
2419
|
+
`--${input8.visibility}`,
|
|
2420
|
+
"--source",
|
|
2421
|
+
input8.folder,
|
|
2422
|
+
"--remote",
|
|
2423
|
+
"origin",
|
|
2424
|
+
"--push"
|
|
2425
|
+
];
|
|
2426
|
+
const r = spawnSync12("gh", args, { stdio: "inherit" });
|
|
2427
|
+
if (r.status !== 0) {
|
|
2428
|
+
if (r.status === 1) {
|
|
2429
|
+
throw new RepoAlreadyExistsError(fullName);
|
|
2430
|
+
}
|
|
2431
|
+
throw new Error(`gh repo create th\u1EA5t b\u1EA1i (exit ${r.status})`);
|
|
2432
|
+
}
|
|
2433
|
+
return {
|
|
2434
|
+
sshUrl: `git@github.com:${fullName}.git`,
|
|
2435
|
+
httpsUrl: `https://github.com/${fullName}.git`
|
|
2436
|
+
};
|
|
2337
2437
|
}
|
|
2338
|
-
|
|
2339
|
-
|
|
2438
|
+
|
|
2439
|
+
// src/lib/resolve-github-username-default.ts
|
|
2440
|
+
import { spawnSync as spawnSync13 } from "child_process";
|
|
2441
|
+
function resolveGithubUsernameDefault() {
|
|
2442
|
+
const r = spawnSync13("gh", ["api", "user", "--jq", ".login"], {
|
|
2340
2443
|
encoding: "utf8",
|
|
2341
2444
|
stdio: ["ignore", "pipe", "pipe"]
|
|
2342
2445
|
});
|
|
2343
|
-
if (r.status !== 0) return null;
|
|
2344
|
-
return r.stdout.trim() || null;
|
|
2345
|
-
}
|
|
2346
|
-
function triggerGhAuthLoginInteractive() {
|
|
2347
|
-
log.info("M\u1EDF browser \u0111\u1EC3 switch GitHub account...");
|
|
2348
|
-
const r = spawnSync12("gh", ["auth", "login", "--web"], { stdio: "inherit" });
|
|
2349
2446
|
if (r.status !== 0) {
|
|
2350
|
-
|
|
2447
|
+
throw new Error(`Kh\xF4ng l\u1EA5y \u0111\u01B0\u1EE3c GitHub username: ${r.stderr?.trim()}`);
|
|
2351
2448
|
}
|
|
2449
|
+
return r.stdout.trim();
|
|
2352
2450
|
}
|
|
2353
|
-
|
|
2354
|
-
|
|
2355
|
-
|
|
2356
|
-
|
|
2357
|
-
|
|
2358
|
-
|
|
2359
|
-
|
|
2360
|
-
|
|
2361
|
-
|
|
2362
|
-
|
|
2363
|
-
|
|
2364
|
-
|
|
2451
|
+
|
|
2452
|
+
// src/lib/validate-repo-name-and-visibility.ts
|
|
2453
|
+
var REPO_NAME_REGEX = /^[a-zA-Z0-9._-]{1,100}$/;
|
|
2454
|
+
var InvalidRepoNameError = class extends Error {
|
|
2455
|
+
constructor(name) {
|
|
2456
|
+
super(
|
|
2457
|
+
`T\xEAn repo "${name}" kh\xF4ng h\u1EE3p l\u1EC7. Ch\u1EC9 d\xF9ng ch\u1EEF/s\u1ED1/d\u1EA5u ch\u1EA5m/g\u1EA1ch/underscore, d\xE0i 1-100 k\xFD t\u1EF1.`
|
|
2458
|
+
);
|
|
2459
|
+
this.name = "InvalidRepoNameError";
|
|
2460
|
+
}
|
|
2461
|
+
};
|
|
2462
|
+
function validateRepoName(name) {
|
|
2463
|
+
if (!REPO_NAME_REGEX.test(name)) {
|
|
2464
|
+
throw new InvalidRepoNameError(name);
|
|
2365
2465
|
}
|
|
2366
2466
|
}
|
|
2367
|
-
function
|
|
2368
|
-
|
|
2369
|
-
|
|
2370
|
-
|
|
2371
|
-
`Repo: ${chalk.bold(repoSlug)}`,
|
|
2372
|
-
"",
|
|
2373
|
-
"B\u1EA1n c\u1EA7n \u0111\u01B0\u1EE3c admin add v\xE0o org \u0111\u1EC3 pull team-ai-pack.",
|
|
2374
|
-
"",
|
|
2375
|
-
`${chalk.dim("Th\xF4ng tin g\u1EEDi admin:")}`,
|
|
2376
|
-
` GitHub username: ${chalk.cyan(ghUser ?? "(ch\u01B0a gh auth \u2014 ch\u1EA1y: gh auth login)")}`,
|
|
2377
|
-
` NAL email: ${chalk.cyan(ssoEmail ?? "(ch\u01B0a avatar login \u2014 ch\u1EA1y: avatar login)")}`,
|
|
2378
|
-
` Date: ${(/* @__PURE__ */ new Date()).toISOString().split("T")[0]}`,
|
|
2379
|
-
"",
|
|
2380
|
-
`${chalk.dim("Li\xEAn h\u1EC7:")} luke@nal.vn (Slack #avatar-setup)`
|
|
2381
|
-
];
|
|
2382
|
-
process.stdout.write(
|
|
2383
|
-
`${boxen3(lines.join("\n"), { padding: 1, borderColor: "red", borderStyle: "round" })}
|
|
2384
|
-
`
|
|
2385
|
-
);
|
|
2467
|
+
function validateRepoVisibility(v) {
|
|
2468
|
+
if (v !== "private" && v !== "public") {
|
|
2469
|
+
throw new Error(`Visibility ph\u1EA3i l\xE0 "private" ho\u1EB7c "public", nh\u1EADn: "${v}"`);
|
|
2470
|
+
}
|
|
2386
2471
|
}
|
|
2387
|
-
|
|
2388
|
-
|
|
2389
|
-
|
|
2390
|
-
|
|
2391
|
-
|
|
2392
|
-
|
|
2393
|
-
|
|
2394
|
-
|
|
2472
|
+
|
|
2473
|
+
// src/lib/create-github-remote-from-folder.ts
|
|
2474
|
+
function createGithubRemoteFromFolder(input8) {
|
|
2475
|
+
validateRepoName(input8.name);
|
|
2476
|
+
validateRepoVisibility(input8.visibility);
|
|
2477
|
+
const org = input8.org ?? resolveGithubUsernameDefault();
|
|
2478
|
+
log.info(`T\u1EA1o GitHub repo ${org}/${input8.name} (${input8.visibility})...`);
|
|
2479
|
+
const urls = executeGhRepoCreate({
|
|
2480
|
+
folder: input8.folder,
|
|
2481
|
+
org,
|
|
2482
|
+
name: input8.name,
|
|
2483
|
+
visibility: input8.visibility
|
|
2484
|
+
});
|
|
2485
|
+
log.success(`\u0110\xE3 t\u1EA1o: ${urls.sshUrl}`);
|
|
2486
|
+
return urls;
|
|
2395
2487
|
}
|
|
2396
|
-
|
|
2397
|
-
|
|
2398
|
-
|
|
2399
|
-
|
|
2400
|
-
|
|
2401
|
-
|
|
2488
|
+
|
|
2489
|
+
// src/lib/reset-folder-git-and-create-new-remote-under-current-user.ts
|
|
2490
|
+
function backupTimestamp() {
|
|
2491
|
+
const d = /* @__PURE__ */ new Date();
|
|
2492
|
+
return `${d.getFullYear().toString().slice(-2)}${String(d.getMonth() + 1).padStart(2, "0")}${String(d.getDate()).padStart(2, "0")}-${String(d.getHours()).padStart(2, "0")}${String(d.getMinutes()).padStart(2, "0")}`;
|
|
2493
|
+
}
|
|
2494
|
+
async function backupExistingDotGit(folderPath) {
|
|
2495
|
+
const gitDir = join16(folderPath, ".git");
|
|
2496
|
+
if (!await pathExists(gitDir)) {
|
|
2497
|
+
throw new Error(`.git kh\xF4ng t\u1ED3n t\u1EA1i \u1EDF ${folderPath} \u2014 kh\xF4ng c\u1EA7n reset.`);
|
|
2498
|
+
}
|
|
2499
|
+
const backupName = `.git.backup-${backupTimestamp()}`;
|
|
2500
|
+
const backupPath = join16(folderPath, backupName);
|
|
2501
|
+
await fs9.rename(gitDir, backupPath);
|
|
2502
|
+
log.success(`Backup .git \u2192 ${backupName}`);
|
|
2503
|
+
return backupPath;
|
|
2504
|
+
}
|
|
2505
|
+
function reinitGitInFolder(folderPath) {
|
|
2506
|
+
const r1 = spawnSync14("git", ["-C", folderPath, "init", "-b", "main"], {
|
|
2507
|
+
encoding: "utf8",
|
|
2508
|
+
stdio: ["ignore", "pipe", "pipe"]
|
|
2509
|
+
});
|
|
2510
|
+
if (r1.status !== 0) {
|
|
2511
|
+
throw new Error(`git init th\u1EA5t b\u1EA1i: ${r1.stderr || r1.stdout}`);
|
|
2512
|
+
}
|
|
2513
|
+
log.success("Git init m\u1EDBi (branch main)");
|
|
2514
|
+
spawnSync14("git", ["-C", folderPath, "add", "-A"], {
|
|
2515
|
+
encoding: "utf8",
|
|
2516
|
+
stdio: ["ignore", "pipe", "pipe"]
|
|
2517
|
+
});
|
|
2518
|
+
const r3 = spawnSync14(
|
|
2519
|
+
"git",
|
|
2520
|
+
["-C", folderPath, "commit", "-m", "chore: initial commit from existing folder"],
|
|
2521
|
+
{ encoding: "utf8", stdio: ["ignore", "pipe", "pipe"] }
|
|
2402
2522
|
);
|
|
2403
|
-
|
|
2404
|
-
|
|
2405
|
-
|
|
2406
|
-
|
|
2407
|
-
|
|
2408
|
-
|
|
2409
|
-
|
|
2410
|
-
|
|
2411
|
-
|
|
2412
|
-
|
|
2413
|
-
|
|
2414
|
-
|
|
2415
|
-
|
|
2416
|
-
|
|
2417
|
-
|
|
2418
|
-
|
|
2419
|
-
|
|
2420
|
-
}
|
|
2421
|
-
]
|
|
2523
|
+
if (r3.status !== 0) {
|
|
2524
|
+
log.warn(`git commit th\u1EA5t b\u1EA1i (folder c\xF3 th\u1EC3 r\u1ED7ng): ${(r3.stderr || "").slice(0, 200)}`);
|
|
2525
|
+
} else {
|
|
2526
|
+
log.success("Initial commit");
|
|
2527
|
+
}
|
|
2528
|
+
}
|
|
2529
|
+
async function resetFolderGitAndCreateNewRemoteUnderCurrentUser(opts) {
|
|
2530
|
+
const folderName = basename(opts.folderPath);
|
|
2531
|
+
const repoName = opts.repoName ?? folderName;
|
|
2532
|
+
if (!opts.autoYes) {
|
|
2533
|
+
const confirmed = await confirm4({
|
|
2534
|
+
message: `Folder '${folderName}' s\u1EBD \u0111\u01B0\u1EE3c reset:
|
|
2535
|
+
1. Backup .git \u2192 .git.backup-{timestamp}
|
|
2536
|
+
2. Git init m\u1EDBi (branch main, initial commit)
|
|
2537
|
+
3. T\u1EA1o GitHub repo m\u1EDBi '${repoName}' d\u01B0\u1EDBi account c\u1EE7a b\u1EA1n
|
|
2538
|
+
Ti\u1EBFp t\u1EE5c?`,
|
|
2539
|
+
default: true
|
|
2422
2540
|
});
|
|
2423
|
-
if (
|
|
2424
|
-
|
|
2425
|
-
return false;
|
|
2426
|
-
}
|
|
2427
|
-
if (action === "switch-account") {
|
|
2428
|
-
triggerGhAuthLoginInteractive();
|
|
2541
|
+
if (!confirmed) {
|
|
2542
|
+
throw new Error("User abort reset folder.");
|
|
2429
2543
|
}
|
|
2430
|
-
log.info("Ki\u1EC3m tra access...");
|
|
2431
|
-
if (checkRepoAccess(args.repoSlug)) {
|
|
2432
|
-
const finalUser = getCurrentGhUser();
|
|
2433
|
-
log.success(`\u0110\xE3 c\xF3 access v\u1EDBi '${finalUser ?? "(unknown)"}' \u2014 ti\u1EBFp t\u1EE5c.`);
|
|
2434
|
-
return true;
|
|
2435
|
-
}
|
|
2436
|
-
log.warn(
|
|
2437
|
-
`V\u1EABn ch\u01B0a c\xF3 access v\u1EDBi account '${ghUser ?? "(unknown)"}'. \u0110\u1EA3m b\u1EA3o \u0111\xE3 accept email invite ho\u1EB7c switch \u0111\xFAng account.`
|
|
2438
|
-
);
|
|
2439
2544
|
}
|
|
2545
|
+
const visibility = opts.visibility ?? (opts.autoYes ? "private" : await select5({
|
|
2546
|
+
message: "Visibility cho repo m\u1EDBi?",
|
|
2547
|
+
choices: [
|
|
2548
|
+
{ name: "private (m\u1EB7c \u0111\u1ECBnh)", value: "private" },
|
|
2549
|
+
{ name: "public", value: "public" }
|
|
2550
|
+
]
|
|
2551
|
+
}));
|
|
2552
|
+
const backupPath = await backupExistingDotGit(opts.folderPath);
|
|
2553
|
+
reinitGitInFolder(opts.folderPath);
|
|
2554
|
+
const urls = createGithubRemoteFromFolder({
|
|
2555
|
+
folder: opts.folderPath,
|
|
2556
|
+
name: repoName,
|
|
2557
|
+
visibility,
|
|
2558
|
+
org: opts.org
|
|
2559
|
+
});
|
|
2560
|
+
return {
|
|
2561
|
+
newRemoteUrl: urls.httpsUrl,
|
|
2562
|
+
backupPath
|
|
2563
|
+
};
|
|
2440
2564
|
}
|
|
2441
2565
|
|
|
2442
|
-
// src/lib/
|
|
2443
|
-
|
|
2444
|
-
|
|
2445
|
-
|
|
2446
|
-
|
|
2447
|
-
|
|
2448
|
-
|
|
2449
|
-
|
|
2450
|
-
|
|
2451
|
-
|
|
2452
|
-
|
|
2453
|
-
|
|
2454
|
-
|
|
2566
|
+
// src/lib/verify-git-remote-accessible.ts
|
|
2567
|
+
import { spawnSync as spawnSync15 } from "child_process";
|
|
2568
|
+
var TIMEOUT_MS = 5e3;
|
|
2569
|
+
function classifyRemoteError(stderr) {
|
|
2570
|
+
const text = stderr.toLowerCase();
|
|
2571
|
+
if (text.includes("authentication") || text.includes("could not read username") || text.includes("permission denied") || text.includes("403") || text.includes("access denied") || text.includes("repository not found")) {
|
|
2572
|
+
return "no-access";
|
|
2573
|
+
}
|
|
2574
|
+
if (text.includes("404") || text.includes("does not exist")) {
|
|
2575
|
+
return "not-found";
|
|
2576
|
+
}
|
|
2577
|
+
if (text.includes("could not resolve host") || text.includes("network") || text.includes("connection refused") || text.includes("connection timed out")) {
|
|
2578
|
+
return "network";
|
|
2579
|
+
}
|
|
2580
|
+
return "unknown";
|
|
2455
2581
|
}
|
|
2456
|
-
function
|
|
2457
|
-
const
|
|
2458
|
-
|
|
2459
|
-
|
|
2460
|
-
|
|
2461
|
-
if (a.minor !== b.minor) return a.minor - b.minor;
|
|
2462
|
-
return a.patch - b.patch;
|
|
2582
|
+
function tryVerifyGitRemoteAccessible(url) {
|
|
2583
|
+
const r = spawnSync15("git", ["ls-remote", "--exit-code", url, "HEAD"], {
|
|
2584
|
+
encoding: "utf8",
|
|
2585
|
+
timeout: TIMEOUT_MS,
|
|
2586
|
+
stdio: ["ignore", "pipe", "pipe"]
|
|
2463
2587
|
});
|
|
2464
|
-
|
|
2465
|
-
|
|
2466
|
-
|
|
2467
|
-
// src/lib/resolve-team-pack-repo-url.ts
|
|
2468
|
-
var ORG_DEFAULT = "git@github.com:nalvn/team-ai-pack.git";
|
|
2469
|
-
function resolveTeamPackRepoUrl() {
|
|
2470
|
-
if (process.env.AVATAR_TEAM_PACK_REPO_URL) {
|
|
2471
|
-
return process.env.AVATAR_TEAM_PACK_REPO_URL;
|
|
2588
|
+
if (r.status === 0) return { ok: true };
|
|
2589
|
+
if (r.signal === "SIGTERM") {
|
|
2590
|
+
return { ok: false, reason: "timeout", detail: "git ls-remote > 5s" };
|
|
2472
2591
|
}
|
|
2473
|
-
|
|
2592
|
+
const stderr = (r.stderr || "").trim();
|
|
2593
|
+
const reason = classifyRemoteError(stderr);
|
|
2594
|
+
return { ok: false, reason, detail: stderr.slice(0, 300) };
|
|
2474
2595
|
}
|
|
2475
2596
|
|
|
2476
|
-
// src/lib/
|
|
2477
|
-
var
|
|
2478
|
-
var TEAM_PACK_RELATIVE_PATH = ".claude/pack";
|
|
2479
|
-
var TeamPackAccessAbortedError = class extends Error {
|
|
2597
|
+
// src/lib/handle-remote-access-failure-with-account-switch.ts
|
|
2598
|
+
var RemoteAccessAbortedError = class extends Error {
|
|
2480
2599
|
constructor(message) {
|
|
2481
2600
|
super(message);
|
|
2482
|
-
this.name = "
|
|
2601
|
+
this.name = "RemoteAccessAbortedError";
|
|
2483
2602
|
}
|
|
2484
2603
|
};
|
|
2485
|
-
|
|
2486
|
-
|
|
2487
|
-
|
|
2488
|
-
|
|
2489
|
-
|
|
2490
|
-
|
|
2491
|
-
|
|
2492
|
-
throw new TeamPackAccessAbortedError(
|
|
2493
|
-
"User ch\u1ECDn t\u1EA1m ng\u01B0ng. Ch\u1EA1y l\u1EA1i 'avatar init' sau khi \u0111\u01B0\u1EE3c add v\xE0o org."
|
|
2494
|
-
);
|
|
2495
|
-
}
|
|
2496
|
-
}
|
|
2497
|
-
try {
|
|
2498
|
-
await addSubmodule(url, TEAM_PACK_RELATIVE_PATH, projectRoot);
|
|
2499
|
-
} catch (err) {
|
|
2500
|
-
const msg = err instanceof Error ? err.message : String(err);
|
|
2501
|
-
if (msg.includes("Repository not found") || msg.includes("not found")) {
|
|
2502
|
-
log.error(
|
|
2503
|
-
`Repo team-ai-pack kh\xF4ng t\u1ED3n t\u1EA1i: ${url}
|
|
2504
|
-
C\xE1ch fix:
|
|
2505
|
-
1. T\u1EA1o repo: gh repo create <owner>/team-ai-pack --private --add-readme
|
|
2506
|
-
2. Ho\u1EB7c override URL: export AVATAR_TEAM_PACK_REPO_URL=<url-repo-c\u1EE7a-b\u1EA1n>
|
|
2507
|
-
3. Ho\u1EB7c d\xF9ng flag --skip-team-pack`
|
|
2508
|
-
);
|
|
2509
|
-
}
|
|
2510
|
-
throw err;
|
|
2511
|
-
}
|
|
2512
|
-
if (tag) {
|
|
2513
|
-
await checkoutTagInSubmodule(TEAM_PACK_RELATIVE_PATH, tag, projectRoot);
|
|
2514
|
-
return { pinnedTag: tag };
|
|
2515
|
-
}
|
|
2516
|
-
if (latest) {
|
|
2517
|
-
await checkoutBranchHeadInSubmodule(TEAM_PACK_RELATIVE_PATH, DEFAULT_PACK_BRANCH, projectRoot);
|
|
2518
|
-
return { pinnedTag: `${DEFAULT_PACK_BRANCH} (HEAD)` };
|
|
2519
|
-
}
|
|
2520
|
-
const submoduleDir = join16(projectRoot, TEAM_PACK_RELATIVE_PATH);
|
|
2521
|
-
const allTags = await listTags(submoduleDir);
|
|
2522
|
-
const target = pickLatestStableSemVerTag(allTags);
|
|
2523
|
-
if (target) {
|
|
2524
|
-
await checkoutTagInSubmodule(TEAM_PACK_RELATIVE_PATH, target, projectRoot);
|
|
2525
|
-
}
|
|
2526
|
-
return { pinnedTag: target };
|
|
2527
|
-
}
|
|
2528
|
-
async function readPinnedPackVersion(projectRoot) {
|
|
2529
|
-
const submoduleRoot = join16(projectRoot, TEAM_PACK_RELATIVE_PATH);
|
|
2530
|
-
const tag = await tagAtHead(submoduleRoot);
|
|
2531
|
-
if (tag) return tag;
|
|
2532
|
-
const sha = await currentCommitSha(submoduleRoot);
|
|
2533
|
-
return sha.slice(0, 7);
|
|
2534
|
-
}
|
|
2535
|
-
|
|
2536
|
-
// src/lib/add-team-pack-submodule-with-retry-on-network-fail.ts
|
|
2537
|
-
function isSshPermissionError(message) {
|
|
2538
|
-
const text = message.toLowerCase();
|
|
2539
|
-
return text.includes("permission denied (publickey)") || text.includes("publickey)") || text.includes("ssh: could not resolve") || text.includes("host key verification failed");
|
|
2604
|
+
function getCurrentGhUser() {
|
|
2605
|
+
const r = spawnSync16("gh", ["api", "user", "--jq", ".login"], {
|
|
2606
|
+
encoding: "utf8",
|
|
2607
|
+
stdio: ["ignore", "pipe", "pipe"]
|
|
2608
|
+
});
|
|
2609
|
+
if (r.status !== 0) return null;
|
|
2610
|
+
return r.stdout.trim() || null;
|
|
2540
2611
|
}
|
|
2541
|
-
function
|
|
2612
|
+
function triggerGhAuthLoginInteractive() {
|
|
2542
2613
|
log.info("M\u1EDF browser \u0111\u1EC3 switch GitHub account...");
|
|
2543
|
-
const r =
|
|
2614
|
+
const r = spawnSync16("gh", ["auth", "login", "--web"], { stdio: "inherit" });
|
|
2544
2615
|
if (r.status !== 0) {
|
|
2545
|
-
log.warn(`gh auth login exit ${r.status}.
|
|
2616
|
+
log.warn(`gh auth login exit ${r.status}. B\u1EA1n c\xF3 th\u1EC3 ch\u1EA1y tay r\u1ED3i quay l\u1EA1i retry.`);
|
|
2546
2617
|
}
|
|
2547
2618
|
}
|
|
2548
|
-
function
|
|
2549
|
-
|
|
2550
|
-
|
|
2551
|
-
|
|
2552
|
-
|
|
2619
|
+
function getReasonHint(reason, url, ghUser) {
|
|
2620
|
+
switch (reason) {
|
|
2621
|
+
case "no-access":
|
|
2622
|
+
return ghUser ? `Repo c\xF3 th\u1EC3 private v\xE0 account '${ghUser}' kh\xF4ng c\xF3 quy\u1EC1n access. Switch sang account c\xF3 quy\u1EC1n, ho\u1EB7c xin invite t\u1EEB owner repo.` : "Repo c\xF3 th\u1EC3 private v\xE0 gh CLI ch\u01B0a login. Login tr\u01B0\u1EDBc r\u1ED3i retry.";
|
|
2623
|
+
case "not-found":
|
|
2624
|
+
return `URL c\xF3 th\u1EC3 sai ch\xEDnh t\u1EA3, ho\u1EB7c repo \u0111\xE3 b\u1ECB x\xF3a/rename. Nh\u1EADp l\u1EA1i URL \u0111\xFAng. Check: ${url}`;
|
|
2625
|
+
case "network":
|
|
2626
|
+
return "Kh\xF4ng k\u1EBFt n\u1ED1i \u0111\u01B0\u1EE3c GitHub. Check m\u1EA1ng / VPN / firewall.";
|
|
2627
|
+
case "timeout":
|
|
2628
|
+
return "Network ch\u1EADm > 5s. Check m\u1EA1ng r\u1ED3i retry.";
|
|
2629
|
+
default:
|
|
2630
|
+
return "L\u1ED7i kh\xF4ng x\xE1c \u0111\u1ECBnh. URL c\xF3 th\u1EC3 sai \u2014 nh\u1EADp l\u1EA1i, ho\u1EB7c check gh auth status.";
|
|
2553
2631
|
}
|
|
2554
2632
|
}
|
|
2555
|
-
|
|
2556
|
-
|
|
2557
|
-
|
|
2558
|
-
|
|
2633
|
+
function isValidGitUrl(url) {
|
|
2634
|
+
const trimmed = url.trim();
|
|
2635
|
+
if (!trimmed) return false;
|
|
2636
|
+
return /^https?:\/\/[\w.@/-]+$/.test(trimmed) || /^git@[\w.-]+:[\w./-]+\.git$/.test(trimmed) || /^[\w.-]+\/[\w.-]+$/.test(trimmed);
|
|
2637
|
+
}
|
|
2638
|
+
async function handleRemoteAccessFailureWithAccountSwitch(args) {
|
|
2639
|
+
let currentUrl = args.url;
|
|
2640
|
+
let reason = args.initialReason;
|
|
2641
|
+
let detail = args.initialDetail;
|
|
2642
|
+
while (true) {
|
|
2643
|
+
const ghUser = getCurrentGhUser();
|
|
2644
|
+
log.warn(`Kh\xF4ng truy c\u1EADp \u0111\u01B0\u1EE3c ${currentUrl}`);
|
|
2645
|
+
log.dim(` L\xFD do: ${reason}${detail ? ` \u2014 ${detail.slice(0, 150)}` : ""}`);
|
|
2646
|
+
log.info(getReasonHint(reason, currentUrl, ghUser));
|
|
2647
|
+
if (ghUser) log.dim(` gh CLI hi\u1EC7n \u0111ang login: ${ghUser}`);
|
|
2648
|
+
const choices = [
|
|
2559
2649
|
{
|
|
2560
|
-
name: "
|
|
2561
|
-
value: "
|
|
2650
|
+
name: "Nh\u1EADp l\u1EA1i URL \u0111\xFAng (recommended khi URL sai ch\xEDnh t\u1EA3)",
|
|
2651
|
+
value: "re-input-url"
|
|
2562
2652
|
},
|
|
2563
2653
|
{
|
|
2564
|
-
name: "
|
|
2565
|
-
value: "
|
|
2654
|
+
name: "Switch GitHub account (gh auth login \u2014 m\u1EDF browser)",
|
|
2655
|
+
value: "switch"
|
|
2566
2656
|
},
|
|
2567
2657
|
{
|
|
2568
|
-
name: "T\xF4i v\u1EEBa
|
|
2658
|
+
name: "T\xF4i v\u1EEBa fix (accept invite / s\u1EEDa permission) \u2014 retry verify",
|
|
2569
2659
|
value: "retry"
|
|
2570
|
-
},
|
|
2571
|
-
{
|
|
2572
|
-
name: "B\u1ECF qua team-ai-pack (d\xF9ng avatar sync sau)",
|
|
2573
|
-
value: "skip"
|
|
2574
|
-
},
|
|
2575
|
-
{
|
|
2576
|
-
name: "T\u1EA1m ng\u01B0ng init \u2014 fix SSH key tay r\u1ED3i ch\u1EA1y l\u1EA1i",
|
|
2577
|
-
value: "abort"
|
|
2578
|
-
}
|
|
2579
|
-
]
|
|
2580
|
-
});
|
|
2581
|
-
}
|
|
2582
|
-
async function addTeamPackSubmoduleWithRetryOnNetworkFail(projectRoot, tag, ssoEmail, latest = false) {
|
|
2583
|
-
while (true) {
|
|
2584
|
-
try {
|
|
2585
|
-
const result = await addTeamPackSubmodule(projectRoot, tag, ssoEmail, latest);
|
|
2586
|
-
return { pinnedTag: result.pinnedTag, skipped: false };
|
|
2587
|
-
} catch (err) {
|
|
2588
|
-
if (err instanceof TeamPackAccessAbortedError) throw err;
|
|
2589
|
-
const message = err instanceof Error ? err.message : String(err);
|
|
2590
|
-
if (isSshPermissionError(message)) {
|
|
2591
|
-
log.warn("Pull team-ai-pack th\u1EA5t b\u1EA1i: SSH permission denied (publickey).");
|
|
2592
|
-
log.dim(
|
|
2593
|
-
" \u2192 M\xE1y n\xE0y ch\u01B0a c\xF3 SSH key \u0111\u01B0\u1EE3c register tr\xEAn GitHub, ho\u1EB7c key thu\u1ED9c account kh\xE1c."
|
|
2594
|
-
);
|
|
2595
|
-
const action2 = await handleSshPermissionError();
|
|
2596
|
-
if (action2 === "abort") {
|
|
2597
|
-
throw new UserAbortedRecoveryError(
|
|
2598
|
-
"User abort t\u1EA1i b\u01B0\u1EDBc pull team-ai-pack. Fix SSH key (https://github.com/settings/keys) r\u1ED3i ch\u1EA1y l\u1EA1i 'avatar init'."
|
|
2599
|
-
);
|
|
2600
|
-
}
|
|
2601
|
-
if (action2 === "skip") {
|
|
2602
|
-
log.warn(
|
|
2603
|
-
"Skip team-ai-pack. Workspace d\xF9ng \u0111\u01B0\u1EE3c nh\u01B0ng kh\xF4ng c\xF3 shared knowledge. Pull sau qua `avatar sync`."
|
|
2604
|
-
);
|
|
2605
|
-
return { pinnedTag: null, skipped: true };
|
|
2606
|
-
}
|
|
2607
|
-
if (action2 === "switch") {
|
|
2608
|
-
triggerGhAuthLoginInteractive2();
|
|
2609
|
-
continue;
|
|
2610
|
-
}
|
|
2611
|
-
if (action2 === "https") {
|
|
2612
|
-
process.env.AVATAR_TEAM_PACK_REPO_URL = "https://github.com/nalvn/team-ai-pack.git";
|
|
2613
|
-
log.info("Override URL sang HTTPS. L\u01B0u \xFD: HTTPS c\xF3 th\u1EC3 fail 403 n\u1EBFu read-only role.");
|
|
2614
|
-
openGithubSshKeysPage();
|
|
2615
|
-
continue;
|
|
2616
|
-
}
|
|
2617
|
-
continue;
|
|
2618
2660
|
}
|
|
2619
|
-
|
|
2620
|
-
|
|
2621
|
-
|
|
2622
|
-
|
|
2623
|
-
|
|
2661
|
+
];
|
|
2662
|
+
if (args.folderPath) {
|
|
2663
|
+
choices.push({
|
|
2664
|
+
name: "Reset folder & t\u1EA1o repo M\u1EDAI d\u01B0\u1EDBi account c\u1EE7a t\xF4i (backup .git c\u0169)",
|
|
2665
|
+
value: "reset-folder"
|
|
2624
2666
|
});
|
|
2625
|
-
|
|
2626
|
-
|
|
2667
|
+
}
|
|
2668
|
+
choices.push({
|
|
2669
|
+
name: "T\u1EA1m ng\u01B0ng init \u2014 ch\u1EA1y l\u1EA1i 'avatar init' sau",
|
|
2670
|
+
value: "abort"
|
|
2671
|
+
});
|
|
2672
|
+
const action = await select6({ message: "C\xE1ch x\u1EED l\xFD?", choices });
|
|
2673
|
+
if (action === "abort") {
|
|
2674
|
+
throw new RemoteAccessAbortedError(
|
|
2675
|
+
`User ch\u1ECDn t\u1EA1m ng\u01B0ng. Fix access ${currentUrl} r\u1ED3i ch\u1EA1y l\u1EA1i 'avatar init'.`
|
|
2676
|
+
);
|
|
2677
|
+
}
|
|
2678
|
+
if (action === "reset-folder") {
|
|
2679
|
+
if (!args.folderPath) {
|
|
2680
|
+
log.warn("Reset folder c\u1EA7n folderPath \u2014 internal error.");
|
|
2681
|
+
continue;
|
|
2627
2682
|
}
|
|
2628
|
-
|
|
2629
|
-
|
|
2630
|
-
|
|
2631
|
-
|
|
2632
|
-
|
|
2683
|
+
try {
|
|
2684
|
+
const reset = await resetFolderGitAndCreateNewRemoteUnderCurrentUser({
|
|
2685
|
+
folderPath: args.folderPath,
|
|
2686
|
+
visibility: args.defaultVisibility
|
|
2687
|
+
});
|
|
2688
|
+
log.success(`Folder \u0111\xE3 reset. Backup t\u1EA1i: ${reset.backupPath}`);
|
|
2689
|
+
log.success(`Remote m\u1EDBi: ${reset.newRemoteUrl}`);
|
|
2690
|
+
return { resolvedUrl: reset.newRemoteUrl };
|
|
2691
|
+
} catch (err) {
|
|
2692
|
+
log.warn(`Reset folder th\u1EA5t b\u1EA1i: ${err.message}`);
|
|
2693
|
+
continue;
|
|
2633
2694
|
}
|
|
2634
2695
|
}
|
|
2696
|
+
if (action === "re-input-url") {
|
|
2697
|
+
const newUrl = await input4({
|
|
2698
|
+
message: "URL git remote (https://github.com/owner/repo.git ho\u1EB7c git@github.com:owner/repo.git):",
|
|
2699
|
+
default: currentUrl,
|
|
2700
|
+
validate: (v) => isValidGitUrl(v) || "URL kh\xF4ng \u0111\xFAng format git remote"
|
|
2701
|
+
});
|
|
2702
|
+
currentUrl = newUrl.trim();
|
|
2703
|
+
}
|
|
2704
|
+
if (action === "switch") {
|
|
2705
|
+
triggerGhAuthLoginInteractive();
|
|
2706
|
+
}
|
|
2707
|
+
log.info(`Verify remote l\u1EA1i: ${currentUrl}...`);
|
|
2708
|
+
const result = tryVerifyGitRemoteAccessible(currentUrl);
|
|
2709
|
+
if (result.ok) {
|
|
2710
|
+
log.success(`Remote accessible: ${currentUrl}`);
|
|
2711
|
+
return { resolvedUrl: currentUrl };
|
|
2712
|
+
}
|
|
2713
|
+
reason = result.reason ?? "unknown";
|
|
2714
|
+
detail = result.detail;
|
|
2635
2715
|
}
|
|
2636
2716
|
}
|
|
2637
2717
|
|
|
2638
|
-
// src/lib/
|
|
2639
|
-
import
|
|
2640
|
-
|
|
2641
|
-
|
|
2642
|
-
"\u2588\u2588\u2554\u2550\u2550\u2588\u2588\u2557\u2588\u2588\u2551 \u2588\u2588\u2551\u2588\u2588\u2554\u2550\u2550\u2588\u2588\u2557\u255A\u2550\u2550\u2588\u2588\u2554\u2550\u2550\u255D\u2588\u2588\u2554\u2550\u2550\u2588\u2588\u2557\u2588\u2588\u2554\u2550\u2550\u2588\u2588\u2557",
|
|
2643
|
-
"\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2551\u2588\u2588\u2551 \u2588\u2588\u2551\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2551 \u2588\u2588\u2551 \u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2551\u2588\u2588\u2588\u2588\u2588\u2588\u2554\u255D",
|
|
2644
|
-
"\u2588\u2588\u2554\u2550\u2550\u2588\u2588\u2551\u255A\u2588\u2588\u2557 \u2588\u2588\u2554\u255D\u2588\u2588\u2554\u2550\u2550\u2588\u2588\u2551 \u2588\u2588\u2551 \u2588\u2588\u2554\u2550\u2550\u2588\u2588\u2551\u2588\u2588\u2554\u2550\u2550\u2588\u2588\u2557",
|
|
2645
|
-
"\u2588\u2588\u2551 \u2588\u2588\u2551 \u255A\u2588\u2588\u2588\u2588\u2554\u255D \u2588\u2588\u2551 \u2588\u2588\u2551 \u2588\u2588\u2551 \u2588\u2588\u2551 \u2588\u2588\u2551\u2588\u2588\u2551 \u2588\u2588\u2551",
|
|
2646
|
-
"\u255A\u2550\u255D \u255A\u2550\u255D \u255A\u2550\u2550\u2550\u255D \u255A\u2550\u255D \u255A\u2550\u255D \u255A\u2550\u255D \u255A\u2550\u255D \u255A\u2550\u255D\u255A\u2550\u255D \u255A\u2550\u255D"
|
|
2647
|
-
];
|
|
2648
|
-
var GRADIENT_STOPS = [
|
|
2649
|
-
[217, 79, 30],
|
|
2650
|
-
// cam-cháy (#d94f1e)
|
|
2651
|
-
[200, 70, 80],
|
|
2652
|
-
// cam-hồng
|
|
2653
|
-
[170, 70, 140],
|
|
2654
|
-
// hồng-tím
|
|
2655
|
-
[125, 88, 217]
|
|
2656
|
-
// tím (#7d58d9)
|
|
2657
|
-
];
|
|
2658
|
-
function lerpChannel(a, b, t) {
|
|
2659
|
-
return Math.round(a + (b - a) * t);
|
|
2660
|
-
}
|
|
2661
|
-
function gradientAt(t) {
|
|
2662
|
-
const clamped = Math.max(0, Math.min(1, t));
|
|
2663
|
-
const scaled = clamped * (GRADIENT_STOPS.length - 1);
|
|
2664
|
-
const lo = Math.floor(scaled);
|
|
2665
|
-
const hi = Math.min(GRADIENT_STOPS.length - 1, lo + 1);
|
|
2666
|
-
const localT = scaled - lo;
|
|
2667
|
-
const a = GRADIENT_STOPS[lo];
|
|
2668
|
-
const b = GRADIENT_STOPS[hi];
|
|
2669
|
-
return [
|
|
2670
|
-
lerpChannel(a[0], b[0], localT),
|
|
2671
|
-
lerpChannel(a[1], b[1], localT),
|
|
2672
|
-
lerpChannel(a[2], b[2], localT)
|
|
2673
|
-
];
|
|
2674
|
-
}
|
|
2675
|
-
function renderAvatarBanner(opts) {
|
|
2676
|
-
const isTty = process.stdout.isTTY ?? false;
|
|
2677
|
-
const supportsColor = isTty && chalk2.level > 0;
|
|
2678
|
-
if (!supportsColor) {
|
|
2679
|
-
return [...BANNER_LINES, ...opts?.tagline ? ["", opts.tagline] : []].join("\n");
|
|
2680
|
-
}
|
|
2681
|
-
const colored = BANNER_LINES.map((line, idx) => {
|
|
2682
|
-
const t = BANNER_LINES.length === 1 ? 0 : idx / (BANNER_LINES.length - 1);
|
|
2683
|
-
const [r, g, b] = gradientAt(t);
|
|
2684
|
-
return chalk2.rgb(r, g, b).bold(line);
|
|
2685
|
-
});
|
|
2686
|
-
if (opts?.tagline) {
|
|
2687
|
-
colored.push("");
|
|
2688
|
-
colored.push(chalk2.dim(opts.tagline));
|
|
2689
|
-
}
|
|
2690
|
-
return colored.join("\n");
|
|
2691
|
-
}
|
|
2692
|
-
function printAvatarBanner(opts) {
|
|
2693
|
-
process.stdout.write(`
|
|
2694
|
-
${renderAvatarBanner(opts)}
|
|
2718
|
+
// src/lib/safe-bootstrap-for-dirty-folder.ts
|
|
2719
|
+
import { readdirSync } from "fs";
|
|
2720
|
+
import { select as select7 } from "@inquirer/prompts";
|
|
2721
|
+
import { simpleGit as simpleGit3 } from "simple-git";
|
|
2695
2722
|
|
|
2696
|
-
|
|
2723
|
+
// src/lib/check-folder-has-git.ts
|
|
2724
|
+
import { existsSync as existsSync6, statSync } from "fs";
|
|
2725
|
+
import { join as join17 } from "path";
|
|
2726
|
+
function checkFolderHasGit(folderPath) {
|
|
2727
|
+
const gitPath = join17(folderPath, ".git");
|
|
2728
|
+
if (!existsSync6(gitPath)) return false;
|
|
2729
|
+
const stat = statSync(gitPath);
|
|
2730
|
+
return stat.isDirectory() || stat.isFile();
|
|
2697
2731
|
}
|
|
2698
2732
|
|
|
2699
|
-
// src/lib/
|
|
2700
|
-
import {
|
|
2701
|
-
var
|
|
2702
|
-
|
|
2703
|
-
|
|
2704
|
-
|
|
2733
|
+
// src/lib/create-initial-git-commit.ts
|
|
2734
|
+
import { simpleGit as simpleGit2 } from "simple-git";
|
|
2735
|
+
var INITIAL_COMMIT_MESSAGE = "chore: initial commit";
|
|
2736
|
+
async function createInitialGitCommit(folderPath) {
|
|
2737
|
+
const g = simpleGit2({ baseDir: folderPath });
|
|
2738
|
+
const isRepo = await g.checkIsRepo().catch(() => false);
|
|
2739
|
+
if (!isRepo) {
|
|
2740
|
+
await g.init();
|
|
2705
2741
|
}
|
|
2706
|
-
|
|
2707
|
-
|
|
2708
|
-
|
|
2709
|
-
const args = [
|
|
2710
|
-
"repo",
|
|
2711
|
-
"create",
|
|
2712
|
-
fullName,
|
|
2713
|
-
`--${input6.visibility}`,
|
|
2714
|
-
"--source",
|
|
2715
|
-
input6.folder,
|
|
2716
|
-
"--remote",
|
|
2717
|
-
"origin",
|
|
2718
|
-
"--push"
|
|
2719
|
-
];
|
|
2720
|
-
const r = spawnSync14("gh", args, { stdio: "inherit" });
|
|
2721
|
-
if (r.status !== 0) {
|
|
2722
|
-
if (r.status === 1) {
|
|
2723
|
-
throw new RepoAlreadyExistsError(fullName);
|
|
2724
|
-
}
|
|
2725
|
-
throw new Error(`gh repo create th\u1EA5t b\u1EA1i (exit ${r.status})`);
|
|
2742
|
+
try {
|
|
2743
|
+
await g.branch(["-M", "main"]);
|
|
2744
|
+
} catch {
|
|
2726
2745
|
}
|
|
2727
|
-
|
|
2728
|
-
|
|
2729
|
-
|
|
2730
|
-
|
|
2731
|
-
|
|
2732
|
-
|
|
2733
|
-
|
|
2734
|
-
|
|
2735
|
-
function resolveGithubUsernameDefault() {
|
|
2736
|
-
const r = spawnSync15("gh", ["api", "user", "--jq", ".login"], {
|
|
2737
|
-
encoding: "utf8",
|
|
2738
|
-
stdio: ["ignore", "pipe", "pipe"]
|
|
2739
|
-
});
|
|
2740
|
-
if (r.status !== 0) {
|
|
2741
|
-
throw new Error(`Kh\xF4ng l\u1EA5y \u0111\u01B0\u1EE3c GitHub username: ${r.stderr?.trim()}`);
|
|
2746
|
+
await g.add(".");
|
|
2747
|
+
const status = await g.status();
|
|
2748
|
+
const hasCommits = (await g.raw(["rev-list", "-n", "1", "--all"]).catch(() => "")).trim();
|
|
2749
|
+
if (hasCommits) return;
|
|
2750
|
+
if (status.files.length === 0) {
|
|
2751
|
+
await g.commit(INITIAL_COMMIT_MESSAGE, void 0, { "--allow-empty": null });
|
|
2752
|
+
} else {
|
|
2753
|
+
await g.commit(INITIAL_COMMIT_MESSAGE);
|
|
2742
2754
|
}
|
|
2743
|
-
return r.stdout.trim();
|
|
2744
2755
|
}
|
|
2745
2756
|
|
|
2746
|
-
// src/lib/
|
|
2747
|
-
|
|
2748
|
-
|
|
2749
|
-
|
|
2750
|
-
|
|
2751
|
-
|
|
2752
|
-
|
|
2753
|
-
|
|
2754
|
-
|
|
2757
|
+
// src/lib/detect-folder-tech-stack.ts
|
|
2758
|
+
import { existsSync as existsSync7 } from "fs";
|
|
2759
|
+
import { join as join18 } from "path";
|
|
2760
|
+
var SIGNATURES = {
|
|
2761
|
+
node: ["package.json"],
|
|
2762
|
+
python: ["pyproject.toml", "requirements.txt", "setup.py", "Pipfile"],
|
|
2763
|
+
go: ["go.mod"],
|
|
2764
|
+
rust: ["Cargo.toml"],
|
|
2765
|
+
java: ["pom.xml", "build.gradle", "build.gradle.kts"],
|
|
2766
|
+
ruby: ["Gemfile"]
|
|
2755
2767
|
};
|
|
2756
|
-
function
|
|
2757
|
-
|
|
2758
|
-
|
|
2768
|
+
function detectFolderTechStack(folderPath) {
|
|
2769
|
+
const matched = [];
|
|
2770
|
+
for (const [stack, files] of Object.entries(SIGNATURES)) {
|
|
2771
|
+
if (files.some((f) => existsSync7(join18(folderPath, f)))) {
|
|
2772
|
+
matched.push(stack);
|
|
2773
|
+
}
|
|
2759
2774
|
}
|
|
2775
|
+
return matched.length > 0 ? matched : ["generic"];
|
|
2760
2776
|
}
|
|
2761
|
-
|
|
2762
|
-
|
|
2763
|
-
|
|
2777
|
+
|
|
2778
|
+
// src/lib/gitignore-template-loader.ts
|
|
2779
|
+
import { readFileSync as readFileSync3 } from "fs";
|
|
2780
|
+
import { dirname as dirname4, join as join19 } from "path";
|
|
2781
|
+
import { fileURLToPath as fileURLToPath2 } from "url";
|
|
2782
|
+
var __dirname = dirname4(fileURLToPath2(import.meta.url));
|
|
2783
|
+
var CANDIDATE_DIRS = [
|
|
2784
|
+
// Bundled production: dist/index.js → __dirname = .../dist/, sibling dist/templates
|
|
2785
|
+
join19(__dirname, "templates", "gitignore"),
|
|
2786
|
+
// Legacy bundled: nếu file là dist/lib/*.js (sub-bundle), templates ở dist/templates
|
|
2787
|
+
join19(__dirname, "..", "templates", "gitignore"),
|
|
2788
|
+
// Dev mode (vitest/tsx run src/ trực tiếp): __dirname = src/lib/
|
|
2789
|
+
join19(__dirname, "..", "..", "src", "templates", "gitignore"),
|
|
2790
|
+
// npm-installed alt: __dirname = .../dist/ → package_root/src/templates
|
|
2791
|
+
join19(__dirname, "..", "src", "templates", "gitignore")
|
|
2792
|
+
];
|
|
2793
|
+
var AVATAR_MARKER_START = "# === avatar ===";
|
|
2794
|
+
var AVATAR_MARKER_END = "# === /avatar ===";
|
|
2795
|
+
function readTemplate(stack) {
|
|
2796
|
+
for (const dir of CANDIDATE_DIRS) {
|
|
2797
|
+
try {
|
|
2798
|
+
return readFileSync3(join19(dir, `${stack}.txt`), "utf8");
|
|
2799
|
+
} catch {
|
|
2800
|
+
}
|
|
2764
2801
|
}
|
|
2802
|
+
throw new Error(`Kh\xF4ng t\xECm th\u1EA5y template gitignore cho stack "${stack}"`);
|
|
2765
2803
|
}
|
|
2766
|
-
|
|
2767
|
-
|
|
2768
|
-
|
|
2769
|
-
|
|
2770
|
-
|
|
2771
|
-
const org = input6.org ?? resolveGithubUsernameDefault();
|
|
2772
|
-
log.info(`T\u1EA1o GitHub repo ${org}/${input6.name} (${input6.visibility})...`);
|
|
2773
|
-
const urls = executeGhRepoCreate({
|
|
2774
|
-
folder: input6.folder,
|
|
2775
|
-
org,
|
|
2776
|
-
name: input6.name,
|
|
2777
|
-
visibility: input6.visibility
|
|
2778
|
-
});
|
|
2779
|
-
log.success(`\u0110\xE3 t\u1EA1o: ${urls.sshUrl}`);
|
|
2780
|
-
return urls;
|
|
2804
|
+
function composeGitignoreContent(stacks) {
|
|
2805
|
+
const all = ["generic", ...stacks.filter((s) => s !== "generic")];
|
|
2806
|
+
const sections = all.map((s) => `# --- ${s} ---
|
|
2807
|
+
${readTemplate(s).trim()}`);
|
|
2808
|
+
return [AVATAR_MARKER_START, ...sections, AVATAR_MARKER_END, ""].join("\n");
|
|
2781
2809
|
}
|
|
2782
2810
|
|
|
2783
|
-
// src/lib/
|
|
2784
|
-
import {
|
|
2785
|
-
|
|
2786
|
-
|
|
2787
|
-
|
|
2788
|
-
|
|
2789
|
-
|
|
2790
|
-
|
|
2791
|
-
return "not-installed";
|
|
2811
|
+
// src/lib/write-or-merge-gitignore.ts
|
|
2812
|
+
import { existsSync as existsSync8, readFileSync as readFileSync4, writeFileSync } from "fs";
|
|
2813
|
+
import { join as join20 } from "path";
|
|
2814
|
+
function writeOrMergeGitignore(folderPath, avatarBlock) {
|
|
2815
|
+
const path = join20(folderPath, ".gitignore");
|
|
2816
|
+
if (!existsSync8(path)) {
|
|
2817
|
+
writeFileSync(path, avatarBlock, "utf8");
|
|
2818
|
+
return;
|
|
2792
2819
|
}
|
|
2793
|
-
|
|
2794
|
-
|
|
2820
|
+
const existing = readFileSync4(path, "utf8");
|
|
2821
|
+
const startIdx = existing.indexOf(AVATAR_MARKER_START);
|
|
2822
|
+
const endIdx = existing.indexOf(AVATAR_MARKER_END);
|
|
2823
|
+
if (startIdx !== -1 && endIdx !== -1 && endIdx > startIdx) {
|
|
2824
|
+
const before = existing.slice(0, startIdx);
|
|
2825
|
+
const after = existing.slice(endIdx + AVATAR_MARKER_END.length);
|
|
2826
|
+
writeFileSync(path, `${before.trimEnd()}
|
|
2795
2827
|
|
|
2796
|
-
|
|
2797
|
-
|
|
2798
|
-
function hasBinary(name) {
|
|
2799
|
-
const platform2 = detectHostPlatform();
|
|
2800
|
-
const probe = platform2 === "win32" ? "where" : "command";
|
|
2801
|
-
const args = platform2 === "win32" ? [name] : ["-v", name];
|
|
2802
|
-
const r = spawnSync17(probe, args, {
|
|
2803
|
-
shell: platform2 !== "win32",
|
|
2804
|
-
stdio: "ignore"
|
|
2805
|
-
});
|
|
2806
|
-
return r.status === 0;
|
|
2807
|
-
}
|
|
2808
|
-
function detectPackageManager() {
|
|
2809
|
-
const platform2 = detectHostPlatform();
|
|
2810
|
-
const candidates = platform2 === "darwin" ? ["brew"] : platform2 === "win32" ? ["winget"] : platform2 === "linux" ? ["apt", "dnf", "pacman"] : [];
|
|
2811
|
-
for (const pm of candidates) {
|
|
2812
|
-
if (hasBinary(pm)) return pm;
|
|
2828
|
+
${avatarBlock}${after.trimStart()}`, "utf8");
|
|
2829
|
+
return;
|
|
2813
2830
|
}
|
|
2814
|
-
|
|
2815
|
-
}
|
|
2831
|
+
writeFileSync(path, `${existing.trimEnd()}
|
|
2816
2832
|
|
|
2817
|
-
|
|
2818
|
-
|
|
2819
|
-
import { input as input4, select as select7 } from "@inquirer/prompts";
|
|
2833
|
+
${avatarBlock}`, "utf8");
|
|
2834
|
+
}
|
|
2820
2835
|
|
|
2821
|
-
// src/lib/
|
|
2822
|
-
|
|
2823
|
-
|
|
2824
|
-
|
|
2825
|
-
|
|
2826
|
-
if (text.includes("authentication") || text.includes("could not read username") || text.includes("permission denied") || text.includes("403") || text.includes("access denied") || text.includes("repository not found")) {
|
|
2827
|
-
return "no-access";
|
|
2828
|
-
}
|
|
2829
|
-
if (text.includes("404") || text.includes("does not exist")) {
|
|
2830
|
-
return "not-found";
|
|
2836
|
+
// src/lib/safe-bootstrap-for-dirty-folder.ts
|
|
2837
|
+
var InitAbortedByUserError = class extends Error {
|
|
2838
|
+
constructor(message) {
|
|
2839
|
+
super(message);
|
|
2840
|
+
this.name = "InitAbortedByUserError";
|
|
2831
2841
|
}
|
|
2832
|
-
|
|
2833
|
-
|
|
2842
|
+
};
|
|
2843
|
+
async function detectFolderGitState(folderPath) {
|
|
2844
|
+
const hasGit = checkFolderHasGit(folderPath);
|
|
2845
|
+
if (!hasGit) {
|
|
2846
|
+
const entries = readdirSync(folderPath).filter((e) => e !== ".git");
|
|
2847
|
+
return entries.length === 0 ? "empty" : "untracked-only";
|
|
2834
2848
|
}
|
|
2835
|
-
|
|
2849
|
+
const g = simpleGit3({ baseDir: folderPath });
|
|
2850
|
+
const status = await g.status();
|
|
2851
|
+
return status.isClean() ? "clean" : "dirty";
|
|
2836
2852
|
}
|
|
2837
|
-
function
|
|
2838
|
-
|
|
2839
|
-
|
|
2840
|
-
|
|
2841
|
-
|
|
2853
|
+
async function promptBootstrapStrategy(state, opts) {
|
|
2854
|
+
if (opts.presetStrategy) return opts.presetStrategy;
|
|
2855
|
+
if (opts.autoYes) return "stash";
|
|
2856
|
+
if (state === "empty" || state === "clean") return "commit-all";
|
|
2857
|
+
return await select7({
|
|
2858
|
+
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:",
|
|
2859
|
+
choices: [
|
|
2860
|
+
{
|
|
2861
|
+
value: "stash",
|
|
2862
|
+
name: "1. Stash changes \u2192 bootstrap \u2192 restore (KHUY\u1EBEN NGH\u1ECA)"
|
|
2863
|
+
},
|
|
2864
|
+
{
|
|
2865
|
+
value: "commit-all",
|
|
2866
|
+
name: "2. Commit to\xE0n b\u1ED9 v\xE0o initial commit (legacy v1.1.6)"
|
|
2867
|
+
},
|
|
2868
|
+
{
|
|
2869
|
+
value: "skip",
|
|
2870
|
+
name: "3. Skip \u2014 t\xF4i commit th\u1EE7 c\xF4ng r\u1ED3i ch\u1EA1y l\u1EA1i"
|
|
2871
|
+
},
|
|
2872
|
+
{
|
|
2873
|
+
value: "branch",
|
|
2874
|
+
name: "4. Commit v\xE0o branch ri\xEAng `avatar/init` (main gi\u1EEF s\u1EA1ch)"
|
|
2875
|
+
}
|
|
2876
|
+
],
|
|
2877
|
+
default: "stash"
|
|
2842
2878
|
});
|
|
2843
|
-
|
|
2844
|
-
|
|
2845
|
-
|
|
2879
|
+
}
|
|
2880
|
+
async function stashUserChanges(g, stashName) {
|
|
2881
|
+
const status = await g.status();
|
|
2882
|
+
if (status.isClean() && status.not_added.length === 0) return false;
|
|
2883
|
+
await g.stash(["push", "--include-untracked", "-m", stashName]);
|
|
2884
|
+
log.info(`Stashed changes: ${stashName}`);
|
|
2885
|
+
return true;
|
|
2886
|
+
}
|
|
2887
|
+
async function restoreStash(g, stashName) {
|
|
2888
|
+
try {
|
|
2889
|
+
await g.stash(["pop"]);
|
|
2890
|
+
log.success(`Restored stash: ${stashName}`);
|
|
2891
|
+
} catch (err) {
|
|
2892
|
+
log.warn(
|
|
2893
|
+
"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}."
|
|
2894
|
+
);
|
|
2895
|
+
log.warn("Resolve: git stash show -p stash@{0} \u2192 fix conflict \u2192 git stash drop");
|
|
2896
|
+
log.dim(`Detail: ${err.message}`);
|
|
2846
2897
|
}
|
|
2847
|
-
const stderr = (r.stderr || "").trim();
|
|
2848
|
-
const reason = classifyRemoteError(stderr);
|
|
2849
|
-
return { ok: false, reason, detail: stderr.slice(0, 300) };
|
|
2850
2898
|
}
|
|
2851
|
-
|
|
2852
|
-
|
|
2853
|
-
|
|
2854
|
-
|
|
2855
|
-
|
|
2856
|
-
|
|
2899
|
+
async function getCurrentBranch(g) {
|
|
2900
|
+
try {
|
|
2901
|
+
const result = await g.revparse(["--abbrev-ref", "HEAD"]);
|
|
2902
|
+
const branch = result.trim();
|
|
2903
|
+
return branch === "HEAD" ? "main" : branch;
|
|
2904
|
+
} catch {
|
|
2905
|
+
return "main";
|
|
2857
2906
|
}
|
|
2858
|
-
}
|
|
2907
|
+
}
|
|
2908
|
+
async function writeAvatarGitignore(folderPath) {
|
|
2909
|
+
const stacks = detectFolderTechStack(folderPath);
|
|
2910
|
+
log.info(`Tech stack: ${stacks.join(", ")}`);
|
|
2911
|
+
writeOrMergeGitignore(folderPath, composeGitignoreContent(stacks));
|
|
2912
|
+
log.success(".gitignore \u0111\xE3 ghi (Avatar block)");
|
|
2913
|
+
}
|
|
2914
|
+
async function executeBootstrapWithStrategy(folderPath, strategy) {
|
|
2915
|
+
const g = simpleGit3({ baseDir: folderPath });
|
|
2916
|
+
switch (strategy) {
|
|
2917
|
+
case "skip":
|
|
2918
|
+
throw new InitAbortedByUserError(
|
|
2919
|
+
"Init aborted. Commit th\u1EE7 c\xF4ng changes hi\u1EC7n t\u1EA1i r\u1ED3i ch\u1EA1y l\u1EA1i `avatar init`."
|
|
2920
|
+
);
|
|
2921
|
+
case "stash": {
|
|
2922
|
+
const stashName = `avatar-init-backup-${Date.now()}`;
|
|
2923
|
+
const hadGit = checkFolderHasGit(folderPath);
|
|
2924
|
+
if (!hadGit) {
|
|
2925
|
+
await g.init();
|
|
2926
|
+
await g.branch(["-M", "main"]).catch(() => void 0);
|
|
2927
|
+
}
|
|
2928
|
+
const hasCommit = (await g.raw(["rev-list", "-n", "1", "--all"]).catch(() => "")).trim();
|
|
2929
|
+
if (!hasCommit) {
|
|
2930
|
+
await g.commit("chore: avatar baseline (pre-stash)", void 0, { "--allow-empty": null });
|
|
2931
|
+
}
|
|
2932
|
+
const stashed = await stashUserChanges(g, stashName);
|
|
2933
|
+
try {
|
|
2934
|
+
await writeAvatarGitignore(folderPath);
|
|
2935
|
+
await createInitialGitCommit(folderPath);
|
|
2936
|
+
} finally {
|
|
2937
|
+
if (stashed) await restoreStash(g, stashName);
|
|
2938
|
+
}
|
|
2939
|
+
break;
|
|
2940
|
+
}
|
|
2941
|
+
case "commit-all": {
|
|
2942
|
+
await writeAvatarGitignore(folderPath);
|
|
2943
|
+
await createInitialGitCommit(folderPath);
|
|
2944
|
+
break;
|
|
2945
|
+
}
|
|
2946
|
+
case "branch": {
|
|
2947
|
+
const hadGit = checkFolderHasGit(folderPath);
|
|
2948
|
+
if (!hadGit) {
|
|
2949
|
+
await g.init();
|
|
2950
|
+
await g.branch(["-M", "main"]);
|
|
2951
|
+
}
|
|
2952
|
+
const originalBranch = await getCurrentBranch(g);
|
|
2953
|
+
try {
|
|
2954
|
+
await g.checkoutLocalBranch("avatar/init");
|
|
2955
|
+
} catch {
|
|
2956
|
+
await g.checkout("avatar/init");
|
|
2957
|
+
}
|
|
2958
|
+
await writeAvatarGitignore(folderPath);
|
|
2959
|
+
await createInitialGitCommit(folderPath);
|
|
2960
|
+
try {
|
|
2961
|
+
await g.checkout(originalBranch);
|
|
2962
|
+
log.info(
|
|
2963
|
+
`Avatar init committed \u1EDF branch 'avatar/init'. Switch back v\u1EC1 '${originalBranch}'. Merge khi s\u1EB5n s\xE0ng: git merge avatar/init`
|
|
2964
|
+
);
|
|
2965
|
+
} catch {
|
|
2966
|
+
log.warn(
|
|
2967
|
+
`Kh\xF4ng switch v\u1EC1 '${originalBranch}' \u0111\u01B0\u1EE3c \u2014 \u1EDF l\u1EA1i branch 'avatar/init'. Switch tay sau.`
|
|
2968
|
+
);
|
|
2969
|
+
}
|
|
2970
|
+
break;
|
|
2971
|
+
}
|
|
2972
|
+
}
|
|
2973
|
+
}
|
|
2974
|
+
async function safeBootstrapGitInFolder(folderPath, opts = {}) {
|
|
2975
|
+
const state = await detectFolderGitState(folderPath);
|
|
2976
|
+
log.info(`Folder state: ${state}`);
|
|
2977
|
+
if (state === "empty" || state === "clean") {
|
|
2978
|
+
await writeAvatarGitignore(folderPath);
|
|
2979
|
+
if (state === "empty") {
|
|
2980
|
+
await createInitialGitCommit(folderPath);
|
|
2981
|
+
}
|
|
2982
|
+
await appendAuditEntry("bootstrap", `state=${state},strategy=auto`);
|
|
2983
|
+
return;
|
|
2984
|
+
}
|
|
2985
|
+
const strategy = await promptBootstrapStrategy(state, opts);
|
|
2986
|
+
await executeBootstrapWithStrategy(folderPath, strategy);
|
|
2987
|
+
await appendAuditEntry("bootstrap", `state=${state},strategy=${strategy}`);
|
|
2988
|
+
}
|
|
2989
|
+
|
|
2990
|
+
// src/lib/team-pack-submodule-manager.ts
|
|
2991
|
+
import { join as join21 } from "path";
|
|
2992
|
+
|
|
2993
|
+
// src/lib/check-team-pack-access-with-retry-loop.ts
|
|
2994
|
+
import { spawnSync as spawnSync17 } from "child_process";
|
|
2995
|
+
import { confirm as confirm5, select as select8 } from "@inquirer/prompts";
|
|
2996
|
+
import boxen3 from "boxen";
|
|
2997
|
+
function parseRepoSlugFromGitUrl(url) {
|
|
2998
|
+
const httpsMatch = url.match(/github\.com[/:]([^/]+\/[^/]+?)(?:\.git)?$/);
|
|
2999
|
+
return httpsMatch?.[1] ?? null;
|
|
3000
|
+
}
|
|
3001
|
+
function checkRepoAccess(repoSlug) {
|
|
3002
|
+
const r = spawnSync17("gh", ["api", `repos/${repoSlug}`], { stdio: "ignore" });
|
|
3003
|
+
return r.status === 0;
|
|
3004
|
+
}
|
|
2859
3005
|
function getCurrentGhUser2() {
|
|
2860
|
-
const r =
|
|
3006
|
+
const r = spawnSync17("gh", ["api", "user", "--jq", ".login"], {
|
|
2861
3007
|
encoding: "utf8",
|
|
2862
3008
|
stdio: ["ignore", "pipe", "pipe"]
|
|
2863
3009
|
});
|
|
2864
3010
|
if (r.status !== 0) return null;
|
|
2865
3011
|
return r.stdout.trim() || null;
|
|
2866
3012
|
}
|
|
2867
|
-
function
|
|
3013
|
+
function triggerGhAuthLoginInteractive2() {
|
|
2868
3014
|
log.info("M\u1EDF browser \u0111\u1EC3 switch GitHub account...");
|
|
2869
|
-
const r =
|
|
3015
|
+
const r = spawnSync17("gh", ["auth", "login", "--web"], { stdio: "inherit" });
|
|
2870
3016
|
if (r.status !== 0) {
|
|
2871
|
-
log.warn(`gh auth login exit ${r.status}.
|
|
3017
|
+
log.warn(`gh auth login exit ${r.status}. C\xF3 th\u1EC3 ch\u1EA1y tay r\u1ED3i quay l\u1EA1i retry.`);
|
|
2872
3018
|
}
|
|
2873
3019
|
}
|
|
2874
|
-
function
|
|
2875
|
-
|
|
2876
|
-
|
|
2877
|
-
|
|
2878
|
-
|
|
2879
|
-
|
|
2880
|
-
|
|
2881
|
-
|
|
2882
|
-
|
|
2883
|
-
|
|
2884
|
-
|
|
2885
|
-
|
|
3020
|
+
async function copyInfoToClipboardWithConsent(info) {
|
|
3021
|
+
const ok = await confirm5({
|
|
3022
|
+
message: "Copy th\xF4ng tin (GitHub username + email) v\xE0o clipboard \u0111\u1EC3 d\xE1n v\xE0o Slack/email?",
|
|
3023
|
+
default: true
|
|
3024
|
+
});
|
|
3025
|
+
if (!ok) return;
|
|
3026
|
+
try {
|
|
3027
|
+
const { default: clipboardy } = await import("clipboardy");
|
|
3028
|
+
await clipboardy.write(info);
|
|
3029
|
+
log.success("\u0110\xE3 copy v\xE0o clipboard");
|
|
3030
|
+
} catch (err) {
|
|
3031
|
+
log.dim(`Copy clipboard fail: ${err.message}`);
|
|
2886
3032
|
}
|
|
2887
3033
|
}
|
|
2888
|
-
function
|
|
2889
|
-
const
|
|
2890
|
-
|
|
2891
|
-
|
|
3034
|
+
function printAccessWarningBox(repoSlug, ghUser, ssoEmail) {
|
|
3035
|
+
const lines = [
|
|
3036
|
+
`${chalk.red("\u26D4 KH\xD4NG C\xD3 QUY\u1EC0N ACCESS")}`,
|
|
3037
|
+
"",
|
|
3038
|
+
`Repo: ${chalk.bold(repoSlug)}`,
|
|
3039
|
+
"",
|
|
3040
|
+
"B\u1EA1n c\u1EA7n \u0111\u01B0\u1EE3c admin add v\xE0o org \u0111\u1EC3 pull team-ai-pack.",
|
|
3041
|
+
"",
|
|
3042
|
+
`${chalk.dim("Th\xF4ng tin g\u1EEDi admin:")}`,
|
|
3043
|
+
` GitHub username: ${chalk.cyan(ghUser ?? "(ch\u01B0a gh auth \u2014 ch\u1EA1y: gh auth login)")}`,
|
|
3044
|
+
` NAL email: ${chalk.cyan(ssoEmail ?? "(ch\u01B0a avatar login \u2014 ch\u1EA1y: avatar login)")}`,
|
|
3045
|
+
` Date: ${(/* @__PURE__ */ new Date()).toISOString().split("T")[0]}`,
|
|
3046
|
+
"",
|
|
3047
|
+
`${chalk.dim("Li\xEAn h\u1EC7:")} luke@nal.vn (Slack #avatar-setup)`
|
|
3048
|
+
];
|
|
3049
|
+
process.stdout.write(
|
|
3050
|
+
`${boxen3(lines.join("\n"), { padding: 1, borderColor: "red", borderStyle: "round" })}
|
|
3051
|
+
`
|
|
3052
|
+
);
|
|
2892
3053
|
}
|
|
2893
|
-
|
|
2894
|
-
|
|
2895
|
-
|
|
2896
|
-
|
|
3054
|
+
function buildAccessRequestInfo(repoSlug, ghUser, ssoEmail) {
|
|
3055
|
+
return [
|
|
3056
|
+
`Request access ${repoSlug} (NAL)`,
|
|
3057
|
+
"",
|
|
3058
|
+
`GitHub username: ${ghUser ?? "(ch\u01B0a gh auth \u2014 ch\u1EA1y: gh auth login)"}`,
|
|
3059
|
+
`NAL email: ${ssoEmail ?? "(ch\u01B0a avatar login \u2014 ch\u1EA1y: avatar login)"}`,
|
|
3060
|
+
`Date: ${(/* @__PURE__ */ new Date()).toISOString().split("T")[0]}`
|
|
3061
|
+
].join("\n");
|
|
3062
|
+
}
|
|
3063
|
+
async function ensureTeamPackAccessWithRetry(args) {
|
|
3064
|
+
if (checkRepoAccess(args.repoSlug)) return true;
|
|
3065
|
+
const initialGhUser = getCurrentGhUser2();
|
|
3066
|
+
printAccessWarningBox(args.repoSlug, initialGhUser, args.ssoEmail ?? null);
|
|
3067
|
+
await copyInfoToClipboardWithConsent(
|
|
3068
|
+
buildAccessRequestInfo(args.repoSlug, initialGhUser, args.ssoEmail ?? null)
|
|
3069
|
+
);
|
|
2897
3070
|
while (true) {
|
|
2898
3071
|
const ghUser = getCurrentGhUser2();
|
|
2899
|
-
|
|
2900
|
-
|
|
2901
|
-
log.info(getReasonHint(reason, currentUrl, ghUser));
|
|
2902
|
-
if (ghUser) log.dim(` gh CLI hi\u1EC7n \u0111ang login: ${ghUser}`);
|
|
2903
|
-
const action = await select7({
|
|
3072
|
+
const ghUserDisplay = ghUser ?? "(ch\u01B0a gh auth)";
|
|
3073
|
+
const action = await select8({
|
|
2904
3074
|
message: "C\xE1ch x\u1EED l\xFD?",
|
|
2905
3075
|
choices: [
|
|
2906
3076
|
{
|
|
2907
|
-
name:
|
|
2908
|
-
value: "
|
|
2909
|
-
},
|
|
2910
|
-
{
|
|
2911
|
-
name: "Switch GitHub account (gh auth login \u2014 m\u1EDF browser)",
|
|
2912
|
-
value: "switch"
|
|
3077
|
+
name: `\u0110\xE3 \u0111\u01B0\u1EE3c grant access v\u1EDBi GitHub username '${ghUserDisplay}' \u2014 ki\u1EC3m tra l\u1EA1i`,
|
|
3078
|
+
value: "retry-same"
|
|
2913
3079
|
},
|
|
2914
3080
|
{
|
|
2915
|
-
name: "
|
|
2916
|
-
value: "
|
|
3081
|
+
name: "\u0110\xE3 grant v\u1EDBi GitHub account kh\xE1c \u2014 switch gh (m\u1EDF browser)",
|
|
3082
|
+
value: "switch-account"
|
|
2917
3083
|
},
|
|
2918
3084
|
{
|
|
2919
|
-
name: "T\u1EA1m ng\u01B0ng
|
|
3085
|
+
name: "T\u1EA1m ng\u01B0ng \u2014 ch\u1EA1y l\u1EA1i 'avatar init' sau",
|
|
2920
3086
|
value: "abort"
|
|
2921
3087
|
}
|
|
2922
3088
|
]
|
|
2923
3089
|
});
|
|
2924
3090
|
if (action === "abort") {
|
|
2925
|
-
|
|
2926
|
-
|
|
2927
|
-
);
|
|
3091
|
+
log.dim("T\u1EA1m ng\u01B0ng. Ch\u1EA1y l\u1EA1i 'avatar init' sau khi \u0111\xE3 accept invite t\u1EEB GitHub.");
|
|
3092
|
+
return false;
|
|
2928
3093
|
}
|
|
2929
|
-
if (action === "
|
|
2930
|
-
|
|
2931
|
-
message: "URL git remote (https://github.com/owner/repo.git ho\u1EB7c git@github.com:owner/repo.git):",
|
|
2932
|
-
default: currentUrl,
|
|
2933
|
-
validate: (v) => isValidGitUrl(v) || "URL kh\xF4ng \u0111\xFAng format git remote"
|
|
2934
|
-
});
|
|
2935
|
-
currentUrl = newUrl.trim();
|
|
3094
|
+
if (action === "switch-account") {
|
|
3095
|
+
triggerGhAuthLoginInteractive2();
|
|
2936
3096
|
}
|
|
2937
|
-
|
|
2938
|
-
|
|
3097
|
+
log.info("Ki\u1EC3m tra access...");
|
|
3098
|
+
if (checkRepoAccess(args.repoSlug)) {
|
|
3099
|
+
const finalUser = getCurrentGhUser2();
|
|
3100
|
+
log.success(`\u0110\xE3 c\xF3 access v\u1EDBi '${finalUser ?? "(unknown)"}' \u2014 ti\u1EBFp t\u1EE5c.`);
|
|
3101
|
+
return true;
|
|
2939
3102
|
}
|
|
2940
|
-
log.
|
|
2941
|
-
|
|
2942
|
-
|
|
2943
|
-
|
|
2944
|
-
|
|
3103
|
+
log.warn(
|
|
3104
|
+
`V\u1EABn ch\u01B0a c\xF3 access v\u1EDBi account '${ghUser ?? "(unknown)"}'. \u0110\u1EA3m b\u1EA3o \u0111\xE3 accept email invite ho\u1EB7c switch \u0111\xFAng account.`
|
|
3105
|
+
);
|
|
3106
|
+
}
|
|
3107
|
+
}
|
|
3108
|
+
|
|
3109
|
+
// src/lib/pick-latest-stable-semver-tag.ts
|
|
3110
|
+
var SEMVER_REGEX3 = /^v?(\d+)\.(\d+)\.(\d+)(?:-([0-9A-Za-z.-]+))?$/;
|
|
3111
|
+
function parseSemVerTag(tag) {
|
|
3112
|
+
const match = tag.match(SEMVER_REGEX3);
|
|
3113
|
+
if (!match) return null;
|
|
3114
|
+
const [, major, minor, patch, prerelease] = match;
|
|
3115
|
+
return {
|
|
3116
|
+
raw: tag,
|
|
3117
|
+
major: Number.parseInt(major ?? "0", 10),
|
|
3118
|
+
minor: Number.parseInt(minor ?? "0", 10),
|
|
3119
|
+
patch: Number.parseInt(patch ?? "0", 10),
|
|
3120
|
+
prerelease: prerelease ?? null
|
|
3121
|
+
};
|
|
3122
|
+
}
|
|
3123
|
+
function pickLatestStableSemVerTag(tags, includePrerelease = false) {
|
|
3124
|
+
const parsed = tags.map(parseSemVerTag).filter((t) => t !== null).filter((t) => includePrerelease || t.prerelease === null);
|
|
3125
|
+
if (parsed.length === 0) return null;
|
|
3126
|
+
parsed.sort((a, b) => {
|
|
3127
|
+
if (a.major !== b.major) return a.major - b.major;
|
|
3128
|
+
if (a.minor !== b.minor) return a.minor - b.minor;
|
|
3129
|
+
return a.patch - b.patch;
|
|
3130
|
+
});
|
|
3131
|
+
return parsed[parsed.length - 1]?.raw ?? null;
|
|
3132
|
+
}
|
|
3133
|
+
|
|
3134
|
+
// src/lib/resolve-team-pack-repo-url.ts
|
|
3135
|
+
var ORG_DEFAULT = "git@github.com:nalvn/team-ai-pack.git";
|
|
3136
|
+
function resolveTeamPackRepoUrl() {
|
|
3137
|
+
if (process.env.AVATAR_TEAM_PACK_REPO_URL) {
|
|
3138
|
+
return process.env.AVATAR_TEAM_PACK_REPO_URL;
|
|
3139
|
+
}
|
|
3140
|
+
return ORG_DEFAULT;
|
|
3141
|
+
}
|
|
3142
|
+
|
|
3143
|
+
// src/lib/team-pack-submodule-manager.ts
|
|
3144
|
+
var TEAM_PACK_REPO_URL = resolveTeamPackRepoUrl();
|
|
3145
|
+
var TEAM_PACK_RELATIVE_PATH = ".claude/pack";
|
|
3146
|
+
var TeamPackAccessAbortedError = class extends Error {
|
|
3147
|
+
constructor(message) {
|
|
3148
|
+
super(message);
|
|
3149
|
+
this.name = "TeamPackAccessAbortedError";
|
|
3150
|
+
}
|
|
3151
|
+
};
|
|
3152
|
+
var DEFAULT_PACK_BRANCH = "main";
|
|
3153
|
+
async function addTeamPackSubmodule(projectRoot, tag, ssoEmail, latest = false) {
|
|
3154
|
+
const url = resolveTeamPackRepoUrl();
|
|
3155
|
+
const repoSlug = parseRepoSlugFromGitUrl(url);
|
|
3156
|
+
if (repoSlug) {
|
|
3157
|
+
const hasAccess = await ensureTeamPackAccessWithRetry({ repoSlug, ssoEmail });
|
|
3158
|
+
if (!hasAccess) {
|
|
3159
|
+
throw new TeamPackAccessAbortedError(
|
|
3160
|
+
"User ch\u1ECDn t\u1EA1m ng\u01B0ng. Ch\u1EA1y l\u1EA1i 'avatar init' sau khi \u0111\u01B0\u1EE3c add v\xE0o org."
|
|
3161
|
+
);
|
|
2945
3162
|
}
|
|
2946
|
-
reason = result.reason ?? "unknown";
|
|
2947
|
-
detail = result.detail;
|
|
2948
3163
|
}
|
|
3164
|
+
try {
|
|
3165
|
+
await addSubmodule(url, TEAM_PACK_RELATIVE_PATH, projectRoot);
|
|
3166
|
+
} catch (err) {
|
|
3167
|
+
const msg = err instanceof Error ? err.message : String(err);
|
|
3168
|
+
if (msg.includes("Repository not found") || msg.includes("not found")) {
|
|
3169
|
+
log.error(
|
|
3170
|
+
`Repo team-ai-pack kh\xF4ng t\u1ED3n t\u1EA1i: ${url}
|
|
3171
|
+
C\xE1ch fix:
|
|
3172
|
+
1. T\u1EA1o repo: gh repo create <owner>/team-ai-pack --private --add-readme
|
|
3173
|
+
2. Ho\u1EB7c override URL: export AVATAR_TEAM_PACK_REPO_URL=<url-repo-c\u1EE7a-b\u1EA1n>
|
|
3174
|
+
3. Ho\u1EB7c d\xF9ng flag --skip-team-pack`
|
|
3175
|
+
);
|
|
3176
|
+
}
|
|
3177
|
+
throw err;
|
|
3178
|
+
}
|
|
3179
|
+
if (tag) {
|
|
3180
|
+
await checkoutTagInSubmodule(TEAM_PACK_RELATIVE_PATH, tag, projectRoot);
|
|
3181
|
+
return { pinnedTag: tag };
|
|
3182
|
+
}
|
|
3183
|
+
if (latest) {
|
|
3184
|
+
await checkoutBranchHeadInSubmodule(TEAM_PACK_RELATIVE_PATH, DEFAULT_PACK_BRANCH, projectRoot);
|
|
3185
|
+
return { pinnedTag: `${DEFAULT_PACK_BRANCH} (HEAD)` };
|
|
3186
|
+
}
|
|
3187
|
+
const submoduleDir = join21(projectRoot, TEAM_PACK_RELATIVE_PATH);
|
|
3188
|
+
const allTags = await listTags(submoduleDir);
|
|
3189
|
+
const target = pickLatestStableSemVerTag(allTags);
|
|
3190
|
+
if (target) {
|
|
3191
|
+
await checkoutTagInSubmodule(TEAM_PACK_RELATIVE_PATH, target, projectRoot);
|
|
3192
|
+
}
|
|
3193
|
+
return { pinnedTag: target };
|
|
3194
|
+
}
|
|
3195
|
+
async function readPinnedPackVersion(projectRoot) {
|
|
3196
|
+
const submoduleRoot = join21(projectRoot, TEAM_PACK_RELATIVE_PATH);
|
|
3197
|
+
const tag = await tagAtHead(submoduleRoot);
|
|
3198
|
+
if (tag) return tag;
|
|
3199
|
+
const sha = await currentCommitSha(submoduleRoot);
|
|
3200
|
+
return sha.slice(0, 7);
|
|
3201
|
+
}
|
|
3202
|
+
|
|
3203
|
+
// src/commands/init-flow-handlers-for-each-project-status.ts
|
|
3204
|
+
import { basename as basename3, join as join27, resolve as resolve2 } from "path";
|
|
3205
|
+
import { input as input7, select as select12 } from "@inquirer/prompts";
|
|
3206
|
+
|
|
3207
|
+
// src/lib/check-gh-cli-auth-status.ts
|
|
3208
|
+
import { spawnSync as spawnSync18 } from "child_process";
|
|
3209
|
+
function checkGhCliAuthStatus() {
|
|
3210
|
+
const r = spawnSync18("gh", ["auth", "status"], { stdio: "ignore" });
|
|
3211
|
+
if (r.error && r.error.code === "ENOENT") {
|
|
3212
|
+
return "not-installed";
|
|
3213
|
+
}
|
|
3214
|
+
return r.status === 0 ? "authenticated" : "not-authenticated";
|
|
3215
|
+
}
|
|
3216
|
+
|
|
3217
|
+
// src/lib/detect-package-manager.ts
|
|
3218
|
+
import { spawnSync as spawnSync19 } from "child_process";
|
|
3219
|
+
function hasBinary(name) {
|
|
3220
|
+
const platform2 = detectHostPlatform();
|
|
3221
|
+
const probe = platform2 === "win32" ? "where" : "command";
|
|
3222
|
+
const args = platform2 === "win32" ? [name] : ["-v", name];
|
|
3223
|
+
const r = spawnSync19(probe, args, {
|
|
3224
|
+
shell: platform2 !== "win32",
|
|
3225
|
+
stdio: "ignore"
|
|
3226
|
+
});
|
|
3227
|
+
return r.status === 0;
|
|
3228
|
+
}
|
|
3229
|
+
function detectPackageManager() {
|
|
3230
|
+
const platform2 = detectHostPlatform();
|
|
3231
|
+
const candidates = platform2 === "darwin" ? ["brew"] : platform2 === "win32" ? ["winget"] : platform2 === "linux" ? ["apt", "dnf", "pacman"] : [];
|
|
3232
|
+
for (const pm of candidates) {
|
|
3233
|
+
if (hasBinary(pm)) return pm;
|
|
3234
|
+
}
|
|
3235
|
+
return null;
|
|
2949
3236
|
}
|
|
2950
3237
|
|
|
2951
3238
|
// src/lib/install-gh-cli-via-package-manager.ts
|
|
@@ -3070,602 +3357,495 @@ async function ensureGitHubReady(remoteUrl) {
|
|
|
3070
3357
|
return {};
|
|
3071
3358
|
}
|
|
3072
3359
|
|
|
3073
|
-
// src/lib/
|
|
3074
|
-
|
|
3075
|
-
|
|
3076
|
-
|
|
3077
|
-
|
|
3078
|
-
|
|
3079
|
-
super(message);
|
|
3080
|
-
this.name = "CreateWorkspaceRemoteError";
|
|
3081
|
-
this.reason = reason;
|
|
3082
|
-
this.fullName = fullName;
|
|
3083
|
-
this.stderr = stderr;
|
|
3084
|
-
}
|
|
3085
|
-
};
|
|
3086
|
-
function classifyGhCreateError(stderr) {
|
|
3087
|
-
const text = stderr.toLowerCase();
|
|
3088
|
-
if (text.includes("name already exists") || text.includes("already exists on this account") || text.includes("repository already exists")) {
|
|
3089
|
-
return "repo-exists";
|
|
3090
|
-
}
|
|
3091
|
-
if (text.includes("403") || text.includes("permission") || text.includes("not authorized") || text.includes("forbidden")) {
|
|
3092
|
-
return "no-permission";
|
|
3093
|
-
}
|
|
3094
|
-
if (text.includes("invalid") && text.includes("name")) {
|
|
3095
|
-
return "name-invalid";
|
|
3096
|
-
}
|
|
3097
|
-
if (text.includes("could not resolve") || text.includes("network") || text.includes("connection refused")) {
|
|
3098
|
-
return "network";
|
|
3099
|
-
}
|
|
3100
|
-
return "unknown";
|
|
3101
|
-
}
|
|
3102
|
-
function repoExistsOnGitHub(fullName) {
|
|
3103
|
-
const r = spawnSync23("gh", ["repo", "view", fullName, "--json", "name"], {
|
|
3104
|
-
stdio: "ignore"
|
|
3105
|
-
});
|
|
3106
|
-
return r.status === 0;
|
|
3360
|
+
// src/lib/add-team-pack-submodule-with-retry-on-network-fail.ts
|
|
3361
|
+
import { spawnSync as spawnSync23 } from "child_process";
|
|
3362
|
+
import { select as select9 } from "@inquirer/prompts";
|
|
3363
|
+
function isSshPermissionError(message) {
|
|
3364
|
+
const text = message.toLowerCase();
|
|
3365
|
+
return text.includes("permission denied (publickey)") || text.includes("publickey)") || text.includes("ssh: could not resolve") || text.includes("host key verification failed");
|
|
3107
3366
|
}
|
|
3108
|
-
function
|
|
3109
|
-
|
|
3110
|
-
const r = spawnSync23("gh", ["
|
|
3111
|
-
|
|
3112
|
-
|
|
3113
|
-
if (r.status === 0) return { ok: true };
|
|
3114
|
-
const orgCheck = spawnSync23("gh", ["api", `orgs/${org}`, "--silent"], { stdio: "ignore" });
|
|
3115
|
-
if (orgCheck.status !== 0) {
|
|
3116
|
-
const userCheck = spawnSync23("gh", ["api", `users/${org}`, "--silent"], { stdio: "ignore" });
|
|
3117
|
-
if (userCheck.status === 0) {
|
|
3118
|
-
return {
|
|
3119
|
-
ok: false,
|
|
3120
|
-
reason: `'${org}' l\xE0 personal account kh\xE1c (kh\xF4ng ph\u1EA3i org). B\u1EA1n (${ghUser}) kh\xF4ng th\u1EC3 t\u1EA1o repo d\u01B0\u1EDBi account c\u1EE7a user kh\xE1c. Pass --repo-org=${ghUser} ho\u1EB7c switch gh: gh auth login`
|
|
3121
|
-
};
|
|
3122
|
-
}
|
|
3123
|
-
return {
|
|
3124
|
-
ok: false,
|
|
3125
|
-
reason: `'${org}' kh\xF4ng t\u1ED3n t\u1EA1i tr\xEAn GitHub. Check ch\xEDnh t\u1EA3 ho\u1EB7c t\u1EA1o org tr\u01B0\u1EDBc.`
|
|
3126
|
-
};
|
|
3367
|
+
function triggerGhAuthLoginInteractive3() {
|
|
3368
|
+
log.info("M\u1EDF browser \u0111\u1EC3 switch GitHub account...");
|
|
3369
|
+
const r = spawnSync23("gh", ["auth", "login", "--web"], { stdio: "inherit" });
|
|
3370
|
+
if (r.status !== 0) {
|
|
3371
|
+
log.warn(`gh auth login exit ${r.status}. C\xF3 th\u1EC3 ch\u1EA1y tay r\u1ED3i quay l\u1EA1i retry.`);
|
|
3127
3372
|
}
|
|
3128
|
-
return {
|
|
3129
|
-
ok: false,
|
|
3130
|
-
reason: `'${ghUser}' kh\xF4ng ph\u1EA3i member c\u1EE7a org '${org}'. Li\xEAn h\u1EC7 admin org \u0111\u1EC3 \u0111\u01B0\u1EE3c invite, ho\u1EB7c pass --repo-org=${ghUser} t\u1EA1o d\u01B0\u1EDBi personal account.`
|
|
3131
|
-
};
|
|
3132
3373
|
}
|
|
3133
|
-
|
|
3134
|
-
|
|
3135
|
-
|
|
3136
|
-
await ensureGitHubReady();
|
|
3137
|
-
const ghUser = resolveGithubUsernameDefault();
|
|
3138
|
-
const org = input6.org ?? ghUser;
|
|
3139
|
-
const namespaceCheck = canCreateInNamespace(org, ghUser);
|
|
3140
|
-
if (!namespaceCheck.ok) {
|
|
3141
|
-
throw new Error(`Kh\xF4ng th\u1EC3 t\u1EA1o repo d\u01B0\u1EDBi '${org}/': ${namespaceCheck.reason}`);
|
|
3142
|
-
}
|
|
3143
|
-
const fullName = `${org}/${input6.workspaceName}`;
|
|
3144
|
-
if (repoExistsOnGitHub(fullName)) {
|
|
3145
|
-
throw new CreateWorkspaceRemoteError(
|
|
3146
|
-
"repo-exists",
|
|
3147
|
-
fullName,
|
|
3148
|
-
`Repo '${fullName}' \u0111\xE3 t\u1ED3n t\u1EA1i tr\xEAn GitHub. C\xF3 th\u1EC3 b\u1EA1n \u0111\xE3 init workspace n\xE0y tr\u01B0\u1EDBc \u0111\xF3.`
|
|
3149
|
-
);
|
|
3150
|
-
}
|
|
3151
|
-
log.info(`T\u1EA1o GitHub repo cho workspace: ${fullName} (${input6.visibility})...`);
|
|
3152
|
-
const r = spawnSync23(
|
|
3153
|
-
"gh",
|
|
3154
|
-
[
|
|
3155
|
-
"repo",
|
|
3156
|
-
"create",
|
|
3157
|
-
fullName,
|
|
3158
|
-
`--${input6.visibility}`,
|
|
3159
|
-
"--source",
|
|
3160
|
-
input6.workspacePath,
|
|
3161
|
-
"--remote",
|
|
3162
|
-
"origin",
|
|
3163
|
-
"--push"
|
|
3164
|
-
],
|
|
3165
|
-
{ stdio: ["ignore", "pipe", "pipe"], encoding: "utf8" }
|
|
3166
|
-
);
|
|
3374
|
+
function openGithubSshKeysPage() {
|
|
3375
|
+
log.info("M\u1EDF trang GitHub Settings \u2192 SSH Keys...");
|
|
3376
|
+
const r = spawnSync23("open", ["https://github.com/settings/keys"], { stdio: "ignore" });
|
|
3167
3377
|
if (r.status !== 0) {
|
|
3168
|
-
|
|
3169
|
-
const stdout = (r.stdout || "").trim();
|
|
3170
|
-
const combined = [stderr, stdout].filter(Boolean).join("\n");
|
|
3171
|
-
const reason = classifyGhCreateError(combined);
|
|
3172
|
-
if (combined) {
|
|
3173
|
-
process.stderr.write(`
|
|
3174
|
-
${combined}
|
|
3175
|
-
|
|
3176
|
-
`);
|
|
3177
|
-
}
|
|
3178
|
-
throw new CreateWorkspaceRemoteError(
|
|
3179
|
-
reason,
|
|
3180
|
-
fullName,
|
|
3181
|
-
`T\u1EA1o workspace remote th\u1EA5t b\u1EA1i (exit ${r.status}): ${reason}.`,
|
|
3182
|
-
combined
|
|
3183
|
-
);
|
|
3378
|
+
log.info("URL: https://github.com/settings/keys");
|
|
3184
3379
|
}
|
|
3185
|
-
const sshUrl = `git@github.com:${fullName}.git`;
|
|
3186
|
-
const httpsUrl = `https://github.com/${fullName}.git`;
|
|
3187
|
-
log.success(`Workspace remote: ${sshUrl}`);
|
|
3188
|
-
return { sshUrl, httpsUrl };
|
|
3189
3380
|
}
|
|
3190
|
-
function
|
|
3191
|
-
|
|
3192
|
-
|
|
3193
|
-
|
|
3194
|
-
|
|
3195
|
-
|
|
3196
|
-
|
|
3197
|
-
|
|
3198
|
-
|
|
3199
|
-
|
|
3200
|
-
|
|
3201
|
-
|
|
3202
|
-
|
|
3203
|
-
|
|
3204
|
-
|
|
3205
|
-
|
|
3206
|
-
|
|
3207
|
-
|
|
3208
|
-
|
|
3209
|
-
|
|
3210
|
-
|
|
3211
|
-
|
|
3212
|
-
|
|
3213
|
-
|
|
3214
|
-
|
|
3215
|
-
{ cmd: "/avatar:plan", desc: "T\u1EA1o plan th\xF4ng minh v\u1EDBi prompt enhancement" },
|
|
3216
|
-
{ cmd: "/avatar:scout", desc: "T\xECm file li\xEAn quan trong codebase, ti\u1EBFt ki\u1EC7m token" },
|
|
3217
|
-
{ cmd: "/avatar:implement", desc: "B\u1EAFt \u0111\u1EA7u code & test theo plan c\xF3 s\u1EB5n" },
|
|
3218
|
-
{ cmd: "/avatar:build-full-flow", desc: "Implement m\u1ED9t feature t\u1EEBng b\u01B0\u1EDBc (end-to-end)" },
|
|
3219
|
-
{ cmd: "/avatar:fix", desc: "Ph\xE2n t\xEDch v\xE0 fix v\u1EA5n \u0111\u1EC1 t\u1EF1 \u0111i\u1EC1u h\u01B0\u1EDBng" },
|
|
3220
|
-
{ cmd: "/avatar:debug", desc: "Debug v\u1EA5n \u0111\u1EC1 k\u1EF9 thu\u1EADt + \u0111\u01B0a ra gi\u1EA3i ph\xE1p" },
|
|
3221
|
-
{ cmd: "/avatar:test", desc: "Ch\u1EA1y test tr\xEAn m\xE1y + ph\xE2n t\xEDch b\xE1o c\xE1o t\u1ED5ng h\u1EE3p" },
|
|
3222
|
-
{ cmd: "/avatar:design:good", desc: "T\u1EA1o thi\u1EBFt k\u1EBF ch\u1EC9n chu, s\u1ED1ng \u0111\u1ED9ng" },
|
|
3223
|
-
{ cmd: "/avatar:docs:init", desc: "Ph\xE2n t\xEDch codebase + t\u1EA1o t\xE0i li\u1EC7u kh\u1EDFi \u0111\u1EA7u" },
|
|
3224
|
-
{ cmd: "/avatar:status", desc: "Xem l\u1EA1i thay \u0111\u1ED5i g\u1EA7n \u0111\xE2y + t\u1ED5ng k\u1EBFt c\xF4ng vi\u1EC7c" },
|
|
3225
|
-
{ cmd: "/avatar:journal", desc: "Ghi nh\u1EADt k\xFD session" }
|
|
3226
|
-
];
|
|
3227
|
-
function formatPackCommandsCheatsheetBox() {
|
|
3228
|
-
const maxCmdWidth = Math.max(...PACK_COMMAND_CHEATSHEET.map((e) => e.cmd.length));
|
|
3229
|
-
const header = chalk.bold("\u{1F3AF} Slash commands t\u1EEB team-ai-pack");
|
|
3230
|
-
const subheader = chalk.dim("G\xF5 trong Claude Code session \u0111\u1EC3 g\u1ECDi capability c\u1EE7a pack:");
|
|
3231
|
-
const lines = PACK_COMMAND_CHEATSHEET.map((e) => {
|
|
3232
|
-
const cmdPadded = chalk.cyan(e.cmd.padEnd(maxCmdWidth));
|
|
3233
|
-
return ` ${cmdPadded} ${chalk.dim(e.desc)}`;
|
|
3234
|
-
});
|
|
3235
|
-
const footer = chalk.dim(
|
|
3236
|
-
"Catalog \u0111\u1EA7y \u0111\u1EE7 46 commands: cat .claude/pack/scripts/commands_data.yaml"
|
|
3237
|
-
);
|
|
3238
|
-
const content = [header, subheader, "", ...lines, "", footer].join("\n");
|
|
3239
|
-
return boxen4(content, {
|
|
3240
|
-
padding: 1,
|
|
3241
|
-
borderStyle: "round",
|
|
3242
|
-
borderColor: "cyan"
|
|
3381
|
+
async function handleSshPermissionError() {
|
|
3382
|
+
return await select9({
|
|
3383
|
+
message: "SSH permission denied. C\xE1ch x\u1EED l\xFD?",
|
|
3384
|
+
choices: [
|
|
3385
|
+
{
|
|
3386
|
+
name: "Switch GitHub account (gh auth login \u2014 m\u1EDF browser)",
|
|
3387
|
+
value: "switch"
|
|
3388
|
+
},
|
|
3389
|
+
{
|
|
3390
|
+
name: "D\xF9ng HTTPS thay SSH (override URL b\u1EB1ng env AVATAR_TEAM_PACK_REPO_URL)",
|
|
3391
|
+
value: "https"
|
|
3392
|
+
},
|
|
3393
|
+
{
|
|
3394
|
+
name: "T\xF4i v\u1EEBa add SSH key l\xEAn GitHub \u2014 retry",
|
|
3395
|
+
value: "retry"
|
|
3396
|
+
},
|
|
3397
|
+
{
|
|
3398
|
+
name: "B\u1ECF qua team-ai-pack (d\xF9ng avatar sync sau)",
|
|
3399
|
+
value: "skip"
|
|
3400
|
+
},
|
|
3401
|
+
{
|
|
3402
|
+
name: "T\u1EA1m ng\u01B0ng init \u2014 fix SSH key tay r\u1ED3i ch\u1EA1y l\u1EA1i",
|
|
3403
|
+
value: "abort"
|
|
3404
|
+
}
|
|
3405
|
+
]
|
|
3243
3406
|
});
|
|
3244
3407
|
}
|
|
3245
|
-
|
|
3246
|
-
|
|
3247
|
-
import { promises as fs9 } from "fs";
|
|
3248
|
-
import { join as join17 } from "path";
|
|
3249
|
-
async function isStatusLineCommandResolvable(workspacePath, command) {
|
|
3250
|
-
const trimmed = command.trim();
|
|
3251
|
-
const match = trimmed.match(/^(node|python|python3|bash|sh)\s+([^\s]+)/);
|
|
3252
|
-
if (!match?.[2]) {
|
|
3253
|
-
return true;
|
|
3254
|
-
}
|
|
3255
|
-
const filePath = match[2];
|
|
3256
|
-
const fullPath = filePath.startsWith("/") ? filePath : join17(workspacePath, filePath);
|
|
3257
|
-
return await pathExists(fullPath);
|
|
3258
|
-
}
|
|
3259
|
-
function backupFilename(originalPath) {
|
|
3260
|
-
const d = /* @__PURE__ */ new Date();
|
|
3261
|
-
const stamp = `${d.getFullYear().toString().slice(-2) + String(d.getMonth() + 1).padStart(2, "0") + String(d.getDate()).padStart(2, "0")}-${String(d.getHours()).padStart(2, "0")}${String(d.getMinutes()).padStart(2, "0")}`;
|
|
3262
|
-
return `${originalPath}.backup-${stamp}`;
|
|
3263
|
-
}
|
|
3264
|
-
function unionDedupe(a, b) {
|
|
3265
|
-
const seen = /* @__PURE__ */ new Set();
|
|
3266
|
-
const out = [];
|
|
3267
|
-
for (const item of [...a, ...b]) {
|
|
3268
|
-
const key = typeof item === "string" ? item : JSON.stringify(item);
|
|
3269
|
-
if (!seen.has(key)) {
|
|
3270
|
-
seen.add(key);
|
|
3271
|
-
out.push(item);
|
|
3272
|
-
}
|
|
3273
|
-
}
|
|
3274
|
-
return out;
|
|
3275
|
-
}
|
|
3276
|
-
function mergeHooksPerEvent(packHooks, userHooks) {
|
|
3277
|
-
const touched = [];
|
|
3278
|
-
const merged = { ...userHooks };
|
|
3279
|
-
for (const [event, packEntries] of Object.entries(packHooks)) {
|
|
3280
|
-
const userEntries = userHooks[event] || [];
|
|
3281
|
-
const union = unionDedupe(userEntries, packEntries);
|
|
3282
|
-
if (union.length !== userEntries.length) {
|
|
3283
|
-
touched.push(event);
|
|
3284
|
-
}
|
|
3285
|
-
merged[event] = union;
|
|
3286
|
-
}
|
|
3287
|
-
return { merged, touchedEvents: touched };
|
|
3288
|
-
}
|
|
3289
|
-
async function mergePackSettingsIntoProjectSettings(workspacePath) {
|
|
3290
|
-
const packTemplatePath = join17(workspacePath, ".claude", "pack", "templates", "settings.json.tpl");
|
|
3291
|
-
const projectSettingsPath = join17(workspacePath, ".claude", "settings.json");
|
|
3292
|
-
if (!await pathExists(packTemplatePath)) {
|
|
3293
|
-
return { action: "no-pack-template", changes: [] };
|
|
3294
|
-
}
|
|
3295
|
-
let packTemplate;
|
|
3296
|
-
try {
|
|
3297
|
-
const raw = await readText(packTemplatePath);
|
|
3298
|
-
packTemplate = JSON.parse(raw);
|
|
3299
|
-
} catch (err) {
|
|
3300
|
-
throw new Error(
|
|
3301
|
-
`Pack settings template kh\xF4ng parse \u0111\u01B0\u1EE3c JSON: ${err.message}. Path: ${packTemplatePath}`
|
|
3302
|
-
);
|
|
3303
|
-
}
|
|
3304
|
-
let userSettings = {};
|
|
3305
|
-
let projectHasSettings = false;
|
|
3306
|
-
if (await pathExists(projectSettingsPath)) {
|
|
3307
|
-
projectHasSettings = true;
|
|
3408
|
+
async function addTeamPackSubmoduleWithRetryOnNetworkFail(projectRoot, tag, ssoEmail, latest = false) {
|
|
3409
|
+
while (true) {
|
|
3308
3410
|
try {
|
|
3309
|
-
|
|
3411
|
+
const result = await addTeamPackSubmodule(projectRoot, tag, ssoEmail, latest);
|
|
3412
|
+
return { pinnedTag: result.pinnedTag, skipped: false };
|
|
3310
3413
|
} catch (err) {
|
|
3311
|
-
throw
|
|
3312
|
-
|
|
3313
|
-
)
|
|
3314
|
-
|
|
3315
|
-
|
|
3316
|
-
|
|
3317
|
-
|
|
3318
|
-
|
|
3319
|
-
|
|
3320
|
-
|
|
3321
|
-
|
|
3322
|
-
|
|
3323
|
-
|
|
3324
|
-
|
|
3325
|
-
|
|
3326
|
-
|
|
3327
|
-
|
|
3328
|
-
|
|
3329
|
-
|
|
3330
|
-
|
|
3331
|
-
|
|
3332
|
-
|
|
3333
|
-
|
|
3334
|
-
|
|
3335
|
-
|
|
3336
|
-
|
|
3337
|
-
|
|
3338
|
-
|
|
3339
|
-
|
|
3340
|
-
|
|
3341
|
-
|
|
3342
|
-
|
|
3343
|
-
|
|
3344
|
-
|
|
3345
|
-
|
|
3346
|
-
|
|
3414
|
+
if (err instanceof TeamPackAccessAbortedError) throw err;
|
|
3415
|
+
const message = err instanceof Error ? err.message : String(err);
|
|
3416
|
+
if (isSshPermissionError(message)) {
|
|
3417
|
+
log.warn("Pull team-ai-pack th\u1EA5t b\u1EA1i: SSH permission denied (publickey).");
|
|
3418
|
+
log.dim(
|
|
3419
|
+
" \u2192 M\xE1y n\xE0y ch\u01B0a c\xF3 SSH key \u0111\u01B0\u1EE3c register tr\xEAn GitHub, ho\u1EB7c key thu\u1ED9c account kh\xE1c."
|
|
3420
|
+
);
|
|
3421
|
+
const action2 = await handleSshPermissionError();
|
|
3422
|
+
if (action2 === "abort") {
|
|
3423
|
+
throw new UserAbortedRecoveryError(
|
|
3424
|
+
"User abort t\u1EA1i b\u01B0\u1EDBc pull team-ai-pack. Fix SSH key (https://github.com/settings/keys) r\u1ED3i ch\u1EA1y l\u1EA1i 'avatar init'."
|
|
3425
|
+
);
|
|
3426
|
+
}
|
|
3427
|
+
if (action2 === "skip") {
|
|
3428
|
+
log.warn(
|
|
3429
|
+
"Skip team-ai-pack. Workspace d\xF9ng \u0111\u01B0\u1EE3c nh\u01B0ng kh\xF4ng c\xF3 shared knowledge. Pull sau qua `avatar sync`."
|
|
3430
|
+
);
|
|
3431
|
+
return { pinnedTag: null, skipped: true };
|
|
3432
|
+
}
|
|
3433
|
+
if (action2 === "switch") {
|
|
3434
|
+
triggerGhAuthLoginInteractive3();
|
|
3435
|
+
continue;
|
|
3436
|
+
}
|
|
3437
|
+
if (action2 === "https") {
|
|
3438
|
+
process.env.AVATAR_TEAM_PACK_REPO_URL = "https://github.com/nalvn/team-ai-pack.git";
|
|
3439
|
+
log.info("Override URL sang HTTPS. L\u01B0u \xFD: HTTPS c\xF3 th\u1EC3 fail 403 n\u1EBFu read-only role.");
|
|
3440
|
+
openGithubSshKeysPage();
|
|
3441
|
+
continue;
|
|
3442
|
+
}
|
|
3443
|
+
continue;
|
|
3444
|
+
}
|
|
3445
|
+
const action = await promptRetryOrSkip({
|
|
3446
|
+
taskName: "Pull team-ai-pack submodule",
|
|
3447
|
+
reason: message,
|
|
3448
|
+
allowSkip: true,
|
|
3449
|
+
hint: "Network glitch? Retry th\u01B0\u1EDDng work. N\u1EBFu skip, d\xF9ng `avatar sync` sau \u0111\u1EC3 pull pack."
|
|
3450
|
+
});
|
|
3451
|
+
if (action === "abort") {
|
|
3452
|
+
throw new UserAbortedRecoveryError("User abort t\u1EA1i b\u01B0\u1EDBc pull team-ai-pack.");
|
|
3453
|
+
}
|
|
3454
|
+
if (action === "skip") {
|
|
3455
|
+
log.warn(
|
|
3456
|
+
"Skip team-ai-pack. Workspace d\xF9ng \u0111\u01B0\u1EE3c nh\u01B0ng kh\xF4ng c\xF3 shared knowledge. Pull sau qua `avatar sync`."
|
|
3457
|
+
);
|
|
3458
|
+
return { pinnedTag: null, skipped: true };
|
|
3347
3459
|
}
|
|
3348
|
-
}
|
|
3349
|
-
if (envChanged) {
|
|
3350
|
-
merged.env = mergedEnv;
|
|
3351
|
-
changes.push("env vars added from pack");
|
|
3352
|
-
}
|
|
3353
|
-
}
|
|
3354
|
-
if (packTemplate.permissions) {
|
|
3355
|
-
const userAllow = userSettings.permissions?.allow || [];
|
|
3356
|
-
const userDeny = userSettings.permissions?.deny || [];
|
|
3357
|
-
const packAllow = packTemplate.permissions.allow || [];
|
|
3358
|
-
const packDeny = packTemplate.permissions.deny || [];
|
|
3359
|
-
const mergedAllow = unionDedupe(userAllow, packAllow);
|
|
3360
|
-
const mergedDeny = unionDedupe(userDeny, packDeny);
|
|
3361
|
-
if (mergedAllow.length !== userAllow.length || mergedDeny.length !== userDeny.length) {
|
|
3362
|
-
merged.permissions = { allow: mergedAllow, deny: mergedDeny };
|
|
3363
|
-
changes.push(
|
|
3364
|
-
`permissions union (+${mergedAllow.length - userAllow.length} allow, +${mergedDeny.length - userDeny.length} deny)`
|
|
3365
|
-
);
|
|
3366
3460
|
}
|
|
3367
3461
|
}
|
|
3368
|
-
|
|
3369
|
-
|
|
3370
|
-
|
|
3371
|
-
|
|
3372
|
-
|
|
3373
|
-
|
|
3374
|
-
|
|
3375
|
-
|
|
3376
|
-
|
|
3462
|
+
}
|
|
3463
|
+
|
|
3464
|
+
// src/lib/read-cli-version-from-package-json.ts
|
|
3465
|
+
import { readFileSync as readFileSync5 } from "fs";
|
|
3466
|
+
import { dirname as dirname5, resolve } from "path";
|
|
3467
|
+
import { fileURLToPath as fileURLToPath3 } from "url";
|
|
3468
|
+
var cachedVersion = null;
|
|
3469
|
+
function readCliVersion() {
|
|
3470
|
+
if (cachedVersion !== null) return cachedVersion;
|
|
3471
|
+
const here = dirname5(fileURLToPath3(import.meta.url));
|
|
3472
|
+
for (let i = 0; i < 5; i++) {
|
|
3473
|
+
const candidate = resolve(here, ...Array(i).fill(".."), "package.json");
|
|
3474
|
+
try {
|
|
3475
|
+
const raw = readFileSync5(candidate, "utf8");
|
|
3476
|
+
const pkg = JSON.parse(raw);
|
|
3477
|
+
if (pkg.name === "@nalvietnam/avatar-cli" && typeof pkg.version === "string") {
|
|
3478
|
+
cachedVersion = pkg.version;
|
|
3479
|
+
return cachedVersion;
|
|
3480
|
+
}
|
|
3481
|
+
} catch {
|
|
3377
3482
|
}
|
|
3378
3483
|
}
|
|
3379
|
-
|
|
3380
|
-
|
|
3381
|
-
}
|
|
3382
|
-
let backupPath;
|
|
3383
|
-
if (projectHasSettings) {
|
|
3384
|
-
backupPath = backupFilename(projectSettingsPath);
|
|
3385
|
-
await fs9.copyFile(projectSettingsPath, backupPath);
|
|
3386
|
-
}
|
|
3387
|
-
await writeJsonAtomic(projectSettingsPath, merged);
|
|
3388
|
-
return { action: "merged", backupPath, changes };
|
|
3484
|
+
cachedVersion = "unknown";
|
|
3485
|
+
return cachedVersion;
|
|
3389
3486
|
}
|
|
3390
3487
|
|
|
3391
|
-
// src/
|
|
3392
|
-
|
|
3393
|
-
|
|
3394
|
-
|
|
3488
|
+
// src/commands/init-scaffold-variable-builders.ts
|
|
3489
|
+
function inferWorkspaceName(repoUrl) {
|
|
3490
|
+
const trimmed = repoUrl.trim().replace(/\/+$/, "");
|
|
3491
|
+
const lastSegment = trimmed.split(/[/:]/).pop() ?? "";
|
|
3492
|
+
const baseName = lastSegment.replace(/\.git$/, "");
|
|
3493
|
+
if (!baseName) return "avatar-client-workspace";
|
|
3494
|
+
const withoutPrefix = baseName.replace(/^avatar-/, "");
|
|
3495
|
+
return `avatar-${withoutPrefix}-workspace`;
|
|
3496
|
+
}
|
|
3497
|
+
function buildGitnexusSection(gitnexusReady) {
|
|
3498
|
+
if (!gitnexusReady) return "";
|
|
3499
|
+
return `
|
|
3500
|
+
### \u{1F9E0} CODEBASE INTELLIGENCE \u2014 GitNexus
|
|
3395
3501
|
|
|
3396
|
-
|
|
3397
|
-
|
|
3398
|
-
|
|
3399
|
-
|
|
3400
|
-
|
|
3401
|
-
|
|
3402
|
-
|
|
3403
|
-
|
|
3502
|
+
Workspace c\xF3 GitNexus index t\u1EA1i \`.gitnexus/\` cung c\u1EA5p architectural awareness
|
|
3503
|
+
cho Claude Code (impact analysis, call chains, blast radius).
|
|
3504
|
+
|
|
3505
|
+
**H\u01B0\u1EDBng d\u1EABn cho Claude:**
|
|
3506
|
+
|
|
3507
|
+
- Khi c\u1EA7n hi\u1EC3u codebase architecture (vd: tr\u01B0\u1EDBc refactor, debug call chain) \u2192
|
|
3508
|
+
query GitNexus MCP resource \`gitnexus://repo/<name>/context\`
|
|
3509
|
+
- Khi resource tr\u1EA3 staleness hint "\u26A0\uFE0F Index is N commit(s) behind HEAD" \u2192
|
|
3510
|
+
T\u1EF0 ch\u1EA1y \`gitnexus analyze .\` (trong workspace root) tr\u01B0\u1EDBc khi ti\u1EBFp t\u1EE5c task
|
|
3511
|
+
- Sau khi \`gitnexus analyze\` xong: **KH\xD4NG t\u1EF1 ch\u1EA1y \`gitnexus wiki\`**
|
|
3512
|
+
(cost cao, c\xF3 th\u1EC3 block session 2-5 ph\xFAt) \u2014 user s\u1EBD ch\u1EA1y manual khi c\u1EA7n
|
|
3513
|
+
|
|
3514
|
+
**Folders Claude scan auto cho skills:**
|
|
3515
|
+
|
|
3516
|
+
- \`~/.claude/skills/gitnexus-*/\` \u2014 GitNexus global skills (exploring, debugging, ...)
|
|
3517
|
+
- \`.claude/pack/skills/\` \u2014 NAL team-shared skills (qua team-ai-pack submodule)
|
|
3518
|
+
- C\u1EA3 2 \u0111\u1EC1u \u0111\u01B0\u1EE3c scan, kh\xF4ng xung \u0111\u1ED9t (different naming prefix)
|
|
3519
|
+
|
|
3520
|
+
**Manual wiki update:**
|
|
3521
|
+
|
|
3522
|
+
Khi user c\u1EA7n regenerate wiki sau refactor l\u1EDBn \u2014 ch\u1EA1y:
|
|
3523
|
+
|
|
3524
|
+
\`\`\`bash
|
|
3525
|
+
gitnexus wiki . --api-key <key> --base-url <url>
|
|
3526
|
+
\`\`\`
|
|
3527
|
+
`;
|
|
3528
|
+
}
|
|
3529
|
+
function buildScaffoldVariables(args) {
|
|
3530
|
+
return {
|
|
3531
|
+
projectName: args.projectName,
|
|
3532
|
+
projectDescription: args.projectDescription,
|
|
3533
|
+
teamOwner: args.teamOwner,
|
|
3534
|
+
avatarVersion: readCliVersion(),
|
|
3535
|
+
packVersion: args.packVersion,
|
|
3536
|
+
lastScan: (/* @__PURE__ */ new Date()).toISOString(),
|
|
3537
|
+
mode: args.mode,
|
|
3538
|
+
gitnexusSection: buildGitnexusSection(args.gitnexusReady ?? false)
|
|
3539
|
+
};
|
|
3404
3540
|
}
|
|
3405
3541
|
|
|
3406
|
-
// src/
|
|
3407
|
-
|
|
3408
|
-
|
|
3409
|
-
|
|
3410
|
-
const
|
|
3411
|
-
|
|
3412
|
-
|
|
3413
|
-
await g.init();
|
|
3414
|
-
}
|
|
3415
|
-
try {
|
|
3416
|
-
await g.branch(["-M", "main"]);
|
|
3417
|
-
} catch {
|
|
3418
|
-
}
|
|
3419
|
-
await g.add(".");
|
|
3420
|
-
const status = await g.status();
|
|
3421
|
-
const hasCommits = (await g.raw(["rev-list", "-n", "1", "--all"]).catch(() => "")).trim();
|
|
3422
|
-
if (hasCommits) return;
|
|
3423
|
-
if (status.files.length === 0) {
|
|
3424
|
-
await g.commit(INITIAL_COMMIT_MESSAGE, void 0, { "--allow-empty": null });
|
|
3425
|
-
} else {
|
|
3426
|
-
await g.commit(INITIAL_COMMIT_MESSAGE);
|
|
3542
|
+
// src/commands/init-options-and-bootstrap-strategy-parser.ts
|
|
3543
|
+
function parseBootstrapStrategyOpts(opts) {
|
|
3544
|
+
if (opts.preserveUncommitted) return "stash";
|
|
3545
|
+
if (!opts.bootstrapStrategy) return void 0;
|
|
3546
|
+
const valid = ["stash", "commit-all", "skip", "branch"];
|
|
3547
|
+
if (valid.includes(opts.bootstrapStrategy)) {
|
|
3548
|
+
return opts.bootstrapStrategy;
|
|
3427
3549
|
}
|
|
3550
|
+
throw new Error(
|
|
3551
|
+
`--bootstrap-strategy kh\xF4ng h\u1EE3p l\u1EC7: ${opts.bootstrapStrategy}. Ch\u1ECDn: ${valid.join(" | ")}`
|
|
3552
|
+
);
|
|
3428
3553
|
}
|
|
3429
3554
|
|
|
3430
|
-
// src/
|
|
3431
|
-
import {
|
|
3432
|
-
import {
|
|
3433
|
-
|
|
3434
|
-
|
|
3435
|
-
|
|
3436
|
-
|
|
3437
|
-
|
|
3438
|
-
|
|
3439
|
-
|
|
3555
|
+
// src/commands/workspace-scaffold-and-finalize-orchestrator.ts
|
|
3556
|
+
import { join as join26 } from "path";
|
|
3557
|
+
import { basename as basename2 } from "path";
|
|
3558
|
+
import { confirm as confirm6, input as input6, select as select11 } from "@inquirer/prompts";
|
|
3559
|
+
|
|
3560
|
+
// src/lib/create-workspace-remote-via-gh.ts
|
|
3561
|
+
import { spawnSync as spawnSync24 } from "child_process";
|
|
3562
|
+
var CreateWorkspaceRemoteError = class extends Error {
|
|
3563
|
+
reason;
|
|
3564
|
+
fullName;
|
|
3565
|
+
stderr;
|
|
3566
|
+
constructor(reason, fullName, message, stderr) {
|
|
3567
|
+
super(message);
|
|
3568
|
+
this.name = "CreateWorkspaceRemoteError";
|
|
3569
|
+
this.reason = reason;
|
|
3570
|
+
this.fullName = fullName;
|
|
3571
|
+
this.stderr = stderr;
|
|
3572
|
+
}
|
|
3440
3573
|
};
|
|
3441
|
-
function
|
|
3442
|
-
const
|
|
3443
|
-
|
|
3444
|
-
|
|
3445
|
-
matched.push(stack);
|
|
3446
|
-
}
|
|
3574
|
+
function classifyGhCreateError(stderr) {
|
|
3575
|
+
const text = stderr.toLowerCase();
|
|
3576
|
+
if (text.includes("name already exists") || text.includes("already exists on this account") || text.includes("repository already exists")) {
|
|
3577
|
+
return "repo-exists";
|
|
3447
3578
|
}
|
|
3448
|
-
|
|
3579
|
+
if (text.includes("403") || text.includes("permission") || text.includes("not authorized") || text.includes("forbidden")) {
|
|
3580
|
+
return "no-permission";
|
|
3581
|
+
}
|
|
3582
|
+
if (text.includes("invalid") && text.includes("name")) {
|
|
3583
|
+
return "name-invalid";
|
|
3584
|
+
}
|
|
3585
|
+
if (text.includes("could not resolve") || text.includes("network") || text.includes("connection refused")) {
|
|
3586
|
+
return "network";
|
|
3587
|
+
}
|
|
3588
|
+
return "unknown";
|
|
3449
3589
|
}
|
|
3450
|
-
|
|
3451
|
-
|
|
3452
|
-
|
|
3453
|
-
|
|
3454
|
-
|
|
3455
|
-
|
|
3456
|
-
|
|
3457
|
-
|
|
3458
|
-
|
|
3459
|
-
|
|
3460
|
-
|
|
3461
|
-
|
|
3462
|
-
|
|
3463
|
-
|
|
3464
|
-
|
|
3465
|
-
|
|
3466
|
-
|
|
3467
|
-
|
|
3468
|
-
|
|
3469
|
-
|
|
3470
|
-
try {
|
|
3471
|
-
return readFileSync3(join20(dir, `${stack}.txt`), "utf8");
|
|
3472
|
-
} catch {
|
|
3590
|
+
function repoExistsOnGitHub(fullName) {
|
|
3591
|
+
const r = spawnSync24("gh", ["repo", "view", fullName, "--json", "name"], {
|
|
3592
|
+
stdio: "ignore"
|
|
3593
|
+
});
|
|
3594
|
+
return r.status === 0;
|
|
3595
|
+
}
|
|
3596
|
+
function canCreateInNamespace(org, ghUser) {
|
|
3597
|
+
if (org.toLowerCase() === ghUser.toLowerCase()) return { ok: true };
|
|
3598
|
+
const r = spawnSync24("gh", ["api", `orgs/${org}/members/${ghUser}`, "--silent"], {
|
|
3599
|
+
stdio: "ignore"
|
|
3600
|
+
});
|
|
3601
|
+
if (r.status === 0) return { ok: true };
|
|
3602
|
+
const orgCheck = spawnSync24("gh", ["api", `orgs/${org}`, "--silent"], { stdio: "ignore" });
|
|
3603
|
+
if (orgCheck.status !== 0) {
|
|
3604
|
+
const userCheck = spawnSync24("gh", ["api", `users/${org}`, "--silent"], { stdio: "ignore" });
|
|
3605
|
+
if (userCheck.status === 0) {
|
|
3606
|
+
return {
|
|
3607
|
+
ok: false,
|
|
3608
|
+
reason: `'${org}' l\xE0 personal account kh\xE1c (kh\xF4ng ph\u1EA3i org). B\u1EA1n (${ghUser}) kh\xF4ng th\u1EC3 t\u1EA1o repo d\u01B0\u1EDBi account c\u1EE7a user kh\xE1c. Pass --repo-org=${ghUser} ho\u1EB7c switch gh: gh auth login`
|
|
3609
|
+
};
|
|
3473
3610
|
}
|
|
3611
|
+
return {
|
|
3612
|
+
ok: false,
|
|
3613
|
+
reason: `'${org}' kh\xF4ng t\u1ED3n t\u1EA1i tr\xEAn GitHub. Check ch\xEDnh t\u1EA3 ho\u1EB7c t\u1EA1o org tr\u01B0\u1EDBc.`
|
|
3614
|
+
};
|
|
3474
3615
|
}
|
|
3475
|
-
|
|
3476
|
-
|
|
3477
|
-
|
|
3478
|
-
|
|
3479
|
-
const sections = all.map((s) => `# --- ${s} ---
|
|
3480
|
-
${readTemplate(s).trim()}`);
|
|
3481
|
-
return [AVATAR_MARKER_START, ...sections, AVATAR_MARKER_END, ""].join("\n");
|
|
3616
|
+
return {
|
|
3617
|
+
ok: false,
|
|
3618
|
+
reason: `'${ghUser}' kh\xF4ng ph\u1EA3i member c\u1EE7a org '${org}'. Li\xEAn h\u1EC7 admin org \u0111\u1EC3 \u0111\u01B0\u1EE3c invite, ho\u1EB7c pass --repo-org=${ghUser} t\u1EA1o d\u01B0\u1EDBi personal account.`
|
|
3619
|
+
};
|
|
3482
3620
|
}
|
|
3483
|
-
|
|
3484
|
-
|
|
3485
|
-
|
|
3486
|
-
|
|
3487
|
-
|
|
3488
|
-
const
|
|
3489
|
-
|
|
3490
|
-
|
|
3491
|
-
|
|
3621
|
+
async function createWorkspaceRemoteViaGh(input8) {
|
|
3622
|
+
validateRepoName(input8.workspaceName);
|
|
3623
|
+
validateRepoVisibility(input8.visibility);
|
|
3624
|
+
await ensureGitHubReady();
|
|
3625
|
+
const ghUser = resolveGithubUsernameDefault();
|
|
3626
|
+
const org = input8.org ?? ghUser;
|
|
3627
|
+
const namespaceCheck = canCreateInNamespace(org, ghUser);
|
|
3628
|
+
if (!namespaceCheck.ok) {
|
|
3629
|
+
throw new Error(`Kh\xF4ng th\u1EC3 t\u1EA1o repo d\u01B0\u1EDBi '${org}/': ${namespaceCheck.reason}`);
|
|
3492
3630
|
}
|
|
3493
|
-
const
|
|
3494
|
-
|
|
3495
|
-
|
|
3496
|
-
|
|
3497
|
-
|
|
3498
|
-
|
|
3499
|
-
|
|
3631
|
+
const fullName = `${org}/${input8.workspaceName}`;
|
|
3632
|
+
if (repoExistsOnGitHub(fullName)) {
|
|
3633
|
+
throw new CreateWorkspaceRemoteError(
|
|
3634
|
+
"repo-exists",
|
|
3635
|
+
fullName,
|
|
3636
|
+
`Repo '${fullName}' \u0111\xE3 t\u1ED3n t\u1EA1i tr\xEAn GitHub. C\xF3 th\u1EC3 b\u1EA1n \u0111\xE3 init workspace n\xE0y tr\u01B0\u1EDBc \u0111\xF3.`
|
|
3637
|
+
);
|
|
3638
|
+
}
|
|
3639
|
+
log.info(`T\u1EA1o GitHub repo cho workspace: ${fullName} (${input8.visibility})...`);
|
|
3640
|
+
const r = spawnSync24(
|
|
3641
|
+
"gh",
|
|
3642
|
+
[
|
|
3643
|
+
"repo",
|
|
3644
|
+
"create",
|
|
3645
|
+
fullName,
|
|
3646
|
+
`--${input8.visibility}`,
|
|
3647
|
+
"--source",
|
|
3648
|
+
input8.workspacePath,
|
|
3649
|
+
"--remote",
|
|
3650
|
+
"origin",
|
|
3651
|
+
"--push"
|
|
3652
|
+
],
|
|
3653
|
+
{ stdio: ["ignore", "pipe", "pipe"], encoding: "utf8" }
|
|
3654
|
+
);
|
|
3655
|
+
if (r.status !== 0) {
|
|
3656
|
+
const stderr = (r.stderr || "").trim();
|
|
3657
|
+
const stdout = (r.stdout || "").trim();
|
|
3658
|
+
const combined = [stderr, stdout].filter(Boolean).join("\n");
|
|
3659
|
+
const reason = classifyGhCreateError(combined);
|
|
3660
|
+
if (combined) {
|
|
3661
|
+
process.stderr.write(`
|
|
3662
|
+
${combined}
|
|
3500
3663
|
|
|
3501
|
-
|
|
3502
|
-
|
|
3664
|
+
`);
|
|
3665
|
+
}
|
|
3666
|
+
throw new CreateWorkspaceRemoteError(
|
|
3667
|
+
reason,
|
|
3668
|
+
fullName,
|
|
3669
|
+
`T\u1EA1o workspace remote th\u1EA5t b\u1EA1i (exit ${r.status}): ${reason}.`,
|
|
3670
|
+
combined
|
|
3671
|
+
);
|
|
3503
3672
|
}
|
|
3504
|
-
|
|
3505
|
-
|
|
3506
|
-
${
|
|
3673
|
+
const sshUrl = `git@github.com:${fullName}.git`;
|
|
3674
|
+
const httpsUrl = `https://github.com/${fullName}.git`;
|
|
3675
|
+
log.success(`Workspace remote: ${sshUrl}`);
|
|
3676
|
+
return { sshUrl, httpsUrl };
|
|
3507
3677
|
}
|
|
3508
|
-
|
|
3509
|
-
|
|
3510
|
-
|
|
3511
|
-
|
|
3512
|
-
|
|
3513
|
-
|
|
3678
|
+
function linkExistingRemoteToWorkspace(args) {
|
|
3679
|
+
const sshUrl = `git@github.com:${args.fullName}.git`;
|
|
3680
|
+
const httpsUrl = `https://github.com/${args.fullName}.git`;
|
|
3681
|
+
const addResult = spawnSync24(
|
|
3682
|
+
"git",
|
|
3683
|
+
["-C", args.workspacePath, "remote", "add", "origin", sshUrl],
|
|
3684
|
+
{
|
|
3685
|
+
encoding: "utf8",
|
|
3686
|
+
stdio: ["ignore", "pipe", "pipe"]
|
|
3687
|
+
}
|
|
3688
|
+
);
|
|
3689
|
+
if (addResult.status !== 0) {
|
|
3690
|
+
spawnSync24("git", ["-C", args.workspacePath, "remote", "set-url", "origin", sshUrl], {
|
|
3691
|
+
stdio: "ignore"
|
|
3692
|
+
});
|
|
3514
3693
|
}
|
|
3515
|
-
};
|
|
3516
|
-
|
|
3517
|
-
|
|
3518
|
-
|
|
3519
|
-
|
|
3520
|
-
|
|
3694
|
+
log.success(`Linked existing remote: ${sshUrl}`);
|
|
3695
|
+
return { sshUrl, httpsUrl };
|
|
3696
|
+
}
|
|
3697
|
+
|
|
3698
|
+
// src/lib/merge-pack-settings-into-project-settings.ts
|
|
3699
|
+
import { promises as fs10 } from "fs";
|
|
3700
|
+
import { join as join22 } from "path";
|
|
3701
|
+
async function isStatusLineCommandResolvable(workspacePath, command) {
|
|
3702
|
+
const trimmed = command.trim();
|
|
3703
|
+
const match = trimmed.match(/^(node|python|python3|bash|sh)\s+([^\s]+)/);
|
|
3704
|
+
if (!match?.[2]) {
|
|
3705
|
+
return true;
|
|
3521
3706
|
}
|
|
3522
|
-
const
|
|
3523
|
-
const
|
|
3524
|
-
return
|
|
3707
|
+
const filePath = match[2];
|
|
3708
|
+
const fullPath = filePath.startsWith("/") ? filePath : join22(workspacePath, filePath);
|
|
3709
|
+
return await pathExists(fullPath);
|
|
3525
3710
|
}
|
|
3526
|
-
|
|
3527
|
-
|
|
3528
|
-
|
|
3529
|
-
|
|
3530
|
-
return await select8({
|
|
3531
|
-
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:",
|
|
3532
|
-
choices: [
|
|
3533
|
-
{
|
|
3534
|
-
value: "stash",
|
|
3535
|
-
name: "1. Stash changes \u2192 bootstrap \u2192 restore (KHUY\u1EBEN NGH\u1ECA)"
|
|
3536
|
-
},
|
|
3537
|
-
{
|
|
3538
|
-
value: "commit-all",
|
|
3539
|
-
name: "2. Commit to\xE0n b\u1ED9 v\xE0o initial commit (legacy v1.1.6)"
|
|
3540
|
-
},
|
|
3541
|
-
{
|
|
3542
|
-
value: "skip",
|
|
3543
|
-
name: "3. Skip \u2014 t\xF4i commit th\u1EE7 c\xF4ng r\u1ED3i ch\u1EA1y l\u1EA1i"
|
|
3544
|
-
},
|
|
3545
|
-
{
|
|
3546
|
-
value: "branch",
|
|
3547
|
-
name: "4. Commit v\xE0o branch ri\xEAng `avatar/init` (main gi\u1EEF s\u1EA1ch)"
|
|
3548
|
-
}
|
|
3549
|
-
],
|
|
3550
|
-
default: "stash"
|
|
3551
|
-
});
|
|
3711
|
+
function backupFilename(originalPath) {
|
|
3712
|
+
const d = /* @__PURE__ */ new Date();
|
|
3713
|
+
const stamp = `${d.getFullYear().toString().slice(-2) + String(d.getMonth() + 1).padStart(2, "0") + String(d.getDate()).padStart(2, "0")}-${String(d.getHours()).padStart(2, "0")}${String(d.getMinutes()).padStart(2, "0")}`;
|
|
3714
|
+
return `${originalPath}.backup-${stamp}`;
|
|
3552
3715
|
}
|
|
3553
|
-
|
|
3554
|
-
const
|
|
3555
|
-
|
|
3556
|
-
|
|
3557
|
-
|
|
3558
|
-
|
|
3716
|
+
function unionDedupe(a, b) {
|
|
3717
|
+
const seen = /* @__PURE__ */ new Set();
|
|
3718
|
+
const out = [];
|
|
3719
|
+
for (const item of [...a, ...b]) {
|
|
3720
|
+
const key = typeof item === "string" ? item : JSON.stringify(item);
|
|
3721
|
+
if (!seen.has(key)) {
|
|
3722
|
+
seen.add(key);
|
|
3723
|
+
out.push(item);
|
|
3724
|
+
}
|
|
3725
|
+
}
|
|
3726
|
+
return out;
|
|
3559
3727
|
}
|
|
3560
|
-
|
|
3728
|
+
function mergeHooksPerEvent(packHooks, userHooks) {
|
|
3729
|
+
const touched = [];
|
|
3730
|
+
const merged = { ...userHooks };
|
|
3731
|
+
for (const [event, packEntries] of Object.entries(packHooks)) {
|
|
3732
|
+
const userEntries = userHooks[event] || [];
|
|
3733
|
+
const union = unionDedupe(userEntries, packEntries);
|
|
3734
|
+
if (union.length !== userEntries.length) {
|
|
3735
|
+
touched.push(event);
|
|
3736
|
+
}
|
|
3737
|
+
merged[event] = union;
|
|
3738
|
+
}
|
|
3739
|
+
return { merged, touchedEvents: touched };
|
|
3740
|
+
}
|
|
3741
|
+
async function mergePackSettingsIntoProjectSettings(workspacePath) {
|
|
3742
|
+
const packTemplatePath = join22(workspacePath, ".claude", "pack", "templates", "settings.json.tpl");
|
|
3743
|
+
const projectSettingsPath = join22(workspacePath, ".claude", "settings.json");
|
|
3744
|
+
if (!await pathExists(packTemplatePath)) {
|
|
3745
|
+
return { action: "no-pack-template", changes: [] };
|
|
3746
|
+
}
|
|
3747
|
+
let packTemplate;
|
|
3561
3748
|
try {
|
|
3562
|
-
await
|
|
3563
|
-
|
|
3749
|
+
const raw = await readText(packTemplatePath);
|
|
3750
|
+
packTemplate = JSON.parse(raw);
|
|
3564
3751
|
} catch (err) {
|
|
3565
|
-
|
|
3566
|
-
|
|
3752
|
+
throw new Error(
|
|
3753
|
+
`Pack settings template kh\xF4ng parse \u0111\u01B0\u1EE3c JSON: ${err.message}. Path: ${packTemplatePath}`
|
|
3567
3754
|
);
|
|
3568
|
-
log.warn("Resolve: git stash show -p stash@{0} \u2192 fix conflict \u2192 git stash drop");
|
|
3569
|
-
log.dim(`Detail: ${err.message}`);
|
|
3570
3755
|
}
|
|
3571
|
-
}
|
|
3572
|
-
|
|
3573
|
-
|
|
3574
|
-
|
|
3575
|
-
|
|
3576
|
-
|
|
3577
|
-
|
|
3578
|
-
|
|
3756
|
+
let userSettings = {};
|
|
3757
|
+
let projectHasSettings = false;
|
|
3758
|
+
if (await pathExists(projectSettingsPath)) {
|
|
3759
|
+
projectHasSettings = true;
|
|
3760
|
+
try {
|
|
3761
|
+
userSettings = await readJson(projectSettingsPath);
|
|
3762
|
+
} catch (err) {
|
|
3763
|
+
throw new Error(
|
|
3764
|
+
`Project settings.json kh\xF4ng parse \u0111\u01B0\u1EE3c: ${err.message}. Manual fix tr\u01B0\u1EDBc khi sync.`
|
|
3765
|
+
);
|
|
3766
|
+
}
|
|
3579
3767
|
}
|
|
3580
|
-
|
|
3581
|
-
|
|
3582
|
-
|
|
3583
|
-
|
|
3584
|
-
|
|
3585
|
-
|
|
3586
|
-
|
|
3587
|
-
|
|
3588
|
-
|
|
3589
|
-
|
|
3590
|
-
|
|
3591
|
-
|
|
3592
|
-
|
|
3768
|
+
const changes = [];
|
|
3769
|
+
const merged = { ...userSettings };
|
|
3770
|
+
if (packTemplate.statusLine && !userSettings.statusLine) {
|
|
3771
|
+
const statusLineOk = await isStatusLineCommandResolvable(
|
|
3772
|
+
workspacePath,
|
|
3773
|
+
packTemplate.statusLine.command
|
|
3774
|
+
);
|
|
3775
|
+
if (statusLineOk) {
|
|
3776
|
+
merged.statusLine = packTemplate.statusLine;
|
|
3777
|
+
changes.push("statusLine added");
|
|
3778
|
+
} else {
|
|
3779
|
+
changes.push(
|
|
3780
|
+
`statusLine SKIPPED (file ref '${packTemplate.statusLine.command}' kh\xF4ng t\u1ED3n t\u1EA1i)`
|
|
3593
3781
|
);
|
|
3594
|
-
|
|
3595
|
-
|
|
3596
|
-
|
|
3597
|
-
|
|
3598
|
-
|
|
3599
|
-
|
|
3600
|
-
|
|
3601
|
-
|
|
3602
|
-
|
|
3603
|
-
|
|
3604
|
-
|
|
3605
|
-
|
|
3606
|
-
|
|
3607
|
-
|
|
3608
|
-
|
|
3609
|
-
|
|
3610
|
-
|
|
3782
|
+
}
|
|
3783
|
+
}
|
|
3784
|
+
if (typeof packTemplate.includeCoAuthoredBy === "boolean" && typeof userSettings.includeCoAuthoredBy !== "boolean") {
|
|
3785
|
+
merged.includeCoAuthoredBy = packTemplate.includeCoAuthoredBy;
|
|
3786
|
+
changes.push("includeCoAuthoredBy added");
|
|
3787
|
+
}
|
|
3788
|
+
if (packTemplate.model && !userSettings.model) {
|
|
3789
|
+
merged.model = packTemplate.model;
|
|
3790
|
+
changes.push("model added");
|
|
3791
|
+
}
|
|
3792
|
+
if (packTemplate.env) {
|
|
3793
|
+
const mergedEnv = { ...userSettings.env || {} };
|
|
3794
|
+
let envChanged = false;
|
|
3795
|
+
for (const [k, v] of Object.entries(packTemplate.env)) {
|
|
3796
|
+
if (!(k in mergedEnv)) {
|
|
3797
|
+
mergedEnv[k] = v;
|
|
3798
|
+
envChanged = true;
|
|
3611
3799
|
}
|
|
3612
|
-
break;
|
|
3613
3800
|
}
|
|
3614
|
-
|
|
3615
|
-
|
|
3616
|
-
|
|
3617
|
-
break;
|
|
3801
|
+
if (envChanged) {
|
|
3802
|
+
merged.env = mergedEnv;
|
|
3803
|
+
changes.push("env vars added from pack");
|
|
3618
3804
|
}
|
|
3619
|
-
|
|
3620
|
-
|
|
3621
|
-
|
|
3622
|
-
|
|
3623
|
-
|
|
3624
|
-
|
|
3625
|
-
|
|
3626
|
-
|
|
3627
|
-
|
|
3628
|
-
|
|
3629
|
-
|
|
3630
|
-
|
|
3631
|
-
|
|
3632
|
-
|
|
3633
|
-
|
|
3634
|
-
|
|
3635
|
-
|
|
3636
|
-
|
|
3637
|
-
|
|
3638
|
-
|
|
3639
|
-
|
|
3640
|
-
|
|
3641
|
-
|
|
3642
|
-
}
|
|
3643
|
-
break;
|
|
3805
|
+
}
|
|
3806
|
+
if (packTemplate.permissions) {
|
|
3807
|
+
const userAllow = userSettings.permissions?.allow || [];
|
|
3808
|
+
const userDeny = userSettings.permissions?.deny || [];
|
|
3809
|
+
const packAllow = packTemplate.permissions.allow || [];
|
|
3810
|
+
const packDeny = packTemplate.permissions.deny || [];
|
|
3811
|
+
const mergedAllow = unionDedupe(userAllow, packAllow);
|
|
3812
|
+
const mergedDeny = unionDedupe(userDeny, packDeny);
|
|
3813
|
+
if (mergedAllow.length !== userAllow.length || mergedDeny.length !== userDeny.length) {
|
|
3814
|
+
merged.permissions = { allow: mergedAllow, deny: mergedDeny };
|
|
3815
|
+
changes.push(
|
|
3816
|
+
`permissions union (+${mergedAllow.length - userAllow.length} allow, +${mergedDeny.length - userDeny.length} deny)`
|
|
3817
|
+
);
|
|
3818
|
+
}
|
|
3819
|
+
}
|
|
3820
|
+
if (packTemplate.hooks) {
|
|
3821
|
+
const userHooks = userSettings.hooks || {};
|
|
3822
|
+
const { merged: mergedHooks, touchedEvents } = mergeHooksPerEvent(
|
|
3823
|
+
packTemplate.hooks,
|
|
3824
|
+
userHooks
|
|
3825
|
+
);
|
|
3826
|
+
if (touchedEvents.length > 0) {
|
|
3827
|
+
merged.hooks = mergedHooks;
|
|
3828
|
+
changes.push(`hooks added for events: ${touchedEvents.join(", ")}`);
|
|
3644
3829
|
}
|
|
3645
3830
|
}
|
|
3646
|
-
|
|
3647
|
-
|
|
3648
|
-
const state = await detectFolderGitState(folderPath);
|
|
3649
|
-
log.info(`Folder state: ${state}`);
|
|
3650
|
-
if (state === "empty" || state === "clean") {
|
|
3651
|
-
await writeAvatarGitignore(folderPath);
|
|
3652
|
-
if (state === "empty") {
|
|
3653
|
-
await createInitialGitCommit(folderPath);
|
|
3654
|
-
}
|
|
3655
|
-
await appendAuditEntry("bootstrap", `state=${state},strategy=auto`);
|
|
3656
|
-
return;
|
|
3831
|
+
if (changes.length === 0) {
|
|
3832
|
+
return { action: "no-change", changes: [] };
|
|
3657
3833
|
}
|
|
3658
|
-
|
|
3659
|
-
|
|
3660
|
-
|
|
3834
|
+
let backupPath;
|
|
3835
|
+
if (projectHasSettings) {
|
|
3836
|
+
backupPath = backupFilename(projectSettingsPath);
|
|
3837
|
+
await fs10.copyFile(projectSettingsPath, backupPath);
|
|
3838
|
+
}
|
|
3839
|
+
await writeJsonAtomic(projectSettingsPath, merged);
|
|
3840
|
+
return { action: "merged", backupPath, changes };
|
|
3661
3841
|
}
|
|
3662
3842
|
|
|
3663
3843
|
// src/lib/symlink-farm-for-team-pack-mount-dirs.ts
|
|
3664
|
-
import { promises as
|
|
3665
|
-
import { dirname as
|
|
3844
|
+
import { promises as fs12 } from "fs";
|
|
3845
|
+
import { dirname as dirname6, join as join23, relative as relative2 } from "path";
|
|
3666
3846
|
|
|
3667
3847
|
// src/lib/backup-existing-dir-before-symlink-override.ts
|
|
3668
|
-
import { promises as
|
|
3848
|
+
import { promises as fs11 } from "fs";
|
|
3669
3849
|
function timestamp() {
|
|
3670
3850
|
const d = /* @__PURE__ */ new Date();
|
|
3671
3851
|
const pad = (n) => n.toString().padStart(2, "0");
|
|
@@ -3673,7 +3853,7 @@ function timestamp() {
|
|
|
3673
3853
|
}
|
|
3674
3854
|
async function backupDirBeforeReplace(targetPath) {
|
|
3675
3855
|
const backupPath = `${targetPath}.backup-${timestamp()}`;
|
|
3676
|
-
await
|
|
3856
|
+
await fs11.rename(targetPath, backupPath);
|
|
3677
3857
|
return backupPath;
|
|
3678
3858
|
}
|
|
3679
3859
|
|
|
@@ -3689,46 +3869,87 @@ var TEAM_PACK_MOUNT_DIRS = [
|
|
|
3689
3869
|
];
|
|
3690
3870
|
async function isSymbolicLink(path) {
|
|
3691
3871
|
try {
|
|
3692
|
-
const st = await
|
|
3872
|
+
const st = await fs12.lstat(path);
|
|
3693
3873
|
return st.isSymbolicLink();
|
|
3694
3874
|
} catch {
|
|
3695
3875
|
return false;
|
|
3696
3876
|
}
|
|
3697
3877
|
}
|
|
3698
3878
|
async function syncMountedDir(source, dest, force) {
|
|
3699
|
-
const dir = relative2(
|
|
3879
|
+
const dir = relative2(dirname6(dest), dest) || dest;
|
|
3700
3880
|
if (!await pathExists(source)) {
|
|
3701
3881
|
return { dir, action: "source-missing" };
|
|
3702
3882
|
}
|
|
3703
3883
|
if (await pathExists(dest)) {
|
|
3704
3884
|
if (await isSymbolicLink(dest)) {
|
|
3705
|
-
await
|
|
3885
|
+
await fs12.unlink(dest);
|
|
3706
3886
|
} else if (force) {
|
|
3707
3887
|
const backupPath = await backupDirBeforeReplace(dest);
|
|
3708
|
-
const relativeSource2 = relative2(
|
|
3709
|
-
await
|
|
3888
|
+
const relativeSource2 = relative2(dirname6(dest), source);
|
|
3889
|
+
await fs12.symlink(relativeSource2, dest);
|
|
3710
3890
|
return { dir, action: "backed-up-and-linked", backupPath };
|
|
3711
3891
|
} else {
|
|
3712
3892
|
return { dir, action: "skipped-conflict" };
|
|
3713
3893
|
}
|
|
3714
3894
|
}
|
|
3715
|
-
const relativeSource = relative2(
|
|
3716
|
-
await
|
|
3895
|
+
const relativeSource = relative2(dirname6(dest), source);
|
|
3896
|
+
await fs12.symlink(relativeSource, dest);
|
|
3717
3897
|
return { dir, action: "created" };
|
|
3718
3898
|
}
|
|
3719
3899
|
async function syncAllMountDirs(packDir, claudeDir, force) {
|
|
3720
3900
|
const results = [];
|
|
3721
3901
|
for (const dir of TEAM_PACK_MOUNT_DIRS) {
|
|
3722
|
-
const source =
|
|
3723
|
-
const dest =
|
|
3902
|
+
const source = join23(packDir, dir);
|
|
3903
|
+
const dest = join23(claudeDir, dir);
|
|
3724
3904
|
results.push(await syncMountedDir(source, dest, force));
|
|
3725
3905
|
}
|
|
3726
3906
|
return results;
|
|
3727
3907
|
}
|
|
3728
3908
|
|
|
3909
|
+
// src/commands/init-success-rendering-and-helpers.ts
|
|
3910
|
+
import { join as join25, relative as relative3 } from "path";
|
|
3911
|
+
import { input as input5, select as select10 } from "@inquirer/prompts";
|
|
3912
|
+
import boxen5 from "boxen";
|
|
3913
|
+
|
|
3914
|
+
// src/lib/format-pack-commands-cheatsheet-box.ts
|
|
3915
|
+
import boxen4 from "boxen";
|
|
3916
|
+
var PACK_COMMAND_CHEATSHEET = [
|
|
3917
|
+
{ cmd: "/avatar:help", desc: "H\u01B0\u1EDBng d\u1EABn s\u1EED d\u1EE5ng Avatar \u2014 ch\u1EC9 c\u1EA7n g\xF5 t\u1EF1 nhi\xEAn" },
|
|
3918
|
+
{ cmd: "/avatar:brainstorm", desc: "Brainstorm \xFD t\u01B0\u1EDFng cho m\u1ED9t feature" },
|
|
3919
|
+
{ cmd: "/avatar:plan", desc: "T\u1EA1o plan th\xF4ng minh v\u1EDBi prompt enhancement" },
|
|
3920
|
+
{ cmd: "/avatar:scout", desc: "T\xECm file li\xEAn quan trong codebase, ti\u1EBFt ki\u1EC7m token" },
|
|
3921
|
+
{ cmd: "/avatar:implement", desc: "B\u1EAFt \u0111\u1EA7u code & test theo plan c\xF3 s\u1EB5n" },
|
|
3922
|
+
{ cmd: "/avatar:build-full-flow", desc: "Implement m\u1ED9t feature t\u1EEBng b\u01B0\u1EDBc (end-to-end)" },
|
|
3923
|
+
{ cmd: "/avatar:fix", desc: "Ph\xE2n t\xEDch v\xE0 fix v\u1EA5n \u0111\u1EC1 t\u1EF1 \u0111i\u1EC1u h\u01B0\u1EDBng" },
|
|
3924
|
+
{ cmd: "/avatar:debug", desc: "Debug v\u1EA5n \u0111\u1EC1 k\u1EF9 thu\u1EADt + \u0111\u01B0a ra gi\u1EA3i ph\xE1p" },
|
|
3925
|
+
{ cmd: "/avatar:test", desc: "Ch\u1EA1y test tr\xEAn m\xE1y + ph\xE2n t\xEDch b\xE1o c\xE1o t\u1ED5ng h\u1EE3p" },
|
|
3926
|
+
{ cmd: "/avatar:design:good", desc: "T\u1EA1o thi\u1EBFt k\u1EBF ch\u1EC9n chu, s\u1ED1ng \u0111\u1ED9ng" },
|
|
3927
|
+
{ cmd: "/avatar:docs:init", desc: "Ph\xE2n t\xEDch codebase + t\u1EA1o t\xE0i li\u1EC7u kh\u1EDFi \u0111\u1EA7u" },
|
|
3928
|
+
{ cmd: "/avatar:status", desc: "Xem l\u1EA1i thay \u0111\u1ED5i g\u1EA7n \u0111\xE2y + t\u1ED5ng k\u1EBFt c\xF4ng vi\u1EC7c" },
|
|
3929
|
+
{ cmd: "/avatar:journal", desc: "Ghi nh\u1EADt k\xFD session" }
|
|
3930
|
+
];
|
|
3931
|
+
function formatPackCommandsCheatsheetBox() {
|
|
3932
|
+
const maxCmdWidth = Math.max(...PACK_COMMAND_CHEATSHEET.map((e) => e.cmd.length));
|
|
3933
|
+
const header = chalk.bold("\u{1F3AF} Slash commands t\u1EEB team-ai-pack");
|
|
3934
|
+
const subheader = chalk.dim("G\xF5 trong Claude Code session \u0111\u1EC3 g\u1ECDi capability c\u1EE7a pack:");
|
|
3935
|
+
const lines = PACK_COMMAND_CHEATSHEET.map((e) => {
|
|
3936
|
+
const cmdPadded = chalk.cyan(e.cmd.padEnd(maxCmdWidth));
|
|
3937
|
+
return ` ${cmdPadded} ${chalk.dim(e.desc)}`;
|
|
3938
|
+
});
|
|
3939
|
+
const footer = chalk.dim(
|
|
3940
|
+
"Catalog \u0111\u1EA7y \u0111\u1EE7 46 commands: cat .claude/pack/scripts/commands_data.yaml"
|
|
3941
|
+
);
|
|
3942
|
+
const content = [header, subheader, "", ...lines, "", footer].join("\n");
|
|
3943
|
+
return boxen4(content, {
|
|
3944
|
+
padding: 1,
|
|
3945
|
+
borderStyle: "round",
|
|
3946
|
+
borderColor: "cyan"
|
|
3947
|
+
});
|
|
3948
|
+
}
|
|
3949
|
+
|
|
3729
3950
|
// src/commands/init-conflict-detection-helpers.ts
|
|
3730
3951
|
import { readdir } from "fs/promises";
|
|
3731
|
-
import { join as
|
|
3952
|
+
import { join as join24 } from "path";
|
|
3732
3953
|
async function isEmptyOrMissing(path) {
|
|
3733
3954
|
if (!await pathExists(path)) return true;
|
|
3734
3955
|
try {
|
|
@@ -3741,389 +3962,365 @@ async function isEmptyOrMissing(path) {
|
|
|
3741
3962
|
}
|
|
3742
3963
|
async function findAlternativeWorkspaceName(parent, desiredName, maxAttempts = 10) {
|
|
3743
3964
|
for (let i = 2; i < maxAttempts; i++) {
|
|
3744
|
-
const candidate =
|
|
3965
|
+
const candidate = join24(parent, `${desiredName}-${i}`);
|
|
3745
3966
|
if (await isEmptyOrMissing(candidate)) return candidate;
|
|
3746
3967
|
}
|
|
3747
3968
|
return null;
|
|
3748
3969
|
}
|
|
3749
3970
|
|
|
3750
|
-
// src/
|
|
3751
|
-
|
|
3752
|
-
|
|
3753
|
-
|
|
3754
|
-
|
|
3755
|
-
|
|
3756
|
-
|
|
3757
|
-
|
|
3758
|
-
|
|
3759
|
-
|
|
3760
|
-
try {
|
|
3761
|
-
const raw = readFileSync5(candidate, "utf8");
|
|
3762
|
-
const pkg = JSON.parse(raw);
|
|
3763
|
-
if (pkg.name === "@nalvietnam/avatar-cli" && typeof pkg.version === "string") {
|
|
3764
|
-
cachedVersion = pkg.version;
|
|
3765
|
-
return cachedVersion;
|
|
3766
|
-
}
|
|
3767
|
-
} catch {
|
|
3971
|
+
// src/commands/init-success-rendering-and-helpers.ts
|
|
3972
|
+
async function resolveWorkspacePath(parent, desiredName, force) {
|
|
3973
|
+
const desired = join25(parent, desiredName);
|
|
3974
|
+
if (await isEmptyOrMissing(desired)) return desired;
|
|
3975
|
+
log.warn(`Workspace path "${desired}" \u0111\xE3 c\xF3 n\u1ED9i dung.`);
|
|
3976
|
+
while (true) {
|
|
3977
|
+
const alternative = await findAlternativeWorkspaceName(parent, desiredName);
|
|
3978
|
+
if (force && alternative) {
|
|
3979
|
+
log.info(`--force: d\xF9ng ${alternative}`);
|
|
3980
|
+
return alternative;
|
|
3768
3981
|
}
|
|
3769
|
-
|
|
3770
|
-
|
|
3771
|
-
|
|
3772
|
-
|
|
3773
|
-
|
|
3774
|
-
|
|
3775
|
-
|
|
3776
|
-
|
|
3777
|
-
|
|
3778
|
-
|
|
3779
|
-
|
|
3982
|
+
const choices = [];
|
|
3983
|
+
if (alternative) {
|
|
3984
|
+
choices.push({ name: `D\xF9ng "${alternative}" (suggest)`, value: "use-alt" });
|
|
3985
|
+
}
|
|
3986
|
+
choices.push({ name: "Nh\u1EADp t\xEAn workspace kh\xE1c (manual)", value: "manual" });
|
|
3987
|
+
choices.push({ name: "T\u1EA1m ng\u01B0ng init", value: "abort" });
|
|
3988
|
+
const action = await select10({
|
|
3989
|
+
message: "C\xE1ch x\u1EED l\xFD workspace path conflict?",
|
|
3990
|
+
choices
|
|
3991
|
+
});
|
|
3992
|
+
if (action === "abort") {
|
|
3993
|
+
throw new UserAbortedRecoveryError(
|
|
3994
|
+
"User abort t\u1EA1i b\u01B0\u1EDBc resolve workspace path. Ch\u1EA1y l\u1EA1i v\u1EDBi --workspace-name kh\xE1c."
|
|
3995
|
+
);
|
|
3996
|
+
}
|
|
3997
|
+
if (action === "use-alt" && alternative) {
|
|
3998
|
+
return alternative;
|
|
3780
3999
|
}
|
|
4000
|
+
const newName = await input5({
|
|
4001
|
+
message: "T\xEAn workspace m\u1EDBi:",
|
|
4002
|
+
validate: (v) => v.trim().length > 0 ? true : "T\xEAn kh\xF4ng \u0111\u01B0\u1EE3c r\u1ED7ng"
|
|
4003
|
+
});
|
|
4004
|
+
const newPath = join25(parent, newName.trim());
|
|
4005
|
+
if (await isEmptyOrMissing(newPath)) return newPath;
|
|
4006
|
+
log.warn(`"${newPath}" c\u0169ng \u0111\xE3 c\xF3 n\u1ED9i dung. Th\u1EED t\xEAn kh\xE1c.`);
|
|
3781
4007
|
}
|
|
3782
|
-
cachedVersion = "unknown";
|
|
3783
|
-
return cachedVersion;
|
|
3784
|
-
}
|
|
3785
|
-
|
|
3786
|
-
// src/commands/init-scaffold-variable-builders.ts
|
|
3787
|
-
function inferWorkspaceName(repoUrl) {
|
|
3788
|
-
const trimmed = repoUrl.trim().replace(/\/+$/, "");
|
|
3789
|
-
const lastSegment = trimmed.split(/[/:]/).pop() ?? "";
|
|
3790
|
-
const baseName = lastSegment.replace(/\.git$/, "");
|
|
3791
|
-
if (!baseName) return "avatar-client-workspace";
|
|
3792
|
-
const withoutPrefix = baseName.replace(/^avatar-/, "");
|
|
3793
|
-
return `avatar-${withoutPrefix}-workspace`;
|
|
3794
|
-
}
|
|
3795
|
-
function buildGitnexusSection(gitnexusReady) {
|
|
3796
|
-
if (!gitnexusReady) return "";
|
|
3797
|
-
return `
|
|
3798
|
-
### \u{1F9E0} CODEBASE INTELLIGENCE \u2014 GitNexus
|
|
3799
|
-
|
|
3800
|
-
Workspace c\xF3 GitNexus index t\u1EA1i \`.gitnexus/\` cung c\u1EA5p architectural awareness
|
|
3801
|
-
cho Claude Code (impact analysis, call chains, blast radius).
|
|
3802
|
-
|
|
3803
|
-
**H\u01B0\u1EDBng d\u1EABn cho Claude:**
|
|
3804
|
-
|
|
3805
|
-
- Khi c\u1EA7n hi\u1EC3u codebase architecture (vd: tr\u01B0\u1EDBc refactor, debug call chain) \u2192
|
|
3806
|
-
query GitNexus MCP resource \`gitnexus://repo/<name>/context\`
|
|
3807
|
-
- Khi resource tr\u1EA3 staleness hint "\u26A0\uFE0F Index is N commit(s) behind HEAD" \u2192
|
|
3808
|
-
T\u1EF0 ch\u1EA1y \`gitnexus analyze .\` (trong workspace root) tr\u01B0\u1EDBc khi ti\u1EBFp t\u1EE5c task
|
|
3809
|
-
- Sau khi \`gitnexus analyze\` xong: **KH\xD4NG t\u1EF1 ch\u1EA1y \`gitnexus wiki\`**
|
|
3810
|
-
(cost cao, c\xF3 th\u1EC3 block session 2-5 ph\xFAt) \u2014 user s\u1EBD ch\u1EA1y manual khi c\u1EA7n
|
|
3811
|
-
|
|
3812
|
-
**Folders Claude scan auto cho skills:**
|
|
3813
|
-
|
|
3814
|
-
- \`~/.claude/skills/gitnexus-*/\` \u2014 GitNexus global skills (exploring, debugging, ...)
|
|
3815
|
-
- \`.claude/pack/skills/\` \u2014 NAL team-shared skills (qua team-ai-pack submodule)
|
|
3816
|
-
- C\u1EA3 2 \u0111\u1EC1u \u0111\u01B0\u1EE3c scan, kh\xF4ng xung \u0111\u1ED9t (different naming prefix)
|
|
3817
|
-
|
|
3818
|
-
**Manual wiki update:**
|
|
3819
|
-
|
|
3820
|
-
Khi user c\u1EA7n regenerate wiki sau refactor l\u1EDBn \u2014 ch\u1EA1y:
|
|
3821
|
-
|
|
3822
|
-
\`\`\`bash
|
|
3823
|
-
gitnexus wiki . --api-key <key> --base-url <url>
|
|
3824
|
-
\`\`\`
|
|
3825
|
-
`;
|
|
3826
4008
|
}
|
|
3827
|
-
function
|
|
3828
|
-
return {
|
|
3829
|
-
projectName: args.projectName,
|
|
3830
|
-
projectDescription: args.projectDescription,
|
|
3831
|
-
teamOwner: args.teamOwner,
|
|
3832
|
-
avatarVersion: readCliVersion(),
|
|
3833
|
-
packVersion: args.packVersion,
|
|
3834
|
-
lastScan: (/* @__PURE__ */ new Date()).toISOString(),
|
|
3835
|
-
mode: args.mode,
|
|
3836
|
-
gitnexusSection: buildGitnexusSection(args.gitnexusReady ?? false)
|
|
3837
|
-
};
|
|
4009
|
+
async function promptTeamOwner(currentUserEmail) {
|
|
4010
|
+
return await input5({ message: "Team owner email:", default: currentUserEmail });
|
|
3838
4011
|
}
|
|
3839
|
-
|
|
3840
|
-
|
|
3841
|
-
|
|
3842
|
-
|
|
3843
|
-
|
|
3844
|
-
// src/lib/google-oauth-device-flow.ts
|
|
3845
|
-
var GOOGLE_CLIENT_ID = "1014766441755-i4jimivh5rd7vt8phuhmepmt45sovtph.apps.googleusercontent.com";
|
|
3846
|
-
var GOOGLE_CLIENT_SECRET = "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";
|
|
3847
|
-
var HOSTED_DOMAIN = "nal.vn";
|
|
3848
|
-
var SCOPES = ["openid", "email", "profile"];
|
|
3849
|
-
var DEVICE_CODE_URL = "https://oauth2.googleapis.com/device/code";
|
|
3850
|
-
var TOKEN_URL = "https://oauth2.googleapis.com/token";
|
|
3851
|
-
var REVOKE_URL = "https://oauth2.googleapis.com/revoke";
|
|
3852
|
-
async function requestDeviceCode() {
|
|
3853
|
-
const body = new URLSearchParams({
|
|
3854
|
-
client_id: GOOGLE_CLIENT_ID,
|
|
3855
|
-
scope: SCOPES.join(" ")
|
|
3856
|
-
});
|
|
3857
|
-
const res = await fetch(DEVICE_CODE_URL, {
|
|
3858
|
-
method: "POST",
|
|
3859
|
-
headers: { "Content-Type": "application/x-www-form-urlencoded" },
|
|
3860
|
-
body
|
|
3861
|
-
});
|
|
3862
|
-
if (!res.ok) {
|
|
3863
|
-
const text = await res.text();
|
|
3864
|
-
throw new Error(`Device code request failed (${res.status}): ${text}`);
|
|
4012
|
+
async function maybeCommitWorkspace(workspacePath, skipCommit) {
|
|
4013
|
+
if (skipCommit) {
|
|
4014
|
+
log.warn("Skip commit (--no-commit). Ch\u1EA1y 'git status' + commit th\u1EE7 c\xF4ng sau.");
|
|
4015
|
+
return;
|
|
3865
4016
|
}
|
|
3866
|
-
|
|
4017
|
+
const g = git(workspacePath);
|
|
4018
|
+
await g.add(["CLAUDE.md", ".claude/", ".gitignore", ".gitmodules", "notes/", "scripts/"]);
|
|
4019
|
+
await g.commit("chore: initialize Avatar workspace");
|
|
4020
|
+
log.success("\u0110\xE3 commit workspace");
|
|
3867
4021
|
}
|
|
3868
|
-
|
|
3869
|
-
|
|
3870
|
-
|
|
3871
|
-
client_secret: GOOGLE_CLIENT_SECRET,
|
|
3872
|
-
device_code: deviceCode,
|
|
3873
|
-
grant_type: "urn:ietf:params:oauth:grant-type:device_code"
|
|
3874
|
-
});
|
|
3875
|
-
const res = await fetch(TOKEN_URL, {
|
|
3876
|
-
method: "POST",
|
|
3877
|
-
headers: { "Content-Type": "application/x-www-form-urlencoded" },
|
|
3878
|
-
body
|
|
3879
|
-
});
|
|
3880
|
-
if (res.ok) {
|
|
3881
|
-
return await res.json();
|
|
3882
|
-
}
|
|
3883
|
-
let errorCode = "";
|
|
3884
|
-
try {
|
|
3885
|
-
const data = await res.json();
|
|
3886
|
-
errorCode = data.error ?? "";
|
|
3887
|
-
} catch {
|
|
3888
|
-
errorCode = "";
|
|
4022
|
+
function formatAiStatusLine(aiResult) {
|
|
4023
|
+
if (aiResult === null) {
|
|
4024
|
+
return ` ${chalk.yellow("AI:")} skipped \xB7 ${chalk.cyan("avatar ai setup")} \u0111\u1EC3 config sau`;
|
|
3889
4025
|
}
|
|
3890
|
-
if (
|
|
3891
|
-
|
|
4026
|
+
if (aiResult.ok) {
|
|
4027
|
+
const modelPart = aiResult.model ? ` \xB7 model=${aiResult.model}` : "";
|
|
4028
|
+
return ` ${chalk.green("AI:")} ready \xB7 ${aiResult.provider}${modelPart}`;
|
|
3892
4029
|
}
|
|
3893
|
-
|
|
3894
|
-
|
|
4030
|
+
return ` ${chalk.yellow("AI:")} failed (${aiResult.reason.slice(0, 60)}) \xB7 th\u1EED ${chalk.cyan("avatar ai setup")}`;
|
|
4031
|
+
}
|
|
4032
|
+
function formatGitnexusStatusLine(result) {
|
|
4033
|
+
if (result === null) {
|
|
4034
|
+
return ` ${chalk.yellow("GitNexus:")} skipped \xB7 ${chalk.cyan("avatar gitnexus install")} \u0111\u1EC3 setup sau`;
|
|
3895
4035
|
}
|
|
3896
|
-
if (
|
|
3897
|
-
|
|
4036
|
+
if (result.ok) {
|
|
4037
|
+
const parts = ["ready"];
|
|
4038
|
+
if (result.analyzed) parts.push("indexed");
|
|
4039
|
+
if (result.wikiGenerated) parts.push("wiki");
|
|
4040
|
+
if (result.mcpRegistered) parts.push("mcp");
|
|
4041
|
+
return ` ${chalk.green("GitNexus:")} ${parts.join(" \xB7 ")}`;
|
|
3898
4042
|
}
|
|
3899
|
-
|
|
4043
|
+
return ` ${chalk.yellow("GitNexus:")} skipped (${(result.reason ?? "unknown").slice(0, 40)}) \xB7 th\u1EED ${chalk.cyan("avatar gitnexus install")}`;
|
|
3900
4044
|
}
|
|
3901
|
-
function
|
|
3902
|
-
const
|
|
3903
|
-
|
|
3904
|
-
|
|
4045
|
+
async function printInitSuccessBox(rootPath, flow, aiResult = null, gitnexusResult = null) {
|
|
4046
|
+
const lines = [
|
|
4047
|
+
`${chalk.green("\u2713")} Workspace s\u1EB5n s\xE0ng: ${relative3(process.cwd(), rootPath) || rootPath}`,
|
|
4048
|
+
` ${chalk.dim(`(flow: ${flow})`)}`,
|
|
4049
|
+
formatAiStatusLine(aiResult),
|
|
4050
|
+
formatGitnexusStatusLine(gitnexusResult),
|
|
4051
|
+
"",
|
|
4052
|
+
` ${chalk.cyan(`cd ${rootPath}`)}`,
|
|
4053
|
+
` ${chalk.cyan("claude")} M\u1EDF Claude Code \u1EDF workspace root`,
|
|
4054
|
+
"",
|
|
4055
|
+
` ${chalk.cyan("avatar commit src")} Commit code l\xEAn client remote`,
|
|
4056
|
+
` ${chalk.cyan("avatar sync")} Pull team-ai-pack m\u1EDBi`,
|
|
4057
|
+
` ${chalk.cyan("avatar uninstall")} G\u1EE1 Avatar (gi\u1EEF code)`
|
|
4058
|
+
];
|
|
4059
|
+
process.stdout.write(`${boxen5(lines.join("\n"), { padding: 1, borderStyle: "round" })}
|
|
4060
|
+
`);
|
|
4061
|
+
const packDir = join25(rootPath, TEAM_PACK_RELATIVE_PATH);
|
|
4062
|
+
if (await pathExists(packDir)) {
|
|
4063
|
+
process.stdout.write(`
|
|
4064
|
+
${formatPackCommandsCheatsheetBox()}
|
|
4065
|
+
`);
|
|
3905
4066
|
}
|
|
3906
|
-
const payload = parts[1];
|
|
3907
|
-
if (!payload) throw new Error("id_token thi\u1EBFu payload");
|
|
3908
|
-
const base64 = payload.replace(/-/g, "+").replace(/_/g, "/");
|
|
3909
|
-
const json = Buffer.from(base64, "base64").toString("utf8");
|
|
3910
|
-
return JSON.parse(json);
|
|
3911
4067
|
}
|
|
3912
|
-
|
|
3913
|
-
|
|
3914
|
-
|
|
3915
|
-
|
|
3916
|
-
|
|
4068
|
+
|
|
4069
|
+
// src/commands/workspace-scaffold-and-finalize-orchestrator.ts
|
|
4070
|
+
async function getOrCreateOriginRemote(folderPath, opts) {
|
|
4071
|
+
const remotes = await git(folderPath).getRemotes(true);
|
|
4072
|
+
const origin = remotes.find((r) => r.name === "origin");
|
|
4073
|
+
if (origin?.refs.push) {
|
|
4074
|
+
log.success(`Folder \u0111\xE3 c\xF3 remote origin: ${origin.refs.push}`);
|
|
4075
|
+
return origin.refs.push;
|
|
3917
4076
|
}
|
|
3918
|
-
|
|
3919
|
-
|
|
4077
|
+
const shouldCreate = opts.createRemote ?? await confirm6({
|
|
4078
|
+
message: "Folder ch\u01B0a c\xF3 remote. T\u1EA1o GitHub repo ngay \u0111\u1EC3 share team?",
|
|
4079
|
+
default: true
|
|
4080
|
+
});
|
|
4081
|
+
if (!shouldCreate) {
|
|
4082
|
+
log.warn("Ti\u1EBFp t\u1EE5c v\u1EDBi local path. Workspace ch\u1EC9 ch\u1EA1y \u0111\u01B0\u1EE3c tr\xEAn m\xE1y b\u1EA1n.");
|
|
4083
|
+
return void 0;
|
|
3920
4084
|
}
|
|
3921
|
-
|
|
3922
|
-
|
|
3923
|
-
|
|
3924
|
-
|
|
3925
|
-
|
|
3926
|
-
|
|
3927
|
-
|
|
3928
|
-
refresh_token: token.refresh_token,
|
|
3929
|
-
expires_at: expiresAt,
|
|
3930
|
-
id_token: token.id_token
|
|
3931
|
-
};
|
|
3932
|
-
}
|
|
3933
|
-
async function revokeToken(token) {
|
|
3934
|
-
const body = new URLSearchParams({ token });
|
|
3935
|
-
await fetch(REVOKE_URL, {
|
|
3936
|
-
method: "POST",
|
|
3937
|
-
headers: { "Content-Type": "application/x-www-form-urlencoded" },
|
|
3938
|
-
body
|
|
3939
|
-
}).catch(() => {
|
|
4085
|
+
await ensureGitHubReady();
|
|
4086
|
+
const visibility = opts.repoVisibility ?? await select11({
|
|
4087
|
+
message: "Visibility?",
|
|
4088
|
+
choices: [
|
|
4089
|
+
{ name: "private (m\u1EB7c \u0111\u1ECBnh)", value: "private" },
|
|
4090
|
+
{ name: "public", value: "public" }
|
|
4091
|
+
]
|
|
3940
4092
|
});
|
|
3941
|
-
|
|
3942
|
-
|
|
3943
|
-
|
|
3944
|
-
|
|
3945
|
-
|
|
3946
|
-
|
|
3947
|
-
|
|
3948
|
-
|
|
3949
|
-
|
|
3950
|
-
function registerLoginCommand(program2) {
|
|
3951
|
-
program2.command("login").description("\u0110\u0103ng nh\u1EADp Google SSO (workspace @nal.vn)").option("--reset", "X\xF3a credential c\u0169 v\xE0 \u0111\u0103ng nh\u1EADp l\u1EA1i").action(async (opts) => {
|
|
3952
|
-
try {
|
|
3953
|
-
await runLogin(opts);
|
|
3954
|
-
} catch (err) {
|
|
3955
|
-
log.error(err instanceof Error ? err.message : String(err));
|
|
3956
|
-
process.exit(1);
|
|
3957
|
-
}
|
|
4093
|
+
const repoName = await input6({
|
|
4094
|
+
message: "T\xEAn repo:",
|
|
4095
|
+
default: basename2(folderPath)
|
|
4096
|
+
});
|
|
4097
|
+
const urls = createGithubRemoteFromFolder({
|
|
4098
|
+
folder: folderPath,
|
|
4099
|
+
name: repoName,
|
|
4100
|
+
visibility,
|
|
4101
|
+
org: opts.repoOrg
|
|
3958
4102
|
});
|
|
4103
|
+
return urls.sshUrl;
|
|
3959
4104
|
}
|
|
3960
|
-
async function
|
|
3961
|
-
|
|
3962
|
-
|
|
3963
|
-
|
|
3964
|
-
|
|
3965
|
-
|
|
3966
|
-
const existing = await readUserConfig();
|
|
3967
|
-
if (existing && !isTokenExpired(existing)) {
|
|
3968
|
-
log.success(`\u0110\xE3 \u0111\u0103ng nh\u1EADp: ${existing.email}`);
|
|
3969
|
-
return;
|
|
3970
|
-
}
|
|
3971
|
-
}
|
|
3972
|
-
const deviceSpinner = spinner("\u0110ang y\xEAu c\u1EA7u device code t\u1EEB Google...");
|
|
3973
|
-
let deviceCode;
|
|
4105
|
+
async function scaffoldWorkspaceWithSrcSubmodule(args) {
|
|
4106
|
+
await ensureDir(args.workspacePath);
|
|
4107
|
+
await git(args.workspacePath).init();
|
|
4108
|
+
const sp = spinner(
|
|
4109
|
+
args.skipTeamPack ? "Add submodule src/..." : "Add submodule src/ + team-ai-pack..."
|
|
4110
|
+
);
|
|
3974
4111
|
try {
|
|
3975
|
-
|
|
3976
|
-
|
|
4112
|
+
await git(args.workspacePath).subModule(["add", args.srcRemoteUrl, "src"]);
|
|
4113
|
+
let pinnedTag = "HEAD";
|
|
4114
|
+
if (!args.skipTeamPack) {
|
|
4115
|
+
sp.stop();
|
|
4116
|
+
const result = await addTeamPackSubmoduleWithRetryOnNetworkFail(
|
|
4117
|
+
args.workspacePath,
|
|
4118
|
+
args.packVersion,
|
|
4119
|
+
args.ssoEmail,
|
|
4120
|
+
args.packLatest === true && !args.packVersion
|
|
4121
|
+
// v1.10.0
|
|
4122
|
+
);
|
|
4123
|
+
pinnedTag = result.pinnedTag ?? "HEAD";
|
|
4124
|
+
sp.succeed(`Pin team-ai-pack v\xE0o ${pinnedTag}`);
|
|
4125
|
+
} else {
|
|
4126
|
+
sp.succeed("Skip team-ai-pack (--skip-team-pack)");
|
|
4127
|
+
}
|
|
4128
|
+
await finalizeWorkspaceScaffold({
|
|
4129
|
+
workspacePath: args.workspacePath,
|
|
4130
|
+
workspaceName: args.workspaceName,
|
|
4131
|
+
teamOwner: args.teamOwner,
|
|
4132
|
+
description: args.description,
|
|
4133
|
+
packVersion: pinnedTag,
|
|
4134
|
+
autoYes: args.autoYes,
|
|
4135
|
+
skipCommit: args.skipCommit,
|
|
4136
|
+
createWorkspaceRemote: args.createWorkspaceRemote,
|
|
4137
|
+
repoVisibility: args.repoVisibility,
|
|
4138
|
+
repoOrg: args.repoOrg,
|
|
4139
|
+
flow: args.flow,
|
|
4140
|
+
aiSkip: args.aiSkip,
|
|
4141
|
+
gitnexusSkip: args.gitnexusSkip
|
|
4142
|
+
});
|
|
3977
4143
|
} catch (err) {
|
|
3978
|
-
|
|
4144
|
+
sp.fail("Init workspace th\u1EA5t b\u1EA1i");
|
|
3979
4145
|
throw err;
|
|
3980
4146
|
}
|
|
3981
|
-
|
|
3982
|
-
|
|
3983
|
-
|
|
3984
|
-
|
|
3985
|
-
|
|
3986
|
-
|
|
3987
|
-
|
|
3988
|
-
|
|
3989
|
-
`);
|
|
3990
|
-
void open(verificationUrl).catch(() => {
|
|
3991
|
-
log.dim("(Kh\xF4ng m\u1EDF \u0111\u01B0\u1EE3c browser t\u1EF1 \u0111\u1ED9ng \u2014 copy URL \u1EDF tr\xEAn)");
|
|
4147
|
+
}
|
|
4148
|
+
async function finalizeWorkspaceScaffold(args) {
|
|
4149
|
+
const vars = buildScaffoldVariables({
|
|
4150
|
+
projectName: args.workspaceName,
|
|
4151
|
+
projectDescription: args.description,
|
|
4152
|
+
teamOwner: args.teamOwner,
|
|
4153
|
+
packVersion: args.packVersion,
|
|
4154
|
+
mode: "client"
|
|
3992
4155
|
});
|
|
3993
|
-
|
|
3994
|
-
|
|
3995
|
-
|
|
3996
|
-
|
|
3997
|
-
|
|
3998
|
-
|
|
3999
|
-
|
|
4000
|
-
|
|
4001
|
-
|
|
4002
|
-
|
|
4003
|
-
|
|
4004
|
-
|
|
4156
|
+
await createClaudeDirTree(args.workspacePath);
|
|
4157
|
+
await writeProjectKnowledgeFiles(args.workspacePath, vars);
|
|
4158
|
+
await writeRootClaudeMd(args.workspacePath, vars);
|
|
4159
|
+
await writeProjectSettings(args.workspacePath, vars);
|
|
4160
|
+
await appendGitignoreEntries(args.workspacePath);
|
|
4161
|
+
await ensureDir(join26(args.workspacePath, "notes"));
|
|
4162
|
+
await ensureDir(join26(args.workspacePath, "scripts"));
|
|
4163
|
+
await installGitHook(join26(args.workspacePath, ".git"), "post-merge");
|
|
4164
|
+
await installGitHook(join26(args.workspacePath, ".git", "modules", "src"), "pre-push");
|
|
4165
|
+
log.success("C\xE0i post-merge (workspace) + pre-push (src/)");
|
|
4166
|
+
await autoSyncPackOnInit(args.workspacePath);
|
|
4167
|
+
await appendAuditEntry("init", `flow=${args.flow},workspace=${args.workspaceName}`);
|
|
4168
|
+
await maybeCommitWorkspace(args.workspacePath, args.skipCommit);
|
|
4169
|
+
await maybeCreateWorkspaceRemote(args);
|
|
4170
|
+
let aiResult = null;
|
|
4171
|
+
if (args.aiSkip) {
|
|
4172
|
+
log.dim("B\u1ECF qua AI setup (--ai-skip). Setup sau qua: avatar ai setup");
|
|
4173
|
+
} else {
|
|
4174
|
+
aiResult = await runAiSetupPhase({ workspacePath: args.workspacePath });
|
|
4175
|
+
}
|
|
4176
|
+
let gitnexusResult = null;
|
|
4177
|
+
const skipGitnexus = args.aiSkip || args.gitnexusSkip;
|
|
4178
|
+
if (skipGitnexus) {
|
|
4179
|
+
if (args.gitnexusSkip) {
|
|
4180
|
+
log.dim("B\u1ECF qua GitNexus setup (--gitnexus-skip). Setup sau: avatar gitnexus install");
|
|
4181
|
+
} else {
|
|
4182
|
+
log.dim("B\u1ECF qua GitNexus setup (auto-skip do --ai-skip).");
|
|
4005
4183
|
}
|
|
4184
|
+
} else {
|
|
4185
|
+
gitnexusResult = await runGitnexusSetupPhase({ workspacePath: args.workspacePath });
|
|
4006
4186
|
}
|
|
4007
|
-
if (
|
|
4008
|
-
|
|
4009
|
-
|
|
4187
|
+
if (gitnexusResult?.ok) {
|
|
4188
|
+
const updatedVars = buildScaffoldVariables({
|
|
4189
|
+
projectName: args.workspaceName,
|
|
4190
|
+
projectDescription: args.description,
|
|
4191
|
+
teamOwner: args.teamOwner,
|
|
4192
|
+
packVersion: args.packVersion,
|
|
4193
|
+
mode: "client",
|
|
4194
|
+
gitnexusReady: true
|
|
4195
|
+
});
|
|
4196
|
+
await writeRootClaudeMd(args.workspacePath, updatedVars);
|
|
4197
|
+
log.dim("Updated CLAUDE.md v\u1EDBi GitNexus section");
|
|
4010
4198
|
}
|
|
4011
|
-
|
|
4012
|
-
|
|
4199
|
+
await printInitSuccessBox(args.workspacePath, args.flow, aiResult, gitnexusResult);
|
|
4200
|
+
}
|
|
4201
|
+
async function autoSyncPackOnInit(workspacePath) {
|
|
4202
|
+
const packDir = join26(workspacePath, TEAM_PACK_RELATIVE_PATH);
|
|
4203
|
+
if (!await pathExists(packDir)) {
|
|
4204
|
+
log.dim("Pack submodule kh\xF4ng t\u1ED3n t\u1EA1i (skip auto-sync). C\xF3 th\u1EC3 ch\u1EA1y `avatar sync` sau.");
|
|
4205
|
+
return;
|
|
4206
|
+
}
|
|
4207
|
+
const claudeDir = join26(workspacePath, ".claude");
|
|
4208
|
+
log.info("Auto-sync pack content v\xE0o .claude/ (symlinks + settings merge)...");
|
|
4013
4209
|
try {
|
|
4014
|
-
|
|
4210
|
+
const results = await syncAllMountDirs(packDir, claudeDir, false);
|
|
4211
|
+
const created = results.filter((r) => r.action === "created" || r.action === "updated").length;
|
|
4212
|
+
const missing = results.filter((r) => r.action === "source-missing").length;
|
|
4213
|
+
log.success(
|
|
4214
|
+
` \u2713 Symlinks: ${created} created${missing > 0 ? `, ${missing} source-missing (pack thi\u1EBFu dir)` : ""}`
|
|
4215
|
+
);
|
|
4216
|
+
const mergeResult = await mergePackSettingsIntoProjectSettings(workspacePath);
|
|
4217
|
+
switch (mergeResult.action) {
|
|
4218
|
+
case "merged":
|
|
4219
|
+
log.success(` \u2713 settings.json merged (${mergeResult.changes.join("; ")})`);
|
|
4220
|
+
break;
|
|
4221
|
+
case "no-change":
|
|
4222
|
+
log.dim(" - settings.json \u0111\xE3 sync, kh\xF4ng c\u1EA7n thay \u0111\u1ED5i.");
|
|
4223
|
+
break;
|
|
4224
|
+
case "no-pack-template":
|
|
4225
|
+
log.dim(" - Pack kh\xF4ng c\xF3 templates/settings.json.tpl, skip merge.");
|
|
4226
|
+
break;
|
|
4227
|
+
}
|
|
4015
4228
|
} catch (err) {
|
|
4016
|
-
|
|
4017
|
-
|
|
4229
|
+
log.warn(
|
|
4230
|
+
`Auto-sync pack fail: ${err instanceof Error ? err.message : err}. Ch\u1EA1y \`avatar sync\` th\u1EE7 c\xF4ng \u0111\u1EC3 retry.`
|
|
4231
|
+
);
|
|
4018
4232
|
}
|
|
4019
|
-
const userConfig = buildUserConfig(token, claims);
|
|
4020
|
-
await writeUserConfig(userConfig);
|
|
4021
|
-
await appendAuditEntry("login", userConfig.email);
|
|
4022
|
-
log.success(`X\xE1c th\u1EF1c th\xE0nh c\xF4ng: ${userConfig.email}`);
|
|
4023
|
-
log.success(`Verify hosted domain: ${claims.hd} \u2713`);
|
|
4024
|
-
log.success(`L\u01B0u credential v\xE0o ${USER_CONFIG_PATH} (chmod 600)`);
|
|
4025
|
-
}
|
|
4026
|
-
function sleep(ms) {
|
|
4027
|
-
return new Promise((resolve3) => setTimeout(resolve3, ms));
|
|
4028
4233
|
}
|
|
4029
|
-
|
|
4030
|
-
|
|
4031
|
-
|
|
4032
|
-
|
|
4033
|
-
if (!opts.bootstrapStrategy) return void 0;
|
|
4034
|
-
const valid = ["stash", "commit-all", "skip", "branch"];
|
|
4035
|
-
if (valid.includes(opts.bootstrapStrategy)) {
|
|
4036
|
-
return opts.bootstrapStrategy;
|
|
4234
|
+
async function maybeCreateWorkspaceRemote(args) {
|
|
4235
|
+
if (args.skipCommit) {
|
|
4236
|
+
log.dim("Skip workspace remote (ch\u01B0a commit). Setup sau qua: gh repo create ...");
|
|
4237
|
+
return;
|
|
4037
4238
|
}
|
|
4038
|
-
|
|
4039
|
-
|
|
4040
|
-
|
|
4041
|
-
|
|
4042
|
-
|
|
4043
|
-
|
|
4044
|
-
|
|
4045
|
-
|
|
4046
|
-
)
|
|
4047
|
-
|
|
4048
|
-
|
|
4049
|
-
|
|
4239
|
+
let shouldCreate = args.createWorkspaceRemote;
|
|
4240
|
+
if (shouldCreate === void 0) {
|
|
4241
|
+
if (args.autoYes) return;
|
|
4242
|
+
shouldCreate = await confirm6({
|
|
4243
|
+
message: "T\u1EA1o remote GitHub cho workspace \u0111\u1EC3 share team? (Avatar state)",
|
|
4244
|
+
default: false
|
|
4245
|
+
});
|
|
4246
|
+
}
|
|
4247
|
+
if (!shouldCreate) return;
|
|
4248
|
+
const visibility = args.repoVisibility ?? (args.autoYes ? "private" : await select11({
|
|
4249
|
+
message: "Workspace visibility?",
|
|
4250
|
+
choices: [
|
|
4251
|
+
{ name: "private (m\u1EB7c \u0111\u1ECBnh, an to\xE0n)", value: "private" },
|
|
4252
|
+
{ name: "public", value: "public" }
|
|
4253
|
+
]
|
|
4254
|
+
}));
|
|
4255
|
+
while (true) {
|
|
4050
4256
|
try {
|
|
4051
|
-
await
|
|
4257
|
+
await createWorkspaceRemoteViaGh({
|
|
4258
|
+
workspacePath: args.workspacePath,
|
|
4259
|
+
workspaceName: args.workspaceName,
|
|
4260
|
+
visibility,
|
|
4261
|
+
org: args.repoOrg
|
|
4262
|
+
});
|
|
4263
|
+
return;
|
|
4052
4264
|
} catch (err) {
|
|
4053
|
-
if (err instanceof
|
|
4054
|
-
|
|
4055
|
-
|
|
4056
|
-
|
|
4057
|
-
|
|
4058
|
-
|
|
4059
|
-
|
|
4265
|
+
if (err instanceof CreateWorkspaceRemoteError && err.reason === "repo-exists") {
|
|
4266
|
+
const fullName = err.fullName;
|
|
4267
|
+
const reuseAction = await select11({
|
|
4268
|
+
message: `Repo '${fullName}' \u0111\xE3 t\u1ED3n t\u1EA1i tr\xEAn GitHub. C\xE1ch x\u1EED l\xFD?`,
|
|
4269
|
+
choices: [
|
|
4270
|
+
{
|
|
4271
|
+
name: "D\xF9ng remote \u0111\xE3 c\xF3 (link workspace local v\xE0o repo n\xE0y)",
|
|
4272
|
+
value: "reuse"
|
|
4273
|
+
},
|
|
4274
|
+
{
|
|
4275
|
+
name: "Nh\u1EADp t\xEAn workspace kh\xE1c (t\u1EA1o repo m\u1EDBi)",
|
|
4276
|
+
value: "rename"
|
|
4277
|
+
},
|
|
4278
|
+
{ name: "B\u1ECF qua (workspace local-only)", value: "skip" },
|
|
4279
|
+
{ name: "T\u1EA1m ng\u01B0ng init", value: "abort" }
|
|
4280
|
+
]
|
|
4281
|
+
});
|
|
4282
|
+
if (reuseAction === "abort") {
|
|
4283
|
+
throw new UserAbortedRecoveryError("User abort t\u1EA1i b\u01B0\u1EDBc t\u1EA1o workspace remote.");
|
|
4284
|
+
}
|
|
4285
|
+
if (reuseAction === "skip") {
|
|
4286
|
+
log.warn("Workspace v\u1EABn s\u1EB5n s\xE0ng local-only. Setup remote sau khi c\u1EA7n.");
|
|
4287
|
+
return;
|
|
4288
|
+
}
|
|
4289
|
+
if (reuseAction === "reuse") {
|
|
4290
|
+
linkExistingRemoteToWorkspace({
|
|
4291
|
+
workspacePath: args.workspacePath,
|
|
4292
|
+
fullName
|
|
4293
|
+
});
|
|
4294
|
+
return;
|
|
4295
|
+
}
|
|
4296
|
+
const newName = await input6({
|
|
4297
|
+
message: "T\xEAn workspace m\u1EDBi (s\u1EBD t\u1EA1o repo new):",
|
|
4298
|
+
validate: (v) => v.trim().length > 0 ? true : "T\xEAn kh\xF4ng \u0111\u01B0\u1EE3c r\u1ED7ng"
|
|
4299
|
+
});
|
|
4300
|
+
args.workspaceName = newName.trim();
|
|
4301
|
+
continue;
|
|
4060
4302
|
}
|
|
4061
|
-
|
|
4062
|
-
|
|
4063
|
-
|
|
4303
|
+
const action = await promptRetryOrSkip({
|
|
4304
|
+
taskName: "T\u1EA1o workspace remote tr\xEAn GitHub",
|
|
4305
|
+
reason: err instanceof Error ? err.message : String(err),
|
|
4306
|
+
allowSkip: true,
|
|
4307
|
+
// Workspace remote OPTIONAL — skip OK, workspace local vẫn dùng được.
|
|
4308
|
+
hint: "Tip: sai org? Pass --repo-org=<your-gh-user>. Ho\u1EB7c switch gh: gh auth login."
|
|
4309
|
+
});
|
|
4310
|
+
if (action === "abort") {
|
|
4311
|
+
throw new UserAbortedRecoveryError("User abort t\u1EA1i b\u01B0\u1EDBc t\u1EA1o workspace remote.");
|
|
4064
4312
|
}
|
|
4065
|
-
if (
|
|
4066
|
-
log.
|
|
4067
|
-
|
|
4313
|
+
if (action === "skip") {
|
|
4314
|
+
log.warn("Workspace v\u1EABn s\u1EB5n s\xE0ng local-only. Setup remote sau khi c\u1EA7n.");
|
|
4315
|
+
return;
|
|
4068
4316
|
}
|
|
4069
|
-
log.error(err instanceof Error ? err.message : String(err));
|
|
4070
|
-
process.exit(1);
|
|
4071
|
-
}
|
|
4072
|
-
});
|
|
4073
|
-
}
|
|
4074
|
-
async function runInit(opts) {
|
|
4075
|
-
if (!opts.yes) printAvatarBanner({ tagline: "Kh\u1EDFi t\u1EA1o Avatar trong d\u1EF1 \xE1n c\u1EE7a b\u1EA1n" });
|
|
4076
|
-
if (opts.mode) {
|
|
4077
|
-
log.warn("Flag --mode \u0111\xE3 deprecated t\u1EEB v1.1. D\xF9ng --project-status thay th\u1EBF.");
|
|
4078
|
-
}
|
|
4079
|
-
let userConfig = await readUserConfig();
|
|
4080
|
-
while (!userConfig || isTokenExpired(userConfig)) {
|
|
4081
|
-
log.info("Ch\u01B0a \u0111\u0103ng nh\u1EADp \u2014 ch\u1EA1y login flow tr\u01B0\u1EDBc khi init...");
|
|
4082
|
-
try {
|
|
4083
|
-
await runLogin({});
|
|
4084
|
-
} catch (err) {
|
|
4085
|
-
log.warn(`Login fail: ${err.message}`);
|
|
4086
|
-
}
|
|
4087
|
-
userConfig = await readUserConfig();
|
|
4088
|
-
if (userConfig && !isTokenExpired(userConfig)) break;
|
|
4089
|
-
const action = await promptRetryOrSkip({
|
|
4090
|
-
taskName: "\u0110\u0103ng nh\u1EADp SSO Google",
|
|
4091
|
-
reason: "Token ch\u01B0a \u0111\u01B0\u1EE3c t\u1EA1o ho\u1EB7c \u0111\xE3 h\u1EBFt h\u1EA1n.",
|
|
4092
|
-
allowSkip: false,
|
|
4093
|
-
// Login bắt buộc, không skip được.
|
|
4094
|
-
hint: "\u0110\u1EA3m b\u1EA3o b\u1EA1n ch\u1ECDn 'Allow' tr\xEAn browser v\xE0 d\xF9ng email @nal.vn."
|
|
4095
|
-
});
|
|
4096
|
-
if (action === "abort") {
|
|
4097
|
-
throw new UserAbortedRecoveryError(
|
|
4098
|
-
"User abort t\u1EA1i b\u01B0\u1EDBc login. Ch\u1EA1y 'avatar login' tay r\u1ED3i 'avatar init' l\u1EA1i."
|
|
4099
|
-
);
|
|
4100
4317
|
}
|
|
4101
4318
|
}
|
|
4102
|
-
const status = opts.projectStatus ?? await promptProjectStatus();
|
|
4103
|
-
switch (status) {
|
|
4104
|
-
case "existing-remote":
|
|
4105
|
-
await runInitFromExistingRemote(opts, userConfig.email);
|
|
4106
|
-
break;
|
|
4107
|
-
case "existing-folder":
|
|
4108
|
-
await runInitFromExistingFolder(opts, userConfig.email);
|
|
4109
|
-
break;
|
|
4110
|
-
case "new-project":
|
|
4111
|
-
await runInitFromScratch(opts, userConfig.email);
|
|
4112
|
-
break;
|
|
4113
|
-
}
|
|
4114
|
-
}
|
|
4115
|
-
async function promptProjectStatus() {
|
|
4116
|
-
return await select9({
|
|
4117
|
-
message: "T\xECnh tr\u1EA1ng d\u1EF1 \xE1n c\u1EE7a b\u1EA1n?",
|
|
4118
|
-
choices: [
|
|
4119
|
-
{ name: "1. \u0110\xE3 c\xF3 repo git remote (URL c\xF3 s\u1EB5n)", value: "existing-remote" },
|
|
4120
|
-
{ name: "2. \u0110\xE3 c\xF3 folder code local", value: "existing-folder" },
|
|
4121
|
-
{ name: "3. D\u1EF1 \xE1n m\u1EDBi ho\xE0n to\xE0n", value: "new-project" }
|
|
4122
|
-
]
|
|
4123
|
-
});
|
|
4124
4319
|
}
|
|
4320
|
+
|
|
4321
|
+
// src/commands/init-flow-handlers-for-each-project-status.ts
|
|
4125
4322
|
async function runInitFromExistingRemote(opts, ownerEmail) {
|
|
4126
|
-
const initialRemoteUrl = opts.clientRepo ?? await
|
|
4323
|
+
const initialRemoteUrl = opts.clientRepo ?? await input7({
|
|
4127
4324
|
message: "URL git c\u1EE7a repo:",
|
|
4128
4325
|
validate: (v) => v.length > 0 ? true : "URL b\u1EAFt bu\u1ED9c"
|
|
4129
4326
|
});
|
|
@@ -4131,7 +4328,7 @@ async function runInitFromExistingRemote(opts, ownerEmail) {
|
|
|
4131
4328
|
const remoteUrl = resolvedRemoteUrl ?? initialRemoteUrl;
|
|
4132
4329
|
const teamOwner = opts.teamOwner ?? await promptTeamOwner(ownerEmail);
|
|
4133
4330
|
const inferredName = inferWorkspaceName(remoteUrl);
|
|
4134
|
-
const workspaceName = opts.workspaceName ?? await
|
|
4331
|
+
const workspaceName = opts.workspaceName ?? await input7({ message: "T\xEAn workspace:", default: inferredName });
|
|
4135
4332
|
const workspaceParent = resolve2(opts.workspaceParent ?? ".");
|
|
4136
4333
|
const workspacePath = await resolveWorkspacePath(workspaceParent, workspaceName, opts.force);
|
|
4137
4334
|
await scaffoldWorkspaceWithSrcSubmodule({
|
|
@@ -4156,7 +4353,7 @@ async function runInitFromExistingRemote(opts, ownerEmail) {
|
|
|
4156
4353
|
}
|
|
4157
4354
|
async function runInitFromExistingFolder(opts, ownerEmail) {
|
|
4158
4355
|
const folderPath = resolve2(
|
|
4159
|
-
opts.folderPath ?? await
|
|
4356
|
+
opts.folderPath ?? await input7({
|
|
4160
4357
|
message: "\u0110\u01B0\u1EDDng d\u1EABn folder hi\u1EC7n c\xF3:",
|
|
4161
4358
|
validate: (v) => v.length > 0 ? true : "Path b\u1EAFt bu\u1ED9c"
|
|
4162
4359
|
})
|
|
@@ -4165,10 +4362,25 @@ async function runInitFromExistingFolder(opts, ownerEmail) {
|
|
|
4165
4362
|
presetStrategy: parseBootstrapStrategyOpts(opts),
|
|
4166
4363
|
autoYes: opts.yes
|
|
4167
4364
|
});
|
|
4168
|
-
|
|
4365
|
+
let remoteUrl = await getOrCreateOriginRemote(folderPath, opts);
|
|
4366
|
+
if (remoteUrl) {
|
|
4367
|
+
const verify = tryVerifyGitRemoteAccessible(remoteUrl);
|
|
4368
|
+
if (!verify.ok) {
|
|
4369
|
+
log.warn(`Remote ${remoteUrl} kh\xF4ng accessible (${verify.reason ?? "unknown"}).`);
|
|
4370
|
+
const recovered = await handleRemoteAccessFailureWithAccountSwitch({
|
|
4371
|
+
url: remoteUrl,
|
|
4372
|
+
initialReason: verify.reason ?? "unknown",
|
|
4373
|
+
initialDetail: verify.detail,
|
|
4374
|
+
folderPath,
|
|
4375
|
+
// enable option "Reset folder & tạo repo mới"
|
|
4376
|
+
defaultVisibility: opts.repoVisibility
|
|
4377
|
+
});
|
|
4378
|
+
remoteUrl = recovered.resolvedUrl;
|
|
4379
|
+
}
|
|
4380
|
+
}
|
|
4169
4381
|
const teamOwner = opts.teamOwner ?? await promptTeamOwner(ownerEmail);
|
|
4170
|
-
const inferredName = opts.workspaceName ?? `${
|
|
4171
|
-
const workspaceName = opts.workspaceName ?? await
|
|
4382
|
+
const inferredName = opts.workspaceName ?? `${basename3(folderPath)}-avatar-workspace`;
|
|
4383
|
+
const workspaceName = opts.workspaceName ?? await input7({ message: "T\xEAn workspace:", default: inferredName });
|
|
4172
4384
|
const workspaceParent = resolve2(opts.workspaceParent ?? ".");
|
|
4173
4385
|
const workspacePath = await resolveWorkspacePath(workspaceParent, workspaceName, opts.force);
|
|
4174
4386
|
await scaffoldWorkspaceWithSrcSubmodule({
|
|
@@ -4194,11 +4406,11 @@ async function runInitFromExistingFolder(opts, ownerEmail) {
|
|
|
4194
4406
|
}
|
|
4195
4407
|
async function runInitFromScratch(opts, ownerEmail) {
|
|
4196
4408
|
await ensureGitHubReady();
|
|
4197
|
-
const projectName = opts.workspaceName ?? await
|
|
4409
|
+
const projectName = opts.workspaceName ?? await input7({
|
|
4198
4410
|
message: "T\xEAn d\u1EF1 \xE1n:",
|
|
4199
4411
|
validate: (v) => v.length > 0 ? true : "T\xEAn b\u1EAFt bu\u1ED9c"
|
|
4200
4412
|
});
|
|
4201
|
-
const visibility = opts.repoVisibility ?? await
|
|
4413
|
+
const visibility = opts.repoVisibility ?? await select12({
|
|
4202
4414
|
message: "Visibility?",
|
|
4203
4415
|
choices: [
|
|
4204
4416
|
{ name: "private (m\u1EB7c \u0111\u1ECBnh)", value: "private" },
|
|
@@ -4208,7 +4420,7 @@ async function runInitFromScratch(opts, ownerEmail) {
|
|
|
4208
4420
|
const teamOwner = opts.teamOwner ?? await promptTeamOwner(ownerEmail);
|
|
4209
4421
|
const workspaceParent = resolve2(opts.workspaceParent ?? ".");
|
|
4210
4422
|
const workspacePath = await resolveWorkspacePath(workspaceParent, projectName, opts.force);
|
|
4211
|
-
const srcPath =
|
|
4423
|
+
const srcPath = join27(workspacePath, "src");
|
|
4212
4424
|
await ensureDir(workspacePath);
|
|
4213
4425
|
await ensureDir(srcPath);
|
|
4214
4426
|
await safeBootstrapGitInFolder(srcPath, { autoYes: true });
|
|
@@ -4232,85 +4444,7 @@ async function runInitFromScratch(opts, ownerEmail) {
|
|
|
4232
4444
|
opts.packVersion,
|
|
4233
4445
|
ownerEmail,
|
|
4234
4446
|
opts.latest === true && !opts.packVersion
|
|
4235
|
-
// v1.10.0: latest mode khi flag set + không có explicit tag
|
|
4236
|
-
);
|
|
4237
|
-
pinnedTag = result.pinnedTag ?? "HEAD";
|
|
4238
|
-
sp.succeed(`Pin team-ai-pack v\xE0o ${pinnedTag}`);
|
|
4239
|
-
} else {
|
|
4240
|
-
sp.succeed("Skip team-ai-pack (--skip-team-pack)");
|
|
4241
|
-
}
|
|
4242
|
-
await finalizeWorkspaceScaffold({
|
|
4243
|
-
workspacePath,
|
|
4244
|
-
workspaceName: projectName,
|
|
4245
|
-
teamOwner,
|
|
4246
|
-
description: opts.description ?? `D\u1EF1 \xE1n m\u1EDBi: ${projectName}`,
|
|
4247
|
-
packVersion: pinnedTag,
|
|
4248
|
-
autoYes: opts.yes,
|
|
4249
|
-
skipCommit: opts.commit === false,
|
|
4250
|
-
createWorkspaceRemote: opts.workspaceRemote,
|
|
4251
|
-
repoVisibility: opts.repoVisibility,
|
|
4252
|
-
repoOrg: opts.repoOrg,
|
|
4253
|
-
flow: "new-project",
|
|
4254
|
-
aiSkip: opts.aiSkip,
|
|
4255
|
-
gitnexusSkip: opts.gitnexusSkip
|
|
4256
|
-
});
|
|
4257
|
-
} catch (err) {
|
|
4258
|
-
sp.fail("Init workspace th\u1EA5t b\u1EA1i");
|
|
4259
|
-
throw err;
|
|
4260
|
-
}
|
|
4261
|
-
}
|
|
4262
|
-
async function getOrCreateOriginRemote(folderPath, opts) {
|
|
4263
|
-
const remotes = await git(folderPath).getRemotes(true);
|
|
4264
|
-
const origin = remotes.find((r) => r.name === "origin");
|
|
4265
|
-
if (origin?.refs.push) {
|
|
4266
|
-
log.success(`Folder \u0111\xE3 c\xF3 remote origin: ${origin.refs.push}`);
|
|
4267
|
-
return origin.refs.push;
|
|
4268
|
-
}
|
|
4269
|
-
const shouldCreate = opts.createRemote ?? await confirm5({
|
|
4270
|
-
message: "Folder ch\u01B0a c\xF3 remote. T\u1EA1o GitHub repo ngay \u0111\u1EC3 share team?",
|
|
4271
|
-
default: true
|
|
4272
|
-
});
|
|
4273
|
-
if (!shouldCreate) {
|
|
4274
|
-
log.warn("Ti\u1EBFp t\u1EE5c v\u1EDBi local path. Workspace ch\u1EC9 ch\u1EA1y \u0111\u01B0\u1EE3c tr\xEAn m\xE1y b\u1EA1n.");
|
|
4275
|
-
return void 0;
|
|
4276
|
-
}
|
|
4277
|
-
await ensureGitHubReady();
|
|
4278
|
-
const visibility = opts.repoVisibility ?? await select9({
|
|
4279
|
-
message: "Visibility?",
|
|
4280
|
-
choices: [
|
|
4281
|
-
{ name: "private (m\u1EB7c \u0111\u1ECBnh)", value: "private" },
|
|
4282
|
-
{ name: "public", value: "public" }
|
|
4283
|
-
]
|
|
4284
|
-
});
|
|
4285
|
-
const repoName = await input5({
|
|
4286
|
-
message: "T\xEAn repo:",
|
|
4287
|
-
default: basename(folderPath)
|
|
4288
|
-
});
|
|
4289
|
-
const urls = createGithubRemoteFromFolder({
|
|
4290
|
-
folder: folderPath,
|
|
4291
|
-
name: repoName,
|
|
4292
|
-
visibility,
|
|
4293
|
-
org: opts.repoOrg
|
|
4294
|
-
});
|
|
4295
|
-
return urls.sshUrl;
|
|
4296
|
-
}
|
|
4297
|
-
async function scaffoldWorkspaceWithSrcSubmodule(args) {
|
|
4298
|
-
await ensureDir(args.workspacePath);
|
|
4299
|
-
await git(args.workspacePath).init();
|
|
4300
|
-
const sp = spinner(
|
|
4301
|
-
args.skipTeamPack ? "Add submodule src/..." : "Add submodule src/ + team-ai-pack..."
|
|
4302
|
-
);
|
|
4303
|
-
try {
|
|
4304
|
-
await git(args.workspacePath).subModule(["add", args.srcRemoteUrl, "src"]);
|
|
4305
|
-
let pinnedTag = "HEAD";
|
|
4306
|
-
if (!args.skipTeamPack) {
|
|
4307
|
-
sp.stop();
|
|
4308
|
-
const result = await addTeamPackSubmoduleWithRetryOnNetworkFail(
|
|
4309
|
-
args.workspacePath,
|
|
4310
|
-
args.packVersion,
|
|
4311
|
-
args.ssoEmail,
|
|
4312
|
-
args.packLatest === true && !args.packVersion
|
|
4313
|
-
// v1.10.0
|
|
4447
|
+
// v1.10.0: latest mode khi flag set + không có explicit tag
|
|
4314
4448
|
);
|
|
4315
4449
|
pinnedTag = result.pinnedTag ?? "HEAD";
|
|
4316
4450
|
sp.succeed(`Pin team-ai-pack v\xE0o ${pinnedTag}`);
|
|
@@ -4318,292 +4452,299 @@ async function scaffoldWorkspaceWithSrcSubmodule(args) {
|
|
|
4318
4452
|
sp.succeed("Skip team-ai-pack (--skip-team-pack)");
|
|
4319
4453
|
}
|
|
4320
4454
|
await finalizeWorkspaceScaffold({
|
|
4321
|
-
workspacePath
|
|
4322
|
-
workspaceName:
|
|
4323
|
-
teamOwner
|
|
4324
|
-
description:
|
|
4455
|
+
workspacePath,
|
|
4456
|
+
workspaceName: projectName,
|
|
4457
|
+
teamOwner,
|
|
4458
|
+
description: opts.description ?? `D\u1EF1 \xE1n m\u1EDBi: ${projectName}`,
|
|
4325
4459
|
packVersion: pinnedTag,
|
|
4326
|
-
autoYes:
|
|
4327
|
-
skipCommit:
|
|
4328
|
-
createWorkspaceRemote:
|
|
4329
|
-
repoVisibility:
|
|
4330
|
-
repoOrg:
|
|
4331
|
-
flow:
|
|
4332
|
-
aiSkip:
|
|
4333
|
-
gitnexusSkip:
|
|
4460
|
+
autoYes: opts.yes,
|
|
4461
|
+
skipCommit: opts.commit === false,
|
|
4462
|
+
createWorkspaceRemote: opts.workspaceRemote,
|
|
4463
|
+
repoVisibility: opts.repoVisibility,
|
|
4464
|
+
repoOrg: opts.repoOrg,
|
|
4465
|
+
flow: "new-project",
|
|
4466
|
+
aiSkip: opts.aiSkip,
|
|
4467
|
+
gitnexusSkip: opts.gitnexusSkip
|
|
4334
4468
|
});
|
|
4335
4469
|
} catch (err) {
|
|
4336
4470
|
sp.fail("Init workspace th\u1EA5t b\u1EA1i");
|
|
4337
4471
|
throw err;
|
|
4338
4472
|
}
|
|
4339
4473
|
}
|
|
4340
|
-
|
|
4341
|
-
|
|
4342
|
-
|
|
4343
|
-
|
|
4344
|
-
|
|
4345
|
-
|
|
4346
|
-
|
|
4474
|
+
|
|
4475
|
+
// src/commands/login.ts
|
|
4476
|
+
import boxen6 from "boxen";
|
|
4477
|
+
import open from "open";
|
|
4478
|
+
|
|
4479
|
+
// src/lib/google-oauth-device-flow.ts
|
|
4480
|
+
var GOOGLE_CLIENT_ID = "1014766441755-i4jimivh5rd7vt8phuhmepmt45sovtph.apps.googleusercontent.com";
|
|
4481
|
+
var GOOGLE_CLIENT_SECRET = "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";
|
|
4482
|
+
var HOSTED_DOMAIN = "nal.vn";
|
|
4483
|
+
var SCOPES = ["openid", "email", "profile"];
|
|
4484
|
+
var DEVICE_CODE_URL = "https://oauth2.googleapis.com/device/code";
|
|
4485
|
+
var TOKEN_URL = "https://oauth2.googleapis.com/token";
|
|
4486
|
+
var REVOKE_URL = "https://oauth2.googleapis.com/revoke";
|
|
4487
|
+
async function requestDeviceCode() {
|
|
4488
|
+
const body = new URLSearchParams({
|
|
4489
|
+
client_id: GOOGLE_CLIENT_ID,
|
|
4490
|
+
scope: SCOPES.join(" ")
|
|
4347
4491
|
});
|
|
4348
|
-
await
|
|
4349
|
-
|
|
4350
|
-
|
|
4351
|
-
|
|
4352
|
-
|
|
4353
|
-
|
|
4354
|
-
|
|
4355
|
-
|
|
4356
|
-
await installGitHook(join25(args.workspacePath, ".git", "modules", "src"), "pre-push");
|
|
4357
|
-
log.success("C\xE0i post-merge (workspace) + pre-push (src/)");
|
|
4358
|
-
await autoSyncPackOnInit(args.workspacePath);
|
|
4359
|
-
await appendAuditEntry("init", `flow=${args.flow},workspace=${args.workspaceName}`);
|
|
4360
|
-
await maybeCommitWorkspace(args.workspacePath, args.skipCommit);
|
|
4361
|
-
await maybeCreateWorkspaceRemote(args);
|
|
4362
|
-
let aiResult = null;
|
|
4363
|
-
if (args.aiSkip) {
|
|
4364
|
-
log.dim("B\u1ECF qua AI setup (--ai-skip). Setup sau qua: avatar ai setup");
|
|
4365
|
-
} else {
|
|
4366
|
-
aiResult = await runAiSetupPhase({ workspacePath: args.workspacePath });
|
|
4492
|
+
const res = await fetch(DEVICE_CODE_URL, {
|
|
4493
|
+
method: "POST",
|
|
4494
|
+
headers: { "Content-Type": "application/x-www-form-urlencoded" },
|
|
4495
|
+
body
|
|
4496
|
+
});
|
|
4497
|
+
if (!res.ok) {
|
|
4498
|
+
const text = await res.text();
|
|
4499
|
+
throw new Error(`Device code request failed (${res.status}): ${text}`);
|
|
4367
4500
|
}
|
|
4368
|
-
|
|
4369
|
-
|
|
4370
|
-
|
|
4371
|
-
|
|
4372
|
-
|
|
4373
|
-
|
|
4374
|
-
|
|
4375
|
-
|
|
4376
|
-
}
|
|
4377
|
-
|
|
4501
|
+
return await res.json();
|
|
4502
|
+
}
|
|
4503
|
+
async function pollForToken(deviceCode) {
|
|
4504
|
+
const body = new URLSearchParams({
|
|
4505
|
+
client_id: GOOGLE_CLIENT_ID,
|
|
4506
|
+
client_secret: GOOGLE_CLIENT_SECRET,
|
|
4507
|
+
device_code: deviceCode,
|
|
4508
|
+
grant_type: "urn:ietf:params:oauth:grant-type:device_code"
|
|
4509
|
+
});
|
|
4510
|
+
const res = await fetch(TOKEN_URL, {
|
|
4511
|
+
method: "POST",
|
|
4512
|
+
headers: { "Content-Type": "application/x-www-form-urlencoded" },
|
|
4513
|
+
body
|
|
4514
|
+
});
|
|
4515
|
+
if (res.ok) {
|
|
4516
|
+
return await res.json();
|
|
4378
4517
|
}
|
|
4379
|
-
|
|
4380
|
-
|
|
4381
|
-
|
|
4382
|
-
|
|
4383
|
-
|
|
4384
|
-
|
|
4385
|
-
mode: "client",
|
|
4386
|
-
gitnexusReady: true
|
|
4387
|
-
});
|
|
4388
|
-
await writeRootClaudeMd(args.workspacePath, updatedVars);
|
|
4389
|
-
log.dim("Updated CLAUDE.md v\u1EDBi GitNexus section");
|
|
4518
|
+
let errorCode = "";
|
|
4519
|
+
try {
|
|
4520
|
+
const data = await res.json();
|
|
4521
|
+
errorCode = data.error ?? "";
|
|
4522
|
+
} catch {
|
|
4523
|
+
errorCode = "";
|
|
4390
4524
|
}
|
|
4391
|
-
|
|
4525
|
+
if (errorCode === "authorization_pending" || errorCode === "slow_down") {
|
|
4526
|
+
return null;
|
|
4527
|
+
}
|
|
4528
|
+
if (errorCode === "access_denied") {
|
|
4529
|
+
throw new Error("User t\u1EEB ch\u1ED1i quy\u1EC1n truy c\u1EADp");
|
|
4530
|
+
}
|
|
4531
|
+
if (errorCode === "expired_token") {
|
|
4532
|
+
throw new Error("H\u1EBFt h\u1EA1n ch\u1EDD (5 ph\xFAt). Ch\u1EA1y l\u1EA1i 'avatar login'.");
|
|
4533
|
+
}
|
|
4534
|
+
throw new Error(`OAuth token endpoint tr\u1EA3 l\u1ED7i: ${errorCode || res.status}`);
|
|
4392
4535
|
}
|
|
4393
|
-
|
|
4394
|
-
const
|
|
4395
|
-
if (
|
|
4396
|
-
|
|
4397
|
-
return;
|
|
4536
|
+
function decodeIdToken(idToken) {
|
|
4537
|
+
const parts = idToken.split(".");
|
|
4538
|
+
if (parts.length !== 3) {
|
|
4539
|
+
throw new Error("id_token format kh\xF4ng h\u1EE3p l\u1EC7");
|
|
4398
4540
|
}
|
|
4399
|
-
const
|
|
4400
|
-
|
|
4401
|
-
|
|
4402
|
-
|
|
4403
|
-
|
|
4404
|
-
|
|
4405
|
-
|
|
4406
|
-
|
|
4541
|
+
const payload = parts[1];
|
|
4542
|
+
if (!payload) throw new Error("id_token thi\u1EBFu payload");
|
|
4543
|
+
const base64 = payload.replace(/-/g, "+").replace(/_/g, "/");
|
|
4544
|
+
const json = Buffer.from(base64, "base64").toString("utf8");
|
|
4545
|
+
return JSON.parse(json);
|
|
4546
|
+
}
|
|
4547
|
+
function verifyHostedDomain(claims) {
|
|
4548
|
+
if (claims.hd !== HOSTED_DOMAIN) {
|
|
4549
|
+
throw new Error(
|
|
4550
|
+
`Email kh\xF4ng thu\u1ED9c workspace NAL (y\xEAu c\u1EA7u @${HOSTED_DOMAIN}). Nh\u1EADn: ${claims.email}`
|
|
4407
4551
|
);
|
|
4408
|
-
|
|
4409
|
-
|
|
4410
|
-
|
|
4411
|
-
|
|
4412
|
-
|
|
4413
|
-
|
|
4414
|
-
|
|
4415
|
-
|
|
4416
|
-
|
|
4417
|
-
|
|
4418
|
-
|
|
4552
|
+
}
|
|
4553
|
+
if (!claims.email_verified) {
|
|
4554
|
+
throw new Error("Email ch\u01B0a \u0111\u01B0\u1EE3c Google verify");
|
|
4555
|
+
}
|
|
4556
|
+
}
|
|
4557
|
+
function buildUserConfig(token, claims) {
|
|
4558
|
+
const expiresAt = new Date(Date.now() + token.expires_in * 1e3).toISOString();
|
|
4559
|
+
return {
|
|
4560
|
+
email: claims.email,
|
|
4561
|
+
name: claims.name ?? claims.email,
|
|
4562
|
+
access_token: token.access_token,
|
|
4563
|
+
refresh_token: token.refresh_token,
|
|
4564
|
+
expires_at: expiresAt,
|
|
4565
|
+
id_token: token.id_token
|
|
4566
|
+
};
|
|
4567
|
+
}
|
|
4568
|
+
async function revokeToken(token) {
|
|
4569
|
+
const body = new URLSearchParams({ token });
|
|
4570
|
+
await fetch(REVOKE_URL, {
|
|
4571
|
+
method: "POST",
|
|
4572
|
+
headers: { "Content-Type": "application/x-www-form-urlencoded" },
|
|
4573
|
+
body
|
|
4574
|
+
}).catch(() => {
|
|
4575
|
+
});
|
|
4576
|
+
}
|
|
4577
|
+
function buildVerificationUrl(response) {
|
|
4578
|
+
const url = new URL(response.verification_url);
|
|
4579
|
+
url.searchParams.set("user_code", response.user_code);
|
|
4580
|
+
url.searchParams.set("hd", HOSTED_DOMAIN);
|
|
4581
|
+
return url.toString();
|
|
4582
|
+
}
|
|
4583
|
+
|
|
4584
|
+
// src/commands/login.ts
|
|
4585
|
+
function registerLoginCommand(program2) {
|
|
4586
|
+
program2.command("login").description("\u0110\u0103ng nh\u1EADp Google SSO (workspace @nal.vn)").option("--reset", "X\xF3a credential c\u0169 v\xE0 \u0111\u0103ng nh\u1EADp l\u1EA1i").action(async (opts) => {
|
|
4587
|
+
try {
|
|
4588
|
+
await runLogin(opts);
|
|
4589
|
+
} catch (err) {
|
|
4590
|
+
log.error(err instanceof Error ? err.message : String(err));
|
|
4591
|
+
process.exit(1);
|
|
4592
|
+
}
|
|
4593
|
+
});
|
|
4594
|
+
}
|
|
4595
|
+
async function runLogin(opts) {
|
|
4596
|
+
printAvatarBanner({ tagline: "\u0110\u0103ng nh\u1EADp Google SSO \xB7 workspace @nal.vn" });
|
|
4597
|
+
if (opts.reset) {
|
|
4598
|
+
await clearUserConfig();
|
|
4599
|
+
await appendAuditEntry("login_reset");
|
|
4600
|
+
} else {
|
|
4601
|
+
const existing = await readUserConfig();
|
|
4602
|
+
if (existing && !isTokenExpired(existing)) {
|
|
4603
|
+
log.success(`\u0110\xE3 \u0111\u0103ng nh\u1EADp: ${existing.email}`);
|
|
4604
|
+
return;
|
|
4605
|
+
}
|
|
4606
|
+
}
|
|
4607
|
+
const deviceSpinner = spinner("\u0110ang y\xEAu c\u1EA7u device code t\u1EEB Google...");
|
|
4608
|
+
let deviceCode;
|
|
4609
|
+
try {
|
|
4610
|
+
deviceCode = await requestDeviceCode();
|
|
4611
|
+
deviceSpinner.succeed("Nh\u1EADn device code");
|
|
4612
|
+
} catch (err) {
|
|
4613
|
+
deviceSpinner.fail("Kh\xF4ng k\u1EBFt n\u1ED1i \u0111\u01B0\u1EE3c Google");
|
|
4614
|
+
throw err;
|
|
4615
|
+
}
|
|
4616
|
+
const verificationUrl = buildVerificationUrl(deviceCode);
|
|
4617
|
+
const instructions = [
|
|
4618
|
+
`1. Truy c\u1EADp: ${chalk.cyan(deviceCode.verification_url)}`,
|
|
4619
|
+
`2. Nh\u1EADp code: ${chalk.bold.yellow(deviceCode.user_code)}`,
|
|
4620
|
+
"",
|
|
4621
|
+
`Ho\u1EB7c Avatar t\u1EF1 m\u1EDF browser, click ${chalk.green("Allow")}...`
|
|
4622
|
+
].join("\n");
|
|
4623
|
+
process.stdout.write(`${boxen6(instructions, { padding: 1, borderStyle: "round" })}
|
|
4624
|
+
`);
|
|
4625
|
+
void open(verificationUrl).catch(() => {
|
|
4626
|
+
log.dim("(Kh\xF4ng m\u1EDF \u0111\u01B0\u1EE3c browser t\u1EF1 \u0111\u1ED9ng \u2014 copy URL \u1EDF tr\xEAn)");
|
|
4627
|
+
});
|
|
4628
|
+
const waitSpinner = spinner("\u0110ang ch\u1EDD x\xE1c nh\u1EADn trong browser...");
|
|
4629
|
+
const intervalMs = deviceCode.interval * 1e3;
|
|
4630
|
+
const deadline = Date.now() + deviceCode.expires_in * 1e3;
|
|
4631
|
+
let token = null;
|
|
4632
|
+
while (Date.now() < deadline) {
|
|
4633
|
+
await sleep(intervalMs);
|
|
4634
|
+
try {
|
|
4635
|
+
token = await pollForToken(deviceCode.device_code);
|
|
4636
|
+
if (token) break;
|
|
4637
|
+
} catch (err) {
|
|
4638
|
+
waitSpinner.fail("X\xE1c th\u1EF1c th\u1EA5t b\u1EA1i");
|
|
4639
|
+
throw err;
|
|
4419
4640
|
}
|
|
4641
|
+
}
|
|
4642
|
+
if (!token) {
|
|
4643
|
+
waitSpinner.fail("H\u1EBFt h\u1EA1n ch\u1EDD (5 ph\xFAt). Ch\u1EA1y l\u1EA1i 'avatar login'.");
|
|
4644
|
+
process.exit(1);
|
|
4645
|
+
}
|
|
4646
|
+
waitSpinner.succeed("\u0110\xE3 nh\u1EADn token t\u1EEB Google");
|
|
4647
|
+
const claims = decodeIdToken(token.id_token);
|
|
4648
|
+
try {
|
|
4649
|
+
verifyHostedDomain(claims);
|
|
4420
4650
|
} catch (err) {
|
|
4421
|
-
|
|
4422
|
-
|
|
4423
|
-
);
|
|
4651
|
+
await revokeToken(token.access_token);
|
|
4652
|
+
throw err;
|
|
4424
4653
|
}
|
|
4654
|
+
const userConfig = buildUserConfig(token, claims);
|
|
4655
|
+
await writeUserConfig(userConfig);
|
|
4656
|
+
await appendAuditEntry("login", userConfig.email);
|
|
4657
|
+
log.success(`X\xE1c th\u1EF1c th\xE0nh c\xF4ng: ${userConfig.email}`);
|
|
4658
|
+
log.success(`Verify hosted domain: ${claims.hd} \u2713`);
|
|
4659
|
+
log.success(`L\u01B0u credential v\xE0o ${USER_CONFIG_PATH} (chmod 600)`);
|
|
4425
4660
|
}
|
|
4426
|
-
|
|
4427
|
-
|
|
4428
|
-
|
|
4429
|
-
|
|
4430
|
-
|
|
4431
|
-
|
|
4432
|
-
|
|
4433
|
-
|
|
4434
|
-
|
|
4435
|
-
|
|
4436
|
-
|
|
4437
|
-
|
|
4438
|
-
|
|
4439
|
-
if (!shouldCreate) return;
|
|
4440
|
-
const visibility = args.repoVisibility ?? (args.autoYes ? "private" : await select9({
|
|
4441
|
-
message: "Workspace visibility?",
|
|
4442
|
-
choices: [
|
|
4443
|
-
{ name: "private (m\u1EB7c \u0111\u1ECBnh, an to\xE0n)", value: "private" },
|
|
4444
|
-
{ name: "public", value: "public" }
|
|
4445
|
-
]
|
|
4446
|
-
}));
|
|
4447
|
-
while (true) {
|
|
4661
|
+
function sleep(ms) {
|
|
4662
|
+
return new Promise((resolve3) => setTimeout(resolve3, ms));
|
|
4663
|
+
}
|
|
4664
|
+
|
|
4665
|
+
// src/commands/init.ts
|
|
4666
|
+
function registerInitCommand(program2) {
|
|
4667
|
+
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("--latest", "Pull HEAD c\u1EE7a team-ai-pack main branch (b\u1ECF qua tag SemVer, bleeding-edge)").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(
|
|
4668
|
+
"--gitnexus-skip",
|
|
4669
|
+
"B\u1ECF qua phase GitNexus setup (M10 \u2014 ch\u1EA1y `avatar gitnexus install` sau)"
|
|
4670
|
+
).option(
|
|
4671
|
+
"--bootstrap-strategy <s>",
|
|
4672
|
+
"X\u1EED l\xFD folder dirty: stash | commit-all | skip | branch (default: prompt)"
|
|
4673
|
+
).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) => {
|
|
4448
4674
|
try {
|
|
4449
|
-
await
|
|
4450
|
-
workspacePath: args.workspacePath,
|
|
4451
|
-
workspaceName: args.workspaceName,
|
|
4452
|
-
visibility,
|
|
4453
|
-
org: args.repoOrg
|
|
4454
|
-
});
|
|
4455
|
-
return;
|
|
4675
|
+
await runInit(opts);
|
|
4456
4676
|
} catch (err) {
|
|
4457
|
-
if (err instanceof
|
|
4458
|
-
|
|
4459
|
-
|
|
4460
|
-
message: `Repo '${fullName}' \u0111\xE3 t\u1ED3n t\u1EA1i tr\xEAn GitHub. C\xE1ch x\u1EED l\xFD?`,
|
|
4461
|
-
choices: [
|
|
4462
|
-
{
|
|
4463
|
-
name: "D\xF9ng remote \u0111\xE3 c\xF3 (link workspace local v\xE0o repo n\xE0y)",
|
|
4464
|
-
value: "reuse"
|
|
4465
|
-
},
|
|
4466
|
-
{
|
|
4467
|
-
name: "Nh\u1EADp t\xEAn workspace kh\xE1c (t\u1EA1o repo m\u1EDBi)",
|
|
4468
|
-
value: "rename"
|
|
4469
|
-
},
|
|
4470
|
-
{ name: "B\u1ECF qua (workspace local-only)", value: "skip" },
|
|
4471
|
-
{ name: "T\u1EA1m ng\u01B0ng init", value: "abort" }
|
|
4472
|
-
]
|
|
4473
|
-
});
|
|
4474
|
-
if (reuseAction === "abort") {
|
|
4475
|
-
throw new UserAbortedRecoveryError("User abort t\u1EA1i b\u01B0\u1EDBc t\u1EA1o workspace remote.");
|
|
4476
|
-
}
|
|
4477
|
-
if (reuseAction === "skip") {
|
|
4478
|
-
log.warn("Workspace v\u1EABn s\u1EB5n s\xE0ng local-only. Setup remote sau khi c\u1EA7n.");
|
|
4479
|
-
return;
|
|
4480
|
-
}
|
|
4481
|
-
if (reuseAction === "reuse") {
|
|
4482
|
-
linkExistingRemoteToWorkspace({
|
|
4483
|
-
workspacePath: args.workspacePath,
|
|
4484
|
-
fullName
|
|
4485
|
-
});
|
|
4486
|
-
return;
|
|
4487
|
-
}
|
|
4488
|
-
const newName = await input5({
|
|
4489
|
-
message: "T\xEAn workspace m\u1EDBi (s\u1EBD t\u1EA1o repo new):",
|
|
4490
|
-
validate: (v) => v.trim().length > 0 ? true : "T\xEAn kh\xF4ng \u0111\u01B0\u1EE3c r\u1ED7ng"
|
|
4491
|
-
});
|
|
4492
|
-
args.workspaceName = newName.trim();
|
|
4493
|
-
continue;
|
|
4677
|
+
if (err instanceof InitAbortedByUserError) {
|
|
4678
|
+
log.dim(err.message);
|
|
4679
|
+
process.exit(0);
|
|
4494
4680
|
}
|
|
4495
|
-
|
|
4496
|
-
|
|
4497
|
-
|
|
4498
|
-
allowSkip: true,
|
|
4499
|
-
// Workspace remote OPTIONAL — skip OK, workspace local vẫn dùng được.
|
|
4500
|
-
hint: "Tip: sai org? Pass --repo-org=<your-gh-user>. Ho\u1EB7c switch gh: gh auth login."
|
|
4501
|
-
});
|
|
4502
|
-
if (action === "abort") {
|
|
4503
|
-
throw new UserAbortedRecoveryError("User abort t\u1EA1i b\u01B0\u1EDBc t\u1EA1o workspace remote.");
|
|
4681
|
+
if (err instanceof TeamPackAccessAbortedError) {
|
|
4682
|
+
log.dim(err.message);
|
|
4683
|
+
process.exit(0);
|
|
4504
4684
|
}
|
|
4505
|
-
if (
|
|
4506
|
-
log.
|
|
4507
|
-
|
|
4685
|
+
if (err instanceof RemoteAccessAbortedError) {
|
|
4686
|
+
log.dim(err.message);
|
|
4687
|
+
process.exit(0);
|
|
4688
|
+
}
|
|
4689
|
+
if (err instanceof UserAbortedRecoveryError) {
|
|
4690
|
+
log.dim(err.message);
|
|
4691
|
+
process.exit(0);
|
|
4508
4692
|
}
|
|
4693
|
+
log.error(err instanceof Error ? err.message : String(err));
|
|
4694
|
+
process.exit(1);
|
|
4509
4695
|
}
|
|
4510
|
-
}
|
|
4696
|
+
});
|
|
4511
4697
|
}
|
|
4512
|
-
async function
|
|
4513
|
-
|
|
4514
|
-
if (
|
|
4515
|
-
|
|
4516
|
-
|
|
4517
|
-
|
|
4518
|
-
|
|
4519
|
-
|
|
4520
|
-
|
|
4521
|
-
|
|
4522
|
-
|
|
4523
|
-
|
|
4524
|
-
choices.push({ name: `D\xF9ng "${alternative}" (suggest)`, value: "use-alt" });
|
|
4698
|
+
async function runInit(opts) {
|
|
4699
|
+
if (!opts.yes) printAvatarBanner({ tagline: "Kh\u1EDFi t\u1EA1o Avatar trong d\u1EF1 \xE1n c\u1EE7a b\u1EA1n" });
|
|
4700
|
+
if (opts.mode) {
|
|
4701
|
+
log.warn("Flag --mode \u0111\xE3 deprecated t\u1EEB v1.1. D\xF9ng --project-status thay th\u1EBF.");
|
|
4702
|
+
}
|
|
4703
|
+
let userConfig = await readUserConfig();
|
|
4704
|
+
while (!userConfig || isTokenExpired(userConfig)) {
|
|
4705
|
+
log.info("Ch\u01B0a \u0111\u0103ng nh\u1EADp \u2014 ch\u1EA1y login flow tr\u01B0\u1EDBc khi init...");
|
|
4706
|
+
try {
|
|
4707
|
+
await runLogin({});
|
|
4708
|
+
} catch (err) {
|
|
4709
|
+
log.warn(`Login fail: ${err.message}`);
|
|
4525
4710
|
}
|
|
4526
|
-
|
|
4527
|
-
|
|
4528
|
-
const action = await
|
|
4529
|
-
|
|
4530
|
-
|
|
4711
|
+
userConfig = await readUserConfig();
|
|
4712
|
+
if (userConfig && !isTokenExpired(userConfig)) break;
|
|
4713
|
+
const action = await promptRetryOrSkip({
|
|
4714
|
+
taskName: "\u0110\u0103ng nh\u1EADp SSO Google",
|
|
4715
|
+
reason: "Token ch\u01B0a \u0111\u01B0\u1EE3c t\u1EA1o ho\u1EB7c \u0111\xE3 h\u1EBFt h\u1EA1n.",
|
|
4716
|
+
allowSkip: false,
|
|
4717
|
+
// Login bắt buộc, không skip được.
|
|
4718
|
+
hint: "\u0110\u1EA3m b\u1EA3o b\u1EA1n ch\u1ECDn 'Allow' tr\xEAn browser v\xE0 d\xF9ng email @nal.vn."
|
|
4531
4719
|
});
|
|
4532
4720
|
if (action === "abort") {
|
|
4533
4721
|
throw new UserAbortedRecoveryError(
|
|
4534
|
-
"User abort t\u1EA1i b\u01B0\u1EDBc
|
|
4722
|
+
"User abort t\u1EA1i b\u01B0\u1EDBc login. Ch\u1EA1y 'avatar login' tay r\u1ED3i 'avatar init' l\u1EA1i."
|
|
4535
4723
|
);
|
|
4536
4724
|
}
|
|
4537
|
-
if (action === "use-alt" && alternative) {
|
|
4538
|
-
return alternative;
|
|
4539
|
-
}
|
|
4540
|
-
const newName = await input5({
|
|
4541
|
-
message: "T\xEAn workspace m\u1EDBi:",
|
|
4542
|
-
validate: (v) => v.trim().length > 0 ? true : "T\xEAn kh\xF4ng \u0111\u01B0\u1EE3c r\u1ED7ng"
|
|
4543
|
-
});
|
|
4544
|
-
const newPath = join25(parent, newName.trim());
|
|
4545
|
-
if (await isEmptyOrMissing(newPath)) return newPath;
|
|
4546
|
-
log.warn(`"${newPath}" c\u0169ng \u0111\xE3 c\xF3 n\u1ED9i dung. Th\u1EED t\xEAn kh\xE1c.`);
|
|
4547
|
-
}
|
|
4548
|
-
}
|
|
4549
|
-
async function promptTeamOwner(currentUserEmail) {
|
|
4550
|
-
return await input5({ message: "Team owner email:", default: currentUserEmail });
|
|
4551
|
-
}
|
|
4552
|
-
async function maybeCommitWorkspace(workspacePath, skipCommit) {
|
|
4553
|
-
if (skipCommit) {
|
|
4554
|
-
log.warn("Skip commit (--no-commit). Ch\u1EA1y 'git status' + commit th\u1EE7 c\xF4ng sau.");
|
|
4555
|
-
return;
|
|
4556
|
-
}
|
|
4557
|
-
const g = git(workspacePath);
|
|
4558
|
-
await g.add(["CLAUDE.md", ".claude/", ".gitignore", ".gitmodules", "notes/", "scripts/"]);
|
|
4559
|
-
await g.commit("chore: initialize Avatar workspace");
|
|
4560
|
-
log.success("\u0110\xE3 commit workspace");
|
|
4561
|
-
}
|
|
4562
|
-
function formatAiStatusLine(aiResult) {
|
|
4563
|
-
if (aiResult === null) {
|
|
4564
|
-
return ` ${chalk.yellow("AI:")} skipped \xB7 ${chalk.cyan("avatar ai setup")} \u0111\u1EC3 config sau`;
|
|
4565
|
-
}
|
|
4566
|
-
if (aiResult.ok) {
|
|
4567
|
-
const modelPart = aiResult.model ? ` \xB7 model=${aiResult.model}` : "";
|
|
4568
|
-
return ` ${chalk.green("AI:")} ready \xB7 ${aiResult.provider}${modelPart}`;
|
|
4569
|
-
}
|
|
4570
|
-
return ` ${chalk.yellow("AI:")} failed (${aiResult.reason.slice(0, 60)}) \xB7 th\u1EED ${chalk.cyan("avatar ai setup")}`;
|
|
4571
|
-
}
|
|
4572
|
-
function formatGitnexusStatusLine(result) {
|
|
4573
|
-
if (result === null) {
|
|
4574
|
-
return ` ${chalk.yellow("GitNexus:")} skipped \xB7 ${chalk.cyan("avatar gitnexus install")} \u0111\u1EC3 setup sau`;
|
|
4575
4725
|
}
|
|
4576
|
-
|
|
4577
|
-
|
|
4578
|
-
|
|
4579
|
-
|
|
4580
|
-
|
|
4581
|
-
|
|
4726
|
+
const status = opts.projectStatus ?? await promptProjectStatus();
|
|
4727
|
+
switch (status) {
|
|
4728
|
+
case "existing-remote":
|
|
4729
|
+
await runInitFromExistingRemote(opts, userConfig.email);
|
|
4730
|
+
break;
|
|
4731
|
+
case "existing-folder":
|
|
4732
|
+
await runInitFromExistingFolder(opts, userConfig.email);
|
|
4733
|
+
break;
|
|
4734
|
+
case "new-project":
|
|
4735
|
+
await runInitFromScratch(opts, userConfig.email);
|
|
4736
|
+
break;
|
|
4582
4737
|
}
|
|
4583
|
-
return ` ${chalk.yellow("GitNexus:")} skipped (${(result.reason ?? "unknown").slice(0, 40)}) \xB7 th\u1EED ${chalk.cyan("avatar gitnexus install")}`;
|
|
4584
4738
|
}
|
|
4585
|
-
async function
|
|
4586
|
-
|
|
4587
|
-
|
|
4588
|
-
|
|
4589
|
-
|
|
4590
|
-
|
|
4591
|
-
|
|
4592
|
-
|
|
4593
|
-
|
|
4594
|
-
"",
|
|
4595
|
-
` ${chalk.cyan("avatar commit src")} Commit code l\xEAn client remote`,
|
|
4596
|
-
` ${chalk.cyan("avatar sync")} Pull team-ai-pack m\u1EDBi`,
|
|
4597
|
-
` ${chalk.cyan("avatar uninstall")} G\u1EE1 Avatar (gi\u1EEF code)`
|
|
4598
|
-
];
|
|
4599
|
-
process.stdout.write(`${boxen6(lines.join("\n"), { padding: 1, borderStyle: "round" })}
|
|
4600
|
-
`);
|
|
4601
|
-
const packDir = join25(rootPath, TEAM_PACK_RELATIVE_PATH);
|
|
4602
|
-
if (await pathExists(packDir)) {
|
|
4603
|
-
process.stdout.write(`
|
|
4604
|
-
${formatPackCommandsCheatsheetBox()}
|
|
4605
|
-
`);
|
|
4606
|
-
}
|
|
4739
|
+
async function promptProjectStatus() {
|
|
4740
|
+
return await select13({
|
|
4741
|
+
message: "T\xECnh tr\u1EA1ng d\u1EF1 \xE1n c\u1EE7a b\u1EA1n?",
|
|
4742
|
+
choices: [
|
|
4743
|
+
{ name: "1. \u0110\xE3 c\xF3 repo git remote (URL c\xF3 s\u1EB5n)", value: "existing-remote" },
|
|
4744
|
+
{ name: "2. \u0110\xE3 c\xF3 folder code local", value: "existing-folder" },
|
|
4745
|
+
{ name: "3. D\u1EF1 \xE1n m\u1EDBi ho\xE0n to\xE0n", value: "new-project" }
|
|
4746
|
+
]
|
|
4747
|
+
});
|
|
4607
4748
|
}
|
|
4608
4749
|
|
|
4609
4750
|
// src/lib/not-implemented-stub.ts
|
|
@@ -4628,7 +4769,7 @@ function registerMcpRunCommand(program2) {
|
|
|
4628
4769
|
}
|
|
4629
4770
|
|
|
4630
4771
|
// src/commands/pack-status-and-version-check.ts
|
|
4631
|
-
import { join as
|
|
4772
|
+
import { join as join28 } from "path";
|
|
4632
4773
|
import boxen7 from "boxen";
|
|
4633
4774
|
var PACK_RELATIVE_PATH = ".claude/pack";
|
|
4634
4775
|
function registerPackCommand(program2) {
|
|
@@ -4649,7 +4790,7 @@ function registerPackCommand(program2) {
|
|
|
4649
4790
|
});
|
|
4650
4791
|
}
|
|
4651
4792
|
async function gatherPackStatus(cwd, doFetch) {
|
|
4652
|
-
const packDir =
|
|
4793
|
+
const packDir = join28(cwd, PACK_RELATIVE_PATH);
|
|
4653
4794
|
if (!await pathExists(packDir) || !await isGitRepo(packDir)) {
|
|
4654
4795
|
return {
|
|
4655
4796
|
installed: false,
|
|
@@ -4738,18 +4879,18 @@ function registerSecretsCommand(program2) {
|
|
|
4738
4879
|
}
|
|
4739
4880
|
|
|
4740
4881
|
// src/commands/status.ts
|
|
4741
|
-
import { promises as
|
|
4742
|
-
import { join as
|
|
4882
|
+
import { promises as fs14 } from "fs";
|
|
4883
|
+
import { join as join30 } from "path";
|
|
4743
4884
|
import boxen8 from "boxen";
|
|
4744
4885
|
|
|
4745
4886
|
// src/lib/pack-backup-manager.ts
|
|
4746
|
-
import { promises as
|
|
4747
|
-
import { join as
|
|
4887
|
+
import { promises as fs13 } from "fs";
|
|
4888
|
+
import { join as join29 } from "path";
|
|
4748
4889
|
var BACKUP_DIR_NAME = "_backup";
|
|
4749
4890
|
async function listBackups(projectRoot) {
|
|
4750
|
-
const dir =
|
|
4891
|
+
const dir = join29(projectRoot, ".claude", BACKUP_DIR_NAME);
|
|
4751
4892
|
if (!await pathExists(dir)) return [];
|
|
4752
|
-
const entries = await
|
|
4893
|
+
const entries = await fs13.readdir(dir, { withFileTypes: true });
|
|
4753
4894
|
return entries.filter((e) => e.isDirectory()).map((e) => e.name).sort().reverse();
|
|
4754
4895
|
}
|
|
4755
4896
|
|
|
@@ -4772,7 +4913,7 @@ function registerStatusCommand(program2) {
|
|
|
4772
4913
|
}
|
|
4773
4914
|
async function gatherStatus(cwd) {
|
|
4774
4915
|
const projectName = cwd.split("/").filter(Boolean).pop() ?? "unknown";
|
|
4775
|
-
const claudeRoot =
|
|
4916
|
+
const claudeRoot = join30(cwd, ".claude");
|
|
4776
4917
|
const hasAvatar = await pathExists(claudeRoot);
|
|
4777
4918
|
if (!hasAvatar) {
|
|
4778
4919
|
return {
|
|
@@ -4785,9 +4926,9 @@ async function gatherStatus(cwd) {
|
|
|
4785
4926
|
hasAvatar: false
|
|
4786
4927
|
};
|
|
4787
4928
|
}
|
|
4788
|
-
const packVersion = await isGitRepo(
|
|
4789
|
-
const pendingDir =
|
|
4790
|
-
const pendingCount = await pathExists(pendingDir) ? (await
|
|
4929
|
+
const packVersion = await isGitRepo(join30(claudeRoot, "pack")) ? await readPinnedPackVersion(cwd).catch(() => null) : null;
|
|
4930
|
+
const pendingDir = join30(claudeRoot, "_pending");
|
|
4931
|
+
const pendingCount = await pathExists(pendingDir) ? (await fs14.readdir(pendingDir)).filter((n) => n.endsWith(".diff.md")).length : 0;
|
|
4791
4932
|
const backupCount = (await listBackups(cwd)).length;
|
|
4792
4933
|
const techStackSummary = await readTechStackFirstLine(claudeRoot);
|
|
4793
4934
|
return {
|
|
@@ -4801,7 +4942,7 @@ async function gatherStatus(cwd) {
|
|
|
4801
4942
|
};
|
|
4802
4943
|
}
|
|
4803
4944
|
async function readTechStackFirstLine(claudeRoot) {
|
|
4804
|
-
const techStackPath =
|
|
4945
|
+
const techStackPath = join30(claudeRoot, "project", "tech-stack.md");
|
|
4805
4946
|
if (!await pathExists(techStackPath)) return "(no tech-stack.md)";
|
|
4806
4947
|
const content = await readText(techStackPath);
|
|
4807
4948
|
const firstNonHeaderLine = content.split("\n").find((l) => l.trim() && !l.startsWith("#") && !l.startsWith(">"));
|
|
@@ -4822,17 +4963,17 @@ function renderStatusBox(s) {
|
|
|
4822
4963
|
}
|
|
4823
4964
|
|
|
4824
4965
|
// src/commands/sync.ts
|
|
4825
|
-
import { join as
|
|
4966
|
+
import { join as join32 } from "path";
|
|
4826
4967
|
|
|
4827
4968
|
// src/lib/preview-team-pack-sync-changes-for-dry-run.ts
|
|
4828
|
-
import { join as
|
|
4969
|
+
import { join as join31 } from "path";
|
|
4829
4970
|
async function inspectMountDir(packDir, claudeDir, dir) {
|
|
4830
|
-
const source =
|
|
4831
|
-
const dest =
|
|
4971
|
+
const source = join31(packDir, dir);
|
|
4972
|
+
const dest = join31(claudeDir, dir);
|
|
4832
4973
|
if (!await pathExists(source)) return "source-missing";
|
|
4833
4974
|
if (!await pathExists(dest)) return "needs-creation";
|
|
4834
|
-
const { promises:
|
|
4835
|
-
const st = await
|
|
4975
|
+
const { promises: fs15 } = await import("fs");
|
|
4976
|
+
const st = await fs15.lstat(dest);
|
|
4836
4977
|
if (st.isSymbolicLink()) return "already-linked";
|
|
4837
4978
|
return "conflict-real-dir";
|
|
4838
4979
|
}
|
|
@@ -4870,15 +5011,14 @@ async function buildSyncPreview(packDir, claudeDir, targetVersion) {
|
|
|
4870
5011
|
var DEFAULT_PACK_BRANCH2 = "main";
|
|
4871
5012
|
async function syncAction(opts) {
|
|
4872
5013
|
const projectRoot = process.cwd();
|
|
4873
|
-
const claudeDir =
|
|
4874
|
-
const packDir =
|
|
5014
|
+
const claudeDir = join32(projectRoot, ".claude");
|
|
5015
|
+
const packDir = join32(projectRoot, TEAM_PACK_RELATIVE_PATH);
|
|
4875
5016
|
if (!await pathExists(packDir)) {
|
|
4876
5017
|
log.error(
|
|
4877
5018
|
`team-ai-pack submodule ch\u01B0a \u0111\u01B0\u1EE3c kh\u1EDFi t\u1EA1o \u1EDF ${TEAM_PACK_RELATIVE_PATH}/.
|
|
4878
5019
|
Ch\u1EA1y 'avatar init' \u0111\u1EC3 add submodule, ho\u1EB7c 'git submodule update --init' n\u1EBFu \u0111\xE3 clone repo.`
|
|
4879
5020
|
);
|
|
4880
5021
|
process.exit(1);
|
|
4881
|
-
return;
|
|
4882
5022
|
}
|
|
4883
5023
|
try {
|
|
4884
5024
|
await git(packDir).fetch(["--tags", "origin"]);
|
|
@@ -4901,7 +5041,6 @@ async function syncAction(opts) {
|
|
|
4901
5041
|
Pass --version <tag> r\xF5 r\xE0ng, ho\u1EB7c d\xF9ng --latest \u0111\u1EC3 pull HEAD branch ${DEFAULT_PACK_BRANCH2}.`
|
|
4902
5042
|
);
|
|
4903
5043
|
process.exit(1);
|
|
4904
|
-
return;
|
|
4905
5044
|
}
|
|
4906
5045
|
targetVersion = picked;
|
|
4907
5046
|
}
|
|
@@ -5008,33 +5147,33 @@ function registerToolsCommand(program2) {
|
|
|
5008
5147
|
|
|
5009
5148
|
// src/commands/uninstall.ts
|
|
5010
5149
|
import { relative as relative4 } from "path";
|
|
5011
|
-
import { confirm as
|
|
5150
|
+
import { confirm as confirm7 } from "@inquirer/prompts";
|
|
5012
5151
|
import boxen9 from "boxen";
|
|
5013
5152
|
|
|
5014
5153
|
// src/lib/create-uninstall-backup-snapshot.ts
|
|
5015
5154
|
import { cp, mkdir, writeFile } from "fs/promises";
|
|
5016
5155
|
import { homedir as homedir4 } from "os";
|
|
5017
|
-
import { basename as
|
|
5018
|
-
var UNINSTALL_BACKUPS_DIR =
|
|
5156
|
+
import { basename as basename4, join as join33 } from "path";
|
|
5157
|
+
var UNINSTALL_BACKUPS_DIR = join33(homedir4(), ".avatar", "uninstall-backups");
|
|
5019
5158
|
async function createUninstallBackupSnapshot(projectRoot, artifacts, avatarVersion) {
|
|
5020
|
-
const projectName =
|
|
5159
|
+
const projectName = basename4(projectRoot);
|
|
5021
5160
|
const timestamp2 = (/* @__PURE__ */ new Date()).toISOString().replace(/[:.]/g, "-");
|
|
5022
|
-
const backupDir =
|
|
5161
|
+
const backupDir = join33(UNINSTALL_BACKUPS_DIR, `${projectName}-${timestamp2}`);
|
|
5023
5162
|
await mkdir(backupDir, { recursive: true, mode: 448 });
|
|
5024
5163
|
if (artifacts.claudeDir) {
|
|
5025
|
-
await cp(artifacts.claudeDir,
|
|
5164
|
+
await cp(artifacts.claudeDir, join33(backupDir, ".claude"), { recursive: true });
|
|
5026
5165
|
}
|
|
5027
5166
|
if (artifacts.claudeMd) {
|
|
5028
|
-
await cp(artifacts.claudeMd,
|
|
5167
|
+
await cp(artifacts.claudeMd, join33(backupDir, "CLAUDE.md"));
|
|
5029
5168
|
}
|
|
5030
5169
|
if (artifacts.postMergeHook || artifacts.prePushHook) {
|
|
5031
|
-
const hooksBackupDir =
|
|
5170
|
+
const hooksBackupDir = join33(backupDir, "hooks");
|
|
5032
5171
|
await mkdir(hooksBackupDir, { recursive: true });
|
|
5033
5172
|
if (artifacts.postMergeHook) {
|
|
5034
|
-
await cp(artifacts.postMergeHook,
|
|
5173
|
+
await cp(artifacts.postMergeHook, join33(hooksBackupDir, "post-merge"));
|
|
5035
5174
|
}
|
|
5036
5175
|
if (artifacts.prePushHook) {
|
|
5037
|
-
await cp(artifacts.prePushHook,
|
|
5176
|
+
await cp(artifacts.prePushHook, join33(hooksBackupDir, "pre-push"));
|
|
5038
5177
|
}
|
|
5039
5178
|
}
|
|
5040
5179
|
const manifest = {
|
|
@@ -5049,27 +5188,27 @@ async function createUninstallBackupSnapshot(projectRoot, artifacts, avatarVersi
|
|
|
5049
5188
|
prePushHook: !!artifacts.prePushHook
|
|
5050
5189
|
}
|
|
5051
5190
|
};
|
|
5052
|
-
await writeFile(
|
|
5191
|
+
await writeFile(join33(backupDir, "manifest.json"), JSON.stringify(manifest, null, 2), "utf8");
|
|
5053
5192
|
return backupDir;
|
|
5054
5193
|
}
|
|
5055
5194
|
|
|
5056
5195
|
// src/lib/detect-avatar-project-artifacts.ts
|
|
5057
5196
|
import { existsSync as existsSync9 } from "fs";
|
|
5058
|
-
import { join as
|
|
5197
|
+
import { join as join34 } from "path";
|
|
5059
5198
|
function existsOrNull(path) {
|
|
5060
5199
|
return existsSync9(path) ? path : null;
|
|
5061
5200
|
}
|
|
5062
5201
|
function detectAvatarProjectArtifacts(projectRoot) {
|
|
5063
|
-
const claudeDir = existsOrNull(
|
|
5064
|
-
const claudeMd = existsOrNull(
|
|
5065
|
-
const postMergeHook = existsOrNull(
|
|
5202
|
+
const claudeDir = existsOrNull(join34(projectRoot, ".claude"));
|
|
5203
|
+
const claudeMd = existsOrNull(join34(projectRoot, "CLAUDE.md"));
|
|
5204
|
+
const postMergeHook = existsOrNull(join34(projectRoot, ".git", "hooks", "post-merge"));
|
|
5066
5205
|
const prePushHook = existsOrNull(
|
|
5067
|
-
|
|
5206
|
+
join34(projectRoot, ".git", "modules", "src", "hooks", "pre-push")
|
|
5068
5207
|
);
|
|
5069
|
-
const gitignorePath = existsOrNull(
|
|
5070
|
-
const gitmodulesPath = existsOrNull(
|
|
5071
|
-
const notesDir = existsOrNull(
|
|
5072
|
-
const scriptsDir = existsOrNull(
|
|
5208
|
+
const gitignorePath = existsOrNull(join34(projectRoot, ".gitignore"));
|
|
5209
|
+
const gitmodulesPath = existsOrNull(join34(projectRoot, ".gitmodules"));
|
|
5210
|
+
const notesDir = existsOrNull(join34(projectRoot, "notes"));
|
|
5211
|
+
const scriptsDir = existsOrNull(join34(projectRoot, "scripts"));
|
|
5073
5212
|
const hasAnyArtifact = !!(claudeDir || claudeMd || postMergeHook || prePushHook);
|
|
5074
5213
|
return {
|
|
5075
5214
|
hasAnyArtifact,
|
|
@@ -5090,11 +5229,11 @@ async function executeUninstallDeletion(artifacts, flags) {
|
|
|
5090
5229
|
if (artifacts.claudeDir) {
|
|
5091
5230
|
if (flags.keepSubmodule) {
|
|
5092
5231
|
const { readdir: readdir2 } = await import("fs/promises");
|
|
5093
|
-
const { join:
|
|
5232
|
+
const { join: join35 } = await import("path");
|
|
5094
5233
|
const entries = await readdir2(artifacts.claudeDir);
|
|
5095
5234
|
for (const entry of entries) {
|
|
5096
5235
|
if (entry === "pack") continue;
|
|
5097
|
-
await rm(
|
|
5236
|
+
await rm(join35(artifacts.claudeDir, entry), { recursive: true, force: true });
|
|
5098
5237
|
}
|
|
5099
5238
|
} else {
|
|
5100
5239
|
await rm(artifacts.claudeDir, { recursive: true, force: true });
|
|
@@ -5187,7 +5326,7 @@ async function runUninstall(opts) {
|
|
|
5187
5326
|
return;
|
|
5188
5327
|
}
|
|
5189
5328
|
if (!opts.yes) {
|
|
5190
|
-
const ok = await
|
|
5329
|
+
const ok = await confirm7({
|
|
5191
5330
|
message: "Ti\u1EBFp t\u1EE5c g\u1EE1 Avatar?",
|
|
5192
5331
|
default: false
|
|
5193
5332
|
});
|