@jhizzard/termdeck 1.0.6 → 1.0.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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@jhizzard/termdeck",
3
- "version": "1.0.6",
3
+ "version": "1.0.8",
4
4
  "description": "Browser-based terminal multiplexer with metadata overlays, panel flashback memory recall, and AI-aware session management",
5
5
  "bin": {
6
6
  "termdeck": "./packages/cli/src/index.js"
@@ -412,11 +412,19 @@ function deployFunctions(rumenVersion, projectRef, dryRun) {
412
412
  for (const name of fnNames) {
413
413
  // Sprint 51.6 T3 — `--project-ref <ref>` explicit, dodging supabase
414
414
  // link-state subprocess isolation (Bug C, Brad's 2026-05-03 install).
415
- step(`Running: supabase functions deploy ${name} --project-ref ${projectRef} --no-verify-jwt...`);
415
+ // Sprint 52 dogfood (2026-05-04): `--use-api` added because the default
416
+ // Docker-bundler path fails on macOS when the staging dir is under
417
+ // os.tmpdir() (= /var/folders/... on macOS, which is NOT in Docker
418
+ // Desktop's default file-sharing allowlist). Symptom on stale supabase
419
+ // CLI 2.75.0: `entrypoint path does not exist (supabase/functions/<name>/
420
+ // index.ts)` even though the file IS present. `--use-api` uploads via
421
+ // the Management API and bypasses Docker entirely. v1.0.8 fold-in.
422
+ step(`Running: supabase functions deploy ${name} --project-ref ${projectRef} --no-verify-jwt --use-api...`);
416
423
  const r = runShell('supabase', [
417
424
  'functions', 'deploy', name,
418
425
  '--project-ref', projectRef,
419
426
  '--no-verify-jwt',
427
+ '--use-api',
420
428
  ], { cwd: stage });
421
429
  if (!r.ok) {
422
430
  fail(`deploy of ${name} failed (exit ${r.code})`);
@@ -41,6 +41,8 @@
41
41
  'use strict';
42
42
 
43
43
  const path = require('path');
44
+ const fs = require('fs');
45
+ const { spawnSync } = require('child_process');
44
46
 
45
47
  const migrations = require('./migrations');
46
48
  const { applyTemplating } = require('./migration-templating');
@@ -183,6 +185,47 @@ const PROBES = Object.freeze([
183
185
  functionSlug: 'graph-inference',
184
186
  requiredMarker: "Deno.env.get('SUPABASE_DB_URL')",
185
187
  presentWhen: 'sourceMatch'
188
+ },
189
+ // Sprint 52 — Class O: deployed-state pin drift between npm-published
190
+ // packages and Supabase-deployed Edge Functions. `npm publish` doesn't
191
+ // touch Supabase; `init --rumen` redeploys. If a user upgraded the npm
192
+ // package but didn't re-run init --rumen, the Edge Function is pinned to
193
+ // whatever rumen version was current at last deploy.
194
+ //
195
+ // probeKind 'edgeFunctionPin':
196
+ // - Downloads deployed Edge Function body via Management API.
197
+ // - Greps the npm:<pkg>@<version> import line.
198
+ // - Compares against the EXPECTED version. Two resolution shapes:
199
+ // - 'npmRegistry': run `npm view <pkg> version` (used when bundled
200
+ // source has a __RUMEN_VERSION__-style placeholder substituted at
201
+ // deploy time).
202
+ // - 'bundledSource': read bundled file, grep same npm:<pkg>@<version>
203
+ // (used when bundled source pins a static version verbatim).
204
+ // - On drift: returns absent → goes to skipped[] with a recommendation
205
+ // pointing at `termdeck init --rumen --yes`.
206
+ // - On unreachable Management API / npm view failure: skipped with the
207
+ // fail-soft reason (mirrors functionSource probe degradation pattern).
208
+ //
209
+ // YELLOW (skipped[]) is the right severity — pin drift is non-blocking
210
+ // for the wizard and non-blocking for any single Rumen tick. It just
211
+ // means stale runtime. The user-actionable fix is `init --rumen --yes`.
212
+ {
213
+ name: 'rumen-tick deployed pin matches current @jhizzard/rumen',
214
+ kind: 'rumen',
215
+ probeKind: 'edgeFunctionPin',
216
+ functionSlug: 'rumen-tick',
217
+ importPattern: /npm:@jhizzard\/rumen@(\d+\.\d+\.\d+(?:-[a-z0-9.]+)?)/,
218
+ expectedFrom: 'npmRegistry',
219
+ npmRegistryPkg: '@jhizzard/rumen'
220
+ },
221
+ {
222
+ name: 'graph-inference deployed pin matches bundled postgres',
223
+ kind: 'rumen',
224
+ probeKind: 'edgeFunctionPin',
225
+ functionSlug: 'graph-inference',
226
+ importPattern: /npm:postgres@(\d+\.\d+\.\d+(?:-[a-z0-9.]+)?)/,
227
+ expectedFrom: 'bundledSource',
228
+ bundledPath: 'packages/server/src/setup/rumen/functions/graph-inference/index.ts'
186
229
  }
187
230
  ]);
188
231
 
@@ -251,13 +294,164 @@ async function probeFunctionSource(target, { projectRef, fetchImpl }) {
251
294
  };
252
295
  }
253
296
 
297
+ // Sprint 52 — Class O: deployed-state pin drift between npm-published
298
+ // packages and Supabase-deployed Edge Functions.
299
+ //
300
+ // Probes one Edge Function for npm:<pkg>@<version> drift between the
301
+ // deployed body and the EXPECTED version. Returns:
302
+ // - { present: true } when deployed pin == expected pin (no drift)
303
+ // - { present: false, probeError: '<recommendation>' } when drift is
304
+ // detected — caller routes to skipped[] (YELLOW, non-blocking).
305
+ // - { present: false, probeError: '<reason>' } when probe can't run
306
+ // (no fetch impl, no token, Management API HTTP error, npm view
307
+ // failure, deployed body doesn't match the importPattern).
308
+ //
309
+ // Required ctx:
310
+ // - projectRef: passed through from auditUpgrade()
311
+ // - SUPABASE_ACCESS_TOKEN in env (sbp_*) — same as functionSource probe
312
+ // Optional ctx:
313
+ // - fetchImpl: test injection for HTTP. Defaults to globalThis.fetch.
314
+ // - npmViewImpl: test injection for `npm view <pkg> version`. Defaults
315
+ // to a real spawnSync('npm', ['view', pkg, 'version']). Returns
316
+ // { ok, version, error }.
317
+ // - readFileImpl: test injection for bundled-source reads. Defaults to
318
+ // fs.readFileSync(absPath, 'utf-8').
319
+ // - repoRoot: optional — used to resolve target.bundledPath. Defaults to
320
+ // packages/server/src/setup/.. relative resolution (3 levels up from
321
+ // this file is the repo root).
322
+ async function probeEdgeFunctionPin(target, ctx = {}) {
323
+ const fn = ctx.fetchImpl || (typeof globalThis !== 'undefined' ? globalThis.fetch : undefined);
324
+ if (typeof fn !== 'function') {
325
+ return { present: false, probeError: 'no fetch implementation available' };
326
+ }
327
+ if (!ctx.projectRef) {
328
+ return { present: false, probeError: 'projectRef required for edgeFunctionPin probe' };
329
+ }
330
+ const accessToken = process.env.SUPABASE_ACCESS_TOKEN;
331
+ if (!accessToken) {
332
+ return {
333
+ present: false,
334
+ probeError: 'SUPABASE_ACCESS_TOKEN not set; cannot fetch deployed function body. Set the personal access token (`supabase login` writes it to ~/.supabase/access-token) to enable Edge Function pin-drift detection.'
335
+ };
336
+ }
337
+
338
+ // Resolve EXPECTED version first — if this fails we can short-circuit
339
+ // before the Management API round trip.
340
+ let expected;
341
+ if (target.expectedFrom === 'npmRegistry') {
342
+ if (!target.npmRegistryPkg) {
343
+ return { present: false, probeError: `edgeFunctionPin probe ${target.name} missing npmRegistryPkg` };
344
+ }
345
+ const npmView = ctx.npmViewImpl || defaultNpmViewVersion;
346
+ let r;
347
+ try { r = await npmView(target.npmRegistryPkg); }
348
+ catch (err) { return { present: false, probeError: `npm view ${target.npmRegistryPkg} failed: ${err.message}` }; }
349
+ if (!r || !r.ok) {
350
+ return {
351
+ present: false,
352
+ probeError: `npm view ${target.npmRegistryPkg} version failed: ${r && r.error ? r.error : 'unknown error'}`
353
+ };
354
+ }
355
+ expected = r.version;
356
+ } else if (target.expectedFrom === 'bundledSource') {
357
+ if (!target.bundledPath || !target.importPattern) {
358
+ return { present: false, probeError: `edgeFunctionPin probe ${target.name} missing bundledPath or importPattern` };
359
+ }
360
+ // __dirname = .../packages/server/src/setup/. Four `..` lands at repo
361
+ // root: setup → src → server → packages → <repoRoot>. Sprint 52 shipped
362
+ // five `..` (off-by-one), which on a globally-installed @jhizzard/termdeck
363
+ // resolved to the parent of the package dir and the bundled-source read
364
+ // fail-softed into skipped[] with an ENOENT reason instead of comparing
365
+ // pins. v1.0.8 fold-in.
366
+ const repoRoot = ctx.repoRoot || path.resolve(__dirname, '..', '..', '..', '..');
367
+ const bundledAbs = path.isAbsolute(target.bundledPath)
368
+ ? target.bundledPath
369
+ : path.join(repoRoot, target.bundledPath);
370
+ const readImpl = ctx.readFileImpl || ((p) => fs.readFileSync(p, 'utf-8'));
371
+ let bundledBody;
372
+ try { bundledBody = readImpl(bundledAbs); }
373
+ catch (err) { return { present: false, probeError: `bundled source read failed at ${bundledAbs}: ${err.message}` }; }
374
+ const m = bundledBody.match(target.importPattern);
375
+ if (!m) {
376
+ return { present: false, probeError: `bundled source at ${target.bundledPath} does not contain ${target.importPattern} — has the import been removed or renamed?` };
377
+ }
378
+ expected = m[1];
379
+ } else {
380
+ return { present: false, probeError: `edgeFunctionPin probe ${target.name} has unknown expectedFrom: ${target.expectedFrom}` };
381
+ }
382
+
383
+ // Fetch deployed body via Management API.
384
+ let res;
385
+ try {
386
+ res = await fn(
387
+ `https://api.supabase.com/v1/projects/${ctx.projectRef}/functions/${target.functionSlug}/body`,
388
+ { headers: { 'Authorization': `Bearer ${accessToken}` } }
389
+ );
390
+ } catch (err) {
391
+ return { present: false, probeError: `Management API fetch failed: ${err.message}` };
392
+ }
393
+ if (!res.ok) {
394
+ return {
395
+ present: false,
396
+ probeError: `Management API returned HTTP ${res.status} for ${target.functionSlug}/body — function may not be deployed yet, or access token lacks permission.`
397
+ };
398
+ }
399
+ let body;
400
+ try { body = await res.text(); }
401
+ catch (err) { return { present: false, probeError: `body decode failed: ${err.message}` }; }
402
+
403
+ const m = body.match(target.importPattern);
404
+ if (!m) {
405
+ return {
406
+ present: false,
407
+ probeError: `deployed ${target.functionSlug} body does not match ${target.importPattern} — function may be at an unexpected source revision; re-run \`termdeck init --rumen --yes\` to redeploy from bundled.`
408
+ };
409
+ }
410
+ const deployed = m[1];
411
+ if (deployed === expected) {
412
+ return { present: true };
413
+ }
414
+ return {
415
+ present: false,
416
+ probeError: `pin drift on ${target.functionSlug}: deployed=${deployed}, expected=${expected}. Run \`termdeck init --rumen --yes\` to redeploy from current.`
417
+ };
418
+ }
419
+
420
+ // Default `npm view <pkg> version` shellout. Returns { ok, version, error }.
421
+ // Synchronous spawnSync with 15s timeout — same shape as init-rumen.js
422
+ // resolveRumenVersion helper. Wrapped in a thenable so the probe can await.
423
+ function defaultNpmViewVersion(pkg) {
424
+ return Promise.resolve().then(() => {
425
+ const r = spawnSync('npm', ['view', pkg, 'version'], {
426
+ encoding: 'utf-8',
427
+ stdio: ['ignore', 'pipe', 'pipe'],
428
+ timeout: 15000
429
+ });
430
+ if (r.status === 0) {
431
+ const v = (r.stdout || '').trim();
432
+ if (/^\d+\.\d+\.\d+/.test(v)) return { ok: true, version: v };
433
+ return { ok: false, error: `unexpected output: ${JSON.stringify(v)}` };
434
+ }
435
+ const stderr = (r.stderr || '').trim();
436
+ return {
437
+ ok: false,
438
+ error: stderr ? stderr.split('\n').pop() : `exit ${r.status === null ? 'timeout' : r.status} — offline?`
439
+ };
440
+ });
441
+ }
442
+
254
443
  // Run a probe and decide present/absent based on the probe's contract.
255
444
  // Sprint 51.6 T3: dispatches by target.probeKind. Default is the legacy
256
445
  // pgClient.query path; 'functionSource' calls probeFunctionSource (HTTP).
446
+ // Sprint 52 (Class O): 'edgeFunctionPin' calls probeEdgeFunctionPin (HTTP
447
+ // + npm view / bundled-source resolution).
257
448
  async function probeOne(pgClient, target, ctx = {}) {
258
449
  if (target.probeKind === 'functionSource') {
259
450
  return probeFunctionSource(target, ctx);
260
451
  }
452
+ if (target.probeKind === 'edgeFunctionPin') {
453
+ return probeEdgeFunctionPin(target, ctx);
454
+ }
261
455
  let result;
262
456
  try {
263
457
  result = await pgClient.query(target.probeSql);
@@ -343,7 +537,10 @@ async function auditUpgrade({
343
537
  dryRun = false,
344
538
  probes,
345
539
  _migrations,
346
- _fetch
540
+ _fetch,
541
+ _npmView,
542
+ _readFile,
543
+ _repoRoot
347
544
  } = {}) {
348
545
  if (!pgClient || typeof pgClient.query !== 'function') {
349
546
  throw new Error('auditUpgrade: pgClient with .query() is required');
@@ -365,7 +562,13 @@ async function auditUpgrade({
365
562
 
366
563
  for (const target of targets) {
367
564
  probed.push(target.name);
368
- const probeResult = await probeOne(pgClient, target, { projectRef, fetchImpl: _fetch });
565
+ const probeResult = await probeOne(pgClient, target, {
566
+ projectRef,
567
+ fetchImpl: _fetch,
568
+ npmViewImpl: _npmView,
569
+ readFileImpl: _readFile,
570
+ repoRoot: _repoRoot
571
+ });
369
572
  if (probeResult.present) {
370
573
  present.push(target.name);
371
574
  continue;
@@ -374,10 +577,15 @@ async function auditUpgrade({
374
577
  // Sprint 51.6 T3 — Bug D: functionSource probes go to skipped[] (not
375
578
  // missing[]). The corresponding fix is a re-run of `init --rumen` which
376
579
  // calls deployFunctions; audit-upgrade does not auto-redeploy.
377
- if (target.probeKind === 'functionSource') {
580
+ // Sprint 52 Class O: same treatment for edgeFunctionPin probes —
581
+ // pin drift is non-blocking (YELLOW), recommendation in skipped reason.
582
+ if (target.probeKind === 'functionSource' || target.probeKind === 'edgeFunctionPin') {
583
+ const fallbackReason = target.probeKind === 'edgeFunctionPin'
584
+ ? 'pin drift — redeploy via init --rumen'
585
+ : 'function source drift — redeploy via init --rumen';
378
586
  skipped.push({
379
587
  name: target.name,
380
- reason: probeResult.probeError || 'function source drift — redeploy via init --rumen',
588
+ reason: probeResult.probeError || fallbackReason,
381
589
  });
382
590
  continue;
383
591
  }
@@ -420,6 +628,7 @@ module.exports = {
420
628
  // selection / apply pathway behavior without needing a live pg client.
421
629
  _probeOne: probeOne,
422
630
  _probeFunctionSource: probeFunctionSource,
631
+ _probeEdgeFunctionPin: probeEdgeFunctionPin,
423
632
  _applyOne: applyOne,
424
633
  _resolveMigrationFile: resolveMigrationFile
425
634
  };