@abarcenas/ai-workflow-template 1.49.0 → 2.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 (35) hide show
  1. package/.agents/instructions/knowledgebase.instructions.md +2 -2
  2. package/.agents/instructions/learned-knowledge.instructions.md +47 -2
  3. package/.agents/instructions/memory-schema.instructions.md +1 -1
  4. package/.github/workflows/npm-publish.yml +69 -1
  5. package/.opencode/plugins/graphify.js +22 -0
  6. package/LICENSE +15 -0
  7. package/README.md +56 -22
  8. package/memory-bank/.vocabulary.json +3 -0
  9. package/opencode.mcp.example.json +13 -0
  10. package/package.json +20 -10
  11. package/scripts/bump-version.js +18 -2
  12. package/scripts/mcp/playwright-mcp-launcher.js +45 -2
  13. package/scripts/bump-version.test.js +0 -238
  14. package/scripts/knowledgebase-cli.test.js +0 -576
  15. package/scripts/knowledgebase-index.test.js +0 -759
  16. package/scripts/mcp/playwright-mcp-launcher.test.js +0 -217
  17. package/scripts/mcp-knowledgebase-server.test.js +0 -745
  18. package/scripts/mcp-memory-server.test.js +0 -398
  19. package/scripts/memory-cli.test.js +0 -297
  20. package/scripts/memory-index.test.js +0 -1173
  21. package/scripts/normalize-memory.test.js +0 -753
  22. package/scripts/setup/discover.test.js +0 -530
  23. package/scripts/setup/hooks.test.js +0 -722
  24. package/scripts/setup/husky-init.test.js +0 -311
  25. package/scripts/setup/index.test.js +0 -1021
  26. package/scripts/setup/knowledgebase.test.js +0 -352
  27. package/scripts/setup/prepare.test.js +0 -718
  28. package/scripts/setup/skills-phase.test.js +0 -1274
  29. package/scripts/setup/sync-phase.test.js +0 -373
  30. package/scripts/setup/ui.test.js +0 -579
  31. package/scripts/setup/utils.test.js +0 -586
  32. package/scripts/sync-unit.test.js +0 -1881
  33. package/scripts/sync.test.js +0 -472
  34. package/scripts/validate-memory-schema.test.js +0 -328
  35. package/scripts/vocab-sync.test.js +0 -902
