@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.
Files changed (77) hide show
  1. package/CHANGELOG.md +169 -0
  2. package/LICENSE +21 -0
  3. package/README.md +83 -0
  4. package/bin/bitrix-skills.js +3 -0
  5. package/dist/cli.js +1510 -0
  6. package/dist/features/bx-task/install.js +111 -0
  7. package/dist/features/task-sync/index.js +1053 -0
  8. package/package.json +69 -0
  9. package/src/features/bx/assets/SKILL.md +34 -0
  10. package/src/features/bx/feature.json +8 -0
  11. package/src/features/bx-calendar/assets/SKILL.md +61 -0
  12. package/src/features/bx-calendar/assets/availability.md +65 -0
  13. package/src/features/bx-calendar/assets/meeting.md +87 -0
  14. package/src/features/bx-calendar/assets/reminder.md +71 -0
  15. package/src/features/bx-calendar/assets/sync.md +70 -0
  16. package/src/features/bx-calendar/feature.json +10 -0
  17. package/src/features/bx-crm/assets/SKILL.md +59 -0
  18. package/src/features/bx-crm/assets/commerce.md +96 -0
  19. package/src/features/bx-crm/assets/onboard.md +127 -0
  20. package/src/features/bx-crm/assets/report.md +98 -0
  21. package/src/features/bx-crm/assets/research.md +71 -0
  22. package/src/features/bx-crm/feature.json +10 -0
  23. package/src/features/bx-task/assets/SKILL.md +148 -0
  24. package/src/features/bx-task/assets/lib/bx-api.sh +39 -0
  25. package/src/features/bx-task/assets/lib/bx-checklist.sh +127 -0
  26. package/src/features/bx-task/assets/lib/bx-resolve-task.sh +41 -0
  27. package/src/features/bx-task/assets/lib/bx-state.sh +131 -0
  28. package/src/features/bx-task/assets/lib/bx-tasks.sh +109 -0
  29. package/src/features/bx-task/assets/references/bootstrap.md +184 -0
  30. package/src/features/bx-task/assets/references/feature.md +97 -0
  31. package/src/features/bx-task/assets/references/init-templates/cli-tool.md +47 -0
  32. package/src/features/bx-task/assets/references/init-templates/generic.md +31 -0
  33. package/src/features/bx-task/assets/references/init-templates/library.md +45 -0
  34. package/src/features/bx-task/assets/references/init-templates/monorepo.md +38 -0
  35. package/src/features/bx-task/assets/references/init-templates/npm-package.md +40 -0
  36. package/src/features/bx-task/assets/references/init-templates/web-app.md +46 -0
  37. package/src/features/bx-task/assets/references/init.md +107 -0
  38. package/src/features/bx-task/assets/references/roadmap.md +93 -0
  39. package/src/features/bx-task/assets/references/summary.md +269 -0
  40. package/src/features/bx-task/assets/references/sync.md +104 -0
  41. package/src/features/bx-task/assets/references/time-log.md +214 -0
  42. package/src/features/bx-task/feature.json +10 -0
  43. package/src/features/bx-task/install.ts +117 -0
  44. package/src/features/task-sync/assets/docs/bitrix-task-reference.md +318 -0
  45. package/src/features/task-sync/assets/docs/bitrix-task-sync.md +254 -0
  46. package/src/features/task-sync/assets/githooks/commit-msg +44 -0
  47. package/src/features/task-sync/assets/githooks/install.sh +15 -0
  48. package/src/features/task-sync/assets/manifest.json +108 -0
  49. package/src/features/task-sync/assets/rules/00-bitrix-task-sync.md +161 -0
  50. package/src/features/task-sync/assets/scripts/bitrix-attach-files.sh +55 -0
  51. package/src/features/task-sync/assets/scripts/bitrix-lib.sh +540 -0
  52. package/src/features/task-sync/assets/scripts/bitrix-render-digest.sh +116 -0
  53. package/src/features/task-sync/assets/scripts/bitrix-session-check.sh +51 -0
  54. package/src/features/task-sync/assets/scripts/bitrix-session-sync.sh +89 -0
  55. package/src/features/task-sync/assets/scripts/bitrix-skill-end.sh +165 -0
  56. package/src/features/task-sync/assets/scripts/bitrix-skill-start.sh +58 -0
  57. package/src/features/task-sync/assets/scripts/lib/bb-formatter.sh +110 -0
  58. package/src/features/task-sync/assets/scripts/lib/bitrix-lib.sh +540 -0
  59. package/src/features/task-sync/assets/scripts/lib/time-helpers.sh +57 -0
  60. package/src/features/task-sync/assets/workflows/bitrix-sync.yml +85 -0
  61. package/src/features/task-sync/commands/install.ts +296 -0
  62. package/src/features/task-sync/commands/uninstall.ts +189 -0
  63. package/src/features/task-sync/commands/update.ts +11 -0
  64. package/src/features/task-sync/commands/verify.ts +141 -0
  65. package/src/features/task-sync/feature.json +12 -0
  66. package/src/features/task-sync/index.ts +121 -0
  67. package/src/features/task-sync/lib/dest-map.ts +96 -0
  68. package/src/features/task-sync/lib/drift-check.ts +47 -0
  69. package/src/features/task-sync/lib/file-ops.ts +36 -0
  70. package/src/features/task-sync/lib/manifest.ts +66 -0
  71. package/src/features/task-sync/lib/project-root.ts +38 -0
  72. package/src/features/task-sync/lib/settings-merge.ts +112 -0
  73. package/src/features/task-sync/lib/skill-refs.ts +106 -0
  74. package/src/features/task-sync/lib/task-id-finder.ts +31 -0
  75. package/src/features/task-sync/lib/token-extractor.ts +52 -0
  76. package/src/features/task-sync/lib/version.ts +36 -0
  77. package/src/features/task-sync/types.ts +40 -0
