insta 0.0.20 → 0.0.22

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.
@@ -0,0 +1,136 @@
1
+ // `insta mcp install` — write the insta-cloud remote MCP server into each coding agent's own
2
+ // config format. Claude Code is NOT handled here — it has a real registry CLI (`claude mcp add`,
3
+ // see setup.ts registerMcp); these are the config-file agents. All entries are OAuth (no
4
+ // credential written): each client discovers the platform AS via RFC 9728 and runs the browser
5
+ // flow on first use.
6
+ import { promises as fs } from 'node:fs';
7
+ import { existsSync } from 'node:fs';
8
+ import os from 'node:os';
9
+ import path from 'node:path';
10
+ import { info } from '../util.js';
11
+ import { DEFAULT_MCP_URL, MCP_SERVER_NAME, registerMcp } from './setup.js';
12
+ export const MCP_AGENT_TARGETS = ['cursor', 'codex', 'opencode', 'copilot', 'factory-droid'];
13
+ export function configPath(slug, home) {
14
+ switch (slug) {
15
+ case 'cursor': return path.join(home, '.cursor', 'mcp.json');
16
+ case 'codex': return path.join(home, '.codex', 'config.toml');
17
+ case 'opencode': return path.join(home, '.config', 'opencode', 'opencode.json');
18
+ case 'copilot': return path.join(home, '.copilot', 'mcp-config.json');
19
+ case 'factory-droid': return path.join(home, '.factory', 'mcp.json');
20
+ }
21
+ }
22
+ // An agent counts as "on this machine" when its config dir already exists — we configure what's
23
+ // installed, never scaffold a tool the user doesn't have.
24
+ export function detectAgents(home) {
25
+ return MCP_AGENT_TARGETS.filter((slug) => existsSync(path.dirname(configPath(slug, home))));
26
+ }
27
+ // Merge our entry into existing JSON config. Returns null (skip, leave file alone) when the
28
+ // existing content isn't valid JSON — never clobber a config we can't parse.
29
+ export function renderJsonConfig(slug, existing, url) {
30
+ let root = {};
31
+ if (existing && existing.trim()) {
32
+ try {
33
+ root = JSON.parse(existing);
34
+ }
35
+ catch {
36
+ return null;
37
+ }
38
+ if (typeof root !== 'object' || root === null || Array.isArray(root))
39
+ return null;
40
+ }
41
+ if (slug === 'opencode') {
42
+ // OpenCode: `mcp` key, `type: "remote"` schema (docs.opencode.ai).
43
+ root.mcp = { ...(root.mcp ?? {}), [MCP_SERVER_NAME]: { type: 'remote', url, enabled: true } };
44
+ root.$schema ??= 'https://opencode.ai/config.json';
45
+ }
46
+ else {
47
+ const entry = slug === 'cursor' ? { url } // Cursor auto-detects HTTP from `url`
48
+ : slug === 'copilot' ? { type: 'http', url, tools: ['*'] }
49
+ : { type: 'http', url, disabled: false }; // factory-droid
50
+ root.mcpServers = { ...(root.mcpServers ?? {}), [MCP_SERVER_NAME]: entry };
51
+ }
52
+ return JSON.stringify(root, null, 2) + '\n';
53
+ }
54
+ // Codex config is TOML. Appending a complete `[mcp_servers.<name>]` table is always valid at
55
+ // EOF, so we avoid a TOML parser: string-detect for idempotency, append for install.
56
+ export function renderCodexConfig(existing, url) {
57
+ const base = existing ?? '';
58
+ if (base.includes(`[mcp_servers.${MCP_SERVER_NAME}]`))
59
+ return null; // already configured
60
+ const sep = base.length && !base.endsWith('\n') ? '\n' : '';
61
+ return `${base}${sep}\n[mcp_servers.${MCP_SERVER_NAME}]\nurl = "${url}"\n`;
62
+ }
63
+ // Install for one agent. Returns 'installed' | 'already' | 'skipped' (unparseable config).
64
+ export async function installFor(slug, home, url) {
65
+ const file = configPath(slug, home);
66
+ let existing = null;
67
+ try {
68
+ existing = await fs.readFile(file, 'utf8');
69
+ }
70
+ catch {
71
+ existing = null;
72
+ }
73
+ if (slug === 'codex') {
74
+ const next = renderCodexConfig(existing, url);
75
+ if (next === null)
76
+ return 'already';
77
+ await fs.mkdir(path.dirname(file), { recursive: true });
78
+ await fs.writeFile(file, next);
79
+ return 'installed';
80
+ }
81
+ if (existing) {
82
+ try {
83
+ const root = JSON.parse(existing);
84
+ const entry = slug === 'opencode' ? root?.mcp?.[MCP_SERVER_NAME] : root?.mcpServers?.[MCP_SERVER_NAME];
85
+ if (entry)
86
+ return 'already';
87
+ }
88
+ catch { /* fall through to renderJsonConfig, which refuses to clobber */ }
89
+ }
90
+ const next = renderJsonConfig(slug, existing, url);
91
+ if (next === null)
92
+ return 'skipped';
93
+ await fs.mkdir(path.dirname(file), { recursive: true });
94
+ await fs.writeFile(file, next);
95
+ return 'installed';
96
+ }
97
+ const AGENT_LABELS = {
98
+ cursor: 'Cursor', codex: 'OpenAI Codex', opencode: 'OpenCode', copilot: 'GitHub Copilot', 'factory-droid': 'Factory Droid',
99
+ };
100
+ // Configure every detected config-file agent (or one forced via `agent`). Returns the labels of
101
+ // agents now configured (installed or already present) for the caller's summary line.
102
+ export async function installAgentConfigs(agent, home = os.homedir()) {
103
+ const url = process.env.INSTA_MCP_URL || DEFAULT_MCP_URL;
104
+ const targets = agent
105
+ ? MCP_AGENT_TARGETS.includes(agent) ? [agent] : []
106
+ : detectAgents(home);
107
+ if (agent && targets.length === 0) {
108
+ info(`unknown --agent "${agent}" — supported: claude-code, ${MCP_AGENT_TARGETS.join(', ')}`);
109
+ return [];
110
+ }
111
+ const done = [];
112
+ for (const slug of targets) {
113
+ const result = await installFor(slug, home, url);
114
+ if (result === 'skipped')
115
+ info(` ${AGENT_LABELS[slug]}: existing config at ${configPath(slug, home)} isn't valid JSON — add ${MCP_SERVER_NAME} manually`);
116
+ else
117
+ done.push(AGENT_LABELS[slug]);
118
+ }
119
+ return done;
120
+ }
121
+ // `insta mcp install [--agent <slug>] [--mcp-token]` — claude-code goes through its registry CLI
122
+ // (registerMcp); everything else is a config-file write. No --agent = claude-code + all detected.
123
+ export async function mcpInstall(opts) {
124
+ if (!opts.agent || opts.agent === 'claude-code') {
125
+ await registerMcp(undefined, undefined, !!opts.mcpToken);
126
+ if (opts.agent)
127
+ return;
128
+ }
129
+ const done = await installAgentConfigs(opts.agent);
130
+ if (done.length)
131
+ info(`✓ MCP — configured for ${done.join(', ')} (restart those tools to pick it up)`);
132
+ else if (opts.agent) { /* messages already printed */ }
133
+ else
134
+ info(' no other MCP-capable agents detected (supported: cursor, codex, opencode, copilot, factory-droid)');
135
+ }
136
+ //# sourceMappingURL=mcp.js.map
@@ -0,0 +1,12 @@
1
+ import { ApiClient } from '../api.js';
2
+ import { info, printJson } from '../util.js';
3
+ // insta regions — list the regions a postgres/compute service can be created in.
4
+ export async function regionsList(opts = {}) {
5
+ const api = await ApiClient.load();
6
+ const { regions } = await api.request('GET', '/regions');
7
+ if (opts.json)
8
+ return printJson(regions);
9
+ for (const r of regions)
10
+ info(`${r.slug} ${r.label}`);
11
+ }
12
+ //# sourceMappingURL=regions.js.map
@@ -28,15 +28,61 @@ export async function secrets(opts) {
28
28
  info(` .gitignore += ${out} (credentials must never be committed)`);
29
29
  info(' tip: `insta run -- <cmd>` injects these per-run with nothing written to disk');
30
30
  }
