@jhizzard/termdeck 1.0.9 → 1.0.10

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.9",
3
+ "version": "1.0.10",
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"
@@ -203,6 +203,24 @@ function renderFooter(rows, exitCode) {
203
203
  ` Or upgrade individually: npm install -g @jhizzard/termdeck@latest`
204
204
  );
205
205
  }
206
+ // Sprint 56 (T1 Cross-Cutting #2 Part A) — logical inversion fix. Pre-
207
+ // Sprint-56 the footer always read "All packages up to date" on exit 0,
208
+ // even when every row was NOT_INSTALLED. That's logically wrong: saying
209
+ // "all up to date" when "all not installed" misleads the user into
210
+ // thinking the stack is healthy. Distinguish the two states explicitly.
211
+ const notInstalled = rows.filter((r) => r.status === STATUS.NOT_INSTALLED).length;
212
+ if (notInstalled === rows.length && rows.length > 0) {
213
+ return (
214
+ `\n No stack packages detected (${rows.length} of ${rows.length} not installed).\n` +
215
+ ` To bootstrap the full stack: npx @jhizzard/termdeck-stack`
216
+ );
217
+ }
218
+ if (notInstalled > 0) {
219
+ return (
220
+ `\n ${notInstalled} of ${rows.length} stack packages not installed; the rest up to date.\n` +
221
+ ` To install missing pieces: npx @jhizzard/termdeck-stack`
222
+ );
223
+ }
206
224
  return `\n All packages up to date.`;
207
225
  }
208
226
 
