insta 0.0.19 → 0.0.21

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.
@@ -38,4 +38,22 @@ export async function branchDelete(name) {
38
38
  return;
39
39
  info(`deleted branch ${name}`);
40
40
  }
41
+ // insta branch merge <source> [--into <target>] — structurally merge source's services into target
42
+ // (default target: current branch). No data is copied; existing services are left untouched.
43
+ export async function branchMerge(source, opts = {}) {
44
+ const api = await ApiClient.load();
45
+ const p = await requireProject();
46
+ const target = opts.into ?? p.branch;
47
+ if (!target)
48
+ throw new Error('no target branch — pass --into <branch> (or link a branch first)');
49
+ const res = await api.rawRequest('POST', `/projects/${p.projectId}/branches/${encodeURIComponent(target)}/merge`, { from: source });
50
+ if (handleApproval(res))
51
+ return;
52
+ const { created = [], skipped = [] } = (res.body ?? {});
53
+ info(`merged ${source} → ${target}: ${created.length} created, ${skipped.length} skipped`);
54
+ for (const c of created)
55
+ info(` + ${c.type}/${c.name}`);
56
+ for (const s of skipped)
57
+ info(` = ${s.type}/${s.name} (${s.reason})`);
58
+ }
41
59
  //# sourceMappingURL=branch.js.map
@@ -1,6 +1,6 @@
1
1
  import { ApiClient, requireProject } from '../api.js';
2
2
  import { info, printJson, handleApproval } from '../util.js';
3
- import { resolveComputeServiceId } from './services.js';
3
+ import { resolveComputeServiceId, q } from './services.js';
4
4
  // Attach a developer-owned custom domain to a branch's compute service. Fly issues the cert + routes
5
5
  // it; the platform returns the DNS records to set in your OWN zone.
