@nalvietnam/avatar-cli 1.0.1 → 1.1.1
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/bin/avatar.js +0 -0
- package/bin/postinstall.js +13 -0
- package/dist/hooks/post-merge.sh.tpl +24 -0
- package/dist/hooks/pre-push.sh.tpl +33 -0
- package/dist/index.js +855 -180
- package/dist/index.js.map +1 -1
- package/dist/lib/print-welcome-screen.js +117 -0
- package/dist/lib/print-welcome-screen.js.map +1 -0
- package/dist/templates/CLAUDE.md.tpl +40 -0
- package/dist/templates/gitignore/generic.txt +20 -0
- package/dist/templates/gitignore/go.txt +16 -0
- package/dist/templates/gitignore/java.txt +23 -0
- package/dist/templates/gitignore/node.txt +21 -0
- package/dist/templates/gitignore/python.txt +26 -0
- package/dist/templates/gitignore/ruby.txt +16 -0
- package/dist/templates/gitignore/rust.txt +5 -0
- package/dist/templates/gitignore.tpl +4 -0
- package/dist/templates/project/architecture.md.tpl +27 -0
- package/dist/templates/project/conventions.md.tpl +27 -0
- package/dist/templates/project/domain.md.tpl +23 -0
- package/dist/templates/project/gotchas.md.tpl +28 -0
- package/dist/templates/project/tech-stack.md.tpl +32 -0
- package/dist/templates/settings.json.tpl +32 -0
- package/package.json +3 -2
- package/src/templates/gitignore/generic.txt +20 -0
- package/src/templates/gitignore/go.txt +16 -0
- package/src/templates/gitignore/java.txt +23 -0
- package/src/templates/gitignore/node.txt +21 -0
- package/src/templates/gitignore/python.txt +26 -0
- package/src/templates/gitignore/ruby.txt +16 -0
- package/src/templates/gitignore/rust.txt +5 -0
package/dist/index.js
CHANGED
|
@@ -166,7 +166,6 @@ async function loadHook(name) {
|
|
|
166
166
|
}
|
|
167
167
|
|
|
168
168
|
// src/lib/project-tree-scaffolder.ts
|
|
169
|
-
var AVATAR_MANAGED_PATHS = [".claude", "CLAUDE.md", ".gitmodules"];
|
|
170
169
|
async function backupIfExists(path) {
|
|
171
170
|
if (!await pathExists(path)) return null;
|
|
172
171
|
const ts = (/* @__PURE__ */ new Date()).toISOString().replace(/[:.]/g, "-");
|
|
@@ -232,8 +231,8 @@ async function writeProjectKnowledgeFiles(projectRoot, vars) {
|
|
|
232
231
|
const backups = [];
|
|
233
232
|
for (const tpl of PROJECT_KNOWLEDGE_TEMPLATES) {
|
|
234
233
|
const content = await renderTemplateByName(tpl, baseVars);
|
|
235
|
-
const
|
|
236
|
-
const outPath = join4(projectRoot, ".claude", "project",
|
|
234
|
+
const relative4 = tpl.replace(/^project\//, "");
|
|
235
|
+
const outPath = join4(projectRoot, ".claude", "project", relative4);
|
|
237
236
|
const backup = await writeWithBackup(outPath, content);
|
|
238
237
|
if (backup) backups.push(backup);
|
|
239
238
|
}
|
|
@@ -479,7 +478,7 @@ async function applyFixes(checks) {
|
|
|
479
478
|
}
|
|
480
479
|
|
|
481
480
|
// src/commands/init.ts
|
|
482
|
-
import { join as
|
|
481
|
+
import { basename, join as join13, relative as relative2, resolve } from "path";
|
|
483
482
|
import { confirm, input, select } from "@inquirer/prompts";
|
|
484
483
|
import boxen2 from "boxen";
|
|
485
484
|
|
|
@@ -497,15 +496,406 @@ async function appendAuditEntry(action, detail) {
|
|
|
497
496
|
await fs4.appendFile(AUDIT_LOG_PATH, line, "utf8");
|
|
498
497
|
}
|
|
499
498
|
|
|
500
|
-
// src/lib/
|
|
499
|
+
// src/lib/avatar-ascii-banner.ts
|
|
500
|
+
import chalk2 from "chalk";
|
|
501
|
+
var BANNER_LINES = [
|
|
502
|
+
" \u2588\u2588\u2588\u2588\u2588\u2557 \u2588\u2588\u2557 \u2588\u2588\u2557 \u2588\u2588\u2588\u2588\u2588\u2557 \u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2557 \u2588\u2588\u2588\u2588\u2588\u2557 \u2588\u2588\u2588\u2588\u2588\u2588\u2557 ",
|
|
503
|
+
"\u2588\u2588\u2554\u2550\u2550\u2588\u2588\u2557\u2588\u2588\u2551 \u2588\u2588\u2551\u2588\u2588\u2554\u2550\u2550\u2588\u2588\u2557\u255A\u2550\u2550\u2588\u2588\u2554\u2550\u2550\u255D\u2588\u2588\u2554\u2550\u2550\u2588\u2588\u2557\u2588\u2588\u2554\u2550\u2550\u2588\u2588\u2557",
|
|
504
|
+
"\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2551\u2588\u2588\u2551 \u2588\u2588\u2551\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2551 \u2588\u2588\u2551 \u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2551\u2588\u2588\u2588\u2588\u2588\u2588\u2554\u255D",
|
|
505
|
+
"\u2588\u2588\u2554\u2550\u2550\u2588\u2588\u2551\u255A\u2588\u2588\u2557 \u2588\u2588\u2554\u255D\u2588\u2588\u2554\u2550\u2550\u2588\u2588\u2551 \u2588\u2588\u2551 \u2588\u2588\u2554\u2550\u2550\u2588\u2588\u2551\u2588\u2588\u2554\u2550\u2550\u2588\u2588\u2557",
|
|
506
|
+
"\u2588\u2588\u2551 \u2588\u2588\u2551 \u255A\u2588\u2588\u2588\u2588\u2554\u255D \u2588\u2588\u2551 \u2588\u2588\u2551 \u2588\u2588\u2551 \u2588\u2588\u2551 \u2588\u2588\u2551\u2588\u2588\u2551 \u2588\u2588\u2551",
|
|
507
|
+
"\u255A\u2550\u255D \u255A\u2550\u255D \u255A\u2550\u2550\u2550\u255D \u255A\u2550\u255D \u255A\u2550\u255D \u255A\u2550\u255D \u255A\u2550\u255D \u255A\u2550\u255D\u255A\u2550\u255D \u255A\u2550\u255D"
|
|
508
|
+
];
|
|
509
|
+
var GRADIENT_STOPS = [
|
|
510
|
+
[217, 79, 30],
|
|
511
|
+
// cam-cháy (#d94f1e)
|
|
512
|
+
[200, 70, 80],
|
|
513
|
+
// cam-hồng
|
|
514
|
+
[170, 70, 140],
|
|
515
|
+
// hồng-tím
|
|
516
|
+
[125, 88, 217]
|
|
517
|
+
// tím (#7d58d9)
|
|
518
|
+
];
|
|
519
|
+
function lerpChannel(a, b, t) {
|
|
520
|
+
return Math.round(a + (b - a) * t);
|
|
521
|
+
}
|
|
522
|
+
function gradientAt(t) {
|
|
523
|
+
const clamped = Math.max(0, Math.min(1, t));
|
|
524
|
+
const scaled = clamped * (GRADIENT_STOPS.length - 1);
|
|
525
|
+
const lo = Math.floor(scaled);
|
|
526
|
+
const hi = Math.min(GRADIENT_STOPS.length - 1, lo + 1);
|
|
527
|
+
const localT = scaled - lo;
|
|
528
|
+
const a = GRADIENT_STOPS[lo];
|
|
529
|
+
const b = GRADIENT_STOPS[hi];
|
|
530
|
+
return [
|
|
531
|
+
lerpChannel(a[0], b[0], localT),
|
|
532
|
+
lerpChannel(a[1], b[1], localT),
|
|
533
|
+
lerpChannel(a[2], b[2], localT)
|
|
534
|
+
];
|
|
535
|
+
}
|
|
536
|
+
function renderAvatarBanner(opts) {
|
|
537
|
+
const isTty = process.stdout.isTTY ?? false;
|
|
538
|
+
const supportsColor = isTty && chalk2.level > 0;
|
|
539
|
+
if (!supportsColor) {
|
|
540
|
+
return [...BANNER_LINES, ...opts?.tagline ? ["", opts.tagline] : []].join("\n");
|
|
541
|
+
}
|
|
542
|
+
const colored = BANNER_LINES.map((line, idx) => {
|
|
543
|
+
const t = BANNER_LINES.length === 1 ? 0 : idx / (BANNER_LINES.length - 1);
|
|
544
|
+
const [r, g, b] = gradientAt(t);
|
|
545
|
+
return chalk2.rgb(r, g, b).bold(line);
|
|
546
|
+
});
|
|
547
|
+
if (opts?.tagline) {
|
|
548
|
+
colored.push("");
|
|
549
|
+
colored.push(chalk2.dim(opts.tagline));
|
|
550
|
+
}
|
|
551
|
+
return colored.join("\n");
|
|
552
|
+
}
|
|
553
|
+
function printAvatarBanner(opts) {
|
|
554
|
+
process.stdout.write(`
|
|
555
|
+
${renderAvatarBanner(opts)}
|
|
556
|
+
|
|
557
|
+
`);
|
|
558
|
+
}
|
|
559
|
+
|
|
560
|
+
// src/lib/execute-gh-repo-create.ts
|
|
561
|
+
import { spawnSync as spawnSync2 } from "child_process";
|
|
562
|
+
var RepoAlreadyExistsError = class extends Error {
|
|
563
|
+
constructor(fullName) {
|
|
564
|
+
super(`Repo "${fullName}" \u0111\xE3 t\u1ED3n t\u1EA1i tr\xEAn GitHub. \u0110\u1ED5i t\xEAn ho\u1EB7c x\xF3a repo c\u0169.`);
|
|
565
|
+
this.name = "RepoAlreadyExistsError";
|
|
566
|
+
}
|
|
567
|
+
};
|
|
568
|
+
function executeGhRepoCreate(input2) {
|
|
569
|
+
const fullName = `${input2.org}/${input2.name}`;
|
|
570
|
+
const args = [
|
|
571
|
+
"repo",
|
|
572
|
+
"create",
|
|
573
|
+
fullName,
|
|
574
|
+
`--${input2.visibility}`,
|
|
575
|
+
"--source",
|
|
576
|
+
input2.folder,
|
|
577
|
+
"--remote",
|
|
578
|
+
"origin",
|
|
579
|
+
"--push"
|
|
580
|
+
];
|
|
581
|
+
const r = spawnSync2("gh", args, { stdio: "inherit" });
|
|
582
|
+
if (r.status !== 0) {
|
|
583
|
+
if (r.status === 1) {
|
|
584
|
+
throw new RepoAlreadyExistsError(fullName);
|
|
585
|
+
}
|
|
586
|
+
throw new Error(`gh repo create th\u1EA5t b\u1EA1i (exit ${r.status})`);
|
|
587
|
+
}
|
|
588
|
+
return {
|
|
589
|
+
sshUrl: `git@github.com:${fullName}.git`,
|
|
590
|
+
httpsUrl: `https://github.com/${fullName}.git`
|
|
591
|
+
};
|
|
592
|
+
}
|
|
593
|
+
|
|
594
|
+
// src/lib/resolve-github-username-default.ts
|
|
595
|
+
import { spawnSync as spawnSync3 } from "child_process";
|
|
596
|
+
function resolveGithubUsernameDefault() {
|
|
597
|
+
const r = spawnSync3("gh", ["api", "user", "--jq", ".login"], {
|
|
598
|
+
encoding: "utf8",
|
|
599
|
+
stdio: ["ignore", "pipe", "pipe"]
|
|
600
|
+
});
|
|
601
|
+
if (r.status !== 0) {
|
|
602
|
+
throw new Error(`Kh\xF4ng l\u1EA5y \u0111\u01B0\u1EE3c GitHub username: ${r.stderr?.trim()}`);
|
|
603
|
+
}
|
|
604
|
+
return r.stdout.trim();
|
|
605
|
+
}
|
|
606
|
+
|
|
607
|
+
// src/lib/validate-repo-name-and-visibility.ts
|
|
608
|
+
var REPO_NAME_REGEX = /^[a-zA-Z0-9._-]{1,100}$/;
|
|
609
|
+
var InvalidRepoNameError = class extends Error {
|
|
610
|
+
constructor(name) {
|
|
611
|
+
super(
|
|
612
|
+
`T\xEAn repo "${name}" kh\xF4ng h\u1EE3p l\u1EC7. Ch\u1EC9 d\xF9ng ch\u1EEF/s\u1ED1/d\u1EA5u ch\u1EA5m/g\u1EA1ch/underscore, d\xE0i 1-100 k\xFD t\u1EF1.`
|
|
613
|
+
);
|
|
614
|
+
this.name = "InvalidRepoNameError";
|
|
615
|
+
}
|
|
616
|
+
};
|
|
617
|
+
function validateRepoName(name) {
|
|
618
|
+
if (!REPO_NAME_REGEX.test(name)) {
|
|
619
|
+
throw new InvalidRepoNameError(name);
|
|
620
|
+
}
|
|
621
|
+
}
|
|
622
|
+
function validateRepoVisibility(v) {
|
|
623
|
+
if (v !== "private" && v !== "public") {
|
|
624
|
+
throw new Error(`Visibility ph\u1EA3i l\xE0 "private" ho\u1EB7c "public", nh\u1EADn: "${v}"`);
|
|
625
|
+
}
|
|
626
|
+
}
|
|
627
|
+
|
|
628
|
+
// src/lib/create-github-remote-from-folder.ts
|
|
629
|
+
function createGithubRemoteFromFolder(input2) {
|
|
630
|
+
validateRepoName(input2.name);
|
|
631
|
+
validateRepoVisibility(input2.visibility);
|
|
632
|
+
const org = input2.org ?? resolveGithubUsernameDefault();
|
|
633
|
+
log.info(`T\u1EA1o GitHub repo ${org}/${input2.name} (${input2.visibility})...`);
|
|
634
|
+
const urls = executeGhRepoCreate({
|
|
635
|
+
folder: input2.folder,
|
|
636
|
+
org,
|
|
637
|
+
name: input2.name,
|
|
638
|
+
visibility: input2.visibility
|
|
639
|
+
});
|
|
640
|
+
log.success(`\u0110\xE3 t\u1EA1o: ${urls.sshUrl}`);
|
|
641
|
+
return urls;
|
|
642
|
+
}
|
|
643
|
+
|
|
644
|
+
// src/lib/check-gh-cli-auth-status.ts
|
|
645
|
+
import { spawnSync as spawnSync4 } from "child_process";
|
|
646
|
+
function checkGhCliAuthStatus() {
|
|
647
|
+
const r = spawnSync4("gh", ["auth", "status"], { stdio: "ignore" });
|
|
648
|
+
if (r.error && r.error.code === "ENOENT") {
|
|
649
|
+
return "not-installed";
|
|
650
|
+
}
|
|
651
|
+
return r.status === 0 ? "authenticated" : "not-authenticated";
|
|
652
|
+
}
|
|
653
|
+
|
|
654
|
+
// src/lib/detect-package-manager.ts
|
|
655
|
+
import { spawnSync as spawnSync5 } from "child_process";
|
|
656
|
+
|
|
657
|
+
// src/lib/detect-host-platform.ts
|
|
658
|
+
import { platform } from "os";
|
|
659
|
+
function detectHostPlatform() {
|
|
660
|
+
const p = platform();
|
|
661
|
+
if (p === "darwin" || p === "linux" || p === "win32") return p;
|
|
662
|
+
return "unsupported";
|
|
663
|
+
}
|
|
664
|
+
|
|
665
|
+
// src/lib/detect-package-manager.ts
|
|
666
|
+
function hasBinary(name) {
|
|
667
|
+
const platform2 = detectHostPlatform();
|
|
668
|
+
const probe = platform2 === "win32" ? "where" : "command";
|
|
669
|
+
const args = platform2 === "win32" ? [name] : ["-v", name];
|
|
670
|
+
const r = spawnSync5(probe, args, {
|
|
671
|
+
shell: platform2 !== "win32",
|
|
672
|
+
stdio: "ignore"
|
|
673
|
+
});
|
|
674
|
+
return r.status === 0;
|
|
675
|
+
}
|
|
676
|
+
function detectPackageManager() {
|
|
677
|
+
const platform2 = detectHostPlatform();
|
|
678
|
+
const candidates = platform2 === "darwin" ? ["brew"] : platform2 === "win32" ? ["winget"] : platform2 === "linux" ? ["apt", "dnf", "pacman"] : [];
|
|
679
|
+
for (const pm of candidates) {
|
|
680
|
+
if (hasBinary(pm)) return pm;
|
|
681
|
+
}
|
|
682
|
+
return null;
|
|
683
|
+
}
|
|
684
|
+
|
|
685
|
+
// src/lib/install-gh-cli-via-package-manager.ts
|
|
686
|
+
import { spawnSync as spawnSync6 } from "child_process";
|
|
687
|
+
var INSTALL_COMMANDS = {
|
|
688
|
+
brew: { cmd: "brew", args: ["install", "gh"] },
|
|
689
|
+
apt: { cmd: "sudo", args: ["apt-get", "install", "-y", "gh"] },
|
|
690
|
+
dnf: { cmd: "sudo", args: ["dnf", "install", "-y", "gh"] },
|
|
691
|
+
pacman: { cmd: "sudo", args: ["pacman", "-S", "--noconfirm", "github-cli"] },
|
|
692
|
+
winget: { cmd: "winget", args: ["install", "--id", "GitHub.cli", "-e", "--silent"] }
|
|
693
|
+
};
|
|
694
|
+
function installGhCliViaPackageManager(pm) {
|
|
695
|
+
const spec = INSTALL_COMMANDS[pm];
|
|
696
|
+
log.info(`\u0110ang c\xE0i gh CLI qua ${pm}...`);
|
|
697
|
+
const r = spawnSync6(spec.cmd, spec.args, { stdio: "inherit" });
|
|
698
|
+
if (r.status !== 0) {
|
|
699
|
+
throw new Error(`C\xE0i gh CLI th\u1EA5t b\u1EA1i qua ${pm} (exit ${r.status}). C\xE0i tay r\u1ED3i ch\u1EA1y l\u1EA1i.`);
|
|
700
|
+
}
|
|
701
|
+
log.success("\u0110\xE3 c\xE0i gh CLI");
|
|
702
|
+
}
|
|
703
|
+
|
|
704
|
+
// src/lib/trigger-gh-cli-auth-login.ts
|
|
705
|
+
import { spawnSync as spawnSync7 } from "child_process";
|
|
706
|
+
function triggerGhCliAuthLogin() {
|
|
707
|
+
log.info("Kh\u1EDFi \u0111\u1ED9ng \u0111\u0103ng nh\u1EADp GitHub qua gh CLI (browser s\u1EBD m\u1EDF)...");
|
|
708
|
+
const r = spawnSync7(
|
|
709
|
+
"gh",
|
|
710
|
+
["auth", "login", "--hostname", "github.com", "--web", "--git-protocol", "ssh"],
|
|
711
|
+
{ stdio: "inherit" }
|
|
712
|
+
);
|
|
713
|
+
if (r.status !== 0) {
|
|
714
|
+
throw new Error(`gh auth login th\u1EA5t b\u1EA1i (exit ${r.status}). Th\u1EED 'gh auth login' tay.`);
|
|
715
|
+
}
|
|
716
|
+
log.success("\u0110\xE3 \u0111\u0103ng nh\u1EADp GitHub");
|
|
717
|
+
}
|
|
718
|
+
|
|
719
|
+
// src/lib/verify-git-remote-accessible.ts
|
|
720
|
+
import { spawnSync as spawnSync8 } from "child_process";
|
|
721
|
+
var TIMEOUT_MS = 5e3;
|
|
722
|
+
var RemoteNotAccessibleError = class extends Error {
|
|
723
|
+
constructor(url, reason) {
|
|
724
|
+
super(`Kh\xF4ng truy c\u1EADp \u0111\u01B0\u1EE3c remote ${url}: ${reason}`);
|
|
725
|
+
this.name = "RemoteNotAccessibleError";
|
|
726
|
+
}
|
|
727
|
+
};
|
|
728
|
+
function verifyGitRemoteAccessible(url) {
|
|
729
|
+
const r = spawnSync8("git", ["ls-remote", "--exit-code", url, "HEAD"], {
|
|
730
|
+
stdio: "ignore",
|
|
731
|
+
timeout: TIMEOUT_MS
|
|
732
|
+
});
|
|
733
|
+
if (r.status === 0) return;
|
|
734
|
+
if (r.signal === "SIGTERM") throw new RemoteNotAccessibleError(url, "timeout 5s");
|
|
735
|
+
throw new RemoteNotAccessibleError(url, `git ls-remote exit ${r.status}`);
|
|
736
|
+
}
|
|
737
|
+
|
|
738
|
+
// src/lib/git-auth-and-install-orchestrator.ts
|
|
739
|
+
async function ensureGitHubReady(remoteUrl) {
|
|
740
|
+
let state = checkGhCliAuthStatus();
|
|
741
|
+
if (state === "not-installed") {
|
|
742
|
+
log.warn("gh CLI ch\u01B0a c\xE0i. Avatar s\u1EBD t\u1EF1 c\xE0i.");
|
|
743
|
+
const pm = detectPackageManager();
|
|
744
|
+
if (!pm) {
|
|
745
|
+
throw new Error(
|
|
746
|
+
"Kh\xF4ng ph\xE1t hi\u1EC7n package manager (brew/apt/dnf/pacman/winget). C\xE0i gh CLI tay r\u1ED3i ch\u1EA1y l\u1EA1i: https://cli.github.com"
|
|
747
|
+
);
|
|
748
|
+
}
|
|
749
|
+
installGhCliViaPackageManager(pm);
|
|
750
|
+
state = checkGhCliAuthStatus();
|
|
751
|
+
}
|
|
752
|
+
if (state === "not-authenticated") {
|
|
753
|
+
log.warn("Ch\u01B0a \u0111\u0103ng nh\u1EADp GitHub.");
|
|
754
|
+
triggerGhCliAuthLogin();
|
|
755
|
+
state = checkGhCliAuthStatus();
|
|
756
|
+
if (state !== "authenticated") {
|
|
757
|
+
throw new Error("Sau gh auth login v\u1EABn ch\u01B0a authenticated. Th\u1EED l\u1EA1i.");
|
|
758
|
+
}
|
|
759
|
+
}
|
|
760
|
+
log.success("gh CLI s\u1EB5n s\xE0ng");
|
|
761
|
+
if (remoteUrl) {
|
|
762
|
+
verifyGitRemoteAccessible(remoteUrl);
|
|
763
|
+
log.success(`Remote accessible: ${remoteUrl}`);
|
|
764
|
+
}
|
|
765
|
+
}
|
|
766
|
+
|
|
767
|
+
// src/lib/check-folder-has-git.ts
|
|
768
|
+
import { existsSync as existsSync2, statSync } from "fs";
|
|
501
769
|
import { join as join7 } from "path";
|
|
770
|
+
function checkFolderHasGit(folderPath) {
|
|
771
|
+
const gitPath = join7(folderPath, ".git");
|
|
772
|
+
if (!existsSync2(gitPath)) return false;
|
|
773
|
+
const stat = statSync(gitPath);
|
|
774
|
+
return stat.isDirectory() || stat.isFile();
|
|
775
|
+
}
|
|
776
|
+
|
|
777
|
+
// src/lib/create-initial-git-commit.ts
|
|
778
|
+
import { simpleGit as simpleGit2 } from "simple-git";
|
|
779
|
+
var INITIAL_COMMIT_MESSAGE = "chore: initial commit";
|
|
780
|
+
async function createInitialGitCommit(folderPath) {
|
|
781
|
+
const g = simpleGit2({ baseDir: folderPath });
|
|
782
|
+
const isRepo = await g.checkIsRepo().catch(() => false);
|
|
783
|
+
if (!isRepo) {
|
|
784
|
+
await g.init();
|
|
785
|
+
}
|
|
786
|
+
try {
|
|
787
|
+
await g.branch(["-M", "main"]);
|
|
788
|
+
} catch {
|
|
789
|
+
}
|
|
790
|
+
await g.add(".");
|
|
791
|
+
const status = await g.status();
|
|
792
|
+
const hasCommits = (await g.raw(["rev-list", "-n", "1", "--all"]).catch(() => "")).trim();
|
|
793
|
+
if (hasCommits) return;
|
|
794
|
+
if (status.files.length === 0) {
|
|
795
|
+
await g.commit(INITIAL_COMMIT_MESSAGE, void 0, { "--allow-empty": null });
|
|
796
|
+
} else {
|
|
797
|
+
await g.commit(INITIAL_COMMIT_MESSAGE);
|
|
798
|
+
}
|
|
799
|
+
}
|
|
800
|
+
|
|
801
|
+
// src/lib/detect-folder-tech-stack.ts
|
|
802
|
+
import { existsSync as existsSync3 } from "fs";
|
|
803
|
+
import { join as join8 } from "path";
|
|
804
|
+
var SIGNATURES = {
|
|
805
|
+
node: ["package.json"],
|
|
806
|
+
python: ["pyproject.toml", "requirements.txt", "setup.py", "Pipfile"],
|
|
807
|
+
go: ["go.mod"],
|
|
808
|
+
rust: ["Cargo.toml"],
|
|
809
|
+
java: ["pom.xml", "build.gradle", "build.gradle.kts"],
|
|
810
|
+
ruby: ["Gemfile"]
|
|
811
|
+
};
|
|
812
|
+
function detectFolderTechStack(folderPath) {
|
|
813
|
+
const matched = [];
|
|
814
|
+
for (const [stack, files] of Object.entries(SIGNATURES)) {
|
|
815
|
+
if (files.some((f) => existsSync3(join8(folderPath, f)))) {
|
|
816
|
+
matched.push(stack);
|
|
817
|
+
}
|
|
818
|
+
}
|
|
819
|
+
return matched.length > 0 ? matched : ["generic"];
|
|
820
|
+
}
|
|
821
|
+
|
|
822
|
+
// src/lib/gitignore-template-loader.ts
|
|
823
|
+
import { readFileSync } from "fs";
|
|
824
|
+
import { dirname as dirname3, join as join9 } from "path";
|
|
825
|
+
import { fileURLToPath as fileURLToPath2 } from "url";
|
|
826
|
+
var __dirname = dirname3(fileURLToPath2(import.meta.url));
|
|
827
|
+
var CANDIDATE_DIRS = [
|
|
828
|
+
join9(__dirname, "..", "templates", "gitignore"),
|
|
829
|
+
join9(__dirname, "..", "..", "src", "templates", "gitignore")
|
|
830
|
+
];
|
|
831
|
+
var AVATAR_MARKER_START = "# === avatar ===";
|
|
832
|
+
var AVATAR_MARKER_END = "# === /avatar ===";
|
|
833
|
+
function readTemplate(stack) {
|
|
834
|
+
for (const dir of CANDIDATE_DIRS) {
|
|
835
|
+
try {
|
|
836
|
+
return readFileSync(join9(dir, `${stack}.txt`), "utf8");
|
|
837
|
+
} catch {
|
|
838
|
+
}
|
|
839
|
+
}
|
|
840
|
+
throw new Error(`Kh\xF4ng t\xECm th\u1EA5y template gitignore cho stack "${stack}"`);
|
|
841
|
+
}
|
|
842
|
+
function composeGitignoreContent(stacks) {
|
|
843
|
+
const all = ["generic", ...stacks.filter((s) => s !== "generic")];
|
|
844
|
+
const sections = all.map((s) => `# --- ${s} ---
|
|
845
|
+
${readTemplate(s).trim()}`);
|
|
846
|
+
return [AVATAR_MARKER_START, ...sections, AVATAR_MARKER_END, ""].join("\n");
|
|
847
|
+
}
|
|
848
|
+
|
|
849
|
+
// src/lib/write-or-merge-gitignore.ts
|
|
850
|
+
import { existsSync as existsSync4, readFileSync as readFileSync2, writeFileSync } from "fs";
|
|
851
|
+
import { join as join10 } from "path";
|
|
852
|
+
function writeOrMergeGitignore(folderPath, avatarBlock) {
|
|
853
|
+
const path = join10(folderPath, ".gitignore");
|
|
854
|
+
if (!existsSync4(path)) {
|
|
855
|
+
writeFileSync(path, avatarBlock, "utf8");
|
|
856
|
+
return;
|
|
857
|
+
}
|
|
858
|
+
const existing = readFileSync2(path, "utf8");
|
|
859
|
+
const startIdx = existing.indexOf(AVATAR_MARKER_START);
|
|
860
|
+
const endIdx = existing.indexOf(AVATAR_MARKER_END);
|
|
861
|
+
if (startIdx !== -1 && endIdx !== -1 && endIdx > startIdx) {
|
|
862
|
+
const before = existing.slice(0, startIdx);
|
|
863
|
+
const after = existing.slice(endIdx + AVATAR_MARKER_END.length);
|
|
864
|
+
writeFileSync(path, `${before.trimEnd()}
|
|
865
|
+
|
|
866
|
+
${avatarBlock}${after.trimStart()}`, "utf8");
|
|
867
|
+
return;
|
|
868
|
+
}
|
|
869
|
+
writeFileSync(path, `${existing.trimEnd()}
|
|
870
|
+
|
|
871
|
+
${avatarBlock}`, "utf8");
|
|
872
|
+
}
|
|
873
|
+
|
|
874
|
+
// src/lib/git-bootstrap-orchestrator.ts
|
|
875
|
+
async function bootstrapGitInFolder(folderPath) {
|
|
876
|
+
const hadGit = checkFolderHasGit(folderPath);
|
|
877
|
+
const stacks = detectFolderTechStack(folderPath);
|
|
878
|
+
log.info(`Tech stack detected: ${stacks.join(", ")}`);
|
|
879
|
+
writeOrMergeGitignore(folderPath, composeGitignoreContent(stacks));
|
|
880
|
+
log.success(".gitignore \u0111\xE3 ghi (Avatar block)");
|
|
881
|
+
if (!hadGit) {
|
|
882
|
+
log.info(`Bootstrap git cho ${folderPath}...`);
|
|
883
|
+
await createInitialGitCommit(folderPath);
|
|
884
|
+
log.success("\u0110\xE3 git init + initial commit");
|
|
885
|
+
} else {
|
|
886
|
+
log.dim("Folder \u0111\xE3 c\xF3 .git \u2014 skip init.");
|
|
887
|
+
}
|
|
888
|
+
}
|
|
889
|
+
|
|
890
|
+
// src/lib/team-pack-submodule-manager.ts
|
|
891
|
+
import { join as join11 } from "path";
|
|
502
892
|
var TEAM_PACK_REPO_URL = process.env.AVATAR_TEAM_PACK_REPO_URL ?? "https://github.com/LukeNALS/team-ai-pack.git";
|
|
503
893
|
var TEAM_PACK_RELATIVE_PATH = ".claude/pack";
|
|
504
894
|
async function addTeamPackSubmodule(projectRoot, tag) {
|
|
505
895
|
await addSubmodule(TEAM_PACK_REPO_URL, TEAM_PACK_RELATIVE_PATH, projectRoot);
|
|
506
896
|
let target = tag ?? null;
|
|
507
897
|
if (!target) {
|
|
508
|
-
target = await latestTag(
|
|
898
|
+
target = await latestTag(join11(projectRoot, TEAM_PACK_RELATIVE_PATH));
|
|
509
899
|
}
|
|
510
900
|
if (target) {
|
|
511
901
|
await checkoutTagInSubmodule(TEAM_PACK_RELATIVE_PATH, target, projectRoot);
|
|
@@ -513,7 +903,7 @@ async function addTeamPackSubmodule(projectRoot, tag) {
|
|
|
513
903
|
return { pinnedTag: target };
|
|
514
904
|
}
|
|
515
905
|
async function readPinnedPackVersion(projectRoot) {
|
|
516
|
-
const submoduleRoot =
|
|
906
|
+
const submoduleRoot = join11(projectRoot, TEAM_PACK_RELATIVE_PATH);
|
|
517
907
|
const tag = await latestTag(submoduleRoot);
|
|
518
908
|
if (tag) return tag;
|
|
519
909
|
const sha = await currentCommitSha(submoduleRoot);
|
|
@@ -522,7 +912,7 @@ async function readPinnedPackVersion(projectRoot) {
|
|
|
522
912
|
|
|
523
913
|
// src/commands/init-conflict-detection-helpers.ts
|
|
524
914
|
import { readdir } from "fs/promises";
|
|
525
|
-
import { join as
|
|
915
|
+
import { join as join12 } from "path";
|
|
526
916
|
async function isEmptyOrMissing(path) {
|
|
527
917
|
if (!await pathExists(path)) return true;
|
|
528
918
|
try {
|
|
@@ -533,16 +923,9 @@ async function isEmptyOrMissing(path) {
|
|
|
533
923
|
return false;
|
|
534
924
|
}
|
|
535
925
|
}
|
|
536
|
-
async function detectAvatarConflicts(projectRoot) {
|
|
537
|
-
const found = [];
|
|
538
|
-
for (const rel of AVATAR_MANAGED_PATHS) {
|
|
539
|
-
if (await pathExists(join8(projectRoot, rel))) found.push(rel);
|
|
540
|
-
}
|
|
541
|
-
return found;
|
|
542
|
-
}
|
|
543
926
|
async function findAlternativeWorkspaceName(parent, desiredName, maxAttempts = 10) {
|
|
544
927
|
for (let i = 2; i < maxAttempts; i++) {
|
|
545
|
-
const candidate =
|
|
928
|
+
const candidate = join12(parent, `${desiredName}-${i}`);
|
|
546
929
|
if (await isEmptyOrMissing(candidate)) return candidate;
|
|
547
930
|
}
|
|
548
931
|
return null;
|
|
@@ -550,9 +933,6 @@ async function findAlternativeWorkspaceName(parent, desiredName, maxAttempts = 1
|
|
|
550
933
|
|
|
551
934
|
// src/commands/init-scaffold-variable-builders.ts
|
|
552
935
|
var AVATAR_CLI_VERSION = "1.0.1";
|
|
553
|
-
function projectNameOf(projectRoot) {
|
|
554
|
-
return projectRoot.split("/").filter(Boolean).pop() ?? "avatar-project";
|
|
555
|
-
}
|
|
556
936
|
function inferWorkspaceName(repoUrl) {
|
|
557
937
|
const m = repoUrl.match(/[/:]([^/]+?)(\.git)?$/);
|
|
558
938
|
const base = m?.[1] ?? "client";
|
|
@@ -572,7 +952,7 @@ function buildScaffoldVariables(args) {
|
|
|
572
952
|
|
|
573
953
|
// src/commands/init.ts
|
|
574
954
|
function registerInitCommand(program2) {
|
|
575
|
-
program2.command("init").description("Kh\u1EDFi t\u1EA1o Avatar
|
|
955
|
+
program2.command("init").description("Kh\u1EDFi t\u1EA1o Avatar \u2014 3 flow t\u1EF1 nh\u1EADn di\u1EC7n (repo / folder / new)").option("--project-status <val>", "existing-remote | existing-folder | new-project").option("--folder-path <path>", "\u0110\u01B0\u1EDDng d\u1EABn folder hi\u1EC7n c\xF3 (flow existing-folder)").option("--create-remote", "Force t\u1EA1o remote qua gh (flow existing-folder ho\u1EB7c new-project)").option("--repo-visibility <val>", "private (m\u1EB7c \u0111\u1ECBnh) | public").option("--repo-org <name>", "GitHub org/owner cho repo m\u1EDBi").option("--client-repo <url>", "URL git remote (flow existing-remote)").option("--workspace-name <name>", "T\xEAn workspace").option("--workspace-parent <path>", "Th\u01B0 m\u1EE5c cha t\u1EA1o workspace (m\u1EB7c \u0111\u1ECBnh ..)").option("--pack-version <tag>", "Pin team-ai-pack v\xE0o tag c\u1EE5 th\u1EC3").option("--team-owner <email>", "Email team owner (b\u1ECF qua prompt)").option("--description <text>", "M\xF4 t\u1EA3 1 d\xF2ng c\u1EE7a d\u1EF1 \xE1n").option("--skip-scan", "B\u1ECF qua project-scanner sau scaffold").option("--force", "B\u1ECF qua prompt khi workspace path \u0111\xE3 t\u1ED3n t\u1EA1i").option("--yes", "Auto-confirm t\u1EA5t c\u1EA3 prompt").option("--mode <mode>", "[DEPRECATED] D\xF9ng --project-status thay th\u1EBF").action(async (opts) => {
|
|
576
956
|
try {
|
|
577
957
|
await runInit(opts);
|
|
578
958
|
} catch (err) {
|
|
@@ -582,140 +962,213 @@ function registerInitCommand(program2) {
|
|
|
582
962
|
});
|
|
583
963
|
}
|
|
584
964
|
async function runInit(opts) {
|
|
965
|
+
if (!opts.yes) printAvatarBanner({ tagline: "Kh\u1EDFi t\u1EA1o Avatar trong d\u1EF1 \xE1n c\u1EE7a b\u1EA1n" });
|
|
966
|
+
if (opts.mode) {
|
|
967
|
+
log.warn("Flag --mode \u0111\xE3 deprecated t\u1EEB v1.1. D\xF9ng --project-status thay th\u1EBF.");
|
|
968
|
+
}
|
|
585
969
|
const userConfig = await readUserConfig();
|
|
586
970
|
if (!userConfig || isTokenExpired(userConfig)) {
|
|
587
|
-
log.error("Ch\u01B0a \u0111\u0103ng nh\u1EADp ho\u1EB7c token
|
|
971
|
+
log.error("Ch\u01B0a \u0111\u0103ng nh\u1EADp ho\u1EB7c token h\u1EBFt h\u1EA1n. Ch\u1EA1y 'avatar login' tr\u01B0\u1EDBc.");
|
|
588
972
|
process.exit(1);
|
|
589
973
|
}
|
|
590
|
-
const
|
|
591
|
-
|
|
592
|
-
|
|
593
|
-
|
|
594
|
-
|
|
974
|
+
const status = opts.projectStatus ?? await promptProjectStatus();
|
|
975
|
+
switch (status) {
|
|
976
|
+
case "existing-remote":
|
|
977
|
+
await runInitFromExistingRemote(opts, userConfig.email);
|
|
978
|
+
break;
|
|
979
|
+
case "existing-folder":
|
|
980
|
+
await runInitFromExistingFolder(opts, userConfig.email);
|
|
981
|
+
break;
|
|
982
|
+
case "new-project":
|
|
983
|
+
await runInitFromScratch(opts, userConfig.email);
|
|
984
|
+
break;
|
|
595
985
|
}
|
|
596
986
|
}
|
|
597
|
-
async function
|
|
987
|
+
async function promptProjectStatus() {
|
|
598
988
|
return await select({
|
|
599
|
-
message: "\
|
|
989
|
+
message: "T\xECnh tr\u1EA1ng d\u1EF1 \xE1n c\u1EE7a b\u1EA1n?",
|
|
600
990
|
choices: [
|
|
601
|
-
{
|
|
602
|
-
|
|
603
|
-
|
|
604
|
-
},
|
|
605
|
-
{ name: "Client (Pattern A \u2014 t\xE1ch workspace)", value: "client" },
|
|
606
|
-
{ name: "Library/SDK public (t\xE1ch workspace)", value: "library" }
|
|
991
|
+
{ name: "1. \u0110\xE3 c\xF3 repo git remote (URL c\xF3 s\u1EB5n)", value: "existing-remote" },
|
|
992
|
+
{ name: "2. \u0110\xE3 c\xF3 folder code local", value: "existing-folder" },
|
|
993
|
+
{ name: "3. D\u1EF1 \xE1n m\u1EDBi ho\xE0n to\xE0n", value: "new-project" }
|
|
607
994
|
]
|
|
608
995
|
});
|
|
609
996
|
}
|
|
610
|
-
async function
|
|
611
|
-
const
|
|
612
|
-
|
|
613
|
-
|
|
614
|
-
}
|
|
615
|
-
const conflicts = await detectAvatarConflicts(projectRoot);
|
|
616
|
-
if (conflicts.length > 0) {
|
|
617
|
-
log.warn("C\xE1c path sau \u0111\xE3 t\u1ED3n t\u1EA1i \u2014 Avatar s\u1EBD backup t\u1EEBng file con b\u1ECB ghi \u0111\xE8:");
|
|
618
|
-
for (const c of conflicts) log.warn(` - ${c}`);
|
|
619
|
-
log.warn("File con c\u1EE7a b\u1EA1n kh\xF4ng b\u1ECB Avatar t\u1EA1o s\u1EBD \u0111\u01B0\u1EE3c gi\u1EEF nguy\xEAn.");
|
|
620
|
-
log.warn("Khuy\u1EBFn ngh\u1ECB mode 'client'/'library' n\u1EBFu mu\u1ED1n c\xF4 l\u1EADp ho\xE0n to\xE0n.");
|
|
621
|
-
if (!opts.force) {
|
|
622
|
-
const proceed = await confirm({
|
|
623
|
-
message: "Ti\u1EBFp t\u1EE5c backup-and-merge?",
|
|
624
|
-
default: true
|
|
625
|
-
});
|
|
626
|
-
if (!proceed) {
|
|
627
|
-
throw new Error("H\u1EE7y init. D\xF9ng mode 'client'/'library' ho\u1EB7c x\xF3a th\u1EE7 c\xF4ng c\xE1c file tr\xEAn.");
|
|
628
|
-
}
|
|
629
|
-
}
|
|
630
|
-
}
|
|
631
|
-
const teamOwner = opts.teamOwner ?? await promptTeamOwner(ownerEmail);
|
|
632
|
-
const projectName = projectNameOf(projectRoot);
|
|
633
|
-
const projectDescription = opts.description ?? await input({
|
|
634
|
-
message: "M\xF4 t\u1EA3 ng\u1EAFn 1 d\xF2ng c\u1EE7a d\u1EF1 \xE1n:",
|
|
635
|
-
default: `Avatar-managed project: ${projectName}`
|
|
997
|
+
async function runInitFromExistingRemote(opts, ownerEmail) {
|
|
998
|
+
const remoteUrl = opts.clientRepo ?? await input({
|
|
999
|
+
message: "URL git c\u1EE7a repo:",
|
|
1000
|
+
validate: (v) => v.length > 0 ? true : "URL b\u1EAFt bu\u1ED9c"
|
|
636
1001
|
});
|
|
637
|
-
|
|
638
|
-
|
|
639
|
-
|
|
640
|
-
|
|
641
|
-
|
|
642
|
-
|
|
643
|
-
|
|
644
|
-
|
|
645
|
-
|
|
646
|
-
|
|
647
|
-
const vars = buildScaffoldVariables({
|
|
648
|
-
projectName,
|
|
649
|
-
projectDescription,
|
|
1002
|
+
await ensureGitHubReady(remoteUrl);
|
|
1003
|
+
const teamOwner = opts.teamOwner ?? await promptTeamOwner(ownerEmail);
|
|
1004
|
+
const inferredName = inferWorkspaceName(remoteUrl);
|
|
1005
|
+
const workspaceName = opts.workspaceName ?? await input({ message: "T\xEAn workspace:", default: inferredName });
|
|
1006
|
+
const workspaceParent = resolve(opts.workspaceParent ?? "..");
|
|
1007
|
+
const workspacePath = await resolveWorkspacePath(workspaceParent, workspaceName, opts.force);
|
|
1008
|
+
await scaffoldWorkspaceWithSrcSubmodule({
|
|
1009
|
+
workspacePath,
|
|
1010
|
+
workspaceName,
|
|
1011
|
+
srcRemoteUrl: remoteUrl,
|
|
650
1012
|
teamOwner,
|
|
651
|
-
|
|
652
|
-
|
|
1013
|
+
description: opts.description ?? `Avatar workspace cho ${remoteUrl}`,
|
|
1014
|
+
packVersion: opts.packVersion,
|
|
1015
|
+
autoYes: opts.yes,
|
|
1016
|
+
flow: "existing-remote"
|
|
653
1017
|
});
|
|
654
|
-
|
|
655
|
-
|
|
656
|
-
|
|
657
|
-
|
|
658
|
-
|
|
659
|
-
|
|
660
|
-
|
|
661
|
-
|
|
662
|
-
|
|
663
|
-
|
|
664
|
-
for (const b of allBackups) log.info(` \u2192 ${relative2(projectRoot, b) || b}`);
|
|
665
|
-
}
|
|
666
|
-
await installGitHook(join9(projectRoot, ".git"), "post-merge");
|
|
667
|
-
log.success("C\xE0i git hook post-merge");
|
|
668
|
-
await appendAuditEntry("init", `mode=internal,project=${projectName}`);
|
|
669
|
-
await maybeCommit(projectRoot, "internal", opts.yes);
|
|
670
|
-
printInitSuccessBox(projectRoot, "internal");
|
|
671
|
-
}
|
|
672
|
-
async function runInitClientOrLibrary(opts, mode, ownerEmail) {
|
|
1018
|
+
}
|
|
1019
|
+
async function runInitFromExistingFolder(opts, ownerEmail) {
|
|
1020
|
+
const folderPath = resolve(
|
|
1021
|
+
opts.folderPath ?? await input({
|
|
1022
|
+
message: "\u0110\u01B0\u1EDDng d\u1EABn folder hi\u1EC7n c\xF3:",
|
|
1023
|
+
validate: (v) => v.length > 0 ? true : "Path b\u1EAFt bu\u1ED9c"
|
|
1024
|
+
})
|
|
1025
|
+
);
|
|
1026
|
+
await bootstrapGitInFolder(folderPath);
|
|
1027
|
+
const remoteUrl = await getOrCreateOriginRemote(folderPath, opts);
|
|
673
1028
|
const teamOwner = opts.teamOwner ?? await promptTeamOwner(ownerEmail);
|
|
674
|
-
const
|
|
675
|
-
|
|
676
|
-
validate: (v) => v.length > 0 ? true : "URL b\u1EAFt bu\u1ED9c"
|
|
677
|
-
});
|
|
678
|
-
const inferredName = inferWorkspaceName(clientRepoUrl);
|
|
679
|
-
const workspaceName = opts.workspaceName ?? await input({
|
|
680
|
-
message: "T\xEAn workspace:",
|
|
681
|
-
default: inferredName
|
|
682
|
-
});
|
|
1029
|
+
const inferredName = opts.workspaceName ?? `${basename(folderPath)}-avatar-workspace`;
|
|
1030
|
+
const workspaceName = opts.workspaceName ?? await input({ message: "T\xEAn workspace:", default: inferredName });
|
|
683
1031
|
const workspaceParent = resolve(opts.workspaceParent ?? "..");
|
|
684
1032
|
const workspacePath = await resolveWorkspacePath(workspaceParent, workspaceName, opts.force);
|
|
1033
|
+
await scaffoldWorkspaceWithSrcSubmodule({
|
|
1034
|
+
workspacePath,
|
|
1035
|
+
workspaceName,
|
|
1036
|
+
srcRemoteUrl: remoteUrl ?? folderPath,
|
|
1037
|
+
// fallback local path nếu user từ chối tạo remote
|
|
1038
|
+
teamOwner,
|
|
1039
|
+
description: opts.description ?? `Avatar workspace cho folder ${folderPath}`,
|
|
1040
|
+
packVersion: opts.packVersion,
|
|
1041
|
+
autoYes: opts.yes,
|
|
1042
|
+
flow: "existing-folder"
|
|
1043
|
+
});
|
|
1044
|
+
}
|
|
1045
|
+
async function runInitFromScratch(opts, ownerEmail) {
|
|
1046
|
+
await ensureGitHubReady();
|
|
1047
|
+
const projectName = opts.workspaceName ?? await input({
|
|
1048
|
+
message: "T\xEAn d\u1EF1 \xE1n:",
|
|
1049
|
+
validate: (v) => v.length > 0 ? true : "T\xEAn b\u1EAFt bu\u1ED9c"
|
|
1050
|
+
});
|
|
1051
|
+
const visibility = opts.repoVisibility ?? await select({
|
|
1052
|
+
message: "Visibility?",
|
|
1053
|
+
choices: [
|
|
1054
|
+
{ name: "private (m\u1EB7c \u0111\u1ECBnh)", value: "private" },
|
|
1055
|
+
{ name: "public", value: "public" }
|
|
1056
|
+
]
|
|
1057
|
+
});
|
|
1058
|
+
const teamOwner = opts.teamOwner ?? await promptTeamOwner(ownerEmail);
|
|
1059
|
+
const workspaceParent = resolve(opts.workspaceParent ?? ".");
|
|
1060
|
+
const workspacePath = await resolveWorkspacePath(workspaceParent, projectName, opts.force);
|
|
1061
|
+
const srcPath = join13(workspacePath, "src");
|
|
685
1062
|
await ensureDir(workspacePath);
|
|
1063
|
+
await ensureDir(srcPath);
|
|
1064
|
+
await bootstrapGitInFolder(srcPath);
|
|
1065
|
+
const urls = createGithubRemoteFromFolder({
|
|
1066
|
+
folder: srcPath,
|
|
1067
|
+
name: projectName,
|
|
1068
|
+
visibility,
|
|
1069
|
+
org: opts.repoOrg
|
|
1070
|
+
});
|
|
686
1071
|
await git(workspacePath).init();
|
|
687
|
-
const sp = spinner("Add submodule
|
|
1072
|
+
const sp = spinner("Add submodule src/ + team-ai-pack...");
|
|
688
1073
|
try {
|
|
689
|
-
await git(workspacePath).subModule(["add",
|
|
1074
|
+
await git(workspacePath).subModule(["add", urls.sshUrl, "src"]);
|
|
690
1075
|
const result = await addTeamPackSubmodule(workspacePath, opts.packVersion);
|
|
691
|
-
sp.succeed(`
|
|
692
|
-
|
|
693
|
-
|
|
694
|
-
|
|
1076
|
+
sp.succeed(`Pin team-ai-pack v\xE0o ${result.pinnedTag ?? "HEAD"}`);
|
|
1077
|
+
await finalizeWorkspaceScaffold({
|
|
1078
|
+
workspacePath,
|
|
1079
|
+
workspaceName: projectName,
|
|
695
1080
|
teamOwner,
|
|
1081
|
+
description: opts.description ?? `D\u1EF1 \xE1n m\u1EDBi: ${projectName}`,
|
|
1082
|
+
packVersion: result.pinnedTag ?? "HEAD",
|
|
1083
|
+
autoYes: opts.yes,
|
|
1084
|
+
flow: "new-project"
|
|
1085
|
+
});
|
|
1086
|
+
} catch (err) {
|
|
1087
|
+
sp.fail("Init workspace th\u1EA5t b\u1EA1i");
|
|
1088
|
+
throw err;
|
|
1089
|
+
}
|
|
1090
|
+
}
|
|
1091
|
+
async function getOrCreateOriginRemote(folderPath, opts) {
|
|
1092
|
+
const remotes = await git(folderPath).getRemotes(true);
|
|
1093
|
+
const origin = remotes.find((r) => r.name === "origin");
|
|
1094
|
+
if (origin?.refs.push) {
|
|
1095
|
+
log.success(`Folder \u0111\xE3 c\xF3 remote origin: ${origin.refs.push}`);
|
|
1096
|
+
return origin.refs.push;
|
|
1097
|
+
}
|
|
1098
|
+
const shouldCreate = opts.createRemote ?? await confirm({
|
|
1099
|
+
message: "Folder ch\u01B0a c\xF3 remote. T\u1EA1o GitHub repo ngay \u0111\u1EC3 share team?",
|
|
1100
|
+
default: true
|
|
1101
|
+
});
|
|
1102
|
+
if (!shouldCreate) {
|
|
1103
|
+
log.warn("Ti\u1EBFp t\u1EE5c v\u1EDBi local path. Workspace ch\u1EC9 ch\u1EA1y \u0111\u01B0\u1EE3c tr\xEAn m\xE1y b\u1EA1n.");
|
|
1104
|
+
return void 0;
|
|
1105
|
+
}
|
|
1106
|
+
await ensureGitHubReady();
|
|
1107
|
+
const visibility = opts.repoVisibility ?? await select({
|
|
1108
|
+
message: "Visibility?",
|
|
1109
|
+
choices: [
|
|
1110
|
+
{ name: "private (m\u1EB7c \u0111\u1ECBnh)", value: "private" },
|
|
1111
|
+
{ name: "public", value: "public" }
|
|
1112
|
+
]
|
|
1113
|
+
});
|
|
1114
|
+
const repoName = await input({
|
|
1115
|
+
message: "T\xEAn repo:",
|
|
1116
|
+
default: basename(folderPath)
|
|
1117
|
+
});
|
|
1118
|
+
const urls = createGithubRemoteFromFolder({
|
|
1119
|
+
folder: folderPath,
|
|
1120
|
+
name: repoName,
|
|
1121
|
+
visibility,
|
|
1122
|
+
org: opts.repoOrg
|
|
1123
|
+
});
|
|
1124
|
+
return urls.sshUrl;
|
|
1125
|
+
}
|
|
1126
|
+
async function scaffoldWorkspaceWithSrcSubmodule(args) {
|
|
1127
|
+
await ensureDir(args.workspacePath);
|
|
1128
|
+
await git(args.workspacePath).init();
|
|
1129
|
+
const sp = spinner("Add submodule src/ + team-ai-pack...");
|
|
1130
|
+
try {
|
|
1131
|
+
await git(args.workspacePath).subModule(["add", args.srcRemoteUrl, "src"]);
|
|
1132
|
+
const result = await addTeamPackSubmodule(args.workspacePath, args.packVersion);
|
|
1133
|
+
sp.succeed(`Pin team-ai-pack v\xE0o ${result.pinnedTag ?? "HEAD"}`);
|
|
1134
|
+
await finalizeWorkspaceScaffold({
|
|
1135
|
+
workspacePath: args.workspacePath,
|
|
1136
|
+
workspaceName: args.workspaceName,
|
|
1137
|
+
teamOwner: args.teamOwner,
|
|
1138
|
+
description: args.description,
|
|
696
1139
|
packVersion: result.pinnedTag ?? "HEAD",
|
|
697
|
-
|
|
1140
|
+
autoYes: args.autoYes,
|
|
1141
|
+
flow: args.flow
|
|
698
1142
|
});
|
|
699
|
-
await createClaudeDirTree(workspacePath);
|
|
700
|
-
await writeProjectKnowledgeFiles(workspacePath, vars);
|
|
701
|
-
await writeRootClaudeMd(workspacePath, vars);
|
|
702
|
-
await writeProjectSettings(workspacePath, vars);
|
|
703
|
-
await appendGitignoreEntries(workspacePath);
|
|
704
|
-
await ensureDir(join9(workspacePath, "notes"));
|
|
705
|
-
await ensureDir(join9(workspacePath, "scripts"));
|
|
706
|
-
await installGitHook(join9(workspacePath, ".git"), "post-merge");
|
|
707
|
-
await installGitHook(join9(workspacePath, ".git", "modules", "src"), "pre-push");
|
|
708
|
-
log.success("C\xE0i post-merge (workspace) + pre-push (src/)");
|
|
709
|
-
await appendAuditEntry("init", `mode=${mode},workspace=${workspaceName}`);
|
|
710
|
-
await maybeCommitWorkspace(workspacePath, opts.yes);
|
|
711
|
-
printInitSuccessBox(workspacePath, mode);
|
|
712
1143
|
} catch (err) {
|
|
713
1144
|
sp.fail("Init workspace th\u1EA5t b\u1EA1i");
|
|
714
1145
|
throw err;
|
|
715
1146
|
}
|
|
716
1147
|
}
|
|
1148
|
+
async function finalizeWorkspaceScaffold(args) {
|
|
1149
|
+
const vars = buildScaffoldVariables({
|
|
1150
|
+
projectName: args.workspaceName,
|
|
1151
|
+
projectDescription: args.description,
|
|
1152
|
+
teamOwner: args.teamOwner,
|
|
1153
|
+
packVersion: args.packVersion,
|
|
1154
|
+
mode: "client"
|
|
1155
|
+
});
|
|
1156
|
+
await createClaudeDirTree(args.workspacePath);
|
|
1157
|
+
await writeProjectKnowledgeFiles(args.workspacePath, vars);
|
|
1158
|
+
await writeRootClaudeMd(args.workspacePath, vars);
|
|
1159
|
+
await writeProjectSettings(args.workspacePath, vars);
|
|
1160
|
+
await appendGitignoreEntries(args.workspacePath);
|
|
1161
|
+
await ensureDir(join13(args.workspacePath, "notes"));
|
|
1162
|
+
await ensureDir(join13(args.workspacePath, "scripts"));
|
|
1163
|
+
await installGitHook(join13(args.workspacePath, ".git"), "post-merge");
|
|
1164
|
+
await installGitHook(join13(args.workspacePath, ".git", "modules", "src"), "pre-push");
|
|
1165
|
+
log.success("C\xE0i post-merge (workspace) + pre-push (src/)");
|
|
1166
|
+
await appendAuditEntry("init", `flow=${args.flow},workspace=${args.workspaceName}`);
|
|
1167
|
+
await maybeCommitWorkspace(args.workspacePath, args.autoYes);
|
|
1168
|
+
printInitSuccessBox(args.workspacePath, args.flow);
|
|
1169
|
+
}
|
|
717
1170
|
async function resolveWorkspacePath(parent, desiredName, force) {
|
|
718
|
-
const desired =
|
|
1171
|
+
const desired = join13(parent, desiredName);
|
|
719
1172
|
if (await isEmptyOrMissing(desired)) return desired;
|
|
720
1173
|
const alternative = await findAlternativeWorkspaceName(parent, desiredName);
|
|
721
1174
|
if (!alternative) {
|
|
@@ -726,63 +1179,34 @@ async function resolveWorkspacePath(parent, desiredName, force) {
|
|
|
726
1179
|
log.info(`--force: d\xF9ng ${alternative}`);
|
|
727
1180
|
return alternative;
|
|
728
1181
|
}
|
|
729
|
-
const useAlt = await confirm({
|
|
730
|
-
|
|
731
|
-
default: true
|
|
732
|
-
});
|
|
733
|
-
if (!useAlt) {
|
|
734
|
-
throw new Error("H\u1EE7y init. Ch\u1EA1y l\u1EA1i v\u1EDBi --workspace-name kh\xE1c.");
|
|
735
|
-
}
|
|
1182
|
+
const useAlt = await confirm({ message: `D\xF9ng "${alternative}" thay th\u1EBF?`, default: true });
|
|
1183
|
+
if (!useAlt) throw new Error("H\u1EE7y init. Ch\u1EA1y l\u1EA1i v\u1EDBi --workspace-name kh\xE1c.");
|
|
736
1184
|
return alternative;
|
|
737
1185
|
}
|
|
738
1186
|
async function promptTeamOwner(currentUserEmail) {
|
|
739
|
-
return await input({
|
|
740
|
-
message: "Team owner email:",
|
|
741
|
-
default: currentUserEmail
|
|
742
|
-
});
|
|
743
|
-
}
|
|
744
|
-
async function maybeCommit(projectRoot, mode, autoYes) {
|
|
745
|
-
const wantCommit = autoYes ?? await confirm({
|
|
746
|
-
message: "Commit ngay c\xE1c file Avatar \u0111\xE3 t\u1EA1o?",
|
|
747
|
-
default: true
|
|
748
|
-
});
|
|
749
|
-
if (!wantCommit) return;
|
|
750
|
-
const g = git(projectRoot);
|
|
751
|
-
await g.add([".claude/", "CLAUDE.md", ".gitignore", ".gitmodules"]);
|
|
752
|
-
await g.commit(`chore: initialize Avatar in ${mode} mode`);
|
|
753
|
-
log.success("\u0110\xE3 commit");
|
|
1187
|
+
return await input({ message: "Team owner email:", default: currentUserEmail });
|
|
754
1188
|
}
|
|
755
1189
|
async function maybeCommitWorkspace(workspacePath, autoYes) {
|
|
756
|
-
const wantCommit = autoYes ?? await confirm({
|
|
757
|
-
message: "Commit workspace ngay?",
|
|
758
|
-
default: true
|
|
759
|
-
});
|
|
1190
|
+
const wantCommit = autoYes ?? await confirm({ message: "Commit workspace ngay?", default: true });
|
|
760
1191
|
if (!wantCommit) return;
|
|
761
1192
|
const g = git(workspacePath);
|
|
762
1193
|
await g.add(["CLAUDE.md", ".claude/", ".gitignore", ".gitmodules", "notes/", "scripts/"]);
|
|
763
|
-
await g.commit("chore: initialize Avatar workspace
|
|
1194
|
+
await g.commit("chore: initialize Avatar workspace");
|
|
764
1195
|
log.success("\u0110\xE3 commit workspace");
|
|
765
1196
|
}
|
|
766
|
-
function printInitSuccessBox(rootPath,
|
|
767
|
-
const lines = [
|
|
768
|
-
|
|
769
|
-
|
|
770
|
-
|
|
771
|
-
|
|
772
|
-
|
|
773
|
-
|
|
774
|
-
}
|
|
775
|
-
|
|
776
|
-
|
|
777
|
-
|
|
778
|
-
|
|
779
|
-
lines.push("");
|
|
780
|
-
lines.push(` ${chalk.cyan("avatar commit --src")} Commit code kh\xE1ch l\xEAn client remote`);
|
|
781
|
-
lines.push(
|
|
782
|
-
` ${chalk.cyan("avatar commit --avatar")} Commit Avatar state l\xEAn workspace remote`
|
|
783
|
-
);
|
|
784
|
-
lines.push(` ${chalk.cyan("avatar sync")} Sync team pack`);
|
|
785
|
-
}
|
|
1197
|
+
function printInitSuccessBox(rootPath, flow) {
|
|
1198
|
+
const lines = [
|
|
1199
|
+
`${chalk.green("\u2713")} Workspace s\u1EB5n s\xE0ng: ${relative2(process.cwd(), rootPath) || rootPath}`,
|
|
1200
|
+
` ${chalk.dim(`(flow: ${flow})`)}`,
|
|
1201
|
+
"",
|
|
1202
|
+
` ${chalk.cyan(`cd ${rootPath}`)}`,
|
|
1203
|
+
` ${chalk.cyan("claude")} M\u1EDF Claude Code \u1EDF workspace root`,
|
|
1204
|
+
"",
|
|
1205
|
+
` ${chalk.cyan("avatar commit --src")} Commit code l\xEAn remote src`,
|
|
1206
|
+
` ${chalk.cyan("avatar commit --avatar")} Commit Avatar state`,
|
|
1207
|
+
` ${chalk.cyan("avatar sync")} Pull team-ai-pack m\u1EDBi`,
|
|
1208
|
+
` ${chalk.cyan("avatar uninstall")} G\u1EE1 Avatar (gi\u1EEF code)`
|
|
1209
|
+
];
|
|
786
1210
|
process.stdout.write(`${boxen2(lines.join("\n"), { padding: 1, borderStyle: "round" })}
|
|
787
1211
|
`);
|
|
788
1212
|
}
|
|
@@ -908,6 +1332,7 @@ function registerLoginCommand(program2) {
|
|
|
908
1332
|
});
|
|
909
1333
|
}
|
|
910
1334
|
async function runLogin(opts) {
|
|
1335
|
+
printAvatarBanner({ tagline: "\u0110\u0103ng nh\u1EADp Google SSO \xB7 workspace @nal.vn" });
|
|
911
1336
|
if (opts.reset) {
|
|
912
1337
|
await clearUserConfig();
|
|
913
1338
|
await appendAuditEntry("login_reset");
|
|
@@ -1008,15 +1433,15 @@ function registerSecretsCommand(program2) {
|
|
|
1008
1433
|
|
|
1009
1434
|
// src/commands/status.ts
|
|
1010
1435
|
import { promises as fs6 } from "fs";
|
|
1011
|
-
import { join as
|
|
1436
|
+
import { join as join15 } from "path";
|
|
1012
1437
|
import boxen4 from "boxen";
|
|
1013
1438
|
|
|
1014
1439
|
// src/lib/pack-backup-manager.ts
|
|
1015
1440
|
import { promises as fs5 } from "fs";
|
|
1016
|
-
import { join as
|
|
1441
|
+
import { join as join14 } from "path";
|
|
1017
1442
|
var BACKUP_DIR_NAME = "_backup";
|
|
1018
1443
|
async function listBackups(projectRoot) {
|
|
1019
|
-
const dir =
|
|
1444
|
+
const dir = join14(projectRoot, ".claude", BACKUP_DIR_NAME);
|
|
1020
1445
|
if (!await pathExists(dir)) return [];
|
|
1021
1446
|
const entries = await fs5.readdir(dir, { withFileTypes: true });
|
|
1022
1447
|
return entries.filter((e) => e.isDirectory()).map((e) => e.name).sort().reverse();
|
|
@@ -1042,7 +1467,7 @@ function registerStatusCommand(program2) {
|
|
|
1042
1467
|
}
|
|
1043
1468
|
async function gatherStatus(cwd) {
|
|
1044
1469
|
const projectName = cwd.split("/").filter(Boolean).pop() ?? "unknown";
|
|
1045
|
-
const claudeRoot =
|
|
1470
|
+
const claudeRoot = join15(cwd, ".claude");
|
|
1046
1471
|
const hasAvatar = await pathExists(claudeRoot);
|
|
1047
1472
|
if (!hasAvatar) {
|
|
1048
1473
|
return {
|
|
@@ -1055,8 +1480,8 @@ async function gatherStatus(cwd) {
|
|
|
1055
1480
|
hasAvatar: false
|
|
1056
1481
|
};
|
|
1057
1482
|
}
|
|
1058
|
-
const packVersion = await isGitRepo(
|
|
1059
|
-
const pendingDir =
|
|
1483
|
+
const packVersion = await isGitRepo(join15(claudeRoot, "pack")) ? await readPinnedPackVersion(cwd).catch(() => null) : null;
|
|
1484
|
+
const pendingDir = join15(claudeRoot, "_pending");
|
|
1060
1485
|
const pendingCount = await pathExists(pendingDir) ? (await fs6.readdir(pendingDir)).filter((n) => n.endsWith(".diff.md")).length : 0;
|
|
1061
1486
|
const backupCount = (await listBackups(cwd)).length;
|
|
1062
1487
|
const techStackSummary = await readTechStackFirstLine(claudeRoot);
|
|
@@ -1071,7 +1496,7 @@ async function gatherStatus(cwd) {
|
|
|
1071
1496
|
};
|
|
1072
1497
|
}
|
|
1073
1498
|
async function readTechStackFirstLine(claudeRoot) {
|
|
1074
|
-
const techStackPath =
|
|
1499
|
+
const techStackPath = join15(claudeRoot, "project", "tech-stack.md");
|
|
1075
1500
|
if (!await pathExists(techStackPath)) return "(no tech-stack.md)";
|
|
1076
1501
|
const content = await readText(techStackPath);
|
|
1077
1502
|
const firstNonHeaderLine = content.split("\n").find((l) => l.trim() && !l.startsWith("#") && !l.startsWith(">"));
|
|
@@ -1104,10 +1529,259 @@ function registerToolsCommand(program2) {
|
|
|
1104
1529
|
tools.command("remove <tool-id>").description("G\u1EE1 tool kh\u1ECFi ~/.claude.json (optional uninstall binary)").option("--keep-secrets", "Kh\xF4ng x\xF3a secrets kh\u1ECFi keychain").option("--keep-binary", "Kh\xF4ng uninstall npm global binary").action(notImplementedYet("tools remove", "Milestone 09"));
|
|
1105
1530
|
}
|
|
1106
1531
|
|
|
1532
|
+
// src/commands/uninstall.ts
|
|
1533
|
+
import { relative as relative3 } from "path";
|
|
1534
|
+
import { confirm as confirm2 } from "@inquirer/prompts";
|
|
1535
|
+
import boxen5 from "boxen";
|
|
1536
|
+
|
|
1537
|
+
// src/lib/create-uninstall-backup-snapshot.ts
|
|
1538
|
+
import { cp, mkdir, writeFile } from "fs/promises";
|
|
1539
|
+
import { homedir as homedir2 } from "os";
|
|
1540
|
+
import { basename as basename2, join as join16 } from "path";
|
|
1541
|
+
var UNINSTALL_BACKUPS_DIR = join16(homedir2(), ".avatar", "uninstall-backups");
|
|
1542
|
+
async function createUninstallBackupSnapshot(projectRoot, artifacts, avatarVersion) {
|
|
1543
|
+
const projectName = basename2(projectRoot);
|
|
1544
|
+
const timestamp = (/* @__PURE__ */ new Date()).toISOString().replace(/[:.]/g, "-");
|
|
1545
|
+
const backupDir = join16(UNINSTALL_BACKUPS_DIR, `${projectName}-${timestamp}`);
|
|
1546
|
+
await mkdir(backupDir, { recursive: true, mode: 448 });
|
|
1547
|
+
if (artifacts.claudeDir) {
|
|
1548
|
+
await cp(artifacts.claudeDir, join16(backupDir, ".claude"), { recursive: true });
|
|
1549
|
+
}
|
|
1550
|
+
if (artifacts.claudeMd) {
|
|
1551
|
+
await cp(artifacts.claudeMd, join16(backupDir, "CLAUDE.md"));
|
|
1552
|
+
}
|
|
1553
|
+
if (artifacts.postMergeHook || artifacts.prePushHook) {
|
|
1554
|
+
const hooksBackupDir = join16(backupDir, "hooks");
|
|
1555
|
+
await mkdir(hooksBackupDir, { recursive: true });
|
|
1556
|
+
if (artifacts.postMergeHook) {
|
|
1557
|
+
await cp(artifacts.postMergeHook, join16(hooksBackupDir, "post-merge"));
|
|
1558
|
+
}
|
|
1559
|
+
if (artifacts.prePushHook) {
|
|
1560
|
+
await cp(artifacts.prePushHook, join16(hooksBackupDir, "pre-push"));
|
|
1561
|
+
}
|
|
1562
|
+
}
|
|
1563
|
+
const manifest = {
|
|
1564
|
+
projectName,
|
|
1565
|
+
projectPath: projectRoot,
|
|
1566
|
+
timestamp,
|
|
1567
|
+
avatarVersion,
|
|
1568
|
+
artifacts: {
|
|
1569
|
+
claudeDir: !!artifacts.claudeDir,
|
|
1570
|
+
claudeMd: !!artifacts.claudeMd,
|
|
1571
|
+
postMergeHook: !!artifacts.postMergeHook,
|
|
1572
|
+
prePushHook: !!artifacts.prePushHook
|
|
1573
|
+
}
|
|
1574
|
+
};
|
|
1575
|
+
await writeFile(join16(backupDir, "manifest.json"), JSON.stringify(manifest, null, 2), "utf8");
|
|
1576
|
+
return backupDir;
|
|
1577
|
+
}
|
|
1578
|
+
|
|
1579
|
+
// src/lib/detect-avatar-project-artifacts.ts
|
|
1580
|
+
import { existsSync as existsSync5 } from "fs";
|
|
1581
|
+
import { join as join17 } from "path";
|
|
1582
|
+
function existsOrNull(path) {
|
|
1583
|
+
return existsSync5(path) ? path : null;
|
|
1584
|
+
}
|
|
1585
|
+
function detectAvatarProjectArtifacts(projectRoot) {
|
|
1586
|
+
const claudeDir = existsOrNull(join17(projectRoot, ".claude"));
|
|
1587
|
+
const claudeMd = existsOrNull(join17(projectRoot, "CLAUDE.md"));
|
|
1588
|
+
const postMergeHook = existsOrNull(join17(projectRoot, ".git", "hooks", "post-merge"));
|
|
1589
|
+
const prePushHook = existsOrNull(
|
|
1590
|
+
join17(projectRoot, ".git", "modules", "src", "hooks", "pre-push")
|
|
1591
|
+
);
|
|
1592
|
+
const gitignorePath = existsOrNull(join17(projectRoot, ".gitignore"));
|
|
1593
|
+
const gitmodulesPath = existsOrNull(join17(projectRoot, ".gitmodules"));
|
|
1594
|
+
const notesDir = existsOrNull(join17(projectRoot, "notes"));
|
|
1595
|
+
const scriptsDir = existsOrNull(join17(projectRoot, "scripts"));
|
|
1596
|
+
const hasAnyArtifact = !!(claudeDir || claudeMd || postMergeHook || prePushHook);
|
|
1597
|
+
return {
|
|
1598
|
+
hasAnyArtifact,
|
|
1599
|
+
claudeDir,
|
|
1600
|
+
claudeMd,
|
|
1601
|
+
postMergeHook,
|
|
1602
|
+
prePushHook,
|
|
1603
|
+
gitignorePath,
|
|
1604
|
+
gitmodulesPath,
|
|
1605
|
+
notesDir,
|
|
1606
|
+
scriptsDir
|
|
1607
|
+
};
|
|
1608
|
+
}
|
|
1609
|
+
|
|
1610
|
+
// src/lib/execute-uninstall-deletion.ts
|
|
1611
|
+
import { readFile, rm, writeFile as writeFile2 } from "fs/promises";
|
|
1612
|
+
async function executeUninstallDeletion(artifacts, flags) {
|
|
1613
|
+
if (artifacts.claudeDir) {
|
|
1614
|
+
if (flags.keepSubmodule) {
|
|
1615
|
+
const { readdir: readdir2 } = await import("fs/promises");
|
|
1616
|
+
const { join: join18 } = await import("path");
|
|
1617
|
+
const entries = await readdir2(artifacts.claudeDir);
|
|
1618
|
+
for (const entry of entries) {
|
|
1619
|
+
if (entry === "pack") continue;
|
|
1620
|
+
await rm(join18(artifacts.claudeDir, entry), { recursive: true, force: true });
|
|
1621
|
+
}
|
|
1622
|
+
} else {
|
|
1623
|
+
await rm(artifacts.claudeDir, { recursive: true, force: true });
|
|
1624
|
+
}
|
|
1625
|
+
}
|
|
1626
|
+
if (artifacts.claudeMd) {
|
|
1627
|
+
await rm(artifacts.claudeMd, { force: true });
|
|
1628
|
+
}
|
|
1629
|
+
if (!flags.keepHooks) {
|
|
1630
|
+
if (artifacts.postMergeHook) await rm(artifacts.postMergeHook, { force: true });
|
|
1631
|
+
if (artifacts.prePushHook) await rm(artifacts.prePushHook, { force: true });
|
|
1632
|
+
}
|
|
1633
|
+
if (artifacts.gitignorePath) {
|
|
1634
|
+
await stripAvatarBlockFromGitignore(artifacts.gitignorePath);
|
|
1635
|
+
}
|
|
1636
|
+
if (artifacts.gitmodulesPath && !flags.keepSubmodule) {
|
|
1637
|
+
await removeSubmoduleEntry(artifacts.gitmodulesPath, ".claude/pack");
|
|
1638
|
+
}
|
|
1639
|
+
for (const dir of [artifacts.notesDir, artifacts.scriptsDir]) {
|
|
1640
|
+
if (!dir) continue;
|
|
1641
|
+
const { readdir: readdir2 } = await import("fs/promises");
|
|
1642
|
+
const entries = await readdir2(dir);
|
|
1643
|
+
if (entries.length === 0) {
|
|
1644
|
+
await rm(dir, { recursive: true, force: true });
|
|
1645
|
+
}
|
|
1646
|
+
}
|
|
1647
|
+
}
|
|
1648
|
+
async function stripAvatarBlockFromGitignore(path) {
|
|
1649
|
+
const content = await readFile(path, "utf8");
|
|
1650
|
+
const startIdx = content.indexOf(AVATAR_MARKER_START);
|
|
1651
|
+
const endIdx = content.indexOf(AVATAR_MARKER_END);
|
|
1652
|
+
if (startIdx === -1 || endIdx === -1) return;
|
|
1653
|
+
const before = content.slice(0, startIdx);
|
|
1654
|
+
const after = content.slice(endIdx + AVATAR_MARKER_END.length);
|
|
1655
|
+
const cleaned = `${before.trimEnd()}
|
|
1656
|
+
${after.trimStart()}`.trim();
|
|
1657
|
+
if (cleaned.length === 0) {
|
|
1658
|
+
await rm(path, { force: true });
|
|
1659
|
+
} else {
|
|
1660
|
+
await writeFile2(path, `${cleaned}
|
|
1661
|
+
`, "utf8");
|
|
1662
|
+
}
|
|
1663
|
+
}
|
|
1664
|
+
async function removeSubmoduleEntry(gitmodulesPath, submodulePath) {
|
|
1665
|
+
const content = await readFile(gitmodulesPath, "utf8");
|
|
1666
|
+
const lines = content.split("\n");
|
|
1667
|
+
const result = [];
|
|
1668
|
+
let skip = false;
|
|
1669
|
+
for (const line of lines) {
|
|
1670
|
+
if (line.trim().startsWith("[submodule") && line.includes(submodulePath)) {
|
|
1671
|
+
skip = true;
|
|
1672
|
+
continue;
|
|
1673
|
+
}
|
|
1674
|
+
if (skip && line.trim().startsWith("[submodule")) {
|
|
1675
|
+
skip = false;
|
|
1676
|
+
}
|
|
1677
|
+
if (!skip) result.push(line);
|
|
1678
|
+
}
|
|
1679
|
+
const cleaned = result.join("\n").trim();
|
|
1680
|
+
if (cleaned.length === 0) {
|
|
1681
|
+
await rm(gitmodulesPath, { force: true });
|
|
1682
|
+
} else {
|
|
1683
|
+
await writeFile2(gitmodulesPath, `${cleaned}
|
|
1684
|
+
`, "utf8");
|
|
1685
|
+
}
|
|
1686
|
+
}
|
|
1687
|
+
|
|
1688
|
+
// src/commands/uninstall.ts
|
|
1689
|
+
var CLI_VERSION = "1.1.1";
|
|
1690
|
+
function registerUninstallCommand(program2) {
|
|
1691
|
+
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) => {
|
|
1692
|
+
try {
|
|
1693
|
+
await runUninstall(opts);
|
|
1694
|
+
} catch (err) {
|
|
1695
|
+
log.error(err instanceof Error ? err.message : String(err));
|
|
1696
|
+
process.exit(1);
|
|
1697
|
+
}
|
|
1698
|
+
});
|
|
1699
|
+
}
|
|
1700
|
+
async function runUninstall(opts) {
|
|
1701
|
+
const projectRoot = process.cwd();
|
|
1702
|
+
const artifacts = detectAvatarProjectArtifacts(projectRoot);
|
|
1703
|
+
if (!artifacts.hasAnyArtifact) {
|
|
1704
|
+
log.info("Project ch\u01B0a c\xE0i Avatar \u2014 kh\xF4ng c\xF3 g\xEC \u0111\u1EC3 g\u1EE1.");
|
|
1705
|
+
return;
|
|
1706
|
+
}
|
|
1707
|
+
printUninstallSummary(projectRoot, artifacts, opts);
|
|
1708
|
+
if (opts.dryRun) {
|
|
1709
|
+
log.dim("--dry-run: k\u1EBFt th\xFAc, kh\xF4ng x\xF3a.");
|
|
1710
|
+
return;
|
|
1711
|
+
}
|
|
1712
|
+
if (!opts.yes) {
|
|
1713
|
+
const ok = await confirm2({
|
|
1714
|
+
message: "Ti\u1EBFp t\u1EE5c g\u1EE1 Avatar?",
|
|
1715
|
+
default: false
|
|
1716
|
+
});
|
|
1717
|
+
if (!ok) {
|
|
1718
|
+
log.info("\u0110\xE3 h\u1EE7y.");
|
|
1719
|
+
return;
|
|
1720
|
+
}
|
|
1721
|
+
}
|
|
1722
|
+
let backupPath = null;
|
|
1723
|
+
if (!opts.noBackup) {
|
|
1724
|
+
backupPath = await createUninstallBackupSnapshot(projectRoot, artifacts, CLI_VERSION);
|
|
1725
|
+
log.success(`Backup t\u1EA1o t\u1EA1i: ${backupPath}`);
|
|
1726
|
+
}
|
|
1727
|
+
await executeUninstallDeletion(artifacts, {
|
|
1728
|
+
keepSubmodule: opts.keepSubmodule,
|
|
1729
|
+
keepHooks: opts.keepHooks
|
|
1730
|
+
});
|
|
1731
|
+
await appendAuditEntry("uninstall", `project=${projectRoot},backup=${backupPath ?? "skipped"}`);
|
|
1732
|
+
printUninstallSuccessBox(backupPath);
|
|
1733
|
+
}
|
|
1734
|
+
function printUninstallSummary(projectRoot, artifacts, opts) {
|
|
1735
|
+
log.info(`Project: ${projectRoot}`);
|
|
1736
|
+
log.plain("");
|
|
1737
|
+
log.plain("C\xE1c artifact s\u1EBD g\u1EE1:");
|
|
1738
|
+
if (artifacts.claudeDir)
|
|
1739
|
+
log.plain(` ${chalk.red("\u2717")} ${relative3(projectRoot, artifacts.claudeDir) || ".claude/"}`);
|
|
1740
|
+
if (artifacts.claudeMd) log.plain(` ${chalk.red("\u2717")} CLAUDE.md`);
|
|
1741
|
+
if (artifacts.postMergeHook && !opts.keepHooks) {
|
|
1742
|
+
log.plain(` ${chalk.red("\u2717")} .git/hooks/post-merge`);
|
|
1743
|
+
}
|
|
1744
|
+
if (artifacts.prePushHook && !opts.keepHooks) {
|
|
1745
|
+
log.plain(` ${chalk.red("\u2717")} .git/modules/src/hooks/pre-push`);
|
|
1746
|
+
}
|
|
1747
|
+
if (artifacts.gitignorePath) log.plain(` ${chalk.yellow("\u270E")} .gitignore (g\u1EE1 Avatar block)`);
|
|
1748
|
+
if (artifacts.gitmodulesPath && !opts.keepSubmodule) {
|
|
1749
|
+
log.plain(` ${chalk.yellow("\u270E")} .gitmodules (g\u1EE1 entry .claude/pack)`);
|
|
1750
|
+
}
|
|
1751
|
+
log.plain("");
|
|
1752
|
+
log.plain("Kh\xF4ng \u0111\u1EE5ng:");
|
|
1753
|
+
log.plain(` ${chalk.green("\u2713")} src/ (code kh\xE1ch)`);
|
|
1754
|
+
log.plain(` ${chalk.green("\u2713")} Git history`);
|
|
1755
|
+
log.plain(` ${chalk.green("\u2713")} ~/.avatar/config.json (token SSO)`);
|
|
1756
|
+
log.plain(` ${chalk.green("\u2713")} Secrets trong keychain`);
|
|
1757
|
+
log.plain("");
|
|
1758
|
+
}
|
|
1759
|
+
function printUninstallSuccessBox(backupPath) {
|
|
1760
|
+
const lines = [`${chalk.green("\u2713")} Avatar \u0111\xE3 \u0111\u01B0\u1EE3c g\u1EE1 kh\u1ECFi project`];
|
|
1761
|
+
if (backupPath) {
|
|
1762
|
+
lines.push("");
|
|
1763
|
+
lines.push(` ${chalk.dim("Backup:")} ${backupPath}`);
|
|
1764
|
+
lines.push(` ${chalk.dim("Restore:")} ${chalk.cyan(`cp -r "${backupPath}"/* .`)}`);
|
|
1765
|
+
}
|
|
1766
|
+
process.stdout.write(`${boxen5(lines.join("\n"), { padding: 1, borderStyle: "round" })}
|
|
1767
|
+
`);
|
|
1768
|
+
}
|
|
1769
|
+
|
|
1107
1770
|
// src/index.ts
|
|
1108
|
-
var
|
|
1771
|
+
var CLI_VERSION2 = "1.1.1";
|
|
1109
1772
|
var program = new Command();
|
|
1110
|
-
program.name("avatar").description("AI harness CLI for NAL Vietnam engineering").version(
|
|
1773
|
+
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(
|
|
1774
|
+
"beforeAll",
|
|
1775
|
+
() => `
|
|
1776
|
+
${renderAvatarBanner({ tagline: `v${CLI_VERSION2} \xB7 AI harness CLI for NAL Vietnam` })}
|
|
1777
|
+
|
|
1778
|
+
`
|
|
1779
|
+
);
|
|
1780
|
+
var isVersionCall = process.argv.includes("-v") || process.argv.includes("--version");
|
|
1781
|
+
if (isVersionCall) {
|
|
1782
|
+
printAvatarBanner({ tagline: `v${CLI_VERSION2} \xB7 AI harness CLI for NAL Vietnam` });
|
|
1783
|
+
process.exit(0);
|
|
1784
|
+
}
|
|
1111
1785
|
registerLoginCommand(program);
|
|
1112
1786
|
registerInitCommand(program);
|
|
1113
1787
|
registerSyncCommand(program);
|
|
@@ -1120,6 +1794,7 @@ registerCommitCommand(program);
|
|
|
1120
1794
|
registerToolsCommand(program);
|
|
1121
1795
|
registerSecretsCommand(program);
|
|
1122
1796
|
registerMcpRunCommand(program);
|
|
1797
|
+
registerUninstallCommand(program);
|
|
1123
1798
|
program.parseAsync(process.argv).catch((err) => {
|
|
1124
1799
|
const msg = err instanceof Error ? err.message : String(err);
|
|
1125
1800
|
process.stderr.write(`\u2717 L\u1ED7i kh\xF4ng x\u1EED l\xFD \u0111\u01B0\u1EE3c: ${msg}
|