@synity/bitrix-skills 1.3.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/CHANGELOG.md +169 -0
- package/LICENSE +21 -0
- package/README.md +83 -0
- package/bin/bitrix-skills.js +3 -0
- package/dist/cli.js +1510 -0
- package/dist/features/bx-task/install.js +111 -0
- package/dist/features/task-sync/index.js +1053 -0
- package/package.json +69 -0
- package/src/features/bx/assets/SKILL.md +34 -0
- package/src/features/bx/feature.json +8 -0
- package/src/features/bx-calendar/assets/SKILL.md +61 -0
- package/src/features/bx-calendar/assets/availability.md +65 -0
- package/src/features/bx-calendar/assets/meeting.md +87 -0
- package/src/features/bx-calendar/assets/reminder.md +71 -0
- package/src/features/bx-calendar/assets/sync.md +70 -0
- package/src/features/bx-calendar/feature.json +10 -0
- package/src/features/bx-crm/assets/SKILL.md +59 -0
- package/src/features/bx-crm/assets/commerce.md +96 -0
- package/src/features/bx-crm/assets/onboard.md +127 -0
- package/src/features/bx-crm/assets/report.md +98 -0
- package/src/features/bx-crm/assets/research.md +71 -0
- package/src/features/bx-crm/feature.json +10 -0
- package/src/features/bx-task/assets/SKILL.md +148 -0
- package/src/features/bx-task/assets/lib/bx-api.sh +39 -0
- package/src/features/bx-task/assets/lib/bx-checklist.sh +127 -0
- package/src/features/bx-task/assets/lib/bx-resolve-task.sh +41 -0
- package/src/features/bx-task/assets/lib/bx-state.sh +131 -0
- package/src/features/bx-task/assets/lib/bx-tasks.sh +109 -0
- package/src/features/bx-task/assets/references/bootstrap.md +184 -0
- package/src/features/bx-task/assets/references/feature.md +97 -0
- package/src/features/bx-task/assets/references/init-templates/cli-tool.md +47 -0
- package/src/features/bx-task/assets/references/init-templates/generic.md +31 -0
- package/src/features/bx-task/assets/references/init-templates/library.md +45 -0
- package/src/features/bx-task/assets/references/init-templates/monorepo.md +38 -0
- package/src/features/bx-task/assets/references/init-templates/npm-package.md +40 -0
- package/src/features/bx-task/assets/references/init-templates/web-app.md +46 -0
- package/src/features/bx-task/assets/references/init.md +107 -0
- package/src/features/bx-task/assets/references/roadmap.md +93 -0
- package/src/features/bx-task/assets/references/summary.md +269 -0
- package/src/features/bx-task/assets/references/sync.md +104 -0
- package/src/features/bx-task/assets/references/time-log.md +214 -0
- package/src/features/bx-task/feature.json +10 -0
- package/src/features/bx-task/install.ts +117 -0
- package/src/features/task-sync/assets/docs/bitrix-task-reference.md +318 -0
- package/src/features/task-sync/assets/docs/bitrix-task-sync.md +254 -0
- package/src/features/task-sync/assets/githooks/commit-msg +44 -0
- package/src/features/task-sync/assets/githooks/install.sh +15 -0
- package/src/features/task-sync/assets/manifest.json +108 -0
- package/src/features/task-sync/assets/rules/00-bitrix-task-sync.md +161 -0
- package/src/features/task-sync/assets/scripts/bitrix-attach-files.sh +55 -0
- package/src/features/task-sync/assets/scripts/bitrix-lib.sh +540 -0
- package/src/features/task-sync/assets/scripts/bitrix-render-digest.sh +116 -0
- package/src/features/task-sync/assets/scripts/bitrix-session-check.sh +51 -0
- package/src/features/task-sync/assets/scripts/bitrix-session-sync.sh +89 -0
- package/src/features/task-sync/assets/scripts/bitrix-skill-end.sh +165 -0
- package/src/features/task-sync/assets/scripts/bitrix-skill-start.sh +58 -0
- package/src/features/task-sync/assets/scripts/lib/bb-formatter.sh +110 -0
- package/src/features/task-sync/assets/scripts/lib/bitrix-lib.sh +540 -0
- package/src/features/task-sync/assets/scripts/lib/time-helpers.sh +57 -0
- package/src/features/task-sync/assets/workflows/bitrix-sync.yml +85 -0
- package/src/features/task-sync/commands/install.ts +296 -0
- package/src/features/task-sync/commands/uninstall.ts +189 -0
- package/src/features/task-sync/commands/update.ts +11 -0
- package/src/features/task-sync/commands/verify.ts +141 -0
- package/src/features/task-sync/feature.json +12 -0
- package/src/features/task-sync/index.ts +121 -0
- package/src/features/task-sync/lib/dest-map.ts +96 -0
- package/src/features/task-sync/lib/drift-check.ts +47 -0
- package/src/features/task-sync/lib/file-ops.ts +36 -0
- package/src/features/task-sync/lib/manifest.ts +66 -0
- package/src/features/task-sync/lib/project-root.ts +38 -0
- package/src/features/task-sync/lib/settings-merge.ts +112 -0
- package/src/features/task-sync/lib/skill-refs.ts +106 -0
- package/src/features/task-sync/lib/task-id-finder.ts +31 -0
- package/src/features/task-sync/lib/token-extractor.ts +52 -0
- package/src/features/task-sync/lib/version.ts +36 -0
- package/src/features/task-sync/types.ts +40 -0
package/dist/cli.js
ADDED
|
@@ -0,0 +1,1510 @@
|
|
|
1
|
+
var __defProp = Object.defineProperty;
|
|
2
|
+
var __getOwnPropNames = Object.getOwnPropertyNames;
|
|
3
|
+
var __esm = (fn, res) => function __init() {
|
|
4
|
+
return fn && (res = (0, fn[__getOwnPropNames(fn)[0]])(fn = 0)), res;
|
|
5
|
+
};
|
|
6
|
+
var __export = (target, all) => {
|
|
7
|
+
for (var name in all)
|
|
8
|
+
__defProp(target, name, { get: all[name], enumerable: true });
|
|
9
|
+
};
|
|
10
|
+
|
|
11
|
+
// node_modules/.pnpm/tsup@8.5.1_postcss@8.5.14_typescript@5.9.3/node_modules/tsup/assets/esm_shims.js
|
|
12
|
+
import path from "path";
|
|
13
|
+
import { fileURLToPath } from "url";
|
|
14
|
+
var init_esm_shims = __esm({
|
|
15
|
+
"node_modules/.pnpm/tsup@8.5.1_postcss@8.5.14_typescript@5.9.3/node_modules/tsup/assets/esm_shims.js"() {
|
|
16
|
+
"use strict";
|
|
17
|
+
}
|
|
18
|
+
});
|
|
19
|
+
|
|
20
|
+
// src/lib/fs-safety.ts
|
|
21
|
+
import { lstat } from "fs/promises";
|
|
22
|
+
import path2 from "path";
|
|
23
|
+
async function assertNotSymlink(destPath) {
|
|
24
|
+
try {
|
|
25
|
+
const stat2 = await lstat(destPath);
|
|
26
|
+
if (stat2.isSymbolicLink()) {
|
|
27
|
+
throw new Error(
|
|
28
|
+
`Refusing to overwrite symlink: ${destPath}. Remove it manually then re-run install.`
|
|
29
|
+
);
|
|
30
|
+
}
|
|
31
|
+
} catch (err) {
|
|
32
|
+
if (err.code === "ENOENT") return;
|
|
33
|
+
throw err;
|
|
34
|
+
}
|
|
35
|
+
}
|
|
36
|
+
function assertContainedIn(destAbs, allowedRoot, label) {
|
|
37
|
+
const root = allowedRoot.endsWith(path2.sep) ? allowedRoot : allowedRoot + path2.sep;
|
|
38
|
+
if (!destAbs.startsWith(root)) {
|
|
39
|
+
throw new Error(
|
|
40
|
+
`"${label}" resolves outside allowed root (${root}). Refusing \u2014 possible path traversal.`
|
|
41
|
+
);
|
|
42
|
+
}
|
|
43
|
+
}
|
|
44
|
+
var init_fs_safety = __esm({
|
|
45
|
+
"src/lib/fs-safety.ts"() {
|
|
46
|
+
"use strict";
|
|
47
|
+
init_esm_shims();
|
|
48
|
+
}
|
|
49
|
+
});
|
|
50
|
+
|
|
51
|
+
// src/features/task-sync/lib/file-ops.ts
|
|
52
|
+
import { createHash as createHash2 } from "crypto";
|
|
53
|
+
import { readFile, access, mkdir, writeFile, rename, chmod } from "fs/promises";
|
|
54
|
+
import { constants } from "fs";
|
|
55
|
+
import path3 from "path";
|
|
56
|
+
async function sha256File(filePath) {
|
|
57
|
+
const buf = await readFile(filePath);
|
|
58
|
+
return createHash2("sha256").update(buf).digest("hex");
|
|
59
|
+
}
|
|
60
|
+
async function fileExists(filePath) {
|
|
61
|
+
try {
|
|
62
|
+
await access(filePath, constants.F_OK);
|
|
63
|
+
return true;
|
|
64
|
+
} catch {
|
|
65
|
+
return false;
|
|
66
|
+
}
|
|
67
|
+
}
|
|
68
|
+
async function ensureDir(dirPath) {
|
|
69
|
+
await mkdir(dirPath, { recursive: true });
|
|
70
|
+
}
|
|
71
|
+
async function atomicWrite(filePath, content, mode = 420) {
|
|
72
|
+
await ensureDir(path3.dirname(filePath));
|
|
73
|
+
const tmp = `${filePath}.${process.pid}.${Date.now()}.tmp`;
|
|
74
|
+
await writeFile(tmp, content, { mode });
|
|
75
|
+
await chmod(tmp, mode);
|
|
76
|
+
await rename(tmp, filePath);
|
|
77
|
+
}
|
|
78
|
+
var init_file_ops = __esm({
|
|
79
|
+
"src/features/task-sync/lib/file-ops.ts"() {
|
|
80
|
+
"use strict";
|
|
81
|
+
init_esm_shims();
|
|
82
|
+
}
|
|
83
|
+
});
|
|
84
|
+
|
|
85
|
+
// src/features/task-sync/lib/project-root.ts
|
|
86
|
+
import path4 from "path";
|
|
87
|
+
async function detectProjectRoot(startDir) {
|
|
88
|
+
let dir = path4.resolve(startDir);
|
|
89
|
+
const root = path4.parse(dir).root;
|
|
90
|
+
while (true) {
|
|
91
|
+
for (const marker of ROOT_MARKERS) {
|
|
92
|
+
if (await fileExists(path4.join(dir, marker))) {
|
|
93
|
+
return dir;
|
|
94
|
+
}
|
|
95
|
+
}
|
|
96
|
+
const parent = path4.dirname(dir);
|
|
97
|
+
if (parent === dir || dir === root) {
|
|
98
|
+
break;
|
|
99
|
+
}
|
|
100
|
+
dir = parent;
|
|
101
|
+
}
|
|
102
|
+
dir = path4.resolve(startDir);
|
|
103
|
+
while (true) {
|
|
104
|
+
if (await fileExists(path4.join(dir, "package.json"))) {
|
|
105
|
+
return dir;
|
|
106
|
+
}
|
|
107
|
+
const parent = path4.dirname(dir);
|
|
108
|
+
if (parent === dir) break;
|
|
109
|
+
dir = parent;
|
|
110
|
+
}
|
|
111
|
+
return path4.resolve(startDir);
|
|
112
|
+
}
|
|
113
|
+
async function hasGitDir(rootDir) {
|
|
114
|
+
return fileExists(path4.join(rootDir, ".git"));
|
|
115
|
+
}
|
|
116
|
+
var ROOT_MARKERS;
|
|
117
|
+
var init_project_root = __esm({
|
|
118
|
+
"src/features/task-sync/lib/project-root.ts"() {
|
|
119
|
+
"use strict";
|
|
120
|
+
init_esm_shims();
|
|
121
|
+
init_file_ops();
|
|
122
|
+
ROOT_MARKERS = [".git", "pnpm-workspace.yaml"];
|
|
123
|
+
}
|
|
124
|
+
});
|
|
125
|
+
|
|
126
|
+
// src/features/task-sync/lib/manifest.ts
|
|
127
|
+
var manifest_exports = {};
|
|
128
|
+
__export(manifest_exports, {
|
|
129
|
+
loadManifest: () => loadManifest,
|
|
130
|
+
resolveAssetsDir: () => resolveAssetsDir
|
|
131
|
+
});
|
|
132
|
+
import { readFile as readFile2 } from "fs/promises";
|
|
133
|
+
import { existsSync as existsSync3 } from "fs";
|
|
134
|
+
import path5 from "path";
|
|
135
|
+
import { fileURLToPath as fileURLToPath3 } from "url";
|
|
136
|
+
function resolveAssetsDir() {
|
|
137
|
+
const candidates = [
|
|
138
|
+
// From dist/ (cli.js context): ../src/features/task-sync/assets
|
|
139
|
+
path5.resolve(__dirname2, "../src/features/task-sync/assets"),
|
|
140
|
+
// From dist/features/task-sync/ (index.js context): ../../../src/features/task-sync/assets
|
|
141
|
+
path5.resolve(__dirname2, "../../../src/features/task-sync/assets"),
|
|
142
|
+
// Dev: src/features/task-sync/lib/ → ../assets
|
|
143
|
+
path5.resolve(__dirname2, "../assets"),
|
|
144
|
+
// Legacy fallbacks (should not match before correct paths above)
|
|
145
|
+
path5.resolve(__dirname2, "../../assets"),
|
|
146
|
+
path5.resolve(__dirname2, "../../../assets"),
|
|
147
|
+
path5.resolve(__dirname2, "../../../../assets")
|
|
148
|
+
];
|
|
149
|
+
for (const c of candidates) {
|
|
150
|
+
if (existsSync3(path5.join(c, "manifest.json"))) {
|
|
151
|
+
return c;
|
|
152
|
+
}
|
|
153
|
+
}
|
|
154
|
+
return candidates[0];
|
|
155
|
+
}
|
|
156
|
+
async function loadManifest() {
|
|
157
|
+
const dir = resolveAssetsDir();
|
|
158
|
+
const manifestPath = path5.join(dir, "manifest.json");
|
|
159
|
+
if (!await fileExists(manifestPath)) {
|
|
160
|
+
throw new Error(`assets/manifest.json not found at ${manifestPath} (was 'pnpm build' run?)`);
|
|
161
|
+
}
|
|
162
|
+
const raw = await readFile2(manifestPath, "utf8");
|
|
163
|
+
let parsed;
|
|
164
|
+
try {
|
|
165
|
+
parsed = JSON.parse(raw);
|
|
166
|
+
} catch (err) {
|
|
167
|
+
throw new Error(`malformed manifest.json: ${err.message}`);
|
|
168
|
+
}
|
|
169
|
+
validateManifest(parsed);
|
|
170
|
+
return parsed;
|
|
171
|
+
}
|
|
172
|
+
function validateManifest(obj) {
|
|
173
|
+
if (!obj || typeof obj !== "object") throw new Error("manifest must be an object");
|
|
174
|
+
const m = obj;
|
|
175
|
+
if (typeof m["version"] !== "string") throw new Error("manifest.version must be string");
|
|
176
|
+
if (!Array.isArray(m["files"])) throw new Error("manifest.files must be array");
|
|
177
|
+
for (const f of m["files"]) {
|
|
178
|
+
if (!f || typeof f !== "object") throw new Error("manifest.files[] entry must be object");
|
|
179
|
+
const e = f;
|
|
180
|
+
if (typeof e["src"] !== "string") throw new Error("manifest entry.src must be string");
|
|
181
|
+
if (typeof e["sha256"] !== "string") throw new Error("manifest entry.sha256 must be string");
|
|
182
|
+
if (typeof e["size"] !== "number") throw new Error("manifest entry.size must be number");
|
|
183
|
+
if (typeof e["mode"] !== "number") throw new Error("manifest entry.mode must be number");
|
|
184
|
+
}
|
|
185
|
+
}
|
|
186
|
+
var __dirname2;
|
|
187
|
+
var init_manifest = __esm({
|
|
188
|
+
"src/features/task-sync/lib/manifest.ts"() {
|
|
189
|
+
"use strict";
|
|
190
|
+
init_esm_shims();
|
|
191
|
+
init_file_ops();
|
|
192
|
+
__dirname2 = path5.dirname(fileURLToPath3(import.meta.url));
|
|
193
|
+
}
|
|
194
|
+
});
|
|
195
|
+
|
|
196
|
+
// src/features/task-sync/lib/dest-map.ts
|
|
197
|
+
var dest_map_exports = {};
|
|
198
|
+
__export(dest_map_exports, {
|
|
199
|
+
buildDestMap: () => buildDestMap
|
|
200
|
+
});
|
|
201
|
+
import path6 from "path";
|
|
202
|
+
import os from "os";
|
|
203
|
+
function buildDestMap(manifest, cwd) {
|
|
204
|
+
const out = [];
|
|
205
|
+
const home = os.homedir();
|
|
206
|
+
const skillsRoot = path6.join(home, ".claude", "skills");
|
|
207
|
+
for (const entry of manifest.files) {
|
|
208
|
+
const src = entry.src;
|
|
209
|
+
if (src.startsWith("scripts/")) {
|
|
210
|
+
const name = src.slice("scripts/".length);
|
|
211
|
+
const destRel = path6.posix.join(".claude/scripts", name);
|
|
212
|
+
out.push({
|
|
213
|
+
manifestEntry: entry,
|
|
214
|
+
destAbs: path6.resolve(cwd, destRel),
|
|
215
|
+
destRel,
|
|
216
|
+
category: "script"
|
|
217
|
+
});
|
|
218
|
+
} else if (src.startsWith("githooks/")) {
|
|
219
|
+
const name = src.slice("githooks/".length);
|
|
220
|
+
const destRel = path6.posix.join(".githooks", name);
|
|
221
|
+
out.push({
|
|
222
|
+
manifestEntry: entry,
|
|
223
|
+
destAbs: path6.resolve(cwd, destRel),
|
|
224
|
+
destRel,
|
|
225
|
+
category: "githook",
|
|
226
|
+
optionalFlag: "noGithooks"
|
|
227
|
+
});
|
|
228
|
+
} else if (src.startsWith("rules/")) {
|
|
229
|
+
const name = src.slice("rules/".length);
|
|
230
|
+
const destRel = path6.posix.join(".claude/rules", name);
|
|
231
|
+
out.push({
|
|
232
|
+
manifestEntry: entry,
|
|
233
|
+
destAbs: path6.resolve(cwd, destRel),
|
|
234
|
+
destRel,
|
|
235
|
+
category: "rule"
|
|
236
|
+
});
|
|
237
|
+
} else if (src.startsWith("docs/")) {
|
|
238
|
+
const name = src.slice("docs/".length);
|
|
239
|
+
const destRel = path6.posix.join("docs", name);
|
|
240
|
+
out.push({
|
|
241
|
+
manifestEntry: entry,
|
|
242
|
+
destAbs: path6.resolve(cwd, destRel),
|
|
243
|
+
destRel,
|
|
244
|
+
category: "doc"
|
|
245
|
+
});
|
|
246
|
+
} else if (src.startsWith("workflows/")) {
|
|
247
|
+
const name = src.slice("workflows/".length);
|
|
248
|
+
const destRel = path6.posix.join(".github/workflows", name);
|
|
249
|
+
out.push({
|
|
250
|
+
manifestEntry: entry,
|
|
251
|
+
destAbs: path6.resolve(cwd, destRel),
|
|
252
|
+
destRel,
|
|
253
|
+
category: "workflow",
|
|
254
|
+
optionalFlag: "noWorkflow"
|
|
255
|
+
});
|
|
256
|
+
} else if (src.startsWith("skill/")) {
|
|
257
|
+
const name = src.slice("skill/".length);
|
|
258
|
+
const destAbs = path6.resolve(home, ".claude", "skills", "bitrix-sync-install", name);
|
|
259
|
+
const destRel = path6.posix.join("~/.claude/skills/bitrix-sync-install", name);
|
|
260
|
+
out.push({
|
|
261
|
+
manifestEntry: entry,
|
|
262
|
+
destAbs,
|
|
263
|
+
destRel,
|
|
264
|
+
category: "skill",
|
|
265
|
+
optionalFlag: "noSkill"
|
|
266
|
+
});
|
|
267
|
+
}
|
|
268
|
+
}
|
|
269
|
+
for (const e of out) {
|
|
270
|
+
const allowedRoot = e.category === "skill" ? skillsRoot : cwd;
|
|
271
|
+
assertContainedIn(e.destAbs, allowedRoot, e.manifestEntry.src);
|
|
272
|
+
}
|
|
273
|
+
return out;
|
|
274
|
+
}
|
|
275
|
+
var init_dest_map = __esm({
|
|
276
|
+
"src/features/task-sync/lib/dest-map.ts"() {
|
|
277
|
+
"use strict";
|
|
278
|
+
init_esm_shims();
|
|
279
|
+
init_fs_safety();
|
|
280
|
+
}
|
|
281
|
+
});
|
|
282
|
+
|
|
283
|
+
// src/features/task-sync/lib/settings-merge.ts
|
|
284
|
+
import { readFile as readFile3 } from "fs/promises";
|
|
285
|
+
import deepmerge from "deepmerge";
|
|
286
|
+
function arrayMerge(target, source) {
|
|
287
|
+
const combined = [...target, ...source];
|
|
288
|
+
const seen = /* @__PURE__ */ new Set();
|
|
289
|
+
const result = [];
|
|
290
|
+
for (const item of combined) {
|
|
291
|
+
const key = JSON.stringify(item);
|
|
292
|
+
if (seen.has(key)) continue;
|
|
293
|
+
seen.add(key);
|
|
294
|
+
result.push(item);
|
|
295
|
+
}
|
|
296
|
+
return result;
|
|
297
|
+
}
|
|
298
|
+
function mergeSettings(existing, template) {
|
|
299
|
+
return deepmerge(existing, template, {
|
|
300
|
+
arrayMerge,
|
|
301
|
+
clone: true
|
|
302
|
+
});
|
|
303
|
+
}
|
|
304
|
+
async function loadSettingsFile(filePath) {
|
|
305
|
+
if (!await fileExists(filePath)) return {};
|
|
306
|
+
const raw = await readFile3(filePath, "utf8");
|
|
307
|
+
if (!raw.trim()) return {};
|
|
308
|
+
try {
|
|
309
|
+
const parsed = JSON.parse(raw);
|
|
310
|
+
if (!parsed || typeof parsed !== "object" || Array.isArray(parsed)) {
|
|
311
|
+
throw new Error("settings.json must be a JSON object");
|
|
312
|
+
}
|
|
313
|
+
return parsed;
|
|
314
|
+
} catch (err) {
|
|
315
|
+
throw new Error(`malformed settings.json (${filePath}): ${err.message}`);
|
|
316
|
+
}
|
|
317
|
+
}
|
|
318
|
+
async function writeSettingsFile(filePath, obj) {
|
|
319
|
+
const json = JSON.stringify(obj, null, 2) + "\n";
|
|
320
|
+
await atomicWrite(filePath, json, 420);
|
|
321
|
+
}
|
|
322
|
+
function isBitrixHookEntry(entry) {
|
|
323
|
+
const cmds = (entry.hooks ?? []).map((h) => h.command).filter((c) => typeof c === "string");
|
|
324
|
+
return cmds.some((c) => /\.claude\/scripts\/bitrix-/.test(c));
|
|
325
|
+
}
|
|
326
|
+
function removeBitrixHooks(settings) {
|
|
327
|
+
const cloned = JSON.parse(JSON.stringify(settings));
|
|
328
|
+
const hooks = cloned.hooks;
|
|
329
|
+
if (!hooks) return cloned;
|
|
330
|
+
for (const trigger of Object.keys(hooks)) {
|
|
331
|
+
const list = hooks[trigger];
|
|
332
|
+
if (!Array.isArray(list)) continue;
|
|
333
|
+
const filtered = list.filter((entry) => !isBitrixHookEntry(entry));
|
|
334
|
+
if (filtered.length === 0) {
|
|
335
|
+
delete hooks[trigger];
|
|
336
|
+
} else {
|
|
337
|
+
hooks[trigger] = filtered;
|
|
338
|
+
}
|
|
339
|
+
}
|
|
340
|
+
if (Object.keys(hooks).length === 0) {
|
|
341
|
+
delete cloned.hooks;
|
|
342
|
+
}
|
|
343
|
+
return cloned;
|
|
344
|
+
}
|
|
345
|
+
var BITRIX_HOOKS_TEMPLATE;
|
|
346
|
+
var init_settings_merge = __esm({
|
|
347
|
+
"src/features/task-sync/lib/settings-merge.ts"() {
|
|
348
|
+
"use strict";
|
|
349
|
+
init_esm_shims();
|
|
350
|
+
init_file_ops();
|
|
351
|
+
BITRIX_HOOKS_TEMPLATE = {
|
|
352
|
+
hooks: {
|
|
353
|
+
SessionStart: [
|
|
354
|
+
{ hooks: [{ type: "command", command: "bash .claude/scripts/bitrix-session-check.sh" }] }
|
|
355
|
+
],
|
|
356
|
+
PreToolUse: [
|
|
357
|
+
{ matcher: "Skill", hooks: [{ type: "command", command: "bash .claude/scripts/bitrix-skill-start.sh" }] }
|
|
358
|
+
],
|
|
359
|
+
PostToolUse: [
|
|
360
|
+
{ matcher: "Skill", hooks: [{ type: "command", command: "bash .claude/scripts/bitrix-skill-end.sh" }] }
|
|
361
|
+
],
|
|
362
|
+
Stop: [
|
|
363
|
+
{ hooks: [{ type: "command", command: "bash .claude/scripts/bitrix-session-sync.sh" }] }
|
|
364
|
+
]
|
|
365
|
+
}
|
|
366
|
+
};
|
|
367
|
+
}
|
|
368
|
+
});
|
|
369
|
+
|
|
370
|
+
// src/features/task-sync/lib/skill-refs.ts
|
|
371
|
+
import { readFile as readFile4, writeFile as writeFile2, mkdir as mkdir2, rm, access as access2 } from "fs/promises";
|
|
372
|
+
import path7 from "path";
|
|
373
|
+
import os2 from "os";
|
|
374
|
+
function getSkillDir() {
|
|
375
|
+
return path7.join(os2.homedir(), ".claude/skills/bitrix-sync-install");
|
|
376
|
+
}
|
|
377
|
+
function getRefsFile() {
|
|
378
|
+
return path7.join(getSkillDir(), ".refs.json");
|
|
379
|
+
}
|
|
380
|
+
async function loadRefs() {
|
|
381
|
+
try {
|
|
382
|
+
const raw = await readFile4(getRefsFile(), "utf8");
|
|
383
|
+
const parsed = JSON.parse(raw);
|
|
384
|
+
if (parsed && typeof parsed === "object" && Array.isArray(parsed.projects)) {
|
|
385
|
+
return { version: 1, projects: [...parsed.projects] };
|
|
386
|
+
}
|
|
387
|
+
} catch {
|
|
388
|
+
}
|
|
389
|
+
return { version: 1, projects: [] };
|
|
390
|
+
}
|
|
391
|
+
async function saveRefs(refs) {
|
|
392
|
+
await mkdir2(getSkillDir(), { recursive: true });
|
|
393
|
+
await writeFile2(getRefsFile(), JSON.stringify(refs, null, 2) + "\n", "utf8");
|
|
394
|
+
}
|
|
395
|
+
async function addProjectRef(projectPath) {
|
|
396
|
+
const abs = path7.resolve(projectPath);
|
|
397
|
+
const refs = await loadRefs();
|
|
398
|
+
if (!refs.projects.includes(abs)) {
|
|
399
|
+
refs.projects.push(abs);
|
|
400
|
+
await saveRefs(refs);
|
|
401
|
+
}
|
|
402
|
+
return refs.projects;
|
|
403
|
+
}
|
|
404
|
+
async function removeProjectRef(projectPath) {
|
|
405
|
+
const abs = path7.resolve(projectPath);
|
|
406
|
+
const refs = await loadRefs();
|
|
407
|
+
const before = refs.projects.length;
|
|
408
|
+
refs.projects = refs.projects.filter((p) => p !== abs);
|
|
409
|
+
const removed = refs.projects.length < before;
|
|
410
|
+
if (refs.projects.length === 0) {
|
|
411
|
+
try {
|
|
412
|
+
await rm(getRefsFile(), { force: true });
|
|
413
|
+
} catch {
|
|
414
|
+
}
|
|
415
|
+
} else if (removed) {
|
|
416
|
+
await saveRefs(refs);
|
|
417
|
+
}
|
|
418
|
+
return { remaining: refs.projects.length, removed };
|
|
419
|
+
}
|
|
420
|
+
async function getRefs() {
|
|
421
|
+
const refs = await loadRefs();
|
|
422
|
+
return refs.projects;
|
|
423
|
+
}
|
|
424
|
+
async function clearAllRefs() {
|
|
425
|
+
try {
|
|
426
|
+
await rm(getSkillDir(), { recursive: true, force: true });
|
|
427
|
+
} catch {
|
|
428
|
+
}
|
|
429
|
+
}
|
|
430
|
+
var init_skill_refs = __esm({
|
|
431
|
+
"src/features/task-sync/lib/skill-refs.ts"() {
|
|
432
|
+
"use strict";
|
|
433
|
+
init_esm_shims();
|
|
434
|
+
}
|
|
435
|
+
});
|
|
436
|
+
|
|
437
|
+
// src/features/task-sync/commands/install.ts
|
|
438
|
+
var install_exports = {};
|
|
439
|
+
__export(install_exports, {
|
|
440
|
+
installFile: () => installFile,
|
|
441
|
+
installManifestFiles: () => installManifestFiles,
|
|
442
|
+
run: () => run
|
|
443
|
+
});
|
|
444
|
+
import { copyFile, chmod as chmod2 } from "fs/promises";
|
|
445
|
+
import path8 from "path";
|
|
446
|
+
import kleur from "kleur";
|
|
447
|
+
import { execa } from "execa";
|
|
448
|
+
async function installFile(entry, ctx) {
|
|
449
|
+
const srcAbs = path8.join(ctx.assetsDir, entry.manifestEntry.src);
|
|
450
|
+
const destAbs = entry.destAbs;
|
|
451
|
+
await assertNotSymlink(destAbs);
|
|
452
|
+
if (await fileExists(destAbs)) {
|
|
453
|
+
const destSha = await sha256File(destAbs);
|
|
454
|
+
if (destSha === entry.manifestEntry.sha256) {
|
|
455
|
+
return "skipped";
|
|
456
|
+
}
|
|
457
|
+
if (!ctx.opts.force) {
|
|
458
|
+
return "skipped";
|
|
459
|
+
}
|
|
460
|
+
if (ctx.opts.dryRun) {
|
|
461
|
+
console.log(` ${kleur.yellow("[dry-run]")} overwrite ${entry.destRel}`);
|
|
462
|
+
return "planned";
|
|
463
|
+
}
|
|
464
|
+
await ensureDir(path8.dirname(destAbs));
|
|
465
|
+
await copyFile(srcAbs, destAbs);
|
|
466
|
+
await chmod2(destAbs, entry.manifestEntry.mode);
|
|
467
|
+
return "overwritten";
|
|
468
|
+
}
|
|
469
|
+
if (ctx.opts.dryRun) {
|
|
470
|
+
console.log(` ${kleur.yellow("[dry-run]")} copy \u2192 ${entry.destRel}`);
|
|
471
|
+
return "planned";
|
|
472
|
+
}
|
|
473
|
+
await ensureDir(path8.dirname(destAbs));
|
|
474
|
+
await copyFile(srcAbs, destAbs);
|
|
475
|
+
await chmod2(destAbs, entry.manifestEntry.mode);
|
|
476
|
+
return "copied";
|
|
477
|
+
}
|
|
478
|
+
async function installManifestFiles(entries, ctx) {
|
|
479
|
+
const stats = {
|
|
480
|
+
copied: 0,
|
|
481
|
+
skipped: 0,
|
|
482
|
+
overwritten: 0,
|
|
483
|
+
planned: 0,
|
|
484
|
+
filtered: 0,
|
|
485
|
+
warnings: []
|
|
486
|
+
};
|
|
487
|
+
for (const entry of entries) {
|
|
488
|
+
if (entry.optionalFlag && ctx.opts[entry.optionalFlag]) {
|
|
489
|
+
stats.filtered += 1;
|
|
490
|
+
continue;
|
|
491
|
+
}
|
|
492
|
+
const result = await installFile(entry, ctx);
|
|
493
|
+
if (result === "skipped") {
|
|
494
|
+
const destSha = await fileExists(entry.destAbs) ? await sha256File(entry.destAbs) : null;
|
|
495
|
+
if (destSha && destSha !== entry.manifestEntry.sha256 && !ctx.opts.force) {
|
|
496
|
+
stats.warnings.push(`skipped (user-modified): ${entry.destRel}`);
|
|
497
|
+
}
|
|
498
|
+
}
|
|
499
|
+
stats[result] += 1;
|
|
500
|
+
}
|
|
501
|
+
return stats;
|
|
502
|
+
}
|
|
503
|
+
async function mergeSettingsStep(ctx) {
|
|
504
|
+
const settingsPath = path8.join(ctx.cwd, ".claude/settings.json");
|
|
505
|
+
const existing = await loadSettingsFile(settingsPath);
|
|
506
|
+
const merged = mergeSettings(existing, BITRIX_HOOKS_TEMPLATE);
|
|
507
|
+
const alreadyMerged = JSON.stringify(existing) === JSON.stringify(merged);
|
|
508
|
+
if (ctx.opts.dryRun) {
|
|
509
|
+
if (alreadyMerged) {
|
|
510
|
+
console.log(` ${kleur.yellow("[dry-run]")} settings.json: already up-to-date`);
|
|
511
|
+
} else {
|
|
512
|
+
console.log(` ${kleur.yellow("[dry-run]")} settings.json: would merge 4 Bitrix hooks`);
|
|
513
|
+
}
|
|
514
|
+
return { added: !alreadyMerged, alreadyMerged };
|
|
515
|
+
}
|
|
516
|
+
if (!alreadyMerged) {
|
|
517
|
+
await writeSettingsFile(settingsPath, merged);
|
|
518
|
+
}
|
|
519
|
+
return { added: !alreadyMerged, alreadyMerged };
|
|
520
|
+
}
|
|
521
|
+
async function ensureUfTokensTotal(cwd, stats) {
|
|
522
|
+
const webhookUrl = process.env["BITRIX_WEBHOOK_URL"];
|
|
523
|
+
if (!webhookUrl) return;
|
|
524
|
+
const libPath = path8.join(cwd, ".claude/scripts/bitrix-lib.sh");
|
|
525
|
+
if (!await fileExists(libPath)) return;
|
|
526
|
+
const script = `
|
|
527
|
+
set -e
|
|
528
|
+
source "$1"
|
|
529
|
+
result=$(b24_call "task.item.userfield.add" "$(jq -n '{PARAMS:{USER_TYPE_ID:"double",FIELD_NAME:"UF_AI_TOKENS_TOTAL",XML_ID:"UF_AI_TOKENS_TOTAL",LABEL:"AI Tokens (cumulative)",SORT:500,MULTIPLE:"N",MANDATORY:"N",SETTINGS:{DEFAULT_VALUE:0,PRECISION:0}}}')")
|
|
530
|
+
err=$(echo "$result" | jq -r '.error // empty')
|
|
531
|
+
# ERROR_CORE = already exists \u2192 treat as success
|
|
532
|
+
if [[ -n "$err" && "$err" != "ERROR_CORE" ]]; then
|
|
533
|
+
echo "warn:$err" >&2
|
|
534
|
+
fi
|
|
535
|
+
exit 0
|
|
536
|
+
`;
|
|
537
|
+
try {
|
|
538
|
+
const result = await execa("bash", ["-c", script, "--", libPath], {
|
|
539
|
+
cwd,
|
|
540
|
+
reject: false,
|
|
541
|
+
env: { ...process.env }
|
|
542
|
+
});
|
|
543
|
+
if (result.stderr && !result.stderr.includes("ERROR_CORE")) {
|
|
544
|
+
const warnLine = (result.stderr.match(/warn:(.+)/) ?? [])[1];
|
|
545
|
+
if (warnLine) stats.warnings.push(`UF_AI_TOKENS_TOTAL: ${warnLine.trim()}`);
|
|
546
|
+
}
|
|
547
|
+
} catch {
|
|
548
|
+
stats.warnings.push("UF_AI_TOKENS_TOTAL: ensure skipped (bash error)");
|
|
549
|
+
}
|
|
550
|
+
}
|
|
551
|
+
async function runGitHooksInstall(ctx) {
|
|
552
|
+
const hooksScript = path8.join(ctx.cwd, ".githooks/install.sh");
|
|
553
|
+
if (!await fileExists(hooksScript)) {
|
|
554
|
+
return { run: false, warning: "githooks install.sh not found (skipped)" };
|
|
555
|
+
}
|
|
556
|
+
if (!await hasGitDir(ctx.cwd)) {
|
|
557
|
+
return { run: false, warning: "no .git/ in project root \u2014 skipped githooks setup" };
|
|
558
|
+
}
|
|
559
|
+
if (ctx.opts.dryRun) {
|
|
560
|
+
console.log(` ${kleur.yellow("[dry-run]")} would run: bash .githooks/install.sh`);
|
|
561
|
+
return { run: false };
|
|
562
|
+
}
|
|
563
|
+
try {
|
|
564
|
+
await chmod2(hooksScript, 493);
|
|
565
|
+
await execa("bash", [hooksScript], { cwd: ctx.cwd, stdio: "pipe" });
|
|
566
|
+
return { run: true };
|
|
567
|
+
} catch (err) {
|
|
568
|
+
return { run: false, warning: `githooks install failed: ${err.message}` };
|
|
569
|
+
}
|
|
570
|
+
}
|
|
571
|
+
async function run(opts) {
|
|
572
|
+
let cwd;
|
|
573
|
+
try {
|
|
574
|
+
cwd = await detectProjectRoot(opts.cwd);
|
|
575
|
+
} catch (err) {
|
|
576
|
+
console.error(kleur.red(`error: ${err.message}`));
|
|
577
|
+
return 2;
|
|
578
|
+
}
|
|
579
|
+
const assetsDir = resolveAssetsDir();
|
|
580
|
+
const ctx = { cwd, assetsDir, opts };
|
|
581
|
+
console.log("");
|
|
582
|
+
console.log(kleur.bold().cyan(" \u26A1 bitrix-skills task-sync install"));
|
|
583
|
+
console.log(kleur.gray(` project root: ${cwd}`));
|
|
584
|
+
console.log(kleur.gray(` assets: ${assetsDir}`));
|
|
585
|
+
if (opts.dryRun) console.log(kleur.yellow(" mode: --dry-run (no FS changes)"));
|
|
586
|
+
console.log("");
|
|
587
|
+
let manifest;
|
|
588
|
+
try {
|
|
589
|
+
manifest = await loadManifest();
|
|
590
|
+
} catch (err) {
|
|
591
|
+
console.error(kleur.red(`error: ${err.message}`));
|
|
592
|
+
return 1;
|
|
593
|
+
}
|
|
594
|
+
const dests = buildDestMap(manifest, cwd);
|
|
595
|
+
let stats;
|
|
596
|
+
try {
|
|
597
|
+
stats = await installManifestFiles(dests, ctx);
|
|
598
|
+
} catch (err) {
|
|
599
|
+
console.error(kleur.red(`copy error: ${err.message}`));
|
|
600
|
+
return 1;
|
|
601
|
+
}
|
|
602
|
+
let settingsResult;
|
|
603
|
+
try {
|
|
604
|
+
settingsResult = await mergeSettingsStep(ctx);
|
|
605
|
+
} catch (err) {
|
|
606
|
+
console.error(kleur.red(`settings merge error: ${err.message}`));
|
|
607
|
+
return 3;
|
|
608
|
+
}
|
|
609
|
+
if (!opts.noSkill && !opts.dryRun) {
|
|
610
|
+
await ensureUfTokensTotal(cwd, stats);
|
|
611
|
+
}
|
|
612
|
+
const githooksResult = opts.noGithooks ? { run: false, warning: "--no-githooks: skipped" } : await runGitHooksInstall(ctx);
|
|
613
|
+
let skillRefCount = null;
|
|
614
|
+
if (!opts.noSkill && !opts.dryRun) {
|
|
615
|
+
try {
|
|
616
|
+
const refs = await addProjectRef(cwd);
|
|
617
|
+
skillRefCount = refs.length;
|
|
618
|
+
} catch (err) {
|
|
619
|
+
stats.warnings.push(`skill ref registry: ${err.message}`);
|
|
620
|
+
}
|
|
621
|
+
}
|
|
622
|
+
console.log("");
|
|
623
|
+
if (opts.dryRun) {
|
|
624
|
+
const parts = [
|
|
625
|
+
`${stats.planned} would be copied/overwritten`,
|
|
626
|
+
`${stats.skipped} skipped`,
|
|
627
|
+
stats.filtered > 0 ? `${stats.filtered} filtered (--no-* flags)` : null
|
|
628
|
+
].filter(Boolean);
|
|
629
|
+
console.log(kleur.yellow(` [dry-run] ${parts.join(", ")}`));
|
|
630
|
+
} else {
|
|
631
|
+
const parts = [
|
|
632
|
+
`${stats.copied} copied`,
|
|
633
|
+
stats.overwritten > 0 ? `${stats.overwritten} overwritten` : null,
|
|
634
|
+
`${stats.skipped} skipped`,
|
|
635
|
+
stats.filtered > 0 ? `${stats.filtered} filtered (--no-* flags)` : null
|
|
636
|
+
].filter(Boolean);
|
|
637
|
+
console.log(kleur.green(` \u2713 files: ${parts.join(", ")}`));
|
|
638
|
+
}
|
|
639
|
+
if (settingsResult.alreadyMerged) {
|
|
640
|
+
console.log(kleur.gray(" \xB7 settings.json: already up-to-date"));
|
|
641
|
+
} else if (settingsResult.added && !opts.dryRun) {
|
|
642
|
+
console.log(kleur.green(" \u2713 settings.json: 4 Bitrix hooks merged"));
|
|
643
|
+
}
|
|
644
|
+
if (githooksResult.run) {
|
|
645
|
+
console.log(kleur.green(" \u2713 git hooks: core.hooksPath = .githooks (local)"));
|
|
646
|
+
} else if (githooksResult.warning) {
|
|
647
|
+
console.log(kleur.yellow(` ! ${githooksResult.warning}`));
|
|
648
|
+
}
|
|
649
|
+
if (skillRefCount !== null) {
|
|
650
|
+
const noun = skillRefCount === 1 ? "project" : "projects";
|
|
651
|
+
console.log(kleur.gray(` \xB7 skill: shared across ${skillRefCount} ${noun}`));
|
|
652
|
+
}
|
|
653
|
+
for (const w of stats.warnings) {
|
|
654
|
+
console.log(kleur.yellow(` ! ${w}`));
|
|
655
|
+
}
|
|
656
|
+
if (!opts.dryRun) {
|
|
657
|
+
console.log("");
|
|
658
|
+
console.log(kleur.bold(" Next steps:"));
|
|
659
|
+
console.log(" 1. Set TASK_ID in CLAUDE.md (see docs/bitrix-task-sync.md \xA72)");
|
|
660
|
+
console.log(" 2. Export BITRIX_WEBHOOK_URL in your shell rc");
|
|
661
|
+
console.log(" 3. Run: npx @synity/bitrix-skills verify");
|
|
662
|
+
console.log("");
|
|
663
|
+
}
|
|
664
|
+
return 0;
|
|
665
|
+
}
|
|
666
|
+
var init_install = __esm({
|
|
667
|
+
"src/features/task-sync/commands/install.ts"() {
|
|
668
|
+
"use strict";
|
|
669
|
+
init_esm_shims();
|
|
670
|
+
init_fs_safety();
|
|
671
|
+
init_project_root();
|
|
672
|
+
init_manifest();
|
|
673
|
+
init_dest_map();
|
|
674
|
+
init_file_ops();
|
|
675
|
+
init_settings_merge();
|
|
676
|
+
init_skill_refs();
|
|
677
|
+
}
|
|
678
|
+
});
|
|
679
|
+
|
|
680
|
+
// src/features/bx-task/install.ts
|
|
681
|
+
var install_exports2 = {};
|
|
682
|
+
__export(install_exports2, {
|
|
683
|
+
install: () => install,
|
|
684
|
+
uninstall: () => uninstall
|
|
685
|
+
});
|
|
686
|
+
import { homedir } from "os";
|
|
687
|
+
import { resolve, join as join3, dirname as dirname2 } from "path";
|
|
688
|
+
import { fileURLToPath as fileURLToPath4 } from "url";
|
|
689
|
+
import { mkdir as mkdir3, copyFile as copyFile2, readdir, stat, rm as rm2, access as access3 } from "fs/promises";
|
|
690
|
+
async function getAssetsDir() {
|
|
691
|
+
const here = dirname2(fileURLToPath4(import.meta.url));
|
|
692
|
+
const candidates = [
|
|
693
|
+
// dist/ context (inlined in cli.js): ../src/features/bx-task/assets
|
|
694
|
+
resolve(here, "../src/features/bx-task/assets"),
|
|
695
|
+
// dist/features/bx-task/ context: ../../../src/features/bx-task/assets
|
|
696
|
+
resolve(here, "../../../src/features/bx-task/assets"),
|
|
697
|
+
// src/features/bx-task/ (dev)
|
|
698
|
+
resolve(here, "assets")
|
|
699
|
+
];
|
|
700
|
+
for (const c of candidates) {
|
|
701
|
+
try {
|
|
702
|
+
await access3(c);
|
|
703
|
+
return c;
|
|
704
|
+
} catch {
|
|
705
|
+
}
|
|
706
|
+
}
|
|
707
|
+
return candidates[0];
|
|
708
|
+
}
|
|
709
|
+
function getSkillBase() {
|
|
710
|
+
return resolve(homedir(), ".claude", "skills");
|
|
711
|
+
}
|
|
712
|
+
async function install(opts) {
|
|
713
|
+
const skillBase = getSkillBase();
|
|
714
|
+
const dest = opts._destOverride ?? resolve(skillBase, "bx-task");
|
|
715
|
+
const resolvedDest = resolve(dest);
|
|
716
|
+
if (!opts._destOverride && !resolvedDest.startsWith(skillBase)) {
|
|
717
|
+
throw new Error(`Path traversal detected: ${resolvedDest}`);
|
|
718
|
+
}
|
|
719
|
+
const result = {
|
|
720
|
+
installedFiles: [],
|
|
721
|
+
skippedFiles: [],
|
|
722
|
+
warnings: []
|
|
723
|
+
};
|
|
724
|
+
await installDir(await getAssetsDir(), resolvedDest, result, opts);
|
|
725
|
+
result.warnings.push(
|
|
726
|
+
'[bx-task] Requires MCP server "bitrix-synity-mcp" \u2014 verify it is configured in Claude Code settings.'
|
|
727
|
+
);
|
|
728
|
+
return result;
|
|
729
|
+
}
|
|
730
|
+
async function installDir(srcDir, destDir, result, opts) {
|
|
731
|
+
await mkdir3(destDir, { recursive: true });
|
|
732
|
+
const entries = await readdir(srcDir, { withFileTypes: true });
|
|
733
|
+
for (const entry of entries) {
|
|
734
|
+
const srcPath = join3(srcDir, entry.name);
|
|
735
|
+
const destPath = join3(destDir, entry.name);
|
|
736
|
+
if (entry.isDirectory()) {
|
|
737
|
+
await installDir(srcPath, destPath, result, opts);
|
|
738
|
+
continue;
|
|
739
|
+
}
|
|
740
|
+
await assertNotSymlink(destPath);
|
|
741
|
+
const exists = await fileExists2(destPath);
|
|
742
|
+
if (exists && !opts.force && opts.onConflict === "skip") {
|
|
743
|
+
result.skippedFiles.push(destPath);
|
|
744
|
+
continue;
|
|
745
|
+
}
|
|
746
|
+
await copyFile2(srcPath, destPath);
|
|
747
|
+
result.installedFiles.push(destPath);
|
|
748
|
+
}
|
|
749
|
+
}
|
|
750
|
+
async function fileExists2(filePath) {
|
|
751
|
+
try {
|
|
752
|
+
await stat(filePath);
|
|
753
|
+
return true;
|
|
754
|
+
} catch {
|
|
755
|
+
return false;
|
|
756
|
+
}
|
|
757
|
+
}
|
|
758
|
+
async function uninstall(opts) {
|
|
759
|
+
const dest = resolve(opts?._destOverride ?? resolve(homedir(), ".claude", "skills", "bx-task"));
|
|
760
|
+
if (!opts?._destOverride) {
|
|
761
|
+
assertContainedIn(dest, getSkillBase(), dest);
|
|
762
|
+
}
|
|
763
|
+
await rm2(dest, { recursive: true, force: true });
|
|
764
|
+
}
|
|
765
|
+
var init_install2 = __esm({
|
|
766
|
+
"src/features/bx-task/install.ts"() {
|
|
767
|
+
"use strict";
|
|
768
|
+
init_esm_shims();
|
|
769
|
+
init_fs_safety();
|
|
770
|
+
}
|
|
771
|
+
});
|
|
772
|
+
|
|
773
|
+
// src/features/task-sync/commands/uninstall.ts
|
|
774
|
+
var uninstall_exports = {};
|
|
775
|
+
__export(uninstall_exports, {
|
|
776
|
+
run: () => run2
|
|
777
|
+
});
|
|
778
|
+
import { unlink } from "fs/promises";
|
|
779
|
+
import path9 from "path";
|
|
780
|
+
import kleur2 from "kleur";
|
|
781
|
+
import { execa as execa2 } from "execa";
|
|
782
|
+
async function run2(opts) {
|
|
783
|
+
let cwd;
|
|
784
|
+
try {
|
|
785
|
+
cwd = await detectProjectRoot(opts.cwd);
|
|
786
|
+
} catch (err) {
|
|
787
|
+
console.error(kleur2.red(`error: ${err.message}`));
|
|
788
|
+
return 2;
|
|
789
|
+
}
|
|
790
|
+
console.log("");
|
|
791
|
+
console.log(kleur2.bold().cyan(" \u26A1 bitrix-skills task-sync uninstall"));
|
|
792
|
+
console.log(kleur2.gray(` project root: ${cwd}`));
|
|
793
|
+
if (opts.dryRun) console.log(kleur2.yellow(" mode: --dry-run (no FS changes)"));
|
|
794
|
+
console.log("");
|
|
795
|
+
let manifest;
|
|
796
|
+
try {
|
|
797
|
+
manifest = await loadManifest();
|
|
798
|
+
} catch (err) {
|
|
799
|
+
console.error(kleur2.red(`error: ${err.message}`));
|
|
800
|
+
return 1;
|
|
801
|
+
}
|
|
802
|
+
const dests = buildDestMap(manifest, cwd);
|
|
803
|
+
let removed = 0;
|
|
804
|
+
let preserved = 0;
|
|
805
|
+
let absent = 0;
|
|
806
|
+
let filtered = 0;
|
|
807
|
+
const warnings = [];
|
|
808
|
+
const cwdAbs = path9.resolve(cwd);
|
|
809
|
+
const refsBefore = await getRefs();
|
|
810
|
+
const otherProjects = refsBefore.filter((p) => p !== cwdAbs);
|
|
811
|
+
const isLastRegisteredReferrer = refsBefore.length > 0 && otherProjects.length === 0;
|
|
812
|
+
const shouldRemoveSkillFiles = !opts.noSkill && (opts.removeSkill || isLastRegisteredReferrer);
|
|
813
|
+
for (const d of dests) {
|
|
814
|
+
if (d.category === "doc" && opts.keepDocs) {
|
|
815
|
+
filtered += 1;
|
|
816
|
+
continue;
|
|
817
|
+
}
|
|
818
|
+
if (d.category === "skill" && !shouldRemoveSkillFiles) {
|
|
819
|
+
filtered += 1;
|
|
820
|
+
continue;
|
|
821
|
+
}
|
|
822
|
+
if (d.optionalFlag && opts[d.optionalFlag]) {
|
|
823
|
+
filtered += 1;
|
|
824
|
+
continue;
|
|
825
|
+
}
|
|
826
|
+
if (!await fileExists(d.destAbs)) {
|
|
827
|
+
absent += 1;
|
|
828
|
+
continue;
|
|
829
|
+
}
|
|
830
|
+
const actual = await sha256File(d.destAbs);
|
|
831
|
+
if (actual !== d.manifestEntry.sha256) {
|
|
832
|
+
preserved += 1;
|
|
833
|
+
warnings.push(`preserved (user-modified): ${d.destRel}`);
|
|
834
|
+
continue;
|
|
835
|
+
}
|
|
836
|
+
if (opts.dryRun) {
|
|
837
|
+
console.log(` ${kleur2.yellow("[dry-run]")} remove ${d.destRel}`);
|
|
838
|
+
removed += 1;
|
|
839
|
+
continue;
|
|
840
|
+
}
|
|
841
|
+
await unlink(d.destAbs);
|
|
842
|
+
removed += 1;
|
|
843
|
+
}
|
|
844
|
+
const settingsPath = path9.join(cwd, ".claude/settings.json");
|
|
845
|
+
let settingsChanged = false;
|
|
846
|
+
if (await fileExists(settingsPath)) {
|
|
847
|
+
try {
|
|
848
|
+
const existing = await loadSettingsFile(settingsPath);
|
|
849
|
+
const cleaned = removeBitrixHooks(existing);
|
|
850
|
+
const before = JSON.stringify(existing);
|
|
851
|
+
const after = JSON.stringify(cleaned);
|
|
852
|
+
if (before !== after) {
|
|
853
|
+
settingsChanged = true;
|
|
854
|
+
if (!opts.dryRun) {
|
|
855
|
+
await writeSettingsFile(settingsPath, cleaned);
|
|
856
|
+
}
|
|
857
|
+
}
|
|
858
|
+
} catch (err) {
|
|
859
|
+
warnings.push(`settings.json: ${err.message}`);
|
|
860
|
+
}
|
|
861
|
+
}
|
|
862
|
+
let skillRefRemaining = null;
|
|
863
|
+
if (!opts.noSkill && !opts.dryRun) {
|
|
864
|
+
try {
|
|
865
|
+
if (opts.removeSkill) {
|
|
866
|
+
await clearAllRefs();
|
|
867
|
+
skillRefRemaining = 0;
|
|
868
|
+
} else {
|
|
869
|
+
const result = await removeProjectRef(cwdAbs);
|
|
870
|
+
skillRefRemaining = result.remaining;
|
|
871
|
+
}
|
|
872
|
+
} catch (err) {
|
|
873
|
+
warnings.push(`skill ref registry: ${err.message}`);
|
|
874
|
+
}
|
|
875
|
+
}
|
|
876
|
+
let hooksPathUnset = false;
|
|
877
|
+
if (await hasGitDir(cwd)) {
|
|
878
|
+
try {
|
|
879
|
+
const { stdout } = await execa2("git", ["config", "--local", "--get", "core.hooksPath"], {
|
|
880
|
+
cwd,
|
|
881
|
+
reject: false
|
|
882
|
+
});
|
|
883
|
+
if (stdout.trim() === ".githooks") {
|
|
884
|
+
if (!opts.dryRun) {
|
|
885
|
+
await execa2("git", ["config", "--local", "--unset", "core.hooksPath"], { cwd, reject: false });
|
|
886
|
+
}
|
|
887
|
+
hooksPathUnset = true;
|
|
888
|
+
}
|
|
889
|
+
} catch {
|
|
890
|
+
}
|
|
891
|
+
}
|
|
892
|
+
console.log("");
|
|
893
|
+
const filteredSuffix = filtered > 0 ? `, ${filtered} filtered (--keep-docs / no --remove-skill / --no-*)` : "";
|
|
894
|
+
if (opts.dryRun) {
|
|
895
|
+
console.log(kleur2.yellow(` [dry-run] would remove ${removed} files, preserve ${preserved}, ${absent} already absent${filteredSuffix}`));
|
|
896
|
+
} else {
|
|
897
|
+
console.log(kleur2.green(` \u2713 files: ${removed} removed, ${preserved} preserved, ${absent} already absent${filteredSuffix}`));
|
|
898
|
+
}
|
|
899
|
+
if (settingsChanged) {
|
|
900
|
+
console.log(kleur2.green(" \u2713 settings.json: Bitrix hooks removed"));
|
|
901
|
+
} else {
|
|
902
|
+
console.log(kleur2.gray(" \xB7 settings.json: nothing to remove"));
|
|
903
|
+
}
|
|
904
|
+
if (hooksPathUnset) {
|
|
905
|
+
console.log(kleur2.green(" \u2713 git hooks: core.hooksPath unset"));
|
|
906
|
+
}
|
|
907
|
+
if (skillRefRemaining !== null) {
|
|
908
|
+
if (opts.removeSkill) {
|
|
909
|
+
console.log(kleur2.green(" \u2713 skill: ~/.claude/skills/bitrix-sync-install removed (--remove-skill)"));
|
|
910
|
+
} else if (skillRefRemaining === 0) {
|
|
911
|
+
console.log(kleur2.green(" \u2713 skill: last project unlinked, files removed"));
|
|
912
|
+
} else {
|
|
913
|
+
const noun = skillRefRemaining === 1 ? "project" : "projects";
|
|
914
|
+
console.log(
|
|
915
|
+
kleur2.gray(
|
|
916
|
+
` \xB7 skill: preserved (still used by ${skillRefRemaining} other ${noun}); pass --remove-skill to force`
|
|
917
|
+
)
|
|
918
|
+
);
|
|
919
|
+
}
|
|
920
|
+
}
|
|
921
|
+
for (const w of warnings) {
|
|
922
|
+
console.log(kleur2.yellow(` ! ${w}`));
|
|
923
|
+
}
|
|
924
|
+
console.log("");
|
|
925
|
+
return 0;
|
|
926
|
+
}
|
|
927
|
+
var init_uninstall = __esm({
|
|
928
|
+
"src/features/task-sync/commands/uninstall.ts"() {
|
|
929
|
+
"use strict";
|
|
930
|
+
init_esm_shims();
|
|
931
|
+
init_project_root();
|
|
932
|
+
init_manifest();
|
|
933
|
+
init_dest_map();
|
|
934
|
+
init_file_ops();
|
|
935
|
+
init_settings_merge();
|
|
936
|
+
init_skill_refs();
|
|
937
|
+
}
|
|
938
|
+
});
|
|
939
|
+
|
|
940
|
+
// src/cli.ts
|
|
941
|
+
init_esm_shims();
|
|
942
|
+
import { Cli, Builtins } from "clipanion";
|
|
943
|
+
import { createRequire } from "module";
|
|
944
|
+
|
|
945
|
+
// src/commands/install.ts
|
|
946
|
+
init_esm_shims();
|
|
947
|
+
import { Command, Option } from "clipanion";
|
|
948
|
+
import chalk from "chalk";
|
|
949
|
+
import { existsSync as existsSync4 } from "fs";
|
|
950
|
+
import { join as join4, relative as relative2 } from "path";
|
|
951
|
+
|
|
952
|
+
// src/lib/feature-registry.ts
|
|
953
|
+
init_esm_shims();
|
|
954
|
+
import { existsSync, readdirSync, readFileSync, statSync } from "fs";
|
|
955
|
+
import { dirname, join } from "path";
|
|
956
|
+
import { fileURLToPath as fileURLToPath2 } from "url";
|
|
957
|
+
function defaultFeaturesDir() {
|
|
958
|
+
const pkgRoot = join(dirname(fileURLToPath2(import.meta.url)), "..");
|
|
959
|
+
const fromPkg = join(pkgRoot, "src", "features");
|
|
960
|
+
if (existsSync(fromPkg)) return fromPkg;
|
|
961
|
+
const fromCwd = join(process.cwd(), "src", "features");
|
|
962
|
+
if (existsSync(fromCwd)) return fromCwd;
|
|
963
|
+
return join(pkgRoot, "features");
|
|
964
|
+
}
|
|
965
|
+
function validateFeature(json) {
|
|
966
|
+
if (!json || typeof json !== "object") return false;
|
|
967
|
+
const f = json;
|
|
968
|
+
if (typeof f["name"] !== "string" || !f["name"]) return false;
|
|
969
|
+
if (typeof f["displayName"] !== "string") return false;
|
|
970
|
+
if (typeof f["version"] !== "string") return false;
|
|
971
|
+
if (f["target"] !== "project" && f["target"] !== "global") return false;
|
|
972
|
+
if (typeof f["description"] !== "string") return false;
|
|
973
|
+
return true;
|
|
974
|
+
}
|
|
975
|
+
function listFeatures(featuresDir) {
|
|
976
|
+
const dir = featuresDir ?? defaultFeaturesDir();
|
|
977
|
+
if (!existsSync(dir)) return [];
|
|
978
|
+
const features = [];
|
|
979
|
+
for (const entry of readdirSync(dir)) {
|
|
980
|
+
const entryPath = join(dir, entry);
|
|
981
|
+
if (!statSync(entryPath).isDirectory()) continue;
|
|
982
|
+
const featureJsonPath = join(entryPath, "feature.json");
|
|
983
|
+
if (!existsSync(featureJsonPath)) continue;
|
|
984
|
+
try {
|
|
985
|
+
const raw = JSON.parse(readFileSync(featureJsonPath, "utf8"));
|
|
986
|
+
if (validateFeature(raw)) {
|
|
987
|
+
features.push(raw);
|
|
988
|
+
}
|
|
989
|
+
} catch {
|
|
990
|
+
}
|
|
991
|
+
}
|
|
992
|
+
return features;
|
|
993
|
+
}
|
|
994
|
+
|
|
995
|
+
// src/lib/manifest.ts
|
|
996
|
+
init_esm_shims();
|
|
997
|
+
import { createHash } from "crypto";
|
|
998
|
+
import { readFileSync as readFileSync2, writeFileSync, existsSync as existsSync2, readdirSync as readdirSync2, statSync as statSync2, renameSync } from "fs";
|
|
999
|
+
import { join as join2, relative } from "path";
|
|
1000
|
+
function getManifestPath(projectRoot) {
|
|
1001
|
+
return join2(projectRoot, ".bitrix-tools.json");
|
|
1002
|
+
}
|
|
1003
|
+
function readManifest(projectRoot) {
|
|
1004
|
+
const p = getManifestPath(projectRoot);
|
|
1005
|
+
if (!existsSync2(p)) return null;
|
|
1006
|
+
const raw = readFileSync2(p, "utf8");
|
|
1007
|
+
return JSON.parse(raw);
|
|
1008
|
+
}
|
|
1009
|
+
function writeManifest(projectRoot, manifest) {
|
|
1010
|
+
const final = getManifestPath(projectRoot);
|
|
1011
|
+
const tmp = `${final}.tmp`;
|
|
1012
|
+
writeFileSync(tmp, JSON.stringify(manifest, null, 2) + "\n", "utf8");
|
|
1013
|
+
renameSync(tmp, final);
|
|
1014
|
+
}
|
|
1015
|
+
function computeChecksum(filepath) {
|
|
1016
|
+
const buf = readFileSync2(filepath);
|
|
1017
|
+
return createHash("sha256").update(buf).digest("hex");
|
|
1018
|
+
}
|
|
1019
|
+
|
|
1020
|
+
// src/commands/install.ts
|
|
1021
|
+
var DEFAULT_CLI_OPTS = {
|
|
1022
|
+
dryRun: false,
|
|
1023
|
+
force: false,
|
|
1024
|
+
noGithooks: false,
|
|
1025
|
+
noWorkflow: false,
|
|
1026
|
+
noSkill: false,
|
|
1027
|
+
keepDocs: false,
|
|
1028
|
+
quiet: false,
|
|
1029
|
+
removeSkill: false
|
|
1030
|
+
};
|
|
1031
|
+
function detectLegacyInstall(cwd) {
|
|
1032
|
+
const hasOldModule = existsSync4(join4(cwd, "node_modules", "@synity", "bitrix-task-sync"));
|
|
1033
|
+
const hasLibScript = existsSync4(join4(cwd, ".claude", "scripts", "bitrix-lib.sh"));
|
|
1034
|
+
const hasNewManifest = existsSync4(join4(cwd, ".bitrix-tools.json"));
|
|
1035
|
+
return (hasOldModule || hasLibScript) && !hasNewManifest;
|
|
1036
|
+
}
|
|
1037
|
+
async function installTaskSync(cwd) {
|
|
1038
|
+
try {
|
|
1039
|
+
const { run: run4 } = await Promise.resolve().then(() => (init_install(), install_exports));
|
|
1040
|
+
const opts = { ...DEFAULT_CLI_OPTS, cwd };
|
|
1041
|
+
const code = await run4(opts);
|
|
1042
|
+
if (code !== 0) return { ok: false, message: `task-sync install exited with code ${code}` };
|
|
1043
|
+
const { loadManifest: loadManifest2 } = await Promise.resolve().then(() => (init_manifest(), manifest_exports));
|
|
1044
|
+
const { buildDestMap: buildDestMap2 } = await Promise.resolve().then(() => (init_dest_map(), dest_map_exports));
|
|
1045
|
+
const assetManifest = await loadManifest2();
|
|
1046
|
+
const dests = buildDestMap2(assetManifest, cwd);
|
|
1047
|
+
const installedAbsPaths = dests.map((d) => d.destAbs).filter((p) => existsSync4(p));
|
|
1048
|
+
return { ok: true, message: "task-sync installed", installedAbsPaths };
|
|
1049
|
+
} catch (err) {
|
|
1050
|
+
return { ok: false, message: `task-sync: ${err.message}` };
|
|
1051
|
+
}
|
|
1052
|
+
}
|
|
1053
|
+
async function installBxTask(cwd) {
|
|
1054
|
+
try {
|
|
1055
|
+
const { install: install2 } = await Promise.resolve().then(() => (init_install2(), install_exports2));
|
|
1056
|
+
const result = await install2({ projectRoot: cwd, onConflict: "skip" });
|
|
1057
|
+
const msg = `bx-task: ${result.installedFiles.length} installed, ${result.skippedFiles.length} skipped`;
|
|
1058
|
+
return { ok: true, message: msg, installPath: result.installedFiles[0], installedAbsPaths: result.installedFiles };
|
|
1059
|
+
} catch (err) {
|
|
1060
|
+
return { ok: false, message: `bx-task: ${err.message}` };
|
|
1061
|
+
}
|
|
1062
|
+
}
|
|
1063
|
+
function upsertFeature(manifest, entry) {
|
|
1064
|
+
const idx = manifest.features.findIndex((f) => f.name === entry.name);
|
|
1065
|
+
const features = [...manifest.features];
|
|
1066
|
+
if (idx >= 0) {
|
|
1067
|
+
features[idx] = entry;
|
|
1068
|
+
} else {
|
|
1069
|
+
features.push(entry);
|
|
1070
|
+
}
|
|
1071
|
+
return { ...manifest, features };
|
|
1072
|
+
}
|
|
1073
|
+
var InstallCommand = class extends Command {
|
|
1074
|
+
static paths = [["install"]];
|
|
1075
|
+
static usage = Command.Usage({
|
|
1076
|
+
description: "Install features into the current project",
|
|
1077
|
+
details: `
|
|
1078
|
+
With no arguments, shows an interactive feature picker (TTY required).
|
|
1079
|
+
Use --all to install all available features, or pass feature names directly.
|
|
1080
|
+
`,
|
|
1081
|
+
examples: [
|
|
1082
|
+
["Install all features", "bitrix-skills install --all"],
|
|
1083
|
+
["Install specific features", "bitrix-skills install task-sync bx-task"],
|
|
1084
|
+
["Interactive picker", "bitrix-skills install"]
|
|
1085
|
+
]
|
|
1086
|
+
});
|
|
1087
|
+
all = Option.Boolean("--all", false, { description: "Install all available features" });
|
|
1088
|
+
featuresFlag = Option.String("--features", { description: "Comma-separated feature names" });
|
|
1089
|
+
featureArgs = Option.Rest({ required: 0 });
|
|
1090
|
+
async execute() {
|
|
1091
|
+
const cwd = process.cwd();
|
|
1092
|
+
const available = listFeatures();
|
|
1093
|
+
if (available.length === 0) {
|
|
1094
|
+
this.context.stderr.write(chalk.red("No features available.\n"));
|
|
1095
|
+
return 1;
|
|
1096
|
+
}
|
|
1097
|
+
if (detectLegacyInstall(cwd)) {
|
|
1098
|
+
this.context.stderr.write(
|
|
1099
|
+
chalk.yellow(
|
|
1100
|
+
"\n ! Legacy bitrix-task-sync v0.2.x detected.\n Consider running: bitrix-skills install task-sync\n to migrate to the new manifest-tracked installation.\n\n"
|
|
1101
|
+
)
|
|
1102
|
+
);
|
|
1103
|
+
}
|
|
1104
|
+
let selectedNames;
|
|
1105
|
+
const installable = available.filter((f) => f.status !== "planned");
|
|
1106
|
+
if (this.all) {
|
|
1107
|
+
selectedNames = installable.map((f) => f.name);
|
|
1108
|
+
} else if (this.featuresFlag) {
|
|
1109
|
+
selectedNames = this.featuresFlag.split(",").map((s) => s.trim()).filter(Boolean);
|
|
1110
|
+
} else if (this.featureArgs.length > 0) {
|
|
1111
|
+
selectedNames = this.featureArgs;
|
|
1112
|
+
} else {
|
|
1113
|
+
if (!process.stdin.isTTY) {
|
|
1114
|
+
this.context.stderr.write(
|
|
1115
|
+
chalk.red("error: no TTY detected. Use --all or specify feature names.\n")
|
|
1116
|
+
);
|
|
1117
|
+
return 1;
|
|
1118
|
+
}
|
|
1119
|
+
const checkbox = (await import("@inquirer/checkbox")).default;
|
|
1120
|
+
const choices = installable.map((f) => ({
|
|
1121
|
+
name: `${f.name} \u2014 ${f.description}`,
|
|
1122
|
+
value: f.name,
|
|
1123
|
+
checked: false
|
|
1124
|
+
}));
|
|
1125
|
+
const picked = await checkbox({
|
|
1126
|
+
message: "Select features to install:",
|
|
1127
|
+
choices
|
|
1128
|
+
});
|
|
1129
|
+
if (picked.length === 0) {
|
|
1130
|
+
this.context.stdout.write(chalk.gray("No features selected.\n"));
|
|
1131
|
+
return 0;
|
|
1132
|
+
}
|
|
1133
|
+
selectedNames = picked;
|
|
1134
|
+
}
|
|
1135
|
+
const availableNames = new Set(available.map((f) => f.name));
|
|
1136
|
+
const invalid = selectedNames.filter((n) => !availableNames.has(n));
|
|
1137
|
+
if (invalid.length > 0) {
|
|
1138
|
+
this.context.stderr.write(chalk.red(`Unknown features: ${invalid.join(", ")}
|
|
1139
|
+
`));
|
|
1140
|
+
this.context.stderr.write(chalk.gray(`Available: ${[...availableNames].join(", ")}
|
|
1141
|
+
`));
|
|
1142
|
+
return 1;
|
|
1143
|
+
}
|
|
1144
|
+
const existing = readManifest(cwd);
|
|
1145
|
+
let manifest = existing ?? { version: "1", features: [] };
|
|
1146
|
+
let anyFailure = false;
|
|
1147
|
+
for (const name of selectedNames) {
|
|
1148
|
+
const featureInfo = available.find((f) => f.name === name);
|
|
1149
|
+
this.context.stdout.write(`
|
|
1150
|
+
Installing ${chalk.bold(name)}...
|
|
1151
|
+
`);
|
|
1152
|
+
let ok = false;
|
|
1153
|
+
let message = "";
|
|
1154
|
+
let installPath = cwd;
|
|
1155
|
+
let installedAbsPaths = [];
|
|
1156
|
+
if (name === "task-sync") {
|
|
1157
|
+
const r = await installTaskSync(cwd);
|
|
1158
|
+
ok = r.ok;
|
|
1159
|
+
message = r.message;
|
|
1160
|
+
installPath = cwd;
|
|
1161
|
+
installedAbsPaths = r.installedAbsPaths ?? [];
|
|
1162
|
+
} else if (name === "bx-task") {
|
|
1163
|
+
const r = await installBxTask(cwd);
|
|
1164
|
+
ok = r.ok;
|
|
1165
|
+
message = r.message;
|
|
1166
|
+
if (r.installPath) {
|
|
1167
|
+
const { homedir: homedir2 } = await import("os");
|
|
1168
|
+
installPath = join4(homedir2(), ".claude", "skills", "bx-task");
|
|
1169
|
+
}
|
|
1170
|
+
installedAbsPaths = r.installedAbsPaths ?? [];
|
|
1171
|
+
} else {
|
|
1172
|
+
this.context.stderr.write(chalk.yellow(` ! No install handler for feature: ${name}
|
|
1173
|
+
`));
|
|
1174
|
+
anyFailure = true;
|
|
1175
|
+
continue;
|
|
1176
|
+
}
|
|
1177
|
+
if (ok) {
|
|
1178
|
+
this.context.stdout.write(chalk.green(` \u2713 ${message}
|
|
1179
|
+
`));
|
|
1180
|
+
const checksums = {};
|
|
1181
|
+
for (const absPath of installedAbsPaths) {
|
|
1182
|
+
try {
|
|
1183
|
+
if (existsSync4(absPath)) {
|
|
1184
|
+
const rel = relative2(installPath, absPath);
|
|
1185
|
+
checksums[rel] = computeChecksum(absPath);
|
|
1186
|
+
}
|
|
1187
|
+
} catch {
|
|
1188
|
+
}
|
|
1189
|
+
}
|
|
1190
|
+
const entry = {
|
|
1191
|
+
name,
|
|
1192
|
+
version: featureInfo.version,
|
|
1193
|
+
target: featureInfo.target,
|
|
1194
|
+
installPath,
|
|
1195
|
+
installedAt: (/* @__PURE__ */ new Date()).toISOString(),
|
|
1196
|
+
checksums
|
|
1197
|
+
};
|
|
1198
|
+
manifest = upsertFeature(manifest, entry);
|
|
1199
|
+
writeManifest(cwd, manifest);
|
|
1200
|
+
} else {
|
|
1201
|
+
this.context.stderr.write(chalk.red(` \u2717 ${message}
|
|
1202
|
+
`));
|
|
1203
|
+
anyFailure = true;
|
|
1204
|
+
}
|
|
1205
|
+
}
|
|
1206
|
+
return anyFailure ? 1 : 0;
|
|
1207
|
+
}
|
|
1208
|
+
};
|
|
1209
|
+
|
|
1210
|
+
// src/commands/uninstall.ts
|
|
1211
|
+
init_esm_shims();
|
|
1212
|
+
import { Command as Command2, Option as Option2 } from "clipanion";
|
|
1213
|
+
import chalk2 from "chalk";
|
|
1214
|
+
var UninstallCommand = class extends Command2 {
|
|
1215
|
+
static paths = [["uninstall"]];
|
|
1216
|
+
static usage = Command2.Usage({
|
|
1217
|
+
description: "Uninstall a feature from the current project",
|
|
1218
|
+
examples: [
|
|
1219
|
+
["Uninstall task-sync", "bitrix-skills uninstall task-sync"],
|
|
1220
|
+
["Uninstall bx-task", "bitrix-skills uninstall bx-task"]
|
|
1221
|
+
]
|
|
1222
|
+
});
|
|
1223
|
+
featureName = Option2.String({ required: true });
|
|
1224
|
+
async execute() {
|
|
1225
|
+
const cwd = process.cwd();
|
|
1226
|
+
const manifest = readManifest(cwd);
|
|
1227
|
+
const name = this.featureName;
|
|
1228
|
+
this.context.stdout.write(`
|
|
1229
|
+
Uninstalling ${chalk2.bold(name)}...
|
|
1230
|
+
`);
|
|
1231
|
+
if (name === "task-sync") {
|
|
1232
|
+
const { run: run4 } = await Promise.resolve().then(() => (init_uninstall(), uninstall_exports));
|
|
1233
|
+
const opts = {
|
|
1234
|
+
cwd,
|
|
1235
|
+
dryRun: false,
|
|
1236
|
+
force: false,
|
|
1237
|
+
noGithooks: false,
|
|
1238
|
+
noWorkflow: false,
|
|
1239
|
+
noSkill: false,
|
|
1240
|
+
keepDocs: false,
|
|
1241
|
+
quiet: false,
|
|
1242
|
+
removeSkill: false
|
|
1243
|
+
};
|
|
1244
|
+
const code = await run4(opts);
|
|
1245
|
+
if (code !== 0) {
|
|
1246
|
+
this.context.stderr.write(chalk2.red(`task-sync uninstall exited with code ${code}
|
|
1247
|
+
`));
|
|
1248
|
+
return code;
|
|
1249
|
+
}
|
|
1250
|
+
} else if (name === "bx-task") {
|
|
1251
|
+
const { uninstall: uninstall2 } = await Promise.resolve().then(() => (init_install2(), install_exports2));
|
|
1252
|
+
try {
|
|
1253
|
+
await uninstall2();
|
|
1254
|
+
this.context.stdout.write(chalk2.green(" \u2713 bx-task skill removed from ~/.claude/skills/\n"));
|
|
1255
|
+
} catch (err) {
|
|
1256
|
+
this.context.stderr.write(chalk2.red(` \u2717 bx-task: ${err.message}
|
|
1257
|
+
`));
|
|
1258
|
+
return 1;
|
|
1259
|
+
}
|
|
1260
|
+
} else {
|
|
1261
|
+
this.context.stderr.write(chalk2.red(`Unknown feature: ${name}
|
|
1262
|
+
`));
|
|
1263
|
+
this.context.stderr.write(chalk2.gray("Run: bitrix-skills list\n"));
|
|
1264
|
+
return 1;
|
|
1265
|
+
}
|
|
1266
|
+
if (manifest) {
|
|
1267
|
+
const features = manifest.features.filter((f) => f.name !== name);
|
|
1268
|
+
writeManifest(cwd, { ...manifest, features });
|
|
1269
|
+
this.context.stdout.write(chalk2.green(` \u2713 removed ${name} from .bitrix-tools.json
|
|
1270
|
+
`));
|
|
1271
|
+
}
|
|
1272
|
+
this.context.stdout.write("\n");
|
|
1273
|
+
return 0;
|
|
1274
|
+
}
|
|
1275
|
+
};
|
|
1276
|
+
|
|
1277
|
+
// src/commands/list.ts
|
|
1278
|
+
init_esm_shims();
|
|
1279
|
+
import { Command as Command3 } from "clipanion";
|
|
1280
|
+
import chalk3 from "chalk";
|
|
1281
|
+
var ListCommand = class extends Command3 {
|
|
1282
|
+
static paths = [["list"]];
|
|
1283
|
+
static usage = Command3.Usage({
|
|
1284
|
+
description: "List available features and installation status"
|
|
1285
|
+
});
|
|
1286
|
+
async execute() {
|
|
1287
|
+
const features = listFeatures();
|
|
1288
|
+
const manifest = readManifest(process.cwd());
|
|
1289
|
+
const installed = new Map(manifest?.features.map((f) => [f.name, f]) ?? []);
|
|
1290
|
+
if (features.length === 0) {
|
|
1291
|
+
this.context.stdout.write(chalk3.yellow("No features found.\n"));
|
|
1292
|
+
return 0;
|
|
1293
|
+
}
|
|
1294
|
+
this.context.stdout.write("\nAvailable features:\n");
|
|
1295
|
+
for (const f of features) {
|
|
1296
|
+
const entry = installed.get(f.name);
|
|
1297
|
+
const check = entry ? chalk3.green("\u2713") : chalk3.gray("\u25CB");
|
|
1298
|
+
const name = chalk3.bold(f.name.padEnd(12));
|
|
1299
|
+
const ver = chalk3.cyan(f.version.padEnd(14));
|
|
1300
|
+
const target = chalk3.gray(f.target.padEnd(8));
|
|
1301
|
+
const desc = f.description;
|
|
1302
|
+
const status = entry ? chalk3.green("[installed]") : chalk3.gray("[not installed]");
|
|
1303
|
+
this.context.stdout.write(` ${check} ${name} ${ver} ${target} ${desc} ${status}
|
|
1304
|
+
`);
|
|
1305
|
+
}
|
|
1306
|
+
this.context.stdout.write("\n");
|
|
1307
|
+
}
|
|
1308
|
+
};
|
|
1309
|
+
|
|
1310
|
+
// src/commands/verify.ts
|
|
1311
|
+
init_esm_shims();
|
|
1312
|
+
import { Command as Command4 } from "clipanion";
|
|
1313
|
+
import chalk4 from "chalk";
|
|
1314
|
+
import { existsSync as existsSync5 } from "fs";
|
|
1315
|
+
import { join as join5 } from "path";
|
|
1316
|
+
var VerifyCommand = class extends Command4 {
|
|
1317
|
+
static paths = [["verify"]];
|
|
1318
|
+
static usage = Command4.Usage({
|
|
1319
|
+
description: "Verify installed feature files match their recorded checksums"
|
|
1320
|
+
});
|
|
1321
|
+
async execute() {
|
|
1322
|
+
const cwd = process.cwd();
|
|
1323
|
+
const manifest = readManifest(cwd);
|
|
1324
|
+
if (!manifest || manifest.features.length === 0) {
|
|
1325
|
+
this.context.stderr.write(
|
|
1326
|
+
chalk4.yellow("No features installed. Run: bitrix-skills install\n")
|
|
1327
|
+
);
|
|
1328
|
+
return 1;
|
|
1329
|
+
}
|
|
1330
|
+
this.context.stdout.write("\nVerifying installed features...\n");
|
|
1331
|
+
let anyMismatch = false;
|
|
1332
|
+
for (const feature of manifest.features) {
|
|
1333
|
+
this.context.stdout.write(`
|
|
1334
|
+
${chalk4.bold(feature.name)} (${feature.version})
|
|
1335
|
+
`);
|
|
1336
|
+
this.context.stdout.write(chalk4.gray(` install path: ${feature.installPath}
|
|
1337
|
+
`));
|
|
1338
|
+
if (!existsSync5(feature.installPath)) {
|
|
1339
|
+
this.context.stderr.write(chalk4.red(` \u2717 install path missing: ${feature.installPath}
|
|
1340
|
+
`));
|
|
1341
|
+
anyMismatch = true;
|
|
1342
|
+
continue;
|
|
1343
|
+
}
|
|
1344
|
+
const stored = feature.checksums;
|
|
1345
|
+
if (Object.keys(stored).length === 0) {
|
|
1346
|
+
this.context.stdout.write(chalk4.gray(" \xB7 no checksums recorded (skipping file check)\n"));
|
|
1347
|
+
continue;
|
|
1348
|
+
}
|
|
1349
|
+
for (const [rel, expectedHash] of Object.entries(stored)) {
|
|
1350
|
+
const absPath = join5(feature.installPath, rel);
|
|
1351
|
+
if (!existsSync5(absPath)) {
|
|
1352
|
+
this.context.stderr.write(chalk4.red(` \u2717 missing: ${rel}
|
|
1353
|
+
`));
|
|
1354
|
+
anyMismatch = true;
|
|
1355
|
+
} else {
|
|
1356
|
+
try {
|
|
1357
|
+
const actualHash = computeChecksum(absPath);
|
|
1358
|
+
if (actualHash !== expectedHash) {
|
|
1359
|
+
this.context.stderr.write(chalk4.red(` \u2717 modified: ${rel}
|
|
1360
|
+
`));
|
|
1361
|
+
anyMismatch = true;
|
|
1362
|
+
} else {
|
|
1363
|
+
this.context.stdout.write(chalk4.green(` \u2713 ${rel}
|
|
1364
|
+
`));
|
|
1365
|
+
}
|
|
1366
|
+
} catch (err) {
|
|
1367
|
+
this.context.stderr.write(chalk4.red(` \u2717 read error: ${rel}: ${err.message}
|
|
1368
|
+
`));
|
|
1369
|
+
anyMismatch = true;
|
|
1370
|
+
}
|
|
1371
|
+
}
|
|
1372
|
+
}
|
|
1373
|
+
}
|
|
1374
|
+
this.context.stdout.write("\n");
|
|
1375
|
+
if (anyMismatch) {
|
|
1376
|
+
this.context.stderr.write(
|
|
1377
|
+
chalk4.red("Verification failed \u2014 run: bitrix-skills update\n")
|
|
1378
|
+
);
|
|
1379
|
+
return 1;
|
|
1380
|
+
}
|
|
1381
|
+
this.context.stdout.write(chalk4.green("All features verified OK.\n"));
|
|
1382
|
+
return 0;
|
|
1383
|
+
}
|
|
1384
|
+
};
|
|
1385
|
+
|
|
1386
|
+
// src/commands/update.ts
|
|
1387
|
+
init_esm_shims();
|
|
1388
|
+
import { Command as Command5 } from "clipanion";
|
|
1389
|
+
import chalk5 from "chalk";
|
|
1390
|
+
import { existsSync as existsSync6 } from "fs";
|
|
1391
|
+
import { relative as relative3 } from "path";
|
|
1392
|
+
var UpdateCommand = class extends Command5 {
|
|
1393
|
+
static paths = [["update"]];
|
|
1394
|
+
static usage = Command5.Usage({
|
|
1395
|
+
description: "Re-install all managed features, forcing overwrite of managed files"
|
|
1396
|
+
});
|
|
1397
|
+
async execute() {
|
|
1398
|
+
const cwd = process.cwd();
|
|
1399
|
+
let manifest = readManifest(cwd);
|
|
1400
|
+
if (!manifest || manifest.features.length === 0) {
|
|
1401
|
+
this.context.stderr.write(
|
|
1402
|
+
chalk5.yellow("No features installed. Run: bitrix-skills install\n")
|
|
1403
|
+
);
|
|
1404
|
+
return 1;
|
|
1405
|
+
}
|
|
1406
|
+
this.context.stdout.write("\nUpdating installed features...\n");
|
|
1407
|
+
let anyFailure = false;
|
|
1408
|
+
for (const feature of manifest.features) {
|
|
1409
|
+
this.context.stdout.write(`
|
|
1410
|
+
Updating ${chalk5.bold(feature.name)}...
|
|
1411
|
+
`);
|
|
1412
|
+
let installedAbsPaths = [];
|
|
1413
|
+
if (feature.name === "task-sync") {
|
|
1414
|
+
const { run: run4 } = await Promise.resolve().then(() => (init_install(), install_exports));
|
|
1415
|
+
const opts = {
|
|
1416
|
+
cwd,
|
|
1417
|
+
dryRun: false,
|
|
1418
|
+
force: true,
|
|
1419
|
+
noGithooks: false,
|
|
1420
|
+
noWorkflow: false,
|
|
1421
|
+
noSkill: false,
|
|
1422
|
+
keepDocs: false,
|
|
1423
|
+
quiet: false,
|
|
1424
|
+
removeSkill: false
|
|
1425
|
+
};
|
|
1426
|
+
const code = await run4(opts);
|
|
1427
|
+
if (code !== 0) {
|
|
1428
|
+
this.context.stderr.write(chalk5.red(` \u2717 task-sync update failed (exit ${code})
|
|
1429
|
+
`));
|
|
1430
|
+
anyFailure = true;
|
|
1431
|
+
continue;
|
|
1432
|
+
}
|
|
1433
|
+
try {
|
|
1434
|
+
const { loadManifest: loadManifest2 } = await Promise.resolve().then(() => (init_manifest(), manifest_exports));
|
|
1435
|
+
const { buildDestMap: buildDestMap2 } = await Promise.resolve().then(() => (init_dest_map(), dest_map_exports));
|
|
1436
|
+
const assetManifest = await loadManifest2();
|
|
1437
|
+
const dests = buildDestMap2(assetManifest, cwd);
|
|
1438
|
+
installedAbsPaths = dests.map((d) => d.destAbs).filter((p) => existsSync6(p));
|
|
1439
|
+
} catch {
|
|
1440
|
+
}
|
|
1441
|
+
} else if (feature.name === "bx-task") {
|
|
1442
|
+
const { install: install2 } = await Promise.resolve().then(() => (init_install2(), install_exports2));
|
|
1443
|
+
try {
|
|
1444
|
+
const result = await install2({ projectRoot: cwd, onConflict: "overwrite", force: true });
|
|
1445
|
+
installedAbsPaths = result.installedFiles;
|
|
1446
|
+
this.context.stdout.write(
|
|
1447
|
+
chalk5.green(` \u2713 bx-task: ${result.installedFiles.length} updated
|
|
1448
|
+
`)
|
|
1449
|
+
);
|
|
1450
|
+
} catch (err) {
|
|
1451
|
+
this.context.stderr.write(
|
|
1452
|
+
chalk5.red(` \u2717 bx-task: ${err.message}
|
|
1453
|
+
`)
|
|
1454
|
+
);
|
|
1455
|
+
anyFailure = true;
|
|
1456
|
+
continue;
|
|
1457
|
+
}
|
|
1458
|
+
} else {
|
|
1459
|
+
this.context.stderr.write(chalk5.yellow(` ! No update handler for: ${feature.name}
|
|
1460
|
+
`));
|
|
1461
|
+
anyFailure = true;
|
|
1462
|
+
continue;
|
|
1463
|
+
}
|
|
1464
|
+
const checksums = {};
|
|
1465
|
+
for (const absPath of installedAbsPaths) {
|
|
1466
|
+
try {
|
|
1467
|
+
if (existsSync6(absPath)) {
|
|
1468
|
+
const rel = relative3(feature.installPath, absPath);
|
|
1469
|
+
checksums[rel] = computeChecksum(absPath);
|
|
1470
|
+
}
|
|
1471
|
+
} catch {
|
|
1472
|
+
}
|
|
1473
|
+
}
|
|
1474
|
+
const updatedFeatures = manifest.features.map(
|
|
1475
|
+
(f) => f.name === feature.name ? { ...f, checksums, installedAt: (/* @__PURE__ */ new Date()).toISOString() } : f
|
|
1476
|
+
);
|
|
1477
|
+
const nextManifest = { ...manifest, features: updatedFeatures };
|
|
1478
|
+
writeManifest(cwd, nextManifest);
|
|
1479
|
+
manifest = nextManifest;
|
|
1480
|
+
this.context.stdout.write(chalk5.green(` \u2713 ${feature.name} updated
|
|
1481
|
+
`));
|
|
1482
|
+
}
|
|
1483
|
+
this.context.stdout.write("\n");
|
|
1484
|
+
return anyFailure ? 1 : 0;
|
|
1485
|
+
}
|
|
1486
|
+
};
|
|
1487
|
+
|
|
1488
|
+
// src/cli.ts
|
|
1489
|
+
var require2 = createRequire(import.meta.url);
|
|
1490
|
+
var pkg = require2("../package.json");
|
|
1491
|
+
var version = pkg.version ?? "0.0.0";
|
|
1492
|
+
var cli = new Cli({
|
|
1493
|
+
binaryLabel: "bitrix-skills",
|
|
1494
|
+
binaryName: "bitrix-skills",
|
|
1495
|
+
binaryVersion: version,
|
|
1496
|
+
enableCapture: false
|
|
1497
|
+
});
|
|
1498
|
+
cli.register(InstallCommand);
|
|
1499
|
+
cli.register(UninstallCommand);
|
|
1500
|
+
cli.register(ListCommand);
|
|
1501
|
+
cli.register(VerifyCommand);
|
|
1502
|
+
cli.register(UpdateCommand);
|
|
1503
|
+
cli.register(Builtins.HelpCommand);
|
|
1504
|
+
cli.register(Builtins.VersionCommand);
|
|
1505
|
+
function run3(args) {
|
|
1506
|
+
return cli.runExit(args);
|
|
1507
|
+
}
|
|
1508
|
+
export {
|
|
1509
|
+
run3 as run
|
|
1510
|
+
};
|