6
6
  export async function setDomain(host, opts) {
@@ -46,7 +46,8 @@ function printDomain(r, json) {
46
46
  async function lifecycle(verb, serviceName, opts) {
47
47
  const api = await ApiClient.load();
48
48
  const p = await requireProject();
49
- const { services } = await api.request('GET', `/projects/${p.projectId}/services`);
49
+ const branch = opts.branch ?? p.branch;
50
+ const { services } = await api.request('GET', `/projects/${p.projectId}/services${q(branch)}`);
50
51
  const id = resolveComputeServiceId(services, serviceName);
51
52
  const res = await api.rawRequest('POST', `/projects/${p.projectId}/services/${id}/${verb}`);
52
53
  if (handleApproval(res))
@@ -61,7 +62,8 @@ export const computeSuspend = (service, opts) => lifecycle('suspend', service, o
61
62
  export async function computeStatus(serviceName, opts) {
62
63
  const api = await ApiClient.load();
63
64
  const p = await requireProject();
64
- const { services } = await api.request('GET', `/projects/${p.projectId}/services`);
65
+ const branch = opts.branch ?? p.branch;
66
+ const { services } = await api.request('GET', `/projects/${p.projectId}/services${q(branch)}`);
65
67
  const id = resolveComputeServiceId(services, serviceName);
66
68
  const r = await api.request('GET', `/projects/${p.projectId}/services/${id}/state`);
67
69
  if (opts.json)
@@ -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
@@ -2,6 +2,9 @@
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
+ export function q(branch) {
6
+ return branch ? `?branch=${encodeURIComponent(branch)}` : '';
7
+ }
5
8
  // ---- pure, unit-tested helpers (throw plain Errors; the CLI guard turns them into clean output) ----
6
9
  // Validate a service-type argument against the allowed set for a command.
7
10
  export function assertType(type, allowed = SERVICE_TYPES) {
@@ -38,40 +41,69 @@ export function resolveComputeServiceId(services, name) {
38
41
  return compute[0].id;
39
42
  }
40
43
  // ---- commands ----
41
- export async function servicesAdd(type, name) {
44
+ export async function servicesAdd(type, name, opts = {}) {
42
45
  assertType(type);
46
+ if (opts.public && type !== 'storage')
47
+ throw new Error('--public is only valid for storage services');
43
48
  const api = await ApiClient.load();
44
49
  const p = await requireProject();
45
- const res = await api.rawRequest('POST', `/projects/${p.projectId}/services`, { type, name });
50
+ const branch = opts.branch ?? p.branch;
51
+ const res = await api.rawRequest('POST', `/projects/${p.projectId}/services`, { type, name, ...(branch ? { branch } : {}), public: !!opts.public });
46
52
  if (handleApproval(res))
47
53
  return;
48
54
  const svc = res.body.service;
49
- info(`added ${type} service ${name} (${svc.id})${svc.domain ? ` — ${svc.domain}` : ''}`);
55
+ 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}` : ''}`);
50
57
  renderNextActions(res.body.nextActions);
51
58
  }
52
59
  export async function servicesList(opts) {
53
60
  const api = await ApiClient.load();
54
61
  const p = await requireProject();
55
- const { services } = await api.request('GET', `/projects/${p.projectId}/services`);
62
+ const branch = opts.branch ?? p.branch;
63
+ const { services } = await api.request('GET', `/projects/${p.projectId}/services${q(branch)}`);
56
64
  if (opts.json)
57
65
  return printJson(services);
58
66
  if (!services.length)
59
- return info('(no services — add one with `insta services add <postgres|storage|compute> <name>`)');
67
+ return info(`(no services on ${branch ?? 'default'} — add one with \`insta services add <postgres|storage|compute> <name>\`)`);
60
68
  for (const s of services) {
61
- const extra = s.type === 'compute' ? ` x${s.machine_count}` : '';
69
+ const extra = s.type === 'compute' ? ` x${s.machine_count}` : s.type === 'storage' ? ` ${s.public ? 'public' : 'private'}` : '';
62
70
  info(`${s.type}/${s.name} [${s.status}]${extra}${s.domain ? ` ${s.domain}` : ''} ${s.id}`);
63
71
  }
64
72
  }
65
- export async function servicesRemove(type, name) {
73
+ export async function servicesRemove(type, name, opts = {}) {
66
74
  assertType(type);
67
75
  const api = await ApiClient.load();
68
76
  const p = await requireProject();
69
- const { services } = await api.request('GET', `/projects/${p.projectId}/services`);
77
+ const branch = opts.branch ?? p.branch;
78
+ const { services } = await api.request('GET', `/projects/${p.projectId}/services${q(branch)}`);
70
79
  const id = resolveServiceId(services, type, name);
71
80
  const res = await api.rawRequest('DELETE', `/projects/${p.projectId}/services/${id}`);
72
81
  if (handleApproval(res))
73
82
  return;
74
- info(`removed ${type} service ${name}`);
83
+ info(`removed ${type} service ${name} from ${branch ?? 'default'}`);
84
+ }
85
+ // Validate a bucket access-mode argument.
86
+ export function parseAccess(raw) {
87
+ if (raw === 'public')
88
+ return true;
89
+ if (raw === 'private')
90
+ return false;
91
+ throw new Error(`access must be public|private, got: ${raw}`);
92
+ }
93
+ // insta services set-access storage <name> <public|private>
94
+ export async function servicesSetAccess(type, name, access, _opts) {
95
+ assertType(type, ['storage']);
96
+ const isPublic = parseAccess(access);
97
+ const api = await ApiClient.load();
98
+ const p = await requireProject();
99
+ const { services } = await api.request('GET', `/projects/${p.projectId}/services${q(p.branch)}`);
100
+ const id = resolveServiceId(services, type, name);
101
+ const res = await api.rawRequest('PUT', `/projects/${p.projectId}/services/${id}/access`, { public: isPublic });
102
+ if (handleApproval(res))
103
+ return;
104
+ if (_opts.json)
105
+ return printJson(res.body.service);
106
+ info(`set storage ${name} access to ${access}`);
75
107
  }
76
108
  // insta services scale compute <name> <number> [region]
77
109
  export async function servicesScale(type, name, number, region, _opts) {
@@ -79,7 +111,7 @@ export async function servicesScale(type, name, number, region, _opts) {
79
111
  const machineCount = parseCount(number);
80
112
  const api = await ApiClient.load();
81
113
  const p = await requireProject();
82
- const { services } = await api.request('GET', `/projects/${p.projectId}/services`);
114
+ const { services } = await api.request('GET', `/projects/${p.projectId}/services${q(_opts.branch ?? p.branch)}`);
83
115
  const id = resolveServiceId(services, type, name);
84
116
  const res = await api.rawRequest('POST', `/projects/${p.projectId}/services/${id}/scale`, { machineCount, region });
85
117
  if (handleApproval(res))
@@ -93,7 +125,7 @@ export async function servicesUpgrade(type, name, spec, _opts) {
93
125
  assertType(type, ['compute', 'postgres']);
94
126
  const api = await ApiClient.load();
95
127
  const p = await requireProject();
96
- const { services } = await api.request('GET', `/projects/${p.projectId}/services`);
128
+ const { services } = await api.request('GET', `/projects/${p.projectId}/services${q(_opts.branch ?? p.branch)}`);
97
129
  const id = resolveServiceId(services, type, name);
98
130
  const res = await api.rawRequest('POST', `/projects/${p.projectId}/services/${id}/upgrade`, { spec });
99
131
  if (handleApproval(res))
@@ -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,6 +5,7 @@ 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';
@@ -66,7 +67,14 @@ program.command('run <cmd> [args...]').description('Run a command with the branc
66
67
  const setupCmd = program.command('setup').description('Set up this machine for InstaCloud agent workflows');
67
68
  setupCmd.command('agent').description('Install the insta skill user-globally for all coding agents')
68
69
  .option('-y, --yes', 'non-interactive')
70
+ .option('--mcp-token', 'register the MCP server with a minted insta_ API token instead of OAuth (headless machines / CI)')
69
71
  .action(guard((o) => setup.setupAgent(o)));
72
+ // ---- MCP server integration ----
73
+ const mcpCmd = program.command('mcp').description('insta-cloud remote MCP server integration');
74
+ mcpCmd.command('install').description('Register the remote MCP server with coding agents (default: Claude Code + all detected)')
75
+ .option('--agent <slug>', 'one agent: claude-code, cursor, codex, opencode, copilot, factory-droid')
76
+ .option('--mcp-token', 'claude-code only: minted insta_ API token instead of OAuth (headless machines / CI)')
77
+ .action(guard((o) => mcp.mcpInstall(o)));
70
78
  // ---- org ----
71
79
  const orgCmd = program.command('org').description('Manage organizations');
72
80
  orgCmd.command('list').option('--json').action(guard((o) => org.orgList(o)));
@@ -83,17 +91,25 @@ br.command('create <name>').option('--from <branch>', 'parent branch (default: c
83
91
  br.command('list').option('--json').action(guard((o) => branch.branchList(o)));
84
92
  br.command('switch <name>').action(guard((name) => branch.branchSwitch(name)));
85
93
  br.command('delete <name>').action(guard((name) => branch.branchDelete(name)));
94
+ br.command('merge <source>').description('Merge a branch service set into another (structural, no data)')
95
+ .option('--into <branch>', 'target branch (default: current)').action(guard((source, o) => branch.branchMerge(source, o)));
86
96
  // ---- services (opt-in postgres/storage/compute) ----
87
97
  const svc = program.command('services').alias('svc').description('Manage project services (postgres|storage|compute)');
88
98
  svc.command('add <type> <name>').description('Provision a service on demand (assigns a default domain for postgres/compute)')
89
- .action(guard((type, name) => services.servicesAdd(type, name)));
90
- svc.command('list').option('--json').action(guard((o) => services.servicesList(o)));
99
+ .option('--branch <branch>', 'target branch (default: current)')
100
+ .option('--public', 'storage only: serve the bucket with anonymous public-read (default private)')
101
+ .action(guard((type, name, o) => services.servicesAdd(type, name, o)));
102
+ svc.command('list').option('--json').option('--branch <branch>', 'branch (default: current)')
103
+ .action(guard((o) => services.servicesList(o)));
91
104
  svc.command('remove <type> <name>').description('Remove a service and destroy its resources')
92
- .action(guard((type, name) => services.servicesRemove(type, name)));
105
+ .option('--branch <branch>', 'branch (default: current)')
106
+ .action(guard((type, name, o) => services.servicesRemove(type, name, o)));
107
+ svc.command('set-access <type> <name> <access>').description('Set a storage service bucket access mode (access: public|private)')
108
+ .option('--json').action(guard((type, name, access, o) => services.servicesSetAccess(type, name, access, o)));
93
109
  svc.command('scale <type> <name> <number> [region]').description('Set a compute service machine count (paid plans only)')
94
- .option('--json').action(guard((type, name, number, region, o) => services.servicesScale(type, name, number, region, o)));
110
+ .option('--json').option('--branch <branch>', 'branch (default: current)').action(guard((type, name, number, region, o) => services.servicesScale(type, name, number, region, o)));
95
111
  svc.command('upgrade <type> <name> <spec>').description('Change a compute/postgres service spec (paid plans only)')
96
- .option('--json').action(guard((type, name, spec, o) => services.servicesUpgrade(type, name, spec, o)));
112
+ .option('--json').option('--branch <branch>', 'branch (default: current)').action(guard((type, name, spec, o) => services.servicesUpgrade(type, name, spec, o)));
97
113
  // ---- secrets (seam) ----
98
114
  const sec = program.command('secrets').description('Fetch the credential bundle (secret seam) into .env')
99
115
  .option('--branch <branch>').option('-o, --output <file>', 'output file (default .env)').option('--print', 'print instead of writing').option('--json')
@@ -117,13 +133,13 @@ compute.command('check-domain <host>').description("Show a custom domain's cert
117
133
  compute.command('remove-domain <host>').description('Detach a custom domain (gated: deploy)')
118
134
  .option('--branch <b>').option('--group <g>').action(guard((host, o) => computeCmd.removeDomain(host, o)));
119
135
  compute.command('start [service]').description('Bring a compute service online (persistent — re-enables auto-wake)')
120
- .option('--json').action(guard((service, o) => computeCmd.computeStart(service, o)));
136
+ .option('--json').option('--branch <branch>', 'branch (default: current)').action(guard((service, o) => computeCmd.computeStart(service, o)));
121
137
  compute.command('stop [service]').description('Take a compute service offline; traffic will NOT wake it until `start`')
122
- .option('--json').action(guard((service, o) => computeCmd.computeStop(service, o)));
138
+ .option('--json').option('--branch <branch>', 'branch (default: current)').action(guard((service, o) => computeCmd.computeStop(service, o)));
123
139
  compute.command('suspend [service]').description('Suspend a compute service (RAM snapshot); stays down until `start`')
124
- .option('--json').action(guard((service, o) => computeCmd.computeSuspend(service, o)));
140
+ .option('--json').option('--branch <branch>', 'branch (default: current)').action(guard((service, o) => computeCmd.computeSuspend(service, o)));
125
141
  compute.command('status [service]').description("Show a compute service's desired vs. live state")
126
- .option('--json').action(guard((service, o) => computeCmd.computeStatus(service, o)));
142
+ .option('--json').option('--branch <branch>', 'branch (default: current)').action(guard((service, o) => computeCmd.computeStatus(service, o)));
127
143
  // ---- manifest ----
128
144
  program.command('manifest').description('Print an agent-legible view of the project environments').option('--json').action(guard((o) => manifest(o)));
129
145
  // ---- observability ----
@@ -161,7 +177,7 @@ ob.command('sync').description('Upload findings into the project timeline').acti
161
177
  // ---- policy ----
162
178
  const pol = program.command('policy').description('Governance policy');
163
179
  pol.command('get').option('--json').action(guard((o) => govern.policyGet(o)));
164
- pol.command('set <action> <decision>').description('action: secrets.read|secrets.write|deploy|project.delete|branch.delete|service.add|service.remove|service.scale|service.upgrade; decision: allow|deny|approve').action(guard((a, d) => govern.policySet(a, d)));
180
+ pol.command('set <action> <decision>').description('action: secrets.read|secrets.write|deploy|project.delete|branch.delete|service.add|service.remove|service.scale|service.upgrade|service.setAccess; decision: allow|deny|approve').action(guard((a, d) => govern.policySet(a, d)));
165
181
  // ---- self-update ----
166
182
  program.command('upgrade').description('Update the insta CLI to the latest release (binary or npm install)')
167
183
  .action(guard(() => selfUpdate.upgrade()));
package/package.json CHANGED
@@ -1,8 +1,8 @@
1
1
  {
2
2
  "name": "insta",
3
- "version": "0.0.19",
3
+ "version": "0.0.21",
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",