@hungvv/super-ads-skills 0.0.0-stage → 1.0.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 (40) hide show
  1. package/README.md +42 -2
  2. package/bin/super-ads-skills.js +2 -0
  3. package/dist/cli.js +654 -0
  4. package/package.json +54 -4
  5. package/scripts/install-skills.ps1 +38 -0
  6. package/scripts/install-skills.sh +43 -0
  7. package/skills/README.md +59 -0
  8. package/skills/adspro-to-superads/SKILL.md +816 -0
  9. package/skills/adspro-to-superads/references/api-mapping.md +331 -0
  10. package/skills/adspro-to-superads/references/config-template.md +485 -0
  11. package/skills/adspro-to-superads/references/migration-pitfalls.md +201 -0
  12. package/skills/adspro-to-superads/references/troubleshooting.md +221 -0
  13. package/skills/adspro-to-superads/references/verification.md +168 -0
  14. package/skills/adspro-to-superads/references/wrapper-template.md +567 -0
  15. package/skills/super-ads-ab-test/SKILL.md +91 -0
  16. package/skills/super-ads-ab-test/references/firebase-handoff-template.md +54 -0
  17. package/skills/super-ads-adb-test/SKILL.md +107 -0
  18. package/skills/super-ads-adb-test/references/log-assertions.md +53 -0
  19. package/skills/super-ads-adb-test/references/sample-app-notes.md +27 -0
  20. package/skills/super-ads-adb-test/references/scenario-format.md +53 -0
  21. package/skills/super-ads-adb-test/references/scenarios-sample.md +45 -0
  22. package/skills/super-ads-add-placement/SKILL.md +128 -0
  23. package/skills/super-ads-add-placement/references/call-site-snippets.md +90 -0
  24. package/skills/super-ads-config/SKILL.md +132 -0
  25. package/skills/super-ads-config/references/patch-examples.md +162 -0
  26. package/skills/super-ads-debug/SKILL.md +109 -0
  27. package/skills/super-ads-debug/references/error-codes.md +112 -0
  28. package/skills/super-ads-debug/references/logging-setup.md +157 -0
  29. package/skills/super-ads-debug/references/symptom-playbook.md +59 -0
  30. package/skills/super-ads-debug-override/SKILL.md +123 -0
  31. package/skills/super-ads-debug-override/references/hook-implementation.md +73 -0
  32. package/skills/super-ads-integrate/SKILL.md +115 -0
  33. package/skills/super-ads-integrate/references/application-setup.md +61 -0
  34. package/skills/super-ads-integrate/references/gradle-setup.md +89 -0
  35. package/skills/super-ads-release-check/SKILL.md +92 -0
  36. package/skills/super-ads-release-check/references/firebase-ab-checklist.md +94 -0
  37. package/skills/super-ads-release-check/references/ga4-custom-definitions.md +34 -0
  38. package/skills/super-ads-show-rate/SKILL.md +92 -0
  39. package/skills/super-ads-show-rate/references/collect-snippet.md +59 -0
  40. package/skills/super-ads-show-rate/references/metrics.md +55 -0
