@owlmeans/agent-skills 0.1.8

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 (54) hide show
  1. package/README.md +92 -0
  2. package/agent-meta/instructions/shadcn-versions.instructions.md +46 -0
  3. package/agent-meta/instructions/shadcn-web.instructions.md +94 -0
  4. package/agent-meta/manifest.json +37 -0
  5. package/agent-meta/skills/shadcn-versions/SKILL.md +126 -0
  6. package/agent-meta/skills/shadcn-web/SKILL.md +222 -0
  7. package/build/apply.d.ts +13 -0
  8. package/build/apply.d.ts.map +1 -0
  9. package/build/apply.js +28 -0
  10. package/build/apply.js.map +1 -0
  11. package/build/args.d.ts +14 -0
  12. package/build/args.d.ts.map +1 -0
  13. package/build/args.js +93 -0
  14. package/build/args.js.map +1 -0
  15. package/build/bin.d.ts +3 -0
  16. package/build/bin.d.ts.map +1 -0
  17. package/build/bin.js +21 -0
  18. package/build/bin.js.map +1 -0
  19. package/build/discover.d.ts +41 -0
  20. package/build/discover.d.ts.map +1 -0
  21. package/build/discover.js +160 -0
  22. package/build/discover.js.map +1 -0
  23. package/build/index.d.ts +11 -0
  24. package/build/index.d.ts.map +1 -0
  25. package/build/index.js +6 -0
  26. package/build/index.js.map +1 -0
  27. package/build/linked.d.ts +15 -0
  28. package/build/linked.d.ts.map +1 -0
  29. package/build/linked.js +44 -0
  30. package/build/linked.js.map +1 -0
  31. package/build/plan.d.ts +15 -0
  32. package/build/plan.d.ts.map +1 -0
  33. package/build/plan.js +40 -0
  34. package/build/plan.js.map +1 -0
  35. package/build/prompt.d.ts +4 -0
  36. package/build/prompt.d.ts.map +1 -0
  37. package/build/prompt.js +21 -0
  38. package/build/prompt.js.map +1 -0
  39. package/build/run.d.ts +18 -0
  40. package/build/run.d.ts.map +1 -0
  41. package/build/run.js +124 -0
  42. package/build/run.js.map +1 -0
  43. package/package.json +37 -0
  44. package/src/apply.ts +38 -0
  45. package/src/args.ts +108 -0
  46. package/src/bin.ts +20 -0
  47. package/src/discover.ts +202 -0
  48. package/src/index.ts +14 -0
  49. package/src/linked.ts +51 -0
  50. package/src/plan.ts +69 -0
  51. package/src/prompt.ts +25 -0
  52. package/src/run.ts +167 -0
  53. package/tests/installer.test.ts +297 -0
  54. package/tsconfig.json +11 -0
