@iann29/synapse 1.7.0 → 1.8.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.
package/bin/synapse.js CHANGED
@@ -1,566 +1,98 @@
1
1
  #!/usr/bin/env node
2
2
 
3
- const { SynapseAPI, SynapseAPIError } = require("../lib/api");
4
- const { clearConfig, normalizeBaseUrl, requireConfig, writeConfig } = require("../lib/config");
5
- const { quoteEnvValue, writeProjectEnv } = require("../lib/env-file");
6
- const {
7
- buildProjectConfig,
8
- deploymentNameForTarget,
9
- readProjectConfig,
10
- writeProjectConfig,
11
- } = require("../lib/project");
12
- const { BACK, askCredentials, choose, confirm } = require("../lib/prompts");
13
- const colors = require("../lib/colors");
14
- const { runConvex } = require("../lib/convex");
15
-
16
- function debugLog(msg) {
17
- if (process.env.DEBUG_SYNAPSE) {
18
- process.stderr.write(`[DEBUG] ${msg}\n`);
19
- }
20
- }
21
-
22
- function usage() {
23
- return `Usage:
24
- synapse login <url>
25
- synapse logout
26
- synapse whoami
27
- synapse select
28
- synapse dev [...args] Run \`convex dev\` against the linked dev deployment.
29
- synapse deploy [--yes] [...args] Run \`convex deploy\` against the linked prod deployment (asks for confirmation).
30
- synapse credentials <deployment> [--format env|shell|json]
31
- synapse convex [--target dev|prod] [...args] Escape hatch for any other \`convex\` subcommand.
32
-
33
- Tip: \`synapse select\` writes the deployment credentials to .env.local, so you can also
34
- run \`npx convex <args>\` directly without going through this wrapper.
35
- `;
36
- }
37
-
38
- function clientFromConfig() {
39
- const cfg = requireConfig();
40
- const api = new SynapseAPI({ baseUrl: cfg.baseUrl, accessToken: cfg.accessToken });
41
- const refreshable = new Proxy(api, {
42
- get(target, prop) {
43
- const value = target[prop];
44
- if (typeof value !== "function") {
45
- return value;
46
- }
47
- return async (...args) => {
48
- try {
49
- return await value.apply(target, args);
50
- } catch (err) {
51
- if (!(err instanceof SynapseAPIError) || err.status !== 401 || !cfg.refreshToken) {
52
- throw err;
53
- }
54
- const session = await new SynapseAPI({ baseUrl: cfg.baseUrl }).refresh(cfg.refreshToken);
55
- if (!session.accessToken) {
56
- throw err;
57
- }
58
- cfg.accessToken = session.accessToken;
59
- cfg.refreshToken = session.refreshToken || cfg.refreshToken;
60
- cfg.tokenType = session.tokenType || cfg.tokenType || "Bearer";
61
- if (session.user) {
62
- cfg.user = session.user;
63
- }
64
- writeConfig(cfg);
65
- target.accessToken = cfg.accessToken;
66
- return await value.apply(target, args);
67
- }
68
- };
69
- },
70
- });
71
- return {
72
- cfg,
73
- api: refreshable,
74
- };
75
- }
76
-
77
- function labelName(item) {
78
- const name = item.name || item.slug || item.id;
79
- const slug = item.slug && item.slug !== name ? ` (${item.slug})` : "";
80
- return `${name}${slug}`;
81
- }
82
-
83
- function teamRef(team) {
84
- return team.slug || team.id;
85
- }
86
-
87
- function deploymentLabel(deployment) {
88
- const bits = [colors.bold(deployment.name)];
89
- const type = deployment.deploymentType || deployment.type;
90
- if (type) {
91
- bits.push(colors.dim(type));
92
- }
93
- if (deployment.status) {
94
- bits.push(colors.statusBadge(deployment.status));
95
- }
96
- return bits.filter(Boolean).join(" - ");
97
- }
98
-
99
- function deploymentType(deployment) {
100
- return deployment.deploymentType || deployment.type || "";
101
- }
102
-
103
- function sortDeploymentsForChoice(deployments) {
104
- return [...deployments].sort((a, b) => {
105
- if (!!a.isDefault !== !!b.isDefault) {
106
- return a.isDefault ? -1 : 1;
107
- }
108
- return String(b.createTime || b.createdAt || "").localeCompare(String(a.createTime || a.createdAt || ""));
109
- });
110
- }
111
-
112
- async function chooseDeploymentForType(type, deployments, chooseOpts = {}) {
113
- const matches = sortDeploymentsForChoice(
114
- deployments.filter((d) => deploymentType(d) === type && d.status !== "deleted"),
115
- );
116
- debugLog(
117
- `chooseDeploymentForType(${type}): matched ${matches.length} of ${deployments.length} ` +
118
- `(types: ${deployments.map((d) => deploymentType(d) || "?").join(",")})`,
119
- );
120
- if (matches.length === 0) {
121
- return null;
122
- }
123
- return await choose(
124
- `${type} deployments`,
125
- matches.map((d) => ({ label: deploymentLabel(d), value: d })),
126
- { singularLabel: `${type} deployment`, ...chooseOpts },
127
- );
128
- }
129
-
130
- function parseConvexTarget(args) {
131
- let target = null;
132
- let index = 0;
133
- while (index < args.length) {
134
- const arg = args[index];
135
- if (arg === "--target") {
136
- target = args[index + 1];
137
- if (!target) {
138
- throw new Error("--target requires dev or prod");
139
- }
140
- index += 2;
141
- continue;
142
- }
143
- if (arg && arg.startsWith("--target=")) {
144
- target = arg.slice("--target=".length);
145
- index += 1;
146
- continue;
147
- }
148
- break;
149
- }
150
- if (target && target !== "dev" && target !== "prod") {
151
- throw new Error("--target must be dev or prod");
152
- }
153
- return {
154
- explicitTarget: Boolean(target),
155
- target,
156
- args: args.slice(index),
157
- };
158
- }
159
-
160
- function inferConvexTarget(args) {
161
- const command = args.find((arg) => arg && !arg.startsWith("-")) || "";
162
- return command === "deploy" ? "prod" : "dev";
163
- }
164
-
165
- function parseConvexInvocation(args) {
166
- const parsed = parseConvexTarget(args);
167
- return {
168
- ...parsed,
169
- target: parsed.target || inferConvexTarget(parsed.args),
170
- };
171
- }
172
-
173
- async function resolveConvexInvocation(args, { cfg = null, api = null, projectDir = process.cwd() } = {}) {
174
- const parsed = parseConvexInvocation(args);
175
- const projectConfig = readProjectConfig(projectDir);
176
- if (!projectConfig) {
177
- if (parsed.explicitTarget) {
178
- throw new Error("No Synapse project metadata found. Run `synapse select` first.");
179
- }
180
- return {
181
- ...parsed,
182
- credentials: null,
183
- deploymentName: "",
184
- projectConfig: null,
185
- target: null,
186
- };
187
- }
188
-
189
- if (!cfg || !api) {
190
- throw new Error("Not logged in. Run `synapse login <url>` first.");
191
- }
192
- if (
193
- projectConfig.synapseUrl &&
194
- cfg.baseUrl &&
195
- normalizeBaseUrl(projectConfig.synapseUrl) !== normalizeBaseUrl(cfg.baseUrl)
196
- ) {
197
- throw new Error(
198
- `This project is linked to ${projectConfig.synapseUrl}, but the saved Synapse session is for ${cfg.baseUrl}. Run \`synapse login ${projectConfig.synapseUrl}\` or \`synapse select\` again.`,
199
- );
200
- }
201
-
202
- const deploymentName = deploymentNameForTarget(projectConfig, parsed.target);
203
- if (!deploymentName) {
204
- throw new Error(`No ${parsed.target} deployment saved for this project. Run \`synapse select\` again.`);
205
- }
206
- const credentials = await api.cliCredentials(deploymentName);
207
- return {
208
- ...parsed,
209
- credentials,
210
- deploymentName,
211
- projectConfig,
212
- };
213
- }
214
-
215
- function formatCredentials(creds, format) {
216
- switch (format) {
217
- case "json":
218
- return JSON.stringify(creds, null, 2);
219
- case "shell":
220
- return creds.exportSnippet;
221
- case "env":
222
- return creds.envSnippet || `CONVEX_SELF_HOSTED_URL=${quoteEnvValue(creds.convexUrl)}\nCONVEX_SELF_HOSTED_ADMIN_KEY=${quoteEnvValue(creds.adminKey)}`;
223
- default:
224
- throw new Error("format must be one of: env, shell, json");
225
- }
226
- }
227
-
228
- function parseFormat(args) {
229
- let format = "env";
230
- const rest = [];
231
- for (let i = 0; i < args.length; i += 1) {
232
- const arg = args[i];
233
- if (arg === "--format") {
234
- format = args[i + 1];
235
- i += 1;
236
- } else if (arg.startsWith("--format=")) {
237
- format = arg.slice("--format=".length);
238
- } else {
239
- rest.push(arg);
240
- }
241
- }
242
- return { format, rest };
243
- }
244
-
245
- async function login(args) {
246
- const url = args[0];
247
- if (!url) {
248
- throw new Error("Usage: synapse login <url>");
249
- }
250
- const baseUrl = normalizeBaseUrl(url);
251
- const { email, password } = await askCredentials();
252
- const api = new SynapseAPI({ baseUrl });
253
- const session = await api.login(email, password);
254
- if (!session.accessToken) {
255
- throw new Error("Synapse login response did not include accessToken");
256
- }
257
- const file = writeConfig({
258
- baseUrl,
259
- accessToken: session.accessToken,
260
- refreshToken: session.refreshToken || null,
261
- tokenType: session.tokenType || "Bearer",
262
- user: session.user || null,
263
- });
264
- process.stderr.write(`Saved Synapse session to ${file}\n`);
265
- }
266
-
267
- async function logout() {
268
- const removed = clearConfig();
269
- process.stderr.write(removed ? "Logged out of Synapse.\n" : "No Synapse session was saved.\n");
270
- }
271
-
272
- async function whoami() {
273
- const { cfg, api } = clientFromConfig();
274
- const me = await api.me();
275
- const email = me.email || me.user?.email || "(unknown email)";
276
- const name = me.name || me.user?.name || "";
277
- process.stdout.write(`${name ? `${name} ` : ""}<${email}> on ${cfg.baseUrl}\n`);
278
- }
279
-
280
- // selectDeployment walks the operator through team → project → dev → prod
281
- // pickers, then writes .synapse/project.json + .env.local. Implemented as a
282
- // small state machine so the user can type `b` / `back` at any step to
283
- // re-choose the previous selection without restarting the whole CLI.
3
+ // Thin dispatcher. Every command's logic lives in lib/commands/*.js;
4
+ // this file's only jobs are:
5
+ // 1. Parse argv into (cmd, rest) via the two-then-one registry.
6
+ // 2. Short-circuit --help / help.
7
+ // 3. Construct the runtime ctx (output layer, lazy session+API).
8
+ // 4. Catch top-level errors and emit a consistent stderr message.
284
9
  //
