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 +18 -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 +18 -1
- 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 +164 -3
- package/dist/commands/storage.js +3 -3
- package/dist/ensure-skills.js +16 -4
- package/dist/flyctl-build.js +25 -18
- package/dist/index.js +21 -17
- package/dist/util.js +10 -3
- package/package.json +1 -1
package/dist/commands/secrets.js
CHANGED
|
@@ -12,7 +12,7 @@ export async function secrets(opts) {
|
|
|
12
12
|
const p = await requireProject();
|
|
13
13
|
const branch = opts.branch ?? p.branch;
|
|
14
14
|
const res = await api.rawRequest('GET', `/projects/${p.projectId}/secrets${q(branch)}`);
|
|
15
|
-
if (handleApproval(res))
|
|
15
|
+
if (handleApproval(res, opts.json))
|
|
16
16
|
return;
|
|
17
17
|
const bundle = res.body.secrets;
|
|
18
18
|
if (opts.json)
|
|
@@ -47,7 +47,7 @@ export async function secretsTree(opts) {
|
|
|
47
47
|
const api = await ApiClient.load();
|
|
48
48
|
const p = await requireProject();
|
|
49
49
|
const res = await api.rawRequest('GET', `/projects/${p.projectId}/secrets/tree`);
|
|
50
|
-
if (handleApproval(res))
|
|
50
|
+
if (handleApproval(res, opts.json))
|
|
51
51
|
return;
|
|
52
52
|
const tree = res.body;
|
|
53
53
|
if (opts.json)
|
|
@@ -68,7 +68,7 @@ export async function secretsList(opts) {
|
|
|
68
68
|
const p = await requireProject();
|
|
69
69
|
const branch = opts.branch ?? p.branch;
|
|
70
70
|
const res = await api.rawRequest('GET', `/projects/${p.projectId}/secrets/tree`);
|
|
71
|
-
if (handleApproval(res))
|
|
71
|
+
if (handleApproval(res, opts.json))
|
|
72
72
|
return;
|
|
73
73
|
const tree = res.body;
|
|
74
74
|
const b = tree.branches.find((x) => x.name === branch);
|
|
@@ -102,8 +102,10 @@ export async function secretsSet(name, value, opts) {
|
|
|
102
102
|
const branch = opts.service ? (opts.branch ?? p.branch) : opts.branch;
|
|
103
103
|
const payload = { value: v, ...(branch ? { branch } : {}), ...(opts.service ? { service: opts.service } : {}) };
|
|
104
104
|
const res = await api.rawRequest('PUT', `/projects/${p.projectId}/secrets/${encodeURIComponent(name)}`, payload);
|
|
105
|
-
if (handleApproval(res))
|
|
105
|
+
if (handleApproval(res, opts.json))
|
|
106
106
|
return;
|
|
107
|
+
if (opts.json)
|
|
108
|
+
return printJson({ ok: true, name, branch: branch ?? null, service: opts.service ?? null });
|
|
107
109
|
info(`set ${name}${opts.service ? ` → ${opts.service}` : ''} (${branch ? `branch ${branch}` : 'project-wide'})`);
|
|
108
110
|
}
|
|
109
111
|
export async function secretsUnset(name, opts) {
|
|
@@ -111,8 +113,10 @@ export async function secretsUnset(name, opts) {
|
|
|
111
113
|
const p = await requireProject();
|
|
112
114
|
const qs = opts.branch ? `?branch=${encodeURIComponent(opts.branch)}` : '';
|
|
113
115
|
const res = await api.rawRequest('DELETE', `/projects/${p.projectId}/secrets/${encodeURIComponent(name)}${qs}`);
|
|
114
|
-
if (handleApproval(res))
|
|
116
|
+
if (handleApproval(res, opts.json))
|
|
115
117
|
return;
|
|
118
|
+
if (opts.json)
|
|
119
|
+
return printJson({ ok: true, name, branch: opts.branch ?? null });
|
|
116
120
|
info(`unset ${name} (${opts.branch ? `branch ${opts.branch}` : 'project-wide'})`);
|
|
117
121
|
}
|
|
118
122
|
export async function secretsBind(envName, source, opts) {
|
|
@@ -127,7 +131,7 @@ export async function secretsBind(envName, source, opts) {
|
|
|
127
131
|
source,
|
|
128
132
|
...(opts.sourceName ? { sourceName: opts.sourceName } : {}),
|
|
129
133
|
});
|
|
130
|
-
if (handleApproval(res))
|
|
134
|
+
if (handleApproval(res, opts.json))
|
|
131
135
|
return;
|
|
132
136
|
if (opts.json)
|
|
133
137
|
return printJson({ ok: true });
|
|
@@ -140,7 +144,7 @@ export async function secretsUnbind(envName, opts) {
|
|
|
140
144
|
const p = await requireProject();
|
|
141
145
|
const branch = opts.branch ?? p.branch;
|
|
142
146
|
const res = await api.rawRequest('DELETE', `/projects/${p.projectId}/secret-bindings/${encodeURIComponent(envName)}?branch=${encodeURIComponent(branch)}&target=${encodeURIComponent(opts.from)}`);
|
|
143
|
-
if (handleApproval(res))
|
|
147
|
+
if (handleApproval(res, opts.json))
|
|
144
148
|
return;
|
|
145
149
|
if (opts.json)
|
|
146
150
|
return printJson({ ok: true });
|
|
@@ -153,7 +157,7 @@ export async function secretsBindings(opts) {
|
|
|
153
157
|
const p = await requireProject();
|
|
154
158
|
const branch = opts.branch ?? p.branch;
|
|
155
159
|
const res = await api.rawRequest('GET', `/projects/${p.projectId}/secret-bindings?branch=${encodeURIComponent(branch)}&target=${encodeURIComponent(opts.target)}`);
|
|
156
|
-
if (handleApproval(res))
|
|
160
|
+
if (handleApproval(res, opts.json))
|
|
157
161
|
return;
|
|
158
162
|
const bindings = res.body.bindings ?? [];
|
|
159
163
|
if (opts.json)
|
|
@@ -168,7 +172,7 @@ export async function secretsSources(opts) {
|
|
|
168
172
|
const p = await requireProject();
|
|
169
173
|
const branch = opts.branch ?? p.branch;
|
|
170
174
|
const res = await api.rawRequest('GET', `/projects/${p.projectId}/secret-sources?branch=${encodeURIComponent(branch)}`);
|
|
171
|
-
if (handleApproval(res))
|
|
175
|
+
if (handleApproval(res, opts.json))
|
|
172
176
|
return;
|
|
173
177
|
const sources = res.body.sources ?? [];
|
|
174
178
|
if (opts.json)
|
|
@@ -111,7 +111,7 @@ export async function servicesAdd(type, name, opts = {}) {
|
|
|
111
111
|
const p = await requireProject();
|
|
112
112
|
const branch = opts.branch ?? p.branch;
|
|
113
113
|
const res = await api.rawRequest('POST', `/projects/${p.projectId}/services`, servicesAddRequestBody(type, name, branch, opts));
|
|
114
|
-
if (handleApproval(res))
|
|
114
|
+
if (handleApproval(res, opts.json))
|
|
115
115
|
return;
|
|
116
116
|
if (opts.json)
|
|
117
117
|
return printJson(res.body.service);
|
|
@@ -151,8 +151,10 @@ export async function servicesRemove(type, name, opts = {}) {
|
|
|
151
151
|
const { services } = await api.request('GET', `/projects/${p.projectId}/services${q(branch)}`);
|
|
152
152
|
const id = resolveServiceId(services, type, name);
|
|
153
153
|
const res = await api.rawRequest('DELETE', `/projects/${p.projectId}/services/${id}`);
|
|
154
|
-
if (handleApproval(res))
|
|
154
|
+
if (handleApproval(res, opts.json))
|
|
155
155
|
return;
|
|
156
|
+
if (opts.json)
|
|
157
|
+
return printJson({ ok: true, removed: { id, type, name, branch: branch ?? null } });
|
|
156
158
|
info(`removed ${type} service ${name} from ${branch ?? 'default'}`);
|
|
157
159
|
}
|
|
158
160
|
export async function servicesRename(type, name, newName, opts = {}) {
|
|
@@ -164,7 +166,7 @@ export async function servicesRename(type, name, newName, opts = {}) {
|
|
|
164
166
|
const { services } = await api.request('GET', `/projects/${p.projectId}/services${q(branch)}`);
|
|
165
167
|
const id = resolveServiceId(services, type, name);
|
|
166
168
|
const res = await api.rawRequest('POST', `/projects/${p.projectId}/services/${id}/rename`, { name: newName });
|
|
167
|
-
if (handleApproval(res))
|
|
169
|
+
if (handleApproval(res, opts.json))
|
|
168
170
|
return;
|
|
169
171
|
if (opts.json)
|
|
170
172
|
return printJson(res.body.service);
|
|
@@ -187,7 +189,7 @@ export async function servicesSetAccess(type, name, access, _opts) {
|
|
|
187
189
|
const { services } = await api.request('GET', `/projects/${p.projectId}/services${q(p.branch)}`);
|
|
188
190
|
const id = resolveServiceId(services, type, name);
|
|
189
191
|
const res = await api.rawRequest('PUT', `/projects/${p.projectId}/services/${id}/access`, { public: isPublic });
|
|
190
|
-
if (handleApproval(res))
|
|
192
|
+
if (handleApproval(res, _opts.json))
|
|
191
193
|
return;
|
|
192
194
|
if (_opts.json)
|
|
193
195
|
return printJson(res.body.service);
|
|
@@ -202,7 +204,7 @@ export async function servicesScale(type, name, number, region, _opts) {
|
|
|
202
204
|
const { services } = await api.request('GET', `/projects/${p.projectId}/services${q(_opts.branch ?? p.branch)}`);
|
|
203
205
|
const id = resolveServiceId(services, type, name);
|
|
204
206
|
const res = await api.rawRequest('POST', `/projects/${p.projectId}/services/${id}/scale`, { machineCount, region });
|
|
205
|
-
if (handleApproval(res))
|
|
207
|
+
if (handleApproval(res, _opts.json))
|
|
206
208
|
return;
|
|
207
209
|
if (_opts.json)
|
|
208
210
|
return printJson(res.body.service);
|
|
@@ -216,7 +218,7 @@ export async function servicesUpgrade(type, name, spec, _opts) {
|
|
|
216
218
|
const { services } = await api.request('GET', `/projects/${p.projectId}/services${q(_opts.branch ?? p.branch)}`);
|
|
217
219
|
const id = resolveServiceId(services, type, name);
|
|
218
220
|
const res = await api.rawRequest('POST', `/projects/${p.projectId}/services/${id}/upgrade`, { spec });
|
|
219
|
-
if (handleApproval(res))
|
|
221
|
+
if (handleApproval(res, _opts.json))
|
|
220
222
|
return;
|
|
221
223
|
if (_opts.json)
|
|
222
224
|
return printJson(res.body.service);
|
|
@@ -230,7 +232,7 @@ export async function servicesSecrets(type, name, opts = {}) {
|
|
|
230
232
|
const { services } = await api.request('GET', `/projects/${p.projectId}/services${q(opts.branch ?? p.branch)}`);
|
|
231
233
|
const id = resolveServiceId(services, type, name);
|
|
232
234
|
const res = await api.rawRequest('GET', `/projects/${p.projectId}/services/${id}/secrets`);
|
|
233
|
-
if (handleApproval(res))
|
|
235
|
+
if (handleApproval(res, opts.json))
|
|
234
236
|
return;
|
|
235
237
|
const { secrets } = res.body;
|
|
236
238
|
if (opts.json)
|
package/dist/commands/setup.js
CHANGED
|
@@ -6,12 +6,15 @@
|
|
|
6
6
|
// Stack skills (tigris/better-auth) intentionally stay per-project: their presence in a
|
|
7
7
|
// project doubles as its stack manifest — that install happens on `project create|link`.
|
|
8
8
|
import { spawn } from 'node:child_process';
|
|
9
|
+
import { existsSync, readFileSync, statSync } from 'node:fs';
|
|
10
|
+
import { dirname, join } from 'node:path';
|
|
9
11
|
import os from 'node:os';
|
|
10
12
|
import { ApiClient } from '../api.js';
|
|
11
13
|
import { resolveEnv } from '../config.js';
|
|
12
14
|
import { DEFAULT_ENV, ENVS, mcpServerName } from '../env.js';
|
|
13
15
|
import { info } from '../util.js';
|
|
14
16
|
import { installAgentConfigs } from './mcp.js';
|
|
17
|
+
import { detectChannel } from './upgrade.js';
|
|
15
18
|
// The `skills` tool we shell out to prints a clack UI: a frame-by-frame clone spinner, an
|
|
16
19
|
// "Installing to all N agents" banner, a full N-line install-path box, and a third-party
|
|
17
20
|
// "Security Risk Assessment" that flags our OWN first-party skill as "Critical Risk". Streamed
|
|
@@ -76,13 +79,165 @@ export function summarizeInstall(output) {
|
|
|
76
79
|
: `${count} agent${count === 1 ? '' : 's'}`;
|
|
77
80
|
return `✓ Agent skills — ${list}`;
|
|
78
81
|
}
|
|
82
|
+
// ---- CLI self-install (makes `npx -y insta setup agent` a complete one-liner) ----
|
|
83
|
+
// Under npx the CLI runs from the npm cache and vanishes when the process exits — but the skill
|
|
84
|
+
// installed below tells every agent to run `insta …`, which then wouldn't exist. So when this
|
|
85
|
+
// process came from the npm channel and no DURABLE `insta` is on PATH, install ourselves
|
|
86
|
+
// globally first. The scan must ignore any PATH entry under a node_modules directory: npx
|
|
87
|
+
// prepends its cache's node_modules/.bin (where this very process's `insta` shim lives), while
|
|
88
|
+
// durable installs (npm -g bin, nvm/volta/fnm, the native binary's ~/.insta/bin) never sit
|
|
89
|
+
// under one.
|
|
90
|
+
// On POSIX a PATH hit only counts if it would actually run: a plain non-executable file (or a
|
|
91
|
+
// directory) named `insta` must not suppress the self-install. Mode bits, not access(X_OK) —
|
|
92
|
+
// access() answers "can THIS process exec it", which for root is always yes, so a root-run
|
|
93
|
+
// setup would wrongly treat a non-executable file as a durable install. On Windows execute
|
|
94
|
+
// permission is extension-driven, so existence of a regular file is the right check.
|
|
95
|
+
const isRunnableFile = (p, win) => {
|
|
96
|
+
try {
|
|
97
|
+
const st = statSync(p);
|
|
98
|
+
if (!st.isFile())
|
|
99
|
+
return false;
|
|
100
|
+
return win || (st.mode & 0o111) !== 0;
|
|
101
|
+
}
|
|
102
|
+
catch {
|
|
103
|
+
return false;
|
|
104
|
+
}
|
|
105
|
+
};
|
|
106
|
+
/** Resolve a bare command name to its absolute PATH location (PATHEXT-aware on Windows).
|
|
107
|
+
* cmd.exe searches the CURRENT DIRECTORY before PATH for bare names, so handing it a bare
|
|
108
|
+
* `claude` would let a claude.cmd planted in the project directory shadow the real CLI —
|
|
109
|
+
* the cmd.exe wrapper below only ever passes absolute paths. */
|
|
110
|
+
export function whichOnPath(bin, env = process.env, platform = process.platform) {
|
|
111
|
+
const win = platform === 'win32';
|
|
112
|
+
const exts = win ? [...(env.PATHEXT ?? '.COM;.EXE;.BAT;.CMD').split(';'), ''] : [''];
|
|
113
|
+
for (const dir of (env.PATH ?? '').split(win ? ';' : ':')) {
|
|
114
|
+
if (!dir)
|
|
115
|
+
continue;
|
|
116
|
+
for (const ext of exts) {
|
|
117
|
+
const p = join(dir, bin + ext);
|
|
118
|
+
if (isRunnableFile(p, win))
|
|
119
|
+
return p;
|
|
120
|
+
}
|
|
121
|
+
}
|
|
122
|
+
return null;
|
|
123
|
+
}
|
|
124
|
+
export function findDurableOnPath(bin, env = process.env, platform = process.platform) {
|
|
125
|
+
const win = platform === 'win32';
|
|
126
|
+
const dirs = (env.PATH ?? '').split(win ? ';' : ':');
|
|
127
|
+
// npm on Windows writes insta.cmd/insta.ps1 plus an extensionless sh shim; PATHEXT covers the
|
|
128
|
+
// former, the bare name the latter.
|
|
129
|
+
const exts = win ? [...(env.PATHEXT ?? '.COM;.EXE;.BAT;.CMD').split(';'), ''] : [''];
|
|
130
|
+
for (const dir of dirs) {
|
|
131
|
+
// Case-insensitive: Windows paths (and the npx cache) may carry any casing.
|
|
132
|
+
if (!dir || dir.toLowerCase().includes('node_modules'))
|
|
133
|
+
continue;
|
|
134
|
+
for (const ext of exts)
|
|
135
|
+
if (isRunnableFile(join(dir, bin + ext), win))
|
|
136
|
+
return true;
|
|
137
|
+
}
|
|
138
|
+
return false;
|
|
139
|
+
}
|
|
140
|
+
const cliVersion = () => {
|
|
141
|
+
try {
|
|
142
|
+
return JSON.parse(readFileSync(new URL('../../package.json', import.meta.url), 'utf8')).version;
|
|
143
|
+
}
|
|
144
|
+
catch {
|
|
145
|
+
return 'latest';
|
|
146
|
+
}
|
|
147
|
+
};
|
|
148
|
+
/** Best-effort: a failed global install must not block the skill/MCP setup below — the npx run
|
|
149
|
+
* itself still completes the agent onboarding, and the manual fallback is one line. */
|
|
150
|
+
export async function ensureCliInstalled(run, channel = detectChannel(), onPath = findDurableOnPath('insta'), recheck = () => findDurableOnPath('insta')) {
|
|
151
|
+
if (channel !== 'npm' || onPath)
|
|
152
|
+
return;
|
|
153
|
+
info('installing the insta CLI globally (npm) …');
|
|
154
|
+
// Pinned to THIS version so the one-liner installs exactly what it ran. The logical `npm` is
|
|
155
|
+
// resolved to a spawnable invocation ONCE, inside the default runner (resolveSpawnable) —
|
|
156
|
+
// handing it a pre-resolved node/npm-cli.js path here would make the runner resolve it a
|
|
157
|
+
// second time and, on Windows, wrap the real node.exe in cmd.exe.
|
|
158
|
+
const spec = `insta@${cliVersion()}`;
|
|
159
|
+
const res = await run('npm', ['install', '-g', spec]);
|
|
160
|
+
if (res.ok) {
|
|
161
|
+
// A clean `npm i -g` can still land in a bin dir that isn't on PATH (custom npm prefix) —
|
|
162
|
+
// exactly the machines this path exists for. Only claim success after re-finding the shim.
|
|
163
|
+
if (recheck()) {
|
|
164
|
+
info('✓ insta CLI — installed globally (`insta` now works in any shell)');
|
|
165
|
+
}
|
|
166
|
+
else {
|
|
167
|
+
info('✓ insta CLI — installed globally, but npm\'s global bin dir is not on PATH');
|
|
168
|
+
info(' add it to PATH (POSIX: `$(npm prefix -g)/bin`; Windows: the dir `npm prefix -g` prints), then verify with `insta --version`');
|
|
169
|
+
}
|
|
170
|
+
return;
|
|
171
|
+
}
|
|
172
|
+
info(' global CLI install failed — continuing with agent setup; install manually with:');
|
|
173
|
+
info(` npm install -g ${spec}`);
|
|
174
|
+
if (/EACCES|permission denied/i.test(res.output ?? '')) {
|
|
175
|
+
info(' (permission error: the npm prefix is system-owned — use a Node version manager, or elevate that one command)');
|
|
176
|
+
}
|
|
177
|
+
}
|
|
178
|
+
// ---- Windows-safe spawning for npm/npx ----
|
|
179
|
+
// On Windows `npm`/`npx` are .cmd shims, which spawn() without a shell refuses (Node docs:
|
|
180
|
+
// spawning .bat/.cmd needs a shell or cmd.exe). Rather than a shell (argument-quoting hazards),
|
|
181
|
+
// re-enter them as node scripts: the CLI script named by npm_execpath (swapped between
|
|
182
|
+
// npm-cli.js and npx-cli.js as needed), else the one shipped beside the running node, else the
|
|
183
|
+
// bare name (POSIX, where PATH shims resolve fine). Applied ONCE, inside the default runner,
|
|
184
|
+
// so every `run('npm'|'npx', …)` call site benefits and nothing is ever resolved twice.
|
|
185
|
+
export function resolveSpawnable(cmd, args, npmExecpath = process.env.npm_execpath, execPath = process.execPath, platform = process.platform, env = process.env) {
|
|
186
|
+
// Node re-entry is only valid when THIS process runs on node. On the native-binary channel
|
|
187
|
+
// execPath is the compiled `insta` executable — and npm scripts export npm_execpath to their
|
|
188
|
+
// children — so re-entering blindly would spawn `insta npx-cli.js …`. A non-node execPath
|
|
189
|
+
// sends npm/npx down the generic shim path below instead.
|
|
190
|
+
const execIsNode = /(^|[\\/])node(\.exe)?$/i.test(execPath);
|
|
191
|
+
if ((cmd === 'npm' || cmd === 'npx') && execIsNode) {
|
|
192
|
+
if (npmExecpath && /(^|[\\/])np[mx](-cli)?\.[cm]?js$/.test(npmExecpath)) {
|
|
193
|
+
const cli = npmExecpath.replace(/np[mx](-cli)?(\.[cm]?js)$/, `${cmd}$1$2`);
|
|
194
|
+
if (existsSync(cli))
|
|
195
|
+
return { cmd: execPath, args: [cli, ...args] };
|
|
196
|
+
}
|
|
197
|
+
const nodeDir = dirname(execPath);
|
|
198
|
+
const besideNode = platform === 'win32'
|
|
199
|
+
? join(nodeDir, 'node_modules', 'npm', 'bin', `${cmd}-cli.js`)
|
|
200
|
+
: join(nodeDir, '..', 'lib', 'node_modules', 'npm', 'bin', `${cmd}-cli.js`);
|
|
201
|
+
if (existsSync(besideNode))
|
|
202
|
+
return { cmd: execPath, args: [besideNode, ...args] };
|
|
203
|
+
}
|
|
204
|
+
// Generic shim path — every non-npm CLI we shell out to (claude), plus npm/npx themselves
|
|
205
|
+
// when node isn't resolvable (native binary channel). On Windows these are .cmd shims, which
|
|
206
|
+
// spawn() refuses without a shell, so route them through cmd.exe. Guards, in order:
|
|
207
|
+
// - BARE names only: an absolute path or anything .exe (node.exe from a resolved npm/npx
|
|
208
|
+
// invocation passing back through here) is directly spawnable and must NOT see cmd.exe.
|
|
209
|
+
// - The name is resolved to its ABSOLUTE PATH location first: cmd.exe searches the current
|
|
210
|
+
// directory before PATH, so a bare name would let a shim planted in the project dir
|
|
211
|
+
// shadow the real CLI. No PATH hit → pass through (spawn fails; callers degrade).
|
|
212
|
+
// - No manual quoting: libuv already wraps spaced args when building the child command
|
|
213
|
+
// line — pre-quoting would be quoted AGAIN and arrive as literal quote characters.
|
|
214
|
+
// - That leaves cmd.exe metacharacters unprotectable, so an arg carrying one (e.g. a
|
|
215
|
+
// custom INSTA_MCP_URL with `&`) skips the wrapper: the bare-shim spawn fails and every
|
|
216
|
+
// caller degrades gracefully (probe → not-installed; registration → manual-add
|
|
217
|
+
// fallback). Never hand metacharacters to a shell.
|
|
218
|
+
const bareShim = !/[\\/]/.test(cmd) && !/\.exe$/i.test(cmd);
|
|
219
|
+
if (platform === 'win32' && bareShim && !args.some((a) => /[&|<>^%"]/.test(a))) {
|
|
220
|
+
const abs = whichOnPath(cmd, env, platform);
|
|
221
|
+
if (abs)
|
|
222
|
+
return { cmd: 'cmd.exe', args: ['/d', '/s', '/c', abs, ...args] };
|
|
223
|
+
}
|
|
224
|
+
return { cmd, args };
|
|
225
|
+
}
|
|
79
226
|
// Capture stdout+stderr silently (don't stream) so we can print our own clean summary.
|
|
80
227
|
// stdin is 'ignore', NOT 'inherit': under the canonical `curl … | sh` install, stdin is the
|
|
81
228
|
// piped install script itself — a child that inherits it (npx/skills reads for keypresses even
|
|
82
229
|
// with -y) consumes the rest of the script, so the shell never runs the trailing "Get started"
|
|
83
230
|
// guidance. Ignoring stdin keeps the installer's own output intact. (-y means no prompt anyway.)
|
|
84
|
-
const defaultRunner = (
|
|
231
|
+
const defaultRunner = (cmdIn, argsIn) => new Promise((resolve) => {
|
|
232
|
+
const { cmd, args } = resolveSpawnable(cmdIn, argsIn);
|
|
85
233
|
const env = { ...process.env, AI_AGENT: process.env.AI_AGENT || 'insta', FORCE_COLOR: '0' };
|
|
234
|
+
// When THIS process was launched by npx, npx exports its flags as npm_config_* env vars.
|
|
235
|
+
// npm_config_package pins package resolution for every nested npm/npx child — the inner
|
|
236
|
+
// `npx -y skills …` would then resolve `skills` against the insta package and degrade to
|
|
237
|
+
// `sh: skills: command not found`. Scrub the resolution-pinning vars; keep prefix/registry
|
|
238
|
+
// (deliberate user configuration).
|
|
239
|
+
delete env.npm_config_package;
|
|
240
|
+
delete env.npm_config_call;
|
|
86
241
|
const p = spawn(cmd, args, { stdio: ['ignore', 'pipe', 'pipe'], env });
|
|
87
242
|
let output = '';
|
|
88
243
|
const grab = (chunk) => { output += chunk.toString(); };
|
|
@@ -91,11 +246,14 @@ const defaultRunner = (cmd, args) => new Promise((resolve) => {
|
|
|
91
246
|
p.on('error', () => resolve({ ok: false, output }));
|
|
92
247
|
p.on('close', (code) => resolve({ ok: code === 0, output }));
|
|
93
248
|
});
|
|
249
|
+
// Leading -y is npx's OWN flag: on a machine where the `skills` package isn't already in the
|
|
250
|
+
// npx cache, non-TTY `npx skills …` refuses to auto-install and degrades to a shell lookup
|
|
251
|
+
// (`sh: skills: command not found`) — the trailing -y only answers the skills TOOL's prompt.
|
|
94
252
|
// -g = user-level (machine-global); -a '*' = every agent dir the skills tool supports
|
|
95
253
|
// (Claude Code, Codex, Cursor, OpenCode, Copilot, …); --copy = real files, not cache symlinks.
|
|
96
254
|
// `spec` is the skill source for the resolved environment (`owner/repo` or `owner/repo@ref`), so a
|
|
97
255
|
// staging install reads the staging skill text rather than what's published on main.
|
|
98
|
-
export const setupArgs = (spec) => ['skills', 'add', spec, '-s', 'insta', '-a', '*', '-g', '-y', '--copy'];
|
|
256
|
+
export const setupArgs = (spec) => ['-y', 'skills', 'add', spec, '-s', 'insta', '-a', '*', '-g', '-y', '--copy'];
|
|
99
257
|
/** Production's args. Kept as a named export because it is the installed-base default and is
|
|
100
258
|
* asserted directly by tests; runtime goes through `setupArgs(resolveEnv().skills)`. */
|
|
101
259
|
export const SETUP_ARGS = setupArgs(ENVS[DEFAULT_ENV].skills);
|
|
@@ -158,10 +316,13 @@ export async function registerMcp(run = defaultRunner, mint = defaultMinter, use
|
|
|
158
316
|
info(` MCP registration failed — add manually:\n claude mcp add --transport http ${name} ${url}`);
|
|
159
317
|
}
|
|
160
318
|
}
|
|
161
|
-
export async function setupAgent(opts, run = defaultRunner, mint, installConfigs = installAgentConfigs) {
|
|
319
|
+
export async function setupAgent(opts, run = defaultRunner, mint, installConfigs = installAgentConfigs, ensure = (r) => ensureCliInstalled(r)) {
|
|
162
320
|
if (!opts.yes && !process.stdout.isTTY) {
|
|
163
321
|
info('non-interactive shell — assuming -y');
|
|
164
322
|
}
|
|
323
|
+
// BEFORE the skill install: the skill tells agents to run `insta …`, so a durable CLI must
|
|
324
|
+
// exist by the time it lands.
|
|
325
|
+
await ensure(run);
|
|
165
326
|
// One resolve for the whole step, so the skills and the MCP registration below cannot disagree
|
|
166
327
|
// about which environment this machine belongs to.
|
|
167
328
|
const { env, skills } = await resolveEnv();
|
package/dist/commands/storage.js
CHANGED
|
@@ -50,7 +50,7 @@ export async function storageList(opts) {
|
|
|
50
50
|
const branch = opts.branch ?? p.branch;
|
|
51
51
|
const svc = await storageTarget(api, p.projectId, branch, opts.service);
|
|
52
52
|
const res = await api.rawRequest('GET', objectsPath(p.projectId, svc.id, { branch, prefix: opts.prefix, cursor: opts.cursor, limit }));
|
|
53
|
-
if (handleApproval(res))
|
|
53
|
+
if (handleApproval(res, opts.json))
|
|
54
54
|
return;
|
|
55
55
|
if (opts.json)
|
|
56
56
|
return printJson(res.body);
|
|
@@ -143,7 +143,7 @@ export async function storageGet(key, opts, deps = {}) {
|
|
|
143
143
|
const branch = opts.branch ?? p.branch;
|
|
144
144
|
const svc = await storageTarget(api, p.projectId, branch, opts.service);
|
|
145
145
|
const res = await api.rawRequest('GET', objectDownloadPath(p.projectId, svc.id, { branch, key }));
|
|
146
|
-
if (handleApproval(res))
|
|
146
|
+
if (handleApproval(res, opts.json))
|
|
147
147
|
return;
|
|
148
148
|
// --json hands over the presigned URL instead of downloading, as `insta secrets --json` does.
|
|
149
149
|
// Before outputPath, so a key with no filename still works when nothing is written to disk.
|
|
@@ -164,7 +164,7 @@ export async function storageDelete(key, opts) {
|
|
|
164
164
|
const branch = opts.branch ?? p.branch;
|
|
165
165
|
const svc = await storageTarget(api, p.projectId, branch, opts.service);
|
|
166
166
|
const res = await api.rawRequest('DELETE', objectsPath(p.projectId, svc.id, { branch, key }));
|
|
167
|
-
if (handleApproval(res))
|
|
167
|
+
if (handleApproval(res, opts.json))
|
|
168
168
|
return;
|
|
169
169
|
if (opts.json)
|
|
170
170
|
return printJson(res.body);
|
package/dist/ensure-skills.js
CHANGED
|
@@ -9,6 +9,7 @@ import { spawn } from 'node:child_process';
|
|
|
9
9
|
import { existsSync, readFileSync, writeFileSync } from 'node:fs';
|
|
10
10
|
import { join } from 'node:path';
|
|
11
11
|
import { resolveEnv } from './config.js';
|
|
12
|
+
import { resolveSpawnable } from './commands/setup.js';
|
|
12
13
|
import { DEFAULT_ENV, ENVS } from './env.js';
|
|
13
14
|
// Where `npx skills add` drops skills for the agents we pin below: Claude Code → .claude/skills/,
|
|
14
15
|
// Codex → .agents/skills/ (.github/skills/ is the third well-known dir). These are regenerable
|
|
@@ -20,8 +21,15 @@ const SKILL_DIRS = ['.claude/skills/', '.agents/skills/', '.github/skills/'];
|
|
|
20
21
|
// honest here (this IS programmatic, not an interactive prompt) and, because we already pin the
|
|
21
22
|
// agents/skills/-y, has no effect on the install beyond quieting the banner. Preserve a caller's
|
|
22
23
|
// existing AI_AGENT (e.g. running inside another agent) rather than clobbering it.
|
|
23
|
-
const defaultRunner = (
|
|
24
|
+
const defaultRunner = (cmdIn, argsIn, inherit = false) => new Promise((resolve) => {
|
|
25
|
+
// resolveSpawnable: on Windows `npx` is a .cmd shim spawn() refuses without a shell —
|
|
26
|
+
// re-enter npm's CLI script via node instead (same treatment as `insta setup agent`).
|
|
27
|
+
const { cmd, args } = resolveSpawnable(cmdIn, argsIn);
|
|
24
28
|
const env = { ...process.env, AI_AGENT: process.env.AI_AGENT || 'insta' };
|
|
29
|
+
// npx exports its flags as npm_config_* to children; npm_config_package would pin the inner
|
|
30
|
+
// `npx -y skills …` to whatever package launched this CLI (see setup.ts defaultRunner).
|
|
31
|
+
delete env.npm_config_package;
|
|
32
|
+
delete env.npm_config_call;
|
|
25
33
|
const p = spawn(cmd, args, { stdio: inherit ? 'inherit' : 'ignore', env });
|
|
26
34
|
p.on('error', () => resolve({ ok: false })); // e.g. npx not on PATH
|
|
27
35
|
p.on('close', (code) => resolve({ ok: code === 0 }));
|
|
@@ -31,17 +39,21 @@ const defaultRunner = (cmd, args, inherit = false) => new Promise((resolve) => {
|
|
|
31
39
|
// name the exact skills (-s …) so there's no skill picker; -y to skip the scope/confirm prompt;
|
|
32
40
|
// --copy to write real files (not symlinks into a transient npx cache).
|
|
33
41
|
const AGENT_FLAGS = ['-a', 'claude-code', '-a', 'codex', '-y', '--copy'];
|
|
42
|
+
// npx's OWN -y, distinct from the skills tool's -y above: without it, a machine whose npx cache
|
|
43
|
+
// lacks the `skills` package refuses the auto-install in non-TTY runs and the whole command
|
|
44
|
+
// degrades to `sh: skills: command not found`.
|
|
45
|
+
const NPX_YES = ['-y'];
|
|
34
46
|
// `instaSpec` is the insta skill source for the resolved environment (`owner/repo[@ref]`), so a
|
|
35
47
|
// project created against staging gets the staging skill text. The third-party stack skills are
|
|
36
48
|
// environment-independent — they document Tigris/Better Auth, not our control plane.
|
|
37
49
|
const skillTargets = (instaSpec) => [
|
|
38
|
-
{ label: 'insta', args: ['skills', 'add', instaSpec, '-s', 'insta', ...AGENT_FLAGS] },
|
|
39
|
-
{ label: 'tigris', args: ['skills', 'add', 'tigrisdata/skills',
|
|
50
|
+
{ label: 'insta', args: [...NPX_YES, 'skills', 'add', instaSpec, '-s', 'insta', ...AGENT_FLAGS] },
|
|
51
|
+
{ label: 'tigris', args: [...NPX_YES, 'skills', 'add', 'tigrisdata/skills',
|
|
40
52
|
'-s', 'tigris-object-operations', '-s', 'file-storage', '-s', 'tigris-sdk-guide',
|
|
41
53
|
'-s', 'tigris-security-access-control', '-s', 'tigris-image-optimization',
|
|
42
54
|
'-s', 'tigris-s3-migration', '-s', 'tigris-static-assets', '-s', 'tigris-agent-kit',
|
|
43
55
|
...AGENT_FLAGS] },
|
|
44
|
-
{ label: 'better-auth', args: ['skills', 'add', 'better-auth/skills',
|
|
56
|
+
{ label: 'better-auth', args: [...NPX_YES, 'skills', 'add', 'better-auth/skills',
|
|
45
57
|
'-s', 'better-auth-best-practices', '-s', 'email-and-password-best-practices',
|
|
46
58
|
'-s', 'better-auth-security-best-practices', ...AGENT_FLAGS] },
|
|
47
59
|
];
|
package/dist/flyctl-build.js
CHANGED
|
@@ -4,17 +4,21 @@
|
|
|
4
4
|
import { spawn } from 'node:child_process';
|
|
5
5
|
import { existsSync, writeFileSync, unlinkSync } from 'node:fs';
|
|
6
6
|
import { join } from 'node:path';
|
|
7
|
-
import { info } from './util.js';
|
|
8
7
|
// Spawn flyctl, tee its output to the user (so they see buildkit progress) AND capture it for digest
|
|
9
|
-
// parsing.
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
});
|
|
8
|
+
// parsing. `to` picks the tee destination for the child's stdout: `deploy --json` reserves the
|
|
9
|
+
// process's stdout for the final JSON document, so it tees build progress to stderr instead.
|
|
10
|
+
function teeRunner(to) {
|
|
11
|
+
return (cmd, args, opts) => new Promise((resolve) => {
|
|
12
|
+
const child = spawn(cmd, args, { cwd: opts.cwd, env: opts.env, stdio: ['inherit', 'pipe', 'pipe'] });
|
|
13
|
+
let output = '';
|
|
14
|
+
child.stdout?.on('data', (b) => { const s = b.toString(); output += s; to.write(s); });
|
|
15
|
+
child.stderr?.on('data', (b) => { const s = b.toString(); output += s; process.stderr.write(s); });
|
|
16
|
+
child.on('error', (err) => resolve({ code: -1, output: `${output}\n${err.message}` }));
|
|
17
|
+
child.on('close', (code) => resolve({ code: code ?? -1, output }));
|
|
18
|
+
});
|
|
19
|
+
}
|
|
20
|
+
export const defaultBuildRunner = teeRunner(process.stdout);
|
|
21
|
+
export const stderrBuildRunner = teeRunner(process.stderr);
|
|
18
22
|
// buildkit prints "pushing manifest for registry.fly.io/<app>:<label>@sha256:<digest>" on push.
|
|
19
23
|
// Pin to the digest — the bare tag races on Fly's registry (MANIFEST_UNKNOWN); the digest always resolves.
|
|
20
24
|
export function parseImageDigest(output, flyApp) {
|
|
@@ -57,10 +61,13 @@ export async function flyctlBuildAndPush(opts, run = defaultBuildRunner) {
|
|
|
57
61
|
}
|
|
58
62
|
}
|
|
59
63
|
// Best-effort: ensure the fly CLI is available (needed for source-directory deploys). Never blocks —
|
|
60
|
-
// if it can't be installed the subsequent build surfaces a clear error.
|
|
64
|
+
// if it can't be installed the subsequent build surfaces a clear error. Everything here is one-time
|
|
65
|
+
// bootstrap DIAGNOSTICS, so it all goes to stderr — including the installers' own stdout (fd 2 in
|
|
66
|
+
// the stdio triple) — keeping stdout clean for `deploy --json`.
|
|
61
67
|
export async function ensureFlyctl() {
|
|
68
|
+
const note = (m) => process.stderr.write(m + '\n');
|
|
62
69
|
const ok = (cmd, args, inherit = false) => new Promise((resolve) => {
|
|
63
|
-
const p = spawn(cmd, args, { stdio: inherit ? 'inherit' : 'ignore' });
|
|
70
|
+
const p = spawn(cmd, args, { stdio: inherit ? ['inherit', 2, 2] : 'ignore' });
|
|
64
71
|
p.on('error', () => resolve(false));
|
|
65
72
|
p.on('close', (code) => resolve(code === 0));
|
|
66
73
|
});
|
|
@@ -68,8 +75,8 @@ export async function ensureFlyctl() {
|
|
|
68
75
|
if (await ok('flyctl', ['version']))
|
|
69
76
|
return;
|
|
70
77
|
if (process.platform === 'darwin' && (await ok('brew', ['--version']))) {
|
|
71
|
-
|
|
72
|
-
|
|
78
|
+
note('flyctl not found — installing with `brew install flyctl` (one-time)…');
|
|
79
|
+
note((await ok('brew', ['install', 'flyctl'], true)) ? 'flyctl installed ✓' : 'flyctl install failed — install manually: https://fly.io/docs/flyctl/install/');
|
|
73
80
|
return;
|
|
74
81
|
}
|
|
75
82
|
if (process.platform === 'linux') {
|
|
@@ -77,20 +84,20 @@ export async function ensureFlyctl() {
|
|
|
77
84
|
// and no brew; without this branch `insta deploy <dir>` dead-ends on a hand-install of a
|
|
78
85
|
// third-party CLI. Official installer, pinned into ~/.fly; the current process extends its
|
|
79
86
|
// own PATH because the installer's shell-profile edit can't reach an already-running process.
|
|
80
|
-
|
|
87
|
+
note('flyctl not found — installing to ~/.fly (one-time)…');
|
|
81
88
|
const flyHome = `${process.env.HOME ?? '~'}/.fly`;
|
|
82
89
|
const installed = await ok('sh', ['-c', `curl -fsSL https://fly.io/install.sh | FLYCTL_INSTALL="${flyHome}" sh`], true);
|
|
83
90
|
if (installed) {
|
|
84
91
|
process.env.PATH = `${process.env.PATH}:${flyHome}/bin`;
|
|
85
92
|
if (await ok('flyctl', ['version'])) {
|
|
86
|
-
|
|
93
|
+
note('flyctl installed ✓');
|
|
87
94
|
return;
|
|
88
95
|
}
|
|
89
96
|
}
|
|
90
|
-
|
|
97
|
+
note('flyctl install failed — install manually: https://fly.io/docs/flyctl/install/');
|
|
91
98
|
return;
|
|
92
99
|
}
|
|
93
|
-
|
|
100
|
+
note('flyctl (fly CLI) not found — install it to deploy from source: https://fly.io/docs/flyctl/install/');
|
|
94
101
|
}
|
|
95
102
|
catch { /* best-effort convenience */ }
|
|
96
103
|
}
|