@marcusrbrown/infra 0.9.13 → 0.9.15

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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@marcusrbrown/infra",
3
- "version": "0.9.13",
3
+ "version": "0.9.15",
4
4
  "description": "Infrastructure management CLI — deploy automation, health checks, and MCP bridge",
5
5
  "keywords": [
6
6
  "infra",
@@ -181,23 +181,73 @@ describe('keeweb status helpers', () => {
181
181
  })
182
182
 
183
183
  describe('checkContentHash', () => {
184
- it('resolves dist path at correct depth from restructured location', async () => {
185
- // Regression test for P1 path depth bug: after restructure from
186
- // commands/keeweb-status.ts to commands/keeweb/status.ts, the path
187
- // needs 5 levels of ../ to reach apps/keeweb/dist/index.html
188
- let capturedPath = ''
189
- fileSpy = spyOn(Bun, 'file').mockImplementation((...args: unknown[]) => {
190
- capturedPath = String(args[0])
191
- return {exists: async () => false} as ReturnType<typeof Bun.file>
192
- })
184
+ it('does not emit a path-noisy warning when running from a packaged install (bunx)', async () => {
185
+ // Simulate packaged layout: moduleDir is inside node_modules (bunx temp install)
186
+ const packagedDir = '/private/var/folders/xx/T/bunx-1234/node_modules/@marcusrbrown/infra/src/commands/keeweb'
187
+ // Bun.file should not be called for the dist path in packaged mode,
188
+ // but if it is, return false to avoid false positives
189
+ fileSpy = spyOn(Bun, 'file').mockImplementation(
190
+ () =>
191
+ ({
192
+ exists: async () => false,
193
+ }) as ReturnType<typeof Bun.file>,
194
+ )
193
195
 
194
- await checkContentHash(false)
196
+ const result = await checkContentHash(false, packagedDir)
197
+
198
+ // Must NOT contain the bogus node_modules path in the summary
199
+ expect(result.summary).not.toContain('node_modules/apps/keeweb')
200
+ expect(result.summary).not.toContain('/private/var/folders')
201
+ // Must clearly communicate that local dist is unavailable from packaged CLI
202
+ expect(result.summary.toLowerCase()).toMatch(/not available|unavailable|packaged|source checkout/)
203
+ // Level should be info (neutral — not a warning for packaged installs)
204
+ expect(result.level).toBe('info')
205
+ })
206
+
207
+ it('does not emit path-noisy warning for any node_modules install path', async () => {
208
+ // Simulate npm/npx global install: node_modules path but not bunx temp
209
+ const npmInstallDir = '/usr/local/lib/node_modules/@marcusrbrown/infra/src/commands/keeweb'
210
+ fileSpy = spyOn(Bun, 'file').mockImplementation(
211
+ () =>
212
+ ({
213
+ exists: async () => false,
214
+ }) as ReturnType<typeof Bun.file>,
215
+ )
216
+
217
+ const result = await checkContentHash(false, npmInstallDir)
218
+
219
+ expect(result.summary).not.toContain('node_modules/apps/keeweb')
220
+ expect(result.summary.toLowerCase()).toMatch(/not available|unavailable|packaged|source checkout/)
221
+ })
222
+
223
+ it('does not emit path-noisy warning when running from an unpacked tarball outside node_modules', async () => {
224
+ // Simulate unpacked tarball layout: e.g. `npm pack` + extract to /tmp/pkg/package/
225
+ // The path does NOT contain node_modules, but also has no repo apps/keeweb/src tree.
226
+ const unpackedTarballDir = '/tmp/pkg/package/src/commands/keeweb'
227
+ fileSpy = spyOn(Bun, 'file').mockImplementation(
228
+ () =>
229
+ ({
230
+ exists: async () => false,
231
+ }) as ReturnType<typeof Bun.file>,
232
+ )
233
+
234
+ const result = await checkContentHash(false, unpackedTarballDir)
195
235
 
196
- expect(capturedPath).toContain('apps/keeweb/dist/index.html')
197
- expect(capturedPath).not.toContain('commands/keeweb/apps')
236
+ // Must NOT contain the raw temp path in the summary
237
+ expect(result.summary).not.toContain('/tmp/pkg/package')
238
+ // Must NOT contain the path-noisy "Local dist file not found: ..." message
239
+ expect(result.summary).not.toMatch(/Local dist file not found/)
240
+ // Must clearly communicate that local dist is unavailable from packaged CLI
241
+ expect(result.summary.toLowerCase()).toMatch(/not available|unavailable|packaged|source checkout/)
242
+ // Level should be info (neutral — not a warning for packaged installs)
243
+ expect(result.level).toBe('info')
198
244
  })
199
245
 
200
- it('returns warning when the local dist file does not exist', async () => {
246
+ it('packaged install content hash level is info, not warning', async () => {
247
+ // Packaged/non-source installs should not degrade status with a warning.
248
+ // The content hash check is a source-only local comparison; when not in a
249
+ // source checkout, the result should be neutral (info), not warning.
250
+ const packagedDir = '/private/var/folders/xx/T/bunx-1234/node_modules/@marcusrbrown/infra/src/commands/keeweb'
201
251
  fileSpy = spyOn(Bun, 'file').mockImplementation(
202
252
  () =>
203
253
  ({
@@ -205,10 +255,111 @@ describe('keeweb status helpers', () => {
205
255
  }) as ReturnType<typeof Bun.file>,
206
256
  )
207
257
 
208
- const result = await checkContentHash(false)
258
+ const result = await checkContentHash(false, packagedDir)
259
+
260
+ expect(result.level).toBe('info')
261
+ expect(result.summary.toLowerCase()).toMatch(/not available|unavailable|packaged|source checkout/)
262
+ })
263
+
264
+ it('source checkout with matching hashes returns ok', async () => {
265
+ // Simulate source checkout: marker file exists, dist file exists, hashes match.
266
+ const sourceCheckoutDir = '/home/user/projects/infra/packages/cli/src/commands/keeweb'
267
+ const htmlContent = '<html>KeeWeb</html>'
268
+
269
+ fileSpy = spyOn(Bun, 'file').mockImplementation((pathArg: unknown) => {
270
+ const p = String(pathArg)
271
+ if (p.endsWith('apps/keeweb/src/build.ts')) {
272
+ return {exists: async () => true} as ReturnType<typeof Bun.file>
273
+ }
274
+ if (p.endsWith('apps/keeweb/dist/index.html')) {
275
+ return {
276
+ exists: async () => true,
277
+ text: async () => htmlContent,
278
+ } as ReturnType<typeof Bun.file>
279
+ }
280
+ return {exists: async () => false} as ReturnType<typeof Bun.file>
281
+ })
282
+
283
+ fetchMock.mockImplementation(createFetchImplementation(async () => new Response(htmlContent, {status: 200})))
284
+
285
+ const result = await checkContentHash(false, sourceCheckoutDir)
286
+
287
+ expect(result.level).toBe('ok')
288
+ expect(result.summary).toContain('matches')
289
+ })
290
+
291
+ it('source checkout with mismatched hashes returns warning', async () => {
292
+ // Simulate source checkout: marker file exists, dist file exists, hashes differ.
293
+ const sourceCheckoutDir = '/home/user/projects/infra/packages/cli/src/commands/keeweb'
294
+ const localHtml = '<html>KeeWeb local</html>'
295
+ const remoteHtml = '<html>KeeWeb remote</html>'
296
+
297
+ fileSpy = spyOn(Bun, 'file').mockImplementation((pathArg: unknown) => {
298
+ const p = String(pathArg)
299
+ if (p.endsWith('apps/keeweb/src/build.ts')) {
300
+ return {exists: async () => true} as ReturnType<typeof Bun.file>
301
+ }
302
+ if (p.endsWith('apps/keeweb/dist/index.html')) {
303
+ return {
304
+ exists: async () => true,
305
+ text: async () => localHtml,
306
+ } as ReturnType<typeof Bun.file>
307
+ }
308
+ return {exists: async () => false} as ReturnType<typeof Bun.file>
309
+ })
310
+
311
+ fetchMock.mockImplementation(createFetchImplementation(async () => new Response(remoteHtml, {status: 200})))
312
+
313
+ const result = await checkContentHash(false, sourceCheckoutDir)
314
+
315
+ expect(result.level).toBe('warning')
316
+ expect(result.summary).toContain('differs')
317
+ })
318
+
319
+ it('resolves dist path at correct depth from restructured location (path-sensitive mock)', async () => {
320
+ // Regression test for P1 path depth bug: after restructure from
321
+ // commands/keeweb-status.ts to commands/keeweb/status.ts, the path
322
+ // needs 5 levels of ../ to reach apps/keeweb/dist/index.html.
323
+ //
324
+ // Uses path-sensitive mock: marker returns true, dist returns false.
325
+ // This exercises the dist path check and lets us assert the correct path.
326
+ const capturedPaths: string[] = []
327
+ fileSpy = spyOn(Bun, 'file').mockImplementation((pathArg: unknown) => {
328
+ const p = String(pathArg)
329
+ capturedPaths.push(p)
330
+ if (p.endsWith('apps/keeweb/src/build.ts')) {
331
+ return {exists: async () => true} as ReturnType<typeof Bun.file>
332
+ }
333
+ return {exists: async () => false} as ReturnType<typeof Bun.file>
334
+ })
335
+
336
+ await checkContentHash(false)
337
+
338
+ // The dist path must appear in the captured paths
339
+ const distPath = capturedPaths.find(p => p.endsWith('apps/keeweb/dist/index.html'))
340
+ expect(distPath).toBeDefined()
341
+ expect(distPath).not.toContain('commands/keeweb/apps')
342
+ })
343
+
344
+ it('returns warning when the local dist file does not exist in source checkout (path-sensitive mock)', async () => {
345
+ // Simulate source checkout: marker exists, dist file missing (needs a build).
346
+ // Uses path-sensitive mock instead of call-count ordering.
347
+ const sourceCheckoutDir = '/home/user/projects/infra/packages/cli/src/commands/keeweb'
348
+ fileSpy = spyOn(Bun, 'file').mockImplementation((pathArg: unknown) => {
349
+ const p = String(pathArg)
350
+ if (p.endsWith('apps/keeweb/src/build.ts')) {
351
+ return {exists: async () => true} as ReturnType<typeof Bun.file>
352
+ }
353
+ // dist/index.html → missing
354
+ return {exists: async () => false} as ReturnType<typeof Bun.file>
355
+ })
356
+
357
+ const result = await checkContentHash(false, sourceCheckoutDir)
209
358
 
210
359
  expect(result.level).toBe('warning')
211
360
  expect(result.summary).toContain('not found')
361
+ expect(result.details).toEqual(expect.arrayContaining(['Run: bun run --cwd apps/keeweb build']))
362
+ expect(result.summary).not.toContain('node_modules')
212
363
  })
213
364
  })
214
365
  })
@@ -10,7 +10,7 @@ const SITE_URL = 'https://kw.igg.ms/'
10
10
  const GH_REPO = 'marcusrbrown/infra'
11
11
  const HTTP_TIMEOUT_MS = 10_000
12
12
 
13
- type CheckLevel = 'ok' | 'warning' | 'error'
13
+ type CheckLevel = 'ok' | 'info' | 'warning' | 'error'
14
14
 
15
15
  interface CheckResult {
16
16
  title: string
@@ -31,6 +31,10 @@ function levelLabel(level: CheckLevel): string {
31
31
  return 'OK'
32
32
  }
33
33
 
34
+ if (level === 'info') {
35
+ return 'INFO'
36
+ }
37
+
34
38
  if (level === 'warning') {
35
39
  return 'WARN'
36
40
  }
@@ -192,8 +196,44 @@ export async function checkLastDeploy(verbose: boolean): Promise<CheckResult> {
192
196
  }
193
197
  }
194
198
 
195
- export async function checkContentHash(verbose: boolean): Promise<CheckResult> {
196
- const distIndexPath = path.resolve(import.meta.dir, '../../../../../apps/keeweb/dist/index.html')
199
+ /**
200
+ * Detect whether moduleDir belongs to a source checkout of this monorepo.
201
+ *
202
+ * A source checkout has the repo root at 5 levels above the keeweb command
203
+ * directory (packages/cli/src/commands/keeweb → repo root). We confirm the
204
+ * layout by checking for a source-only marker file that is never shipped in
205
+ * the CLI package. This is more robust than checking for `node_modules` in
206
+ * the path, which misses unpacked tarballs and global-style installs.
207
+ */
208
+ async function isSourceCheckout(resolvedModuleDir: string): Promise<boolean> {
209
+ // Candidate repo root is 5 levels up from packages/cli/src/commands/keeweb
210
+ const candidateRoot = path.resolve(resolvedModuleDir, '../../../../../')
211
+ // apps/keeweb/src/build.ts is a source-only file never included in the CLI package
212
+ const sourceMarker = path.join(candidateRoot, 'apps', 'keeweb', 'src', 'build.ts')
213
+ return Bun.file(sourceMarker).exists()
214
+ }
215
+
216
+ export async function checkContentHash(verbose: boolean, moduleDir?: string): Promise<CheckResult> {
217
+ const resolvedModuleDir = moduleDir ?? import.meta.dir
218
+ const distIndexPath = path.resolve(resolvedModuleDir, '../../../../../apps/keeweb/dist/index.html')
219
+
220
+ // When running from a packaged install (bunx, npm global, unpacked tarball, etc.),
221
+ // the resolved path has no repo apps/keeweb source tree — the KeeWeb dist is not
222
+ // shipped with the CLI package. Emit a clean, non-path-noisy message instead of
223
+ // a scary temp path. We detect source checkout by looking for a source-only marker
224
+ // file rather than relying on `node_modules` in the path (which misses tarballs).
225
+ const sourceCheckout = await isSourceCheckout(resolvedModuleDir)
226
+ if (!sourceCheckout) {
227
+ return {
228
+ title: 'Content hash',
229
+ level: 'info',
230
+ summary: 'Content hash not available: local KeeWeb dist is only present in a source checkout',
231
+ details: verbose
232
+ ? ['Clone the repo and run: bun run --cwd apps/keeweb build', 'Then re-run status from the source checkout']
233
+ : undefined,
234
+ }
235
+ }
236
+
197
237
  const localFile = Bun.file(distIndexPath)
198
238
  const localExists = await localFile.exists()
199
239