package/build/run.js ADDED
@@ -0,0 +1,124 @@
1
+ import { resolve } from 'node:path';
2
+ import { discover } from './discover.js';
3
+ import { detectLinked } from './linked.js';
4
+ import { planInstall } from './plan.js';
5
+ import { applyInstall } from './apply.js';
6
+ import { confirm, closeReadline, isTTY } from './prompt.js';
7
+ const ACTION_LABEL = {
8
+ 'install': 'install',
9
+ 'skip-uptodate': 'up-to-date',
10
+ 'update': 'update',
11
+ 'conflict': 'CONFLICT',
12
+ };
13
+ const padEnd = (s, n) => s + ' '.repeat(Math.max(0, n - s.length));
14
+ const printTable = (items) => {
15
+ const colWidths = {
16
+ kind: 11,
17
+ name: Math.max(...items.map(i => i.entry.name.length), 4) + 2,
18
+ pkg: Math.max(...items.map(i => i.entry.packageName.length), 7) + 2,
19
+ action: 10,
20
+ };
21
+ const header = padEnd('kind', colWidths.kind) +
22
+ padEnd('name', colWidths.name) +
23
+ padEnd('package', colWidths.pkg) +
24
+ 'action';
25
+ process.stdout.write(header + '\n');
26
+ process.stdout.write('-'.repeat(header.length) + '\n');
27
+ for (const item of items) {
28
+ const kindLabel = item.entry.kind === 'skill' ? 'skill' : 'instruction';
29
+ const actionLabel = ACTION_LABEL[item.action];
30
+ process.stdout.write(padEnd(kindLabel, colWidths.kind) +
31
+ padEnd(item.entry.name, colWidths.name) +
32
+ padEnd(item.entry.packageName, colWidths.pkg) +
33
+ actionLabel +
34
+ '\n');
35
+ }
36
+ process.stdout.write('\n');
37
+ };
38
+ export const run = async (args) => {
39
+ const targetDir = resolve(args.dir);
40
+ // 1. Linked monorepo check
41
+ if (!args.force) {
42
+ const linked = detectLinked(targetDir);
43
+ if (linked.linked) {
44
+ const pkgList = linked.evidence.slice(0, 5).join(', ');
45
+ const more = linked.evidence.length > 5 ? ` (+${linked.evidence.length - 5} more)` : '';
46
+ process.stderr.write(`\nerror: linked monorepo detected — ${pkgList}${more} are symlinked.\n\n` +
47
+ `In this setup, agents already load guidance from the linked monorepo's\n` +
48
+ `root .claude/skills/ and .github/instructions/. Installing embedded copies\n` +
49
+ `would place stale duplicates that conflict with the canonical live files.\n\n` +
50
+ `Pass --force only if you understand this and want to proceed anyway.\n\n`);
51
+ return { code: 4, message: 'linked monorepo' };
52
+ }
53
+ }
54
+ // 2. Discover
55
+ const entries = discover(targetDir, {
56
+ extras: args.extras,
57
+ only: args.only.length > 0 ? args.only : undefined,
58
+ claudeOnly: args.claudeOnly,
59
+ copilotOnly: args.copilotOnly,
60
+ });
61
+ if (entries.length === 0) {
62
+ process.stdout.write(`No embedded @owlmeans/* agent guidance found in node_modules.\n` +
63
+ `Install @owlmeans/* packages first, then re-run.\n`);
64
+ return { code: 3, message: 'nothing found' };
65
+ }
66
+ // 3. Plan
67
+ const items = planInstall(entries, targetDir, { force: args.force });
68
+ // 4. Print table
69
+ process.stdout.write('\n');
70
+ printTable(items);
71
+ if (args.dryRun) {
72
+ process.stdout.write('(dry-run — no files written)\n');
73
+ return { code: 0 };
74
+ }
75
+ // 5. Count actionable items
76
+ const toWrite = items.filter(i => i.action === 'install' || i.action === 'update');
77
+ const conflicts = items.filter(i => i.action === 'conflict');
78
+ if (toWrite.length === 0 && conflicts.length === 0) {
79
+ process.stdout.write('Everything up to date.\n');
80
+ closeReadline();
81
+ return { code: 0 };
82
+ }
83
+ // 6. Interactive: confirm per conflict, or if not --yes confirm overall
84
+ const resolvedItems = [...items];
85
+ if (isTTY() && !args.yes && toWrite.length > 0) {
86
+ const ok = await confirm(`Install ${toWrite.length} file(s) into ${targetDir}?`);
87
+ if (!ok) {
88
+ process.stdout.write('Aborted.\n');
89
+ closeReadline();
90
+ return { code: 0 };
91
+ }
92
+ }
93
+ // Per-conflict prompts in interactive mode
94
+ if (isTTY() && conflicts.length > 0) {
95
+ for (const item of resolvedItems) {
96
+ if (item.action !== 'conflict')
97
+ continue;
98
+ const ok = await confirm(` ${item.entry.name} has local edits — overwrite ${item.targetPath}?`);
99
+ if (ok) {
100
+ // Upgrade to update
101
+ Object.assign(item, { action: 'update' });
102
+ }
103
+ }
104
+ }
105
+ closeReadline();
106
+ const remainingConflicts = resolvedItems.filter(i => i.action === 'conflict');
107
+ if (remainingConflicts.length > 0) {
108
+ process.stdout.write(`\nSkipping ${remainingConflicts.length} file(s) with local edits:\n`);
109
+ for (const c of remainingConflicts) {
110
+ process.stdout.write(` ${c.targetPath}\n`);
111
+ }
112
+ process.stdout.write(`Pass --force to overwrite.\n\n`);
113
+ // In non-interactive --yes mode with remaining conflicts, exit 5
114
+ if (!isTTY() && !args.yes && !args.force) {
115
+ return { code: 5, message: 'unresolved conflicts' };
116
+ }
117
+ }
118
+ // 7. Apply
119
+ const result = applyInstall(resolvedItems);
120
+ process.stdout.write(`Done: ${result.installed} installed, ${result.updated} updated, ` +
121
+ `${result.skipped} up-to-date, ${result.conflicts} skipped (conflicts).\n`);
122
+ return { code: 0 };
123
+ };
124
+ //# sourceMappingURL=run.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"run.js","sourceRoot":"","sources":["../src/run.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,OAAO,EAAE,MAAM,WAAW,CAAA;AAEnC,OAAO,EAAE,QAAQ,EAAE,MAAM,eAAe,CAAA;AACxC,OAAO,EAAE,YAAY,EAAE,MAAM,aAAa,CAAA;AAC1C,OAAO,EAAE,WAAW,EAAE,MAAM,WAAW,CAAA;AACvC,OAAO,EAAE,YAAY,EAAE,MAAM,YAAY,CAAA;AACzC,OAAO,EAAE,OAAO,EAAE,aAAa,EAAE,KAAK,EAAE,MAAM,aAAa,CAAA;AAG3D,MAAM,YAAY,GAAkC;IAClD,SAAS,EAAE,SAAS;IACpB,eAAe,EAAE,YAAY;IAC7B,QAAQ,EAAE,QAAQ;IAClB,UAAU,EAAE,UAAU;CACvB,CAAA;AAED,MAAM,MAAM,GAAG,CAAC,CAAS,EAAE,CAAS,EAAU,EAAE,CAAC,CAAC,GAAG,GAAG,CAAC,MAAM,CAAC,IAAI,CAAC,GAAG,CAAC,CAAC,EAAE,CAAC,GAAG,CAAC,CAAC,MAAM,CAAC,CAAC,CAAA;AAE1F,MAAM,UAAU,GAAG,CAAC,KAAoB,EAAQ,EAAE;IAChD,MAAM,SAAS,GAAG;QAChB,IAAI,EAAE,EAAE;QACR,IAAI,EAAE,IAAI,CAAC,GAAG,CAAC,GAAG,KAAK,CAAC,GAAG,CAAC,CAAC,CAAC,EAAE,CAAC,CAAC,CAAC,KAAK,CAAC,IAAI,CAAC,MAAM,CAAC,EAAE,CAAC,CAAC,GAAG,CAAC;QAC7D,GAAG,EAAE,IAAI,CAAC,GAAG,CAAC,GAAG,KAAK,CAAC,GAAG,CAAC,CAAC,CAAC,EAAE,CAAC,CAAC,CAAC,KAAK,CAAC,WAAW,CAAC,MAAM,CAAC,EAAE,CAAC,CAAC,GAAG,CAAC;QACnE,MAAM,EAAE,EAAE;KACX,CAAA;IAED,MAAM,MAAM,GACV,MAAM,CAAC,MAAM,EAAE,SAAS,CAAC,IAAI,CAAC;QAC9B,MAAM,CAAC,MAAM,EAAE,SAAS,CAAC,IAAI,CAAC;QAC9B,MAAM,CAAC,SAAS,EAAE,SAAS,CAAC,GAAG,CAAC;QAChC,QAAQ,CAAA;IACV,OAAO,CAAC,MAAM,CAAC,KAAK,CAAC,MAAM,GAAG,IAAI,CAAC,CAAA;IACnC,OAAO,CAAC,MAAM,CAAC,KAAK,CAAC,GAAG,CAAC,MAAM,CAAC,MAAM,CAAC,MAAM,CAAC,GAAG,IAAI,CAAC,CAAA;IAEtD,KAAK,MAAM,IAAI,IAAI,KAAK,EAAE,CAAC;QACzB,MAAM,SAAS,GAAG,IAAI,CAAC,KAAK,CAAC,IAAI,KAAK,OAAO,CAAC,CAAC,CAAC,OAAO,CAAC,CAAC,CAAC,aAAa,CAAA;QACvE,MAAM,WAAW,GAAG,YAAY,CAAC,IAAI,CAAC,MAAM,CAAC,CAAA;QAC7C,OAAO,CAAC,MAAM,CAAC,KAAK,CAClB,MAAM,CAAC,SAAS,EAAE,SAAS,CAAC,IAAI,CAAC;YACjC,MAAM,CAAC,IAAI,CAAC,KAAK,CAAC,IAAI,EAAE,SAAS,CAAC,IAAI,CAAC;YACvC,MAAM,CAAC,IAAI,CAAC,KAAK,CAAC,WAAW,EAAE,SAAS,CAAC,GAAG,CAAC;YAC7C,WAAW;YACX,IAAI,CACL,CAAA;IACH,CAAC;IACD,OAAO,CAAC,MAAM,CAAC,KAAK,CAAC,IAAI,CAAC,CAAA;AAC5B,CAAC,CAAA;AASD,MAAM,CAAC,MAAM,GAAG,GAAG,KAAK,EAAE,IAAa,EAAsB,EAAE;IAC7D,MAAM,SAAS,GAAG,OAAO,CAAC,IAAI,CAAC,GAAG,CAAC,CAAA;IAEnC,2BAA2B;IAC3B,IAAI,CAAC,IAAI,CAAC,KAAK,EAAE,CAAC;QAChB,MAAM,MAAM,GAAG,YAAY,CAAC,SAAS,CAAC,CAAA;QACtC,IAAI,MAAM,CAAC,MAAM,EAAE,CAAC;YAClB,MAAM,OAAO,GAAG,MAAM,CAAC,QAAQ,CAAC,KAAK,CAAC,CAAC,EAAE,CAAC,CAAC,CAAC,IAAI,CAAC,IAAI,CAAC,CAAA;YACtD,MAAM,IAAI,GAAG,MAAM,CAAC,QAAQ,CAAC,MAAM,GAAG,CAAC,CAAC,CAAC,CAAC,MAAM,MAAM,CAAC,QAAQ,CAAC,MAAM,GAAG,CAAC,QAAQ,CAAC,CAAC,CAAC,EAAE,CAAA;YACvF,OAAO,CAAC,MAAM,CAAC,KAAK,CAClB,uCAAuC,OAAO,GAAG,IAAI,qBAAqB;gBAC1E,0EAA0E;gBAC1E,8EAA8E;gBAC9E,+EAA+E;gBAC/E,0EAA0E,CAC3E,CAAA;YACD,OAAO,EAAE,IAAI,EAAE,CAAC,EAAE,OAAO,EAAE,iBAAiB,EAAE,CAAA;QAChD,CAAC;IACH,CAAC;IAED,cAAc;IACd,MAAM,OAAO,GAAG,QAAQ,CAAC,SAAS,EAAE;QAClC,MAAM,EAAE,IAAI,CAAC,MAAM;QACnB,IAAI,EAAE,IAAI,CAAC,IAAI,CAAC,MAAM,GAAG,CAAC,CAAC,CAAC,CAAC,IAAI,CAAC,IAAI,CAAC,CAAC,CAAC,SAAS;QAClD,UAAU,EAAE,IAAI,CAAC,UAAU;QAC3B,WAAW,EAAE,IAAI,CAAC,WAAW;KAC9B,CAAC,CAAA;IAEF,IAAI,OAAO,CAAC,MAAM,KAAK,CAAC,EAAE,CAAC;QACzB,OAAO,CAAC,MAAM,CAAC,KAAK,CAClB,iEAAiE;YACjE,oDAAoD,CACrD,CAAA;QACD,OAAO,EAAE,IAAI,EAAE,CAAC,EAAE,OAAO,EAAE,eAAe,EAAE,CAAA;IAC9C,CAAC;IAED,UAAU;IACV,MAAM,KAAK,GAAG,WAAW,CAAC,OAAO,EAAE,SAAS,EAAE,EAAE,KAAK,EAAE,IAAI,CAAC,KAAK,EAAE,CAAC,CAAA;IAEpE,iBAAiB;IACjB,OAAO,CAAC,MAAM,CAAC,KAAK,CAAC,IAAI,CAAC,CAAA;IAC1B,UAAU,CAAC,KAAK,CAAC,CAAA;IAEjB,IAAI,IAAI,CAAC,MAAM,EAAE,CAAC;QAChB,OAAO,CAAC,MAAM,CAAC,KAAK,CAAC,gCAAgC,CAAC,CAAA;QACtD,OAAO,EAAE,IAAI,EAAE,CAAC,EAAE,CAAA;IACpB,CAAC;IAED,4BAA4B;IAC5B,MAAM,OAAO,GAAG,KAAK,CAAC,MAAM,CAAC,CAAC,CAAC,EAAE,CAAC,CAAC,CAAC,MAAM,KAAK,SAAS,IAAI,CAAC,CAAC,MAAM,KAAK,QAAQ,CAAC,CAAA;IAClF,MAAM,SAAS,GAAG,KAAK,CAAC,MAAM,CAAC,CAAC,CAAC,EAAE,CAAC,CAAC,CAAC,MAAM,KAAK,UAAU,CAAC,CAAA;IAE5D,IAAI,OAAO,CAAC,MAAM,KAAK,CAAC,IAAI,SAAS,CAAC,MAAM,KAAK,CAAC,EAAE,CAAC;QACnD,OAAO,CAAC,MAAM,CAAC,KAAK,CAAC,0BAA0B,CAAC,CAAA;QAChD,aAAa,EAAE,CAAA;QACf,OAAO,EAAE,IAAI,EAAE,CAAC,EAAE,CAAA;IACpB,CAAC;IAED,wEAAwE;IACxE,MAAM,aAAa,GAAG,CAAC,GAAG,KAAK,CAAC,CAAA;IAEhC,IAAI,KAAK,EAAE,IAAI,CAAC,IAAI,CAAC,GAAG,IAAI,OAAO,CAAC,MAAM,GAAG,CAAC,EAAE,CAAC;QAC/C,MAAM,EAAE,GAAG,MAAM,OAAO,CAAC,WAAW,OAAO,CAAC,MAAM,iBAAiB,SAAS,GAAG,CAAC,CAAA;QAChF,IAAI,CAAC,EAAE,EAAE,CAAC;YACR,OAAO,CAAC,MAAM,CAAC,KAAK,CAAC,YAAY,CAAC,CAAA;YAClC,aAAa,EAAE,CAAA;YACf,OAAO,EAAE,IAAI,EAAE,CAAC,EAAE,CAAA;QACpB,CAAC;IACH,CAAC;IAED,2CAA2C;IAC3C,IAAI,KAAK,EAAE,IAAI,SAAS,CAAC,MAAM,GAAG,CAAC,EAAE,CAAC;QACpC,KAAK,MAAM,IAAI,IAAI,aAAa,EAAE,CAAC;YACjC,IAAI,IAAI,CAAC,MAAM,KAAK,UAAU;gBAAE,SAAQ;YACxC,MAAM,EAAE,GAAG,MAAM,OAAO,CACtB,KAAK,IAAI,CAAC,KAAK,CAAC,IAAI,gCAAgC,IAAI,CAAC,UAAU,GAAG,CACvE,CAAA;YACD,IAAI,EAAE,EAAE,CAAC;gBACP,oBAAoB;gBACpB,MAAM,CAAC,MAAM,CAAC,IAAI,EAAE,EAAE,MAAM,EAAE,QAAQ,EAAE,CAAC,CAAA;YAC3C,CAAC;QACH,CAAC;IACH,CAAC;IAED,aAAa,EAAE,CAAA;IAEf,MAAM,kBAAkB,GAAG,aAAa,CAAC,MAAM,CAAC,CAAC,CAAC,EAAE,CAAC,CAAC,CAAC,MAAM,KAAK,UAAU,CAAC,CAAA;IAC7E,IAAI,kBAAkB,CAAC,MAAM,GAAG,CAAC,EAAE,CAAC;QAClC,OAAO,CAAC,MAAM,CAAC,KAAK,CAClB,cAAc,kBAAkB,CAAC,MAAM,8BAA8B,CACtE,CAAA;QACD,KAAK,MAAM,CAAC,IAAI,kBAAkB,EAAE,CAAC;YACnC,OAAO,CAAC,MAAM,CAAC,KAAK,CAAC,KAAK,CAAC,CAAC,UAAU,IAAI,CAAC,CAAA;QAC7C,CAAC;QACD,OAAO,CAAC,MAAM,CAAC,KAAK,CAAC,gCAAgC,CAAC,CAAA;QAEtD,iEAAiE;QACjE,IAAI,CAAC,KAAK,EAAE,IAAI,CAAC,IAAI,CAAC,GAAG,IAAI,CAAC,IAAI,CAAC,KAAK,EAAE,CAAC;YACzC,OAAO,EAAE,IAAI,EAAE,CAAC,EAAE,OAAO,EAAE,sBAAsB,EAAE,CAAA;QACrD,CAAC;IACH,CAAC;IAED,WAAW;IACX,MAAM,MAAM,GAAG,YAAY,CAAC,aAAa,CAAC,CAAA;IAE1C,OAAO,CAAC,MAAM,CAAC,KAAK,CAClB,SAAS,MAAM,CAAC,SAAS,eAAe,MAAM,CAAC,OAAO,YAAY;QAClE,GAAG,MAAM,CAAC,OAAO,gBAAgB,MAAM,CAAC,SAAS,yBAAyB,CAC3E,CAAA;IAED,OAAO,EAAE,IAAI,EAAE,CAAC,EAAE,CAAA;AACpB,CAAC,CAAA"}
package/package.json ADDED
@@ -0,0 +1,37 @@
1
+ {
2
+ "name": "@owlmeans/agent-skills",
3
+ "version": "0.1.8",
4
+ "license": "MIT",
5
+ "type": "module",
6
+ "scripts": {
7
+ "build": "tsc -b",
8
+ "dev": "nodemon -e ts,tsx,json --watch src --exec \"tsc -p ./tsconfig.json\"",
9
+ "watch": "tsc -b -w --preserveWatchOutput --pretty",
10
+ "test": "bun test ./tests"
11
+ },
12
+ "bin": {
13
+ "owlmeans-agent-skills": "./build/bin.js"
14
+ },
15
+ "main": "build/index.js",
16
+ "module": "build/index.js",
17
+ "types": "build/index.d.ts",
18
+ "exports": {
19
+ ".": {
20
+ "import": "./build/index.js",
21
+ "require": "./build/index.js",
22
+ "default": "./build/index.js",
23
+ "module": "./build/index.js",
24
+ "types": "./build/index.d.ts"
25
+ }
26
+ },
27
+ "devDependencies": {
28
+ "@owlmeans/dep-config": "workspace:*",
29
+ "@types/bun": "^1.3.0",
30
+ "@types/node": "^24.10.1",
31
+ "nodemon": "^3.1.11",
32
+ "typescript": "^6.0.2"
33
+ },
34
+ "publishConfig": {
35
+ "access": "public"
36
+ }
37
+ }
package/src/apply.ts ADDED
@@ -0,0 +1,38 @@
1
+ import { mkdirSync, readFileSync, writeFileSync } from 'node:fs'
2
+ import { dirname } from 'node:path'
3
+ import type { InstallItem } from './plan.js'
4
+
5
+ export interface ApplyResult {
6
+ installed: number
7
+ updated: number
8
+ skipped: number
9
+ conflicts: number
10
+ }
11
+
12
+ /**
13
+ * Execute an install plan. Items with action 'conflict' or 'skip-uptodate'
14
+ * are not written. Only 'install' and 'update' items are written.
15
+ */
16
+ export const applyInstall = (items: InstallItem[]): ApplyResult => {
17
+ const result: ApplyResult = { installed: 0, updated: 0, skipped: 0, conflicts: 0 }
18
+
19
+ for (const item of items) {
20
+ if (item.action === 'skip-uptodate') {
21
+ result.skipped++
22
+ continue
23
+ }
24
+ if (item.action === 'conflict') {
25
+ result.conflicts++
26
+ continue
27
+ }
28
+
29
+ const content = readFileSync(item.entry.sourcePath, 'utf8')
30
+ mkdirSync(dirname(item.targetPath), { recursive: true })
31
+ writeFileSync(item.targetPath, content, 'utf8')
32
+
33
+ if (item.action === 'install') result.installed++
34
+ else result.updated++
35
+ }
36
+
37
+ return result
38
+ }
package/src/args.ts ADDED
@@ -0,0 +1,108 @@
1
+ export interface CliArgs {
2
+ dir: string
3
+ yes: boolean
4
+ only: string[]
5
+ claudeOnly: boolean
6
+ copilotOnly: boolean
7
+ extras: boolean
8
+ force: boolean
9
+ dryRun: boolean
10
+ help: boolean
11
+ }
12
+
13
+ const HELP = `owlmeans-agent-skills — install embedded @owlmeans/* agent guidance
14
+
15
+ Usage: npx @owlmeans/agent-skills [options]
16
+
17
+ Options:
18
+ --dir <path> target project directory (default: cwd)
19
+ --yes, -y skip interactive confirmation
20
+ --only <pkg,...> comma-separated @owlmeans/* package names to install from
21
+ --claude-only install only Claude Code skills
22
+ --copilot-only install only Copilot instructions
23
+ --extras include extras bundled with the installer (default: on)
24
+ --no-extras skip installer-bundled extras
25
+ --force overwrite locally-edited files (no AUTO-GENERATED banner)
26
+ --dry-run print plan without writing files
27
+ --help, -h show this help
28
+ `
29
+
30
+ export const parseArgs = (argv: string[]): CliArgs | null => {
31
+ const args = argv.slice(2) // strip node + script
32
+ const result: CliArgs = {
33
+ dir: process.cwd(),
34
+ yes: false,
35
+ only: [],
36
+ claudeOnly: false,
37
+ copilotOnly: false,
38
+ extras: true,
39
+ force: false,
40
+ dryRun: false,
41
+ help: false,
42
+ }
43
+
44
+ let i = 0
45
+ while (i < args.length) {
46
+ const a = args[i]
47
+ switch (a) {
48
+ case '--help':
49
+ case '-h':
50
+ result.help = true
51
+ break
52
+ case '--yes':
53
+ case '-y':
54
+ result.yes = true
55
+ break
56
+ case '--force':
57
+ result.force = true
58
+ break
59
+ case '--dry-run':
60
+ result.dryRun = true
61
+ break
62
+ case '--claude-only':
63
+ result.claudeOnly = true
64
+ break
65
+ case '--copilot-only':
66
+ result.copilotOnly = true
67
+ break
68
+ case '--extras':
69
+ result.extras = true
70
+ break
71
+ case '--no-extras':
72
+ result.extras = false
73
+ break
74
+ case '--dir': {
75
+ const v = args[i + 1]
76
+ if (v == null || v.startsWith('-')) {
77
+ process.stderr.write(`error: --dir requires a path argument\n`)
78
+ return null
79
+ }
80
+ result.dir = v
81
+ i++
82
+ break
83
+ }
84
+ case '--only': {
85
+ const v = args[i + 1]
86
+ if (v == null || v.startsWith('-')) {
87
+ process.stderr.write(`error: --only requires a comma-separated package list\n`)
88
+ return null
89
+ }
90
+ result.only = v.split(',').map(s => s.trim()).filter(Boolean)
91
+ i++
92
+ break
93
+ }
94
+ default:
95
+ if (a.startsWith('--')) {
96
+ process.stderr.write(`error: unknown flag ${a}\n`)
97
+ return null
98
+ }
99
+ }
100
+ i++
101
+ }
102
+
103
+ return result
104
+ }
105
+
106
+ export const printHelp = (): void => {
107
+ process.stdout.write(HELP)
108
+ }
package/src/bin.ts ADDED
@@ -0,0 +1,20 @@
1
+ #!/usr/bin/env node
2
+ import { parseArgs, printHelp } from './args.js'
3
+ import { run } from './run.js'
4
+
5
+ const args = parseArgs(process.argv)
6
+
7
+ if (args == null) {
8
+ process.stderr.write('Run with --help for usage.\n')
9
+ process.exit(2)
10
+ } else if (args.help) {
11
+ printHelp()
12
+ process.exit(0)
13
+ } else {
14
+ run(args).then(result => {
15
+ process.exit(result.code)
16
+ }).catch((err: unknown) => {
17
+ process.stderr.write(`fatal: ${err instanceof Error ? err.message : String(err)}\n`)
18
+ process.exit(1)
19
+ })
20
+ }
@@ -0,0 +1,202 @@
1
+ import { existsSync, readdirSync, readFileSync } from 'node:fs'
2
+ import { dirname, join } from 'node:path'
3
+ import { fileURLToPath } from 'node:url'
4
+
5
+ export interface ManifestEntry {
6
+ kind: 'skill' | 'instruction'
7
+ name: string
8
+ category: 'package-specific' | 'multi-package'
9
+ file: string
10
+ canonicalPath: string
11
+ }
12
+
13
+ export interface Manifest {
14
+ schemaVersion: number
15
+ package: string
16
+ version: string
17
+ generatedAt: string
18
+ canonicalRepo: string
19
+ entries: ManifestEntry[]
20
+ }
21
+
22
+ export interface DiscoveredEntry {
23
+ kind: 'skill' | 'instruction'
24
+ name: string
25
+ category: 'package-specific' | 'multi-package'
26
+ /** Absolute path to the embedded source file. */
27
+ sourcePath: string
28
+ canonicalPath: string
29
+ /** Package this came from. */
30
+ packageName: string
31
+ version: string
32
+ /** True for extras bundled in the installer itself. */
33
+ isExtra: boolean
34
+ }
35
+
36
+ /** Parse a manifest.json, return null if invalid/unparseable. */
37
+ const parseManifest = (manifestPath: string): Manifest | null => {
38
+ try {
39
+ const raw = readFileSync(manifestPath, 'utf8')
40
+ const m = JSON.parse(raw) as Manifest
41
+ if (typeof m.schemaVersion !== 'number' || !Array.isArray(m.entries)) return null
42
+ return m
43
+ } catch {
44
+ return null
45
+ }
46
+ }
47
+
48
+ // Scan node_modules/@owlmeans/*/agent-meta/ for manifests.
49
+ const scanNodeModules = (targetDir: string): DiscoveredEntry[] => {
50
+ const nmDir = join(targetDir, 'node_modules', '@owlmeans')
51
+ if (!existsSync(nmDir)) return []
52
+
53
+ const entries: DiscoveredEntry[] = []
54
+ let pkgDirs: string[]
55
+ try {
56
+ pkgDirs = readdirSync(nmDir)
57
+ } catch {
58
+ return []
59
+ }
60
+
61
+ for (const pkgDir of pkgDirs) {
62
+ const manifestPath = join(nmDir, pkgDir, 'agent-meta', 'manifest.json')
63
+ if (!existsSync(manifestPath)) continue
64
+ const manifest = parseManifest(manifestPath)
65
+ if (manifest == null) continue
66
+
67
+ const agentMetaDir = join(nmDir, pkgDir, 'agent-meta')
68
+ for (const e of manifest.entries) {
69
+ const sourcePath = join(agentMetaDir, e.file)
70
+ if (!existsSync(sourcePath)) continue
71
+ entries.push({
72
+ kind: e.kind,
73
+ name: e.name,
74
+ category: e.category,
75
+ sourcePath,
76
+ canonicalPath: e.canonicalPath,
77
+ packageName: manifest.package,
78
+ version: manifest.version,
79
+ isExtra: false,
80
+ })
81
+ }
82
+ }
83
+
84
+ return entries
85
+ }
86
+
87
+ /** Resolve the installer package's own agent-meta dir (for bundled extras). */
88
+ const selfAgentMetaDir = (): string | null => {
89
+ try {
90
+ const selfDir = dirname(fileURLToPath(import.meta.url))
91
+ // src/ → .. → package root → agent-meta
92
+ const candidate = join(selfDir, '..', 'agent-meta')
93
+ return existsSync(candidate) ? candidate : null
94
+ } catch {
95
+ return null
96
+ }
97
+ }
98
+
99
+ /** Scan the installer's own agent-meta/ for bundled extras. */
100
+ const scanSelfExtras = (packageName: string, version: string): DiscoveredEntry[] => {
101
+ const agentMetaDir = selfAgentMetaDir()
102
+ if (agentMetaDir == null) return []
103
+
104
+ const manifestPath = join(agentMetaDir, 'manifest.json')
105
+ const manifest = parseManifest(manifestPath)
106
+ if (manifest == null) return []
107
+
108
+ const entries: DiscoveredEntry[] = []
109
+ for (const e of manifest.entries) {
110
+ const sourcePath = join(agentMetaDir, e.file)
111
+ if (!existsSync(sourcePath)) continue
112
+ entries.push({
113
+ kind: e.kind,
114
+ name: e.name,
115
+ category: e.category,
116
+ sourcePath,
117
+ canonicalPath: e.canonicalPath,
118
+ packageName,
119
+ version,
120
+ isExtra: true,
121
+ })
122
+ }
123
+ return entries
124
+ }
125
+
126
+ export interface DiscoverOptions {
127
+ /** Include installer's bundled extras. Default: true */
128
+ extras?: boolean
129
+ /** Restrict to entries from these package names. */
130
+ only?: string[]
131
+ /** Install only skills. */
132
+ claudeOnly?: boolean
133
+ /** Install only instructions. */
134
+ copilotOnly?: boolean
135
+ }
136
+
137
+ /** Collect all DiscoveredEntries, deduplicated by (kind, name). */
138
+ export const discover = (targetDir: string, opts: DiscoverOptions = {}): DiscoveredEntry[] => {
139
+ const { extras = true, only, claudeOnly = false, copilotOnly = false } = opts
140
+
141
+ const allEntries = scanNodeModules(targetDir)
142
+
143
+ if (extras) {
144
+ // Get installer's own package name+version from its package.json
145
+ let selfName = '@owlmeans/agent-skills'
146
+ let selfVersion = '0.0.0'
147
+ try {
148
+ const selfPkgPath = join(dirname(fileURLToPath(import.meta.url)), '..', 'package.json')
149
+ if (existsSync(selfPkgPath)) {
150
+ const pkg = JSON.parse(readFileSync(selfPkgPath, 'utf8'))
151
+ if (pkg.name) selfName = pkg.name
152
+ if (pkg.version) selfVersion = pkg.version
153
+ }
154
+ } catch { /* use defaults */ }
155
+ allEntries.push(...scanSelfExtras(selfName, selfVersion))
156
+ }
157
+
158
+ // Deduplicate by (kind, name): prefer highest semver version
159
+ const byKey = new Map<string, DiscoveredEntry>()
160
+ for (const e of allEntries) {
161
+ const key = `${e.kind}:${e.name}`
162
+ const existing = byKey.get(key)
163
+ if (existing == null) {
164
+ byKey.set(key, e)
165
+ } else if (compareSemver(e.version, existing.version) > 0) {
166
+ byKey.set(key, e)
167
+ }
168
+ }
169
+
170
+ let result = Array.from(byKey.values())
171
+
172
+ // Apply --only filter
173
+ if (only != null && only.length > 0) {
174
+ result = result.filter(e => only.some(n => matchPkgName(n, e.packageName)))
175
+ }
176
+
177
+ // Apply tool filters
178
+ if (claudeOnly) result = result.filter(e => e.kind === 'skill')
179
+ if (copilotOnly) result = result.filter(e => e.kind === 'instruction')
180
+
181
+ return result
182
+ }
183
+
184
+ /** Compare two semver strings. Returns >0 if a > b, <0 if a < b, 0 if equal. */
185
+ const compareSemver = (a: string, b: string): number => {
186
+ const parse = (s: string): number[] =>
187
+ s.replace(/^[^0-9]*/, '').split('.').map(n => parseInt(n, 10) || 0)
188
+ const pa = parse(a)
189
+ const pb = parse(b)
190
+ for (let i = 0; i < 3; i++) {
191
+ const diff = (pa[i] ?? 0) - (pb[i] ?? 0)
192
+ if (diff !== 0) return diff
193
+ }
194
+ return 0
195
+ }
196
+
197
+ /** Match package name allowing bare name without scope (@owlmeans/ prefix optional). */
198
+ const matchPkgName = (filter: string, pkgName: string): boolean => {
199
+ if (filter === pkgName) return true
200
+ if (!filter.startsWith('@') && pkgName === `@owlmeans/${filter}`) return true
201
+ return false
202
+ }
package/src/index.ts ADDED
@@ -0,0 +1,14 @@
1
+ export { discover } from './discover.js'
2
+ export type { DiscoveredEntry, Manifest, ManifestEntry, DiscoverOptions } from './discover.js'
3
+
4
+ export { detectLinked } from './linked.js'
5
+ export type { LinkedResult } from './linked.js'
6
+
7
+ export { planInstall, AUTO_GENERATED_BANNER } from './plan.js'
8
+ export type { InstallAction, InstallItem, PlanOptions } from './plan.js'
9
+
10
+ export { applyInstall } from './apply.js'
11
+ export type { ApplyResult } from './apply.js'
12
+
13
+ export { run } from './run.js'
14
+ export type { RunResult } from './run.js'
package/src/linked.ts ADDED
@@ -0,0 +1,51 @@
1
+ import { existsSync, readdirSync, lstatSync, realpathSync } from 'node:fs'
2
+ import { join, resolve } from 'node:path'
3
+
4
+ export interface LinkedResult {
5
+ linked: boolean
6
+ /** Symlinked package names found in node_modules. */
7
+ evidence: string[]
8
+ }
9
+
10
+ /**
11
+ * Detect whether @owlmeans/* packages in node_modules are symlinked
12
+ * (i.e. this project has them via a workspace/libraries/ dev link, not
13
+ * from the npm registry). Returns { linked: true } if any are found.
14
+ *
15
+ * A package is considered linked when its node_modules/@owlmeans/<pkg>
16
+ * entry is a symlink whose real path escapes the node_modules tree.
17
+ */
18
+ export const detectLinked = (targetDir: string): LinkedResult => {
19
+ const nmScope = join(targetDir, 'node_modules', '@owlmeans')
20
+ if (!existsSync(nmScope)) return { linked: false, evidence: [] }
21
+
22
+ let pkgNames: string[]
23
+ try {
24
+ pkgNames = readdirSync(nmScope)
25
+ } catch {
26
+ return { linked: false, evidence: [] }
27
+ }
28
+
29
+ const evidence: string[] = []
30
+ const nmDir = resolve(join(targetDir, 'node_modules'))
31
+
32
+ for (const pkg of pkgNames) {
33
+ const pkgPath = join(nmScope, pkg)
34
+ try {
35
+ const stat = lstatSync(pkgPath)
36
+ if (stat.isSymbolicLink()) {
37
+ evidence.push(`@owlmeans/${pkg}`)
38
+ continue
39
+ }
40
+ // Also check if realpath escapes node_modules (e.g. hoisted symlink)
41
+ const real = realpathSync(pkgPath)
42
+ if (!real.startsWith(nmDir + '/') && !real.startsWith(nmDir + '\\')) {
43
+ evidence.push(`@owlmeans/${pkg}`)
44
+ }
45
+ } catch {
46
+ // Ignore stat errors
47
+ }
48
+ }
49
+
50
+ return { linked: evidence.length > 0, evidence }
51
+ }
package/src/plan.ts ADDED
@@ -0,0 +1,69 @@
1
+ import { existsSync, readFileSync } from 'node:fs'
2
+ import { join } from 'node:path'
3
+ import type { DiscoveredEntry } from './discover.js'
4
+
5
+ export type InstallAction =
6
+ | 'install' // target missing → write
7
+ | 'skip-uptodate' // bytes identical → no-op
8
+ | 'update' // managed (banner present) and differs → overwrite
9
+ | 'conflict' // local edit (no banner) and differs → skip or prompt
10
+
11
+ export interface InstallItem {
12
+ entry: DiscoveredEntry
13
+ targetPath: string
14
+ action: InstallAction
15
+ }
16
+
17
+ /** The banner text injected by sync-agent-meta into every generated file. */
18
+ export const AUTO_GENERATED_BANNER = '<!-- AUTO-GENERATED — do not edit. Regenerate via sync-agent-meta. -->'
19
+
20
+ const targetPath = (targetDir: string, entry: DiscoveredEntry): string => {
21
+ if (entry.kind === 'skill') {
22
+ return join(targetDir, '.claude', 'skills', entry.name, 'SKILL.md')
23
+ }
24
+ return join(targetDir, '.github', 'instructions', `${entry.name}.instructions.md`)
25
+ }
26
+
27
+ export interface PlanOptions {
28
+ /** Treat conflicts as 'overwrite' (--force). */
29
+ force?: boolean
30
+ }
31
+
32
+ export const planInstall = (
33
+ entries: DiscoveredEntry[],
34
+ targetDir: string,
35
+ opts: PlanOptions = {},
36
+ ): InstallItem[] => {
37
+ return entries.map(entry => {
38
+ const path = targetPath(targetDir, entry)
39
+
40
+ if (!existsSync(path)) {
41
+ return { entry, targetPath: path, action: 'install' }
42
+ }
43
+
44
+ let existing: string
45
+ try {
46
+ existing = readFileSync(path, 'utf8')
47
+ } catch {
48
+ return { entry, targetPath: path, action: 'install' }
49
+ }
50
+
51
+ const source = readFileSync(entry.sourcePath, 'utf8')
52
+
53
+ if (existing === source) {
54
+ return { entry, targetPath: path, action: 'skip-uptodate' }
55
+ }
56
+
57
+ const isManaged = existing.includes(AUTO_GENERATED_BANNER)
58
+ if (isManaged) {
59
+ return { entry, targetPath: path, action: 'update' }
60
+ }
61
+
62
+ // Local edit: skip by default, overwrite if --force
63
+ return {
64
+ entry,
65
+ targetPath: path,
66
+ action: opts.force ? 'update' : 'conflict',
67
+ }
68
+ })
69
+ }