285
- // Network results are cached per (team, project) so back-navigation stays
286
- // snappy and doesn't burn pagination roundtrips. `DEBUG_SYNAPSE=1` dumps
287
- // the raw lists at each step useful when an expected deployment is
288
- // missing from the menu.
289
- async function selectDeployment() {
290
- const { cfg, api } = clientFromConfig();
291
-
292
- const cache = {
293
- teamsList: null,
294
- projectsByTeamKey: new Map(),
295
- deploymentsByProjectId: new Map(),
296
- };
297
- async function fetchTeams() {
298
- if (!cache.teamsList) {
299
- cache.teamsList = await api.teams();
300
- debugLog(`teams loaded: ${cache.teamsList.length}`);
301
- }
302
- return cache.teamsList;
303
- }
304
- async function fetchProjects(team) {
305
- const key = team.id || team.slug || team.name;
306
- if (!cache.projectsByTeamKey.has(key)) {
307
- const projects = await api.projects(teamRef(team));
308
- cache.projectsByTeamKey.set(key, projects);
309
- debugLog(`projects for team ${key}: ${projects.length}`);
310
- }
311
- return cache.projectsByTeamKey.get(key);
312
- }
313
- async function fetchDeployments(project) {
314
- if (!cache.deploymentsByProjectId.has(project.id)) {
315
- const deployments = await api.deployments(project.id);
316
- cache.deploymentsByProjectId.set(project.id, deployments);
317
- debugLog(`deployments for project ${project.id}: ${deployments.length}`);
318
- }
319
- return cache.deploymentsByProjectId.get(project.id);
320
- }
10
+ // Legacy named exports at the bottom are kept ONLY for backwards-
11
+ // compatibility with test/bin.test.js production code paths never
12
+ // need to require this file as a library.
321
13
 
322
- let team = null;
323
- let project = null;
324
- let dev = null;
325
- let prod = null;
326
- let step = "team";
327
- while (step !== "done") {
328
- if (step === "team") {
329
- const teams = await fetchTeams();
330
- // Back from team would be "exit" — not useful at the top of the flow.
331
- const picked = await choose(
332
- "teams",
333
- teams.map((t) => ({ label: labelName(t), value: t })),
334
- { singularLabel: "team", allowBack: false },
335
- );
336
- team = picked;
337
- step = "project";
338
- } else if (step === "project") {
339
- const projects = await fetchProjects(team);
340
- const picked = await choose(
341
- "projects",
342
- projects.map((p) => ({ label: labelName(p), value: p })),
343
- { singularLabel: "project", allowBack: true },
344
- );
345
- if (picked === BACK) { step = "team"; continue; }
346
- project = picked;
347
- step = "dev";
348
- } else if (step === "dev") {
349
- const deployments = await fetchDeployments(project);
350
- const picked = await chooseDeploymentForType("dev", deployments, { allowBack: true });
351
- if (picked === BACK) { step = "project"; continue; }
352
- if (picked === null) {
353
- throw new Error(
354
- "No dev deployments available in this project. Create one first in the dashboard.",
355
- );
356
- }
357
- dev = picked;
358
- step = "prod";
359
- } else if (step === "prod") {
360
- const deployments = await fetchDeployments(project);
361
- const picked = await chooseDeploymentForType("prod", deployments, { allowBack: true });
362
- if (picked === BACK) { step = "dev"; continue; }
363
- prod = picked; // null is a valid outcome here (project has no prod yet)
364
- step = "done";
365
- }
366
- }
14
+ const { buildRegistry, resolve, wantsHelp } = require("../lib/commands/_dispatcher");
15
+ const { renderRootHelp, renderCommandHelp } = require("../lib/commands/_help");
16
+ const { createContext } = require("../lib/commands/_context");
17
+ const { createOutput, extractJsonFlag } = require("../lib/output");
18
+ const { SynapseAPIError } = require("../lib/api");
367
19
 
368
- const projectPath = writeProjectConfig(
369
- process.cwd(),
370
- buildProjectConfig({
371
- synapseUrl: cfg.baseUrl,
372
- team,
373
- project,
374
- deployments: { dev, prod },
375
- }),
376
- );
377
- const creds = await api.cliCredentials(dev.name);
378
- const envPath = writeProjectEnv(process.cwd(), creds);
379
-
380
- process.stderr.write(`\nLinked ${labelName(project)} to ${projectPath}.\n`);
381
- process.stderr.write(`Selected dev deployment ${colors.bold(dev.name)}. Updated ${envPath}.\n`);
382
- if (prod) {
383
- process.stderr.write(`Selected prod deployment ${colors.bold(prod.name)}.\n`);
384
- } else {
385
- process.stderr.write(
386
- `\n${colors.yellow("Warning:")} no prod deployment found. ` +
387
- "`synapse deploy` (and `synapse convex deploy`) will fail with a clear " +
388
- "error until you create a prod deployment and run `synapse select` again.\n",
389
- );
390
- }
391
- if (process.env.CONVEX_DEPLOYMENT) {
392
- process.stderr.write(
393
- `\n${colors.yellow("Warning:")} shell CONVEX_DEPLOYMENT is set. ` +
394
- "Use `synapse dev` / `synapse deploy` / `synapse convex ...` " +
395
- "or unset CONVEX_DEPLOYMENT before running `npx convex` directly.\n",
396
- );
397
- }
398
- // Discoverability hint (P3-012). The upstream Convex CLI's `dev` command
399
- // is what pushes the project's schema/functions and starts a dev server;
400
- // many operators land here from frameworks (Next/Vite) without knowing
401
- // that, then hit "page hangs forever" the first time their client tries
402
- // to query a backend that has no code deployed yet. Spell it out.
403
- process.stderr.write(
404
- `\nNext step: run ${colors.bold("synapse dev")} (or ${colors.bold("npx convex dev")}) once in this directory ` +
405
- "to push your schema and watch for changes.\n",
406
- );
407
- }
20
+ const REGISTRY = buildRegistry();
408
21
 