@@ -104,6 +104,18 @@ function checkTranscriptTableHint(databaseUrl) {
104
104
  // Parse CLI args
105
105
  const args = process.argv.slice(2);
106
106
 
107
+ // Sprint 56 (T1 Cell 18) — `--version` / `-v` handler. Pre-Sprint-56 the
108
+ // flag was silently ignored: the CLI fell through to the launcher's stack
109
+ // boot path and never printed a version. Now mirror the convention every
110
+ // CLI in the world honors: print the package version and exit 0. Done
111
+ // BEFORE the `init` dispatch so `termdeck --version init --mnestra`
112
+ // (nonsensical but unambiguous) still terminates with the version.
113
+ if (args.includes('--version') || args.includes('-v')) {
114
+ const pkg = require(path.join(__dirname, '..', '..', '..', 'package.json'));
115
+ process.stdout.write(`@jhizzard/termdeck v${pkg.version}\n`);
116
+ process.exit(0);
117
+ }
118
+
107
119
  // Subcommand dispatch — handle `termdeck init --mnestra|--rumen` before
108
120
  // falling through to the default launcher's flag parsing. The `require` of
109
121
  // init-*.js is lazy so users running the normal `termdeck` command never pay
@@ -172,9 +172,28 @@ function preflight() {
172
172
  const missing = required.filter((k) => !secrets[k]);
173
173
  if (missing.length > 0) {
174
174
  fail(`missing keys: ${missing.join(', ')}`);
175
- process.stderr.write(
176
- '\nRun `termdeck init --mnestra` first it writes the keys this wizard needs.\n'
177
- );
175
+ // Sprint 56 (T1 Cell 10) — distinguish "no secrets file" (current
176
+ // message is correct: run init --mnestra to bootstrap) from "secrets
177
+ // file exists but missing one or more keys" (current message is
178
+ // misleading: user already ran init --mnestra; the actual cause is
179
+ // that ANTHROPIC_API_KEY is OPTIONAL for Mnestra but REQUIRED for
180
+ // Rumen, so a user who skipped it during init --mnestra ends up here).
181
+ const secretsPath = path.join(os.homedir(), '.termdeck', 'secrets.env');
182
+ const fileExists = fs.existsSync(secretsPath);
183
+ if (fileExists) {
184
+ process.stderr.write(
185
+ `\n~/.termdeck/secrets.env exists but is missing the keys above.\n` +
186
+ `${missing.includes('ANTHROPIC_API_KEY')
187
+ ? 'Note: ANTHROPIC_API_KEY is optional for Mnestra but REQUIRED for Rumen.\n'
188
+ : ''}` +
189
+ `Re-run \`termdeck init --mnestra\` to add the missing keys, or edit\n` +
190
+ `${secretsPath} directly and re-run \`termdeck init --rumen\`.\n`
191
+ );
192
+ } else {
193
+ process.stderr.write(
194
+ '\nRun `termdeck init --mnestra` first — it writes the keys this wizard needs.\n'
195
+ );
196
+ }
178
197
  return null;
179
198
  }
180
199
  ok();
@@ -896,7 +915,7 @@ function wireAccessTokenInMcpJson({ token, mcpJsonPath, _testFs } = {}) {
896
915
  return { status: 'updated', path: targetPath };
897
916
  }
898
917
 
899
- function printNextSteps(projectRef, vaultResult, llmResult) {
918
+ function printNextSteps(projectRef, vaultResult, llmResult, skipSchedule) {
900
919
  const rumenTickUrl = `https://${projectRef}.supabase.co/functions/v1/rumen-tick`;
901
920
  const graphInferenceUrl = `https://${projectRef}.supabase.co/functions/v1/graph-inference`;
902
921
  const now = new Date();
@@ -912,13 +931,24 @@ function printNextSteps(projectRef, vaultResult, llmResult) {
912
931
  ? ' Graph edges: classified by Claude Haiku 4.5 (GRAPH_LLM_CLASSIFY=1).'
913
932
  : ' Graph edges: untyped (relates_to). To enable: supabase secrets set GRAPH_LLM_CLASSIFY=1';
914
933
 
934
+ // Sprint 56 (T1 Cell 12) — when --skip-schedule was passed, the cron
935
+ // schedule wasn't applied. Don't print "first run" or the cron-cadence
936
+ // claim — there is no scheduled first run. Show the function URL so
937
+ // the user knows what to manually invoke instead.
938
+ const rumenLine = skipSchedule
939
+ ? ` rumen-tick cron NOT scheduled (--skip-schedule). Manual fire only.`
940
+ : ` rumen-tick every 15 min — first run: ${next.toISOString().replace(/\.\d+Z$/, 'Z')}`;
941
+ const graphLine = skipSchedule
942
+ ? ` graph-inference cron NOT scheduled (--skip-schedule). Manual fire only.`
943
+ : ` graph-inference daily at 03:00 UTC (Sprint 42 cron)`;
944
+
915
945
  process.stdout.write(`
916
946
  Rumen is deployed.
917
947
 
918
948
  Edge Functions:
919
- rumen-tick every 15 min — first run: ${next.toISOString().replace(/\.\d+Z$/, 'Z')}
949
+ ${rumenLine}
920
950
  ${rumenTickUrl}
921
- graph-inference daily at 03:00 UTC (Sprint 42 cron)
951
+ ${graphLine}
922
952
  ${graphInferenceUrl}
923
953
 
924
954
  Next steps:
@@ -954,10 +984,20 @@ async function main(argv) {
954
984
  }
955
985
 
956
986
  if (!flags.yes) {
957
- const go = await prompts.confirm(`? Proceed with deploy to project ${projectRef}?`);
958
- if (!go) {
959
- process.stdout.write('Cancelled.\n');
960
- return 0;
987
+ // Sprint 56 (T1 Cell 13b) dry-run-aware annotation parity. The
988
+ // graph-classify confirm at line 696-698 already prints "(dry-run,
989
+ // defaulting Y)" when flags.dryRun is set; this prompt didn't, leaving
990
+ // users unsure whether they were authorizing a real deploy or a no-op
991
+ // simulation. Match the same shape: print the prompt with the dry-run
992
+ // annotation, auto-progress without blocking on user input in dry-run.
993
+ if (flags.dryRun) {
994
+ process.stdout.write(`? Proceed with deploy to project ${projectRef}? [Y/n] (dry-run, defaulting Y)\n`);
995
+ } else {
996
+ const go = await prompts.confirm(`? Proceed with deploy to project ${projectRef}?`);
997
+ if (!go) {
998
+ process.stdout.write('Cancelled.\n');
999
+ return 0;
1000
+ }
961
1001
  }
962
1002
  }
963
1003
 
@@ -1053,7 +1093,7 @@ async function main(argv) {
1053
1093
  process.stdout.write('→ Skipping pg_cron schedule (per --skip-schedule) ✓\n');
1054
1094
  }
1055
1095
 
1056
- printNextSteps(projectRef, vaultResult, llmResult);
1096
+ printNextSteps(projectRef, vaultResult, llmResult, flags.skipSchedule);
1057
1097
  return 0;
1058
1098
  }
1059
1099
 
@@ -272,6 +272,20 @@ function createServer(config) {
272
272
 
273
273
  app.use(express.json());
274
274
 
275
+ // Sprint 56 (T2 F-T2-1) — malformed-JSON body returns JSON 400, not
276
+ // express's default HTML error page. Pre-Sprint-56 every POST/PATCH
277
+ // endpoint that consumed a JSON body returned `text/html` on parse
278
+ // failure, breaking programmatic clients (the inject script, MCP, CI
279
+ // smoke tests). The status code (400) was correct; only the body
280
+ // shape regressed. Mounted IMMEDIATELY after express.json() so it
281
+ // catches body-parse errors before any route handler runs.
282
+ app.use((err, req, res, next) => {
283
+ if (err && (err.type === 'entity.parse.failed' || err instanceof SyntaxError)) {
284
+ return res.status(400).json({ error: 'Malformed JSON body', detail: err.message });
285
+ }
286
+ return next(err);
287
+ });
288
+
275
289
  // First-run detection (Sprint 19 T3): true when ~/.termdeck/config.yaml
276
290
  // does not exist. Surfaced on /api/config so the client can offer the
277
291
  // setup wizard on first visit. T1's /api/setup endpoint may reuse this.
@@ -46,6 +46,50 @@ const CANONICAL_TS_RE =
46
46
  const CANONICAL_NO_TS_RE =
47
47
  /^[-*]?\s*(T\d+):\s*(FINDING|FIX-PROPOSED|DONE)\s+[—\-]\s+(.+)$/;
48
48
 
49
+ // Sprint 56 (T4 Cell 7) — hardening-rule shape canonized 2026-05-04 yet
50
+ // the merger never knew about it. The new shape every 3+1+1 lane post
51
+ // follows is:
52
+ // `### [Tn(-CODEX)?] STAGE-VERB YYYY-MM-DD HH:MM ET — gist`
53
+ // where STAGE-VERB is liberal (BOOT, FINDING, FIX-PROPOSED, DONE, ACK,
54
+ // CHECKPOINT, AUDIT, REOPEN, NOTE, SKIP, PASS, FAIL, KICKOFF,
55
+ // SUB-FINDING, FINDING-MICRO, RETRACTED, …). Old merger only knew
56
+ // FINDING/FIX-PROPOSED/DONE so we'd silently drop everything else.
57
+ //
58
+ // Two patterns to handle:
59
+ // - Lines using `### ` markdown-header prefix (these are normally
60
+ // rejected by HEADER_RE as section headers — we MUST check this
61
+ // pattern BEFORE the HEADER_RE rejection in mergeStatusLine).
62
+ // - Lines using bare `[Tn]` without the `### ` prefix.
63
+ //
64
+ // Output shape: route the verb to the closest legacy STAGE so dashboard
65
+ // consumers expecting the 3-stage taxonomy still parse cleanly. Verbs
66
+ // outside the legacy set get bucketed by best-effort match (DONE for
67
+ // terminal verbs, FINDING for diagnostic, FIX-PROPOSED for action).
68
+ const HARDENING_RULE_RE =
69
+ /^(?:###\s+)?\[(T\d+(?:-[A-Z]+)?)\]\s+([A-Z][A-Z-]*)\s+(\d{4}-\d{2}-\d{2}\s+\d{1,2}:\d{2}\s+ET)\s+[—\-]\s+(.+)$/;
70
+
71
+ // Best-effort normalization: many of the new verbs are introductory
72
+ // (BOOT/KICKOFF/CHECKPOINT/ACK/NOTE → FINDING-shape information), some
73
+ // are terminal (DONE/PASS/FAIL/SKIP/RETRACTED → DONE-shape closure),
74
+ // and the action ones (FIX-PROPOSED/AUDIT/REOPEN) map to FIX-PROPOSED.
75
+ // FINDING/SUB-FINDING/FINDING-MICRO obviously map to FINDING.
76
+ function normalizeHardeningVerb(verb) {
77
+ const v = verb.toUpperCase();
78
+ if (v === 'FINDING' || v === 'SUB-FINDING' || v === 'FINDING-MICRO' ||
79
+ v === 'BOOT' || v === 'KICKOFF' || v === 'CHECKPOINT' ||
80
+ v === 'ACK' || v === 'NOTE') {
81
+ return 'FINDING';
82
+ }
83
+ if (v === 'FIX-PROPOSED' || v === 'AUDIT' || v === 'REOPEN') {
84
+ return 'FIX-PROPOSED';
85
+ }
86
+ if (v === 'DONE' || v === 'PASS' || v === 'FAIL' || v === 'SKIP' ||
87
+ v === 'RETRACTED' || v === 'COMPLETE' || v === 'COMPLETED') {
88
+ return 'DONE';
89
+ }
90
+ return 'FINDING';
91
+ }
92
+
49
93
  // Markdown section header — never a status line.
50
94
  const HEADER_RE = /^#{1,6}\s/;
51
95
  // Bare bracket-like meta lines: `_(no entries yet)_`, `> note`, etc.
@@ -108,13 +152,27 @@ function mergeStatusLine(rawLine, opts = {}) {
108
152
  if (typeof rawLine !== 'string') return null;
109
153
  const line = rawLine.replace(/\r?\n$/, '').trim();
110
154
  if (!line) return null;
155
+
156
+ // Sprint 56 (T4 Cell 7) — check the hardening-rule shape FIRST,
157
+ // before HEADER_RE rejects every `### `-prefixed line as a markdown
158
+ // section header. Sprint 51.6 hardening canonized lane posts as
159
+ // `### [Tn] STAGE-VERB YYYY-MM-DD HH:MM ET — gist`; without this
160
+ // early-return the merger silently dropped every Sprint 53/55 lane
161
+ // post.
162
+ let m = HARDENING_RULE_RE.exec(line);
163
+ if (m) {
164
+ const [, tag, verb, ts, summary] = m;
165
+ const stage = normalizeHardeningVerb(verb);
166
+ return `- ${tag}: ${stage} — ${summary.trim()} — ${ts.trim()}`;
167
+ }
168
+
111
169
  if (HEADER_RE.test(line)) return null;
112
170
  if (META_RE.test(line)) return null;
113
171
 
114
172
  // 1. Canonical with timestamp — pass through unchanged, normalize only the
115
173
  // leading bullet. The body is never trimmed here: real Sprint 46 lines are
116
174
  // routinely well over 120 chars and the brief mandates "same line out".
117
- let m = CANONICAL_TS_RE.exec(line);
175
+ m = CANONICAL_TS_RE.exec(line);
118
176
  if (m) {
119
177
  const [, tag, stage, summary, ts] = m;
120
178
  return `- ${tag}: ${stage} — ${summary.trim()} — ${ts.trim()}`;