Sign In

insta

Package Overview
Dependencies
Maintainers
3
Versions
44
Alerts
File Explorer

Advanced tools

Socket logo

Install Socket

Detect and block malicious and high-risk dependencies

Install

insta - npm Package Compare versions

Comparing version
0.0.36
to
0.0.37
+4
-2
dist/api.js

@@ -5,3 +5,3 @@ // Thin API client over the platform control-plane. Handles bearer auth + one-shot refresh on 401.

import { autoResolveProject, promptChoice } from './resolve-project.js';
import { die, info } from './util.js';
import { die } from './util.js';
export class ApiError extends Error {

@@ -121,3 +121,5 @@ status;

await writeProject(c);
info(`auto-linked project ${c.projectId} → ./.insta/project.json`);
// stderr: this is a diagnostic that can precede ANY command's output — under --json,
// stdout must stay one parseable document.
process.stderr.write(`auto-linked project ${c.projectId} → ./.insta/project.json\n`);
},

@@ -124,0 +126,0 @@ tty: !!process.stdin.isTTY && !!process.stderr.isTTY,

@@ -8,2 +8,4 @@ import { ApiClient, requireProject } from '../api.js';

const out = await api.request('POST', `/projects/${p.projectId}/branches`, { name, from: opts.from ?? p.branch });
if (opts.json)
return printJson(out);
info(`created branch ${out.branch.name} (${out.branch.id})`);

@@ -21,3 +23,3 @@ renderNextActions(out.nextActions);

}
export async function branchSwitch(name) {
export async function branchSwitch(name, opts = {}) {
const api = await ApiClient.load();

@@ -29,5 +31,7 @@ const p = await requireProject();

await writeProject({ ...p, branch: name });
if (opts.json)
return printJson({ projectId: p.projectId, branch: name });
info(`switched to branch ${name} — run \`insta secrets\` to refresh .env`);
}
export async function branchDelete(name) {
export async function branchDelete(name, opts = {}) {
const api = await ApiClient.load();

@@ -40,4 +44,6 @@ const p = await requireProject();

const res = await api.rawRequest('DELETE', `/projects/${p.projectId}/branches/${b.id}`);
if (handleApproval(res))
if (handleApproval(res, opts.json))
return;
if (opts.json)
return printJson({ ok: true, branch: { id: b.id, name: b.name } });
info(`deleted branch ${name}`);

@@ -54,4 +60,6 @@ }

const res = await api.rawRequest('POST', `/projects/${p.projectId}/branches/${encodeURIComponent(target)}/merge`, { from: source });
if (handleApproval(res))
if (handleApproval(res, opts.json))
return;
if (opts.json)
return printJson(res.body ?? {});
const { created = [], skipped = [] } = (res.body ?? {});

@@ -58,0 +66,0 @@ info(`merged ${source} → ${target}: ${created.length} created, ${skipped.length} skipped`);

@@ -10,3 +10,3 @@ import { ApiClient, ApiError, requireProject } from '../api.js';

const res = await api.rawRequest('POST', `/projects/${p.projectId}/compute/domain`, { hostname: host, branch: opts.branch ?? p.branch, group: opts.group });
if (handleApproval(res))
if (handleApproval(res, opts.json))
return;

@@ -30,6 +30,13 @@ printDomain(res.body, opts.json);

const res = await api.rawRequest('DELETE', `/projects/${p.projectId}/compute/domain`, { hostname: host, branch: opts.branch ?? p.branch, group: opts.group });
if (handleApproval(res))
if (handleApproval(res, opts.json))
return;
info(`removed custom domain ${res.body.hostname} from ${res.body.flyApp}`);
renderRemoveDomain(res.body, opts.json);
}
// Split out (same pattern as applyExecResult) so the --json contract — stdout carries the platform
// response, never prose — is unit-testable without a network mock.
export function renderRemoveDomain(body, json) {
if (json)
return printJson(body);
info(`removed custom domain ${body.hostname} from ${body.flyApp}`);
}
function printDomain(r, json) {

@@ -55,3 +62,3 @@ if (json)

const res = await api.rawRequest('POST', `/projects/${p.projectId}/services/${id}/${verb}`);
if (handleApproval(res))
if (handleApproval(res, opts.json))
return;

@@ -113,15 +120,9 @@ if (opts.json)

//
// A 202 means the command has NOT run: unlike every other gated command (where "nothing happened"
// is the safe default), a caller chaining `insta compute exec … && next` must not see exit 0 here,
// or `next` runs believing the command succeeded. --json prints the raw envelope (so a scripted
// caller can inspect approvalId/action) instead of the human hint; either way exit 1.
// A 202 means the command has NOT run: handleApproval owns the whole contract (hint on stderr,
// raw envelope on stdout with --json, exit 2), so a caller chaining `insta compute exec … && next`
// can never mistake a pending gate for the command having succeeded — and exit 2 stays
// distinguishable from the remote command's own exit 1.
export function applyExecResult(res, json) {
if (res.status === 202 && res.body?.status === 'approval_required') {
if (json)
printJson(res.body);
else
handleApproval(res);
process.exitCode = 1;
if (handleApproval(res, json))
return;
}
const { exitCode, stdout, stderr, truncated } = res.body;

@@ -177,3 +178,3 @@ if (json) {

const res = await api.rawRequest('PUT', `/projects/${p.projectId}/services/${id}/always-on`, { enabled: mode === 'on' });
if (handleApproval(res))
if (handleApproval(res, opts.json))
return;

@@ -279,3 +280,3 @@ if (opts.json)

}
if (handleApproval(res))
if (handleApproval(res, opts.json))
return;

@@ -297,3 +298,3 @@ if (opts.json)

const res = await api.rawRequest('PUT', `/projects/${p.projectId}/services/${id}/volume`, { sizeGib });
if (handleApproval(res))
if (handleApproval(res, opts.json))
return;

@@ -327,3 +328,3 @@ if (opts.json)

const res = await api.rawRequest('PUT', `/projects/${p.projectId}/services/${id}/limits`, body);
if (handleApproval(res))
if (handleApproval(res, opts.json))
return;

@@ -330,0 +331,0 @@ if (opts.json)

@@ -22,3 +22,3 @@ import { ApiClient, ApiError, requireProject } from '../api.js';

const res = await api.rawRequest('PATCH', `/projects/${p.projectId}/database/settings${qs.toString() ? `?${qs}` : ''}`, { scaleToZero: mode !== 'on' });
if (handleApproval(res))
if (handleApproval(res, opts.json))
return;

@@ -115,3 +115,3 @@ if (opts.json)

}
if (handleApproval(res))
if (handleApproval(res, opts.json))
return;

@@ -235,3 +235,3 @@ if (opts.json)

}
if (handleApproval(res))
if (handleApproval(res, opts.json))
return;

@@ -238,0 +238,0 @@ if (opts.json)

import { resolve, join } from 'node:path';
import { existsSync, readFileSync } from 'node:fs';
import { ApiClient, ApiError, requireProject } from '../api.js';
import { info, die, handleApproval, renderNextActions } from '../util.js';
import { flyctlBuildAndPush, ensureFlyctl, defaultBuildRunner } from '../flyctl-build.js';
import { info, die, printJson, handleApproval, renderNextActions } from '../util.js';
import { flyctlBuildAndPush, ensureFlyctl, defaultBuildRunner, stderrBuildRunner } from '../flyctl-build.js';
// With --json, stdout must carry exactly one JSON document (the deploy result), so every progress
// line moves to stderr.
const note = (opts) => (opts.json ? (m) => void process.stderr.write(m + '\n') : info);
// Map CLI options to the platform deploy request body. Pure, so it's unit-tested. --websocket is only

@@ -39,2 +42,3 @@ // sent when set (plain deploys unchanged).

const branch = opts.branch ?? p.branch;
const log = note(opts);
let port = opts.port ? Number(opts.port) : undefined;

@@ -46,3 +50,3 @@ if (dir && port === undefined) {

port = exposed;
info(`using port ${exposed} (Dockerfile EXPOSE) — override with --port`);
log(`using port ${exposed} (Dockerfile EXPOSE) — override with --port`);
}

@@ -53,4 +57,6 @@ }

