@thegetty/quire-cli 1.0.0-rc.35 → 1.0.0-rc.37

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 (159) hide show
  1. package/CHANGELOG.md +36 -0
  2. package/README.md +9 -0
  3. package/bin/cli.js +19 -1
  4. package/package.json +21 -8
  5. package/patches/README.md +19 -0
  6. package/patches/install-npm-version+1.0.9.patch +12119 -0
  7. package/schemas/objects.schema.json +12 -38
  8. package/schemas/publication.schema.json +0 -22
  9. package/schemas/references.schema.json +0 -1
  10. package/src/Command.js +26 -6
  11. package/src/Command.spec.js +99 -0
  12. package/src/commands/README.md +213 -122
  13. package/src/commands/build.js +37 -36
  14. package/src/commands/build.spec.js +113 -0
  15. package/src/commands/build.test.js +397 -0
  16. package/src/commands/clean.js +25 -19
  17. package/src/commands/clean.spec.js +108 -0
  18. package/src/commands/clean.test.js +260 -0
  19. package/src/commands/config.js +251 -0
  20. package/src/commands/config.spec.js +107 -0
  21. package/src/commands/config.test.js +715 -0
  22. package/src/commands/create.js +42 -14
  23. package/src/commands/create.spec.js +112 -0
  24. package/src/commands/create.test.js +415 -0
  25. package/src/commands/epub.js +59 -30
  26. package/src/commands/epub.spec.js +114 -0
  27. package/src/commands/epub.test.js +503 -0
  28. package/src/commands/index.js +9 -2
  29. package/src/commands/info.js +18 -24
  30. package/src/commands/info.spec.js +64 -0
  31. package/src/commands/info.test.js +415 -0
  32. package/src/commands/pdf.js +58 -84
  33. package/src/commands/pdf.spec.js +114 -0
  34. package/src/commands/pdf.test.js +464 -0
  35. package/src/commands/preview.js +32 -31
  36. package/src/commands/preview.spec.js +97 -0
  37. package/src/commands/preview.test.js +250 -0
  38. package/src/commands/use.js +56 -0
  39. package/src/commands/use.spec.js +61 -0
  40. package/src/commands/use.test.js +280 -0
  41. package/src/commands/validate.js +31 -19
  42. package/src/commands/validate.spec.js +82 -0
  43. package/src/commands/validate.test.js +234 -0
  44. package/src/commands/workflows.js +70 -0
  45. package/src/errors/build/build-failed-error.js +19 -0
  46. package/src/errors/build/config-field-missing-error.js +20 -0
  47. package/src/errors/build/config-file-not-found-error.js +20 -0
  48. package/src/errors/build/index.js +11 -0
  49. package/src/errors/index.js +48 -0
  50. package/src/errors/install/dependency-install-error.js +19 -0
  51. package/src/errors/install/directory-not-empty-error.js +25 -0
  52. package/src/errors/install/index.js +12 -0
  53. package/src/errors/install/invalid-path-error.js +31 -0
  54. package/src/errors/install/invalid-starter-error.js +27 -0
  55. package/src/errors/install/version-not-found-error.js +35 -0
  56. package/src/errors/output/epub-generation-error.js +19 -0
  57. package/src/errors/output/index.js +14 -0
  58. package/src/errors/output/invalid-epub-library-error.js +20 -0
  59. package/src/errors/output/invalid-pdf-library-error.js +20 -0
  60. package/src/errors/output/missing-build-output-error.js +21 -0
  61. package/src/errors/output/pdf-generation-error.js +24 -0
  62. package/src/errors/output/tool-not-found-error.js +37 -0
  63. package/src/errors/project/index.js +10 -0
  64. package/src/errors/project/not-in-project-error.js +20 -0
  65. package/src/errors/project/project-create-error.js +21 -0
  66. package/src/errors/quire-error.js +27 -0
  67. package/src/errors/validation/validation-error.js +20 -11
  68. package/src/helpers/clean.js +1 -1
  69. package/src/helpers/docs-url.js +32 -0
  70. package/src/helpers/test-cwd.js +5 -6
  71. package/src/helpers/test-cwd.test.js +192 -0
  72. package/src/helpers/which.js +10 -4
  73. package/src/lib/11ty/README.md +135 -19
  74. package/src/lib/11ty/api.js +196 -94
  75. package/src/lib/11ty/cli.js +91 -37
  76. package/src/lib/11ty/index.js +64 -5
  77. package/src/lib/11ty/index.test.js +655 -0
  78. package/src/lib/README.md +275 -0
  79. package/src/lib/commander/index.js +100 -0
  80. package/src/lib/commander/index.test.js +86 -0
  81. package/src/lib/commander/options.js +195 -0
  82. package/src/lib/commander/options.test.js +109 -0
  83. package/src/lib/conf/README.md +84 -73
  84. package/src/lib/conf/config.js +5 -3
  85. package/src/lib/conf/config.test.js +281 -0
  86. package/src/lib/conf/defaults.js +44 -0
  87. package/src/lib/conf/format.js +60 -0
  88. package/src/lib/conf/format.test.js +106 -0
  89. package/src/lib/conf/helpers.js +91 -0
  90. package/src/lib/conf/helpers.test.js +136 -0
  91. package/src/lib/conf/index.js +22 -0
  92. package/src/lib/conf/schema.js +53 -8
  93. package/src/lib/epub/README.md +133 -2
  94. package/src/lib/epub/engines.js +46 -0
  95. package/src/lib/epub/epub.js +36 -11
  96. package/src/lib/epub/index.js +111 -21
  97. package/src/lib/epub/index.test.js +518 -0
  98. package/src/lib/epub/pandoc.js +33 -4
  99. package/src/lib/epub/pandoc.test.js +122 -0
  100. package/src/lib/epub/schema.js +21 -0
  101. package/src/lib/error/README.md +170 -0
  102. package/src/lib/error/handler.js +107 -0
  103. package/src/lib/git/README.md +151 -2
  104. package/src/lib/git/index.js +217 -13
  105. package/src/lib/git/index.spec.js +80 -0
  106. package/src/lib/git/index.test.js +453 -0
  107. package/src/lib/installer/index.js +309 -0
  108. package/src/lib/installer/index.spec.js +83 -0
  109. package/src/lib/installer/index.test.js +545 -0
  110. package/src/lib/logger/README.md +424 -0
  111. package/src/lib/logger/debug.js +132 -0
  112. package/src/lib/logger/debug.spec.js +130 -0
  113. package/src/lib/logger/debug.test.js +128 -0
  114. package/src/lib/logger/index.js +228 -0
  115. package/src/lib/logger/index.spec.js +131 -0
  116. package/src/lib/logger/index.test.js +477 -0
  117. package/src/lib/npm/README.md +127 -0
  118. package/src/lib/npm/index.js +198 -0
  119. package/src/lib/npm/index.spec.js +60 -0
  120. package/src/lib/npm/index.test.js +355 -0
  121. package/src/lib/pdf/README.md +131 -0
  122. package/src/lib/pdf/engines.js +46 -0
  123. package/src/lib/pdf/index.js +124 -21
  124. package/src/lib/pdf/index.test.js +708 -0
  125. package/src/lib/pdf/paged.js +100 -52
  126. package/src/lib/pdf/paged.test.js +366 -0
  127. package/src/lib/pdf/prince.js +115 -37
  128. package/src/lib/pdf/prince.test.js +202 -0
  129. package/src/lib/pdf/schema.js +21 -0
  130. package/src/lib/pdf/split.js +61 -33
  131. package/src/lib/pdf/split.test.js +445 -0
  132. package/src/lib/process/manager.js +110 -0
  133. package/src/lib/process/manager.test.js +55 -0
  134. package/src/lib/project/build.js +143 -0
  135. package/src/lib/project/build.test.js +253 -0
  136. package/src/lib/project/config.js +48 -0
  137. package/src/lib/project/config.test.js +134 -0
  138. package/src/{helpers/is-quire.js → lib/project/detect.js} +5 -3
  139. package/src/lib/project/detect.test.js +157 -0
  140. package/src/lib/project/index.js +40 -0
  141. package/src/lib/project/paths.js +224 -0
  142. package/src/lib/project/version.js +110 -0
  143. package/src/lib/project/version.test.js +350 -0
  144. package/src/lib/reporter/README.md +211 -2
  145. package/src/lib/reporter/index.js +512 -0
  146. package/src/lib/reporter/index.test.js +593 -0
  147. package/src/main.js +134 -47
  148. package/src/main.spec.js +61 -0
  149. package/src/main.test.js +347 -0
  150. package/src/validators/utils.js +2 -1
  151. package/src/commands/conf.js +0 -43
  152. package/src/commands/version.js +0 -43
  153. package/src/lib/11ty/paths.js +0 -103
  154. package/src/lib/i18n/README.md +0 -3
  155. package/src/lib/i18n/config.js +0 -53
  156. package/src/lib/i18n/index.js +0 -43
  157. package/src/lib/i18n/localeService.js +0 -58
  158. package/src/lib/quire/README.md +0 -19
  159. package/src/lib/quire/index.js +0 -350
