insta 0.0.42 → 0.0.44

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
@@ -157,7 +157,7 @@ That host is a CloudFront cache, so after a change to the installer it can serve
157
157
  previous copy for up to about a day. This form is equivalent and always current:
158
158
 
159
159
  ```bash
160
- curl -fsSL https://raw.githubusercontent.com/InsForge/insta-cli/main/install.sh | sh -s -- --agents --staging -y
160
+ curl -fsSL https://raw.githubusercontent.com/InsForge/insta-cli/main/install.sh | sh -s -- --agents --staging
161
161
  ```
162
162
 
163
163
  If the environment cannot be applied — an installed CLI older than 0.0.23 has no `insta
@@ -6,7 +6,7 @@
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';
9
+ import { closeSync, createReadStream, existsSync, openSync, readFileSync, statSync } from 'node:fs';
10
10
  import { dirname, join } from 'node:path';
11
11
  import os from 'node:os';
12
12
  import { createInterface } from 'node:readline';
@@ -74,13 +74,13 @@ export function parseInstalledAgents(output) {
74
74
  export function summarizeInstall(output) {
75
75
  const { count, names } = parseInstalledAgents(output);
76
76
  if (count === 0)
77
- return '✓ insta skill installed for your coding agents';
77
+ return '✓ Agents set up';
78
78
  const shown = names.slice(0, 6);
79
79
  const more = count - shown.length;
80
80
  const list = shown.length
81
81
  ? shown.join(', ') + (more > 0 ? `, +${more} more` : '')
82
82
  : `${count} agent${count === 1 ? '' : 's'}`;
83
- return `✓ Agent skills — ${list}`;
83
+ return `✓ Agents — ${list}`;
84
84
  }
85
85
  // ---- CLI self-install (makes `npx -y insta setup agent` a complete one-liner) ----
86
86
  // Under npx the CLI runs from the npm cache and vanishes when the process exits — but the skill
@@ -357,8 +357,55 @@ export function shouldOfferLogin(yes, loggedIn, stdinTty, stdoutTty) {
357
357
  // One Enter continues into the browser login; only an explicit n/no declines. Matches the
358
358
  // curl-installer feel: the single command carries you as far as automation can go, and the one
359
359
  // genuinely human step (authorizing in the browser) starts itself instead of being homework.
360
+ // Where to read the answer from. Under `curl … | sh` stdin is the SCRIPT pipe, not the human —
361
+ // but the controlling terminal can still answer, via /dev/tty (the standard installer trick;
362
+ // Homebrew prompts the same way). Never on Windows (no /dev/tty, and the curl path doesn't exist
363
+ // there), and never without one (agents, CI, cron — openSync fails, so they can't be prompted).
364
+ /** Wrap an already-open fd as a prompt input with SINGLE-OWNER cleanup: the stream owns the fd
365
+ * (autoClose), close() only destroys the stream, and post-close stream errors are swallowed.
366
+ * ReadStream.destroy() closes the fd asynchronously on Node 20, so a second closeSync here
367
+ * would race it into an unhandled EBADF right after the user answers. */
368
+ export function makePromptSource(fd) {
369
+ const stream = createReadStream('', { fd, autoClose: true });
370
+ stream.on('error', () => { });
371
+ return { input: stream, close: () => { try {
372
+ stream.destroy();
373
+ }
374
+ catch { /* already destroyed */ } } };
375
+ }
376
+ const openPromptInput = () => {
377
+ if (process.stdin.isTTY)
378
+ return { input: process.stdin, close: () => { } };
379
+ if (process.platform === 'win32')
380
+ return null;
381
+ try {
382
+ return makePromptSource(openSync('/dev/tty', 'r'));
383
+ }
384
+ catch {
385
+ return null;
386
+ }
387
+ };
388
+ /** Whether a human can answer a prompt at all: an interactive stdin, or a reachable /dev/tty
389
+ * (the curl|sh case). The stdout-TTY requirement lives in shouldOfferLogin — an agent piping
390
+ * our output must never be prompted even though its process may have a controlling terminal. */
391
+ export function canPromptViaTty() {
392
+ if (process.stdin.isTTY)
393
+ return true;
394
+ if (process.platform === 'win32')
395
+ return false;
396
+ try {
397
+ closeSync(openSync('/dev/tty', 'r'));
398
+ return true;
399
+ }
400
+ catch {
401
+ return false;
402
+ }
403
+ }
360
404
  const defaultAsk = async (question) => {
361
- const rl = createInterface({ input: process.stdin, output: process.stdout });
405
+ const src = openPromptInput();
406
+ if (!src)
407
+ return false; // gate said yes but the terminal vanished — decline, never hang
408
+ const rl = createInterface({ input: src.input, output: process.stdout });
362
409
  // EOF (Ctrl-D) closes the interface without ever answering the question — resolve that as a
363
410
  // decline instead of hanging forever after the checkmarks.
364
411
  const answer = await new Promise((resolve) => {
@@ -366,12 +413,13 @@ const defaultAsk = async (question) => {
366
413
  rl.question(question, resolve);
367
414
  });
368
415
  rl.close();
416
+ src.close();
369
417
  return !/^n/i.test(answer.trim());
370
418
  };
371
419
  export async function setupAgent(opts, run = defaultRunner, mint, installConfigs = installAgentConfigs, ensure = (r) => ensureCliInstalled(r), readStored = readPersistedGlobal, switchEnv = (n) => envUse(n), loginFlow = {
372
420
  ask: defaultAsk,
373
421
  login: () => loginOauth('github', {}),
374
- stdinTty: !!process.stdin.isTTY,
422
+ stdinTty: canPromptViaTty(),
375
423
  stdoutTty: !!process.stdout.isTTY,
376
424
  }) {
377
425
  if (!opts.yes && !process.stdout.isTTY) {
@@ -392,8 +440,8 @@ export async function setupAgent(opts, run = defaultRunner, mint, installConfigs
392
440
  const { env, skills } = await resolveEnv();
393
441
  const args = setupArgs(skills);
394
442
  info(env && env !== DEFAULT_ENV
395
- ? `setting up coding-agent skills (${env}) …`
396
- : 'setting up coding-agent skills …');
443
+ ? `setting up your coding agents (${env}) …`
444
+ : 'setting up your coding agents …');
397
445
  const res = await run('npx', args);
398
446
  if (!res.ok) {
399
447
  info(' skill install failed — install manually with:');
@@ -409,9 +457,9 @@ export async function setupAgent(opts, run = defaultRunner, mint, installConfigs
409
457
  process.exitCode = 1;
410
458
  return;
411
459
  }
412
- info(summarizeInstall(res.output ?? ''));
413
- // Register everything first, summarize ONCE below: Claude Code via `claude mcp add`, every other
414
- // agent via a config-file entry — different mechanisms, one outcome, one line.
460
+ // Register everything first, then summarize ONCE below: the skill files, Claude Code's
461
+ // `claude mcp add`, and the config-file MCP entries are three mechanisms with one outcome —
462
+ // "your agents are ready" — so they get one line, not three inventories of agent names.
415
463
  let claude = await registerMcp(run, mint, !!opts.mcpToken, false);
416
464
  const others = await installConfigs();
417
465
  // Default into login on an interactive terminal (see shouldOfferLogin) BEFORE the MCP summary:
@@ -434,21 +482,18 @@ export async function setupAgent(opts, run = defaultRunner, mint, installConfigs
434
482
  }
435
483
  }
436
484
  }
437
- // The one MCP line. The restart note exists because config-file agents only read their config
438
- // at startup; Claude Code picks it up live.
439
- const mcpTargets = [...(claude === 'new' || claude === 'existing' ? ['Claude Code'] : []), ...others];
440
- if (mcpTargets.length) {
441
- info(`✓ MCP — ${mcpTargets.join(', ')}${others.length ? ' (already-running tools load it on restart)' : ''}`);
442
- }
485
+ // THE summary line. The restart note exists because config-file agents only read their MCP
486
+ // config at startup; the skill files need no restart.
487
+ const mcpOk = claude === 'new' || claude === 'existing' || others.length > 0;
488
+ info(`${summarizeInstall(res.output ?? '')} — ready to use InstaCloud${mcpOk ? ' (skill + MCP; restart any open tools)' : ''}`);
443
489
  if (claude === 'new' && !opts.mcpToken) {
444
490
  info(' Claude Code first use: run `/mcp` and authorize in the browser (headless machines: `insta setup agent --mcp-token`)');
445
491
  }
446
- // The checkmarks end the install, but the user's next move shouldn't be guesswork. Agent-first:
447
- // the whole point of setup is that the machine's coding agents now know InstaCloud (skill + MCP)
448
- // and can drive `insta` themselves — project create/link, deploys, and (via the device flow)
449
- // even login. The human's next move is simply to go build.
492
+ // The user's next move: one concrete action, not a concept. The agents drive `insta` themselves
493
+ // (project create/link, deploys, login via the device flow), so the human just asks for the
494
+ // thing they actually want.
450
495
  info(loggedIn
451
- ? 'next: open a coding-agent session in your app directory and keep building — your agents know InstaCloud now'
452
- : 'next: open a coding-agent session in your app directory and keep building — your agents know InstaCloud now (they will walk you through `insta login` when it is needed)');
496
+ ? 'next: open your coding agent inside your app and start building — ask it to "deploy this app on InstaCloud" when you\'re ready'
497
+ : 'next: open your coding agent inside your app and start building — ask it to "deploy this app on InstaCloud" when you\'re ready (it will walk you through `insta login`)');
453
498
  }
454
499
  //# sourceMappingURL=setup.js.map
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "insta",
3
- "version": "0.0.42",
3
+ "version": "0.0.44",
4
4
  "type": "module",
5
5
  "description": "InstaCloud CLI — a thin client of the platform control-plane API.",
6
6
  "keywords": [