const res = await api.rawRequest('POST', `/projects/${p.projectId}/deploy`, deployRequestBody(image, branch, effOpts));
if (handleApproval(res))
if (handleApproval(res, opts.json))
return;
if (opts.json)
return printJson({ image, ...res.body });
info(`deployed ${image} -> ${res.body.url} (branch ${res.body.branch}, group ${res.body.group})`);

@@ -77,6 +83,7 @@ renderNextActions(res.body.nextActions);

// Exported with injectable pieces for tests (the repo's DI pattern; no global mocks).
export async function buildFromSource(api, projectId, dir, branch, opts, run = defaultBuildRunner) {
export async function buildFromSource(api, projectId, dir, branch, opts, run = opts.json ? stderrBuildRunner : defaultBuildRunner) {
const absDir = resolve(process.cwd(), dir);
if (!existsSync(join(absDir, 'Dockerfile')))
die(`no Dockerfile at ${join(absDir, 'Dockerfile')} — add one, or use --image <url>`);
const log = note(opts);
let tok;

@@ -92,17 +99,18 @@ try {

const tag = localImageTag(projectId, opts.group);
info(`no remote builder on this daemon — building ${dir} locally with docker…`);
log(`no remote builder on this daemon — building ${dir} locally with docker…`);
const built = await dockerBuildLocal(absDir, tag, run);
info(` built ${built}`);
log(` built ${built}`);
return built;
}
if (handleApproval(tok))
die('deploy requires approval — get it approved, then re-run');
// exit() with no argument honors the exit code handleApproval just set (2).
if (handleApproval(tok, opts.json))
process.exit();
const { token, flyApp } = tok.body;
await ensureFlyctl(); // cloud path only — the local path needs docker, which the daemon requires anyway
const port = opts.port ? Number(opts.port) : 8080;
info(`building ${dir} for ${flyApp} (remote builder)…`);
log(`building ${dir} for ${flyApp} (remote builder)…`);
const { imageRef } = await flyctlBuildAndPush({ dir: absDir, flyApp, imageLabel: `insta-${Date.now()}`, token, port }, run);
info(` pushed ${imageRef}`);
log(` pushed ${imageRef}`);
return imageRef;
}
//# sourceMappingURL=deploy.js.map

@@ -23,3 +23,16 @@ // `insta env` — show or switch the deployment environment (prod | staging).

}
export async function envUse(name) {
// One stable schema for BOTH envUse outcomes (no-op and real switch), so a scripted caller can key
// on any field — mcpServer, previous — without probing which branch ran. Pure, unit-tested.
export function envUseResult(target, previous, changed, sessionDropped) {
return {
env: target,
previous,
apiUrl: ENVS[target].api,
mcpUrl: ENVS[target].mcp,
mcpServer: mcpServerName(target),
changed,
sessionDropped,
};
}
export async function envUse(name, opts = {}) {
const want = name.trim().toLowerCase();

@@ -37,2 +50,4 @@ if (!isEnvName(want))

if (normalizeUrl(stored.apiUrl) === normalizeUrl(nextApi)) {
if (opts.json)
return printJson(envUseResult(target, from ?? target, false, false));
info(`already on ${target} (${nextApi})`);

@@ -54,2 +69,4 @@ return;

await writeGlobal(next);
if (opts.json)
return printJson(envUseResult(target, from ?? null, true, hadSession));
info(`switched ${from ?? '(custom)'} → ${target}`);

@@ -56,0 +73,0 @@ info(` api: ${nextApi}`);

@@ -32,8 +32,12 @@ import { ApiClient, requireProject } from '../api.js';

const out = await api.request('POST', `/projects/${p.projectId}/approvals/${id}/approve`, { always: !!opts.always });
if (opts.json)
return printJson(out);
info(`approved ${out.approval.action} (${id})${opts.always ? ' — policy set to allow' : ''}`);
}
export async function approvalsDeny(id) {
export async function approvalsDeny(id, opts = {}) {
const api = await ApiClient.load();
const p = await requireProject();
const out = await api.request('POST', `/projects/${p.projectId}/approvals/${id}/deny`);
if (opts.json)
return printJson(out);
info(`denied ${out.approval.action} (${id})`);

@@ -50,8 +54,10 @@ }

}
export async function policySet(action, decision) {
export async function policySet(action, decision, opts = {}) {
const api = await ApiClient.load();
const p = await requireProject();
await api.request('PUT', `/projects/${p.projectId}/policy/${action}`, { decision });
const out = await api.request('PUT', `/projects/${p.projectId}/policy/${action}`, { decision });
if (opts.json)
return printJson({ action, decision, ...(out ?? {}) });
info(`policy ${action} = ${decision}`);
}
//# sourceMappingURL=govern.js.map

@@ -143,4 +143,38 @@ import { ApiClient, requireProject } from '../api.js';

}
// A log-window instant from the CLI: unix seconds (all digits) or anything Date.parse reads
// (ISO-8601 etc.). Throws on junk — a mistyped instant must fail here, not become NaN on the wire.
export function parseLogInstant(raw, flag) {
if (/^\d+$/.test(raw))
return Number(raw);
const ms = Date.parse(raw);
if (Number.isNaN(ms))
throw new Error(`invalid ${flag}: ${raw} (unix seconds or an ISO-8601 date)`);
return Math.floor(ms / 1000);
}
// '90s' | '30m' | '2h' | '1d' → seconds. Throws on junk, zero, and unknown units.
export function parseSinceSeconds(raw) {
const m = /^(\d+)([smhd])$/.exec(raw.trim());
if (!m)
throw new Error(`invalid --since: ${raw} (e.g. 90s, 30m, 2h, 1d)`);
const n = Number(m[1]);
if (n === 0)
throw new Error(`invalid --since: ${raw} (must be > 0)`);
return n * { s: 1, m: 60, h: 3600, d: 86400 }[m[2]];
}
// The from/to pair for the logs request, from whichever window flags were given. Pure, unit-tested.
export function resolveLogWindow(opts, now = Math.floor(Date.now() / 1000)) {
if (opts.since && opts.from)
throw new Error('pass --since or --from, not both');
const from = opts.since ? now - parseSinceSeconds(opts.since) : opts.from !== undefined ? parseLogInstant(opts.from, '--from') : undefined;
const to = opts.to !== undefined ? parseLogInstant(opts.to, '--to') : undefined;
if (from !== undefined && to !== undefined && to < from)
throw new Error('--to is before --from');
return { from, to };
}
// insta logs <db|compute|redis|mysql|mongodb> [group]
export async function logs(component, group, opts) {
const windowFlags = opts.from !== undefined || opts.to !== undefined || opts.since !== undefined;
if (opts.deploy && windowFlags)
throw new Error('--from/--to/--since apply to runtime logs, not --deploy events');
const { from, to } = resolveLogWindow(opts);
const api = await ApiClient.load();

@@ -163,3 +197,3 @@ const p = await requireProject();

}
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 })}`);
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 })}`);
if (opts.json)

