@nalvietnam/avatar-cli 1.2.11 → 1.3.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/dist/index.js
CHANGED
|
@@ -5,7 +5,7 @@ import { Command } from "commander";
|
|
|
5
5
|
|
|
6
6
|
// src/commands/ai.ts
|
|
7
7
|
import { promises as fs4 } from "fs";
|
|
8
|
-
import { join as
|
|
8
|
+
import { join as join6 } from "path";
|
|
9
9
|
import { confirm } from "@inquirer/prompts";
|
|
10
10
|
|
|
11
11
|
// src/lib/filesystem-helpers.ts
|
|
@@ -42,12 +42,44 @@ async function writeJsonAtomic(path, data, mode) {
|
|
|
42
42
|
`, mode);
|
|
43
43
|
}
|
|
44
44
|
|
|
45
|
+
// src/lib/resolve-avatar-workspace-root-from-cwd.ts
|
|
46
|
+
import { existsSync, readFileSync } from "fs";
|
|
47
|
+
import { dirname as dirname2, join as join2 } from "path";
|
|
48
|
+
var MAX_WALKUP_LEVELS = 5;
|
|
49
|
+
function isAvatarWorkspace(dir) {
|
|
50
|
+
const hasClaudeDir = existsSync(join2(dir, ".claude"));
|
|
51
|
+
const hasClaudeMd = existsSync(join2(dir, "CLAUDE.md"));
|
|
52
|
+
if (!hasClaudeDir || !hasClaudeMd) return false;
|
|
53
|
+
const hasSrcDir = existsSync(join2(dir, "src"));
|
|
54
|
+
const gitmodulesPath = join2(dir, ".gitmodules");
|
|
55
|
+
if (hasSrcDir) return true;
|
|
56
|
+
if (existsSync(gitmodulesPath)) {
|
|
57
|
+
try {
|
|
58
|
+
const content = readFileSync(gitmodulesPath, "utf8");
|
|
59
|
+
return content.includes("path = src") || content.includes("path = ./src");
|
|
60
|
+
} catch {
|
|
61
|
+
return false;
|
|
62
|
+
}
|
|
63
|
+
}
|
|
64
|
+
return false;
|
|
65
|
+
}
|
|
66
|
+
function resolveAvatarWorkspaceRootFromCwd(startDir) {
|
|
67
|
+
let current = startDir;
|
|
68
|
+
for (let i = 0; i < MAX_WALKUP_LEVELS; i++) {
|
|
69
|
+
if (isAvatarWorkspace(current)) return current;
|
|
70
|
+
const parent = dirname2(current);
|
|
71
|
+
if (parent === current) return null;
|
|
72
|
+
current = parent;
|
|
73
|
+
}
|
|
74
|
+
return null;
|
|
75
|
+
}
|
|
76
|
+
|
|
45
77
|
// src/lib/audit-log-appender.ts
|
|
46
78
|
import { promises as fs2 } from "fs";
|
|
47
79
|
|
|
48
80
|
// src/lib/user-config-store.ts
|
|
49
81
|
import { homedir } from "os";
|
|
50
|
-
import { join as
|
|
82
|
+
import { join as join3 } from "path";
|
|
51
83
|
|
|
52
84
|
// src/types/config-schema.ts
|
|
53
85
|
import { z } from "zod";
|
|
@@ -80,11 +112,11 @@ var projectSettingsSchema = z.object({
|
|
|
80
112
|
var initModeSchema = z.enum(["internal", "client", "library"]);
|
|
81
113
|
|
|
82
114
|
// src/lib/user-config-store.ts
|
|
83
|
-
var AVATAR_HOME =
|
|
84
|
-
var USER_CONFIG_PATH =
|
|
85
|
-
var USER_STATE_PATH =
|
|
86
|
-
var AUDIT_LOG_PATH =
|
|
87
|
-
var BACKUPS_DIR =
|
|
115
|
+
var AVATAR_HOME = join3(homedir(), ".avatar");
|
|
116
|
+
var USER_CONFIG_PATH = join3(AVATAR_HOME, "config.json");
|
|
117
|
+
var USER_STATE_PATH = join3(AVATAR_HOME, "state.json");
|
|
118
|
+
var AUDIT_LOG_PATH = join3(AVATAR_HOME, "audit.log");
|
|
119
|
+
var BACKUPS_DIR = join3(AVATAR_HOME, "backups");
|
|
88
120
|
var SECRET_FILE_MODE = 384;
|
|
89
121
|
async function ensureAvatarHome() {
|
|
90
122
|
await ensureDir(AVATAR_HOME);
|
|
@@ -348,18 +380,18 @@ function installClaudeCodeViaNpm() {
|
|
|
348
380
|
}
|
|
349
381
|
|
|
350
382
|
// src/lib/prompt-ai-provider-choice.ts
|
|
351
|
-
import { readFileSync } from "fs";
|
|
383
|
+
import { readFileSync as readFileSync2 } from "fs";
|
|
352
384
|
import { homedir as homedir2 } from "os";
|
|
353
|
-
import { join as
|
|
385
|
+
import { join as join4 } from "path";
|
|
354
386
|
import { select } from "@inquirer/prompts";
|
|
355
387
|
function getGlobalSettingsPath() {
|
|
356
|
-
return
|
|
388
|
+
return join4(homedir2(), ".claude", "settings.json");
|
|
357
389
|
}
|
|
358
390
|
function detectGlobalClaudeSettings() {
|
|
359
391
|
const path = getGlobalSettingsPath();
|
|
360
392
|
let raw;
|
|
361
393
|
try {
|
|
362
|
-
raw =
|
|
394
|
+
raw = readFileSync2(path, "utf8");
|
|
363
395
|
} catch {
|
|
364
396
|
return { exists: false, hasBaseUrl: false, hasToken: false };
|
|
365
397
|
}
|
|
@@ -497,10 +529,10 @@ async function setupLLMLiteApiKeyAndModel() {
|
|
|
497
529
|
|
|
498
530
|
// src/lib/write-claude-settings-json-per-project.ts
|
|
499
531
|
import { promises as fs3 } from "fs";
|
|
500
|
-
import { join as
|
|
532
|
+
import { join as join5 } from "path";
|
|
501
533
|
var SECRET_FILE_MODE2 = 384;
|
|
502
534
|
function getClaudeSettingsPath(workspacePath) {
|
|
503
|
-
return
|
|
535
|
+
return join5(workspacePath, ".claude", "settings.json");
|
|
504
536
|
}
|
|
505
537
|
async function readExistingSettings(path) {
|
|
506
538
|
if (!await pathExists(path)) return {};
|
|
@@ -546,19 +578,19 @@ function applyUseGlobal(existing, source) {
|
|
|
546
578
|
...sourceModel ? { model: sourceModel } : {}
|
|
547
579
|
};
|
|
548
580
|
}
|
|
549
|
-
async function writeClaudeSettings(workspacePath,
|
|
581
|
+
async function writeClaudeSettings(workspacePath, input6) {
|
|
550
582
|
const path = getClaudeSettingsPath(workspacePath);
|
|
551
583
|
const existing = await readExistingSettings(path);
|
|
552
584
|
let merged;
|
|
553
|
-
switch (
|
|
585
|
+
switch (input6.provider) {
|
|
554
586
|
case "subscription":
|
|
555
|
-
merged = applySubscription(existing,
|
|
587
|
+
merged = applySubscription(existing, input6.model);
|
|
556
588
|
break;
|
|
557
589
|
case "llmlite":
|
|
558
|
-
merged = applyLLMLite(existing,
|
|
590
|
+
merged = applyLLMLite(existing, input6.apiKey, input6.baseUrl, input6.model);
|
|
559
591
|
break;
|
|
560
592
|
case "use-global":
|
|
561
|
-
merged = applyUseGlobal(existing,
|
|
593
|
+
merged = applyUseGlobal(existing, input6.sourceSettings);
|
|
562
594
|
break;
|
|
563
595
|
}
|
|
564
596
|
await writeJsonAtomic(path, merged, SECRET_FILE_MODE2);
|
|
@@ -752,15 +784,23 @@ async function testAiProviderByDetectedMode(settings) {
|
|
|
752
784
|
// src/commands/ai.ts
|
|
753
785
|
async function ensureWorkspaceCwd() {
|
|
754
786
|
const cwd = process.cwd();
|
|
755
|
-
const
|
|
756
|
-
if (!
|
|
757
|
-
log.error(
|
|
787
|
+
const workspaceRoot = resolveAvatarWorkspaceRootFromCwd(cwd);
|
|
788
|
+
if (!workspaceRoot) {
|
|
789
|
+
log.error(
|
|
790
|
+
`Kh\xF4ng t\xECm th\u1EA5y Avatar workspace t\u1EEB th\u01B0 m\u1EE5c hi\u1EC7n t\u1EA1i.
|
|
791
|
+
Avatar workspace c\u1EA7n c\xF3: .claude/ + CLAUDE.md + src/ (ho\u1EB7c .gitmodules).
|
|
792
|
+
B\u1EA1n \u0111ang \u1EDF: ${cwd}
|
|
793
|
+
Cd v\xE0o workspace dir (vd /path/to/<project>-workspace) r\u1ED3i ch\u1EA1y l\u1EA1i.`
|
|
794
|
+
);
|
|
758
795
|
process.exit(1);
|
|
759
796
|
}
|
|
760
|
-
|
|
797
|
+
if (workspaceRoot !== cwd) {
|
|
798
|
+
log.dim(`Detected workspace root: ${workspaceRoot}`);
|
|
799
|
+
}
|
|
800
|
+
return workspaceRoot;
|
|
761
801
|
}
|
|
762
802
|
async function readWorkspaceSettings(workspacePath) {
|
|
763
|
-
const settingsPath =
|
|
803
|
+
const settingsPath = join6(workspacePath, ".claude", "settings.json");
|
|
764
804
|
if (!await pathExists(settingsPath)) return {};
|
|
765
805
|
try {
|
|
766
806
|
return await readJson(settingsPath);
|
|
@@ -798,7 +838,7 @@ async function runAiTest() {
|
|
|
798
838
|
}
|
|
799
839
|
async function runAiReset(opts) {
|
|
800
840
|
const workspacePath = await ensureWorkspaceCwd();
|
|
801
|
-
const settingsPath =
|
|
841
|
+
const settingsPath = join6(workspacePath, ".claude", "settings.json");
|
|
802
842
|
const settings = await readWorkspaceSettings(workspacePath);
|
|
803
843
|
if (!opts.yes) {
|
|
804
844
|
const ok = await confirm({
|
|
@@ -843,47 +883,186 @@ function registerAiCommand(program2) {
|
|
|
843
883
|
});
|
|
844
884
|
}
|
|
845
885
|
|
|
846
|
-
// src/
|
|
847
|
-
|
|
848
|
-
|
|
849
|
-
|
|
850
|
-
|
|
851
|
-
|
|
886
|
+
// src/commands/commit.ts
|
|
887
|
+
import { input as input2 } from "@inquirer/prompts";
|
|
888
|
+
|
|
889
|
+
// src/lib/execute-commit-with-target-selection.ts
|
|
890
|
+
import { spawnSync as spawnSync5 } from "child_process";
|
|
891
|
+
import { existsSync as existsSync2 } from "fs";
|
|
892
|
+
import { join as join7 } from "path";
|
|
893
|
+
function assertAvatarWorkspaceRoot(cwd) {
|
|
894
|
+
const srcGit = join7(cwd, "src", ".git");
|
|
895
|
+
const workspaceGit = join7(cwd, ".git");
|
|
896
|
+
const claudeDir = join7(cwd, ".claude");
|
|
897
|
+
if (!existsSync2(workspaceGit)) {
|
|
898
|
+
throw new Error(
|
|
899
|
+
`Kh\xF4ng ph\u1EA3i workspace root: ${cwd}
|
|
900
|
+
Ch\u1EA1y 'avatar commit' trong workspace dir (c\xF3 .git v\xE0 .claude/).`
|
|
852
901
|
);
|
|
853
|
-
|
|
854
|
-
|
|
855
|
-
|
|
856
|
-
|
|
857
|
-
|
|
858
|
-
|
|
859
|
-
}
|
|
902
|
+
}
|
|
903
|
+
if (!existsSync2(claudeDir)) {
|
|
904
|
+
throw new Error(
|
|
905
|
+
`Kh\xF4ng th\u1EA5y .claude/ trong ${cwd}.
|
|
906
|
+
Ch\u1EA1y 'avatar commit' trong Avatar workspace, kh\xF4ng ph\u1EA3i project b\xECnh th\u01B0\u1EDDng.`
|
|
907
|
+
);
|
|
908
|
+
}
|
|
909
|
+
if (!existsSync2(srcGit)) {
|
|
910
|
+
throw new Error(
|
|
911
|
+
`Kh\xF4ng th\u1EA5y src/.git trong ${cwd}.
|
|
912
|
+
Workspace thi\u1EBFu submodule src/. Ch\u1EA1y 'avatar init' l\u1EA1i?`
|
|
913
|
+
);
|
|
914
|
+
}
|
|
915
|
+
}
|
|
916
|
+
function gitExec(cwd, args) {
|
|
917
|
+
const r = spawnSync5("git", ["-C", cwd, ...args], {
|
|
918
|
+
encoding: "utf8",
|
|
919
|
+
stdio: ["ignore", "pipe", "pipe"]
|
|
920
|
+
});
|
|
921
|
+
if (r.status !== 0) {
|
|
922
|
+
const stderr = (r.stderr || "").trim();
|
|
923
|
+
throw new Error(`git ${args.join(" ")} (in ${cwd}) failed:
|
|
924
|
+
${stderr}`);
|
|
925
|
+
}
|
|
926
|
+
return (r.stdout || "").trim();
|
|
927
|
+
}
|
|
928
|
+
function isDirty(cwd) {
|
|
929
|
+
const status = gitExec(cwd, ["status", "--porcelain"]);
|
|
930
|
+
return status.length > 0;
|
|
931
|
+
}
|
|
932
|
+
async function commitSrc(workspaceRoot, opts) {
|
|
933
|
+
const srcPath = join7(workspaceRoot, "src");
|
|
934
|
+
if (!isDirty(srcPath)) {
|
|
935
|
+
log.dim("src/: nothing to commit (clean)");
|
|
936
|
+
return {};
|
|
937
|
+
}
|
|
938
|
+
log.info("Committing src/ ...");
|
|
939
|
+
gitExec(srcPath, ["add", "."]);
|
|
940
|
+
gitExec(srcPath, ["commit", "-m", opts.message]);
|
|
941
|
+
const sha = gitExec(srcPath, ["rev-parse", "HEAD"]);
|
|
942
|
+
log.success(`src/ committed: ${sha.slice(0, 7)}`);
|
|
943
|
+
let pushed = false;
|
|
944
|
+
if (opts.push) {
|
|
945
|
+
log.info("Pushing src/ ...");
|
|
946
|
+
gitExec(srcPath, ["push"]);
|
|
947
|
+
log.success("src/ pushed");
|
|
948
|
+
pushed = true;
|
|
949
|
+
}
|
|
950
|
+
return { sha, pushed };
|
|
951
|
+
}
|
|
952
|
+
async function commitWorkspace(workspaceRoot, opts) {
|
|
953
|
+
if (!isDirty(workspaceRoot)) {
|
|
954
|
+
log.dim("workspace: nothing to commit (clean)");
|
|
955
|
+
return {};
|
|
956
|
+
}
|
|
957
|
+
log.info("Committing workspace root ...");
|
|
958
|
+
gitExec(workspaceRoot, ["add", "."]);
|
|
959
|
+
gitExec(workspaceRoot, ["commit", "-m", opts.message]);
|
|
960
|
+
const sha = gitExec(workspaceRoot, ["rev-parse", "HEAD"]);
|
|
961
|
+
log.success(`workspace committed: ${sha.slice(0, 7)}`);
|
|
962
|
+
let pushed = false;
|
|
963
|
+
if (opts.push) {
|
|
964
|
+
log.info("Pushing workspace ...");
|
|
965
|
+
gitExec(workspaceRoot, ["push"]);
|
|
966
|
+
log.success("workspace pushed");
|
|
967
|
+
pushed = true;
|
|
968
|
+
}
|
|
969
|
+
return { sha, pushed };
|
|
970
|
+
}
|
|
971
|
+
function warnIfOtherTargetDirty(workspaceRoot, requestedTarget) {
|
|
972
|
+
if (requestedTarget === "all") return;
|
|
973
|
+
const srcDirty = isDirty(join7(workspaceRoot, "src"));
|
|
974
|
+
const workspaceDirty = isDirty(workspaceRoot);
|
|
975
|
+
if (requestedTarget === "src" && workspaceDirty) {
|
|
976
|
+
log.warn(
|
|
977
|
+
"Workspace c\u0169ng c\xF3 changes ch\u01B0a commit. Ch\u1EA1y 'avatar commit workspace' (ho\u1EB7c 'avatar commit all') \u0111\u1EC3 commit lu\xF4n."
|
|
978
|
+
);
|
|
979
|
+
}
|
|
980
|
+
if (requestedTarget === "workspace" && srcDirty) {
|
|
981
|
+
log.warn(
|
|
982
|
+
"src/ c\u0169ng c\xF3 changes ch\u01B0a commit. Ch\u1EA1y 'avatar commit src' (ho\u1EB7c 'avatar commit all') \u0111\u1EC3 commit lu\xF4n."
|
|
983
|
+
);
|
|
984
|
+
}
|
|
985
|
+
}
|
|
986
|
+
async function executeCommitWithTargetSelection(args) {
|
|
987
|
+
assertAvatarWorkspaceRoot(args.workspaceRoot);
|
|
988
|
+
warnIfOtherTargetDirty(args.workspaceRoot, args.target);
|
|
989
|
+
const result = { target: args.target, skipped: [] };
|
|
990
|
+
if (args.target === "src" || args.target === "all") {
|
|
991
|
+
const srcOutcome = await commitSrc(args.workspaceRoot, args.options);
|
|
992
|
+
result.srcCommitSha = srcOutcome.sha;
|
|
993
|
+
result.srcPushed = srcOutcome.pushed;
|
|
994
|
+
if (!srcOutcome.sha) result.skipped?.push("src");
|
|
995
|
+
}
|
|
996
|
+
if (args.target === "workspace" || args.target === "all") {
|
|
997
|
+
const wsOutcome = await commitWorkspace(args.workspaceRoot, args.options);
|
|
998
|
+
result.workspaceCommitSha = wsOutcome.sha;
|
|
999
|
+
result.workspacePushed = wsOutcome.pushed;
|
|
1000
|
+
if (!wsOutcome.sha) result.skipped?.push("workspace");
|
|
1001
|
+
}
|
|
1002
|
+
return result;
|
|
860
1003
|
}
|
|
861
1004
|
|
|
862
1005
|
// src/commands/commit.ts
|
|
863
1006
|
function registerCommitCommand(program2) {
|
|
864
|
-
program2.command("commit").description("Commit code
|
|
1007
|
+
const commit = program2.command("commit").description("Commit code (src/) ho\u1EB7c Avatar state (workspace) \u2014 split-aware (M07)");
|
|
1008
|
+
commit.command("src").description("Commit src/ \u2192 push l\xEAn client repo remote").option("-m, --message <msg>", "Commit message").option("--push", "Auto push sau commit (default: ch\u1EC9 commit)").action(async (opts) => {
|
|
1009
|
+
await runCommit("src", opts);
|
|
1010
|
+
});
|
|
1011
|
+
commit.command("workspace").description("Commit workspace root \u2192 push l\xEAn team remote (Avatar state)").option("-m, --message <msg>", "Commit message").option("--push", "Auto push sau commit (default: ch\u1EC9 commit)").action(async (opts) => {
|
|
1012
|
+
await runCommit("workspace", opts);
|
|
1013
|
+
});
|
|
1014
|
+
commit.command("all").description("Commit c\u1EA3 src/ v\xE0 workspace (src tr\u01B0\u1EDBc, gitlink update, workspace sau)").option("-m, --message <msg>", "Commit message").option("--push", "Auto push sau commit (default: ch\u1EC9 commit)").action(async (opts) => {
|
|
1015
|
+
await runCommit("all", opts);
|
|
1016
|
+
});
|
|
1017
|
+
}
|
|
1018
|
+
async function runCommit(target, opts) {
|
|
1019
|
+
try {
|
|
1020
|
+
const message = opts.message ?? await input2({
|
|
1021
|
+
message: "Commit message:",
|
|
1022
|
+
validate: (v) => v.trim().length > 0 ? true : "Message b\u1EAFt bu\u1ED9c"
|
|
1023
|
+
});
|
|
1024
|
+
const result = await executeCommitWithTargetSelection({
|
|
1025
|
+
workspaceRoot: process.cwd(),
|
|
1026
|
+
target,
|
|
1027
|
+
options: { message, push: opts.push }
|
|
1028
|
+
});
|
|
1029
|
+
if (result.srcCommitSha) {
|
|
1030
|
+
log.success(`src/: ${result.srcCommitSha.slice(0, 7)}${result.srcPushed ? " (pushed)" : ""}`);
|
|
1031
|
+
}
|
|
1032
|
+
if (result.workspaceCommitSha) {
|
|
1033
|
+
log.success(
|
|
1034
|
+
`workspace: ${result.workspaceCommitSha.slice(0, 7)}${result.workspacePushed ? " (pushed)" : ""}`
|
|
1035
|
+
);
|
|
1036
|
+
}
|
|
1037
|
+
if (result.skipped && result.skipped.length > 0) {
|
|
1038
|
+
log.dim(`Skipped (nothing to commit): ${result.skipped.join(", ")}`);
|
|
1039
|
+
}
|
|
1040
|
+
} catch (err) {
|
|
1041
|
+
log.error(err instanceof Error ? err.message : String(err));
|
|
1042
|
+
process.exit(1);
|
|
1043
|
+
}
|
|
865
1044
|
}
|
|
866
1045
|
|
|
867
1046
|
// src/commands/doctor.ts
|
|
868
|
-
import { spawnSync as
|
|
1047
|
+
import { spawnSync as spawnSync6 } from "child_process";
|
|
869
1048
|
import { promises as fs6 } from "fs";
|
|
870
|
-
import { join as
|
|
1049
|
+
import { join as join11 } from "path";
|
|
871
1050
|
import boxen from "boxen";
|
|
872
1051
|
|
|
873
1052
|
// src/lib/git-operations.ts
|
|
874
|
-
import { join as
|
|
1053
|
+
import { join as join8 } from "path";
|
|
875
1054
|
import { simpleGit } from "simple-git";
|
|
876
1055
|
function git(cwd = process.cwd()) {
|
|
877
1056
|
return simpleGit({ baseDir: cwd, binary: "git" });
|
|
878
1057
|
}
|
|
879
1058
|
async function isGitRepo(cwd = process.cwd()) {
|
|
880
|
-
return await pathExists(
|
|
1059
|
+
return await pathExists(join8(cwd, ".git"));
|
|
881
1060
|
}
|
|
882
1061
|
async function addSubmodule(repoUrl, destPath, cwd = process.cwd()) {
|
|
883
1062
|
await git(cwd).subModule(["add", repoUrl, destPath]);
|
|
884
1063
|
}
|
|
885
1064
|
async function checkoutTagInSubmodule(submodulePath, tag, cwd = process.cwd()) {
|
|
886
|
-
const submoduleCwd =
|
|
1065
|
+
const submoduleCwd = join8(cwd, submodulePath);
|
|
887
1066
|
await git(submoduleCwd).fetch(["--tags"]);
|
|
888
1067
|
await git(submoduleCwd).checkout(tag);
|
|
889
1068
|
}
|
|
@@ -902,11 +1081,11 @@ async function currentCommitSha(cwd = process.cwd()) {
|
|
|
902
1081
|
|
|
903
1082
|
// src/lib/project-tree-scaffolder.ts
|
|
904
1083
|
import { promises as fs5 } from "fs";
|
|
905
|
-
import { join as
|
|
1084
|
+
import { join as join10 } from "path";
|
|
906
1085
|
|
|
907
1086
|
// src/lib/template-bundle-loader.ts
|
|
908
|
-
import { existsSync } from "fs";
|
|
909
|
-
import { dirname as
|
|
1087
|
+
import { existsSync as existsSync3 } from "fs";
|
|
1088
|
+
import { dirname as dirname3, join as join9 } from "path";
|
|
910
1089
|
import { fileURLToPath } from "url";
|
|
911
1090
|
|
|
912
1091
|
// src/lib/mustache-template-engine.ts
|
|
@@ -920,15 +1099,15 @@ function renderTemplate(source, variables) {
|
|
|
920
1099
|
}
|
|
921
1100
|
|
|
922
1101
|
// src/lib/template-bundle-loader.ts
|
|
923
|
-
var HERE =
|
|
1102
|
+
var HERE = dirname3(fileURLToPath(import.meta.url));
|
|
924
1103
|
var PACKAGE_ROOT = findPackageRoot(HERE);
|
|
925
|
-
var TEMPLATES_ROOT =
|
|
926
|
-
var HOOKS_ROOT =
|
|
1104
|
+
var TEMPLATES_ROOT = join9(PACKAGE_ROOT, "src", "templates");
|
|
1105
|
+
var HOOKS_ROOT = join9(PACKAGE_ROOT, "src", "hooks");
|
|
927
1106
|
function findPackageRoot(startDir) {
|
|
928
1107
|
let dir = startDir;
|
|
929
1108
|
while (true) {
|
|
930
|
-
if (
|
|
931
|
-
const parent =
|
|
1109
|
+
if (existsSync3(join9(dir, "package.json"))) return dir;
|
|
1110
|
+
const parent = dirname3(dir);
|
|
932
1111
|
if (parent === dir) {
|
|
933
1112
|
throw new Error(`Cannot locate package root from ${startDir}`);
|
|
934
1113
|
}
|
|
@@ -936,14 +1115,14 @@ function findPackageRoot(startDir) {
|
|
|
936
1115
|
}
|
|
937
1116
|
}
|
|
938
1117
|
async function loadTemplate(name) {
|
|
939
|
-
return await readText(
|
|
1118
|
+
return await readText(join9(TEMPLATES_ROOT, `${name}.tpl`));
|
|
940
1119
|
}
|
|
941
1120
|
async function renderTemplateByName(name, variables) {
|
|
942
1121
|
const source = await loadTemplate(name);
|
|
943
1122
|
return renderTemplate(source, variables);
|
|
944
1123
|
}
|
|
945
1124
|
async function loadHook(name) {
|
|
946
|
-
return await readText(
|
|
1125
|
+
return await readText(join9(HOOKS_ROOT, `${name}.sh.tpl`));
|
|
947
1126
|
}
|
|
948
1127
|
|
|
949
1128
|
// src/lib/project-tree-scaffolder.ts
|
|
@@ -977,12 +1156,12 @@ var PROJECT_KNOWLEDGE_TEMPLATES = [
|
|
|
977
1156
|
"project/gotchas.md"
|
|
978
1157
|
];
|
|
979
1158
|
async function createClaudeDirTree(projectRoot) {
|
|
980
|
-
const claudeRoot =
|
|
1159
|
+
const claudeRoot = join10(projectRoot, ".claude");
|
|
981
1160
|
await ensureDir(claudeRoot);
|
|
982
1161
|
for (const sub of CLAUDE_SUBDIRS) {
|
|
983
|
-
const dir =
|
|
1162
|
+
const dir = join10(claudeRoot, sub);
|
|
984
1163
|
await ensureDir(dir);
|
|
985
|
-
await writeTextAtomic(
|
|
1164
|
+
await writeTextAtomic(join10(dir, ".gitkeep"), "");
|
|
986
1165
|
}
|
|
987
1166
|
}
|
|
988
1167
|
async function writeProjectKnowledgeFiles(projectRoot, vars) {
|
|
@@ -1013,7 +1192,7 @@ async function writeProjectKnowledgeFiles(projectRoot, vars) {
|
|
|
1013
1192
|
for (const tpl of PROJECT_KNOWLEDGE_TEMPLATES) {
|
|
1014
1193
|
const content = await renderTemplateByName(tpl, baseVars);
|
|
1015
1194
|
const relative4 = tpl.replace(/^project\//, "");
|
|
1016
|
-
const outPath =
|
|
1195
|
+
const outPath = join10(projectRoot, ".claude", "project", relative4);
|
|
1017
1196
|
const backup = await writeWithBackup(outPath, content);
|
|
1018
1197
|
if (backup) backups.push(backup);
|
|
1019
1198
|
}
|
|
@@ -1021,14 +1200,14 @@ async function writeProjectKnowledgeFiles(projectRoot, vars) {
|
|
|
1021
1200
|
}
|
|
1022
1201
|
async function writeRootClaudeMd(projectRoot, vars) {
|
|
1023
1202
|
const content = await renderTemplateByName("CLAUDE.md", vars);
|
|
1024
|
-
return await writeWithBackup(
|
|
1203
|
+
return await writeWithBackup(join10(projectRoot, "CLAUDE.md"), content);
|
|
1025
1204
|
}
|
|
1026
1205
|
async function writeProjectSettings(projectRoot, vars) {
|
|
1027
1206
|
const content = await renderTemplateByName("settings.json", vars);
|
|
1028
|
-
return await writeWithBackup(
|
|
1207
|
+
return await writeWithBackup(join10(projectRoot, ".claude", "settings.json"), content);
|
|
1029
1208
|
}
|
|
1030
1209
|
async function appendGitignoreEntries(projectRoot) {
|
|
1031
|
-
const path =
|
|
1210
|
+
const path = join10(projectRoot, ".gitignore");
|
|
1032
1211
|
const tpl = await renderTemplateByName("gitignore", {});
|
|
1033
1212
|
const marker = "# Avatar \u2014 git-ignored entries injected on `avatar init`";
|
|
1034
1213
|
let existing = "";
|
|
@@ -1042,9 +1221,9 @@ ${tpl}`);
|
|
|
1042
1221
|
}
|
|
1043
1222
|
async function installGitHook(gitDir, hookName) {
|
|
1044
1223
|
const content = await loadHook(hookName);
|
|
1045
|
-
const hooksDir =
|
|
1224
|
+
const hooksDir = join10(gitDir, "hooks");
|
|
1046
1225
|
await ensureDir(hooksDir);
|
|
1047
|
-
const dest =
|
|
1226
|
+
const dest = join10(hooksDir, hookName);
|
|
1048
1227
|
await writeTextAtomic(dest, content, 493);
|
|
1049
1228
|
}
|
|
1050
1229
|
|
|
@@ -1102,7 +1281,7 @@ async function runChecks(cwd) {
|
|
|
1102
1281
|
detail: gitRepo ? cwd : "Kh\xF4ng ph\u1EA3i git repo (c\u1EA7n cho 'avatar init')",
|
|
1103
1282
|
fixable: false
|
|
1104
1283
|
});
|
|
1105
|
-
const packPath =
|
|
1284
|
+
const packPath = join11(cwd, ".claude", "pack");
|
|
1106
1285
|
const hasPack = await pathExists(packPath);
|
|
1107
1286
|
checks.push({
|
|
1108
1287
|
name: "team-ai-pack submodule",
|
|
@@ -1110,7 +1289,7 @@ async function runChecks(cwd) {
|
|
|
1110
1289
|
detail: hasPack ? packPath : "Avatar ch\u01B0a init \u2014 ch\u1EA1y 'avatar init'",
|
|
1111
1290
|
fixable: false
|
|
1112
1291
|
});
|
|
1113
|
-
const claudeMdPath =
|
|
1292
|
+
const claudeMdPath = join11(cwd, "CLAUDE.md");
|
|
1114
1293
|
const hasClaudeMd = await pathExists(claudeMdPath);
|
|
1115
1294
|
checks.push({
|
|
1116
1295
|
name: "CLAUDE.md",
|
|
@@ -1118,7 +1297,7 @@ async function runChecks(cwd) {
|
|
|
1118
1297
|
detail: hasClaudeMd ? "t\u1ED3n t\u1EA1i \u1EDF project root" : "thi\u1EBFu \u2014 ch\u1EA1y 'avatar init'",
|
|
1119
1298
|
fixable: false
|
|
1120
1299
|
});
|
|
1121
|
-
const hookPath =
|
|
1300
|
+
const hookPath = join11(cwd, ".git", "hooks", "post-merge");
|
|
1122
1301
|
const hasHook = await pathExists(hookPath);
|
|
1123
1302
|
if (gitRepo && hasPack) {
|
|
1124
1303
|
checks.push({
|
|
@@ -1127,11 +1306,11 @@ async function runChecks(cwd) {
|
|
|
1127
1306
|
detail: hasHook ? "installed" : "missing \u2014 fixable",
|
|
1128
1307
|
fixable: !hasHook,
|
|
1129
1308
|
fix: hasHook ? void 0 : async () => {
|
|
1130
|
-
await installGitHook(
|
|
1309
|
+
await installGitHook(join11(cwd, ".git"), "post-merge");
|
|
1131
1310
|
}
|
|
1132
1311
|
});
|
|
1133
1312
|
}
|
|
1134
|
-
const gitignorePath =
|
|
1313
|
+
const gitignorePath = join11(cwd, ".gitignore");
|
|
1135
1314
|
if (gitRepo) {
|
|
1136
1315
|
let gitignoreOk = false;
|
|
1137
1316
|
if (await pathExists(gitignorePath)) {
|
|
@@ -1145,7 +1324,7 @@ async function runChecks(cwd) {
|
|
|
1145
1324
|
fixable: false
|
|
1146
1325
|
});
|
|
1147
1326
|
}
|
|
1148
|
-
const which =
|
|
1327
|
+
const which = spawnSync6("which", ["claude"]);
|
|
1149
1328
|
const hasClaudeCli = which.status === 0;
|
|
1150
1329
|
checks.push({
|
|
1151
1330
|
name: "Claude Code CLI",
|
|
@@ -1193,16 +1372,16 @@ async function applyFixes(checks) {
|
|
|
1193
1372
|
}
|
|
1194
1373
|
|
|
1195
1374
|
// src/commands/init.ts
|
|
1196
|
-
import { basename, join as
|
|
1197
|
-
import { confirm as confirm3, input as
|
|
1375
|
+
import { basename, join as join18, relative as relative2, resolve } from "path";
|
|
1376
|
+
import { confirm as confirm3, input as input5, select as select8 } from "@inquirer/prompts";
|
|
1198
1377
|
import boxen4 from "boxen";
|
|
1199
1378
|
|
|
1200
1379
|
// src/lib/add-team-pack-submodule-with-retry-on-network-fail.ts
|
|
1201
|
-
import { spawnSync as
|
|
1380
|
+
import { spawnSync as spawnSync8 } from "child_process";
|
|
1202
1381
|
import { select as select5 } from "@inquirer/prompts";
|
|
1203
1382
|
|
|
1204
1383
|
// src/lib/prompt-recovery-action-on-failure.ts
|
|
1205
|
-
import { input as
|
|
1384
|
+
import { input as input3, select as select3 } from "@inquirer/prompts";
|
|
1206
1385
|
var UserAbortedRecoveryError = class extends Error {
|
|
1207
1386
|
constructor(message) {
|
|
1208
1387
|
super(message);
|
|
@@ -1226,10 +1405,10 @@ async function promptRetryOrSkip(args) {
|
|
|
1226
1405
|
}
|
|
1227
1406
|
|
|
1228
1407
|
// src/lib/team-pack-submodule-manager.ts
|
|
1229
|
-
import { join as
|
|
1408
|
+
import { join as join12 } from "path";
|
|
1230
1409
|
|
|
1231
1410
|
// src/lib/check-team-pack-access-with-retry-loop.ts
|
|
1232
|
-
import { spawnSync as
|
|
1411
|
+
import { spawnSync as spawnSync7 } from "child_process";
|
|
1233
1412
|
import { confirm as confirm2, select as select4 } from "@inquirer/prompts";
|
|
1234
1413
|
import boxen2 from "boxen";
|
|
1235
1414
|
function parseRepoSlugFromGitUrl(url) {
|
|
@@ -1238,11 +1417,11 @@ function parseRepoSlugFromGitUrl(url) {
|
|
|
1238
1417
|
return null;
|
|
1239
1418
|
}
|
|
1240
1419
|
function checkRepoAccess(repoSlug) {
|
|
1241
|
-
const r =
|
|
1420
|
+
const r = spawnSync7("gh", ["api", `repos/${repoSlug}`], { stdio: "ignore" });
|
|
1242
1421
|
return r.status === 0;
|
|
1243
1422
|
}
|
|
1244
1423
|
function getCurrentGhUser() {
|
|
1245
|
-
const r =
|
|
1424
|
+
const r = spawnSync7("gh", ["api", "user", "--jq", ".login"], {
|
|
1246
1425
|
encoding: "utf8",
|
|
1247
1426
|
stdio: ["ignore", "pipe", "pipe"]
|
|
1248
1427
|
});
|
|
@@ -1251,7 +1430,7 @@ function getCurrentGhUser() {
|
|
|
1251
1430
|
}
|
|
1252
1431
|
function triggerGhAuthLoginInteractive() {
|
|
1253
1432
|
log.info("M\u1EDF browser \u0111\u1EC3 switch GitHub account...");
|
|
1254
|
-
const r =
|
|
1433
|
+
const r = spawnSync7("gh", ["auth", "login", "--web"], { stdio: "inherit" });
|
|
1255
1434
|
if (r.status !== 0) {
|
|
1256
1435
|
log.warn(`gh auth login exit ${r.status}. C\xF3 th\u1EC3 ch\u1EA1y tay r\u1ED3i quay l\u1EA1i retry.`);
|
|
1257
1436
|
}
|
|
@@ -1391,7 +1570,7 @@ async function addTeamPackSubmodule(projectRoot, tag, ssoEmail) {
|
|
|
1391
1570
|
}
|
|
1392
1571
|
let target = tag ?? null;
|
|
1393
1572
|
if (!target) {
|
|
1394
|
-
target = await latestTag(
|
|
1573
|
+
target = await latestTag(join12(projectRoot, TEAM_PACK_RELATIVE_PATH));
|
|
1395
1574
|
}
|
|
1396
1575
|
if (target) {
|
|
1397
1576
|
await checkoutTagInSubmodule(TEAM_PACK_RELATIVE_PATH, target, projectRoot);
|
|
@@ -1399,7 +1578,7 @@ async function addTeamPackSubmodule(projectRoot, tag, ssoEmail) {
|
|
|
1399
1578
|
return { pinnedTag: target };
|
|
1400
1579
|
}
|
|
1401
1580
|
async function readPinnedPackVersion(projectRoot) {
|
|
1402
|
-
const submoduleRoot =
|
|
1581
|
+
const submoduleRoot = join12(projectRoot, TEAM_PACK_RELATIVE_PATH);
|
|
1403
1582
|
const tag = await latestTag(submoduleRoot);
|
|
1404
1583
|
if (tag) return tag;
|
|
1405
1584
|
const sha = await currentCommitSha(submoduleRoot);
|
|
@@ -1413,14 +1592,14 @@ function isSshPermissionError(message) {
|
|
|
1413
1592
|
}
|
|
1414
1593
|
function triggerGhAuthLoginInteractive2() {
|
|
1415
1594
|
log.info("M\u1EDF browser \u0111\u1EC3 switch GitHub account...");
|
|
1416
|
-
const r =
|
|
1595
|
+
const r = spawnSync8("gh", ["auth", "login", "--web"], { stdio: "inherit" });
|
|
1417
1596
|
if (r.status !== 0) {
|
|
1418
1597
|
log.warn(`gh auth login exit ${r.status}. C\xF3 th\u1EC3 ch\u1EA1y tay r\u1ED3i quay l\u1EA1i retry.`);
|
|
1419
1598
|
}
|
|
1420
1599
|
}
|
|
1421
1600
|
function openGithubSshKeysPage() {
|
|
1422
1601
|
log.info("M\u1EDF trang GitHub Settings \u2192 SSH Keys...");
|
|
1423
|
-
const r =
|
|
1602
|
+
const r = spawnSync8("open", ["https://github.com/settings/keys"], { stdio: "ignore" });
|
|
1424
1603
|
if (r.status !== 0) {
|
|
1425
1604
|
log.info("URL: https://github.com/settings/keys");
|
|
1426
1605
|
}
|
|
@@ -1570,27 +1749,27 @@ ${renderAvatarBanner(opts)}
|
|
|
1570
1749
|
}
|
|
1571
1750
|
|
|
1572
1751
|
// src/lib/execute-gh-repo-create.ts
|
|
1573
|
-
import { spawnSync as
|
|
1752
|
+
import { spawnSync as spawnSync9 } from "child_process";
|
|
1574
1753
|
var RepoAlreadyExistsError = class extends Error {
|
|
1575
1754
|
constructor(fullName) {
|
|
1576
1755
|
super(`Repo "${fullName}" \u0111\xE3 t\u1ED3n t\u1EA1i tr\xEAn GitHub. \u0110\u1ED5i t\xEAn ho\u1EB7c x\xF3a repo c\u0169.`);
|
|
1577
1756
|
this.name = "RepoAlreadyExistsError";
|
|
1578
1757
|
}
|
|
1579
1758
|
};
|
|
1580
|
-
function executeGhRepoCreate(
|
|
1581
|
-
const fullName = `${
|
|
1759
|
+
function executeGhRepoCreate(input6) {
|
|
1760
|
+
const fullName = `${input6.org}/${input6.name}`;
|
|
1582
1761
|
const args = [
|
|
1583
1762
|
"repo",
|
|
1584
1763
|
"create",
|
|
1585
1764
|
fullName,
|
|
1586
|
-
`--${
|
|
1765
|
+
`--${input6.visibility}`,
|
|
1587
1766
|
"--source",
|
|
1588
|
-
|
|
1767
|
+
input6.folder,
|
|
1589
1768
|
"--remote",
|
|
1590
1769
|
"origin",
|
|
1591
1770
|
"--push"
|
|
1592
1771
|
];
|
|
1593
|
-
const r =
|
|
1772
|
+
const r = spawnSync9("gh", args, { stdio: "inherit" });
|
|
1594
1773
|
if (r.status !== 0) {
|
|
1595
1774
|
if (r.status === 1) {
|
|
1596
1775
|
throw new RepoAlreadyExistsError(fullName);
|
|
@@ -1604,9 +1783,9 @@ function executeGhRepoCreate(input5) {
|
|
|
1604
1783
|
}
|
|
1605
1784
|
|
|
1606
1785
|
// src/lib/resolve-github-username-default.ts
|
|
1607
|
-
import { spawnSync as
|
|
1786
|
+
import { spawnSync as spawnSync10 } from "child_process";
|
|
1608
1787
|
function resolveGithubUsernameDefault() {
|
|
1609
|
-
const r =
|
|
1788
|
+
const r = spawnSync10("gh", ["api", "user", "--jq", ".login"], {
|
|
1610
1789
|
encoding: "utf8",
|
|
1611
1790
|
stdio: ["ignore", "pipe", "pipe"]
|
|
1612
1791
|
});
|
|
@@ -1638,28 +1817,28 @@ function validateRepoVisibility(v) {
|
|
|
1638
1817
|
}
|
|
1639
1818
|
|
|
1640
1819
|
// src/lib/create-github-remote-from-folder.ts
|
|
1641
|
-
function createGithubRemoteFromFolder(
|
|
1642
|
-
validateRepoName(
|
|
1643
|
-
validateRepoVisibility(
|
|
1644
|
-
const org =
|
|
1645
|
-
log.info(`T\u1EA1o GitHub repo ${org}/${
|
|
1820
|
+
function createGithubRemoteFromFolder(input6) {
|
|
1821
|
+
validateRepoName(input6.name);
|
|
1822
|
+
validateRepoVisibility(input6.visibility);
|
|
1823
|
+
const org = input6.org ?? resolveGithubUsernameDefault();
|
|
1824
|
+
log.info(`T\u1EA1o GitHub repo ${org}/${input6.name} (${input6.visibility})...`);
|
|
1646
1825
|
const urls = executeGhRepoCreate({
|
|
1647
|
-
folder:
|
|
1826
|
+
folder: input6.folder,
|
|
1648
1827
|
org,
|
|
1649
|
-
name:
|
|
1650
|
-
visibility:
|
|
1828
|
+
name: input6.name,
|
|
1829
|
+
visibility: input6.visibility
|
|
1651
1830
|
});
|
|
1652
1831
|
log.success(`\u0110\xE3 t\u1EA1o: ${urls.sshUrl}`);
|
|
1653
1832
|
return urls;
|
|
1654
1833
|
}
|
|
1655
1834
|
|
|
1656
1835
|
// src/lib/create-workspace-remote-via-gh.ts
|
|
1657
|
-
import { spawnSync as
|
|
1836
|
+
import { spawnSync as spawnSync18 } from "child_process";
|
|
1658
1837
|
|
|
1659
1838
|
// src/lib/check-gh-cli-auth-status.ts
|
|
1660
|
-
import { spawnSync as
|
|
1839
|
+
import { spawnSync as spawnSync11 } from "child_process";
|
|
1661
1840
|
function checkGhCliAuthStatus() {
|
|
1662
|
-
const r =
|
|
1841
|
+
const r = spawnSync11("gh", ["auth", "status"], { stdio: "ignore" });
|
|
1663
1842
|
if (r.error && r.error.code === "ENOENT") {
|
|
1664
1843
|
return "not-installed";
|
|
1665
1844
|
}
|
|
@@ -1667,12 +1846,12 @@ function checkGhCliAuthStatus() {
|
|
|
1667
1846
|
}
|
|
1668
1847
|
|
|
1669
1848
|
// src/lib/detect-package-manager.ts
|
|
1670
|
-
import { spawnSync as
|
|
1849
|
+
import { spawnSync as spawnSync12 } from "child_process";
|
|
1671
1850
|
function hasBinary(name) {
|
|
1672
1851
|
const platform2 = detectHostPlatform();
|
|
1673
1852
|
const probe = platform2 === "win32" ? "where" : "command";
|
|
1674
1853
|
const args = platform2 === "win32" ? [name] : ["-v", name];
|
|
1675
|
-
const r =
|
|
1854
|
+
const r = spawnSync12(probe, args, {
|
|
1676
1855
|
shell: platform2 !== "win32",
|
|
1677
1856
|
stdio: "ignore"
|
|
1678
1857
|
});
|
|
@@ -1688,11 +1867,11 @@ function detectPackageManager() {
|
|
|
1688
1867
|
}
|
|
1689
1868
|
|
|
1690
1869
|
// src/lib/handle-remote-access-failure-with-account-switch.ts
|
|
1691
|
-
import { spawnSync as
|
|
1692
|
-
import { input as
|
|
1870
|
+
import { spawnSync as spawnSync14 } from "child_process";
|
|
1871
|
+
import { input as input4, select as select6 } from "@inquirer/prompts";
|
|
1693
1872
|
|
|
1694
1873
|
// src/lib/verify-git-remote-accessible.ts
|
|
1695
|
-
import { spawnSync as
|
|
1874
|
+
import { spawnSync as spawnSync13 } from "child_process";
|
|
1696
1875
|
var TIMEOUT_MS = 5e3;
|
|
1697
1876
|
function classifyRemoteError(stderr) {
|
|
1698
1877
|
const text = stderr.toLowerCase();
|
|
@@ -1708,7 +1887,7 @@ function classifyRemoteError(stderr) {
|
|
|
1708
1887
|
return "unknown";
|
|
1709
1888
|
}
|
|
1710
1889
|
function tryVerifyGitRemoteAccessible(url) {
|
|
1711
|
-
const r =
|
|
1890
|
+
const r = spawnSync13("git", ["ls-remote", "--exit-code", url, "HEAD"], {
|
|
1712
1891
|
encoding: "utf8",
|
|
1713
1892
|
timeout: TIMEOUT_MS,
|
|
1714
1893
|
stdio: ["ignore", "pipe", "pipe"]
|
|
@@ -1730,7 +1909,7 @@ var RemoteAccessAbortedError = class extends Error {
|
|
|
1730
1909
|
}
|
|
1731
1910
|
};
|
|
1732
1911
|
function getCurrentGhUser2() {
|
|
1733
|
-
const r =
|
|
1912
|
+
const r = spawnSync14("gh", ["api", "user", "--jq", ".login"], {
|
|
1734
1913
|
encoding: "utf8",
|
|
1735
1914
|
stdio: ["ignore", "pipe", "pipe"]
|
|
1736
1915
|
});
|
|
@@ -1739,7 +1918,7 @@ function getCurrentGhUser2() {
|
|
|
1739
1918
|
}
|
|
1740
1919
|
function triggerGhAuthLoginInteractive3() {
|
|
1741
1920
|
log.info("M\u1EDF browser \u0111\u1EC3 switch GitHub account...");
|
|
1742
|
-
const r =
|
|
1921
|
+
const r = spawnSync14("gh", ["auth", "login", "--web"], { stdio: "inherit" });
|
|
1743
1922
|
if (r.status !== 0) {
|
|
1744
1923
|
log.warn(`gh auth login exit ${r.status}. B\u1EA1n c\xF3 th\u1EC3 ch\u1EA1y tay r\u1ED3i quay l\u1EA1i retry.`);
|
|
1745
1924
|
}
|
|
@@ -1800,7 +1979,7 @@ async function handleRemoteAccessFailureWithAccountSwitch(args) {
|
|
|
1800
1979
|
);
|
|
1801
1980
|
}
|
|
1802
1981
|
if (action === "re-input-url") {
|
|
1803
|
-
const newUrl = await
|
|
1982
|
+
const newUrl = await input4({
|
|
1804
1983
|
message: "URL git remote (https://github.com/owner/repo.git ho\u1EB7c git@github.com:owner/repo.git):",
|
|
1805
1984
|
default: currentUrl,
|
|
1806
1985
|
validate: (v) => isValidGitUrl(v) || "URL kh\xF4ng \u0111\xFAng format git remote"
|
|
@@ -1822,7 +2001,7 @@ async function handleRemoteAccessFailureWithAccountSwitch(args) {
|
|
|
1822
2001
|
}
|
|
1823
2002
|
|
|
1824
2003
|
// src/lib/install-gh-cli-via-package-manager.ts
|
|
1825
|
-
import { spawnSync as
|
|
2004
|
+
import { spawnSync as spawnSync15 } from "child_process";
|
|
1826
2005
|
var INSTALL_COMMANDS = {
|
|
1827
2006
|
brew: { cmd: "brew", args: ["install", "gh"] },
|
|
1828
2007
|
apt: { cmd: "sudo", args: ["apt-get", "install", "-y", "gh"] },
|
|
@@ -1833,7 +2012,7 @@ var INSTALL_COMMANDS = {
|
|
|
1833
2012
|
function installGhCliViaPackageManager(pm) {
|
|
1834
2013
|
const spec = INSTALL_COMMANDS[pm];
|
|
1835
2014
|
log.info(`\u0110ang c\xE0i gh CLI qua ${pm}...`);
|
|
1836
|
-
const r =
|
|
2015
|
+
const r = spawnSync15(spec.cmd, spec.args, { stdio: "inherit" });
|
|
1837
2016
|
if (r.status !== 0) {
|
|
1838
2017
|
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.`);
|
|
1839
2018
|
}
|
|
@@ -1841,9 +2020,9 @@ function installGhCliViaPackageManager(pm) {
|
|
|
1841
2020
|
}
|
|
1842
2021
|
|
|
1843
2022
|
// src/lib/setup-git-credential-via-gh.ts
|
|
1844
|
-
import { spawnSync as
|
|
2023
|
+
import { spawnSync as spawnSync16 } from "child_process";
|
|
1845
2024
|
function setupGitCredentialViaGh() {
|
|
1846
|
-
const r =
|
|
2025
|
+
const r = spawnSync16("gh", ["auth", "setup-git"], { stdio: "ignore" });
|
|
1847
2026
|
if (r.status !== 0) {
|
|
1848
2027
|
log.warn("gh auth setup-git fail (non-fatal). N\u1EBFu git clone l\u1ED7i 128 \u2192 ch\u1EA1y th\u1EE7 c\xF4ng.");
|
|
1849
2028
|
return;
|
|
@@ -1852,10 +2031,10 @@ function setupGitCredentialViaGh() {
|
|
|
1852
2031
|
}
|
|
1853
2032
|
|
|
1854
2033
|
// src/lib/trigger-gh-cli-auth-login.ts
|
|
1855
|
-
import { spawnSync as
|
|
2034
|
+
import { spawnSync as spawnSync17 } from "child_process";
|
|
1856
2035
|
function triggerGhCliAuthLogin() {
|
|
1857
2036
|
log.info("Kh\u1EDFi \u0111\u1ED9ng \u0111\u0103ng nh\u1EADp GitHub qua gh CLI (browser s\u1EBD m\u1EDF)...");
|
|
1858
|
-
const r =
|
|
2037
|
+
const r = spawnSync17(
|
|
1859
2038
|
"gh",
|
|
1860
2039
|
["auth", "login", "--hostname", "github.com", "--web", "--git-protocol", "ssh"],
|
|
1861
2040
|
{ stdio: "inherit" }
|
|
@@ -1973,20 +2152,20 @@ function classifyGhCreateError(stderr) {
|
|
|
1973
2152
|
return "unknown";
|
|
1974
2153
|
}
|
|
1975
2154
|
function repoExistsOnGitHub(fullName) {
|
|
1976
|
-
const r =
|
|
2155
|
+
const r = spawnSync18("gh", ["repo", "view", fullName, "--json", "name"], {
|
|
1977
2156
|
stdio: "ignore"
|
|
1978
2157
|
});
|
|
1979
2158
|
return r.status === 0;
|
|
1980
2159
|
}
|
|
1981
2160
|
function canCreateInNamespace(org, ghUser) {
|
|
1982
2161
|
if (org.toLowerCase() === ghUser.toLowerCase()) return { ok: true };
|
|
1983
|
-
const r =
|
|
2162
|
+
const r = spawnSync18("gh", ["api", `orgs/${org}/members/${ghUser}`, "--silent"], {
|
|
1984
2163
|
stdio: "ignore"
|
|
1985
2164
|
});
|
|
1986
2165
|
if (r.status === 0) return { ok: true };
|
|
1987
|
-
const orgCheck =
|
|
2166
|
+
const orgCheck = spawnSync18("gh", ["api", `orgs/${org}`, "--silent"], { stdio: "ignore" });
|
|
1988
2167
|
if (orgCheck.status !== 0) {
|
|
1989
|
-
const userCheck =
|
|
2168
|
+
const userCheck = spawnSync18("gh", ["api", `users/${org}`, "--silent"], { stdio: "ignore" });
|
|
1990
2169
|
if (userCheck.status === 0) {
|
|
1991
2170
|
return {
|
|
1992
2171
|
ok: false,
|
|
@@ -2003,17 +2182,17 @@ function canCreateInNamespace(org, ghUser) {
|
|
|
2003
2182
|
reason: `'${ghUser}' kh\xF4ng ph\u1EA3i member c\u1EE7a org '${org}'. Li\xEAn h\u1EC7 admin org \u0111\u1EC3 \u0111\u01B0\u1EE3c invite, ho\u1EB7c pass --repo-org=${ghUser} t\u1EA1o d\u01B0\u1EDBi personal account.`
|
|
2004
2183
|
};
|
|
2005
2184
|
}
|
|
2006
|
-
async function createWorkspaceRemoteViaGh(
|
|
2007
|
-
validateRepoName(
|
|
2008
|
-
validateRepoVisibility(
|
|
2185
|
+
async function createWorkspaceRemoteViaGh(input6) {
|
|
2186
|
+
validateRepoName(input6.workspaceName);
|
|
2187
|
+
validateRepoVisibility(input6.visibility);
|
|
2009
2188
|
await ensureGitHubReady();
|
|
2010
2189
|
const ghUser = resolveGithubUsernameDefault();
|
|
2011
|
-
const org =
|
|
2190
|
+
const org = input6.org ?? ghUser;
|
|
2012
2191
|
const namespaceCheck = canCreateInNamespace(org, ghUser);
|
|
2013
2192
|
if (!namespaceCheck.ok) {
|
|
2014
2193
|
throw new Error(`Kh\xF4ng th\u1EC3 t\u1EA1o repo d\u01B0\u1EDBi '${org}/': ${namespaceCheck.reason}`);
|
|
2015
2194
|
}
|
|
2016
|
-
const fullName = `${org}/${
|
|
2195
|
+
const fullName = `${org}/${input6.workspaceName}`;
|
|
2017
2196
|
if (repoExistsOnGitHub(fullName)) {
|
|
2018
2197
|
throw new CreateWorkspaceRemoteError(
|
|
2019
2198
|
"repo-exists",
|
|
@@ -2021,16 +2200,16 @@ async function createWorkspaceRemoteViaGh(input5) {
|
|
|
2021
2200
|
`Repo '${fullName}' \u0111\xE3 t\u1ED3n t\u1EA1i tr\xEAn GitHub. C\xF3 th\u1EC3 b\u1EA1n \u0111\xE3 init workspace n\xE0y tr\u01B0\u1EDBc \u0111\xF3.`
|
|
2022
2201
|
);
|
|
2023
2202
|
}
|
|
2024
|
-
log.info(`T\u1EA1o GitHub repo cho workspace: ${fullName} (${
|
|
2025
|
-
const r =
|
|
2203
|
+
log.info(`T\u1EA1o GitHub repo cho workspace: ${fullName} (${input6.visibility})...`);
|
|
2204
|
+
const r = spawnSync18(
|
|
2026
2205
|
"gh",
|
|
2027
2206
|
[
|
|
2028
2207
|
"repo",
|
|
2029
2208
|
"create",
|
|
2030
2209
|
fullName,
|
|
2031
|
-
`--${
|
|
2210
|
+
`--${input6.visibility}`,
|
|
2032
2211
|
"--source",
|
|
2033
|
-
|
|
2212
|
+
input6.workspacePath,
|
|
2034
2213
|
"--remote",
|
|
2035
2214
|
"origin",
|
|
2036
2215
|
"--push"
|
|
@@ -2063,7 +2242,7 @@ ${combined}
|
|
|
2063
2242
|
function linkExistingRemoteToWorkspace(args) {
|
|
2064
2243
|
const sshUrl = `git@github.com:${args.fullName}.git`;
|
|
2065
2244
|
const httpsUrl = `https://github.com/${args.fullName}.git`;
|
|
2066
|
-
const addResult =
|
|
2245
|
+
const addResult = spawnSync18(
|
|
2067
2246
|
"git",
|
|
2068
2247
|
["-C", args.workspacePath, "remote", "add", "origin", sshUrl],
|
|
2069
2248
|
{
|
|
@@ -2072,7 +2251,7 @@ function linkExistingRemoteToWorkspace(args) {
|
|
|
2072
2251
|
}
|
|
2073
2252
|
);
|
|
2074
2253
|
if (addResult.status !== 0) {
|
|
2075
|
-
|
|
2254
|
+
spawnSync18("git", ["-C", args.workspacePath, "remote", "set-url", "origin", sshUrl], {
|
|
2076
2255
|
stdio: "ignore"
|
|
2077
2256
|
});
|
|
2078
2257
|
}
|
|
@@ -2086,11 +2265,11 @@ import { select as select7 } from "@inquirer/prompts";
|
|
|
2086
2265
|
import { simpleGit as simpleGit3 } from "simple-git";
|
|
2087
2266
|
|
|
2088
2267
|
// src/lib/check-folder-has-git.ts
|
|
2089
|
-
import { existsSync as
|
|
2090
|
-
import { join as
|
|
2268
|
+
import { existsSync as existsSync4, statSync } from "fs";
|
|
2269
|
+
import { join as join13 } from "path";
|
|
2091
2270
|
function checkFolderHasGit(folderPath) {
|
|
2092
|
-
const gitPath =
|
|
2093
|
-
if (!
|
|
2271
|
+
const gitPath = join13(folderPath, ".git");
|
|
2272
|
+
if (!existsSync4(gitPath)) return false;
|
|
2094
2273
|
const stat = statSync(gitPath);
|
|
2095
2274
|
return stat.isDirectory() || stat.isFile();
|
|
2096
2275
|
}
|
|
@@ -2120,8 +2299,8 @@ async function createInitialGitCommit(folderPath) {
|
|
|
2120
2299
|
}
|
|
2121
2300
|
|
|
2122
2301
|
// src/lib/detect-folder-tech-stack.ts
|
|
2123
|
-
import { existsSync as
|
|
2124
|
-
import { join as
|
|
2302
|
+
import { existsSync as existsSync5 } from "fs";
|
|
2303
|
+
import { join as join14 } from "path";
|
|
2125
2304
|
var SIGNATURES = {
|
|
2126
2305
|
node: ["package.json"],
|
|
2127
2306
|
python: ["pyproject.toml", "requirements.txt", "setup.py", "Pipfile"],
|
|
@@ -2133,7 +2312,7 @@ var SIGNATURES = {
|
|
|
2133
2312
|
function detectFolderTechStack(folderPath) {
|
|
2134
2313
|
const matched = [];
|
|
2135
2314
|
for (const [stack, files] of Object.entries(SIGNATURES)) {
|
|
2136
|
-
if (files.some((f) =>
|
|
2315
|
+
if (files.some((f) => existsSync5(join14(folderPath, f)))) {
|
|
2137
2316
|
matched.push(stack);
|
|
2138
2317
|
}
|
|
2139
2318
|
}
|
|
@@ -2141,26 +2320,26 @@ function detectFolderTechStack(folderPath) {
|
|
|
2141
2320
|
}
|
|
2142
2321
|
|
|
2143
2322
|
// src/lib/gitignore-template-loader.ts
|
|
2144
|
-
import { readFileSync as
|
|
2145
|
-
import { dirname as
|
|
2323
|
+
import { readFileSync as readFileSync3 } from "fs";
|
|
2324
|
+
import { dirname as dirname4, join as join15 } from "path";
|
|
2146
2325
|
import { fileURLToPath as fileURLToPath2 } from "url";
|
|
2147
|
-
var __dirname =
|
|
2326
|
+
var __dirname = dirname4(fileURLToPath2(import.meta.url));
|
|
2148
2327
|
var CANDIDATE_DIRS = [
|
|
2149
2328
|
// Bundled production: dist/index.js → __dirname = .../dist/, sibling dist/templates
|
|
2150
|
-
|
|
2329
|
+
join15(__dirname, "templates", "gitignore"),
|
|
2151
2330
|
// Legacy bundled: nếu file là dist/lib/*.js (sub-bundle), templates ở dist/templates
|
|
2152
|
-
|
|
2331
|
+
join15(__dirname, "..", "templates", "gitignore"),
|
|
2153
2332
|
// Dev mode (vitest/tsx run src/ trực tiếp): __dirname = src/lib/
|
|
2154
|
-
|
|
2333
|
+
join15(__dirname, "..", "..", "src", "templates", "gitignore"),
|
|
2155
2334
|
// npm-installed alt: __dirname = .../dist/ → package_root/src/templates
|
|
2156
|
-
|
|
2335
|
+
join15(__dirname, "..", "src", "templates", "gitignore")
|
|
2157
2336
|
];
|
|
2158
2337
|
var AVATAR_MARKER_START = "# === avatar ===";
|
|
2159
2338
|
var AVATAR_MARKER_END = "# === /avatar ===";
|
|
2160
2339
|
function readTemplate(stack) {
|
|
2161
2340
|
for (const dir of CANDIDATE_DIRS) {
|
|
2162
2341
|
try {
|
|
2163
|
-
return
|
|
2342
|
+
return readFileSync3(join15(dir, `${stack}.txt`), "utf8");
|
|
2164
2343
|
} catch {
|
|
2165
2344
|
}
|
|
2166
2345
|
}
|
|
@@ -2174,15 +2353,15 @@ ${readTemplate(s).trim()}`);
|
|
|
2174
2353
|
}
|
|
2175
2354
|
|
|
2176
2355
|
// src/lib/write-or-merge-gitignore.ts
|
|
2177
|
-
import { existsSync as
|
|
2178
|
-
import { join as
|
|
2356
|
+
import { existsSync as existsSync6, readFileSync as readFileSync4, writeFileSync } from "fs";
|
|
2357
|
+
import { join as join16 } from "path";
|
|
2179
2358
|
function writeOrMergeGitignore(folderPath, avatarBlock) {
|
|
2180
|
-
const path =
|
|
2181
|
-
if (!
|
|
2359
|
+
const path = join16(folderPath, ".gitignore");
|
|
2360
|
+
if (!existsSync6(path)) {
|
|
2182
2361
|
writeFileSync(path, avatarBlock, "utf8");
|
|
2183
2362
|
return;
|
|
2184
2363
|
}
|
|
2185
|
-
const existing =
|
|
2364
|
+
const existing = readFileSync4(path, "utf8");
|
|
2186
2365
|
const startIdx = existing.indexOf(AVATAR_MARKER_START);
|
|
2187
2366
|
const endIdx = existing.indexOf(AVATAR_MARKER_END);
|
|
2188
2367
|
if (startIdx !== -1 && endIdx !== -1 && endIdx > startIdx) {
|
|
@@ -2354,7 +2533,7 @@ async function safeBootstrapGitInFolder(folderPath, opts = {}) {
|
|
|
2354
2533
|
|
|
2355
2534
|
// src/commands/init-conflict-detection-helpers.ts
|
|
2356
2535
|
import { readdir } from "fs/promises";
|
|
2357
|
-
import { join as
|
|
2536
|
+
import { join as join17 } from "path";
|
|
2358
2537
|
async function isEmptyOrMissing(path) {
|
|
2359
2538
|
if (!await pathExists(path)) return true;
|
|
2360
2539
|
try {
|
|
@@ -2367,7 +2546,7 @@ async function isEmptyOrMissing(path) {
|
|
|
2367
2546
|
}
|
|
2368
2547
|
async function findAlternativeWorkspaceName(parent, desiredName, maxAttempts = 10) {
|
|
2369
2548
|
for (let i = 2; i < maxAttempts; i++) {
|
|
2370
|
-
const candidate =
|
|
2549
|
+
const candidate = join17(parent, `${desiredName}-${i}`);
|
|
2371
2550
|
if (await isEmptyOrMissing(candidate)) return candidate;
|
|
2372
2551
|
}
|
|
2373
2552
|
return null;
|
|
@@ -2678,7 +2857,7 @@ async function promptProjectStatus() {
|
|
|
2678
2857
|
});
|
|
2679
2858
|
}
|
|
2680
2859
|
async function runInitFromExistingRemote(opts, ownerEmail) {
|
|
2681
|
-
const initialRemoteUrl = opts.clientRepo ?? await
|
|
2860
|
+
const initialRemoteUrl = opts.clientRepo ?? await input5({
|
|
2682
2861
|
message: "URL git c\u1EE7a repo:",
|
|
2683
2862
|
validate: (v) => v.length > 0 ? true : "URL b\u1EAFt bu\u1ED9c"
|
|
2684
2863
|
});
|
|
@@ -2686,7 +2865,7 @@ async function runInitFromExistingRemote(opts, ownerEmail) {
|
|
|
2686
2865
|
const remoteUrl = resolvedRemoteUrl ?? initialRemoteUrl;
|
|
2687
2866
|
const teamOwner = opts.teamOwner ?? await promptTeamOwner(ownerEmail);
|
|
2688
2867
|
const inferredName = inferWorkspaceName(remoteUrl);
|
|
2689
|
-
const workspaceName = opts.workspaceName ?? await
|
|
2868
|
+
const workspaceName = opts.workspaceName ?? await input5({ message: "T\xEAn workspace:", default: inferredName });
|
|
2690
2869
|
const workspaceParent = resolve(opts.workspaceParent ?? ".");
|
|
2691
2870
|
const workspacePath = await resolveWorkspacePath(workspaceParent, workspaceName, opts.force);
|
|
2692
2871
|
await scaffoldWorkspaceWithSrcSubmodule({
|
|
@@ -2709,7 +2888,7 @@ async function runInitFromExistingRemote(opts, ownerEmail) {
|
|
|
2709
2888
|
}
|
|
2710
2889
|
async function runInitFromExistingFolder(opts, ownerEmail) {
|
|
2711
2890
|
const folderPath = resolve(
|
|
2712
|
-
opts.folderPath ?? await
|
|
2891
|
+
opts.folderPath ?? await input5({
|
|
2713
2892
|
message: "\u0110\u01B0\u1EDDng d\u1EABn folder hi\u1EC7n c\xF3:",
|
|
2714
2893
|
validate: (v) => v.length > 0 ? true : "Path b\u1EAFt bu\u1ED9c"
|
|
2715
2894
|
})
|
|
@@ -2721,7 +2900,7 @@ async function runInitFromExistingFolder(opts, ownerEmail) {
|
|
|
2721
2900
|
const remoteUrl = await getOrCreateOriginRemote(folderPath, opts);
|
|
2722
2901
|
const teamOwner = opts.teamOwner ?? await promptTeamOwner(ownerEmail);
|
|
2723
2902
|
const inferredName = opts.workspaceName ?? `${basename(folderPath)}-avatar-workspace`;
|
|
2724
|
-
const workspaceName = opts.workspaceName ?? await
|
|
2903
|
+
const workspaceName = opts.workspaceName ?? await input5({ message: "T\xEAn workspace:", default: inferredName });
|
|
2725
2904
|
const workspaceParent = resolve(opts.workspaceParent ?? ".");
|
|
2726
2905
|
const workspacePath = await resolveWorkspacePath(workspaceParent, workspaceName, opts.force);
|
|
2727
2906
|
await scaffoldWorkspaceWithSrcSubmodule({
|
|
@@ -2745,7 +2924,7 @@ async function runInitFromExistingFolder(opts, ownerEmail) {
|
|
|
2745
2924
|
}
|
|
2746
2925
|
async function runInitFromScratch(opts, ownerEmail) {
|
|
2747
2926
|
await ensureGitHubReady();
|
|
2748
|
-
const projectName = opts.workspaceName ?? await
|
|
2927
|
+
const projectName = opts.workspaceName ?? await input5({
|
|
2749
2928
|
message: "T\xEAn d\u1EF1 \xE1n:",
|
|
2750
2929
|
validate: (v) => v.length > 0 ? true : "T\xEAn b\u1EAFt bu\u1ED9c"
|
|
2751
2930
|
});
|
|
@@ -2759,7 +2938,7 @@ async function runInitFromScratch(opts, ownerEmail) {
|
|
|
2759
2938
|
const teamOwner = opts.teamOwner ?? await promptTeamOwner(ownerEmail);
|
|
2760
2939
|
const workspaceParent = resolve(opts.workspaceParent ?? ".");
|
|
2761
2940
|
const workspacePath = await resolveWorkspacePath(workspaceParent, projectName, opts.force);
|
|
2762
|
-
const srcPath =
|
|
2941
|
+
const srcPath = join18(workspacePath, "src");
|
|
2763
2942
|
await ensureDir(workspacePath);
|
|
2764
2943
|
await ensureDir(srcPath);
|
|
2765
2944
|
await safeBootstrapGitInFolder(srcPath, { autoYes: true });
|
|
@@ -2830,7 +3009,7 @@ async function getOrCreateOriginRemote(folderPath, opts) {
|
|
|
2830
3009
|
{ name: "public", value: "public" }
|
|
2831
3010
|
]
|
|
2832
3011
|
});
|
|
2833
|
-
const repoName = await
|
|
3012
|
+
const repoName = await input5({
|
|
2834
3013
|
message: "T\xEAn repo:",
|
|
2835
3014
|
default: basename(folderPath)
|
|
2836
3015
|
});
|
|
@@ -2895,10 +3074,10 @@ async function finalizeWorkspaceScaffold(args) {
|
|
|
2895
3074
|
await writeRootClaudeMd(args.workspacePath, vars);
|
|
2896
3075
|
await writeProjectSettings(args.workspacePath, vars);
|
|
2897
3076
|
await appendGitignoreEntries(args.workspacePath);
|
|
2898
|
-
await ensureDir(
|
|
2899
|
-
await ensureDir(
|
|
2900
|
-
await installGitHook(
|
|
2901
|
-
await installGitHook(
|
|
3077
|
+
await ensureDir(join18(args.workspacePath, "notes"));
|
|
3078
|
+
await ensureDir(join18(args.workspacePath, "scripts"));
|
|
3079
|
+
await installGitHook(join18(args.workspacePath, ".git"), "post-merge");
|
|
3080
|
+
await installGitHook(join18(args.workspacePath, ".git", "modules", "src"), "pre-push");
|
|
2902
3081
|
log.success("C\xE0i post-merge (workspace) + pre-push (src/)");
|
|
2903
3082
|
await appendAuditEntry("init", `flow=${args.flow},workspace=${args.workspaceName}`);
|
|
2904
3083
|
await maybeCommitWorkspace(args.workspacePath, args.skipCommit);
|
|
@@ -2973,7 +3152,7 @@ async function maybeCreateWorkspaceRemote(args) {
|
|
|
2973
3152
|
});
|
|
2974
3153
|
return;
|
|
2975
3154
|
}
|
|
2976
|
-
const newName = await
|
|
3155
|
+
const newName = await input5({
|
|
2977
3156
|
message: "T\xEAn workspace m\u1EDBi (s\u1EBD t\u1EA1o repo new):",
|
|
2978
3157
|
validate: (v) => v.trim().length > 0 ? true : "T\xEAn kh\xF4ng \u0111\u01B0\u1EE3c r\u1ED7ng"
|
|
2979
3158
|
});
|
|
@@ -2998,7 +3177,7 @@ async function maybeCreateWorkspaceRemote(args) {
|
|
|
2998
3177
|
}
|
|
2999
3178
|
}
|
|
3000
3179
|
async function resolveWorkspacePath(parent, desiredName, force) {
|
|
3001
|
-
const desired =
|
|
3180
|
+
const desired = join18(parent, desiredName);
|
|
3002
3181
|
if (await isEmptyOrMissing(desired)) return desired;
|
|
3003
3182
|
log.warn(`Workspace path "${desired}" \u0111\xE3 c\xF3 n\u1ED9i dung.`);
|
|
3004
3183
|
while (true) {
|
|
@@ -3025,17 +3204,17 @@ async function resolveWorkspacePath(parent, desiredName, force) {
|
|
|
3025
3204
|
if (action === "use-alt" && alternative) {
|
|
3026
3205
|
return alternative;
|
|
3027
3206
|
}
|
|
3028
|
-
const newName = await
|
|
3207
|
+
const newName = await input5({
|
|
3029
3208
|
message: "T\xEAn workspace m\u1EDBi:",
|
|
3030
3209
|
validate: (v) => v.trim().length > 0 ? true : "T\xEAn kh\xF4ng \u0111\u01B0\u1EE3c r\u1ED7ng"
|
|
3031
3210
|
});
|
|
3032
|
-
const newPath =
|
|
3211
|
+
const newPath = join18(parent, newName.trim());
|
|
3033
3212
|
if (await isEmptyOrMissing(newPath)) return newPath;
|
|
3034
3213
|
log.warn(`"${newPath}" c\u0169ng \u0111\xE3 c\xF3 n\u1ED9i dung. Th\u1EED t\xEAn kh\xE1c.`);
|
|
3035
3214
|
}
|
|
3036
3215
|
}
|
|
3037
3216
|
async function promptTeamOwner(currentUserEmail) {
|
|
3038
|
-
return await
|
|
3217
|
+
return await input5({ message: "Team owner email:", default: currentUserEmail });
|
|
3039
3218
|
}
|
|
3040
3219
|
async function maybeCommitWorkspace(workspacePath, skipCommit) {
|
|
3041
3220
|
if (skipCommit) {
|
|
@@ -3075,6 +3254,22 @@ function printInitSuccessBox(rootPath, flow, aiResult = null) {
|
|
|
3075
3254
|
`);
|
|
3076
3255
|
}
|
|
3077
3256
|
|
|
3257
|
+
// src/lib/not-implemented-stub.ts
|
|
3258
|
+
function notImplementedYet(commandName, milestone) {
|
|
3259
|
+
return () => {
|
|
3260
|
+
process.stdout.write(
|
|
3261
|
+
`${chalk.yellow("\u23F3")} ${chalk.bold(`avatar ${commandName}`)} \u2014 ch\u01B0a implement \u1EDF milestone hi\u1EC7n t\u1EA1i.
|
|
3262
|
+
`
|
|
3263
|
+
);
|
|
3264
|
+
if (milestone) {
|
|
3265
|
+
process.stdout.write(` D\u1EF1 ki\u1EBFn: ${chalk.cyan(milestone)}
|
|
3266
|
+
`);
|
|
3267
|
+
}
|
|
3268
|
+
process.stdout.write(" Spec \u0111\xE3 c\xF3 trong avatar-cli-implementation_4.html.\n");
|
|
3269
|
+
process.exit(0);
|
|
3270
|
+
};
|
|
3271
|
+
}
|
|
3272
|
+
|
|
3078
3273
|
// src/commands/mcp-run.ts
|
|
3079
3274
|
function registerMcpRunCommand(program2) {
|
|
3080
3275
|
program2.command("mcp-run <tool-id>", { hidden: true }).description("[internal] Spawn MCP v\u1EDBi secrets injected (M09)").action(notImplementedYet("mcp-run", "Milestone 09"));
|
|
@@ -3107,15 +3302,15 @@ function registerSecretsCommand(program2) {
|
|
|
3107
3302
|
|
|
3108
3303
|
// src/commands/status.ts
|
|
3109
3304
|
import { promises as fs8 } from "fs";
|
|
3110
|
-
import { join as
|
|
3305
|
+
import { join as join20 } from "path";
|
|
3111
3306
|
import boxen5 from "boxen";
|
|
3112
3307
|
|
|
3113
3308
|
// src/lib/pack-backup-manager.ts
|
|
3114
3309
|
import { promises as fs7 } from "fs";
|
|
3115
|
-
import { join as
|
|
3310
|
+
import { join as join19 } from "path";
|
|
3116
3311
|
var BACKUP_DIR_NAME = "_backup";
|
|
3117
3312
|
async function listBackups(projectRoot) {
|
|
3118
|
-
const dir =
|
|
3313
|
+
const dir = join19(projectRoot, ".claude", BACKUP_DIR_NAME);
|
|
3119
3314
|
if (!await pathExists(dir)) return [];
|
|
3120
3315
|
const entries = await fs7.readdir(dir, { withFileTypes: true });
|
|
3121
3316
|
return entries.filter((e) => e.isDirectory()).map((e) => e.name).sort().reverse();
|
|
@@ -3141,7 +3336,7 @@ function registerStatusCommand(program2) {
|
|
|
3141
3336
|
}
|
|
3142
3337
|
async function gatherStatus(cwd) {
|
|
3143
3338
|
const projectName = cwd.split("/").filter(Boolean).pop() ?? "unknown";
|
|
3144
|
-
const claudeRoot =
|
|
3339
|
+
const claudeRoot = join20(cwd, ".claude");
|
|
3145
3340
|
const hasAvatar = await pathExists(claudeRoot);
|
|
3146
3341
|
if (!hasAvatar) {
|
|
3147
3342
|
return {
|
|
@@ -3154,8 +3349,8 @@ async function gatherStatus(cwd) {
|
|
|
3154
3349
|
hasAvatar: false
|
|
3155
3350
|
};
|
|
3156
3351
|
}
|
|
3157
|
-
const packVersion = await isGitRepo(
|
|
3158
|
-
const pendingDir =
|
|
3352
|
+
const packVersion = await isGitRepo(join20(claudeRoot, "pack")) ? await readPinnedPackVersion(cwd).catch(() => null) : null;
|
|
3353
|
+
const pendingDir = join20(claudeRoot, "_pending");
|
|
3159
3354
|
const pendingCount = await pathExists(pendingDir) ? (await fs8.readdir(pendingDir)).filter((n) => n.endsWith(".diff.md")).length : 0;
|
|
3160
3355
|
const backupCount = (await listBackups(cwd)).length;
|
|
3161
3356
|
const techStackSummary = await readTechStackFirstLine(claudeRoot);
|
|
@@ -3170,7 +3365,7 @@ async function gatherStatus(cwd) {
|
|
|
3170
3365
|
};
|
|
3171
3366
|
}
|
|
3172
3367
|
async function readTechStackFirstLine(claudeRoot) {
|
|
3173
|
-
const techStackPath =
|
|
3368
|
+
const techStackPath = join20(claudeRoot, "project", "tech-stack.md");
|
|
3174
3369
|
if (!await pathExists(techStackPath)) return "(no tech-stack.md)";
|
|
3175
3370
|
const content = await readText(techStackPath);
|
|
3176
3371
|
const firstNonHeaderLine = content.split("\n").find((l) => l.trim() && !l.startsWith("#") && !l.startsWith(">"));
|
|
@@ -3211,27 +3406,27 @@ import boxen6 from "boxen";
|
|
|
3211
3406
|
// src/lib/create-uninstall-backup-snapshot.ts
|
|
3212
3407
|
import { cp, mkdir, writeFile } from "fs/promises";
|
|
3213
3408
|
import { homedir as homedir3 } from "os";
|
|
3214
|
-
import { basename as basename2, join as
|
|
3215
|
-
var UNINSTALL_BACKUPS_DIR =
|
|
3409
|
+
import { basename as basename2, join as join21 } from "path";
|
|
3410
|
+
var UNINSTALL_BACKUPS_DIR = join21(homedir3(), ".avatar", "uninstall-backups");
|
|
3216
3411
|
async function createUninstallBackupSnapshot(projectRoot, artifacts, avatarVersion) {
|
|
3217
3412
|
const projectName = basename2(projectRoot);
|
|
3218
3413
|
const timestamp = (/* @__PURE__ */ new Date()).toISOString().replace(/[:.]/g, "-");
|
|
3219
|
-
const backupDir =
|
|
3414
|
+
const backupDir = join21(UNINSTALL_BACKUPS_DIR, `${projectName}-${timestamp}`);
|
|
3220
3415
|
await mkdir(backupDir, { recursive: true, mode: 448 });
|
|
3221
3416
|
if (artifacts.claudeDir) {
|
|
3222
|
-
await cp(artifacts.claudeDir,
|
|
3417
|
+
await cp(artifacts.claudeDir, join21(backupDir, ".claude"), { recursive: true });
|
|
3223
3418
|
}
|
|
3224
3419
|
if (artifacts.claudeMd) {
|
|
3225
|
-
await cp(artifacts.claudeMd,
|
|
3420
|
+
await cp(artifacts.claudeMd, join21(backupDir, "CLAUDE.md"));
|
|
3226
3421
|
}
|
|
3227
3422
|
if (artifacts.postMergeHook || artifacts.prePushHook) {
|
|
3228
|
-
const hooksBackupDir =
|
|
3423
|
+
const hooksBackupDir = join21(backupDir, "hooks");
|
|
3229
3424
|
await mkdir(hooksBackupDir, { recursive: true });
|
|
3230
3425
|
if (artifacts.postMergeHook) {
|
|
3231
|
-
await cp(artifacts.postMergeHook,
|
|
3426
|
+
await cp(artifacts.postMergeHook, join21(hooksBackupDir, "post-merge"));
|
|
3232
3427
|
}
|
|
3233
3428
|
if (artifacts.prePushHook) {
|
|
3234
|
-
await cp(artifacts.prePushHook,
|
|
3429
|
+
await cp(artifacts.prePushHook, join21(hooksBackupDir, "pre-push"));
|
|
3235
3430
|
}
|
|
3236
3431
|
}
|
|
3237
3432
|
const manifest = {
|
|
@@ -3246,27 +3441,27 @@ async function createUninstallBackupSnapshot(projectRoot, artifacts, avatarVersi
|
|
|
3246
3441
|
prePushHook: !!artifacts.prePushHook
|
|
3247
3442
|
}
|
|
3248
3443
|
};
|
|
3249
|
-
await writeFile(
|
|
3444
|
+
await writeFile(join21(backupDir, "manifest.json"), JSON.stringify(manifest, null, 2), "utf8");
|
|
3250
3445
|
return backupDir;
|
|
3251
3446
|
}
|
|
3252
3447
|
|
|
3253
3448
|
// src/lib/detect-avatar-project-artifacts.ts
|
|
3254
|
-
import { existsSync as
|
|
3255
|
-
import { join as
|
|
3449
|
+
import { existsSync as existsSync7 } from "fs";
|
|
3450
|
+
import { join as join22 } from "path";
|
|
3256
3451
|
function existsOrNull(path) {
|
|
3257
|
-
return
|
|
3452
|
+
return existsSync7(path) ? path : null;
|
|
3258
3453
|
}
|
|
3259
3454
|
function detectAvatarProjectArtifacts(projectRoot) {
|
|
3260
|
-
const claudeDir = existsOrNull(
|
|
3261
|
-
const claudeMd = existsOrNull(
|
|
3262
|
-
const postMergeHook = existsOrNull(
|
|
3455
|
+
const claudeDir = existsOrNull(join22(projectRoot, ".claude"));
|
|
3456
|
+
const claudeMd = existsOrNull(join22(projectRoot, "CLAUDE.md"));
|
|
3457
|
+
const postMergeHook = existsOrNull(join22(projectRoot, ".git", "hooks", "post-merge"));
|
|
3263
3458
|
const prePushHook = existsOrNull(
|
|
3264
|
-
|
|
3459
|
+
join22(projectRoot, ".git", "modules", "src", "hooks", "pre-push")
|
|
3265
3460
|
);
|
|
3266
|
-
const gitignorePath = existsOrNull(
|
|
3267
|
-
const gitmodulesPath = existsOrNull(
|
|
3268
|
-
const notesDir = existsOrNull(
|
|
3269
|
-
const scriptsDir = existsOrNull(
|
|
3461
|
+
const gitignorePath = existsOrNull(join22(projectRoot, ".gitignore"));
|
|
3462
|
+
const gitmodulesPath = existsOrNull(join22(projectRoot, ".gitmodules"));
|
|
3463
|
+
const notesDir = existsOrNull(join22(projectRoot, "notes"));
|
|
3464
|
+
const scriptsDir = existsOrNull(join22(projectRoot, "scripts"));
|
|
3270
3465
|
const hasAnyArtifact = !!(claudeDir || claudeMd || postMergeHook || prePushHook);
|
|
3271
3466
|
return {
|
|
3272
3467
|
hasAnyArtifact,
|
|
@@ -3287,11 +3482,11 @@ async function executeUninstallDeletion(artifacts, flags) {
|
|
|
3287
3482
|
if (artifacts.claudeDir) {
|
|
3288
3483
|
if (flags.keepSubmodule) {
|
|
3289
3484
|
const { readdir: readdir2 } = await import("fs/promises");
|
|
3290
|
-
const { join:
|
|
3485
|
+
const { join: join23 } = await import("path");
|
|
3291
3486
|
const entries = await readdir2(artifacts.claudeDir);
|
|
3292
3487
|
for (const entry of entries) {
|
|
3293
3488
|
if (entry === "pack") continue;
|
|
3294
|
-
await rm(
|
|
3489
|
+
await rm(join23(artifacts.claudeDir, entry), { recursive: true, force: true });
|
|
3295
3490
|
}
|
|
3296
3491
|
} else {
|
|
3297
3492
|
await rm(artifacts.claudeDir, { recursive: true, force: true });
|
|
@@ -3360,7 +3555,7 @@ async function removeSubmoduleEntry(gitmodulesPath, submodulePath) {
|
|
|
3360
3555
|
}
|
|
3361
3556
|
|
|
3362
3557
|
// src/commands/uninstall.ts
|
|
3363
|
-
var CLI_VERSION = "1.
|
|
3558
|
+
var CLI_VERSION = "1.3.1";
|
|
3364
3559
|
function registerUninstallCommand(program2) {
|
|
3365
3560
|
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) => {
|
|
3366
3561
|
try {
|
|
@@ -3442,7 +3637,7 @@ function printUninstallSuccessBox(backupPath) {
|
|
|
3442
3637
|
}
|
|
3443
3638
|
|
|
3444
3639
|
// src/index.ts
|
|
3445
|
-
var CLI_VERSION2 = "1.
|
|
3640
|
+
var CLI_VERSION2 = "1.3.1";
|
|
3446
3641
|
var program = new Command();
|
|
3447
3642
|
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(
|
|
3448
3643
|
"beforeAll",
|