@iann29/synapse 1.6.17 → 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.
@@ -0,0 +1,133 @@
1
+ // Runtime context handed to every command's `run(rest, ctx)`.
2
+ //
3
+ // The dispatcher constructs one of these per invocation. It carries:
4
+ // - cfg / api: the operator's session and a refresh-aware API client,
5
+ // lazily built (commands like `version` that don't need a session
6
+ // can read ctx.cfgOrNull without forcing the user to log in first).
7
+ // - out: the shared output layer (createOutput()), already aware of
8
+ // whether --json was on the argv.
9
+ // - projectDir / projectConfig: convenience accessors for commands
10
+ // that need the linked project metadata (.synapse/project.json).
11
+ //
12
+ // Commands MUST go through ctx instead of touching process.stdout /
13
+ // process.cwd directly — otherwise --json piping and the test fakes
14
+ // stop working.
15
+
16
+ const { SynapseAPI, SynapseAPIError } = require("../api");
17
+ const {
18
+ clearConfig: _clearConfig,
19
+ normalizeBaseUrl,
20
+ readConfig,
21
+ requireConfig,
22
+ writeConfig,
23
+ } = require("../config");
24
+ const { readProjectConfig } = require("../project");
25
+
26
+ // Wraps an API client so any 401 transparently retries against
27
+ // /v1/auth/refresh once. Mirrors what bin/synapse.js had before the
28
+ // refactor; lives here so every command shares it.
29
+ function makeRefreshableApi(cfg) {
30
+ const api = new SynapseAPI({ baseUrl: cfg.baseUrl, accessToken: cfg.accessToken });
31
+ return new Proxy(api, {
32
+ get(target, prop) {
33
+ const value = target[prop];
34
+ if (typeof value !== "function") return value;
35
+ return async (...args) => {
36
+ try {
37
+ return await value.apply(target, args);
38
+ } catch (err) {
39
+ if (
40
+ !(err instanceof SynapseAPIError) ||
41
+ err.status !== 401 ||
42
+ !cfg.refreshToken
43
+ ) {
44
+ throw err;
45
+ }
46
+ const session = await new SynapseAPI({ baseUrl: cfg.baseUrl }).refresh(
47
+ cfg.refreshToken,
48
+ );
49
+ if (!session.accessToken) throw err;
50
+ cfg.accessToken = session.accessToken;
51
+ cfg.refreshToken = session.refreshToken || cfg.refreshToken;
52
+ cfg.tokenType = session.tokenType || cfg.tokenType || "Bearer";
53
+ if (session.user) cfg.user = session.user;
54
+ writeConfig(cfg);
55
+ target.accessToken = cfg.accessToken;
56
+ return await value.apply(target, args);
57
+ }
58
+ };
59
+ },
60
+ });
61
+ }
62
+
63
+ function createContext({ out, cwd = process.cwd(), env = process.env } = {}) {
64
+ let _cfg = null;
65
+ let _api = null;
66
+ let _projectConfig;
67
+ const cfgLoad = () => {
68
+ if (_cfg === undefined || _cfg === null) {
69
+ _cfg = readConfig(); // may be null if not logged in
70
+ }
71
+ return _cfg;
72
+ };
73
+
74
+ return {
75
+ out,
76
+ cwd,
77
+ env,
78
+
79
+ // Returns the saved config or null. Commands that don't need a
80
+ // session (version, doctor's local checks) use this.
81
+ get cfgOrNull() {
82
+ return cfgLoad();
83
+ },
84
+
85
+ // Throws "Not logged in" with a helpful message when no session
86
+ // exists. Commands that REQUIRE auth call this.
87
+ get cfg() {
88
+ const c = cfgLoad();
89
+ if (!c || !c.baseUrl || !c.accessToken) {
90
+ throw new Error("Not logged in. Run `synapse login <url>` first.");
91
+ }
92
+ _cfg = c;
93
+ return c;
94
+ },
95
+
96
+ // Builds an auth+refresh-aware API client on demand.
97
+ get api() {
98
+ if (_api) return _api;
99
+ const c = this.cfg; // throws if not logged in
100
+ _api = makeRefreshableApi(c);
101
+ return _api;
102
+ },
103
+
104
+ // Re-reads the project metadata from disk on first access only —
105
+ // commands that mutate it (synapse select) call this AFTER writing.
106
+ get projectConfig() {
107
+ if (_projectConfig === undefined) {
108
+ _projectConfig = readProjectConfig(cwd);
109
+ }
110
+ return _projectConfig;
111
+ },
112
+
113
+ // Convenience for commands that need to verify a project is linked.
114
+ requireProject() {
115
+ const p = this.projectConfig;
116
+ if (!p) {
117
+ throw new Error(
118
+ "No Synapse project metadata found in this directory. Run `synapse select` first.",
119
+ );
120
+ }
121
+ return p;
122
+ },
123
+
124
+ // Force a re-read of the project config (e.g. after `synapse select`
125
+ // wrote a new file mid-handler).
126
+ refreshProjectConfig() {
127
+ _projectConfig = readProjectConfig(cwd);
128
+ return _projectConfig;
129
+ },
130
+ };
131
+ }
132
+
133
+ module.exports = { createContext, makeRefreshableApi, normalizeBaseUrl };
@@ -0,0 +1,75 @@
1
+ // Command registry + two-word-then-one-word dispatcher.
2
+ //
3
+ // The CLI ships ~28 commands across both flat shortcuts (`synapse dev`,
4
+ // `synapse login`) and resource subcommands (`synapse deployment create`,
5
+ // `synapse env set`). A registry keyed by the literal command path
6
+ // ("dev" or "deployment create") makes routing predictable, keeps each
7
+ // handler in its own file, and avoids a 2k-line nested switch.
8
+ //
9
+ // Resolution order, applied left-to-right against argv:
10
+ // 1. Two-word lookup ("deployment create") — wins when present so
11
+ // subcommand groups override any bare top-level synonym.
12
+ // 2. One-word lookup ("dev") — falls through for legacy shortcuts.
13
+ // 3. Miss → caller surfaces "unknown command" with help.
14
+ //
15
+ // Each command module exports:
16
+ // {
17
+ // name: string, // canonical id, e.g. "deployment create"
18
+ // summary: string, // 1-line description for root help
19
+ // usage: string, // exact usage line for per-command help
20
+ // description?: string, // multi-paragraph help body
21
+ // run: async (rest, ctx) => void | number
22
+ // }
23
+ //
24
+ // `ctx` is the runtime context (see context.js) — gives every handler
25
+ // access to the resolved config, an API client, the output layer, and
26
+ // process I/O without each one re-bootstrapping.
27
+
28
+ const fs = require("node:fs");
29
+ const path = require("node:path");
30
+
31
+ function buildRegistry() {
32
+ const dir = __dirname;
33
+ const map = new Map();
34
+ for (const file of fs.readdirSync(dir)) {
35
+ if (!file.endsWith(".js")) continue;
36
+ if (file.startsWith("_")) continue; // dispatcher.js, context.js, etc
37
+ if (file === "index.js") continue;
38
+ const mod = require(path.join(dir, file));
39
+ if (!mod || typeof mod.name !== "string" || typeof mod.run !== "function") {
40
+ throw new Error(
41
+ `commands/${file} does not export the { name, run } shape`,
42
+ );
43
+ }
44
+ if (map.has(mod.name)) {
45
+ throw new Error(
46
+ `Duplicate command registered: ${mod.name} (in ${file})`,
47
+ );
48
+ }
49
+ map.set(mod.name, mod);
50
+ }
51
+ return map;
52
+ }
53
+
54
+ // Resolve argv into { cmd, rest } using the two-then-one rule.
55
+ // Returns { cmd: null, rest: argv } on miss so the caller can surface
56
+ // the unknown-command error consistently.
57
+ function resolve(registry, argv) {
58
+ if (!argv || argv.length === 0) return { cmd: null, rest: argv ?? [] };
59
+ if (argv.length >= 2) {
60
+ const two = `${argv[0]} ${argv[1]}`;
61
+ if (registry.has(two)) return { cmd: registry.get(two), rest: argv.slice(2) };
62
+ }
63
+ if (registry.has(argv[0])) return { cmd: registry.get(argv[0]), rest: argv.slice(1) };
64
+ return { cmd: null, rest: argv };
65
+ }
66
+
67
+ // Detect `--help` / `-h` anywhere in the args (positional-tolerant).
68
+ // Returns boolean; doesn't mutate argv — handlers don't see the flag
69
+ // because the dispatcher short-circuits to help rendering first.
70
+ function wantsHelp(argv) {
71
+ if (!argv) return false;
72
+ return argv.some((a) => a === "--help" || a === "-h");
73
+ }
74
+
75
+ module.exports = { buildRegistry, resolve, wantsHelp };
@@ -0,0 +1,105 @@
1
+ // Root and per-command help rendering.
2
+ //
3
+ // `synapse help` and `synapse <cmd> --help` are routed here from the
4
+ // dispatcher; no parser library, no template engine. We group commands
5
+ // by their first space-delimited word so the root listing reads as
6
+ // "top-level shortcuts" + "deployment.*", "project.*", "team.*", etc.
7
+
8
+ const colors = require("../colors");
9
+
10
+ function renderRootHelp(registry, { stdout = process.stdout } = {}) {
11
+ // Stable section order so the help page stays predictable across
12
+ // releases. Groups not listed here fall to the bottom alphabetically.
13
+ const SECTION_ORDER = [
14
+ ["Session", ["login", "logout", "whoami"]],
15
+ ["Project linking", ["select", "credentials"]],
16
+ ["Day-to-day", ["dev", "deploy"]],
17
+ ["Visibility", ["version", "status", "doctor", "open", "logs"]],
18
+ ["Deployments", "deployment"],
19
+ ["Projects", "project"],
20
+ ["Teams", "team"],
21
+ ["Env vars", "env"],
22
+ ["Domains", "domain"],
23
+ ["Escape hatch", ["convex"]],
24
+ ];
25
+
26
+ const used = new Set();
27
+ const lines = [
28
+ colors.bold("Synapse CLI") + colors.dim(" — manage Synapse-self-hosted Convex deployments"),
29
+ "",
30
+ "Usage:",
31
+ " " + colors.bold("synapse") + " <command> [...args] [--json] [--help]",
32
+ "",
33
+ ];
34
+
35
+ for (const [title, spec] of SECTION_ORDER) {
36
+ const cmds = [];
37
+ if (Array.isArray(spec)) {
38
+ // Explicit command names.
39
+ for (const name of spec) {
40
+ if (registry.has(name)) {
41
+ cmds.push(registry.get(name));
42
+ used.add(name);
43
+ }
44
+ }
45
+ } else {
46
+ // Prefix-match (every command whose name starts with `<spec> `).
47
+ const prefix = spec + " ";
48
+ for (const [name, cmd] of registry) {
49
+ if (name.startsWith(prefix) && !used.has(name)) {
50
+ cmds.push(cmd);
51
+ used.add(name);
52
+ }
53
+ }
54
+ cmds.sort((a, b) => a.name.localeCompare(b.name));
55
+ }
56
+ if (cmds.length === 0) continue;
57
+ lines.push(colors.dim(title));
58
+ const widest = Math.max(...cmds.map((c) => c.name.length));
59
+ for (const c of cmds) {
60
+ lines.push(" " + colors.bold(c.name.padEnd(widest)) + " " + (c.summary || ""));
61
+ }
62
+ lines.push("");
63
+ }
64
+
65
+ // Any leftover registered commands fall here — keeps the help honest
66
+ // when someone adds a command without updating SECTION_ORDER.
67
+ const leftover = [];
68
+ for (const [name, cmd] of registry) {
69
+ if (!used.has(name)) leftover.push(cmd);
70
+ }
71
+ if (leftover.length > 0) {
72
+ lines.push(colors.dim("Other"));
73
+ leftover.sort((a, b) => a.name.localeCompare(b.name));
74
+ const widest = Math.max(...leftover.map((c) => c.name.length));
75
+ for (const c of leftover) {
76
+ lines.push(" " + colors.bold(c.name.padEnd(widest)) + " " + (c.summary || ""));
77
+ }
78
+ lines.push("");
79
+ }
80
+
81
+ lines.push(
82
+ "Run " + colors.bold("synapse <command> --help") + " for command-specific help.",
83
+ );
84
+ lines.push("");
85
+ stdout.write(lines.join("\n"));
86
+ }
87
+
88
+ function renderCommandHelp(cmd, { stdout = process.stdout } = {}) {
89
+ const lines = [];
90
+ lines.push(colors.bold("synapse " + cmd.name));
91
+ if (cmd.summary) {
92
+ lines.push(colors.dim(cmd.summary));
93
+ }
94
+ lines.push("");
95
+ lines.push("Usage:");
96
+ lines.push(" " + (cmd.usage || `synapse ${cmd.name}`));
97
+ if (cmd.description) {
98
+ lines.push("");
99
+ lines.push(cmd.description.trimEnd());
100
+ }
101
+ lines.push("");
102
+ stdout.write(lines.join("\n"));
103
+ }
104
+
105
+ module.exports = { renderRootHelp, renderCommandHelp };
@@ -0,0 +1,151 @@
1
+ // `synapse convex [--target dev|prod] [...args]` — escape hatch that
2
+ // delegates to `npx convex <args>` while wiring up Synapse credentials
3
+ // transparently. Used directly when the operator needs a Convex
4
+ // subcommand without a Synapse shortcut (run, env list, import, etc).
5
+ //
6
+ // `synapse dev` and `synapse deploy` are thin wrappers around this
7
+ // same flow with the target pre-set and (for deploy) a confirmation
8
+ // prompt.
9
+
10
+ const { runConvex } = require("../convex");
11
+ const { normalizeBaseUrl } = require("../config");
12
+ const {
13
+ deploymentNameForTarget,
14
+ readProjectConfig,
15
+ } = require("../project");
16
+
17
+ function parseConvexTarget(args) {
18
+ let target = null;
19
+ let index = 0;
20
+ while (index < args.length) {
21
+ const arg = args[index];
22
+ if (arg === "--target") {
23
+ target = args[index + 1];
24
+ if (!target) {
25
+ throw new Error("--target requires dev or prod");
26
+ }
27
+ index += 2;
28
+ continue;
29
+ }
30
+ if (arg && arg.startsWith("--target=")) {
31
+ target = arg.slice("--target=".length);
32
+ index += 1;
33
+ continue;
34
+ }
35
+ break;
36
+ }
37
+ if (target && target !== "dev" && target !== "prod") {
38
+ throw new Error("--target must be dev or prod");
39
+ }
40
+ return {
41
+ explicitTarget: Boolean(target),
42
+ target,
43
+ args: args.slice(index),
44
+ };
45
+ }
46
+
47
+ function inferConvexTarget(args) {
48
+ const command = args.find((arg) => arg && !arg.startsWith("-")) || "";
49
+ return command === "deploy" ? "prod" : "dev";
50
+ }
51
+
52
+ function parseConvexInvocation(args) {
53
+ const parsed = parseConvexTarget(args);
54
+ return {
55
+ ...parsed,
56
+ target: parsed.target || inferConvexTarget(parsed.args),
57
+ };
58
+ }
59
+
60
+ async function resolveConvexInvocation(
61
+ args,
62
+ { cfg = null, api = null, projectDir = process.cwd() } = {},
63
+ ) {
64
+ const parsed = parseConvexInvocation(args);
65
+ const projectConfig = readProjectConfig(projectDir);
66
+ if (!projectConfig) {
67
+ if (parsed.explicitTarget) {
68
+ throw new Error(
69
+ "No Synapse project metadata found. Run `synapse select` first.",
70
+ );
71
+ }
72
+ return {
73
+ ...parsed,
74
+ credentials: null,
75
+ deploymentName: "",
76
+ projectConfig: null,
77
+ target: null,
78
+ };
79
+ }
80
+
81
+ if (!cfg || !api) {
82
+ throw new Error("Not logged in. Run `synapse login <url>` first.");
83
+ }
84
+ if (
85
+ projectConfig.synapseUrl &&
86
+ cfg.baseUrl &&
87
+ normalizeBaseUrl(projectConfig.synapseUrl) !== normalizeBaseUrl(cfg.baseUrl)
88
+ ) {
89
+ throw new Error(
90
+ `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.`,
91
+ );
92
+ }
93
+
94
+ const deploymentName = deploymentNameForTarget(projectConfig, parsed.target);
95
+ if (!deploymentName) {
96
+ throw new Error(
97
+ `No ${parsed.target} deployment saved for this project. Run \`synapse select\` again.`,
98
+ );
99
+ }
100
+ const credentials = await api.cliCredentials(deploymentName);
101
+ return {
102
+ ...parsed,
103
+ credentials,
104
+ deploymentName,
105
+ projectConfig,
106
+ };
107
+ }
108
+
109
+ async function runConvexCommand(args, ctx) {
110
+ const projectConfig = readProjectConfig(ctx.cwd);
111
+ let resolved;
112
+ if (projectConfig) {
113
+ // Need an authenticated session to fetch fresh credentials.
114
+ const cfg = ctx.cfg;
115
+ const api = ctx.api;
116
+ resolved = await resolveConvexInvocation(args, { cfg, api, projectDir: ctx.cwd });
117
+ ctx.out.info(
118
+ `Using Synapse ${resolved.target} deployment ${resolved.deploymentName}.`,
119
+ );
120
+ } else {
121
+ resolved = await resolveConvexInvocation(args, { projectDir: ctx.cwd });
122
+ }
123
+ const code = await runConvex(resolved.args, { credentials: resolved.credentials });
124
+ process.exitCode = code;
125
+ }
126
+
127
+ module.exports = {
128
+ name: "convex",
129
+ summary: "Run any `convex` subcommand with Synapse credentials injected.",
130
+ usage: "synapse convex [--target dev|prod] [...args]",
131
+ description: `Escape hatch for any \`convex\` invocation that doesn't have a Synapse shortcut. Resolves the right deployment from the linked project, fetches fresh credentials from the backend, scrubs CONVEX_DEPLOYMENT from the child env, and spawns \`npx convex\` with the rest of the args.
132
+
133
+ Examples:
134
+ synapse convex --help # show convex's help
135
+ synapse convex run messages:list # run a function against dev
136
+ synapse convex --target prod env list # list prod env vars
137
+ synapse convex import data.snapshot.gz # restore a snapshot to dev
138
+
139
+ By default the target is inferred: \`deploy\` → prod, everything else → dev.`,
140
+
141
+ // Exports kept for the legacy test imports.
142
+ inferConvexTarget,
143
+ parseConvexInvocation,
144
+ parseConvexTarget,
145
+ resolveConvexInvocation,
146
+ runConvexCommand,
147
+
148
+ async run(args, ctx) {
149
+ return await runConvexCommand(args, ctx);
150
+ },
151
+ };
@@ -0,0 +1,85 @@
1
+ // `synapse credentials <deployment> [--format env|shell|json]` — fetch a
2
+ // deployment's CONVEX_SELF_HOSTED_* pair from the backend and emit it
3
+ // in the requested format. Read-only; doesn't touch any local file.
4
+ //
5
+ // Operators mostly use --format env (pasteable into .env.local) and
6
+ // --format shell (eval-able to export the vars). --format json is for
7
+ // CI scripts that want machine-parseable output.
8
+
9
+ const { quoteEnvValue } = require("../env-file");
10
+
11
+ const FORMATS = new Set(["env", "shell", "json"]);
12
+
13
+ function parseFormat(args) {
14
+ let format = "env";
15
+ const rest = [];
16
+ for (let i = 0; i < args.length; i += 1) {
17
+ const arg = args[i];
18
+ if (arg === "--format") {
19
+ format = args[i + 1];
20
+ i += 1;
21
+ } else if (arg.startsWith("--format=")) {
22
+ format = arg.slice("--format=".length);
23
+ } else {
24
+ rest.push(arg);
25
+ }
26
+ }
27
+ return { format, rest };
28
+ }
29
+
30
+ function formatCredentials(creds, format) {
31
+ switch (format) {
32
+ case "json":
33
+ return JSON.stringify(creds, null, 2);
34
+ case "shell":
35
+ return creds.exportSnippet;
36
+ case "env":
37
+ return (
38
+ creds.envSnippet ||
39
+ `CONVEX_SELF_HOSTED_URL=${quoteEnvValue(creds.convexUrl)}\nCONVEX_SELF_HOSTED_ADMIN_KEY=${quoteEnvValue(creds.adminKey)}`
40
+ );
41
+ default:
42
+ throw new Error("format must be one of: env, shell, json");
43
+ }
44
+ }
45
+
46
+ module.exports = {
47
+ name: "credentials",
48
+ summary: "Print a deployment's CONVEX_SELF_HOSTED_* pair in env/shell/json form.",
49
+ usage: "synapse credentials <deployment> [--format env|shell|json]",
50
+ description: `Hits /v1/deployments/{name}/cli_credentials and renders the result. No local file is written — pipe the output where you want it.
51
+
52
+ Formats:
53
+ env KEY=value pairs, single-quoted (default; .env.local-ready)
54
+ shell export KEY=value pairs (paste into a shell or eval $(...))
55
+ json the full response body, including envSnippet/exportSnippet`,
56
+
57
+ // Re-exported for legacy test imports; the dispatcher path doesn't
58
+ // need either helper directly.
59
+ formatCredentials,
60
+ parseFormat,
61
+
62
+ async run(args, ctx) {
63
+ const { format, rest } = parseFormat(args);
64
+ const deployment = rest[0];
65
+ if (!deployment) {
66
+ throw new Error("Usage: synapse credentials <deployment> [--format env|shell|json]");
67
+ }
68
+ if (!FORMATS.has(format)) {
69
+ throw new Error("format must be one of: env, shell, json");
70
+ }
71
+ const creds = await ctx.api.cliCredentials(deployment);
72
+ if (format === "json") {
73
+ // json format already prints the structured response; keep raw
74
+ // CLI output identical to pre-refactor (stdout, no newline tax).
75
+ ctx.out.result(creds, (d, { stdout }) =>
76
+ stdout.write(formatCredentials(d, "json") + "\n"),
77
+ );
78
+ return;
79
+ }
80
+ // env/shell — emit the snippet verbatim to stdout.
81
+ ctx.out.result(creds, (d, { stdout }) =>
82
+ stdout.write(formatCredentials(d, format) + "\n"),
83
+ );
84
+ },
85
+ };
@@ -0,0 +1,72 @@
1
+ // `synapse deploy [--yes] [args]` — alias for `synapse convex --target
2
+ // prod deploy` with a confirmation gate. Refuses to run in non-TTY
3
+ // contexts unless --yes is passed so a CI script doesn't deadlock on a
4
+ // readline prompt.
5
+
6
+ const { confirm } = require("../prompts");
7
+ const { deploymentNameForTarget, readProjectConfig } = require("../project");
8
+ const convexCmd = require("./convex");
9
+
10
+ function extractYesFlag(args) {
11
+ let yes = false;
12
+ const rest = [];
13
+ for (const arg of args) {
14
+ if (arg === "--yes" || arg === "-y") {
15
+ yes = true;
16
+ } else {
17
+ rest.push(arg);
18
+ }
19
+ }
20
+ return { yes, rest };
21
+ }
22
+
23
+ // Signature kept at (args, opts) — not (args, ctx, opts) — so the
24
+ // existing test/bin.test.js callers that pass `{ convexImpl, ... }`
25
+ // as the second positional arg keep working. The dispatcher's `run()`
26
+ // below adapts ctx → opts.
27
+ async function deploy(
28
+ args,
29
+ {
30
+ input = process.stdin,
31
+ output = process.stderr,
32
+ confirmImpl = confirm,
33
+ convexImpl,
34
+ cwd = process.cwd(),
35
+ ctx,
36
+ } = {},
37
+ ) {
38
+ const impl = convexImpl ?? ((a) => convexCmd.runConvexCommand(a, ctx));
39
+ const { yes, rest } = extractYesFlag(args);
40
+ const projectConfig = readProjectConfig(cwd);
41
+ const deploymentName = deploymentNameForTarget(projectConfig, "prod");
42
+ if (deploymentName && !yes) {
43
+ if (!input.isTTY) {
44
+ throw new Error(
45
+ "synapse deploy needs confirmation. Pass --yes to skip in non-interactive contexts (CI, scripts), or run `synapse deploy` again inside a regular terminal.",
46
+ );
47
+ }
48
+ const ok = await confirmImpl(
49
+ `About to run \`convex deploy\` against PROD deployment ${deploymentName}. Continue? [y/N] `,
50
+ { input, output, defaultAnswer: false },
51
+ );
52
+ if (!ok) {
53
+ output.write("Deploy cancelled.\n");
54
+ return;
55
+ }
56
+ }
57
+ return await impl(["--target", "prod", "deploy", ...rest]);
58
+ }
59
+
60
+ module.exports = {
61
+ name: "deploy",
62
+ summary: "Confirm + run `convex deploy` against the linked prod deployment.",
63
+ usage: "synapse deploy [--yes] [...args]",
64
+ description: `Sugar for \`synapse convex --target prod deploy [...args]\` with a confirmation prompt. The first deploy of the session asks \`Continue? [y/N]\` — pass \`--yes\` (or \`-y\`) to skip in CI. Refuses in non-TTY contexts without --yes so a hung readline doesn't deadlock pipelines.`,
65
+
66
+ deploy,
67
+ extractYesFlag,
68
+
69
+ async run(args, ctx) {
70
+ return await deploy(args, { cwd: ctx.cwd, ctx });
71
+ },
72
+ };
@@ -0,0 +1,24 @@
1
+ // `synapse dev [args]` — thin alias for `synapse convex --target dev dev`.
2
+ // We don't replicate the credentials/spawn pipeline here; just call
3
+ // the convex command with the target hard-coded.
4
+
5
+ const convexCmd = require("./convex");
6
+
7
+ // Same (args, opts) signature as deploy — preserves the test seam.
8
+ async function dev(args, { convexImpl, ctx } = {}) {
9
+ const impl = convexImpl ?? ((a) => convexCmd.runConvexCommand(a, ctx));
10
+ return await impl(["--target", "dev", "dev", ...args]);
11
+ }
12
+
13
+ module.exports = {
14
+ name: "dev",
15
+ summary: "Run `convex dev` against the linked dev deployment.",
16
+ usage: "synapse dev [...args]",
17
+ description: `Sugar for \`synapse convex --target dev dev [...args]\`. Watches and pushes schema/functions to the dev deployment selected by \`synapse select\`. Pass \`--once\` (a convex flag) to run a single push and exit.`,
18
+
19
+ dev,
20
+
21
+ async run(args, ctx) {
22
+ return await dev(args, { ctx });
23
+ },
24
+ };