@nalvietnam/avatar-cli 1.0.0 → 1.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/bin/avatar.js +0 -0
- package/dist/hooks/post-merge.sh.tpl +24 -0
- package/dist/hooks/pre-push.sh.tpl +33 -0
- package/dist/index.js +949 -170
- package/dist/index.js.map +1 -1
- 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 +25 -16
- 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
|
@@ -124,6 +124,7 @@ import { promises as fs2 } from "fs";
|
|
|
124
124
|
import { join as join4 } from "path";
|
|
125
125
|
|
|
126
126
|
// src/lib/template-bundle-loader.ts
|
|
127
|
+
import { existsSync } from "fs";
|
|
127
128
|
import { dirname as dirname2, join as join3 } from "path";
|
|
128
129
|
import { fileURLToPath } from "url";
|
|
129
130
|
|
|
@@ -139,8 +140,20 @@ function renderTemplate(source, variables) {
|
|
|
139
140
|
|
|
140
141
|
// src/lib/template-bundle-loader.ts
|
|
141
142
|
var HERE = dirname2(fileURLToPath(import.meta.url));
|
|
142
|
-
var
|
|
143
|
-
var
|
|
143
|
+
var PACKAGE_ROOT = findPackageRoot(HERE);
|
|
144
|
+
var TEMPLATES_ROOT = join3(PACKAGE_ROOT, "src", "templates");
|
|
145
|
+
var HOOKS_ROOT = join3(PACKAGE_ROOT, "src", "hooks");
|
|
146
|
+
function findPackageRoot(startDir) {
|
|
147
|
+
let dir = startDir;
|
|
148
|
+
while (true) {
|
|
149
|
+
if (existsSync(join3(dir, "package.json"))) return dir;
|
|
150
|
+
const parent = dirname2(dir);
|
|
151
|
+
if (parent === dir) {
|
|
152
|
+
throw new Error(`Cannot locate package root from ${startDir}`);
|
|
153
|
+
}
|
|
154
|
+
dir = parent;
|
|
155
|
+
}
|
|
156
|
+
}
|
|
144
157
|
async function loadTemplate(name) {
|
|
145
158
|
return await readText(join3(TEMPLATES_ROOT, `${name}.tpl`));
|
|
146
159
|
}
|
|
@@ -153,6 +166,27 @@ async function loadHook(name) {
|
|
|
153
166
|
}
|
|
154
167
|
|
|
155
168
|
// src/lib/project-tree-scaffolder.ts
|
|
169
|
+
async function backupIfExists(path) {
|
|
170
|
+
if (!await pathExists(path)) return null;
|
|
171
|
+
const ts = (/* @__PURE__ */ new Date()).toISOString().replace(/[:.]/g, "-");
|
|
172
|
+
const basePath = `${path}.avatar-backup-${ts}`;
|
|
173
|
+
let backupPath = basePath;
|
|
174
|
+
let counter = 1;
|
|
175
|
+
while (await pathExists(backupPath)) {
|
|
176
|
+
backupPath = `${basePath}-${counter}`;
|
|
177
|
+
counter++;
|
|
178
|
+
if (counter > 5) {
|
|
179
|
+
throw new Error(`Could not find free backup name for ${path}`);
|
|
180
|
+
}
|
|
181
|
+
}
|
|
182
|
+
await fs2.rename(path, backupPath);
|
|
183
|
+
return backupPath;
|
|
184
|
+
}
|
|
185
|
+
async function writeWithBackup(path, content, mode) {
|
|
186
|
+
const backup = await backupIfExists(path);
|
|
187
|
+
await writeTextAtomic(path, content, mode);
|
|
188
|
+
return backup;
|
|
189
|
+
}
|
|
156
190
|
var CLAUDE_SUBDIRS = ["project", "state", "_pending", "_backup"];
|
|
157
191
|
var PROJECT_KNOWLEDGE_TEMPLATES = [
|
|
158
192
|
"project/tech-stack.md",
|
|
@@ -194,20 +228,23 @@ async function writeProjectKnowledgeFiles(projectRoot, vars) {
|
|
|
194
228
|
primaryUseCases: "(ch\u01B0a scan)",
|
|
195
229
|
domainGlossary: "(ch\u01B0a scan)"
|
|
196
230
|
};
|
|
231
|
+
const backups = [];
|
|
197
232
|
for (const tpl of PROJECT_KNOWLEDGE_TEMPLATES) {
|
|
198
233
|
const content = await renderTemplateByName(tpl, baseVars);
|
|
199
|
-
const
|
|
200
|
-
const outPath = join4(projectRoot, ".claude", "project",
|
|
201
|
-
await
|
|
234
|
+
const relative4 = tpl.replace(/^project\//, "");
|
|
235
|
+
const outPath = join4(projectRoot, ".claude", "project", relative4);
|
|
236
|
+
const backup = await writeWithBackup(outPath, content);
|
|
237
|
+
if (backup) backups.push(backup);
|
|
202
238
|
}
|
|
239
|
+
return backups;
|
|
203
240
|
}
|
|
204
241
|
async function writeRootClaudeMd(projectRoot, vars) {
|
|
205
242
|
const content = await renderTemplateByName("CLAUDE.md", vars);
|
|
206
|
-
await
|
|
243
|
+
return await writeWithBackup(join4(projectRoot, "CLAUDE.md"), content);
|
|
207
244
|
}
|
|
208
245
|
async function writeProjectSettings(projectRoot, vars) {
|
|
209
246
|
const content = await renderTemplateByName("settings.json", vars);
|
|
210
|
-
await
|
|
247
|
+
return await writeWithBackup(join4(projectRoot, ".claude", "settings.json"), content);
|
|
211
248
|
}
|
|
212
249
|
async function appendGitignoreEntries(projectRoot) {
|
|
213
250
|
const path = join4(projectRoot, ".gitignore");
|
|
@@ -441,7 +478,7 @@ async function applyFixes(checks) {
|
|
|
441
478
|
}
|
|
442
479
|
|
|
443
480
|
// src/commands/init.ts
|
|
444
|
-
import { join as
|
|
481
|
+
import { basename, join as join13, relative as relative2, resolve } from "path";
|
|
445
482
|
import { confirm, input, select } from "@inquirer/prompts";
|
|
446
483
|
import boxen2 from "boxen";
|
|
447
484
|
|
|
@@ -459,15 +496,406 @@ async function appendAuditEntry(action, detail) {
|
|
|
459
496
|
await fs4.appendFile(AUDIT_LOG_PATH, line, "utf8");
|
|
460
497
|
}
|
|
461
498
|
|
|
462
|
-
// 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";
|
|
463
769
|
import { join as join7 } from "path";
|
|
464
|
-
|
|
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";
|
|
892
|
+
var TEAM_PACK_REPO_URL = process.env.AVATAR_TEAM_PACK_REPO_URL ?? "https://github.com/LukeNALS/team-ai-pack.git";
|
|
465
893
|
var TEAM_PACK_RELATIVE_PATH = ".claude/pack";
|
|
466
894
|
async function addTeamPackSubmodule(projectRoot, tag) {
|
|
467
895
|
await addSubmodule(TEAM_PACK_REPO_URL, TEAM_PACK_RELATIVE_PATH, projectRoot);
|
|
468
896
|
let target = tag ?? null;
|
|
469
897
|
if (!target) {
|
|
470
|
-
target = await latestTag(
|
|
898
|
+
target = await latestTag(join11(projectRoot, TEAM_PACK_RELATIVE_PATH));
|
|
471
899
|
}
|
|
472
900
|
if (target) {
|
|
473
901
|
await checkoutTagInSubmodule(TEAM_PACK_RELATIVE_PATH, target, projectRoot);
|
|
@@ -475,17 +903,56 @@ async function addTeamPackSubmodule(projectRoot, tag) {
|
|
|
475
903
|
return { pinnedTag: target };
|
|
476
904
|
}
|
|
477
905
|
async function readPinnedPackVersion(projectRoot) {
|
|
478
|
-
const submoduleRoot =
|
|
906
|
+
const submoduleRoot = join11(projectRoot, TEAM_PACK_RELATIVE_PATH);
|
|
479
907
|
const tag = await latestTag(submoduleRoot);
|
|
480
908
|
if (tag) return tag;
|
|
481
909
|
const sha = await currentCommitSha(submoduleRoot);
|
|
482
910
|
return sha.slice(0, 7);
|
|
483
911
|
}
|
|
484
912
|
|
|
913
|
+
// src/commands/init-conflict-detection-helpers.ts
|
|
914
|
+
import { readdir } from "fs/promises";
|
|
915
|
+
import { join as join12 } from "path";
|
|
916
|
+
async function isEmptyOrMissing(path) {
|
|
917
|
+
if (!await pathExists(path)) return true;
|
|
918
|
+
try {
|
|
919
|
+
const entries = await readdir(path);
|
|
920
|
+
const meaningful = entries.filter((e) => !e.startsWith(".") && e !== "Thumbs.db");
|
|
921
|
+
return meaningful.length === 0;
|
|
922
|
+
} catch {
|
|
923
|
+
return false;
|
|
924
|
+
}
|
|
925
|
+
}
|
|
926
|
+
async function findAlternativeWorkspaceName(parent, desiredName, maxAttempts = 10) {
|
|
927
|
+
for (let i = 2; i < maxAttempts; i++) {
|
|
928
|
+
const candidate = join12(parent, `${desiredName}-${i}`);
|
|
929
|
+
if (await isEmptyOrMissing(candidate)) return candidate;
|
|
930
|
+
}
|
|
931
|
+
return null;
|
|
932
|
+
}
|
|
933
|
+
|
|
934
|
+
// src/commands/init-scaffold-variable-builders.ts
|
|
935
|
+
var AVATAR_CLI_VERSION = "1.0.1";
|
|
936
|
+
function inferWorkspaceName(repoUrl) {
|
|
937
|
+
const m = repoUrl.match(/[/:]([^/]+?)(\.git)?$/);
|
|
938
|
+
const base = m?.[1] ?? "client";
|
|
939
|
+
return `avatar-${base}-workspace`;
|
|
940
|
+
}
|
|
941
|
+
function buildScaffoldVariables(args) {
|
|
942
|
+
return {
|
|
943
|
+
projectName: args.projectName,
|
|
944
|
+
projectDescription: args.projectDescription,
|
|
945
|
+
teamOwner: args.teamOwner,
|
|
946
|
+
avatarVersion: AVATAR_CLI_VERSION,
|
|
947
|
+
packVersion: args.packVersion,
|
|
948
|
+
lastScan: (/* @__PURE__ */ new Date()).toISOString(),
|
|
949
|
+
mode: args.mode
|
|
950
|
+
};
|
|
951
|
+
}
|
|
952
|
+
|
|
485
953
|
// src/commands/init.ts
|
|
486
|
-
var AVATAR_CLI_VERSION = "1.0.0";
|
|
487
954
|
function registerInitCommand(program2) {
|
|
488
|
-
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) => {
|
|
489
956
|
try {
|
|
490
957
|
await runInit(opts);
|
|
491
958
|
} catch (err) {
|
|
@@ -495,190 +962,251 @@ function registerInitCommand(program2) {
|
|
|
495
962
|
});
|
|
496
963
|
}
|
|
497
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
|
+
}
|
|
498
969
|
const userConfig = await readUserConfig();
|
|
499
970
|
if (!userConfig || isTokenExpired(userConfig)) {
|
|
500
|
-
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.");
|
|
501
972
|
process.exit(1);
|
|
502
973
|
}
|
|
503
|
-
const
|
|
504
|
-
|
|
505
|
-
|
|
506
|
-
|
|
507
|
-
|
|
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;
|
|
508
985
|
}
|
|
509
986
|
}
|
|
510
|
-
async function
|
|
987
|
+
async function promptProjectStatus() {
|
|
511
988
|
return await select({
|
|
512
|
-
message: "\
|
|
989
|
+
message: "T\xECnh tr\u1EA1ng d\u1EF1 \xE1n c\u1EE7a b\u1EA1n?",
|
|
513
990
|
choices: [
|
|
514
|
-
{
|
|
515
|
-
|
|
516
|
-
|
|
517
|
-
},
|
|
518
|
-
{ name: "Client (Pattern A \u2014 t\xE1ch workspace)", value: "client" },
|
|
519
|
-
{ 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" }
|
|
520
994
|
]
|
|
521
995
|
});
|
|
522
996
|
}
|
|
523
|
-
async function
|
|
524
|
-
const
|
|
525
|
-
|
|
526
|
-
|
|
527
|
-
}
|
|
528
|
-
if (await pathExists(join8(projectRoot, ".claude"))) {
|
|
529
|
-
throw new Error(
|
|
530
|
-
".claude/ \u0111\xE3 t\u1ED3n t\u1EA1i. Avatar kh\xF4ng override. X\xF3a th\u1EE7 c\xF4ng ho\u1EB7c d\xF9ng mode kh\xE1c."
|
|
531
|
-
);
|
|
532
|
-
}
|
|
533
|
-
const teamOwner = await promptTeamOwner(ownerEmail);
|
|
534
|
-
const projectName = projectNameOf(projectRoot);
|
|
535
|
-
const projectDescription = await input({
|
|
536
|
-
message: "M\xF4 t\u1EA3 ng\u1EAFn 1 d\xF2ng c\u1EE7a d\u1EF1 \xE1n:",
|
|
537
|
-
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"
|
|
538
1001
|
});
|
|
539
|
-
|
|
540
|
-
|
|
541
|
-
|
|
542
|
-
|
|
543
|
-
|
|
544
|
-
|
|
545
|
-
|
|
546
|
-
|
|
547
|
-
|
|
548
|
-
|
|
549
|
-
const vars = buildScaffoldVariables({
|
|
550
|
-
projectName,
|
|
551
|
-
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,
|
|
552
1012
|
teamOwner,
|
|
553
|
-
|
|
554
|
-
|
|
1013
|
+
description: opts.description ?? `Avatar workspace cho ${remoteUrl}`,
|
|
1014
|
+
packVersion: opts.packVersion,
|
|
1015
|
+
autoYes: opts.yes,
|
|
1016
|
+
flow: "existing-remote"
|
|
555
1017
|
});
|
|
556
|
-
|
|
557
|
-
|
|
558
|
-
|
|
559
|
-
|
|
560
|
-
|
|
561
|
-
|
|
562
|
-
|
|
563
|
-
|
|
564
|
-
await
|
|
565
|
-
|
|
566
|
-
|
|
567
|
-
|
|
568
|
-
const
|
|
569
|
-
const
|
|
570
|
-
|
|
571
|
-
|
|
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);
|
|
1028
|
+
const teamOwner = opts.teamOwner ?? await promptTeamOwner(ownerEmail);
|
|
1029
|
+
const inferredName = opts.workspaceName ?? `${basename(folderPath)}-avatar-workspace`;
|
|
1030
|
+
const workspaceName = opts.workspaceName ?? await input({ message: "T\xEAn workspace:", default: inferredName });
|
|
1031
|
+
const workspaceParent = resolve(opts.workspaceParent ?? "..");
|
|
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"
|
|
572
1043
|
});
|
|
573
|
-
|
|
574
|
-
|
|
575
|
-
|
|
576
|
-
|
|
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"
|
|
577
1050
|
});
|
|
578
|
-
const
|
|
579
|
-
|
|
580
|
-
|
|
581
|
-
|
|
582
|
-
|
|
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");
|
|
583
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
|
+
});
|
|
584
1071
|
await git(workspacePath).init();
|
|
585
|
-
const sp = spinner("Add submodule
|
|
1072
|
+
const sp = spinner("Add submodule src/ + team-ai-pack...");
|
|
586
1073
|
try {
|
|
587
|
-
await git(workspacePath).subModule(["add",
|
|
1074
|
+
await git(workspacePath).subModule(["add", urls.sshUrl, "src"]);
|
|
588
1075
|
const result = await addTeamPackSubmodule(workspacePath, opts.packVersion);
|
|
589
|
-
sp.succeed(`
|
|
590
|
-
|
|
591
|
-
|
|
592
|
-
|
|
1076
|
+
sp.succeed(`Pin team-ai-pack v\xE0o ${result.pinnedTag ?? "HEAD"}`);
|
|
1077
|
+
await finalizeWorkspaceScaffold({
|
|
1078
|
+
workspacePath,
|
|
1079
|
+
workspaceName: projectName,
|
|
593
1080
|
teamOwner,
|
|
1081
|
+
description: opts.description ?? `D\u1EF1 \xE1n m\u1EDBi: ${projectName}`,
|
|
594
1082
|
packVersion: result.pinnedTag ?? "HEAD",
|
|
595
|
-
|
|
1083
|
+
autoYes: opts.yes,
|
|
1084
|
+
flow: "new-project"
|
|
596
1085
|
});
|
|
597
|
-
await createClaudeDirTree(workspacePath);
|
|
598
|
-
await writeProjectKnowledgeFiles(workspacePath, vars);
|
|
599
|
-
await writeRootClaudeMd(workspacePath, vars);
|
|
600
|
-
await writeProjectSettings(workspacePath, vars);
|
|
601
|
-
await appendGitignoreEntries(workspacePath);
|
|
602
|
-
await ensureDir(join8(workspacePath, "notes"));
|
|
603
|
-
await ensureDir(join8(workspacePath, "scripts"));
|
|
604
|
-
await installGitHook(join8(workspacePath, ".git"), "post-merge");
|
|
605
|
-
await installGitHook(join8(workspacePath, "src", ".git"), "pre-push");
|
|
606
|
-
log.success("C\xE0i post-merge (workspace) + pre-push (src/)");
|
|
607
|
-
await appendAuditEntry("init", `mode=${mode},workspace=${workspaceName}`);
|
|
608
|
-
await maybeCommitWorkspace(workspacePath);
|
|
609
|
-
printInitSuccessBox(workspacePath, mode);
|
|
610
1086
|
} catch (err) {
|
|
611
1087
|
sp.fail("Init workspace th\u1EA5t b\u1EA1i");
|
|
612
1088
|
throw err;
|
|
613
1089
|
}
|
|
614
1090
|
}
|
|
615
|
-
function
|
|
616
|
-
|
|
617
|
-
|
|
618
|
-
|
|
619
|
-
|
|
620
|
-
|
|
621
|
-
|
|
622
|
-
|
|
623
|
-
|
|
624
|
-
|
|
625
|
-
|
|
626
|
-
|
|
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
|
|
627
1123
|
});
|
|
1124
|
+
return urls.sshUrl;
|
|
628
1125
|
}
|
|
629
|
-
function
|
|
630
|
-
|
|
631
|
-
|
|
632
|
-
|
|
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,
|
|
1139
|
+
packVersion: result.pinnedTag ?? "HEAD",
|
|
1140
|
+
autoYes: args.autoYes,
|
|
1141
|
+
flow: args.flow
|
|
1142
|
+
});
|
|
1143
|
+
} catch (err) {
|
|
1144
|
+
sp.fail("Init workspace th\u1EA5t b\u1EA1i");
|
|
1145
|
+
throw err;
|
|
1146
|
+
}
|
|
1147
|
+
}
|
|
1148
|
+
async function finalizeWorkspaceScaffold(args) {
|
|
1149
|
+
const vars = buildScaffoldVariables({
|
|
1150
|
+
projectName: args.workspaceName,
|
|
1151
|
+
projectDescription: args.description,
|
|
633
1152
|
teamOwner: args.teamOwner,
|
|
634
|
-
avatarVersion: AVATAR_CLI_VERSION,
|
|
635
1153
|
packVersion: args.packVersion,
|
|
636
|
-
|
|
637
|
-
mode: args.mode
|
|
638
|
-
};
|
|
639
|
-
}
|
|
640
|
-
async function maybeCommit(projectRoot, mode) {
|
|
641
|
-
const wantCommit = await confirm({
|
|
642
|
-
message: "Commit ngay c\xE1c file Avatar \u0111\xE3 t\u1EA1o?",
|
|
643
|
-
default: true
|
|
644
|
-
});
|
|
645
|
-
if (!wantCommit) return;
|
|
646
|
-
const g = git(projectRoot);
|
|
647
|
-
await g.add([".claude/", "CLAUDE.md", ".gitignore", ".gitmodules"]);
|
|
648
|
-
await g.commit(`chore: initialize Avatar in ${mode} mode`);
|
|
649
|
-
log.success("\u0110\xE3 commit");
|
|
650
|
-
}
|
|
651
|
-
async function maybeCommitWorkspace(workspacePath) {
|
|
652
|
-
const wantCommit = await confirm({
|
|
653
|
-
message: "Commit workspace ngay?",
|
|
654
|
-
default: true
|
|
1154
|
+
mode: "client"
|
|
655
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
|
+
}
|
|
1170
|
+
async function resolveWorkspacePath(parent, desiredName, force) {
|
|
1171
|
+
const desired = join13(parent, desiredName);
|
|
1172
|
+
if (await isEmptyOrMissing(desired)) return desired;
|
|
1173
|
+
const alternative = await findAlternativeWorkspaceName(parent, desiredName);
|
|
1174
|
+
if (!alternative) {
|
|
1175
|
+
throw new Error(`Kh\xF4ng t\xECm \u0111\u01B0\u1EE3c workspace path kh\u1EA3 d\u1EE5ng trong ${parent}`);
|
|
1176
|
+
}
|
|
1177
|
+
log.warn(`Workspace path "${desired}" \u0111\xE3 c\xF3 n\u1ED9i dung.`);
|
|
1178
|
+
if (force) {
|
|
1179
|
+
log.info(`--force: d\xF9ng ${alternative}`);
|
|
1180
|
+
return alternative;
|
|
1181
|
+
}
|
|
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.");
|
|
1184
|
+
return alternative;
|
|
1185
|
+
}
|
|
1186
|
+
async function promptTeamOwner(currentUserEmail) {
|
|
1187
|
+
return await input({ message: "Team owner email:", default: currentUserEmail });
|
|
1188
|
+
}
|
|
1189
|
+
async function maybeCommitWorkspace(workspacePath, autoYes) {
|
|
1190
|
+
const wantCommit = autoYes ?? await confirm({ message: "Commit workspace ngay?", default: true });
|
|
656
1191
|
if (!wantCommit) return;
|
|
657
1192
|
const g = git(workspacePath);
|
|
658
1193
|
await g.add(["CLAUDE.md", ".claude/", ".gitignore", ".gitmodules", "notes/", "scripts/"]);
|
|
659
|
-
await g.commit("chore: initialize Avatar workspace
|
|
1194
|
+
await g.commit("chore: initialize Avatar workspace");
|
|
660
1195
|
log.success("\u0110\xE3 commit workspace");
|
|
661
1196
|
}
|
|
662
|
-
function printInitSuccessBox(rootPath,
|
|
663
|
-
const lines = [
|
|
664
|
-
|
|
665
|
-
|
|
666
|
-
|
|
667
|
-
|
|
668
|
-
|
|
669
|
-
|
|
670
|
-
}
|
|
671
|
-
|
|
672
|
-
|
|
673
|
-
|
|
674
|
-
|
|
675
|
-
lines.push("");
|
|
676
|
-
lines.push(` ${chalk.cyan("avatar commit --src")} Commit code kh\xE1ch l\xEAn client remote`);
|
|
677
|
-
lines.push(
|
|
678
|
-
` ${chalk.cyan("avatar commit --avatar")} Commit Avatar state l\xEAn workspace remote`
|
|
679
|
-
);
|
|
680
|
-
lines.push(` ${chalk.cyan("avatar sync")} Sync team pack`);
|
|
681
|
-
}
|
|
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
|
+
];
|
|
682
1210
|
process.stdout.write(`${boxen2(lines.join("\n"), { padding: 1, borderStyle: "round" })}
|
|
683
1211
|
`);
|
|
684
1212
|
}
|
|
@@ -804,6 +1332,7 @@ function registerLoginCommand(program2) {
|
|
|
804
1332
|
});
|
|
805
1333
|
}
|
|
806
1334
|
async function runLogin(opts) {
|
|
1335
|
+
printAvatarBanner({ tagline: "\u0110\u0103ng nh\u1EADp Google SSO \xB7 workspace @nal.vn" });
|
|
807
1336
|
if (opts.reset) {
|
|
808
1337
|
await clearUserConfig();
|
|
809
1338
|
await appendAuditEntry("login_reset");
|
|
@@ -904,22 +1433,22 @@ function registerSecretsCommand(program2) {
|
|
|
904
1433
|
|
|
905
1434
|
// src/commands/status.ts
|
|
906
1435
|
import { promises as fs6 } from "fs";
|
|
907
|
-
import { join as
|
|
1436
|
+
import { join as join15 } from "path";
|
|
908
1437
|
import boxen4 from "boxen";
|
|
909
1438
|
|
|
910
1439
|
// src/lib/pack-backup-manager.ts
|
|
911
1440
|
import { promises as fs5 } from "fs";
|
|
912
|
-
import { join as
|
|
1441
|
+
import { join as join14 } from "path";
|
|
913
1442
|
var BACKUP_DIR_NAME = "_backup";
|
|
914
1443
|
async function listBackups(projectRoot) {
|
|
915
|
-
const dir =
|
|
1444
|
+
const dir = join14(projectRoot, ".claude", BACKUP_DIR_NAME);
|
|
916
1445
|
if (!await pathExists(dir)) return [];
|
|
917
1446
|
const entries = await fs5.readdir(dir, { withFileTypes: true });
|
|
918
1447
|
return entries.filter((e) => e.isDirectory()).map((e) => e.name).sort().reverse();
|
|
919
1448
|
}
|
|
920
1449
|
|
|
921
1450
|
// src/commands/status.ts
|
|
922
|
-
var AVATAR_CLI_VERSION2 = "1.0.
|
|
1451
|
+
var AVATAR_CLI_VERSION2 = "1.0.1";
|
|
923
1452
|
function registerStatusCommand(program2) {
|
|
924
1453
|
program2.command("status").description("Snapshot t\u1EE9c th\xEC: project, pack version, pending, backup").option("--json", "Output JSON cho script").action(async (opts) => {
|
|
925
1454
|
try {
|
|
@@ -938,7 +1467,7 @@ function registerStatusCommand(program2) {
|
|
|
938
1467
|
}
|
|
939
1468
|
async function gatherStatus(cwd) {
|
|
940
1469
|
const projectName = cwd.split("/").filter(Boolean).pop() ?? "unknown";
|
|
941
|
-
const claudeRoot =
|
|
1470
|
+
const claudeRoot = join15(cwd, ".claude");
|
|
942
1471
|
const hasAvatar = await pathExists(claudeRoot);
|
|
943
1472
|
if (!hasAvatar) {
|
|
944
1473
|
return {
|
|
@@ -951,8 +1480,8 @@ async function gatherStatus(cwd) {
|
|
|
951
1480
|
hasAvatar: false
|
|
952
1481
|
};
|
|
953
1482
|
}
|
|
954
|
-
const packVersion = await isGitRepo(
|
|
955
|
-
const pendingDir =
|
|
1483
|
+
const packVersion = await isGitRepo(join15(claudeRoot, "pack")) ? await readPinnedPackVersion(cwd).catch(() => null) : null;
|
|
1484
|
+
const pendingDir = join15(claudeRoot, "_pending");
|
|
956
1485
|
const pendingCount = await pathExists(pendingDir) ? (await fs6.readdir(pendingDir)).filter((n) => n.endsWith(".diff.md")).length : 0;
|
|
957
1486
|
const backupCount = (await listBackups(cwd)).length;
|
|
958
1487
|
const techStackSummary = await readTechStackFirstLine(claudeRoot);
|
|
@@ -967,7 +1496,7 @@ async function gatherStatus(cwd) {
|
|
|
967
1496
|
};
|
|
968
1497
|
}
|
|
969
1498
|
async function readTechStackFirstLine(claudeRoot) {
|
|
970
|
-
const techStackPath =
|
|
1499
|
+
const techStackPath = join15(claudeRoot, "project", "tech-stack.md");
|
|
971
1500
|
if (!await pathExists(techStackPath)) return "(no tech-stack.md)";
|
|
972
1501
|
const content = await readText(techStackPath);
|
|
973
1502
|
const firstNonHeaderLine = content.split("\n").find((l) => l.trim() && !l.startsWith("#") && !l.startsWith(">"));
|
|
@@ -1000,10 +1529,259 @@ function registerToolsCommand(program2) {
|
|
|
1000
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"));
|
|
1001
1530
|
}
|
|
1002
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.0";
|
|
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
|
+
|
|
1003
1770
|
// src/index.ts
|
|
1004
|
-
var
|
|
1771
|
+
var CLI_VERSION2 = "1.1.0";
|
|
1005
1772
|
var program = new Command();
|
|
1006
|
-
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
|
+
}
|
|
1007
1785
|
registerLoginCommand(program);
|
|
1008
1786
|
registerInitCommand(program);
|
|
1009
1787
|
registerSyncCommand(program);
|
|
@@ -1016,6 +1794,7 @@ registerCommitCommand(program);
|
|
|
1016
1794
|
registerToolsCommand(program);
|
|
1017
1795
|
registerSecretsCommand(program);
|
|
1018
1796
|
registerMcpRunCommand(program);
|
|
1797
|
+
registerUninstallCommand(program);
|
|
1019
1798
|
program.parseAsync(process.argv).catch((err) => {
|
|
1020
1799
|
const msg = err instanceof Error ? err.message : String(err);
|
|
1021
1800
|
process.stderr.write(`\u2717 L\u1ED7i kh\xF4ng x\u1EED l\xFD \u0111\u01B0\u1EE3c: ${msg}
|