31
+ // Render one branch's service-grouped secrets, then its unbound (branch-level) secrets.
32
+ function renderBranch(b) {
33
+ for (const s of b.services)
34
+ if (s.secrets.length) {
35
+ info(` ${s.type}/${s.name}`);
36
+ for (const n of s.secrets)
37
+ info(` ${n}`);
38
+ }
39
+ if (b.unbound.length) {
40
+ info(' (branch-level)');
41
+ for (const n of b.unbound)
42
+ info(` ${n}`);
43
+ }
44
+ }
45
+ // Show the full binding tree: project-wide, then every branch grouped by service.
46
+ export async function secretsTree(opts) {
47
+ const api = await ApiClient.load();
48
+ const p = await requireProject();
49
+ const res = await api.rawRequest('GET', `/projects/${p.projectId}/secrets/tree`);
50
+ if (handleApproval(res))
51
+ return;
52
+ const tree = res.body;
53
+ if (opts.json)
54
+ return printJson(tree);
55
+ if (tree.projectWide.length) {
56
+ info('(project-wide)');
57
+ for (const n of tree.projectWide)
58
+ info(` ${n}`);
59
+ }
60
+ for (const b of tree.branches) {
61
+ info(`${b.name}${b.isDefault ? ' *' : ''}`);
62
+ renderBranch(b);
63
+ }
64
+ }
65
+ // List secret names for the current (or given) branch, grouped by service.
31
66
  export async function secretsList(opts) {
32
67
  const api = await ApiClient.load();
33
68
  const p = await requireProject();
34
69
  const branch = opts.branch ?? p.branch;
35
- const res = await api.rawRequest('GET', `/projects/${p.projectId}/secrets${q(branch)}`);
70
+ const res = await api.rawRequest('GET', `/projects/${p.projectId}/secrets/tree`);
36
71
  if (handleApproval(res))
37
72
  return;
38
- for (const name of Object.keys(res.body.secrets))
39
- info(name);
73
+ const tree = res.body;
74
+ const b = tree.branches.find((x) => x.name === branch);
75
+ if (opts.json)
76
+ return printJson({ projectWide: tree.projectWide, branch: b });
77
+ if (tree.projectWide.length) {
78
+ info('(project-wide)');
79
+ for (const n of tree.projectWide)
80
+ info(` ${n}`);
81
+ }
82
+ if (b) {
83
+ info(`${b.name}`);
84
+ renderBranch(b);
85
+ }
40
86
  }
