@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.
- package/README.md +20 -0
- package/bin/synapse.js +75 -349
- package/lib/api.js +45 -3
- package/lib/colors.js +51 -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 +82 -0
- package/lib/commands/login.js +43 -0
- package/lib/commands/logout.js +20 -0
- package/lib/commands/open.js +96 -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/convex.js +20 -2
- package/lib/doctor/checks.js +484 -0
- package/lib/doctor/renderer.js +93 -0
- package/lib/doctor/runner.js +148 -0
- package/lib/output.js +140 -0
- package/lib/prompts.js +80 -4
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -15,6 +15,26 @@ npm install -g @iann29/synapse
|
|
|
15
15
|
synapse --help
|
|
16
16
|
```
|
|
17
17
|
|
|
18
|
+
### Windows: ensure the npm global bin directory is in PATH
|
|
19
|
+
|
|
20
|
+
On a fresh Node.js install, Windows does **not** always add
|
|
21
|
+
`%APPDATA%\npm` to the user PATH. After `npm install -g`, the
|
|
22
|
+
`synapse` binary exists but `synapse --help` errors with
|
|
23
|
+
*"not recognised as the name of a cmdlet"*. Fix once in PowerShell:
|
|
24
|
+
|
|
25
|
+
```powershell
|
|
26
|
+
[Environment]::SetEnvironmentVariable(
|
|
27
|
+
'PATH',
|
|
28
|
+
"$([Environment]::GetEnvironmentVariable('PATH','User'));$env:APPDATA\npm",
|
|
29
|
+
'User'
|
|
30
|
+
)
|
|
31
|
+
```
|
|
32
|
+
|
|
33
|
+
Close every terminal (and your IDE — VS Code caches the env at launch)
|
|
34
|
+
and reopen. `synapse --help` should now print the usage. This is a
|
|
35
|
+
one-time, Node-installer-version-dependent issue; it is not specific
|
|
36
|
+
to this package.
|
|
37
|
+
|
|
18
38
|
For one app/project:
|
|
19
39
|
|
|
20
40
|
```bash
|
package/bin/synapse.js
CHANGED
|
@@ -1,372 +1,98 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
2
|
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
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.
|
|
9
|
+
//
|
|
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.
|
|
13
|
+
|
|
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");
|
|
19
|
+
|
|
20
|
+
const REGISTRY = buildRegistry();
|
|
14
21
|
|
|
15
|
-
function
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
synapse logout
|
|
19
|
-
synapse whoami
|
|
20
|
-
synapse select
|
|
21
|
-
synapse credentials <deployment> [--format env|shell|json]
|
|
22
|
-
synapse convex [--target dev|prod] [...args]
|
|
23
|
-
`;
|
|
24
|
-
}
|
|
25
|
-
|
|
26
|
-
function clientFromConfig() {
|
|
27
|
-
const cfg = requireConfig();
|
|
28
|
-
const api = new SynapseAPI({ baseUrl: cfg.baseUrl, accessToken: cfg.accessToken });
|
|
29
|
-
const refreshable = new Proxy(api, {
|
|
30
|
-
get(target, prop) {
|
|
31
|
-
const value = target[prop];
|
|
32
|
-
if (typeof value !== "function") {
|
|
33
|
-
return value;
|
|
34
|
-
}
|
|
35
|
-
return async (...args) => {
|
|
36
|
-
try {
|
|
37
|
-
return await value.apply(target, args);
|
|
38
|
-
} catch (err) {
|
|
39
|
-
if (!(err instanceof SynapseAPIError) || err.status !== 401 || !cfg.refreshToken) {
|
|
40
|
-
throw err;
|
|
41
|
-
}
|
|
42
|
-
const session = await new SynapseAPI({ baseUrl: cfg.baseUrl }).refresh(cfg.refreshToken);
|
|
43
|
-
if (!session.accessToken) {
|
|
44
|
-
throw err;
|
|
45
|
-
}
|
|
46
|
-
cfg.accessToken = session.accessToken;
|
|
47
|
-
cfg.refreshToken = session.refreshToken || cfg.refreshToken;
|
|
48
|
-
cfg.tokenType = session.tokenType || cfg.tokenType || "Bearer";
|
|
49
|
-
if (session.user) {
|
|
50
|
-
cfg.user = session.user;
|
|
51
|
-
}
|
|
52
|
-
writeConfig(cfg);
|
|
53
|
-
target.accessToken = cfg.accessToken;
|
|
54
|
-
return await value.apply(target, args);
|
|
55
|
-
}
|
|
56
|
-
};
|
|
57
|
-
},
|
|
58
|
-
});
|
|
59
|
-
return {
|
|
60
|
-
cfg,
|
|
61
|
-
api: refreshable,
|
|
62
|
-
};
|
|
63
|
-
}
|
|
64
|
-
|
|
65
|
-
function labelName(item) {
|
|
66
|
-
const name = item.name || item.slug || item.id;
|
|
67
|
-
const slug = item.slug && item.slug !== name ? ` (${item.slug})` : "";
|
|
68
|
-
return `${name}${slug}`;
|
|
69
|
-
}
|
|
70
|
-
|
|
71
|
-
function teamRef(team) {
|
|
72
|
-
return team.slug || team.id;
|
|
73
|
-
}
|
|
74
|
-
|
|
75
|
-
function deploymentLabel(deployment) {
|
|
76
|
-
const bits = [deployment.name];
|
|
77
|
-
if (deployment.deploymentType || deployment.type) {
|
|
78
|
-
bits.push(deployment.deploymentType || deployment.type);
|
|
79
|
-
}
|
|
80
|
-
if (deployment.status) {
|
|
81
|
-
bits.push(deployment.status);
|
|
82
|
-
}
|
|
83
|
-
return bits.filter(Boolean).join(" - ");
|
|
84
|
-
}
|
|
85
|
-
|
|
86
|
-
function deploymentType(deployment) {
|
|
87
|
-
return deployment.deploymentType || deployment.type || "";
|
|
88
|
-
}
|
|
89
|
-
|
|
90
|
-
function sortDeploymentsForChoice(deployments) {
|
|
91
|
-
return [...deployments].sort((a, b) => {
|
|
92
|
-
if (!!a.isDefault !== !!b.isDefault) {
|
|
93
|
-
return a.isDefault ? -1 : 1;
|
|
94
|
-
}
|
|
95
|
-
return String(b.createTime || b.createdAt || "").localeCompare(String(a.createTime || a.createdAt || ""));
|
|
96
|
-
});
|
|
97
|
-
}
|
|
98
|
-
|
|
99
|
-
async function chooseDeploymentForType(type, deployments) {
|
|
100
|
-
const matches = sortDeploymentsForChoice(
|
|
101
|
-
deployments.filter((d) => deploymentType(d) === type && d.status !== "deleted"),
|
|
102
|
-
);
|
|
103
|
-
if (matches.length === 0) {
|
|
104
|
-
return null;
|
|
105
|
-
}
|
|
106
|
-
return await choose(
|
|
107
|
-
`${type} deployments`,
|
|
108
|
-
matches.map((d) => ({ label: deploymentLabel(d), value: d })),
|
|
109
|
-
);
|
|
110
|
-
}
|
|
22
|
+
async function main(argv) {
|
|
23
|
+
// Strip --json from any position so commands see clean positionals.
|
|
24
|
+
const { json, rest: cleanArgv } = extractJsonFlag(argv);
|
|
111
25
|
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
while (index < args.length) {
|
|
116
|
-
const arg = args[index];
|
|
117
|
-
if (arg === "--target") {
|
|
118
|
-
target = args[index + 1];
|
|
119
|
-
if (!target) {
|
|
120
|
-
throw new Error("--target requires dev or prod");
|
|
121
|
-
}
|
|
122
|
-
index += 2;
|
|
123
|
-
continue;
|
|
124
|
-
}
|
|
125
|
-
if (arg && arg.startsWith("--target=")) {
|
|
126
|
-
target = arg.slice("--target=".length);
|
|
127
|
-
index += 1;
|
|
128
|
-
continue;
|
|
129
|
-
}
|
|
130
|
-
break;
|
|
131
|
-
}
|
|
132
|
-
if (target && target !== "dev" && target !== "prod") {
|
|
133
|
-
throw new Error("--target must be dev or prod");
|
|
26
|
+
// help / no-args → root help.
|
|
27
|
+
if (cleanArgv.length === 0 || cleanArgv[0] === "help" || cleanArgv[0] === "-h" || cleanArgv[0] === "--help") {
|
|
28
|
+
return renderRootHelp(REGISTRY);
|
|
134
29
|
}
|
|
135
|
-
return {
|
|
136
|
-
explicitTarget: Boolean(target),
|
|
137
|
-
target,
|
|
138
|
-
args: args.slice(index),
|
|
139
|
-
};
|
|
140
|
-
}
|
|
141
30
|
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
function parseConvexInvocation(args) {
|
|
148
|
-
const parsed = parseConvexTarget(args);
|
|
149
|
-
return {
|
|
150
|
-
...parsed,
|
|
151
|
-
target: parsed.target || inferConvexTarget(parsed.args),
|
|
152
|
-
};
|
|
153
|
-
}
|
|
154
|
-
|
|
155
|
-
async function resolveConvexInvocation(args, { cfg = null, api = null, projectDir = process.cwd() } = {}) {
|
|
156
|
-
const parsed = parseConvexInvocation(args);
|
|
157
|
-
const projectConfig = readProjectConfig(projectDir);
|
|
158
|
-
if (!projectConfig) {
|
|
159
|
-
if (parsed.explicitTarget) {
|
|
160
|
-
throw new Error("No Synapse project metadata found. Run `synapse select` first.");
|
|
161
|
-
}
|
|
162
|
-
return {
|
|
163
|
-
...parsed,
|
|
164
|
-
credentials: null,
|
|
165
|
-
deploymentName: "",
|
|
166
|
-
projectConfig: null,
|
|
167
|
-
target: null,
|
|
168
|
-
};
|
|
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;
|
|
169
36
|
}
|
|
170
37
|
|
|
171
|
-
if (
|
|
172
|
-
|
|
173
|
-
}
|
|
174
|
-
if (
|
|
175
|
-
projectConfig.synapseUrl &&
|
|
176
|
-
cfg.baseUrl &&
|
|
177
|
-
normalizeBaseUrl(projectConfig.synapseUrl) !== normalizeBaseUrl(cfg.baseUrl)
|
|
178
|
-
) {
|
|
179
|
-
throw new Error(
|
|
180
|
-
`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.`,
|
|
181
|
-
);
|
|
38
|
+
if (wantsHelp(rest)) {
|
|
39
|
+
return renderCommandHelp(cmd);
|
|
182
40
|
}
|
|
183
41
|
|
|
184
|
-
const
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
}
|
|
188
|
-
const credentials = await api.cliCredentials(deploymentName);
|
|
189
|
-
return {
|
|
190
|
-
...parsed,
|
|
191
|
-
credentials,
|
|
192
|
-
deploymentName,
|
|
193
|
-
projectConfig,
|
|
194
|
-
};
|
|
42
|
+
const out = createOutput({ json });
|
|
43
|
+
const ctx = createContext({ out });
|
|
44
|
+
return await cmd.run(rest, ctx);
|
|
195
45
|
}
|
|
196
46
|
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
return creds.envSnippet || `CONVEX_SELF_HOSTED_URL=${quoteEnvValue(creds.convexUrl)}\nCONVEX_SELF_HOSTED_ADMIN_KEY=${quoteEnvValue(creds.adminKey)}`;
|
|
205
|
-
default:
|
|
206
|
-
throw new Error("format must be one of: env, shell, json");
|
|
207
|
-
}
|
|
208
|
-
}
|
|
209
|
-
|
|
210
|
-
function parseFormat(args) {
|
|
211
|
-
let format = "env";
|
|
212
|
-
const rest = [];
|
|
213
|
-
for (let i = 0; i < args.length; i += 1) {
|
|
214
|
-
const arg = args[i];
|
|
215
|
-
if (arg === "--format") {
|
|
216
|
-
format = args[i + 1];
|
|
217
|
-
i += 1;
|
|
218
|
-
} else if (arg.startsWith("--format=")) {
|
|
219
|
-
format = arg.slice("--format=".length);
|
|
220
|
-
} else {
|
|
221
|
-
rest.push(arg);
|
|
47
|
+
if (require.main === module) {
|
|
48
|
+
main(process.argv.slice(2)).catch((err) => {
|
|
49
|
+
process.stderr.write(`${err.message}\n`);
|
|
50
|
+
if (err && err.code === "network_error") {
|
|
51
|
+
process.stderr.write(
|
|
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",
|
|
53
|
+
);
|
|
222
54
|
}
|
|
223
|
-
|
|
224
|
-
return { format, rest };
|
|
225
|
-
}
|
|
226
|
-
|
|
227
|
-
async function login(args) {
|
|
228
|
-
const url = args[0];
|
|
229
|
-
if (!url) {
|
|
230
|
-
throw new Error("Usage: synapse login <url>");
|
|
231
|
-
}
|
|
232
|
-
const baseUrl = normalizeBaseUrl(url);
|
|
233
|
-
const { email, password } = await askCredentials();
|
|
234
|
-
const api = new SynapseAPI({ baseUrl });
|
|
235
|
-
const session = await api.login(email, password);
|
|
236
|
-
if (!session.accessToken) {
|
|
237
|
-
throw new Error("Synapse login response did not include accessToken");
|
|
238
|
-
}
|
|
239
|
-
const file = writeConfig({
|
|
240
|
-
baseUrl,
|
|
241
|
-
accessToken: session.accessToken,
|
|
242
|
-
refreshToken: session.refreshToken || null,
|
|
243
|
-
tokenType: session.tokenType || "Bearer",
|
|
244
|
-
user: session.user || null,
|
|
55
|
+
process.exitCode = 1;
|
|
245
56
|
});
|
|
246
|
-
process.stderr.write(`Saved Synapse session to ${file}\n`);
|
|
247
57
|
}
|
|
248
58
|
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
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.
|
|
253
64
|
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
}
|
|
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");
|
|
261
71
|
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
const
|
|
268
|
-
const
|
|
269
|
-
const
|
|
270
|
-
|
|
271
|
-
throw new Error("No dev deployments available in this project. Create one first.");
|
|
272
|
-
}
|
|
273
|
-
const prod = await chooseDeploymentForType("prod", deployments);
|
|
274
|
-
const projectPath = writeProjectConfig(
|
|
275
|
-
process.cwd(),
|
|
276
|
-
buildProjectConfig({
|
|
277
|
-
synapseUrl: cfg.baseUrl,
|
|
278
|
-
team,
|
|
279
|
-
project,
|
|
280
|
-
deployments: { dev, prod },
|
|
281
|
-
}),
|
|
282
|
-
);
|
|
283
|
-
const creds = await api.cliCredentials(dev.name);
|
|
284
|
-
const envPath = writeProjectEnv(process.cwd(), creds);
|
|
285
|
-
process.stderr.write(`Linked ${labelName(project)} to ${projectPath}.\n`);
|
|
286
|
-
process.stderr.write(`Selected dev deployment ${dev.name}. Updated ${envPath}.\n`);
|
|
287
|
-
if (prod) {
|
|
288
|
-
process.stderr.write(`Selected prod deployment ${prod.name}.\n`);
|
|
289
|
-
} else {
|
|
290
|
-
process.stderr.write("Warning: no prod deployment found. `synapse convex deploy` will require a prod deployment saved by `synapse select`.\n");
|
|
291
|
-
}
|
|
292
|
-
if (process.env.CONVEX_DEPLOYMENT) {
|
|
293
|
-
process.stderr.write("Warning: shell CONVEX_DEPLOYMENT is set. Use `synapse convex ...` or unset it before running `npx convex` directly.\n");
|
|
294
|
-
}
|
|
295
|
-
}
|
|
296
|
-
|
|
297
|
-
async function credentials(args) {
|
|
298
|
-
const { format, rest } = parseFormat(args);
|
|
299
|
-
const deployment = rest[0];
|
|
300
|
-
if (!deployment) {
|
|
301
|
-
throw new Error("Usage: synapse credentials <deployment> [--format env|shell|json]");
|
|
302
|
-
}
|
|
303
|
-
if (!["env", "shell", "json"].includes(format)) {
|
|
304
|
-
throw new Error("format must be one of: env, shell, json");
|
|
305
|
-
}
|
|
306
|
-
const { api } = clientFromConfig();
|
|
307
|
-
const creds = await api.cliCredentials(deployment);
|
|
308
|
-
process.stdout.write(formatCredentials(creds, format) + "\n");
|
|
309
|
-
}
|
|
310
|
-
|
|
311
|
-
async function convex(args) {
|
|
312
|
-
const projectConfig = readProjectConfig(process.cwd());
|
|
313
|
-
let resolved = {
|
|
314
|
-
args,
|
|
315
|
-
credentials: null,
|
|
316
|
-
deploymentName: "",
|
|
317
|
-
target: null,
|
|
318
|
-
};
|
|
319
|
-
if (projectConfig) {
|
|
320
|
-
const { cfg, api } = clientFromConfig();
|
|
321
|
-
resolved = await resolveConvexInvocation(args, { cfg, api });
|
|
322
|
-
process.stderr.write(`Using Synapse ${resolved.target} deployment ${resolved.deploymentName}.\n`);
|
|
323
|
-
} else {
|
|
324
|
-
resolved = await resolveConvexInvocation(args);
|
|
325
|
-
}
|
|
326
|
-
const code = await runConvex(resolved.args, { credentials: resolved.credentials });
|
|
327
|
-
process.exitCode = code;
|
|
328
|
-
}
|
|
329
|
-
|
|
330
|
-
async function main(argv) {
|
|
331
|
-
const [command, ...args] = argv;
|
|
332
|
-
switch (command) {
|
|
333
|
-
case "login":
|
|
334
|
-
return await login(args);
|
|
335
|
-
case "logout":
|
|
336
|
-
return await logout();
|
|
337
|
-
case "whoami":
|
|
338
|
-
return await whoami();
|
|
339
|
-
case "select":
|
|
340
|
-
return await selectDeployment();
|
|
341
|
-
case "credentials":
|
|
342
|
-
return await credentials(args);
|
|
343
|
-
case "convex":
|
|
344
|
-
return await convex(args);
|
|
345
|
-
case "-h":
|
|
346
|
-
case "--help":
|
|
347
|
-
case "help":
|
|
348
|
-
case undefined:
|
|
349
|
-
process.stdout.write(usage());
|
|
350
|
-
return;
|
|
351
|
-
default:
|
|
352
|
-
throw new Error(`Unknown command: ${command}\n\n${usage()}`);
|
|
353
|
-
}
|
|
354
|
-
}
|
|
355
|
-
|
|
356
|
-
if (require.main === module) {
|
|
357
|
-
main(process.argv.slice(2)).catch((err) => {
|
|
358
|
-
process.stderr.write(`${err.message}\n`);
|
|
359
|
-
process.exitCode = 1;
|
|
360
|
-
});
|
|
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 };
|
|
361
81
|
}
|
|
362
82
|
|
|
363
83
|
module.exports = {
|
|
364
|
-
chooseDeploymentForType,
|
|
365
|
-
clientFromConfig,
|
|
366
|
-
formatCredentials,
|
|
367
|
-
inferConvexTarget,
|
|
368
84
|
main,
|
|
369
|
-
|
|
370
|
-
|
|
371
|
-
|
|
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,
|
|
372
98
|
};
|
package/lib/api.js
CHANGED
|
@@ -75,10 +75,23 @@ class SynapseAPI {
|
|
|
75
75
|
pageURL.searchParams.set("cursor", cursor);
|
|
76
76
|
}
|
|
77
77
|
const page = await this.request("GET", `${pageURL.pathname}${pageURL.search}`, undefined, { includeHeaders: true });
|
|
78
|
-
|
|
79
|
-
|
|
78
|
+
// Backend currently returns a bare JSON array for every paginated
|
|
79
|
+
// endpoint, but the dashboard already tolerates both `[...]` and
|
|
80
|
+
// `{ <noun>: [...] }` (see `collectPaginated` in dashboard/lib/api.ts).
|
|
81
|
+
// Mirror that resilience here so a future server-side reshape doesn't
|
|
82
|
+
// brick `synapse select` for every CLI user simultaneously.
|
|
83
|
+
const arr = extractListPayload(page.data);
|
|
84
|
+
if (arr === null) {
|
|
85
|
+
const shape = page.data && typeof page.data === "object"
|
|
86
|
+
? `object with keys [${Object.keys(page.data).join(", ")}]`
|
|
87
|
+
: typeof page.data;
|
|
88
|
+
throw new SynapseAPIError(
|
|
89
|
+
0,
|
|
90
|
+
"bad_response",
|
|
91
|
+
`Expected ${path} to return a JSON array (got ${shape})`,
|
|
92
|
+
);
|
|
80
93
|
}
|
|
81
|
-
items.push(...
|
|
94
|
+
items.push(...arr);
|
|
82
95
|
cursor = page.headers.get("x-next-cursor") || "";
|
|
83
96
|
} while (cursor);
|
|
84
97
|
return items;
|
|
@@ -113,7 +126,36 @@ class SynapseAPI {
|
|
|
113
126
|
}
|
|
114
127
|
}
|
|
115
128
|
|
|
129
|
+
// Known envelope keys, in priority order. We try these explicitly before
|
|
130
|
+
// falling back to a generic "first array-valued property" lookup so a
|
|
131
|
+
// future endpoint that wraps results in `{ items: [...] }` (or similar)
|
|
132
|
+
// keeps working. The fallback handles the unlikely case of a renamed
|
|
133
|
+
// envelope without crashing the CLI.
|
|
134
|
+
const KNOWN_LIST_KEYS = [
|
|
135
|
+
"teams",
|
|
136
|
+
"projects",
|
|
137
|
+
"deployments",
|
|
138
|
+
"members",
|
|
139
|
+
"items",
|
|
140
|
+
"data",
|
|
141
|
+
"results",
|
|
142
|
+
];
|
|
143
|
+
|
|
144
|
+
function extractListPayload(data) {
|
|
145
|
+
if (Array.isArray(data)) return data;
|
|
146
|
+
if (data && typeof data === "object") {
|
|
147
|
+
for (const key of KNOWN_LIST_KEYS) {
|
|
148
|
+
if (Array.isArray(data[key])) return data[key];
|
|
149
|
+
}
|
|
150
|
+
for (const value of Object.values(data)) {
|
|
151
|
+
if (Array.isArray(value)) return value;
|
|
152
|
+
}
|
|
153
|
+
}
|
|
154
|
+
return null;
|
|
155
|
+
}
|
|
156
|
+
|
|
116
157
|
module.exports = {
|
|
117
158
|
SynapseAPI,
|
|
118
159
|
SynapseAPIError,
|
|
160
|
+
extractListPayload,
|
|
119
161
|
};
|
package/lib/colors.js
ADDED
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
// Zero-dependency ANSI helpers. Each helper returns its argument as-is when
|
|
2
|
+
// the terminal does not support colour (stdout is not a TTY) or the user
|
|
3
|
+
// opted out via NO_COLOR (https://no-color.org). Checked at call time, not
|
|
4
|
+
// at require time, so tests that toggle `process.env.NO_COLOR` or that run
|
|
5
|
+
// under `node --test` (no TTY) get plain output without further wiring.
|
|
6
|
+
//
|
|
7
|
+
// We deliberately do not depend on `chalk` / `kleur` / etc — the @iann29/
|
|
8
|
+
// synapse package ships zero runtime deps, and a handful of escape codes
|
|
9
|
+
// don't justify breaking that.
|
|
10
|
+
|
|
11
|
+
function enabled(stream = process.stdout) {
|
|
12
|
+
if (process.env.NO_COLOR) return false;
|
|
13
|
+
if (process.env.FORCE_COLOR === "1" || process.env.FORCE_COLOR === "true") return true;
|
|
14
|
+
return Boolean(stream && stream.isTTY);
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
function wrap(code) {
|
|
18
|
+
return (value, stream) =>
|
|
19
|
+
enabled(stream) ? `\x1b[${code}m${value}\x1b[0m` : String(value);
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
// Renders the status portion of a deployment row. `running` reads as success,
|
|
23
|
+
// `failed` as error, `provisioning` as in-flight, anything else as dim noise.
|
|
24
|
+
// Falls through to the plain string when colour is off so logs grep cleanly.
|
|
25
|
+
function statusBadge(status, stream) {
|
|
26
|
+
switch (status) {
|
|
27
|
+
case "running":
|
|
28
|
+
return module.exports.green(status, stream);
|
|
29
|
+
case "failed":
|
|
30
|
+
case "errored":
|
|
31
|
+
return module.exports.red(status, stream);
|
|
32
|
+
case "provisioning":
|
|
33
|
+
return module.exports.yellow(status, stream);
|
|
34
|
+
case "stopped":
|
|
35
|
+
case "deleted":
|
|
36
|
+
return module.exports.dim(status, stream);
|
|
37
|
+
default:
|
|
38
|
+
return String(status || "");
|
|
39
|
+
}
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
module.exports = {
|
|
43
|
+
enabled,
|
|
44
|
+
bold: wrap("1"),
|
|
45
|
+
dim: wrap("2"),
|
|
46
|
+
red: wrap("31"),
|
|
47
|
+
green: wrap("32"),
|
|
48
|
+
yellow: wrap("33"),
|
|
49
|
+
cyan: wrap("36"),
|
|
50
|
+
statusBadge,
|
|
51
|
+
};
|