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.
- package/dist/commands/mcp.js +136 -0
- package/dist/commands/regions.js +12 -0
- package/dist/commands/secrets.js +55 -7
- package/dist/commands/services.js +67 -7
- package/dist/commands/setup.js +57 -1
- package/dist/index.js +24 -2
- package/package.json +2 -2
|
@@ -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
|
package/dist/commands/secrets.js
CHANGED
|
@@ -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
|
|
70
|
+
const res = await api.rawRequest('GET', `/projects/${p.projectId}/secrets/tree`);
|
|
36
71
|
if (handleApproval(res))
|
|
37
72
|
return;
|
|
38
|
-
|
|
39
|
-
|
|
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.
|
|
48
|
-
//
|
|
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
|
|
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}
|
|
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
|
-
//
|
|
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`,
|
|
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
|
-
|
|
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
|
-
|
|
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
|
package/dist/commands/setup.js
CHANGED
|
@@ -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
|
-
|
|
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
|
|
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').
|
|
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.
|
|
3
|
+
"version": "0.0.22",
|
|
4
4
|
"type": "module",
|
|
5
|
-
"description": "InstaCloud CLI
|
|
5
|
+
"description": "InstaCloud CLI \u2014 a thin client of the platform control-plane API.",
|
|
6
6
|
"keywords": [
|
|
7
7
|
"insta",
|
|
8
8
|
"insforge",
|