@nalvietnam/avatar-cli 1.2.3 → 1.2.5
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 +526 -270
- package/dist/index.js.map +1 -1
- package/dist/lib/print-welcome-screen.js +1 -1
- package/dist/lib/print-welcome-screen.js.map +1 -1
- package/package.json +1 -1
package/dist/index.js
CHANGED
|
@@ -546,19 +546,19 @@ function applyUseGlobal(existing, source) {
|
|
|
546
546
|
...sourceModel ? { model: sourceModel } : {}
|
|
547
547
|
};
|
|
548
548
|
}
|
|
549
|
-
async function writeClaudeSettings(workspacePath,
|
|
549
|
+
async function writeClaudeSettings(workspacePath, input4) {
|
|
550
550
|
const path = getClaudeSettingsPath(workspacePath);
|
|
551
551
|
const existing = await readExistingSettings(path);
|
|
552
552
|
let merged;
|
|
553
|
-
switch (
|
|
553
|
+
switch (input4.provider) {
|
|
554
554
|
case "subscription":
|
|
555
|
-
merged = applySubscription(existing,
|
|
555
|
+
merged = applySubscription(existing, input4.model);
|
|
556
556
|
break;
|
|
557
557
|
case "llmlite":
|
|
558
|
-
merged = applyLLMLite(existing,
|
|
558
|
+
merged = applyLLMLite(existing, input4.apiKey, input4.baseUrl, input4.model);
|
|
559
559
|
break;
|
|
560
560
|
case "use-global":
|
|
561
|
-
merged = applyUseGlobal(existing,
|
|
561
|
+
merged = applyUseGlobal(existing, input4.sourceSettings);
|
|
562
562
|
break;
|
|
563
563
|
}
|
|
564
564
|
await writeJsonAtomic(path, merged, SECRET_FILE_MODE2);
|
|
@@ -1194,9 +1194,199 @@ async function applyFixes(checks) {
|
|
|
1194
1194
|
|
|
1195
1195
|
// src/commands/init.ts
|
|
1196
1196
|
import { basename, join as join16, relative as relative2, resolve } from "path";
|
|
1197
|
-
import { confirm as confirm3, input as
|
|
1197
|
+
import { confirm as confirm3, input as input3, select as select7 } from "@inquirer/prompts";
|
|
1198
1198
|
import boxen3 from "boxen";
|
|
1199
1199
|
|
|
1200
|
+
// src/lib/prompt-recovery-action-on-failure.ts
|
|
1201
|
+
import { input as input2, select as select3 } from "@inquirer/prompts";
|
|
1202
|
+
var UserAbortedRecoveryError = class extends Error {
|
|
1203
|
+
constructor(message) {
|
|
1204
|
+
super(message);
|
|
1205
|
+
this.name = "UserAbortedRecoveryError";
|
|
1206
|
+
}
|
|
1207
|
+
};
|
|
1208
|
+
async function promptRetryOrSkip(args) {
|
|
1209
|
+
log.warn(`${args.taskName} th\u1EA5t b\u1EA1i: ${args.reason}`);
|
|
1210
|
+
if (args.hint) log.info(args.hint);
|
|
1211
|
+
const choices = [
|
|
1212
|
+
{ name: "Th\u1EED l\u1EA1i (Retry)", value: "retry" }
|
|
1213
|
+
];
|
|
1214
|
+
if (args.allowSkip) {
|
|
1215
|
+
choices.push({ name: "B\u1ECF qua b\u01B0\u1EDBc n\xE0y v\xE0 ti\u1EBFp t\u1EE5c (Skip)", value: "skip" });
|
|
1216
|
+
}
|
|
1217
|
+
choices.push({ name: "T\u1EA1m ng\u01B0ng init \u2014 ch\u1EA1y l\u1EA1i sau (Abort)", value: "abort" });
|
|
1218
|
+
return await select3({
|
|
1219
|
+
message: "C\xE1ch x\u1EED l\xFD?",
|
|
1220
|
+
choices
|
|
1221
|
+
});
|
|
1222
|
+
}
|
|
1223
|
+
|
|
1224
|
+
// src/lib/team-pack-submodule-manager.ts
|
|
1225
|
+
import { join as join10 } from "path";
|
|
1226
|
+
|
|
1227
|
+
// src/lib/check-team-pack-access-with-retry-loop.ts
|
|
1228
|
+
import { spawnSync as spawnSync6 } from "child_process";
|
|
1229
|
+
import { confirm as confirm2, select as select4 } from "@inquirer/prompts";
|
|
1230
|
+
function parseRepoSlugFromGitUrl(url) {
|
|
1231
|
+
const httpsMatch = url.match(/github\.com[/:]([^/]+\/[^/]+?)(?:\.git)?$/);
|
|
1232
|
+
if (httpsMatch) return httpsMatch[1];
|
|
1233
|
+
return null;
|
|
1234
|
+
}
|
|
1235
|
+
function checkRepoAccess(repoSlug) {
|
|
1236
|
+
const r = spawnSync6("gh", ["api", `repos/${repoSlug}`], { stdio: "ignore" });
|
|
1237
|
+
return r.status === 0;
|
|
1238
|
+
}
|
|
1239
|
+
function getCurrentGhUser() {
|
|
1240
|
+
const r = spawnSync6("gh", ["api", "user", "--jq", ".login"], {
|
|
1241
|
+
encoding: "utf8",
|
|
1242
|
+
stdio: ["ignore", "pipe", "pipe"]
|
|
1243
|
+
});
|
|
1244
|
+
if (r.status !== 0) return null;
|
|
1245
|
+
return r.stdout.trim() || null;
|
|
1246
|
+
}
|
|
1247
|
+
async function copyInfoToClipboardWithConsent(info) {
|
|
1248
|
+
const ok = await confirm2({
|
|
1249
|
+
message: "Copy th\xF4ng tin (GitHub username + email) v\xE0o clipboard \u0111\u1EC3 d\xE1n v\xE0o Slack/email?",
|
|
1250
|
+
default: true
|
|
1251
|
+
});
|
|
1252
|
+
if (!ok) return;
|
|
1253
|
+
try {
|
|
1254
|
+
const { default: clipboardy } = await import("clipboardy");
|
|
1255
|
+
await clipboardy.write(info);
|
|
1256
|
+
log.success("\u0110\xE3 copy v\xE0o clipboard");
|
|
1257
|
+
} catch (err) {
|
|
1258
|
+
log.dim(`Copy clipboard fail: ${err.message}`);
|
|
1259
|
+
}
|
|
1260
|
+
}
|
|
1261
|
+
function buildAccessRequestInfo(ghUser, ssoEmail) {
|
|
1262
|
+
const lines = [
|
|
1263
|
+
"Request access team-ai-pack (NAL)",
|
|
1264
|
+
"",
|
|
1265
|
+
`GitHub username: ${ghUser ?? "(ch\u01B0a gh auth \u2014 ch\u1EA1y: gh auth login)"}`,
|
|
1266
|
+
`NAL email: ${ssoEmail ?? "(ch\u01B0a avatar login \u2014 ch\u1EA1y: avatar login)"}`,
|
|
1267
|
+
`Date: ${(/* @__PURE__ */ new Date()).toISOString().split("T")[0]}`
|
|
1268
|
+
];
|
|
1269
|
+
return lines.join("\n");
|
|
1270
|
+
}
|
|
1271
|
+
async function ensureTeamPackAccessWithRetry(args) {
|
|
1272
|
+
if (checkRepoAccess(args.repoSlug)) return true;
|
|
1273
|
+
const ghUser = getCurrentGhUser();
|
|
1274
|
+
const info = buildAccessRequestInfo(ghUser, args.ssoEmail ?? null);
|
|
1275
|
+
log.warn(`B\u1EA1n ch\u01B0a c\xF3 quy\u1EC1n access v\xE0o b\u1ED9 package ${args.repoSlug}.`);
|
|
1276
|
+
log.info("Li\xEAn h\u1EC7 admin (Luke @nal.vn) \u0111\u1EC3 \u0111\u01B0\u1EE3c add v\xE0o org nalvn.");
|
|
1277
|
+
log.plain("");
|
|
1278
|
+
log.plain(info);
|
|
1279
|
+
log.plain("");
|
|
1280
|
+
await copyInfoToClipboardWithConsent(info);
|
|
1281
|
+
while (true) {
|
|
1282
|
+
const action = await select4({
|
|
1283
|
+
message: "Ti\u1EBFp t\u1EE5c?",
|
|
1284
|
+
choices: [
|
|
1285
|
+
{ name: "\u0110\xE3 \u0111\u01B0\u1EE3c add \u2014 ki\u1EC3m tra l\u1EA1i v\xE0 ti\u1EBFp t\u1EE5c", value: "retry" },
|
|
1286
|
+
{ name: "T\u1EA1m ng\u01B0ng \u2014 ch\u1EA1y l\u1EA1i 'avatar init' sau", value: "abort" }
|
|
1287
|
+
]
|
|
1288
|
+
});
|
|
1289
|
+
if (action === "abort") {
|
|
1290
|
+
log.dim("T\u1EA1m ng\u01B0ng. Ch\u1EA1y l\u1EA1i 'avatar init' sau khi \u0111\xE3 accept invite t\u1EEB GitHub.");
|
|
1291
|
+
return false;
|
|
1292
|
+
}
|
|
1293
|
+
log.info("Ki\u1EC3m tra access...");
|
|
1294
|
+
if (checkRepoAccess(args.repoSlug)) {
|
|
1295
|
+
log.success("\u0110\xE3 c\xF3 access \u2014 ti\u1EBFp t\u1EE5c.");
|
|
1296
|
+
return true;
|
|
1297
|
+
}
|
|
1298
|
+
log.warn("V\u1EABn ch\u01B0a c\xF3 access. \u0110\u1EA3m b\u1EA3o b\u1EA1n \u0111\xE3 accept email invite t\u1EEB GitHub (Inbox + Spam).");
|
|
1299
|
+
}
|
|
1300
|
+
}
|
|
1301
|
+
|
|
1302
|
+
// src/lib/resolve-team-pack-repo-url.ts
|
|
1303
|
+
var ORG_DEFAULT = "https://github.com/nalvn/team-ai-pack.git";
|
|
1304
|
+
function resolveTeamPackRepoUrl() {
|
|
1305
|
+
if (process.env.AVATAR_TEAM_PACK_REPO_URL) {
|
|
1306
|
+
return process.env.AVATAR_TEAM_PACK_REPO_URL;
|
|
1307
|
+
}
|
|
1308
|
+
return ORG_DEFAULT;
|
|
1309
|
+
}
|
|
1310
|
+
|
|
1311
|
+
// src/lib/team-pack-submodule-manager.ts
|
|
1312
|
+
var TEAM_PACK_REPO_URL = resolveTeamPackRepoUrl();
|
|
1313
|
+
var TEAM_PACK_RELATIVE_PATH = ".claude/pack";
|
|
1314
|
+
var TeamPackAccessAbortedError = class extends Error {
|
|
1315
|
+
constructor(message) {
|
|
1316
|
+
super(message);
|
|
1317
|
+
this.name = "TeamPackAccessAbortedError";
|
|
1318
|
+
}
|
|
1319
|
+
};
|
|
1320
|
+
async function addTeamPackSubmodule(projectRoot, tag, ssoEmail) {
|
|
1321
|
+
const url = resolveTeamPackRepoUrl();
|
|
1322
|
+
const repoSlug = parseRepoSlugFromGitUrl(url);
|
|
1323
|
+
if (repoSlug) {
|
|
1324
|
+
const hasAccess = await ensureTeamPackAccessWithRetry({ repoSlug, ssoEmail });
|
|
1325
|
+
if (!hasAccess) {
|
|
1326
|
+
throw new TeamPackAccessAbortedError(
|
|
1327
|
+
"User ch\u1ECDn t\u1EA1m ng\u01B0ng. Ch\u1EA1y l\u1EA1i 'avatar init' sau khi \u0111\u01B0\u1EE3c add v\xE0o org."
|
|
1328
|
+
);
|
|
1329
|
+
}
|
|
1330
|
+
}
|
|
1331
|
+
try {
|
|
1332
|
+
await addSubmodule(url, TEAM_PACK_RELATIVE_PATH, projectRoot);
|
|
1333
|
+
} catch (err) {
|
|
1334
|
+
const msg = err instanceof Error ? err.message : String(err);
|
|
1335
|
+
if (msg.includes("Repository not found") || msg.includes("not found")) {
|
|
1336
|
+
log.error(
|
|
1337
|
+
`Repo team-ai-pack kh\xF4ng t\u1ED3n t\u1EA1i: ${url}
|
|
1338
|
+
C\xE1ch fix:
|
|
1339
|
+
1. T\u1EA1o repo: gh repo create <owner>/team-ai-pack --private --add-readme
|
|
1340
|
+
2. Ho\u1EB7c override URL: export AVATAR_TEAM_PACK_REPO_URL=<url-repo-c\u1EE7a-b\u1EA1n>
|
|
1341
|
+
3. Ho\u1EB7c d\xF9ng flag --skip-team-pack`
|
|
1342
|
+
);
|
|
1343
|
+
}
|
|
1344
|
+
throw err;
|
|
1345
|
+
}
|
|
1346
|
+
let target = tag ?? null;
|
|
1347
|
+
if (!target) {
|
|
1348
|
+
target = await latestTag(join10(projectRoot, TEAM_PACK_RELATIVE_PATH));
|
|
1349
|
+
}
|
|
1350
|
+
if (target) {
|
|
1351
|
+
await checkoutTagInSubmodule(TEAM_PACK_RELATIVE_PATH, target, projectRoot);
|
|
1352
|
+
}
|
|
1353
|
+
return { pinnedTag: target };
|
|
1354
|
+
}
|
|
1355
|
+
async function readPinnedPackVersion(projectRoot) {
|
|
1356
|
+
const submoduleRoot = join10(projectRoot, TEAM_PACK_RELATIVE_PATH);
|
|
1357
|
+
const tag = await latestTag(submoduleRoot);
|
|
1358
|
+
if (tag) return tag;
|
|
1359
|
+
const sha = await currentCommitSha(submoduleRoot);
|
|
1360
|
+
return sha.slice(0, 7);
|
|
1361
|
+
}
|
|
1362
|
+
|
|
1363
|
+
// src/lib/add-team-pack-submodule-with-retry-on-network-fail.ts
|
|
1364
|
+
async function addTeamPackSubmoduleWithRetryOnNetworkFail(projectRoot, tag) {
|
|
1365
|
+
while (true) {
|
|
1366
|
+
try {
|
|
1367
|
+
const result = await addTeamPackSubmodule(projectRoot, tag);
|
|
1368
|
+
return { pinnedTag: result.pinnedTag, skipped: false };
|
|
1369
|
+
} catch (err) {
|
|
1370
|
+
if (err instanceof TeamPackAccessAbortedError) throw err;
|
|
1371
|
+
const action = await promptRetryOrSkip({
|
|
1372
|
+
taskName: "Pull team-ai-pack submodule",
|
|
1373
|
+
reason: err instanceof Error ? err.message : String(err),
|
|
1374
|
+
allowSkip: true,
|
|
1375
|
+
hint: "Network glitch? Retry th\u01B0\u1EDDng work. N\u1EBFu skip, d\xF9ng `avatar sync` sau \u0111\u1EC3 pull pack."
|
|
1376
|
+
});
|
|
1377
|
+
if (action === "abort") {
|
|
1378
|
+
throw new UserAbortedRecoveryError("User abort t\u1EA1i b\u01B0\u1EDBc pull team-ai-pack.");
|
|
1379
|
+
}
|
|
1380
|
+
if (action === "skip") {
|
|
1381
|
+
log.warn(
|
|
1382
|
+
"Skip team-ai-pack. Workspace d\xF9ng \u0111\u01B0\u1EE3c nh\u01B0ng kh\xF4ng c\xF3 shared knowledge. Pull sau qua `avatar sync`."
|
|
1383
|
+
);
|
|
1384
|
+
return { pinnedTag: null, skipped: true };
|
|
1385
|
+
}
|
|
1386
|
+
}
|
|
1387
|
+
}
|
|
1388
|
+
}
|
|
1389
|
+
|
|
1200
1390
|
// src/lib/avatar-ascii-banner.ts
|
|
1201
1391
|
import chalk2 from "chalk";
|
|
1202
1392
|
var BANNER_LINES = [
|
|
@@ -1259,27 +1449,27 @@ ${renderAvatarBanner(opts)}
|
|
|
1259
1449
|
}
|
|
1260
1450
|
|
|
1261
1451
|
// src/lib/execute-gh-repo-create.ts
|
|
1262
|
-
import { spawnSync as
|
|
1452
|
+
import { spawnSync as spawnSync7 } from "child_process";
|
|
1263
1453
|
var RepoAlreadyExistsError = class extends Error {
|
|
1264
1454
|
constructor(fullName) {
|
|
1265
1455
|
super(`Repo "${fullName}" \u0111\xE3 t\u1ED3n t\u1EA1i tr\xEAn GitHub. \u0110\u1ED5i t\xEAn ho\u1EB7c x\xF3a repo c\u0169.`);
|
|
1266
1456
|
this.name = "RepoAlreadyExistsError";
|
|
1267
1457
|
}
|
|
1268
1458
|
};
|
|
1269
|
-
function executeGhRepoCreate(
|
|
1270
|
-
const fullName = `${
|
|
1459
|
+
function executeGhRepoCreate(input4) {
|
|
1460
|
+
const fullName = `${input4.org}/${input4.name}`;
|
|
1271
1461
|
const args = [
|
|
1272
1462
|
"repo",
|
|
1273
1463
|
"create",
|
|
1274
1464
|
fullName,
|
|
1275
|
-
`--${
|
|
1465
|
+
`--${input4.visibility}`,
|
|
1276
1466
|
"--source",
|
|
1277
|
-
|
|
1467
|
+
input4.folder,
|
|
1278
1468
|
"--remote",
|
|
1279
1469
|
"origin",
|
|
1280
1470
|
"--push"
|
|
1281
1471
|
];
|
|
1282
|
-
const r =
|
|
1472
|
+
const r = spawnSync7("gh", args, { stdio: "inherit" });
|
|
1283
1473
|
if (r.status !== 0) {
|
|
1284
1474
|
if (r.status === 1) {
|
|
1285
1475
|
throw new RepoAlreadyExistsError(fullName);
|
|
@@ -1293,9 +1483,9 @@ function executeGhRepoCreate(input3) {
|
|
|
1293
1483
|
}
|
|
1294
1484
|
|
|
1295
1485
|
// src/lib/resolve-github-username-default.ts
|
|
1296
|
-
import { spawnSync as
|
|
1486
|
+
import { spawnSync as spawnSync8 } from "child_process";
|
|
1297
1487
|
function resolveGithubUsernameDefault() {
|
|
1298
|
-
const r =
|
|
1488
|
+
const r = spawnSync8("gh", ["api", "user", "--jq", ".login"], {
|
|
1299
1489
|
encoding: "utf8",
|
|
1300
1490
|
stdio: ["ignore", "pipe", "pipe"]
|
|
1301
1491
|
});
|
|
@@ -1327,28 +1517,28 @@ function validateRepoVisibility(v) {
|
|
|
1327
1517
|
}
|
|
1328
1518
|
|
|
1329
1519
|
// src/lib/create-github-remote-from-folder.ts
|
|
1330
|
-
function createGithubRemoteFromFolder(
|
|
1331
|
-
validateRepoName(
|
|
1332
|
-
validateRepoVisibility(
|
|
1333
|
-
const org =
|
|
1334
|
-
log.info(`T\u1EA1o GitHub repo ${org}/${
|
|
1520
|
+
function createGithubRemoteFromFolder(input4) {
|
|
1521
|
+
validateRepoName(input4.name);
|
|
1522
|
+
validateRepoVisibility(input4.visibility);
|
|
1523
|
+
const org = input4.org ?? resolveGithubUsernameDefault();
|
|
1524
|
+
log.info(`T\u1EA1o GitHub repo ${org}/${input4.name} (${input4.visibility})...`);
|
|
1335
1525
|
const urls = executeGhRepoCreate({
|
|
1336
|
-
folder:
|
|
1526
|
+
folder: input4.folder,
|
|
1337
1527
|
org,
|
|
1338
|
-
name:
|
|
1339
|
-
visibility:
|
|
1528
|
+
name: input4.name,
|
|
1529
|
+
visibility: input4.visibility
|
|
1340
1530
|
});
|
|
1341
1531
|
log.success(`\u0110\xE3 t\u1EA1o: ${urls.sshUrl}`);
|
|
1342
1532
|
return urls;
|
|
1343
1533
|
}
|
|
1344
1534
|
|
|
1345
1535
|
// src/lib/create-workspace-remote-via-gh.ts
|
|
1346
|
-
import { spawnSync as
|
|
1536
|
+
import { spawnSync as spawnSync16 } from "child_process";
|
|
1347
1537
|
|
|
1348
1538
|
// src/lib/check-gh-cli-auth-status.ts
|
|
1349
|
-
import { spawnSync as
|
|
1539
|
+
import { spawnSync as spawnSync9 } from "child_process";
|
|
1350
1540
|
function checkGhCliAuthStatus() {
|
|
1351
|
-
const r =
|
|
1541
|
+
const r = spawnSync9("gh", ["auth", "status"], { stdio: "ignore" });
|
|
1352
1542
|
if (r.error && r.error.code === "ENOENT") {
|
|
1353
1543
|
return "not-installed";
|
|
1354
1544
|
}
|
|
@@ -1356,12 +1546,12 @@ function checkGhCliAuthStatus() {
|
|
|
1356
1546
|
}
|
|
1357
1547
|
|
|
1358
1548
|
// src/lib/detect-package-manager.ts
|
|
1359
|
-
import { spawnSync as
|
|
1549
|
+
import { spawnSync as spawnSync10 } from "child_process";
|
|
1360
1550
|
function hasBinary(name) {
|
|
1361
1551
|
const platform2 = detectHostPlatform();
|
|
1362
1552
|
const probe = platform2 === "win32" ? "where" : "command";
|
|
1363
1553
|
const args = platform2 === "win32" ? [name] : ["-v", name];
|
|
1364
|
-
const r =
|
|
1554
|
+
const r = spawnSync10(probe, args, {
|
|
1365
1555
|
shell: platform2 !== "win32",
|
|
1366
1556
|
stdio: "ignore"
|
|
1367
1557
|
});
|
|
@@ -1376,8 +1566,124 @@ function detectPackageManager() {
|
|
|
1376
1566
|
return null;
|
|
1377
1567
|
}
|
|
1378
1568
|
|
|
1569
|
+
// src/lib/handle-remote-access-failure-with-account-switch.ts
|
|
1570
|
+
import { spawnSync as spawnSync12 } from "child_process";
|
|
1571
|
+
import { select as select5 } from "@inquirer/prompts";
|
|
1572
|
+
|
|
1573
|
+
// src/lib/verify-git-remote-accessible.ts
|
|
1574
|
+
import { spawnSync as spawnSync11 } from "child_process";
|
|
1575
|
+
var TIMEOUT_MS = 5e3;
|
|
1576
|
+
function classifyRemoteError(stderr) {
|
|
1577
|
+
const text = stderr.toLowerCase();
|
|
1578
|
+
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")) {
|
|
1579
|
+
return "no-access";
|
|
1580
|
+
}
|
|
1581
|
+
if (text.includes("404") || text.includes("does not exist")) {
|
|
1582
|
+
return "not-found";
|
|
1583
|
+
}
|
|
1584
|
+
if (text.includes("could not resolve host") || text.includes("network") || text.includes("connection refused") || text.includes("connection timed out")) {
|
|
1585
|
+
return "network";
|
|
1586
|
+
}
|
|
1587
|
+
return "unknown";
|
|
1588
|
+
}
|
|
1589
|
+
function tryVerifyGitRemoteAccessible(url) {
|
|
1590
|
+
const r = spawnSync11("git", ["ls-remote", "--exit-code", url, "HEAD"], {
|
|
1591
|
+
encoding: "utf8",
|
|
1592
|
+
timeout: TIMEOUT_MS,
|
|
1593
|
+
stdio: ["ignore", "pipe", "pipe"]
|
|
1594
|
+
});
|
|
1595
|
+
if (r.status === 0) return { ok: true };
|
|
1596
|
+
if (r.signal === "SIGTERM") {
|
|
1597
|
+
return { ok: false, reason: "timeout", detail: "git ls-remote > 5s" };
|
|
1598
|
+
}
|
|
1599
|
+
const stderr = (r.stderr || "").trim();
|
|
1600
|
+
const reason = classifyRemoteError(stderr);
|
|
1601
|
+
return { ok: false, reason, detail: stderr.slice(0, 300) };
|
|
1602
|
+
}
|
|
1603
|
+
|
|
1604
|
+
// src/lib/handle-remote-access-failure-with-account-switch.ts
|
|
1605
|
+
var RemoteAccessAbortedError = class extends Error {
|
|
1606
|
+
constructor(message) {
|
|
1607
|
+
super(message);
|
|
1608
|
+
this.name = "RemoteAccessAbortedError";
|
|
1609
|
+
}
|
|
1610
|
+
};
|
|
1611
|
+
function getCurrentGhUser2() {
|
|
1612
|
+
const r = spawnSync12("gh", ["api", "user", "--jq", ".login"], {
|
|
1613
|
+
encoding: "utf8",
|
|
1614
|
+
stdio: ["ignore", "pipe", "pipe"]
|
|
1615
|
+
});
|
|
1616
|
+
if (r.status !== 0) return null;
|
|
1617
|
+
return r.stdout.trim() || null;
|
|
1618
|
+
}
|
|
1619
|
+
function triggerGhAuthLoginInteractive() {
|
|
1620
|
+
log.info("M\u1EDF browser \u0111\u1EC3 switch GitHub account...");
|
|
1621
|
+
const r = spawnSync12("gh", ["auth", "login", "--web"], { stdio: "inherit" });
|
|
1622
|
+
if (r.status !== 0) {
|
|
1623
|
+
log.warn(`gh auth login exit ${r.status}. B\u1EA1n c\xF3 th\u1EC3 ch\u1EA1y tay r\u1ED3i quay l\u1EA1i retry.`);
|
|
1624
|
+
}
|
|
1625
|
+
}
|
|
1626
|
+
function getReasonHint(reason, url, ghUser) {
|
|
1627
|
+
switch (reason) {
|
|
1628
|
+
case "no-access":
|
|
1629
|
+
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.";
|
|
1630
|
+
case "not-found":
|
|
1631
|
+
return `URL c\xF3 th\u1EC3 sai ch\xEDnh t\u1EA3, ho\u1EB7c repo \u0111\xE3 b\u1ECB x\xF3a/rename. Check: ${url}`;
|
|
1632
|
+
case "network":
|
|
1633
|
+
return "Kh\xF4ng k\u1EBFt n\u1ED1i \u0111\u01B0\u1EE3c GitHub. Check m\u1EA1ng / VPN / firewall.";
|
|
1634
|
+
case "timeout":
|
|
1635
|
+
return "Network ch\u1EADm > 5s. Check m\u1EA1ng r\u1ED3i retry.";
|
|
1636
|
+
default:
|
|
1637
|
+
return "L\u1ED7i kh\xF4ng x\xE1c \u0111\u1ECBnh. Check URL + gh auth status.";
|
|
1638
|
+
}
|
|
1639
|
+
}
|
|
1640
|
+
async function handleRemoteAccessFailureWithAccountSwitch(args) {
|
|
1641
|
+
let reason = args.initialReason;
|
|
1642
|
+
let detail = args.initialDetail;
|
|
1643
|
+
while (true) {
|
|
1644
|
+
const ghUser = getCurrentGhUser2();
|
|
1645
|
+
log.warn(`Kh\xF4ng truy c\u1EADp \u0111\u01B0\u1EE3c ${args.url}`);
|
|
1646
|
+
log.dim(` L\xFD do: ${reason}${detail ? ` \u2014 ${detail.slice(0, 150)}` : ""}`);
|
|
1647
|
+
log.info(getReasonHint(reason, args.url, ghUser));
|
|
1648
|
+
if (ghUser) log.dim(` gh CLI hi\u1EC7n \u0111ang login: ${ghUser}`);
|
|
1649
|
+
const action = await select5({
|
|
1650
|
+
message: "C\xE1ch x\u1EED l\xFD?",
|
|
1651
|
+
choices: [
|
|
1652
|
+
{
|
|
1653
|
+
name: "Switch GitHub account (gh auth login \u2014 m\u1EDF browser)",
|
|
1654
|
+
value: "switch"
|
|
1655
|
+
},
|
|
1656
|
+
{
|
|
1657
|
+
name: "T\xF4i v\u1EEBa fix (accept invite / s\u1EEDa URL) \u2014 retry verify",
|
|
1658
|
+
value: "retry"
|
|
1659
|
+
},
|
|
1660
|
+
{
|
|
1661
|
+
name: "T\u1EA1m ng\u01B0ng init \u2014 ch\u1EA1y l\u1EA1i 'avatar init' sau",
|
|
1662
|
+
value: "abort"
|
|
1663
|
+
}
|
|
1664
|
+
]
|
|
1665
|
+
});
|
|
1666
|
+
if (action === "abort") {
|
|
1667
|
+
throw new RemoteAccessAbortedError(
|
|
1668
|
+
`User ch\u1ECDn t\u1EA1m ng\u01B0ng. Fix access ${args.url} r\u1ED3i ch\u1EA1y l\u1EA1i 'avatar init'.`
|
|
1669
|
+
);
|
|
1670
|
+
}
|
|
1671
|
+
if (action === "switch") {
|
|
1672
|
+
triggerGhAuthLoginInteractive();
|
|
1673
|
+
}
|
|
1674
|
+
log.info("Verify remote l\u1EA1i...");
|
|
1675
|
+
const result = tryVerifyGitRemoteAccessible(args.url);
|
|
1676
|
+
if (result.ok) {
|
|
1677
|
+
log.success(`Remote accessible: ${args.url}`);
|
|
1678
|
+
return;
|
|
1679
|
+
}
|
|
1680
|
+
reason = result.reason ?? "unknown";
|
|
1681
|
+
detail = result.detail;
|
|
1682
|
+
}
|
|
1683
|
+
}
|
|
1684
|
+
|
|
1379
1685
|
// src/lib/install-gh-cli-via-package-manager.ts
|
|
1380
|
-
import { spawnSync as
|
|
1686
|
+
import { spawnSync as spawnSync13 } from "child_process";
|
|
1381
1687
|
var INSTALL_COMMANDS = {
|
|
1382
1688
|
brew: { cmd: "brew", args: ["install", "gh"] },
|
|
1383
1689
|
apt: { cmd: "sudo", args: ["apt-get", "install", "-y", "gh"] },
|
|
@@ -1388,7 +1694,7 @@ var INSTALL_COMMANDS = {
|
|
|
1388
1694
|
function installGhCliViaPackageManager(pm) {
|
|
1389
1695
|
const spec = INSTALL_COMMANDS[pm];
|
|
1390
1696
|
log.info(`\u0110ang c\xE0i gh CLI qua ${pm}...`);
|
|
1391
|
-
const r =
|
|
1697
|
+
const r = spawnSync13(spec.cmd, spec.args, { stdio: "inherit" });
|
|
1392
1698
|
if (r.status !== 0) {
|
|
1393
1699
|
throw new Error(`C\xE0i gh CLI th\u1EA5t b\u1EA1i qua ${pm} (exit ${r.status}). C\xE0i tay r\u1ED3i ch\u1EA1y l\u1EA1i.`);
|
|
1394
1700
|
}
|
|
@@ -1396,9 +1702,9 @@ function installGhCliViaPackageManager(pm) {
|
|
|
1396
1702
|
}
|
|
1397
1703
|
|
|
1398
1704
|
// src/lib/setup-git-credential-via-gh.ts
|
|
1399
|
-
import { spawnSync as
|
|
1705
|
+
import { spawnSync as spawnSync14 } from "child_process";
|
|
1400
1706
|
function setupGitCredentialViaGh() {
|
|
1401
|
-
const r =
|
|
1707
|
+
const r = spawnSync14("gh", ["auth", "setup-git"], { stdio: "ignore" });
|
|
1402
1708
|
if (r.status !== 0) {
|
|
1403
1709
|
log.warn("gh auth setup-git fail (non-fatal). N\u1EBFu git clone l\u1ED7i 128 \u2192 ch\u1EA1y th\u1EE7 c\xF4ng.");
|
|
1404
1710
|
return;
|
|
@@ -1407,10 +1713,10 @@ function setupGitCredentialViaGh() {
|
|
|
1407
1713
|
}
|
|
1408
1714
|
|
|
1409
1715
|
// src/lib/trigger-gh-cli-auth-login.ts
|
|
1410
|
-
import { spawnSync as
|
|
1716
|
+
import { spawnSync as spawnSync15 } from "child_process";
|
|
1411
1717
|
function triggerGhCliAuthLogin() {
|
|
1412
1718
|
log.info("Kh\u1EDFi \u0111\u1ED9ng \u0111\u0103ng nh\u1EADp GitHub qua gh CLI (browser s\u1EBD m\u1EDF)...");
|
|
1413
|
-
const r =
|
|
1719
|
+
const r = spawnSync15(
|
|
1414
1720
|
"gh",
|
|
1415
1721
|
["auth", "login", "--hostname", "github.com", "--web", "--git-protocol", "ssh"],
|
|
1416
1722
|
{ stdio: "inherit" }
|
|
@@ -1421,65 +1727,91 @@ function triggerGhCliAuthLogin() {
|
|
|
1421
1727
|
log.success("\u0110\xE3 \u0111\u0103ng nh\u1EADp GitHub");
|
|
1422
1728
|
}
|
|
1423
1729
|
|
|
1424
|
-
// src/lib/verify-git-remote-accessible.ts
|
|
1425
|
-
import { spawnSync as spawnSync13 } from "child_process";
|
|
1426
|
-
var TIMEOUT_MS = 5e3;
|
|
1427
|
-
var RemoteNotAccessibleError = class extends Error {
|
|
1428
|
-
constructor(url, reason) {
|
|
1429
|
-
super(`Kh\xF4ng truy c\u1EADp \u0111\u01B0\u1EE3c remote ${url}: ${reason}`);
|
|
1430
|
-
this.name = "RemoteNotAccessibleError";
|
|
1431
|
-
}
|
|
1432
|
-
};
|
|
1433
|
-
function verifyGitRemoteAccessible(url) {
|
|
1434
|
-
const r = spawnSync13("git", ["ls-remote", "--exit-code", url, "HEAD"], {
|
|
1435
|
-
stdio: "ignore",
|
|
1436
|
-
timeout: TIMEOUT_MS
|
|
1437
|
-
});
|
|
1438
|
-
if (r.status === 0) return;
|
|
1439
|
-
if (r.signal === "SIGTERM") throw new RemoteNotAccessibleError(url, "timeout 5s");
|
|
1440
|
-
throw new RemoteNotAccessibleError(url, `git ls-remote exit ${r.status}`);
|
|
1441
|
-
}
|
|
1442
|
-
|
|
1443
1730
|
// src/lib/git-auth-and-install-orchestrator.ts
|
|
1444
1731
|
async function ensureGitHubReady(remoteUrl) {
|
|
1445
|
-
|
|
1446
|
-
if (state === "not-installed") {
|
|
1732
|
+
while (checkGhCliAuthStatus() === "not-installed") {
|
|
1447
1733
|
log.warn("gh CLI ch\u01B0a c\xE0i. Avatar s\u1EBD t\u1EF1 c\xE0i.");
|
|
1448
1734
|
const pm = detectPackageManager();
|
|
1449
1735
|
if (!pm) {
|
|
1450
|
-
|
|
1451
|
-
"
|
|
1452
|
-
|
|
1736
|
+
const action = await promptRetryOrSkip({
|
|
1737
|
+
taskName: "Ph\xE1t hi\u1EC7n package manager",
|
|
1738
|
+
reason: "Kh\xF4ng t\xECm th\u1EA5y brew/apt/dnf/pacman/winget tr\xEAn m\xE1y.",
|
|
1739
|
+
allowSkip: false,
|
|
1740
|
+
hint: "C\xE0i gh CLI tay (https://cli.github.com) r\u1ED3i ch\u1ECDn Retry."
|
|
1741
|
+
});
|
|
1742
|
+
if (action === "abort") {
|
|
1743
|
+
throw new UserAbortedRecoveryError("User abort t\u1EA1i b\u01B0\u1EDBc c\xE0i gh CLI.");
|
|
1744
|
+
}
|
|
1745
|
+
continue;
|
|
1746
|
+
}
|
|
1747
|
+
try {
|
|
1748
|
+
installGhCliViaPackageManager(pm);
|
|
1749
|
+
} catch (err) {
|
|
1750
|
+
const action = await promptRetryOrSkip({
|
|
1751
|
+
taskName: `C\xE0i gh CLI qua ${pm}`,
|
|
1752
|
+
reason: err.message,
|
|
1753
|
+
allowSkip: false,
|
|
1754
|
+
hint: "C\xE0i tay (https://cli.github.com) r\u1ED3i ch\u1ECDn Retry, ho\u1EB7c Abort."
|
|
1755
|
+
});
|
|
1756
|
+
if (action === "abort") {
|
|
1757
|
+
throw new UserAbortedRecoveryError("User abort t\u1EA1i b\u01B0\u1EDBc c\xE0i gh CLI.");
|
|
1758
|
+
}
|
|
1453
1759
|
}
|
|
1454
|
-
installGhCliViaPackageManager(pm);
|
|
1455
|
-
state = checkGhCliAuthStatus();
|
|
1456
1760
|
}
|
|
1457
|
-
|
|
1761
|
+
while (checkGhCliAuthStatus() === "not-authenticated") {
|
|
1458
1762
|
log.warn("Ch\u01B0a \u0111\u0103ng nh\u1EADp GitHub.");
|
|
1459
|
-
|
|
1460
|
-
|
|
1461
|
-
|
|
1462
|
-
|
|
1763
|
+
try {
|
|
1764
|
+
triggerGhCliAuthLogin();
|
|
1765
|
+
} catch (err) {
|
|
1766
|
+
const action = await promptRetryOrSkip({
|
|
1767
|
+
taskName: "\u0110\u0103ng nh\u1EADp GitHub qua gh",
|
|
1768
|
+
reason: err.message,
|
|
1769
|
+
allowSkip: false,
|
|
1770
|
+
hint: "Th\u1EED l\u1EA1i \u2014 ch\u1ECDn c\xE1ch login kh\xE1c (browser vs token) khi gh prompt."
|
|
1771
|
+
});
|
|
1772
|
+
if (action === "abort") {
|
|
1773
|
+
throw new UserAbortedRecoveryError("User abort t\u1EA1i b\u01B0\u1EDBc gh auth login.");
|
|
1774
|
+
}
|
|
1775
|
+
continue;
|
|
1776
|
+
}
|
|
1777
|
+
if (checkGhCliAuthStatus() !== "authenticated") {
|
|
1778
|
+
const action = await promptRetryOrSkip({
|
|
1779
|
+
taskName: "Verify gh auth",
|
|
1780
|
+
reason: "Sau gh auth login v\u1EABn b\xE1o not-authenticated.",
|
|
1781
|
+
allowSkip: false,
|
|
1782
|
+
hint: "C\xF3 th\u1EC3 user cancel browser. Th\u1EED l\u1EA1i ho\u1EB7c abort."
|
|
1783
|
+
});
|
|
1784
|
+
if (action === "abort") {
|
|
1785
|
+
throw new UserAbortedRecoveryError("User abort t\u1EA1i b\u01B0\u1EDBc verify gh auth.");
|
|
1786
|
+
}
|
|
1463
1787
|
}
|
|
1464
1788
|
}
|
|
1465
1789
|
log.success("gh CLI s\u1EB5n s\xE0ng");
|
|
1466
1790
|
setupGitCredentialViaGh();
|
|
1467
1791
|
if (remoteUrl) {
|
|
1468
|
-
|
|
1469
|
-
|
|
1792
|
+
const result = tryVerifyGitRemoteAccessible(remoteUrl);
|
|
1793
|
+
if (result.ok) {
|
|
1794
|
+
log.success(`Remote accessible: ${remoteUrl}`);
|
|
1795
|
+
} else {
|
|
1796
|
+
await handleRemoteAccessFailureWithAccountSwitch({
|
|
1797
|
+
url: remoteUrl,
|
|
1798
|
+
initialReason: result.reason ?? "unknown",
|
|
1799
|
+
initialDetail: result.detail
|
|
1800
|
+
});
|
|
1801
|
+
}
|
|
1470
1802
|
}
|
|
1471
1803
|
}
|
|
1472
1804
|
|
|
1473
1805
|
// src/lib/create-workspace-remote-via-gh.ts
|
|
1474
1806
|
function canCreateInNamespace(org, ghUser) {
|
|
1475
1807
|
if (org.toLowerCase() === ghUser.toLowerCase()) return { ok: true };
|
|
1476
|
-
const r =
|
|
1808
|
+
const r = spawnSync16("gh", ["api", `orgs/${org}/members/${ghUser}`, "--silent"], {
|
|
1477
1809
|
stdio: "ignore"
|
|
1478
1810
|
});
|
|
1479
1811
|
if (r.status === 0) return { ok: true };
|
|
1480
|
-
const orgCheck =
|
|
1812
|
+
const orgCheck = spawnSync16("gh", ["api", `orgs/${org}`, "--silent"], { stdio: "ignore" });
|
|
1481
1813
|
if (orgCheck.status !== 0) {
|
|
1482
|
-
const userCheck =
|
|
1814
|
+
const userCheck = spawnSync16("gh", ["api", `users/${org}`, "--silent"], { stdio: "ignore" });
|
|
1483
1815
|
if (userCheck.status === 0) {
|
|
1484
1816
|
return {
|
|
1485
1817
|
ok: false,
|
|
@@ -1496,27 +1828,27 @@ function canCreateInNamespace(org, ghUser) {
|
|
|
1496
1828
|
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.`
|
|
1497
1829
|
};
|
|
1498
1830
|
}
|
|
1499
|
-
async function createWorkspaceRemoteViaGh(
|
|
1500
|
-
validateRepoName(
|
|
1501
|
-
validateRepoVisibility(
|
|
1831
|
+
async function createWorkspaceRemoteViaGh(input4) {
|
|
1832
|
+
validateRepoName(input4.workspaceName);
|
|
1833
|
+
validateRepoVisibility(input4.visibility);
|
|
1502
1834
|
await ensureGitHubReady();
|
|
1503
1835
|
const ghUser = resolveGithubUsernameDefault();
|
|
1504
|
-
const org =
|
|
1836
|
+
const org = input4.org ?? ghUser;
|
|
1505
1837
|
const namespaceCheck = canCreateInNamespace(org, ghUser);
|
|
1506
1838
|
if (!namespaceCheck.ok) {
|
|
1507
1839
|
throw new Error(`Kh\xF4ng th\u1EC3 t\u1EA1o repo d\u01B0\u1EDBi '${org}/': ${namespaceCheck.reason}`);
|
|
1508
1840
|
}
|
|
1509
|
-
const fullName = `${org}/${
|
|
1510
|
-
log.info(`T\u1EA1o GitHub repo cho workspace: ${fullName} (${
|
|
1511
|
-
const r =
|
|
1841
|
+
const fullName = `${org}/${input4.workspaceName}`;
|
|
1842
|
+
log.info(`T\u1EA1o GitHub repo cho workspace: ${fullName} (${input4.visibility})...`);
|
|
1843
|
+
const r = spawnSync16(
|
|
1512
1844
|
"gh",
|
|
1513
1845
|
[
|
|
1514
1846
|
"repo",
|
|
1515
1847
|
"create",
|
|
1516
1848
|
fullName,
|
|
1517
|
-
`--${
|
|
1849
|
+
`--${input4.visibility}`,
|
|
1518
1850
|
"--source",
|
|
1519
|
-
|
|
1851
|
+
input4.workspacePath,
|
|
1520
1852
|
"--remote",
|
|
1521
1853
|
"origin",
|
|
1522
1854
|
"--push"
|
|
@@ -1528,7 +1860,7 @@ async function createWorkspaceRemoteViaGh(input3) {
|
|
|
1528
1860
|
if (stderr) process.stderr.write(`${stderr}
|
|
1529
1861
|
`);
|
|
1530
1862
|
throw new Error(
|
|
1531
|
-
`T\u1EA1o workspace remote th\u1EA5t b\u1EA1i (exit ${r.status}). Workspace v\u1EABn d\xF9ng \u0111\u01B0\u1EE3c local. Setup remote sau qua: gh repo create ${fullName} --${
|
|
1863
|
+
`T\u1EA1o workspace remote th\u1EA5t b\u1EA1i (exit ${r.status}). Workspace v\u1EABn d\xF9ng \u0111\u01B0\u1EE3c local. Setup remote sau qua: gh repo create ${fullName} --${input4.visibility} --source=. --remote=origin --push`
|
|
1532
1864
|
);
|
|
1533
1865
|
}
|
|
1534
1866
|
const sshUrl = `git@github.com:${fullName}.git`;
|
|
@@ -1539,14 +1871,14 @@ async function createWorkspaceRemoteViaGh(input3) {
|
|
|
1539
1871
|
|
|
1540
1872
|
// src/lib/safe-bootstrap-for-dirty-folder.ts
|
|
1541
1873
|
import { readdirSync } from "fs";
|
|
1542
|
-
import { select as
|
|
1874
|
+
import { select as select6 } from "@inquirer/prompts";
|
|
1543
1875
|
import { simpleGit as simpleGit3 } from "simple-git";
|
|
1544
1876
|
|
|
1545
1877
|
// src/lib/check-folder-has-git.ts
|
|
1546
1878
|
import { existsSync as existsSync2, statSync } from "fs";
|
|
1547
|
-
import { join as
|
|
1879
|
+
import { join as join11 } from "path";
|
|
1548
1880
|
function checkFolderHasGit(folderPath) {
|
|
1549
|
-
const gitPath =
|
|
1881
|
+
const gitPath = join11(folderPath, ".git");
|
|
1550
1882
|
if (!existsSync2(gitPath)) return false;
|
|
1551
1883
|
const stat = statSync(gitPath);
|
|
1552
1884
|
return stat.isDirectory() || stat.isFile();
|
|
@@ -1578,7 +1910,7 @@ async function createInitialGitCommit(folderPath) {
|
|
|
1578
1910
|
|
|
1579
1911
|
// src/lib/detect-folder-tech-stack.ts
|
|
1580
1912
|
import { existsSync as existsSync3 } from "fs";
|
|
1581
|
-
import { join as
|
|
1913
|
+
import { join as join12 } from "path";
|
|
1582
1914
|
var SIGNATURES = {
|
|
1583
1915
|
node: ["package.json"],
|
|
1584
1916
|
python: ["pyproject.toml", "requirements.txt", "setup.py", "Pipfile"],
|
|
@@ -1590,7 +1922,7 @@ var SIGNATURES = {
|
|
|
1590
1922
|
function detectFolderTechStack(folderPath) {
|
|
1591
1923
|
const matched = [];
|
|
1592
1924
|
for (const [stack, files] of Object.entries(SIGNATURES)) {
|
|
1593
|
-
if (files.some((f) => existsSync3(
|
|
1925
|
+
if (files.some((f) => existsSync3(join12(folderPath, f)))) {
|
|
1594
1926
|
matched.push(stack);
|
|
1595
1927
|
}
|
|
1596
1928
|
}
|
|
@@ -1599,19 +1931,19 @@ function detectFolderTechStack(folderPath) {
|
|
|
1599
1931
|
|
|
1600
1932
|
// src/lib/gitignore-template-loader.ts
|
|
1601
1933
|
import { readFileSync as readFileSync2 } from "fs";
|
|
1602
|
-
import { dirname as dirname3, join as
|
|
1934
|
+
import { dirname as dirname3, join as join13 } from "path";
|
|
1603
1935
|
import { fileURLToPath as fileURLToPath2 } from "url";
|
|
1604
1936
|
var __dirname = dirname3(fileURLToPath2(import.meta.url));
|
|
1605
1937
|
var CANDIDATE_DIRS = [
|
|
1606
|
-
|
|
1607
|
-
|
|
1938
|
+
join13(__dirname, "..", "templates", "gitignore"),
|
|
1939
|
+
join13(__dirname, "..", "..", "src", "templates", "gitignore")
|
|
1608
1940
|
];
|
|
1609
1941
|
var AVATAR_MARKER_START = "# === avatar ===";
|
|
1610
1942
|
var AVATAR_MARKER_END = "# === /avatar ===";
|
|
1611
1943
|
function readTemplate(stack) {
|
|
1612
1944
|
for (const dir of CANDIDATE_DIRS) {
|
|
1613
1945
|
try {
|
|
1614
|
-
return readFileSync2(
|
|
1946
|
+
return readFileSync2(join13(dir, `${stack}.txt`), "utf8");
|
|
1615
1947
|
} catch {
|
|
1616
1948
|
}
|
|
1617
1949
|
}
|
|
@@ -1626,9 +1958,9 @@ ${readTemplate(s).trim()}`);
|
|
|
1626
1958
|
|
|
1627
1959
|
// src/lib/write-or-merge-gitignore.ts
|
|
1628
1960
|
import { existsSync as existsSync4, readFileSync as readFileSync3, writeFileSync } from "fs";
|
|
1629
|
-
import { join as
|
|
1961
|
+
import { join as join14 } from "path";
|
|
1630
1962
|
function writeOrMergeGitignore(folderPath, avatarBlock) {
|
|
1631
|
-
const path =
|
|
1963
|
+
const path = join14(folderPath, ".gitignore");
|
|
1632
1964
|
if (!existsSync4(path)) {
|
|
1633
1965
|
writeFileSync(path, avatarBlock, "utf8");
|
|
1634
1966
|
return;
|
|
@@ -1670,7 +2002,7 @@ async function promptBootstrapStrategy(state, opts) {
|
|
|
1670
2002
|
if (opts.presetStrategy) return opts.presetStrategy;
|
|
1671
2003
|
if (opts.autoYes) return "stash";
|
|
1672
2004
|
if (state === "empty" || state === "clean") return "commit-all";
|
|
1673
|
-
return await
|
|
2005
|
+
return await select6({
|
|
1674
2006
|
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:",
|
|
1675
2007
|
choices: [
|
|
1676
2008
|
{
|
|
@@ -1803,145 +2135,6 @@ async function safeBootstrapGitInFolder(folderPath, opts = {}) {
|
|
|
1803
2135
|
await appendAuditEntry("bootstrap", `state=${state},strategy=${strategy}`);
|
|
1804
2136
|
}
|
|
1805
2137
|
|
|
1806
|
-
// src/lib/team-pack-submodule-manager.ts
|
|
1807
|
-
import { join as join14 } from "path";
|
|
1808
|
-
|
|
1809
|
-
// src/lib/check-team-pack-access-with-retry-loop.ts
|
|
1810
|
-
import { spawnSync as spawnSync15 } from "child_process";
|
|
1811
|
-
import { confirm as confirm2, select as select4 } from "@inquirer/prompts";
|
|
1812
|
-
function parseRepoSlugFromGitUrl(url) {
|
|
1813
|
-
const httpsMatch = url.match(/github\.com[/:]([^/]+\/[^/]+?)(?:\.git)?$/);
|
|
1814
|
-
if (httpsMatch) return httpsMatch[1];
|
|
1815
|
-
return null;
|
|
1816
|
-
}
|
|
1817
|
-
function checkRepoAccess(repoSlug) {
|
|
1818
|
-
const r = spawnSync15("gh", ["api", `repos/${repoSlug}`], { stdio: "ignore" });
|
|
1819
|
-
return r.status === 0;
|
|
1820
|
-
}
|
|
1821
|
-
function getCurrentGhUser() {
|
|
1822
|
-
const r = spawnSync15("gh", ["api", "user", "--jq", ".login"], {
|
|
1823
|
-
encoding: "utf8",
|
|
1824
|
-
stdio: ["ignore", "pipe", "pipe"]
|
|
1825
|
-
});
|
|
1826
|
-
if (r.status !== 0) return null;
|
|
1827
|
-
return r.stdout.trim() || null;
|
|
1828
|
-
}
|
|
1829
|
-
async function copyInfoToClipboardWithConsent(info) {
|
|
1830
|
-
const ok = await confirm2({
|
|
1831
|
-
message: "Copy th\xF4ng tin (GitHub username + email) v\xE0o clipboard \u0111\u1EC3 d\xE1n v\xE0o Slack/email?",
|
|
1832
|
-
default: true
|
|
1833
|
-
});
|
|
1834
|
-
if (!ok) return;
|
|
1835
|
-
try {
|
|
1836
|
-
const { default: clipboardy } = await import("clipboardy");
|
|
1837
|
-
await clipboardy.write(info);
|
|
1838
|
-
log.success("\u0110\xE3 copy v\xE0o clipboard");
|
|
1839
|
-
} catch (err) {
|
|
1840
|
-
log.dim(`Copy clipboard fail: ${err.message}`);
|
|
1841
|
-
}
|
|
1842
|
-
}
|
|
1843
|
-
function buildAccessRequestInfo(ghUser, ssoEmail) {
|
|
1844
|
-
const lines = [
|
|
1845
|
-
"Request access team-ai-pack (NAL)",
|
|
1846
|
-
"",
|
|
1847
|
-
`GitHub username: ${ghUser ?? "(ch\u01B0a gh auth \u2014 ch\u1EA1y: gh auth login)"}`,
|
|
1848
|
-
`NAL email: ${ssoEmail ?? "(ch\u01B0a avatar login \u2014 ch\u1EA1y: avatar login)"}`,
|
|
1849
|
-
`Date: ${(/* @__PURE__ */ new Date()).toISOString().split("T")[0]}`
|
|
1850
|
-
];
|
|
1851
|
-
return lines.join("\n");
|
|
1852
|
-
}
|
|
1853
|
-
async function ensureTeamPackAccessWithRetry(args) {
|
|
1854
|
-
if (checkRepoAccess(args.repoSlug)) return true;
|
|
1855
|
-
const ghUser = getCurrentGhUser();
|
|
1856
|
-
const info = buildAccessRequestInfo(ghUser, args.ssoEmail ?? null);
|
|
1857
|
-
log.warn(`B\u1EA1n ch\u01B0a c\xF3 quy\u1EC1n access v\xE0o b\u1ED9 package ${args.repoSlug}.`);
|
|
1858
|
-
log.info("Li\xEAn h\u1EC7 admin (Luke @nal.vn) \u0111\u1EC3 \u0111\u01B0\u1EE3c add v\xE0o org nalvn.");
|
|
1859
|
-
log.plain("");
|
|
1860
|
-
log.plain(info);
|
|
1861
|
-
log.plain("");
|
|
1862
|
-
await copyInfoToClipboardWithConsent(info);
|
|
1863
|
-
while (true) {
|
|
1864
|
-
const action = await select4({
|
|
1865
|
-
message: "Ti\u1EBFp t\u1EE5c?",
|
|
1866
|
-
choices: [
|
|
1867
|
-
{ name: "\u0110\xE3 \u0111\u01B0\u1EE3c add \u2014 ki\u1EC3m tra l\u1EA1i v\xE0 ti\u1EBFp t\u1EE5c", value: "retry" },
|
|
1868
|
-
{ name: "T\u1EA1m ng\u01B0ng \u2014 ch\u1EA1y l\u1EA1i 'avatar init' sau", value: "abort" }
|
|
1869
|
-
]
|
|
1870
|
-
});
|
|
1871
|
-
if (action === "abort") {
|
|
1872
|
-
log.dim("T\u1EA1m ng\u01B0ng. Ch\u1EA1y l\u1EA1i 'avatar init' sau khi \u0111\xE3 accept invite t\u1EEB GitHub.");
|
|
1873
|
-
return false;
|
|
1874
|
-
}
|
|
1875
|
-
log.info("Ki\u1EC3m tra access...");
|
|
1876
|
-
if (checkRepoAccess(args.repoSlug)) {
|
|
1877
|
-
log.success("\u0110\xE3 c\xF3 access \u2014 ti\u1EBFp t\u1EE5c.");
|
|
1878
|
-
return true;
|
|
1879
|
-
}
|
|
1880
|
-
log.warn("V\u1EABn ch\u01B0a c\xF3 access. \u0110\u1EA3m b\u1EA3o b\u1EA1n \u0111\xE3 accept email invite t\u1EEB GitHub (Inbox + Spam).");
|
|
1881
|
-
}
|
|
1882
|
-
}
|
|
1883
|
-
|
|
1884
|
-
// src/lib/resolve-team-pack-repo-url.ts
|
|
1885
|
-
var ORG_DEFAULT = "https://github.com/nalvn/team-ai-pack.git";
|
|
1886
|
-
function resolveTeamPackRepoUrl() {
|
|
1887
|
-
if (process.env.AVATAR_TEAM_PACK_REPO_URL) {
|
|
1888
|
-
return process.env.AVATAR_TEAM_PACK_REPO_URL;
|
|
1889
|
-
}
|
|
1890
|
-
return ORG_DEFAULT;
|
|
1891
|
-
}
|
|
1892
|
-
|
|
1893
|
-
// src/lib/team-pack-submodule-manager.ts
|
|
1894
|
-
var TEAM_PACK_REPO_URL = resolveTeamPackRepoUrl();
|
|
1895
|
-
var TEAM_PACK_RELATIVE_PATH = ".claude/pack";
|
|
1896
|
-
var TeamPackAccessAbortedError = class extends Error {
|
|
1897
|
-
constructor(message) {
|
|
1898
|
-
super(message);
|
|
1899
|
-
this.name = "TeamPackAccessAbortedError";
|
|
1900
|
-
}
|
|
1901
|
-
};
|
|
1902
|
-
async function addTeamPackSubmodule(projectRoot, tag, ssoEmail) {
|
|
1903
|
-
const url = resolveTeamPackRepoUrl();
|
|
1904
|
-
const repoSlug = parseRepoSlugFromGitUrl(url);
|
|
1905
|
-
if (repoSlug) {
|
|
1906
|
-
const hasAccess = await ensureTeamPackAccessWithRetry({ repoSlug, ssoEmail });
|
|
1907
|
-
if (!hasAccess) {
|
|
1908
|
-
throw new TeamPackAccessAbortedError(
|
|
1909
|
-
"User ch\u1ECDn t\u1EA1m ng\u01B0ng. Ch\u1EA1y l\u1EA1i 'avatar init' sau khi \u0111\u01B0\u1EE3c add v\xE0o org."
|
|
1910
|
-
);
|
|
1911
|
-
}
|
|
1912
|
-
}
|
|
1913
|
-
try {
|
|
1914
|
-
await addSubmodule(url, TEAM_PACK_RELATIVE_PATH, projectRoot);
|
|
1915
|
-
} catch (err) {
|
|
1916
|
-
const msg = err instanceof Error ? err.message : String(err);
|
|
1917
|
-
if (msg.includes("Repository not found") || msg.includes("not found")) {
|
|
1918
|
-
log.error(
|
|
1919
|
-
`Repo team-ai-pack kh\xF4ng t\u1ED3n t\u1EA1i: ${url}
|
|
1920
|
-
C\xE1ch fix:
|
|
1921
|
-
1. T\u1EA1o repo: gh repo create <owner>/team-ai-pack --private --add-readme
|
|
1922
|
-
2. Ho\u1EB7c override URL: export AVATAR_TEAM_PACK_REPO_URL=<url-repo-c\u1EE7a-b\u1EA1n>
|
|
1923
|
-
3. Ho\u1EB7c d\xF9ng flag --skip-team-pack`
|
|
1924
|
-
);
|
|
1925
|
-
}
|
|
1926
|
-
throw err;
|
|
1927
|
-
}
|
|
1928
|
-
let target = tag ?? null;
|
|
1929
|
-
if (!target) {
|
|
1930
|
-
target = await latestTag(join14(projectRoot, TEAM_PACK_RELATIVE_PATH));
|
|
1931
|
-
}
|
|
1932
|
-
if (target) {
|
|
1933
|
-
await checkoutTagInSubmodule(TEAM_PACK_RELATIVE_PATH, target, projectRoot);
|
|
1934
|
-
}
|
|
1935
|
-
return { pinnedTag: target };
|
|
1936
|
-
}
|
|
1937
|
-
async function readPinnedPackVersion(projectRoot) {
|
|
1938
|
-
const submoduleRoot = join14(projectRoot, TEAM_PACK_RELATIVE_PATH);
|
|
1939
|
-
const tag = await latestTag(submoduleRoot);
|
|
1940
|
-
if (tag) return tag;
|
|
1941
|
-
const sha = await currentCommitSha(submoduleRoot);
|
|
1942
|
-
return sha.slice(0, 7);
|
|
1943
|
-
}
|
|
1944
|
-
|
|
1945
2138
|
// src/commands/init-conflict-detection-helpers.ts
|
|
1946
2139
|
import { readdir } from "fs/promises";
|
|
1947
2140
|
import { join as join15 } from "path";
|
|
@@ -2200,6 +2393,14 @@ function registerInitCommand(program2) {
|
|
|
2200
2393
|
log.dim(err.message);
|
|
2201
2394
|
process.exit(0);
|
|
2202
2395
|
}
|
|
2396
|
+
if (err instanceof RemoteAccessAbortedError) {
|
|
2397
|
+
log.dim(err.message);
|
|
2398
|
+
process.exit(0);
|
|
2399
|
+
}
|
|
2400
|
+
if (err instanceof UserAbortedRecoveryError) {
|
|
2401
|
+
log.dim(err.message);
|
|
2402
|
+
process.exit(0);
|
|
2403
|
+
}
|
|
2203
2404
|
log.error(err instanceof Error ? err.message : String(err));
|
|
2204
2405
|
process.exit(1);
|
|
2205
2406
|
}
|
|
@@ -2211,13 +2412,26 @@ async function runInit(opts) {
|
|
|
2211
2412
|
log.warn("Flag --mode \u0111\xE3 deprecated t\u1EEB v1.1. D\xF9ng --project-status thay th\u1EBF.");
|
|
2212
2413
|
}
|
|
2213
2414
|
let userConfig = await readUserConfig();
|
|
2214
|
-
|
|
2415
|
+
while (!userConfig || isTokenExpired(userConfig)) {
|
|
2215
2416
|
log.info("Ch\u01B0a \u0111\u0103ng nh\u1EADp \u2014 ch\u1EA1y login flow tr\u01B0\u1EDBc khi init...");
|
|
2216
|
-
|
|
2417
|
+
try {
|
|
2418
|
+
await runLogin({});
|
|
2419
|
+
} catch (err) {
|
|
2420
|
+
log.warn(`Login fail: ${err.message}`);
|
|
2421
|
+
}
|
|
2217
2422
|
userConfig = await readUserConfig();
|
|
2218
|
-
if (
|
|
2219
|
-
|
|
2220
|
-
|
|
2423
|
+
if (userConfig && !isTokenExpired(userConfig)) break;
|
|
2424
|
+
const action = await promptRetryOrSkip({
|
|
2425
|
+
taskName: "\u0110\u0103ng nh\u1EADp SSO Google",
|
|
2426
|
+
reason: "Token ch\u01B0a \u0111\u01B0\u1EE3c t\u1EA1o ho\u1EB7c \u0111\xE3 h\u1EBFt h\u1EA1n.",
|
|
2427
|
+
allowSkip: false,
|
|
2428
|
+
// Login bắt buộc, không skip được.
|
|
2429
|
+
hint: "\u0110\u1EA3m b\u1EA3o b\u1EA1n ch\u1ECDn 'Allow' tr\xEAn browser v\xE0 d\xF9ng email @nal.vn."
|
|
2430
|
+
});
|
|
2431
|
+
if (action === "abort") {
|
|
2432
|
+
throw new UserAbortedRecoveryError(
|
|
2433
|
+
"User abort t\u1EA1i b\u01B0\u1EDBc login. Ch\u1EA1y 'avatar login' tay r\u1ED3i 'avatar init' l\u1EA1i."
|
|
2434
|
+
);
|
|
2221
2435
|
}
|
|
2222
2436
|
}
|
|
2223
2437
|
const status = opts.projectStatus ?? await promptProjectStatus();
|
|
@@ -2234,7 +2448,7 @@ async function runInit(opts) {
|
|
|
2234
2448
|
}
|
|
2235
2449
|
}
|
|
2236
2450
|
async function promptProjectStatus() {
|
|
2237
|
-
return await
|
|
2451
|
+
return await select7({
|
|
2238
2452
|
message: "T\xECnh tr\u1EA1ng d\u1EF1 \xE1n c\u1EE7a b\u1EA1n?",
|
|
2239
2453
|
choices: [
|
|
2240
2454
|
{ name: "1. \u0110\xE3 c\xF3 repo git remote (URL c\xF3 s\u1EB5n)", value: "existing-remote" },
|
|
@@ -2244,14 +2458,14 @@ async function promptProjectStatus() {
|
|
|
2244
2458
|
});
|
|
2245
2459
|
}
|
|
2246
2460
|
async function runInitFromExistingRemote(opts, ownerEmail) {
|
|
2247
|
-
const remoteUrl = opts.clientRepo ?? await
|
|
2461
|
+
const remoteUrl = opts.clientRepo ?? await input3({
|
|
2248
2462
|
message: "URL git c\u1EE7a repo:",
|
|
2249
2463
|
validate: (v) => v.length > 0 ? true : "URL b\u1EAFt bu\u1ED9c"
|
|
2250
2464
|
});
|
|
2251
2465
|
await ensureGitHubReady(remoteUrl);
|
|
2252
2466
|
const teamOwner = opts.teamOwner ?? await promptTeamOwner(ownerEmail);
|
|
2253
2467
|
const inferredName = inferWorkspaceName(remoteUrl);
|
|
2254
|
-
const workspaceName = opts.workspaceName ?? await
|
|
2468
|
+
const workspaceName = opts.workspaceName ?? await input3({ message: "T\xEAn workspace:", default: inferredName });
|
|
2255
2469
|
const workspaceParent = resolve(opts.workspaceParent ?? ".");
|
|
2256
2470
|
const workspacePath = await resolveWorkspacePath(workspaceParent, workspaceName, opts.force);
|
|
2257
2471
|
await scaffoldWorkspaceWithSrcSubmodule({
|
|
@@ -2273,7 +2487,7 @@ async function runInitFromExistingRemote(opts, ownerEmail) {
|
|
|
2273
2487
|
}
|
|
2274
2488
|
async function runInitFromExistingFolder(opts, ownerEmail) {
|
|
2275
2489
|
const folderPath = resolve(
|
|
2276
|
-
opts.folderPath ?? await
|
|
2490
|
+
opts.folderPath ?? await input3({
|
|
2277
2491
|
message: "\u0110\u01B0\u1EDDng d\u1EABn folder hi\u1EC7n c\xF3:",
|
|
2278
2492
|
validate: (v) => v.length > 0 ? true : "Path b\u1EAFt bu\u1ED9c"
|
|
2279
2493
|
})
|
|
@@ -2285,7 +2499,7 @@ async function runInitFromExistingFolder(opts, ownerEmail) {
|
|
|
2285
2499
|
const remoteUrl = await getOrCreateOriginRemote(folderPath, opts);
|
|
2286
2500
|
const teamOwner = opts.teamOwner ?? await promptTeamOwner(ownerEmail);
|
|
2287
2501
|
const inferredName = opts.workspaceName ?? `${basename(folderPath)}-avatar-workspace`;
|
|
2288
|
-
const workspaceName = opts.workspaceName ?? await
|
|
2502
|
+
const workspaceName = opts.workspaceName ?? await input3({ message: "T\xEAn workspace:", default: inferredName });
|
|
2289
2503
|
const workspaceParent = resolve(opts.workspaceParent ?? ".");
|
|
2290
2504
|
const workspacePath = await resolveWorkspacePath(workspaceParent, workspaceName, opts.force);
|
|
2291
2505
|
await scaffoldWorkspaceWithSrcSubmodule({
|
|
@@ -2308,11 +2522,11 @@ async function runInitFromExistingFolder(opts, ownerEmail) {
|
|
|
2308
2522
|
}
|
|
2309
2523
|
async function runInitFromScratch(opts, ownerEmail) {
|
|
2310
2524
|
await ensureGitHubReady();
|
|
2311
|
-
const projectName = opts.workspaceName ?? await
|
|
2525
|
+
const projectName = opts.workspaceName ?? await input3({
|
|
2312
2526
|
message: "T\xEAn d\u1EF1 \xE1n:",
|
|
2313
2527
|
validate: (v) => v.length > 0 ? true : "T\xEAn b\u1EAFt bu\u1ED9c"
|
|
2314
2528
|
});
|
|
2315
|
-
const visibility = opts.repoVisibility ?? await
|
|
2529
|
+
const visibility = opts.repoVisibility ?? await select7({
|
|
2316
2530
|
message: "Visibility?",
|
|
2317
2531
|
choices: [
|
|
2318
2532
|
{ name: "private (m\u1EB7c \u0111\u1ECBnh)", value: "private" },
|
|
@@ -2340,7 +2554,10 @@ async function runInitFromScratch(opts, ownerEmail) {
|
|
|
2340
2554
|
await git(workspacePath).subModule(["add", urls.sshUrl, "src"]);
|
|
2341
2555
|
let pinnedTag = "HEAD";
|
|
2342
2556
|
if (!opts.skipTeamPack) {
|
|
2343
|
-
const result = await
|
|
2557
|
+
const result = await addTeamPackSubmoduleWithRetryOnNetworkFail(
|
|
2558
|
+
workspacePath,
|
|
2559
|
+
opts.packVersion
|
|
2560
|
+
);
|
|
2344
2561
|
pinnedTag = result.pinnedTag ?? "HEAD";
|
|
2345
2562
|
sp.succeed(`Pin team-ai-pack v\xE0o ${pinnedTag}`);
|
|
2346
2563
|
} else {
|
|
@@ -2381,14 +2598,14 @@ async function getOrCreateOriginRemote(folderPath, opts) {
|
|
|
2381
2598
|
return void 0;
|
|
2382
2599
|
}
|
|
2383
2600
|
await ensureGitHubReady();
|
|
2384
|
-
const visibility = opts.repoVisibility ?? await
|
|
2601
|
+
const visibility = opts.repoVisibility ?? await select7({
|
|
2385
2602
|
message: "Visibility?",
|
|
2386
2603
|
choices: [
|
|
2387
2604
|
{ name: "private (m\u1EB7c \u0111\u1ECBnh)", value: "private" },
|
|
2388
2605
|
{ name: "public", value: "public" }
|
|
2389
2606
|
]
|
|
2390
2607
|
});
|
|
2391
|
-
const repoName = await
|
|
2608
|
+
const repoName = await input3({
|
|
2392
2609
|
message: "T\xEAn repo:",
|
|
2393
2610
|
default: basename(folderPath)
|
|
2394
2611
|
});
|
|
@@ -2410,7 +2627,10 @@ async function scaffoldWorkspaceWithSrcSubmodule(args) {
|
|
|
2410
2627
|
await git(args.workspacePath).subModule(["add", args.srcRemoteUrl, "src"]);
|
|
2411
2628
|
let pinnedTag = "HEAD";
|
|
2412
2629
|
if (!args.skipTeamPack) {
|
|
2413
|
-
const result = await
|
|
2630
|
+
const result = await addTeamPackSubmoduleWithRetryOnNetworkFail(
|
|
2631
|
+
args.workspacePath,
|
|
2632
|
+
args.packVersion
|
|
2633
|
+
);
|
|
2414
2634
|
pinnedTag = result.pinnedTag ?? "HEAD";
|
|
2415
2635
|
sp.succeed(`Pin team-ai-pack v\xE0o ${pinnedTag}`);
|
|
2416
2636
|
} else {
|
|
@@ -2478,43 +2698,79 @@ async function maybeCreateWorkspaceRemote(args) {
|
|
|
2478
2698
|
});
|
|
2479
2699
|
}
|
|
2480
2700
|
if (!shouldCreate) return;
|
|
2481
|
-
const visibility = args.repoVisibility ?? (args.autoYes ? "private" : await
|
|
2701
|
+
const visibility = args.repoVisibility ?? (args.autoYes ? "private" : await select7({
|
|
2482
2702
|
message: "Workspace visibility?",
|
|
2483
2703
|
choices: [
|
|
2484
2704
|
{ name: "private (m\u1EB7c \u0111\u1ECBnh, an to\xE0n)", value: "private" },
|
|
2485
2705
|
{ name: "public", value: "public" }
|
|
2486
2706
|
]
|
|
2487
2707
|
}));
|
|
2488
|
-
|
|
2489
|
-
|
|
2490
|
-
|
|
2491
|
-
|
|
2492
|
-
|
|
2493
|
-
|
|
2494
|
-
|
|
2495
|
-
|
|
2496
|
-
|
|
2497
|
-
|
|
2708
|
+
while (true) {
|
|
2709
|
+
try {
|
|
2710
|
+
await createWorkspaceRemoteViaGh({
|
|
2711
|
+
workspacePath: args.workspacePath,
|
|
2712
|
+
workspaceName: args.workspaceName,
|
|
2713
|
+
visibility,
|
|
2714
|
+
org: args.repoOrg
|
|
2715
|
+
});
|
|
2716
|
+
return;
|
|
2717
|
+
} catch (err) {
|
|
2718
|
+
const action = await promptRetryOrSkip({
|
|
2719
|
+
taskName: "T\u1EA1o workspace remote tr\xEAn GitHub",
|
|
2720
|
+
reason: err instanceof Error ? err.message : String(err),
|
|
2721
|
+
allowSkip: true,
|
|
2722
|
+
// Workspace remote OPTIONAL — skip OK, workspace local vẫn dùng được.
|
|
2723
|
+
hint: "Tip: sai org? Pass --repo-org=<your-gh-user>. Ho\u1EB7c switch gh: gh auth login."
|
|
2724
|
+
});
|
|
2725
|
+
if (action === "abort") {
|
|
2726
|
+
throw new UserAbortedRecoveryError("User abort t\u1EA1i b\u01B0\u1EDBc t\u1EA1o workspace remote.");
|
|
2727
|
+
}
|
|
2728
|
+
if (action === "skip") {
|
|
2729
|
+
log.warn("Workspace v\u1EABn s\u1EB5n s\xE0ng local-only. Setup remote sau khi c\u1EA7n.");
|
|
2730
|
+
return;
|
|
2731
|
+
}
|
|
2732
|
+
}
|
|
2498
2733
|
}
|
|
2499
2734
|
}
|
|
2500
2735
|
async function resolveWorkspacePath(parent, desiredName, force) {
|
|
2501
2736
|
const desired = join16(parent, desiredName);
|
|
2502
2737
|
if (await isEmptyOrMissing(desired)) return desired;
|
|
2503
|
-
const alternative = await findAlternativeWorkspaceName(parent, desiredName);
|
|
2504
|
-
if (!alternative) {
|
|
2505
|
-
throw new Error(`Kh\xF4ng t\xECm \u0111\u01B0\u1EE3c workspace path kh\u1EA3 d\u1EE5ng trong ${parent}`);
|
|
2506
|
-
}
|
|
2507
2738
|
log.warn(`Workspace path "${desired}" \u0111\xE3 c\xF3 n\u1ED9i dung.`);
|
|
2508
|
-
|
|
2509
|
-
|
|
2510
|
-
|
|
2739
|
+
while (true) {
|
|
2740
|
+
const alternative = await findAlternativeWorkspaceName(parent, desiredName);
|
|
2741
|
+
if (force && alternative) {
|
|
2742
|
+
log.info(`--force: d\xF9ng ${alternative}`);
|
|
2743
|
+
return alternative;
|
|
2744
|
+
}
|
|
2745
|
+
const choices = [];
|
|
2746
|
+
if (alternative) {
|
|
2747
|
+
choices.push({ name: `D\xF9ng "${alternative}" (suggest)`, value: "use-alt" });
|
|
2748
|
+
}
|
|
2749
|
+
choices.push({ name: "Nh\u1EADp t\xEAn workspace kh\xE1c (manual)", value: "manual" });
|
|
2750
|
+
choices.push({ name: "T\u1EA1m ng\u01B0ng init", value: "abort" });
|
|
2751
|
+
const action = await select7({
|
|
2752
|
+
message: "C\xE1ch x\u1EED l\xFD workspace path conflict?",
|
|
2753
|
+
choices
|
|
2754
|
+
});
|
|
2755
|
+
if (action === "abort") {
|
|
2756
|
+
throw new UserAbortedRecoveryError(
|
|
2757
|
+
"User abort t\u1EA1i b\u01B0\u1EDBc resolve workspace path. Ch\u1EA1y l\u1EA1i v\u1EDBi --workspace-name kh\xE1c."
|
|
2758
|
+
);
|
|
2759
|
+
}
|
|
2760
|
+
if (action === "use-alt" && alternative) {
|
|
2761
|
+
return alternative;
|
|
2762
|
+
}
|
|
2763
|
+
const newName = await input3({
|
|
2764
|
+
message: "T\xEAn workspace m\u1EDBi:",
|
|
2765
|
+
validate: (v) => v.trim().length > 0 ? true : "T\xEAn kh\xF4ng \u0111\u01B0\u1EE3c r\u1ED7ng"
|
|
2766
|
+
});
|
|
2767
|
+
const newPath = join16(parent, newName.trim());
|
|
2768
|
+
if (await isEmptyOrMissing(newPath)) return newPath;
|
|
2769
|
+
log.warn(`"${newPath}" c\u0169ng \u0111\xE3 c\xF3 n\u1ED9i dung. Th\u1EED t\xEAn kh\xE1c.`);
|
|
2511
2770
|
}
|
|
2512
|
-
const useAlt = await confirm3({ message: `D\xF9ng "${alternative}" thay th\u1EBF?`, default: true });
|
|
2513
|
-
if (!useAlt) throw new Error("H\u1EE7y init. Ch\u1EA1y l\u1EA1i v\u1EDBi --workspace-name kh\xE1c.");
|
|
2514
|
-
return alternative;
|
|
2515
2771
|
}
|
|
2516
2772
|
async function promptTeamOwner(currentUserEmail) {
|
|
2517
|
-
return await
|
|
2773
|
+
return await input3({ message: "Team owner email:", default: currentUserEmail });
|
|
2518
2774
|
}
|
|
2519
2775
|
async function maybeCommitWorkspace(workspacePath, skipCommit) {
|
|
2520
2776
|
if (skipCommit) {
|
|
@@ -2839,7 +3095,7 @@ async function removeSubmoduleEntry(gitmodulesPath, submodulePath) {
|
|
|
2839
3095
|
}
|
|
2840
3096
|
|
|
2841
3097
|
// src/commands/uninstall.ts
|
|
2842
|
-
var CLI_VERSION = "1.2.
|
|
3098
|
+
var CLI_VERSION = "1.2.5";
|
|
2843
3099
|
function registerUninstallCommand(program2) {
|
|
2844
3100
|
program2.command("uninstall").description("G\u1EE1 Avatar kh\u1ECFi project \u2014 backup t\u1EF1 \u0111\u1ED9ng (M11)").option("--yes", "Skip confirm prompt").option("--no-backup", "Kh\xF4ng t\u1EA1o backup tr\u01B0\u1EDBc khi x\xF3a (nguy hi\u1EC3m)").option("--keep-submodule", "Gi\u1EEF submodule .claude/pack/").option("--keep-hooks", "Gi\u1EEF git hooks post-merge, pre-push").option("--dry-run", "Hi\u1EC3n th\u1ECB danh s\xE1ch s\u1EBD x\xF3a, kh\xF4ng th\u1EF1c thi").action(async (opts) => {
|
|
2845
3101
|
try {
|
|
@@ -2921,7 +3177,7 @@ function printUninstallSuccessBox(backupPath) {
|
|
|
2921
3177
|
}
|
|
2922
3178
|
|
|
2923
3179
|
// src/index.ts
|
|
2924
|
-
var CLI_VERSION2 = "1.2.
|
|
3180
|
+
var CLI_VERSION2 = "1.2.5";
|
|
2925
3181
|
var program = new Command();
|
|
2926
3182
|
program.name("avatar").description("AI harness CLI for NAL Vietnam engineering").version(CLI_VERSION2, "-v, --version", "Hi\u1EC3n th\u1ECB phi\xEAn b\u1EA3n Avatar CLI").addHelpText(
|
|
2927
3183
|
"beforeAll",
|