@jhizzard/termdeck 1.0.6 → 1.0.7
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.
|
|
3
|
+
"version": "1.0.7",
|
|
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"
|
|
@@ -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,158 @@ 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
|
+
const repoRoot = ctx.repoRoot || path.resolve(__dirname, '..', '..', '..', '..', '..');
|
|
361
|
+
const bundledAbs = path.isAbsolute(target.bundledPath)
|
|
362
|
+
? target.bundledPath
|
|
363
|
+
: path.join(repoRoot, target.bundledPath);
|
|
364
|
+
const readImpl = ctx.readFileImpl || ((p) => fs.readFileSync(p, 'utf-8'));
|
|
365
|
+
let bundledBody;
|
|
366
|
+
try { bundledBody = readImpl(bundledAbs); }
|
|
367
|
+
catch (err) { return { present: false, probeError: `bundled source read failed at ${bundledAbs}: ${err.message}` }; }
|
|
368
|
+
const m = bundledBody.match(target.importPattern);
|
|
369
|
+
if (!m) {
|
|
370
|
+
return { present: false, probeError: `bundled source at ${target.bundledPath} does not contain ${target.importPattern} — has the import been removed or renamed?` };
|
|
371
|
+
}
|
|
372
|
+
expected = m[1];
|
|
373
|
+
} else {
|
|
374
|
+
return { present: false, probeError: `edgeFunctionPin probe ${target.name} has unknown expectedFrom: ${target.expectedFrom}` };
|
|
375
|
+
}
|
|
376
|
+
|
|
377
|
+
// Fetch deployed body via Management API.
|
|
378
|
+
let res;
|
|
379
|
+
try {
|
|
380
|
+
res = await fn(
|
|
381
|
+
`https://api.supabase.com/v1/projects/${ctx.projectRef}/functions/${target.functionSlug}/body`,
|
|
382
|
+
{ headers: { 'Authorization': `Bearer ${accessToken}` } }
|
|
383
|
+
);
|
|
384
|
+
} catch (err) {
|
|
385
|
+
return { present: false, probeError: `Management API fetch failed: ${err.message}` };
|
|
386
|
+
}
|
|
387
|
+
if (!res.ok) {
|
|
388
|
+
return {
|
|
389
|
+
present: false,
|
|
390
|
+
probeError: `Management API returned HTTP ${res.status} for ${target.functionSlug}/body — function may not be deployed yet, or access token lacks permission.`
|
|
391
|
+
};
|
|
392
|
+
}
|
|
393
|
+
let body;
|
|
394
|
+
try { body = await res.text(); }
|
|
395
|
+
catch (err) { return { present: false, probeError: `body decode failed: ${err.message}` }; }
|
|
396
|
+
|
|
397
|
+
const m = body.match(target.importPattern);
|
|
398
|
+
if (!m) {
|
|
399
|
+
return {
|
|
400
|
+
present: false,
|
|
401
|
+
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.`
|
|
402
|
+
};
|
|
403
|
+
}
|
|
404
|
+
const deployed = m[1];
|
|
405
|
+
if (deployed === expected) {
|
|
406
|
+
return { present: true };
|
|
407
|
+
}
|
|
408
|
+
return {
|
|
409
|
+
present: false,
|
|
410
|
+
probeError: `pin drift on ${target.functionSlug}: deployed=${deployed}, expected=${expected}. Run \`termdeck init --rumen --yes\` to redeploy from current.`
|
|
411
|
+
};
|
|
412
|
+
}
|
|
413
|
+
|
|
414
|
+
// Default `npm view <pkg> version` shellout. Returns { ok, version, error }.
|
|
415
|
+
// Synchronous spawnSync with 15s timeout — same shape as init-rumen.js
|
|
416
|
+
// resolveRumenVersion helper. Wrapped in a thenable so the probe can await.
|
|
417
|
+
function defaultNpmViewVersion(pkg) {
|
|
418
|
+
return Promise.resolve().then(() => {
|
|
419
|
+
const r = spawnSync('npm', ['view', pkg, 'version'], {
|
|
420
|
+
encoding: 'utf-8',
|
|
421
|
+
stdio: ['ignore', 'pipe', 'pipe'],
|
|
422
|
+
timeout: 15000
|
|
423
|
+
});
|
|
424
|
+
if (r.status === 0) {
|
|
425
|
+
const v = (r.stdout || '').trim();
|
|
426
|
+
if (/^\d+\.\d+\.\d+/.test(v)) return { ok: true, version: v };
|
|
427
|
+
return { ok: false, error: `unexpected output: ${JSON.stringify(v)}` };
|
|
428
|
+
}
|
|
429
|
+
const stderr = (r.stderr || '').trim();
|
|
430
|
+
return {
|
|
431
|
+
ok: false,
|
|
432
|
+
error: stderr ? stderr.split('\n').pop() : `exit ${r.status === null ? 'timeout' : r.status} — offline?`
|
|
433
|
+
};
|
|
434
|
+
});
|
|
435
|
+
}
|
|
436
|
+
|
|
254
437
|
// Run a probe and decide present/absent based on the probe's contract.
|
|
255
438
|
// Sprint 51.6 T3: dispatches by target.probeKind. Default is the legacy
|
|
256
439
|
// pgClient.query path; 'functionSource' calls probeFunctionSource (HTTP).
|
|
440
|
+
// Sprint 52 (Class O): 'edgeFunctionPin' calls probeEdgeFunctionPin (HTTP
|
|
441
|
+
// + npm view / bundled-source resolution).
|
|
257
442
|
async function probeOne(pgClient, target, ctx = {}) {
|
|
258
443
|
if (target.probeKind === 'functionSource') {
|
|
259
444
|
return probeFunctionSource(target, ctx);
|
|
260
445
|
}
|
|
446
|
+
if (target.probeKind === 'edgeFunctionPin') {
|
|
447
|
+
return probeEdgeFunctionPin(target, ctx);
|
|
448
|
+
}
|
|
261
449
|
let result;
|
|
262
450
|
try {
|
|
263
451
|
result = await pgClient.query(target.probeSql);
|
|
@@ -343,7 +531,10 @@ async function auditUpgrade({
|
|
|
343
531
|
dryRun = false,
|
|
344
532
|
probes,
|
|
345
533
|
_migrations,
|
|
346
|
-
_fetch
|
|
534
|
+
_fetch,
|
|
535
|
+
_npmView,
|
|
536
|
+
_readFile,
|
|
537
|
+
_repoRoot
|
|
347
538
|
} = {}) {
|
|
348
539
|
if (!pgClient || typeof pgClient.query !== 'function') {
|
|
349
540
|
throw new Error('auditUpgrade: pgClient with .query() is required');
|
|
@@ -365,7 +556,13 @@ async function auditUpgrade({
|
|
|
365
556
|
|
|
366
557
|
for (const target of targets) {
|
|
367
558
|
probed.push(target.name);
|
|
368
|
-
const probeResult = await probeOne(pgClient, target, {
|
|
559
|
+
const probeResult = await probeOne(pgClient, target, {
|
|
560
|
+
projectRef,
|
|
561
|
+
fetchImpl: _fetch,
|
|
562
|
+
npmViewImpl: _npmView,
|
|
563
|
+
readFileImpl: _readFile,
|
|
564
|
+
repoRoot: _repoRoot
|
|
565
|
+
});
|
|
369
566
|
if (probeResult.present) {
|
|
370
567
|
present.push(target.name);
|
|
371
568
|
continue;
|
|
@@ -374,10 +571,15 @@ async function auditUpgrade({
|
|
|
374
571
|
// Sprint 51.6 T3 — Bug D: functionSource probes go to skipped[] (not
|
|
375
572
|
// missing[]). The corresponding fix is a re-run of `init --rumen` which
|
|
376
573
|
// calls deployFunctions; audit-upgrade does not auto-redeploy.
|
|
377
|
-
|
|
574
|
+
// Sprint 52 — Class O: same treatment for edgeFunctionPin probes —
|
|
575
|
+
// pin drift is non-blocking (YELLOW), recommendation in skipped reason.
|
|
576
|
+
if (target.probeKind === 'functionSource' || target.probeKind === 'edgeFunctionPin') {
|
|
577
|
+
const fallbackReason = target.probeKind === 'edgeFunctionPin'
|
|
578
|
+
? 'pin drift — redeploy via init --rumen'
|
|
579
|
+
: 'function source drift — redeploy via init --rumen';
|
|
378
580
|
skipped.push({
|
|
379
581
|
name: target.name,
|
|
380
|
-
reason: probeResult.probeError ||
|
|
582
|
+
reason: probeResult.probeError || fallbackReason,
|
|
381
583
|
});
|
|
382
584
|
continue;
|
|
383
585
|
}
|
|
@@ -420,6 +622,7 @@ module.exports = {
|
|
|
420
622
|
// selection / apply pathway behavior without needing a live pg client.
|
|
421
623
|
_probeOne: probeOne,
|
|
422
624
|
_probeFunctionSource: probeFunctionSource,
|
|
625
|
+
_probeEdgeFunctionPin: probeEdgeFunctionPin,
|
|
423
626
|
_applyOne: applyOne,
|
|
424
627
|
_resolveMigrationFile: resolveMigrationFile
|
|
425
628
|
};
|