neonctl 2.22.2 → 2.23.0

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.
Files changed (113) hide show
  1. package/README.md +84 -0
  2. package/analytics.js +5 -2
  3. package/commands/branches.js +9 -1
  4. package/commands/connection_string.js +9 -1
  5. package/commands/functions.js +277 -0
  6. package/commands/index.js +4 -0
  7. package/commands/neon_auth.js +1013 -0
  8. package/commands/projects.js +9 -1
  9. package/commands/psql.js +6 -1
  10. package/functions_api.js +44 -0
  11. package/package.json +15 -5
  12. package/psql/cli.js +51 -0
  13. package/psql/command/cmd_cond.js +437 -0
  14. package/psql/command/cmd_connect.js +815 -0
  15. package/psql/command/cmd_copy.js +1025 -0
  16. package/psql/command/cmd_describe.js +1810 -0
  17. package/psql/command/cmd_format.js +909 -0
  18. package/psql/command/cmd_io.js +2187 -0
  19. package/psql/command/cmd_lo.js +385 -0
  20. package/psql/command/cmd_meta.js +970 -0
  21. package/psql/command/cmd_misc.js +187 -0
  22. package/psql/command/cmd_pipeline.js +1141 -0
  23. package/psql/command/cmd_restrict.js +171 -0
  24. package/psql/command/cmd_show.js +751 -0
  25. package/psql/command/dispatch.js +343 -0
  26. package/psql/command/inputQueue.js +42 -0
  27. package/psql/command/shared.js +71 -0
  28. package/psql/complete/filenames.js +139 -0
  29. package/psql/complete/index.js +104 -0
  30. package/psql/complete/matcher.js +314 -0
  31. package/psql/complete/psqlVars.js +247 -0
  32. package/psql/complete/queries.js +491 -0
  33. package/psql/complete/rules.js +2387 -0
  34. package/psql/core/common.js +1250 -0
  35. package/psql/core/help.js +576 -0
  36. package/psql/core/mainloop.js +1353 -0
  37. package/psql/core/prompt.js +437 -0
  38. package/psql/core/settings.js +684 -0
  39. package/psql/core/sqlHelp.js +1066 -0
  40. package/psql/core/startup.js +840 -0
  41. package/psql/core/syncVars.js +116 -0
  42. package/psql/core/variables.js +287 -0
  43. package/psql/describe/formatters.js +1277 -0
  44. package/psql/describe/processNamePattern.js +270 -0
  45. package/psql/describe/queries.js +2373 -0
  46. package/psql/describe/versionGate.js +43 -0
  47. package/psql/index.js +2005 -0
  48. package/psql/io/history.js +299 -0
  49. package/psql/io/input.js +120 -0
  50. package/psql/io/lineEditor/buffer.js +323 -0
  51. package/psql/io/lineEditor/complete.js +227 -0
  52. package/psql/io/lineEditor/filename.js +159 -0
  53. package/psql/io/lineEditor/index.js +891 -0
  54. package/psql/io/lineEditor/keymap.js +738 -0
  55. package/psql/io/lineEditor/vt100.js +363 -0
  56. package/psql/io/pgpass.js +202 -0
  57. package/psql/io/pgservice.js +194 -0
  58. package/psql/io/psqlrc.js +422 -0
  59. package/psql/print/aligned.js +1756 -0
  60. package/psql/print/asciidoc.js +248 -0
  61. package/psql/print/crosstab.js +460 -0
  62. package/psql/print/csv.js +92 -0
  63. package/psql/print/html.js +258 -0
  64. package/psql/print/json.js +96 -0
  65. package/psql/print/latex.js +396 -0
  66. package/psql/print/pager.js +265 -0
  67. package/psql/print/troff.js +258 -0
  68. package/psql/print/unaligned.js +118 -0
  69. package/psql/print/units.js +135 -0
  70. package/psql/scanner/slash.js +513 -0
  71. package/psql/scanner/sql.js +910 -0
  72. package/psql/scanner/stringutils.js +390 -0
  73. package/psql/types/backslash.js +1 -0
  74. package/psql/types/connection.js +1 -0
  75. package/psql/types/index.js +7 -0
  76. package/psql/types/printer.js +1 -0
  77. package/psql/types/repl.js +1 -0
  78. package/psql/types/scanner.js +24 -0
  79. package/psql/types/settings.js +1 -0
  80. package/psql/types/variables.js +1 -0
  81. package/psql/wire/connection.js +2844 -0
  82. package/psql/wire/copy.js +108 -0
  83. package/psql/wire/notify.js +59 -0
  84. package/psql/wire/pipeline.js +519 -0
  85. package/psql/wire/protocol.js +466 -0
  86. package/psql/wire/sasl.js +296 -0
  87. package/psql/wire/tls.js +596 -0
  88. package/test_utils/fixtures.js +1 -0
  89. package/utils/esbuild.js +147 -0
  90. package/utils/psql.js +107 -11
  91. package/utils/zip.js +4 -0
  92. package/writer.js +1 -1
  93. package/commands/auth.test.js +0 -211
  94. package/commands/branches.test.js +0 -460
  95. package/commands/checkout.test.js +0 -170
  96. package/commands/connection_string.test.js +0 -196
  97. package/commands/data_api.test.js +0 -169
  98. package/commands/databases.test.js +0 -39
  99. package/commands/help.test.js +0 -9
  100. package/commands/init.test.js +0 -56
  101. package/commands/ip_allow.test.js +0 -59
  102. package/commands/link.test.js +0 -381
  103. package/commands/operations.test.js +0 -7
  104. package/commands/orgs.test.js +0 -7
  105. package/commands/projects.test.js +0 -144
  106. package/commands/psql.test.js +0 -49
  107. package/commands/roles.test.js +0 -37
  108. package/commands/set_context.test.js +0 -159
  109. package/commands/vpc_endpoints.test.js +0 -69
  110. package/context.test.js +0 -119
  111. package/env.test.js +0 -55
  112. package/utils/formats.test.js +0 -32
  113. package/writer.test.js +0 -104
