@nalvietnam/avatar-cli 1.4.2 → 1.6.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/dist/index.js +629 -66
- package/dist/index.js.map +1 -1
- package/dist/lib/print-welcome-screen.js +1 -1
- package/dist/lib/print-welcome-screen.js.map +1 -1
- package/dist/templates/CLAUDE.md.tpl +49 -3
- package/dist/templates/settings.json.tpl +40 -26
- package/package.json +1 -1
- package/src/templates/CLAUDE.md.tpl +49 -3
- package/src/templates/settings.json.tpl +40 -26
package/dist/index.js
CHANGED
|
@@ -103,11 +103,22 @@ var userStateSchema = z.object({
|
|
|
103
103
|
tool_inputs: z.record(z.string(), z.unknown()).default({})
|
|
104
104
|
});
|
|
105
105
|
var projectSettingsSchema = z.object({
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
106
|
+
$schema: z.string().optional(),
|
|
107
|
+
includeCoAuthoredBy: z.boolean().optional(),
|
|
108
|
+
env: z.record(z.string(), z.string()).default({}),
|
|
109
|
+
permissions: z.object({
|
|
110
|
+
allow: z.array(z.string()).default([]),
|
|
111
|
+
deny: z.array(z.string()).default([])
|
|
109
112
|
}).partial().optional(),
|
|
110
|
-
|
|
113
|
+
// Hooks shape per Claude Code spec: each event key maps to array of matcher entries.
|
|
114
|
+
// We accept unknown body since pack/template control concrete schema; Zod just guards
|
|
115
|
+
// top-level structure to avoid corrupting user file on merge.
|
|
116
|
+
hooks: z.record(z.string(), z.array(z.unknown())).optional(),
|
|
117
|
+
statusLine: z.object({
|
|
118
|
+
type: z.string(),
|
|
119
|
+
command: z.string(),
|
|
120
|
+
padding: z.number().optional()
|
|
121
|
+
}).optional()
|
|
111
122
|
});
|
|
112
123
|
var initModeSchema = z.enum(["internal", "client", "library"]);
|
|
113
124
|
|
|
@@ -134,8 +145,8 @@ async function writeUserConfig(config) {
|
|
|
134
145
|
}
|
|
135
146
|
async function clearUserConfig() {
|
|
136
147
|
if (await pathExists(USER_CONFIG_PATH)) {
|
|
137
|
-
const { promises:
|
|
138
|
-
await
|
|
148
|
+
const { promises: fs14 } = await import("fs");
|
|
149
|
+
await fs14.unlink(USER_CONFIG_PATH);
|
|
139
150
|
}
|
|
140
151
|
}
|
|
141
152
|
function isTokenExpired(config) {
|
|
@@ -462,27 +473,126 @@ async function promptAiProviderChoice(globalInfo = detectGlobalClaudeSettings())
|
|
|
462
473
|
message: "Ch\u1ECDn provider cho AI features:",
|
|
463
474
|
choices: [
|
|
464
475
|
{
|
|
465
|
-
name: "1. Claude Code Subscription (d\xF9ng quota c\xE1 nh\xE2n Anthropic)",
|
|
476
|
+
name: "1. Claude Code Subscription (d\xF9ng quota c\xE1 nh\xE2n Anthropic, OAuth login)",
|
|
466
477
|
value: "subscription"
|
|
467
478
|
},
|
|
468
479
|
{
|
|
469
|
-
name: "2. LLMLite API key (llm.nal.vn \u2014 NAL
|
|
480
|
+
name: "2. LLMLite API key (llm.nal.vn \u2014 NAL gateway, key sk-...)",
|
|
470
481
|
value: "llmlite"
|
|
482
|
+
},
|
|
483
|
+
{
|
|
484
|
+
name: "3. Anthropic API key tr\u1EF1c ti\u1EBFp (console.anthropic.com, key sk-ant-...)",
|
|
485
|
+
value: "anthropic"
|
|
471
486
|
}
|
|
472
487
|
]
|
|
473
488
|
});
|
|
474
489
|
}
|
|
475
490
|
|
|
491
|
+
// src/lib/setup-anthropic-api-key-and-model.ts
|
|
492
|
+
import { password, select as select2 } from "@inquirer/prompts";
|
|
493
|
+
var ANTHROPIC_BASE_URL = "https://api.anthropic.com";
|
|
494
|
+
var ANTHROPIC_API_VERSION = "2023-06-01";
|
|
495
|
+
var FETCH_TIMEOUT_MS = 1e4;
|
|
496
|
+
function maskAnthropicKey(key) {
|
|
497
|
+
if (key.length <= 12) return "sk-ant-***";
|
|
498
|
+
return `${key.slice(0, 7)}...${key.slice(-4)}`;
|
|
499
|
+
}
|
|
500
|
+
function validateAnthropicKeyFormat(key) {
|
|
501
|
+
const trimmed = key.trim();
|
|
502
|
+
if (trimmed.length === 0) return "API key b\u1EAFt bu\u1ED9c";
|
|
503
|
+
if (!trimmed.startsWith("sk-ant-")) {
|
|
504
|
+
return "Anthropic API key th\u01B0\u1EDDng b\u1EAFt \u0111\u1EA7u b\u1EB1ng 'sk-ant-' (l\u1EA5y t\u1EEB console.anthropic.com).";
|
|
505
|
+
}
|
|
506
|
+
return true;
|
|
507
|
+
}
|
|
508
|
+
async function promptAnthropicKeyHidden() {
|
|
509
|
+
return await password({
|
|
510
|
+
message: "Anthropic API key (sk-ant-..., \u1EA9n input):",
|
|
511
|
+
mask: "*",
|
|
512
|
+
validate: validateAnthropicKeyFormat
|
|
513
|
+
});
|
|
514
|
+
}
|
|
515
|
+
async function fetchAnthropicModels(apiKey) {
|
|
516
|
+
const controller = new AbortController();
|
|
517
|
+
const timer = setTimeout(() => controller.abort(), FETCH_TIMEOUT_MS);
|
|
518
|
+
try {
|
|
519
|
+
const res = await fetch(`${ANTHROPIC_BASE_URL}/v1/models`, {
|
|
520
|
+
method: "GET",
|
|
521
|
+
headers: {
|
|
522
|
+
"x-api-key": apiKey,
|
|
523
|
+
"anthropic-version": ANTHROPIC_API_VERSION,
|
|
524
|
+
Accept: "application/json"
|
|
525
|
+
},
|
|
526
|
+
signal: controller.signal
|
|
527
|
+
});
|
|
528
|
+
if (res.status === 401) {
|
|
529
|
+
throw new Error("API key invalid (HTTP 401). Check key tr\xEAn console.anthropic.com.");
|
|
530
|
+
}
|
|
531
|
+
if (res.status === 403) {
|
|
532
|
+
throw new Error(
|
|
533
|
+
"API key b\u1ECB reject (HTTP 403). Key c\xF3 th\u1EC3 \u0111\xE3 b\u1ECB revoke ho\u1EB7c thi\u1EBFu permission."
|
|
534
|
+
);
|
|
535
|
+
}
|
|
536
|
+
if (res.status === 429) {
|
|
537
|
+
throw new Error("Rate limit (HTTP 429). Ch\u1EDD v\xE0i gi\xE2y r\u1ED3i th\u1EED l\u1EA1i.");
|
|
538
|
+
}
|
|
539
|
+
if (!res.ok) {
|
|
540
|
+
throw new Error(`Fetch models th\u1EA5t b\u1EA1i (HTTP ${res.status}).`);
|
|
541
|
+
}
|
|
542
|
+
const json = await res.json();
|
|
543
|
+
const models = (json.data || []).map((m) => typeof m.id === "string" ? m.id : null).filter((id) => id !== null);
|
|
544
|
+
if (models.length === 0) {
|
|
545
|
+
throw new Error("Anthropic tr\u1EA3 v\u1EC1 list r\u1ED7ng. Li\xEAn h\u1EC7 support ho\u1EB7c check account.");
|
|
546
|
+
}
|
|
547
|
+
return models;
|
|
548
|
+
} catch (err) {
|
|
549
|
+
if (err.name === "AbortError") {
|
|
550
|
+
throw new Error(`Connect ${ANTHROPIC_BASE_URL} timeout sau ${FETCH_TIMEOUT_MS / 1e3}s.`);
|
|
551
|
+
}
|
|
552
|
+
throw err;
|
|
553
|
+
} finally {
|
|
554
|
+
clearTimeout(timer);
|
|
555
|
+
}
|
|
556
|
+
}
|
|
557
|
+
async function promptAnthropicModelChoice(models) {
|
|
558
|
+
if (models.length === 1) {
|
|
559
|
+
log.info(`Auto-pick model: ${models[0]} (ch\u1EC9 1 model available)`);
|
|
560
|
+
return models[0];
|
|
561
|
+
}
|
|
562
|
+
const sorted = [...models].sort((a, b) => {
|
|
563
|
+
const score = (m) => {
|
|
564
|
+
const lower = m.toLowerCase();
|
|
565
|
+
if (lower.includes("sonnet")) return 0;
|
|
566
|
+
if (lower.includes("opus")) return 1;
|
|
567
|
+
if (lower.includes("haiku")) return 2;
|
|
568
|
+
return 3;
|
|
569
|
+
};
|
|
570
|
+
return score(a) - score(b);
|
|
571
|
+
});
|
|
572
|
+
return await select2({
|
|
573
|
+
message: "Ch\u1ECDn model m\u1EB7c \u0111\u1ECBnh cho project:",
|
|
574
|
+
choices: sorted.map((m) => ({ name: m, value: m }))
|
|
575
|
+
});
|
|
576
|
+
}
|
|
577
|
+
async function setupAnthropicApiKeyAndModel() {
|
|
578
|
+
const apiKey = await promptAnthropicKeyHidden();
|
|
579
|
+
log.info(`Verify key (${maskAnthropicKey(apiKey)}) qua ${ANTHROPIC_BASE_URL}/v1/models...`);
|
|
580
|
+
const models = await fetchAnthropicModels(apiKey);
|
|
581
|
+
log.success(`Endpoint OK \u2014 ${models.length} models available`);
|
|
582
|
+
const model = await promptAnthropicModelChoice(models);
|
|
583
|
+
return { apiKey, baseUrl: ANTHROPIC_BASE_URL, model };
|
|
584
|
+
}
|
|
585
|
+
|
|
476
586
|
// src/lib/setup-llmlite-api-key-and-model.ts
|
|
477
|
-
import { input, password, select as
|
|
587
|
+
import { input, password as password2, select as select3 } from "@inquirer/prompts";
|
|
478
588
|
var DEFAULT_BASE_URL = "https://llm.nal.vn";
|
|
479
|
-
var
|
|
589
|
+
var FETCH_TIMEOUT_MS2 = 1e4;
|
|
480
590
|
function maskApiKey(key) {
|
|
481
591
|
if (key.length <= 8) return "sk-***";
|
|
482
592
|
return `${key.slice(0, 3)}...${key.slice(-4)}`;
|
|
483
593
|
}
|
|
484
594
|
async function promptApiKeyHidden() {
|
|
485
|
-
return await
|
|
595
|
+
return await password2({
|
|
486
596
|
message: "LLMLite API key (\u1EA9n input):",
|
|
487
597
|
mask: "*",
|
|
488
598
|
validate: (v) => v.trim().length > 0 ? true : "API key b\u1EAFt bu\u1ED9c"
|
|
@@ -498,7 +608,7 @@ async function promptBaseUrl(defaultUrl = DEFAULT_BASE_URL) {
|
|
|
498
608
|
}
|
|
499
609
|
async function fetchAvailableModels(baseUrl, apiKey) {
|
|
500
610
|
const controller = new AbortController();
|
|
501
|
-
const timer = setTimeout(() => controller.abort(),
|
|
611
|
+
const timer = setTimeout(() => controller.abort(), FETCH_TIMEOUT_MS2);
|
|
502
612
|
try {
|
|
503
613
|
const res = await fetch(`${baseUrl}/v1/models`, {
|
|
504
614
|
method: "GET",
|
|
@@ -525,7 +635,7 @@ async function fetchAvailableModels(baseUrl, apiKey) {
|
|
|
525
635
|
return models;
|
|
526
636
|
} catch (err) {
|
|
527
637
|
if (err.name === "AbortError") {
|
|
528
|
-
throw new Error(`Connect ${baseUrl} timeout sau ${
|
|
638
|
+
throw new Error(`Connect ${baseUrl} timeout sau ${FETCH_TIMEOUT_MS2 / 1e3}s.`);
|
|
529
639
|
}
|
|
530
640
|
throw err;
|
|
531
641
|
} finally {
|
|
@@ -539,7 +649,7 @@ async function promptModelChoice(models) {
|
|
|
539
649
|
return claudeAliases[0];
|
|
540
650
|
}
|
|
541
651
|
const choiceList = claudeAliases.length > 0 ? claudeAliases : models;
|
|
542
|
-
return await
|
|
652
|
+
return await select3({
|
|
543
653
|
message: "Ch\u1ECDn model m\u1EB7c \u0111\u1ECBnh cho project:",
|
|
544
654
|
choices: choiceList.map((m) => ({ name: m, value: m }))
|
|
545
655
|
});
|
|
@@ -575,7 +685,12 @@ function applySubscription(existing, model) {
|
|
|
575
685
|
const { env: existingEnv, ...rest } = existing;
|
|
576
686
|
const merged = { ...rest, model };
|
|
577
687
|
if (existingEnv) {
|
|
578
|
-
const {
|
|
688
|
+
const {
|
|
689
|
+
ANTHROPIC_BASE_URL: _b,
|
|
690
|
+
ANTHROPIC_AUTH_TOKEN: _t,
|
|
691
|
+
ANTHROPIC_API_KEY: _k,
|
|
692
|
+
...envRest
|
|
693
|
+
} = existingEnv;
|
|
579
694
|
if (Object.keys(envRest).length > 0) {
|
|
580
695
|
merged.env = envRest;
|
|
581
696
|
}
|
|
@@ -583,16 +698,29 @@ function applySubscription(existing, model) {
|
|
|
583
698
|
return merged;
|
|
584
699
|
}
|
|
585
700
|
function applyLLMLite(existing, apiKey, baseUrl, model) {
|
|
701
|
+
const { ANTHROPIC_API_KEY: _k, ...envRest } = existing.env || {};
|
|
586
702
|
return {
|
|
587
703
|
...existing,
|
|
588
704
|
env: {
|
|
589
|
-
...
|
|
705
|
+
...envRest,
|
|
590
706
|
ANTHROPIC_BASE_URL: baseUrl,
|
|
591
707
|
ANTHROPIC_AUTH_TOKEN: apiKey
|
|
592
708
|
},
|
|
593
709
|
model
|
|
594
710
|
};
|
|
595
711
|
}
|
|
712
|
+
function applyAnthropic(existing, apiKey, baseUrl, model) {
|
|
713
|
+
const { ANTHROPIC_AUTH_TOKEN: _t, ...envRest } = existing.env || {};
|
|
714
|
+
return {
|
|
715
|
+
...existing,
|
|
716
|
+
env: {
|
|
717
|
+
...envRest,
|
|
718
|
+
ANTHROPIC_BASE_URL: baseUrl,
|
|
719
|
+
ANTHROPIC_API_KEY: apiKey
|
|
720
|
+
},
|
|
721
|
+
model
|
|
722
|
+
};
|
|
723
|
+
}
|
|
596
724
|
function applyUseGlobal(existing, source) {
|
|
597
725
|
const sourceEnv = source.env || {};
|
|
598
726
|
const sourceModel = typeof source.model === "string" ? source.model : void 0;
|
|
@@ -616,6 +744,9 @@ async function writeClaudeSettings(workspacePath, input6) {
|
|
|
616
744
|
case "llmlite":
|
|
617
745
|
merged = applyLLMLite(existing, input6.apiKey, input6.baseUrl, input6.model);
|
|
618
746
|
break;
|
|
747
|
+
case "anthropic":
|
|
748
|
+
merged = applyAnthropic(existing, input6.apiKey, input6.baseUrl, input6.model);
|
|
749
|
+
break;
|
|
619
750
|
case "use-global":
|
|
620
751
|
merged = applyUseGlobal(existing, input6.sourceSettings);
|
|
621
752
|
break;
|
|
@@ -690,6 +821,23 @@ async function runAiSetupPhase(args) {
|
|
|
690
821
|
log.success(`AI ready \xB7 LLMLite \xB7 model=${llmConfig.model} \xB7 ${llmConfig.baseUrl}`);
|
|
691
822
|
return { ok: true, provider: "llmlite", model: llmConfig.model };
|
|
692
823
|
}
|
|
824
|
+
case "anthropic": {
|
|
825
|
+
const anthropicConfig = await setupAnthropicApiKeyAndModel();
|
|
826
|
+
await writeClaudeSettings(args.workspacePath, {
|
|
827
|
+
provider: "anthropic",
|
|
828
|
+
apiKey: anthropicConfig.apiKey,
|
|
829
|
+
baseUrl: anthropicConfig.baseUrl,
|
|
830
|
+
model: anthropicConfig.model
|
|
831
|
+
});
|
|
832
|
+
await appendAuditEntry(
|
|
833
|
+
"ai_setup",
|
|
834
|
+
`provider=anthropic,result=ok,model=${anthropicConfig.model}`
|
|
835
|
+
);
|
|
836
|
+
log.success(
|
|
837
|
+
`AI ready \xB7 Anthropic Direct \xB7 model=${anthropicConfig.model} \xB7 ${anthropicConfig.baseUrl}`
|
|
838
|
+
);
|
|
839
|
+
return { ok: true, provider: "anthropic", model: anthropicConfig.model };
|
|
840
|
+
}
|
|
693
841
|
case "use-global": {
|
|
694
842
|
if (!globalInfo.rawSettings) {
|
|
695
843
|
throw new Error("use-global ch\u1ECDn nh\u01B0ng kh\xF4ng \u0111\u1ECDc \u0111\u01B0\u1EE3c global settings.");
|
|
@@ -714,14 +862,15 @@ async function runAiSetupPhase(args) {
|
|
|
714
862
|
|
|
715
863
|
// src/lib/test-ai-provider-by-detected-mode.ts
|
|
716
864
|
import { spawnSync as spawnSync4 } from "child_process";
|
|
717
|
-
var
|
|
865
|
+
var FETCH_TIMEOUT_MS3 = 1e4;
|
|
718
866
|
var CLAUDE_PRINT_TIMEOUT_MS = 3e4;
|
|
719
867
|
var TEST_CHAT_MAX_TOKENS = 5;
|
|
720
868
|
var TEST_CHAT_PROMPT = "say ok";
|
|
869
|
+
var ANTHROPIC_API_VERSION2 = "2023-06-01";
|
|
721
870
|
async function testLLMLiteProvider(baseUrl, token, model) {
|
|
722
871
|
log.info(`Testing LLMLite provider: ${baseUrl} (key: ${maskApiKey(token)})`);
|
|
723
872
|
const controller = new AbortController();
|
|
724
|
-
const timer = setTimeout(() => controller.abort(),
|
|
873
|
+
const timer = setTimeout(() => controller.abort(), FETCH_TIMEOUT_MS3);
|
|
725
874
|
try {
|
|
726
875
|
const modelsRes = await fetch(`${baseUrl}/v1/models`, {
|
|
727
876
|
headers: { Authorization: `Bearer ${token}` },
|
|
@@ -766,7 +915,7 @@ async function testLLMLiteProvider(baseUrl, token, model) {
|
|
|
766
915
|
log.dim(` Tokens used: ${tokens}`);
|
|
767
916
|
} catch (err) {
|
|
768
917
|
if (err.name === "AbortError") {
|
|
769
|
-
throw new Error(`Timeout ${
|
|
918
|
+
throw new Error(`Timeout ${FETCH_TIMEOUT_MS3 / 1e3}s. Check m\u1EA1ng / endpoint ${baseUrl}.`);
|
|
770
919
|
}
|
|
771
920
|
throw err;
|
|
772
921
|
} finally {
|
|
@@ -795,11 +944,63 @@ function testSubscriptionProvider() {
|
|
|
795
944
|
}
|
|
796
945
|
log.success(`Response: "${(result.stdout || "").trim().slice(0, 100)}"`);
|
|
797
946
|
}
|
|
947
|
+
async function testAnthropicProvider(baseUrl, apiKey, model) {
|
|
948
|
+
log.info(`Testing Anthropic Direct provider: ${baseUrl} (key: ${maskApiKey(apiKey)})`);
|
|
949
|
+
const controller = new AbortController();
|
|
950
|
+
const timer = setTimeout(() => controller.abort(), FETCH_TIMEOUT_MS3);
|
|
951
|
+
try {
|
|
952
|
+
const modelsRes = await fetch(`${baseUrl}/v1/models`, {
|
|
953
|
+
headers: {
|
|
954
|
+
"x-api-key": apiKey,
|
|
955
|
+
"anthropic-version": ANTHROPIC_API_VERSION2
|
|
956
|
+
},
|
|
957
|
+
signal: controller.signal
|
|
958
|
+
});
|
|
959
|
+
if (modelsRes.status === 401 || modelsRes.status === 403) {
|
|
960
|
+
throw new Error(`API key invalid (HTTP ${modelsRes.status}). Re-run: avatar ai setup`);
|
|
961
|
+
}
|
|
962
|
+
if (!modelsRes.ok) {
|
|
963
|
+
throw new Error(`Endpoint /v1/models l\u1ED7i (HTTP ${modelsRes.status}).`);
|
|
964
|
+
}
|
|
965
|
+
const modelsJson = await modelsRes.json();
|
|
966
|
+
const models = (modelsJson.data || []).map((m) => typeof m.id === "string" ? m.id : null).filter((id) => id !== null);
|
|
967
|
+
log.success(`Connectivity OK \xB7 ${models.length} models available`);
|
|
968
|
+
log.info(`Testing message v\u1EDBi model "${model}"...`);
|
|
969
|
+
const msgRes = await fetch(`${baseUrl}/v1/messages`, {
|
|
970
|
+
method: "POST",
|
|
971
|
+
headers: {
|
|
972
|
+
"x-api-key": apiKey,
|
|
973
|
+
"anthropic-version": ANTHROPIC_API_VERSION2,
|
|
974
|
+
"Content-Type": "application/json"
|
|
975
|
+
},
|
|
976
|
+
body: JSON.stringify({
|
|
977
|
+
model,
|
|
978
|
+
max_tokens: TEST_CHAT_MAX_TOKENS,
|
|
979
|
+
messages: [{ role: "user", content: TEST_CHAT_PROMPT }]
|
|
980
|
+
}),
|
|
981
|
+
signal: controller.signal
|
|
982
|
+
});
|
|
983
|
+
if (!msgRes.ok) {
|
|
984
|
+
const errText = (await msgRes.text()).slice(0, 200);
|
|
985
|
+
throw new Error(`Message endpoint fail (HTTP ${msgRes.status}): ${errText}`);
|
|
986
|
+
}
|
|
987
|
+
const msgJson = await msgRes.json();
|
|
988
|
+
const text = (msgJson.content || []).map((c) => typeof c.text === "string" ? c.text : "").join("").trim().slice(0, 100);
|
|
989
|
+
log.success(`Response: "${text}"`);
|
|
990
|
+
} finally {
|
|
991
|
+
clearTimeout(timer);
|
|
992
|
+
}
|
|
993
|
+
}
|
|
798
994
|
async function testAiProviderByDetectedMode(settings) {
|
|
799
995
|
const env = settings.env || {};
|
|
800
996
|
const baseUrl = typeof env.ANTHROPIC_BASE_URL === "string" ? env.ANTHROPIC_BASE_URL : void 0;
|
|
801
997
|
const token = typeof env.ANTHROPIC_AUTH_TOKEN === "string" ? env.ANTHROPIC_AUTH_TOKEN : void 0;
|
|
998
|
+
const apiKey = typeof env.ANTHROPIC_API_KEY === "string" ? env.ANTHROPIC_API_KEY : void 0;
|
|
802
999
|
const model = typeof settings.model === "string" ? settings.model : "default";
|
|
1000
|
+
if (apiKey && baseUrl) {
|
|
1001
|
+
await testAnthropicProvider(baseUrl, apiKey, model);
|
|
1002
|
+
return { ok: true, provider: "anthropic", message: "Anthropic Direct provider working" };
|
|
1003
|
+
}
|
|
803
1004
|
if (baseUrl && token) {
|
|
804
1005
|
await testLLMLiteProvider(baseUrl, token, model);
|
|
805
1006
|
return { ok: true, provider: "llmlite", message: "LLMLite provider working" };
|
|
@@ -845,12 +1046,27 @@ async function runAiStatus() {
|
|
|
845
1046
|
const env = settings.env || {};
|
|
846
1047
|
const baseUrl = typeof env.ANTHROPIC_BASE_URL === "string" ? env.ANTHROPIC_BASE_URL : void 0;
|
|
847
1048
|
const token = typeof env.ANTHROPIC_AUTH_TOKEN === "string" ? env.ANTHROPIC_AUTH_TOKEN : void 0;
|
|
1049
|
+
const apiKey = typeof env.ANTHROPIC_API_KEY === "string" ? env.ANTHROPIC_API_KEY : void 0;
|
|
848
1050
|
const model = typeof settings.model === "string" ? settings.model : void 0;
|
|
849
|
-
|
|
1051
|
+
let provider;
|
|
1052
|
+
let credentialDisplay;
|
|
1053
|
+
if (apiKey) {
|
|
1054
|
+
provider = baseUrl?.includes("api.anthropic.com") || apiKey.startsWith("sk-ant-") ? "Anthropic Direct" : "Custom (API key)";
|
|
1055
|
+
credentialDisplay = maskApiKey(apiKey);
|
|
1056
|
+
} else if (baseUrl && token) {
|
|
1057
|
+
provider = "LLMLite";
|
|
1058
|
+
credentialDisplay = maskApiKey(token);
|
|
1059
|
+
} else if (token) {
|
|
1060
|
+
provider = "Custom";
|
|
1061
|
+
credentialDisplay = maskApiKey(token);
|
|
1062
|
+
} else {
|
|
1063
|
+
provider = "Subscription (default)";
|
|
1064
|
+
credentialDisplay = "(kh\xF4ng set \u2014 d\xF9ng subscription auth)";
|
|
1065
|
+
}
|
|
850
1066
|
log.info(`Project: ${workspacePath}`);
|
|
851
1067
|
log.info(`Provider: ${provider}${baseUrl ? ` (${baseUrl})` : ""}`);
|
|
852
1068
|
log.info(`Model: ${model ?? "(default \u2014 Claude Code ch\u1ECDn)"}`);
|
|
853
|
-
log.info(`Token: ${
|
|
1069
|
+
log.info(`Token: ${credentialDisplay}`);
|
|
854
1070
|
}
|
|
855
1071
|
async function runAiTest() {
|
|
856
1072
|
const workspacePath = await ensureWorkspaceCwd();
|
|
@@ -880,7 +1096,12 @@ async function runAiReset(opts) {
|
|
|
880
1096
|
const { env: existingEnv, ...rest } = settings;
|
|
881
1097
|
const reset = { ...rest };
|
|
882
1098
|
if (existingEnv) {
|
|
883
|
-
const {
|
|
1099
|
+
const {
|
|
1100
|
+
ANTHROPIC_BASE_URL: _b,
|
|
1101
|
+
ANTHROPIC_AUTH_TOKEN: _t,
|
|
1102
|
+
ANTHROPIC_API_KEY: _k,
|
|
1103
|
+
...envRest
|
|
1104
|
+
} = existingEnv;
|
|
884
1105
|
if (Object.keys(envRest).length > 0) {
|
|
885
1106
|
reset.env = envRest;
|
|
886
1107
|
}
|
|
@@ -1538,7 +1759,7 @@ function installGitnexusViaNpm() {
|
|
|
1538
1759
|
}
|
|
1539
1760
|
|
|
1540
1761
|
// src/lib/prompt-recovery-action-on-failure.ts
|
|
1541
|
-
import { input as input3, select as
|
|
1762
|
+
import { input as input3, select as select4 } from "@inquirer/prompts";
|
|
1542
1763
|
var UserAbortedRecoveryError = class extends Error {
|
|
1543
1764
|
constructor(message) {
|
|
1544
1765
|
super(message);
|
|
@@ -1555,7 +1776,7 @@ async function promptRetryOrSkip(args) {
|
|
|
1555
1776
|
choices.push({ name: "B\u1ECF qua b\u01B0\u1EDBc n\xE0y v\xE0 ti\u1EBFp t\u1EE5c (Skip)", value: "skip" });
|
|
1556
1777
|
}
|
|
1557
1778
|
choices.push({ name: "T\u1EA1m ng\u01B0ng init \u2014 ch\u1EA1y l\u1EA1i sau (Abort)", value: "abort" });
|
|
1558
|
-
return await
|
|
1779
|
+
return await select4({
|
|
1559
1780
|
message: "C\xE1ch x\u1EED l\xFD?",
|
|
1560
1781
|
choices
|
|
1561
1782
|
});
|
|
@@ -1949,19 +2170,19 @@ function registerGitnexusCommand(program2) {
|
|
|
1949
2170
|
|
|
1950
2171
|
// src/commands/init.ts
|
|
1951
2172
|
import { basename, join as join22, relative as relative2, resolve } from "path";
|
|
1952
|
-
import { confirm as confirm5, input as input5, select as
|
|
2173
|
+
import { confirm as confirm5, input as input5, select as select9 } from "@inquirer/prompts";
|
|
1953
2174
|
import boxen5 from "boxen";
|
|
1954
2175
|
|
|
1955
2176
|
// src/lib/add-team-pack-submodule-with-retry-on-network-fail.ts
|
|
1956
2177
|
import { spawnSync as spawnSync13 } from "child_process";
|
|
1957
|
-
import { select as
|
|
2178
|
+
import { select as select6 } from "@inquirer/prompts";
|
|
1958
2179
|
|
|
1959
2180
|
// src/lib/team-pack-submodule-manager.ts
|
|
1960
2181
|
import { join as join16 } from "path";
|
|
1961
2182
|
|
|
1962
2183
|
// src/lib/check-team-pack-access-with-retry-loop.ts
|
|
1963
2184
|
import { spawnSync as spawnSync12 } from "child_process";
|
|
1964
|
-
import { confirm as confirm4, select as
|
|
2185
|
+
import { confirm as confirm4, select as select5 } from "@inquirer/prompts";
|
|
1965
2186
|
import boxen3 from "boxen";
|
|
1966
2187
|
function parseRepoSlugFromGitUrl(url) {
|
|
1967
2188
|
const httpsMatch = url.match(/github\.com[/:]([^/]+\/[^/]+?)(?:\.git)?$/);
|
|
@@ -2040,7 +2261,7 @@ async function ensureTeamPackAccessWithRetry(args) {
|
|
|
2040
2261
|
while (true) {
|
|
2041
2262
|
const ghUser = getCurrentGhUser();
|
|
2042
2263
|
const ghUserDisplay = ghUser ?? "(ch\u01B0a gh auth)";
|
|
2043
|
-
const action = await
|
|
2264
|
+
const action = await select5({
|
|
2044
2265
|
message: "C\xE1ch x\u1EED l\xFD?",
|
|
2045
2266
|
choices: [
|
|
2046
2267
|
{
|
|
@@ -2157,7 +2378,7 @@ function openGithubSshKeysPage() {
|
|
|
2157
2378
|
}
|
|
2158
2379
|
}
|
|
2159
2380
|
async function handleSshPermissionError() {
|
|
2160
|
-
return await
|
|
2381
|
+
return await select6({
|
|
2161
2382
|
message: "SSH permission denied. C\xE1ch x\u1EED l\xFD?",
|
|
2162
2383
|
choices: [
|
|
2163
2384
|
{
|
|
@@ -2420,7 +2641,7 @@ function detectPackageManager() {
|
|
|
2420
2641
|
|
|
2421
2642
|
// src/lib/handle-remote-access-failure-with-account-switch.ts
|
|
2422
2643
|
import { spawnSync as spawnSync19 } from "child_process";
|
|
2423
|
-
import { input as input4, select as
|
|
2644
|
+
import { input as input4, select as select7 } from "@inquirer/prompts";
|
|
2424
2645
|
|
|
2425
2646
|
// src/lib/verify-git-remote-accessible.ts
|
|
2426
2647
|
import { spawnSync as spawnSync18 } from "child_process";
|
|
@@ -2504,7 +2725,7 @@ async function handleRemoteAccessFailureWithAccountSwitch(args) {
|
|
|
2504
2725
|
log.dim(` L\xFD do: ${reason}${detail ? ` \u2014 ${detail.slice(0, 150)}` : ""}`);
|
|
2505
2726
|
log.info(getReasonHint(reason, currentUrl, ghUser));
|
|
2506
2727
|
if (ghUser) log.dim(` gh CLI hi\u1EC7n \u0111ang login: ${ghUser}`);
|
|
2507
|
-
const action = await
|
|
2728
|
+
const action = await select7({
|
|
2508
2729
|
message: "C\xE1ch x\u1EED l\xFD?",
|
|
2509
2730
|
choices: [
|
|
2510
2731
|
{
|
|
@@ -2813,7 +3034,7 @@ function linkExistingRemoteToWorkspace(args) {
|
|
|
2813
3034
|
|
|
2814
3035
|
// src/lib/safe-bootstrap-for-dirty-folder.ts
|
|
2815
3036
|
import { readdirSync } from "fs";
|
|
2816
|
-
import { select as
|
|
3037
|
+
import { select as select8 } from "@inquirer/prompts";
|
|
2817
3038
|
import { simpleGit as simpleGit3 } from "simple-git";
|
|
2818
3039
|
|
|
2819
3040
|
// src/lib/check-folder-has-git.ts
|
|
@@ -2950,7 +3171,7 @@ async function promptBootstrapStrategy(state, opts) {
|
|
|
2950
3171
|
if (opts.presetStrategy) return opts.presetStrategy;
|
|
2951
3172
|
if (opts.autoYes) return "stash";
|
|
2952
3173
|
if (state === "empty" || state === "clean") return "commit-all";
|
|
2953
|
-
return await
|
|
3174
|
+
return await select8({
|
|
2954
3175
|
message: state === "dirty" ? "Folder c\xF3 changes ch\u01B0a commit. C\xE1ch x\u1EED l\xFD:" : "Folder c\xF3 file ch\u01B0a version. C\xE1ch x\u1EED l\xFD:",
|
|
2955
3176
|
choices: [
|
|
2956
3177
|
{
|
|
@@ -3435,7 +3656,7 @@ async function runInit(opts) {
|
|
|
3435
3656
|
}
|
|
3436
3657
|
}
|
|
3437
3658
|
async function promptProjectStatus() {
|
|
3438
|
-
return await
|
|
3659
|
+
return await select9({
|
|
3439
3660
|
message: "T\xECnh tr\u1EA1ng d\u1EF1 \xE1n c\u1EE7a b\u1EA1n?",
|
|
3440
3661
|
choices: [
|
|
3441
3662
|
{ name: "1. \u0110\xE3 c\xF3 repo git remote (URL c\xF3 s\u1EB5n)", value: "existing-remote" },
|
|
@@ -3518,7 +3739,7 @@ async function runInitFromScratch(opts, ownerEmail) {
|
|
|
3518
3739
|
message: "T\xEAn d\u1EF1 \xE1n:",
|
|
3519
3740
|
validate: (v) => v.length > 0 ? true : "T\xEAn b\u1EAFt bu\u1ED9c"
|
|
3520
3741
|
});
|
|
3521
|
-
const visibility = opts.repoVisibility ?? await
|
|
3742
|
+
const visibility = opts.repoVisibility ?? await select9({
|
|
3522
3743
|
message: "Visibility?",
|
|
3523
3744
|
choices: [
|
|
3524
3745
|
{ name: "private (m\u1EB7c \u0111\u1ECBnh)", value: "private" },
|
|
@@ -3593,7 +3814,7 @@ async function getOrCreateOriginRemote(folderPath, opts) {
|
|
|
3593
3814
|
return void 0;
|
|
3594
3815
|
}
|
|
3595
3816
|
await ensureGitHubReady();
|
|
3596
|
-
const visibility = opts.repoVisibility ?? await
|
|
3817
|
+
const visibility = opts.repoVisibility ?? await select9({
|
|
3597
3818
|
message: "Visibility?",
|
|
3598
3819
|
choices: [
|
|
3599
3820
|
{ name: "private (m\u1EB7c \u0111\u1ECBnh)", value: "private" },
|
|
@@ -3719,7 +3940,7 @@ async function maybeCreateWorkspaceRemote(args) {
|
|
|
3719
3940
|
});
|
|
3720
3941
|
}
|
|
3721
3942
|
if (!shouldCreate) return;
|
|
3722
|
-
const visibility = args.repoVisibility ?? (args.autoYes ? "private" : await
|
|
3943
|
+
const visibility = args.repoVisibility ?? (args.autoYes ? "private" : await select9({
|
|
3723
3944
|
message: "Workspace visibility?",
|
|
3724
3945
|
choices: [
|
|
3725
3946
|
{ name: "private (m\u1EB7c \u0111\u1ECBnh, an to\xE0n)", value: "private" },
|
|
@@ -3738,7 +3959,7 @@ async function maybeCreateWorkspaceRemote(args) {
|
|
|
3738
3959
|
} catch (err) {
|
|
3739
3960
|
if (err instanceof CreateWorkspaceRemoteError && err.reason === "repo-exists") {
|
|
3740
3961
|
const fullName = err.fullName;
|
|
3741
|
-
const reuseAction = await
|
|
3962
|
+
const reuseAction = await select9({
|
|
3742
3963
|
message: `Repo '${fullName}' \u0111\xE3 t\u1ED3n t\u1EA1i tr\xEAn GitHub. C\xE1ch x\u1EED l\xFD?`,
|
|
3743
3964
|
choices: [
|
|
3744
3965
|
{
|
|
@@ -3807,7 +4028,7 @@ async function resolveWorkspacePath(parent, desiredName, force) {
|
|
|
3807
4028
|
}
|
|
3808
4029
|
choices.push({ name: "Nh\u1EADp t\xEAn workspace kh\xE1c (manual)", value: "manual" });
|
|
3809
4030
|
choices.push({ name: "T\u1EA1m ng\u01B0ng init", value: "abort" });
|
|
3810
|
-
const action = await
|
|
4031
|
+
const action = await select9({
|
|
3811
4032
|
message: "C\xE1ch x\u1EED l\xFD workspace path conflict?",
|
|
3812
4033
|
choices
|
|
3813
4034
|
});
|
|
@@ -4014,8 +4235,350 @@ function renderStatusBox(s) {
|
|
|
4014
4235
|
}
|
|
4015
4236
|
|
|
4016
4237
|
// src/commands/sync.ts
|
|
4238
|
+
import { join as join28 } from "path";
|
|
4239
|
+
|
|
4240
|
+
// src/lib/merge-pack-settings-into-project-settings.ts
|
|
4241
|
+
import { promises as fs11 } from "fs";
|
|
4242
|
+
import { join as join25 } from "path";
|
|
4243
|
+
function backupFilename(originalPath) {
|
|
4244
|
+
const d = /* @__PURE__ */ new Date();
|
|
4245
|
+
const stamp = `${d.getFullYear().toString().slice(-2) + String(d.getMonth() + 1).padStart(2, "0") + String(d.getDate()).padStart(2, "0")}-${String(d.getHours()).padStart(2, "0")}${String(d.getMinutes()).padStart(2, "0")}`;
|
|
4246
|
+
return `${originalPath}.backup-${stamp}`;
|
|
4247
|
+
}
|
|
4248
|
+
function unionDedupe(a, b) {
|
|
4249
|
+
const seen = /* @__PURE__ */ new Set();
|
|
4250
|
+
const out = [];
|
|
4251
|
+
for (const item of [...a, ...b]) {
|
|
4252
|
+
const key = typeof item === "string" ? item : JSON.stringify(item);
|
|
4253
|
+
if (!seen.has(key)) {
|
|
4254
|
+
seen.add(key);
|
|
4255
|
+
out.push(item);
|
|
4256
|
+
}
|
|
4257
|
+
}
|
|
4258
|
+
return out;
|
|
4259
|
+
}
|
|
4260
|
+
function mergeHooksPerEvent(packHooks, userHooks) {
|
|
4261
|
+
const touched = [];
|
|
4262
|
+
const merged = { ...userHooks };
|
|
4263
|
+
for (const [event, packEntries] of Object.entries(packHooks)) {
|
|
4264
|
+
const userEntries = userHooks[event] || [];
|
|
4265
|
+
const union = unionDedupe(userEntries, packEntries);
|
|
4266
|
+
if (union.length !== userEntries.length) {
|
|
4267
|
+
touched.push(event);
|
|
4268
|
+
}
|
|
4269
|
+
merged[event] = union;
|
|
4270
|
+
}
|
|
4271
|
+
return { merged, touchedEvents: touched };
|
|
4272
|
+
}
|
|
4273
|
+
async function mergePackSettingsIntoProjectSettings(workspacePath) {
|
|
4274
|
+
const packTemplatePath = join25(workspacePath, ".claude", "pack", "templates", "settings.json.tpl");
|
|
4275
|
+
const projectSettingsPath = join25(workspacePath, ".claude", "settings.json");
|
|
4276
|
+
if (!await pathExists(packTemplatePath)) {
|
|
4277
|
+
return { action: "no-pack-template", changes: [] };
|
|
4278
|
+
}
|
|
4279
|
+
let packTemplate;
|
|
4280
|
+
try {
|
|
4281
|
+
const raw = await readText(packTemplatePath);
|
|
4282
|
+
packTemplate = JSON.parse(raw);
|
|
4283
|
+
} catch (err) {
|
|
4284
|
+
throw new Error(
|
|
4285
|
+
`Pack settings template kh\xF4ng parse \u0111\u01B0\u1EE3c JSON: ${err.message}. Path: ${packTemplatePath}`
|
|
4286
|
+
);
|
|
4287
|
+
}
|
|
4288
|
+
let userSettings = {};
|
|
4289
|
+
let projectHasSettings = false;
|
|
4290
|
+
if (await pathExists(projectSettingsPath)) {
|
|
4291
|
+
projectHasSettings = true;
|
|
4292
|
+
try {
|
|
4293
|
+
userSettings = await readJson(projectSettingsPath);
|
|
4294
|
+
} catch (err) {
|
|
4295
|
+
throw new Error(
|
|
4296
|
+
`Project settings.json kh\xF4ng parse \u0111\u01B0\u1EE3c: ${err.message}. Manual fix tr\u01B0\u1EDBc khi sync.`
|
|
4297
|
+
);
|
|
4298
|
+
}
|
|
4299
|
+
}
|
|
4300
|
+
const changes = [];
|
|
4301
|
+
const merged = { ...userSettings };
|
|
4302
|
+
if (packTemplate.statusLine && !userSettings.statusLine) {
|
|
4303
|
+
merged.statusLine = packTemplate.statusLine;
|
|
4304
|
+
changes.push("statusLine added");
|
|
4305
|
+
}
|
|
4306
|
+
if (typeof packTemplate.includeCoAuthoredBy === "boolean" && typeof userSettings.includeCoAuthoredBy !== "boolean") {
|
|
4307
|
+
merged.includeCoAuthoredBy = packTemplate.includeCoAuthoredBy;
|
|
4308
|
+
changes.push("includeCoAuthoredBy added");
|
|
4309
|
+
}
|
|
4310
|
+
if (packTemplate.model && !userSettings.model) {
|
|
4311
|
+
merged.model = packTemplate.model;
|
|
4312
|
+
changes.push("model added");
|
|
4313
|
+
}
|
|
4314
|
+
if (packTemplate.env) {
|
|
4315
|
+
const mergedEnv = { ...userSettings.env || {} };
|
|
4316
|
+
let envChanged = false;
|
|
4317
|
+
for (const [k, v] of Object.entries(packTemplate.env)) {
|
|
4318
|
+
if (!(k in mergedEnv)) {
|
|
4319
|
+
mergedEnv[k] = v;
|
|
4320
|
+
envChanged = true;
|
|
4321
|
+
}
|
|
4322
|
+
}
|
|
4323
|
+
if (envChanged) {
|
|
4324
|
+
merged.env = mergedEnv;
|
|
4325
|
+
changes.push("env vars added from pack");
|
|
4326
|
+
}
|
|
4327
|
+
}
|
|
4328
|
+
if (packTemplate.permissions) {
|
|
4329
|
+
const userAllow = userSettings.permissions?.allow || [];
|
|
4330
|
+
const userDeny = userSettings.permissions?.deny || [];
|
|
4331
|
+
const packAllow = packTemplate.permissions.allow || [];
|
|
4332
|
+
const packDeny = packTemplate.permissions.deny || [];
|
|
4333
|
+
const mergedAllow = unionDedupe(userAllow, packAllow);
|
|
4334
|
+
const mergedDeny = unionDedupe(userDeny, packDeny);
|
|
4335
|
+
if (mergedAllow.length !== userAllow.length || mergedDeny.length !== userDeny.length) {
|
|
4336
|
+
merged.permissions = { allow: mergedAllow, deny: mergedDeny };
|
|
4337
|
+
changes.push(
|
|
4338
|
+
`permissions union (+${mergedAllow.length - userAllow.length} allow, +${mergedDeny.length - userDeny.length} deny)`
|
|
4339
|
+
);
|
|
4340
|
+
}
|
|
4341
|
+
}
|
|
4342
|
+
if (packTemplate.hooks) {
|
|
4343
|
+
const userHooks = userSettings.hooks || {};
|
|
4344
|
+
const { merged: mergedHooks, touchedEvents } = mergeHooksPerEvent(
|
|
4345
|
+
packTemplate.hooks,
|
|
4346
|
+
userHooks
|
|
4347
|
+
);
|
|
4348
|
+
if (touchedEvents.length > 0) {
|
|
4349
|
+
merged.hooks = mergedHooks;
|
|
4350
|
+
changes.push(`hooks added for events: ${touchedEvents.join(", ")}`);
|
|
4351
|
+
}
|
|
4352
|
+
}
|
|
4353
|
+
if (changes.length === 0) {
|
|
4354
|
+
return { action: "no-change", changes: [] };
|
|
4355
|
+
}
|
|
4356
|
+
let backupPath;
|
|
4357
|
+
if (projectHasSettings) {
|
|
4358
|
+
backupPath = backupFilename(projectSettingsPath);
|
|
4359
|
+
await fs11.copyFile(projectSettingsPath, backupPath);
|
|
4360
|
+
}
|
|
4361
|
+
await writeJsonAtomic(projectSettingsPath, merged);
|
|
4362
|
+
return { action: "merged", backupPath, changes };
|
|
4363
|
+
}
|
|
4364
|
+
|
|
4365
|
+
// src/lib/preview-team-pack-sync-changes-for-dry-run.ts
|
|
4366
|
+
import { join as join27 } from "path";
|
|
4367
|
+
|
|
4368
|
+
// src/lib/symlink-farm-for-team-pack-mount-dirs.ts
|
|
4369
|
+
import { promises as fs13 } from "fs";
|
|
4370
|
+
import { dirname as dirname5, join as join26, relative as relative3 } from "path";
|
|
4371
|
+
|
|
4372
|
+
// src/lib/backup-existing-dir-before-symlink-override.ts
|
|
4373
|
+
import { promises as fs12 } from "fs";
|
|
4374
|
+
function timestamp() {
|
|
4375
|
+
const d = /* @__PURE__ */ new Date();
|
|
4376
|
+
const pad = (n) => n.toString().padStart(2, "0");
|
|
4377
|
+
return `${d.getFullYear()}-${pad(d.getMonth() + 1)}-${pad(d.getDate())}-${pad(d.getHours())}${pad(d.getMinutes())}`;
|
|
4378
|
+
}
|
|
4379
|
+
async function backupDirBeforeReplace(targetPath) {
|
|
4380
|
+
const backupPath = `${targetPath}.backup-${timestamp()}`;
|
|
4381
|
+
await fs12.rename(targetPath, backupPath);
|
|
4382
|
+
return backupPath;
|
|
4383
|
+
}
|
|
4384
|
+
|
|
4385
|
+
// src/lib/symlink-farm-for-team-pack-mount-dirs.ts
|
|
4386
|
+
var TEAM_PACK_MOUNT_DIRS = [
|
|
4387
|
+
"skills",
|
|
4388
|
+
"agents",
|
|
4389
|
+
"commands",
|
|
4390
|
+
"hooks",
|
|
4391
|
+
"workflows",
|
|
4392
|
+
"scripts",
|
|
4393
|
+
"knowledge"
|
|
4394
|
+
];
|
|
4395
|
+
async function isSymbolicLink(path) {
|
|
4396
|
+
try {
|
|
4397
|
+
const st = await fs13.lstat(path);
|
|
4398
|
+
return st.isSymbolicLink();
|
|
4399
|
+
} catch {
|
|
4400
|
+
return false;
|
|
4401
|
+
}
|
|
4402
|
+
}
|
|
4403
|
+
async function syncMountedDir(source, dest, force) {
|
|
4404
|
+
const dir = relative3(dirname5(dest), dest) || dest;
|
|
4405
|
+
if (!await pathExists(source)) {
|
|
4406
|
+
return { dir, action: "source-missing" };
|
|
4407
|
+
}
|
|
4408
|
+
if (await pathExists(dest)) {
|
|
4409
|
+
if (await isSymbolicLink(dest)) {
|
|
4410
|
+
await fs13.unlink(dest);
|
|
4411
|
+
} else if (force) {
|
|
4412
|
+
const backupPath = await backupDirBeforeReplace(dest);
|
|
4413
|
+
const relativeSource2 = relative3(dirname5(dest), source);
|
|
4414
|
+
await fs13.symlink(relativeSource2, dest);
|
|
4415
|
+
return { dir, action: "backed-up-and-linked", backupPath };
|
|
4416
|
+
} else {
|
|
4417
|
+
return { dir, action: "skipped-conflict" };
|
|
4418
|
+
}
|
|
4419
|
+
}
|
|
4420
|
+
const relativeSource = relative3(dirname5(dest), source);
|
|
4421
|
+
await fs13.symlink(relativeSource, dest);
|
|
4422
|
+
return { dir, action: "created" };
|
|
4423
|
+
}
|
|
4424
|
+
async function syncAllMountDirs(packDir, claudeDir, force) {
|
|
4425
|
+
const results = [];
|
|
4426
|
+
for (const dir of TEAM_PACK_MOUNT_DIRS) {
|
|
4427
|
+
const source = join26(packDir, dir);
|
|
4428
|
+
const dest = join26(claudeDir, dir);
|
|
4429
|
+
results.push(await syncMountedDir(source, dest, force));
|
|
4430
|
+
}
|
|
4431
|
+
return results;
|
|
4432
|
+
}
|
|
4433
|
+
|
|
4434
|
+
// src/lib/preview-team-pack-sync-changes-for-dry-run.ts
|
|
4435
|
+
async function inspectMountDir(packDir, claudeDir, dir) {
|
|
4436
|
+
const source = join27(packDir, dir);
|
|
4437
|
+
const dest = join27(claudeDir, dir);
|
|
4438
|
+
if (!await pathExists(source)) return "source-missing";
|
|
4439
|
+
if (!await pathExists(dest)) return "needs-creation";
|
|
4440
|
+
const { promises: fs14 } = await import("fs");
|
|
4441
|
+
const st = await fs14.lstat(dest);
|
|
4442
|
+
if (st.isSymbolicLink()) return "already-linked";
|
|
4443
|
+
return "conflict-real-dir";
|
|
4444
|
+
}
|
|
4445
|
+
async function listCommitsBetween(packDir, fromSha, toRef) {
|
|
4446
|
+
try {
|
|
4447
|
+
const result = await git(packDir).raw(["log", "--oneline", `${fromSha}..${toRef}`]);
|
|
4448
|
+
return result.split("\n").map((l) => l.trim()).filter((l) => l.length > 0);
|
|
4449
|
+
} catch {
|
|
4450
|
+
return [];
|
|
4451
|
+
}
|
|
4452
|
+
}
|
|
4453
|
+
async function buildSyncPreview(packDir, claudeDir, targetVersion) {
|
|
4454
|
+
const currentSha = await currentCommitSha(packDir);
|
|
4455
|
+
const currentVersion = currentSha.slice(0, 7);
|
|
4456
|
+
const target = targetVersion ?? await latestTag(packDir) ?? "HEAD";
|
|
4457
|
+
const commits = await listCommitsBetween(packDir, currentSha, target);
|
|
4458
|
+
const mountStatuses = [];
|
|
4459
|
+
for (const dir of TEAM_PACK_MOUNT_DIRS) {
|
|
4460
|
+
mountStatuses.push({
|
|
4461
|
+
dir,
|
|
4462
|
+
status: await inspectMountDir(packDir, claudeDir, dir)
|
|
4463
|
+
});
|
|
4464
|
+
}
|
|
4465
|
+
return {
|
|
4466
|
+
currentVersion,
|
|
4467
|
+
targetVersion: target,
|
|
4468
|
+
commitsBehind: commits,
|
|
4469
|
+
mountDirStatuses: mountStatuses
|
|
4470
|
+
};
|
|
4471
|
+
}
|
|
4472
|
+
|
|
4473
|
+
// src/commands/sync.ts
|
|
4474
|
+
async function syncAction(opts) {
|
|
4475
|
+
const projectRoot = process.cwd();
|
|
4476
|
+
const claudeDir = join28(projectRoot, ".claude");
|
|
4477
|
+
const packDir = join28(projectRoot, TEAM_PACK_RELATIVE_PATH);
|
|
4478
|
+
if (!await pathExists(packDir)) {
|
|
4479
|
+
log.error(
|
|
4480
|
+
`team-ai-pack submodule ch\u01B0a \u0111\u01B0\u1EE3c kh\u1EDFi t\u1EA1o \u1EDF ${TEAM_PACK_RELATIVE_PATH}/.
|
|
4481
|
+
Ch\u1EA1y 'avatar init' \u0111\u1EC3 add submodule, ho\u1EB7c 'git submodule update --init' n\u1EBFu \u0111\xE3 clone repo.`
|
|
4482
|
+
);
|
|
4483
|
+
process.exit(1);
|
|
4484
|
+
return;
|
|
4485
|
+
}
|
|
4486
|
+
try {
|
|
4487
|
+
await git(packDir).fetch(["--tags", "origin"]);
|
|
4488
|
+
} catch (err) {
|
|
4489
|
+
log.warn(
|
|
4490
|
+
`Kh\xF4ng fetch \u0111\u01B0\u1EE3c tags t\u1EEB origin (${err instanceof Error ? err.message : err}). S\u1EBD d\xF9ng tag local hi\u1EC7n c\xF3.`
|
|
4491
|
+
);
|
|
4492
|
+
}
|
|
4493
|
+
const targetVersion = opts.version ?? await latestTag(packDir);
|
|
4494
|
+
if (!targetVersion) {
|
|
4495
|
+
log.error(
|
|
4496
|
+
"Kh\xF4ng t\xECm th\u1EA5y tag n\xE0o trong team-ai-pack submodule. Pass --version <tag> r\xF5 r\xE0ng, ho\u1EB7c ki\u1EC3m tra repo c\xF3 tag \u0111\u01B0\u1EE3c kh\xF4ng."
|
|
4497
|
+
);
|
|
4498
|
+
process.exit(1);
|
|
4499
|
+
return;
|
|
4500
|
+
}
|
|
4501
|
+
if (opts.dryRun) {
|
|
4502
|
+
const preview = await buildSyncPreview(packDir, claudeDir, targetVersion);
|
|
4503
|
+
log.info(`Pack version hi\u1EC7n t\u1EA1i: ${preview.currentVersion}`);
|
|
4504
|
+
log.info(`Target version: ${preview.targetVersion}`);
|
|
4505
|
+
if (preview.commitsBehind.length === 0) {
|
|
4506
|
+
log.info("\u0110\xE3 \u1EDF target version, kh\xF4ng c\xF3 commit m\u1EDBi.");
|
|
4507
|
+
} else {
|
|
4508
|
+
log.info(`Commits c\u1EA7n pull (${preview.commitsBehind.length}):`);
|
|
4509
|
+
for (const c of preview.commitsBehind.slice(0, 20)) {
|
|
4510
|
+
console.log(` ${c}`);
|
|
4511
|
+
}
|
|
4512
|
+
if (preview.commitsBehind.length > 20) {
|
|
4513
|
+
console.log(` ... v\xE0 ${preview.commitsBehind.length - 20} commits kh\xE1c`);
|
|
4514
|
+
}
|
|
4515
|
+
}
|
|
4516
|
+
log.info("\nMount dir statuses:");
|
|
4517
|
+
for (const m of preview.mountDirStatuses) {
|
|
4518
|
+
console.log(` ${m.dir.padEnd(12)} ${m.status}`);
|
|
4519
|
+
}
|
|
4520
|
+
log.info("\nDry-run done. Kh\xF4ng apply thay \u0111\u1ED5i. B\u1ECF --dry-run \u0111\u1EC3 th\u1EF1c thi.");
|
|
4521
|
+
return;
|
|
4522
|
+
}
|
|
4523
|
+
log.info(`Checking out ${targetVersion} trong submodule...`);
|
|
4524
|
+
await checkoutTagInSubmodule(TEAM_PACK_RELATIVE_PATH, targetVersion, projectRoot);
|
|
4525
|
+
log.info("Creating symlink farm...");
|
|
4526
|
+
const results = await syncAllMountDirs(packDir, claudeDir, opts.force === true);
|
|
4527
|
+
reportResults(results, opts.force === true);
|
|
4528
|
+
log.info("Merging pack settings.json template into project settings.json...");
|
|
4529
|
+
try {
|
|
4530
|
+
const mergeResult = await mergePackSettingsIntoProjectSettings(projectRoot);
|
|
4531
|
+
switch (mergeResult.action) {
|
|
4532
|
+
case "merged":
|
|
4533
|
+
log.success(
|
|
4534
|
+
` \u2713 settings.json merged (${mergeResult.changes.join("; ")}). Backup: ${mergeResult.backupPath ?? "n/a"}`
|
|
4535
|
+
);
|
|
4536
|
+
break;
|
|
4537
|
+
case "no-change":
|
|
4538
|
+
log.info(" - settings.json \u0111\xE3 sync v\u1EDBi pack, kh\xF4ng c\xF3 thay \u0111\u1ED5i.");
|
|
4539
|
+
break;
|
|
4540
|
+
case "no-pack-template":
|
|
4541
|
+
log.dim(" - Pack kh\xF4ng c\xF3 templates/settings.json.tpl, skip merge.");
|
|
4542
|
+
break;
|
|
4543
|
+
}
|
|
4544
|
+
} catch (err) {
|
|
4545
|
+
log.warn(
|
|
4546
|
+
` ! Merge settings.json fail: ${err instanceof Error ? err.message : err}. Symlinks \u0111\xE3 t\u1EA1o OK, hooks c\xF3 th\u1EC3 ch\u01B0a active. Manual merge n\u1EBFu c\u1EA7n.`
|
|
4547
|
+
);
|
|
4548
|
+
}
|
|
4549
|
+
log.success(`Synced team-ai-pack to ${targetVersion}.`);
|
|
4550
|
+
}
|
|
4551
|
+
function reportResults(results, force) {
|
|
4552
|
+
for (const r of results) {
|
|
4553
|
+
switch (r.action) {
|
|
4554
|
+
case "created":
|
|
4555
|
+
log.info(` \u2713 ${r.dir} \u2192 symlinked (new)`);
|
|
4556
|
+
break;
|
|
4557
|
+
case "updated":
|
|
4558
|
+
log.info(` \u2713 ${r.dir} \u2192 symlink refreshed`);
|
|
4559
|
+
break;
|
|
4560
|
+
case "backed-up-and-linked":
|
|
4561
|
+
log.info(` \u2713 ${r.dir} \u2192 symlinked (backup: ${r.backupPath})`);
|
|
4562
|
+
break;
|
|
4563
|
+
case "source-missing":
|
|
4564
|
+
log.warn(` - ${r.dir} \u2192 pack kh\xF4ng c\xF3 dir n\xE0y, skip`);
|
|
4565
|
+
break;
|
|
4566
|
+
case "skipped-conflict":
|
|
4567
|
+
log.warn(
|
|
4568
|
+
` ! ${r.dir} \u2192 CONFLICT: existing real dir. D\xF9ng --force \u0111\u1EC3 backup + override (s\u1EBD gi\u1EEF data \u1EDF ${r.dir}.backup-<timestamp>/).`
|
|
4569
|
+
);
|
|
4570
|
+
break;
|
|
4571
|
+
}
|
|
4572
|
+
}
|
|
4573
|
+
const conflicts = results.filter((r) => r.action === "skipped-conflict").length;
|
|
4574
|
+
if (conflicts > 0 && !force) {
|
|
4575
|
+
log.warn(
|
|
4576
|
+
`${conflicts} mount dir(s) b\u1ECB skip do conflict. Ch\u1EA1y l\u1EA1i v\u1EDBi --force n\u1EBFu mu\u1ED1n override (backup t\u1EF1 \u0111\u1ED9ng).`
|
|
4577
|
+
);
|
|
4578
|
+
}
|
|
4579
|
+
}
|
|
4017
4580
|
function registerSyncCommand(program2) {
|
|
4018
|
-
program2.command("sync").description("Pull team-ai-pack m\u1EDBi nh\u1EA5t
|
|
4581
|
+
program2.command("sync").description("Pull team-ai-pack m\u1EDBi nh\u1EA5t + t\u1EA1o symlink farm v\xE0o .claude/").option("--force", "Override .claude/<dir>/ n\u1EBFu l\xE0 real dir (backup tr\u01B0\u1EDBc)").option("--version <tag>", "Pin v\xE0o version c\u1EE5 th\u1EC3 (vd: v0.2.0)").option("--dry-run", "Hi\u1EC3n th\u1ECB preview, kh\xF4ng apply thay \u0111\u1ED5i").action(syncAction);
|
|
4019
4582
|
}
|
|
4020
4583
|
|
|
4021
4584
|
// src/commands/tools.ts
|
|
@@ -4027,40 +4590,40 @@ function registerToolsCommand(program2) {
|
|
|
4027
4590
|
}
|
|
4028
4591
|
|
|
4029
4592
|
// src/commands/uninstall.ts
|
|
4030
|
-
import { relative as
|
|
4593
|
+
import { relative as relative4 } from "path";
|
|
4031
4594
|
import { confirm as confirm6 } from "@inquirer/prompts";
|
|
4032
4595
|
import boxen7 from "boxen";
|
|
4033
4596
|
|
|
4034
4597
|
// src/lib/create-uninstall-backup-snapshot.ts
|
|
4035
4598
|
import { cp, mkdir, writeFile } from "fs/promises";
|
|
4036
4599
|
import { homedir as homedir4 } from "os";
|
|
4037
|
-
import { basename as basename2, join as
|
|
4038
|
-
var UNINSTALL_BACKUPS_DIR =
|
|
4600
|
+
import { basename as basename2, join as join29 } from "path";
|
|
4601
|
+
var UNINSTALL_BACKUPS_DIR = join29(homedir4(), ".avatar", "uninstall-backups");
|
|
4039
4602
|
async function createUninstallBackupSnapshot(projectRoot, artifacts, avatarVersion) {
|
|
4040
4603
|
const projectName = basename2(projectRoot);
|
|
4041
|
-
const
|
|
4042
|
-
const backupDir =
|
|
4604
|
+
const timestamp2 = (/* @__PURE__ */ new Date()).toISOString().replace(/[:.]/g, "-");
|
|
4605
|
+
const backupDir = join29(UNINSTALL_BACKUPS_DIR, `${projectName}-${timestamp2}`);
|
|
4043
4606
|
await mkdir(backupDir, { recursive: true, mode: 448 });
|
|
4044
4607
|
if (artifacts.claudeDir) {
|
|
4045
|
-
await cp(artifacts.claudeDir,
|
|
4608
|
+
await cp(artifacts.claudeDir, join29(backupDir, ".claude"), { recursive: true });
|
|
4046
4609
|
}
|
|
4047
4610
|
if (artifacts.claudeMd) {
|
|
4048
|
-
await cp(artifacts.claudeMd,
|
|
4611
|
+
await cp(artifacts.claudeMd, join29(backupDir, "CLAUDE.md"));
|
|
4049
4612
|
}
|
|
4050
4613
|
if (artifacts.postMergeHook || artifacts.prePushHook) {
|
|
4051
|
-
const hooksBackupDir =
|
|
4614
|
+
const hooksBackupDir = join29(backupDir, "hooks");
|
|
4052
4615
|
await mkdir(hooksBackupDir, { recursive: true });
|
|
4053
4616
|
if (artifacts.postMergeHook) {
|
|
4054
|
-
await cp(artifacts.postMergeHook,
|
|
4617
|
+
await cp(artifacts.postMergeHook, join29(hooksBackupDir, "post-merge"));
|
|
4055
4618
|
}
|
|
4056
4619
|
if (artifacts.prePushHook) {
|
|
4057
|
-
await cp(artifacts.prePushHook,
|
|
4620
|
+
await cp(artifacts.prePushHook, join29(hooksBackupDir, "pre-push"));
|
|
4058
4621
|
}
|
|
4059
4622
|
}
|
|
4060
4623
|
const manifest = {
|
|
4061
4624
|
projectName,
|
|
4062
4625
|
projectPath: projectRoot,
|
|
4063
|
-
timestamp,
|
|
4626
|
+
timestamp: timestamp2,
|
|
4064
4627
|
avatarVersion,
|
|
4065
4628
|
artifacts: {
|
|
4066
4629
|
claudeDir: !!artifacts.claudeDir,
|
|
@@ -4069,27 +4632,27 @@ async function createUninstallBackupSnapshot(projectRoot, artifacts, avatarVersi
|
|
|
4069
4632
|
prePushHook: !!artifacts.prePushHook
|
|
4070
4633
|
}
|
|
4071
4634
|
};
|
|
4072
|
-
await writeFile(
|
|
4635
|
+
await writeFile(join29(backupDir, "manifest.json"), JSON.stringify(manifest, null, 2), "utf8");
|
|
4073
4636
|
return backupDir;
|
|
4074
4637
|
}
|
|
4075
4638
|
|
|
4076
4639
|
// src/lib/detect-avatar-project-artifacts.ts
|
|
4077
4640
|
import { existsSync as existsSync9 } from "fs";
|
|
4078
|
-
import { join as
|
|
4641
|
+
import { join as join30 } from "path";
|
|
4079
4642
|
function existsOrNull(path) {
|
|
4080
4643
|
return existsSync9(path) ? path : null;
|
|
4081
4644
|
}
|
|
4082
4645
|
function detectAvatarProjectArtifacts(projectRoot) {
|
|
4083
|
-
const claudeDir = existsOrNull(
|
|
4084
|
-
const claudeMd = existsOrNull(
|
|
4085
|
-
const postMergeHook = existsOrNull(
|
|
4646
|
+
const claudeDir = existsOrNull(join30(projectRoot, ".claude"));
|
|
4647
|
+
const claudeMd = existsOrNull(join30(projectRoot, "CLAUDE.md"));
|
|
4648
|
+
const postMergeHook = existsOrNull(join30(projectRoot, ".git", "hooks", "post-merge"));
|
|
4086
4649
|
const prePushHook = existsOrNull(
|
|
4087
|
-
|
|
4650
|
+
join30(projectRoot, ".git", "modules", "src", "hooks", "pre-push")
|
|
4088
4651
|
);
|
|
4089
|
-
const gitignorePath = existsOrNull(
|
|
4090
|
-
const gitmodulesPath = existsOrNull(
|
|
4091
|
-
const notesDir = existsOrNull(
|
|
4092
|
-
const scriptsDir = existsOrNull(
|
|
4652
|
+
const gitignorePath = existsOrNull(join30(projectRoot, ".gitignore"));
|
|
4653
|
+
const gitmodulesPath = existsOrNull(join30(projectRoot, ".gitmodules"));
|
|
4654
|
+
const notesDir = existsOrNull(join30(projectRoot, "notes"));
|
|
4655
|
+
const scriptsDir = existsOrNull(join30(projectRoot, "scripts"));
|
|
4093
4656
|
const hasAnyArtifact = !!(claudeDir || claudeMd || postMergeHook || prePushHook);
|
|
4094
4657
|
return {
|
|
4095
4658
|
hasAnyArtifact,
|
|
@@ -4110,11 +4673,11 @@ async function executeUninstallDeletion(artifacts, flags) {
|
|
|
4110
4673
|
if (artifacts.claudeDir) {
|
|
4111
4674
|
if (flags.keepSubmodule) {
|
|
4112
4675
|
const { readdir: readdir2 } = await import("fs/promises");
|
|
4113
|
-
const { join:
|
|
4676
|
+
const { join: join31 } = await import("path");
|
|
4114
4677
|
const entries = await readdir2(artifacts.claudeDir);
|
|
4115
4678
|
for (const entry of entries) {
|
|
4116
4679
|
if (entry === "pack") continue;
|
|
4117
|
-
await rm(
|
|
4680
|
+
await rm(join31(artifacts.claudeDir, entry), { recursive: true, force: true });
|
|
4118
4681
|
}
|
|
4119
4682
|
} else {
|
|
4120
4683
|
await rm(artifacts.claudeDir, { recursive: true, force: true });
|
|
@@ -4183,7 +4746,7 @@ async function removeSubmoduleEntry(gitmodulesPath, submodulePath) {
|
|
|
4183
4746
|
}
|
|
4184
4747
|
|
|
4185
4748
|
// src/commands/uninstall.ts
|
|
4186
|
-
var CLI_VERSION = "1.
|
|
4749
|
+
var CLI_VERSION = "1.6.0";
|
|
4187
4750
|
function registerUninstallCommand(program2) {
|
|
4188
4751
|
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) => {
|
|
4189
4752
|
try {
|
|
@@ -4233,7 +4796,7 @@ function printUninstallSummary(projectRoot, artifacts, opts) {
|
|
|
4233
4796
|
log.plain("");
|
|
4234
4797
|
log.plain("C\xE1c artifact s\u1EBD g\u1EE1:");
|
|
4235
4798
|
if (artifacts.claudeDir)
|
|
4236
|
-
log.plain(` ${chalk.red("\u2717")} ${
|
|
4799
|
+
log.plain(` ${chalk.red("\u2717")} ${relative4(projectRoot, artifacts.claudeDir) || ".claude/"}`);
|
|
4237
4800
|
if (artifacts.claudeMd) log.plain(` ${chalk.red("\u2717")} CLAUDE.md`);
|
|
4238
4801
|
if (artifacts.postMergeHook && !opts.keepHooks) {
|
|
4239
4802
|
log.plain(` ${chalk.red("\u2717")} .git/hooks/post-merge`);
|
|
@@ -4265,7 +4828,7 @@ function printUninstallSuccessBox(backupPath) {
|
|
|
4265
4828
|
}
|
|
4266
4829
|
|
|
4267
4830
|
// src/index.ts
|
|
4268
|
-
var CLI_VERSION2 = "1.
|
|
4831
|
+
var CLI_VERSION2 = "1.6.0";
|
|
4269
4832
|
var program = new Command();
|
|
4270
4833
|
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(
|
|
4271
4834
|
"beforeAll",
|