@iann29/synapse 1.8.6 → 1.8.8
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/_help.js +2 -1
- package/lib/commands/_resource.js +152 -0
- 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/https-doctor.js +73 -0
- package/lib/commands/https-migrate.js +168 -0
- package/lib/commands/https-remove.js +103 -0
- package/lib/commands/https-setup.js +263 -0
- package/lib/commands/https-status.js +154 -0
- package/lib/commands/list.js +186 -0
- package/lib/https/detect.js +603 -0
- package/lib/https/executor.js +102 -0
- package/lib/https/hosts.js +356 -0
- package/lib/https/mkcert.js +131 -0
- package/lib/https/nextjs.js +91 -0
- package/lib/https/planner.js +412 -0
- 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/_help.js
CHANGED
|
@@ -14,12 +14,13 @@ 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"],
|
|
21
21
|
["Env vars", "env"],
|
|
22
22
|
["Domains", "domain"],
|
|
23
|
+
["Local HTTPS dev", "https"],
|
|
23
24
|
["Escape hatch", ["convex"]],
|
|
24
25
|
];
|
|
25
26
|
|
|
@@ -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
|
+
};
|
|
@@ -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
|
+
};
|
|
@@ -0,0 +1,173 @@
|
|
|
1
|
+
// `synapse deployment rotate-key <name> [--yes] [--confirm=<name>] [--write] [--json]`
|
|
2
|
+
//
|
|
3
|
+
// Calls POST /v1/deployments/{name}/reissue_admin_key. Re-mints the
|
|
4
|
+
// admin_key from the current INSTANCE_SECRET (no container restart).
|
|
5
|
+
//
|
|
6
|
+
// What this does NOT do:
|
|
7
|
+
// - It does NOT rotate INSTANCE_SECRET (so existing deploy keys keep
|
|
8
|
+
// working). To actually invalidate a leaked credential you'd need
|
|
9
|
+
// to recreate the deployment.
|
|
10
|
+
// - It does NOT auto-update the operator's .env.local. We surface a
|
|
11
|
+
// --write flag that opt-in does it (using the same writer as
|
|
12
|
+
// `synapse select`), but the default is "show the key and let the
|
|
13
|
+
// operator paste it".
|
|
14
|
+
//
|
|
15
|
+
// Safety:
|
|
16
|
+
// - prod deployments REQUIRE typed-confirm (--confirm=<name>) since
|
|
17
|
+
// rotating mid-flight breaks the iframed Convex Dashboard until the
|
|
18
|
+
// operator hits "Refresh credentials".
|
|
19
|
+
// - dev: `--yes` suffices.
|
|
20
|
+
|
|
21
|
+
const colors = require("../colors");
|
|
22
|
+
const { extractFlags } = require("./_resource");
|
|
23
|
+
const { confirm, typedConfirm } = require("../prompts");
|
|
24
|
+
const { SynapseAPIError } = require("../api");
|
|
25
|
+
const { writeProjectEnv } = require("../env-file");
|
|
26
|
+
|
|
27
|
+
module.exports = {
|
|
28
|
+
name: "deployment rotate-key",
|
|
29
|
+
summary: "Re-mint a deployment's admin key (cures stale credentials).",
|
|
30
|
+
usage:
|
|
31
|
+
"synapse deployment rotate-key <name> [--yes] [--confirm=<name>] [--write] [--json]",
|
|
32
|
+
description: `Calls POST /v1/deployments/{name}/reissue_admin_key. Used when the stored admin_key drifts out of sync with the running container (symptom: "deployment URL or admin key is invalid" inside the Convex Dashboard iframe).
|
|
33
|
+
|
|
34
|
+
The deployment's INSTANCE_SECRET is NOT rotated — existing deploy keys keep working. If you need to invalidate a leaked credential, delete + recreate the deployment.
|
|
35
|
+
|
|
36
|
+
Flags:
|
|
37
|
+
--yes Skip the y/N prompt (dev only).
|
|
38
|
+
--confirm=<name> Pre-supply the typed confirmation for prod.
|
|
39
|
+
--write Also rewrite .env.local in the current directory
|
|
40
|
+
IF this is the linked dev deployment.
|
|
41
|
+
--json Machine-readable output.
|
|
42
|
+
|
|
43
|
+
Examples:
|
|
44
|
+
synapse deployment rotate-key brave-dolphin-1060 --yes
|
|
45
|
+
synapse deployment rotate-key wise-otter-2200 --confirm=wise-otter-2200 --write`,
|
|
46
|
+
|
|
47
|
+
async run(args, ctx) {
|
|
48
|
+
const { flags, rest } = extractFlags(args, {
|
|
49
|
+
string: ["confirm"],
|
|
50
|
+
boolean: ["yes", "write", "json"],
|
|
51
|
+
});
|
|
52
|
+
const name = rest[0];
|
|
53
|
+
if (!name) {
|
|
54
|
+
throw new Error(
|
|
55
|
+
"Usage: synapse deployment rotate-key <name> [--yes|--confirm=<name>]",
|
|
56
|
+
);
|
|
57
|
+
}
|
|
58
|
+
if (rest.length > 1) {
|
|
59
|
+
throw new Error(`Unexpected positional: ${rest[1]}.`);
|
|
60
|
+
}
|
|
61
|
+
const yes = flags.yes === true || flags.yes === "true";
|
|
62
|
+
const write = flags.write === true || flags.write === "true";
|
|
63
|
+
const typed = typeof flags.confirm === "string" ? flags.confirm : null;
|
|
64
|
+
|
|
65
|
+
let dep;
|
|
66
|
+
try {
|
|
67
|
+
dep = await ctx.api.getDeployment(name);
|
|
68
|
+
} catch (err) {
|
|
69
|
+
if (err instanceof SynapseAPIError && err.status === 404) {
|
|
70
|
+
throw new Error(
|
|
71
|
+
`Deployment "${name}" not found. Run \`synapse list deployments\` to see your deployments.`,
|
|
72
|
+
);
|
|
73
|
+
}
|
|
74
|
+
throw err;
|
|
75
|
+
}
|
|
76
|
+
const type = dep.deploymentType || dep.type || "unknown";
|
|
77
|
+
|
|
78
|
+
// Adopted deployments can't be rotated — backend will 400, but we
|
|
79
|
+
// catch it earlier with a clearer message.
|
|
80
|
+
if (dep.adopted) {
|
|
81
|
+
throw new Error(
|
|
82
|
+
`Cannot rotate key for "${name}" — it's an adopted (external) deployment. Rotate the admin key on the source side and re-adopt.`,
|
|
83
|
+
);
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
if (type === "prod") {
|
|
87
|
+
if (!typed && ctx.out.json) {
|
|
88
|
+
throw new Error(
|
|
89
|
+
`Refusing to rotate prod key for "${name}" in --json mode without --confirm=${name}.`,
|
|
90
|
+
);
|
|
91
|
+
}
|
|
92
|
+
const ok = await typedConfirm("rotate-key", name, { typed });
|
|
93
|
+
if (!ok) {
|
|
94
|
+
ctx.out.info(`Aborted (confirmation didn't match "${name}").`);
|
|
95
|
+
process.exitCode = 1;
|
|
96
|
+
return;
|
|
97
|
+
}
|
|
98
|
+
} else {
|
|
99
|
+
if (!yes && !typed && !ctx.out.json) {
|
|
100
|
+
const confirmed = await confirm(
|
|
101
|
+
`Rotate admin key for ${type} deployment ${colors.bold(name)}? [y/N] `,
|
|
102
|
+
{ defaultAnswer: false },
|
|
103
|
+
);
|
|
104
|
+
if (!confirmed) {
|
|
105
|
+
ctx.out.info("Aborted.");
|
|
106
|
+
process.exitCode = 1;
|
|
107
|
+
return;
|
|
108
|
+
}
|
|
109
|
+
}
|
|
110
|
+
if (!yes && !typed && ctx.out.json) {
|
|
111
|
+
throw new Error(
|
|
112
|
+
`Refusing to rotate "${name}" in --json mode without --yes.`,
|
|
113
|
+
);
|
|
114
|
+
}
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
if (!ctx.out.json) {
|
|
118
|
+
ctx.out.info(`Rotating admin key for ${name}…`);
|
|
119
|
+
}
|
|
120
|
+
const res = await ctx.api.reissueAdminKey(name);
|
|
121
|
+
|
|
122
|
+
// Optional .env.local rewrite. Only fires when:
|
|
123
|
+
// - --write was passed
|
|
124
|
+
// - the rotated deployment is the linked dev deployment in CWD
|
|
125
|
+
// - we can fetch fresh credentials to wire in the new url+key
|
|
126
|
+
let envWritten = null;
|
|
127
|
+
if (write) {
|
|
128
|
+
const linked = ctx.projectConfig;
|
|
129
|
+
const matchedDev = linked?.deployments?.dev?.name === name;
|
|
130
|
+
if (!matchedDev) {
|
|
131
|
+
if (!ctx.out.json) {
|
|
132
|
+
ctx.out.warn(
|
|
133
|
+
"--write skipped: this is not the linked dev deployment in the current directory.",
|
|
134
|
+
);
|
|
135
|
+
}
|
|
136
|
+
} else {
|
|
137
|
+
const creds = await ctx.api.cliCredentials(name);
|
|
138
|
+
envWritten = writeProjectEnv(ctx.cwd, creds, {
|
|
139
|
+
team: { slug: linked.team?.slug, name: linked.team?.name },
|
|
140
|
+
project: { slug: linked.project?.slug, name: linked.project?.name },
|
|
141
|
+
target: "dev",
|
|
142
|
+
});
|
|
143
|
+
}
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
ctx.out.result(
|
|
147
|
+
{
|
|
148
|
+
rotated: true,
|
|
149
|
+
deploymentName: res.deploymentName || name,
|
|
150
|
+
prefix: res.prefix,
|
|
151
|
+
// Full new key. Same exposure profile as `synapse credentials`
|
|
152
|
+
// — operator already had this on disk before rotation; the
|
|
153
|
+
// surface is unchanged.
|
|
154
|
+
adminKey: res.adminKey,
|
|
155
|
+
envWritten,
|
|
156
|
+
},
|
|
157
|
+
(_d, { stdout }) => {
|
|
158
|
+
stdout.write(
|
|
159
|
+
`${colors.green("Rotated")} ${colors.bold(name)} ${colors.dim(`(prefix ${res.prefix})`)}\n`,
|
|
160
|
+
);
|
|
161
|
+
stdout.write(colors.dim("New admin key (paste into .env.local or pass --write):\n"));
|
|
162
|
+
stdout.write(res.adminKey + "\n");
|
|
163
|
+
if (envWritten) {
|
|
164
|
+
stdout.write(colors.green(`Updated ${envWritten}\n`));
|
|
165
|
+
} else if (write) {
|
|
166
|
+
stdout.write(
|
|
167
|
+
colors.dim("Hint: pass --write only when rotating the linked dev deployment.\n"),
|
|
168
|
+
);
|
|
169
|
+
}
|
|
170
|
+
},
|
|
171
|
+
);
|
|
172
|
+
},
|
|
173
|
+
};
|