@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
|
@@ -0,0 +1,173 @@
|
|
|
1
|
+
// `synapse status` — quick summary of every deployment in the linked
|
|
2
|
+
// project. Status icon + type badge + URL + URL form chip. Mirrors
|
|
3
|
+
// what the dashboard's project page shows, in 1 terminal screen.
|
|
4
|
+
//
|
|
5
|
+
// Works without `synapse select` IF --project=<id> is passed. Otherwise
|
|
6
|
+
// reads .synapse/project.json from cwd.
|
|
7
|
+
|
|
8
|
+
const colors = require("../colors");
|
|
9
|
+
|
|
10
|
+
// Same heuristic as dashboard/app/embed/[name]/page.tsx:isBrowserReachableURL.
|
|
11
|
+
// We classify the URL form so the operator knows at a glance which
|
|
12
|
+
// deployments are reachable from the browser vs which fall back to
|
|
13
|
+
// the host:port form that needs SYNAPSE_BASE_DOMAIN or a custom domain.
|
|
14
|
+
function urlForm(rawUrl, publicUrl) {
|
|
15
|
+
if (!rawUrl) return "unknown";
|
|
16
|
+
let u;
|
|
17
|
+
try {
|
|
18
|
+
u = new URL(rawUrl);
|
|
19
|
+
} catch {
|
|
20
|
+
return "unknown";
|
|
21
|
+
}
|
|
22
|
+
if (u.pathname.startsWith("/d/")) return "path";
|
|
23
|
+
const STANDARD = new Set(["", "443", "80", "6791"]);
|
|
24
|
+
if (
|
|
25
|
+
!STANDARD.has(u.port) &&
|
|
26
|
+
u.hostname !== "localhost" &&
|
|
27
|
+
u.hostname !== "127.0.0.1"
|
|
28
|
+
) {
|
|
29
|
+
return "host";
|
|
30
|
+
}
|
|
31
|
+
if (!publicUrl) return "custom";
|
|
32
|
+
try {
|
|
33
|
+
const pu = new URL(publicUrl);
|
|
34
|
+
if (u.hostname === pu.hostname) return "custom";
|
|
35
|
+
if (u.hostname.endsWith("." + pu.hostname)) return "wildcard";
|
|
36
|
+
return "custom";
|
|
37
|
+
} catch {
|
|
38
|
+
return "custom";
|
|
39
|
+
}
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
function parseFlags(args) {
|
|
43
|
+
let projectId = null;
|
|
44
|
+
const rest = [];
|
|
45
|
+
for (let i = 0; i < args.length; i += 1) {
|
|
46
|
+
const arg = args[i];
|
|
47
|
+
if (arg === "--project") {
|
|
48
|
+
projectId = args[i + 1];
|
|
49
|
+
i += 1;
|
|
50
|
+
} else if (arg.startsWith("--project=")) {
|
|
51
|
+
projectId = arg.slice("--project=".length);
|
|
52
|
+
} else {
|
|
53
|
+
rest.push(arg);
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
return { projectId, rest };
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
module.exports = {
|
|
60
|
+
name: "status",
|
|
61
|
+
summary: "List deployments in the linked project with status + URL form.",
|
|
62
|
+
usage: "synapse status [--project=<id>] [--json]",
|
|
63
|
+
description: `Lists every deployment in the linked project (or the project given by --project=<id>) along with its status, URL, and URL form ('custom' / 'wildcard' / 'path' / 'host').
|
|
64
|
+
|
|
65
|
+
The URL form tells you whether the deployment is reachable from a browser:
|
|
66
|
+
custom bound to a custom domain (api.client.com) — reachable
|
|
67
|
+
wildcard uses SYNAPSE_BASE_DOMAIN subdomain — reachable
|
|
68
|
+
path /d/<name>/* via the proxy — browser-reachable but CLI-broken
|
|
69
|
+
(the Convex CLI strips paths from base URLs)
|
|
70
|
+
host host:port form — NOT reachable from a normal browser
|
|
71
|
+
(Caddy doesn't TLS-front dynamic ports)
|
|
72
|
+
|
|
73
|
+
Run \`synapse doctor\` for a deeper health check.`,
|
|
74
|
+
|
|
75
|
+
// Re-exports for tests.
|
|
76
|
+
urlForm,
|
|
77
|
+
parseFlags,
|
|
78
|
+
|
|
79
|
+
async run(args, ctx) {
|
|
80
|
+
const { projectId: explicitProjectId } = parseFlags(args);
|
|
81
|
+
let projectId = explicitProjectId;
|
|
82
|
+
let projectLabel = null;
|
|
83
|
+
if (!projectId) {
|
|
84
|
+
const pc = ctx.requireProject();
|
|
85
|
+
projectId = pc.project.id;
|
|
86
|
+
projectLabel = pc.project.name || pc.project.slug;
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
const deployments = await ctx.api.deployments(projectId);
|
|
90
|
+
const baseUrl = ctx.cfg.baseUrl;
|
|
91
|
+
|
|
92
|
+
const rows = deployments.map((d) => {
|
|
93
|
+
const type = d.deploymentType || d.type || "";
|
|
94
|
+
const url = d.deploymentUrl || d.url || "";
|
|
95
|
+
return {
|
|
96
|
+
name: d.name,
|
|
97
|
+
type,
|
|
98
|
+
status: d.status,
|
|
99
|
+
url,
|
|
100
|
+
urlForm: urlForm(url, baseUrl),
|
|
101
|
+
isDefault: !!d.isDefault,
|
|
102
|
+
adopted: !!d.adopted,
|
|
103
|
+
haEnabled: !!d.haEnabled,
|
|
104
|
+
replicaCount: d.replicaCount,
|
|
105
|
+
};
|
|
106
|
+
});
|
|
107
|
+
|
|
108
|
+
ctx.out.result(
|
|
109
|
+
{ projectId, project: projectLabel, deployments: rows },
|
|
110
|
+
(data, { stdout }) => {
|
|
111
|
+
if (data.project) {
|
|
112
|
+
stdout.write(colors.bold(`Project: ${data.project}\n`));
|
|
113
|
+
}
|
|
114
|
+
if (rows.length === 0) {
|
|
115
|
+
stdout.write(colors.dim("(no deployments)\n"));
|
|
116
|
+
return;
|
|
117
|
+
}
|
|
118
|
+
const tag = (form) => {
|
|
119
|
+
switch (form) {
|
|
120
|
+
case "custom":
|
|
121
|
+
return colors.green("custom");
|
|
122
|
+
case "wildcard":
|
|
123
|
+
return colors.green("wildcard");
|
|
124
|
+
case "path":
|
|
125
|
+
return colors.dim("path");
|
|
126
|
+
case "host":
|
|
127
|
+
return colors.red("no-domain");
|
|
128
|
+
default:
|
|
129
|
+
return colors.dim("?");
|
|
130
|
+
}
|
|
131
|
+
};
|
|
132
|
+
ctx.out.table(
|
|
133
|
+
rows.map((r) => ({
|
|
134
|
+
name: colors.bold(r.name),
|
|
135
|
+
type: typeBadge(r.type),
|
|
136
|
+
status: colors.statusBadge(r.status || ""),
|
|
137
|
+
urlForm: tag(r.urlForm),
|
|
138
|
+
url: r.url,
|
|
139
|
+
})),
|
|
140
|
+
[
|
|
141
|
+
{ key: "name", header: "NAME" },
|
|
142
|
+
{ key: "type", header: "TYPE" },
|
|
143
|
+
{ key: "status", header: "STATUS" },
|
|
144
|
+
{ key: "urlForm", header: "FORM" },
|
|
145
|
+
{ key: "url", header: "URL" },
|
|
146
|
+
],
|
|
147
|
+
);
|
|
148
|
+
const broken = rows.filter((r) => r.urlForm === "host").length;
|
|
149
|
+
if (broken > 0) {
|
|
150
|
+
stdout.write("\n");
|
|
151
|
+
ctx.out.warn(
|
|
152
|
+
`${broken} deployment${broken > 1 ? "s" : ""} not browser-reachable — set SYNAPSE_BASE_DOMAIN on the server OR add a custom domain per deployment. Run \`synapse doctor\` for the full diagnosis.`,
|
|
153
|
+
);
|
|
154
|
+
}
|
|
155
|
+
},
|
|
156
|
+
);
|
|
157
|
+
},
|
|
158
|
+
};
|
|
159
|
+
|
|
160
|
+
// Tiny tone helper that mirrors the dashboard's envTone() so colors
|
|
161
|
+
// stay consistent across UI surfaces.
|
|
162
|
+
function typeBadge(t) {
|
|
163
|
+
switch (t) {
|
|
164
|
+
case "prod":
|
|
165
|
+
return colors.yellow(t.toUpperCase());
|
|
166
|
+
case "dev":
|
|
167
|
+
return colors.cyan ? colors.cyan(t.toUpperCase()) : t.toUpperCase();
|
|
168
|
+
case "preview":
|
|
169
|
+
return colors.dim(t.toUpperCase());
|
|
170
|
+
default:
|
|
171
|
+
return t ? colors.dim(t) : "";
|
|
172
|
+
}
|
|
173
|
+
}
|
|
@@ -0,0 +1,77 @@
|
|
|
1
|
+
// `synapse version` — print the CLI's own version, plus the version of
|
|
2
|
+
// the Synapse backend it's logged into (best-effort), plus Node + OS.
|
|
3
|
+
//
|
|
4
|
+
// Designed to be the first thing operators run when reporting a bug —
|
|
5
|
+
// the data here is enough to triage without further questions.
|
|
6
|
+
|
|
7
|
+
const os = require("node:os");
|
|
8
|
+
const pkg = require("../../package.json");
|
|
9
|
+
|
|
10
|
+
module.exports = {
|
|
11
|
+
name: "version",
|
|
12
|
+
summary: "Show CLI, backend, Node and OS versions.",
|
|
13
|
+
usage: "synapse version [--json]",
|
|
14
|
+
description: `Reports:
|
|
15
|
+
cli this package's npm version (from package.json)
|
|
16
|
+
backend the Synapse instance's version (via /v1/install_status)
|
|
17
|
+
node runtime version
|
|
18
|
+
platform os + arch
|
|
19
|
+
|
|
20
|
+
If you're not logged in, backend is reported as null with a reason.
|
|
21
|
+
Output is stable across releases — safe to grep in CI logs.`,
|
|
22
|
+
|
|
23
|
+
async run(_args, ctx) {
|
|
24
|
+
const cliVersion = pkg.version;
|
|
25
|
+
const node = process.version;
|
|
26
|
+
const platform = `${os.platform()} ${os.release()} (${process.arch})`;
|
|
27
|
+
|
|
28
|
+
let backend = null;
|
|
29
|
+
let backendError = null;
|
|
30
|
+
const cfg = ctx.cfgOrNull;
|
|
31
|
+
if (!cfg || !cfg.baseUrl) {
|
|
32
|
+
backendError = "not logged in";
|
|
33
|
+
} else {
|
|
34
|
+
try {
|
|
35
|
+
// install_status is public — no auth needed, no refresh races.
|
|
36
|
+
// Use the unauthenticated SynapseAPI so a 401 on /me/ doesn't
|
|
37
|
+
// muddy this purely informational call.
|
|
38
|
+
const { SynapseAPI } = require("../api");
|
|
39
|
+
const probe = new SynapseAPI({ baseUrl: cfg.baseUrl });
|
|
40
|
+
const status = await probe.request(
|
|
41
|
+
"GET",
|
|
42
|
+
"/v1/install_status",
|
|
43
|
+
undefined,
|
|
44
|
+
{ auth: false },
|
|
45
|
+
);
|
|
46
|
+
backend = { url: cfg.baseUrl, version: status.version, firstRun: status.firstRun };
|
|
47
|
+
} catch (err) {
|
|
48
|
+
backendError = err && err.message ? err.message : String(err);
|
|
49
|
+
backend = { url: cfg.baseUrl, version: null };
|
|
50
|
+
}
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
const payload = {
|
|
54
|
+
cli: cliVersion,
|
|
55
|
+
backend,
|
|
56
|
+
backendError,
|
|
57
|
+
node,
|
|
58
|
+
platform,
|
|
59
|
+
};
|
|
60
|
+
|
|
61
|
+
ctx.out.result(payload, (d, { stdout }) => {
|
|
62
|
+
const widest = "platform".length;
|
|
63
|
+
const pad = (k) => k.padEnd(widest);
|
|
64
|
+
stdout.write(`${pad("cli")} ${d.cli}\n`);
|
|
65
|
+
if (d.backend) {
|
|
66
|
+
const v = d.backend.version ? `${d.backend.version}` : "(unknown)";
|
|
67
|
+
const reason = d.backendError ? ` — ${d.backendError}` : "";
|
|
68
|
+
stdout.write(`${pad("backend")} ${v}${reason}\n`);
|
|
69
|
+
stdout.write(`${pad("")} at ${d.backend.url}\n`);
|
|
70
|
+
} else {
|
|
71
|
+
stdout.write(`${pad("backend")} (not logged in)\n`);
|
|
72
|
+
}
|
|
73
|
+
stdout.write(`${pad("node")} ${d.node}\n`);
|
|
74
|
+
stdout.write(`${pad("platform")} ${d.platform}\n`);
|
|
75
|
+
});
|
|
76
|
+
},
|
|
77
|
+
};
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
// `synapse whoami` — confirm the saved session is alive against the
|
|
2
|
+
// backend. Hits /v1/me; if the access token is expired but the
|
|
3
|
+
// refresh token still works, the api proxy silently rotates the
|
|
4
|
+
// bundle transparently before reporting OK.
|
|
5
|
+
|
|
6
|
+
module.exports = {
|
|
7
|
+
name: "whoami",
|
|
8
|
+
summary: "Show the email + URL the saved session is authenticated for.",
|
|
9
|
+
usage: "synapse whoami",
|
|
10
|
+
description: `Calls /v1/me/ on the backend. Returns the user's display name + email + the base URL of the Synapse instance. Triggers silent-refresh under the hood if the access token has expired but the refresh token is still good.`,
|
|
11
|
+
|
|
12
|
+
async run(_args, ctx) {
|
|
13
|
+
const me = await ctx.api.me();
|
|
14
|
+
const email = me.email || me.user?.email || "(unknown email)";
|
|
15
|
+
const name = me.name || me.user?.name || "";
|
|
16
|
+
ctx.out.result(
|
|
17
|
+
{ email, name, baseUrl: ctx.cfg.baseUrl },
|
|
18
|
+
({ email, name, baseUrl }, { stdout }) =>
|
|
19
|
+
stdout.write(`${name ? `${name} ` : ""}<${email}> on ${baseUrl}\n`),
|
|
20
|
+
);
|
|
21
|
+
},
|
|
22
|
+
};
|