@nalvietnam/avatar-cli 1.6.1 → 1.6.3
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 +268 -234
- package/dist/index.js.map +1 -1
- package/dist/lib/print-welcome-screen.js +1 -1
- package/dist/lib/print-welcome-screen.js.map +1 -1
- package/package.json +1 -1
package/dist/index.js
CHANGED
|
@@ -1851,8 +1851,8 @@ import { existsSync as existsSync5 } from "fs";
|
|
|
1851
1851
|
import { join as join14 } from "path";
|
|
1852
1852
|
import { confirm as confirm2 } from "@inquirer/prompts";
|
|
1853
1853
|
var WIKI_TIMEOUT_MS = 15 * 60 * 1e3;
|
|
1854
|
-
var
|
|
1855
|
-
var
|
|
1854
|
+
var FALLBACK_LLMLITE_MODEL = "nal-claude";
|
|
1855
|
+
var FALLBACK_ANTHROPIC_MODEL = "claude-sonnet-4-5";
|
|
1856
1856
|
function normalizeAnthropicBaseUrl(rawBaseUrl) {
|
|
1857
1857
|
const cleaned = rawBaseUrl.replace(/\/+$/, "");
|
|
1858
1858
|
if (cleaned.endsWith("/v1")) return `${cleaned}/`;
|
|
@@ -1867,14 +1867,16 @@ async function readSettingsForWikiCredentials(workspacePath) {
|
|
|
1867
1867
|
const env = settings.env || {};
|
|
1868
1868
|
const baseUrl = typeof env.ANTHROPIC_BASE_URL === "string" ? env.ANTHROPIC_BASE_URL : null;
|
|
1869
1869
|
if (!baseUrl) return null;
|
|
1870
|
-
const
|
|
1870
|
+
const topLevelModel = typeof settings.model === "string" ? settings.model : "";
|
|
1871
|
+
const envModel = typeof env.ANTHROPIC_MODEL === "string" ? env.ANTHROPIC_MODEL : "";
|
|
1872
|
+
const userModel = topLevelModel.length > 0 ? topLevelModel : envModel;
|
|
1871
1873
|
const anthropicKey = typeof env.ANTHROPIC_API_KEY === "string" ? env.ANTHROPIC_API_KEY : null;
|
|
1872
1874
|
if (anthropicKey) {
|
|
1873
1875
|
return {
|
|
1874
1876
|
provider: "anthropic",
|
|
1875
1877
|
apiKey: anthropicKey,
|
|
1876
1878
|
baseUrl: normalizeAnthropicBaseUrl(baseUrl),
|
|
1877
|
-
model: userModel.length > 0 ? userModel :
|
|
1879
|
+
model: userModel.length > 0 ? userModel : FALLBACK_ANTHROPIC_MODEL
|
|
1878
1880
|
};
|
|
1879
1881
|
}
|
|
1880
1882
|
const llmliteToken = typeof env.ANTHROPIC_AUTH_TOKEN === "string" ? env.ANTHROPIC_AUTH_TOKEN : null;
|
|
@@ -1883,7 +1885,7 @@ async function readSettingsForWikiCredentials(workspacePath) {
|
|
|
1883
1885
|
provider: "llmlite",
|
|
1884
1886
|
apiKey: llmliteToken,
|
|
1885
1887
|
baseUrl,
|
|
1886
|
-
model: userModel.length > 0 ? userModel :
|
|
1888
|
+
model: userModel.length > 0 ? userModel : FALLBACK_LLMLITE_MODEL
|
|
1887
1889
|
};
|
|
1888
1890
|
}
|
|
1889
1891
|
return null;
|
|
@@ -2197,7 +2199,7 @@ function registerGitnexusCommand(program2) {
|
|
|
2197
2199
|
}
|
|
2198
2200
|
|
|
2199
2201
|
// src/commands/init.ts
|
|
2200
|
-
import { basename, join as
|
|
2202
|
+
import { basename, join as join24, relative as relative3, resolve } from "path";
|
|
2201
2203
|
import { confirm as confirm5, input as input5, select as select9 } from "@inquirer/prompts";
|
|
2202
2204
|
import boxen5 from "boxen";
|
|
2203
2205
|
|
|
@@ -3060,6 +3062,131 @@ function linkExistingRemoteToWorkspace(args) {
|
|
|
3060
3062
|
return { sshUrl, httpsUrl };
|
|
3061
3063
|
}
|
|
3062
3064
|
|
|
3065
|
+
// src/lib/merge-pack-settings-into-project-settings.ts
|
|
3066
|
+
import { promises as fs9 } from "fs";
|
|
3067
|
+
import { join as join17 } from "path";
|
|
3068
|
+
function backupFilename(originalPath) {
|
|
3069
|
+
const d = /* @__PURE__ */ new Date();
|
|
3070
|
+
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")}`;
|
|
3071
|
+
return `${originalPath}.backup-${stamp}`;
|
|
3072
|
+
}
|
|
3073
|
+
function unionDedupe(a, b) {
|
|
3074
|
+
const seen = /* @__PURE__ */ new Set();
|
|
3075
|
+
const out = [];
|
|
3076
|
+
for (const item of [...a, ...b]) {
|
|
3077
|
+
const key = typeof item === "string" ? item : JSON.stringify(item);
|
|
3078
|
+
if (!seen.has(key)) {
|
|
3079
|
+
seen.add(key);
|
|
3080
|
+
out.push(item);
|
|
3081
|
+
}
|
|
3082
|
+
}
|
|
3083
|
+
return out;
|
|
3084
|
+
}
|
|
3085
|
+
function mergeHooksPerEvent(packHooks, userHooks) {
|
|
3086
|
+
const touched = [];
|
|
3087
|
+
const merged = { ...userHooks };
|
|
3088
|
+
for (const [event, packEntries] of Object.entries(packHooks)) {
|
|
3089
|
+
const userEntries = userHooks[event] || [];
|
|
3090
|
+
const union = unionDedupe(userEntries, packEntries);
|
|
3091
|
+
if (union.length !== userEntries.length) {
|
|
3092
|
+
touched.push(event);
|
|
3093
|
+
}
|
|
3094
|
+
merged[event] = union;
|
|
3095
|
+
}
|
|
3096
|
+
return { merged, touchedEvents: touched };
|
|
3097
|
+
}
|
|
3098
|
+
async function mergePackSettingsIntoProjectSettings(workspacePath) {
|
|
3099
|
+
const packTemplatePath = join17(workspacePath, ".claude", "pack", "templates", "settings.json.tpl");
|
|
3100
|
+
const projectSettingsPath = join17(workspacePath, ".claude", "settings.json");
|
|
3101
|
+
if (!await pathExists(packTemplatePath)) {
|
|
3102
|
+
return { action: "no-pack-template", changes: [] };
|
|
3103
|
+
}
|
|
3104
|
+
let packTemplate;
|
|
3105
|
+
try {
|
|
3106
|
+
const raw = await readText(packTemplatePath);
|
|
3107
|
+
packTemplate = JSON.parse(raw);
|
|
3108
|
+
} catch (err) {
|
|
3109
|
+
throw new Error(
|
|
3110
|
+
`Pack settings template kh\xF4ng parse \u0111\u01B0\u1EE3c JSON: ${err.message}. Path: ${packTemplatePath}`
|
|
3111
|
+
);
|
|
3112
|
+
}
|
|
3113
|
+
let userSettings = {};
|
|
3114
|
+
let projectHasSettings = false;
|
|
3115
|
+
if (await pathExists(projectSettingsPath)) {
|
|
3116
|
+
projectHasSettings = true;
|
|
3117
|
+
try {
|
|
3118
|
+
userSettings = await readJson(projectSettingsPath);
|
|
3119
|
+
} catch (err) {
|
|
3120
|
+
throw new Error(
|
|
3121
|
+
`Project settings.json kh\xF4ng parse \u0111\u01B0\u1EE3c: ${err.message}. Manual fix tr\u01B0\u1EDBc khi sync.`
|
|
3122
|
+
);
|
|
3123
|
+
}
|
|
3124
|
+
}
|
|
3125
|
+
const changes = [];
|
|
3126
|
+
const merged = { ...userSettings };
|
|
3127
|
+
if (packTemplate.statusLine && !userSettings.statusLine) {
|
|
3128
|
+
merged.statusLine = packTemplate.statusLine;
|
|
3129
|
+
changes.push("statusLine added");
|
|
3130
|
+
}
|
|
3131
|
+
if (typeof packTemplate.includeCoAuthoredBy === "boolean" && typeof userSettings.includeCoAuthoredBy !== "boolean") {
|
|
3132
|
+
merged.includeCoAuthoredBy = packTemplate.includeCoAuthoredBy;
|
|
3133
|
+
changes.push("includeCoAuthoredBy added");
|
|
3134
|
+
}
|
|
3135
|
+
if (packTemplate.model && !userSettings.model) {
|
|
3136
|
+
merged.model = packTemplate.model;
|
|
3137
|
+
changes.push("model added");
|
|
3138
|
+
}
|
|
3139
|
+
if (packTemplate.env) {
|
|
3140
|
+
const mergedEnv = { ...userSettings.env || {} };
|
|
3141
|
+
let envChanged = false;
|
|
3142
|
+
for (const [k, v] of Object.entries(packTemplate.env)) {
|
|
3143
|
+
if (!(k in mergedEnv)) {
|
|
3144
|
+
mergedEnv[k] = v;
|
|
3145
|
+
envChanged = true;
|
|
3146
|
+
}
|
|
3147
|
+
}
|
|
3148
|
+
if (envChanged) {
|
|
3149
|
+
merged.env = mergedEnv;
|
|
3150
|
+
changes.push("env vars added from pack");
|
|
3151
|
+
}
|
|
3152
|
+
}
|
|
3153
|
+
if (packTemplate.permissions) {
|
|
3154
|
+
const userAllow = userSettings.permissions?.allow || [];
|
|
3155
|
+
const userDeny = userSettings.permissions?.deny || [];
|
|
3156
|
+
const packAllow = packTemplate.permissions.allow || [];
|
|
3157
|
+
const packDeny = packTemplate.permissions.deny || [];
|
|
3158
|
+
const mergedAllow = unionDedupe(userAllow, packAllow);
|
|
3159
|
+
const mergedDeny = unionDedupe(userDeny, packDeny);
|
|
3160
|
+
if (mergedAllow.length !== userAllow.length || mergedDeny.length !== userDeny.length) {
|
|
3161
|
+
merged.permissions = { allow: mergedAllow, deny: mergedDeny };
|
|
3162
|
+
changes.push(
|
|
3163
|
+
`permissions union (+${mergedAllow.length - userAllow.length} allow, +${mergedDeny.length - userDeny.length} deny)`
|
|
3164
|
+
);
|
|
3165
|
+
}
|
|
3166
|
+
}
|
|
3167
|
+
if (packTemplate.hooks) {
|
|
3168
|
+
const userHooks = userSettings.hooks || {};
|
|
3169
|
+
const { merged: mergedHooks, touchedEvents } = mergeHooksPerEvent(
|
|
3170
|
+
packTemplate.hooks,
|
|
3171
|
+
userHooks
|
|
3172
|
+
);
|
|
3173
|
+
if (touchedEvents.length > 0) {
|
|
3174
|
+
merged.hooks = mergedHooks;
|
|
3175
|
+
changes.push(`hooks added for events: ${touchedEvents.join(", ")}`);
|
|
3176
|
+
}
|
|
3177
|
+
}
|
|
3178
|
+
if (changes.length === 0) {
|
|
3179
|
+
return { action: "no-change", changes: [] };
|
|
3180
|
+
}
|
|
3181
|
+
let backupPath;
|
|
3182
|
+
if (projectHasSettings) {
|
|
3183
|
+
backupPath = backupFilename(projectSettingsPath);
|
|
3184
|
+
await fs9.copyFile(projectSettingsPath, backupPath);
|
|
3185
|
+
}
|
|
3186
|
+
await writeJsonAtomic(projectSettingsPath, merged);
|
|
3187
|
+
return { action: "merged", backupPath, changes };
|
|
3188
|
+
}
|
|
3189
|
+
|
|
3063
3190
|
// src/lib/safe-bootstrap-for-dirty-folder.ts
|
|
3064
3191
|
import { readdirSync } from "fs";
|
|
3065
3192
|
import { select as select8 } from "@inquirer/prompts";
|
|
@@ -3067,9 +3194,9 @@ import { simpleGit as simpleGit3 } from "simple-git";
|
|
|
3067
3194
|
|
|
3068
3195
|
// src/lib/check-folder-has-git.ts
|
|
3069
3196
|
import { existsSync as existsSync6, statSync } from "fs";
|
|
3070
|
-
import { join as
|
|
3197
|
+
import { join as join18 } from "path";
|
|
3071
3198
|
function checkFolderHasGit(folderPath) {
|
|
3072
|
-
const gitPath =
|
|
3199
|
+
const gitPath = join18(folderPath, ".git");
|
|
3073
3200
|
if (!existsSync6(gitPath)) return false;
|
|
3074
3201
|
const stat = statSync(gitPath);
|
|
3075
3202
|
return stat.isDirectory() || stat.isFile();
|
|
@@ -3101,7 +3228,7 @@ async function createInitialGitCommit(folderPath) {
|
|
|
3101
3228
|
|
|
3102
3229
|
// src/lib/detect-folder-tech-stack.ts
|
|
3103
3230
|
import { existsSync as existsSync7 } from "fs";
|
|
3104
|
-
import { join as
|
|
3231
|
+
import { join as join19 } from "path";
|
|
3105
3232
|
var SIGNATURES = {
|
|
3106
3233
|
node: ["package.json"],
|
|
3107
3234
|
python: ["pyproject.toml", "requirements.txt", "setup.py", "Pipfile"],
|
|
@@ -3113,7 +3240,7 @@ var SIGNATURES = {
|
|
|
3113
3240
|
function detectFolderTechStack(folderPath) {
|
|
3114
3241
|
const matched = [];
|
|
3115
3242
|
for (const [stack, files] of Object.entries(SIGNATURES)) {
|
|
3116
|
-
if (files.some((f) => existsSync7(
|
|
3243
|
+
if (files.some((f) => existsSync7(join19(folderPath, f)))) {
|
|
3117
3244
|
matched.push(stack);
|
|
3118
3245
|
}
|
|
3119
3246
|
}
|
|
@@ -3122,25 +3249,25 @@ function detectFolderTechStack(folderPath) {
|
|
|
3122
3249
|
|
|
3123
3250
|
// src/lib/gitignore-template-loader.ts
|
|
3124
3251
|
import { readFileSync as readFileSync3 } from "fs";
|
|
3125
|
-
import { dirname as dirname4, join as
|
|
3252
|
+
import { dirname as dirname4, join as join20 } from "path";
|
|
3126
3253
|
import { fileURLToPath as fileURLToPath2 } from "url";
|
|
3127
3254
|
var __dirname = dirname4(fileURLToPath2(import.meta.url));
|
|
3128
3255
|
var CANDIDATE_DIRS = [
|
|
3129
3256
|
// Bundled production: dist/index.js → __dirname = .../dist/, sibling dist/templates
|
|
3130
|
-
|
|
3257
|
+
join20(__dirname, "templates", "gitignore"),
|
|
3131
3258
|
// Legacy bundled: nếu file là dist/lib/*.js (sub-bundle), templates ở dist/templates
|
|
3132
|
-
|
|
3259
|
+
join20(__dirname, "..", "templates", "gitignore"),
|
|
3133
3260
|
// Dev mode (vitest/tsx run src/ trực tiếp): __dirname = src/lib/
|
|
3134
|
-
|
|
3261
|
+
join20(__dirname, "..", "..", "src", "templates", "gitignore"),
|
|
3135
3262
|
// npm-installed alt: __dirname = .../dist/ → package_root/src/templates
|
|
3136
|
-
|
|
3263
|
+
join20(__dirname, "..", "src", "templates", "gitignore")
|
|
3137
3264
|
];
|
|
3138
3265
|
var AVATAR_MARKER_START = "# === avatar ===";
|
|
3139
3266
|
var AVATAR_MARKER_END = "# === /avatar ===";
|
|
3140
3267
|
function readTemplate(stack) {
|
|
3141
3268
|
for (const dir of CANDIDATE_DIRS) {
|
|
3142
3269
|
try {
|
|
3143
|
-
return readFileSync3(
|
|
3270
|
+
return readFileSync3(join20(dir, `${stack}.txt`), "utf8");
|
|
3144
3271
|
} catch {
|
|
3145
3272
|
}
|
|
3146
3273
|
}
|
|
@@ -3155,9 +3282,9 @@ ${readTemplate(s).trim()}`);
|
|
|
3155
3282
|
|
|
3156
3283
|
// src/lib/write-or-merge-gitignore.ts
|
|
3157
3284
|
import { existsSync as existsSync8, readFileSync as readFileSync4, writeFileSync } from "fs";
|
|
3158
|
-
import { join as
|
|
3285
|
+
import { join as join21 } from "path";
|
|
3159
3286
|
function writeOrMergeGitignore(folderPath, avatarBlock) {
|
|
3160
|
-
const path =
|
|
3287
|
+
const path = join21(folderPath, ".gitignore");
|
|
3161
3288
|
if (!existsSync8(path)) {
|
|
3162
3289
|
writeFileSync(path, avatarBlock, "utf8");
|
|
3163
3290
|
return;
|
|
@@ -3332,9 +3459,75 @@ async function safeBootstrapGitInFolder(folderPath, opts = {}) {
|
|
|
3332
3459
|
await appendAuditEntry("bootstrap", `state=${state},strategy=${strategy}`);
|
|
3333
3460
|
}
|
|
3334
3461
|
|
|
3462
|
+
// src/lib/symlink-farm-for-team-pack-mount-dirs.ts
|
|
3463
|
+
import { promises as fs11 } from "fs";
|
|
3464
|
+
import { dirname as dirname5, join as join22, relative as relative2 } from "path";
|
|
3465
|
+
|
|
3466
|
+
// src/lib/backup-existing-dir-before-symlink-override.ts
|
|
3467
|
+
import { promises as fs10 } from "fs";
|
|
3468
|
+
function timestamp() {
|
|
3469
|
+
const d = /* @__PURE__ */ new Date();
|
|
3470
|
+
const pad = (n) => n.toString().padStart(2, "0");
|
|
3471
|
+
return `${d.getFullYear()}-${pad(d.getMonth() + 1)}-${pad(d.getDate())}-${pad(d.getHours())}${pad(d.getMinutes())}`;
|
|
3472
|
+
}
|
|
3473
|
+
async function backupDirBeforeReplace(targetPath) {
|
|
3474
|
+
const backupPath = `${targetPath}.backup-${timestamp()}`;
|
|
3475
|
+
await fs10.rename(targetPath, backupPath);
|
|
3476
|
+
return backupPath;
|
|
3477
|
+
}
|
|
3478
|
+
|
|
3479
|
+
// src/lib/symlink-farm-for-team-pack-mount-dirs.ts
|
|
3480
|
+
var TEAM_PACK_MOUNT_DIRS = [
|
|
3481
|
+
"skills",
|
|
3482
|
+
"agents",
|
|
3483
|
+
"commands",
|
|
3484
|
+
"hooks",
|
|
3485
|
+
"workflows",
|
|
3486
|
+
"scripts",
|
|
3487
|
+
"knowledge"
|
|
3488
|
+
];
|
|
3489
|
+
async function isSymbolicLink(path) {
|
|
3490
|
+
try {
|
|
3491
|
+
const st = await fs11.lstat(path);
|
|
3492
|
+
return st.isSymbolicLink();
|
|
3493
|
+
} catch {
|
|
3494
|
+
return false;
|
|
3495
|
+
}
|
|
3496
|
+
}
|
|
3497
|
+
async function syncMountedDir(source, dest, force) {
|
|
3498
|
+
const dir = relative2(dirname5(dest), dest) || dest;
|
|
3499
|
+
if (!await pathExists(source)) {
|
|
3500
|
+
return { dir, action: "source-missing" };
|
|
3501
|
+
}
|
|
3502
|
+
if (await pathExists(dest)) {
|
|
3503
|
+
if (await isSymbolicLink(dest)) {
|
|
3504
|
+
await fs11.unlink(dest);
|
|
3505
|
+
} else if (force) {
|
|
3506
|
+
const backupPath = await backupDirBeforeReplace(dest);
|
|
3507
|
+
const relativeSource2 = relative2(dirname5(dest), source);
|
|
3508
|
+
await fs11.symlink(relativeSource2, dest);
|
|
3509
|
+
return { dir, action: "backed-up-and-linked", backupPath };
|
|
3510
|
+
} else {
|
|
3511
|
+
return { dir, action: "skipped-conflict" };
|
|
3512
|
+
}
|
|
3513
|
+
}
|
|
3514
|
+
const relativeSource = relative2(dirname5(dest), source);
|
|
3515
|
+
await fs11.symlink(relativeSource, dest);
|
|
3516
|
+
return { dir, action: "created" };
|
|
3517
|
+
}
|
|
3518
|
+
async function syncAllMountDirs(packDir, claudeDir, force) {
|
|
3519
|
+
const results = [];
|
|
3520
|
+
for (const dir of TEAM_PACK_MOUNT_DIRS) {
|
|
3521
|
+
const source = join22(packDir, dir);
|
|
3522
|
+
const dest = join22(claudeDir, dir);
|
|
3523
|
+
results.push(await syncMountedDir(source, dest, force));
|
|
3524
|
+
}
|
|
3525
|
+
return results;
|
|
3526
|
+
}
|
|
3527
|
+
|
|
3335
3528
|
// src/commands/init-conflict-detection-helpers.ts
|
|
3336
3529
|
import { readdir } from "fs/promises";
|
|
3337
|
-
import { join as
|
|
3530
|
+
import { join as join23 } from "path";
|
|
3338
3531
|
async function isEmptyOrMissing(path) {
|
|
3339
3532
|
if (!await pathExists(path)) return true;
|
|
3340
3533
|
try {
|
|
@@ -3347,7 +3540,7 @@ async function isEmptyOrMissing(path) {
|
|
|
3347
3540
|
}
|
|
3348
3541
|
async function findAlternativeWorkspaceName(parent, desiredName, maxAttempts = 10) {
|
|
3349
3542
|
for (let i = 2; i < maxAttempts; i++) {
|
|
3350
|
-
const candidate =
|
|
3543
|
+
const candidate = join23(parent, `${desiredName}-${i}`);
|
|
3351
3544
|
if (await isEmptyOrMissing(candidate)) return candidate;
|
|
3352
3545
|
}
|
|
3353
3546
|
return null;
|
|
@@ -3777,7 +3970,7 @@ async function runInitFromScratch(opts, ownerEmail) {
|
|
|
3777
3970
|
const teamOwner = opts.teamOwner ?? await promptTeamOwner(ownerEmail);
|
|
3778
3971
|
const workspaceParent = resolve(opts.workspaceParent ?? ".");
|
|
3779
3972
|
const workspacePath = await resolveWorkspacePath(workspaceParent, projectName, opts.force);
|
|
3780
|
-
const srcPath =
|
|
3973
|
+
const srcPath = join24(workspacePath, "src");
|
|
3781
3974
|
await ensureDir(workspacePath);
|
|
3782
3975
|
await ensureDir(srcPath);
|
|
3783
3976
|
await safeBootstrapGitInFolder(srcPath, { autoYes: true });
|
|
@@ -3915,11 +4108,12 @@ async function finalizeWorkspaceScaffold(args) {
|
|
|
3915
4108
|
await writeRootClaudeMd(args.workspacePath, vars);
|
|
3916
4109
|
await writeProjectSettings(args.workspacePath, vars);
|
|
3917
4110
|
await appendGitignoreEntries(args.workspacePath);
|
|
3918
|
-
await ensureDir(
|
|
3919
|
-
await ensureDir(
|
|
3920
|
-
await installGitHook(
|
|
3921
|
-
await installGitHook(
|
|
4111
|
+
await ensureDir(join24(args.workspacePath, "notes"));
|
|
4112
|
+
await ensureDir(join24(args.workspacePath, "scripts"));
|
|
4113
|
+
await installGitHook(join24(args.workspacePath, ".git"), "post-merge");
|
|
4114
|
+
await installGitHook(join24(args.workspacePath, ".git", "modules", "src"), "pre-push");
|
|
3922
4115
|
log.success("C\xE0i post-merge (workspace) + pre-push (src/)");
|
|
4116
|
+
await autoSyncPackOnInit(args.workspacePath);
|
|
3923
4117
|
await appendAuditEntry("init", `flow=${args.flow},workspace=${args.workspaceName}`);
|
|
3924
4118
|
await maybeCommitWorkspace(args.workspacePath, args.skipCommit);
|
|
3925
4119
|
await maybeCreateWorkspaceRemote(args);
|
|
@@ -3954,6 +4148,39 @@ async function finalizeWorkspaceScaffold(args) {
|
|
|
3954
4148
|
}
|
|
3955
4149
|
printInitSuccessBox(args.workspacePath, args.flow, aiResult, gitnexusResult);
|
|
3956
4150
|
}
|
|
4151
|
+
async function autoSyncPackOnInit(workspacePath) {
|
|
4152
|
+
const packDir = join24(workspacePath, TEAM_PACK_RELATIVE_PATH);
|
|
4153
|
+
if (!await pathExists(packDir)) {
|
|
4154
|
+
log.dim("Pack submodule kh\xF4ng t\u1ED3n t\u1EA1i (skip auto-sync). C\xF3 th\u1EC3 ch\u1EA1y `avatar sync` sau.");
|
|
4155
|
+
return;
|
|
4156
|
+
}
|
|
4157
|
+
const claudeDir = join24(workspacePath, ".claude");
|
|
4158
|
+
log.info("Auto-sync pack content v\xE0o .claude/ (symlinks + settings merge)...");
|
|
4159
|
+
try {
|
|
4160
|
+
const results = await syncAllMountDirs(packDir, claudeDir, false);
|
|
4161
|
+
const created = results.filter((r) => r.action === "created" || r.action === "updated").length;
|
|
4162
|
+
const missing = results.filter((r) => r.action === "source-missing").length;
|
|
4163
|
+
log.success(
|
|
4164
|
+
` \u2713 Symlinks: ${created} created${missing > 0 ? `, ${missing} source-missing (pack thi\u1EBFu dir)` : ""}`
|
|
4165
|
+
);
|
|
4166
|
+
const mergeResult = await mergePackSettingsIntoProjectSettings(workspacePath);
|
|
4167
|
+
switch (mergeResult.action) {
|
|
4168
|
+
case "merged":
|
|
4169
|
+
log.success(` \u2713 settings.json merged (${mergeResult.changes.join("; ")})`);
|
|
4170
|
+
break;
|
|
4171
|
+
case "no-change":
|
|
4172
|
+
log.dim(" - settings.json \u0111\xE3 sync, kh\xF4ng c\u1EA7n thay \u0111\u1ED5i.");
|
|
4173
|
+
break;
|
|
4174
|
+
case "no-pack-template":
|
|
4175
|
+
log.dim(" - Pack kh\xF4ng c\xF3 templates/settings.json.tpl, skip merge.");
|
|
4176
|
+
break;
|
|
4177
|
+
}
|
|
4178
|
+
} catch (err) {
|
|
4179
|
+
log.warn(
|
|
4180
|
+
`Auto-sync pack fail: ${err instanceof Error ? err.message : err}. Ch\u1EA1y \`avatar sync\` th\u1EE7 c\xF4ng \u0111\u1EC3 retry.`
|
|
4181
|
+
);
|
|
4182
|
+
}
|
|
4183
|
+
}
|
|
3957
4184
|
async function maybeCreateWorkspaceRemote(args) {
|
|
3958
4185
|
if (args.skipCommit) {
|
|
3959
4186
|
log.dim("Skip workspace remote (ch\u01B0a commit). Setup sau qua: gh repo create ...");
|
|
@@ -4041,7 +4268,7 @@ async function maybeCreateWorkspaceRemote(args) {
|
|
|
4041
4268
|
}
|
|
4042
4269
|
}
|
|
4043
4270
|
async function resolveWorkspacePath(parent, desiredName, force) {
|
|
4044
|
-
const desired =
|
|
4271
|
+
const desired = join24(parent, desiredName);
|
|
4045
4272
|
if (await isEmptyOrMissing(desired)) return desired;
|
|
4046
4273
|
log.warn(`Workspace path "${desired}" \u0111\xE3 c\xF3 n\u1ED9i dung.`);
|
|
4047
4274
|
while (true) {
|
|
@@ -4072,7 +4299,7 @@ async function resolveWorkspacePath(parent, desiredName, force) {
|
|
|
4072
4299
|
message: "T\xEAn workspace m\u1EDBi:",
|
|
4073
4300
|
validate: (v) => v.trim().length > 0 ? true : "T\xEAn kh\xF4ng \u0111\u01B0\u1EE3c r\u1ED7ng"
|
|
4074
4301
|
});
|
|
4075
|
-
const newPath =
|
|
4302
|
+
const newPath = join24(parent, newName.trim());
|
|
4076
4303
|
if (await isEmptyOrMissing(newPath)) return newPath;
|
|
4077
4304
|
log.warn(`"${newPath}" c\u0169ng \u0111\xE3 c\xF3 n\u1ED9i dung. Th\u1EED t\xEAn kh\xE1c.`);
|
|
4078
4305
|
}
|
|
@@ -4115,7 +4342,7 @@ function formatGitnexusStatusLine(result) {
|
|
|
4115
4342
|
}
|
|
4116
4343
|
function printInitSuccessBox(rootPath, flow, aiResult = null, gitnexusResult = null) {
|
|
4117
4344
|
const lines = [
|
|
4118
|
-
`${chalk.green("\u2713")} Workspace s\u1EB5n s\xE0ng: ${
|
|
4345
|
+
`${chalk.green("\u2713")} Workspace s\u1EB5n s\xE0ng: ${relative3(process.cwd(), rootPath) || rootPath}`,
|
|
4119
4346
|
` ${chalk.dim(`(flow: ${flow})`)}`,
|
|
4120
4347
|
formatAiStatusLine(aiResult),
|
|
4121
4348
|
formatGitnexusStatusLine(gitnexusResult),
|
|
@@ -4178,18 +4405,18 @@ function registerSecretsCommand(program2) {
|
|
|
4178
4405
|
}
|
|
4179
4406
|
|
|
4180
4407
|
// src/commands/status.ts
|
|
4181
|
-
import { promises as
|
|
4182
|
-
import { join as
|
|
4408
|
+
import { promises as fs13 } from "fs";
|
|
4409
|
+
import { join as join26 } from "path";
|
|
4183
4410
|
import boxen6 from "boxen";
|
|
4184
4411
|
|
|
4185
4412
|
// src/lib/pack-backup-manager.ts
|
|
4186
|
-
import { promises as
|
|
4187
|
-
import { join as
|
|
4413
|
+
import { promises as fs12 } from "fs";
|
|
4414
|
+
import { join as join25 } from "path";
|
|
4188
4415
|
var BACKUP_DIR_NAME = "_backup";
|
|
4189
4416
|
async function listBackups(projectRoot) {
|
|
4190
|
-
const dir =
|
|
4417
|
+
const dir = join25(projectRoot, ".claude", BACKUP_DIR_NAME);
|
|
4191
4418
|
if (!await pathExists(dir)) return [];
|
|
4192
|
-
const entries = await
|
|
4419
|
+
const entries = await fs12.readdir(dir, { withFileTypes: true });
|
|
4193
4420
|
return entries.filter((e) => e.isDirectory()).map((e) => e.name).sort().reverse();
|
|
4194
4421
|
}
|
|
4195
4422
|
|
|
@@ -4213,7 +4440,7 @@ function registerStatusCommand(program2) {
|
|
|
4213
4440
|
}
|
|
4214
4441
|
async function gatherStatus(cwd) {
|
|
4215
4442
|
const projectName = cwd.split("/").filter(Boolean).pop() ?? "unknown";
|
|
4216
|
-
const claudeRoot =
|
|
4443
|
+
const claudeRoot = join26(cwd, ".claude");
|
|
4217
4444
|
const hasAvatar = await pathExists(claudeRoot);
|
|
4218
4445
|
if (!hasAvatar) {
|
|
4219
4446
|
return {
|
|
@@ -4226,9 +4453,9 @@ async function gatherStatus(cwd) {
|
|
|
4226
4453
|
hasAvatar: false
|
|
4227
4454
|
};
|
|
4228
4455
|
}
|
|
4229
|
-
const packVersion = await isGitRepo(
|
|
4230
|
-
const pendingDir =
|
|
4231
|
-
const pendingCount = await pathExists(pendingDir) ? (await
|
|
4456
|
+
const packVersion = await isGitRepo(join26(claudeRoot, "pack")) ? await readPinnedPackVersion(cwd).catch(() => null) : null;
|
|
4457
|
+
const pendingDir = join26(claudeRoot, "_pending");
|
|
4458
|
+
const pendingCount = await pathExists(pendingDir) ? (await fs13.readdir(pendingDir)).filter((n) => n.endsWith(".diff.md")).length : 0;
|
|
4232
4459
|
const backupCount = (await listBackups(cwd)).length;
|
|
4233
4460
|
const techStackSummary = await readTechStackFirstLine(claudeRoot);
|
|
4234
4461
|
return {
|
|
@@ -4242,7 +4469,7 @@ async function gatherStatus(cwd) {
|
|
|
4242
4469
|
};
|
|
4243
4470
|
}
|
|
4244
4471
|
async function readTechStackFirstLine(claudeRoot) {
|
|
4245
|
-
const techStackPath =
|
|
4472
|
+
const techStackPath = join26(claudeRoot, "project", "tech-stack.md");
|
|
4246
4473
|
if (!await pathExists(techStackPath)) return "(no tech-stack.md)";
|
|
4247
4474
|
const content = await readText(techStackPath);
|
|
4248
4475
|
const firstNonHeaderLine = content.split("\n").find((l) => l.trim() && !l.startsWith("#") && !l.startsWith(">"));
|
|
@@ -4265,201 +4492,8 @@ function renderStatusBox(s) {
|
|
|
4265
4492
|
// src/commands/sync.ts
|
|
4266
4493
|
import { join as join28 } from "path";
|
|
4267
4494
|
|
|
4268
|
-
// src/lib/merge-pack-settings-into-project-settings.ts
|
|
4269
|
-
import { promises as fs11 } from "fs";
|
|
4270
|
-
import { join as join25 } from "path";
|
|
4271
|
-
function backupFilename(originalPath) {
|
|
4272
|
-
const d = /* @__PURE__ */ new Date();
|
|
4273
|
-
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")}`;
|
|
4274
|
-
return `${originalPath}.backup-${stamp}`;
|
|
4275
|
-
}
|
|
4276
|
-
function unionDedupe(a, b) {
|
|
4277
|
-
const seen = /* @__PURE__ */ new Set();
|
|
4278
|
-
const out = [];
|
|
4279
|
-
for (const item of [...a, ...b]) {
|
|
4280
|
-
const key = typeof item === "string" ? item : JSON.stringify(item);
|
|
4281
|
-
if (!seen.has(key)) {
|
|
4282
|
-
seen.add(key);
|
|
4283
|
-
out.push(item);
|
|
4284
|
-
}
|
|
4285
|
-
}
|
|
4286
|
-
return out;
|
|
4287
|
-
}
|
|
4288
|
-
function mergeHooksPerEvent(packHooks, userHooks) {
|
|
4289
|
-
const touched = [];
|
|
4290
|
-
const merged = { ...userHooks };
|
|
4291
|
-
for (const [event, packEntries] of Object.entries(packHooks)) {
|
|
4292
|
-
const userEntries = userHooks[event] || [];
|
|
4293
|
-
const union = unionDedupe(userEntries, packEntries);
|
|
4294
|
-
if (union.length !== userEntries.length) {
|
|
4295
|
-
touched.push(event);
|
|
4296
|
-
}
|
|
4297
|
-
merged[event] = union;
|
|
4298
|
-
}
|
|
4299
|
-
return { merged, touchedEvents: touched };
|
|
4300
|
-
}
|
|
4301
|
-
async function mergePackSettingsIntoProjectSettings(workspacePath) {
|
|
4302
|
-
const packTemplatePath = join25(workspacePath, ".claude", "pack", "templates", "settings.json.tpl");
|
|
4303
|
-
const projectSettingsPath = join25(workspacePath, ".claude", "settings.json");
|
|
4304
|
-
if (!await pathExists(packTemplatePath)) {
|
|
4305
|
-
return { action: "no-pack-template", changes: [] };
|
|
4306
|
-
}
|
|
4307
|
-
let packTemplate;
|
|
4308
|
-
try {
|
|
4309
|
-
const raw = await readText(packTemplatePath);
|
|
4310
|
-
packTemplate = JSON.parse(raw);
|
|
4311
|
-
} catch (err) {
|
|
4312
|
-
throw new Error(
|
|
4313
|
-
`Pack settings template kh\xF4ng parse \u0111\u01B0\u1EE3c JSON: ${err.message}. Path: ${packTemplatePath}`
|
|
4314
|
-
);
|
|
4315
|
-
}
|
|
4316
|
-
let userSettings = {};
|
|
4317
|
-
let projectHasSettings = false;
|
|
4318
|
-
if (await pathExists(projectSettingsPath)) {
|
|
4319
|
-
projectHasSettings = true;
|
|
4320
|
-
try {
|
|
4321
|
-
userSettings = await readJson(projectSettingsPath);
|
|
4322
|
-
} catch (err) {
|
|
4323
|
-
throw new Error(
|
|
4324
|
-
`Project settings.json kh\xF4ng parse \u0111\u01B0\u1EE3c: ${err.message}. Manual fix tr\u01B0\u1EDBc khi sync.`
|
|
4325
|
-
);
|
|
4326
|
-
}
|
|
4327
|
-
}
|
|
4328
|
-
const changes = [];
|
|
4329
|
-
const merged = { ...userSettings };
|
|
4330
|
-
if (packTemplate.statusLine && !userSettings.statusLine) {
|
|
4331
|
-
merged.statusLine = packTemplate.statusLine;
|
|
4332
|
-
changes.push("statusLine added");
|
|
4333
|
-
}
|
|
4334
|
-
if (typeof packTemplate.includeCoAuthoredBy === "boolean" && typeof userSettings.includeCoAuthoredBy !== "boolean") {
|
|
4335
|
-
merged.includeCoAuthoredBy = packTemplate.includeCoAuthoredBy;
|
|
4336
|
-
changes.push("includeCoAuthoredBy added");
|
|
4337
|
-
}
|
|
4338
|
-
if (packTemplate.model && !userSettings.model) {
|
|
4339
|
-
merged.model = packTemplate.model;
|
|
4340
|
-
changes.push("model added");
|
|
4341
|
-
}
|
|
4342
|
-
if (packTemplate.env) {
|
|
4343
|
-
const mergedEnv = { ...userSettings.env || {} };
|
|
4344
|
-
let envChanged = false;
|
|
4345
|
-
for (const [k, v] of Object.entries(packTemplate.env)) {
|
|
4346
|
-
if (!(k in mergedEnv)) {
|
|
4347
|
-
mergedEnv[k] = v;
|
|
4348
|
-
envChanged = true;
|
|
4349
|
-
}
|
|
4350
|
-
}
|
|
4351
|
-
if (envChanged) {
|
|
4352
|
-
merged.env = mergedEnv;
|
|
4353
|
-
changes.push("env vars added from pack");
|
|
4354
|
-
}
|
|
4355
|
-
}
|
|
4356
|
-
if (packTemplate.permissions) {
|
|
4357
|
-
const userAllow = userSettings.permissions?.allow || [];
|
|
4358
|
-
const userDeny = userSettings.permissions?.deny || [];
|
|
4359
|
-
const packAllow = packTemplate.permissions.allow || [];
|
|
4360
|
-
const packDeny = packTemplate.permissions.deny || [];
|
|
4361
|
-
const mergedAllow = unionDedupe(userAllow, packAllow);
|
|
4362
|
-
const mergedDeny = unionDedupe(userDeny, packDeny);
|
|
4363
|
-
if (mergedAllow.length !== userAllow.length || mergedDeny.length !== userDeny.length) {
|
|
4364
|
-
merged.permissions = { allow: mergedAllow, deny: mergedDeny };
|
|
4365
|
-
changes.push(
|
|
4366
|
-
`permissions union (+${mergedAllow.length - userAllow.length} allow, +${mergedDeny.length - userDeny.length} deny)`
|
|
4367
|
-
);
|
|
4368
|
-
}
|
|
4369
|
-
}
|
|
4370
|
-
if (packTemplate.hooks) {
|
|
4371
|
-
const userHooks = userSettings.hooks || {};
|
|
4372
|
-
const { merged: mergedHooks, touchedEvents } = mergeHooksPerEvent(
|
|
4373
|
-
packTemplate.hooks,
|
|
4374
|
-
userHooks
|
|
4375
|
-
);
|
|
4376
|
-
if (touchedEvents.length > 0) {
|
|
4377
|
-
merged.hooks = mergedHooks;
|
|
4378
|
-
changes.push(`hooks added for events: ${touchedEvents.join(", ")}`);
|
|
4379
|
-
}
|
|
4380
|
-
}
|
|
4381
|
-
if (changes.length === 0) {
|
|
4382
|
-
return { action: "no-change", changes: [] };
|
|
4383
|
-
}
|
|
4384
|
-
let backupPath;
|
|
4385
|
-
if (projectHasSettings) {
|
|
4386
|
-
backupPath = backupFilename(projectSettingsPath);
|
|
4387
|
-
await fs11.copyFile(projectSettingsPath, backupPath);
|
|
4388
|
-
}
|
|
4389
|
-
await writeJsonAtomic(projectSettingsPath, merged);
|
|
4390
|
-
return { action: "merged", backupPath, changes };
|
|
4391
|
-
}
|
|
4392
|
-
|
|
4393
4495
|
// src/lib/preview-team-pack-sync-changes-for-dry-run.ts
|
|
4394
4496
|
import { join as join27 } from "path";
|
|
4395
|
-
|
|
4396
|
-
// src/lib/symlink-farm-for-team-pack-mount-dirs.ts
|
|
4397
|
-
import { promises as fs13 } from "fs";
|
|
4398
|
-
import { dirname as dirname5, join as join26, relative as relative3 } from "path";
|
|
4399
|
-
|
|
4400
|
-
// src/lib/backup-existing-dir-before-symlink-override.ts
|
|
4401
|
-
import { promises as fs12 } from "fs";
|
|
4402
|
-
function timestamp() {
|
|
4403
|
-
const d = /* @__PURE__ */ new Date();
|
|
4404
|
-
const pad = (n) => n.toString().padStart(2, "0");
|
|
4405
|
-
return `${d.getFullYear()}-${pad(d.getMonth() + 1)}-${pad(d.getDate())}-${pad(d.getHours())}${pad(d.getMinutes())}`;
|
|
4406
|
-
}
|
|
4407
|
-
async function backupDirBeforeReplace(targetPath) {
|
|
4408
|
-
const backupPath = `${targetPath}.backup-${timestamp()}`;
|
|
4409
|
-
await fs12.rename(targetPath, backupPath);
|
|
4410
|
-
return backupPath;
|
|
4411
|
-
}
|
|
4412
|
-
|
|
4413
|
-
// src/lib/symlink-farm-for-team-pack-mount-dirs.ts
|
|
4414
|
-
var TEAM_PACK_MOUNT_DIRS = [
|
|
4415
|
-
"skills",
|
|
4416
|
-
"agents",
|
|
4417
|
-
"commands",
|
|
4418
|
-
"hooks",
|
|
4419
|
-
"workflows",
|
|
4420
|
-
"scripts",
|
|
4421
|
-
"knowledge"
|
|
4422
|
-
];
|
|
4423
|
-
async function isSymbolicLink(path) {
|
|
4424
|
-
try {
|
|
4425
|
-
const st = await fs13.lstat(path);
|
|
4426
|
-
return st.isSymbolicLink();
|
|
4427
|
-
} catch {
|
|
4428
|
-
return false;
|
|
4429
|
-
}
|
|
4430
|
-
}
|
|
4431
|
-
async function syncMountedDir(source, dest, force) {
|
|
4432
|
-
const dir = relative3(dirname5(dest), dest) || dest;
|
|
4433
|
-
if (!await pathExists(source)) {
|
|
4434
|
-
return { dir, action: "source-missing" };
|
|
4435
|
-
}
|
|
4436
|
-
if (await pathExists(dest)) {
|
|
4437
|
-
if (await isSymbolicLink(dest)) {
|
|
4438
|
-
await fs13.unlink(dest);
|
|
4439
|
-
} else if (force) {
|
|
4440
|
-
const backupPath = await backupDirBeforeReplace(dest);
|
|
4441
|
-
const relativeSource2 = relative3(dirname5(dest), source);
|
|
4442
|
-
await fs13.symlink(relativeSource2, dest);
|
|
4443
|
-
return { dir, action: "backed-up-and-linked", backupPath };
|
|
4444
|
-
} else {
|
|
4445
|
-
return { dir, action: "skipped-conflict" };
|
|
4446
|
-
}
|
|
4447
|
-
}
|
|
4448
|
-
const relativeSource = relative3(dirname5(dest), source);
|
|
4449
|
-
await fs13.symlink(relativeSource, dest);
|
|
4450
|
-
return { dir, action: "created" };
|
|
4451
|
-
}
|
|
4452
|
-
async function syncAllMountDirs(packDir, claudeDir, force) {
|
|
4453
|
-
const results = [];
|
|
4454
|
-
for (const dir of TEAM_PACK_MOUNT_DIRS) {
|
|
4455
|
-
const source = join26(packDir, dir);
|
|
4456
|
-
const dest = join26(claudeDir, dir);
|
|
4457
|
-
results.push(await syncMountedDir(source, dest, force));
|
|
4458
|
-
}
|
|
4459
|
-
return results;
|
|
4460
|
-
}
|
|
4461
|
-
|
|
4462
|
-
// src/lib/preview-team-pack-sync-changes-for-dry-run.ts
|
|
4463
4497
|
async function inspectMountDir(packDir, claudeDir, dir) {
|
|
4464
4498
|
const source = join27(packDir, dir);
|
|
4465
4499
|
const dest = join27(claudeDir, dir);
|
|
@@ -4774,7 +4808,7 @@ async function removeSubmoduleEntry(gitmodulesPath, submodulePath) {
|
|
|
4774
4808
|
}
|
|
4775
4809
|
|
|
4776
4810
|
// src/commands/uninstall.ts
|
|
4777
|
-
var CLI_VERSION = "1.6.
|
|
4811
|
+
var CLI_VERSION = "1.6.3";
|
|
4778
4812
|
function registerUninstallCommand(program2) {
|
|
4779
4813
|
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) => {
|
|
4780
4814
|
try {
|
|
@@ -4856,7 +4890,7 @@ function printUninstallSuccessBox(backupPath) {
|
|
|
4856
4890
|
}
|
|
4857
4891
|
|
|
4858
4892
|
// src/index.ts
|
|
4859
|
-
var CLI_VERSION2 = "1.6.
|
|
4893
|
+
var CLI_VERSION2 = "1.6.3";
|
|
4860
4894
|
var program = new Command();
|
|
4861
4895
|
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(
|
|
4862
4896
|
"beforeAll",
|