insta 0.0.35 → 0.0.37
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 +18 -6
- package/dist/api.js +4 -2
- package/dist/commands/branch.js +12 -4
- package/dist/commands/build.js +256 -0
- package/dist/commands/compute.js +99 -8
- package/dist/commands/db.js +3 -3
- package/dist/commands/deploy.js +53 -15
- package/dist/commands/env.js +18 -1
- package/dist/commands/govern.js +9 -3
- package/dist/commands/metrics.js +45 -8
- package/dist/commands/org.js +3 -1
- package/dist/commands/project.js +33 -15
- package/dist/commands/run.js +7 -5
- package/dist/commands/secrets.js +72 -5
- package/dist/commands/services.js +16 -11
- package/dist/commands/setup.js +164 -3
- package/dist/commands/storage.js +3 -3
- package/dist/ensure-skills.js +16 -4
- package/dist/flyctl-build.js +25 -18
- package/dist/index.js +62 -24
- package/dist/nixpacks.js +69 -0
- package/dist/resolve-service.js +3 -1
- package/dist/util.js +10 -3
- package/package.json +1 -1
package/dist/commands/deploy.js
CHANGED
|
@@ -1,8 +1,11 @@
|
|
|
1
1
|
import { resolve, join } from 'node:path';
|
|
2
2
|
import { existsSync, readFileSync } from 'node:fs';
|
|
3
|
-
import { ApiClient, requireProject } from '../api.js';
|
|
4
|
-
import { info, die, handleApproval, renderNextActions } from '../util.js';
|
|
5
|
-
import { flyctlBuildAndPush, ensureFlyctl } from '../flyctl-build.js';
|
|
3
|
+
import { ApiClient, ApiError, requireProject } from '../api.js';
|
|
4
|
+
import { info, die, printJson, handleApproval, renderNextActions } from '../util.js';
|
|
5
|
+
import { flyctlBuildAndPush, ensureFlyctl, defaultBuildRunner, stderrBuildRunner } from '../flyctl-build.js';
|
|
6
|
+
// With --json, stdout must carry exactly one JSON document (the deploy result), so every progress
|
|
7
|
+
// line moves to stderr.
|
|
8
|
+
const note = (opts) => (opts.json ? (m) => void process.stderr.write(m + '\n') : info);
|
|
6
9
|
// Map CLI options to the platform deploy request body. Pure, so it's unit-tested. --websocket is only
|
|
7
10
|
// sent when set (plain deploys unchanged).
|
|
8
11
|
export function deployRequestBody(image, branch, opts) {
|
|
@@ -36,38 +39,73 @@ export async function deploy(dir, opts) {
|
|
|
36
39
|
const api = await ApiClient.load();
|
|
37
40
|
const p = await requireProject();
|
|
38
41
|
const branch = opts.branch ?? p.branch;
|
|
42
|
+
const log = note(opts);
|
|
39
43
|
let port = opts.port ? Number(opts.port) : undefined;
|
|
40
44
|
if (dir && port === undefined) {
|
|
41
45
|
const dockerfile = join(resolve(process.cwd(), dir), 'Dockerfile');
|
|
42
46
|
const exposed = existsSync(dockerfile) ? dockerfileExposedPort(readFileSync(dockerfile, 'utf8')) : undefined;
|
|
43
47
|
if (exposed) {
|
|
44
48
|
port = exposed;
|
|
45
|
-
|
|
49
|
+
log(`using port ${exposed} (Dockerfile EXPOSE) — override with --port`);
|
|
46
50
|
}
|
|
47
51
|
}
|
|
48
52
|
const effOpts = { ...opts, port: port?.toString() };
|
|
49
53
|
const image = dir ? await buildFromSource(api, p.projectId, dir, branch, effOpts) : opts.image;
|
|
50
54
|
const res = await api.rawRequest('POST', `/projects/${p.projectId}/deploy`, deployRequestBody(image, branch, effOpts));
|
|
51
|
-
if (handleApproval(res))
|
|
55
|
+
if (handleApproval(res, opts.json))
|
|
52
56
|
return;
|
|
57
|
+
if (opts.json)
|
|
58
|
+
return printJson({ image, ...res.body });
|
|
53
59
|
info(`deployed ${image} -> ${res.body.url} (branch ${res.body.branch}, group ${res.body.group})`);
|
|
54
60
|
renderNextActions(res.body.nextActions);
|
|
55
61
|
}
|
|
62
|
+
// The local image tag a daemon-side deploy runs: unique per build so a redeploy replaces, and
|
|
63
|
+
// legible in `docker images`. Pure, so it's unit-tested.
|
|
64
|
+
export function localImageTag(projectId, group, now = Date.now()) {
|
|
65
|
+
return `insta-src-${projectId.slice(0, 8)}-${group ?? 'default'}:${now}`;
|
|
66
|
+
}
|
|
67
|
+
// Local build for a local daemon (insta-oss): the CLI and the daemon share ONE docker, so a
|
|
68
|
+
// locally-built tag is directly runnable — no registry, no push. Same injectable-runner pattern
|
|
69
|
+
// as flyctl-build.ts.
|
|
70
|
+
export async function dockerBuildLocal(absDir, tag, run = defaultBuildRunner) {
|
|
71
|
+
const { code } = await run('docker', ['build', '-t', tag, '.'], { cwd: absDir, env: process.env });
|
|
72
|
+
if (code !== 0)
|
|
73
|
+
throw new Error(`docker build failed (exit ${code}). See output above.`);
|
|
74
|
+
return tag;
|
|
75
|
+
}
|
|
56
76
|
// Source mode: mint a scoped Fly deploy token from the platform, then build+push <dir> (needs a
|
|
57
|
-
// Dockerfile) with flyctl's remote builder, returning the pushed image ref to deploy.
|
|
58
|
-
|
|
77
|
+
// Dockerfile) with flyctl's remote builder, returning the pushed image ref to deploy. Against a
|
|
78
|
+
// local daemon (insta-oss) the token mint answers 501 — build with docker instead, same contract.
|
|
79
|
+
// Exported with injectable pieces for tests (the repo's DI pattern; no global mocks).
|
|
80
|
+
export async function buildFromSource(api, projectId, dir, branch, opts, run = opts.json ? stderrBuildRunner : defaultBuildRunner) {
|
|
59
81
|
const absDir = resolve(process.cwd(), dir);
|
|
60
82
|
if (!existsSync(join(absDir, 'Dockerfile')))
|
|
61
83
|
die(`no Dockerfile at ${join(absDir, 'Dockerfile')} — add one, or use --image <url>`);
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
84
|
+
const log = note(opts);
|
|
85
|
+
let tok;
|
|
86
|
+
try {
|
|
87
|
+
tok = await api.rawRequest('POST', `/projects/${projectId}/deploy-token`, { branch, group: opts.group });
|
|
88
|
+
}
|
|
89
|
+
catch (e) {
|
|
90
|
+
// 501 = no remote builder here (insta-oss is the only deployment that answers it) — the
|
|
91
|
+
// daemon deploys from the SAME docker this shell uses, so build locally and hand it the tag.
|
|
92
|
+
if (!(e instanceof ApiError) || e.status !== 501)
|
|
93
|
+
throw e;
|
|
94
|
+
const tag = localImageTag(projectId, opts.group);
|
|
95
|
+
log(`no remote builder on this daemon — building ${dir} locally with docker…`);
|
|
96
|
+
const built = await dockerBuildLocal(absDir, tag, run);
|
|
97
|
+
log(` built ${built}`);
|
|
98
|
+
return built;
|
|
99
|
+
}
|
|
100
|
+
// exit() with no argument honors the exit code handleApproval just set (2).
|
|
101
|
+
if (handleApproval(tok, opts.json))
|
|
102
|
+
process.exit();
|
|
67
103
|
const { token, flyApp } = tok.body;
|
|
68
|
-
|
|
69
|
-
const
|
|
70
|
-
|
|
104
|
+
await ensureFlyctl(); // cloud path only — the local path needs docker, which the daemon requires anyway
|
|
105
|
+
const port = opts.port ? Number(opts.port) : 8080;
|
|
106
|
+
log(`building ${dir} for ${flyApp} (remote builder)…`);
|
|
107
|
+
const { imageRef } = await flyctlBuildAndPush({ dir: absDir, flyApp, imageLabel: `insta-${Date.now()}`, token, port }, run);
|
|
108
|
+
log(` pushed ${imageRef}`);
|
|
71
109
|
return imageRef;
|
|
72
110
|
}
|
|
73
111
|
//# sourceMappingURL=deploy.js.map
|
package/dist/commands/env.js
CHANGED
|
@@ -20,7 +20,20 @@ export async function envShow(opts) {
|
|
|
20
20
|
if (!env)
|
|
21
21
|
info(' (custom apiUrl — `insta env use <name>` to switch to a named environment)');
|
|
22
22
|
}
|
|
23
|
-
|
|
23
|
+
// One stable schema for BOTH envUse outcomes (no-op and real switch), so a scripted caller can key
|
|
24
|
+
// on any field — mcpServer, previous — without probing which branch ran. Pure, unit-tested.
|
|
25
|
+
export function envUseResult(target, previous, changed, sessionDropped) {
|
|
26
|
+
return {
|
|
27
|
+
env: target,
|
|
28
|
+
previous,
|
|
29
|
+
apiUrl: ENVS[target].api,
|
|
30
|
+
mcpUrl: ENVS[target].mcp,
|
|
31
|
+
mcpServer: mcpServerName(target),
|
|
32
|
+
changed,
|
|
33
|
+
sessionDropped,
|
|
34
|
+
};
|
|
35
|
+
}
|
|
36
|
+
export async function envUse(name, opts = {}) {
|
|
24
37
|
const want = name.trim().toLowerCase();
|
|
25
38
|
if (!isEnvName(want))
|
|
26
39
|
die(`unknown environment "${name}" — expected one of: ${ENV_NAMES.join(', ')}`);
|
|
@@ -33,6 +46,8 @@ export async function envUse(name) {
|
|
|
33
46
|
// how envForApiUrl already treats it) instead of being rewritten as a "switch" that needlessly
|
|
34
47
|
// drops a perfectly good session.
|
|
35
48
|
if (normalizeUrl(stored.apiUrl) === normalizeUrl(nextApi)) {
|
|
49
|
+
if (opts.json)
|
|
50
|
+
return printJson(envUseResult(target, from ?? target, false, false));
|
|
36
51
|
info(`already on ${target} (${nextApi})`);
|
|
37
52
|
return;
|
|
38
53
|
}
|
|
@@ -49,6 +64,8 @@ export async function envUse(name) {
|
|
|
49
64
|
delete next.refreshToken;
|
|
50
65
|
delete next.user;
|
|
51
66
|
await writeGlobal(next);
|
|
67
|
+
if (opts.json)
|
|
68
|
+
return printJson(envUseResult(target, from ?? null, true, hadSession));
|
|
52
69
|
info(`switched ${from ?? '(custom)'} → ${target}`);
|
|
53
70
|
info(` api: ${nextApi}`);
|
|
54
71
|
info(` mcp: ${ENVS[target].mcp} (registers as \`${mcpServerName(target)}\`)`);
|
package/dist/commands/govern.js
CHANGED
|
@@ -29,12 +29,16 @@ export async function approvalsApprove(id, opts) {
|
|
|
29
29
|
const api = await ApiClient.load();
|
|
30
30
|
const p = await requireProject();
|
|
31
31
|
const out = await api.request('POST', `/projects/${p.projectId}/approvals/${id}/approve`, { always: !!opts.always });
|
|
32
|
+
if (opts.json)
|
|
33
|
+
return printJson(out);
|
|
32
34
|
info(`approved ${out.approval.action} (${id})${opts.always ? ' — policy set to allow' : ''}`);
|
|
33
35
|
}
|
|
34
|
-
export async function approvalsDeny(id) {
|
|
36
|
+
export async function approvalsDeny(id, opts = {}) {
|
|
35
37
|
const api = await ApiClient.load();
|
|
36
38
|
const p = await requireProject();
|
|
37
39
|
const out = await api.request('POST', `/projects/${p.projectId}/approvals/${id}/deny`);
|
|
40
|
+
if (opts.json)
|
|
41
|
+
return printJson(out);
|
|
38
42
|
info(`denied ${out.approval.action} (${id})`);
|
|
39
43
|
}
|
|
40
44
|
export async function policyGet(opts) {
|
|
@@ -46,10 +50,12 @@ export async function policyGet(opts) {
|
|
|
46
50
|
for (const [action, decision] of Object.entries(policy))
|
|
47
51
|
info(`${action}: ${decision}`);
|
|
48
52
|
}
|
|
49
|
-
export async function policySet(action, decision) {
|
|
53
|
+
export async function policySet(action, decision, opts = {}) {
|
|
50
54
|
const api = await ApiClient.load();
|
|
51
55
|
const p = await requireProject();
|
|
52
|
-
await api.request('PUT', `/projects/${p.projectId}/policy/${action}`, { decision });
|
|
56
|
+
const out = await api.request('PUT', `/projects/${p.projectId}/policy/${action}`, { decision });
|
|
57
|
+
if (opts.json)
|
|
58
|
+
return printJson({ action, decision, ...(out ?? {}) });
|
|
53
59
|
info(`policy ${action} = ${decision}`);
|
|
54
60
|
}
|
|
55
61
|
//# sourceMappingURL=govern.js.map
|
package/dist/commands/metrics.js
CHANGED
|
@@ -60,7 +60,7 @@ function humanBytes(v, base) {
|
|
|
60
60
|
const shown = i === 0 && Number.isInteger(value) ? String(value) : value.toFixed(1);
|
|
61
61
|
return `${shown} ${units[i]}`;
|
|
62
62
|
}
|
|
63
|
-
// insta metrics <db|compute> [group]
|
|
63
|
+
// insta metrics <db|compute|redis|mysql|mongodb> [group]
|
|
64
64
|
export async function metrics(component, group, opts) {
|
|
65
65
|
const api = await ApiClient.load();
|
|
66
66
|
const p = await requireProject();
|
|
@@ -129,23 +129,60 @@ export async function usage(opts) {
|
|
|
129
129
|
info(` ${pr.name}: $${Number(pr.totalCostUsd ?? 0).toFixed(4)}`);
|
|
130
130
|
}
|
|
131
131
|
}
|
|
132
|
-
// pure: platform path for a
|
|
132
|
+
// pure: platform path for a deploy-events request (used by `insta logs --deploy`). Any Fly-backed
|
|
133
|
+
// component (compute or a managed database) has machine lifecycle events; omitted → the platform
|
|
134
|
+
// defaults to compute.
|
|
133
135
|
export function deployEventsPath(projectId, opts) {
|
|
134
|
-
return `/projects/${projectId}/deploy-events${qs({ group: opts.group, branch: opts.branch, limit: opts.limit, instance: opts.instance })}`;
|
|
136
|
+
return `/projects/${projectId}/deploy-events${qs({ component: opts.component, group: opts.group, branch: opts.branch, limit: opts.limit, instance: opts.instance })}`;
|
|
135
137
|
}
|
|
136
138
|
// pure: render one deploy event as a log-style line.
|
|
137
139
|
export function deployEventLine(ev) {
|
|
138
140
|
const inst = ev.instance ? ` (${ev.instance})` : '';
|
|
139
141
|
return `${ev.ts ?? ''} [${ev.origin ?? ''}] ${ev.type ?? ''}: ${ev.status ?? ''}${inst}`;
|
|
140
142
|
}
|
|
141
|
-
//
|
|
143
|
+
// A log-window instant from the CLI: unix seconds (all digits) or anything Date.parse reads
|
|
144
|
+
// (ISO-8601 etc.). Throws on junk — a mistyped instant must fail here, not become NaN on the wire.
|
|
145
|
+
export function parseLogInstant(raw, flag) {
|
|
146
|
+
if (/^\d+$/.test(raw))
|
|
147
|
+
return Number(raw);
|
|
148
|
+
const ms = Date.parse(raw);
|
|
149
|
+
if (Number.isNaN(ms))
|
|
150
|
+
throw new Error(`invalid ${flag}: ${raw} (unix seconds or an ISO-8601 date)`);
|
|
151
|
+
return Math.floor(ms / 1000);
|
|
152
|
+
}
|
|
153
|
+
// '90s' | '30m' | '2h' | '1d' → seconds. Throws on junk, zero, and unknown units.
|
|
154
|
+
export function parseSinceSeconds(raw) {
|
|
155
|
+
const m = /^(\d+)([smhd])$/.exec(raw.trim());
|
|
156
|
+
if (!m)
|
|
157
|
+
throw new Error(`invalid --since: ${raw} (e.g. 90s, 30m, 2h, 1d)`);
|
|
158
|
+
const n = Number(m[1]);
|
|
159
|
+
if (n === 0)
|
|
160
|
+
throw new Error(`invalid --since: ${raw} (must be > 0)`);
|
|
161
|
+
return n * { s: 1, m: 60, h: 3600, d: 86400 }[m[2]];
|
|
162
|
+
}
|
|
163
|
+
// The from/to pair for the logs request, from whichever window flags were given. Pure, unit-tested.
|
|
164
|
+
export function resolveLogWindow(opts, now = Math.floor(Date.now() / 1000)) {
|
|
165
|
+
if (opts.since && opts.from)
|
|
166
|
+
throw new Error('pass --since or --from, not both');
|
|
167
|
+
const from = opts.since ? now - parseSinceSeconds(opts.since) : opts.from !== undefined ? parseLogInstant(opts.from, '--from') : undefined;
|
|
168
|
+
const to = opts.to !== undefined ? parseLogInstant(opts.to, '--to') : undefined;
|
|
169
|
+
if (from !== undefined && to !== undefined && to < from)
|
|
170
|
+
throw new Error('--to is before --from');
|
|
171
|
+
return { from, to };
|
|
172
|
+
}
|
|
173
|
+
// insta logs <db|compute|redis|mysql|mongodb> [group]
|
|
142
174
|
export async function logs(component, group, opts) {
|
|
175
|
+
const windowFlags = opts.from !== undefined || opts.to !== undefined || opts.since !== undefined;
|
|
176
|
+
if (opts.deploy && windowFlags)
|
|
177
|
+
throw new Error('--from/--to/--since apply to runtime logs, not --deploy events');
|
|
178
|
+
const { from, to } = resolveLogWindow(opts);
|
|
143
179
|
const api = await ApiClient.load();
|
|
144
180
|
const p = await requireProject();
|
|
145
181
|
if (opts.deploy) {
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
182
|
+
// Machine lifecycle events exist for every Fly-backed component; 'db' (postgres) has no machines.
|
|
183
|
+
if (component === 'db')
|
|
184
|
+
return info('deploy events are not available for db — use compute, redis, mysql or mongodb');
|
|
185
|
+
const res = await api.request('GET', deployEventsPath(p.projectId, { component, group, branch: opts.branch ?? p.branch, limit: opts.limit, instance: opts.instance }));
|
|
149
186
|
if (opts.json)
|
|
150
187
|
return printJson(res);
|
|
151
188
|
if (res.note)
|
|
@@ -156,7 +193,7 @@ export async function logs(component, group, opts) {
|
|
|
156
193
|
info(deployEventLine(ev));
|
|
157
194
|
return;
|
|
158
195
|
}
|
|
159
|
-
const res = await api.request('GET', `/projects/${p.projectId}/logs${qs({ component, group, branch: opts.branch ?? p.branch, limit: opts.limit, region: opts.region, instance: opts.instance })}`);
|
|
196
|
+
const res = await api.request('GET', `/projects/${p.projectId}/logs${qs({ component, group, branch: opts.branch ?? p.branch, limit: opts.limit, region: opts.region, instance: opts.instance, from: from !== undefined ? String(from) : undefined, to: to !== undefined ? String(to) : undefined })}`);
|
|
160
197
|
if (opts.json)
|
|
161
198
|
return printJson(res);
|
|
162
199
|
if (res.note)
|
package/dist/commands/org.js
CHANGED
|
@@ -8,9 +8,11 @@ export async function orgList(opts) {
|
|
|
8
8
|
for (const o of orgs)
|
|
9
9
|
info(`${o.id} ${o.name}${o.is_personal ? ' (personal)' : ''} [${o.role}]`);
|
|
10
10
|
}
|
|
11
|
-
export async function orgCreate(name) {
|
|
11
|
+
export async function orgCreate(name, opts = {}) {
|
|
12
12
|
const api = await ApiClient.load();
|
|
13
13
|
const { org } = await api.request('POST', '/orgs', { name });
|
|
14
|
+
if (opts.json)
|
|
15
|
+
return printJson(org);
|
|
14
16
|
info(`created org ${org.id} (${org.name})`);
|
|
15
17
|
}
|
|
16
18
|
//# sourceMappingURL=org.js.map
|
package/dist/commands/project.js
CHANGED
|
@@ -13,14 +13,19 @@ const GENERIC_DIRS = new Set([
|
|
|
13
13
|
'app', 'apps', 'users', 'user', 'bin', 'new', 'test', 'tests',
|
|
14
14
|
]);
|
|
15
15
|
// Best-effort: wire the credential-audit hook into the project (no-op if assets aren't built).
|
|
16
|
-
|
|
16
|
+
// quiet: with --json the install still runs, but its note moves to stderr (stdout is JSON-only).
|
|
17
|
+
function tryInstallObserve(quiet = false) {
|
|
17
18
|
try {
|
|
18
19
|
const r = installObserve({ cwd: process.cwd() });
|
|
19
|
-
if (r.claude || r.codex)
|
|
20
|
-
|
|
20
|
+
if (r.claude || r.codex) {
|
|
21
|
+
const line = ' installed observe hook (credential audit) → ./.insta/observe';
|
|
22
|
+
quiet ? process.stderr.write(line + '\n') : info(line);
|
|
23
|
+
}
|
|
21
24
|
}
|
|
22
25
|
catch { /* assets missing (dev/unbuilt) — skip silently */ }
|
|
23
26
|
}
|
|
27
|
+
// installSkills prints to stdout by default; with --json its notes go to stderr instead.
|
|
28
|
+
const skillsPrint = (json) => (json ? (s) => void process.stderr.write(s + '\n') : undefined);
|
|
24
29
|
async function resolveOrg(api, given) {
|
|
25
30
|
if (given)
|
|
26
31
|
return given;
|
|
@@ -51,7 +56,10 @@ export async function projectCreate(name, opts) {
|
|
|
51
56
|
const resolved = resolveProjectName(name, process.cwd());
|
|
52
57
|
if (!resolved) {
|
|
53
58
|
// No name given and the cwd name is generic — don't provision resources under a junk name.
|
|
54
|
-
//
|
|
59
|
+
// A terminal gets guidance (no hang, no error); --json is a scripted caller with no human to
|
|
60
|
+
// guide, so it gets a hard error instead of an empty success.
|
|
61
|
+
if (opts.json)
|
|
62
|
+
die('no project name — pass one: insta project create <name>');
|
|
55
63
|
info('name your project: insta project create <name>');
|
|
56
64
|
info(' (or just ask your coding agent — it has the insta skill and will do this for you)');
|
|
57
65
|
return;
|
|
@@ -60,12 +68,17 @@ export async function projectCreate(name, opts) {
|
|
|
60
68
|
const orgId = await resolveOrg(api, opts.org);
|
|
61
69
|
const out = await api.request('POST', `/orgs/${orgId}/projects`, { name: resolved });
|
|
62
70
|
await writeProject({ projectId: out.project.id, orgId, branch: out.defaultBranch.name });
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
71
|
+
if (opts.json) {
|
|
72
|
+
printJson({ ...out, linked: { projectId: out.project.id, orgId, branch: out.defaultBranch.name } });
|
|
73
|
+
}
|
|
74
|
+
else {
|
|
75
|
+
info(`created project ${out.project.id} (${resolved})`);
|
|
76
|
+
info(` resources: ${out.resources.map((r) => r.kind).join(', ')}`);
|
|
77
|
+
info(` linked ./.insta/project.json (branch ${out.defaultBranch.name})`);
|
|
78
|
+
renderNextActions(out.nextActions);
|
|
79
|
+
}
|
|
80
|
+
tryInstallObserve(opts.json);
|
|
81
|
+
await installSkills({ cwd: process.cwd(), print: skillsPrint(opts.json) });
|
|
69
82
|
}
|
|
70
83
|
export async function projectList(opts) {
|
|
71
84
|
const api = await ApiClient.load();
|
|
@@ -78,20 +91,25 @@ export async function projectList(opts) {
|
|
|
78
91
|
for (const p of projects)
|
|
79
92
|
info(`${p.id} ${p.name} [${p.status}]`);
|
|
80
93
|
}
|
|
81
|
-
export async function projectLink(id) {
|
|
94
|
+
export async function projectLink(id, opts = {}) {
|
|
82
95
|
const api = await ApiClient.load();
|
|
83
96
|
const { project } = await api.request('GET', `/projects/${id}`);
|
|
84
97
|
await writeProject({ projectId: project.id, orgId: project.org_id, branch: 'main' });
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
98
|
+
if (opts.json)
|
|
99
|
+
printJson({ project, linked: { projectId: project.id, orgId: project.org_id, branch: 'main' } });
|
|
100
|
+
else
|
|
101
|
+
info(`linked project ${project.id} (${project.name})`);
|
|
102
|
+
tryInstallObserve(opts.json);
|
|
103
|
+
await installSkills({ cwd: process.cwd(), print: skillsPrint(opts.json) });
|
|
88
104
|
}
|
|
89
105
|
export async function projectDelete(opts) {
|
|
90
106
|
const api = await ApiClient.load();
|
|
91
107
|
const projectId = opts.project ?? (await requireProject()).projectId;
|
|
92
108
|
const res = await api.rawRequest('DELETE', `/projects/${projectId}`);
|
|
93
|
-
if (handleApproval(res))
|
|
109
|
+
if (handleApproval(res, opts.json))
|
|
94
110
|
return;
|
|
111
|
+
if (opts.json)
|
|
112
|
+
return printJson({ ok: true, projectId });
|
|
95
113
|
info(`deleted project ${projectId}`);
|
|
96
114
|
}
|
|
97
115
|
//# sourceMappingURL=project.js.map
|
package/dist/commands/run.js
CHANGED
|
@@ -4,7 +4,7 @@
|
|
|
4
4
|
// as the process does.
|
|
5
5
|
import { spawn } from 'node:child_process';
|
|
6
6
|
import { ApiClient, requireProject } from '../api.js';
|
|
7
|
-
import { die,
|
|
7
|
+
import { die, handleApproval } from '../util.js';
|
|
8
8
|
/** Core, dependency-injected for tests: spawn cmd with the bundle in env, return its exit code. */
|
|
9
9
|
export async function runWithSecrets(cmd, args, deps) {
|
|
10
10
|
const bundle = await deps.fetchBundle();
|
|
@@ -28,10 +28,12 @@ export async function run(cmdAndArgs, opts) {
|
|
|
28
28
|
const code = await runWithSecrets(cmd, rest, {
|
|
29
29
|
fetchBundle: async () => {
|
|
30
30
|
const res = await api.rawRequest('GET', `/projects/${p.projectId}/secrets?branch=${encodeURIComponent(branch)}`);
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
31
|
+
// exit() with no argument honors the exit code handleApproval just set (2).
|
|
32
|
+
if (handleApproval(res))
|
|
33
|
+
process.exit();
|
|
34
|
+
// stderr, not stdout: `insta run`'s stdout belongs entirely to the child command (that's why
|
|
35
|
+
// run has no --json — wrapping would break the child's own output contract).
|
|
36
|
+
process.stderr.write(`running with ${Object.keys(res.body.secrets).length} injected secrets (branch ${branch}) — nothing written to disk\n`);
|
|
35
37
|
return res.body.secrets;
|
|
36
38
|
},
|
|
37
39
|
});
|
package/dist/commands/secrets.js
CHANGED
|
@@ -12,7 +12,7 @@ export async function secrets(opts) {
|
|
|
12
12
|
const p = await requireProject();
|
|
13
13
|
const branch = opts.branch ?? p.branch;
|
|
14
14
|
const res = await api.rawRequest('GET', `/projects/${p.projectId}/secrets${q(branch)}`);
|
|
15
|
-
if (handleApproval(res))
|
|
15
|
+
if (handleApproval(res, opts.json))
|
|
16
16
|
return;
|
|
17
17
|
const bundle = res.body.secrets;
|
|
18
18
|
if (opts.json)
|
|
@@ -47,7 +47,7 @@ export async function secretsTree(opts) {
|
|
|
47
47
|
const api = await ApiClient.load();
|
|
48
48
|
const p = await requireProject();
|
|
49
49
|
const res = await api.rawRequest('GET', `/projects/${p.projectId}/secrets/tree`);
|
|
50
|
-
if (handleApproval(res))
|
|
50
|
+
if (handleApproval(res, opts.json))
|
|
51
51
|
return;
|
|
52
52
|
const tree = res.body;
|
|
53
53
|
if (opts.json)
|
|
@@ -68,7 +68,7 @@ export async function secretsList(opts) {
|
|
|
68
68
|
const p = await requireProject();
|
|
69
69
|
const branch = opts.branch ?? p.branch;
|
|
70
70
|
const res = await api.rawRequest('GET', `/projects/${p.projectId}/secrets/tree`);
|
|
71
|
-
if (handleApproval(res))
|
|
71
|
+
if (handleApproval(res, opts.json))
|
|
72
72
|
return;
|
|
73
73
|
const tree = res.body;
|
|
74
74
|
const b = tree.branches.find((x) => x.name === branch);
|
|
@@ -102,8 +102,10 @@ export async function secretsSet(name, value, opts) {
|
|
|
102
102
|
const branch = opts.service ? (opts.branch ?? p.branch) : opts.branch;
|
|
103
103
|
const payload = { value: v, ...(branch ? { branch } : {}), ...(opts.service ? { service: opts.service } : {}) };
|
|
104
104
|
const res = await api.rawRequest('PUT', `/projects/${p.projectId}/secrets/${encodeURIComponent(name)}`, payload);
|
|
105
|
-
if (handleApproval(res))
|
|
105
|
+
if (handleApproval(res, opts.json))
|
|
106
106
|
return;
|
|
107
|
+
if (opts.json)
|
|
108
|
+
return printJson({ ok: true, name, branch: branch ?? null, service: opts.service ?? null });
|
|
107
109
|
info(`set ${name}${opts.service ? ` → ${opts.service}` : ''} (${branch ? `branch ${branch}` : 'project-wide'})`);
|
|
108
110
|
}
|
|
109
111
|
export async function secretsUnset(name, opts) {
|
|
@@ -111,10 +113,75 @@ export async function secretsUnset(name, opts) {
|
|
|
111
113
|
const p = await requireProject();
|
|
112
114
|
const qs = opts.branch ? `?branch=${encodeURIComponent(opts.branch)}` : '';
|
|
113
115
|
const res = await api.rawRequest('DELETE', `/projects/${p.projectId}/secrets/${encodeURIComponent(name)}${qs}`);
|
|
114
|
-
if (handleApproval(res))
|
|
116
|
+
if (handleApproval(res, opts.json))
|
|
115
117
|
return;
|
|
118
|
+
if (opts.json)
|
|
119
|
+
return printJson({ ok: true, name, branch: opts.branch ?? null });
|
|
116
120
|
info(`unset ${name} (${opts.branch ? `branch ${opts.branch}` : 'project-wide'})`);
|
|
117
121
|
}
|
|
122
|
+
export async function secretsBind(envName, source, opts) {
|
|
123
|
+
if (!opts.to)
|
|
124
|
+
die('--to <compute/name> is required');
|
|
125
|
+
const api = await ApiClient.load();
|
|
126
|
+
const p = await requireProject();
|
|
127
|
+
const branch = opts.branch ?? p.branch;
|
|
128
|
+
const res = await api.rawRequest('PUT', `/projects/${p.projectId}/secret-bindings/${encodeURIComponent(envName)}`, {
|
|
129
|
+
branch,
|
|
130
|
+
target: opts.to,
|
|
131
|
+
source,
|
|
132
|
+
...(opts.sourceName ? { sourceName: opts.sourceName } : {}),
|
|
133
|
+
});
|
|
134
|
+
if (handleApproval(res, opts.json))
|
|
135
|
+
return;
|
|
136
|
+
if (opts.json)
|
|
137
|
+
return printJson({ ok: true });
|
|
138
|
+
info(`bound ${envName} on ${opts.to} to ${source}${opts.sourceName ? `.${opts.sourceName}` : ''} (branch ${branch})`);
|
|
139
|
+
}
|
|
140
|
+
export async function secretsUnbind(envName, opts) {
|
|
141
|
+
if (!opts.from)
|
|
142
|
+
die('--from <compute/name> is required');
|
|
143
|
+
const api = await ApiClient.load();
|
|
144
|
+
const p = await requireProject();
|
|
145
|
+
const branch = opts.branch ?? p.branch;
|
|
146
|
+
const res = await api.rawRequest('DELETE', `/projects/${p.projectId}/secret-bindings/${encodeURIComponent(envName)}?branch=${encodeURIComponent(branch)}&target=${encodeURIComponent(opts.from)}`);
|
|
147
|
+
if (handleApproval(res, opts.json))
|
|
148
|
+
return;
|
|
149
|
+
if (opts.json)
|
|
150
|
+
return printJson({ ok: true });
|
|
151
|
+
info(`unbound ${envName} from ${opts.from} (branch ${branch})`);
|
|
152
|
+
}
|
|
153
|
+
export async function secretsBindings(opts) {
|
|
154
|
+
if (!opts.target)
|
|
155
|
+
die('--target <compute/name> is required');
|
|
156
|
+
const api = await ApiClient.load();
|
|
157
|
+
const p = await requireProject();
|
|
158
|
+
const branch = opts.branch ?? p.branch;
|
|
159
|
+
const res = await api.rawRequest('GET', `/projects/${p.projectId}/secret-bindings?branch=${encodeURIComponent(branch)}&target=${encodeURIComponent(opts.target)}`);
|
|
160
|
+
if (handleApproval(res, opts.json))
|
|
161
|
+
return;
|
|
162
|
+
const bindings = res.body.bindings ?? [];
|
|
163
|
+
if (opts.json)
|
|
164
|
+
return printJson(bindings);
|
|
165
|
+
if (!bindings.length)
|
|
166
|
+
return info(`(no secret bindings for ${opts.target} on ${branch})`);
|
|
167
|
+
for (const b of bindings)
|
|
168
|
+
info(`${b.envName} <- ${b.source.type}/${b.source.name}.${b.sourceName}`);
|
|
169
|
+
}
|
|
170
|
+
export async function secretsSources(opts) {
|
|
171
|
+
const api = await ApiClient.load();
|
|
172
|
+
const p = await requireProject();
|
|
173
|
+
const branch = opts.branch ?? p.branch;
|
|
174
|
+
const res = await api.rawRequest('GET', `/projects/${p.projectId}/secret-sources?branch=${encodeURIComponent(branch)}`);
|
|
175
|
+
if (handleApproval(res, opts.json))
|
|
176
|
+
return;
|
|
177
|
+
const sources = res.body.sources ?? [];
|
|
178
|
+
if (opts.json)
|
|
179
|
+
return printJson(sources);
|
|
180
|
+
if (!sources.length)
|
|
181
|
+
return info(`(no credential sources on ${branch})`);
|
|
182
|
+
for (const s of sources)
|
|
183
|
+
info(`${s.service.type}/${s.service.name}: ${s.secrets.join(', ')}`);
|
|
184
|
+
}
|
|
118
185
|
/** Gitignore the env file we just wrote (git repos only; idempotent). Returns true if added. */
|
|
119
186
|
export function ensureIgnored(cwd, name) {
|
|
120
187
|
if (!existsSync(join(cwd, '.git')))
|
|
@@ -1,7 +1,7 @@
|
|
|
1
|
-
// `insta services` — manage a project's opt-in services (postgres | storage | compute | redis).
|
|
1
|
+
// `insta services` — manage a project's opt-in services (postgres | storage | compute | redis | mysql | mongodb).
|
|
2
2
|
import { ApiClient, requireProject } from '../api.js';
|
|
3
3
|
import { info, printJson, handleApproval, renderNextActions } from '../util.js';
|
|
4
|
-
export const SERVICE_TYPES = ['postgres', 'storage', 'compute', 'redis'];
|
|
4
|
+
export const SERVICE_TYPES = ['postgres', 'storage', 'compute', 'redis', 'mysql', 'mongodb'];
|
|
5
5
|
const SERVICE_NAME_RE = /^[a-z0-9][a-z0-9-]{0,38}$/;
|
|
6
6
|
export function q(branch) {
|
|
7
7
|
return branch ? `?branch=${encodeURIComponent(branch)}` : '';
|
|
@@ -72,6 +72,9 @@ export function resolveSoleService(services, type, name) {
|
|
|
72
72
|
export function resolveComputeServiceId(services, name) {
|
|
73
73
|
return resolveSoleService(services, 'compute', name).id;
|
|
74
74
|
}
|
|
75
|
+
function defaultDatabasePort(type) {
|
|
76
|
+
return type === 'mysql' ? 3306 : type === 'mongodb' ? 27017 : 6379;
|
|
77
|
+
}
|
|
75
78
|
// Map service-add options to the platform POST body. Pure, so it's unit-tested without a network
|
|
76
79
|
// mock (mirrors deployRequestBody in deploy.ts). Validation (which options are valid for which
|
|
77
80
|
// type) stays in servicesAdd, ahead of any network/config access.
|
|
@@ -108,7 +111,7 @@ export async function servicesAdd(type, name, opts = {}) {
|
|
|
108
111
|
const p = await requireProject();
|
|
109
112
|
const branch = opts.branch ?? p.branch;
|
|
110
113
|
const res = await api.rawRequest('POST', `/projects/${p.projectId}/services`, servicesAddRequestBody(type, name, branch, opts));
|
|
111
|
-
if (handleApproval(res))
|
|
114
|
+
if (handleApproval(res, opts.json))
|
|
112
115
|
return;
|
|
113
116
|
if (opts.json)
|
|
114
117
|
return printJson(res.body.service);
|
|
@@ -124,7 +127,7 @@ export async function servicesAdd(type, name, opts = {}) {
|
|
|
124
127
|
export function serviceListLine(s) {
|
|
125
128
|
const extra = s.type === 'compute'
|
|
126
129
|
? ` x${s.machine_count}${s.volume_gib ? ` vol ${s.volume_gib}Gi` : ''}${s.image ? ` running ${s.image}${s.port ? `:${s.port}` : ''}` : ''}`
|
|
127
|
-
:
|
|
130
|
+
: ['redis', 'mysql', 'mongodb'].includes(s.type) ? ` tcp/${s.port ?? defaultDatabasePort(s.type)}${s.volume_gib ? ` vol ${s.volume_gib}Gi` : ''}`
|
|
128
131
|
: s.type === 'storage' ? ` ${s.public ? 'public' : 'private'}` : '';
|
|
129
132
|
return `${s.type}/${s.name} [${s.status}]${extra}${s.domain ? ` ${s.domain}` : ''} ${s.id}`;
|
|
130
133
|
}
|
|
@@ -136,7 +139,7 @@ export async function servicesList(opts) {
|
|
|
136
139
|
if (opts.json)
|
|
137
140
|
return printJson(services);
|
|
138
141
|
if (!services.length)
|
|
139
|
-
return info(`(no services on ${branch ?? 'default'} — add one with \`insta services add <postgres|storage|compute|redis> <name>\`)`);
|
|
142
|
+
return info(`(no services on ${branch ?? 'default'} — add one with \`insta services add <postgres|storage|compute|redis|mysql|mongodb> <name>\`)`);
|
|
140
143
|
for (const s of services)
|
|
141
144
|
info(serviceListLine(s));
|
|
142
145
|
}
|
|
@@ -148,8 +151,10 @@ export async function servicesRemove(type, name, opts = {}) {
|
|
|
148
151
|
const { services } = await api.request('GET', `/projects/${p.projectId}/services${q(branch)}`);
|
|
149
152
|
const id = resolveServiceId(services, type, name);
|
|
150
153
|
const res = await api.rawRequest('DELETE', `/projects/${p.projectId}/services/${id}`);
|
|
151
|
-
if (handleApproval(res))
|
|
154
|
+
if (handleApproval(res, opts.json))
|
|
152
155
|
return;
|
|
156
|
+
if (opts.json)
|
|
157
|
+
return printJson({ ok: true, removed: { id, type, name, branch: branch ?? null } });
|
|
153
158
|
info(`removed ${type} service ${name} from ${branch ?? 'default'}`);
|
|
154
159
|
}
|
|
155
160
|
export async function servicesRename(type, name, newName, opts = {}) {
|
|
@@ -161,7 +166,7 @@ export async function servicesRename(type, name, newName, opts = {}) {
|
|
|
161
166
|
const { services } = await api.request('GET', `/projects/${p.projectId}/services${q(branch)}`);
|
|
162
167
|
const id = resolveServiceId(services, type, name);
|
|
163
168
|
const res = await api.rawRequest('POST', `/projects/${p.projectId}/services/${id}/rename`, { name: newName });
|
|
164
|
-
if (handleApproval(res))
|
|
169
|
+
if (handleApproval(res, opts.json))
|
|
165
170
|
return;
|
|
166
171
|
if (opts.json)
|
|
167
172
|
return printJson(res.body.service);
|
|
@@ -184,7 +189,7 @@ export async function servicesSetAccess(type, name, access, _opts) {
|
|
|
184
189
|
const { services } = await api.request('GET', `/projects/${p.projectId}/services${q(p.branch)}`);
|
|
185
190
|
const id = resolveServiceId(services, type, name);
|
|
186
191
|
const res = await api.rawRequest('PUT', `/projects/${p.projectId}/services/${id}/access`, { public: isPublic });
|
|
187
|
-
if (handleApproval(res))
|
|
192
|
+
if (handleApproval(res, _opts.json))
|
|
188
193
|
return;
|
|
189
194
|
if (_opts.json)
|
|
190
195
|
return printJson(res.body.service);
|
|
@@ -199,7 +204,7 @@ export async function servicesScale(type, name, number, region, _opts) {
|
|
|
199
204
|
const { services } = await api.request('GET', `/projects/${p.projectId}/services${q(_opts.branch ?? p.branch)}`);
|
|
200
205
|
const id = resolveServiceId(services, type, name);
|
|
201
206
|
const res = await api.rawRequest('POST', `/projects/${p.projectId}/services/${id}/scale`, { machineCount, region });
|
|
202
|
-
if (handleApproval(res))
|
|
207
|
+
if (handleApproval(res, _opts.json))
|
|
203
208
|
return;
|
|
204
209
|
if (_opts.json)
|
|
205
210
|
return printJson(res.body.service);
|
|
@@ -213,7 +218,7 @@ export async function servicesUpgrade(type, name, spec, _opts) {
|
|
|
213
218
|
const { services } = await api.request('GET', `/projects/${p.projectId}/services${q(_opts.branch ?? p.branch)}`);
|
|
214
219
|
const id = resolveServiceId(services, type, name);
|
|
215
220
|
const res = await api.rawRequest('POST', `/projects/${p.projectId}/services/${id}/upgrade`, { spec });
|
|
216
|
-
if (handleApproval(res))
|
|
221
|
+
if (handleApproval(res, _opts.json))
|
|
217
222
|
return;
|
|
218
223
|
if (_opts.json)
|
|
219
224
|
return printJson(res.body.service);
|
|
@@ -227,7 +232,7 @@ export async function servicesSecrets(type, name, opts = {}) {
|
|
|
227
232
|
const { services } = await api.request('GET', `/projects/${p.projectId}/services${q(opts.branch ?? p.branch)}`);
|
|
228
233
|
const id = resolveServiceId(services, type, name);
|
|
229
234
|
const res = await api.rawRequest('GET', `/projects/${p.projectId}/services/${id}/secrets`);
|
|
230
|
-
if (handleApproval(res))
|
|
235
|
+
if (handleApproval(res, opts.json))
|
|
231
236
|
return;
|
|
232
237
|
const { secrets } = res.body;
|
|
233
238
|
if (opts.json)
|