insta 0.0.36 → 0.0.38
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 +26 -6
- package/dist/api.js +4 -2
- package/dist/commands/branch.js +12 -4
- package/dist/commands/compute.js +20 -19
- package/dist/commands/db.js +3 -3
- package/dist/commands/deploy.js +19 -11
- package/dist/commands/env.js +21 -4
- package/dist/commands/feedback.js +16 -3
- package/dist/commands/govern.js +9 -3
- package/dist/commands/metrics.js +35 -1
- 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 +13 -9
- package/dist/commands/services.js +9 -7
- package/dist/commands/setup.js +215 -5
- 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 +22 -17
- package/dist/util.js +10 -3
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -26,14 +26,33 @@ 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,
|
|
33
|
+
and Windows (PowerShell + cmd):
|
|
30
34
|
|
|
31
35
|
```bash
|
|
32
|
-
|
|
36
|
+
npx -y insta setup agent
|
|
37
|
+
```
|
|
38
|
+
|
|
39
|
+
This command means **production** (CLI ≥ 0.0.38): if the machine was previously switched to
|
|
40
|
+
staging it switches back — announced, session dropped, like `insta env use prod`. Staging is
|
|
41
|
+
its own explicit command, which also persists the choice:
|
|
42
|
+
|
|
43
|
+
```bash
|
|
44
|
+
npx -y insta setup agent --env staging
|
|
33
45
|
```
|
|
34
46
|
|
|
35
|
-
On
|
|
36
|
-
|
|
47
|
+
On macOS/Linux without Node, the native-binary installer puts the `insta` CLI on PATH (the
|
|
48
|
+
skill + MCP steps it then runs still need Node — the skills tool runs via npx). Never run it
|
|
49
|
+
on native Windows — PowerShell's `curl` alias and the WSL `bash` shim break it; use npx
|
|
50
|
+
above, or download `insta-windows-x64.exe` from the
|
|
51
|
+
[releases page](https://github.com/InsForge/insta-cli/releases):
|
|
52
|
+
|
|
53
|
+
```bash
|
|
54
|
+
curl -fsSL agents.instacloud.com | sh
|
|
55
|
+
```
|
|
37
56
|
|
|
38
57
|
Pin a version with `INSTA_VERSION=v0.0.22`; change the install directory with
|
|
39
58
|
`INSTA_INSTALL_DIR`. While the CLI is pre-1.0 it updates itself on new releases. Turn that
|
|
@@ -101,7 +120,8 @@ prints an approval id for an admin to grant with `insta approvals approve <id>`.
|
|
|
101
120
|
|
|
102
121
|
`insta manifest` prints an agent-legible view of every branch and its URLs. `insta setup
|
|
103
122
|
agent` installs the InstaCloud skill and registers the remote MCP server for the coding
|
|
104
|
-
agents on the machine
|
|
123
|
+
agents on the machine — and, when running from the npx cache with no durable `insta` on
|
|
124
|
+
PATH, first installs the CLI itself globally.
|
|
105
125
|
|
|
106
126
|
## Environments
|
|
107
127
|
|
|
@@ -167,7 +187,7 @@ build never reaches a production installer.
|
|
|
167
187
|
|---|---|
|
|
168
188
|
| `insta login` · `logout` · `status` | Email/password or `--oauth github\|google`; `status` shows the environment, login and linked project/branch |
|
|
169
189
|
| `insta env` | `show` · `use <prod\|staging>` |
|
|
170
|
-
| `insta setup` | `agent` — install the skill and
|
|
190
|
+
| `insta setup` | `agent` — install the CLI (if missing), the skill, and MCP for every coding agent; targets prod, `--env staging` for staging |
|
|
171
191
|
| `insta mcp` | `install` — register the remote MCP server only |
|
|
172
192
|
| `insta org` | `list` · `create` (one free org per user) |
|
|
173
193
|
| `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
|
|
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
|
-
|
|
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
|
});
|
package/dist/commands/branch.js
CHANGED
|
@@ -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)
|
package/dist/commands/compute.js
CHANGED
|
@@ -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
|
-
|
|
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:
|
|
111
|
-
//
|
|
112
|
-
//
|
|
113
|
-
//
|
|
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
|
|
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);
|
package/dist/commands/db.js
CHANGED
|
@@ -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);
|
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
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
|
-
|
|
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
|
-
|
|
95
|
+
log(`no remote builder on this daemon — building ${dir} locally with docker…`);
|
|
89
96
|
const built = await dockerBuildLocal(absDir, tag, run);
|
|
90
|
-
|
|
97
|
+
log(` built ${built}`);
|
|
91
98
|
return built;
|
|
92
99
|
}
|
|
93
|
-
|
|
94
|
-
|
|
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
|
-
|
|
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
|
-
|
|
108
|
+
log(` pushed ${imageRef}`);
|
|
101
109
|
return imageRef;
|
|
102
110
|
}
|
|
103
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)}\`)`);
|
|
@@ -56,8 +73,8 @@ export async function envUse(name) {
|
|
|
56
73
|
info(' previous session dropped (separate deployment) — run `insta login --oauth github`');
|
|
57
74
|
// Switching the CLI does NOT re-point already-installed agents: their MCP registration and skill
|
|
58
75
|
// files were written for the previous environment and are keyed by a different server name, so
|
|
59
|
-
// they keep talking to it until setup is re-run.
|
|
60
|
-
// `
|
|
61
|
-
info(
|
|
76
|
+
// they keep talking to it until setup is re-run. --env is REQUIRED in the hint: since 0.0.38 a
|
|
77
|
+
// bare `setup agent` forces prod, which would silently undo the switch the user just made.
|
|
78
|
+
info(` re-point this machine's agents at it with: insta setup agent --env ${target}`);
|
|
62
79
|
}
|
|
63
80
|
//# sourceMappingURL=env.js.map
|
|
@@ -37,7 +37,10 @@ export const LIMITS = {
|
|
|
37
37
|
const FEEDBACK_ENDPOINT = process.env.INSTA_FEEDBACK_URL ||
|
|
38
38
|
'https://feedback.instacloud.com/v1/feedback';
|
|
39
39
|
const FEEDBACK_INGEST_TOKEN = process.env.INSTA_FEEDBACK_TOKEN || 'insta-feedback-public-v1';
|
|
40
|
-
|
|
40
|
+
// 15s gives the backend's scale-to-zero cold start room to answer (the ingest service waits out
|
|
41
|
+
// the DB wake and persists, so a report can land after the old 10s deadline gave up on it).
|
|
42
|
+
// An expired deadline is reported as UNCONFIRMED, not failed — the report may well be stored.
|
|
43
|
+
const FEEDBACK_TIMEOUT_MS = 15_000;
|
|
41
44
|
const MAX_FILE_BYTES = 256 * 1024;
|
|
42
45
|
function resolveCliVersion() {
|
|
43
46
|
// Same resolution as index.ts: the standalone binary bakes INSTA_CLI_VERSION via --define;
|
|
@@ -178,8 +181,10 @@ export async function submit(payload, fetchImpl) {
|
|
|
178
181
|
});
|
|
179
182
|
}
|
|
180
183
|
catch (e) {
|
|
181
|
-
|
|
182
|
-
|
|
184
|
+
if (e instanceof Error && e.name === 'TimeoutError') {
|
|
185
|
+
return { status: 'unconfirmed', error: `no response after ${FEEDBACK_TIMEOUT_MS / 1000}s — the report may have been recorded anyway` };
|
|
186
|
+
}
|
|
187
|
+
return { status: 'error', error: `network error: ${e instanceof Error ? e.message : String(e)}` };
|
|
183
188
|
}
|
|
184
189
|
let body = {};
|
|
185
190
|
try {
|
|
@@ -211,6 +216,14 @@ export async function feedback(opts, deps = {}) {
|
|
|
211
216
|
return;
|
|
212
217
|
}
|
|
213
218
|
const result = await submit(payload, deps.fetchImpl ?? fetch);
|
|
219
|
+
if (result.status === 'unconfirmed') {
|
|
220
|
+
// NOT a failure claim: the request was still in flight at the deadline and the server
|
|
221
|
+
// finishes what it started, so saying "not submitted" here would be a false negative.
|
|
222
|
+
if (opts.json)
|
|
223
|
+
return printJson({ status: 'unconfirmed', error: result.error });
|
|
224
|
+
process.stderr.write(`warning: feedback receipt unconfirmed (${result.error}) — continue with your task, do not retry\n`);
|
|
225
|
+
return;
|
|
226
|
+
}
|
|
214
227
|
if (result.status === 'error') {
|
|
215
228
|
// Deliberate exit 0: an agent CANNOT fix a down/rate-limited backend, and feedback must never
|
|
216
229
|
// fail or distract from the task the user actually asked for. Do not retry.
|
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
|
@@ -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)
|
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
|
});
|