planche 0.7.1 → 0.7.2
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/README.md +57 -4
- package/dist/index.js +38 -6
- package/package.json +2 -2
package/README.md
CHANGED
|
@@ -116,6 +116,10 @@ database connections open and forwards queries with the matching driver
|
|
|
116
116
|
`~/.ssh/known_hosts` are honoured too). A changed key refuses the
|
|
117
117
|
connection until the stale entry is removed.
|
|
118
118
|
|
|
119
|
+
When `planche.dev` cannot be reached, the agent serves the copy of the app it
|
|
120
|
+
ships with (at its own address) instead of opening a browser error page, so it
|
|
121
|
+
keeps working offline. `--no-serve-app` turns that off.
|
|
122
|
+
|
|
119
123
|
## Gateway mode
|
|
120
124
|
|
|
121
125
|
Run one agent on a shared host so a team can use Planche with just a
|
|
@@ -132,14 +136,63 @@ npx planche --gateway gateway.json --addr 0.0.0.0 --allow-remote \
|
|
|
132
136
|
{
|
|
133
137
|
"name": "Acme staging",
|
|
134
138
|
"profiles": [
|
|
135
|
-
{
|
|
136
|
-
"
|
|
137
|
-
|
|
138
|
-
"host": "
|
|
139
|
+
{
|
|
140
|
+
"name": "Shop (read-only)",
|
|
141
|
+
"driver": "mysql",
|
|
142
|
+
"host": "db.internal",
|
|
143
|
+
"port": 3306,
|
|
144
|
+
"user": "reporting",
|
|
145
|
+
"password": "…",
|
|
146
|
+
"database": "shop"
|
|
147
|
+
},
|
|
148
|
+
{
|
|
149
|
+
"name": "Shop (writable)",
|
|
150
|
+
"readOnly": false,
|
|
151
|
+
"driver": "postgres",
|
|
152
|
+
"host": "pg.internal",
|
|
153
|
+
"port": 5432,
|
|
154
|
+
"user": "app",
|
|
155
|
+
"password": "…",
|
|
156
|
+
"database": "shop"
|
|
157
|
+
}
|
|
139
158
|
]
|
|
140
159
|
}
|
|
141
160
|
```
|
|
142
161
|
|
|
162
|
+
`name` is the gateway's own name and `profiles[].name` is what browsers pick;
|
|
163
|
+
every other field is what the connection form asks for, under the same names
|
|
164
|
+
(`serviceName` for Oracle, `file` for SQLite, `accountId` / `databaseId` /
|
|
165
|
+
`apiToken` for D1, and an `ssh` block where you need one).
|
|
166
|
+
|
|
167
|
+
A profile behind a jump host carries that `ssh` block; `host`/`port` are then
|
|
168
|
+
resolved on the jump host, so they are usually loopback:
|
|
169
|
+
|
|
170
|
+
```json
|
|
171
|
+
{
|
|
172
|
+
"name": "Shop via bastion (read-only)",
|
|
173
|
+
"driver": "mysql",
|
|
174
|
+
"host": "127.0.0.1",
|
|
175
|
+
"port": 3306,
|
|
176
|
+
"user": "reporting",
|
|
177
|
+
"password": "…",
|
|
178
|
+
"database": "shop",
|
|
179
|
+
"ssh": {
|
|
180
|
+
"host": "bastion.example.com",
|
|
181
|
+
"port": 22,
|
|
182
|
+
"user": "deploy",
|
|
183
|
+
"auth": "key"
|
|
184
|
+
}
|
|
185
|
+
}
|
|
186
|
+
```
|
|
187
|
+
|
|
188
|
+
With `auth: "key"` and no `privateKey`, the agent uses the `IdentityFile` from
|
|
189
|
+
this machine's `~/.ssh/config` (falling back to `~/.ssh/id_*`), so the key
|
|
190
|
+
never lands in the config file; `auth: "agent"` uses `SSH_AUTH_SOCK`. A
|
|
191
|
+
`privateKey` you do set is the key *contents*, not a path. The jump host's key
|
|
192
|
+
must already be in a `known_hosts` file here, or be pinned in the profile with
|
|
193
|
+
`acceptHostKey` — in gateway mode the browser sends only a profile name and
|
|
194
|
+
cannot answer the unknown-host-key prompt.
|
|
195
|
+
|
|
143
196
|
- Browsers only ever send `{ "profile": "Shop (read-only)" }`; `/connect`
|
|
144
197
|
bodies with credentials are refused in gateway mode.
|
|
145
198
|
- Profiles are **read-only by default**: the agent refuses anything but a
|
package/dist/index.js
CHANGED
|
@@ -2172,6 +2172,29 @@ var DEV_ORIGINS = [
|
|
|
2172
2172
|
"http://localhost:4173",
|
|
2173
2173
|
"http://127.0.0.1:4173"
|
|
2174
2174
|
];
|
|
2175
|
+
function bundledAppDir() {
|
|
2176
|
+
const dir = fileURLToPath(new URL("../app/", import.meta.url));
|
|
2177
|
+
return existsSync3(join5(dir, "index.html")) ? dir : null;
|
|
2178
|
+
}
|
|
2179
|
+
function isLoopbackUrl(url) {
|
|
2180
|
+
try {
|
|
2181
|
+
return LOOPBACK_ADDRS.has(new URL(url).hostname);
|
|
2182
|
+
} catch {
|
|
2183
|
+
return false;
|
|
2184
|
+
}
|
|
2185
|
+
}
|
|
2186
|
+
async function appIsReachable(url, timeoutMs = 2500) {
|
|
2187
|
+
const controller = new AbortController();
|
|
2188
|
+
const timer = setTimeout(() => controller.abort(), timeoutMs);
|
|
2189
|
+
try {
|
|
2190
|
+
const response = await fetch(url, { signal: controller.signal, redirect: "follow", headers: { "cache-control": "no-store" } });
|
|
2191
|
+
return response.ok;
|
|
2192
|
+
} catch {
|
|
2193
|
+
return false;
|
|
2194
|
+
} finally {
|
|
2195
|
+
clearTimeout(timer);
|
|
2196
|
+
}
|
|
2197
|
+
}
|
|
2175
2198
|
function resolveAppDir(explicit) {
|
|
2176
2199
|
const dir = explicit ? resolve3(explicit) : fileURLToPath(new URL("../app/", import.meta.url));
|
|
2177
2200
|
if (!existsSync3(join5(dir, "index.html"))) {
|
|
@@ -2198,8 +2221,8 @@ Options:
|
|
|
2198
2221
|
config instead of accepting credentials (env PLANCHE_GATEWAY)
|
|
2199
2222
|
--public-url <u> URL users open when the agent serves the app itself
|
|
2200
2223
|
(behind your TLS proxy; env PLANCHE_PUBLIC_URL)
|
|
2201
|
-
--no-serve-app
|
|
2202
|
-
|
|
2224
|
+
--no-serve-app Never serve the web app at / (it is served in gateway
|
|
2225
|
+
mode, and when planche.dev cannot be reached)
|
|
2203
2226
|
--app-dir <dir> Serve this build of the web app instead of the bundled one
|
|
2204
2227
|
--no-open Do not open the browser
|
|
2205
2228
|
--verbose Log every request
|
|
@@ -2230,7 +2253,7 @@ function openInBrowser(url) {
|
|
|
2230
2253
|
} catch {
|
|
2231
2254
|
}
|
|
2232
2255
|
}
|
|
2233
|
-
function main() {
|
|
2256
|
+
async function main() {
|
|
2234
2257
|
const major = Number(process.versions.node.split(".")[0]);
|
|
2235
2258
|
if (major < 22) {
|
|
2236
2259
|
console.error(`planche needs Node.js 22 or newer (you are running ${process.version}).`);
|
|
@@ -2288,10 +2311,13 @@ function main() {
|
|
|
2288
2311
|
process.exit(2);
|
|
2289
2312
|
}
|
|
2290
2313
|
}
|
|
2291
|
-
const
|
|
2314
|
+
const hostedAppUrl = (flags.app ?? process.env.PLANCHE_APP_URL ?? DEFAULT_APP_URL).replace(/\/+$/, "");
|
|
2315
|
+
let serveApp2 = flags["serve-app"] ?? Boolean(gateway);
|
|
2316
|
+
const offlineFallback = !serveApp2 && flags["serve-app"] !== false && bundledAppDir() !== null && !isLoopbackUrl(hostedAppUrl) && !await appIsReachable(hostedAppUrl);
|
|
2317
|
+
if (offlineFallback) serveApp2 = true;
|
|
2292
2318
|
const appDir = serveApp2 ? resolveAppDir(flags["app-dir"] ?? process.env.PLANCHE_APP_DIR) : void 0;
|
|
2293
2319
|
const publicUrl = (flags["public-url"] ?? process.env.PLANCHE_PUBLIC_URL ?? "").replace(/\/+$/, "");
|
|
2294
|
-
const appUrl = (serveApp2 ? publicUrl || null : null) ??
|
|
2320
|
+
const appUrl = (serveApp2 ? publicUrl || null : null) ?? hostedAppUrl;
|
|
2295
2321
|
const shouldOpen = flags.open ?? true;
|
|
2296
2322
|
const bodyLimit = process.env.PLANCHE_BODY_LIMIT || "50mb";
|
|
2297
2323
|
const appOrigin = originOf(appUrl);
|
|
@@ -2341,6 +2367,9 @@ function main() {
|
|
|
2341
2367
|
appDir
|
|
2342
2368
|
});
|
|
2343
2369
|
const pairUrl = serveApp2 ? `${publicUrl || agentUrl}/#pair=${encodeURIComponent(pairingCode.code)}` : `${appUrl}/#agent=${encodeURIComponent(agentUrl)}&pair=${encodeURIComponent(pairingCode.code)}`;
|
|
2370
|
+
if (offlineFallback) {
|
|
2371
|
+
console.log(`${DEFAULT_APP_URL === hostedAppUrl ? "planche.dev" : hostedAppUrl} is unreachable \u2014 serving the app bundled with this agent (v${VERSION}) instead.`);
|
|
2372
|
+
}
|
|
2344
2373
|
const server = app.listen(port, listenAddr, () => {
|
|
2345
2374
|
const line = "-".repeat(72);
|
|
2346
2375
|
console.log(line);
|
|
@@ -2377,4 +2406,7 @@ function main() {
|
|
|
2377
2406
|
process.on("uncaughtException", (err) => console.error("[agent] Uncaught exception:", err instanceof Error ? err.message : String(err)));
|
|
2378
2407
|
process.on("unhandledRejection", (err) => console.error("[agent] Unhandled rejection:", err instanceof Error ? err.message : String(err)));
|
|
2379
2408
|
}
|
|
2380
|
-
main()
|
|
2409
|
+
main().catch((err) => {
|
|
2410
|
+
console.error(err instanceof Error ? err.message : String(err));
|
|
2411
|
+
process.exit(1);
|
|
2412
|
+
});
|
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "planche",
|
|
3
|
-
"version": "0.7.
|
|
4
|
-
"description": "Local agent for Planche
|
|
3
|
+
"version": "0.7.2",
|
|
4
|
+
"description": "Local agent for Planche \u2014 lets the browser-based Planche SQL client reach MySQL/MariaDB/PostgreSQL/SQLite/SQL Server/Oracle/Neon/Supabase/Cloudflare D1 databases from your machine.",
|
|
5
5
|
"license": "UNLICENSED",
|
|
6
6
|
"homepage": "https://planche.dev",
|
|
7
7
|
"repository": {
|