@@ -0,0 +1,545 @@
1
+ import esmock from 'esmock'
2
+ import path from 'node:path'
3
+ import sinon from 'sinon'
4
+ import test from 'ava'
5
+
6
+ /**
7
+ * Installer Module Integration Tests
8
+ *
9
+ * Tests the installer façade module behavior with mocked dependencies.
10
+ */
11
+
12
+ test.beforeEach((t) => {
13
+ t.context.sandbox = sinon.createSandbox()
14
+ })
15
+
16
+ test.afterEach.always((t) => {
17
+ t.context.sandbox.restore()
18
+ })
19
+
20
+ // ─────────────────────────────────────────────────────────────────────────────
21
+ // getVersionFromPath() tests
22
+ // ─────────────────────────────────────────────────────────────────────────────
23
+
24
+ test('getVersionFromPath() throws InvalidPathError when path does not exist', async (t) => {
25
+ const { sandbox } = t.context
26
+
27
+ const mockFs = {
28
+ existsSync: sandbox.stub().returns(false),
29
+ readJsonSync: sandbox.stub()
30
+ }
31
+
32
+ const { getVersionFromPath } = await esmock('./index.js', {
33
+ '#lib/npm/index.js': { default: {} },
34
+ '#lib/project/index.js': {
35
+ getVersionsFromStarter: sandbox.stub(),
36
+ setVersion: sandbox.stub(),
37
+ writeVersionFile: sandbox.stub()
38
+ },
39
+ '#lib/git/index.js': { default: {} },
40
+ 'fs-extra': mockFs,
41
+ '#helpers/is-empty.js': { isEmpty: sandbox.stub() },
42
+ '#src/packageConfig.js': { default: { version: '1.0.0' } }
43
+ })
44
+
45
+ const error = t.throws(() => getVersionFromPath('/nonexistent/path'))
46
+
47
+ t.is(error.code, 'INVALID_PATH', 'should throw InvalidPathError')
48
+ t.true(error.message.includes('/nonexistent/path'))
49
+ })
50
+
51
+ test('getVersionFromPath() throws VersionNotFoundError when package.json is missing', async (t) => {
52
+ const { sandbox } = t.context
53
+
54
+ const resolvedQuirePath = path.resolve('/local/quire')
55
+ const packageJsonPath = path.join(resolvedQuirePath, 'package.json')
56
+
57
+ const existsSyncStub = sandbox.stub()
58
+ existsSyncStub.withArgs(resolvedQuirePath).returns(true)
59
+ existsSyncStub.withArgs(packageJsonPath).returns(false)
60
+
61
+ const mockFs = {
62
+ existsSync: existsSyncStub,
63
+ readJsonSync: sandbox.stub()
64
+ }
65
+
66
+ const { getVersionFromPath } = await esmock('./index.js', {
67
+ '#lib/npm/index.js': { default: {} },
68
+ '#lib/project/index.js': {
69
+ getVersionsFromStarter: sandbox.stub(),
70
+ setVersion: sandbox.stub(),
71
+ writeVersionFile: sandbox.stub()
72
+ },
73
+ '#lib/git/index.js': { default: {} },
74
+ 'fs-extra': mockFs,
75
+ '#helpers/is-empty.js': { isEmpty: sandbox.stub() },
76
+ '#src/packageConfig.js': { default: { version: '1.0.0' } }
77
+ })
78
+
79
+ const error = t.throws(() => getVersionFromPath('/local/quire'))
80
+
81
+ t.is(error.code, 'VERSION_NOT_FOUND', 'should throw VersionNotFoundError')
82
+ t.true(error.message.includes('package.json not found'))
83
+ })
84
+
85
+ test('getVersionFromPath() throws VersionNotFoundError when package.json has no version', async (t) => {
86
+ const { sandbox } = t.context
87
+
88
+ const mockFs = {
89
+ existsSync: sandbox.stub().returns(true),
90
+ readJsonSync: sandbox.stub().returns({ name: '@thegetty/quire-11ty' }) // no version
91
+ }
92
+
93
+ const { getVersionFromPath } = await esmock('./index.js', {
94
+ '#lib/npm/index.js': { default: {} },
95
+ '#lib/project/index.js': {
96
+ getVersionsFromStarter: sandbox.stub(),
97
+ setVersion: sandbox.stub(),
98
+ writeVersionFile: sandbox.stub()
99
+ },
100
+ '#lib/git/index.js': { default: {} },
101
+ 'fs-extra': mockFs,
102
+ '#helpers/is-empty.js': { isEmpty: sandbox.stub() },
103
+ '#src/packageConfig.js': { default: { version: '1.0.0' } }
104
+ })
105
+
106
+ const error = t.throws(() => getVersionFromPath('/local/quire'))
107
+
108
+ t.is(error.code, 'VERSION_NOT_FOUND', 'should throw VersionNotFoundError')
109
+ t.true(error.message.includes('no version field'))
110
+ })
111
+
112
+ test('getVersionFromPath() returns version and resolved path when valid', async (t) => {
113
+ const { sandbox } = t.context
114
+
115
+ const mockFs = {
116
+ existsSync: sandbox.stub().returns(true),
117
+ readJsonSync: sandbox.stub().returns({ version: '1.0.0-local' })
118
+ }
119
+
120
+ const { getVersionFromPath } = await esmock('./index.js', {
121
+ '#lib/npm/index.js': { default: {} },
122
+ '#lib/project/index.js': {
123
+ getVersionsFromStarter: sandbox.stub(),
124
+ setVersion: sandbox.stub(),
125
+ writeVersionFile: sandbox.stub()
126
+ },
127
+ '#lib/git/index.js': { default: {} },
128
+ 'fs-extra': mockFs,
129
+ '#helpers/is-empty.js': { isEmpty: sandbox.stub() },
130
+ '#src/packageConfig.js': { default: { version: '1.0.0' } }
131
+ })
132
+
133
+ const result = getVersionFromPath('/local/quire')
134
+
135
+ t.is(result.version, '1.0.0-local')
136
+ t.truthy(result.resolvedPath)
137
+ })
138
+
139
+ // ─────────────────────────────────────────────────────────────────────────────
140
+ // latest() tests
141
+ // ─────────────────────────────────────────────────────────────────────────────
142
+
143
+ test('latest() returns latest version when called with "latest"', async (t) => {
144
+ const { sandbox } = t.context
145
+
146
+ const mockNpm = {
147
+ view: sandbox.stub().resolves('1.0.5'),
148
+ getCompatibleVersion: sandbox.stub()
149
+ }
150
+
151
+ const { latest } = await esmock('./index.js', {
152
+ '#lib/npm/index.js': { default: mockNpm },
153
+ '#lib/project/index.js': {
154
+ getVersionsFromStarter: sandbox.stub(),
155
+ setVersion: sandbox.stub(),
156
+ writeVersionFile: sandbox.stub()
157
+ },
158
+ '#lib/git/index.js': { default: {} },
159
+ 'fs-extra': {},
160
+ '#helpers/is-empty.js': { isEmpty: sandbox.stub() },
161
+ '#src/packageConfig.js': { default: { version: '1.0.0' } }
162
+ })
163
+
164
+ const result = await latest('latest')
165
+
166
+ t.is(result, '1.0.5')
167
+ t.true(mockNpm.view.calledWith('@thegetty/quire-11ty', 'version'))
168
+ })
169
+
170
+ test('latest() returns latest version when called without argument', async (t) => {
171
+ const { sandbox } = t.context
172
+
173
+ const mockNpm = {
174
+ view: sandbox.stub().resolves('1.0.5'),
175
+ getCompatibleVersion: sandbox.stub()
176
+ }
177
+
178
+ const { latest } = await esmock('./index.js', {
179
+ '#lib/npm/index.js': { default: mockNpm },
180
+ '#lib/project/index.js': {
181
+ getVersionsFromStarter: sandbox.stub(),
182
+ setVersion: sandbox.stub(),
183
+ writeVersionFile: sandbox.stub()
184
+ },
185
+ '#lib/git/index.js': { default: {} },
186
+ 'fs-extra': {},
187
+ '#helpers/is-empty.js': { isEmpty: sandbox.stub() },
188
+ '#src/packageConfig.js': { default: { version: '1.0.0' } }
189
+ })
190
+
191
+ const result = await latest()
192
+
193
+ t.is(result, '1.0.5')
194
+ t.true(mockNpm.view.calledWith('@thegetty/quire-11ty', 'version'))
195
+ })
196
+
197
+ test('latest() resolves semver range to compatible version', async (t) => {
198
+ const { sandbox } = t.context
199
+
200
+ const mockNpm = {
201
+ view: sandbox.stub(),
202
+ getCompatibleVersion: sandbox.stub().resolves('1.2.3')
203
+ }
204
+
205
+ const { latest } = await esmock('./index.js', {
206
+ '#lib/npm/index.js': { default: mockNpm },
207
+ '#lib/project/index.js': {
208
+ getVersionsFromStarter: sandbox.stub(),
209
+ setVersion: sandbox.stub(),
210
+ writeVersionFile: sandbox.stub()
211
+ },
212
+ '#lib/git/index.js': { default: {} },
213
+ 'fs-extra': {},
214
+ '#helpers/is-empty.js': { isEmpty: sandbox.stub() },
215
+ '#src/packageConfig.js': { default: { version: '1.0.0' } }
216
+ })
217
+
218
+ const result = await latest('^1.0.0')
219
+
220
+ t.is(result, '1.2.3')
221
+ t.true(mockNpm.getCompatibleVersion.calledWith('@thegetty/quire-11ty', '^1.0.0'))
222
+ t.false(mockNpm.view.called)
223
+ })
224
+
225
+ test('latest() throws error when no compatible version found', async (t) => {
226
+ const { sandbox } = t.context
227
+
228
+ const mockNpm = {
229
+ view: sandbox.stub(),
230
+ getCompatibleVersion: sandbox.stub().resolves(null)
231
+ }
232
+
233
+ const { latest } = await esmock('./index.js', {
234
+ '#lib/npm/index.js': { default: mockNpm },
235
+ '#lib/project/index.js': {
236
+ getVersionsFromStarter: sandbox.stub(),
237
+ setVersion: sandbox.stub(),
238
+ writeVersionFile: sandbox.stub()
239
+ },
240
+ '#lib/git/index.js': { default: {} },
241
+ 'fs-extra': {},
242
+ '#helpers/is-empty.js': { isEmpty: sandbox.stub() },
243
+ '#src/packageConfig.js': { default: { version: '1.0.0' } }
244
+ })
245
+
246
+ const error = await t.throwsAsync(() => latest('^99.0.0'))
247
+
248
+ t.is(error.code, 'VERSION_NOT_FOUND')
249
+ t.true(error.message.includes('^99.0.0'))
250
+ })
251
+
252
+ // ─────────────────────────────────────────────────────────────────────────────
253
+ // versions() tests
254
+ // ─────────────────────────────────────────────────────────────────────────────
255
+
256
+ test('versions() returns all published versions', async (t) => {
257
+ const { sandbox } = t.context
258
+
259
+ const mockNpm = {
260
+ show: sandbox.stub().resolves(['1.0.0', '1.0.1', '1.1.0'])
261
+ }
262
+
263
+ const { versions } = await esmock('./index.js', {
264
+ '#lib/npm/index.js': { default: mockNpm },
265
+ '#lib/project/index.js': {
266
+ getVersionsFromStarter: sandbox.stub(),
267
+ setVersion: sandbox.stub(),
268
+ writeVersionFile: sandbox.stub()
269
+ },
270
+ '#lib/git/index.js': { default: {} },
271
+ 'fs-extra': {},
272
+ '#helpers/is-empty.js': { isEmpty: sandbox.stub() },
273
+ '#src/packageConfig.js': { default: { version: '1.0.0' } }
274
+ })
275
+
276
+ const result = await versions()
277
+
278
+ t.deepEqual(result, ['1.0.0', '1.0.1', '1.1.0'])
279
+ t.true(mockNpm.show.calledWith('@thegetty/quire-11ty', 'versions'))
280
+ })
281
+
282
+ // ─────────────────────────────────────────────────────────────────────────────
283
+ // initStarter() tests
284
+ // ─────────────────────────────────────────────────────────────────────────────
285
+
286
+ test('initStarter() throws when target directory is not empty', async (t) => {
287
+ const { sandbox } = t.context
288
+
289
+ const mockFs = {
290
+ ensureDirSync: sandbox.stub(),
291
+ remove: sandbox.stub()
292
+ }
293
+
294
+ // Create a mock Git class
295
+ class MockGit {
296
+ constructor() {}
297
+ clone() { return Promise.resolve() }
298
+ init() { return Promise.resolve() }
299
+ add() { return Promise.resolve() }
300
+ commit() { return Promise.resolve() }
301
+ }
302
+
303
+ const { initStarter } = await esmock('./index.js', {
304
+ '#lib/npm/index.js': { default: {} },
305
+ '#lib/project/index.js': {
306
+ getVersionsFromStarter: sandbox.stub(),
307
+ setVersion: sandbox.stub(),
308
+ writeVersionFile: sandbox.stub()
309
+ },
310
+ '#lib/git/index.js': { Git: MockGit },
311
+ 'fs-extra': mockFs,
312
+ '#helpers/is-empty.js': { isEmpty: sandbox.stub().returns(false) },
313
+ '#src/packageConfig.js': { default: { version: '1.0.0' } }
314
+ })
315
+
316
+ await t.throwsAsync(
317
+ () => initStarter('https://github.com/test/starter', '/project'),
318
+ { message: /non-empty directory/ }
319
+ )
320
+ })
321
+
322
+ test('initStarter() clones starter and sets up project', async (t) => {
323
+ const { sandbox } = t.context
324
+
325
+ const mockFs = {
326
+ ensureDirSync: sandbox.stub(),
327
+ remove: sandbox.stub().resolves(),
328
+ readFileSync: sandbox.stub().returns(JSON.stringify({
329
+ version: '1.0.0',
330
+ peerDependencies: { '@thegetty/quire-11ty': '^1.0.0' }
331
+ })),
332
+ writeFileSync: sandbox.stub()
333
+ }
334
+
335
+ // Create a mock Git class with spies
336
+ const gitSpies = {
337
+ clone: sandbox.stub().resolves(),
338
+ init: sandbox.stub().resolves(),
339
+ add: sandbox.stub().resolves(),
340
+ commit: sandbox.stub().resolves()
341
+ }
342
+
343
+ class MockGit {
344
+ constructor() {}
345
+ clone(...args) { return gitSpies.clone(...args) }
346
+ init(...args) { return gitSpies.init(...args) }
347
+ add(...args) { return gitSpies.add(...args) }
348
+ commit(...args) { return gitSpies.commit(...args) }
349
+ }
350
+
351
+ const mockNpm = {
352
+ init: sandbox.stub().resolves(),
353
+ getCompatibleVersion: sandbox.stub().resolves('1.2.0')
354
+ }
355
+
356
+ const { initStarter } = await esmock('./index.js', {
357
+ '#lib/npm/index.js': { default: mockNpm },
358
+ '#lib/project/index.js': {
359
+ getVersionsFromStarter: sandbox.stub().resolves({
360
+ quire11tyVersion: '^1.0.0',
361
+ starterVersion: '1.0.0'
362
+ }),
363
+ setVersion: sandbox.stub(),
364
+ writeVersionFile: sandbox.stub()
365
+ },
366
+ '#lib/git/index.js': { Git: MockGit },
367
+ 'fs-extra': mockFs,
368
+ '#helpers/is-empty.js': { isEmpty: sandbox.stub().returns(true) },
369
+ '#src/packageConfig.js': { default: { version: '1.0.0' } }
370
+ })
371
+
372
+ const result = await initStarter('https://github.com/test/starter', '/project')
373
+
374
+ t.is(result, '1.2.0', 'should return resolved quire version')
375
+ t.true(gitSpies.clone.called, 'should clone starter repository')
376
+ t.true(gitSpies.init.called, 'should initialize git repository')
377
+ t.true(mockNpm.init.called, 'should run npm init')
378
+ })
379
+
380
+ // ─────────────────────────────────────────────────────────────────────────────
381
+ // installer object export tests
382
+ // ─────────────────────────────────────────────────────────────────────────────
383
+
384
+ test('installer object exports all required functions', async (t) => {
385
+ const { sandbox } = t.context
386
+
387
+ // Create a mock Git class
388
+ class MockGit {
389
+ constructor() {}
390
+ clone() { return Promise.resolve() }
391
+ init() { return Promise.resolve() }
392
+ add() { return Promise.resolve() }
393
+ commit() { return Promise.resolve() }
394
+ rm() { return Promise.resolve() }
395
+ }
396
+
397
+ const { installer } = await esmock('./index.js', {
398
+ '#lib/npm/index.js': { default: {} },
399
+ '#lib/project/index.js': {
400
+ getVersionsFromStarter: sandbox.stub(),
401
+ setVersion: sandbox.stub(),
402
+ writeVersionFile: sandbox.stub()
403
+ },
404
+ '#lib/git/index.js': { Git: MockGit },
405
+ 'fs-extra': {},
406
+ '#helpers/is-empty.js': { isEmpty: sandbox.stub() },
407
+ '#src/packageConfig.js': { default: { version: '1.0.0' } }
408
+ })
409
+
410
+ t.is(typeof installer.initStarter, 'function', 'should export initStarter')
411
+ t.is(typeof installer.installInProject, 'function', 'should export installInProject')
412
+ t.is(typeof installer.latest, 'function', 'should export latest')
413
+ t.is(typeof installer.versions, 'function', 'should export versions')
414
+ })
415
+
416
+ // ─────────────────────────────────────────────────────────────────────────────
417
+ // installInProject() quirePath validation tests
418
+ // ─────────────────────────────────────────────────────────────────────────────
419
+
420
+ test('installInProject() throws InvalidPathError when quirePath does not exist', async (t) => {
421
+ const { sandbox } = t.context
422
+
423
+ // Create a mock Git class
424
+ class MockGit {
425
+ constructor() {}
426
+ rm() { return Promise.resolve() }
427
+ add() { return Promise.resolve() }
428
+ commit() { return Promise.resolve() }
429
+ }
430
+
431
+ const mockFs = {
432
+ mkdirSync: sandbox.stub(),
433
+ existsSync: sandbox.stub().returns(false)
434
+ }
435
+
436
+ const { installInProject } = await esmock('./index.js', {
437
+ '#lib/npm/index.js': { default: {} },
438
+ '#lib/project/index.js': {
439
+ getVersionsFromStarter: sandbox.stub(),
440
+ setVersion: sandbox.stub(),
441
+ writeVersionFile: sandbox.stub()
442
+ },
443
+ '#lib/git/index.js': { Git: MockGit },
444
+ 'fs-extra': mockFs,
445
+ '#helpers/is-empty.js': { isEmpty: sandbox.stub() },
446
+ '#src/packageConfig.js': { default: { version: '1.0.0' } }
447
+ })
448
+
449
+ const error = await t.throwsAsync(
450
+ () => installInProject('/project', '1.0.0', { quirePath: '/nonexistent/path' })
451
+ )
452
+
453
+ t.is(error.code, 'INVALID_PATH', 'should throw InvalidPathError')
454
+ t.true(error.message.includes('--quire-path does not exist'))
455
+ t.true(error.message.includes('/nonexistent/path'))
456
+ })
457
+
458
+ test('installInProject() throws InvalidPathError with resolved path for relative quirePath', async (t) => {
459
+ const { sandbox } = t.context
460
+
461
+ // Create a mock Git class
462
+ class MockGit {
463
+ constructor() {}
464
+ rm() { return Promise.resolve() }
465
+ add() { return Promise.resolve() }
466
+ commit() { return Promise.resolve() }
467
+ }
468
+
469
+ const mockFs = {
470
+ mkdirSync: sandbox.stub(),
471
+ existsSync: sandbox.stub().returns(false)
472
+ }
473
+
474
+ const { installInProject } = await esmock('./index.js', {
475
+ '#lib/npm/index.js': { default: {} },
476
+ '#lib/project/index.js': {
477
+ getVersionsFromStarter: sandbox.stub(),
478
+ setVersion: sandbox.stub(),
479
+ writeVersionFile: sandbox.stub()
480
+ },
481
+ '#lib/git/index.js': { Git: MockGit },
482
+ 'fs-extra': mockFs,
483
+ '#helpers/is-empty.js': { isEmpty: sandbox.stub() },
484
+ '#src/packageConfig.js': { default: { version: '1.0.0' } }
485
+ })
486
+
487
+ const error = await t.throwsAsync(
488
+ () => installInProject('/project', '1.0.0', { quirePath: '../relative/path' })
489
+ )
490
+
491
+ t.is(error.code, 'INVALID_PATH', 'should throw InvalidPathError')
492
+ t.true(error.message.includes('--quire-path does not exist'))
493
+ t.true(error.message.includes('../relative/path'))
494
+ t.true(error.message.includes('resolved to:'), 'should show resolved path for relative paths')
495
+ })
496
+
497
+ test('installInProject() copies from quirePath when it exists', async (t) => {
498
+ const { sandbox } = t.context
499
+
500
+ // Create a mock Git class
501
+ class MockGit {
502
+ constructor() {}
503
+ rm() { return Promise.resolve() }
504
+ add() { return Promise.resolve() }
505
+ commit() { return Promise.resolve() }
506
+ }
507
+
508
+ const cpSyncStub = sandbox.stub()
509
+ const rmSyncStub = sandbox.stub()
510
+
511
+ const mockFs = {
512
+ mkdirSync: sandbox.stub(),
513
+ existsSync: sandbox.stub().returns(true),
514
+ readJsonSync: sandbox.stub().returns({ version: '1.0.0-local' }),
515
+ cpSync: cpSyncStub,
516
+ rmSync: rmSyncStub
517
+ }
518
+
519
+ const mockNpm = {
520
+ install: sandbox.stub().resolves()
521
+ }
522
+
523
+ const { installInProject } = await esmock('./index.js', {
524
+ '#lib/npm/index.js': { default: mockNpm },
525
+ '#lib/project/index.js': {
526
+ getVersionsFromStarter: sandbox.stub(),
527
+ setVersion: sandbox.stub(),
528
+ writeVersionFile: sandbox.stub()
529
+ },
530
+ '#lib/git/index.js': { Git: MockGit },
531
+ 'fs-extra': mockFs,
532
+ '#helpers/is-empty.js': { isEmpty: sandbox.stub() },
533
+ '#src/packageConfig.js': { default: { version: '1.0.0' } }
534
+ })
535
+
536
+ await installInProject('/project', '1.0.0', { quirePath: '/local/quire-11ty' })
537
+
538
+ // Should copy from local path, not download
539
+ t.true(cpSyncStub.called, 'should copy from local path')
540
+ const [srcPath] = cpSyncStub.firstCall.args
541
+ t.is(srcPath, path.resolve('/local/quire-11ty'), 'should use resolved quirePath as source')
542
+ })
543
+
544
+ // Nota bene: Version reading from local package.json is now done in initStarter
545
+ // via getVersionFromPath, not in installInProject. See getVersionFromPath tests above.