@iann29/synapse 1.8.4 → 1.8.6
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/lib/commands/_context.js +58 -6
- package/lib/commands/convex.js +17 -0
- package/lib/commands/credentials.js +22 -1
- package/lib/commands/login.js +14 -1
- package/lib/commands/select.js +7 -1
- package/lib/commands/status.js +16 -1
- package/lib/doctor/renderer.js +48 -1
- package/lib/doctor/runner.js +27 -1
- package/package.json +1 -1
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/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");
|
package/lib/commands/login.js
CHANGED
|
@@ -37,7 +37,20 @@ The saved session carries both the access token (1h TTL) and a refresh token (30
|
|
|
37
37
|
});
|
|
38
38
|
ctx.out.result(
|
|
39
39
|
{ baseUrl, user: session.user || null, configPath: file },
|
|
40
|
-
() =>
|
|
40
|
+
() => {
|
|
41
|
+
ctx.out.info(`Saved Synapse session to ${file}`);
|
|
42
|
+
// v1.8.6 (A2): post-login next-step hint. Without this, the
|
|
43
|
+
// operator just sees "saved" and stares at the prompt — login
|
|
44
|
+
// is step 1 of a 3-step onboarding (login → select → dev),
|
|
45
|
+
// and the absence of step 2 was the most common first-run
|
|
46
|
+
// dead-end reported.
|
|
47
|
+
ctx.out.info(
|
|
48
|
+
`\nNext step: \`cd\` into your app directory and run \`synapse select\` to link it to a project + deployment.`,
|
|
49
|
+
);
|
|
50
|
+
ctx.out.info(
|
|
51
|
+
`Then run \`synapse doctor\` to confirm everything is healthy, or \`synapse open\` to browse the dashboard.`,
|
|
52
|
+
);
|
|
53
|
+
},
|
|
41
54
|
);
|
|
42
55
|
},
|
|
43
56
|
};
|
package/lib/commands/select.js
CHANGED
|
@@ -222,8 +222,11 @@ show up in the menu).`,
|
|
|
222
222
|
if (prod) {
|
|
223
223
|
ctx.out.info(`Selected prod deployment ${colors.bold(prod.name)}.`);
|
|
224
224
|
} else {
|
|
225
|
+
// v1.8.5: surface the dashboard URL so the operator can click
|
|
226
|
+
// straight to "New deployment" instead of hunting for it.
|
|
227
|
+
const projectUrl = `${cfg.baseUrl}/teams/${encodeURIComponent(team.slug || team.id)}/${encodeURIComponent(project.id)}`;
|
|
225
228
|
ctx.out.warn(
|
|
226
|
-
|
|
229
|
+
`no prod deployment found. Create one at ${projectUrl} → "New deployment" → mark as PROD, then run \`synapse select\` again. \`synapse deploy\` fails until then.`,
|
|
227
230
|
);
|
|
228
231
|
}
|
|
229
232
|
if (process.env.CONVEX_DEPLOYMENT) {
|
|
@@ -234,6 +237,9 @@ show up in the menu).`,
|
|
|
234
237
|
ctx.out.info(
|
|
235
238
|
`\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.`,
|
|
236
239
|
);
|
|
240
|
+
ctx.out.info(
|
|
241
|
+
`Or run ${colors.bold("synapse doctor")} first to confirm everything is healthy (add ${colors.bold("--fix")} to auto-clean .gitignore + file modes).`,
|
|
242
|
+
);
|
|
237
243
|
},
|
|
238
244
|
);
|
|
239
245
|
// Force ctx.projectConfig to re-read on next access (commands chained
|
package/lib/commands/status.js
CHANGED
|
@@ -146,12 +146,27 @@ Run \`synapse doctor\` for a deeper health check.`,
|
|
|
146
146
|
],
|
|
147
147
|
);
|
|
148
148
|
const broken = rows.filter((r) => r.urlForm === "host").length;
|
|
149
|
+
const hasProd = rows.some((r) => r.type === "prod");
|
|
150
|
+
const hasDev = rows.some((r) => r.type === "dev");
|
|
151
|
+
|
|
149
152
|
if (broken > 0) {
|
|
150
153
|
stdout.write("\n");
|
|
151
154
|
ctx.out.warn(
|
|
152
|
-
`${broken} deployment${broken > 1 ? "s" : ""} not browser-reachable —
|
|
155
|
+
`${broken} deployment${broken > 1 ? "s" : ""} not browser-reachable — instance admin can enable wildcard at ${baseUrl}/admin/host-domain, or add a custom domain per deployment.`,
|
|
153
156
|
);
|
|
154
157
|
}
|
|
158
|
+
// v1.8.5 hints: when nothing's broken, surface the next-step
|
|
159
|
+
// bread-crumb so first-time operators know what to do next.
|
|
160
|
+
if (broken === 0 && rows.length > 0) {
|
|
161
|
+
stdout.write("\n");
|
|
162
|
+
if (!hasProd && hasDev) {
|
|
163
|
+
ctx.out.info(
|
|
164
|
+
`No prod deployment yet. Create one at ${baseUrl}/teams/<team>/${projectId} → "New deployment" → PROD, then \`synapse select\` again.`,
|
|
165
|
+
);
|
|
166
|
+
} else if (hasDev) {
|
|
167
|
+
ctx.out.info("Run `synapse dev` to start a Convex dev session in this directory.");
|
|
168
|
+
}
|
|
169
|
+
}
|
|
155
170
|
},
|
|
156
171
|
);
|
|
157
172
|
},
|
package/lib/doctor/renderer.js
CHANGED
|
@@ -33,6 +33,18 @@ function renderHeader(report, write) {
|
|
|
33
33
|
write(parts.join(" ") + "\n\n");
|
|
34
34
|
}
|
|
35
35
|
|
|
36
|
+
// v1.8.5: inline-tag formatter for the fix hint. Mirrors the cyan-on-dim
|
|
37
|
+
// rest of the remediation line; falls back to plain text if colors.cyan
|
|
38
|
+
// isn't defined (the `colors` shim is a no-op on non-tty stdouts).
|
|
39
|
+
function fixableTag(r) {
|
|
40
|
+
if (!r.fixable) return "";
|
|
41
|
+
if (r.fixedBy) return ""; // already fixed; no nudge needed
|
|
42
|
+
const text = r.fixable === "auto"
|
|
43
|
+
? " [run `synapse doctor --fix`]"
|
|
44
|
+
: " [run `synapse doctor --fix --yes`]";
|
|
45
|
+
return colors.cyan ? colors.cyan(text) : text;
|
|
46
|
+
}
|
|
47
|
+
|
|
36
48
|
function renderCheck(r, { verbose }, write) {
|
|
37
49
|
const sym = TONE[r.status](SYMBOL[r.status]);
|
|
38
50
|
write(` ${sym} ${r.title}`);
|
|
@@ -40,7 +52,9 @@ function renderCheck(r, { verbose }, write) {
|
|
|
40
52
|
if (r.fixedBy) write(` ${colors.cyan ? colors.cyan("↻ " + r.fixedBy) : "↻ " + r.fixedBy}`);
|
|
41
53
|
write("\n");
|
|
42
54
|
if (r.remediation && (r.status === "issue" || r.status === "warn")) {
|
|
43
|
-
write(colors.dim(` → ${r.remediation}
|
|
55
|
+
write(colors.dim(` → ${r.remediation}`));
|
|
56
|
+
write(fixableTag(r));
|
|
57
|
+
write("\n");
|
|
44
58
|
}
|
|
45
59
|
if (verbose && r.detail) {
|
|
46
60
|
write(colors.dim(` ${r.detail.replace(/\n/g, "\n ")}\n`));
|
|
@@ -88,6 +102,39 @@ function renderReport(report, { stdout, verbose = false } = {}) {
|
|
|
88
102
|
if (report.exitCode !== 0) {
|
|
89
103
|
write(` ${colors.dim("exit " + report.exitCode)}\n`);
|
|
90
104
|
}
|
|
105
|
+
|
|
106
|
+
// v1.8.5: actionable tip footer. Surfaces the right `--fix` invocation
|
|
107
|
+
// for whichever class of fixable issues remains, so an operator who
|
|
108
|
+
// just sees a wall of `! ...` warnings knows exactly which command
|
|
109
|
+
// would clean them up — no need to memorise the auto-vs-prompt split.
|
|
110
|
+
const autoCount = t.fixableAuto || 0;
|
|
111
|
+
const promptCount = t.fixablePrompt || 0;
|
|
112
|
+
const tip = (msg) =>
|
|
113
|
+
` ${colors.cyan ? colors.cyan("💡 Tip:") : "Tip:"} ${msg}\n`;
|
|
114
|
+
if (autoCount + promptCount > 0) {
|
|
115
|
+
write("\n");
|
|
116
|
+
if (autoCount > 0 && promptCount > 0) {
|
|
117
|
+
write(tip(
|
|
118
|
+
`${autoCount} auto-fixable + ${promptCount} prompt-fixable — run \`synapse doctor --fix --yes\` to apply both.`,
|
|
119
|
+
));
|
|
120
|
+
} else if (autoCount > 0) {
|
|
121
|
+
write(tip(
|
|
122
|
+
`${autoCount} ${autoCount === 1 ? "issue is" : "issues are"} auto-fixable — run \`synapse doctor --fix\`.`,
|
|
123
|
+
));
|
|
124
|
+
} else {
|
|
125
|
+
write(tip(
|
|
126
|
+
`${promptCount} ${promptCount === 1 ? "issue needs" : "issues need"} confirmation — run \`synapse doctor --fix --yes\`.`,
|
|
127
|
+
));
|
|
128
|
+
}
|
|
129
|
+
} else if (t.warn + t.issue === 0 && t.ok > 0) {
|
|
130
|
+
// Clean bill of health → point at the next step. Operators who run
|
|
131
|
+
// doctor first thing after `synapse select` shouldn't have to wonder
|
|
132
|
+
// what's next.
|
|
133
|
+
write("\n");
|
|
134
|
+
write(tip(
|
|
135
|
+
"everything healthy — run `synapse dev` to start working, or `synapse status` to list deployments.",
|
|
136
|
+
));
|
|
137
|
+
}
|
|
91
138
|
}
|
|
92
139
|
|
|
93
140
|
module.exports = { renderReport };
|
package/lib/doctor/runner.js
CHANGED
|
@@ -53,10 +53,21 @@ async function runChecks(checks, ctx) {
|
|
|
53
53
|
continue;
|
|
54
54
|
}
|
|
55
55
|
const result = await check.run(ctx);
|
|
56
|
+
// `fixable` carries the autoFix kind into the result so the
|
|
57
|
+
// renderer (and --json consumers) can surface a "this is
|
|
58
|
+
// fixable, here's the right flag" hint without re-importing
|
|
59
|
+
// the check catalog. Skipped checks aren't marked fixable —
|
|
60
|
+
// the fix wouldn't have ctx to operate on anyway.
|
|
61
|
+
const fixable =
|
|
62
|
+
typeof check.fix === "function" &&
|
|
63
|
+
(check.autoFix === "auto" || check.autoFix === "prompt")
|
|
64
|
+
? check.autoFix
|
|
65
|
+
: null;
|
|
56
66
|
resultsById.set(id, {
|
|
57
67
|
id,
|
|
58
68
|
category: check.category,
|
|
59
69
|
title: check.title,
|
|
70
|
+
fixable,
|
|
60
71
|
...result,
|
|
61
72
|
});
|
|
62
73
|
pending.delete(id);
|
|
@@ -83,13 +94,28 @@ async function runChecks(checks, ctx) {
|
|
|
83
94
|
}
|
|
84
95
|
|
|
85
96
|
function totalize(results) {
|
|
86
|
-
const t = {
|
|
97
|
+
const t = {
|
|
98
|
+
ok: 0,
|
|
99
|
+
warn: 0,
|
|
100
|
+
issue: 0,
|
|
101
|
+
skipped: 0,
|
|
102
|
+
fixed: 0,
|
|
103
|
+
// v1.8.5: count of results still in warn/issue that have a
|
|
104
|
+
// registered fix. Splits auto vs prompt so the renderer can show
|
|
105
|
+
// the right CLI hint at the bottom.
|
|
106
|
+
fixableAuto: 0,
|
|
107
|
+
fixablePrompt: 0,
|
|
108
|
+
};
|
|
87
109
|
for (const r of results) {
|
|
88
110
|
if (r.fixedBy) t.fixed += 1;
|
|
89
111
|
if (r.status === "ok") t.ok += 1;
|
|
90
112
|
else if (r.status === "warn") t.warn += 1;
|
|
91
113
|
else if (r.status === "issue") t.issue += 1;
|
|
92
114
|
else if (r.status === "skipped") t.skipped += 1;
|
|
115
|
+
if ((r.status === "warn" || r.status === "issue") && !r.fixedBy) {
|
|
116
|
+
if (r.fixable === "auto") t.fixableAuto += 1;
|
|
117
|
+
else if (r.fixable === "prompt") t.fixablePrompt += 1;
|
|
118
|
+
}
|
|
93
119
|
}
|
|
94
120
|
return t;
|
|
95
121
|
}
|