@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.
- package/README.md +92 -0
- package/agent-meta/instructions/shadcn-versions.instructions.md +46 -0
- package/agent-meta/instructions/shadcn-web.instructions.md +94 -0
- package/agent-meta/manifest.json +37 -0
- package/agent-meta/skills/shadcn-versions/SKILL.md +126 -0
- package/agent-meta/skills/shadcn-web/SKILL.md +222 -0
- package/build/apply.d.ts +13 -0
- package/build/apply.d.ts.map +1 -0
- package/build/apply.js +28 -0
- package/build/apply.js.map +1 -0
- package/build/args.d.ts +14 -0
- package/build/args.d.ts.map +1 -0
- package/build/args.js +93 -0
- package/build/args.js.map +1 -0
- package/build/bin.d.ts +3 -0
- package/build/bin.d.ts.map +1 -0
- package/build/bin.js +21 -0
- package/build/bin.js.map +1 -0
- package/build/discover.d.ts +41 -0
- package/build/discover.d.ts.map +1 -0
- package/build/discover.js +160 -0
- package/build/discover.js.map +1 -0
- package/build/index.d.ts +11 -0
- package/build/index.d.ts.map +1 -0
- package/build/index.js +6 -0
- package/build/index.js.map +1 -0
- package/build/linked.d.ts +15 -0
- package/build/linked.d.ts.map +1 -0
- package/build/linked.js +44 -0
- package/build/linked.js.map +1 -0
- package/build/plan.d.ts +15 -0
- package/build/plan.d.ts.map +1 -0
- package/build/plan.js +40 -0
- package/build/plan.js.map +1 -0
- package/build/prompt.d.ts +4 -0
- package/build/prompt.d.ts.map +1 -0
- package/build/prompt.js +21 -0
- package/build/prompt.js.map +1 -0
- package/build/run.d.ts +18 -0
- package/build/run.d.ts.map +1 -0
- package/build/run.js +124 -0
- package/build/run.js.map +1 -0
- package/package.json +37 -0
- package/src/apply.ts +38 -0
- package/src/args.ts +108 -0
- package/src/bin.ts +20 -0
- package/src/discover.ts +202 -0
- package/src/index.ts +14 -0
- package/src/linked.ts +51 -0
- package/src/plan.ts +69 -0
- package/src/prompt.ts +25 -0
- package/src/run.ts +167 -0
- package/tests/installer.test.ts +297 -0
- 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
|
package/build/run.js.map
ADDED
|
@@ -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
|
+
}
|
package/src/discover.ts
ADDED
|
@@ -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
|
+
}
|