@nalvietnam/avatar-cli 1.6.1 → 1.6.2
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 +261 -229
- 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
|
@@ -2197,7 +2197,7 @@ function registerGitnexusCommand(program2) {
|
|
|
2197
2197
|
}
|
|
2198
2198
|
|
|
2199
2199
|
// src/commands/init.ts
|
|
2200
|
-
import { basename, join as
|
|
2200
|
+
import { basename, join as join24, relative as relative3, resolve } from "path";
|
|
2201
2201
|
import { confirm as confirm5, input as input5, select as select9 } from "@inquirer/prompts";
|
|
2202
2202
|
import boxen5 from "boxen";
|
|
2203
2203
|
|
|
@@ -3060,6 +3060,131 @@ function linkExistingRemoteToWorkspace(args) {
|
|
|
3060
3060
|
return { sshUrl, httpsUrl };
|
|
3061
3061
|
}
|
|
3062
3062
|
|
|
3063
|
+
// src/lib/merge-pack-settings-into-project-settings.ts
|
|
3064
|
+
import { promises as fs9 } from "fs";
|
|
3065
|
+
import { join as join17 } from "path";
|
|
3066
|
+
function backupFilename(originalPath) {
|
|
3067
|
+
const d = /* @__PURE__ */ new Date();
|
|
3068
|
+
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")}`;
|
|
3069
|
+
return `${originalPath}.backup-${stamp}`;
|
|
3070
|
+
}
|
|
3071
|
+
function unionDedupe(a, b) {
|
|
3072
|
+
const seen = /* @__PURE__ */ new Set();
|
|
3073
|
+
const out = [];
|
|
3074
|
+
for (const item of [...a, ...b]) {
|
|
3075
|
+
const key = typeof item === "string" ? item : JSON.stringify(item);
|
|
3076
|
+
if (!seen.has(key)) {
|
|
3077
|
+
seen.add(key);
|
|
3078
|
+
out.push(item);
|
|
3079
|
+
}
|
|
3080
|
+
}
|
|
3081
|
+
return out;
|
|
3082
|
+
}
|
|
3083
|
+
function mergeHooksPerEvent(packHooks, userHooks) {
|
|
3084
|
+
const touched = [];
|
|
3085
|
+
const merged = { ...userHooks };
|
|
3086
|
+
for (const [event, packEntries] of Object.entries(packHooks)) {
|
|
3087
|
+
const userEntries = userHooks[event] || [];
|
|
3088
|
+
const union = unionDedupe(userEntries, packEntries);
|
|
3089
|
+
if (union.length !== userEntries.length) {
|
|
3090
|
+
touched.push(event);
|
|
3091
|
+
}
|
|
3092
|
+
merged[event] = union;
|
|
3093
|
+
}
|
|
3094
|
+
return { merged, touchedEvents: touched };
|
|
3095
|
+
}
|
|
3096
|
+
async function mergePackSettingsIntoProjectSettings(workspacePath) {
|
|
3097
|
+
const packTemplatePath = join17(workspacePath, ".claude", "pack", "templates", "settings.json.tpl");
|
|
3098
|
+
const projectSettingsPath = join17(workspacePath, ".claude", "settings.json");
|
|
3099
|
+
if (!await pathExists(packTemplatePath)) {
|
|
3100
|
+
return { action: "no-pack-template", changes: [] };
|
|
3101
|
+
}
|
|
3102
|
+
let packTemplate;
|
|
3103
|
+
try {
|
|
3104
|
+
const raw = await readText(packTemplatePath);
|
|
3105
|
+
packTemplate = JSON.parse(raw);
|
|
3106
|
+
} catch (err) {
|
|
3107
|
+
throw new Error(
|
|
3108
|
+
`Pack settings template kh\xF4ng parse \u0111\u01B0\u1EE3c JSON: ${err.message}. Path: ${packTemplatePath}`
|
|
3109
|
+
);
|
|
3110
|
+
}
|
|
3111
|
+
let userSettings = {};
|
|
3112
|
+
let projectHasSettings = false;
|
|
3113
|
+
if (await pathExists(projectSettingsPath)) {
|
|
3114
|
+
projectHasSettings = true;
|
|
3115
|
+
try {
|
|
3116
|
+
userSettings = await readJson(projectSettingsPath);
|
|
3117
|
+
} catch (err) {
|
|
3118
|
+
throw new Error(
|
|
3119
|
+
`Project settings.json kh\xF4ng parse \u0111\u01B0\u1EE3c: ${err.message}. Manual fix tr\u01B0\u1EDBc khi sync.`
|
|
3120
|
+
);
|
|
3121
|
+
}
|
|
3122
|
+
}
|
|
3123
|
+
const changes = [];
|
|
3124
|
+
const merged = { ...userSettings };
|
|
3125
|
+
if (packTemplate.statusLine && !userSettings.statusLine) {
|
|
3126
|
+
merged.statusLine = packTemplate.statusLine;
|
|
3127
|
+
changes.push("statusLine added");
|
|
3128
|
+
}
|
|
3129
|
+
if (typeof packTemplate.includeCoAuthoredBy === "boolean" && typeof userSettings.includeCoAuthoredBy !== "boolean") {
|
|
3130
|
+
merged.includeCoAuthoredBy = packTemplate.includeCoAuthoredBy;
|
|
3131
|
+
changes.push("includeCoAuthoredBy added");
|
|
3132
|
+
}
|
|
3133
|
+
if (packTemplate.model && !userSettings.model) {
|
|
3134
|
+
merged.model = packTemplate.model;
|
|
3135
|
+
changes.push("model added");
|
|
3136
|
+
}
|
|
3137
|
+
if (packTemplate.env) {
|
|
3138
|
+
const mergedEnv = { ...userSettings.env || {} };
|
|
3139
|
+
let envChanged = false;
|
|
3140
|
+
for (const [k, v] of Object.entries(packTemplate.env)) {
|
|
3141
|
+
if (!(k in mergedEnv)) {
|
|
3142
|
+
mergedEnv[k] = v;
|
|
3143
|
+
envChanged = true;
|
|
3144
|
+
}
|
|
3145
|
+
}
|
|
3146
|
+
if (envChanged) {
|
|
3147
|
+
merged.env = mergedEnv;
|
|
3148
|
+
changes.push("env vars added from pack");
|
|
3149
|
+
}
|
|
3150
|
+
}
|
|
3151
|
+
if (packTemplate.permissions) {
|
|
3152
|
+
const userAllow = userSettings.permissions?.allow || [];
|
|
3153
|
+
const userDeny = userSettings.permissions?.deny || [];
|
|
3154
|
+
const packAllow = packTemplate.permissions.allow || [];
|
|
3155
|
+
const packDeny = packTemplate.permissions.deny || [];
|
|
3156
|
+
const mergedAllow = unionDedupe(userAllow, packAllow);
|
|
3157
|
+
const mergedDeny = unionDedupe(userDeny, packDeny);
|
|
3158
|
+
if (mergedAllow.length !== userAllow.length || mergedDeny.length !== userDeny.length) {
|
|
3159
|
+
merged.permissions = { allow: mergedAllow, deny: mergedDeny };
|
|
3160
|
+
changes.push(
|
|
3161
|
+
`permissions union (+${mergedAllow.length - userAllow.length} allow, +${mergedDeny.length - userDeny.length} deny)`
|
|
3162
|
+
);
|
|
3163
|
+
}
|
|
3164
|
+
}
|
|
3165
|
+
if (packTemplate.hooks) {
|
|
3166
|
+
const userHooks = userSettings.hooks || {};
|
|
3167
|
+
const { merged: mergedHooks, touchedEvents } = mergeHooksPerEvent(
|
|
3168
|
+
packTemplate.hooks,
|
|
3169
|
+
userHooks
|
|
3170
|
+
);
|
|
3171
|
+
if (touchedEvents.length > 0) {
|
|
3172
|
+
merged.hooks = mergedHooks;
|
|
3173
|
+
changes.push(`hooks added for events: ${touchedEvents.join(", ")}`);
|
|
3174
|
+
}
|
|
3175
|
+
}
|
|
3176
|
+
if (changes.length === 0) {
|
|
3177
|
+
return { action: "no-change", changes: [] };
|
|
3178
|
+
}
|
|
3179
|
+
let backupPath;
|
|
3180
|
+
if (projectHasSettings) {
|
|
3181
|
+
backupPath = backupFilename(projectSettingsPath);
|
|
3182
|
+
await fs9.copyFile(projectSettingsPath, backupPath);
|
|
3183
|
+
}
|
|
3184
|
+
await writeJsonAtomic(projectSettingsPath, merged);
|
|
3185
|
+
return { action: "merged", backupPath, changes };
|
|
3186
|
+
}
|
|
3187
|
+
|
|
3063
3188
|
// src/lib/safe-bootstrap-for-dirty-folder.ts
|
|
3064
3189
|
import { readdirSync } from "fs";
|
|
3065
3190
|
import { select as select8 } from "@inquirer/prompts";
|
|
@@ -3067,9 +3192,9 @@ import { simpleGit as simpleGit3 } from "simple-git";
|
|
|
3067
3192
|
|
|
3068
3193
|
// src/lib/check-folder-has-git.ts
|
|
3069
3194
|
import { existsSync as existsSync6, statSync } from "fs";
|
|
3070
|
-
import { join as
|
|
3195
|
+
import { join as join18 } from "path";
|
|
3071
3196
|
function checkFolderHasGit(folderPath) {
|
|
3072
|
-
const gitPath =
|
|
3197
|
+
const gitPath = join18(folderPath, ".git");
|
|
3073
3198
|
if (!existsSync6(gitPath)) return false;
|
|
3074
3199
|
const stat = statSync(gitPath);
|
|
3075
3200
|
return stat.isDirectory() || stat.isFile();
|
|
@@ -3101,7 +3226,7 @@ async function createInitialGitCommit(folderPath) {
|
|
|
3101
3226
|
|
|
3102
3227
|
// src/lib/detect-folder-tech-stack.ts
|
|
3103
3228
|
import { existsSync as existsSync7 } from "fs";
|
|
3104
|
-
import { join as
|
|
3229
|
+
import { join as join19 } from "path";
|
|
3105
3230
|
var SIGNATURES = {
|
|
3106
3231
|
node: ["package.json"],
|
|
3107
3232
|
python: ["pyproject.toml", "requirements.txt", "setup.py", "Pipfile"],
|
|
@@ -3113,7 +3238,7 @@ var SIGNATURES = {
|
|
|
3113
3238
|
function detectFolderTechStack(folderPath) {
|
|
3114
3239
|
const matched = [];
|
|
3115
3240
|
for (const [stack, files] of Object.entries(SIGNATURES)) {
|
|
3116
|
-
if (files.some((f) => existsSync7(
|
|
3241
|
+
if (files.some((f) => existsSync7(join19(folderPath, f)))) {
|
|
3117
3242
|
matched.push(stack);
|
|
3118
3243
|
}
|
|
3119
3244
|
}
|
|
@@ -3122,25 +3247,25 @@ function detectFolderTechStack(folderPath) {
|
|
|
3122
3247
|
|
|
3123
3248
|
// src/lib/gitignore-template-loader.ts
|
|
3124
3249
|
import { readFileSync as readFileSync3 } from "fs";
|
|
3125
|
-
import { dirname as dirname4, join as
|
|
3250
|
+
import { dirname as dirname4, join as join20 } from "path";
|
|
3126
3251
|
import { fileURLToPath as fileURLToPath2 } from "url";
|
|
3127
3252
|
var __dirname = dirname4(fileURLToPath2(import.meta.url));
|
|
3128
3253
|
var CANDIDATE_DIRS = [
|
|
3129
3254
|
// Bundled production: dist/index.js → __dirname = .../dist/, sibling dist/templates
|
|
3130
|
-
|
|
3255
|
+
join20(__dirname, "templates", "gitignore"),
|
|
3131
3256
|
// Legacy bundled: nếu file là dist/lib/*.js (sub-bundle), templates ở dist/templates
|
|
3132
|
-
|
|
3257
|
+
join20(__dirname, "..", "templates", "gitignore"),
|
|
3133
3258
|
// Dev mode (vitest/tsx run src/ trực tiếp): __dirname = src/lib/
|
|
3134
|
-
|
|
3259
|
+
join20(__dirname, "..", "..", "src", "templates", "gitignore"),
|
|
3135
3260
|
// npm-installed alt: __dirname = .../dist/ → package_root/src/templates
|
|
3136
|
-
|
|
3261
|
+
join20(__dirname, "..", "src", "templates", "gitignore")
|
|
3137
3262
|
];
|
|
3138
3263
|
var AVATAR_MARKER_START = "# === avatar ===";
|
|
3139
3264
|
var AVATAR_MARKER_END = "# === /avatar ===";
|
|
3140
3265
|
function readTemplate(stack) {
|
|
3141
3266
|
for (const dir of CANDIDATE_DIRS) {
|
|
3142
3267
|
try {
|
|
3143
|
-
return readFileSync3(
|
|
3268
|
+
return readFileSync3(join20(dir, `${stack}.txt`), "utf8");
|
|
3144
3269
|
} catch {
|
|
3145
3270
|
}
|
|
3146
3271
|
}
|
|
@@ -3155,9 +3280,9 @@ ${readTemplate(s).trim()}`);
|
|
|
3155
3280
|
|
|
3156
3281
|
// src/lib/write-or-merge-gitignore.ts
|
|
3157
3282
|
import { existsSync as existsSync8, readFileSync as readFileSync4, writeFileSync } from "fs";
|
|
3158
|
-
import { join as
|
|
3283
|
+
import { join as join21 } from "path";
|
|
3159
3284
|
function writeOrMergeGitignore(folderPath, avatarBlock) {
|
|
3160
|
-
const path =
|
|
3285
|
+
const path = join21(folderPath, ".gitignore");
|
|
3161
3286
|
if (!existsSync8(path)) {
|
|
3162
3287
|
writeFileSync(path, avatarBlock, "utf8");
|
|
3163
3288
|
return;
|
|
@@ -3332,9 +3457,75 @@ async function safeBootstrapGitInFolder(folderPath, opts = {}) {
|
|
|
3332
3457
|
await appendAuditEntry("bootstrap", `state=${state},strategy=${strategy}`);
|
|
3333
3458
|
}
|
|
3334
3459
|
|
|
3460
|
+
// src/lib/symlink-farm-for-team-pack-mount-dirs.ts
|
|
3461
|
+
import { promises as fs11 } from "fs";
|
|
3462
|
+
import { dirname as dirname5, join as join22, relative as relative2 } from "path";
|
|
3463
|
+
|
|
3464
|
+
// src/lib/backup-existing-dir-before-symlink-override.ts
|
|
3465
|
+
import { promises as fs10 } from "fs";
|
|
3466
|
+
function timestamp() {
|
|
3467
|
+
const d = /* @__PURE__ */ new Date();
|
|
3468
|
+
const pad = (n) => n.toString().padStart(2, "0");
|
|
3469
|
+
return `${d.getFullYear()}-${pad(d.getMonth() + 1)}-${pad(d.getDate())}-${pad(d.getHours())}${pad(d.getMinutes())}`;
|
|
3470
|
+
}
|
|
3471
|
+
async function backupDirBeforeReplace(targetPath) {
|
|
3472
|
+
const backupPath = `${targetPath}.backup-${timestamp()}`;
|
|
3473
|
+
await fs10.rename(targetPath, backupPath);
|
|
3474
|
+
return backupPath;
|
|
3475
|
+
}
|
|
3476
|
+
|
|
3477
|
+
// src/lib/symlink-farm-for-team-pack-mount-dirs.ts
|
|
3478
|
+
var TEAM_PACK_MOUNT_DIRS = [
|
|
3479
|
+
"skills",
|
|
3480
|
+
"agents",
|
|
3481
|
+
"commands",
|
|
3482
|
+
"hooks",
|
|
3483
|
+
"workflows",
|
|
3484
|
+
"scripts",
|
|
3485
|
+
"knowledge"
|
|
3486
|
+
];
|
|
3487
|
+
async function isSymbolicLink(path) {
|
|
3488
|
+
try {
|
|
3489
|
+
const st = await fs11.lstat(path);
|
|
3490
|
+
return st.isSymbolicLink();
|
|
3491
|
+
} catch {
|
|
3492
|
+
return false;
|
|
3493
|
+
}
|
|
3494
|
+
}
|
|
3495
|
+
async function syncMountedDir(source, dest, force) {
|
|
3496
|
+
const dir = relative2(dirname5(dest), dest) || dest;
|
|
3497
|
+
if (!await pathExists(source)) {
|
|
3498
|
+
return { dir, action: "source-missing" };
|
|
3499
|
+
}
|
|
3500
|
+
if (await pathExists(dest)) {
|
|
3501
|
+
if (await isSymbolicLink(dest)) {
|
|
3502
|
+
await fs11.unlink(dest);
|
|
3503
|
+
} else if (force) {
|
|
3504
|
+
const backupPath = await backupDirBeforeReplace(dest);
|
|
3505
|
+
const relativeSource2 = relative2(dirname5(dest), source);
|
|
3506
|
+
await fs11.symlink(relativeSource2, dest);
|
|
3507
|
+
return { dir, action: "backed-up-and-linked", backupPath };
|
|
3508
|
+
} else {
|
|
3509
|
+
return { dir, action: "skipped-conflict" };
|
|
3510
|
+
}
|
|
3511
|
+
}
|
|
3512
|
+
const relativeSource = relative2(dirname5(dest), source);
|
|
3513
|
+
await fs11.symlink(relativeSource, dest);
|
|
3514
|
+
return { dir, action: "created" };
|
|
3515
|
+
}
|
|
3516
|
+
async function syncAllMountDirs(packDir, claudeDir, force) {
|
|
3517
|
+
const results = [];
|
|
3518
|
+
for (const dir of TEAM_PACK_MOUNT_DIRS) {
|
|
3519
|
+
const source = join22(packDir, dir);
|
|
3520
|
+
const dest = join22(claudeDir, dir);
|
|
3521
|
+
results.push(await syncMountedDir(source, dest, force));
|
|
3522
|
+
}
|
|
3523
|
+
return results;
|
|
3524
|
+
}
|
|
3525
|
+
|
|
3335
3526
|
// src/commands/init-conflict-detection-helpers.ts
|
|
3336
3527
|
import { readdir } from "fs/promises";
|
|
3337
|
-
import { join as
|
|
3528
|
+
import { join as join23 } from "path";
|
|
3338
3529
|
async function isEmptyOrMissing(path) {
|
|
3339
3530
|
if (!await pathExists(path)) return true;
|
|
3340
3531
|
try {
|
|
@@ -3347,7 +3538,7 @@ async function isEmptyOrMissing(path) {
|
|
|
3347
3538
|
}
|
|
3348
3539
|
async function findAlternativeWorkspaceName(parent, desiredName, maxAttempts = 10) {
|
|
3349
3540
|
for (let i = 2; i < maxAttempts; i++) {
|
|
3350
|
-
const candidate =
|
|
3541
|
+
const candidate = join23(parent, `${desiredName}-${i}`);
|
|
3351
3542
|
if (await isEmptyOrMissing(candidate)) return candidate;
|
|
3352
3543
|
}
|
|
3353
3544
|
return null;
|
|
@@ -3777,7 +3968,7 @@ async function runInitFromScratch(opts, ownerEmail) {
|
|
|
3777
3968
|
const teamOwner = opts.teamOwner ?? await promptTeamOwner(ownerEmail);
|
|
3778
3969
|
const workspaceParent = resolve(opts.workspaceParent ?? ".");
|
|
3779
3970
|
const workspacePath = await resolveWorkspacePath(workspaceParent, projectName, opts.force);
|
|
3780
|
-
const srcPath =
|
|
3971
|
+
const srcPath = join24(workspacePath, "src");
|
|
3781
3972
|
await ensureDir(workspacePath);
|
|
3782
3973
|
await ensureDir(srcPath);
|
|
3783
3974
|
await safeBootstrapGitInFolder(srcPath, { autoYes: true });
|
|
@@ -3915,11 +4106,12 @@ async function finalizeWorkspaceScaffold(args) {
|
|
|
3915
4106
|
await writeRootClaudeMd(args.workspacePath, vars);
|
|
3916
4107
|
await writeProjectSettings(args.workspacePath, vars);
|
|
3917
4108
|
await appendGitignoreEntries(args.workspacePath);
|
|
3918
|
-
await ensureDir(
|
|
3919
|
-
await ensureDir(
|
|
3920
|
-
await installGitHook(
|
|
3921
|
-
await installGitHook(
|
|
4109
|
+
await ensureDir(join24(args.workspacePath, "notes"));
|
|
4110
|
+
await ensureDir(join24(args.workspacePath, "scripts"));
|
|
4111
|
+
await installGitHook(join24(args.workspacePath, ".git"), "post-merge");
|
|
4112
|
+
await installGitHook(join24(args.workspacePath, ".git", "modules", "src"), "pre-push");
|
|
3922
4113
|
log.success("C\xE0i post-merge (workspace) + pre-push (src/)");
|
|
4114
|
+
await autoSyncPackOnInit(args.workspacePath);
|
|
3923
4115
|
await appendAuditEntry("init", `flow=${args.flow},workspace=${args.workspaceName}`);
|
|
3924
4116
|
await maybeCommitWorkspace(args.workspacePath, args.skipCommit);
|
|
3925
4117
|
await maybeCreateWorkspaceRemote(args);
|
|
@@ -3954,6 +4146,39 @@ async function finalizeWorkspaceScaffold(args) {
|
|
|
3954
4146
|
}
|
|
3955
4147
|
printInitSuccessBox(args.workspacePath, args.flow, aiResult, gitnexusResult);
|
|
3956
4148
|
}
|
|
4149
|
+
async function autoSyncPackOnInit(workspacePath) {
|
|
4150
|
+
const packDir = join24(workspacePath, TEAM_PACK_RELATIVE_PATH);
|
|
4151
|
+
if (!await pathExists(packDir)) {
|
|
4152
|
+
log.dim("Pack submodule kh\xF4ng t\u1ED3n t\u1EA1i (skip auto-sync). C\xF3 th\u1EC3 ch\u1EA1y `avatar sync` sau.");
|
|
4153
|
+
return;
|
|
4154
|
+
}
|
|
4155
|
+
const claudeDir = join24(workspacePath, ".claude");
|
|
4156
|
+
log.info("Auto-sync pack content v\xE0o .claude/ (symlinks + settings merge)...");
|
|
4157
|
+
try {
|
|
4158
|
+
const results = await syncAllMountDirs(packDir, claudeDir, false);
|
|
4159
|
+
const created = results.filter((r) => r.action === "created" || r.action === "updated").length;
|
|
4160
|
+
const missing = results.filter((r) => r.action === "source-missing").length;
|
|
4161
|
+
log.success(
|
|
4162
|
+
` \u2713 Symlinks: ${created} created${missing > 0 ? `, ${missing} source-missing (pack thi\u1EBFu dir)` : ""}`
|
|
4163
|
+
);
|
|
4164
|
+
const mergeResult = await mergePackSettingsIntoProjectSettings(workspacePath);
|
|
4165
|
+
switch (mergeResult.action) {
|
|
4166
|
+
case "merged":
|
|
4167
|
+
log.success(` \u2713 settings.json merged (${mergeResult.changes.join("; ")})`);
|
|
4168
|
+
break;
|
|
4169
|
+
case "no-change":
|
|
4170
|
+
log.dim(" - settings.json \u0111\xE3 sync, kh\xF4ng c\u1EA7n thay \u0111\u1ED5i.");
|
|
4171
|
+
break;
|
|
4172
|
+
case "no-pack-template":
|
|
4173
|
+
log.dim(" - Pack kh\xF4ng c\xF3 templates/settings.json.tpl, skip merge.");
|
|
4174
|
+
break;
|
|
4175
|
+
}
|
|
4176
|
+
} catch (err) {
|
|
4177
|
+
log.warn(
|
|
4178
|
+
`Auto-sync pack fail: ${err instanceof Error ? err.message : err}. Ch\u1EA1y \`avatar sync\` th\u1EE7 c\xF4ng \u0111\u1EC3 retry.`
|
|
4179
|
+
);
|
|
4180
|
+
}
|
|
4181
|
+
}
|
|
3957
4182
|
async function maybeCreateWorkspaceRemote(args) {
|
|
3958
4183
|
if (args.skipCommit) {
|
|
3959
4184
|
log.dim("Skip workspace remote (ch\u01B0a commit). Setup sau qua: gh repo create ...");
|
|
@@ -4041,7 +4266,7 @@ async function maybeCreateWorkspaceRemote(args) {
|
|
|
4041
4266
|
}
|
|
4042
4267
|
}
|
|
4043
4268
|
async function resolveWorkspacePath(parent, desiredName, force) {
|
|
4044
|
-
const desired =
|
|
4269
|
+
const desired = join24(parent, desiredName);
|
|
4045
4270
|
if (await isEmptyOrMissing(desired)) return desired;
|
|
4046
4271
|
log.warn(`Workspace path "${desired}" \u0111\xE3 c\xF3 n\u1ED9i dung.`);
|
|
4047
4272
|
while (true) {
|
|
@@ -4072,7 +4297,7 @@ async function resolveWorkspacePath(parent, desiredName, force) {
|
|
|
4072
4297
|
message: "T\xEAn workspace m\u1EDBi:",
|
|
4073
4298
|
validate: (v) => v.trim().length > 0 ? true : "T\xEAn kh\xF4ng \u0111\u01B0\u1EE3c r\u1ED7ng"
|
|
4074
4299
|
});
|
|
4075
|
-
const newPath =
|
|
4300
|
+
const newPath = join24(parent, newName.trim());
|
|
4076
4301
|
if (await isEmptyOrMissing(newPath)) return newPath;
|
|
4077
4302
|
log.warn(`"${newPath}" c\u0169ng \u0111\xE3 c\xF3 n\u1ED9i dung. Th\u1EED t\xEAn kh\xE1c.`);
|
|
4078
4303
|
}
|
|
@@ -4115,7 +4340,7 @@ function formatGitnexusStatusLine(result) {
|
|
|
4115
4340
|
}
|
|
4116
4341
|
function printInitSuccessBox(rootPath, flow, aiResult = null, gitnexusResult = null) {
|
|
4117
4342
|
const lines = [
|
|
4118
|
-
`${chalk.green("\u2713")} Workspace s\u1EB5n s\xE0ng: ${
|
|
4343
|
+
`${chalk.green("\u2713")} Workspace s\u1EB5n s\xE0ng: ${relative3(process.cwd(), rootPath) || rootPath}`,
|
|
4119
4344
|
` ${chalk.dim(`(flow: ${flow})`)}`,
|
|
4120
4345
|
formatAiStatusLine(aiResult),
|
|
4121
4346
|
formatGitnexusStatusLine(gitnexusResult),
|
|
@@ -4178,18 +4403,18 @@ function registerSecretsCommand(program2) {
|
|
|
4178
4403
|
}
|
|
4179
4404
|
|
|
4180
4405
|
// src/commands/status.ts
|
|
4181
|
-
import { promises as
|
|
4182
|
-
import { join as
|
|
4406
|
+
import { promises as fs13 } from "fs";
|
|
4407
|
+
import { join as join26 } from "path";
|
|
4183
4408
|
import boxen6 from "boxen";
|
|
4184
4409
|
|
|
4185
4410
|
// src/lib/pack-backup-manager.ts
|
|
4186
|
-
import { promises as
|
|
4187
|
-
import { join as
|
|
4411
|
+
import { promises as fs12 } from "fs";
|
|
4412
|
+
import { join as join25 } from "path";
|
|
4188
4413
|
var BACKUP_DIR_NAME = "_backup";
|
|
4189
4414
|
async function listBackups(projectRoot) {
|
|
4190
|
-
const dir =
|
|
4415
|
+
const dir = join25(projectRoot, ".claude", BACKUP_DIR_NAME);
|
|
4191
4416
|
if (!await pathExists(dir)) return [];
|
|
4192
|
-
const entries = await
|
|
4417
|
+
const entries = await fs12.readdir(dir, { withFileTypes: true });
|
|
4193
4418
|
return entries.filter((e) => e.isDirectory()).map((e) => e.name).sort().reverse();
|
|
4194
4419
|
}
|
|
4195
4420
|
|
|
@@ -4213,7 +4438,7 @@ function registerStatusCommand(program2) {
|
|
|
4213
4438
|
}
|
|
4214
4439
|
async function gatherStatus(cwd) {
|
|
4215
4440
|
const projectName = cwd.split("/").filter(Boolean).pop() ?? "unknown";
|
|
4216
|
-
const claudeRoot =
|
|
4441
|
+
const claudeRoot = join26(cwd, ".claude");
|
|
4217
4442
|
const hasAvatar = await pathExists(claudeRoot);
|
|
4218
4443
|
if (!hasAvatar) {
|
|
4219
4444
|
return {
|
|
@@ -4226,9 +4451,9 @@ async function gatherStatus(cwd) {
|
|
|
4226
4451
|
hasAvatar: false
|
|
4227
4452
|
};
|
|
4228
4453
|
}
|
|
4229
|
-
const packVersion = await isGitRepo(
|
|
4230
|
-
const pendingDir =
|
|
4231
|
-
const pendingCount = await pathExists(pendingDir) ? (await
|
|
4454
|
+
const packVersion = await isGitRepo(join26(claudeRoot, "pack")) ? await readPinnedPackVersion(cwd).catch(() => null) : null;
|
|
4455
|
+
const pendingDir = join26(claudeRoot, "_pending");
|
|
4456
|
+
const pendingCount = await pathExists(pendingDir) ? (await fs13.readdir(pendingDir)).filter((n) => n.endsWith(".diff.md")).length : 0;
|
|
4232
4457
|
const backupCount = (await listBackups(cwd)).length;
|
|
4233
4458
|
const techStackSummary = await readTechStackFirstLine(claudeRoot);
|
|
4234
4459
|
return {
|
|
@@ -4242,7 +4467,7 @@ async function gatherStatus(cwd) {
|
|
|
4242
4467
|
};
|
|
4243
4468
|
}
|
|
4244
4469
|
async function readTechStackFirstLine(claudeRoot) {
|
|
4245
|
-
const techStackPath =
|
|
4470
|
+
const techStackPath = join26(claudeRoot, "project", "tech-stack.md");
|
|
4246
4471
|
if (!await pathExists(techStackPath)) return "(no tech-stack.md)";
|
|
4247
4472
|
const content = await readText(techStackPath);
|
|
4248
4473
|
const firstNonHeaderLine = content.split("\n").find((l) => l.trim() && !l.startsWith("#") && !l.startsWith(">"));
|
|
@@ -4265,201 +4490,8 @@ function renderStatusBox(s) {
|
|
|
4265
4490
|
// src/commands/sync.ts
|
|
4266
4491
|
import { join as join28 } from "path";
|
|
4267
4492
|
|
|
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
4493
|
// src/lib/preview-team-pack-sync-changes-for-dry-run.ts
|
|
4394
4494
|
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
4495
|
async function inspectMountDir(packDir, claudeDir, dir) {
|
|
4464
4496
|
const source = join27(packDir, dir);
|
|
4465
4497
|
const dest = join27(claudeDir, dir);
|
|
@@ -4774,7 +4806,7 @@ async function removeSubmoduleEntry(gitmodulesPath, submodulePath) {
|
|
|
4774
4806
|
}
|
|
4775
4807
|
|
|
4776
4808
|
// src/commands/uninstall.ts
|
|
4777
|
-
var CLI_VERSION = "1.6.
|
|
4809
|
+
var CLI_VERSION = "1.6.2";
|
|
4778
4810
|
function registerUninstallCommand(program2) {
|
|
4779
4811
|
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
4812
|
try {
|
|
@@ -4856,7 +4888,7 @@ function printUninstallSuccessBox(backupPath) {
|
|
|
4856
4888
|
}
|
|
4857
4889
|
|
|
4858
4890
|
// src/index.ts
|
|
4859
|
-
var CLI_VERSION2 = "1.6.
|
|
4891
|
+
var CLI_VERSION2 = "1.6.2";
|
|
4860
4892
|
var program = new Command();
|
|
4861
4893
|
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
4894
|
"beforeAll",
|