@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/src/prompt.ts ADDED
@@ -0,0 +1,25 @@
1
+ import { createInterface } from 'node:readline/promises'
2
+ import { stdin, stdout } from 'node:process'
3
+
4
+ let rl: ReturnType<typeof createInterface> | null = null
5
+
6
+ const getReadline = (): ReturnType<typeof createInterface> => {
7
+ if (rl == null) {
8
+ rl = createInterface({ input: stdin, output: stdout })
9
+ }
10
+ return rl
11
+ }
12
+
13
+ export const closeReadline = (): void => {
14
+ if (rl != null) {
15
+ rl.close()
16
+ rl = null
17
+ }
18
+ }
19
+
20
+ export const confirm = async (question: string): Promise<boolean> => {
21
+ const answer = await getReadline().question(`${question} [y/N] `)
22
+ return answer.trim().toLowerCase() === 'y' || answer.trim().toLowerCase() === 'yes'
23
+ }
24
+
25
+ export const isTTY = (): boolean => process.stdin.isTTY === true
package/src/run.ts ADDED
@@ -0,0 +1,167 @@
1
+ import { resolve } from 'node:path'
2
+ import type { CliArgs } from './args.js'
3
+ import { discover } from './discover.js'
4
+ import { detectLinked } from './linked.js'
5
+ import { planInstall } from './plan.js'
6
+ import { applyInstall } from './apply.js'
7
+ import { confirm, closeReadline, isTTY } from './prompt.js'
8
+ import type { InstallAction, InstallItem } from './plan.js'
9
+
10
+ const ACTION_LABEL: Record<InstallAction, string> = {
11
+ 'install': 'install',
12
+ 'skip-uptodate': 'up-to-date',
13
+ 'update': 'update',
14
+ 'conflict': 'CONFLICT',
15
+ }
16
+
17
+ const padEnd = (s: string, n: number): string => s + ' '.repeat(Math.max(0, n - s.length))
18
+
19
+ const printTable = (items: InstallItem[]): void => {
20
+ const colWidths = {
21
+ kind: 11,
22
+ name: Math.max(...items.map(i => i.entry.name.length), 4) + 2,
23
+ pkg: Math.max(...items.map(i => i.entry.packageName.length), 7) + 2,
24
+ action: 10,
25
+ }
26
+
27
+ const header =
28
+ padEnd('kind', colWidths.kind) +
29
+ padEnd('name', colWidths.name) +
30
+ padEnd('package', colWidths.pkg) +
31
+ 'action'
32
+ process.stdout.write(header + '\n')
33
+ process.stdout.write('-'.repeat(header.length) + '\n')
34
+
35
+ for (const item of items) {
36
+ const kindLabel = item.entry.kind === 'skill' ? 'skill' : 'instruction'
37
+ const actionLabel = ACTION_LABEL[item.action]
38
+ process.stdout.write(
39
+ padEnd(kindLabel, colWidths.kind) +
40
+ padEnd(item.entry.name, colWidths.name) +
41
+ padEnd(item.entry.packageName, colWidths.pkg) +
42
+ actionLabel +
43
+ '\n',
44
+ )
45
+ }
46
+ process.stdout.write('\n')
47
+ }
48
+
49
+ export type RunResult =
50
+ | { code: 0 }
51
+ | { code: 2; message: string } // CLI parse
52
+ | { code: 3; message: string } // nothing found
53
+ | { code: 4; message: string } // linked refusal
54
+ | { code: 5; message: string } // unresolved conflicts
55
+
56
+ export const run = async (args: CliArgs): Promise<RunResult> => {
57
+ const targetDir = resolve(args.dir)
58
+
59
+ // 1. Linked monorepo check
60
+ if (!args.force) {
61
+ const linked = detectLinked(targetDir)
62
+ if (linked.linked) {
63
+ const pkgList = linked.evidence.slice(0, 5).join(', ')
64
+ const more = linked.evidence.length > 5 ? ` (+${linked.evidence.length - 5} more)` : ''
65
+ process.stderr.write(
66
+ `\nerror: linked monorepo detected — ${pkgList}${more} are symlinked.\n\n` +
67
+ `In this setup, agents already load guidance from the linked monorepo's\n` +
68
+ `root .claude/skills/ and .github/instructions/. Installing embedded copies\n` +
69
+ `would place stale duplicates that conflict with the canonical live files.\n\n` +
70
+ `Pass --force only if you understand this and want to proceed anyway.\n\n`,
71
+ )
72
+ return { code: 4, message: 'linked monorepo' }
73
+ }
74
+ }
75
+
76
+ // 2. Discover
77
+ const entries = discover(targetDir, {
78
+ extras: args.extras,
79
+ only: args.only.length > 0 ? args.only : undefined,
80
+ claudeOnly: args.claudeOnly,
81
+ copilotOnly: args.copilotOnly,
82
+ })
83
+
84
+ if (entries.length === 0) {
85
+ process.stdout.write(
86
+ `No embedded @owlmeans/* agent guidance found in node_modules.\n` +
87
+ `Install @owlmeans/* packages first, then re-run.\n`,
88
+ )
89
+ return { code: 3, message: 'nothing found' }
90
+ }
91
+
92
+ // 3. Plan
93
+ const items = planInstall(entries, targetDir, { force: args.force })
94
+
95
+ // 4. Print table
96
+ process.stdout.write('\n')
97
+ printTable(items)
98
+
99
+ if (args.dryRun) {
100
+ process.stdout.write('(dry-run — no files written)\n')
101
+ return { code: 0 }
102
+ }
103
+
104
+ // 5. Count actionable items
105
+ const toWrite = items.filter(i => i.action === 'install' || i.action === 'update')
106
+ const conflicts = items.filter(i => i.action === 'conflict')
107
+
108
+ if (toWrite.length === 0 && conflicts.length === 0) {
109
+ process.stdout.write('Everything up to date.\n')
110
+ closeReadline()
111
+ return { code: 0 }
112
+ }
113
+
114
+ // 6. Interactive: confirm per conflict, or if not --yes confirm overall
115
+ const resolvedItems = [...items]
116
+
117
+ if (isTTY() && !args.yes && toWrite.length > 0) {
118
+ const ok = await confirm(`Install ${toWrite.length} file(s) into ${targetDir}?`)
119
+ if (!ok) {
120
+ process.stdout.write('Aborted.\n')
121
+ closeReadline()
122
+ return { code: 0 }
123
+ }
124
+ }
125
+
126
+ // Per-conflict prompts in interactive mode
127
+ if (isTTY() && conflicts.length > 0) {
128
+ for (const item of resolvedItems) {
129
+ if (item.action !== 'conflict') continue
130
+ const ok = await confirm(
131
+ ` ${item.entry.name} has local edits — overwrite ${item.targetPath}?`,
132
+ )
133
+ if (ok) {
134
+ // Upgrade to update
135
+ Object.assign(item, { action: 'update' })
136
+ }
137
+ }
138
+ }
139
+
140
+ closeReadline()
141
+
142
+ const remainingConflicts = resolvedItems.filter(i => i.action === 'conflict')
143
+ if (remainingConflicts.length > 0) {
144
+ process.stdout.write(
145
+ `\nSkipping ${remainingConflicts.length} file(s) with local edits:\n`,
146
+ )
147
+ for (const c of remainingConflicts) {
148
+ process.stdout.write(` ${c.targetPath}\n`)
149
+ }
150
+ process.stdout.write(`Pass --force to overwrite.\n\n`)
151
+
152
+ // In non-interactive --yes mode with remaining conflicts, exit 5
153
+ if (!isTTY() && !args.yes && !args.force) {
154
+ return { code: 5, message: 'unresolved conflicts' }
155
+ }
156
+ }
157
+
158
+ // 7. Apply
159
+ const result = applyInstall(resolvedItems)
160
+
161
+ process.stdout.write(
162
+ `Done: ${result.installed} installed, ${result.updated} updated, ` +
163
+ `${result.skipped} up-to-date, ${result.conflicts} skipped (conflicts).\n`,
164
+ )
165
+
166
+ return { code: 0 }
167
+ }
@@ -0,0 +1,297 @@
1
+ import { describe, it, expect, beforeEach, afterEach } from 'bun:test'
2
+ import { existsSync, mkdirSync, readFileSync, rmSync, symlinkSync, writeFileSync } from 'node:fs'
3
+ import { join } from 'node:path'
4
+ import { tmpdir } from 'node:os'
5
+
6
+ import { discover } from '../src/discover.js'
7
+ import { detectLinked } from '../src/linked.js'
8
+ import { planInstall, AUTO_GENERATED_BANNER } from '../src/plan.js'
9
+ import { applyInstall } from '../src/apply.js'
10
+
11
+ // ---------------------------------------------------------------------------
12
+ // Fixture helpers
13
+ // ---------------------------------------------------------------------------
14
+
15
+ const BANNER = AUTO_GENERATED_BANNER
16
+
17
+ const makeFixtureManifest = (pkgName: string, version: string, entries: Array<{
18
+ kind: 'skill' | 'instruction'
19
+ name: string
20
+ category?: 'package-specific' | 'multi-package'
21
+ }>) => ({
22
+ schemaVersion: 1,
23
+ package: pkgName,
24
+ version,
25
+ generatedAt: '2026-06-10T00:00:00Z',
26
+ canonicalRepo: 'https://github.com/owlmeans/common',
27
+ entries: entries.map(e => ({
28
+ kind: e.kind,
29
+ name: e.name,
30
+ category: e.category ?? 'package-specific',
31
+ file: e.kind === 'skill'
32
+ ? `skills/${e.name}/SKILL.md`
33
+ : `instructions/${e.name}.instructions.md`,
34
+ canonicalPath: e.kind === 'skill'
35
+ ? `.claude/skills/${e.name}/SKILL.md`
36
+ : `.github/instructions/${e.name}.instructions.md`,
37
+ })),
38
+ })
39
+
40
+ const writeFixturePackage = (
41
+ nmDir: string,
42
+ pkgName: string,
43
+ version: string,
44
+ entries: Array<{ kind: 'skill' | 'instruction'; name: string }>,
45
+ ): void => {
46
+ const pkgDir = join(nmDir, '@owlmeans', pkgName.replace('@owlmeans/', ''))
47
+ const agentMetaDir = join(pkgDir, 'agent-meta')
48
+ mkdirSync(join(agentMetaDir, 'skills'), { recursive: true })
49
+ mkdirSync(join(agentMetaDir, 'instructions'), { recursive: true })
50
+
51
+ const manifest = makeFixtureManifest(pkgName, version, entries)
52
+ writeFileSync(join(agentMetaDir, 'manifest.json'), JSON.stringify(manifest, null, 2), 'utf8')
53
+
54
+ for (const e of entries) {
55
+ let content = `${BANNER}\n`
56
+ if (e.kind === 'skill') {
57
+ content += `---\nname: ${e.name}\n---\n# ${e.name}\nSkill content.\n`
58
+ const skillDir = join(agentMetaDir, 'skills', e.name)
59
+ mkdirSync(skillDir, { recursive: true })
60
+ writeFileSync(join(skillDir, 'SKILL.md'), content, 'utf8')
61
+ } else {
62
+ content += `---\ndescription: "${e.name}"\napplyTo: "**/*.ts"\n---\n# ${e.name}\n`
63
+ writeFileSync(join(agentMetaDir, 'instructions', `${e.name}.instructions.md`), content, 'utf8')
64
+ }
65
+ }
66
+ }
67
+
68
+ // ---------------------------------------------------------------------------
69
+ // Test setup
70
+ // ---------------------------------------------------------------------------
71
+
72
+ let tmpDir: string
73
+
74
+ beforeEach(() => {
75
+ tmpDir = join(tmpdir(), `agent-skills-test-${Date.now()}-${Math.random().toString(36).slice(2)}`)
76
+ mkdirSync(tmpDir, { recursive: true })
77
+ })
78
+
79
+ afterEach(() => {
80
+ rmSync(tmpDir, { recursive: true, force: true })
81
+ })
82
+
83
+ // ---------------------------------------------------------------------------
84
+ // discover()
85
+ // ---------------------------------------------------------------------------
86
+
87
+ describe('discover()', () => {
88
+ it('returns empty when no node_modules/@owlmeans exists', () => {
89
+ const entries = discover(tmpDir, { extras: false })
90
+ expect(entries).toEqual([])
91
+ })
92
+
93
+ it('discovers entries from node_modules', () => {
94
+ writeFixturePackage(join(tmpDir, 'node_modules'), '@owlmeans/context', '0.1.7', [
95
+ { kind: 'skill', name: 'context' },
96
+ { kind: 'instruction', name: 'context' },
97
+ ])
98
+ const entries = discover(tmpDir, { extras: false })
99
+ expect(entries).toHaveLength(2)
100
+ expect(entries.some(e => e.kind === 'skill' && e.name === 'context')).toBe(true)
101
+ expect(entries.some(e => e.kind === 'instruction' && e.name === 'context')).toBe(true)
102
+ })
103
+
104
+ it('deduplicates by (kind, name), keeping highest version', () => {
105
+ writeFixturePackage(join(tmpDir, 'node_modules'), '@owlmeans/context', '0.1.5', [
106
+ { kind: 'skill', name: 'context' },
107
+ ])
108
+ writeFixturePackage(join(tmpDir, 'node_modules'), '@owlmeans/auth', '0.1.7', [
109
+ { kind: 'skill', name: 'context' }, // same name, higher version
110
+ ])
111
+ const entries = discover(tmpDir, { extras: false })
112
+ expect(entries).toHaveLength(1)
113
+ expect(entries[0].version).toBe('0.1.7')
114
+ })
115
+
116
+ it('--only filter restricts by package name', () => {
117
+ writeFixturePackage(join(tmpDir, 'node_modules'), '@owlmeans/context', '0.1.7', [
118
+ { kind: 'skill', name: 'context' },
119
+ ])
120
+ writeFixturePackage(join(tmpDir, 'node_modules'), '@owlmeans/auth', '0.1.7', [
121
+ { kind: 'skill', name: 'auth' },
122
+ ])
123
+ const entries = discover(tmpDir, { extras: false, only: ['@owlmeans/context'] })
124
+ expect(entries).toHaveLength(1)
125
+ expect(entries[0].name).toBe('context')
126
+ })
127
+
128
+ it('--claude-only filter', () => {
129
+ writeFixturePackage(join(tmpDir, 'node_modules'), '@owlmeans/context', '0.1.7', [
130
+ { kind: 'skill', name: 'context' },
131
+ { kind: 'instruction', name: 'context' },
132
+ ])
133
+ const entries = discover(tmpDir, { extras: false, claudeOnly: true })
134
+ expect(entries.every(e => e.kind === 'skill')).toBe(true)
135
+ })
136
+
137
+ it('--copilot-only filter', () => {
138
+ writeFixturePackage(join(tmpDir, 'node_modules'), '@owlmeans/context', '0.1.7', [
139
+ { kind: 'skill', name: 'context' },
140
+ { kind: 'instruction', name: 'context' },
141
+ ])
142
+ const entries = discover(tmpDir, { extras: false, copilotOnly: true })
143
+ expect(entries.every(e => e.kind === 'instruction')).toBe(true)
144
+ })
145
+ })
146
+
147
+ // ---------------------------------------------------------------------------
148
+ // detectLinked()
149
+ // ---------------------------------------------------------------------------
150
+
151
+ describe('detectLinked()', () => {
152
+ it('returns linked=false when no node_modules', () => {
153
+ const result = detectLinked(tmpDir)
154
+ expect(result.linked).toBe(false)
155
+ expect(result.evidence).toEqual([])
156
+ })
157
+
158
+ it('returns linked=false for regular (non-symlinked) packages', () => {
159
+ writeFixturePackage(join(tmpDir, 'node_modules'), '@owlmeans/context', '0.1.7', [])
160
+ const result = detectLinked(tmpDir)
161
+ expect(result.linked).toBe(false)
162
+ })
163
+
164
+ it('detects symlinked packages', () => {
165
+ const nmScope = join(tmpDir, 'node_modules', '@owlmeans')
166
+ mkdirSync(nmScope, { recursive: true })
167
+ // Create real package elsewhere and symlink it
168
+ const realPkg = join(tmpDir, 'real-pkg')
169
+ mkdirSync(realPkg)
170
+ symlinkSync(realPkg, join(nmScope, 'context'))
171
+
172
+ const result = detectLinked(tmpDir)
173
+ expect(result.linked).toBe(true)
174
+ expect(result.evidence).toContain('@owlmeans/context')
175
+ })
176
+ })
177
+
178
+ // ---------------------------------------------------------------------------
179
+ // planInstall()
180
+ // ---------------------------------------------------------------------------
181
+
182
+ describe('planInstall()', () => {
183
+ it('marks missing targets as install', () => {
184
+ writeFixturePackage(join(tmpDir, 'node_modules'), '@owlmeans/context', '0.1.7', [
185
+ { kind: 'skill', name: 'context' },
186
+ ])
187
+ const entries = discover(tmpDir, { extras: false })
188
+ const items = planInstall(entries, tmpDir)
189
+ expect(items).toHaveLength(1)
190
+ expect(items[0].action).toBe('install')
191
+ })
192
+
193
+ it('marks identical files as skip-uptodate', () => {
194
+ writeFixturePackage(join(tmpDir, 'node_modules'), '@owlmeans/context', '0.1.7', [
195
+ { kind: 'skill', name: 'context' },
196
+ ])
197
+ const entries = discover(tmpDir, { extras: false })
198
+ // Write same content to target
199
+ const targetDir = join(tmpDir, '.claude', 'skills', 'context')
200
+ mkdirSync(targetDir, { recursive: true })
201
+ const source = readFileSync(entries[0].sourcePath, 'utf8')
202
+ writeFileSync(join(targetDir, 'SKILL.md'), source, 'utf8')
203
+
204
+ const items = planInstall(entries, tmpDir)
205
+ expect(items[0].action).toBe('skip-uptodate')
206
+ })
207
+
208
+ it('marks managed (banner) changed files as update', () => {
209
+ writeFixturePackage(join(tmpDir, 'node_modules'), '@owlmeans/context', '0.1.7', [
210
+ { kind: 'skill', name: 'context' },
211
+ ])
212
+ const entries = discover(tmpDir, { extras: false })
213
+ const targetDir = join(tmpDir, '.claude', 'skills', 'context')
214
+ mkdirSync(targetDir, { recursive: true })
215
+ // Write old managed content (has banner, but different)
216
+ writeFileSync(join(targetDir, 'SKILL.md'), `${BANNER}\n# Old content\n`, 'utf8')
217
+
218
+ const items = planInstall(entries, tmpDir)
219
+ expect(items[0].action).toBe('update')
220
+ })
221
+
222
+ it('marks locally-edited (no banner) files as conflict', () => {
223
+ writeFixturePackage(join(tmpDir, 'node_modules'), '@owlmeans/context', '0.1.7', [
224
+ { kind: 'skill', name: 'context' },
225
+ ])
226
+ const entries = discover(tmpDir, { extras: false })
227
+ const targetDir = join(tmpDir, '.claude', 'skills', 'context')
228
+ mkdirSync(targetDir, { recursive: true })
229
+ writeFileSync(join(targetDir, 'SKILL.md'), '# My custom skill\n', 'utf8')
230
+
231
+ const items = planInstall(entries, tmpDir)
232
+ expect(items[0].action).toBe('conflict')
233
+ })
234
+
235
+ it('--force upgrades conflict to update', () => {
236
+ writeFixturePackage(join(tmpDir, 'node_modules'), '@owlmeans/context', '0.1.7', [
237
+ { kind: 'skill', name: 'context' },
238
+ ])
239
+ const entries = discover(tmpDir, { extras: false })
240
+ const targetDir = join(tmpDir, '.claude', 'skills', 'context')
241
+ mkdirSync(targetDir, { recursive: true })
242
+ writeFileSync(join(targetDir, 'SKILL.md'), '# My custom skill\n', 'utf8')
243
+
244
+ const items = planInstall(entries, tmpDir, { force: true })
245
+ expect(items[0].action).toBe('update')
246
+ })
247
+ })
248
+
249
+ // ---------------------------------------------------------------------------
250
+ // applyInstall() — idempotency
251
+ // ---------------------------------------------------------------------------
252
+
253
+ describe('applyInstall()', () => {
254
+ it('writes install items', () => {
255
+ writeFixturePackage(join(tmpDir, 'node_modules'), '@owlmeans/context', '0.1.7', [
256
+ { kind: 'skill', name: 'context' },
257
+ { kind: 'instruction', name: 'context' },
258
+ ])
259
+ const entries = discover(tmpDir, { extras: false })
260
+ const items = planInstall(entries, tmpDir)
261
+ const result = applyInstall(items)
262
+ expect(result.installed).toBe(2)
263
+ expect(existsSync(join(tmpDir, '.claude', 'skills', 'context', 'SKILL.md'))).toBe(true)
264
+ expect(existsSync(join(tmpDir, '.github', 'instructions', 'context.instructions.md'))).toBe(true)
265
+ })
266
+
267
+ it('is idempotent — second run yields all skip-uptodate', () => {
268
+ writeFixturePackage(join(tmpDir, 'node_modules'), '@owlmeans/context', '0.1.7', [
269
+ { kind: 'skill', name: 'context' },
270
+ ])
271
+ const entries = discover(tmpDir, { extras: false })
272
+ // First run
273
+ applyInstall(planInstall(entries, tmpDir))
274
+ // Second run
275
+ const items2 = planInstall(entries, tmpDir)
276
+ expect(items2[0].action).toBe('skip-uptodate')
277
+ const result2 = applyInstall(items2)
278
+ expect(result2.installed).toBe(0)
279
+ expect(result2.skipped).toBe(1)
280
+ })
281
+
282
+ it('does not write conflict items', () => {
283
+ writeFixturePackage(join(tmpDir, 'node_modules'), '@owlmeans/context', '0.1.7', [
284
+ { kind: 'skill', name: 'context' },
285
+ ])
286
+ const entries = discover(tmpDir, { extras: false })
287
+ const targetDir = join(tmpDir, '.claude', 'skills', 'context')
288
+ mkdirSync(targetDir, { recursive: true })
289
+ const localContent = '# My custom skill\n'
290
+ writeFileSync(join(targetDir, 'SKILL.md'), localContent, 'utf8')
291
+
292
+ const items = planInstall(entries, tmpDir)
293
+ applyInstall(items)
294
+ // File should remain unchanged
295
+ expect(readFileSync(join(targetDir, 'SKILL.md'), 'utf8')).toBe(localContent)
296
+ })
297
+ })
package/tsconfig.json ADDED
@@ -0,0 +1,11 @@
1
+ {
2
+ "extends": [
3
+ "@owlmeans/dep-config/tsconfig.base.json"
4
+ ],
5
+ "compilerOptions": {
6
+ "rootDir": "./src/",
7
+ "outDir": "./build/",
8
+ "types": ["node"]
9
+ },
10
+ "exclude": ["./dist/**/*", "./build/**/*", "./tests/**/*", "./*.ts"]
11
+ }