@iann29/synapse 1.7.0 → 1.8.1
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 +69 -537
- package/lib/api.js +7 -0
- package/lib/commands/_context.js +133 -0
- package/lib/commands/_dispatcher.js +75 -0
- package/lib/commands/_help.js +105 -0
- package/lib/commands/convex.js +151 -0
- package/lib/commands/credentials.js +85 -0
- package/lib/commands/deploy.js +72 -0
- package/lib/commands/dev.js +24 -0
- package/lib/commands/doctor.js +86 -0
- package/lib/commands/login.js +43 -0
- package/lib/commands/logout.js +20 -0
- package/lib/commands/open.js +143 -0
- package/lib/commands/select.js +239 -0
- package/lib/commands/status.js +173 -0
- package/lib/commands/version.js +77 -0
- package/lib/commands/whoami.js +22 -0
- package/lib/doctor/checks.js +619 -0
- package/lib/doctor/renderer.js +93 -0
- package/lib/doctor/runner.js +148 -0
- package/lib/output.js +140 -0
- package/lib/project.js +8 -0
- package/package.json +1 -1
package/bin/synapse.js
CHANGED
|
@@ -1,566 +1,98 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
2
|
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
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
|
-
//
|
|
286
|
-
//
|
|
287
|
-
//
|
|
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
|
-
|
|
323
|
-
|
|
324
|
-
|
|
325
|
-
|
|
326
|
-
|
|
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
|
-
|
|
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
|
|
410
|
-
|
|
411
|
-
const
|
|
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
|
-
|
|
424
|
-
|
|
425
|
-
|
|
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
|
-
|
|
443
|
-
|
|
444
|
-
|
|
445
|
-
|
|
446
|
-
|
|
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
|
-
|
|
470
|
-
|
|
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
|
-
|
|
508
|
-
const
|
|
509
|
-
|
|
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
|
-
|
|
564
|
-
|
|
565
|
-
|
|
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
|
};
|
package/lib/api.js
CHANGED
|
@@ -117,6 +117,13 @@ class SynapseAPI {
|
|
|
117
117
|
return this.listAll(`/v1/teams/${encodeURIComponent(teamRef)}/list_projects`);
|
|
118
118
|
}
|
|
119
119
|
|
|
120
|
+
// Single-project lookup by ID. Cheaper than listing a team's projects
|
|
121
|
+
// (no pagination) and FK cascade team→projects means this also detects
|
|
122
|
+
// a deleted team — backend returns 404 in both cases.
|
|
123
|
+
getProject(projectId) {
|
|
124
|
+
return this.request("GET", `/v1/projects/${encodeURIComponent(projectId)}/`);
|
|
125
|
+
}
|
|
126
|
+
|
|
120
127
|
deployments(projectId) {
|
|
121
128
|
return this.listAll(`/v1/projects/${encodeURIComponent(projectId)}/list_deployments`);
|
|
122
129
|
}
|