@@ -166,0 +200,0 @@ return printJson(res);

@@ -11,7 +11,9 @@ import { ApiClient } from '../api.js';

}
export async function orgCreate(name) {
export async function orgCreate(name, opts = {}) {
const api = await ApiClient.load();
const { org } = await api.request('POST', '/orgs', { name });
if (opts.json)
return printJson(org);
info(`created org ${org.id} (${org.name})`);
}
//# sourceMappingURL=org.js.map

@@ -16,10 +16,15 @@ import { homedir } from 'node:os';

// Best-effort: wire the credential-audit hook into the project (no-op if assets aren't built).
function tryInstallObserve() {
// quiet: with --json the install still runs, but its note moves to stderr (stdout is JSON-only).
function tryInstallObserve(quiet = false) {
try {
const r = installObserve({ cwd: process.cwd() });
if (r.claude || r.codex)
info(' installed observe hook (credential audit) → ./.insta/observe');
if (r.claude || r.codex) {
const line = ' installed observe hook (credential audit) → ./.insta/observe';
quiet ? process.stderr.write(line + '\n') : info(line);
}
}
catch { /* assets missing (dev/unbuilt) — skip silently */ }
}
// installSkills prints to stdout by default; with --json its notes go to stderr instead.
const skillsPrint = (json) => (json ? (s) => void process.stderr.write(s + '\n') : undefined);
async function resolveOrg(api, given) {

@@ -55,3 +60,6 @@ if (given)

// No name given and the cwd name is generic — don't provision resources under a junk name.
// Guide instead (no hang, no error): name it explicitly, or just ask the skill-equipped agent.
// A terminal gets guidance (no hang, no error); --json is a scripted caller with no human to
// guide, so it gets a hard error instead of an empty success.
if (opts.json)
die('no project name — pass one: insta project create <name>');
info('name your project: insta project create <name>');

@@ -65,8 +73,13 @@ info(' (or just ask your coding agent — it has the insta skill and will do this for you)');

await writeProject({ projectId: out.project.id, orgId, branch: out.defaultBranch.name });
info(`created project ${out.project.id} (${resolved})`);
info(` resources: ${out.resources.map((r) => r.kind).join(', ')}`);
info(` linked ./.insta/project.json (branch ${out.defaultBranch.name})`);
renderNextActions(out.nextActions);
tryInstallObserve();
await installSkills({ cwd: process.cwd() });
if (opts.json) {
printJson({ ...out, linked: { projectId: out.project.id, orgId, branch: out.defaultBranch.name } });
}
else {
info(`created project ${out.project.id} (${resolved})`);
info(` resources: ${out.resources.map((r) => r.kind).join(', ')}`);
info(` linked ./.insta/project.json (branch ${out.defaultBranch.name})`);
renderNextActions(out.nextActions);
}
tryInstallObserve(opts.json);
await installSkills({ cwd: process.cwd(), print: skillsPrint(opts.json) });
}

@@ -84,9 +97,12 @@ export async function projectList(opts) {

}
export async function projectLink(id) {
export async function projectLink(id, opts = {}) {
const api = await ApiClient.load();
const { project } = await api.request('GET', `/projects/${id}`);
await writeProject({ projectId: project.id, orgId: project.org_id, branch: 'main' });
info(`linked project ${project.id} (${project.name})`);
tryInstallObserve();
await installSkills({ cwd: process.cwd() });
if (opts.json)
printJson({ project, linked: { projectId: project.id, orgId: project.org_id, branch: 'main' } });
else
info(`linked project ${project.id} (${project.name})`);
tryInstallObserve(opts.json);
await installSkills({ cwd: process.cwd(), print: skillsPrint(opts.json) });
}

@@ -97,6 +113,8 @@ export async function projectDelete(opts) {

const res = await api.rawRequest('DELETE', `/projects/${projectId}`);
if (handleApproval(res))
if (handleApproval(res, opts.json))
return;
if (opts.json)
return printJson({ ok: true, projectId });
info(`deleted project ${projectId}`);
}
//# sourceMappingURL=project.js.map

@@ -7,3 +7,3 @@ // `insta run -- <cmd>` — the Railway model for credentials: fetch the branch's secret bundle

import { ApiClient, requireProject } from '../api.js';
import { die, info } from '../util.js';
import { die, handleApproval } from '../util.js';
/** Core, dependency-injected for tests: spawn cmd with the bundle in env, return its exit code. */

@@ -32,6 +32,8 @@ export async function runWithSecrets(cmd, args, deps) {

const res = await api.rawRequest('GET', `/projects/${p.projectId}/secrets?branch=${encodeURIComponent(branch)}`);
if (res.status === 202) {
die(`secrets.read requires approval — run: insta approvals approve ${res.body.approvalId}, then re-run`);
}
info(`running with ${Object.keys(res.body.secrets).length} injected secrets (branch ${branch}) — nothing written to disk`);
// exit() with no argument honors the exit code handleApproval just set (2).
if (handleApproval(res))
process.exit();
// stderr, not stdout: `insta run`'s stdout belongs entirely to the child command (that's why
// run has no --json — wrapping would break the child's own output contract).
process.stderr.write(`running with ${Object.keys(res.body.secrets).length} injected secrets (branch ${branch}) — nothing written to disk\n`);
return res.body.secrets;

@@ -38,0 +40,0 @@ },

@@ -15,3 +15,3 @@ import { writeFile } from 'node:fs/promises';

const res = await api.rawRequest('GET', `/projects/${p.projectId}/secrets${q(branch)}`);
if (handleApproval(res))
if (handleApproval(res, opts.json))
return;

@@ -51,3 +51,3 @@ const bundle = res.body.secrets;

const res = await api.rawRequest('GET', `/projects/${p.projectId}/secrets/tree`);
if (handleApproval(res))
if (handleApproval(res, opts.json))
return;

@@ -73,3 +73,3 @@ const tree = res.body;

const res = await api.rawRequest('GET', `/projects/${p.projectId}/secrets/tree`);
if (handleApproval(res))
if (handleApproval(res, opts.json))
return;

@@ -108,4 +108,6 @@ const tree = res.body;

const res = await api.rawRequest('PUT', `/projects/${p.projectId}/secrets/${encodeURIComponent(name)}`, payload);
if (handleApproval(res))
if (handleApproval(res, opts.json))
return;
if (opts.json)
return printJson({ ok: true, name, branch: branch ?? null, service: opts.service ?? null });
info(`set ${name}${opts.service ? ` → ${opts.service}` : ''} (${branch ? `branch ${branch}` : 'project-wide'})`);

@@ -118,4 +120,6 @@ }

const res = await api.rawRequest('DELETE', `/projects/${p.projectId}/secrets/${encodeURIComponent(name)}${qs}`);
if (handleApproval(res))
if (handleApproval(res, opts.json))
return;
if (opts.json)
return printJson({ ok: true, name, branch: opts.branch ?? null });
info(`unset ${name} (${opts.branch ? `branch ${opts.branch}` : 'project-wide'})`);

@@ -135,3 +139,3 @@ }

});
if (handleApproval(res))
if (handleApproval(res, opts.json))
return;

