insta 0.0.45 → 0.0.48
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 +11 -6
- package/dist/commands/auth.js +6 -2
- package/dist/commands/billing.js +5 -2
- package/dist/commands/db.js +96 -1
- package/dist/commands/manifest.js +25 -4
- package/dist/commands/services.js +8 -0
- package/dist/commands/setup.js +27 -1
- package/dist/commands/template.js +2 -1
- package/dist/index.js +8 -1
- package/dist/util.js +36 -3
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -65,13 +65,15 @@ insta login --oauth github
|
|
|
65
65
|
insta project create my-app
|
|
66
66
|
insta services add postgres db
|
|
67
67
|
insta services add compute api
|
|
68
|
+
insta secrets bind DATABASE_URL postgres/db --to compute/api
|
|
68
69
|
insta secrets
|
|
69
70
|
insta deploy .
|
|
70
71
|
```
|
|
71
72
|
|
|
72
73
|
`project create` makes an empty project and links the current directory. Services are
|
|
73
|
-
opt-in, so you add only what you need. `secrets` writes the current branch's
|
|
74
|
-
`./.env
|
|
74
|
+
opt-in, so you add only what you need. `secrets` writes the current branch's user-defined
|
|
75
|
+
secrets to `./.env` (the postgres connection string is read with `insta db url`). `deploy .`
|
|
76
|
+
builds the directory remotely and ships it to the branch's compute
|
|
75
77
|
service; it needs a `Dockerfile`, but no local Docker.
|
|
76
78
|
|
|
77
79
|
## Authentication
|
|
@@ -104,10 +106,12 @@ the two branches diverge independently. A project is capped at 10 branches.
|
|
|
104
106
|
|
|
105
107
|
### Credentials come from the secret seam, not a file you maintain
|
|
106
108
|
|
|
107
|
-
`insta secrets` fetches the current branch's
|
|
108
|
-
does the same without touching disk, injecting
|
|
109
|
-
|
|
110
|
-
|
|
109
|
+
`insta secrets` fetches the current branch's **user-defined** secrets and writes `./.env`.
|
|
110
|
+
`insta run <cmd>` does the same without touching disk, injecting them into the child process
|
|
111
|
+
only. Provider-minted service credentials (`DATABASE_URL`, `BUCKET_NAME`,
|
|
112
|
+
`AWS_ACCESS_KEY_ID`, …) are not in that bundle — they reach compute through explicit
|
|
113
|
+
`insta secrets bind` rules, and the postgres connection string is read directly with
|
|
114
|
+
`insta db url` (or `insta db connect` for a psql session).
|
|
111
115
|
|
|
112
116
|
### Destructive actions can require approval
|
|
113
117
|
|
|
@@ -197,6 +201,7 @@ build never reaches a production installer.
|
|
|
197
201
|
| `insta run <cmd>` | Run a command with the branch bundle injected, nothing written to disk |
|
|
198
202
|
| `insta deploy [dir]` | Deploy a source directory (built remotely) or `--image <url>` |
|
|
199
203
|
| `insta compute` | `start` · `stop` · `suspend` · `status` · `set-domain` · `check-domain` · `remove-domain` |
|
|
204
|
+
| `insta db` | `url` (print the postgres DSN) · `connect` (psql session) · `limits` · `stats` · `always-on` · `volume` |
|
|
200
205
|
| `insta regions` | Regions available for postgres and compute |
|
|
201
206
|
| `insta manifest` | Agent-legible view of every branch and its URLs |
|
|
202
207
|
| `insta metrics` · `logs` · `events` | Service metrics; runtime logs (`--deploy` for deploy events); audit timeline |
|
package/dist/commands/auth.js
CHANGED
|
@@ -194,8 +194,12 @@ function browserOauth(apiUrl, provider) {
|
|
|
194
194
|
const redirect = `http://127.0.0.1:${port}/callback`;
|
|
195
195
|
const authorizeUrl = `${apiUrl}/auth/cli/authorize?provider=${encodeURIComponent(provider)}&redirect=${encodeURIComponent(redirect)}&state=${state}`;
|
|
196
196
|
info(`opening browser to authorize with ${provider}…`);
|
|
197
|
-
|
|
198
|
-
|
|
197
|
+
// Always print the URL: a launcher that fails to start reports it on spawn's ASYNC error
|
|
198
|
+
// event, so openUrl's return value cannot see it (e.g. powershell.exe blocked by AppLocker
|
|
199
|
+
// on hardened fleets) — and the silent variant of that failure looks exactly like a hang.
|
|
200
|
+
info(`if nothing opens, use this URL:\n ${authorizeUrl}`);
|
|
201
|
+
openUrl(authorizeUrl);
|
|
202
|
+
info('waiting for you to finish in the browser… (times out in 2m; ctrl-c to abort)');
|
|
199
203
|
timer = setTimeout(() => { server.close(); reject(new Error('timed out waiting for browser login (2m)')); }, 120_000);
|
|
200
204
|
});
|
|
201
205
|
});
|
package/dist/commands/billing.js
CHANGED
|
@@ -67,11 +67,14 @@ export async function billingPortal(opts) {
|
|
|
67
67
|
return printJson({ url });
|
|
68
68
|
presentUrl(url, 'Manage billing in your browser:', opts.open);
|
|
69
69
|
}
|
|
70
|
-
// Print the URL and, unless --no-open, try to open it in the browser.
|
|
70
|
+
// Print the URL and, unless --no-open, try to open it in the browser. The message says
|
|
71
|
+
// "opening", not "opened": a launcher that starts and then fails reports it asynchronously,
|
|
72
|
+
// so openUrl's true return is an attempt, not a confirmation (see util.ts) — and the URL is
|
|
73
|
+
// already printed above for exactly that case.
|
|
71
74
|
function presentUrl(url, label, open) {
|
|
72
75
|
info(label);
|
|
73
76
|
info(` ${url}`);
|
|
74
77
|
if (open !== false && openUrl(url))
|
|
75
|
-
info('(
|
|
78
|
+
info('(opening in your default browser…)');
|
|
76
79
|
}
|
|
77
80
|
//# sourceMappingURL=billing.js.map
|
package/dist/commands/db.js
CHANGED
|
@@ -1,6 +1,8 @@
|
|
|
1
|
+
import { spawn } from 'node:child_process';
|
|
2
|
+
import { constants as osConstants } from 'node:os';
|
|
1
3
|
import { ApiClient, ApiError, requireProject } from '../api.js';
|
|
2
4
|
import { info, printJson, handleApproval } from '../util.js';
|
|
3
|
-
import { parseVolumeGib } from './services.js';
|
|
5
|
+
import { parseVolumeGib, q, resolveSoleService } from './services.js';
|
|
4
6
|
// Toggle a postgres service between scale-to-zero (the default: instance suspends when idle,
|
|
5
7
|
// cold-starts on the next connection) and always-on (instance stays warm; idle RAM bills at
|
|
6
8
|
// actual usage). Thin wrapper over PATCH /database/settings {scaleToZero} — insta-db-backed
|
|
@@ -237,4 +239,97 @@ export async function dbVolume(opts) {
|
|
|
237
239
|
const vg = res.body?.volumeGib;
|
|
238
240
|
info(`postgres ${opts.group ?? 'default'}: volume ${typeof vg === 'number' ? `grown to ${vg}Gi` : `set to ${sizeGib}Gi`}`);
|
|
239
241
|
}
|
|
242
|
+
// Resolve the postgres service (sole, or --group) and its connection string. Two reads: the
|
|
243
|
+
// branch's services list names the service; GET /services/:id/credentials (gated secrets.read)
|
|
244
|
+
// carries the value. Provider-minted credentials are canonical within their source service
|
|
245
|
+
// (DATABASE_URL) and deliberately absent from the general `insta secrets` bundle, so this is the
|
|
246
|
+
// read that yields the DSN. The credentials call carries no branch param — the service id is
|
|
247
|
+
// already branch-scoped by the list. Returns null when the read parked on an approval
|
|
248
|
+
// (handleApproval already spoke). Takes the client as an argument so tests drive it with a stub,
|
|
249
|
+
// per this repo's pure-seam convention.
|
|
250
|
+
export async function resolveDbUrl(api, projectId, branch, group, json) {
|
|
251
|
+
const { services } = await api.request('GET', `/projects/${projectId}/services${q(branch)}`);
|
|
252
|
+
const svc = resolveSoleService(services, 'postgres', group);
|
|
253
|
+
const res = await api.rawRequest('GET', `/projects/${projectId}/services/${svc.id}/credentials`);
|
|
254
|
+
if (handleApproval(res, json))
|
|
255
|
+
return null;
|
|
256
|
+
const url = res.body?.credentials?.DATABASE_URL;
|
|
257
|
+
if (typeof url !== 'string' || !url) {
|
|
258
|
+
throw new Error(`postgres ${svc.name} has no DATABASE_URL credential yet — still provisioning? (\`insta services list\` shows status)`);
|
|
259
|
+
}
|
|
260
|
+
return { serviceName: svc.name, url };
|
|
261
|
+
}
|
|
262
|
+
// Print the postgres connection string: the bare DSN on stdout, nothing else — pipe-friendly
|
|
263
|
+
// (`psql "$(insta db url)"`), like `storage get --json` keeps stdout parseable.
|
|
264
|
+
export async function dbUrl(opts) {
|
|
265
|
+
const api = await ApiClient.load();
|
|
266
|
+
const p = await requireProject();
|
|
267
|
+
const branch = opts.branch ?? p.branch;
|
|
268
|
+
const r = await resolveDbUrl(api, p.projectId, branch, opts.group, opts.json);
|
|
269
|
+
if (!r)
|
|
270
|
+
return;
|
|
271
|
+
if (opts.json)
|
|
272
|
+
return printJson({ service: r.serviceName, branch: branch ?? null, url: r.url });
|
|
273
|
+
process.stdout.write(r.url + '\n');
|
|
274
|
+
}
|
|
275
|
+
// Decompose a postgres DSN into libpq PG* environment variables. Pure, exported for tests.
|
|
276
|
+
// The credential must NOT ride in psql's argv — process arguments are visible to every local
|
|
277
|
+
// user via `ps`, so a secrets.read-gated value would leak the moment the session starts. Child
|
|
278
|
+
// environment is not (the `insta run` model), and PG* env is a libpq-supported mechanism, so
|
|
279
|
+
// psql runs with an empty argv. `sslmode` is the only query param the platform's DSNs carry;
|
|
280
|
+
// anything else would be a platform-side change this mapping should then learn about.
|
|
281
|
+
export function psqlEnvFromUrl(url) {
|
|
282
|
+
const u = new URL(url);
|
|
283
|
+
const env = {};
|
|
284
|
+
if (u.hostname)
|
|
285
|
+
env.PGHOST = decodeURIComponent(u.hostname);
|
|
286
|
+
if (u.port)
|
|
287
|
+
env.PGPORT = u.port;
|
|
288
|
+
if (u.username)
|
|
289
|
+
env.PGUSER = decodeURIComponent(u.username);
|
|
290
|
+
if (u.password)
|
|
291
|
+
env.PGPASSWORD = decodeURIComponent(u.password);
|
|
292
|
+
const db = u.pathname.replace(/^\//, '');
|
|
293
|
+
if (db)
|
|
294
|
+
env.PGDATABASE = decodeURIComponent(db);
|
|
295
|
+
const sslmode = u.searchParams.get('sslmode');
|
|
296
|
+
if (sslmode)
|
|
297
|
+
env.PGSSLMODE = sslmode;
|
|
298
|
+
return env;
|
|
299
|
+
}
|
|
300
|
+
/** Core, dependency-injected for tests: spawn psql against the DSN (via PG* env, never argv), return its exit code. */
|
|
301
|
+
export async function connectWithPsql(url, spawnImpl = spawn) {
|
|
302
|
+
// Strip ambient PG* first: parent-env PGHOSTADDR/PGSERVICE/PGOPTIONS/PGSSL* would silently
|
|
303
|
+
// redirect or reshape the connection away from the service this command just resolved.
|
|
304
|
+
const env = { ...process.env };
|
|
305
|
+
// Case-insensitive: Windows env names are case-insensitive, so ambient `pgservice` redirects
|
|
306
|
+
// psql just as PGSERVICE does.
|
|
307
|
+
for (const k of Object.keys(env))
|
|
308
|
+
if (k.slice(0, 2).toUpperCase() === 'PG')
|
|
309
|
+
delete env[k];
|
|
310
|
+
Object.assign(env, psqlEnvFromUrl(url));
|
|
311
|
+
return await new Promise((resolve, reject) => {
|
|
312
|
+
const child = spawnImpl('psql', [], { stdio: 'inherit', env });
|
|
313
|
+
child.on('error', (e) => reject(e.code === 'ENOENT'
|
|
314
|
+
? new Error('psql not found on PATH — install the postgres client, or print the DSN with `insta db url`')
|
|
315
|
+
: e));
|
|
316
|
+
// Signal death reports code null — map to the conventional 128+signo (full table from
|
|
317
|
+
// os.constants) so the advertised exit-status passthrough holds for Ctrl-C'd/killed sessions.
|
|
318
|
+
child.on('close', (code, signal) => resolve(code ?? (signal ? 128 + (osConstants.signals[signal] ?? 0) : 1)));
|
|
319
|
+
});
|
|
320
|
+
}
|
|
321
|
+
// Open an interactive psql session on the postgres service. The DSN never touches disk or argv
|
|
322
|
+
// history beyond the child process. Exits with psql's own exit code (agents rely on this, as
|
|
323
|
+
// with `compute exec`).
|
|
324
|
+
export async function dbConnect(opts) {
|
|
325
|
+
const api = await ApiClient.load();
|
|
326
|
+
const p = await requireProject();
|
|
327
|
+
const branch = opts.branch ?? p.branch;
|
|
328
|
+
const r = await resolveDbUrl(api, p.projectId, branch, opts.group, opts.json);
|
|
329
|
+
if (!r)
|
|
330
|
+
return;
|
|
331
|
+
// stderr: stdout belongs to psql (the `insta run` rule).
|
|
332
|
+
process.stderr.write(`psql → postgres/${r.serviceName}${branch ? ` (branch ${branch})` : ''} — a suspended instance wakes on connect, so the first prompt can take a few seconds\n`);
|
|
333
|
+
process.exit(await connectWithPsql(r.url));
|
|
334
|
+
}
|
|
240
335
|
//# sourceMappingURL=db.js.map
|
|
@@ -1,5 +1,28 @@
|
|
|
1
1
|
import { ApiClient, requireProject } from '../api.js';
|
|
2
2
|
import { info, printJson } from '../util.js';
|
|
3
|
+
/**
|
|
4
|
+
* The label that names WHERE a resource runs.
|
|
5
|
+
*
|
|
6
|
+
* `kind` is the platform's internal resource kind, and for compute it is always 'fly' — 'fly' is
|
|
7
|
+
* the compute SEAT, occupied by the microvm plane on any environment that has cut over. Printing
|
|
8
|
+
* it is how `insta manifest` came to tell users and agents that a microvm-backed service ran on
|
|
9
|
+
* Fly (staging, 2026-08-25: `fly(api)` for a row serving from warm pod insta-warm-00178a-16).
|
|
10
|
+
*
|
|
11
|
+
* So for compute rows the label is the platform's explicit `provider`, and when that is absent --
|
|
12
|
+
* an older platform, or a row whose provider the platform itself could not determine -- we fall
|
|
13
|
+
* back to the neutral 'compute'. That names the resource without asserting a plane: no provider
|
|
14
|
+
* beats a wrong one. Non-compute kinds keep printing their kind.
|
|
15
|
+
*/
|
|
16
|
+
export function resourceLabel(r) {
|
|
17
|
+
const kind = r.kind ?? 'resource';
|
|
18
|
+
const label = kind === 'fly' ? (r.provider === 'fly' || r.provider === 'microvm' ? r.provider : 'compute') : kind;
|
|
19
|
+
return `${label}${r.name ? `(${r.name})` : ''}`;
|
|
20
|
+
}
|
|
21
|
+
// One `manifest` resource line. Pure, so the label is unit-tested without a network mock.
|
|
22
|
+
export function resourceLine(r) {
|
|
23
|
+
const where = r.ref?.url ?? r.ref?.bucket ?? r.ref?.neonProjectId ?? '';
|
|
24
|
+
return ` - ${resourceLabel(r)} ${where} [${r.status}]`;
|
|
25
|
+
}
|
|
3
26
|
// Agent-legible view of each environment's databases / storage / compute.
|
|
4
27
|
export async function manifest(opts) {
|
|
5
28
|
const api = await ApiClient.load();
|
|
@@ -11,10 +34,8 @@ export async function manifest(opts) {
|
|
|
11
34
|
for (const b of detail.branches) {
|
|
12
35
|
info(` branch ${b.name}${b.is_default ? ' *' : ''} [${b.status}]`);
|
|
13
36
|
const rs = detail.resources.filter((r) => r.branchId === b.id || (b.is_default && r.branchId === null));
|
|
14
|
-
for (const r of rs)
|
|
15
|
-
|
|
16
|
-
info(` - ${r.kind}${r.name ? `(${r.name})` : ''} ${where} [${r.status}]`);
|
|
17
|
-
}
|
|
37
|
+
for (const r of rs)
|
|
38
|
+
info(resourceLine(r));
|
|
18
39
|
}
|
|
19
40
|
}
|
|
20
41
|
//# sourceMappingURL=manifest.js.map
|
|
@@ -120,6 +120,14 @@ export async function servicesAdd(type, name, opts = {}) {
|
|
|
120
120
|
const img = svc.image ? ` running ${svc.image}${svc.port ? `:${svc.port}` : ''}` : '';
|
|
121
121
|
const vol = svc.volume_gib ? ` vol ${svc.volume_gib}Gi at /data` : '';
|
|
122
122
|
info(`added ${type} service ${name} on ${branch ?? 'default'} (${svc.id})${access}${svc.region ? ` ${svc.region}` : ''}${img}${vol}${svc.domain ? ` — ${svc.domain}` : ''}`);
|
|
123
|
+
// Discoverability: the DB is directly dialable, but its DSN is deliberately absent from the
|
|
124
|
+
// general `insta secrets` bundle — without this line nothing in the product says how to reach it.
|
|
125
|
+
if (type === 'postgres') {
|
|
126
|
+
// The hint must be runnable as printed: carry --branch when the service was created on a
|
|
127
|
+
// branch other than the linked one, and --group so it survives multiple postgres services.
|
|
128
|
+
const flags = `${opts.branch ? ` --branch ${opts.branch}` : ''} --group ${name}`;
|
|
129
|
+
info(` connect: \`insta db url${flags}\` prints the connection string, \`insta db connect${flags}\` opens psql (--group optional with a single postgres service)`);
|
|
130
|
+
}
|
|
123
131
|
renderNextActions(res.body.nextActions);
|
|
124
132
|
}
|
|
125
133
|
// Render one `services list` row. Pure, so it's unit-tested without a network mock (mirrors
|
package/dist/commands/setup.js
CHANGED
|
@@ -15,6 +15,7 @@ import { readPersistedGlobal, resolveEnv } from '../config.js';
|
|
|
15
15
|
import { DEFAULT_ENV, ENVS, ENV_NAMES, envForApiUrl, envFromEnvVar, isEnvName, mcpServerName } from '../env.js';
|
|
16
16
|
import { info } from '../util.js';
|
|
17
17
|
import { loginOauth } from './auth.js';
|
|
18
|
+
import { projectLink } from './project.js';
|
|
18
19
|
import { envUse } from './env.js';
|
|
19
20
|
import { installAgentConfigs } from './mcp.js';
|
|
20
21
|
import { detectChannel } from './upgrade.js';
|
|
@@ -424,7 +425,7 @@ export async function setupAgent(opts, run = defaultRunner, mint, installConfigs
|
|
|
424
425
|
login: () => loginOauth('github', {}),
|
|
425
426
|
stdinTty: canPromptViaTty(),
|
|
426
427
|
stdoutTty: !!process.stdout.isTTY,
|
|
427
|
-
}) {
|
|
428
|
+
}, link = projectLink) {
|
|
428
429
|
if (!opts.yes && !process.stdout.isTTY) {
|
|
429
430
|
info('non-interactive shell — assuming -y');
|
|
430
431
|
}
|
|
@@ -485,6 +486,31 @@ export async function setupAgent(opts, run = defaultRunner, mint, installConfigs
|
|
|
485
486
|
}
|
|
486
487
|
}
|
|
487
488
|
}
|
|
489
|
+
// --project: link this directory inside the SAME process. The console's connect panel used to
|
|
490
|
+
// print `setup agent && insta project link <id>` as one paste — but no shell joiner survives
|
|
491
|
+
// every Windows shell, and in shells without bracketed paste the queued link line is eaten as
|
|
492
|
+
// the answer to the login prompt above (console PR #290). Carrying the id as a flag is the one
|
|
493
|
+
// form where "one line" is safe. Linking needs the session: without one the manual command is
|
|
494
|
+
// the hint, never a hang; a failed link (bad id, no access) is a REAL error — the link is the
|
|
495
|
+
// entire point of the flag — so it sets the exit code instead of pretending setup succeeded.
|
|
496
|
+
if (opts.project) {
|
|
497
|
+
if (!loggedIn) {
|
|
498
|
+
info(` not logged in — project not linked; run \`insta login\`, then \`insta project link ${opts.project}\``);
|
|
499
|
+
}
|
|
500
|
+
else {
|
|
501
|
+
try {
|
|
502
|
+
await link(opts.project);
|
|
503
|
+
}
|
|
504
|
+
catch (e) {
|
|
505
|
+
// Stop here — like the skill-install failure above, finishing with the success summary
|
|
506
|
+
// and a cheerful `next:` after an error is mixed messaging. Setup itself did succeed,
|
|
507
|
+
// so say exactly that alongside the retry command.
|
|
508
|
+
info(` project link failed (${e instanceof Error ? e.message : String(e)}) — agent setup itself is done; run \`insta project link ${opts.project}\` to retry the link`);
|
|
509
|
+
process.exitCode = 1;
|
|
510
|
+
return;
|
|
511
|
+
}
|
|
512
|
+
}
|
|
513
|
+
}
|
|
488
514
|
// THE summary line. The restart note exists because config-file agents only read their MCP
|
|
489
515
|
// config at startup; the skill files need no restart.
|
|
490
516
|
const mcpOk = claude === 'new' || claude === 'existing' || others.length > 0;
|
|
@@ -351,7 +351,8 @@ export async function templateDeploy(target, opts = {}, deps = {}) {
|
|
|
351
351
|
info(`template ${codeLabel} deployed to branch ${branchName}`);
|
|
352
352
|
for (const u of deploymentUrls(dep))
|
|
353
353
|
info(` ${u}`);
|
|
354
|
-
|
|
354
|
+
// Provider credentials are not in the `insta secrets` bundle — point at the paths that exist.
|
|
355
|
+
info('next: `insta db url` prints the postgres DSN; bind service credentials into compute with `insta secrets bind`; `insta secrets` refreshes user-defined secrets in .env');
|
|
355
356
|
renderNextActions(dep.nextActions);
|
|
356
357
|
}
|
|
357
358
|
//# sourceMappingURL=template.js.map
|
package/dist/index.js
CHANGED
|
@@ -87,6 +87,7 @@ setupCmd.command('agent').description('Install the insta CLI (if missing), the i
|
|
|
87
87
|
.option('-y, --yes', 'non-interactive')
|
|
88
88
|
.option('--env <prod|staging>', 'deployment to set this machine up for (default: prod — switches and persists, like `insta env use`)')
|
|
89
89
|
.option('--mcp-token', 'register the MCP server with a minted insta_ API token instead of OAuth (headless machines / CI)')
|
|
90
|
+
.option('--project <id>', 'also link this directory to an existing project after setup (flows through login first if needed)')
|
|
90
91
|
.action(guard((o) => setup.setupAgent(o)));
|
|
91
92
|
// ---- MCP server integration ----
|
|
92
93
|
const mcpCmd = program.command('mcp').description('insta-cloud remote MCP server integration');
|
|
@@ -223,7 +224,13 @@ compute.command('volume [service]').description("Show, attach, grow, or delete a
|
|
|
223
224
|
.option('--delete', 'destroy the volume and ALL its data (irreversible; download anything you need first)')
|
|
224
225
|
.option('--json').option('--branch <branch>', 'branch (default: current)').action(guard((service, o) => computeCmd.computeVolume(service, o)));
|
|
225
226
|
// ---- db (postgres service controls) ----
|
|
226
|
-
const db = program.command('db').description('Postgres service controls (limits / volume / always-on / scale-to-zero)');
|
|
227
|
+
const db = program.command('db').description('Postgres service controls (url / connect / limits / volume / always-on / scale-to-zero)');
|
|
228
|
+
db.command('url').description('Print the postgres connection string (DSN) — bare on stdout for piping, e.g. `psql "$(insta db url)"` (gated: secrets.read). Provider credentials are not in `insta secrets` — this is the command that yields the DSN')
|
|
229
|
+
.option('--json').option('--branch <branch>', 'branch (default: current)').option('--group <g>', 'postgres service name (default: the sole/default one)')
|
|
230
|
+
.action(guard((o) => dbCmd.dbUrl(o)));
|
|
231
|
+
db.command('connect').description("Open an interactive psql session on the postgres service (needs psql on PATH; gated: secrets.read). A suspended instance wakes on connect — the first prompt can take a few seconds. Exits with psql's own exit code")
|
|
232
|
+
.option('--branch <branch>', 'branch (default: current)').option('--group <g>', 'postgres service name (default: the sole/default one)')
|
|
233
|
+
.action(guard((o) => dbCmd.dbConnect(o)));
|
|
227
234
|
db.command('limits').description("Show or set a postgres service's resource ceiling (paid plans; insta-db-backed only). Moves both directions")
|
|
228
235
|
.option('--cpu <n>', "vCPU ceiling, e.g. 2 or 2500m").option('--memory <size>', "memory ceiling, e.g. 4Gi")
|
|
229
236
|
.option('--json').option('--branch <branch>', 'branch (default: current)').option('--group <g>', 'postgres service name (default: the sole/default one)')
|
package/dist/util.js
CHANGED
|
@@ -1,10 +1,43 @@
|
|
|
1
1
|
// Output + small pure helpers (env serialization is unit-tested).
|
|
2
2
|
import { createInterface } from 'node:readline';
|
|
3
3
|
import { spawn } from 'node:child_process';
|
|
4
|
-
|
|
4
|
+
/** How to launch the default browser for `url` on `platform`. Pure so the Windows encoding is
|
|
5
|
+
* testable. On Windows NO shell may ever parse the URL: cmd.exe splits at bare `&` (which #138
|
|
6
|
+
* fixed by quoting) but ALSO expands `%…%` sequences even inside quotes, and a percent-encoded
|
|
7
|
+
* OAuth redirect (`http%3A%2F%2F127.0.0.1…`) is nothing but such sequences. So the launch goes
|
|
8
|
+
* through PowerShell's -EncodedCommand: a pure-ASCII script travels as base64(UTF-16LE) — no
|
|
9
|
+
* argument parsing anywhere — and the URL itself rides as a second base64 payload INSIDE that
|
|
10
|
+
* script, decoded by .NET at runtime, so no URL byte ever appears in PowerShell source (see the
|
|
11
|
+
* win32 branch). Start-Process on a URL is ShellExecute, i.e. the default browser. */
|
|
12
|
+
export function openUrlSpawn(url, platform = process.platform,
|
|
13
|
+
// Absolute path, not bare `powershell`: CreateProcess-style lookup searches the current
|
|
14
|
+
// directory before PATH, so a planted powershell.exe beside the user's shell would win.
|
|
15
|
+
systemRoot = process.env.SYSTEMROOT ?? process.env.windir ?? 'C:\\Windows') {
|
|
16
|
+
if (platform === 'win32') {
|
|
17
|
+
// The URL never appears in PowerShell SOURCE at all: it travels as base64 inside the script
|
|
18
|
+
// and is decoded by .NET at runtime. Interpolating it into a quoted literal is not enough —
|
|
19
|
+
// PowerShell honors smart quotes (U+2018–U+201B) as string delimiters too, so ASCII-only
|
|
20
|
+
// escaping still leaves a breakout. The script below is pure ASCII by construction (the
|
|
21
|
+
// base64 alphabet), so no byte of any URL can terminate anything.
|
|
22
|
+
const urlB64 = Buffer.from(url, 'utf8').toString('base64');
|
|
23
|
+
const script = `Start-Process ([Text.Encoding]::UTF8.GetString([Convert]::FromBase64String('${urlB64}')))`;
|
|
24
|
+
return {
|
|
25
|
+
cmd: `${systemRoot}\\System32\\WindowsPowerShell\\v1.0\\powershell.exe`,
|
|
26
|
+
args: ['-NoProfile', '-NonInteractive', '-EncodedCommand', Buffer.from(script, 'utf16le').toString('base64')],
|
|
27
|
+
};
|
|
28
|
+
}
|
|
29
|
+
return { cmd: platform === 'darwin' ? 'open' : 'xdg-open', args: [url] };
|
|
30
|
+
}
|
|
31
|
+
// ShellExecute-family launchers (Start-Process/open/xdg-open) run ANY target they're handed —
|
|
32
|
+
// a UNC path is an execution, not a navigation — so only web URLs may reach them.
|
|
33
|
+
export const isWebUrl = (url) => /^https?:\/\//i.test(url);
|
|
34
|
+
// Best-effort: open a URL in the user's default browser. Returns false if we couldn't launch —
|
|
35
|
+
// but a launcher that starts and THEN fails (ENOENT arrives on the async 'error' event) still
|
|
36
|
+
// reads as true, so callers must not treat true as proof the browser opened.
|
|
5
37
|
export function openUrl(url) {
|
|
6
|
-
|
|
7
|
-
|
|
38
|
+
if (!isWebUrl(url))
|
|
39
|
+
return false;
|
|
40
|
+
const { cmd, args } = openUrlSpawn(url);
|
|
8
41
|
try {
|
|
9
42
|
const child = spawn(cmd, args, { stdio: 'ignore', detached: true });
|
|
10
43
|
child.on('error', () => { });
|