@@ -0,0 +1,1053 @@
1
+ #!/usr/bin/env node
2
+
3
+ // src/features/task-sync/index.ts
4
+ import { parseArgs } from "util";
5
+ import kleur4 from "kleur";
6
+
7
+ // src/features/task-sync/lib/version.ts
8
+ import { readFile } from "fs/promises";
9
+ import path from "path";
10
+ import { fileURLToPath } from "url";
11
+ var __dirname = path.dirname(fileURLToPath(import.meta.url));
12
+ var cached = null;
13
+ async function getVersion() {
14
+ if (cached) return cached;
15
+ const candidates = [
16
+ path.resolve(__dirname, "../package.json"),
17
+ path.resolve(__dirname, "../../package.json"),
18
+ path.resolve(__dirname, "../../../package.json"),
19
+ path.resolve(__dirname, "../../../../package.json")
20
+ ];
21
+ for (const p of candidates) {
22
+ try {
23
+ const raw = await readFile(p, "utf8");
24
+ const pkg = JSON.parse(raw);
25
+ if (pkg && typeof pkg.version === "string" && pkg.name?.includes("bitrix-skills")) {
26
+ const v = pkg.version;
27
+ cached = v;
28
+ return v;
29
+ }
30
+ } catch {
31
+ }
32
+ }
33
+ const fallback = "0.0.0-unknown";
34
+ cached = fallback;
35
+ return fallback;
36
+ }
37
+
38
+ // src/features/task-sync/commands/install.ts
39
+ import { copyFile, chmod as chmod2 } from "fs/promises";
40
+
41
+ // src/lib/fs-safety.ts
42
+ import { lstat } from "fs/promises";
43
+ import path2 from "path";
44
+ async function assertNotSymlink(destPath) {
45
+ try {
46
+ const stat = await lstat(destPath);
47
+ if (stat.isSymbolicLink()) {
48
+ throw new Error(
49
+ `Refusing to overwrite symlink: ${destPath}. Remove it manually then re-run install.`
50
+ );
51
+ }
52
+ } catch (err) {
53
+ if (err.code === "ENOENT") return;
54
+ throw err;
55
+ }
56
+ }
57
+ function assertContainedIn(destAbs, allowedRoot, label) {
58
+ const root = allowedRoot.endsWith(path2.sep) ? allowedRoot : allowedRoot + path2.sep;
59
+ if (!destAbs.startsWith(root)) {
60
+ throw new Error(
61
+ `"${label}" resolves outside allowed root (${root}). Refusing \u2014 possible path traversal.`
62
+ );
63
+ }
64
+ }
65
+
66
+ // src/features/task-sync/commands/install.ts
67
+ import path8 from "path";
68
+ import kleur from "kleur";
69
+ import { execa } from "execa";
70
+
71
+ // src/features/task-sync/lib/project-root.ts
72
+ import path4 from "path";
73
+
74
+ // src/features/task-sync/lib/file-ops.ts
75
+ import { createHash } from "crypto";
76
+ import { readFile as readFile2, access, mkdir, writeFile, rename, chmod } from "fs/promises";
77
+ import { constants } from "fs";
78
+ import path3 from "path";
79
+ async function sha256File(filePath) {
80
+ const buf = await readFile2(filePath);
81
+ return createHash("sha256").update(buf).digest("hex");
82
+ }
83
+ async function fileExists(filePath) {
84
+ try {
85
+ await access(filePath, constants.F_OK);
86
+ return true;
87
+ } catch {
88
+ return false;
89
+ }
90
+ }
91
+ async function ensureDir(dirPath) {
92
+ await mkdir(dirPath, { recursive: true });
93
+ }
94
+ async function atomicWrite(filePath, content, mode = 420) {
95
+ await ensureDir(path3.dirname(filePath));
96
+ const tmp = `${filePath}.${process.pid}.${Date.now()}.tmp`;
97
+ await writeFile(tmp, content, { mode });
98
+ await chmod(tmp, mode);
99
+ await rename(tmp, filePath);
100
+ }
101
+
102
+ // src/features/task-sync/lib/project-root.ts
103
+ var ROOT_MARKERS = [".git", "pnpm-workspace.yaml"];
104
+ async function detectProjectRoot(startDir) {
105
+ let dir = path4.resolve(startDir);
106
+ const root = path4.parse(dir).root;
107
+ while (true) {
108
+ for (const marker of ROOT_MARKERS) {
109
+ if (await fileExists(path4.join(dir, marker))) {
110
+ return dir;
111
+ }
112
+ }
113
+ const parent = path4.dirname(dir);
114
+ if (parent === dir || dir === root) {
115
+ break;
116
+ }
117
+ dir = parent;
118
+ }
119
+ dir = path4.resolve(startDir);
120
+ while (true) {
121
+ if (await fileExists(path4.join(dir, "package.json"))) {
122
+ return dir;
123
+ }
124
+ const parent = path4.dirname(dir);
125
+ if (parent === dir) break;
126
+ dir = parent;
127
+ }
128
+ return path4.resolve(startDir);
129
+ }
130
+ async function hasGitDir(rootDir) {
131
+ return fileExists(path4.join(rootDir, ".git"));
132
+ }
133
+
134
+ // src/features/task-sync/lib/manifest.ts
135
+ import { readFile as readFile3 } from "fs/promises";
136
+ import { existsSync } from "fs";
137
+ import path5 from "path";
138
+ import { fileURLToPath as fileURLToPath2 } from "url";
139
+ var __dirname3 = path5.dirname(fileURLToPath2(import.meta.url));
140
+ function resolveAssetsDir() {
141
+ const candidates = [
142
+ // From dist/ (cli.js context): ../src/features/task-sync/assets
143
+ path5.resolve(__dirname3, "../src/features/task-sync/assets"),
144
+ // From dist/features/task-sync/ (index.js context): ../../../src/features/task-sync/assets
145
+ path5.resolve(__dirname3, "../../../src/features/task-sync/assets"),
146
+ // Dev: src/features/task-sync/lib/ → ../assets
147
+ path5.resolve(__dirname3, "../assets"),
148
+ // Legacy fallbacks (should not match before correct paths above)
149
+ path5.resolve(__dirname3, "../../assets"),
150
+ path5.resolve(__dirname3, "../../../assets"),
151
+ path5.resolve(__dirname3, "../../../../assets")
152
+ ];
153
+ for (const c of candidates) {
154
+ if (existsSync(path5.join(c, "manifest.json"))) {
155
+ return c;
156
+ }
157
+ }
158
+ return candidates[0];
159
+ }
160
+ async function loadManifest() {
161
+ const dir = resolveAssetsDir();
162
+ const manifestPath = path5.join(dir, "manifest.json");
163
+ if (!await fileExists(manifestPath)) {
164
+ throw new Error(`assets/manifest.json not found at ${manifestPath} (was 'pnpm build' run?)`);
165
+ }
166
+ const raw = await readFile3(manifestPath, "utf8");
167
+ let parsed;
168
+ try {
169
+ parsed = JSON.parse(raw);
170
+ } catch (err) {
171
+ throw new Error(`malformed manifest.json: ${err.message}`);
172
+ }
173
+ validateManifest(parsed);
174
+ return parsed;
175
+ }
176
+ function validateManifest(obj) {
177
+ if (!obj || typeof obj !== "object") throw new Error("manifest must be an object");
178
+ const m = obj;
179
+ if (typeof m["version"] !== "string") throw new Error("manifest.version must be string");
180
+ if (!Array.isArray(m["files"])) throw new Error("manifest.files must be array");
181
+ for (const f of m["files"]) {
182
+ if (!f || typeof f !== "object") throw new Error("manifest.files[] entry must be object");
183
+ const e = f;
184
+ if (typeof e["src"] !== "string") throw new Error("manifest entry.src must be string");
185
+ if (typeof e["sha256"] !== "string") throw new Error("manifest entry.sha256 must be string");
186
+ if (typeof e["size"] !== "number") throw new Error("manifest entry.size must be number");
187
+ if (typeof e["mode"] !== "number") throw new Error("manifest entry.mode must be number");
188
+ }
189
+ }
190
+
191
+ // src/features/task-sync/lib/dest-map.ts
192
+ import path6 from "path";
193
+ import os from "os";
194
+ function buildDestMap(manifest, cwd) {
195
+ const out = [];
196
+ const home = os.homedir();
197
+ const skillsRoot = path6.join(home, ".claude", "skills");
198
+ for (const entry of manifest.files) {
199
+ const src = entry.src;
200
+ if (src.startsWith("scripts/")) {
201
+ const name = src.slice("scripts/".length);
202
+ const destRel = path6.posix.join(".claude/scripts", name);
203
+ out.push({
204
+ manifestEntry: entry,
205
+ destAbs: path6.resolve(cwd, destRel),
206
+ destRel,
207
+ category: "script"
208
+ });
209
+ } else if (src.startsWith("githooks/")) {
210
+ const name = src.slice("githooks/".length);
211
+ const destRel = path6.posix.join(".githooks", name);
212
+ out.push({
213
+ manifestEntry: entry,
214
+ destAbs: path6.resolve(cwd, destRel),
215
+ destRel,
216
+ category: "githook",
217
+ optionalFlag: "noGithooks"
218
+ });
219
+ } else if (src.startsWith("rules/")) {
220
+ const name = src.slice("rules/".length);
221
+ const destRel = path6.posix.join(".claude/rules", name);
222
+ out.push({
223
+ manifestEntry: entry,
224
+ destAbs: path6.resolve(cwd, destRel),
225
+ destRel,
226
+ category: "rule"
227
+ });
228
+ } else if (src.startsWith("docs/")) {
229
+ const name = src.slice("docs/".length);
230
+ const destRel = path6.posix.join("docs", name);
231
+ out.push({
232
+ manifestEntry: entry,
233
+ destAbs: path6.resolve(cwd, destRel),
234
+ destRel,
235
+ category: "doc"
236
+ });
237
+ } else if (src.startsWith("workflows/")) {
238
+ const name = src.slice("workflows/".length);
239
+ const destRel = path6.posix.join(".github/workflows", name);
240
+ out.push({
241
+ manifestEntry: entry,
242
+ destAbs: path6.resolve(cwd, destRel),
243
+ destRel,
244
+ category: "workflow",
245
+ optionalFlag: "noWorkflow"
246
+ });
247
+ } else if (src.startsWith("skill/")) {
248
+ const name = src.slice("skill/".length);
249
+ const destAbs = path6.resolve(home, ".claude", "skills", "bitrix-sync-install", name);
250
+ const destRel = path6.posix.join("~/.claude/skills/bitrix-sync-install", name);
251
+ out.push({
252
+ manifestEntry: entry,
253
+ destAbs,
254
+ destRel,
255
+ category: "skill",
256
+ optionalFlag: "noSkill"
257
+ });
258
+ }
259
+ }
260
+ for (const e of out) {
261
+ const allowedRoot = e.category === "skill" ? skillsRoot : cwd;
262
+ assertContainedIn(e.destAbs, allowedRoot, e.manifestEntry.src);
263
+ }
264
+ return out;
265
+ }
266
+
267
+ // src/features/task-sync/lib/settings-merge.ts
268
+ import { readFile as readFile4 } from "fs/promises";
269
+ import deepmerge from "deepmerge";
270
+ var BITRIX_HOOKS_TEMPLATE = {
271
+ hooks: {
272
+ SessionStart: [
273
+ { hooks: [{ type: "command", command: "bash .claude/scripts/bitrix-session-check.sh" }] }
274
+ ],
275
+ PreToolUse: [
276
+ { matcher: "Skill", hooks: [{ type: "command", command: "bash .claude/scripts/bitrix-skill-start.sh" }] }
277
+ ],
278
+ PostToolUse: [
279
+ { matcher: "Skill", hooks: [{ type: "command", command: "bash .claude/scripts/bitrix-skill-end.sh" }] }
280
+ ],
281
+ Stop: [
282
+ { hooks: [{ type: "command", command: "bash .claude/scripts/bitrix-session-sync.sh" }] }
283
+ ]
284
+ }
285
+ };
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 readFile4(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
+
346
+ // src/features/task-sync/lib/skill-refs.ts
347
+ import { readFile as readFile5, writeFile as writeFile2, mkdir as mkdir2, rm, access as access2 } from "fs/promises";
348
+ import path7 from "path";
349
+ import os2 from "os";
350
+ function getSkillDir() {
351
+ return path7.join(os2.homedir(), ".claude/skills/bitrix-sync-install");
352
+ }
353
+ function getRefsFile() {
354
+ return path7.join(getSkillDir(), ".refs.json");
355
+ }
356
+ async function loadRefs() {
357
+ try {
358
+ const raw = await readFile5(getRefsFile(), "utf8");
359
+ const parsed = JSON.parse(raw);
360
+ if (parsed && typeof parsed === "object" && Array.isArray(parsed.projects)) {
361
+ return { version: 1, projects: [...parsed.projects] };
362
+ }
363
+ } catch {
364
+ }
365
+ return { version: 1, projects: [] };
366
+ }
367
+ async function saveRefs(refs) {
368
+ await mkdir2(getSkillDir(), { recursive: true });
369
+ await writeFile2(getRefsFile(), JSON.stringify(refs, null, 2) + "\n", "utf8");
370
+ }
371
+ async function addProjectRef(projectPath) {
372
+ const abs = path7.resolve(projectPath);
373
+ const refs = await loadRefs();
374
+ if (!refs.projects.includes(abs)) {
375
+ refs.projects.push(abs);
376
+ await saveRefs(refs);
377
+ }
378
+ return refs.projects;
379
+ }
380
+ async function removeProjectRef(projectPath) {
381
+ const abs = path7.resolve(projectPath);
382
+ const refs = await loadRefs();
383
+ const before = refs.projects.length;
384
+ refs.projects = refs.projects.filter((p) => p !== abs);
385
+ const removed = refs.projects.length < before;
386
+ if (refs.projects.length === 0) {
387
+ try {
388
+ await rm(getRefsFile(), { force: true });
389
+ } catch {
390
+ }
391
+ } else if (removed) {
392
+ await saveRefs(refs);
393
+ }
394
+ return { remaining: refs.projects.length, removed };
395
+ }
396
+ async function getRefs() {
397
+ const refs = await loadRefs();
398
+ return refs.projects;
399
+ }
400
+ async function clearAllRefs() {
401
+ try {
402
+ await rm(getSkillDir(), { recursive: true, force: true });
403
+ } catch {
404
+ }
405
+ }
406
+
407
+ // src/features/task-sync/commands/install.ts
408
+ async function installFile(entry, ctx) {
409
+ const srcAbs = path8.join(ctx.assetsDir, entry.manifestEntry.src);
410
+ const destAbs = entry.destAbs;
411
+ await assertNotSymlink(destAbs);
412
+ if (await fileExists(destAbs)) {
413
+ const destSha = await sha256File(destAbs);
414
+ if (destSha === entry.manifestEntry.sha256) {
415
+ return "skipped";
416
+ }
417
+ if (!ctx.opts.force) {
418
+ return "skipped";
419
+ }
420
+ if (ctx.opts.dryRun) {
421
+ console.log(` ${kleur.yellow("[dry-run]")} overwrite ${entry.destRel}`);
422
+ return "planned";
423
+ }
424
+ await ensureDir(path8.dirname(destAbs));
425
+ await copyFile(srcAbs, destAbs);
426
+ await chmod2(destAbs, entry.manifestEntry.mode);
427
+ return "overwritten";
428
+ }
429
+ if (ctx.opts.dryRun) {
430
+ console.log(` ${kleur.yellow("[dry-run]")} copy \u2192 ${entry.destRel}`);
431
+ return "planned";
432
+ }
433
+ await ensureDir(path8.dirname(destAbs));
434
+ await copyFile(srcAbs, destAbs);
435
+ await chmod2(destAbs, entry.manifestEntry.mode);
436
+ return "copied";
437
+ }
438
+ async function installManifestFiles(entries, ctx) {
439
+ const stats = {
440
+ copied: 0,
441
+ skipped: 0,
442
+ overwritten: 0,
443
+ planned: 0,
444
+ filtered: 0,
445
+ warnings: []
446
+ };
447
+ for (const entry of entries) {
448
+ if (entry.optionalFlag && ctx.opts[entry.optionalFlag]) {
449
+ stats.filtered += 1;
450
+ continue;
451
+ }
452
+ const result = await installFile(entry, ctx);
453
+ if (result === "skipped") {
454
+ const destSha = await fileExists(entry.destAbs) ? await sha256File(entry.destAbs) : null;
455
+ if (destSha && destSha !== entry.manifestEntry.sha256 && !ctx.opts.force) {
456
+ stats.warnings.push(`skipped (user-modified): ${entry.destRel}`);
457
+ }
458
+ }
459
+ stats[result] += 1;
460
+ }
461
+ return stats;
462
+ }
463
+ async function mergeSettingsStep(ctx) {
464
+ const settingsPath = path8.join(ctx.cwd, ".claude/settings.json");
465
+ const existing = await loadSettingsFile(settingsPath);
466
+ const merged = mergeSettings(existing, BITRIX_HOOKS_TEMPLATE);
467
+ const alreadyMerged = JSON.stringify(existing) === JSON.stringify(merged);
468
+ if (ctx.opts.dryRun) {
469
+ if (alreadyMerged) {
470
+ console.log(` ${kleur.yellow("[dry-run]")} settings.json: already up-to-date`);
471
+ } else {
472
+ console.log(` ${kleur.yellow("[dry-run]")} settings.json: would merge 4 Bitrix hooks`);
473
+ }
474
+ return { added: !alreadyMerged, alreadyMerged };
475
+ }
476
+ if (!alreadyMerged) {
477
+ await writeSettingsFile(settingsPath, merged);
478
+ }
479
+ return { added: !alreadyMerged, alreadyMerged };
480
+ }
481
+ async function ensureUfTokensTotal(cwd, stats) {
482
+ const webhookUrl = process.env["BITRIX_WEBHOOK_URL"];
483
+ if (!webhookUrl) return;
484
+ const libPath = path8.join(cwd, ".claude/scripts/bitrix-lib.sh");
485
+ if (!await fileExists(libPath)) return;
486
+ const script = `
487
+ set -e
488
+ source "$1"
489
+ 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}}}')")
490
+ err=$(echo "$result" | jq -r '.error // empty')
491
+ # ERROR_CORE = already exists \u2192 treat as success
492
+ if [[ -n "$err" && "$err" != "ERROR_CORE" ]]; then
493
+ echo "warn:$err" >&2
494
+ fi
495
+ exit 0
496
+ `;
497
+ try {
498
+ const result = await execa("bash", ["-c", script, "--", libPath], {
499
+ cwd,
500
+ reject: false,
501
+ env: { ...process.env }
502
+ });
503
+ if (result.stderr && !result.stderr.includes("ERROR_CORE")) {
504
+ const warnLine = (result.stderr.match(/warn:(.+)/) ?? [])[1];
505
+ if (warnLine) stats.warnings.push(`UF_AI_TOKENS_TOTAL: ${warnLine.trim()}`);
506
+ }
507
+ } catch {
508
+ stats.warnings.push("UF_AI_TOKENS_TOTAL: ensure skipped (bash error)");
509
+ }
510
+ }
511
+ async function runGitHooksInstall(ctx) {
512
+ const hooksScript = path8.join(ctx.cwd, ".githooks/install.sh");
513
+ if (!await fileExists(hooksScript)) {
514
+ return { run: false, warning: "githooks install.sh not found (skipped)" };
515
+ }
516
+ if (!await hasGitDir(ctx.cwd)) {
517
+ return { run: false, warning: "no .git/ in project root \u2014 skipped githooks setup" };
518
+ }
519
+ if (ctx.opts.dryRun) {
520
+ console.log(` ${kleur.yellow("[dry-run]")} would run: bash .githooks/install.sh`);
521
+ return { run: false };
522
+ }
523
+ try {
524
+ await chmod2(hooksScript, 493);
525
+ await execa("bash", [hooksScript], { cwd: ctx.cwd, stdio: "pipe" });
526
+ return { run: true };
527
+ } catch (err) {
528
+ return { run: false, warning: `githooks install failed: ${err.message}` };
529
+ }
530
+ }
531
+ async function run(opts) {
532
+ let cwd;
533
+ try {
534
+ cwd = await detectProjectRoot(opts.cwd);
535
+ } catch (err) {
536
+ console.error(kleur.red(`error: ${err.message}`));
537
+ return 2;
538
+ }
539
+ const assetsDir = resolveAssetsDir();
540
+ const ctx = { cwd, assetsDir, opts };
541
+ console.log("");
542
+ console.log(kleur.bold().cyan(" \u26A1 bitrix-skills task-sync install"));
543
+ console.log(kleur.gray(` project root: ${cwd}`));
544
+ console.log(kleur.gray(` assets: ${assetsDir}`));
545
+ if (opts.dryRun) console.log(kleur.yellow(" mode: --dry-run (no FS changes)"));
546
+ console.log("");
547
+ let manifest;
548
+ try {
549
+ manifest = await loadManifest();
550
+ } catch (err) {
551
+ console.error(kleur.red(`error: ${err.message}`));
552
+ return 1;
553
+ }
554
+ const dests = buildDestMap(manifest, cwd);
555
+ let stats;
556
+ try {
557
+ stats = await installManifestFiles(dests, ctx);
558
+ } catch (err) {
559
+ console.error(kleur.red(`copy error: ${err.message}`));
560
+ return 1;
561
+ }
562
+ let settingsResult;
563
+ try {
564
+ settingsResult = await mergeSettingsStep(ctx);
565
+ } catch (err) {
566
+ console.error(kleur.red(`settings merge error: ${err.message}`));
567
+ return 3;
568
+ }
569
+ if (!opts.noSkill && !opts.dryRun) {
570
+ await ensureUfTokensTotal(cwd, stats);
571
+ }
572
+ const githooksResult = opts.noGithooks ? { run: false, warning: "--no-githooks: skipped" } : await runGitHooksInstall(ctx);
573
+ let skillRefCount = null;
574
+ if (!opts.noSkill && !opts.dryRun) {
575
+ try {
576
+ const refs = await addProjectRef(cwd);
577
+ skillRefCount = refs.length;
578
+ } catch (err) {
579
+ stats.warnings.push(`skill ref registry: ${err.message}`);
580
+ }
581
+ }
582
+ console.log("");
583
+ if (opts.dryRun) {
584
+ const parts = [
585
+ `${stats.planned} would be copied/overwritten`,
586
+ `${stats.skipped} skipped`,
587
+ stats.filtered > 0 ? `${stats.filtered} filtered (--no-* flags)` : null
588
+ ].filter(Boolean);
589
+ console.log(kleur.yellow(` [dry-run] ${parts.join(", ")}`));
590
+ } else {
591
+ const parts = [
592
+ `${stats.copied} copied`,
593
+ stats.overwritten > 0 ? `${stats.overwritten} overwritten` : null,
594
+ `${stats.skipped} skipped`,
595
+ stats.filtered > 0 ? `${stats.filtered} filtered (--no-* flags)` : null
596
+ ].filter(Boolean);
597
+ console.log(kleur.green(` \u2713 files: ${parts.join(", ")}`));
598
+ }
599
+ if (settingsResult.alreadyMerged) {
600
+ console.log(kleur.gray(" \xB7 settings.json: already up-to-date"));
601
+ } else if (settingsResult.added && !opts.dryRun) {
602
+ console.log(kleur.green(" \u2713 settings.json: 4 Bitrix hooks merged"));
603
+ }
604
+ if (githooksResult.run) {
605
+ console.log(kleur.green(" \u2713 git hooks: core.hooksPath = .githooks (local)"));
606
+ } else if (githooksResult.warning) {
607
+ console.log(kleur.yellow(` ! ${githooksResult.warning}`));
608
+ }
609
+ if (skillRefCount !== null) {
610
+ const noun = skillRefCount === 1 ? "project" : "projects";
611
+ console.log(kleur.gray(` \xB7 skill: shared across ${skillRefCount} ${noun}`));
612
+ }
613
+ for (const w of stats.warnings) {
614
+ console.log(kleur.yellow(` ! ${w}`));
615
+ }
616
+ if (!opts.dryRun) {
617
+ console.log("");
618
+ console.log(kleur.bold(" Next steps:"));
619
+ console.log(" 1. Set TASK_ID in CLAUDE.md (see docs/bitrix-task-sync.md \xA72)");
620
+ console.log(" 2. Export BITRIX_WEBHOOK_URL in your shell rc");
621
+ console.log(" 3. Run: npx @synity/bitrix-skills verify");
622
+ console.log("");
623
+ }
624
+ return 0;
625
+ }
626
+
627
+ // src/features/task-sync/commands/uninstall.ts
628
+ import { unlink } from "fs/promises";
629
+ import path9 from "path";
630
+ import kleur2 from "kleur";
631
+ import { execa as execa2 } from "execa";
632
+ async function run2(opts) {
633
+ let cwd;
634
+ try {
635
+ cwd = await detectProjectRoot(opts.cwd);
636
+ } catch (err) {
637
+ console.error(kleur2.red(`error: ${err.message}`));
638
+ return 2;
639
+ }
640
+ console.log("");
641
+ console.log(kleur2.bold().cyan(" \u26A1 bitrix-skills task-sync uninstall"));
642
+ console.log(kleur2.gray(` project root: ${cwd}`));
643
+ if (opts.dryRun) console.log(kleur2.yellow(" mode: --dry-run (no FS changes)"));
644
+ console.log("");
645
+ let manifest;
646
+ try {
647
+ manifest = await loadManifest();
648
+ } catch (err) {
649
+ console.error(kleur2.red(`error: ${err.message}`));
650
+ return 1;
651
+ }
652
+ const dests = buildDestMap(manifest, cwd);
653
+ let removed = 0;
654
+ let preserved = 0;
655
+ let absent = 0;
656
+ let filtered = 0;
657
+ const warnings = [];
658
+ const cwdAbs = path9.resolve(cwd);
659
+ const refsBefore = await getRefs();
660
+ const otherProjects = refsBefore.filter((p) => p !== cwdAbs);
661
+ const isLastRegisteredReferrer = refsBefore.length > 0 && otherProjects.length === 0;
662
+ const shouldRemoveSkillFiles = !opts.noSkill && (opts.removeSkill || isLastRegisteredReferrer);
663
+ for (const d of dests) {
664
+ if (d.category === "doc" && opts.keepDocs) {
665
+ filtered += 1;
666
+ continue;
667
+ }
668
+ if (d.category === "skill" && !shouldRemoveSkillFiles) {
669
+ filtered += 1;
670
+ continue;
671
+ }
672
+ if (d.optionalFlag && opts[d.optionalFlag]) {
673
+ filtered += 1;
674
+ continue;
675
+ }
676
+ if (!await fileExists(d.destAbs)) {
677
+ absent += 1;
678
+ continue;
679
+ }
680
+ const actual = await sha256File(d.destAbs);
681
+ if (actual !== d.manifestEntry.sha256) {
682
+ preserved += 1;
683
+ warnings.push(`preserved (user-modified): ${d.destRel}`);
684
+ continue;
685
+ }
686
+ if (opts.dryRun) {
687
+ console.log(` ${kleur2.yellow("[dry-run]")} remove ${d.destRel}`);
688
+ removed += 1;
689
+ continue;
690
+ }
691
+ await unlink(d.destAbs);
692
+ removed += 1;
693
+ }
694
+ const settingsPath = path9.join(cwd, ".claude/settings.json");
695
+ let settingsChanged = false;
696
+ if (await fileExists(settingsPath)) {
697
+ try {
698
+ const existing = await loadSettingsFile(settingsPath);
699
+ const cleaned = removeBitrixHooks(existing);
700
+ const before = JSON.stringify(existing);
701
+ const after = JSON.stringify(cleaned);
702
+ if (before !== after) {
703
+ settingsChanged = true;
704
+ if (!opts.dryRun) {
705
+ await writeSettingsFile(settingsPath, cleaned);
706
+ }
707
+ }
708
+ } catch (err) {
709
+ warnings.push(`settings.json: ${err.message}`);
710
+ }
711
+ }
712
+ let skillRefRemaining = null;
713
+ if (!opts.noSkill && !opts.dryRun) {
714
+ try {
715
+ if (opts.removeSkill) {
716
+ await clearAllRefs();
717
+ skillRefRemaining = 0;
718
+ } else {
719
+ const result = await removeProjectRef(cwdAbs);
720
+ skillRefRemaining = result.remaining;
721
+ }
722
+ } catch (err) {
723
+ warnings.push(`skill ref registry: ${err.message}`);
724
+ }
725
+ }
726
+ let hooksPathUnset = false;
727
+ if (await hasGitDir(cwd)) {
728
+ try {
729
+ const { stdout } = await execa2("git", ["config", "--local", "--get", "core.hooksPath"], {
730
+ cwd,
731
+ reject: false
732
+ });
733
+ if (stdout.trim() === ".githooks") {
734
+ if (!opts.dryRun) {
735
+ await execa2("git", ["config", "--local", "--unset", "core.hooksPath"], { cwd, reject: false });
736
+ }
737
+ hooksPathUnset = true;
738
+ }
739
+ } catch {
740
+ }
741
+ }
742
+ console.log("");
743
+ const filteredSuffix = filtered > 0 ? `, ${filtered} filtered (--keep-docs / no --remove-skill / --no-*)` : "";
744
+ if (opts.dryRun) {
745
+ console.log(kleur2.yellow(` [dry-run] would remove ${removed} files, preserve ${preserved}, ${absent} already absent${filteredSuffix}`));
746
+ } else {
747
+ console.log(kleur2.green(` \u2713 files: ${removed} removed, ${preserved} preserved, ${absent} already absent${filteredSuffix}`));
748
+ }
749
+ if (settingsChanged) {
750
+ console.log(kleur2.green(" \u2713 settings.json: Bitrix hooks removed"));
751
+ } else {
752
+ console.log(kleur2.gray(" \xB7 settings.json: nothing to remove"));
753
+ }
754
+ if (hooksPathUnset) {
755
+ console.log(kleur2.green(" \u2713 git hooks: core.hooksPath unset"));
756
+ }
757
+ if (skillRefRemaining !== null) {
758
+ if (opts.removeSkill) {
759
+ console.log(kleur2.green(" \u2713 skill: ~/.claude/skills/bitrix-sync-install removed (--remove-skill)"));
760
+ } else if (skillRefRemaining === 0) {
761
+ console.log(kleur2.green(" \u2713 skill: last project unlinked, files removed"));
762
+ } else {
763
+ const noun = skillRefRemaining === 1 ? "project" : "projects";
764
+ console.log(
765
+ kleur2.gray(
766
+ ` \xB7 skill: preserved (still used by ${skillRefRemaining} other ${noun}); pass --remove-skill to force`
767
+ )
768
+ );
769
+ }
770
+ }
771
+ for (const w of warnings) {
772
+ console.log(kleur2.yellow(` ! ${w}`));
773
+ }
774
+ console.log("");
775
+ return 0;
776
+ }
777
+
778
+ // src/features/task-sync/commands/verify.ts
779
+ import path11 from "path";
780
+ import kleur3 from "kleur";
781
+ import { execa as execa3 } from "execa";
782
+
783
+ // src/features/task-sync/lib/drift-check.ts
784
+ async function checkDrift(manifest, cwd, opts = {}) {
785
+ const drifted = [];
786
+ const dests = buildDestMap(manifest, cwd);
787
+ for (const d of dests) {
788
+ if (d.optionalFlag && opts[d.optionalFlag]) continue;
789
+ if (!await fileExists(d.destAbs)) {
790
+ drifted.push({
791
+ path: d.destRel,
792
+ reason: "missing",
793
+ expected: d.manifestEntry.sha256,
794
+ actual: null
795
+ });
796
+ continue;
797
+ }
798
+ const actual = await sha256File(d.destAbs);
799
+ if (actual !== d.manifestEntry.sha256) {
800
+ drifted.push({
801
+ path: d.destRel,
802
+ reason: "modified",
803
+ expected: d.manifestEntry.sha256,
804
+ actual
805
+ });
806
+ }
807
+ }
808
+ return drifted;
809
+ }
810
+
811
+ // src/features/task-sync/lib/task-id-finder.ts
812
+ import { readFile as readFile6 } from "fs/promises";
813
+ import path10 from "path";
814
+ async function findTaskId(startDir) {
815
+ let dir = path10.resolve(startDir);
816
+ const root = path10.parse(dir).root;
817
+ while (true) {
818
+ const md = path10.join(dir, "CLAUDE.md");
819
+ if (await fileExists(md)) {
820
+ const content = await readFile6(md, "utf8");
821
+ const match = content.match(/^TASK_ID:\s*(\S+)/m);
822
+ if (match && match[1]) {
823
+ return match[1].replace(/\r$/, "");
824
+ }
825
+ }
826
+ const parent = path10.dirname(dir);
827
+ if (parent === dir || dir === root) break;
828
+ dir = parent;
829
+ }
830
+ return null;
831
+ }
832
+ function isValidTaskId(taskId) {
833
+ return /^\d+$/.test(taskId);
834
+ }
835
+
836
+ // src/features/task-sync/commands/verify.ts
837
+ function redactWebhook(text) {
838
+ let out = text;
839
+ const url = process.env["BITRIX_WEBHOOK_URL"];
840
+ if (url) {
841
+ out = out.split(url).join("<BITRIX_WEBHOOK_URL>");
842
+ out = out.replace(/\/rest\/\d+\/[A-Za-z0-9]+/g, "/rest/<redacted>");
843
+ }
844
+ return out;
845
+ }
846
+ async function run3(opts) {
847
+ let cwd;
848
+ try {
849
+ cwd = await detectProjectRoot(opts.cwd);
850
+ } catch (err) {
851
+ console.error(kleur3.red(`error: ${err.message}`));
852
+ return 2;
853
+ }
854
+ console.log("");
855
+ console.log(kleur3.bold().cyan(" \u26A1 bitrix-skills task-sync verify"));
856
+ console.log(kleur3.gray(` project root: ${cwd}`));
857
+ console.log("");
858
+ let manifest;
859
+ try {
860
+ manifest = await loadManifest();
861
+ } catch (err) {
862
+ console.error(kleur3.red(`error: ${err.message}`));
863
+ return 1;
864
+ }
865
+ const drift = await checkDrift(manifest, cwd, {
866
+ noGithooks: opts.noGithooks,
867
+ noWorkflow: opts.noWorkflow,
868
+ noSkill: opts.noSkill
869
+ });
870
+ if (drift.length > 0) {
871
+ console.error(kleur3.red(" \u2717 drift detected \u2014 run `bitrix-skills update` to fix:"));
872
+ for (const d of drift) {
873
+ console.error(` - ${d.path} (${d.reason})`);
874
+ }
875
+ return 4;
876
+ }
877
+ console.log(kleur3.green(" \u2713 manifest: all managed files match expected sha256"));
878
+ const taskId = await findTaskId(cwd);
879
+ if (!taskId) {
880
+ console.error(kleur3.red(" \u2717 TASK_ID not found in any CLAUDE.md (walk-up from project root)"));
881
+ console.error(kleur3.gray(" add to root or feature CLAUDE.md:\n ## Bitrix Task\n TASK_ID: 12345"));
882
+ return 1;
883
+ }
884
+ if (!isValidTaskId(taskId)) {
885
+ console.error(kleur3.red(` \u2717 TASK_ID "${taskId}" is not a valid numeric Bitrix task ID`));
886
+ console.error(kleur3.gray(" expected digits only (e.g. TASK_ID: 12345)"));
887
+ return 1;
888
+ }
889
+ console.log(kleur3.green(` \u2713 TASK_ID found: ${taskId}`));
890
+ if (!process.env["BITRIX_WEBHOOK_URL"]) {
891
+ console.error(kleur3.red(" \u2717 BITRIX_WEBHOOK_URL not set in env"));
892
+ console.error(kleur3.gray(' export BITRIX_WEBHOOK_URL="https://your.bitrix24.com/rest/USER_ID/TOKEN/"'));
893
+ return 2;
894
+ }
895
+ console.log(kleur3.green(" \u2713 BITRIX_WEBHOOK_URL is set"));
896
+ if (opts.quiet) {
897
+ console.log(kleur3.gray(" \xB7 --quiet: skipping live webhook test"));
898
+ console.log("");
899
+ return 0;
900
+ }
901
+ const libPath = path11.join(cwd, ".claude/scripts/bitrix-lib.sh");
902
+ if (!await fileExists(libPath)) {
903
+ console.error(kleur3.red(` \u2717 ${libPath} missing \u2014 run install first`));
904
+ return 1;
905
+ }
906
+ const stamp = (/* @__PURE__ */ new Date()).toISOString();
907
+ const message = `\u{1F527} [bitrix-skills verify] ${stamp}`;
908
+ const script = 'set -e; source "$1"; b24_comment "$2" "$3"';
909
+ const result = await execa3("bash", ["-c", script, "--", libPath, taskId, message], {
910
+ cwd,
911
+ reject: false
912
+ });
913
+ if (result.exitCode !== 0 || !/"result"/.test(result.stdout || "")) {
914
+ console.error(kleur3.red(" \u2717 webhook call failed"));
915
+ if (result.stderr) console.error(kleur3.gray(` stderr: ${redactWebhook(result.stderr).slice(0, 500)}`));
916
+ if (result.stdout) console.error(kleur3.gray(` stdout: ${redactWebhook(result.stdout).slice(0, 500)}`));
917
+ return 3;
918
+ }
919
+ console.log(kleur3.green(` \u2713 live webhook OK \u2014 comment posted to TASK_ID=${taskId}`));
920
+ const diskScript = `
921
+ set -e
922
+ source "$1"
923
+ result=$(b24_call "disk.storage.getlist" '{}')
924
+ err=$(echo "$result" | jq -r '.error // empty')
925
+ if [[ "$err" == *"insufficient_scope"* || "$err" == *"ACCESS_DENIED"* ]]; then
926
+ echo "missing_scope"
927
+ else
928
+ echo "ok"
929
+ fi
930
+ `;
931
+ const diskResult = await execa3("bash", ["-c", diskScript, "--", libPath], {
932
+ cwd,
933
+ reject: false
934
+ });
935
+ if ((diskResult.stdout || "").trim() === "missing_scope") {
936
+ console.log(kleur3.yellow(" ! disk scope missing \u2014 file-attach feature degraded"));
937
+ console.log(kleur3.gray(' add "disk" to webhook permissions to enable plan/brainstorm file sync'));
938
+ } else {
939
+ console.log(kleur3.green(" \u2713 disk scope OK \u2014 file-attach feature available"));
940
+ }
941
+ console.log("");
942
+ return 0;
943
+ }
944
+
945
+ // src/features/task-sync/commands/update.ts
946
+ async function run4(opts) {
947
+ return run({ ...opts, force: true });
948
+ }
949
+
950
+ // src/features/task-sync/index.ts
951
+ var USAGE = `
952
+ ${kleur4.bold("bitrix-skills")} \u2014 Synity Bitrix24 tooling CLI
953
+
954
+ ${kleur4.bold("Usage:")}
955
+ npx @synity/bitrix-skills <command> [options]
956
+
957
+ ${kleur4.bold("Commands:")}
958
+ install Copy hooks/scripts/docs + merge .claude/settings.json
959
+ uninstall Reverse install (removes managed files, cleans settings)
960
+ verify Smoke test live Bitrix webhook (requires TASK_ID + BITRIX_WEBHOOK_URL)
961
+ update Re-install managed files only (force overwrite, preserves user files)
962
+
963
+ ${kleur4.bold("Options:")}
964
+ --cwd=<path> Project root (default: auto-detect via .git/ or pnpm-workspace.yaml)
965
+ --dry-run Print planned actions, no FS changes
966
+ --force Overwrite user-modified managed files (install/update)
967
+ --no-githooks Skip .githooks/ + commit-msg setup (install)
968
+ --no-workflow Skip .github/workflows/bitrix-sync.yml (install)
969
+ --no-skill Skip ~/.claude/skills/bitrix-sync-install (install)
970
+ --keep-docs Preserve docs/bitrix-task-*.md (uninstall)
971
+ --remove-skill Also remove ~/.claude/skills/bitrix-sync-install (uninstall, default: preserve)
972
+ --quiet Skip live webhook call (verify \u2014 env + manifest only)
973
+ --version, -v Print version
974
+ --help, -h Print this help
975
+
976
+ ${kleur4.bold("Examples:")}
977
+ npx @synity/bitrix-skills install
978
+ npx @synity/bitrix-skills install --dry-run
979
+ npx @synity/bitrix-skills verify
980
+ npx @synity/bitrix-skills uninstall
981
+ `;
982
+ function parseCli() {
983
+ const { values, positionals } = parseArgs({
984
+ args: process.argv.slice(2),
985
+ allowPositionals: true,
986
+ options: {
987
+ cwd: { type: "string" },
988
+ "dry-run": { type: "boolean", default: false },
989
+ force: { type: "boolean", default: false },
990
+ "no-githooks": { type: "boolean", default: false },
991
+ "no-workflow": { type: "boolean", default: false },
992
+ "no-skill": { type: "boolean", default: false },
993
+ "keep-docs": { type: "boolean", default: false },
994
+ "remove-skill": { type: "boolean", default: false },
995
+ quiet: { type: "boolean", default: false },
996
+ version: { type: "boolean", short: "v", default: false },
997
+ help: { type: "boolean", short: "h", default: false }
998
+ }
999
+ });
1000
+ const opts = {
1001
+ cwd: values["cwd"] ?? process.cwd(),
1002
+ dryRun: values["dry-run"],
1003
+ force: values["force"],
1004
+ noGithooks: values["no-githooks"],
1005
+ noWorkflow: values["no-workflow"],
1006
+ noSkill: values["no-skill"],
1007
+ keepDocs: values["keep-docs"],
1008
+ removeSkill: values["remove-skill"],
1009
+ quiet: values["quiet"]
1010
+ };
1011
+ return {
1012
+ command: positionals[0],
1013
+ opts,
1014
+ help: values["help"],
1015
+ version: values["version"]
1016
+ };
1017
+ }
1018
+ async function main() {
1019
+ const { command, opts, help, version } = parseCli();
1020
+ if (version) {
1021
+ const v = await getVersion();
1022
+ console.log(v);
1023
+ return 0;
1024
+ }
1025
+ if (help) {
1026
+ console.log(USAGE);
1027
+ return 0;
1028
+ }
1029
+ if (!command) {
1030
+ console.log(USAGE);
1031
+ return 2;
1032
+ }
1033
+ switch (command) {
1034
+ case "install":
1035
+ return run(opts);
1036
+ case "uninstall":
1037
+ return run2(opts);
1038
+ case "verify":
1039
+ return run3(opts);
1040
+ case "update":
1041
+ return run4(opts);
1042
+ default:
1043
+ console.error(kleur4.red(`unknown command: ${command}`));
1044
+ console.log(USAGE);
1045
+ return 2;
1046
+ }
1047
+ }
1048
+ main().then((code) => process.exit(code)).catch((err) => {
1049
+ console.error(kleur4.red(`
1050
+ fatal: ${err instanceof Error ? err.message : String(err)}
1051
+ `));
1052
+ process.exit(1);
1053
+ });