@nalvietnam/avatar-cli 1.2.4 → 1.2.6
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 +465 -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,8 +1194,240 @@ 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
|
|
1198
|
-
import
|
|
1197
|
+
import { confirm as confirm3, input as input3, select as select7 } from "@inquirer/prompts";
|
|
1198
|
+
import boxen4 from "boxen";
|
|
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
|
+
import boxen2 from "boxen";
|
|
1231
|
+
function parseRepoSlugFromGitUrl(url) {
|
|
1232
|
+
const httpsMatch = url.match(/github\.com[/:]([^/]+\/[^/]+?)(?:\.git)?$/);
|
|
1233
|
+
if (httpsMatch) return httpsMatch[1];
|
|
1234
|
+
return null;
|
|
1235
|
+
}
|
|
1236
|
+
function checkRepoAccess(repoSlug) {
|
|
1237
|
+
const r = spawnSync6("gh", ["api", `repos/${repoSlug}`], { stdio: "ignore" });
|
|
1238
|
+
return r.status === 0;
|
|
1239
|
+
}
|
|
1240
|
+
function getCurrentGhUser() {
|
|
1241
|
+
const r = spawnSync6("gh", ["api", "user", "--jq", ".login"], {
|
|
1242
|
+
encoding: "utf8",
|
|
1243
|
+
stdio: ["ignore", "pipe", "pipe"]
|
|
1244
|
+
});
|
|
1245
|
+
if (r.status !== 0) return null;
|
|
1246
|
+
return r.stdout.trim() || null;
|
|
1247
|
+
}
|
|
1248
|
+
function triggerGhAuthLoginInteractive() {
|
|
1249
|
+
log.info("M\u1EDF browser \u0111\u1EC3 switch GitHub account...");
|
|
1250
|
+
const r = spawnSync6("gh", ["auth", "login", "--web"], { stdio: "inherit" });
|
|
1251
|
+
if (r.status !== 0) {
|
|
1252
|
+
log.warn(`gh auth login exit ${r.status}. C\xF3 th\u1EC3 ch\u1EA1y tay r\u1ED3i quay l\u1EA1i retry.`);
|
|
1253
|
+
}
|
|
1254
|
+
}
|
|
1255
|
+
async function copyInfoToClipboardWithConsent(info) {
|
|
1256
|
+
const ok = await confirm2({
|
|
1257
|
+
message: "Copy th\xF4ng tin (GitHub username + email) v\xE0o clipboard \u0111\u1EC3 d\xE1n v\xE0o Slack/email?",
|
|
1258
|
+
default: true
|
|
1259
|
+
});
|
|
1260
|
+
if (!ok) return;
|
|
1261
|
+
try {
|
|
1262
|
+
const { default: clipboardy } = await import("clipboardy");
|
|
1263
|
+
await clipboardy.write(info);
|
|
1264
|
+
log.success("\u0110\xE3 copy v\xE0o clipboard");
|
|
1265
|
+
} catch (err) {
|
|
1266
|
+
log.dim(`Copy clipboard fail: ${err.message}`);
|
|
1267
|
+
}
|
|
1268
|
+
}
|
|
1269
|
+
function printAccessWarningBox(repoSlug, ghUser, ssoEmail) {
|
|
1270
|
+
const lines = [
|
|
1271
|
+
`${chalk.red("\u26D4 KH\xD4NG C\xD3 QUY\u1EC0N ACCESS")}`,
|
|
1272
|
+
"",
|
|
1273
|
+
`Repo: ${chalk.bold(repoSlug)}`,
|
|
1274
|
+
"",
|
|
1275
|
+
"B\u1EA1n c\u1EA7n \u0111\u01B0\u1EE3c admin add v\xE0o org \u0111\u1EC3 pull team-ai-pack.",
|
|
1276
|
+
"",
|
|
1277
|
+
`${chalk.dim("Th\xF4ng tin g\u1EEDi admin:")}`,
|
|
1278
|
+
` GitHub username: ${chalk.cyan(ghUser ?? "(ch\u01B0a gh auth \u2014 ch\u1EA1y: gh auth login)")}`,
|
|
1279
|
+
` NAL email: ${chalk.cyan(ssoEmail ?? "(ch\u01B0a avatar login \u2014 ch\u1EA1y: avatar login)")}`,
|
|
1280
|
+
` Date: ${(/* @__PURE__ */ new Date()).toISOString().split("T")[0]}`,
|
|
1281
|
+
"",
|
|
1282
|
+
`${chalk.dim("Li\xEAn h\u1EC7:")} luke@nal.vn (Slack #avatar-setup)`
|
|
1283
|
+
];
|
|
1284
|
+
process.stdout.write(
|
|
1285
|
+
`${boxen2(lines.join("\n"), { padding: 1, borderColor: "red", borderStyle: "round" })}
|
|
1286
|
+
`
|
|
1287
|
+
);
|
|
1288
|
+
}
|
|
1289
|
+
function buildAccessRequestInfo(repoSlug, ghUser, ssoEmail) {
|
|
1290
|
+
return [
|
|
1291
|
+
`Request access ${repoSlug} (NAL)`,
|
|
1292
|
+
"",
|
|
1293
|
+
`GitHub username: ${ghUser ?? "(ch\u01B0a gh auth \u2014 ch\u1EA1y: gh auth login)"}`,
|
|
1294
|
+
`NAL email: ${ssoEmail ?? "(ch\u01B0a avatar login \u2014 ch\u1EA1y: avatar login)"}`,
|
|
1295
|
+
`Date: ${(/* @__PURE__ */ new Date()).toISOString().split("T")[0]}`
|
|
1296
|
+
].join("\n");
|
|
1297
|
+
}
|
|
1298
|
+
async function ensureTeamPackAccessWithRetry(args) {
|
|
1299
|
+
if (checkRepoAccess(args.repoSlug)) return true;
|
|
1300
|
+
const initialGhUser = getCurrentGhUser();
|
|
1301
|
+
printAccessWarningBox(args.repoSlug, initialGhUser, args.ssoEmail ?? null);
|
|
1302
|
+
await copyInfoToClipboardWithConsent(
|
|
1303
|
+
buildAccessRequestInfo(args.repoSlug, initialGhUser, args.ssoEmail ?? null)
|
|
1304
|
+
);
|
|
1305
|
+
while (true) {
|
|
1306
|
+
const ghUser = getCurrentGhUser();
|
|
1307
|
+
const ghUserDisplay = ghUser ?? "(ch\u01B0a gh auth)";
|
|
1308
|
+
const action = await select4({
|
|
1309
|
+
message: "C\xE1ch x\u1EED l\xFD?",
|
|
1310
|
+
choices: [
|
|
1311
|
+
{
|
|
1312
|
+
name: `\u0110\xE3 \u0111\u01B0\u1EE3c grant access v\u1EDBi GitHub username '${ghUserDisplay}' \u2014 ki\u1EC3m tra l\u1EA1i`,
|
|
1313
|
+
value: "retry-same"
|
|
1314
|
+
},
|
|
1315
|
+
{
|
|
1316
|
+
name: "\u0110\xE3 grant v\u1EDBi GitHub account kh\xE1c \u2014 switch gh (m\u1EDF browser)",
|
|
1317
|
+
value: "switch-account"
|
|
1318
|
+
},
|
|
1319
|
+
{
|
|
1320
|
+
name: "T\u1EA1m ng\u01B0ng \u2014 ch\u1EA1y l\u1EA1i 'avatar init' sau",
|
|
1321
|
+
value: "abort"
|
|
1322
|
+
}
|
|
1323
|
+
]
|
|
1324
|
+
});
|
|
1325
|
+
if (action === "abort") {
|
|
1326
|
+
log.dim("T\u1EA1m ng\u01B0ng. Ch\u1EA1y l\u1EA1i 'avatar init' sau khi \u0111\xE3 accept invite t\u1EEB GitHub.");
|
|
1327
|
+
return false;
|
|
1328
|
+
}
|
|
1329
|
+
if (action === "switch-account") {
|
|
1330
|
+
triggerGhAuthLoginInteractive();
|
|
1331
|
+
}
|
|
1332
|
+
log.info("Ki\u1EC3m tra access...");
|
|
1333
|
+
if (checkRepoAccess(args.repoSlug)) {
|
|
1334
|
+
const finalUser = getCurrentGhUser();
|
|
1335
|
+
log.success(`\u0110\xE3 c\xF3 access v\u1EDBi '${finalUser ?? "(unknown)"}' \u2014 ti\u1EBFp t\u1EE5c.`);
|
|
1336
|
+
return true;
|
|
1337
|
+
}
|
|
1338
|
+
log.warn(
|
|
1339
|
+
`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.`
|
|
1340
|
+
);
|
|
1341
|
+
}
|
|
1342
|
+
}
|
|
1343
|
+
|
|
1344
|
+
// src/lib/resolve-team-pack-repo-url.ts
|
|
1345
|
+
var ORG_DEFAULT = "https://github.com/nalvn/team-ai-pack.git";
|
|
1346
|
+
function resolveTeamPackRepoUrl() {
|
|
1347
|
+
if (process.env.AVATAR_TEAM_PACK_REPO_URL) {
|
|
1348
|
+
return process.env.AVATAR_TEAM_PACK_REPO_URL;
|
|
1349
|
+
}
|
|
1350
|
+
return ORG_DEFAULT;
|
|
1351
|
+
}
|
|
1352
|
+
|
|
1353
|
+
// src/lib/team-pack-submodule-manager.ts
|
|
1354
|
+
var TEAM_PACK_REPO_URL = resolveTeamPackRepoUrl();
|
|
1355
|
+
var TEAM_PACK_RELATIVE_PATH = ".claude/pack";
|
|
1356
|
+
var TeamPackAccessAbortedError = class extends Error {
|
|
1357
|
+
constructor(message) {
|
|
1358
|
+
super(message);
|
|
1359
|
+
this.name = "TeamPackAccessAbortedError";
|
|
1360
|
+
}
|
|
1361
|
+
};
|
|
1362
|
+
async function addTeamPackSubmodule(projectRoot, tag, ssoEmail) {
|
|
1363
|
+
const url = resolveTeamPackRepoUrl();
|
|
1364
|
+
const repoSlug = parseRepoSlugFromGitUrl(url);
|
|
1365
|
+
if (repoSlug) {
|
|
1366
|
+
const hasAccess = await ensureTeamPackAccessWithRetry({ repoSlug, ssoEmail });
|
|
1367
|
+
if (!hasAccess) {
|
|
1368
|
+
throw new TeamPackAccessAbortedError(
|
|
1369
|
+
"User ch\u1ECDn t\u1EA1m ng\u01B0ng. Ch\u1EA1y l\u1EA1i 'avatar init' sau khi \u0111\u01B0\u1EE3c add v\xE0o org."
|
|
1370
|
+
);
|
|
1371
|
+
}
|
|
1372
|
+
}
|
|
1373
|
+
try {
|
|
1374
|
+
await addSubmodule(url, TEAM_PACK_RELATIVE_PATH, projectRoot);
|
|
1375
|
+
} catch (err) {
|
|
1376
|
+
const msg = err instanceof Error ? err.message : String(err);
|
|
1377
|
+
if (msg.includes("Repository not found") || msg.includes("not found")) {
|
|
1378
|
+
log.error(
|
|
1379
|
+
`Repo team-ai-pack kh\xF4ng t\u1ED3n t\u1EA1i: ${url}
|
|
1380
|
+
C\xE1ch fix:
|
|
1381
|
+
1. T\u1EA1o repo: gh repo create <owner>/team-ai-pack --private --add-readme
|
|
1382
|
+
2. Ho\u1EB7c override URL: export AVATAR_TEAM_PACK_REPO_URL=<url-repo-c\u1EE7a-b\u1EA1n>
|
|
1383
|
+
3. Ho\u1EB7c d\xF9ng flag --skip-team-pack`
|
|
1384
|
+
);
|
|
1385
|
+
}
|
|
1386
|
+
throw err;
|
|
1387
|
+
}
|
|
1388
|
+
let target = tag ?? null;
|
|
1389
|
+
if (!target) {
|
|
1390
|
+
target = await latestTag(join10(projectRoot, TEAM_PACK_RELATIVE_PATH));
|
|
1391
|
+
}
|
|
1392
|
+
if (target) {
|
|
1393
|
+
await checkoutTagInSubmodule(TEAM_PACK_RELATIVE_PATH, target, projectRoot);
|
|
1394
|
+
}
|
|
1395
|
+
return { pinnedTag: target };
|
|
1396
|
+
}
|
|
1397
|
+
async function readPinnedPackVersion(projectRoot) {
|
|
1398
|
+
const submoduleRoot = join10(projectRoot, TEAM_PACK_RELATIVE_PATH);
|
|
1399
|
+
const tag = await latestTag(submoduleRoot);
|
|
1400
|
+
if (tag) return tag;
|
|
1401
|
+
const sha = await currentCommitSha(submoduleRoot);
|
|
1402
|
+
return sha.slice(0, 7);
|
|
1403
|
+
}
|
|
1404
|
+
|
|
1405
|
+
// src/lib/add-team-pack-submodule-with-retry-on-network-fail.ts
|
|
1406
|
+
async function addTeamPackSubmoduleWithRetryOnNetworkFail(projectRoot, tag, ssoEmail) {
|
|
1407
|
+
while (true) {
|
|
1408
|
+
try {
|
|
1409
|
+
const result = await addTeamPackSubmodule(projectRoot, tag, ssoEmail);
|
|
1410
|
+
return { pinnedTag: result.pinnedTag, skipped: false };
|
|
1411
|
+
} catch (err) {
|
|
1412
|
+
if (err instanceof TeamPackAccessAbortedError) throw err;
|
|
1413
|
+
const action = await promptRetryOrSkip({
|
|
1414
|
+
taskName: "Pull team-ai-pack submodule",
|
|
1415
|
+
reason: err instanceof Error ? err.message : String(err),
|
|
1416
|
+
allowSkip: true,
|
|
1417
|
+
hint: "Network glitch? Retry th\u01B0\u1EDDng work. N\u1EBFu skip, d\xF9ng `avatar sync` sau \u0111\u1EC3 pull pack."
|
|
1418
|
+
});
|
|
1419
|
+
if (action === "abort") {
|
|
1420
|
+
throw new UserAbortedRecoveryError("User abort t\u1EA1i b\u01B0\u1EDBc pull team-ai-pack.");
|
|
1421
|
+
}
|
|
1422
|
+
if (action === "skip") {
|
|
1423
|
+
log.warn(
|
|
1424
|
+
"Skip team-ai-pack. Workspace d\xF9ng \u0111\u01B0\u1EE3c nh\u01B0ng kh\xF4ng c\xF3 shared knowledge. Pull sau qua `avatar sync`."
|
|
1425
|
+
);
|
|
1426
|
+
return { pinnedTag: null, skipped: true };
|
|
1427
|
+
}
|
|
1428
|
+
}
|
|
1429
|
+
}
|
|
1430
|
+
}
|
|
1199
1431
|
|
|
1200
1432
|
// src/lib/avatar-ascii-banner.ts
|
|
1201
1433
|
import chalk2 from "chalk";
|
|
@@ -1259,27 +1491,27 @@ ${renderAvatarBanner(opts)}
|
|
|
1259
1491
|
}
|
|
1260
1492
|
|
|
1261
1493
|
// src/lib/execute-gh-repo-create.ts
|
|
1262
|
-
import { spawnSync as
|
|
1494
|
+
import { spawnSync as spawnSync7 } from "child_process";
|
|
1263
1495
|
var RepoAlreadyExistsError = class extends Error {
|
|
1264
1496
|
constructor(fullName) {
|
|
1265
1497
|
super(`Repo "${fullName}" \u0111\xE3 t\u1ED3n t\u1EA1i tr\xEAn GitHub. \u0110\u1ED5i t\xEAn ho\u1EB7c x\xF3a repo c\u0169.`);
|
|
1266
1498
|
this.name = "RepoAlreadyExistsError";
|
|
1267
1499
|
}
|
|
1268
1500
|
};
|
|
1269
|
-
function executeGhRepoCreate(
|
|
1270
|
-
const fullName = `${
|
|
1501
|
+
function executeGhRepoCreate(input4) {
|
|
1502
|
+
const fullName = `${input4.org}/${input4.name}`;
|
|
1271
1503
|
const args = [
|
|
1272
1504
|
"repo",
|
|
1273
1505
|
"create",
|
|
1274
1506
|
fullName,
|
|
1275
|
-
`--${
|
|
1507
|
+
`--${input4.visibility}`,
|
|
1276
1508
|
"--source",
|
|
1277
|
-
|
|
1509
|
+
input4.folder,
|
|
1278
1510
|
"--remote",
|
|
1279
1511
|
"origin",
|
|
1280
1512
|
"--push"
|
|
1281
1513
|
];
|
|
1282
|
-
const r =
|
|
1514
|
+
const r = spawnSync7("gh", args, { stdio: "inherit" });
|
|
1283
1515
|
if (r.status !== 0) {
|
|
1284
1516
|
if (r.status === 1) {
|
|
1285
1517
|
throw new RepoAlreadyExistsError(fullName);
|
|
@@ -1293,9 +1525,9 @@ function executeGhRepoCreate(input3) {
|
|
|
1293
1525
|
}
|
|
1294
1526
|
|
|
1295
1527
|
// src/lib/resolve-github-username-default.ts
|
|
1296
|
-
import { spawnSync as
|
|
1528
|
+
import { spawnSync as spawnSync8 } from "child_process";
|
|
1297
1529
|
function resolveGithubUsernameDefault() {
|
|
1298
|
-
const r =
|
|
1530
|
+
const r = spawnSync8("gh", ["api", "user", "--jq", ".login"], {
|
|
1299
1531
|
encoding: "utf8",
|
|
1300
1532
|
stdio: ["ignore", "pipe", "pipe"]
|
|
1301
1533
|
});
|
|
@@ -1327,28 +1559,28 @@ function validateRepoVisibility(v) {
|
|
|
1327
1559
|
}
|
|
1328
1560
|
|
|
1329
1561
|
// 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}/${
|
|
1562
|
+
function createGithubRemoteFromFolder(input4) {
|
|
1563
|
+
validateRepoName(input4.name);
|
|
1564
|
+
validateRepoVisibility(input4.visibility);
|
|
1565
|
+
const org = input4.org ?? resolveGithubUsernameDefault();
|
|
1566
|
+
log.info(`T\u1EA1o GitHub repo ${org}/${input4.name} (${input4.visibility})...`);
|
|
1335
1567
|
const urls = executeGhRepoCreate({
|
|
1336
|
-
folder:
|
|
1568
|
+
folder: input4.folder,
|
|
1337
1569
|
org,
|
|
1338
|
-
name:
|
|
1339
|
-
visibility:
|
|
1570
|
+
name: input4.name,
|
|
1571
|
+
visibility: input4.visibility
|
|
1340
1572
|
});
|
|
1341
1573
|
log.success(`\u0110\xE3 t\u1EA1o: ${urls.sshUrl}`);
|
|
1342
1574
|
return urls;
|
|
1343
1575
|
}
|
|
1344
1576
|
|
|
1345
1577
|
// src/lib/create-workspace-remote-via-gh.ts
|
|
1346
|
-
import { spawnSync as
|
|
1578
|
+
import { spawnSync as spawnSync16 } from "child_process";
|
|
1347
1579
|
|
|
1348
1580
|
// src/lib/check-gh-cli-auth-status.ts
|
|
1349
|
-
import { spawnSync as
|
|
1581
|
+
import { spawnSync as spawnSync9 } from "child_process";
|
|
1350
1582
|
function checkGhCliAuthStatus() {
|
|
1351
|
-
const r =
|
|
1583
|
+
const r = spawnSync9("gh", ["auth", "status"], { stdio: "ignore" });
|
|
1352
1584
|
if (r.error && r.error.code === "ENOENT") {
|
|
1353
1585
|
return "not-installed";
|
|
1354
1586
|
}
|
|
@@ -1356,12 +1588,12 @@ function checkGhCliAuthStatus() {
|
|
|
1356
1588
|
}
|
|
1357
1589
|
|
|
1358
1590
|
// src/lib/detect-package-manager.ts
|
|
1359
|
-
import { spawnSync as
|
|
1591
|
+
import { spawnSync as spawnSync10 } from "child_process";
|
|
1360
1592
|
function hasBinary(name) {
|
|
1361
1593
|
const platform2 = detectHostPlatform();
|
|
1362
1594
|
const probe = platform2 === "win32" ? "where" : "command";
|
|
1363
1595
|
const args = platform2 === "win32" ? [name] : ["-v", name];
|
|
1364
|
-
const r =
|
|
1596
|
+
const r = spawnSync10(probe, args, {
|
|
1365
1597
|
shell: platform2 !== "win32",
|
|
1366
1598
|
stdio: "ignore"
|
|
1367
1599
|
});
|
|
@@ -1377,11 +1609,11 @@ function detectPackageManager() {
|
|
|
1377
1609
|
}
|
|
1378
1610
|
|
|
1379
1611
|
// src/lib/handle-remote-access-failure-with-account-switch.ts
|
|
1380
|
-
import { spawnSync as
|
|
1381
|
-
import { select as
|
|
1612
|
+
import { spawnSync as spawnSync12 } from "child_process";
|
|
1613
|
+
import { select as select5 } from "@inquirer/prompts";
|
|
1382
1614
|
|
|
1383
1615
|
// src/lib/verify-git-remote-accessible.ts
|
|
1384
|
-
import { spawnSync as
|
|
1616
|
+
import { spawnSync as spawnSync11 } from "child_process";
|
|
1385
1617
|
var TIMEOUT_MS = 5e3;
|
|
1386
1618
|
function classifyRemoteError(stderr) {
|
|
1387
1619
|
const text = stderr.toLowerCase();
|
|
@@ -1397,7 +1629,7 @@ function classifyRemoteError(stderr) {
|
|
|
1397
1629
|
return "unknown";
|
|
1398
1630
|
}
|
|
1399
1631
|
function tryVerifyGitRemoteAccessible(url) {
|
|
1400
|
-
const r =
|
|
1632
|
+
const r = spawnSync11("git", ["ls-remote", "--exit-code", url, "HEAD"], {
|
|
1401
1633
|
encoding: "utf8",
|
|
1402
1634
|
timeout: TIMEOUT_MS,
|
|
1403
1635
|
stdio: ["ignore", "pipe", "pipe"]
|
|
@@ -1418,17 +1650,17 @@ var RemoteAccessAbortedError = class extends Error {
|
|
|
1418
1650
|
this.name = "RemoteAccessAbortedError";
|
|
1419
1651
|
}
|
|
1420
1652
|
};
|
|
1421
|
-
function
|
|
1422
|
-
const r =
|
|
1653
|
+
function getCurrentGhUser2() {
|
|
1654
|
+
const r = spawnSync12("gh", ["api", "user", "--jq", ".login"], {
|
|
1423
1655
|
encoding: "utf8",
|
|
1424
1656
|
stdio: ["ignore", "pipe", "pipe"]
|
|
1425
1657
|
});
|
|
1426
1658
|
if (r.status !== 0) return null;
|
|
1427
1659
|
return r.stdout.trim() || null;
|
|
1428
1660
|
}
|
|
1429
|
-
function
|
|
1661
|
+
function triggerGhAuthLoginInteractive2() {
|
|
1430
1662
|
log.info("M\u1EDF browser \u0111\u1EC3 switch GitHub account...");
|
|
1431
|
-
const r =
|
|
1663
|
+
const r = spawnSync12("gh", ["auth", "login", "--web"], { stdio: "inherit" });
|
|
1432
1664
|
if (r.status !== 0) {
|
|
1433
1665
|
log.warn(`gh auth login exit ${r.status}. B\u1EA1n c\xF3 th\u1EC3 ch\u1EA1y tay r\u1ED3i quay l\u1EA1i retry.`);
|
|
1434
1666
|
}
|
|
@@ -1451,12 +1683,12 @@ async function handleRemoteAccessFailureWithAccountSwitch(args) {
|
|
|
1451
1683
|
let reason = args.initialReason;
|
|
1452
1684
|
let detail = args.initialDetail;
|
|
1453
1685
|
while (true) {
|
|
1454
|
-
const ghUser =
|
|
1686
|
+
const ghUser = getCurrentGhUser2();
|
|
1455
1687
|
log.warn(`Kh\xF4ng truy c\u1EADp \u0111\u01B0\u1EE3c ${args.url}`);
|
|
1456
1688
|
log.dim(` L\xFD do: ${reason}${detail ? ` \u2014 ${detail.slice(0, 150)}` : ""}`);
|
|
1457
1689
|
log.info(getReasonHint(reason, args.url, ghUser));
|
|
1458
1690
|
if (ghUser) log.dim(` gh CLI hi\u1EC7n \u0111ang login: ${ghUser}`);
|
|
1459
|
-
const action = await
|
|
1691
|
+
const action = await select5({
|
|
1460
1692
|
message: "C\xE1ch x\u1EED l\xFD?",
|
|
1461
1693
|
choices: [
|
|
1462
1694
|
{
|
|
@@ -1479,7 +1711,7 @@ async function handleRemoteAccessFailureWithAccountSwitch(args) {
|
|
|
1479
1711
|
);
|
|
1480
1712
|
}
|
|
1481
1713
|
if (action === "switch") {
|
|
1482
|
-
|
|
1714
|
+
triggerGhAuthLoginInteractive2();
|
|
1483
1715
|
}
|
|
1484
1716
|
log.info("Verify remote l\u1EA1i...");
|
|
1485
1717
|
const result = tryVerifyGitRemoteAccessible(args.url);
|
|
@@ -1493,7 +1725,7 @@ async function handleRemoteAccessFailureWithAccountSwitch(args) {
|
|
|
1493
1725
|
}
|
|
1494
1726
|
|
|
1495
1727
|
// src/lib/install-gh-cli-via-package-manager.ts
|
|
1496
|
-
import { spawnSync as
|
|
1728
|
+
import { spawnSync as spawnSync13 } from "child_process";
|
|
1497
1729
|
var INSTALL_COMMANDS = {
|
|
1498
1730
|
brew: { cmd: "brew", args: ["install", "gh"] },
|
|
1499
1731
|
apt: { cmd: "sudo", args: ["apt-get", "install", "-y", "gh"] },
|
|
@@ -1504,7 +1736,7 @@ var INSTALL_COMMANDS = {
|
|
|
1504
1736
|
function installGhCliViaPackageManager(pm) {
|
|
1505
1737
|
const spec = INSTALL_COMMANDS[pm];
|
|
1506
1738
|
log.info(`\u0110ang c\xE0i gh CLI qua ${pm}...`);
|
|
1507
|
-
const r =
|
|
1739
|
+
const r = spawnSync13(spec.cmd, spec.args, { stdio: "inherit" });
|
|
1508
1740
|
if (r.status !== 0) {
|
|
1509
1741
|
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.`);
|
|
1510
1742
|
}
|
|
@@ -1512,9 +1744,9 @@ function installGhCliViaPackageManager(pm) {
|
|
|
1512
1744
|
}
|
|
1513
1745
|
|
|
1514
1746
|
// src/lib/setup-git-credential-via-gh.ts
|
|
1515
|
-
import { spawnSync as
|
|
1747
|
+
import { spawnSync as spawnSync14 } from "child_process";
|
|
1516
1748
|
function setupGitCredentialViaGh() {
|
|
1517
|
-
const r =
|
|
1749
|
+
const r = spawnSync14("gh", ["auth", "setup-git"], { stdio: "ignore" });
|
|
1518
1750
|
if (r.status !== 0) {
|
|
1519
1751
|
log.warn("gh auth setup-git fail (non-fatal). N\u1EBFu git clone l\u1ED7i 128 \u2192 ch\u1EA1y th\u1EE7 c\xF4ng.");
|
|
1520
1752
|
return;
|
|
@@ -1523,10 +1755,10 @@ function setupGitCredentialViaGh() {
|
|
|
1523
1755
|
}
|
|
1524
1756
|
|
|
1525
1757
|
// src/lib/trigger-gh-cli-auth-login.ts
|
|
1526
|
-
import { spawnSync as
|
|
1758
|
+
import { spawnSync as spawnSync15 } from "child_process";
|
|
1527
1759
|
function triggerGhCliAuthLogin() {
|
|
1528
1760
|
log.info("Kh\u1EDFi \u0111\u1ED9ng \u0111\u0103ng nh\u1EADp GitHub qua gh CLI (browser s\u1EBD m\u1EDF)...");
|
|
1529
|
-
const r =
|
|
1761
|
+
const r = spawnSync15(
|
|
1530
1762
|
"gh",
|
|
1531
1763
|
["auth", "login", "--hostname", "github.com", "--web", "--git-protocol", "ssh"],
|
|
1532
1764
|
{ stdio: "inherit" }
|
|
@@ -1539,24 +1771,61 @@ function triggerGhCliAuthLogin() {
|
|
|
1539
1771
|
|
|
1540
1772
|
// src/lib/git-auth-and-install-orchestrator.ts
|
|
1541
1773
|
async function ensureGitHubReady(remoteUrl) {
|
|
1542
|
-
|
|
1543
|
-
if (state === "not-installed") {
|
|
1774
|
+
while (checkGhCliAuthStatus() === "not-installed") {
|
|
1544
1775
|
log.warn("gh CLI ch\u01B0a c\xE0i. Avatar s\u1EBD t\u1EF1 c\xE0i.");
|
|
1545
1776
|
const pm = detectPackageManager();
|
|
1546
1777
|
if (!pm) {
|
|
1547
|
-
|
|
1548
|
-
"
|
|
1549
|
-
|
|
1778
|
+
const action = await promptRetryOrSkip({
|
|
1779
|
+
taskName: "Ph\xE1t hi\u1EC7n package manager",
|
|
1780
|
+
reason: "Kh\xF4ng t\xECm th\u1EA5y brew/apt/dnf/pacman/winget tr\xEAn m\xE1y.",
|
|
1781
|
+
allowSkip: false,
|
|
1782
|
+
hint: "C\xE0i gh CLI tay (https://cli.github.com) r\u1ED3i ch\u1ECDn Retry."
|
|
1783
|
+
});
|
|
1784
|
+
if (action === "abort") {
|
|
1785
|
+
throw new UserAbortedRecoveryError("User abort t\u1EA1i b\u01B0\u1EDBc c\xE0i gh CLI.");
|
|
1786
|
+
}
|
|
1787
|
+
continue;
|
|
1788
|
+
}
|
|
1789
|
+
try {
|
|
1790
|
+
installGhCliViaPackageManager(pm);
|
|
1791
|
+
} catch (err) {
|
|
1792
|
+
const action = await promptRetryOrSkip({
|
|
1793
|
+
taskName: `C\xE0i gh CLI qua ${pm}`,
|
|
1794
|
+
reason: err.message,
|
|
1795
|
+
allowSkip: false,
|
|
1796
|
+
hint: "C\xE0i tay (https://cli.github.com) r\u1ED3i ch\u1ECDn Retry, ho\u1EB7c Abort."
|
|
1797
|
+
});
|
|
1798
|
+
if (action === "abort") {
|
|
1799
|
+
throw new UserAbortedRecoveryError("User abort t\u1EA1i b\u01B0\u1EDBc c\xE0i gh CLI.");
|
|
1800
|
+
}
|
|
1550
1801
|
}
|
|
1551
|
-
installGhCliViaPackageManager(pm);
|
|
1552
|
-
state = checkGhCliAuthStatus();
|
|
1553
1802
|
}
|
|
1554
|
-
|
|
1803
|
+
while (checkGhCliAuthStatus() === "not-authenticated") {
|
|
1555
1804
|
log.warn("Ch\u01B0a \u0111\u0103ng nh\u1EADp GitHub.");
|
|
1556
|
-
|
|
1557
|
-
|
|
1558
|
-
|
|
1559
|
-
|
|
1805
|
+
try {
|
|
1806
|
+
triggerGhCliAuthLogin();
|
|
1807
|
+
} catch (err) {
|
|
1808
|
+
const action = await promptRetryOrSkip({
|
|
1809
|
+
taskName: "\u0110\u0103ng nh\u1EADp GitHub qua gh",
|
|
1810
|
+
reason: err.message,
|
|
1811
|
+
allowSkip: false,
|
|
1812
|
+
hint: "Th\u1EED l\u1EA1i \u2014 ch\u1ECDn c\xE1ch login kh\xE1c (browser vs token) khi gh prompt."
|
|
1813
|
+
});
|
|
1814
|
+
if (action === "abort") {
|
|
1815
|
+
throw new UserAbortedRecoveryError("User abort t\u1EA1i b\u01B0\u1EDBc gh auth login.");
|
|
1816
|
+
}
|
|
1817
|
+
continue;
|
|
1818
|
+
}
|
|
1819
|
+
if (checkGhCliAuthStatus() !== "authenticated") {
|
|
1820
|
+
const action = await promptRetryOrSkip({
|
|
1821
|
+
taskName: "Verify gh auth",
|
|
1822
|
+
reason: "Sau gh auth login v\u1EABn b\xE1o not-authenticated.",
|
|
1823
|
+
allowSkip: false,
|
|
1824
|
+
hint: "C\xF3 th\u1EC3 user cancel browser. Th\u1EED l\u1EA1i ho\u1EB7c abort."
|
|
1825
|
+
});
|
|
1826
|
+
if (action === "abort") {
|
|
1827
|
+
throw new UserAbortedRecoveryError("User abort t\u1EA1i b\u01B0\u1EDBc verify gh auth.");
|
|
1828
|
+
}
|
|
1560
1829
|
}
|
|
1561
1830
|
}
|
|
1562
1831
|
log.success("gh CLI s\u1EB5n s\xE0ng");
|
|
@@ -1578,13 +1847,13 @@ async function ensureGitHubReady(remoteUrl) {
|
|
|
1578
1847
|
// src/lib/create-workspace-remote-via-gh.ts
|
|
1579
1848
|
function canCreateInNamespace(org, ghUser) {
|
|
1580
1849
|
if (org.toLowerCase() === ghUser.toLowerCase()) return { ok: true };
|
|
1581
|
-
const r =
|
|
1850
|
+
const r = spawnSync16("gh", ["api", `orgs/${org}/members/${ghUser}`, "--silent"], {
|
|
1582
1851
|
stdio: "ignore"
|
|
1583
1852
|
});
|
|
1584
1853
|
if (r.status === 0) return { ok: true };
|
|
1585
|
-
const orgCheck =
|
|
1854
|
+
const orgCheck = spawnSync16("gh", ["api", `orgs/${org}`, "--silent"], { stdio: "ignore" });
|
|
1586
1855
|
if (orgCheck.status !== 0) {
|
|
1587
|
-
const userCheck =
|
|
1856
|
+
const userCheck = spawnSync16("gh", ["api", `users/${org}`, "--silent"], { stdio: "ignore" });
|
|
1588
1857
|
if (userCheck.status === 0) {
|
|
1589
1858
|
return {
|
|
1590
1859
|
ok: false,
|
|
@@ -1601,27 +1870,27 @@ function canCreateInNamespace(org, ghUser) {
|
|
|
1601
1870
|
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.`
|
|
1602
1871
|
};
|
|
1603
1872
|
}
|
|
1604
|
-
async function createWorkspaceRemoteViaGh(
|
|
1605
|
-
validateRepoName(
|
|
1606
|
-
validateRepoVisibility(
|
|
1873
|
+
async function createWorkspaceRemoteViaGh(input4) {
|
|
1874
|
+
validateRepoName(input4.workspaceName);
|
|
1875
|
+
validateRepoVisibility(input4.visibility);
|
|
1607
1876
|
await ensureGitHubReady();
|
|
1608
1877
|
const ghUser = resolveGithubUsernameDefault();
|
|
1609
|
-
const org =
|
|
1878
|
+
const org = input4.org ?? ghUser;
|
|
1610
1879
|
const namespaceCheck = canCreateInNamespace(org, ghUser);
|
|
1611
1880
|
if (!namespaceCheck.ok) {
|
|
1612
1881
|
throw new Error(`Kh\xF4ng th\u1EC3 t\u1EA1o repo d\u01B0\u1EDBi '${org}/': ${namespaceCheck.reason}`);
|
|
1613
1882
|
}
|
|
1614
|
-
const fullName = `${org}/${
|
|
1615
|
-
log.info(`T\u1EA1o GitHub repo cho workspace: ${fullName} (${
|
|
1616
|
-
const r =
|
|
1883
|
+
const fullName = `${org}/${input4.workspaceName}`;
|
|
1884
|
+
log.info(`T\u1EA1o GitHub repo cho workspace: ${fullName} (${input4.visibility})...`);
|
|
1885
|
+
const r = spawnSync16(
|
|
1617
1886
|
"gh",
|
|
1618
1887
|
[
|
|
1619
1888
|
"repo",
|
|
1620
1889
|
"create",
|
|
1621
1890
|
fullName,
|
|
1622
|
-
`--${
|
|
1891
|
+
`--${input4.visibility}`,
|
|
1623
1892
|
"--source",
|
|
1624
|
-
|
|
1893
|
+
input4.workspacePath,
|
|
1625
1894
|
"--remote",
|
|
1626
1895
|
"origin",
|
|
1627
1896
|
"--push"
|
|
@@ -1633,7 +1902,7 @@ async function createWorkspaceRemoteViaGh(input3) {
|
|
|
1633
1902
|
if (stderr) process.stderr.write(`${stderr}
|
|
1634
1903
|
`);
|
|
1635
1904
|
throw new Error(
|
|
1636
|
-
`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} --${
|
|
1905
|
+
`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`
|
|
1637
1906
|
);
|
|
1638
1907
|
}
|
|
1639
1908
|
const sshUrl = `git@github.com:${fullName}.git`;
|
|
@@ -1644,14 +1913,14 @@ async function createWorkspaceRemoteViaGh(input3) {
|
|
|
1644
1913
|
|
|
1645
1914
|
// src/lib/safe-bootstrap-for-dirty-folder.ts
|
|
1646
1915
|
import { readdirSync } from "fs";
|
|
1647
|
-
import { select as
|
|
1916
|
+
import { select as select6 } from "@inquirer/prompts";
|
|
1648
1917
|
import { simpleGit as simpleGit3 } from "simple-git";
|
|
1649
1918
|
|
|
1650
1919
|
// src/lib/check-folder-has-git.ts
|
|
1651
1920
|
import { existsSync as existsSync2, statSync } from "fs";
|
|
1652
|
-
import { join as
|
|
1921
|
+
import { join as join11 } from "path";
|
|
1653
1922
|
function checkFolderHasGit(folderPath) {
|
|
1654
|
-
const gitPath =
|
|
1923
|
+
const gitPath = join11(folderPath, ".git");
|
|
1655
1924
|
if (!existsSync2(gitPath)) return false;
|
|
1656
1925
|
const stat = statSync(gitPath);
|
|
1657
1926
|
return stat.isDirectory() || stat.isFile();
|
|
@@ -1683,7 +1952,7 @@ async function createInitialGitCommit(folderPath) {
|
|
|
1683
1952
|
|
|
1684
1953
|
// src/lib/detect-folder-tech-stack.ts
|
|
1685
1954
|
import { existsSync as existsSync3 } from "fs";
|
|
1686
|
-
import { join as
|
|
1955
|
+
import { join as join12 } from "path";
|
|
1687
1956
|
var SIGNATURES = {
|
|
1688
1957
|
node: ["package.json"],
|
|
1689
1958
|
python: ["pyproject.toml", "requirements.txt", "setup.py", "Pipfile"],
|
|
@@ -1695,7 +1964,7 @@ var SIGNATURES = {
|
|
|
1695
1964
|
function detectFolderTechStack(folderPath) {
|
|
1696
1965
|
const matched = [];
|
|
1697
1966
|
for (const [stack, files] of Object.entries(SIGNATURES)) {
|
|
1698
|
-
if (files.some((f) => existsSync3(
|
|
1967
|
+
if (files.some((f) => existsSync3(join12(folderPath, f)))) {
|
|
1699
1968
|
matched.push(stack);
|
|
1700
1969
|
}
|
|
1701
1970
|
}
|
|
@@ -1704,19 +1973,19 @@ function detectFolderTechStack(folderPath) {
|
|
|
1704
1973
|
|
|
1705
1974
|
// src/lib/gitignore-template-loader.ts
|
|
1706
1975
|
import { readFileSync as readFileSync2 } from "fs";
|
|
1707
|
-
import { dirname as dirname3, join as
|
|
1976
|
+
import { dirname as dirname3, join as join13 } from "path";
|
|
1708
1977
|
import { fileURLToPath as fileURLToPath2 } from "url";
|
|
1709
1978
|
var __dirname = dirname3(fileURLToPath2(import.meta.url));
|
|
1710
1979
|
var CANDIDATE_DIRS = [
|
|
1711
|
-
|
|
1712
|
-
|
|
1980
|
+
join13(__dirname, "..", "templates", "gitignore"),
|
|
1981
|
+
join13(__dirname, "..", "..", "src", "templates", "gitignore")
|
|
1713
1982
|
];
|
|
1714
1983
|
var AVATAR_MARKER_START = "# === avatar ===";
|
|
1715
1984
|
var AVATAR_MARKER_END = "# === /avatar ===";
|
|
1716
1985
|
function readTemplate(stack) {
|
|
1717
1986
|
for (const dir of CANDIDATE_DIRS) {
|
|
1718
1987
|
try {
|
|
1719
|
-
return readFileSync2(
|
|
1988
|
+
return readFileSync2(join13(dir, `${stack}.txt`), "utf8");
|
|
1720
1989
|
} catch {
|
|
1721
1990
|
}
|
|
1722
1991
|
}
|
|
@@ -1731,9 +2000,9 @@ ${readTemplate(s).trim()}`);
|
|
|
1731
2000
|
|
|
1732
2001
|
// src/lib/write-or-merge-gitignore.ts
|
|
1733
2002
|
import { existsSync as existsSync4, readFileSync as readFileSync3, writeFileSync } from "fs";
|
|
1734
|
-
import { join as
|
|
2003
|
+
import { join as join14 } from "path";
|
|
1735
2004
|
function writeOrMergeGitignore(folderPath, avatarBlock) {
|
|
1736
|
-
const path =
|
|
2005
|
+
const path = join14(folderPath, ".gitignore");
|
|
1737
2006
|
if (!existsSync4(path)) {
|
|
1738
2007
|
writeFileSync(path, avatarBlock, "utf8");
|
|
1739
2008
|
return;
|
|
@@ -1775,7 +2044,7 @@ async function promptBootstrapStrategy(state, opts) {
|
|
|
1775
2044
|
if (opts.presetStrategy) return opts.presetStrategy;
|
|
1776
2045
|
if (opts.autoYes) return "stash";
|
|
1777
2046
|
if (state === "empty" || state === "clean") return "commit-all";
|
|
1778
|
-
return await
|
|
2047
|
+
return await select6({
|
|
1779
2048
|
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:",
|
|
1780
2049
|
choices: [
|
|
1781
2050
|
{
|
|
@@ -1908,145 +2177,6 @@ async function safeBootstrapGitInFolder(folderPath, opts = {}) {
|
|
|
1908
2177
|
await appendAuditEntry("bootstrap", `state=${state},strategy=${strategy}`);
|
|
1909
2178
|
}
|
|
1910
2179
|
|
|
1911
|
-
// src/lib/team-pack-submodule-manager.ts
|
|
1912
|
-
import { join as join14 } from "path";
|
|
1913
|
-
|
|
1914
|
-
// src/lib/check-team-pack-access-with-retry-loop.ts
|
|
1915
|
-
import { spawnSync as spawnSync16 } from "child_process";
|
|
1916
|
-
import { confirm as confirm2, select as select5 } from "@inquirer/prompts";
|
|
1917
|
-
function parseRepoSlugFromGitUrl(url) {
|
|
1918
|
-
const httpsMatch = url.match(/github\.com[/:]([^/]+\/[^/]+?)(?:\.git)?$/);
|
|
1919
|
-
if (httpsMatch) return httpsMatch[1];
|
|
1920
|
-
return null;
|
|
1921
|
-
}
|
|
1922
|
-
function checkRepoAccess(repoSlug) {
|
|
1923
|
-
const r = spawnSync16("gh", ["api", `repos/${repoSlug}`], { stdio: "ignore" });
|
|
1924
|
-
return r.status === 0;
|
|
1925
|
-
}
|
|
1926
|
-
function getCurrentGhUser2() {
|
|
1927
|
-
const r = spawnSync16("gh", ["api", "user", "--jq", ".login"], {
|
|
1928
|
-
encoding: "utf8",
|
|
1929
|
-
stdio: ["ignore", "pipe", "pipe"]
|
|
1930
|
-
});
|
|
1931
|
-
if (r.status !== 0) return null;
|
|
1932
|
-
return r.stdout.trim() || null;
|
|
1933
|
-
}
|
|
1934
|
-
async function copyInfoToClipboardWithConsent(info) {
|
|
1935
|
-
const ok = await confirm2({
|
|
1936
|
-
message: "Copy th\xF4ng tin (GitHub username + email) v\xE0o clipboard \u0111\u1EC3 d\xE1n v\xE0o Slack/email?",
|
|
1937
|
-
default: true
|
|
1938
|
-
});
|
|
1939
|
-
if (!ok) return;
|
|
1940
|
-
try {
|
|
1941
|
-
const { default: clipboardy } = await import("clipboardy");
|
|
1942
|
-
await clipboardy.write(info);
|
|
1943
|
-
log.success("\u0110\xE3 copy v\xE0o clipboard");
|
|
1944
|
-
} catch (err) {
|
|
1945
|
-
log.dim(`Copy clipboard fail: ${err.message}`);
|
|
1946
|
-
}
|
|
1947
|
-
}
|
|
1948
|
-
function buildAccessRequestInfo(ghUser, ssoEmail) {
|
|
1949
|
-
const lines = [
|
|
1950
|
-
"Request access team-ai-pack (NAL)",
|
|
1951
|
-
"",
|
|
1952
|
-
`GitHub username: ${ghUser ?? "(ch\u01B0a gh auth \u2014 ch\u1EA1y: gh auth login)"}`,
|
|
1953
|
-
`NAL email: ${ssoEmail ?? "(ch\u01B0a avatar login \u2014 ch\u1EA1y: avatar login)"}`,
|
|
1954
|
-
`Date: ${(/* @__PURE__ */ new Date()).toISOString().split("T")[0]}`
|
|
1955
|
-
];
|
|
1956
|
-
return lines.join("\n");
|
|
1957
|
-
}
|
|
1958
|
-
async function ensureTeamPackAccessWithRetry(args) {
|
|
1959
|
-
if (checkRepoAccess(args.repoSlug)) return true;
|
|
1960
|
-
const ghUser = getCurrentGhUser2();
|
|
1961
|
-
const info = buildAccessRequestInfo(ghUser, args.ssoEmail ?? null);
|
|
1962
|
-
log.warn(`B\u1EA1n ch\u01B0a c\xF3 quy\u1EC1n access v\xE0o b\u1ED9 package ${args.repoSlug}.`);
|
|
1963
|
-
log.info("Li\xEAn h\u1EC7 admin (Luke @nal.vn) \u0111\u1EC3 \u0111\u01B0\u1EE3c add v\xE0o org nalvn.");
|
|
1964
|
-
log.plain("");
|
|
1965
|
-
log.plain(info);
|
|
1966
|
-
log.plain("");
|
|
1967
|
-
await copyInfoToClipboardWithConsent(info);
|
|
1968
|
-
while (true) {
|
|
1969
|
-
const action = await select5({
|
|
1970
|
-
message: "Ti\u1EBFp t\u1EE5c?",
|
|
1971
|
-
choices: [
|
|
1972
|
-
{ name: "\u0110\xE3 \u0111\u01B0\u1EE3c add \u2014 ki\u1EC3m tra l\u1EA1i v\xE0 ti\u1EBFp t\u1EE5c", value: "retry" },
|
|
1973
|
-
{ name: "T\u1EA1m ng\u01B0ng \u2014 ch\u1EA1y l\u1EA1i 'avatar init' sau", value: "abort" }
|
|
1974
|
-
]
|
|
1975
|
-
});
|
|
1976
|
-
if (action === "abort") {
|
|
1977
|
-
log.dim("T\u1EA1m ng\u01B0ng. Ch\u1EA1y l\u1EA1i 'avatar init' sau khi \u0111\xE3 accept invite t\u1EEB GitHub.");
|
|
1978
|
-
return false;
|
|
1979
|
-
}
|
|
1980
|
-
log.info("Ki\u1EC3m tra access...");
|
|
1981
|
-
if (checkRepoAccess(args.repoSlug)) {
|
|
1982
|
-
log.success("\u0110\xE3 c\xF3 access \u2014 ti\u1EBFp t\u1EE5c.");
|
|
1983
|
-
return true;
|
|
1984
|
-
}
|
|
1985
|
-
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).");
|
|
1986
|
-
}
|
|
1987
|
-
}
|
|
1988
|
-
|
|
1989
|
-
// src/lib/resolve-team-pack-repo-url.ts
|
|
1990
|
-
var ORG_DEFAULT = "https://github.com/nalvn/team-ai-pack.git";
|
|
1991
|
-
function resolveTeamPackRepoUrl() {
|
|
1992
|
-
if (process.env.AVATAR_TEAM_PACK_REPO_URL) {
|
|
1993
|
-
return process.env.AVATAR_TEAM_PACK_REPO_URL;
|
|
1994
|
-
}
|
|
1995
|
-
return ORG_DEFAULT;
|
|
1996
|
-
}
|
|
1997
|
-
|
|
1998
|
-
// src/lib/team-pack-submodule-manager.ts
|
|
1999
|
-
var TEAM_PACK_REPO_URL = resolveTeamPackRepoUrl();
|
|
2000
|
-
var TEAM_PACK_RELATIVE_PATH = ".claude/pack";
|
|
2001
|
-
var TeamPackAccessAbortedError = class extends Error {
|
|
2002
|
-
constructor(message) {
|
|
2003
|
-
super(message);
|
|
2004
|
-
this.name = "TeamPackAccessAbortedError";
|
|
2005
|
-
}
|
|
2006
|
-
};
|
|
2007
|
-
async function addTeamPackSubmodule(projectRoot, tag, ssoEmail) {
|
|
2008
|
-
const url = resolveTeamPackRepoUrl();
|
|
2009
|
-
const repoSlug = parseRepoSlugFromGitUrl(url);
|
|
2010
|
-
if (repoSlug) {
|
|
2011
|
-
const hasAccess = await ensureTeamPackAccessWithRetry({ repoSlug, ssoEmail });
|
|
2012
|
-
if (!hasAccess) {
|
|
2013
|
-
throw new TeamPackAccessAbortedError(
|
|
2014
|
-
"User ch\u1ECDn t\u1EA1m ng\u01B0ng. Ch\u1EA1y l\u1EA1i 'avatar init' sau khi \u0111\u01B0\u1EE3c add v\xE0o org."
|
|
2015
|
-
);
|
|
2016
|
-
}
|
|
2017
|
-
}
|
|
2018
|
-
try {
|
|
2019
|
-
await addSubmodule(url, TEAM_PACK_RELATIVE_PATH, projectRoot);
|
|
2020
|
-
} catch (err) {
|
|
2021
|
-
const msg = err instanceof Error ? err.message : String(err);
|
|
2022
|
-
if (msg.includes("Repository not found") || msg.includes("not found")) {
|
|
2023
|
-
log.error(
|
|
2024
|
-
`Repo team-ai-pack kh\xF4ng t\u1ED3n t\u1EA1i: ${url}
|
|
2025
|
-
C\xE1ch fix:
|
|
2026
|
-
1. T\u1EA1o repo: gh repo create <owner>/team-ai-pack --private --add-readme
|
|
2027
|
-
2. Ho\u1EB7c override URL: export AVATAR_TEAM_PACK_REPO_URL=<url-repo-c\u1EE7a-b\u1EA1n>
|
|
2028
|
-
3. Ho\u1EB7c d\xF9ng flag --skip-team-pack`
|
|
2029
|
-
);
|
|
2030
|
-
}
|
|
2031
|
-
throw err;
|
|
2032
|
-
}
|
|
2033
|
-
let target = tag ?? null;
|
|
2034
|
-
if (!target) {
|
|
2035
|
-
target = await latestTag(join14(projectRoot, TEAM_PACK_RELATIVE_PATH));
|
|
2036
|
-
}
|
|
2037
|
-
if (target) {
|
|
2038
|
-
await checkoutTagInSubmodule(TEAM_PACK_RELATIVE_PATH, target, projectRoot);
|
|
2039
|
-
}
|
|
2040
|
-
return { pinnedTag: target };
|
|
2041
|
-
}
|
|
2042
|
-
async function readPinnedPackVersion(projectRoot) {
|
|
2043
|
-
const submoduleRoot = join14(projectRoot, TEAM_PACK_RELATIVE_PATH);
|
|
2044
|
-
const tag = await latestTag(submoduleRoot);
|
|
2045
|
-
if (tag) return tag;
|
|
2046
|
-
const sha = await currentCommitSha(submoduleRoot);
|
|
2047
|
-
return sha.slice(0, 7);
|
|
2048
|
-
}
|
|
2049
|
-
|
|
2050
2180
|
// src/commands/init-conflict-detection-helpers.ts
|
|
2051
2181
|
import { readdir } from "fs/promises";
|
|
2052
2182
|
import { join as join15 } from "path";
|
|
@@ -2088,7 +2218,7 @@ function buildScaffoldVariables(args) {
|
|
|
2088
2218
|
}
|
|
2089
2219
|
|
|
2090
2220
|
// src/commands/login.ts
|
|
2091
|
-
import
|
|
2221
|
+
import boxen3 from "boxen";
|
|
2092
2222
|
import open from "open";
|
|
2093
2223
|
|
|
2094
2224
|
// src/lib/google-oauth-device-flow.ts
|
|
@@ -2235,7 +2365,7 @@ async function runLogin(opts) {
|
|
|
2235
2365
|
"",
|
|
2236
2366
|
`Ho\u1EB7c Avatar t\u1EF1 m\u1EDF browser, click ${chalk.green("Allow")}...`
|
|
2237
2367
|
].join("\n");
|
|
2238
|
-
process.stdout.write(`${
|
|
2368
|
+
process.stdout.write(`${boxen3(instructions, { padding: 1, borderStyle: "round" })}
|
|
2239
2369
|
`);
|
|
2240
2370
|
void open(verificationUrl).catch(() => {
|
|
2241
2371
|
log.dim("(Kh\xF4ng m\u1EDF \u0111\u01B0\u1EE3c browser t\u1EF1 \u0111\u1ED9ng \u2014 copy URL \u1EDF tr\xEAn)");
|
|
@@ -2309,6 +2439,10 @@ function registerInitCommand(program2) {
|
|
|
2309
2439
|
log.dim(err.message);
|
|
2310
2440
|
process.exit(0);
|
|
2311
2441
|
}
|
|
2442
|
+
if (err instanceof UserAbortedRecoveryError) {
|
|
2443
|
+
log.dim(err.message);
|
|
2444
|
+
process.exit(0);
|
|
2445
|
+
}
|
|
2312
2446
|
log.error(err instanceof Error ? err.message : String(err));
|
|
2313
2447
|
process.exit(1);
|
|
2314
2448
|
}
|
|
@@ -2320,13 +2454,26 @@ async function runInit(opts) {
|
|
|
2320
2454
|
log.warn("Flag --mode \u0111\xE3 deprecated t\u1EEB v1.1. D\xF9ng --project-status thay th\u1EBF.");
|
|
2321
2455
|
}
|
|
2322
2456
|
let userConfig = await readUserConfig();
|
|
2323
|
-
|
|
2457
|
+
while (!userConfig || isTokenExpired(userConfig)) {
|
|
2324
2458
|
log.info("Ch\u01B0a \u0111\u0103ng nh\u1EADp \u2014 ch\u1EA1y login flow tr\u01B0\u1EDBc khi init...");
|
|
2325
|
-
|
|
2459
|
+
try {
|
|
2460
|
+
await runLogin({});
|
|
2461
|
+
} catch (err) {
|
|
2462
|
+
log.warn(`Login fail: ${err.message}`);
|
|
2463
|
+
}
|
|
2326
2464
|
userConfig = await readUserConfig();
|
|
2327
|
-
if (
|
|
2328
|
-
|
|
2329
|
-
|
|
2465
|
+
if (userConfig && !isTokenExpired(userConfig)) break;
|
|
2466
|
+
const action = await promptRetryOrSkip({
|
|
2467
|
+
taskName: "\u0110\u0103ng nh\u1EADp SSO Google",
|
|
2468
|
+
reason: "Token ch\u01B0a \u0111\u01B0\u1EE3c t\u1EA1o ho\u1EB7c \u0111\xE3 h\u1EBFt h\u1EA1n.",
|
|
2469
|
+
allowSkip: false,
|
|
2470
|
+
// Login bắt buộc, không skip được.
|
|
2471
|
+
hint: "\u0110\u1EA3m b\u1EA3o b\u1EA1n ch\u1ECDn 'Allow' tr\xEAn browser v\xE0 d\xF9ng email @nal.vn."
|
|
2472
|
+
});
|
|
2473
|
+
if (action === "abort") {
|
|
2474
|
+
throw new UserAbortedRecoveryError(
|
|
2475
|
+
"User abort t\u1EA1i b\u01B0\u1EDBc login. Ch\u1EA1y 'avatar login' tay r\u1ED3i 'avatar init' l\u1EA1i."
|
|
2476
|
+
);
|
|
2330
2477
|
}
|
|
2331
2478
|
}
|
|
2332
2479
|
const status = opts.projectStatus ?? await promptProjectStatus();
|
|
@@ -2343,7 +2490,7 @@ async function runInit(opts) {
|
|
|
2343
2490
|
}
|
|
2344
2491
|
}
|
|
2345
2492
|
async function promptProjectStatus() {
|
|
2346
|
-
return await
|
|
2493
|
+
return await select7({
|
|
2347
2494
|
message: "T\xECnh tr\u1EA1ng d\u1EF1 \xE1n c\u1EE7a b\u1EA1n?",
|
|
2348
2495
|
choices: [
|
|
2349
2496
|
{ name: "1. \u0110\xE3 c\xF3 repo git remote (URL c\xF3 s\u1EB5n)", value: "existing-remote" },
|
|
@@ -2353,14 +2500,14 @@ async function promptProjectStatus() {
|
|
|
2353
2500
|
});
|
|
2354
2501
|
}
|
|
2355
2502
|
async function runInitFromExistingRemote(opts, ownerEmail) {
|
|
2356
|
-
const remoteUrl = opts.clientRepo ?? await
|
|
2503
|
+
const remoteUrl = opts.clientRepo ?? await input3({
|
|
2357
2504
|
message: "URL git c\u1EE7a repo:",
|
|
2358
2505
|
validate: (v) => v.length > 0 ? true : "URL b\u1EAFt bu\u1ED9c"
|
|
2359
2506
|
});
|
|
2360
2507
|
await ensureGitHubReady(remoteUrl);
|
|
2361
2508
|
const teamOwner = opts.teamOwner ?? await promptTeamOwner(ownerEmail);
|
|
2362
2509
|
const inferredName = inferWorkspaceName(remoteUrl);
|
|
2363
|
-
const workspaceName = opts.workspaceName ?? await
|
|
2510
|
+
const workspaceName = opts.workspaceName ?? await input3({ message: "T\xEAn workspace:", default: inferredName });
|
|
2364
2511
|
const workspaceParent = resolve(opts.workspaceParent ?? ".");
|
|
2365
2512
|
const workspacePath = await resolveWorkspacePath(workspaceParent, workspaceName, opts.force);
|
|
2366
2513
|
await scaffoldWorkspaceWithSrcSubmodule({
|
|
@@ -2377,12 +2524,13 @@ async function runInitFromExistingRemote(opts, ownerEmail) {
|
|
|
2377
2524
|
repoVisibility: opts.repoVisibility,
|
|
2378
2525
|
repoOrg: opts.repoOrg,
|
|
2379
2526
|
flow: "existing-remote",
|
|
2380
|
-
aiSkip: opts.aiSkip
|
|
2527
|
+
aiSkip: opts.aiSkip,
|
|
2528
|
+
ssoEmail: ownerEmail
|
|
2381
2529
|
});
|
|
2382
2530
|
}
|
|
2383
2531
|
async function runInitFromExistingFolder(opts, ownerEmail) {
|
|
2384
2532
|
const folderPath = resolve(
|
|
2385
|
-
opts.folderPath ?? await
|
|
2533
|
+
opts.folderPath ?? await input3({
|
|
2386
2534
|
message: "\u0110\u01B0\u1EDDng d\u1EABn folder hi\u1EC7n c\xF3:",
|
|
2387
2535
|
validate: (v) => v.length > 0 ? true : "Path b\u1EAFt bu\u1ED9c"
|
|
2388
2536
|
})
|
|
@@ -2394,7 +2542,7 @@ async function runInitFromExistingFolder(opts, ownerEmail) {
|
|
|
2394
2542
|
const remoteUrl = await getOrCreateOriginRemote(folderPath, opts);
|
|
2395
2543
|
const teamOwner = opts.teamOwner ?? await promptTeamOwner(ownerEmail);
|
|
2396
2544
|
const inferredName = opts.workspaceName ?? `${basename(folderPath)}-avatar-workspace`;
|
|
2397
|
-
const workspaceName = opts.workspaceName ?? await
|
|
2545
|
+
const workspaceName = opts.workspaceName ?? await input3({ message: "T\xEAn workspace:", default: inferredName });
|
|
2398
2546
|
const workspaceParent = resolve(opts.workspaceParent ?? ".");
|
|
2399
2547
|
const workspacePath = await resolveWorkspacePath(workspaceParent, workspaceName, opts.force);
|
|
2400
2548
|
await scaffoldWorkspaceWithSrcSubmodule({
|
|
@@ -2412,16 +2560,17 @@ async function runInitFromExistingFolder(opts, ownerEmail) {
|
|
|
2412
2560
|
repoVisibility: opts.repoVisibility,
|
|
2413
2561
|
repoOrg: opts.repoOrg,
|
|
2414
2562
|
flow: "existing-folder",
|
|
2415
|
-
aiSkip: opts.aiSkip
|
|
2563
|
+
aiSkip: opts.aiSkip,
|
|
2564
|
+
ssoEmail: ownerEmail
|
|
2416
2565
|
});
|
|
2417
2566
|
}
|
|
2418
2567
|
async function runInitFromScratch(opts, ownerEmail) {
|
|
2419
2568
|
await ensureGitHubReady();
|
|
2420
|
-
const projectName = opts.workspaceName ?? await
|
|
2569
|
+
const projectName = opts.workspaceName ?? await input3({
|
|
2421
2570
|
message: "T\xEAn d\u1EF1 \xE1n:",
|
|
2422
2571
|
validate: (v) => v.length > 0 ? true : "T\xEAn b\u1EAFt bu\u1ED9c"
|
|
2423
2572
|
});
|
|
2424
|
-
const visibility = opts.repoVisibility ?? await
|
|
2573
|
+
const visibility = opts.repoVisibility ?? await select7({
|
|
2425
2574
|
message: "Visibility?",
|
|
2426
2575
|
choices: [
|
|
2427
2576
|
{ name: "private (m\u1EB7c \u0111\u1ECBnh)", value: "private" },
|
|
@@ -2449,7 +2598,12 @@ async function runInitFromScratch(opts, ownerEmail) {
|
|
|
2449
2598
|
await git(workspacePath).subModule(["add", urls.sshUrl, "src"]);
|
|
2450
2599
|
let pinnedTag = "HEAD";
|
|
2451
2600
|
if (!opts.skipTeamPack) {
|
|
2452
|
-
|
|
2601
|
+
sp.stop();
|
|
2602
|
+
const result = await addTeamPackSubmoduleWithRetryOnNetworkFail(
|
|
2603
|
+
workspacePath,
|
|
2604
|
+
opts.packVersion,
|
|
2605
|
+
ownerEmail
|
|
2606
|
+
);
|
|
2453
2607
|
pinnedTag = result.pinnedTag ?? "HEAD";
|
|
2454
2608
|
sp.succeed(`Pin team-ai-pack v\xE0o ${pinnedTag}`);
|
|
2455
2609
|
} else {
|
|
@@ -2490,14 +2644,14 @@ async function getOrCreateOriginRemote(folderPath, opts) {
|
|
|
2490
2644
|
return void 0;
|
|
2491
2645
|
}
|
|
2492
2646
|
await ensureGitHubReady();
|
|
2493
|
-
const visibility = opts.repoVisibility ?? await
|
|
2647
|
+
const visibility = opts.repoVisibility ?? await select7({
|
|
2494
2648
|
message: "Visibility?",
|
|
2495
2649
|
choices: [
|
|
2496
2650
|
{ name: "private (m\u1EB7c \u0111\u1ECBnh)", value: "private" },
|
|
2497
2651
|
{ name: "public", value: "public" }
|
|
2498
2652
|
]
|
|
2499
2653
|
});
|
|
2500
|
-
const repoName = await
|
|
2654
|
+
const repoName = await input3({
|
|
2501
2655
|
message: "T\xEAn repo:",
|
|
2502
2656
|
default: basename(folderPath)
|
|
2503
2657
|
});
|
|
@@ -2519,7 +2673,12 @@ async function scaffoldWorkspaceWithSrcSubmodule(args) {
|
|
|
2519
2673
|
await git(args.workspacePath).subModule(["add", args.srcRemoteUrl, "src"]);
|
|
2520
2674
|
let pinnedTag = "HEAD";
|
|
2521
2675
|
if (!args.skipTeamPack) {
|
|
2522
|
-
|
|
2676
|
+
sp.stop();
|
|
2677
|
+
const result = await addTeamPackSubmoduleWithRetryOnNetworkFail(
|
|
2678
|
+
args.workspacePath,
|
|
2679
|
+
args.packVersion,
|
|
2680
|
+
args.ssoEmail
|
|
2681
|
+
);
|
|
2523
2682
|
pinnedTag = result.pinnedTag ?? "HEAD";
|
|
2524
2683
|
sp.succeed(`Pin team-ai-pack v\xE0o ${pinnedTag}`);
|
|
2525
2684
|
} else {
|
|
@@ -2587,43 +2746,79 @@ async function maybeCreateWorkspaceRemote(args) {
|
|
|
2587
2746
|
});
|
|
2588
2747
|
}
|
|
2589
2748
|
if (!shouldCreate) return;
|
|
2590
|
-
const visibility = args.repoVisibility ?? (args.autoYes ? "private" : await
|
|
2749
|
+
const visibility = args.repoVisibility ?? (args.autoYes ? "private" : await select7({
|
|
2591
2750
|
message: "Workspace visibility?",
|
|
2592
2751
|
choices: [
|
|
2593
2752
|
{ name: "private (m\u1EB7c \u0111\u1ECBnh, an to\xE0n)", value: "private" },
|
|
2594
2753
|
{ name: "public", value: "public" }
|
|
2595
2754
|
]
|
|
2596
2755
|
}));
|
|
2597
|
-
|
|
2598
|
-
|
|
2599
|
-
|
|
2600
|
-
|
|
2601
|
-
|
|
2602
|
-
|
|
2603
|
-
|
|
2604
|
-
|
|
2605
|
-
|
|
2606
|
-
|
|
2756
|
+
while (true) {
|
|
2757
|
+
try {
|
|
2758
|
+
await createWorkspaceRemoteViaGh({
|
|
2759
|
+
workspacePath: args.workspacePath,
|
|
2760
|
+
workspaceName: args.workspaceName,
|
|
2761
|
+
visibility,
|
|
2762
|
+
org: args.repoOrg
|
|
2763
|
+
});
|
|
2764
|
+
return;
|
|
2765
|
+
} catch (err) {
|
|
2766
|
+
const action = await promptRetryOrSkip({
|
|
2767
|
+
taskName: "T\u1EA1o workspace remote tr\xEAn GitHub",
|
|
2768
|
+
reason: err instanceof Error ? err.message : String(err),
|
|
2769
|
+
allowSkip: true,
|
|
2770
|
+
// Workspace remote OPTIONAL — skip OK, workspace local vẫn dùng được.
|
|
2771
|
+
hint: "Tip: sai org? Pass --repo-org=<your-gh-user>. Ho\u1EB7c switch gh: gh auth login."
|
|
2772
|
+
});
|
|
2773
|
+
if (action === "abort") {
|
|
2774
|
+
throw new UserAbortedRecoveryError("User abort t\u1EA1i b\u01B0\u1EDBc t\u1EA1o workspace remote.");
|
|
2775
|
+
}
|
|
2776
|
+
if (action === "skip") {
|
|
2777
|
+
log.warn("Workspace v\u1EABn s\u1EB5n s\xE0ng local-only. Setup remote sau khi c\u1EA7n.");
|
|
2778
|
+
return;
|
|
2779
|
+
}
|
|
2780
|
+
}
|
|
2607
2781
|
}
|
|
2608
2782
|
}
|
|
2609
2783
|
async function resolveWorkspacePath(parent, desiredName, force) {
|
|
2610
2784
|
const desired = join16(parent, desiredName);
|
|
2611
2785
|
if (await isEmptyOrMissing(desired)) return desired;
|
|
2612
|
-
const alternative = await findAlternativeWorkspaceName(parent, desiredName);
|
|
2613
|
-
if (!alternative) {
|
|
2614
|
-
throw new Error(`Kh\xF4ng t\xECm \u0111\u01B0\u1EE3c workspace path kh\u1EA3 d\u1EE5ng trong ${parent}`);
|
|
2615
|
-
}
|
|
2616
2786
|
log.warn(`Workspace path "${desired}" \u0111\xE3 c\xF3 n\u1ED9i dung.`);
|
|
2617
|
-
|
|
2618
|
-
|
|
2619
|
-
|
|
2787
|
+
while (true) {
|
|
2788
|
+
const alternative = await findAlternativeWorkspaceName(parent, desiredName);
|
|
2789
|
+
if (force && alternative) {
|
|
2790
|
+
log.info(`--force: d\xF9ng ${alternative}`);
|
|
2791
|
+
return alternative;
|
|
2792
|
+
}
|
|
2793
|
+
const choices = [];
|
|
2794
|
+
if (alternative) {
|
|
2795
|
+
choices.push({ name: `D\xF9ng "${alternative}" (suggest)`, value: "use-alt" });
|
|
2796
|
+
}
|
|
2797
|
+
choices.push({ name: "Nh\u1EADp t\xEAn workspace kh\xE1c (manual)", value: "manual" });
|
|
2798
|
+
choices.push({ name: "T\u1EA1m ng\u01B0ng init", value: "abort" });
|
|
2799
|
+
const action = await select7({
|
|
2800
|
+
message: "C\xE1ch x\u1EED l\xFD workspace path conflict?",
|
|
2801
|
+
choices
|
|
2802
|
+
});
|
|
2803
|
+
if (action === "abort") {
|
|
2804
|
+
throw new UserAbortedRecoveryError(
|
|
2805
|
+
"User abort t\u1EA1i b\u01B0\u1EDBc resolve workspace path. Ch\u1EA1y l\u1EA1i v\u1EDBi --workspace-name kh\xE1c."
|
|
2806
|
+
);
|
|
2807
|
+
}
|
|
2808
|
+
if (action === "use-alt" && alternative) {
|
|
2809
|
+
return alternative;
|
|
2810
|
+
}
|
|
2811
|
+
const newName = await input3({
|
|
2812
|
+
message: "T\xEAn workspace m\u1EDBi:",
|
|
2813
|
+
validate: (v) => v.trim().length > 0 ? true : "T\xEAn kh\xF4ng \u0111\u01B0\u1EE3c r\u1ED7ng"
|
|
2814
|
+
});
|
|
2815
|
+
const newPath = join16(parent, newName.trim());
|
|
2816
|
+
if (await isEmptyOrMissing(newPath)) return newPath;
|
|
2817
|
+
log.warn(`"${newPath}" c\u0169ng \u0111\xE3 c\xF3 n\u1ED9i dung. Th\u1EED t\xEAn kh\xE1c.`);
|
|
2620
2818
|
}
|
|
2621
|
-
const useAlt = await confirm3({ message: `D\xF9ng "${alternative}" thay th\u1EBF?`, default: true });
|
|
2622
|
-
if (!useAlt) throw new Error("H\u1EE7y init. Ch\u1EA1y l\u1EA1i v\u1EDBi --workspace-name kh\xE1c.");
|
|
2623
|
-
return alternative;
|
|
2624
2819
|
}
|
|
2625
2820
|
async function promptTeamOwner(currentUserEmail) {
|
|
2626
|
-
return await
|
|
2821
|
+
return await input3({ message: "Team owner email:", default: currentUserEmail });
|
|
2627
2822
|
}
|
|
2628
2823
|
async function maybeCommitWorkspace(workspacePath, skipCommit) {
|
|
2629
2824
|
if (skipCommit) {
|
|
@@ -2659,7 +2854,7 @@ function printInitSuccessBox(rootPath, flow, aiResult = null) {
|
|
|
2659
2854
|
` ${chalk.cyan("avatar sync")} Pull team-ai-pack m\u1EDBi`,
|
|
2660
2855
|
` ${chalk.cyan("avatar uninstall")} G\u1EE1 Avatar (gi\u1EEF code)`
|
|
2661
2856
|
];
|
|
2662
|
-
process.stdout.write(`${
|
|
2857
|
+
process.stdout.write(`${boxen4(lines.join("\n"), { padding: 1, borderStyle: "round" })}
|
|
2663
2858
|
`);
|
|
2664
2859
|
}
|
|
2665
2860
|
|
|
@@ -2696,7 +2891,7 @@ function registerSecretsCommand(program2) {
|
|
|
2696
2891
|
// src/commands/status.ts
|
|
2697
2892
|
import { promises as fs8 } from "fs";
|
|
2698
2893
|
import { join as join18 } from "path";
|
|
2699
|
-
import
|
|
2894
|
+
import boxen5 from "boxen";
|
|
2700
2895
|
|
|
2701
2896
|
// src/lib/pack-backup-manager.ts
|
|
2702
2897
|
import { promises as fs7 } from "fs";
|
|
@@ -2774,7 +2969,7 @@ function renderStatusBox(s) {
|
|
|
2774
2969
|
`${chalk.dim("Backups:")} ${s.backupCount}`,
|
|
2775
2970
|
`${chalk.dim("Tech stack:")} ${s.techStackSummary}`
|
|
2776
2971
|
];
|
|
2777
|
-
process.stdout.write(`${
|
|
2972
|
+
process.stdout.write(`${boxen5(lines.join("\n"), { padding: 1, borderStyle: "round" })}
|
|
2778
2973
|
`);
|
|
2779
2974
|
}
|
|
2780
2975
|
|
|
@@ -2794,7 +2989,7 @@ function registerToolsCommand(program2) {
|
|
|
2794
2989
|
// src/commands/uninstall.ts
|
|
2795
2990
|
import { relative as relative3 } from "path";
|
|
2796
2991
|
import { confirm as confirm4 } from "@inquirer/prompts";
|
|
2797
|
-
import
|
|
2992
|
+
import boxen6 from "boxen";
|
|
2798
2993
|
|
|
2799
2994
|
// src/lib/create-uninstall-backup-snapshot.ts
|
|
2800
2995
|
import { cp, mkdir, writeFile } from "fs/promises";
|
|
@@ -2948,7 +3143,7 @@ async function removeSubmoduleEntry(gitmodulesPath, submodulePath) {
|
|
|
2948
3143
|
}
|
|
2949
3144
|
|
|
2950
3145
|
// src/commands/uninstall.ts
|
|
2951
|
-
var CLI_VERSION = "1.2.
|
|
3146
|
+
var CLI_VERSION = "1.2.6";
|
|
2952
3147
|
function registerUninstallCommand(program2) {
|
|
2953
3148
|
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) => {
|
|
2954
3149
|
try {
|
|
@@ -3025,12 +3220,12 @@ function printUninstallSuccessBox(backupPath) {
|
|
|
3025
3220
|
lines.push(` ${chalk.dim("Backup:")} ${backupPath}`);
|
|
3026
3221
|
lines.push(` ${chalk.dim("Restore:")} ${chalk.cyan(`cp -r "${backupPath}"/* .`)}`);
|
|
3027
3222
|
}
|
|
3028
|
-
process.stdout.write(`${
|
|
3223
|
+
process.stdout.write(`${boxen6(lines.join("\n"), { padding: 1, borderStyle: "round" })}
|
|
3029
3224
|
`);
|
|
3030
3225
|
}
|
|
3031
3226
|
|
|
3032
3227
|
// src/index.ts
|
|
3033
|
-
var CLI_VERSION2 = "1.2.
|
|
3228
|
+
var CLI_VERSION2 = "1.2.6";
|
|
3034
3229
|
var program = new Command();
|
|
3035
3230
|
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(
|
|
3036
3231
|
"beforeAll",
|