409
- async function credentials(args) {
410
- const { format, rest } = parseFormat(args);
411
- const deployment = rest[0];
412
- if (!deployment) {
413
- throw new Error("Usage: synapse credentials <deployment> [--format env|shell|json]");
414
- }
415
- if (!["env", "shell", "json"].includes(format)) {
416
- throw new Error("format must be one of: env, shell, json");
417
- }
418
- const { api } = clientFromConfig();
419
- const creds = await api.cliCredentials(deployment);
420
- process.stdout.write(formatCredentials(creds, format) + "\n");
421
- }
22
+ async function main(argv) {
23
+ // Strip --json from any position so commands see clean positionals.
24
+ const { json, rest: cleanArgv } = extractJsonFlag(argv);
422
25
 
423
- async function convex(args) {
424
- const projectConfig = readProjectConfig(process.cwd());
425
- let resolved = {
426
- args,
427
- credentials: null,
428
- deploymentName: "",
429
- target: null,
430
- };
431
- if (projectConfig) {
432
- const { cfg, api } = clientFromConfig();
433
- resolved = await resolveConvexInvocation(args, { cfg, api });
434
- process.stderr.write(`Using Synapse ${resolved.target} deployment ${resolved.deploymentName}.\n`);
435
- } else {
436
- resolved = await resolveConvexInvocation(args);
26
+ // help / no-args → root help.
27
+ if (cleanArgv.length === 0 || cleanArgv[0] === "help" || cleanArgv[0] === "-h" || cleanArgv[0] === "--help") {
28
+ return renderRootHelp(REGISTRY);
437
29
  }
438
- const code = await runConvex(resolved.args, { credentials: resolved.credentials });
439
- process.exitCode = code;
440
- }
441
30
 
442
- // extractYesFlag pulls --yes / -y out of an arg vector so the rest can be
443
- // passed verbatim to the underlying `convex` invocation. We strip only
444
- // these synapse-level flags; everything else is forwarded.
445
- function extractYesFlag(args) {
446
- let yes = false;
447
- const rest = [];
448
- for (const arg of args) {
449
- if (arg === "--yes" || arg === "-y") {
450
- yes = true;
451
- } else {
452
- rest.push(arg);
453
- }
31
+ const { cmd, rest } = resolve(REGISTRY, cleanArgv);
32
+ if (!cmd) {
33
+ process.stderr.write(`Unknown command: ${cleanArgv.join(" ")}\n\nRun \`synapse help\` for the full list.\n`);
34
+ process.exitCode = 1;
35
+ return;
454
36
  }
455
- return { yes, rest };
456
- }
457
-
458
- // dev is a convenience for `synapse convex --target dev dev`. We delegate
459
- // to the existing convex pipeline so target resolution, credential fetching,
460
- // and env-var sanitization stay in one place.
461
- //
462
- // The `convexImpl` seam exists so unit tests can short-circuit before
463
- // runConvex actually spawns `npx`. Production wiring uses the local
464
- // `convex` function above unchanged.
465
- async function dev(args, { convexImpl = convex } = {}) {
466
- return await convexImpl(["--target", "dev", "dev", ...args]);
467
- }
468
37
 
469
- // deploy is the same delegation pattern as `dev`, but with a confirmation
470
- // gate because publishing to prod is destructive (overwrites functions and
471
- // schema). The gate is skippable via --yes / -y for CI use. Non-interactive
472
- // callers without --yes get a clear refusal rather than a hang on
473
- // readline.question() that never fires.
474
- //
475
- // We resolve the prod deployment name from the local project metadata so the
476
- // prompt names the exact target. When there's no metadata (no `synapse
477
- // select` yet), we let `convex()` produce its own "run select first" error
478
- // without prompting — the operator obviously isn't ready to deploy.
479
- async function deploy(args, {
480
- input = process.stdin,
481
- output = process.stderr,
482
- confirmImpl = confirm,
483
- convexImpl = convex,
484
- } = {}) {
485
- const { yes, rest } = extractYesFlag(args);
486
- const projectConfig = readProjectConfig(process.cwd());
487
- const deploymentName = deploymentNameForTarget(projectConfig, "prod");
488
- if (deploymentName && !yes) {
489
- if (!input.isTTY) {
490
- throw new Error(
491
- "synapse deploy needs confirmation. Pass --yes to skip in non-interactive contexts (CI, scripts), " +
492
- "or run `synapse deploy` again inside a regular terminal.",
493
- );
494
- }
495
- const ok = await confirmImpl(
496
- `About to run \`convex deploy\` against PROD deployment ${deploymentName}. Continue? [y/N] `,
497
- { input, output, defaultAnswer: false },
498
- );
499
- if (!ok) {
500
- output.write("Deploy cancelled.\n");
501
- return;
502
- }
38
+ if (wantsHelp(rest)) {
39
+ return renderCommandHelp(cmd);
503
40
  }
504
- return await convexImpl(["--target", "prod", "deploy", ...rest]);
505
- }
506
41
 
507
- async function main(argv) {
508
- const [command, ...args] = argv;
509
- switch (command) {
510
- case "login":
511
- return await login(args);
512
- case "logout":
513
- return await logout();
514
- case "whoami":
515
- return await whoami();
516
- case "select":
517
- return await selectDeployment();
518
- case "credentials":
519
- return await credentials(args);
520
- case "dev":
521
- return await dev(args);
522
- case "deploy":
523
- return await deploy(args);
524
- case "convex":
525
- return await convex(args);
526
- case "-h":
527
- case "--help":
528
- case "help":
529
- case undefined:
530
- process.stdout.write(usage());
531
- return;
532
- default:
533
- throw new Error(`Unknown command: ${command}\n\n${usage()}`);
534
- }
42
+ const out = createOutput({ json });
43
+ const ctx = createContext({ out });
44
+ return await cmd.run(rest, ctx);
535
45
  }
536
46
 
537
47
  if (require.main === module) {
538
48
  main(process.argv.slice(2)).catch((err) => {
539
49
  process.stderr.write(`${err.message}\n`);
540
- // Surface a concrete next step for the most common failure mode —
541
- // the user typed a Synapse URL that doesn't resolve or whose server
542
- // refused the connection. Without this hint, "fetch failed" reads
543
- // like a Node bug instead of a config / connectivity problem.
544
50
  if (err && err.code === "network_error") {
545
51
  process.stderr.write(
546
- "Hint: double-check the URL is reachable from this machine (try `curl <url>/v1/install_status`) " +
547
- "and that the Synapse server is running.\n",
52
+ "Hint: double-check the URL is reachable from this machine (try `curl <url>/v1/install_status`) and that the Synapse server is running.\n",
548
53
  );
549
54
  }
550
55
  process.exitCode = 1;
551
56
  });
552
57
  }
553
58
 
59
+ // ---- Legacy exports (test/bin.test.js consumes these) --------------
60
+ //
61
+ // Re-export the helpers the existing tests already import. Adding new
62
+ // commands does NOT add to this list — new code lives in lib/commands/
63
+ // and is tested directly there.
64
+
65
+ const _convexCmd = require("../lib/commands/convex");
66
+ const _deployCmd = require("../lib/commands/deploy");
67
+ const _devCmd = require("../lib/commands/dev");
68
+ const _credentialsCmd = require("../lib/commands/credentials");
69
+ const _selectCmd = require("../lib/commands/select");
70
+ const _ctxModule = require("../lib/commands/_context");
71
+
72
+ // `clientFromConfig` was the pre-refactor entry point that returned
73
+ // { cfg, api } for any command that needed auth. Kept here as a thin
74
+ // shim around the same underlying helper so test/bin.test.js's
75
+ // "clientFromConfig refreshes an expired access token" still passes.
76
+ function clientFromConfig() {
77
+ const { requireConfig } = require("../lib/config");
78
+ const cfg = requireConfig();
79
+ const api = _ctxModule.makeRefreshableApi(cfg);
80
+ return { cfg, api };
81
+ }
82
+
554
83
  module.exports = {
555
- chooseDeploymentForType,
556
- clientFromConfig,
557
- deploy,
558
- dev,
559
- extractYesFlag,
560
- formatCredentials,
561
- inferConvexTarget,
562
84
  main,
563
- parseConvexInvocation,
564
- parseFormat,
565
- resolveConvexInvocation,
85
+ clientFromConfig,
86
+ // dev / deploy keep their pre-refactor signatures for test injectors.
87
+ deploy: _deployCmd.deploy,
88
+ dev: _devCmd.dev,
89
+ extractYesFlag: _deployCmd.extractYesFlag,
90
+ formatCredentials: _credentialsCmd.formatCredentials,
91
+ parseFormat: _credentialsCmd.parseFormat,
92
+ // convex command exposes the pure parsers used by tests.
93
+ inferConvexTarget: _convexCmd.inferConvexTarget,
94
+ parseConvexInvocation: _convexCmd.parseConvexInvocation,
95
+ resolveConvexInvocation: _convexCmd.resolveConvexInvocation,
96
+ // select command's helpers.
97
+ chooseDeploymentForType: _selectCmd.chooseDeploymentForType,
566
98
  };