@@ -149,3 +153,3 @@ if (opts.json)

const res = await api.rawRequest('DELETE', `/projects/${p.projectId}/secret-bindings/${encodeURIComponent(envName)}?branch=${encodeURIComponent(branch)}&target=${encodeURIComponent(opts.from)}`);
if (handleApproval(res))
if (handleApproval(res, opts.json))
return;

@@ -163,3 +167,3 @@ if (opts.json)

const res = await api.rawRequest('GET', `/projects/${p.projectId}/secret-bindings?branch=${encodeURIComponent(branch)}&target=${encodeURIComponent(opts.target)}`);
if (handleApproval(res))
if (handleApproval(res, opts.json))
return;

@@ -179,3 +183,3 @@ const bindings = res.body.bindings ?? [];

const res = await api.rawRequest('GET', `/projects/${p.projectId}/secret-sources?branch=${encodeURIComponent(branch)}`);
if (handleApproval(res))
if (handleApproval(res, opts.json))
return;

@@ -182,0 +186,0 @@ const sources = res.body.sources ?? [];

@@ -114,3 +114,3 @@ // `insta services` — manage a project's opt-in services (postgres | storage | compute | redis | mysql | mongodb).

const res = await api.rawRequest('POST', `/projects/${p.projectId}/services`, servicesAddRequestBody(type, name, branch, opts));
if (handleApproval(res))
if (handleApproval(res, opts.json))
return;

@@ -155,4 +155,6 @@ if (opts.json)

const res = await api.rawRequest('DELETE', `/projects/${p.projectId}/services/${id}`);
if (handleApproval(res))
if (handleApproval(res, opts.json))
return;
if (opts.json)
return printJson({ ok: true, removed: { id, type, name, branch: branch ?? null } });
info(`removed ${type} service ${name} from ${branch ?? 'default'}`);

@@ -169,3 +171,3 @@ }

const res = await api.rawRequest('POST', `/projects/${p.projectId}/services/${id}/rename`, { name: newName });
if (handleApproval(res))
if (handleApproval(res, opts.json))
return;

@@ -193,3 +195,3 @@ if (opts.json)

const res = await api.rawRequest('PUT', `/projects/${p.projectId}/services/${id}/access`, { public: isPublic });
if (handleApproval(res))
if (handleApproval(res, _opts.json))
return;

@@ -209,3 +211,3 @@ if (_opts.json)

const res = await api.rawRequest('POST', `/projects/${p.projectId}/services/${id}/scale`, { machineCount, region });
if (handleApproval(res))
if (handleApproval(res, _opts.json))
return;

@@ -224,3 +226,3 @@ if (_opts.json)

const res = await api.rawRequest('POST', `/projects/${p.projectId}/services/${id}/upgrade`, { spec });
if (handleApproval(res))
if (handleApproval(res, _opts.json))
return;

@@ -239,3 +241,3 @@ if (_opts.json)

const res = await api.rawRequest('GET', `/projects/${p.projectId}/services/${id}/secrets`);
if (handleApproval(res))
if (handleApproval(res, opts.json))
return;

@@ -242,0 +244,0 @@ const { secrets } = res.body;

@@ -9,2 +9,4 @@ // `insta setup agent` — make this machine's coding agents InstaCloud-native in one step

import { spawn } from 'node:child_process';
import { existsSync, readFileSync, statSync } from 'node:fs';
import { dirname, join } from 'node:path';
import os from 'node:os';

@@ -16,2 +18,3 @@ import { ApiClient } from '../api.js';

import { installAgentConfigs } from './mcp.js';
import { detectChannel } from './upgrade.js';
// The `skills` tool we shell out to prints a clack UI: a frame-by-frame clone spinner, an

