@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,86 @@
|
|
|
1
|
+
// `synapse doctor` — the health-check battery. Walks ~12 checks
|
|
2
|
+
// across 4 categories (local-env, project, backend, deployments)
|
|
3
|
+
// in parallel where possible, then renders a panorama.
|
|
4
|
+
//
|
|
5
|
+
// Output is either a pretty terminal report (default) or a stable
|
|
6
|
+
// machine-readable JSON tree (--json). Exit codes: 0 ok, 1 warnings
|
|
7
|
+
// only, 2 at least one issue.
|
|
8
|
+
|
|
9
|
+
const path = require("node:path");
|
|
10
|
+
const os = require("node:os");
|
|
11
|
+
const { configPath } = require("../config");
|
|
12
|
+
const { generateReport, SCHEMA_VERSION } = require("../doctor/runner");
|
|
13
|
+
const { renderReport } = require("../doctor/renderer");
|
|
14
|
+
|
|
15
|
+
function parseFlags(args) {
|
|
16
|
+
let fix = false;
|
|
17
|
+
let yes = false;
|
|
18
|
+
let verbose = false;
|
|
19
|
+
const rest = [];
|
|
20
|
+
for (const a of args) {
|
|
21
|
+
if (a === "--fix") fix = true;
|
|
22
|
+
else if (a === "--yes" || a === "-y") yes = true;
|
|
23
|
+
else if (a === "--verbose" || a === "-v") verbose = true;
|
|
24
|
+
else rest.push(a);
|
|
25
|
+
}
|
|
26
|
+
return { fix, yes, verbose, rest };
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
module.exports = {
|
|
30
|
+
name: "doctor",
|
|
31
|
+
summary: "Run a health-check panorama on your local setup + Synapse backend + deployments.",
|
|
32
|
+
usage: "synapse doctor [--fix] [--yes] [--verbose] [--json]",
|
|
33
|
+
description: `Runs ~12 checks across local environment, project metadata, backend connectivity, and per-deployment health. Reports each check, the bottom-line totals, and exits with status:
|
|
34
|
+
|
|
35
|
+
0 everything ok
|
|
36
|
+
1 at least one warning (still functional)
|
|
37
|
+
2 at least one issue (probably blocks your next deploy)
|
|
38
|
+
|
|
39
|
+
Flags:
|
|
40
|
+
--fix apply safe fixes automatically (chmod, gitignore appends, etc).
|
|
41
|
+
Checks marked 'prompt' require --yes too.
|
|
42
|
+
--yes upgrade 'prompt' fixes to auto. Combined with --fix, also
|
|
43
|
+
cleans up a stale .synapse/project.json — re-links if the
|
|
44
|
+
project was transferred to another of your teams,
|
|
45
|
+
otherwise marks the entry stale so \`synapse select\` can
|
|
46
|
+
start fresh.
|
|
47
|
+
--verbose show detailed data per check.
|
|
48
|
+
--json stable schema (current: v${SCHEMA_VERSION}); CI-friendly.
|
|
49
|
+
|
|
50
|
+
The --json output schema is documented in cli/schema/doctor-v${SCHEMA_VERSION}.json.`,
|
|
51
|
+
|
|
52
|
+
async run(args, ctx) {
|
|
53
|
+
const { fix, yes, verbose } = parseFlags(args);
|
|
54
|
+
// Build doctor's ctx — augment the dispatcher ctx with helpers
|
|
55
|
+
// checks expect (cfgPath for the home config, env for shell vars).
|
|
56
|
+
let api = null;
|
|
57
|
+
try {
|
|
58
|
+
// ctx.api throws if not logged in; we want checks to handle the
|
|
59
|
+
// "not logged in" case gracefully via cfgOrNull, so build the
|
|
60
|
+
// api lazily and only when we have a session.
|
|
61
|
+
if (ctx.cfgOrNull && ctx.cfgOrNull.accessToken) {
|
|
62
|
+
api = ctx.api;
|
|
63
|
+
}
|
|
64
|
+
} catch {
|
|
65
|
+
api = null;
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
const doctorCtx = {
|
|
69
|
+
cwd: ctx.cwd,
|
|
70
|
+
env: ctx.env,
|
|
71
|
+
cfg: ctx.cfgOrNull,
|
|
72
|
+
api,
|
|
73
|
+
projectConfig: ctx.projectConfig,
|
|
74
|
+
cfgPath: configPath(),
|
|
75
|
+
homedir: os.homedir(),
|
|
76
|
+
};
|
|
77
|
+
|
|
78
|
+
const report = await generateReport(doctorCtx, { fix, allowPrompt: yes });
|
|
79
|
+
|
|
80
|
+
ctx.out.result(report, (d, { stdout }) => {
|
|
81
|
+
renderReport(d, { stdout, verbose });
|
|
82
|
+
});
|
|
83
|
+
|
|
84
|
+
process.exitCode = report.exitCode;
|
|
85
|
+
},
|
|
86
|
+
};
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
// `synapse login <url>` — register the operator's session against a
|
|
2
|
+
// Synapse instance. The flow is intentionally minimal so it works
|
|
3
|
+
// over piped stdin (CI bootstrap): the prompts library detects a
|
|
4
|
+
// non-TTY and accepts `email\npassword\n` instead of rendering the
|
|
5
|
+
// hidden-password prompt.
|
|
6
|
+
|
|
7
|
+
const { askCredentials } = require("../prompts");
|
|
8
|
+
const { SynapseAPI } = require("../api");
|
|
9
|
+
const { normalizeBaseUrl, writeConfig } = require("../config");
|
|
10
|
+
|
|
11
|
+
module.exports = {
|
|
12
|
+
name: "login",
|
|
13
|
+
summary: "Authenticate against a Synapse instance and save the session locally.",
|
|
14
|
+
usage: "synapse login <url>",
|
|
15
|
+
description: `Prompts for email + password (or reads them piped on stdin) and writes the resulting session to ~/.synapse/config.json (mode 0600).
|
|
16
|
+
|
|
17
|
+
The saved session carries both the access token (1h TTL) and a refresh token (30d TTL); subsequent commands silent-refresh on 401 transparently — operators don't see /login prompts mid-task.`,
|
|
18
|
+
|
|
19
|
+
async run(args, ctx) {
|
|
20
|
+
const url = args[0];
|
|
21
|
+
if (!url) {
|
|
22
|
+
throw new Error("Usage: synapse login <url>");
|
|
23
|
+
}
|
|
24
|
+
const baseUrl = normalizeBaseUrl(url);
|
|
25
|
+
const { email, password } = await askCredentials();
|
|
26
|
+
const api = new SynapseAPI({ baseUrl });
|
|
27
|
+
const session = await api.login(email, password);
|
|
28
|
+
if (!session.accessToken) {
|
|
29
|
+
throw new Error("Synapse login response did not include accessToken");
|
|
30
|
+
}
|
|
31
|
+
const file = writeConfig({
|
|
32
|
+
baseUrl,
|
|
33
|
+
accessToken: session.accessToken,
|
|
34
|
+
refreshToken: session.refreshToken || null,
|
|
35
|
+
tokenType: session.tokenType || "Bearer",
|
|
36
|
+
user: session.user || null,
|
|
37
|
+
});
|
|
38
|
+
ctx.out.result(
|
|
39
|
+
{ baseUrl, user: session.user || null, configPath: file },
|
|
40
|
+
() => ctx.out.info(`Saved Synapse session to ${file}`),
|
|
41
|
+
);
|
|
42
|
+
},
|
|
43
|
+
};
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
// `synapse logout` — clear the saved session from ~/.synapse/config.json.
|
|
2
|
+
// No backend call (the JWT stays technically valid until exp; the
|
|
3
|
+
// point is to delete the refresh token from this machine so future
|
|
4
|
+
// 401s can't silent-refresh into a fresh session).
|
|
5
|
+
|
|
6
|
+
const { clearConfig } = require("../config");
|
|
7
|
+
|
|
8
|
+
module.exports = {
|
|
9
|
+
name: "logout",
|
|
10
|
+
summary: "Clear the saved Synapse session from this machine.",
|
|
11
|
+
usage: "synapse logout",
|
|
12
|
+
description: `Deletes ~/.synapse/config.json. Does not contact the backend — tokens stay technically valid until their natural exp, but the refresh token is gone so silent-refresh can no longer pick up where you left off.`,
|
|
13
|
+
|
|
14
|
+
async run(_args, ctx) {
|
|
15
|
+
const removed = clearConfig();
|
|
16
|
+
ctx.out.result({ removed }, () =>
|
|
17
|
+
ctx.out.info(removed ? "Logged out of Synapse." : "No Synapse session was saved."),
|
|
18
|
+
);
|
|
19
|
+
},
|
|
20
|
+
};
|
|
@@ -0,0 +1,143 @@
|
|
|
1
|
+
// `synapse open [target]` — launches a URL in the operator's default
|
|
2
|
+
// browser. Cross-platform via the right `open` / `xdg-open` / `start`
|
|
3
|
+
// command per OS.
|
|
4
|
+
//
|
|
5
|
+
// Targets:
|
|
6
|
+
// (default) the linked project's page in the Synapse dashboard
|
|
7
|
+
// dashboard same as default
|
|
8
|
+
// docs docs.convex.dev (the upstream Convex docs)
|
|
9
|
+
// deployment <n> /embed/<name> — the dashboard with Convex Dashboard iframe
|
|
10
|
+
// url just the synapse base URL (for "open the dashboard root")
|
|
11
|
+
//
|
|
12
|
+
// For `dashboard` (the default), we do a single cheap GET probe against
|
|
13
|
+
// the linked project before spawning the browser, so an operator with a
|
|
14
|
+
// stale .synapse/project.json gets a warning on stderr instead of
|
|
15
|
+
// landing on a page that cascades "Failed to load X" errors. We never
|
|
16
|
+
// BLOCK the launch — operator may want to see the broken state.
|
|
17
|
+
// `docs` / `deployment <name>` / `url` skip the probe.
|
|
18
|
+
|
|
19
|
+
const { spawn } = require("node:child_process");
|
|
20
|
+
const { SynapseAPIError } = require("../api");
|
|
21
|
+
|
|
22
|
+
function buildUrl(target, restArgs, { cfg, projectConfig }) {
|
|
23
|
+
const base = cfg?.baseUrl ?? "";
|
|
24
|
+
switch (target) {
|
|
25
|
+
case undefined:
|
|
26
|
+
case "dashboard": {
|
|
27
|
+
if (projectConfig?.team?.slug && projectConfig?.project?.id) {
|
|
28
|
+
return `${base}/teams/${encodeURIComponent(projectConfig.team.slug)}/${encodeURIComponent(projectConfig.project.id)}`;
|
|
29
|
+
}
|
|
30
|
+
// Fall back to /teams (operator picks the project visually).
|
|
31
|
+
return `${base}/teams`;
|
|
32
|
+
}
|
|
33
|
+
case "docs":
|
|
34
|
+
return "https://docs.convex.dev";
|
|
35
|
+
case "deployment": {
|
|
36
|
+
const name = restArgs[0];
|
|
37
|
+
if (!name) {
|
|
38
|
+
throw new Error("Usage: synapse open deployment <name>");
|
|
39
|
+
}
|
|
40
|
+
return `${base}/embed/${encodeURIComponent(name)}`;
|
|
41
|
+
}
|
|
42
|
+
case "url":
|
|
43
|
+
return base || "https://docs.convex.dev";
|
|
44
|
+
default:
|
|
45
|
+
throw new Error(
|
|
46
|
+
`Unknown target: ${target}. Try: dashboard | docs | deployment <name> | url`,
|
|
47
|
+
);
|
|
48
|
+
}
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
function launcher(platform = process.platform) {
|
|
52
|
+
if (platform === "darwin") return { cmd: "open", shell: false };
|
|
53
|
+
if (platform === "win32") return { cmd: "start", shell: true };
|
|
54
|
+
// Linux + others
|
|
55
|
+
return { cmd: "xdg-open", shell: false };
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
// Cheap pre-flight probe: confirm the linked project still exists before
|
|
59
|
+
// launching the browser. Returns one of:
|
|
60
|
+
// "ok" — project resolved on the backend
|
|
61
|
+
// "not_found" — backend returned 404 (project deleted or transferred)
|
|
62
|
+
// "unverified" — couldn't reach backend / non-404 error / no session /
|
|
63
|
+
// no linked project
|
|
64
|
+
// Only `dashboard` target calls this — `docs` is external, `deployment
|
|
65
|
+
// <name>` and `url` are operator-supplied + assumed intentional.
|
|
66
|
+
async function checkProjectStatus(ctx, projectConfig) {
|
|
67
|
+
if (!projectConfig?.project?.id) return "unverified";
|
|
68
|
+
if (!ctx?.cfgOrNull?.accessToken) return "unverified";
|
|
69
|
+
if (!ctx.api) return "unverified";
|
|
70
|
+
try {
|
|
71
|
+
await ctx.api.getProject(projectConfig.project.id);
|
|
72
|
+
return "ok";
|
|
73
|
+
} catch (err) {
|
|
74
|
+
if (err instanceof SynapseAPIError && err.status === 404) {
|
|
75
|
+
return "not_found";
|
|
76
|
+
}
|
|
77
|
+
return "unverified";
|
|
78
|
+
}
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
module.exports = {
|
|
82
|
+
name: "open",
|
|
83
|
+
summary: "Open a Synapse-related URL in your default browser.",
|
|
84
|
+
usage: "synapse open [dashboard|docs|deployment <name>|url] [--json]",
|
|
85
|
+
description: `Launches a browser. With no argument, opens the linked project's page in the dashboard.
|
|
86
|
+
|
|
87
|
+
Targets:
|
|
88
|
+
dashboard project page on the Synapse dashboard (default)
|
|
89
|
+
docs https://docs.convex.dev
|
|
90
|
+
deployment <n> /embed/<n> — Convex Dashboard iframe shell
|
|
91
|
+
url the saved Synapse base URL`,
|
|
92
|
+
|
|
93
|
+
// Exports for tests.
|
|
94
|
+
buildUrl,
|
|
95
|
+
launcher,
|
|
96
|
+
checkProjectStatus,
|
|
97
|
+
|
|
98
|
+
async run(args, ctx) {
|
|
99
|
+
const target = args[0];
|
|
100
|
+
const url = buildUrl(target, args.slice(1), {
|
|
101
|
+
cfg: ctx.cfgOrNull,
|
|
102
|
+
projectConfig: ctx.projectConfig,
|
|
103
|
+
});
|
|
104
|
+
|
|
105
|
+
// Pre-flight only for dashboard (default + explicit). Other targets
|
|
106
|
+
// either point externally or are intentionally URL-direct.
|
|
107
|
+
const shouldProbe = target === undefined || target === "dashboard";
|
|
108
|
+
let projectStatus = "unverified";
|
|
109
|
+
if (shouldProbe && ctx.projectConfig?.project?.id) {
|
|
110
|
+
projectStatus = await checkProjectStatus(ctx, ctx.projectConfig);
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
if (ctx.out.json) {
|
|
114
|
+
ctx.out.result({ url, target: target ?? "dashboard", projectStatus }, () => {});
|
|
115
|
+
return;
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
if (projectStatus === "not_found") {
|
|
119
|
+
const projName = ctx.projectConfig?.project?.name || ctx.projectConfig?.project?.id;
|
|
120
|
+
const base = ctx.cfgOrNull?.baseUrl ?? "";
|
|
121
|
+
ctx.out.warn(
|
|
122
|
+
`Linked project ${projName} was not found on ${base}. It may have been deleted. Run \`synapse select\` to relink.`,
|
|
123
|
+
);
|
|
124
|
+
} else if (shouldProbe && projectStatus === "unverified" && ctx.projectConfig?.project?.id && ctx.cfgOrNull?.accessToken) {
|
|
125
|
+
ctx.out.info("Could not verify project state (offline?), opening anyway.");
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
const { cmd, shell } = launcher();
|
|
129
|
+
ctx.out.info(`Opening ${url}`);
|
|
130
|
+
try {
|
|
131
|
+
const child = spawn(cmd, [url], { stdio: "ignore", detached: true, shell });
|
|
132
|
+
child.unref();
|
|
133
|
+
} catch (err) {
|
|
134
|
+
// If the launcher isn't on PATH (rare — xdg-open is part of
|
|
135
|
+
// xdg-utils, ships on every desktop Linux), the operator can
|
|
136
|
+
// still see the URL we tried.
|
|
137
|
+
ctx.out.error(`Could not launch browser: ${err.message}`, {
|
|
138
|
+
hint: `Open this URL manually: ${url}`,
|
|
139
|
+
});
|
|
140
|
+
process.exitCode = 1;
|
|
141
|
+
}
|
|
142
|
+
},
|
|
143
|
+
};
|
|
@@ -0,0 +1,239 @@
|
|
|
1
|
+
// `synapse select` — link the current directory to a Synapse project +
|
|
2
|
+
// pick a dev/prod deployment, then write the local artifacts
|
|
3
|
+
// (.synapse/project.json + .env.local).
|
|
4
|
+
//
|
|
5
|
+
// The picker walks team → project → dev → prod as a state machine so
|
|
6
|
+
// the operator can type `b` at any level to step back one. Auto-
|
|
7
|
+
// selects each level when only one option exists. DEBUG_SYNAPSE=1
|
|
8
|
+
// dumps the underlying lists at every step — useful when a real
|
|
9
|
+
// deployment is mysteriously missing from the menu.
|
|
10
|
+
|
|
11
|
+
const colors = require("../colors");
|
|
12
|
+
const { writeProjectEnv } = require("../env-file");
|
|
13
|
+
const {
|
|
14
|
+
buildProjectConfig,
|
|
15
|
+
writeProjectConfig,
|
|
16
|
+
} = require("../project");
|
|
17
|
+
const { BACK, choose } = require("../prompts");
|
|
18
|
+
|
|
19
|
+
function labelName(item) {
|
|
20
|
+
const name = item.name || item.slug || item.id;
|
|
21
|
+
const slug = item.slug && item.slug !== name ? ` (${item.slug})` : "";
|
|
22
|
+
return `${name}${slug}`;
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
function teamRef(team) {
|
|
26
|
+
return team.slug || team.id;
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
function deploymentType(deployment) {
|
|
30
|
+
return deployment.deploymentType || deployment.type || "";
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
function deploymentLabel(deployment) {
|
|
34
|
+
const bits = [colors.bold(deployment.name)];
|
|
35
|
+
const type = deploymentType(deployment);
|
|
36
|
+
if (type) bits.push(colors.dim(type));
|
|
37
|
+
if (deployment.status) bits.push(colors.statusBadge(deployment.status));
|
|
38
|
+
return bits.filter(Boolean).join(" - ");
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
function sortDeploymentsForChoice(deployments) {
|
|
42
|
+
return [...deployments].sort((a, b) => {
|
|
43
|
+
if (!!a.isDefault !== !!b.isDefault) {
|
|
44
|
+
return a.isDefault ? -1 : 1;
|
|
45
|
+
}
|
|
46
|
+
return String(b.createTime || b.createdAt || "").localeCompare(
|
|
47
|
+
String(a.createTime || a.createdAt || ""),
|
|
48
|
+
);
|
|
49
|
+
});
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
function debugLog(msg) {
|
|
53
|
+
if (process.env.DEBUG_SYNAPSE) {
|
|
54
|
+
process.stderr.write(`[DEBUG] ${msg}\n`);
|
|
55
|
+
}
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
async function chooseDeploymentForType(type, deployments, chooseOpts = {}) {
|
|
59
|
+
const matches = sortDeploymentsForChoice(
|
|
60
|
+
deployments.filter(
|
|
61
|
+
(d) => deploymentType(d) === type && d.status !== "deleted",
|
|
62
|
+
),
|
|
63
|
+
);
|
|
64
|
+
debugLog(
|
|
65
|
+
`chooseDeploymentForType(${type}): matched ${matches.length} of ${deployments.length} ` +
|
|
66
|
+
`(types: ${deployments.map((d) => deploymentType(d) || "?").join(",")})`,
|
|
67
|
+
);
|
|
68
|
+
if (matches.length === 0) return null;
|
|
69
|
+
return await choose(
|
|
70
|
+
`${type} deployments`,
|
|
71
|
+
matches.map((d) => ({ label: deploymentLabel(d), value: d })),
|
|
72
|
+
{ singularLabel: `${type} deployment`, ...chooseOpts },
|
|
73
|
+
);
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
module.exports = {
|
|
77
|
+
name: "select",
|
|
78
|
+
summary: "Link this directory to a Synapse project and pick its dev/prod deployments.",
|
|
79
|
+
usage: "synapse select",
|
|
80
|
+
description: `Walks an interactive picker (team → project → dev → prod) and writes:
|
|
81
|
+
.synapse/project.json refs only — no secrets, safe to commit
|
|
82
|
+
.env.local CONVEX_SELF_HOSTED_URL + CONVEX_SELF_HOSTED_ADMIN_KEY
|
|
83
|
+
|
|
84
|
+
Auto-selects levels where only one option exists. Type 'b' at any
|
|
85
|
+
prompt to step back. Set DEBUG_SYNAPSE=1 to print the raw lists
|
|
86
|
+
returned at every step (useful when an expected deployment doesn't
|
|
87
|
+
show up in the menu).`,
|
|
88
|
+
|
|
89
|
+
// Exported for legacy test imports.
|
|
90
|
+
chooseDeploymentForType,
|
|
91
|
+
deploymentLabel,
|
|
92
|
+
deploymentType,
|
|
93
|
+
labelName,
|
|
94
|
+
sortDeploymentsForChoice,
|
|
95
|
+
|
|
96
|
+
async run(_args, ctx) {
|
|
97
|
+
const { api } = ctx;
|
|
98
|
+
const cfg = ctx.cfg;
|
|
99
|
+
|
|
100
|
+
const cache = {
|
|
101
|
+
teamsList: null,
|
|
102
|
+
projectsByTeamKey: new Map(),
|
|
103
|
+
deploymentsByProjectId: new Map(),
|
|
104
|
+
};
|
|
105
|
+
async function fetchTeams() {
|
|
106
|
+
if (!cache.teamsList) {
|
|
107
|
+
cache.teamsList = await api.teams();
|
|
108
|
+
debugLog(`teams loaded: ${cache.teamsList.length}`);
|
|
109
|
+
}
|
|
110
|
+
return cache.teamsList;
|
|
111
|
+
}
|
|
112
|
+
async function fetchProjects(team) {
|
|
113
|
+
const key = team.id || team.slug || team.name;
|
|
114
|
+
if (!cache.projectsByTeamKey.has(key)) {
|
|
115
|
+
const projects = await api.projects(teamRef(team));
|
|
116
|
+
cache.projectsByTeamKey.set(key, projects);
|
|
117
|
+
debugLog(`projects for team ${key}: ${projects.length}`);
|
|
118
|
+
}
|
|
119
|
+
return cache.projectsByTeamKey.get(key);
|
|
120
|
+
}
|
|
121
|
+
async function fetchDeployments(project) {
|
|
122
|
+
if (!cache.deploymentsByProjectId.has(project.id)) {
|
|
123
|
+
const deployments = await api.deployments(project.id);
|
|
124
|
+
cache.deploymentsByProjectId.set(project.id, deployments);
|
|
125
|
+
debugLog(`deployments for project ${project.id}: ${deployments.length}`);
|
|
126
|
+
}
|
|
127
|
+
return cache.deploymentsByProjectId.get(project.id);
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
let team = null;
|
|
131
|
+
let project = null;
|
|
132
|
+
let dev = null;
|
|
133
|
+
let prod = null;
|
|
134
|
+
let step = "team";
|
|
135
|
+
while (step !== "done") {
|
|
136
|
+
if (step === "team") {
|
|
137
|
+
const teams = await fetchTeams();
|
|
138
|
+
const picked = await choose(
|
|
139
|
+
"teams",
|
|
140
|
+
teams.map((t) => ({ label: labelName(t), value: t })),
|
|
141
|
+
{ singularLabel: "team", allowBack: false },
|
|
142
|
+
);
|
|
143
|
+
team = picked;
|
|
144
|
+
step = "project";
|
|
145
|
+
} else if (step === "project") {
|
|
146
|
+
const projects = await fetchProjects(team);
|
|
147
|
+
const picked = await choose(
|
|
148
|
+
"projects",
|
|
149
|
+
projects.map((p) => ({ label: labelName(p), value: p })),
|
|
150
|
+
{ singularLabel: "project", allowBack: true },
|
|
151
|
+
);
|
|
152
|
+
if (picked === BACK) {
|
|
153
|
+
step = "team";
|
|
154
|
+
continue;
|
|
155
|
+
}
|
|
156
|
+
project = picked;
|
|
157
|
+
step = "dev";
|
|
158
|
+
} else if (step === "dev") {
|
|
159
|
+
const deployments = await fetchDeployments(project);
|
|
160
|
+
const picked = await chooseDeploymentForType("dev", deployments, {
|
|
161
|
+
allowBack: true,
|
|
162
|
+
});
|
|
163
|
+
if (picked === BACK) {
|
|
164
|
+
step = "project";
|
|
165
|
+
continue;
|
|
166
|
+
}
|
|
167
|
+
if (picked === null) {
|
|
168
|
+
throw new Error(
|
|
169
|
+
"No dev deployments available in this project. Create one first in the dashboard.",
|
|
170
|
+
);
|
|
171
|
+
}
|
|
172
|
+
dev = picked;
|
|
173
|
+
step = "prod";
|
|
174
|
+
} else if (step === "prod") {
|
|
175
|
+
const deployments = await fetchDeployments(project);
|
|
176
|
+
const picked = await chooseDeploymentForType("prod", deployments, {
|
|
177
|
+
allowBack: true,
|
|
178
|
+
});
|
|
179
|
+
if (picked === BACK) {
|
|
180
|
+
step = "dev";
|
|
181
|
+
continue;
|
|
182
|
+
}
|
|
183
|
+
prod = picked; // null is valid (no prod yet)
|
|
184
|
+
step = "done";
|
|
185
|
+
}
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
const projectPath = writeProjectConfig(
|
|
189
|
+
ctx.cwd,
|
|
190
|
+
buildProjectConfig({
|
|
191
|
+
synapseUrl: cfg.baseUrl,
|
|
192
|
+
team,
|
|
193
|
+
project,
|
|
194
|
+
deployments: { dev, prod },
|
|
195
|
+
}),
|
|
196
|
+
);
|
|
197
|
+
const creds = await api.cliCredentials(dev.name);
|
|
198
|
+
const envPath = writeProjectEnv(ctx.cwd, creds);
|
|
199
|
+
|
|
200
|
+
ctx.out.result(
|
|
201
|
+
{
|
|
202
|
+
synapseUrl: cfg.baseUrl,
|
|
203
|
+
team: { id: team.id, slug: team.slug, name: team.name },
|
|
204
|
+
project: { id: project.id, slug: project.slug, name: project.name },
|
|
205
|
+
deployments: {
|
|
206
|
+
dev: { name: dev.name, type: deploymentType(dev), status: dev.status },
|
|
207
|
+
prod: prod
|
|
208
|
+
? { name: prod.name, type: deploymentType(prod), status: prod.status }
|
|
209
|
+
: null,
|
|
210
|
+
},
|
|
211
|
+
files: { projectConfig: projectPath, envLocal: envPath },
|
|
212
|
+
},
|
|
213
|
+
() => {
|
|
214
|
+
ctx.out.info(`\nLinked ${labelName(project)} to ${projectPath}.`);
|
|
215
|
+
ctx.out.info(
|
|
216
|
+
`Selected dev deployment ${colors.bold(dev.name)}. Updated ${envPath}.`,
|
|
217
|
+
);
|
|
218
|
+
if (prod) {
|
|
219
|
+
ctx.out.info(`Selected prod deployment ${colors.bold(prod.name)}.`);
|
|
220
|
+
} else {
|
|
221
|
+
ctx.out.warn(
|
|
222
|
+
"no prod deployment found. `synapse deploy` (and `synapse convex deploy`) will fail with a clear error until you create a prod deployment and run `synapse select` again.",
|
|
223
|
+
);
|
|
224
|
+
}
|
|
225
|
+
if (process.env.CONVEX_DEPLOYMENT) {
|
|
226
|
+
ctx.out.warn(
|
|
227
|
+
"shell CONVEX_DEPLOYMENT is set. Use `synapse dev` / `synapse deploy` / `synapse convex ...` or unset CONVEX_DEPLOYMENT before running `npx convex` directly.",
|
|
228
|
+
);
|
|
229
|
+
}
|
|
230
|
+
ctx.out.info(
|
|
231
|
+
`\nNext step: run ${colors.bold("synapse dev")} (or ${colors.bold("npx convex dev")}) once in this directory to push your schema and watch for changes.`,
|
|
232
|
+
);
|
|
233
|
+
},
|
|
234
|
+
);
|
|
235
|
+
// Force ctx.projectConfig to re-read on next access (commands chained
|
|
236
|
+
// after select within the same process should see the new file).
|
|
237
|
+
ctx.refreshProjectConfig();
|
|
238
|
+
},
|
|
239
|
+
};
|