41
87
  async function readStdin() {
42
88
  let data = '';
@@ -44,19 +90,21 @@ async function readStdin() {
44
90
  data += chunk;
45
91
  return data.trim();
46
92
  }
47
- // Set a user secret. Project-wide by default; --branch scopes it to one branch. Value comes from
48
- // the argument, or stdin when omitted (keeps secret values out of shell history).
93
+ // Set a user secret. Project-wide by default; --branch scopes it to one branch. --service binds
94
+ // it to a branch service instead, which implies the current branch (binding requires one). Value
95
+ // comes from the argument, or stdin when omitted (keeps secret values out of shell history).
49
96
  export async function secretsSet(name, value, opts) {
50
97
  const api = await ApiClient.load();
51
98
  const p = await requireProject();
52
99
  const v = value ?? (await readStdin());
53
100
  if (!v)
54
101
  die('value is required (pass as an argument or on stdin)');
55
- const payload = opts.branch ? { value: v, branch: opts.branch } : { value: v };
102
+ const branch = opts.service ? (opts.branch ?? p.branch) : opts.branch;
103
+ const payload = { value: v, ...(branch ? { branch } : {}), ...(opts.service ? { service: opts.service } : {}) };
56
104
  const res = await api.rawRequest('PUT', `/projects/${p.projectId}/secrets/${encodeURIComponent(name)}`, payload);
57
105
  if (handleApproval(res))
58
106
  return;
59
- info(`set ${name} (${opts.branch ? `branch ${opts.branch}` : 'project-wide'})`);
107
+ info(`set ${name}${opts.service ? ` → ${opts.service}` : ''} (${branch ? `branch ${branch}` : 'project-wide'})`);
60
108
  }
61
109
  export async function secretsUnset(name, opts) {
62
110
  const api = await ApiClient.load();
@@ -2,6 +2,7 @@
2
2
  import { ApiClient, requireProject } from '../api.js';
3
3
  import { info, printJson, handleApproval, renderNextActions } from '../util.js';
4
4
  export const SERVICE_TYPES = ['postgres', 'storage', 'compute'];
5
+ const SERVICE_NAME_RE = /^[a-z0-9][a-z0-9-]{0,38}$/;
5
6
  export function q(branch) {
6
7
  return branch ? `?branch=${encodeURIComponent(branch)}` : '';
7
8
  }
@@ -11,6 +12,10 @@ export function assertType(type, allowed = SERVICE_TYPES) {
11
12
  if (!allowed.includes(type))
12
13
  throw new Error(`type must be ${allowed.join('|')}`);
13
14
  }
15
+ export function assertServiceName(name) {
16
+ if (!SERVICE_NAME_RE.test(name))
17
+ throw new Error('service name must be lower-kebab (a-z, 0-9, -)');
18
+ }
14
19
  // Parse a positive-integer machine count.
15
20
  export function parseCount(raw) {
16
21
  const n = Number(raw);
@@ -40,22 +45,46 @@ export function resolveComputeServiceId(services, name) {
40
45
  throw new Error(`multiple compute services — specify one: ${compute.map((s) => s.name).join(', ')}`);
41
46
  return compute[0].id;
42
47
  }
43
- // ---- commands ----
48
+ // Map service-add options to the platform POST body. Pure, so it's unit-tested without a network
49
+ // mock (mirrors deployRequestBody in deploy.ts). Validation (which options are valid for which
50
+ // type) stays in servicesAdd, ahead of any network/config access.
51
+ export function servicesAddRequestBody(type, name, branch, opts) {
52
+ return {
53
+ type, name, ...(branch ? { branch } : {}), public: !!opts.public,
54
+ ...(opts.image ? { image: opts.image } : {}), ...(opts.port ? { port: Number(opts.port) } : {}),
55
+ ...(opts.region ? { region: opts.region } : {}),
56
+ };
57
+ }
44
58
  export async function servicesAdd(type, name, opts = {}) {
45
59
  assertType(type);
46
60
  if (opts.public && type !== 'storage')
47
61
  throw new Error('--public is only valid for storage services');
62
+ if (opts.region && type === 'storage')
63
+ throw new Error('--region is not valid for storage services');
64
+ if (opts.image && type !== 'compute')
65
+ throw new Error('--image is only valid for compute services');
66
+ if (opts.port && type !== 'compute')
67
+ throw new Error('--port is only valid for compute services');
48
68
  const api = await ApiClient.load();
49
69
  const p = await requireProject();
50
70
  const branch = opts.branch ?? p.branch;
51
- const res = await api.rawRequest('POST', `/projects/${p.projectId}/services`, { type, name, ...(branch ? { branch } : {}), public: !!opts.public });
71
+ const res = await api.rawRequest('POST', `/projects/${p.projectId}/services`, servicesAddRequestBody(type, name, branch, opts));
52
72
  if (handleApproval(res))
53
73
  return;
54
74
  const svc = res.body.service;
55
75
  const access = svc.type === 'storage' ? ` [${svc.public ? 'public' : 'private'}]` : '';
56
- info(`added ${type} service ${name} on ${branch ?? 'default'} (${svc.id})${access}${svc.domain ? ` — ${svc.domain}` : ''}`);
76
+ const img = svc.image ? ` running ${svc.image}${svc.port ? `:${svc.port}` : ''}` : '';
77
+ info(`added ${type} service ${name} on ${branch ?? 'default'} (${svc.id})${access}${svc.region ? ` ${svc.region}` : ''}${img}${svc.domain ? ` — ${svc.domain}` : ''}`);
57
78
  renderNextActions(res.body.nextActions);
58
79
  }
80
+ // Render one `services list` row. Pure, so it's unit-tested without a network mock (mirrors
81
+ // billingLines in billing.ts). Compute rows show the running image when the platform reports one.
82
+ export function serviceListLine(s) {
83
+ const extra = s.type === 'compute'
84
+ ? ` x${s.machine_count}${s.image ? ` running ${s.image}${s.port ? `:${s.port}` : ''}` : ''}`
85
+ : s.type === 'storage' ? ` ${s.public ? 'public' : 'private'}` : '';
86
+ return `${s.type}/${s.name} [${s.status}]${extra}${s.domain ? ` ${s.domain}` : ''} ${s.id}`;
87
+ }
59
88
  export async function servicesList(opts) {
60
89
  const api = await ApiClient.load();
61
90
  const p = await requireProject();
@@ -65,10 +94,8 @@ export async function servicesList(opts) {
65
94
  return printJson(services);
66
95
  if (!services.length)
67
96
  return info(`(no services on ${branch ?? 'default'} — add one with \`insta services add <postgres|storage|compute> <name>\`)`);
68
- for (const s of services) {
69
- const extra = s.type === 'compute' ? ` x${s.machine_count}` : s.type === 'storage' ? ` ${s.public ? 'public' : 'private'}` : '';
70
- info(`${s.type}/${s.name} [${s.status}]${extra}${s.domain ? ` ${s.domain}` : ''} ${s.id}`);
71
- }
97
+ for (const s of services)
98
+ info(serviceListLine(s));
72
99
  }
73
100
  export async function servicesRemove(type, name, opts = {}) {
74
101
  assertType(type);
@@ -82,6 +109,21 @@ export async function servicesRemove(type, name, opts = {}) {
82
109
  return;
83
110
  info(`removed ${type} service ${name} from ${branch ?? 'default'}`);
84
111
  }
112
+ export async function servicesRename(type, name, newName, opts = {}) {
113
+ assertType(type);
114
+ assertServiceName(newName);
115
+ const api = await ApiClient.load();
116
+ const p = await requireProject();
117
+ const branch = opts.branch ?? p.branch;
118
+ const { services } = await api.request('GET', `/projects/${p.projectId}/services${q(branch)}`);
119
+ const id = resolveServiceId(services, type, name);
120
+ const res = await api.rawRequest('POST', `/projects/${p.projectId}/services/${id}/rename`, { name: newName });
121
+ if (handleApproval(res))
122
+ return;
123
+ if (opts.json)
124
+ return printJson(res.body.service);
125
+ info(`renamed ${type} service ${name} to ${newName}`);
126
+ }
85
127
  // Validate a bucket access-mode argument.
86
128
  export function parseAccess(raw) {
87
129
  if (raw === 'public')
@@ -134,4 +176,22 @@ export async function servicesUpgrade(type, name, spec, _opts) {
134
176
  return printJson(res.body.service);
135
177
  info(`upgraded ${type} ${name} to ${spec}`);
136
178
  }
179
+ // insta services secrets <type> <name> — the secret names bound to a service.
180
+ export async function servicesSecrets(type, name, opts = {}) {
181
+ assertType(type);
182
+ const api = await ApiClient.load();
183
+ const p = await requireProject();
184
+ const { services } = await api.request('GET', `/projects/${p.projectId}/services${q(opts.branch ?? p.branch)}`);
185
+ const id = resolveServiceId(services, type, name);
186
+ const res = await api.rawRequest('GET', `/projects/${p.projectId}/services/${id}/secrets`);
187
+ if (handleApproval(res))
188
+ return;
189
+ const { secrets } = res.body;
190
+ if (opts.json)
191
+ return printJson(secrets);
192
+ if (!secrets.length)
193
+ return info(`(no secrets bound to ${type}/${name})`);
194
+ for (const n of secrets)
195
+ info(n);
196
+ }
137
197
  //# sourceMappingURL=services.js.map
@@ -6,7 +6,10 @@
6
6
  // Stack skills (neon/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 os from 'node:os';
10
+ import { ApiClient } from '../api.js';
9
11
  import { info } from '../util.js';
12
+ import { installAgentConfigs } from './mcp.js';
10
13
  // The `skills` tool we shell out to prints a clack UI: a frame-by-frame clone spinner, an
11
14
  // "Installing to all N agents" banner, a full N-line install-path box, and a third-party
12
15
  // "Security Risk Assessment" that flags our OWN first-party skill as "Critical Risk". Streamed
@@ -89,7 +92,56 @@ const defaultRunner = (cmd, args) => new Promise((resolve) => {
89
92
  // -g = user-level (machine-global); -a '*' = every agent dir the skills tool supports
90
93
  // (Claude Code, Codex, Cursor, OpenCode, Copilot, …); --copy = real files, not cache symlinks.
91
94
  export const SETUP_ARGS = ['skills', 'add', 'InsForge/insta-skills', '-s', 'insta', '-a', '*', '-g', '-y', '--copy'];
92
- export async function setupAgent(opts, run = defaultRunner) {
95
+ // ---- remote MCP registration ----
96
+ export const MCP_SERVER_NAME = 'insta-cloud';
97
+ export const DEFAULT_MCP_URL = 'https://mcp.instacloud.com/mcp';
98
+ const defaultMinter = async () => {
99
+ try {
100
+ const api = await ApiClient.load();
101
+ if (!api.config.accessToken)
102
+ return null;
103
+ const { token } = await api.request('POST', '/tokens', { name: `mcp-${os.hostname()}` });
104
+ return token ?? null;
105
+ }
106
+ catch {
107
+ return null;
108
+ }
109
+ };
110
+ // Register the insta-cloud remote MCP server with Claude Code (user scope, so it follows the
111
+ // machine like the skill install above). Default is OAuth: register with NO credential — the
112
+ // platform's Better Auth MCP authorization server is discovered via RFC 9728 and Claude runs
113
+ // the browser flow on first `/mcp` use, so no static token ever lands on disk. `--mcp-token`
114
+ // is the headless fallback (CI, no browser): mint a durable token into the header instead.
115
+ // Idempotent — an existing registration is left alone. Best-effort: the skill install is the
116
+ // primary outcome; agents without an MCP registry are covered by the skill alone.
117
+ export async function registerMcp(run = defaultRunner, mint = defaultMinter, useToken = false) {
118
+ const url = process.env.INSTA_MCP_URL || DEFAULT_MCP_URL;
119
+ if (!(await run('claude', ['--version'])).ok)
120
+ return; // no Claude Code on this machine
121
+ if ((await run('claude', ['mcp', 'get', MCP_SERVER_NAME])).ok) {
122
+ info(`✓ MCP — ${MCP_SERVER_NAME} already registered with Claude Code`);
123
+ return;
124
+ }
125
+ const args = ['mcp', 'add', '--transport', 'http', '--scope', 'user', MCP_SERVER_NAME, url];
126
+ if (useToken) {
127
+ const token = await mint();
128
+ if (!token) {
129
+ info(' MCP not registered (--mcp-token needs a login) — run `insta login`, then `insta setup agent --mcp-token` again');
130
+ return;
131
+ }
132
+ args.push('--header', `Authorization: Bearer ${token}`);
133
+ }
134
+ const res = await run('claude', args);
135
+ if (res.ok) {
136
+ info(`✓ MCP — ${MCP_SERVER_NAME} registered with Claude Code (\`claude mcp list\` to verify)`);
137
+ if (!useToken)
138
+ info(' first use: run `/mcp` in Claude Code and authorize in the browser (headless machines: `insta setup agent --mcp-token`)');
139
+ }
140
+ else {
141
+ info(` MCP registration failed — add manually:\n claude mcp add --transport http ${MCP_SERVER_NAME} ${url}`);
142
+ }
143
+ }
144
+ export async function setupAgent(opts, run = defaultRunner, mint, installConfigs = installAgentConfigs) {
93
145
  if (!opts.yes && !process.stdout.isTTY) {
94
146
  info('non-interactive shell — assuming -y');
95
147
  }
@@ -111,5 +163,9 @@ export async function setupAgent(opts, run = defaultRunner) {
111
163
  }
112
164
  info(summarizeInstall(res.output ?? ''));
113
165
  info(' every coding agent on this machine now knows InstaCloud (review skills before use — they run with full permissions).');
166
+ await registerMcp(run, mint, !!opts.mcpToken);
167
+ const others = await installConfigs();
168
+ if (others.length)
169
+ info(`✓ MCP — also configured for ${others.join(', ')} (restart those tools to pick it up)`);
114
170
  }
115
171
  //# sourceMappingURL=setup.js.map
package/dist/index.js CHANGED
@@ -5,11 +5,13 @@ import { ApiError } from './api.js';
5
5
  import { die } from './util.js';
6
6
  import * as auth from './commands/auth.js';
7
7
  import * as setup from './commands/setup.js';
8
+ import * as mcp from './commands/mcp.js';
8
9
  import * as runCmd from './commands/run.js';
9
10
  import * as org from './commands/org.js';
10
11
  import * as project from './commands/project.js';
11
12
  import * as branch from './commands/branch.js';
12
13
  import * as services from './commands/services.js';
14
+ import * as regions from './commands/regions.js';
13
15
  import * as secretsCmd from './commands/secrets.js';
14
16
  import { deploy } from './commands/deploy.js';
15
17
  import * as computeCmd from './commands/compute.js';
@@ -66,7 +68,14 @@ program.command('run <cmd> [args...]').description('Run a command with the branc
66
68
  const setupCmd = program.command('setup').description('Set up this machine for InstaCloud agent workflows');
67
69
  setupCmd.command('agent').description('Install the insta skill user-globally for all coding agents')
68
70
  .option('-y, --yes', 'non-interactive')
71
+ .option('--mcp-token', 'register the MCP server with a minted insta_ API token instead of OAuth (headless machines / CI)')
69
72
  .action(guard((o) => setup.setupAgent(o)));
73
+ // ---- MCP server integration ----
74
+ const mcpCmd = program.command('mcp').description('insta-cloud remote MCP server integration');
75
+ mcpCmd.command('install').description('Register the remote MCP server with coding agents (default: Claude Code + all detected)')
76
+ .option('--agent <slug>', 'one agent: claude-code, cursor, codex, opencode, copilot, factory-droid')
77
+ .option('--mcp-token', 'claude-code only: minted insta_ API token instead of OAuth (headless machines / CI)')
78
+ .action(guard((o) => mcp.mcpInstall(o)));
70
79
  // ---- org ----
71
80
  const orgCmd = program.command('org').description('Manage organizations');
72
81
  orgCmd.command('list').option('--json').action(guard((o) => org.orgList(o)));
@@ -89,28 +98,39 @@ br.command('merge <source>').description('Merge a branch service set into anothe
89
98
  const svc = program.command('services').alias('svc').description('Manage project services (postgres|storage|compute)');
90
99
  svc.command('add <type> <name>').description('Provision a service on demand (assigns a default domain for postgres/compute)')
91
100
  .option('--branch <branch>', 'target branch (default: current)')
101
+ .option('--region <region>', 'region for postgres/compute, e.g. us-east (see `insta regions`)')
92
102
  .option('--public', 'storage only: serve the bucket with anonymous public-read (default private)')
103
+ .option('--image <url>', 'compute only: run this container image at creation')
104
+ .option('--port <n>', 'compute only: port the image listens on (default 8080)')
93
105
  .action(guard((type, name, o) => services.servicesAdd(type, name, o)));
94
106
  svc.command('list').option('--json').option('--branch <branch>', 'branch (default: current)')
95
107
  .action(guard((o) => services.servicesList(o)));
96
108
  svc.command('remove <type> <name>').description('Remove a service and destroy its resources')
97
109
  .option('--branch <branch>', 'branch (default: current)')
98
110
  .action(guard((type, name, o) => services.servicesRemove(type, name, o)));
111
+ svc.command('rename <type> <name> <new-name>').description('Rename a service and re-key its managed secret names')
112
+ .option('--json').option('--branch <branch>', 'branch (default: current)')
113
+ .action(guard((type, name, newName, o) => services.servicesRename(type, name, newName, o)));
99
114
  svc.command('set-access <type> <name> <access>').description('Set a storage service bucket access mode (access: public|private)')
100
115
  .option('--json').action(guard((type, name, access, o) => services.servicesSetAccess(type, name, access, o)));
101
116
  svc.command('scale <type> <name> <number> [region]').description('Set a compute service machine count (paid plans only)')
102
117
  .option('--json').option('--branch <branch>', 'branch (default: current)').action(guard((type, name, number, region, o) => services.servicesScale(type, name, number, region, o)));
103
118
  svc.command('upgrade <type> <name> <spec>').description('Change a compute/postgres service spec (paid plans only)')
104
119
  .option('--json').option('--branch <branch>', 'branch (default: current)').action(guard((type, name, spec, o) => services.servicesUpgrade(type, name, spec, o)));
120
+ svc.command('secrets <type> <name>').description("List a service's secret names")
121
+ .option('--branch <b>').option('--json').action(guard((type, name, o) => services.servicesSecrets(type, name, o)));
105
122
  // ---- secrets (seam) ----
106
123
  const sec = program.command('secrets').description('Fetch the credential bundle (secret seam) into .env')
107
124
  .option('--branch <branch>').option('-o, --output <file>', 'output file (default .env)').option('--print', 'print instead of writing').option('--json')
108
125
  .action(guard((o) => secretsCmd.secrets(o)));
109
- sec.command('list').description('List secret names only').option('--branch <branch>').action(guard((o) => secretsCmd.secretsList(o)));
126
+ sec.command('list').description('List secret names, grouped by service').option('--branch <branch>').option('--json').action(guard((o) => secretsCmd.secretsList(o)));
110
127
  sec.command('set <name> [value]').description('Set a user secret (project-wide; value from stdin if omitted)')
111
- .option('--branch <branch>', 'scope to one branch').action(guard((n, v, o) => secretsCmd.secretsSet(n, v, o)));
128
+ .option('--branch <branch>', 'scope to one branch').option('--service <type/name>', 'bind to a branch service (implies current branch)')
129
+ .action(guard((n, v, o) => secretsCmd.secretsSet(n, v, o)));
112
130
  sec.command('unset <name>').description('Remove a user secret')
113
131
  .option('--branch <branch>', 'scope to one branch').action(guard((n, o) => secretsCmd.secretsUnset(n, o)));
132
+ sec.command('tree').description('Show secrets as project → branch → service → secrets').option('--json')
133
+ .action(guard((o) => secretsCmd.secretsTree(o)));
114
134
  // ---- deploy ----
115
135
  program.command('deploy [dir]').description('Deploy a source directory (built remotely on Fly) or a prebuilt --image to a branch compute group')
116
136
  .option('--image <url>', 'prebuilt container image to deploy (instead of a source dir)').option('--branch <b>').option('--group <g>').option('--port <p>')
@@ -134,6 +154,8 @@ compute.command('status [service]').description("Show a compute service's desire
134
154
  .option('--json').option('--branch <branch>', 'branch (default: current)').action(guard((service, o) => computeCmd.computeStatus(service, o)));
135
155
  // ---- manifest ----
136
156
  program.command('manifest').description('Print an agent-legible view of the project environments').option('--json').action(guard((o) => manifest(o)));
157
+ // ---- regions ----
158
+ program.command('regions').description('List regions available for postgres/compute services').option('--json').action(guard((o) => regions.regionsList(o)));
137
159
  // ---- observability ----
138
160
  program.command('metrics <target> [group]').description('Service metrics (target: db|compute)')
139
161
  .option('--branch <b>').option('--from <unix>').option('--to <unix>').option('--step <s>').option('--json')
package/package.json CHANGED
@@ -1,8 +1,8 @@
1
1
  {
2
2
  "name": "insta",
3
- "version": "0.0.20",
3
+ "version": "0.0.22",
4
4
  "type": "module",
5
- "description": "InstaCloud CLI — a thin client of the platform control-plane API.",
5
+ "description": "InstaCloud CLI \u2014 a thin client of the platform control-plane API.",
6
6
  "keywords": [
7
7
  "insta",
8
8
  "insforge",