@@ -0,0 +1,116 @@
1
+ /**
2
+ * Connection-driven psql variable sync.
3
+ *
4
+ * TypeScript port of `SyncVariables()` in upstream PostgreSQL's
5
+ * `src/bin/psql/command.c`. After every successful connection (the initial
6
+ * connect in `runPsql` and each `\c` reconnect), psql refreshes the set of
7
+ * read-only "connection" variables that scripts interpolate with `:DBNAME`,
8
+ * `:USER`, etc. Without this the names interpolate to their literal text
9
+ * (`:USER` → `:USER`) instead of the live connection facts.
10
+ *
11
+ * The variables synced here mirror upstream's `SyncVariables()`:
12
+ *
13
+ * - `DBNAME` — connection database
14
+ * - `USER` — connection user
15
+ * - `HOST` — host the client connected to (a socket directory
16
+ * for a Unix-domain socket — starts with `/`)
17
+ * - `PORT` — connection port (stringified)
18
+ * - `ENCODING` — client encoding name (the `client_encoding`
19
+ * ParameterStatus). The mainloop keeps this in
20
+ * sync on later `SET client_encoding`; we seed it
21
+ * here so it is correct from the first prompt.
22
+ * - `SERVER_VERSION_NAME`— the `server_version` ParameterStatus (e.g. `18.4`)
23
+ * - `SERVER_VERSION_NUM` — the numeric server version (e.g. `180004`),
24
+ * stringified from `Connection.serverVersion`
25
+ *
26
+ * The CLIENT version variables (`VERSION` / `VERSION_NAME` / `VERSION_NUM`)
27
+ * are constant for the life of the process and set once at startup — see
28
+ * {@link setStartupVars}.
29
+ */
30
+ const asString = (value) => typeof value === 'string' ? value : undefined;
31
+ const asPort = (value) => typeof value === 'number' && Number.isFinite(value)
32
+ ? String(value)
33
+ : undefined;
34
+ /**
35
+ * Refresh the connection variables (`DBNAME`, `USER`, `HOST`, `PORT`,
36
+ * `ENCODING`, `SERVER_VERSION_NAME`, `SERVER_VERSION_NUM`) from the live
37
+ * connection. Mirrors upstream `SyncVariables()`. Call after a successful
38
+ * initial connect and after each `\c` reconnect.
39
+ *
40
+ * A variable is only set when the connection actually surfaces a value; this
41
+ * leaves any user-set value untouched if the connection cannot report one.
42
+ */
43
+ export const syncConnectionVars = (vars, conn) => {
44
+ const target = conn;
45
+ const database = asString(target.database);
46
+ if (database !== undefined)
47
+ vars.set('DBNAME', database);
48
+ const user = asString(target.user);
49
+ if (user !== undefined)
50
+ vars.set('USER', user);
51
+ const host = asString(target.host);
52
+ if (host !== undefined)
53
+ vars.set('HOST', host);
54
+ const port = asPort(target.port);
55
+ if (port !== undefined)
56
+ vars.set('PORT', port);
57
+ // ENCODING tracks the server's `client_encoding` ParameterStatus. The
58
+ // mainloop refreshes it after each `SET client_encoding`; seed it here so
59
+ // it is populated from the very first prompt (and re-seeded after `\c`).
60
+ const encoding = conn.parameterStatus('client_encoding');
61
+ if (encoding !== undefined)
62
+ vars.set('ENCODING', encoding);
63
+ const serverVersionName = conn.parameterStatus('server_version');
64
+ if (serverVersionName !== undefined) {
65
+ vars.set('SERVER_VERSION_NAME', serverVersionName);
66
+ }
67
+ // `Connection.serverVersion` is the libpq-style integer (e.g. 180004 for
68
+ // 18.4); 0 means "not yet reported" — skip it so we don't write a bogus 0.
69
+ if (conn.serverVersion > 0) {
70
+ vars.set('SERVER_VERSION_NUM', String(conn.serverVersion));
71
+ }
72
+ };
73
+ /**
74
+ * Set the constant CLIENT version variables once at startup, mirroring how
75
+ * upstream psql seeds `VERSION` / `VERSION_NAME` / `VERSION_NUM` from its
76
+ * compiled-in `PG_VERSION` / `PG_VERSION_NUM`:
77
+ *
78
+ * - `VERSION` — the full banner string (upstream: `PostgreSQL <ver> …`).
79
+ * - `VERSION_NAME` — the bare version number (upstream: e.g. `18.4`).
80
+ * - `VERSION_NUM` — the numeric version (upstream: e.g. `180004`).
81
+ *
82
+ * The embedded psql is not a real PostgreSQL build, so there is no
83
+ * `PG_VERSION` to read. We derive the values from neonctl's own package
84
+ * version (passed in as {@link clientVersion}, e.g. `2.22.0`) so the client
85
+ * identifier is real and traceable to the shipped binary, while keeping
86
+ * upstream's variable *shapes*:
87
+ *
88
+ * - `VERSION` → `psql-ts (neonctl) <clientVersion>` — a banner that
89
+ * names the implementation so users can tell they are on the embedded
90
+ * TS port, mirroring the startup banner's `psql-ts (neonctl, …)` shape.
91
+ * - `VERSION_NAME` → `<clientVersion>` (e.g. `2.22.0`).
92
+ * - `VERSION_NUM` → the same version mapped into PG's NNMMPP integer form
93
+ * (`2.22.0` → `22200`) via {@link clientVersionNum}, so a script doing a
94
+ * numeric `:VERSION_NUM` comparison gets a monotonic integer.
95
+ */
96
+ export const setStartupVars = (vars, clientVersion) => {
97
+ vars.set('VERSION', `psql-ts (neonctl) ${clientVersion}`);
98
+ vars.set('VERSION_NAME', clientVersion);
99
+ vars.set('VERSION_NUM', String(clientVersionNum(clientVersion)));
100
+ };
101
+ /**
102
+ * Map a `MAJOR.MINOR.PATCH` semver-style version string into PG's
103
+ * `PG_VERSION_NUM` integer layout (`MAJOR * 10000 + MINOR * 100 + PATCH`,
104
+ * e.g. `2.22.0` → `22200`). Missing components default to 0; a non-numeric
105
+ * leading component yields `0`. Kept deliberately tolerant — the value only
106
+ * needs to be a monotonic integer for `:VERSION_NUM` comparisons.
107
+ */
108
+ export const clientVersionNum = (version) => {
109
+ const m = /^(\d+)(?:\.(\d+))?(?:\.(\d+))?/.exec(version.trim());
110
+ if (!m)
111
+ return 0;
112
+ const major = parseInt(m[1], 10);
113
+ const minor = m[2] !== undefined ? parseInt(m[2], 10) : 0;
114
+ const patch = m[3] !== undefined ? parseInt(m[3], 10) : 0;
115
+ return major * 10000 + minor * 100 + patch;
116
+ };
@@ -0,0 +1,287 @@
1
+ /**
2
+ * psql Variables store.
3
+ *
4
+ * TypeScript port of PostgreSQL's `src/bin/psql/variables.c`. Models a psql
5
+ * "variable space" — a name → string-value mapping with per-variable
6
+ * notification hooks and the parsing helpers psql uses to coerce values to
7
+ * booleans, tri-state ("on"/"off"/"auto") values, and integers.
8
+ *
9
+ * Deviations from upstream that are intentional:
10
+ *
11
+ * - Storage is a `Map<string, string>` rather than a doubly-linked list.
12
+ * Insertion order is preserved by Map (psql sorts alphabetically purely for
13
+ * pretty-printing in `\set`; that responsibility belongs to a future
14
+ * printer/help WP, not the store).
15
+ * - Multiple hooks per name are allowed (upstream has at most one substitute
16
+ * + one assign hook per variable). All hooks must return `true` for a
17
+ * `set()` to be accepted; if any vetoes, the value is left unchanged.
18
+ * - On `addHook()` we synchronously replay the current value (or `null` if
19
+ * unset) through the new hook. This matches the upstream behaviour where
20
+ * `SetVariableHooks` fires the substitute and assign hooks immediately so
21
+ * derived psql state can sync.
22
+ * - On `unset()` registered hooks are notified with `null` and remain
23
+ * registered. The value is removed from the map (so `has()` returns
24
+ * `false`), but a later `set()` will still consult the hooks.
25
+ * - Variable names are validated against `[A-Za-z_][A-Za-z0-9_]*` per the
26
+ * WP-06 spec. Upstream additionally accepts non-ASCII bytes and a leading
27
+ * digit; we deliberately tighten the rule for the TS port.
28
+ */
29
+ const VALID_NAME_RE = /^[A-Za-z_][A-Za-z0-9_]*$/;
30
+ /** Case-insensitive prefix match: does `value` start with `prefix`? */
31
+ const isPrefixOf = (value, prefix) => value.length > 0 &&
32
+ value.length <= prefix.length &&
33
+ prefix.slice(0, value.length).toLowerCase() === value.toLowerCase();
34
+ /**
35
+ * Parse a string the way psql's `ParseVariableBool` does.
36
+ *
37
+ * Recognised tokens (case-insensitive, with unique-prefix matching for the
38
+ * word forms): `true`, `false`, `yes`, `no`, `on`, `off`, `1`, `0`. For `on`
39
+ * and `off` we require at least two characters of input — a bare `o` is
40
+ * ambiguous and upstream rejects it.
41
+ *
42
+ * Returns the parsed boolean, or `null` if the string is not recognised.
43
+ */
44
+ const parseBool = (value) => {
45
+ if (value.length === 0)
46
+ return null;
47
+ if (isPrefixOf(value, 'true'))
48
+ return true;
49
+ if (isPrefixOf(value, 'false'))
50
+ return false;
51
+ if (isPrefixOf(value, 'yes'))
52
+ return true;
53
+ if (isPrefixOf(value, 'no'))
54
+ return false;
55
+ // 'on'/'off' need at least 2 chars; 'o' alone is ambiguous.
56
+ if (value.length >= 2) {
57
+ const lower = value.toLowerCase();
58
+ if ('on'.startsWith(lower))
59
+ return true;
60
+ if ('off'.startsWith(lower))
61
+ return false;
62
+ }
63
+ if (value === '1')
64
+ return true;
65
+ if (value === '0')
66
+ return false;
67
+ // WP-06 extension: any other strtol-parsable integer is truthy if non-zero,
68
+ // falsy if zero. Upstream `ParseVariableBool` rejects "42" outright; we
69
+ // accept it so callers don't need a separate code path for numeric flags.
70
+ const asNum = parseInt32(value);
71
+ if (asNum !== null)
72
+ return asNum !== 0;
73
+ return null;
74
+ };
75
+ /**
76
+ * Parse a string as an integer the way psql's `ParseVariableNum` does
77
+ * (base 0, i.e. `0x` and leading-zero octal forms are accepted), and clamp
78
+ * to the 32-bit signed range that psql uses (the C cast `numval == (int)
79
+ * numval` check).
80
+ *
81
+ * Returns the integer, or `null` on syntax / range failure.
82
+ */
83
+ const parseInt32 = (value) => {
84
+ if (value.length === 0)
85
+ return null;
86
+ const trimmed = value.trim();
87
+ if (trimmed.length === 0)
88
+ return null;
89
+ // Match the prefixes strtol(_, _, 0) accepts and pick the matching radix
90
+ // explicitly so we can validate the entire string (Number()/parseInt with
91
+ // base 0 are not portable enough for this).
92
+ let body = trimmed;
93
+ let sign = 1;
94
+ if (body.startsWith('+')) {
95
+ body = body.slice(1);
96
+ }
97
+ else if (body.startsWith('-')) {
98
+ sign = -1;
99
+ body = body.slice(1);
100
+ }
101
+ if (body.length === 0)
102
+ return null;
103
+ let radix = 10;
104
+ if (body.startsWith('0x') || body.startsWith('0X')) {
105
+ radix = 16;
106
+ body = body.slice(2);
107
+ }
108
+ else if (body.startsWith('0o') || body.startsWith('0O')) {
109
+ radix = 8;
110
+ body = body.slice(2);
111
+ }
112
+ else if (body.length > 1 && body.startsWith('0')) {
113
+ // C strtol with base 0 treats a leading 0 as octal. JS users typically
114
+ // expect decimal; we follow upstream for behavioural fidelity.
115
+ radix = 8;
116
+ body = body.slice(1);
117
+ }
118
+ if (body.length === 0)
119
+ return null;
120
+ const digitRe = radix === 16 ? /^[0-9a-fA-F]+$/ : radix === 8 ? /^[0-7]+$/ : /^[0-9]+$/;
121
+ if (!digitRe.test(body))
122
+ return null;
123
+ const parsed = sign * parseInt(body, radix);
124
+ if (!Number.isFinite(parsed))
125
+ return null;
126
+ // Match `numval == (int) numval` — 32-bit signed range.
127
+ if (parsed < -0x80000000 || parsed > 0x7fffffff)
128
+ return null;
129
+ return parsed;
130
+ };
131
+ export class VarStore {
132
+ constructor() {
133
+ this.values = new Map();
134
+ this.hooks = new Map();
135
+ }
136
+ set(name, value) {
137
+ return this.trySet(name, value).ok;
138
+ }
139
+ trySet(name, value) {
140
+ if (!VALID_NAME_RE.test(name)) {
141
+ return { ok: false, reason: 'invalid-name' };
142
+ }
143
+ const hooks = this.hooks.get(name);
144
+ let toStore = value;
145
+ if (hooks) {
146
+ // All hooks must accept the value. Each hook can either:
147
+ // - return `true` to accept as-is,
148
+ // - return `false` to reject silently (the prior value is kept),
149
+ // - return a `string` to reject with that error message
150
+ // (cmdSet renders it with the `psql: ` prefix), or
151
+ // - return `{ substitute: '<value>' }` to rewrite the stored value
152
+ // before subsequent hooks see it.
153
+ //
154
+ // The substitute return is the collapsed equivalent of upstream's
155
+ // separate substitute/assign hook pair (see
156
+ // `bool_substitute_hook` + `bool_assign_hook` in `command.c`).
157
+ // Hooks are responsible for ensuring their substituted value passes
158
+ // their own validation — we do NOT re-run a hook against its own
159
+ // substitution.
160
+ for (const hook of hooks) {
161
+ const result = hook(toStore);
162
+ if (result === false) {
163
+ return { ok: false, reason: 'hook-veto' };
164
+ }
165
+ if (typeof result === 'string') {
166
+ return { ok: false, reason: 'hook-veto', error: result };
167
+ }
168
+ if (typeof result === 'object' && result !== null) {
169
+ toStore = result.substitute;
170
+ }
171
+ }
172
+ }
173
+ this.values.set(name, toStore);
174
+ return { ok: true };
175
+ }
176
+ get(name) {
177
+ return this.values.get(name);
178
+ }
179
+ unset(name) {
180
+ const had = this.values.delete(name);
181
+ const hooks = this.hooks.get(name);
182
+ if (hooks) {
183
+ // Notify hooks of deletion so they can clear derived state.
184
+ // Upstream substitute hooks (e.g. `on_error_rollback_substitute_hook`,
185
+ // `bool_substitute_hook`) re-inject a default when `newval == NULL` —
186
+ // so `\unset ON_ERROR_ROLLBACK` actually re-stores "off",
187
+ // `\unset AUTOCOMMIT` re-stores "on", etc. Honor the substitute by
188
+ // re-storing the value the hook returns. Plain `true` / `false` /
189
+ // error-string returns mean "no substitute" and the slot stays empty.
190
+ let substituted = null;
191
+ for (const hook of hooks) {
192
+ const r = hook(null);
193
+ if (typeof r === 'object' && r !== null && 'substitute' in r) {
194
+ substituted = r.substitute;
195
+ }
196
+ }
197
+ if (substituted !== null) {
198
+ this.values.set(name, substituted);
199
+ // Re-notify hooks with the substituted value so derived state
200
+ // (settings.onErrorRollback, etc.) gets the correct default.
201
+ for (const hook of hooks)
202
+ hook(substituted);
203
+ }
204
+ }
205
+ return had;
206
+ }
207
+ has(name) {
208
+ return this.values.has(name);
209
+ }
210
+ hasSubstituteHook(name) {
211
+ const hooks = this.hooks.get(name);
212
+ return hooks !== undefined && hooks.length > 0;
213
+ }
214
+ addHook(name, hook) {
215
+ if (!VALID_NAME_RE.test(name))
216
+ return;
217
+ const existing = this.hooks.get(name);
218
+ if (existing) {
219
+ existing.push(hook);
220
+ }
221
+ else {
222
+ this.hooks.set(name, [hook]);
223
+ }
224
+ // Replay the current value so the hook can sync immediately, matching
225
+ // upstream `SetVariableHooks`:
226
+ // if (shook) current->value = (*shook)(current->value);
227
+ // if (ahook) (void) (*ahook)(current->value);
228
+ // Our hooks combine substitute + assign in a single callback. If the
229
+ // initial replay returns a `{substitute}` (the bool_substitute_hook /
230
+ // verbosity_substitute_hook etc. pattern), persist that and re-run the
231
+ // hook so derived state (settings.verbosity, settings.echo, ...) syncs
232
+ // to the substituted value. This is how upstream seeds defaults like
233
+ // `ON_ERROR_STOP=off`, `VERBOSITY=default`, `QUIET=off` etc. simply
234
+ // from `SetVariableHooks` — no explicit `SetVariable("…", "off")` call
235
+ // is needed for variables whose default matches the unset substitute.
236
+ const current = this.values.get(name);
237
+ const result = hook(current ?? null);
238
+ if (typeof result === 'object' &&
239
+ result !== null &&
240
+ 'substitute' in result) {
241
+ this.values.set(name, result.substitute);
242
+ hook(result.substitute);
243
+ }
244
+ }
245
+ entries() {
246
+ return this.values.entries();
247
+ }
248
+ asBool(name, defaultValue = false) {
249
+ const value = this.values.get(name);
250
+ if (value === undefined)
251
+ return defaultValue;
252
+ const parsed = parseBool(value);
253
+ return parsed ?? defaultValue;
254
+ }
255
+ asTriple(name, defaultValue) {
256
+ const value = this.values.get(name);
257
+ if (value === undefined)
258
+ return defaultValue;
259
+ // "auto" is matched first as a unique prefix, so "a", "au", "aut",
260
+ // "auto" all map to 'auto'. psql's actual call site does an
261
+ // `pg_strncasecmp(value, "auto", len)` before falling through to
262
+ // ParseVariableBool — we do the same.
263
+ if (isPrefixOf(value, 'auto'))
264
+ return 'auto';
265
+ const parsed = parseBool(value);
266
+ if (parsed === null) {
267
+ return {
268
+ error: `unrecognized value "${value}" for "${name}": Boolean expected`,
269
+ };
270
+ }
271
+ return parsed ? 'on' : 'off';
272
+ }
273
+ asInt(name, defaultValue = 0) {
274
+ const value = this.values.get(name);
275
+ if (value === undefined)
276
+ return defaultValue;
277
+ const parsed = parseInt32(value);
278
+ if (parsed === null) {
279
+ return {
280
+ error: `invalid value "${value}" for "${name}": integer expected`,
281
+ };
282
+ }
283
+ return parsed;
284
+ }
285
+ }
286
+ /** Factory mirroring the upstream `CreateVariableSpace()` entry point. */
287
+ export const createVarStore = () => new VarStore();