@jhizzard/termdeck 1.0.11 → 1.0.13

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.
@@ -9,6 +9,26 @@
9
9
  // - init-rumen needs the project ref to run `supabase link --project-ref`
10
10
  // and to substitute into the pg_cron schedule SQL.
11
11
 
12
+ // Brad #2 (Sprint 59) — strip ONE pair of matched surrounding single OR
13
+ // double quotes from a string. Idempotent: a value with no quotes returns
14
+ // unchanged, mismatched quotes (`"foo'`, `bar"`) return unchanged. The
15
+ // dotenv parsers in config.js / dotenv-io.js / launcher.js already strip
16
+ // at file-read time, but Brad's reproducer ships the literal-quoted value
17
+ // through process.env (shell `export DATABASE_URL="\"$URL\""`), bypassing
18
+ // the file parsers entirely. Adding the strip here defends the validator
19
+ // boundary so any caller that hands us a quoted env-var value gets the
20
+ // same handling as a quoted secrets.env line.
21
+ function stripSurroundingQuotes(value) {
22
+ if (typeof value !== 'string') return value;
23
+ if (value.length < 2) return value;
24
+ const first = value[0];
25
+ const last = value[value.length - 1];
26
+ if ((first === '"' || first === "'") && first === last) {
27
+ return value.slice(1, -1);
28
+ }
29
+ return value;
30
+ }
31
+
12
32
  // A Supabase project URL looks like:
13
33
  // https://<project-ref>.supabase.co
14
34
  // The ref is 20 characters of lowercase alphanumerics, but we accept anything
@@ -17,7 +37,7 @@ function parseProjectUrl(url) {
17
37
  if (!url || typeof url !== 'string') {
18
38
  return { ok: false, error: 'empty url' };
19
39
  }
20
- const trimmed = url.trim().replace(/\/+$/, '');
40
+ const trimmed = stripSurroundingQuotes(url.trim()).replace(/\/+$/, '');
21
41
  let u;
22
42
  try {
23
43
  u = new URL(trimmed);
@@ -82,9 +102,10 @@ function looksLikeAnthropicKey(key) {
82
102
  // and direct connection URLs (`postgres://postgres:...@db.<ref>.supabase.co:5432/postgres`).
83
103
  function looksLikePostgresUrl(url) {
84
104
  if (!url || typeof url !== 'string') return 'empty';
105
+ const stripped = stripSurroundingQuotes(url.trim());
85
106
  let u;
86
107
  try {
87
- u = new URL(url);
108
+ u = new URL(stripped);
88
109
  } catch (_err) {
89
110
  return 'not a valid URL';
90
111
  }
@@ -137,16 +158,25 @@ function isTransactionPoolerUrl(parsedUrl) {
137
158
  // because validation is the caller's job (looksLikePostgresUrl handles that).
138
159
  function normalizeDatabaseUrl(url) {
139
160
  if (!url || typeof url !== 'string') return { url, modified: false };
161
+ // Brad #2: strip surrounding quotes silently — `modified` stays scoped
162
+ // to "appended pgbouncer params" so the caller's user-facing message
163
+ // ("Detected transaction pooler URL — appending ...") doesn't fire for
164
+ // a no-op quote strip. The strip itself is reflected in the returned
165
+ // `url` so downstream `new URL(normalized.url)` / pg.Pool consumers
166
+ // don't re-throw.
167
+ const stripped = stripSurroundingQuotes(url.trim());
140
168
  let u;
141
169
  try {
142
- u = new URL(url);
170
+ u = new URL(stripped);
143
171
  } catch (_err) {
144
- return { url, modified: false };
172
+ return { url: stripped, modified: false };
145
173
  }
146
- if (!isTransactionPoolerUrl(u)) return { url, modified: false };
174
+ if (!isTransactionPoolerUrl(u)) return { url: stripped, modified: false };
147
175
 
148
- // Already has pgbouncer set? Don't touch.
149
- if (u.searchParams.has('pgbouncer')) return { url, modified: false };
176
+ // Already has pgbouncer set? Don't touch — but still return the stripped URL,
177
+ // not the original (Sprint 59 T4-CODEX residual fix: pre-fix returned `url`,
178
+ // which would re-leak surrounding quotes from a quoted-pgbouncer-URL secrets.env).
179
+ if (u.searchParams.has('pgbouncer')) return { url: stripped, modified: false };
150
180
 
151
181
  u.searchParams.set('pgbouncer', 'true');
152
182
  // Set connection_limit only if not already set — preserve user intent.
@@ -171,5 +201,6 @@ module.exports = {
171
201
  looksLikePostgresUrl,
172
202
  isTransactionPoolerUrl,
173
203
  normalizeDatabaseUrl,
174
- maskSecret
204
+ maskSecret,
205
+ stripSurroundingQuotes
175
206
  };
@@ -0,0 +1,27 @@
1
+ // Sprint 59 T2 — PTY shell fallback chain helper (Brad #5).
2
+ //
3
+ // Pre-Sprint-59 the call site at packages/server/src/index.js:958 was:
4
+ // const spawnShell = isPlainShell ? cmdTrim : (config.shell || '/bin/zsh');
5
+ // Three failure modes converged on minimal Linux: (a) config.shell empty/unread
6
+ // because the YAML key was wiped or never set, (b) $SHELL ignored entirely,
7
+ // (c) /bin/zsh absent on the host. Result was a silent
8
+ // `execvp(3) failed: No such file or directory` from pty.spawn. The user's
9
+ // login shell was bypassed.
10
+ //
11
+ // /bin/sh is universally present on POSIX; /bin/zsh is not. The chain is:
12
+ // explicit cmdTrim → user's config.shell → $SHELL → /bin/sh universal floor.
13
+ // Caller (index.js) still owns the isPlainShell vs. -c branching; this helper
14
+ // only resolves the FALLBACK chain for the !isPlainShell branch (and for any
15
+ // future caller that wants a single-source-of-truth shell pick).
16
+ //
17
+ // The function intentionally treats "" and undefined identically — both
18
+ // participate in the falsy-OR chain. That matches how config.shell ends up
19
+ // empty when the user has `shell:` (no value) in ~/.termdeck/config.yaml,
20
+ // and how process.env.SHELL is undefined on container-like environments
21
+ // that strip the inherited shell var.
22
+
23
+ function resolveSpawnShell(cmdTrim, configShell, envShell) {
24
+ return cmdTrim || configShell || envShell || '/bin/sh';
25
+ }
26
+
27
+ module.exports = { resolveSpawnShell };