@@ -81,2 +84,146 @@ // "Installing to all N agents" banner, a full N-line install-path box, and a third-party

}
// ---- CLI self-install (makes `npx -y insta setup agent` a complete one-liner) ----
// Under npx the CLI runs from the npm cache and vanishes when the process exits — but the skill
// installed below tells every agent to run `insta …`, which then wouldn't exist. So when this
// process came from the npm channel and no DURABLE `insta` is on PATH, install ourselves
// globally first. The scan must ignore any PATH entry under a node_modules directory: npx
// prepends its cache's node_modules/.bin (where this very process's `insta` shim lives), while
// durable installs (npm -g bin, nvm/volta/fnm, the native binary's ~/.insta/bin) never sit
// under one.
// On POSIX a PATH hit only counts if it would actually run: a plain non-executable file (or a
// directory) named `insta` must not suppress the self-install. Mode bits, not access(X_OK) —
// access() answers "can THIS process exec it", which for root is always yes, so a root-run
// setup would wrongly treat a non-executable file as a durable install. On Windows execute
// permission is extension-driven, so existence of a regular file is the right check.
const isRunnableFile = (p, win) => {
try {
const st = statSync(p);
if (!st.isFile())
return false;
return win || (st.mode & 0o111) !== 0;
}
catch {
return false;
}
};
/** Resolve a bare command name to its absolute PATH location (PATHEXT-aware on Windows).
* cmd.exe searches the CURRENT DIRECTORY before PATH for bare names, so handing it a bare
* `claude` would let a claude.cmd planted in the project directory shadow the real CLI —
* the cmd.exe wrapper below only ever passes absolute paths. */
export function whichOnPath(bin, env = process.env, platform = process.platform) {
const win = platform === 'win32';
const exts = win ? [...(env.PATHEXT ?? '.COM;.EXE;.BAT;.CMD').split(';'), ''] : [''];
for (const dir of (env.PATH ?? '').split(win ? ';' : ':')) {
if (!dir)
continue;
for (const ext of exts) {
const p = join(dir, bin + ext);
if (isRunnableFile(p, win))
return p;
}
}
return null;
}
export function findDurableOnPath(bin, env = process.env, platform = process.platform) {
const win = platform === 'win32';
const dirs = (env.PATH ?? '').split(win ? ';' : ':');
// npm on Windows writes insta.cmd/insta.ps1 plus an extensionless sh shim; PATHEXT covers the
// former, the bare name the latter.
const exts = win ? [...(env.PATHEXT ?? '.COM;.EXE;.BAT;.CMD').split(';'), ''] : [''];
for (const dir of dirs) {
// Case-insensitive: Windows paths (and the npx cache) may carry any casing.
if (!dir || dir.toLowerCase().includes('node_modules'))
continue;
for (const ext of exts)
if (isRunnableFile(join(dir, bin + ext), win))
return true;
}
return false;
}
const cliVersion = () => {
try {
return JSON.parse(readFileSync(new URL('../../package.json', import.meta.url), 'utf8')).version;
}
catch {
return 'latest';
}
};
/** Best-effort: a failed global install must not block the skill/MCP setup below — the npx run
* itself still completes the agent onboarding, and the manual fallback is one line. */
export async function ensureCliInstalled(run, channel = detectChannel(), onPath = findDurableOnPath('insta'), recheck = () => findDurableOnPath('insta')) {
if (channel !== 'npm' || onPath)
return;
info('installing the insta CLI globally (npm) …');
// Pinned to THIS version so the one-liner installs exactly what it ran. The logical `npm` is
// resolved to a spawnable invocation ONCE, inside the default runner (resolveSpawnable) —
// handing it a pre-resolved node/npm-cli.js path here would make the runner resolve it a
// second time and, on Windows, wrap the real node.exe in cmd.exe.
const spec = `insta@${cliVersion()}`;
const res = await run('npm', ['install', '-g', spec]);
if (res.ok) {
// A clean `npm i -g` can still land in a bin dir that isn't on PATH (custom npm prefix) —
// exactly the machines this path exists for. Only claim success after re-finding the shim.
if (recheck()) {
info('✓ insta CLI — installed globally (`insta` now works in any shell)');
}
else {
info('✓ insta CLI — installed globally, but npm\'s global bin dir is not on PATH');
info(' add it to PATH (POSIX: `$(npm prefix -g)/bin`; Windows: the dir `npm prefix -g` prints), then verify with `insta --version`');
}
return;
}
info(' global CLI install failed — continuing with agent setup; install manually with:');
info(` npm install -g ${spec}`);
if (/EACCES|permission denied/i.test(res.output ?? '')) {
info(' (permission error: the npm prefix is system-owned — use a Node version manager, or elevate that one command)');
}
}
// ---- Windows-safe spawning for npm/npx ----
// On Windows `npm`/`npx` are .cmd shims, which spawn() without a shell refuses (Node docs:
// spawning .bat/.cmd needs a shell or cmd.exe). Rather than a shell (argument-quoting hazards),
// re-enter them as node scripts: the CLI script named by npm_execpath (swapped between
// npm-cli.js and npx-cli.js as needed), else the one shipped beside the running node, else the
// bare name (POSIX, where PATH shims resolve fine). Applied ONCE, inside the default runner,
// so every `run('npm'|'npx', …)` call site benefits and nothing is ever resolved twice.
export function resolveSpawnable(cmd, args, npmExecpath = process.env.npm_execpath, execPath = process.execPath, platform = process.platform, env = process.env) {
// Node re-entry is only valid when THIS process runs on node. On the native-binary channel
// execPath is the compiled `insta` executable — and npm scripts export npm_execpath to their
// children — so re-entering blindly would spawn `insta npx-cli.js …`. A non-node execPath
// sends npm/npx down the generic shim path below instead.
const execIsNode = /(^|[\\/])node(\.exe)?$/i.test(execPath);
if ((cmd === 'npm' || cmd === 'npx') && execIsNode) {
if (npmExecpath && /(^|[\\/])np[mx](-cli)?\.[cm]?js$/.test(npmExecpath)) {
const cli = npmExecpath.replace(/np[mx](-cli)?(\.[cm]?js)$/, `${cmd}$1$2`);
if (existsSync(cli))
return { cmd: execPath, args: [cli, ...args] };
}
const nodeDir = dirname(execPath);
const besideNode = platform === 'win32'
? join(nodeDir, 'node_modules', 'npm', 'bin', `${cmd}-cli.js`)
: join(nodeDir, '..', 'lib', 'node_modules', 'npm', 'bin', `${cmd}-cli.js`);
if (existsSync(besideNode))
return { cmd: execPath, args: [besideNode, ...args] };
}
// Generic shim path — every non-npm CLI we shell out to (claude), plus npm/npx themselves
// when node isn't resolvable (native binary channel). On Windows these are .cmd shims, which
// spawn() refuses without a shell, so route them through cmd.exe. Guards, in order:
// - BARE names only: an absolute path or anything .exe (node.exe from a resolved npm/npx
// invocation passing back through here) is directly spawnable and must NOT see cmd.exe.
// - The name is resolved to its ABSOLUTE PATH location first: cmd.exe searches the current
// directory before PATH, so a bare name would let a shim planted in the project dir
// shadow the real CLI. No PATH hit → pass through (spawn fails; callers degrade).
// - No manual quoting: libuv already wraps spaced args when building the child command
// line — pre-quoting would be quoted AGAIN and arrive as literal quote characters.
// - That leaves cmd.exe metacharacters unprotectable, so an arg carrying one (e.g. a
// custom INSTA_MCP_URL with `&`) skips the wrapper: the bare-shim spawn fails and every
// caller degrades gracefully (probe → not-installed; registration → manual-add
// fallback). Never hand metacharacters to a shell.
const bareShim = !/[\\/]/.test(cmd) && !/\.exe$/i.test(cmd);
if (platform === 'win32' && bareShim && !args.some((a) => /[&|<>^%"]/.test(a))) {
const abs = whichOnPath(cmd, env, platform);
if (abs)
return { cmd: 'cmd.exe', args: ['/d', '/s', '/c', abs, ...args] };
}
return { cmd, args };
}
// Capture stdout+stderr silently (don't stream) so we can print our own clean summary.

@@ -87,4 +234,12 @@ // stdin is 'ignore', NOT 'inherit': under the canonical `curl … | sh` install, stdin is the

// guidance. Ignoring stdin keeps the installer's own output intact. (-y means no prompt anyway.)
const defaultRunner = (cmd, args) => new Promise((resolve) => {
const defaultRunner = (cmdIn, argsIn) => new Promise((resolve) => {
const { cmd, args } = resolveSpawnable(cmdIn, argsIn);
const env = { ...process.env, AI_AGENT: process.env.AI_AGENT || 'insta', FORCE_COLOR: '0' };
// When THIS process was launched by npx, npx exports its flags as npm_config_* env vars.
// npm_config_package pins package resolution for every nested npm/npx child — the inner
// `npx -y skills …` would then resolve `skills` against the insta package and degrade to
// `sh: skills: command not found`. Scrub the resolution-pinning vars; keep prefix/registry
// (deliberate user configuration).
delete env.npm_config_package;
delete env.npm_config_call;
const p = spawn(cmd, args, { stdio: ['ignore', 'pipe', 'pipe'], env });

@@ -98,2 +253,5 @@ let output = '';

});
// Leading -y is npx's OWN flag: on a machine where the `skills` package isn't already in the
// npx cache, non-TTY `npx skills …` refuses to auto-install and degrades to a shell lookup
// (`sh: skills: command not found`) — the trailing -y only answers the skills TOOL's prompt.
// -g = user-level (machine-global); -a '*' = every agent dir the skills tool supports

@@ -103,3 +261,3 @@ // (Claude Code, Codex, Cursor, OpenCode, Copilot, …); --copy = real files, not cache symlinks.

// staging install reads the staging skill text rather than what's published on main.
export const setupArgs = (spec) => ['skills', 'add', spec, '-s', 'insta', '-a', '*', '-g', '-y', '--copy'];
export const setupArgs = (spec) => ['-y', 'skills', 'add', spec, '-s', 'insta', '-a', '*', '-g', '-y', '--copy'];
/** Production's args. Kept as a named export because it is the installed-base default and is

@@ -167,6 +325,9 @@ * asserted directly by tests; runtime goes through `setupArgs(resolveEnv().skills)`. */

}
export async function setupAgent(opts, run = defaultRunner, mint, installConfigs = installAgentConfigs) {
export async function setupAgent(opts, run = defaultRunner, mint, installConfigs = installAgentConfigs, ensure = (r) => ensureCliInstalled(r)) {
if (!opts.yes && !process.stdout.isTTY) {
info('non-interactive shell — assuming -y');
}
// BEFORE the skill install: the skill tells agents to run `insta …`, so a durable CLI must
// exist by the time it lands.
await ensure(run);
// One resolve for the whole step, so the skills and the MCP registration below cannot disagree

@@ -173,0 +334,0 @@ // about which environment this machine belongs to.

@@ -53,3 +53,3 @@ // `insta storage` — browse, download, and delete the objects in a storage service's bucket.

const res = await api.rawRequest('GET', objectsPath(p.projectId, svc.id, { branch, prefix: opts.prefix, cursor: opts.cursor, limit }));
if (handleApproval(res))
if (handleApproval(res, opts.json))
return;

@@ -147,3 +147,3 @@ if (opts.json)

const res = await api.rawRequest('GET', objectDownloadPath(p.projectId, svc.id, { branch, key }));
if (handleApproval(res))
if (handleApproval(res, opts.json))
return;

@@ -169,3 +169,3 @@ // --json hands over the presigned URL instead of downloading, as `insta secrets --json` does.

const res = await api.rawRequest('DELETE', objectsPath(p.projectId, svc.id, { branch, key }));
if (handleApproval(res))
if (handleApproval(res, opts.json))
return;

@@ -172,0 +172,0 @@ if (opts.json)

@@ -12,2 +12,3 @@ // Install the related agent skills into a linked project so the developer's coding agent has

import { resolveEnv } from './config.js';
import { resolveSpawnable } from './commands/setup.js';
import { DEFAULT_ENV, ENVS } from './env.js';

@@ -24,4 +25,11 @@ // Where `npx skills add` drops skills for the agents we pin below: Claude Code → .claude/skills/,

// existing AI_AGENT (e.g. running inside another agent) rather than clobbering it.
const defaultRunner = (cmd, args, inherit = false) => new Promise((resolve) => {
const defaultRunner = (cmdIn, argsIn, inherit = false) => new Promise((resolve) => {
// resolveSpawnable: on Windows `npx` is a .cmd shim spawn() refuses without a shell —
// re-enter npm's CLI script via node instead (same treatment as `insta setup agent`).
const { cmd, args } = resolveSpawnable(cmdIn, argsIn);
const env = { ...process.env, AI_AGENT: process.env.AI_AGENT || 'insta' };
// npx exports its flags as npm_config_* to children; npm_config_package would pin the inner
// `npx -y skills …` to whatever package launched this CLI (see setup.ts defaultRunner).
delete env.npm_config_package;
delete env.npm_config_call;
const p = spawn(cmd, args, { stdio: inherit ? 'inherit' : 'ignore', env });

@@ -36,2 +44,6 @@ p.on('error', () => resolve({ ok: false })); // e.g. npx not on PATH

const AGENT_FLAGS = ['-a', 'claude-code', '-a', 'codex', '-y', '--copy'];
// npx's OWN -y, distinct from the skills tool's -y above: without it, a machine whose npx cache
// lacks the `skills` package refuses the auto-install in non-TTY runs and the whole command
// degrades to `sh: skills: command not found`.
const NPX_YES = ['-y'];
// `instaSpec` is the insta skill source for the resolved environment (`owner/repo[@ref]`), so a

@@ -41,4 +53,4 @@ // project created against staging gets the staging skill text. The third-party stack skills are

const skillTargets = (instaSpec) => [
{ label: 'insta', args: ['skills', 'add', instaSpec, '-s', 'insta', ...AGENT_FLAGS] },
{ label: 'tigris', args: ['skills', 'add', 'tigrisdata/skills',
{ label: 'insta', args: [...NPX_YES, 'skills', 'add', instaSpec, '-s', 'insta', ...AGENT_FLAGS] },
{ label: 'tigris', args: [...NPX_YES, 'skills', 'add', 'tigrisdata/skills',
'-s', 'tigris-object-operations', '-s', 'file-storage', '-s', 'tigris-sdk-guide',

@@ -48,3 +60,3 @@ '-s', 'tigris-security-access-control', '-s', 'tigris-image-optimization',

...AGENT_FLAGS] },
{ label: 'better-auth', args: ['skills', 'add', 'better-auth/skills',
{ label: 'better-auth', args: [...NPX_YES, 'skills', 'add', 'better-auth/skills',
'-s', 'better-auth-best-practices', '-s', 'email-and-password-best-practices',

@@ -51,0 +63,0 @@ '-s', 'better-auth-security-best-practices', ...AGENT_FLAGS] },

@@ -7,13 +7,17 @@ // Build a source directory into an image and push it to Fly's registry, using a short-lived,

import { join } from 'node:path';
import { info } from './util.js';
// Spawn flyctl, tee its output to the user (so they see buildkit progress) AND capture it for digest
// parsing.
export const defaultBuildRunner = (cmd, args, opts) => new Promise((resolve) => {
const child = spawn(cmd, args, { cwd: opts.cwd, env: opts.env, stdio: ['inherit', 'pipe', 'pipe'] });
let output = '';
child.stdout?.on('data', (b) => { const s = b.toString(); output += s; process.stdout.write(s); });
child.stderr?.on('data', (b) => { const s = b.toString(); output += s; process.stderr.write(s); });
child.on('error', (err) => resolve({ code: -1, output: `${output}\n${err.message}` }));
child.on('close', (code) => resolve({ code: code ?? -1, output }));
});
// parsing. `to` picks the tee destination for the child's stdout: `deploy --json` reserves the
// process's stdout for the final JSON document, so it tees build progress to stderr instead.
function teeRunner(to) {
return (cmd, args, opts) => new Promise((resolve) => {
const child = spawn(cmd, args, { cwd: opts.cwd, env: opts.env, stdio: ['inherit', 'pipe', 'pipe'] });
let output = '';
child.stdout?.on('data', (b) => { const s = b.toString(); output += s; to.write(s); });
child.stderr?.on('data', (b) => { const s = b.toString(); output += s; process.stderr.write(s); });
child.on('error', (err) => resolve({ code: -1, output: `${output}\n${err.message}` }));
child.on('close', (code) => resolve({ code: code ?? -1, output }));
});
}
export const defaultBuildRunner = teeRunner(process.stdout);
export const stderrBuildRunner = teeRunner(process.stderr);
// buildkit prints "pushing manifest for registry.fly.io/<app>:<label>@sha256:<digest>" on push.

@@ -61,6 +65,9 @@ // Pin to the digest — the bare tag races on Fly's registry (MANIFEST_UNKNOWN); the digest always resolves.

// Best-effort: ensure the fly CLI is available (needed for source-directory deploys). Never blocks —
// if it can't be installed the subsequent build surfaces a clear error.
// if it can't be installed the subsequent build surfaces a clear error. Everything here is one-time
// bootstrap DIAGNOSTICS, so it all goes to stderr — including the installers' own stdout (fd 2 in
// the stdio triple) — keeping stdout clean for `deploy --json`.
export async function ensureFlyctl() {
const note = (m) => process.stderr.write(m + '\n');
const ok = (cmd, args, inherit = false) => new Promise((resolve) => {
const p = spawn(cmd, args, { stdio: inherit ? 'inherit' : 'ignore' });
const p = spawn(cmd, args, { stdio: inherit ? ['inherit', 2, 2] : 'ignore' });
p.on('error', () => resolve(false));

@@ -73,4 +80,4 @@ p.on('close', (code) => resolve(code === 0));

if (process.platform === 'darwin' && (await ok('brew', ['--version']))) {
info('flyctl not found — installing with `brew install flyctl` (one-time)…');
info((await ok('brew', ['install', 'flyctl'], true)) ? 'flyctl installed ✓' : 'flyctl install failed — install manually: https://fly.io/docs/flyctl/install/');
note('flyctl not found — installing with `brew install flyctl` (one-time)…');
note((await ok('brew', ['install', 'flyctl'], true)) ? 'flyctl installed ✓' : 'flyctl install failed — install manually: https://fly.io/docs/flyctl/install/');
return;

@@ -83,3 +90,3 @@ }

// own PATH because the installer's shell-profile edit can't reach an already-running process.
info('flyctl not found — installing to ~/.fly (one-time)…');
note('flyctl not found — installing to ~/.fly (one-time)…');
const flyHome = `${process.env.HOME ?? '~'}/.fly`;

@@ -90,10 +97,10 @@ const installed = await ok('sh', ['-c', `curl -fsSL https://fly.io/install.sh | FLYCTL_INSTALL="${flyHome}" sh`], true);

if (await ok('flyctl', ['version'])) {
info('flyctl installed ✓');
note('flyctl installed ✓');
return;
}
}
info('flyctl install failed — install manually: https://fly.io/docs/flyctl/install/');
note('flyctl install failed — install manually: https://fly.io/docs/flyctl/install/');
return;
}
info('flyctl (fly CLI) not found — install it to deploy from source: https://fly.io/docs/flyctl/install/');
note('flyctl (fly CLI) not found — install it to deploy from source: https://fly.io/docs/flyctl/install/');
}

@@ -100,0 +107,0 @@ catch { /* best-effort convenience */ }

@@ -77,3 +77,3 @@ #!/usr/bin/env node

envCmd.command('use <name>').description(`Switch environment (${ENV_NAMES.join(' | ')}) — drops the stored session, which is deployment-specific`)
.action(guard((name) => envCmd_.envUse(name)));
.option('--json').action(guard((name, o) => envCmd_.envUse(name, o)));
// ---- run (per-request secret injection — nothing written to disk) ----

@@ -86,3 +86,3 @@ program.command('run <cmd> [args...]').description('Run a command with the branch credential bundle injected into its environment (no .env written)')

const setupCmd = program.command('setup').description('Set up this machine for InstaCloud agent workflows');
setupCmd.command('agent').description('Install the insta skill user-globally for all coding agents')
setupCmd.command('agent').description('Install the insta CLI (if missing), the insta skill for all coding agents, and the MCP server')
.option('-y, --yes', 'non-interactive')

@@ -100,17 +100,17 @@ .option('--mcp-token', 'register the MCP server with a minted insta_ API token instead of OAuth (headless machines / CI)')

orgCmd.command('list').option('--json').action(guard((o) => org.orgList(o)));
orgCmd.command('create <name>').action(guard((name) => org.orgCreate(name)));
orgCmd.command('create <name>').option('--json').action(guard((name, o) => org.orgCreate(name, o)));
// ---- project ----
const pj = program.command('project').description('Manage projects');
pj.command('create [name]').option('--org <id>', 'org to create under (default: personal)').action(guard((name, o) => project.projectCreate(name, o)));
pj.command('create [name]').option('--org <id>', 'org to create under (default: personal)').option('--json').action(guard((name, o) => project.projectCreate(name, o)));
pj.command('list').option('--org <id>').option('--json').action(guard((o) => project.projectList(o)));
pj.command('link <id>').description('Link a project to this directory').action(guard((id) => project.projectLink(id)));
pj.command('delete').option('--project <id>').action(guard((o) => project.projectDelete(o)));
pj.command('link <id>').description('Link a project to this directory').option('--json').action(guard((id, o) => project.projectLink(id, o)));
pj.command('delete').option('--project <id>').option('--json').action(guard((o) => project.projectDelete(o)));
// ---- branch ----
const br = program.command('branch').description('Manage branch environments');
br.command('create <name>').option('--from <branch>', 'parent branch (default: current)').action(guard((name, o) => branch.branchCreate(name, o)));
br.command('create <name>').option('--from <branch>', 'parent branch (default: current)').option('--json').action(guard((name, o) => branch.branchCreate(name, o)));
br.command('list').option('--json').action(guard((o) => branch.branchList(o)));
br.command('switch <name>').action(guard((name) => branch.branchSwitch(name)));
br.command('delete <name>').action(guard((name) => branch.branchDelete(name)));
br.command('switch <name>').option('--json').action(guard((name, o) => branch.branchSwitch(name, o)));
br.command('delete <name>').option('--json').action(guard((name, o) => branch.branchDelete(name, o)));
br.command('merge <source>').description('Merge a branch service set into another (structural, no data)')
.option('--into <branch>', 'target branch (default: current)').action(guard((source, o) => branch.branchMerge(source, o)));
.option('--into <branch>', 'target branch (default: current)').option('--json').action(guard((source, o) => branch.branchMerge(source, o)));
// ---- services (opt-in postgres/storage/compute/redis/mysql/mongodb) ----

@@ -137,3 +137,3 @@ const svc = program.command('services').alias('svc').description('Manage project services (postgres|storage|compute|redis|mysql|mongodb)');

svc.command('remove <type> <name>').description('Remove a service and destroy its resources')
.option('--branch <branch>', 'branch (default: current)')
.option('--branch <branch>', 'branch (default: current)').option('--json')
.action(guard((type, name, o) => services.servicesRemove(type, name, o)));

@@ -158,5 +158,5 @@ svc.command('rename <type> <name> <new-name>').description('Rename a service and re-key its managed secret names')

.option('--branch <branch>', 'scope to one branch').option('--service <type/name>', 'bind to a branch service (implies current branch)')
.action(guard((n, v, o) => secretsCmd.secretsSet(n, v, o)));
.option('--json').action(guard((n, v, o) => secretsCmd.secretsSet(n, v, o)));
sec.command('unset <name>').description('Remove a user secret')
.option('--branch <branch>', 'scope to one branch').action(guard((n, o) => secretsCmd.secretsUnset(n, o)));
.option('--branch <branch>', 'scope to one branch').option('--json').action(guard((n, o) => secretsCmd.secretsUnset(n, o)));
sec.command('bind <env-name> <source>').description('Bind a service credential into a compute env var')

@@ -194,2 +194,3 @@ .option('--branch <branch>', 'branch (default: current)')

.option('--websocket', 'run a WebSocket app (larger guest + connection-based concurrency)')
.option('--json', 'print the deploy result as JSON (build progress goes to stderr)')
.action(guard((dir, o) => deploy(dir, o)));

@@ -207,3 +208,3 @@ // `insta compute exec` needs the command verbatim after a literal `--`; split it out of argv here,

compute.command('remove-domain <host>').description('Detach a custom domain (gated: deploy)')
.option('--branch <b>').option('--group <g>').action(guard((host, o) => computeCmd.removeDomain(host, o)));
.option('--branch <b>').option('--group <g>').option('--json').action(guard((host, o) => computeCmd.removeDomain(host, o)));
compute.command('start [service]').description('Bring a compute service online (persistent — re-enables auto-wake)')

@@ -274,2 +275,5 @@ .option('--json').option('--branch <branch>', 'branch (default: current)').action(guard((service, o) => computeCmd.computeStart(service, o)));

.option('--branch <b>').option('--limit <n>').option('--region <r>').option('--instance <i>').option('--deploy', 'show deploy events (machine lifecycle) instead of runtime logs — Fly-backed targets only, not db').option('--json')
.option('--from <t>', 'window start: unix seconds or ISO-8601 — pages history (~7-day retention); without a window one recent provider page (~100 lines) is returned')
.option('--to <t>', 'window end: unix seconds or ISO-8601 (default: now)')
.option('--since <dur>', 'relative window start, e.g. 90s, 30m, 2h, 1d (shorthand for --from now-dur)')
.action(guard((target, group, o) => obs.logs(target, group, o)));

@@ -293,4 +297,4 @@ program.command('usage').description('Usage for the current billing cycle by billing dimension (org by default; --proj for one project)')

ap.command('list').option('--status <s>', 'pending|granted|denied|consumed').option('--json').action(guard((o) => govern.approvalsList(o)));
ap.command('approve <id>').option('--always', 'also set the policy to allow').action(guard((id, o) => govern.approvalsApprove(id, o)));
ap.command('deny <id>').action(guard((id) => govern.approvalsDeny(id)));
ap.command('approve <id>').option('--always', 'also set the policy to allow').option('--json').action(guard((id, o) => govern.approvalsApprove(id, o)));
ap.command('deny <id>').option('--json').action(guard((id, o) => govern.approvalsDeny(id, o)));
// ---- observe (local credential audit) ----

@@ -305,3 +309,3 @@ const ob = program.command('observe').description('Local credential-audit hook');

pol.command('get').option('--json').action(guard((o) => govern.policyGet(o)));
pol.command('set <action> <decision>').description('action: secrets.read|secrets.write|deploy|project.delete|branch.delete|service.add|service.remove|service.scale|service.upgrade|service.setAccess|storage.read|storage.write|storage.delete; decision: allow|deny|approve').action(guard((a, d) => govern.policySet(a, d)));
pol.command('set <action> <decision>').description('action: secrets.read|secrets.write|deploy|project.delete|branch.delete|service.add|service.remove|service.scale|service.upgrade|service.setAccess|storage.read|storage.write|storage.delete; decision: allow|deny|approve').option('--json').action(guard((a, d, o) => govern.policySet(a, d, o)));
// ---- feedback (agent + human hurdle reports → the InstaCloud team) ----

@@ -308,0 +312,0 @@ program.command('feedback')

@@ -29,6 +29,13 @@ // Output + small pure helpers (env serialization is unit-tested).

// If the platform gated the action (HTTP 202), tell the user how to get it approved. Returns
// true when an approval is pending (caller should stop).
export function handleApproval(res) {
// true when an approval is pending (caller should stop). The hint goes to STDERR and the exit
// code is set to 2: a pending gate is not success (redirected stdout must never swallow it as
// output), and not a plain error either (die() owns 1) — it's approvable and re-runnable, and
// scripts/agents branch on the distinct code. With json, stdout carries the platform's raw 202
// envelope so a scripted caller can lift approvalId/action.
export function handleApproval(res, json) {
if (res.status === 202 && res.body?.status === 'approval_required') {
info(`approval required for ${res.body.action} — run: insta approvals approve ${res.body.approvalId}`);
if (json)
printJson(res.body);
process.stderr.write(`approval required for ${res.body.action} — run: insta approvals approve ${res.body.approvalId}\n`);
process.exitCode = 2;
return true;

@@ -35,0 +42,0 @@ }

{
"name": "insta",
"version": "0.0.36",
"version": "0.0.37",
"type": "module",

@@ -5,0 +5,0 @@ "description": "InstaCloud CLI — a thin client of the platform control-plane API.",

@@ -29,11 +29,22 @@ # insta-cli

For coding agents. Installs the CLI, the `insta` skill for every agent on the machine, and
registers the MCP server:
registers the MCP server — one command for macOS, Linux, WSL, and native Windows shells
(PowerShell/cmd). Needs Node 18+ with a writable npm global prefix (a Node version manager
qualifies; if the global install can't write, setup continues and prints the exact
version-pinned `npm install -g` fallback to run yourself). E2e-validated on macOS/Linux; the
Windows spawn paths are unit-tested:
```bash
npx -y insta setup agent
```
On macOS/Linux without Node, the native-binary installer puts the `insta` CLI on PATH (the
skill + MCP steps it then runs still need Node — the skills tool runs via npx). Never run it
on native Windows — PowerShell's `curl` alias and the WSL `bash` shim break it; use npx
above, or download `insta-windows-x64.exe` from the
[releases page](https://github.com/InsForge/insta-cli/releases):
```bash
curl -fsSL agents.instacloud.com | sh
```
On Windows, download `insta-windows-x64.exe` from the
[releases page](https://github.com/InsForge/insta-cli/releases).
Pin a version with `INSTA_VERSION=v0.0.22`; change the install directory with

@@ -105,3 +116,4 @@ `INSTA_INSTALL_DIR`. While the CLI is pre-1.0 it updates itself on new releases. Turn that

agent` installs the InstaCloud skill and registers the remote MCP server for the coding
agents on the machine.
agents on the machine — and, when running from the npx cache with no durable `insta` on
PATH, first installs the CLI itself globally.

@@ -172,3 +184,3 @@ ## Environments

| `insta env` | `show` · `use <prod\|staging>` |
| `insta setup` | `agent` — install the skill and register MCP for every coding agent |
| `insta setup` | `agent` — install the CLI (if missing), the skill, and MCP for every coding agent |
| `insta mcp` | `install` — register the remote MCP server only |

@@ -175,0 +187,0 @@ | `insta org` | `list` · `create` (one free org per user) |