insta 0.0.36 → 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 CHANGED
@@ -26,14 +26,25 @@ npm install -g insta
26
26
  ```
27
27
 
28
28
  For coding agents. Installs the CLI, the `insta` skill for every agent on the machine, and
29
- registers the MCP server:
29
+ registers the MCP server — one command for macOS, Linux, WSL, and native Windows shells
30
+ (PowerShell/cmd). Needs Node 18+ with a writable npm global prefix (a Node version manager
31
+ qualifies; if the global install can't write, setup continues and prints the exact
32
+ version-pinned `npm install -g` fallback to run yourself). E2e-validated on macOS/Linux; the
33
+ Windows spawn paths are unit-tested:
30
34
 
31
35
  ```bash
32
- curl -fsSL agents.instacloud.com | sh
36
+ npx -y insta setup agent
33
37
  ```
34
38
 
35
- On Windows, download `insta-windows-x64.exe` from the
36
- [releases page](https://github.com/InsForge/insta-cli/releases).
39
+ On macOS/Linux without Node, the native-binary installer puts the `insta` CLI on PATH (the
40
+ skill + MCP steps it then runs still need Node — the skills tool runs via npx). Never run it
41
+ on native Windows — PowerShell's `curl` alias and the WSL `bash` shim break it; use npx
42
+ above, or download `insta-windows-x64.exe` from the
43
+ [releases page](https://github.com/InsForge/insta-cli/releases):
44
+
45
+ ```bash
46
+ curl -fsSL agents.instacloud.com | sh
47
+ ```
37
48
 
38
49
  Pin a version with `INSTA_VERSION=v0.0.22`; change the install directory with
39
50
  `INSTA_INSTALL_DIR`. While the CLI is pre-1.0 it updates itself on new releases. Turn that
@@ -101,7 +112,8 @@ prints an approval id for an admin to grant with `insta approvals approve <id>`.
101
112
 
102
113
  `insta manifest` prints an agent-legible view of every branch and its URLs. `insta setup
103
114
  agent` installs the InstaCloud skill and registers the remote MCP server for the coding
104
- agents on the machine.
115
+ agents on the machine — and, when running from the npx cache with no durable `insta` on
116
+ PATH, first installs the CLI itself globally.
105
117
 
106
118
  ## Environments
107
119
 
@@ -167,7 +179,7 @@ build never reaches a production installer.
167
179
  |---|---|
168
180
  | `insta login` · `logout` · `status` | Email/password or `--oauth github\|google`; `status` shows the environment, login and linked project/branch |
169
181
  | `insta env` | `show` · `use <prod\|staging>` |
170
- | `insta setup` | `agent` — install the skill and register MCP for every coding agent |
182
+ | `insta setup` | `agent` — install the CLI (if missing), the skill, and MCP for every coding agent |
171
183
  | `insta mcp` | `install` — register the remote MCP server only |
172
184
  | `insta org` | `list` · `create` (one free org per user) |
173
185
  | `insta project` | `create` · `list` · `link` · `delete` |
package/dist/api.js CHANGED
@@ -2,7 +2,7 @@
2
2
  // 2xx (including 202 approval_required) returns the parsed body; >=400 throws ApiError.
3
3
  import { readGlobal, writeGlobal, readProject, writeProject } from './config.js';
4
4
  import { autoResolveProject, promptChoice } from './resolve-project.js';
5
- import { die, info } from './util.js';
5
+ import { die } from './util.js';
6
6
  export class ApiError extends Error {
7
7
  status;
8
8
  constructor(status, msg) {
@@ -117,7 +117,9 @@ export async function requireProject() {
117
117
  promptChoice,
118
118
  save: async (c) => {
119
119
  await writeProject(c);
120
- info(`auto-linked project ${c.projectId} → ./.insta/project.json`);
120
+ // stderr: this is a diagnostic that can precede ANY command's output — under --json,
121
+ // stdout must stay one parseable document.
122
+ process.stderr.write(`auto-linked project ${c.projectId} → ./.insta/project.json\n`);
121
123
  },
122
124
  tty: !!process.stdin.isTTY && !!process.stderr.isTTY,
123
125
  });
@@ -5,6 +5,8 @@ export async function branchCreate(name, opts) {
5
5
  const api = await ApiClient.load();
6
6
  const p = await requireProject();
7
7
  const out = await api.request('POST', `/projects/${p.projectId}/branches`, { name, from: opts.from ?? p.branch });
8
+ if (opts.json)
9
+ return printJson(out);
8
10
  info(`created branch ${out.branch.name} (${out.branch.id})`);
9
11
  renderNextActions(out.nextActions);
10
12
  }
@@ -17,16 +19,18 @@ export async function branchList(opts) {
17
19
  for (const b of branches)
18
20
  info(`${b.is_default ? '*' : ' '} ${b.name} [${b.status}] ${b.id}`);
19
21
  }
20
- export async function branchSwitch(name) {
22
+ export async function branchSwitch(name, opts = {}) {
21
23
  const api = await ApiClient.load();
22
24
  const p = await requireProject();
23
25
  const { branches } = await api.request('GET', `/projects/${p.projectId}/branches`);
24
26
  if (!branches.some((b) => b.name === name))
25
27
  die(`branch not found: ${name}`);
26
28
  await writeProject({ ...p, branch: name });
29
+ if (opts.json)
30
+ return printJson({ projectId: p.projectId, branch: name });
27
31
  info(`switched to branch ${name} — run \`insta secrets\` to refresh .env`);