@@ -1,718 +0,0 @@
1
- // ═════════════════════════════════════════════════════════════════════════════
2
- // prepare.test.js — Unit tests for the prepare script handling module
3
- // ═════════════════════════════════════════════════════════════════════════════
4
- //
5
- // Tests the 6-case prepare script algorithm implemented in prepare.js using
6
- // real temp directories. The ui module is mocked to keep test output clean.
7
- //
8
- // Covered scenarios (≥12, per plan T16 + task description):
9
- // 1. No package.json → action='skipped', message includes 'No package.json'
10
- // 2. prepare already has "husky" → action='skipped', message includes 'already'
11
- // 3. No prepare script → action='added', package.json gets "prepare": "husky"
12
- // 4. Simple prepare (merge) → action='merged', value becomes "npm run build && husky"
13
- // 5. Complex prepare (multi-line) → action='skipped', manual merge required
14
- // 6. Complex prepare with || → action='skipped', manual merge required
15
- // 7. Complex prepare with ; → action='skipped', manual merge required
16
- // 8. Complex prepare + --force → action='added', value becomes "husky"
17
- // 9. dryRun=true → action='dry-run', package.json NOT modified
18
- // 10. Empty string prepare → treated as no prepare, action='added'
19
- // 11. Whitespace-only prepare → treated as empty, action='added'
20
- // 12. "husky && npm run build" → action='skipped' (substring detection)
21
- // ═════════════════════════════════════════════════════════════════════════════
22
-
23
- import { vi, describe, it, expect, afterEach } from 'vitest'
24
- import {
25
- existsSync,
26
- mkdtempSync,
27
- readFileSync,
28
- rmSync,
29
- writeFileSync,
30
- realpathSync,
31
- } from 'fs'
32
- import { join } from 'path'
33
- import { tmpdir } from 'os'
34
-
35
- // ── Mock ui module to keep test output clean ──────────────────────────────
36
-
37
- vi.mock('./ui.js', () => ({
38
- step: vi.fn(),
39
- stepSuccess: vi.fn(),
40
- stepWarn: vi.fn(),
41
- }))
42
-
43
- import { handlePrepare } from './prepare.js'
44
-
45
- // ── Test helpers ──────────────────────────────────────────────────────────
46
-
47
- const tempDirs = new Set()
48
-
49
- /**
50
- * Creates and registers a temporary directory for a single test case.
51
- * Automatically cleaned up in `afterEach`.
52
- *
53
- * @returns {string} Absolute path to the new temp directory.
54
- */
55
- function createTempDir() {
56
- const dir = mkdtempSync(join(realpathSync(tmpdir()), 'prepare-test-'))
57
- tempDirs.add(dir)
58
- return dir
59
- }
60
-
61
- function cleanupTempDirs() {
62
- for (const dir of tempDirs) {
63
- try {
64
- rmSync(dir, { recursive: true, force: true })
65
- } catch {
66
- /* best-effort cleanup */
67
- }
68
- }
69
- tempDirs.clear()
70
- }
71
-
72
- /**
73
- * Writes a package.json fixture into the given directory.
74
- *
75
- * @param {string} dir - Directory to write into.
76
- * @param {object} [overrides] - Properties to merge into the default package.
77
- * @returns {object} The written package object.
78
- */
79
- function writePkg(dir, overrides = {}) {
80
- const pkg = { name: 'test-pkg', version: '1.0.0', ...overrides }
81
- writeFileSync(join(dir, 'package.json'), JSON.stringify(pkg, null, 2) + '\n')
82
- return pkg
83
- }
84
-
85
- /**
86
- * Reads and parses the package.json in the given directory.
87
- *
88
- * @param {string} dir - Directory containing package.json.
89
- * @returns {object} Parsed package object.
90
- */
91
- function readPkg(dir) {
92
- return JSON.parse(readFileSync(join(dir, 'package.json'), 'utf-8'))
93
- }
94
-
95
- // ── Tests ─────────────────────────────────────────────────────────────────
96
-
97
- describe('handlePrepare', () => {
98
- afterEach(() => {
99
- cleanupTempDirs()
100
- })
101
-
102
- // ── 1. No package.json ─────────────────────────────────────────────────
103
-
104
- it('scenario 1: no consumerPackageJson → action=skipped, message includes "No package.json"', async () => {
105
- const dir = createTempDir()
106
- // No package.json file is written — consumerPackageJson is undefined
107
-
108
- const result = await handlePrepare({
109
- consumerRoot: dir,
110
- consumerPackageJson: undefined,
111
- existingPrepare: undefined,
112
- prepareIsSimple: false,
113
- dryRun: false,
114
- force: false,
115
- })
116
-
117
- expect(result.action).toBe('skipped')
118
- expect(result.message).toContain('No package.json')
119
- // Nothing should have been created
120
- expect(existsSync(join(dir, 'package.json'))).toBe(false)
121
- })
122
-
123
- // ── 2. Prepare already has "husky" ─────────────────────────────────────
124
-
125
- it('scenario 2: prepare already set to exactly "husky" → action=skipped, message includes "already"', async () => {
126
- const dir = createTempDir()
127
- writePkg(dir, { scripts: { prepare: 'husky' } })
128
-
129
- const result = await handlePrepare({
130
- consumerRoot: dir,
131
- consumerPackageJson: { name: 'test-pkg', scripts: { prepare: 'husky' } },
132
- existingPrepare: 'husky',
133
- prepareIsSimple: true,
134
- dryRun: false,
135
- force: false,
136
- })
137
-
138
- expect(result.action).toBe('skipped')
139
- expect(result.message).toContain('already')
140
- // package.json must be unchanged
141
- expect(readPkg(dir).scripts.prepare).toBe('husky')
142
- })
143
-
144
- it('scenario 2b: prepare with "husky && lint" (substring) → action=skipped', async () => {
145
- const dir = createTempDir()
146
- writePkg(dir, { scripts: { prepare: 'husky && lint' } })
147
-
148
- const result = await handlePrepare({
149
- consumerRoot: dir,
150
- consumerPackageJson: { name: 'test-pkg', scripts: { prepare: 'husky && lint' } },
151
- existingPrepare: 'husky && lint',
152
- prepareIsSimple: true,
153
- dryRun: false,
154
- force: false,
155
- })
156
-
157
- expect(result.action).toBe('skipped')
158
- expect(result.message).toContain('already')
159
- })
160
-
161
- it('scenario 2c: prepare with "build && husky" (suffix) → action=skipped', async () => {
162
- const dir = createTempDir()
163
- writePkg(dir, { scripts: { prepare: 'build && husky' } })
164
-
165
- const result = await handlePrepare({
166
- consumerRoot: dir,
167
- consumerPackageJson: { name: 'test-pkg', scripts: { prepare: 'build && husky' } },
168
- existingPrepare: 'build && husky',
169
- prepareIsSimple: true,
170
- dryRun: false,
171
- force: false,
172
- })
173
-
174
- expect(result.action).toBe('skipped')
175
- })
176
-
177
- it('scenario 2d: prepare with "build && husky && lint" (middle) → action=skipped', async () => {
178
- const dir = createTempDir()
179
- writePkg(dir, { scripts: { prepare: 'build && husky && lint' } })
180
-
181
- const result = await handlePrepare({
182
- consumerRoot: dir,
183
- consumerPackageJson: { name: 'test-pkg', scripts: { prepare: 'build && husky && lint' } },
184
- existingPrepare: 'build && husky && lint',
185
- prepareIsSimple: false,
186
- dryRun: false,
187
- force: false,
188
- })
189
-
190
- expect(result.action).toBe('skipped')
191
- })
192
-
193
- // ── 3. No prepare script ───────────────────────────────────────────────
194
-
195
- it('scenario 3: no prepare script → action=added, package.json gets "prepare": "husky"', async () => {
196
- const dir = createTempDir()
197
- writePkg(dir, { scripts: { test: 'vitest' } }) // scripts block present but no prepare
198
-
199
- const result = await handlePrepare({
200
- consumerRoot: dir,
201
- consumerPackageJson: { name: 'test-pkg', scripts: { test: 'vitest' } },
202
- existingPrepare: undefined,
203
- prepareIsSimple: false,
204
- dryRun: false,
205
- force: false,
206
- })
207
-
208
- expect(result.action).toBe('added')
209
- expect(result.message).toContain('Added')
210
- const pkg = readPkg(dir)
211
- expect(pkg.scripts.prepare).toBe('husky')
212
- // Existing scripts are preserved
213
- expect(pkg.scripts.test).toBe('vitest')
214
- })
215
-
216
- it('scenario 3b: no scripts block at all → creates scripts block with prepare', async () => {
217
- const dir = createTempDir()
218
- writePkg(dir, {}) // no scripts block
219
-
220
- const result = await handlePrepare({
221
- consumerRoot: dir,
222
- consumerPackageJson: { name: 'test-pkg' },
223
- existingPrepare: undefined,
224
- prepareIsSimple: false,
225
- dryRun: false,
226
- force: false,
227
- })
228
-
229
- expect(result.action).toBe('added')
230
- const pkg = readPkg(dir)
231
- expect(pkg.scripts).toEqual({ prepare: 'husky' })
232
- })
233
-
234
- // ── 4. Simple prepare (merge) ──────────────────────────────────────────
235
-
236
- it('scenario 4: simple prepare "npm run build" → action=merged, value becomes "npm run build && husky"', async () => {
237
- const dir = createTempDir()
238
- writePkg(dir, { scripts: { prepare: 'npm run build' } })
239
-
240
- const result = await handlePrepare({
241
- consumerRoot: dir,
242
- consumerPackageJson: { name: 'test-pkg', scripts: { prepare: 'npm run build' } },
243
- existingPrepare: 'npm run build',
244
- prepareIsSimple: true,
245
- dryRun: false,
246
- force: false,
247
- })
248
-
249
- expect(result.action).toBe('merged')
250
- expect(result.message).toContain('Merged')
251
- expect(readPkg(dir).scripts.prepare).toBe('npm run build && husky')
252
- })
253
-
254
- it('scenario 4b: simple prepare "echo hello" → action=merged', async () => {
255
- const dir = createTempDir()
256
- writePkg(dir, { scripts: { prepare: 'echo hello' } })
257
-
258
- const result = await handlePrepare({
259
- consumerRoot: dir,
260
- consumerPackageJson: { name: 'test-pkg', scripts: { prepare: 'echo hello' } },
261
- existingPrepare: 'echo hello',
262
- prepareIsSimple: true,
263
- dryRun: false,
264
- force: false,
265
- })
266
-
267
- expect(result.action).toBe('merged')
268
- expect(readPkg(dir).scripts.prepare).toBe('echo hello && husky')
269
- })
270
-
271
- // ── 5. Complex prepare (multi-line) ────────────────────────────────────
272
-
273
- it('scenario 5: multi-line prepare → action=skipped, manual merge required', async () => {
274
- const dir = createTempDir()
275
- const script = 'echo "line1"\necho "line2"\necho "line3"'
276
- writePkg(dir, { scripts: { prepare: script } })
277
-
278
- const result = await handlePrepare({
279
- consumerRoot: dir,
280
- consumerPackageJson: { name: 'test-pkg', scripts: { prepare: script } },
281
- existingPrepare: script,
282
- prepareIsSimple: false,
283
- dryRun: false,
284
- force: false,
285
- })
286
-
287
- expect(result.action).toBe('skipped')
288
- expect(result.message).toContain('manual merge')
289
- // File must not be modified
290
- expect(readPkg(dir).scripts.prepare).toBe(script)
291
- })
292
-
293
- // ── 6. Complex prepare with || ─────────────────────────────────────────
294
-
295
- it('scenario 6: prepare with || operator → action=skipped, manual merge required', async () => {
296
- const dir = createTempDir()
297
- const script = 'cmd1 || cmd2'
298
- writePkg(dir, { scripts: { prepare: script } })
299
-
300
- const result = await handlePrepare({
301
- consumerRoot: dir,
302
- consumerPackageJson: { name: 'test-pkg', scripts: { prepare: script } },
303
- existingPrepare: script,
304
- prepareIsSimple: false,
305
- dryRun: false,
306
- force: false,
307
- })
308
-
309
- expect(result.action).toBe('skipped')
310
- expect(result.message).toContain('manual merge')
311
- expect(readPkg(dir).scripts.prepare).toBe(script)
312
- })
313
-
314
- // ── 7. Complex prepare with ; ──────────────────────────────────────────
315
-
316
- it('scenario 7: prepare with ; separator → action=skipped, manual merge required', async () => {
317
- const dir = createTempDir()
318
- const script = 'cmd1; cmd2'
319
- writePkg(dir, { scripts: { prepare: script } })
320
-
321
- const result = await handlePrepare({
322
- consumerRoot: dir,
323
- consumerPackageJson: { name: 'test-pkg', scripts: { prepare: script } },
324
- existingPrepare: script,
325
- prepareIsSimple: false,
326
- dryRun: false,
327
- force: false,
328
- })
329
-
330
- expect(result.action).toBe('skipped')
331
- expect(result.message).toContain('manual merge')
332
- expect(readPkg(dir).scripts.prepare).toBe(script)
333
- })
334
-
335
- // ── 8. Complex prepare + --force ───────────────────────────────────────
336
-
337
- it('scenario 8: complex prepare + --force → action=added, value becomes "husky"', async () => {
338
- const dir = createTempDir()
339
- const script = 'cmd1 && cmd2 && cmd3 && cmd4'
340
- writePkg(dir, { scripts: { prepare: script } })
341
-
342
- const result = await handlePrepare({
343
- consumerRoot: dir,
344
- consumerPackageJson: { name: 'test-pkg', scripts: { prepare: script } },
345
- existingPrepare: script,
346
- prepareIsSimple: false,
347
- dryRun: false,
348
- force: true,
349
- })
350
-
351
- expect(result.action).toBe('added')
352
- expect(result.message).toContain('Replaced')
353
- expect(readPkg(dir).scripts.prepare).toBe('husky')
354
- })
355
-
356
- it('scenario 8b: multi-line prepare + --force → value replaced with "husky"', async () => {
357
- const dir = createTempDir()
358
- const script = 'echo "a"\necho "b"\necho "c"'
359
- writePkg(dir, { scripts: { prepare: script } })
360
-
361
- const result = await handlePrepare({
362
- consumerRoot: dir,
363
- consumerPackageJson: { name: 'test-pkg', scripts: { prepare: script } },
364
- existingPrepare: script,
365
- prepareIsSimple: false,
366
- dryRun: false,
367
- force: true,
368
- })
369
-
370
- expect(result.action).toBe('added')
371
- expect(readPkg(dir).scripts.prepare).toBe('husky')
372
- })
373
-
374
- // ── 9. dryRun=true ─────────────────────────────────────────────────────
375
-
376
- it('scenario 9a: dryRun + no prepare → action=dry-run, package.json NOT modified', async () => {
377
- const dir = createTempDir()
378
- writePkg(dir, { scripts: { test: 'vitest' } })
379
-
380
- const result = await handlePrepare({
381
- consumerRoot: dir,
382
- consumerPackageJson: { name: 'test-pkg', scripts: { test: 'vitest' } },
383
- existingPrepare: undefined,
384
- prepareIsSimple: false,
385
- dryRun: true,
386
- force: false,
387
- })
388
-
389
- expect(result.action).toBe('dry-run')
390
- expect(result.message).toContain('Would add')
391
- // Verify no change
392
- const pkg = readPkg(dir)
393
- expect(pkg.scripts.prepare).toBeUndefined()
394
- expect(pkg.scripts.test).toBe('vitest')
395
- })
396
-
397
- it('scenario 9b: dryRun + simple prepare → action=dry-run, package.json NOT modified', async () => {
398
- const dir = createTempDir()
399
- writePkg(dir, { scripts: { prepare: 'npm run build' } })
400
-
401
- const result = await handlePrepare({
402
- consumerRoot: dir,
403
- consumerPackageJson: { name: 'test-pkg', scripts: { prepare: 'npm run build' } },
404
- existingPrepare: 'npm run build',
405
- prepareIsSimple: true,
406
- dryRun: true,
407
- force: false,
408
- })
409
-
410
- expect(result.action).toBe('dry-run')
411
- expect(result.message).toContain('Would merge')
412
- expect(readPkg(dir).scripts.prepare).toBe('npm run build') // unchanged
413
- })
414
-
415
- it('scenario 9c: dryRun + complex prepare → action=dry-run, package.json NOT modified', async () => {
416
- const dir = createTempDir()
417
- const script = 'cmd1 && cmd2 && cmd3'
418
- writePkg(dir, { scripts: { prepare: script } })
419
-
420
- const result = await handlePrepare({
421
- consumerRoot: dir,
422
- consumerPackageJson: { name: 'test-pkg', scripts: { prepare: script } },
423
- existingPrepare: script,
424
- prepareIsSimple: false,
425
- dryRun: true,
426
- force: false,
427
- })
428
-
429
- expect(result.action).toBe('dry-run')
430
- expect(result.message).toContain('would skip')
431
- expect(readPkg(dir).scripts.prepare).toBe(script) // unchanged
432
- })
433
-
434
- it('scenario 9d: dryRun + complex prepare + --force → action=dry-run, package.json NOT modified', async () => {
435
- const dir = createTempDir()
436
- const script = 'cmd1 && cmd2 && cmd3'
437
- writePkg(dir, { scripts: { prepare: script } })
438
-
439
- const result = await handlePrepare({
440
- consumerRoot: dir,
441
- consumerPackageJson: { name: 'test-pkg', scripts: { prepare: script } },
442
- existingPrepare: script,
443
- prepareIsSimple: false,
444
- dryRun: true,
445
- force: true,
446
- })
447
-
448
- expect(result.action).toBe('dry-run')
449
- expect(result.message).toContain('Would replace')
450
- expect(readPkg(dir).scripts.prepare).toBe(script) // unchanged
451
- })
452
-
453
- // ── 10. Empty string prepare ───────────────────────────────────────────
454
-
455
- it('scenario 10: empty string prepare → treated as no prepare, action=added', async () => {
456
- const dir = createTempDir()
457
- writePkg(dir, { scripts: { prepare: '' } })
458
-
459
- const result = await handlePrepare({
460
- consumerRoot: dir,
461
- consumerPackageJson: { name: 'test-pkg', scripts: { prepare: '' } },
462
- existingPrepare: '',
463
- prepareIsSimple: false,
464
- dryRun: false,
465
- force: false,
466
- })
467
-
468
- expect(result.action).toBe('added')
469
- expect(readPkg(dir).scripts.prepare).toBe('husky')
470
- })
471
-
472
- // ── 11. Whitespace-only prepare ────────────────────────────────────────
473
-
474
- it('scenario 11: whitespace-only prepare → treated as empty, action=added', async () => {
475
- const dir = createTempDir()
476
- writePkg(dir, { scripts: { prepare: ' ' } })
477
-
478
- const result = await handlePrepare({
479
- consumerRoot: dir,
480
- consumerPackageJson: { name: 'test-pkg', scripts: { prepare: ' ' } },
481
- existingPrepare: ' ',
482
- prepareIsSimple: false,
483
- dryRun: false,
484
- force: false,
485
- })
486
-
487
- expect(result.action).toBe('added')
488
- expect(readPkg(dir).scripts.prepare).toBe('husky')
489
- })
490
-
491
- it('scenario 11b: whitespace-only prepare with tabs and newlines → treated as empty, action=added', async () => {
492
- const dir = createTempDir()
493
- const whitespace = ' \t\n '
494
- writePkg(dir, { scripts: { prepare: whitespace } })
495
-
496
- const result = await handlePrepare({
497
- consumerRoot: dir,
498
- consumerPackageJson: { name: 'test-pkg', scripts: { prepare: whitespace } },
499
- existingPrepare: whitespace,
500
- prepareIsSimple: false,
501
- dryRun: false,
502
- force: false,
503
- })
504
-
505
- expect(result.action).toBe('added')
506
- expect(readPkg(dir).scripts.prepare).toBe('husky')
507
- })
508
-
509
- // ── 12. "husky && ..." substring detection ─────────────────────────────
510
-
511
- it('scenario 12: "husky && npm run build" → action=skipped (substring detects husky)', async () => {
512
- const dir = createTempDir()
513
- const script = 'husky && npm run build'
514
- writePkg(dir, { scripts: { prepare: script } })
515
-
516
- const result = await handlePrepare({
517
- consumerRoot: dir,
518
- consumerPackageJson: { name: 'test-pkg', scripts: { prepare: script } },
519
- existingPrepare: script,
520
- prepareIsSimple: true,
521
- dryRun: false,
522
- force: false,
523
- })
524
-
525
- expect(result.action).toBe('skipped')
526
- expect(result.message).toContain('already')
527
- expect(readPkg(dir).scripts.prepare).toBe(script) // unchanged
528
- })
529
-
530
- // ── Additional edge cases ──────────────────────────────────────────────
531
-
532
- it('preserves other package.json fields when adding prepare script', async () => {
533
- const dir = createTempDir()
534
- writePkg(dir, {
535
- version: '2.0.0',
536
- description: 'test',
537
- scripts: { test: 'vitest', lint: 'eslint' },
538
- dependencies: { lodash: '^4.0.0' },
539
- })
540
-
541
- const result = await handlePrepare({
542
- consumerRoot: dir,
543
- consumerPackageJson: {
544
- name: 'test-pkg',
545
- version: '2.0.0',
546
- description: 'test',
547
- scripts: { test: 'vitest', lint: 'eslint' },
548
- dependencies: { lodash: '^4.0.0' },
549
- },
550
- existingPrepare: undefined,
551
- prepareIsSimple: false,
552
- dryRun: false,
553
- force: false,
554
- })
555
-
556
- expect(result.action).toBe('added')
557
- const pkg = readPkg(dir)
558
- expect(pkg.scripts.prepare).toBe('husky')
559
- expect(pkg.scripts.test).toBe('vitest')
560
- expect(pkg.scripts.lint).toBe('eslint')
561
- expect(pkg.version).toBe('2.0.0')
562
- expect(pkg.dependencies.lodash).toBe('^4.0.0')
563
- })
564
-
565
- it('preserves other package.json fields when merging prepare script', async () => {
566
- const dir = createTempDir()
567
- writePkg(dir, {
568
- version: '3.0.0',
569
- scripts: { prepare: 'npm run build', test: 'vitest' },
570
- devDependencies: { husky: '^9.0.0' },
571
- })
572
-
573
- const result = await handlePrepare({
574
- consumerRoot: dir,
575
- consumerPackageJson: {
576
- name: 'test-pkg',
577
- version: '3.0.0',
578
- scripts: { prepare: 'npm run build', test: 'vitest' },
579
- devDependencies: { husky: '^9.0.0' },
580
- },
581
- existingPrepare: 'npm run build',
582
- prepareIsSimple: true,
583
- dryRun: false,
584
- force: false,
585
- })
586
-
587
- expect(result.action).toBe('merged')
588
- const pkg = readPkg(dir)
589
- expect(pkg.scripts.prepare).toBe('npm run build && husky')
590
- expect(pkg.scripts.test).toBe('vitest')
591
- expect(pkg.version).toBe('3.0.0')
592
- expect(pkg.devDependencies.husky).toBe('^9.0.0')
593
- })
594
-
595
- it('preserves other package.json fields when force-replacing prepare script', async () => {
596
- const dir = createTempDir()
597
- writePkg(dir, {
598
- scripts: { prepare: 'cmd1 && cmd2 && cmd3', test: 'vitest' },
599
- keywords: ['test'],
600
- })
601
-
602
- const result = await handlePrepare({
603
- consumerRoot: dir,
604
- consumerPackageJson: {
605
- name: 'test-pkg',
606
- scripts: { prepare: 'cmd1 && cmd2 && cmd3', test: 'vitest' },
607
- keywords: ['test'],
608
- },
609
- existingPrepare: 'cmd1 && cmd2 && cmd3',
610
- prepareIsSimple: false,
611
- dryRun: false,
612
- force: true,
613
- })
614
-
615
- expect(result.action).toBe('added')
616
- const pkg = readPkg(dir)
617
- expect(pkg.scripts.prepare).toBe('husky')
618
- expect(pkg.scripts.test).toBe('vitest')
619
- expect(pkg.keywords).toEqual(['test'])
620
- })
621
-
622
- it('handles prepare script using "husky" in a non-substring context (edge: "npx husky")', async () => {
623
- // "npx husky" contains "husky" substring → should be skipped by case 2
624
- const dir = createTempDir()
625
- const script = 'npx husky'
626
- writePkg(dir, { scripts: { prepare: script } })
627
-
628
- const result = await handlePrepare({
629
- consumerRoot: dir,
630
- consumerPackageJson: { name: 'test-pkg', scripts: { prepare: script } },
631
- existingPrepare: script,
632
- prepareIsSimple: true,
633
- dryRun: false,
634
- force: false,
635
- })
636
-
637
- expect(result.action).toBe('skipped')
638
- expect(result.message).toContain('already')
639
- })
640
-
641
- it('result object always contains action and message string fields', async () => {
642
- const dir = createTempDir()
643
- writePkg(dir)
644
-
645
- const scenarios = [
646
- // No package.json
647
- await handlePrepare({
648
- consumerRoot: dir,
649
- consumerPackageJson: undefined,
650
- existingPrepare: undefined,
651
- prepareIsSimple: false,
652
- dryRun: false,
653
- force: false,
654
- }),
655
- // Already has husky
656
- await handlePrepare({
657
- consumerRoot: dir,
658
- consumerPackageJson: { name: 'test', scripts: { prepare: 'husky' } },
659
- existingPrepare: 'husky',
660
- prepareIsSimple: true,
661
- dryRun: false,
662
- force: false,
663
- }),
664
- // No prepare script
665
- await handlePrepare({
666
- consumerRoot: dir,
667
- consumerPackageJson: { name: 'test' },
668
- existingPrepare: undefined,
669
- prepareIsSimple: false,
670
- dryRun: false,
671
- force: false,
672
- }),
673
- // Simple prepare
674
- await handlePrepare({
675
- consumerRoot: dir,
676
- consumerPackageJson: { name: 'test', scripts: { prepare: 'npm run build' } },
677
- existingPrepare: 'npm run build',
678
- prepareIsSimple: true,
679
- dryRun: false,
680
- force: false,
681
- }),
682
- // Complex prepare - no force
683
- await handlePrepare({
684
- consumerRoot: dir,
685
- consumerPackageJson: { name: 'test', scripts: { prepare: 'cmd1 || cmd2' } },
686
- existingPrepare: 'cmd1 || cmd2',
687
- prepareIsSimple: false,
688
- dryRun: false,
689
- force: false,
690
- }),
691
- // Complex prepare + force
692
- await handlePrepare({
693
- consumerRoot: dir,
694
- consumerPackageJson: { name: 'test', scripts: { prepare: 'cmd1 || cmd2' } },
695
- existingPrepare: 'cmd1 || cmd2',
696
- prepareIsSimple: false,
697
- dryRun: false,
698
- force: true,
699
- }),
700
- // Dry-run
701
- await handlePrepare({
702
- consumerRoot: dir,
703
- consumerPackageJson: { name: 'test', scripts: { prepare: 'npm run build' } },
704
- existingPrepare: 'npm run build',
705
- prepareIsSimple: true,
706
- dryRun: true,
707
- force: false,
708
- }),
709
- ]
710
-
711
- for (const r of scenarios) {
712
- expect(r).toHaveProperty('action')
713
- expect(r).toHaveProperty('message')
714
- expect(typeof r.action).toBe('string')
715
- expect(typeof r.message).toBe('string')
716
- }
717
- })
718
- })