insta 0.0.47 → 0.0.49

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/dist/index.js CHANGED
@@ -2,7 +2,7 @@
2
2
  import { readFileSync } from 'node:fs';
3
3
  import { Command } from 'commander';
4
4
  import { ApiError } from './api.js';
5
- import { die } from './util.js';
5
+ import { CliExit, fail } from './util.js';
6
6
  import * as auth from './commands/auth.js';
7
7
  import * as envCmd_ from './commands/env.js';
8
8
  import { ENV_NAMES } from './env.js';
@@ -30,9 +30,11 @@ import { billing, billingUpgrade, billingPortal } from './commands/billing.js';
30
30
  import * as selfUpdate from './commands/upgrade.js';
31
31
  import * as feedbackCmd from './commands/feedback.js';
32
32
  function onError(e) {
33
+ if (e instanceof CliExit)
34
+ return;
33
35
  if (e instanceof ApiError)
34
- die(`${e.message} (HTTP ${e.status})`);
35
- die(e instanceof Error ? e.message : String(e));
36
+ return fail(`${e.message} (HTTP ${e.status})`);
37
+ fail(e instanceof Error ? e.message : String(e));
36
38
  }
37
39
  // Wrap an async action so rejections surface as clean CLI errors.
38
40
  const guard = (fn) => (...a) => fn(...a).then(() => undefined).catch(onError);