28
32
  }
29
- export async function branchDelete(name) {
33
+ export async function branchDelete(name, opts = {}) {
30
34
  const api = await ApiClient.load();
31
35
  const p = await requireProject();
32
36
  const { branches } = await api.request('GET', `/projects/${p.projectId}/branches`);
@@ -34,8 +38,10 @@ export async function branchDelete(name) {
34
38
  if (!b)
35
39
  die(`branch not found: ${name}`);
36
40
  const res = await api.rawRequest('DELETE', `/projects/${p.projectId}/branches/${b.id}`);
37
- if (handleApproval(res))
41
+ if (handleApproval(res, opts.json))
38
42
  return;
43
+ if (opts.json)
44
+ return printJson({ ok: true, branch: { id: b.id, name: b.name } });
39
45
  info(`deleted branch ${name}`);
40
46
  }
41
47
  // insta branch merge <source> [--into <target>] — structurally merge source's services into target
@@ -47,8 +53,10 @@ export async function branchMerge(source, opts = {}) {
47
53
  if (!target)
48
54
  throw new Error('no target branch — pass --into <branch> (or link a branch first)');
49
55
  const res = await api.rawRequest('POST', `/projects/${p.projectId}/branches/${encodeURIComponent(target)}/merge`, { from: source });
50
- if (handleApproval(res))
56
+ if (handleApproval(res, opts.json))
51
57
  return;
58
+ if (opts.json)
59
+ return printJson(res.body ?? {});
52
60
  const { created = [], skipped = [] } = (res.body ?? {});
53
61
  info(`merged ${source} → ${target}: ${created.length} created, ${skipped.length} skipped`);
54
62
  for (const c of created)
@@ -7,7 +7,7 @@ export async function setDomain(host, opts) {
7
7
  const api = await ApiClient.load();
8
8
  const p = await requireProject();
9
9
  const res = await api.rawRequest('POST', `/projects/${p.projectId}/compute/domain`, { hostname: host, branch: opts.branch ?? p.branch, group: opts.group });
10
- if (handleApproval(res))
10
+ if (handleApproval(res, opts.json))
11
11
  return;
12
12
  printDomain(res.body, opts.json);
13
13
  }
@@ -26,9 +26,16 @@ export async function removeDomain(host, opts) {
26
26
  const api = await ApiClient.load();
27
27
  const p = await requireProject();
28
28
  const res = await api.rawRequest('DELETE', `/projects/${p.projectId}/compute/domain`, { hostname: host, branch: opts.branch ?? p.branch, group: opts.group });
29
- if (handleApproval(res))
29
+ if (handleApproval(res, opts.json))
30
30
  return;
31
- info(`removed custom domain ${res.body.hostname} from ${res.body.flyApp}`);
31
+ renderRemoveDomain(res.body, opts.json);
32
+ }
33
+ // Split out (same pattern as applyExecResult) so the --json contract — stdout carries the platform
34
+ // response, never prose — is unit-testable without a network mock.
35
+ export function renderRemoveDomain(body, json) {
36
+ if (json)
37
+ return printJson(body);
38
+ info(`removed custom domain ${body.hostname} from ${body.flyApp}`);
32
39
  }
33
40
  function printDomain(r, json) {
34
41
  if (json)
@@ -50,7 +57,7 @@ async function lifecycle(verb, serviceName, opts) {
50
57
  const { services } = await api.request('GET', `/projects/${p.projectId}/services${q(branch)}`);
51
58
  const id = resolveComputeServiceId(services, serviceName);
52
59
  const res = await api.rawRequest('POST', `/projects/${p.projectId}/services/${id}/${verb}`);
53
- if (handleApproval(res))
60
+ if (handleApproval(res, opts.json))
54
61
  return;
55
62
  if (opts.json)
56
63
  return printJson(res.body);
@@ -107,19 +114,13 @@ export function execRequestBody(command, timeoutSec) {
107
114
  // of (res, json) so it's unit-testable without a network mock, same as handleApproval's own
108
115
  // {status, body} shape.
109
116
  //
110
- // A 202 means the command has NOT run: unlike every other gated command (where "nothing happened"
111
- // is the safe default), a caller chaining `insta compute exec … && next` must not see exit 0 here,
112
- // or `next` runs believing the command succeeded. --json prints the raw envelope (so a scripted
113
- // caller can inspect approvalId/action) instead of the human hint; either way exit 1.
117
+ // A 202 means the command has NOT run: handleApproval owns the whole contract (hint on stderr,
118
+ // raw envelope on stdout with --json, exit 2), so a caller chaining `insta compute exec … && next`
119
+ // can never mistake a pending gate for the command having succeeded — and exit 2 stays
120
+ // distinguishable from the remote command's own exit 1.
114
121
  export function applyExecResult(res, json) {
115
- if (res.status === 202 && res.body?.status === 'approval_required') {
116
- if (json)
117
- printJson(res.body);
118
- else
119
- handleApproval(res);
120
- process.exitCode = 1;
122
+ if (handleApproval(res, json))
121
123
  return;
122
- }
123
124
  const { exitCode, stdout, stderr, truncated } = res.body;
124
125
  if (json) {
125
126
  printJson(res.body);
@@ -170,7 +171,7 @@ export async function computeAlwaysOn(mode, serviceName, opts) {
170
171
  const { services } = await api.request('GET', `/projects/${p.projectId}/services${q(branch)}`);
171
172
  const id = resolveComputeServiceId(services, serviceName);
172
173
  const res = await api.rawRequest('PUT', `/projects/${p.projectId}/services/${id}/always-on`, { enabled: mode === 'on' });
173
- if (handleApproval(res))
174
+ if (handleApproval(res, opts.json))
174
175
  return;
175
176
  if (opts.json)
176
177
  return printJson(res.body);
@@ -271,7 +272,7 @@ export async function computeVolume(serviceName, opts) {
271
272
  catch (e) {
272
273
  throw volumeDeleteError(e);
273
274
  }
274
- if (handleApproval(res))
275
+ if (handleApproval(res, opts.json))
275
276
  return;
276
277
  if (opts.json)
277
278
  return printJson(res.body);
@@ -288,7 +289,7 @@ export async function computeVolume(serviceName, opts) {
288
289
  }
289
290
  const sizeGib = parseVolumeGib(opts.size);
290
291
  const res = await api.rawRequest('PUT', `/projects/${p.projectId}/services/${id}/volume`, { sizeGib });
291
- if (handleApproval(res))
292
+ if (handleApproval(res, opts.json))
292
293
  return;
293
294
  if (opts.json)
294
295
  return printJson(res.body);
@@ -317,7 +318,7 @@ export async function computeLimits(serviceName, opts) {
317
318
  if (opts.cpu)
318
319
  body.cpu = parseCpu(opts.cpu);
319
320
  const res = await api.rawRequest('PUT', `/projects/${p.projectId}/services/${id}/limits`, body);
320
- if (handleApproval(res))
321
+ if (handleApproval(res, opts.json))
321
322
  return;
322
323
  if (opts.json)
323
324
  return printJson(res.body);
@@ -19,7 +19,7 @@ export async function dbAlwaysOn(mode, opts) {
19
19
  if (opts.group)
20
20
  qs.set('group', opts.group);
21
21
  const res = await api.rawRequest('PATCH', `/projects/${p.projectId}/database/settings${qs.toString() ? `?${qs}` : ''}`, { scaleToZero: mode !== 'on' });
22
- if (handleApproval(res))
22
+ if (handleApproval(res, opts.json))
23
23
  return;
24
24
  if (opts.json)
25
25
  return printJson(res.body);
@@ -111,7 +111,7 @@ export async function dbLimits(opts) {
111
111
  throw new Error(`setting the ceiling failed (${e.status}): ${e.message}`);
112
112
  throw e;
113
113
  }
114
- if (handleApproval(res))
114
+ if (handleApproval(res, opts.json))
115
115
  return;
116
116
  if (opts.json)
117
117
  return printJson(res.body);
@@ -230,7 +230,7 @@ export async function dbVolume(opts) {
230
230
  throw new Error(`growing the volume failed (${e.status}): ${e.message}`);
231
231
  throw e;
232
232
  }
233
- if (handleApproval(res))
233
+ if (handleApproval(res, opts.json))
234
234
  return;
235
235
  if (opts.json)
236
236
  return printJson(res.body);
@@ -1,8 +1,11 @@
1
1
  import { resolve, join } from 'node:path';
2
2
  import { existsSync, readFileSync } from 'node:fs';
3
3
  import { ApiClient, ApiError, requireProject } from '../api.js';
4
- import { info, die, handleApproval, renderNextActions } from '../util.js';
5
- import { flyctlBuildAndPush, ensureFlyctl, defaultBuildRunner } from '../flyctl-build.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,20 +39,23 @@ 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
- info(`using port ${exposed} (Dockerfile EXPOSE) — override with --port`);
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
  }
@@ -71,10 +77,11 @@ export async function dockerBuildLocal(absDir, tag, run = defaultBuildRunner) {
71
77
  // Dockerfile) with flyctl's remote builder, returning the pushed image ref to deploy. Against a
72
78
  // local daemon (insta-oss) the token mint answers 501 — build with docker instead, same contract.
73
79
  // Exported with injectable pieces for tests (the repo's DI pattern; no global mocks).
74
- export async function buildFromSource(api, projectId, dir, branch, opts, run = defaultBuildRunner) {
80
+ export async function buildFromSource(api, projectId, dir, branch, opts, run = opts.json ? stderrBuildRunner : defaultBuildRunner) {
75
81
  const absDir = resolve(process.cwd(), dir);
76
82
  if (!existsSync(join(absDir, 'Dockerfile')))
77
83
  die(`no Dockerfile at ${join(absDir, 'Dockerfile')} — add one, or use --image <url>`);
84
+ const log = note(opts);
78
85
  let tok;
79
86
  try {
80
87
  tok = await api.rawRequest('POST', `/projects/${projectId}/deploy-token`, { branch, group: opts.group });
@@ -85,19 +92,20 @@ export async function buildFromSource(api, projectId, dir, branch, opts, run = d
85
92
  if (!(e instanceof ApiError) || e.status !== 501)
86
93
  throw e;
87
94
  const tag = localImageTag(projectId, opts.group);
88
- info(`no remote builder on this daemon — building ${dir} locally with docker…`);
95
+ log(`no remote builder on this daemon — building ${dir} locally with docker…`);
89
96
  const built = await dockerBuildLocal(absDir, tag, run);
90
- info(` built ${built}`);
97
+ log(` built ${built}`);
91
98
  return built;
92
99
  }
93
- if (handleApproval(tok))
94
- die('deploy requires approval — get it approved, then re-run');
100
+ // exit() with no argument honors the exit code handleApproval just set (2).
101
+ if (handleApproval(tok, opts.json))
102
+ process.exit();
95
103
  const { token, flyApp } = tok.body;
96
104
  await ensureFlyctl(); // cloud path only — the local path needs docker, which the daemon requires anyway
97
105
  const port = opts.port ? Number(opts.port) : 8080;
98
- info(`building ${dir} for ${flyApp} (remote builder)…`);
106
+ log(`building ${dir} for ${flyApp} (remote builder)…`);
99
107
  const { imageRef } = await flyctlBuildAndPush({ dir: absDir, flyApp, imageLabel: `insta-${Date.now()}`, token, port }, run);
100
- info(` pushed ${imageRef}`);
108
+ log(` pushed ${imageRef}`);
101
109
  return imageRef;
102
110
  }
103
111
  //# sourceMappingURL=deploy.js.map
@@ -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
- export async function envUse(name) {
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)}\`)`);
@@ -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
@@ -140,8 +140,42 @@ export function deployEventLine(ev) {
140
140
  const inst = ev.instance ? ` (${ev.instance})` : '';
141
141
  return `${ev.ts ?? ''} [${ev.origin ?? ''}] ${ev.type ?? ''}: ${ev.status ?? ''}${inst}`;
142
142
  }
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
+ }
143
173
  // insta logs <db|compute|redis|mysql|mongodb> [group]
144
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);
145
179
  const api = await ApiClient.load();
146
180
  const p = await requireProject();
147
181
  if (opts.deploy) {
@@ -159,7 +193,7 @@ export async function logs(component, group, opts) {
159
193
  info(deployEventLine(ev));
160
194
  return;
161
195
  }
162
- 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 })}`);
163
197
  if (opts.json)
164
198
  return printJson(res);
165
199
  if (res.note)
@@ -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
@@ -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
- function tryInstallObserve() {
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
- info(' installed observe hook (credential audit) → ./.insta/observe');
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
- // Guide instead (no hang, no error): name it explicitly, or just ask the skill-equipped agent.
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
- info(`created project ${out.project.id} (${resolved})`);
64
- info(` resources: ${out.resources.map((r) => r.kind).join(', ')}`);
65
- info(` linked ./.insta/project.json (branch ${out.defaultBranch.name})`);
66
- renderNextActions(out.nextActions);
67
- tryInstallObserve();
68
- await installSkills({ cwd: process.cwd() });
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
- info(`linked project ${project.id} (${project.name})`);
86
- tryInstallObserve();
87
- await installSkills({ cwd: process.cwd() });
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
@@ -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, info } from '../util.js';
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
- if (res.status === 202) {
32
- die(`secrets.read requires approval — run: insta approvals approve ${res.body.approvalId}, then re-run`);
33
- }
34
- info(`running with ${Object.keys(res.body.secrets).length} injected secrets (branch ${branch}) — nothing written to disk`);
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
  });