@iann29/synapse 1.8.5 → 1.8.7
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 +18 -1
- package/lib/api.js +54 -0
- package/lib/commands/_context.js +58 -6
- package/lib/commands/_help.js +1 -1
- package/lib/commands/_resource.js +152 -0
- package/lib/commands/convex.js +17 -0
- package/lib/commands/credentials.js +22 -1
- package/lib/commands/deployment-create.js +122 -0
- package/lib/commands/deployment-delete.js +117 -0
- package/lib/commands/deployment-rotate-key.js +173 -0
- package/lib/commands/deployment-status.js +143 -0
- package/lib/commands/env-list.js +105 -0
- package/lib/commands/env-pull.js +117 -0
- package/lib/commands/env-push.js +248 -0
- package/lib/commands/env-set.js +129 -0
- package/lib/commands/env-unset.js +77 -0
- package/lib/commands/list.js +186 -0
- package/lib/commands/login.js +14 -1
- package/lib/prompts.js +50 -0
- package/package.json +1 -1
package/bin/synapse.js
CHANGED
|
@@ -30,7 +30,24 @@ async function main(argv) {
|
|
|
30
30
|
|
|
31
31
|
const { cmd, rest } = resolve(REGISTRY, cleanArgv);
|
|
32
32
|
if (!cmd) {
|
|
33
|
-
|
|
33
|
+
// v1.8.7: if the operator typed `synapse <prefix>` where one or
|
|
34
|
+
// more two-word commands exist under that prefix, suggest them.
|
|
35
|
+
// Turns "Unknown command: deployment" → an actionable list of
|
|
36
|
+
// verbs the operator can pick from.
|
|
37
|
+
const prefix = cleanArgv[0] + " ";
|
|
38
|
+
const verbs = [];
|
|
39
|
+
for (const name of REGISTRY.keys()) {
|
|
40
|
+
if (name.startsWith(prefix)) verbs.push(name);
|
|
41
|
+
}
|
|
42
|
+
process.stderr.write(`Unknown command: ${cleanArgv.join(" ")}\n`);
|
|
43
|
+
if (verbs.length > 0) {
|
|
44
|
+
verbs.sort();
|
|
45
|
+
process.stderr.write(`\nDid you mean one of:\n`);
|
|
46
|
+
for (const v of verbs) process.stderr.write(` synapse ${v}\n`);
|
|
47
|
+
process.stderr.write(`\nRun \`synapse ${cleanArgv[0]} <verb> --help\` for usage.\n`);
|
|
48
|
+
} else {
|
|
49
|
+
process.stderr.write(`\nRun \`synapse help\` for the full list.\n`);
|
|
50
|
+
}
|
|
34
51
|
process.exitCode = 1;
|
|
35
52
|
return;
|
|
36
53
|
}
|
package/lib/api.js
CHANGED
|
@@ -131,6 +131,60 @@ class SynapseAPI {
|
|
|
131
131
|
cliCredentials(deploymentName) {
|
|
132
132
|
return this.request("GET", `/v1/deployments/${encodeURIComponent(deploymentName)}/cli_credentials`);
|
|
133
133
|
}
|
|
134
|
+
|
|
135
|
+
// ---- v1.8.7 control-plane methods ------------------------------
|
|
136
|
+
|
|
137
|
+
// GET /v1/deployments/{name}/ — single-deployment snapshot for the
|
|
138
|
+
// status command (faster than filtering listDeployments client-side
|
|
139
|
+
// when the operator already knows the name).
|
|
140
|
+
getDeployment(name) {
|
|
141
|
+
return this.request("GET", `/v1/deployments/${encodeURIComponent(name)}/`);
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
// GET /v1/deployments/{name}/backend_version — version of the running
|
|
145
|
+
// Convex backend inside the container. Cheap and useful in `status`.
|
|
146
|
+
// Falls back to {} if the deployment isn't ready (status: provisioning).
|
|
147
|
+
getDeploymentBackendVersion(name) {
|
|
148
|
+
return this.request("GET", `/v1/deployments/${encodeURIComponent(name)}/backend_version`);
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
// POST /v1/projects/{id}/create_deployment — body: { type, ha?, reference?, isDefault? }.
|
|
152
|
+
// Backend generates the deployment name; we surface it in the response.
|
|
153
|
+
createDeployment(projectId, body) {
|
|
154
|
+
return this.request("POST", `/v1/projects/${encodeURIComponent(projectId)}/create_deployment`, body);
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
// POST /v1/deployments/{name}/delete — irreversible; container + volume
|
|
158
|
+
// gone. Returns 200 with no body on success. The caller is responsible
|
|
159
|
+
// for typed-confirmation in the UI layer.
|
|
160
|
+
deleteDeployment(name) {
|
|
161
|
+
return this.request("POST", `/v1/deployments/${encodeURIComponent(name)}/delete`, {});
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
// POST /v1/deployments/{name}/reissue_admin_key — re-mints the admin
|
|
165
|
+
// key from the current instance_secret WITHOUT touching the secret.
|
|
166
|
+
// Use when stored credentials drift from the live container.
|
|
167
|
+
// Response: { deploymentName, adminKey, prefix }.
|
|
168
|
+
reissueAdminKey(name) {
|
|
169
|
+
return this.request("POST", `/v1/deployments/${encodeURIComponent(name)}/reissue_admin_key`, {});
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
// GET /v1/projects/{id}/list_default_environment_variables — returns
|
|
173
|
+
// { configs: [{ name, value, deploymentTypes[] }] }. We hand the raw
|
|
174
|
+
// shape back; the command formats it.
|
|
175
|
+
listProjectEnvVars(projectId) {
|
|
176
|
+
return this.request("GET", `/v1/projects/${encodeURIComponent(projectId)}/list_default_environment_variables`);
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
// POST /v1/projects/{id}/update_default_environment_variables — body:
|
|
180
|
+
// { changes: [{ op: "set"|"delete", name, value?, deploymentTypes? }] }.
|
|
181
|
+
// Cloud-spec shape; the backend applies the batch in a transaction so
|
|
182
|
+
// partial-failure is impossible.
|
|
183
|
+
updateProjectEnvVars(projectId, changes) {
|
|
184
|
+
return this.request("POST", `/v1/projects/${encodeURIComponent(projectId)}/update_default_environment_variables`, {
|
|
185
|
+
changes,
|
|
186
|
+
});
|
|
187
|
+
}
|
|
134
188
|
}
|
|
135
189
|
|
|
136
190
|
// Known envelope keys, in priority order. We try these explicitly before
|
package/lib/commands/_context.js
CHANGED
|
@@ -23,6 +23,24 @@ const {
|
|
|
23
23
|
} = require("../config");
|
|
24
24
|
const { readProjectConfig } = require("../project");
|
|
25
25
|
|
|
26
|
+
// v1.8.6 (A3): SessionExpiredError carries an operator-actionable
|
|
27
|
+
// message when the refresh token itself is rejected (>30d old or
|
|
28
|
+
// revoked at server side). bin/synapse.js prints err.message verbatim,
|
|
29
|
+
// so a clear "your session expired, run `synapse login <url>`" lands
|
|
30
|
+
// directly in front of the operator instead of a cryptic
|
|
31
|
+
// "Synapse API returned 401" message.
|
|
32
|
+
class SessionExpiredError extends Error {
|
|
33
|
+
constructor(baseUrl, cause) {
|
|
34
|
+
super(
|
|
35
|
+
`Your Synapse session expired. Run \`synapse login ${baseUrl}\` to sign in again.` +
|
|
36
|
+
(cause ? ` (refresh failed: ${cause})` : ""),
|
|
37
|
+
);
|
|
38
|
+
this.name = "SessionExpiredError";
|
|
39
|
+
this.baseUrl = baseUrl;
|
|
40
|
+
this.cause = cause;
|
|
41
|
+
}
|
|
42
|
+
}
|
|
43
|
+
|
|
26
44
|
// Wraps an API client so any 401 transparently retries against
|
|
27
45
|
// /v1/auth/refresh once. Mirrors what bin/synapse.js had before the
|
|
28
46
|
// refactor; lives here so every command shares it.
|
|
@@ -43,10 +61,30 @@ function makeRefreshableApi(cfg) {
|
|
|
43
61
|
) {
|
|
44
62
|
throw err;
|
|
45
63
|
}
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
)
|
|
49
|
-
|
|
64
|
+
// v1.8.6 (A3): catch refresh failures and surface a clear
|
|
65
|
+
// re-login instruction instead of the raw upstream 401.
|
|
66
|
+
// Refresh tokens expire (30d default) or get revoked
|
|
67
|
+
// server-side; either way the operator needs to log in
|
|
68
|
+
// again and the bare "Synapse API returned 401" gave them
|
|
69
|
+
// no path forward.
|
|
70
|
+
let session;
|
|
71
|
+
try {
|
|
72
|
+
session = await new SynapseAPI({ baseUrl: cfg.baseUrl }).refresh(
|
|
73
|
+
cfg.refreshToken,
|
|
74
|
+
);
|
|
75
|
+
} catch (refreshErr) {
|
|
76
|
+
const detail =
|
|
77
|
+
refreshErr instanceof SynapseAPIError
|
|
78
|
+
? `${refreshErr.code} ${refreshErr.status}`
|
|
79
|
+
: String(refreshErr.message || refreshErr);
|
|
80
|
+
throw new SessionExpiredError(cfg.baseUrl, detail);
|
|
81
|
+
}
|
|
82
|
+
if (!session.accessToken) {
|
|
83
|
+
throw new SessionExpiredError(
|
|
84
|
+
cfg.baseUrl,
|
|
85
|
+
"no access token in refresh response",
|
|
86
|
+
);
|
|
87
|
+
}
|
|
50
88
|
cfg.accessToken = session.accessToken;
|
|
51
89
|
cfg.refreshToken = session.refreshToken || cfg.refreshToken;
|
|
52
90
|
cfg.tokenType = session.tokenType || cfg.tokenType || "Bearer";
|
|
@@ -84,10 +122,19 @@ function createContext({ out, cwd = process.cwd(), env = process.env } = {}) {
|
|
|
84
122
|
|
|
85
123
|
// Throws "Not logged in" with a helpful message when no session
|
|
86
124
|
// exists. Commands that REQUIRE auth call this.
|
|
125
|
+
//
|
|
126
|
+
// v1.8.6 (A1): copy now points operators who don't know the URL
|
|
127
|
+
// at their admin instead of leaving them stuck. This message
|
|
128
|
+
// cascades through 7+ commands (whoami / status / select /
|
|
129
|
+
// credentials / dev / deploy / convex) because the error
|
|
130
|
+
// propagates verbatim through bin/synapse.js's top-level
|
|
131
|
+
// try/catch.
|
|
87
132
|
get cfg() {
|
|
88
133
|
const c = cfgLoad();
|
|
89
134
|
if (!c || !c.baseUrl || !c.accessToken) {
|
|
90
|
-
throw new Error(
|
|
135
|
+
throw new Error(
|
|
136
|
+
"Not logged in. Run `synapse login <your-synapse-url>` (e.g. `synapse login https://synapsepanel.com`). If you don't know the URL, ask the admin who set up your Synapse host.",
|
|
137
|
+
);
|
|
91
138
|
}
|
|
92
139
|
_cfg = c;
|
|
93
140
|
return c;
|
|
@@ -130,4 +177,9 @@ function createContext({ out, cwd = process.cwd(), env = process.env } = {}) {
|
|
|
130
177
|
};
|
|
131
178
|
}
|
|
132
179
|
|
|
133
|
-
module.exports = {
|
|
180
|
+
module.exports = {
|
|
181
|
+
createContext,
|
|
182
|
+
makeRefreshableApi,
|
|
183
|
+
normalizeBaseUrl,
|
|
184
|
+
SessionExpiredError,
|
|
185
|
+
};
|
package/lib/commands/_help.js
CHANGED
|
@@ -14,7 +14,7 @@ function renderRootHelp(registry, { stdout = process.stdout } = {}) {
|
|
|
14
14
|
["Session", ["login", "logout", "whoami"]],
|
|
15
15
|
["Project linking", ["select", "credentials"]],
|
|
16
16
|
["Day-to-day", ["dev", "deploy"]],
|
|
17
|
-
["Visibility", ["version", "status", "doctor", "open", "logs"]],
|
|
17
|
+
["Visibility", ["version", "status", "doctor", "open", "list", "logs"]],
|
|
18
18
|
["Deployments", "deployment"],
|
|
19
19
|
["Projects", "project"],
|
|
20
20
|
["Teams", "team"],
|
|
@@ -0,0 +1,152 @@
|
|
|
1
|
+
// Shared helpers for v1.8.7 control-plane commands.
|
|
2
|
+
//
|
|
3
|
+
// Every command in the new "Deployments" + "Env vars" sections needs
|
|
4
|
+
// to resolve a target project — either from the linked `.synapse/
|
|
5
|
+
// project.json` (no flag) or from an explicit `--project=<id>` / team
|
|
6
|
+
// flag (CI / scripted runs). Centralising the logic here keeps the
|
|
7
|
+
// commands themselves slim and gives us one place to update if the
|
|
8
|
+
// resolution rules change.
|
|
9
|
+
|
|
10
|
+
// Extracts `--project=<id>` / `--project <id>` from argv. Returns the
|
|
11
|
+
// id (or null) plus the remaining args. We accept the equals form
|
|
12
|
+
// and the space-separated form — operators copy-paste from various
|
|
13
|
+
// shells.
|
|
14
|
+
function extractProjectFlag(args) {
|
|
15
|
+
const rest = [];
|
|
16
|
+
let projectId = null;
|
|
17
|
+
for (let i = 0; i < args.length; i += 1) {
|
|
18
|
+
const a = args[i];
|
|
19
|
+
if (a === "--project") {
|
|
20
|
+
projectId = args[i + 1];
|
|
21
|
+
i += 1;
|
|
22
|
+
continue;
|
|
23
|
+
}
|
|
24
|
+
if (a.startsWith("--project=")) {
|
|
25
|
+
projectId = a.slice("--project=".length);
|
|
26
|
+
continue;
|
|
27
|
+
}
|
|
28
|
+
rest.push(a);
|
|
29
|
+
}
|
|
30
|
+
return { projectId, rest };
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
// Same shape as extractProjectFlag for --team. team flag accepts either
|
|
34
|
+
// a slug or an id (backend's loadTeamForRequest accepts both).
|
|
35
|
+
function extractTeamFlag(args) {
|
|
36
|
+
const rest = [];
|
|
37
|
+
let teamRef = null;
|
|
38
|
+
for (let i = 0; i < args.length; i += 1) {
|
|
39
|
+
const a = args[i];
|
|
40
|
+
if (a === "--team") {
|
|
41
|
+
teamRef = args[i + 1];
|
|
42
|
+
i += 1;
|
|
43
|
+
continue;
|
|
44
|
+
}
|
|
45
|
+
if (a.startsWith("--team=")) {
|
|
46
|
+
teamRef = a.slice("--team=".length);
|
|
47
|
+
continue;
|
|
48
|
+
}
|
|
49
|
+
rest.push(a);
|
|
50
|
+
}
|
|
51
|
+
return { teamRef, rest };
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
// Generic flag extractor. Boolean flags (no value) and string flags
|
|
55
|
+
// (with `--name=value` form) must be DECLARED separately so the parser
|
|
56
|
+
// never has to guess whether `--default value-stays` means
|
|
57
|
+
// `default=true, positional=value-stays` (operator intent) or
|
|
58
|
+
// `default=value-stays` (greedy heuristic).
|
|
59
|
+
//
|
|
60
|
+
// Both spec shapes are accepted:
|
|
61
|
+
// extractFlags(args, ["foo", "bar"])
|
|
62
|
+
// → all flags string-or-true (legacy form; used by single-arg
|
|
63
|
+
// parsers that don't have boolean/string ambiguity)
|
|
64
|
+
// extractFlags(args, { string: ["type"], boolean: ["ha", "yes"] })
|
|
65
|
+
// → strict: boolean flags NEVER consume the next token; string
|
|
66
|
+
// flags require `--name=value` OR `--name <value>` (next-token).
|
|
67
|
+
//
|
|
68
|
+
// Unknown flags stay in `rest` (commands surface them with a Usage
|
|
69
|
+
// error).
|
|
70
|
+
function extractFlags(args, spec) {
|
|
71
|
+
let stringSet;
|
|
72
|
+
let booleanSet;
|
|
73
|
+
if (Array.isArray(spec)) {
|
|
74
|
+
// Legacy shape — keep the v1.8.x behaviour for `parseFlags`-style
|
|
75
|
+
// call sites that don't differentiate.
|
|
76
|
+
stringSet = new Set(spec);
|
|
77
|
+
booleanSet = new Set();
|
|
78
|
+
} else {
|
|
79
|
+
stringSet = new Set(spec.string || []);
|
|
80
|
+
booleanSet = new Set(spec.boolean || []);
|
|
81
|
+
}
|
|
82
|
+
const known = (n) => stringSet.has(n) || booleanSet.has(n);
|
|
83
|
+
|
|
84
|
+
const flags = {};
|
|
85
|
+
const rest = [];
|
|
86
|
+
for (let i = 0; i < args.length; i += 1) {
|
|
87
|
+
const a = args[i];
|
|
88
|
+
if (a.startsWith("--")) {
|
|
89
|
+
const eq = a.indexOf("=");
|
|
90
|
+
const name = eq >= 0 ? a.slice(2, eq) : a.slice(2);
|
|
91
|
+
if (known(name)) {
|
|
92
|
+
if (eq >= 0) {
|
|
93
|
+
// --name=value: always string (a boolean flag with `=` is
|
|
94
|
+
// unambiguous, e.g. `--yes=false`).
|
|
95
|
+
flags[name] = a.slice(eq + 1);
|
|
96
|
+
continue;
|
|
97
|
+
}
|
|
98
|
+
if (booleanSet.has(name) && !stringSet.has(name)) {
|
|
99
|
+
// Pure boolean. NEVER consumes the next token.
|
|
100
|
+
flags[name] = true;
|
|
101
|
+
continue;
|
|
102
|
+
}
|
|
103
|
+
// String (or legacy-spec) flag without `=`: consume next token
|
|
104
|
+
// if it isn't another flag. Boolean detection via the
|
|
105
|
+
// boolean-set above already short-circuited, so this branch
|
|
106
|
+
// is reached only for explicit string flags.
|
|
107
|
+
const next = args[i + 1];
|
|
108
|
+
if (next === undefined || next.startsWith("--")) {
|
|
109
|
+
flags[name] = true;
|
|
110
|
+
} else {
|
|
111
|
+
flags[name] = next;
|
|
112
|
+
i += 1;
|
|
113
|
+
}
|
|
114
|
+
continue;
|
|
115
|
+
}
|
|
116
|
+
}
|
|
117
|
+
rest.push(a);
|
|
118
|
+
}
|
|
119
|
+
return { flags, rest };
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
// Resolve the project the command should operate on. Result:
|
|
123
|
+
// { projectId, source: "flag" | "linked", projectConfig?, fallbackError? }
|
|
124
|
+
// Throws when neither a flag NOR a linked project is available (the
|
|
125
|
+
// error message tells the operator exactly what to do).
|
|
126
|
+
function resolveProject(ctx, args) {
|
|
127
|
+
const { projectId, rest } = extractProjectFlag(args);
|
|
128
|
+
if (projectId) {
|
|
129
|
+
return { projectId, source: "flag", rest };
|
|
130
|
+
}
|
|
131
|
+
const linked = ctx.projectConfig;
|
|
132
|
+
if (linked && linked.project && linked.project.id) {
|
|
133
|
+
return {
|
|
134
|
+
projectId: linked.project.id,
|
|
135
|
+
source: "linked",
|
|
136
|
+
projectConfig: linked,
|
|
137
|
+
rest,
|
|
138
|
+
};
|
|
139
|
+
}
|
|
140
|
+
const err = new Error(
|
|
141
|
+
"No linked project in this directory. Pass --project=<id> or run `synapse select` first.",
|
|
142
|
+
);
|
|
143
|
+
err.code = "no_project";
|
|
144
|
+
throw err;
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
module.exports = {
|
|
148
|
+
extractFlags,
|
|
149
|
+
extractProjectFlag,
|
|
150
|
+
extractTeamFlag,
|
|
151
|
+
resolveProject,
|
|
152
|
+
};
|
package/lib/commands/convex.js
CHANGED
|
@@ -117,10 +117,27 @@ async function runConvexCommand(args, ctx) {
|
|
|
117
117
|
ctx.out.info(
|
|
118
118
|
`Using Synapse ${resolved.target} deployment ${resolved.deploymentName}.`,
|
|
119
119
|
);
|
|
120
|
+
// v1.8.6 (A5): the upstream Convex CLI emits "Can't safely modify
|
|
121
|
+
// .env.local for NEXT_PUBLIC_CONVEX_SITE_URL, please edit manually."
|
|
122
|
+
// because our value is a self-hosted URL that doesn't match its
|
|
123
|
+
// `.convex.site` pattern. The warning is benign (the file IS
|
|
124
|
+
// correct — we wrote it), but it's confusing without context.
|
|
125
|
+
// Pre-announce so the operator knows it's expected.
|
|
126
|
+
ctx.out.info(
|
|
127
|
+
"(npx convex may warn it can't modify NEXT_PUBLIC_CONVEX_SITE_URL — benign; Synapse owns those values.)",
|
|
128
|
+
);
|
|
120
129
|
} else {
|
|
121
130
|
resolved = await resolveConvexInvocation(args, { projectDir: ctx.cwd });
|
|
122
131
|
}
|
|
123
132
|
const code = await runConvex(resolved.args, { credentials: resolved.credentials });
|
|
133
|
+
// v1.8.6 (A5): when npx convex exits non-zero, surface a hint about
|
|
134
|
+
// where the failure came from — operators see a `[X]` from convex
|
|
135
|
+
// and assume Synapse broke. Point them at the right `--help`.
|
|
136
|
+
if (code !== 0) {
|
|
137
|
+
ctx.out.info(
|
|
138
|
+
`\n(npx convex exited ${code}. If this looks like an unknown-command typo, run \`synapse convex --help\` for the upstream Convex help.)`,
|
|
139
|
+
);
|
|
140
|
+
}
|
|
124
141
|
process.exitCode = code;
|
|
125
142
|
}
|
|
126
143
|
|
|
@@ -63,7 +63,28 @@ Formats:
|
|
|
63
63
|
const { format, rest } = parseFormat(args);
|
|
64
64
|
const deployment = rest[0];
|
|
65
65
|
if (!deployment) {
|
|
66
|
-
|
|
66
|
+
// v1.8.6 (A4): the operator already linked a project — we know
|
|
67
|
+
// which deployment names exist. Surfacing them turns a dead-end
|
|
68
|
+
// Usage line into an actionable hint without an extra API call.
|
|
69
|
+
// The plain Usage line stays as the fallback for unlinked
|
|
70
|
+
// directories.
|
|
71
|
+
const linked = ctx.projectConfig;
|
|
72
|
+
let hint = "";
|
|
73
|
+
if (linked && linked.deployments) {
|
|
74
|
+
const names = [];
|
|
75
|
+
if (linked.deployments.dev?.name) {
|
|
76
|
+
names.push(`dev=${linked.deployments.dev.name}`);
|
|
77
|
+
}
|
|
78
|
+
if (linked.deployments.prod?.name) {
|
|
79
|
+
names.push(`prod=${linked.deployments.prod.name}`);
|
|
80
|
+
}
|
|
81
|
+
if (names.length > 0) {
|
|
82
|
+
hint = `\n\nThis project has: ${names.join(", ")}. Try \`synapse credentials ${linked.deployments.dev?.name || linked.deployments.prod?.name}\`. Run \`synapse status\` to see them all.`;
|
|
83
|
+
}
|
|
84
|
+
}
|
|
85
|
+
throw new Error(
|
|
86
|
+
`Usage: synapse credentials <deployment> [--format env|shell|json]${hint}`,
|
|
87
|
+
);
|
|
67
88
|
}
|
|
68
89
|
if (!FORMATS.has(format)) {
|
|
69
90
|
throw new Error("format must be one of: env, shell, json");
|
|
@@ -0,0 +1,122 @@
|
|
|
1
|
+
// `synapse deployment create [--type=dev|prod|preview|custom] [--ha]
|
|
2
|
+
// [--default] [--project=<id>] [--json]`
|
|
3
|
+
//
|
|
4
|
+
// Provisions a new Convex backend container under the linked (or
|
|
5
|
+
// --project=) project. The backend generates the deployment name
|
|
6
|
+
// (animal-adjective-NNNN) — we don't accept it as an arg, matching
|
|
7
|
+
// Convex Cloud's contract.
|
|
8
|
+
//
|
|
9
|
+
// Safety: prod creation prompts for `--yes` (or stdin "yes") because
|
|
10
|
+
// the container is real, gets a host port, and counts against quota.
|
|
11
|
+
// dev creates straight through.
|
|
12
|
+
|
|
13
|
+
const colors = require("../colors");
|
|
14
|
+
const { extractFlags, resolveProject } = require("./_resource");
|
|
15
|
+
const { confirm } = require("../prompts");
|
|
16
|
+
|
|
17
|
+
const VALID_TYPES = new Set(["dev", "prod", "preview", "custom"]);
|
|
18
|
+
|
|
19
|
+
module.exports = {
|
|
20
|
+
name: "deployment create",
|
|
21
|
+
summary: "Create a new Convex deployment under the linked project.",
|
|
22
|
+
usage:
|
|
23
|
+
"synapse deployment create [--type=dev|prod|preview|custom] [--ha] [--default] [--project=<id>] [--yes] [--json]",
|
|
24
|
+
description: `Provisions a real Convex backend container. The backend generates the deployment name (animal-adjective-NNNN); you receive it in the response.
|
|
25
|
+
|
|
26
|
+
Flags:
|
|
27
|
+
--type=<dev|prod|preview|custom> Deployment type. Default: dev.
|
|
28
|
+
--ha Provision HA (2 replicas + Postgres + S3).
|
|
29
|
+
Requires SYNAPSE_HA_ENABLED on the host.
|
|
30
|
+
--default Mark as the project's default deployment.
|
|
31
|
+
--project=<id> Operate on a non-linked project.
|
|
32
|
+
--yes Skip the prod-confirmation prompt.
|
|
33
|
+
--json Machine-readable output.
|
|
34
|
+
|
|
35
|
+
Examples:
|
|
36
|
+
synapse deployment create
|
|
37
|
+
synapse deployment create --type=prod --yes
|
|
38
|
+
synapse deployment create --type=dev --ha --default --project=<uuid>`,
|
|
39
|
+
|
|
40
|
+
async run(args, ctx) {
|
|
41
|
+
const { flags, rest } = extractFlags(args, {
|
|
42
|
+
string: ["type", "project"],
|
|
43
|
+
boolean: ["ha", "default", "yes"],
|
|
44
|
+
});
|
|
45
|
+
if (rest.length > 0) {
|
|
46
|
+
throw new Error(
|
|
47
|
+
`Unexpected positional: ${rest[0]}. The backend generates the deployment name; flags only.`,
|
|
48
|
+
);
|
|
49
|
+
}
|
|
50
|
+
// resolveProject reads from --project= via extractProjectFlag. We
|
|
51
|
+
// already consumed --project above; thread it back through args so
|
|
52
|
+
// resolveProject can see it.
|
|
53
|
+
const resolveArgs = flags.project ? [`--project=${flags.project}`] : [];
|
|
54
|
+
const { projectId, source } = resolveProject(ctx, resolveArgs);
|
|
55
|
+
|
|
56
|
+
const type = (flags.type || "dev").toLowerCase();
|
|
57
|
+
if (!VALID_TYPES.has(type)) {
|
|
58
|
+
throw new Error(
|
|
59
|
+
`Invalid --type: ${type}. Must be one of: ${[...VALID_TYPES].join(", ")}.`,
|
|
60
|
+
);
|
|
61
|
+
}
|
|
62
|
+
const ha = flags.ha === true || flags.ha === "true";
|
|
63
|
+
const isDefault = flags.default === true || flags.default === "true";
|
|
64
|
+
const yes = flags.yes === true || flags.yes === "true";
|
|
65
|
+
|
|
66
|
+
// Prod confirmation. Mirrors `synapse deploy`'s prompt — operator
|
|
67
|
+
// muscle-memory expects the same friction. Skipped in --json mode
|
|
68
|
+
// (CI scripts must pass --yes explicitly anyway).
|
|
69
|
+
if (type === "prod" && !yes && !ctx.out.json) {
|
|
70
|
+
const confirmed = await confirm(
|
|
71
|
+
`Create a NEW PROD deployment under ${source === "linked" ? "this directory's linked project" : projectId}? [y/N] `,
|
|
72
|
+
{ defaultAnswer: false },
|
|
73
|
+
);
|
|
74
|
+
if (!confirmed) {
|
|
75
|
+
ctx.out.info("Aborted.");
|
|
76
|
+
process.exitCode = 1;
|
|
77
|
+
return;
|
|
78
|
+
}
|
|
79
|
+
}
|
|
80
|
+
if (type === "prod" && !yes && ctx.out.json) {
|
|
81
|
+
// Fail closed in JSON mode so a script can't accidentally
|
|
82
|
+
// provision prod without an explicit --yes.
|
|
83
|
+
throw new Error(
|
|
84
|
+
"Refusing to create a prod deployment in --json mode without --yes.",
|
|
85
|
+
);
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
if (!ctx.out.json) {
|
|
89
|
+
ctx.out.info(
|
|
90
|
+
`Creating ${type}${ha ? " (HA)" : ""}${isDefault ? " [default]" : ""} deployment in project ${projectId}…`,
|
|
91
|
+
);
|
|
92
|
+
}
|
|
93
|
+
const body = { type };
|
|
94
|
+
if (ha) body.ha = true;
|
|
95
|
+
if (isDefault) body.isDefault = true;
|
|
96
|
+
const created = await ctx.api.createDeployment(projectId, body);
|
|
97
|
+
|
|
98
|
+
ctx.out.result(
|
|
99
|
+
{
|
|
100
|
+
projectId,
|
|
101
|
+
projectSource: source,
|
|
102
|
+
deployment: created,
|
|
103
|
+
},
|
|
104
|
+
(_d, { stdout }) => {
|
|
105
|
+
stdout.write(
|
|
106
|
+
`${colors.green("Created")} ${colors.bold(created.name)} ${colors.dim(`(${created.deploymentType || type})`)}\n`,
|
|
107
|
+
);
|
|
108
|
+
if (created.deploymentUrl || created.url) {
|
|
109
|
+
stdout.write(colors.dim(`URL: ${created.deploymentUrl || created.url}\n`));
|
|
110
|
+
}
|
|
111
|
+
if (created.status) {
|
|
112
|
+
stdout.write(colors.dim(`Status: ${created.status}\n`));
|
|
113
|
+
}
|
|
114
|
+
stdout.write(
|
|
115
|
+
colors.dim(
|
|
116
|
+
`Tip: run \`synapse deployment status ${created.name} --watch\` to track readiness, or \`synapse select\` to link this directory to it.\n`,
|
|
117
|
+
),
|
|
118
|
+
);
|
|
119
|
+
},
|
|
120
|
+
);
|
|
121
|
+
},
|
|
122
|
+
};
|
|
@@ -0,0 +1,117 @@
|
|
|
1
|
+
// `synapse deployment delete <name> [--yes] [--confirm=<name>] [--json]`
|
|
2
|
+
//
|
|
3
|
+
// Destroys the container + storage volume. Irreversible.
|
|
4
|
+
//
|
|
5
|
+
// Safety levels:
|
|
6
|
+
// - dev: single `--yes` confirms (or interactive y/N prompt)
|
|
7
|
+
// - prod: typed-confirmation REQUIRED — operator must re-type the
|
|
8
|
+
// deployment name. `--yes` is NOT enough for prod (mirrors the
|
|
9
|
+
// dashboard's v1.7.2 typed-confirm flow). Non-interactive callers
|
|
10
|
+
// pass `--confirm=<exact-name>` instead.
|
|
11
|
+
//
|
|
12
|
+
// The deployment type is fetched from the backend before the delete
|
|
13
|
+
// fires so we know whether to ask for typed-confirm. One extra GET is
|
|
14
|
+
// cheap and prevents the "I thought this was dev" footgun.
|
|
15
|
+
|
|
16
|
+
const colors = require("../colors");
|
|
17
|
+
const { extractFlags } = require("./_resource");
|
|
18
|
+
const { confirm, typedConfirm } = require("../prompts");
|
|
19
|
+
const { SynapseAPIError } = require("../api");
|
|
20
|
+
|
|
21
|
+
module.exports = {
|
|
22
|
+
name: "deployment delete",
|
|
23
|
+
summary: "Delete a deployment (container + volume — irreversible).",
|
|
24
|
+
usage:
|
|
25
|
+
"synapse deployment delete <name> [--yes] [--confirm=<name>] [--json]",
|
|
26
|
+
description: `Calls POST /v1/deployments/{name}/delete. The container is destroyed and the storage volume is wiped. Production deployments require typed confirmation — re-type the deployment name.
|
|
27
|
+
|
|
28
|
+
Flags:
|
|
29
|
+
--yes Skip the y/N prompt for dev deployments.
|
|
30
|
+
--confirm=<name> Pre-supply the typed confirmation (CI usage). For
|
|
31
|
+
prod deployments this is mandatory in --json mode.
|
|
32
|
+
--json Machine-readable output.
|
|
33
|
+
|
|
34
|
+
Examples:
|
|
35
|
+
synapse deployment delete brave-dolphin-1060
|
|
36
|
+
synapse deployment delete brave-dolphin-1060 --yes # dev only
|
|
37
|
+
synapse deployment delete wise-otter-2200 --confirm=wise-otter-2200`,
|
|
38
|
+
|
|
39
|
+
async run(args, ctx) {
|
|
40
|
+
const { flags, rest } = extractFlags(args, {
|
|
41
|
+
string: ["confirm"],
|
|
42
|
+
boolean: ["yes", "json"],
|
|
43
|
+
});
|
|
44
|
+
const name = rest[0];
|
|
45
|
+
if (!name) {
|
|
46
|
+
throw new Error(
|
|
47
|
+
"Usage: synapse deployment delete <name> [--yes|--confirm=<name>]",
|
|
48
|
+
);
|
|
49
|
+
}
|
|
50
|
+
if (rest.length > 1) {
|
|
51
|
+
throw new Error(`Unexpected positional: ${rest[1]}.`);
|
|
52
|
+
}
|
|
53
|
+
const yes = flags.yes === true || flags.yes === "true";
|
|
54
|
+
const typed = typeof flags.confirm === "string" ? flags.confirm : null;
|
|
55
|
+
|
|
56
|
+
// Look up the deployment to learn its type. 404 here is the most
|
|
57
|
+
// common operator typo case — surface a clear error before any
|
|
58
|
+
// destructive call.
|
|
59
|
+
let dep;
|
|
60
|
+
try {
|
|
61
|
+
dep = await ctx.api.getDeployment(name);
|
|
62
|
+
} catch (err) {
|
|
63
|
+
if (err instanceof SynapseAPIError && err.status === 404) {
|
|
64
|
+
throw new Error(
|
|
65
|
+
`Deployment "${name}" not found. Run \`synapse list deployments\` to see your deployments.`,
|
|
66
|
+
);
|
|
67
|
+
}
|
|
68
|
+
throw err;
|
|
69
|
+
}
|
|
70
|
+
const type = dep.deploymentType || dep.type || "unknown";
|
|
71
|
+
|
|
72
|
+
if (type === "prod") {
|
|
73
|
+
// Typed-confirmation MANDATORY. --yes alone does not bypass it.
|
|
74
|
+
if (!typed && ctx.out.json) {
|
|
75
|
+
throw new Error(
|
|
76
|
+
`Refusing to delete prod deployment "${name}" in --json mode without --confirm=${name}.`,
|
|
77
|
+
);
|
|
78
|
+
}
|
|
79
|
+
const ok = await typedConfirm("delete", name, { typed });
|
|
80
|
+
if (!ok) {
|
|
81
|
+
ctx.out.info(`Aborted (confirmation didn't match "${name}").`);
|
|
82
|
+
process.exitCode = 1;
|
|
83
|
+
return;
|
|
84
|
+
}
|
|
85
|
+
} else {
|
|
86
|
+
// Dev / preview / custom — y/N prompt unless --yes was passed.
|
|
87
|
+
if (!yes && !typed && !ctx.out.json) {
|
|
88
|
+
const confirmed = await confirm(
|
|
89
|
+
`Delete ${type} deployment ${colors.bold(name)}? [y/N] `,
|
|
90
|
+
{ defaultAnswer: false },
|
|
91
|
+
);
|
|
92
|
+
if (!confirmed) {
|
|
93
|
+
ctx.out.info("Aborted.");
|
|
94
|
+
process.exitCode = 1;
|
|
95
|
+
return;
|
|
96
|
+
}
|
|
97
|
+
}
|
|
98
|
+
if (!yes && !typed && ctx.out.json) {
|
|
99
|
+
throw new Error(
|
|
100
|
+
`Refusing to delete "${name}" in --json mode without --yes.`,
|
|
101
|
+
);
|
|
102
|
+
}
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
if (!ctx.out.json) {
|
|
106
|
+
ctx.out.info(`Deleting ${type} deployment ${name}…`);
|
|
107
|
+
}
|
|
108
|
+
await ctx.api.deleteDeployment(name);
|
|
109
|
+
|
|
110
|
+
ctx.out.result(
|
|
111
|
+
{ deleted: true, name, type },
|
|
112
|
+
(_d, { stdout }) => {
|
|
113
|
+
stdout.write(`${colors.red("Deleted")} ${colors.bold(name)} ${colors.dim(`(${type})`)}\n`);
|
|
114
|
+
},
|
|
115
|
+
);
|
|
116
|
+
},
|
|
117
|
+
};
|