@@ -87,6 +89,7 @@ setupCmd.command('agent').description('Install the insta CLI (if missing), the i
87
89
  .option('-y, --yes', 'non-interactive')
88
90
  .option('--env <prod|staging>', 'deployment to set this machine up for (default: prod — switches and persists, like `insta env use`)')
89
91
  .option('--mcp-token', 'register the MCP server with a minted insta_ API token instead of OAuth (headless machines / CI)')
92
+ .option('--project <id>', 'also link this directory to an existing project after setup (flows through login first if needed)')
90
93
  .action(guard((o) => setup.setupAgent(o)));
91
94
  // ---- MCP server integration ----
92
95
  const mcpCmd = program.command('mcp').description('insta-cloud remote MCP server integration');
@@ -179,7 +182,7 @@ sec.command('sources').description('List service credential sources available fo
179
182
  sec.command('tree').description('Show secrets as project → branch → service → secrets').option('--json')
180
183
  .action(guard((o) => secretsCmd.secretsTree(o)));
181
184
  // ---- build (pre-push verification — local, offline, deploys nothing) ----
182
- program.command('build [dir]').description('Verify a source directory would build before deploying: detection plan + the Dockerfile that would be used (yours, or nixpacks-generated) + static checks. Local and offline — no login needed, nothing pushed. Exit 1 when the verdict is failed')
185
+ program.command('build [dir]').description('Verify a source directory would build before deploying: detection plan + the Dockerfile (yours, or the one nixpacks would generate for the GitHub lane — `insta deploy <dir>` needs your own) + static checks. Local and offline — no login needed, nothing pushed. Exit 1 when the verdict is failed')
183
186
  .option('--explain', 'include the Dockerfile content in the output')
184
187
  .option('--port <p>', 'port the app listens on (else the Dockerfile EXPOSE)')
185
188
  .option('--json')
@@ -193,7 +196,7 @@ program.command('deploy [dir]').description('Deploy a source directory (built re
193
196
  // `insta compute exec` needs the command verbatim after a literal `--`; split it out of argv here,
194
197
  // before commander parses anything (see splitExecArgs's own comment for why `service` being
195
198
  // optional makes commander unable to hold that boundary itself).
196
- const { argv: computeArgv, command: execCommand } = computeCmd.splitExecArgs(process.argv);
199
+ const { argv: computeArgv, command: execCommand, windowsFallback: execWindowsFallback, } = computeCmd.splitExecArgs(process.argv);
197
200
  // ---- compute (lifecycle control + custom domains) ----
198
201
  const compute = program.command('compute').description('Control compute lifecycle (start/stop/suspend/status) + custom domains');
199
202
  compute.command('set-domain <host>').description('Attach a custom domain to a branch compute service (gated: deploy)')
@@ -215,9 +218,12 @@ compute.command('limits [service]').description("Show or set a compute service's
215
218
  .option('--json').option('--branch <branch>', 'branch (default: current)').action(guard((service, o) => computeCmd.computeLimits(service, o)));
216
219
  compute.command('always-on <mode> [service]').description('Set a compute service always-on (mode: on|off). on = machines never scale to zero; off = default scale-to-zero. All plans; billing is actual usage either way')
217
220
  .option('--json').option('--branch <branch>', 'branch (default: current)').action(guard((mode, service, o) => computeCmd.computeAlwaysOn(mode, service, o)));
218
- compute.command('exec [service]').description("Run a one-shot command inside a compute service's machine (`insta compute exec [service] -- <command> [args…]`) — no interactive shell/PTY: `command` is argv, no shell is invoked (use [\"sh\", \"-c\", \"...\"] for shell features). Wakes the machine first if it's scaled to zero — expect a few seconds of latency, billed as uptime, not an error. Exits with the remote command's own exit code (agents rely on this)")
219
- .option('--branch <b>').option('--timeout <sec>', 'command timeout in seconds, 1-180 (platform default: 30)').option('--json')
220
- .action(guard((service, o) => computeCmd.computeExec(service, execCommand, o)));
221
+ const execCmd = compute.command('exec [service]').description("Run a one-shot command inside a compute service's machine (`insta compute exec [service] -- <command> [args…]`) — no interactive shell/PTY: `command` is argv, no shell is invoked (use [\"sh\", \"-c\", \"...\"] for shell features). Wakes the machine first if it's scaled to zero — expect a few seconds of latency, billed as uptime, not an error. Exits with the remote command's own exit code (agents rely on this)")
222
+ .action(guard((service, o) => computeCmd.computeExec(service, execCommand, o, { windowsFallback: execWindowsFallback })));
223
+ // Declared from the same list splitExecArgs uses to find where the CLI's own arguments stop, so a
224
+ // new option cannot reach the CLI surface while the split still reads it as part of the command.
225
+ for (const [flags, description] of computeCmd.EXEC_OPTIONS)
226
+ execCmd.option(flags, description);
221
227
  compute.command('volume [service]').description("Show, attach, grow, or delete a compute service's persistent /data volume. No flag: print size, mount path, and the plan cap (any plan). --size on a volumeless service ATTACHES one (any plan at the default 1Gi; larger is paid and plan-capped; the disk mounts at /data on the next deploy); on a volume-bearing one it grows (paid plans; grow-only — a provisioned disk cannot shrink). --delete DESTROYS the disk and ALL its data immediately (no detach, no undo; billing stops now, and suspend fast-wake + scale-out return). Billing is actual data stored — the size is a cap, not a price")
222
228
  .option('--size <gi>', 'new size in whole Gi, e.g. 10 (must be ≥ the current size)')
223
229
  .option('--delete', 'destroy the volume and ALL its data (irreversible; download anything you need first)')
@@ -336,10 +342,10 @@ program.command('feedback')
336
342
  .action(guard((o) => feedbackCmd.feedback(o)));
337
343
  // ---- self-update ----
338
344
  program.command('upgrade').description('Update the insta CLI to the latest release (binary or npm install)')
339
- .action(guard(() => selfUpdate.upgrade()));
345
+ .action(guard(() => selfUpdate.upgrade(resolveVersion())));
340
346
  program.command('autoupdate [mode]').description('Show or set auto-update: on | off (default: on while pre-1.0)')
341
347
  .action(guard((mode) => selfUpdate.autoupdate(mode)));
342
- program.command('__update-check', { hidden: true }).action(guard(() => selfUpdate.backgroundCheck()));
348
+ program.command('__update-check', { hidden: true }).action(guard(() => selfUpdate.backgroundCheck(resolveVersion())));
343
349
  selfUpdate.maybeUpdate(resolveVersion(), process.argv);
344
350
  program.parseAsync(computeArgv);
345
351
  //# sourceMappingURL=index.js.map
package/dist/spawn.js ADDED
@@ -0,0 +1,77 @@
1
+ import { existsSync, statSync } from 'node:fs';
2
+ import { dirname, join, win32 } from 'node:path';
3
+ export const isRunnableFile = (path, win) => {
4
+ try {
5
+ const stat = statSync(path);
6
+ if (!stat.isFile())
7
+ return false;
8
+ return win || (stat.mode & 0o111) !== 0;
9
+ }
10
+ catch {
11
+ return false;
12
+ }
13
+ };
14
+ /** Read an environment variable the way Windows itself does: case-insensitively.
15
+ * `process.env` is already case-insensitive on win32, but a COPY of it (`{ ...process.env }`,
16
+ * which every caller that adds a variable makes) is an ordinary object that keeps whatever
17
+ * casing Windows used — and Windows overwhelmingly spells it `Path`, not `PATH`. Reading
18
+ * `env.PATH` off such a copy yields undefined, nothing resolves, and the caller falls back to
19
+ * spawning the bare `.cmd` shim this module exists to avoid. */
20
+ export function envVar(env, name, win) {
21
+ const direct = env[name];
22
+ if (direct !== undefined || !win)
23
+ return direct;
24
+ const wanted = name.toLowerCase();
25
+ for (const [key, value] of Object.entries(env))
26
+ if (key.toLowerCase() === wanted)
27
+ return value;
28
+ return undefined;
29
+ }
30
+ /** Resolve a bare command name to its absolute PATH location (PATHEXT-aware on Windows).
31
+ * cmd.exe searches the CURRENT DIRECTORY before PATH for bare names, so callers must pass
32
+ * the absolute shim path to the cmd.exe wrapper below. */
33
+ export function whichOnPath(bin, env = process.env, platform = process.platform) {
34
+ const win = platform === 'win32';
35
+ const exts = win ? [...(envVar(env, 'PATHEXT', win) ?? '.COM;.EXE;.BAT;.CMD').split(';'), ''] : [''];
36
+ for (const dir of (envVar(env, 'PATH', win) ?? '').split(win ? ';' : ':')) {
37
+ if (!dir)
38
+ continue;
39
+ for (const ext of exts) {
40
+ const path = join(dir, bin + ext);
41
+ if (isRunnableFile(path, win))
42
+ return path;
43
+ }
44
+ }
45
+ return null;
46
+ }
47
+ // On Windows `npm`/`npx` and most npm-installed CLIs are .cmd shims, which spawn() without a
48
+ // shell refuses. Prefer re-entering npm/npx through node; otherwise resolve a safe absolute shim
49
+ // path and invoke it through cmd.exe. Kept shared so every child-process call uses the same rules.
50
+ export function resolveSpawnable(cmd, args, npmExecpath = process.env.npm_execpath, execPath = process.execPath, platform = process.platform, env = process.env, systemRoot = process.env.SYSTEMROOT ?? process.env.windir ?? 'C:\\Windows') {
51
+ const execIsNode = /(^|[\\/])node(\.exe)?$/i.test(execPath);
52
+ if ((cmd === 'npm' || cmd === 'npx') && execIsNode) {
53
+ if (npmExecpath && /(^|[\\/])np[mx](-cli)?\.[cm]?js$/.test(npmExecpath)) {
54
+ const cli = npmExecpath.replace(/np[mx](-cli)?(\.[cm]?js)$/, `${cmd}$1$2`);
55
+ if (existsSync(cli))
56
+ return { cmd: execPath, args: [cli, ...args] };
57
+ }
58
+ const nodeDir = dirname(execPath);
59
+ const besideNode = platform === 'win32'
60
+ ? join(nodeDir, 'node_modules', 'npm', 'bin', `${cmd}-cli.js`)
61
+ : join(nodeDir, '..', 'lib', 'node_modules', 'npm', 'bin', `${cmd}-cli.js`);
62
+ if (existsSync(besideNode))
63
+ return { cmd: execPath, args: [besideNode, ...args] };
64
+ }
65
+ const bareShim = !/[\\/]/.test(cmd) && !/\.exe$/i.test(cmd);
66
+ if (platform === 'win32' && bareShim && !args.some((arg) => /[&|<>^%"]/.test(arg))) {
67
+ const absolute = whichOnPath(cmd, env, platform);
68
+ // The resolved path itself also becomes cmd.exe input. If a PATH directory contains a shell
69
+ // metacharacter, fail via the caller's normal bare-spawn fallback rather than interpret it.
70
+ // Pin cmd.exe to System32 too: CreateProcess-style lookup checks cwd before PATH.
71
+ if (absolute && !/[&|<>^%"]/.test(absolute)) {
72
+ return { cmd: win32.join(systemRoot, 'System32', 'cmd.exe'), args: ['/d', '/s', '/c', absolute, ...args] };
73
+ }
74
+ }
75
+ return { cmd, args };
76
+ }
77
+ //# sourceMappingURL=spawn.js.map
package/dist/util.js CHANGED
@@ -48,9 +48,24 @@ export function openUrl(url) {
48
48
  return false;
49
49
  }
50
50
  }
51
- export function die(msg) {
51
+ export class CliExit extends Error {
52
+ constructor() {
53
+ // Preserve the observable error used by direct command-unit tests that previously mocked
54
+ // process.exit(1) by throwing `Error('exit 1')`.
55
+ super('exit 1');
56
+ this.name = 'CliExit';
57
+ }
58
+ }
59
+ export function fail(msg) {
52
60
  process.stderr.write(`error: ${msg}\n`);
53
- process.exit(1);
61
+ process.exitCode = 1;
62
+ }
63
+ // Stop the current command without forcing Node to tear down active libuv handles. On Windows,
64
+ // process.exit() can race the detached update-check child and abort in src\win\async.c with
65
+ // UV_HANDLE_CLOSING. The guard absorbs CliExit after fail() records the intended exit status.
66
+ export function die(msg) {
67
+ fail(msg);
68
+ throw new CliExit();
54
69
  }
55
70
  export function printJson(v) {
56
71
  process.stdout.write(JSON.stringify(v, null, 2) + '\n');
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "insta",
3
- "version": "0.0.47",
3
+ "version": "0.0.49",
4
4
  "type": "module",
5
5
  "description": "InstaCloud CLI — a thin client of the platform control-plane API.",
6
6
  "keywords": [