package/dist/cli.js ADDED
@@ -0,0 +1,654 @@
1
+ // src/cli.ts
2
+ import { readFileSync as readFileSync3 } from "fs";
3
+ import path6 from "path";
4
+ import { fileURLToPath as fileURLToPath2 } from "url";
5
+ import { Command } from "commander";
6
+ import pc from "picocolors";
7
+
8
+ // src/agents.ts
9
+ import { existsSync } from "fs";
10
+ import os from "os";
11
+ import path from "path";
12
+ var TARGETS = {
13
+ claude: {
14
+ id: "claude",
15
+ project: path.join(".claude", "skills"),
16
+ global: path.join(os.homedir(), ".claude", "skills")
17
+ },
18
+ cursor: {
19
+ id: "cursor",
20
+ project: path.join(".cursor", "skills"),
21
+ global: path.join(os.homedir(), ".cursor", "skills")
22
+ },
23
+ agents: {
24
+ id: "agents",
25
+ project: path.join(".agents", "skills"),
26
+ global: path.join(os.homedir(), ".agents", "skills")
27
+ },
28
+ antigravity: {
29
+ id: "antigravity",
30
+ project: path.join(".agents", "skills"),
31
+ global: path.join(os.homedir(), ".gemini", "config", "skills")
32
+ },
33
+ // Global-only in practice: no project-level equivalent is known, so this reuses the same project
34
+ // path as `antigravity` (already shared with `agents`/Codex) rather than leaving it unspecified.
35
+ "antigravity-builtin": {
36
+ id: "antigravity-builtin",
37
+ project: path.join(".agents", "skills"),
38
+ global: path.join(os.homedir(), ".gemini", "antigravity-ide", "builtin", "skills")
39
+ }
40
+ };
41
+ var AGENTS = [
42
+ { id: "claude", label: "Claude Code", targets: ["claude"], hints: [".claude", "CLAUDE.md"] },
43
+ { id: "cursor", label: "Cursor", targets: ["cursor"], hints: [".cursor"] },
44
+ { id: "codex", label: "Codex", targets: ["agents"], hints: [".codex", ".agents", "AGENTS.md"] },
45
+ // No project file is unique to Antigravity yet (it shares `.agents/` with Codex), so it is never
46
+ // auto-detected — the user picks it explicitly with `--agent antigravity`. Two targets: its own
47
+ // `config/skills` plus the IDE's `builtin/skills` — both point at the same folder at project scope.
48
+ { id: "antigravity", label: "Antigravity", targets: ["antigravity", "antigravity-builtin"], hints: [] }
49
+ ];
50
+ var DEFAULT_AGENTS = AGENTS.map((a) => a.id);
51
+ function agentById(id) {
52
+ return AGENTS.find((a) => a.id === id);
53
+ }
54
+ function agentsForTarget(target) {
55
+ return AGENTS.filter((a) => a.targets.includes(target));
56
+ }
57
+ function targetsForAgents(agents) {
58
+ const seen = /* @__PURE__ */ new Set();
59
+ for (const id of agents) {
60
+ const agent = agentById(id);
61
+ agent?.targets.forEach((t) => seen.add(t));
62
+ }
63
+ return [...seen];
64
+ }
65
+ function targetRoot(target, opts) {
66
+ const spec = TARGETS[target];
67
+ return opts.global ? spec.global : path.join(opts.cwd, spec.project);
68
+ }
69
+ function targetLabel(target) {
70
+ return agentsForTarget(target).map((a) => a.label).join(" / ");
71
+ }
72
+
73
+ // src/installer.ts
74
+ import {
75
+ copyFileSync,
76
+ existsSync as existsSync2,
77
+ lstatSync,
78
+ mkdirSync as mkdirSync2,
79
+ readlinkSync,
80
+ readdirSync as readdirSync2,
81
+ rmSync,
82
+ rmdirSync,
83
+ statSync as statSync2,
84
+ symlinkSync,
85
+ unlinkSync as unlinkSync2
86
+ } from "fs";
87
+ import os3 from "os";
88
+ import path4 from "path";
89
+ import semver2 from "semver";
90
+
91
+ // src/manifest.ts
92
+ import { mkdirSync, readFileSync, unlinkSync, writeFileSync } from "fs";
93
+ import os2 from "os";
94
+ import path2 from "path";
95
+ var META_FILENAME = ".super-ads-skills-meta.json";
96
+ var PROJECT_MANIFEST = ".super-ads-skills.json";
97
+ function manifestPath(scope) {
98
+ if (!scope.global) return path2.join(scope.cwd, PROJECT_MANIFEST);
99
+ const base = process.env.XDG_CONFIG_HOME || path2.join(os2.homedir(), ".config");
100
+ return path2.join(base, "super-ads-skills", "manifest.json");
101
+ }
102
+ function emptyManifest(packageVersion) {
103
+ return { schemaVersion: 1, packageVersion, installed: {} };
104
+ }
105
+ function readManifest(scope, packageVersion) {
106
+ try {
107
+ const parsed = JSON.parse(readFileSync(manifestPath(scope), "utf8"));
108
+ if (parsed.schemaVersion !== 1) throw new Error(`Unsupported manifest schemaVersion ${parsed.schemaVersion}`);
109
+ return { ...parsed, installed: parsed.installed ?? {} };
110
+ } catch (error) {
111
+ if (error.code === "ENOENT") return emptyManifest(packageVersion);
112
+ throw error;
113
+ }
114
+ }
115
+ function writeManifest(scope, manifest) {
116
+ const file = manifestPath(scope);
117
+ if (!Object.keys(manifest.installed).length) {
118
+ try {
119
+ unlinkSync(file);
120
+ } catch (error) {
121
+ if (error.code !== "ENOENT") throw error;
122
+ }
123
+ return;
124
+ }
125
+ mkdirSync(path2.dirname(file), { recursive: true });
126
+ writeFileSync(file, `${JSON.stringify(manifest, null, 2)}
127
+ `, "utf8");
128
+ }
129
+ function isSafeRelPath(rel) {
130
+ if (typeof rel !== "string" || rel.length === 0) return false;
131
+ if (rel.includes("\0") || rel.includes("\\")) return false;
132
+ if (rel.startsWith("/") || /^[a-zA-Z]:/.test(rel)) return false;
133
+ return rel.split("/").every((part) => part !== "" && part !== "." && part !== "..");
134
+ }
135
+ function readMeta(skillDir) {
136
+ try {
137
+ const meta = JSON.parse(readFileSync(path2.join(skillDir, META_FILENAME), "utf8"));
138
+ if (!Array.isArray(meta?.files) || !meta.files.every(isSafeRelPath)) return null;
139
+ return meta;
140
+ } catch {
141
+ return null;
142
+ }
143
+ }
144
+ function writeMeta(skillDir, meta) {
145
+ writeFileSync(path2.join(skillDir, META_FILENAME), `${JSON.stringify(meta, null, 2)}
146
+ `, "utf8");
147
+ }
148
+
149
+ // src/skills.ts
150
+ import { createHash } from "crypto";
151
+ import { readdirSync, readFileSync as readFileSync2, statSync } from "fs";
152
+ import path3 from "path";
153
+ import { fileURLToPath } from "url";
154
+ import matter from "gray-matter";
155
+ import semver from "semver";
156
+ import { z } from "zod";
157
+ var GROUPS = ["Ads"];
158
+ var OTHER_GROUP = "Other";
159
+ var SKILL_PREFIX = "super-ads";
160
+ var UNPREFIXED_SKILLS = ["adspro-to-superads"];
161
+ function isKitSkillName(name) {
162
+ return name.startsWith(`${SKILL_PREFIX}-`) || UNPREFIXED_SKILLS.includes(name);
163
+ }
164
+ function fullName(input) {
165
+ return isKitSkillName(input) ? input : `${SKILL_PREFIX}-${input}`;
166
+ }
167
+ var SkillFrontmatterSchema = z.object({
168
+ /**
169
+ * Template/frontmatter schema version. Lets the CLI migrate old skills when the template
170
+ * changes, instead of guessing from headings. Optional so pre-schema skills still load; new
171
+ * skills set 1.
172
+ */
173
+ schema: z.coerce.number().int().positive().optional(),
174
+ name: z.string().regex(/^[a-z0-9-]+$/, "name must be kebab-case [a-z0-9-]").refine(isKitSkillName, `name must start with "${SKILL_PREFIX}-" or be one of: ${UNPREFIXED_SKILLS.join(", ")}`),
175
+ description: z.string().min(1, "description is required"),
176
+ version: z.string().refine((v) => !!semver.valid(v), "version must be valid semver"),
177
+ group: z.enum(GROUPS).optional()
178
+ });
179
+ var IGNORED_ENTRIES = /* @__PURE__ */ new Set([".DS_Store", "Thumbs.db", ".super-ads-skills-meta.json"]);
180
+ function listSkillFiles(dir) {
181
+ const out = [];
182
+ const walk = (current, prefix) => {
183
+ for (const entry of readdirSync(current, { withFileTypes: true })) {
184
+ if (IGNORED_ENTRIES.has(entry.name)) continue;
185
+ const rel = prefix ? `${prefix}/${entry.name}` : entry.name;
186
+ const abs = path3.join(current, entry.name);
187
+ if (entry.isSymbolicLink()) throw new Error(`Symlinks are not allowed in skills: ${rel}`);
188
+ if (entry.isDirectory()) walk(abs, rel);
189
+ else if (entry.isFile()) out.push(rel);
190
+ }
191
+ };
192
+ walk(dir, "");
193
+ return out.sort();
194
+ }
195
+ function hashFolder(dir, files) {
196
+ const hash = createHash("sha256");
197
+ for (const rel of files) {
198
+ const content = readFileSync2(path3.join(dir, ...rel.split("/"))).toString("utf8").replace(/\r\n/g, "\n");
199
+ hash.update(rel);
200
+ hash.update("\0");
201
+ hash.update(content);
202
+ hash.update("\0");
203
+ }
204
+ return `sha256:${hash.digest("hex")}`;
205
+ }
206
+ function readSkill(dir) {
207
+ const skillMd = path3.join(dir, "SKILL.md");
208
+ const raw = readFileSync2(skillMd, "utf8");
209
+ if (raw.charCodeAt(0) === 65279) {
210
+ throw new Error("SKILL.md starts with a UTF-8 BOM; save it as UTF-8 without BOM");
211
+ }
212
+ const parsed = matter(raw);
213
+ const front = SkillFrontmatterSchema.parse(parsed.data);
214
+ const folderName = path3.basename(dir);
215
+ if (front.name !== folderName) {
216
+ throw new Error(`frontmatter name "${front.name}" must match folder name "${folderName}"`);
217
+ }
218
+ const files = listSkillFiles(dir);
219
+ return {
220
+ ...front,
221
+ displayGroup: front.group ?? OTHER_GROUP,
222
+ dir,
223
+ files,
224
+ contentHash: hashFolder(dir, files),
225
+ body: parsed.content
226
+ };
227
+ }
228
+ function bundledSkillsDir() {
229
+ const here = path3.dirname(fileURLToPath(import.meta.url));
230
+ return path3.resolve(here, "..", "skills");
231
+ }
232
+ function loadSkills(root = bundledSkillsDir()) {
233
+ const entries = readdirSync(root, { withFileTypes: true }).filter((e) => e.isDirectory()).map((e) => path3.join(root, e.name)).filter((dir) => {
234
+ try {
235
+ return statSync(path3.join(dir, "SKILL.md")).isFile();
236
+ } catch {
237
+ return false;
238
+ }
239
+ });
240
+ return entries.map(readSkill).sort((a, b) => a.name.localeCompare(b.name));
241
+ }
242
+
243
+ // src/installer.ts
244
+ function skillStoreRoot() {
245
+ if (process.env.SUPER_ADS_SKILLS_STORE) return process.env.SUPER_ADS_SKILLS_STORE;
246
+ if (process.platform === "win32") {
247
+ const base2 = process.env.LOCALAPPDATA || path4.join(os3.homedir(), "AppData", "Local");
248
+ return path4.join(base2, "super-ads-skills", "skills");
249
+ }
250
+ const base = process.env.XDG_DATA_HOME || path4.join(os3.homedir(), ".local", "share");
251
+ return path4.join(base, "super-ads-skills", "skills");
252
+ }
253
+ function skillStoreDir(skillName) {
254
+ assertSafeSkillName(skillName);
255
+ return path4.join(skillStoreRoot(), skillName);
256
+ }
257
+ function listSkillDirs(scope) {
258
+ const found = /* @__PURE__ */ new Map();
259
+ for (const target of Object.keys(TARGETS)) {
260
+ const root = targetRoot(target, scope);
261
+ if (!existsSync2(root)) continue;
262
+ for (const entry of readdirSync2(root, { withFileTypes: true })) {
263
+ if (!(entry.isDirectory() || entry.isSymbolicLink()) || !/^[a-z0-9-]+$/.test(entry.name)) continue;
264
+ if (!existsSync2(path4.join(root, entry.name, "SKILL.md"))) continue;
265
+ found.set(entry.name, [...found.get(entry.name) ?? [], target]);
266
+ }
267
+ }
268
+ return found;
269
+ }
270
+ function assertSafeSkillName(name) {
271
+ if (!/^[a-z0-9-]+$/.test(name)) throw new Error(`Unsafe skill name: ${name}`);
272
+ }
273
+ function unlinkInside(destDir, abs) {
274
+ const root = path4.resolve(destDir);
275
+ const target = path4.resolve(abs);
276
+ if (target !== root && !target.startsWith(root + path4.sep)) {
277
+ throw new Error(`Refusing to delete outside the skill folder: ${target}`);
278
+ }
279
+ if (existsSync2(target)) unlinkSync2(target);
280
+ }
281
+ function lstatOrNull(abs) {
282
+ try {
283
+ return lstatSync(abs);
284
+ } catch (error) {
285
+ if (error.code === "ENOENT") return null;
286
+ throw error;
287
+ }
288
+ }
289
+ function unlinkNestedStoreLink(destDir, storeDir) {
290
+ let entries;
291
+ try {
292
+ entries = readdirSync2(destDir, { withFileTypes: true });
293
+ } catch {
294
+ return;
295
+ }
296
+ for (const entry of entries) {
297
+ if (!entry.isSymbolicLink()) continue;
298
+ const abs = path4.join(destDir, entry.name);
299
+ const points = resolvedLink(abs);
300
+ if (points && path4.resolve(points) === storeDir) unlinkSync2(abs);
301
+ }
302
+ }
303
+ function resolvedLink(destDir) {
304
+ const stat = lstatOrNull(destDir);
305
+ if (!stat?.isSymbolicLink()) return null;
306
+ return path4.resolve(path4.dirname(destDir), readlinkSync(destDir));
307
+ }
308
+ function currentHash(dir, files) {
309
+ const present = files.filter((rel) => existsSync2(path4.join(dir, ...rel.split("/"))));
310
+ return hashFolder(dir, present);
311
+ }
312
+ function hasDrift(destDir, meta) {
313
+ const missing = meta.files.some((rel) => !existsSync2(path4.join(destDir, ...rel.split("/"))));
314
+ if (missing) return true;
315
+ return currentHash(destDir, meta.files) !== meta.contentHash;
316
+ }
317
+ function installAction(skill, meta, packageVersion) {
318
+ if (semver2.gt(skill.version, meta.version)) return "update";
319
+ if (semver2.lt(skill.version, meta.version)) return "downgrade";
320
+ if (meta.packageVersion !== packageVersion) return "update";
321
+ if (meta.contentHash !== skill.contentHash) return "update";
322
+ return "skip";
323
+ }
324
+ function planInstall(skill, target, scope, mode = "link", packageVersion) {
325
+ assertSafeSkillName(skill.name);
326
+ const destDir = path4.join(targetRoot(target, scope), skill.name);
327
+ const storeDir = mode === "link" ? skillStoreDir(skill.name) : void 0;
328
+ const files = skill.files.map((rel) => path4.join(storeDir ?? destDir, ...rel.split("/")));
329
+ const base = { skill, target, destDir, files, mode, storeDir };
330
+ if (mode === "copy") {
331
+ const destStat = lstatOrNull(destDir);
332
+ if (destStat?.isSymbolicLink()) {
333
+ return {
334
+ ...base,
335
+ action: isStoreLink(destDir, skill.name) ? "update" : "drift"
336
+ };
337
+ }
338
+ const meta2 = readMeta(destDir);
339
+ if (!existsSync2(destDir) || !meta2) return { ...base, action: "create" };
340
+ if (hasDrift(destDir, meta2)) return { ...base, action: "drift", installedVersion: meta2.version };
341
+ const action = installAction(skill, meta2, packageVersion);
342
+ return { ...base, action, installedVersion: meta2.version };
343
+ }
344
+ const stat = lstatOrNull(destDir);
345
+ if (!stat) return { ...base, action: "create" };
346
+ if (stat.isSymbolicLink()) {
347
+ const points = resolvedLink(destDir);
348
+ const storeMeta = readMeta(skillStoreDir(skill.name));
349
+ if (!points || path4.resolve(points) !== path4.resolve(skillStoreDir(skill.name)) || !storeMeta) {
350
+ return { ...base, action: "update", installedVersion: storeMeta?.version };
351
+ }
352
+ const action = installAction(skill, storeMeta, packageVersion);
353
+ return { ...base, action, installedVersion: storeMeta.version };
354
+ }
355
+ if (!isPreviousCopy(destDir, skill)) return { ...base, action: "drift" };
356
+ const meta = readMeta(destDir);
357
+ return { ...base, action: "update", installedVersion: meta?.version };
358
+ }
359
+ function isPreviousCopy(destDir, skill) {
360
+ const meta = readMeta(destDir);
361
+ if (meta?.skill === skill.name) return true;
362
+ const nested = resolvedLink(path4.join(destDir, skill.name));
363
+ if (nested && path4.resolve(nested) === path4.resolve(skillStoreDir(skill.name))) return true;
364
+ return false;
365
+ }
366
+ function isStoreLink(destDir, skillName) {
367
+ const points = resolvedLink(destDir);
368
+ return Boolean(points && path4.resolve(points) === path4.resolve(skillStoreDir(skillName)));
369
+ }
370
+ function writeSkillTree(skill, destDir, packageVersion) {
371
+ const previous = lstatOrNull(destDir)?.isSymbolicLink() ? null : readMeta(destDir);
372
+ if (previous) {
373
+ for (const rel of previous.files) {
374
+ if (skill.files.includes(rel)) continue;
375
+ unlinkInside(destDir, path4.join(destDir, ...rel.split("/")));
376
+ }
377
+ }
378
+ for (const rel of skill.files) {
379
+ const from = path4.join(skill.dir, ...rel.split("/"));
380
+ const to = path4.join(destDir, ...rel.split("/"));
381
+ mkdirSync2(path4.dirname(to), { recursive: true });
382
+ copyFileSync(from, to);
383
+ }
384
+ const meta = {
385
+ schemaVersion: 1,
386
+ skill: skill.name,
387
+ version: skill.version,
388
+ packageVersion,
389
+ contentHash: skill.contentHash,
390
+ files: [...skill.files]
391
+ };
392
+ writeMeta(destDir, meta);
393
+ pruneEmptyDirs(destDir);
394
+ }
395
+ function linkSkill(storeDir, destDir) {
396
+ const expected = path4.resolve(storeDir);
397
+ if (path4.resolve(destDir) === expected) {
398
+ throw new Error(`Refusing to replace the store with a link to itself: ${destDir}`);
399
+ }
400
+ const current = resolvedLink(destDir);
401
+ if (current && path4.resolve(current) === expected) return;
402
+ const stat = lstatOrNull(destDir);
403
+ if (stat?.isSymbolicLink()) unlinkSync2(destDir);
404
+ else if (stat?.isDirectory()) {
405
+ unlinkNestedStoreLink(destDir, expected);
406
+ rmSync(destDir, { recursive: true });
407
+ } else if (stat) unlinkSync2(destDir);
408
+ mkdirSync2(path4.dirname(destDir), { recursive: true });
409
+ try {
410
+ symlinkSync(storeDir, destDir);
411
+ } catch (error) {
412
+ const code = error.code;
413
+ if (code === "EPERM" || code === "EACCES") {
414
+ throw new Error(
415
+ `Could not create a symlink at ${destDir}. Enable Developer Mode or run as administrator, or re-run with --copy.`
416
+ );
417
+ }
418
+ throw error;
419
+ }
420
+ }
421
+ function executeInstall(plan, packageVersion) {
422
+ if (plan.mode === "copy") {
423
+ const stat = lstatOrNull(plan.destDir);
424
+ if (stat?.isSymbolicLink()) {
425
+ if (!isStoreLink(plan.destDir, plan.skill.name)) {
426
+ throw new Error(`Refusing to replace an unmanaged symlink: ${plan.destDir}`);
427
+ }
428
+ unlinkSync2(plan.destDir);
429
+ }
430
+ writeSkillTree(plan.skill, plan.destDir, packageVersion);
431
+ return;
432
+ }
433
+ const storeDir = plan.storeDir ?? skillStoreDir(plan.skill.name);
434
+ writeSkillTree(plan.skill, storeDir, packageVersion);
435
+ linkSkill(storeDir, plan.destDir);
436
+ }
437
+ function planRemove(skillName, target, scope) {
438
+ assertSafeSkillName(skillName);
439
+ const destDir = path4.join(targetRoot(target, scope), skillName);
440
+ const base = {
441
+ skillName,
442
+ target,
443
+ destDir,
444
+ files: [],
445
+ kept: [],
446
+ unmanaged: false,
447
+ drift: false,
448
+ linked: false
449
+ };
450
+ const stat = lstatOrNull(destDir);
451
+ if (!stat) return base;
452
+ if (stat.isSymbolicLink()) {
453
+ return { ...base, linked: true, files: [destDir] };
454
+ }
455
+ if (!existsSync2(destDir)) return base;
456
+ const meta = readMeta(destDir);
457
+ if (!meta) {
458
+ return {
459
+ ...base,
460
+ unmanaged: true,
461
+ files: listSkillFiles(destDir).map((rel) => path4.join(destDir, ...rel.split("/")))
462
+ };
463
+ }
464
+ const present = listSkillFiles(destDir);
465
+ const managed = new Set(meta.files);
466
+ return {
467
+ ...base,
468
+ files: [
469
+ ...meta.files.map((rel) => path4.join(destDir, ...rel.split("/"))).filter((abs) => existsSync2(abs)),
470
+ path4.join(destDir, META_FILENAME)
471
+ ],
472
+ kept: present.filter((rel) => !managed.has(rel)),
473
+ drift: hasDrift(destDir, meta)
474
+ };
475
+ }
476
+ function executeRemove(plan) {
477
+ if (plan.linked) {
478
+ const stat = lstatSync(plan.destDir);
479
+ if (!stat.isSymbolicLink()) throw new Error(`Refusing to unlink a non-link: ${plan.destDir}`);
480
+ unlinkSync2(plan.destDir);
481
+ return;
482
+ }
483
+ for (const abs of plan.files) {
484
+ unlinkInside(plan.destDir, abs);
485
+ }
486
+ pruneEmptyDirs(plan.destDir, true);
487
+ }
488
+ function purgeSkillStore(skillName, scopes) {
489
+ const storeDir = skillStoreDir(skillName);
490
+ if (!existsSync2(storeDir)) return false;
491
+ const expected = path4.resolve(storeDir);
492
+ for (const scope of scopes) {
493
+ for (const target of Object.keys(TARGETS)) {
494
+ const destDir = path4.join(targetRoot(target, scope), skillName);
495
+ const points = resolvedLink(destDir);
496
+ if (points && path4.resolve(points) === expected) return false;
497
+ }
498
+ }
499
+ rmSync(storeDir, { recursive: true });
500
+ return true;
501
+ }
502
+ function pruneEmptyDirs(dir, includeSelf = false) {
503
+ if (!existsSync2(dir) || !statSync2(dir).isDirectory()) return;
504
+ for (const entry of readdirSync2(dir, { withFileTypes: true })) {
505
+ if (entry.isDirectory()) pruneEmptyDirs(path4.join(dir, entry.name), true);
506
+ }
507
+ if (!includeSelf) return;
508
+ if (readdirSync2(dir).length === 0) rmdirSync(dir);
509
+ }
510
+
511
+ // src/status.ts
512
+ import { existsSync as existsSync3, lstatSync as lstatSync2, readlinkSync as readlinkSync2 } from "fs";
513
+ import path5 from "path";
514
+ function installedInfo(skillName, scope) {
515
+ const out = [];
516
+ for (const target of Object.keys(TARGETS)) {
517
+ const destDir = path5.join(targetRoot(target, scope), skillName);
518
+ let linked = false;
519
+ try {
520
+ linked = lstatSync2(destDir).isSymbolicLink();
521
+ } catch {
522
+ continue;
523
+ }
524
+ if (linked) {
525
+ const meta2 = existsSync3(destDir) ? readMeta(destDir) : null;
526
+ const points = path5.resolve(path5.dirname(destDir), readlinkSync2(destDir));
527
+ const drift = !meta2 || path5.resolve(points) !== path5.resolve(skillStoreDir(skillName));
528
+ if (!meta2) {
529
+ out.push({ target, version: "0.0.0", drift: true, layout: "symlink" });
530
+ continue;
531
+ }
532
+ out.push({ target, version: meta2.version, drift, layout: "symlink" });
533
+ continue;
534
+ }
535
+ const meta = readMeta(destDir);
536
+ if (!meta) continue;
537
+ out.push({ target, version: meta.version, drift: hasDrift(destDir, meta), layout: "copy" });
538
+ }
539
+ return out;
540
+ }
541
+ function skillStatus(skill, scope) {
542
+ const installs = installedInfo(skill.name, scope);
543
+ const agents = [...new Set(installs.flatMap((i) => agentsForTarget(i.target).map((a) => a.label)))];
544
+ if (!installs.length) return { label: skill.name, hint: skill.description, state: "new", agents };
545
+ if (installs.some((i) => i.drift)) return { label: skill.name, hint: "Modified locally", state: "modified", agents };
546
+ const oldest = installs.map((i) => i.version).sort()[0];
547
+ if (installs.some((i) => i.version !== skill.version)) {
548
+ return { label: skill.name, hint: `update available ${oldest} \u2192 ${skill.version}`, state: "outdated", agents };
549
+ }
550
+ return { label: skill.name, hint: `installed v${skill.version}`, state: "installed", agents };
551
+ }
552
+
553
+ // src/cli.ts
554
+ var PACKAGE_VERSION = JSON.parse(
555
+ readFileSync3(path6.resolve(path6.dirname(fileURLToPath2(import.meta.url)), "..", "package.json"), "utf8")
556
+ ).version;
557
+ function scopeOf(opts) {
558
+ return { global: !opts.project, cwd: process.cwd() };
559
+ }
560
+ function agentsOf(opts) {
561
+ if (!opts.agent) return [...DEFAULT_AGENTS];
562
+ const ids = opts.agent.split(",").map((s) => s.trim()).filter(Boolean);
563
+ const unknown = ids.filter((id) => !agentById(id));
564
+ if (unknown.length) {
565
+ throw new Error(`Unknown agent: ${unknown.join(", ")}. Supported: ${AGENTS.map((a) => a.id).join(", ")}`);
566
+ }
567
+ return ids;
568
+ }
569
+ function describe(plan) {
570
+ const where = `${plan.skill.name} -> ${targetLabel(plan.target)}`;
571
+ const version = plan.installedVersion ? `${plan.installedVersion} -> ${plan.skill.version}` : `v${plan.skill.version}`;
572
+ return `${where} (${version})`;
573
+ }
574
+ function syncSkills(opts) {
575
+ const scope = scopeOf(opts);
576
+ const agents = agentsOf(opts);
577
+ const targets = targetsForAgents(agents);
578
+ const mode = opts.copy ? "copy" : "link";
579
+ const skills = loadSkills();
580
+ const manifest = readManifest(scope, PACKAGE_VERSION);
581
+ manifest.packageVersion = PACKAGE_VERSION;
582
+ const plans = skills.flatMap((skill) => targets.map((t) => planInstall(skill, t, scope, mode, PACKAGE_VERSION)));
583
+ const apply = plans.filter((p) => p.action === "create" || p.action === "update" || opts.force && p.action === "drift");
584
+ const blocked = opts.force ? [] : plans.filter((p) => p.action === "drift");
585
+ const upToDate = plans.filter((p) => p.action === "skip" || p.action === "downgrade").length;
586
+ const orphans = Object.keys(manifest.installed).filter((name) => !skills.some((s) => s.name === name));
587
+ for (const p of apply) console.log(`${pc.green(p.action === "create" ? "install" : "update ")} ${describe(p)}`);
588
+ for (const p of blocked) console.log(`${pc.yellow("skip ")} ${describe(p)} - edited locally, use --force to overwrite`);
589
+ for (const name of orphans) console.log(`${pc.red("remove ")} ${name} - no longer in the kit`);
590
+ console.log(pc.dim(`${upToDate} already up to date`));
591
+ if (opts.dryRun) return console.log(pc.dim("dry run: nothing written"));
592
+ for (const p of apply) executeInstall(p, PACKAGE_VERSION);
593
+ for (const name of orphans) removeSkill(name, scope, targets, true);
594
+ for (const skill of skills) {
595
+ const done = plans.filter((p) => p.skill.name === skill.name && !blocked.includes(p));
596
+ if (!done.length) continue;
597
+ manifest.installed[skill.name] = { version: skill.version, targets, agents };
598
+ }
599
+ writeManifest(scope, manifest);
600
+ const labels = agents.map((id) => agentById(id)?.label).join(", ");
601
+ console.log(pc.green(`
602
+ Done: ${skills.length} skills for ${labels} (${scope.global ? "global" : "project"}, ${mode}).`));
603
+ if (blocked.length) process.exitCode = 1;
604
+ }
605
+ function removeSkill(name, scope, targets, force) {
606
+ const plans = targets.map((t) => planRemove(name, t, scope)).filter((p) => p.files.length);
607
+ const removable = plans.filter((p) => force || !p.drift && !p.unmanaged);
608
+ for (const p of plans) {
609
+ if (removable.includes(p)) executeRemove(p);
610
+ else console.log(`${pc.yellow("skip ")} ${name} -> ${targetLabel(p.target)} - not managed or edited, use --force`);
611
+ }
612
+ const manifest = readManifest(scope, PACKAGE_VERSION);
613
+ if (removable.length === plans.length) delete manifest.installed[name];
614
+ writeManifest(scope, manifest);
615
+ purgeSkillStore(name, [scope]);
616
+ return removable.length;
617
+ }
618
+ function runRemove(names, opts) {
619
+ const scope = scopeOf(opts);
620
+ const targets = targetsForAgents(agentsOf(opts));
621
+ const present = [...listSkillDirs(scope).keys()].filter(isKitSkillName);
622
+ const wanted = names.length ? names.map(fullName) : present;
623
+ for (const name of wanted) {
624
+ const n = removeSkill(name, scope, targets, Boolean(opts.force));
625
+ console.log(`${n ? pc.green("removed") : pc.dim("absent ")} ${name}`);
626
+ }
627
+ }
628
+ function runList(opts) {
629
+ const scope = scopeOf(opts);
630
+ for (const skill of loadSkills()) {
631
+ const s = skillStatus(skill, scope);
632
+ const tag = { new: pc.dim("not installed"), installed: pc.green("installed"), outdated: pc.yellow("outdated"), modified: pc.red("modified") }[s.state];
633
+ console.log(`${skill.name.padEnd(28)} v${skill.version.padEnd(8)} ${tag}${s.agents.length ? pc.dim(` [${s.agents.join(", ")}]`) : ""}`);
634
+ }
635
+ }
636
+ var program = new Command();
637
+ program.name("super-ads-skills").description("SuperAds agent skills").version(PACKAGE_VERSION);
638
+ var common = (c) => c.option("--agent <ids>", `limit to some agents (${AGENTS.map((a) => a.id).join(",")}); default: all`).option("--project", "install into the current project instead of globally").option("--copy", "copy files instead of symlinking to the shared store").option("--force", "overwrite or remove skills edited by hand").option("--dry-run", "show what would change, write nothing");
639
+ common(program.command("install").description("Install every skill for every supported agent (no prompts)")).action(syncSkills);
640
+ common(program.command("update").description("Refresh installed skills to this checkout and drop retired ones")).action(syncSkills);
641
+ common(program.command("remove [skills...]").description("Remove skills (all of them when none named)")).action(runRemove);
642
+ common(program.command("list").description("Show every skill and its install state")).action(runList);
643
+ program.action(() => {
644
+ console.log(pc.bold(`SuperAds Skills v${PACKAGE_VERSION}
645
+ `));
646
+ console.log(" super-ads-skills install install all skills for all agents");
647
+ console.log(" super-ads-skills update refresh after git pull");
648
+ console.log(" super-ads-skills list show state");
649
+ console.log(" super-ads-skills remove uninstall");
650
+ });
651
+ program.parseAsync().catch((error) => {
652
+ console.error(pc.red(error instanceof Error ? error.message : String(error)));
653
+ process.exit(1);
654
+ });