insta 0.0.22 → 0.0.23-rc.1
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 +58 -1
- package/dist/commands/auth.js +25 -5
- package/dist/commands/env.js +63 -0
- package/dist/commands/mcp.js +14 -14
- package/dist/commands/metrics.js +23 -0
- package/dist/commands/setup.js +35 -12
- package/dist/config.js +63 -5
- package/dist/ensure-skills.js +19 -3
- package/dist/env.js +52 -0
- package/dist/index.js +11 -2
- package/dist/observe/install.js +4 -1
- package/package.json +2 -2
package/README.md
CHANGED
|
@@ -16,6 +16,26 @@ curl -fsSL https://raw.githubusercontent.com/InsForge/insta-cli/main/install.sh
|
|
|
16
16
|
# Windows: download insta-windows-x64.exe from the releases page.
|
|
17
17
|
```
|
|
18
18
|
|
|
19
|
+
**Agent one-liner** (CLI + agent skills + MCP registration, non-interactive):
|
|
20
|
+
|
|
21
|
+
```bash
|
|
22
|
+
curl -fsSL agents.instacloud.com | sh # production
|
|
23
|
+
curl -fsSL agents.staging.instacloud.com | sh # staging
|
|
24
|
+
```
|
|
25
|
+
|
|
26
|
+
Each installs a complete stack for its environment — CLI build, control plane, MCP registration and
|
|
27
|
+
skill text all match. See [Environments](#environments).
|
|
28
|
+
|
|
29
|
+
> The staging host serves this repo's `agents-staging.sh` from `main`, so it returns 404 until that
|
|
30
|
+
> file is on `main`. Equivalent, and works regardless:
|
|
31
|
+
>
|
|
32
|
+
> ```bash
|
|
33
|
+
> curl -fsSL https://raw.githubusercontent.com/InsForge/insta-cli/main/install.sh | sh -s -- --agents --staging -y
|
|
34
|
+
> ```
|
|
35
|
+
>
|
|
36
|
+
> If the environment can't be applied (a CLI predating `insta env`), the installer exits non-zero
|
|
37
|
+
> rather than silently leaving you on production.
|
|
38
|
+
|
|
19
39
|
**Build from source (requires node):**
|
|
20
40
|
|
|
21
41
|
```bash
|
|
@@ -52,11 +72,48 @@ insta deploy --image <registry/img> # deploy a container image to the current b
|
|
|
52
72
|
insta status # login state + linked project/branch
|
|
53
73
|
```
|
|
54
74
|
|
|
75
|
+
## Environments
|
|
76
|
+
|
|
77
|
+
`prod` and `staging` are **separate deployments** — different regions, different databases,
|
|
78
|
+
different auth. A session from one cannot authenticate against the other, so switching drops the
|
|
79
|
+
stored session and you log in again.
|
|
80
|
+
|
|
81
|
+
| | `prod` (default) | `staging` |
|
|
82
|
+
|---|---|---|
|
|
83
|
+
| control plane | `api.instacloud.com` (us-east-2) | `api.staging.instacloud.com` (us-west-1) |
|
|
84
|
+
| MCP server | `mcp.instacloud.com/mcp` | `mcp.staging.instacloud.com/mcp` |
|
|
85
|
+
| registers as | `insta-cloud` | `insta-cloud-staging` |
|
|
86
|
+
| agent skills | `InsForge/insta-skills` | `InsForge/insta-skills@devel` |
|
|
87
|
+
| CLI channel | latest stable release | newest prerelease (`v*-rc.N`), else stable |
|
|
88
|
+
|
|
89
|
+
```bash
|
|
90
|
+
insta env # show the current environment and everything derived from it
|
|
91
|
+
insta env use staging # switch (persisted to ~/.insta/config.json)
|
|
92
|
+
insta login --env staging --oauth github
|
|
93
|
+
```
|
|
94
|
+
|
|
95
|
+
Control plane, MCP host **and** skill source are all resolved from one switch, so a machine can
|
|
96
|
+
never end up with its CLI on staging while its agents talk to prod and read prod's skill text.
|
|
97
|
+
Distinct MCP registration names mean both environments can be installed side by side.
|
|
98
|
+
|
|
99
|
+
Resolution order, most specific first:
|
|
100
|
+
|
|
101
|
+
1. `INSTA_API_URL` — a literal URL. The only way to reach a host no environment name covers
|
|
102
|
+
(`insta-oss` on localhost, a preview deployment). `INSTA_MCP_URL` and `INSTA_SKILLS_REPO` do the
|
|
103
|
+
same for the MCP host and the skill source.
|
|
104
|
+
2. `INSTA_ENV` — `prod` | `staging`. An unrecognised value is an error, never a silent fallback.
|
|
105
|
+
3. the persisted `apiUrl` in `~/.insta/config.json`.
|
|
106
|
+
4. `prod`.
|
|
107
|
+
|
|
108
|
+
Prereleases never take the `latest` GitHub release or the `latest` npm dist-tag — they publish with
|
|
109
|
+
`--prerelease` and under npm's `next` tag — so a staging build can't reach production installers.
|
|
110
|
+
|
|
55
111
|
## Commands
|
|
56
112
|
|
|
57
113
|
| Command | Description |
|
|
58
114
|
|------|------|
|
|
59
|
-
| `insta login [--email --password --api-url]` | Log in (email/password; tokens auto-refresh) |
|
|
115
|
+
| `insta login [--email --password --api-url --env]` | Log in (email/password; tokens auto-refresh) |
|
|
116
|
+
| `insta env [--json]` / `insta env use <prod\|staging>` | Show or switch deployment environment |
|
|
60
117
|
| `insta login --oauth <github\|google>` | Browser OAuth login (starts a local loopback port; the token is carried back automatically after browser authorization) |
|
|
61
118
|
| `insta logout` / `insta status [--json]` | Log out / show status |
|
|
62
119
|
| `insta org list [--json]` / `org create <name>` | Organizations (each user may own only one free org) |
|
package/dist/commands/auth.js
CHANGED
|
@@ -1,13 +1,28 @@
|
|
|
1
1
|
import { createServer } from 'node:http';
|
|
2
2
|
import { randomBytes } from 'node:crypto';
|
|
3
3
|
import { ApiClient, linkedProject } from '../api.js';
|
|
4
|
+
import { ENVS, ENV_NAMES, envForApiUrl, isEnvName } from '../env.js';
|
|
4
5
|
import { info, die, printJson, promptPassword, openUrl } from '../util.js';
|
|
6
|
+
/** --api-url and --env both set the target host; --api-url wins (more specific), matching the
|
|
7
|
+
* INSTA_API_URL > INSTA_ENV precedence in config.ts. Returns the URL to point at, or undefined
|
|
8
|
+
* to leave whatever is already resolved alone. */
|
|
9
|
+
function targetApiUrl(opts) {
|
|
10
|
+
if (opts.apiUrl)
|
|
11
|
+
return opts.apiUrl;
|
|
12
|
+
if (!opts.env)
|
|
13
|
+
return undefined;
|
|
14
|
+
const want = opts.env.trim().toLowerCase();
|
|
15
|
+
if (!isEnvName(want))
|
|
16
|
+
die(`unknown --env "${opts.env}" — expected one of: ${ENV_NAMES.join(', ')}`);
|
|
17
|
+
return ENVS[want].api;
|
|
18
|
+
}
|
|
5
19
|
export async function login(opts) {
|
|
6
20
|
if (opts.oauth)
|
|
7
21
|
return loginOauth(opts.oauth, opts);
|
|
8
22
|
const api = await ApiClient.load();
|
|
9
|
-
|
|
10
|
-
|
|
23
|
+
const target = targetApiUrl(opts);
|
|
24
|
+
if (target)
|
|
25
|
+
api.setApiUrl(target);
|
|
11
26
|
if (!opts.email)
|
|
12
27
|
die('--email is required (or use --oauth <github|google>)');
|
|
13
28
|
const password = opts.password ?? process.env.INSTA_PASSWORD ?? (await promptPassword());
|
|
@@ -22,8 +37,9 @@ export async function loginOauth(provider, opts) {
|
|
|
22
37
|
if (provider !== 'github' && provider !== 'google')
|
|
23
38
|
die('provider must be github or google');
|
|
24
39
|
const api = await ApiClient.load();
|
|
25
|
-
|
|
26
|
-
|
|
40
|
+
const target = targetApiUrl(opts);
|
|
41
|
+
if (target)
|
|
42
|
+
api.setApiUrl(target);
|
|
27
43
|
const token = await browserOauth(api.apiUrl, provider);
|
|
28
44
|
api.setSession({ accessToken: token, refreshToken: token });
|
|
29
45
|
const me = await api.request('GET', '/me');
|
|
@@ -91,8 +107,12 @@ export async function status(opts) {
|
|
|
91
107
|
}
|
|
92
108
|
catch { /* not logged in */ }
|
|
93
109
|
const project = await linkedProject();
|
|
110
|
+
// Surface the environment name alongside the URL: "api: https://api.staging.instacloud.com" is
|
|
111
|
+
// easy to skim past, and mistaking staging for prod is the mistake worth making loud.
|
|
112
|
+
const env = envForApiUrl(api.apiUrl);
|
|
94
113
|
if (opts.json)
|
|
95
|
-
return printJson({ apiUrl: api.apiUrl, user, project });
|
|
114
|
+
return printJson({ env, apiUrl: api.apiUrl, user, project });
|
|
115
|
+
info(`env: ${env ?? '(custom)'}`);
|
|
96
116
|
info(`api: ${api.apiUrl}`);
|
|
97
117
|
info(`user: ${user ? (user.email ?? user.id) : '(not logged in)'}`);
|
|
98
118
|
info(`project: ${project ? `${project.projectId} (branch ${project.branch})` : '(none linked)'}`);
|
|
@@ -0,0 +1,63 @@
|
|
|
1
|
+
// `insta env` — show or switch the deployment environment (prod | staging).
|
|
2
|
+
//
|
|
3
|
+
// This exists because the canonical install is a pipe: `curl -fsSL agents.staging.instacloud.com | sh`.
|
|
4
|
+
// A piped script cannot export anything into the parent shell, so the staging one-liner has no way
|
|
5
|
+
// to make `INSTA_ENV=staging` stick for the `insta project create` the user runs next. Persisting
|
|
6
|
+
// the choice into ~/.insta/config.json is the only mechanism that survives the pipe — and it is the
|
|
7
|
+
// same file `login --api-url` already writes, so this adds a surface, not a concept.
|
|
8
|
+
import { readPersistedGlobal, resolveEnv, writeGlobal } from '../config.js';
|
|
9
|
+
import { DEFAULT_ENV, ENVS, ENV_NAMES, envForApiUrl, isEnvName, mcpServerName, normalizeUrl } from '../env.js';
|
|
10
|
+
import { die, info, printJson } from '../util.js';
|
|
11
|
+
export async function envShow(opts) {
|
|
12
|
+
const { apiUrl, env, mcpUrl, skills } = await resolveEnv();
|
|
13
|
+
const mcpServer = mcpServerName(env ?? DEFAULT_ENV);
|
|
14
|
+
if (opts.json)
|
|
15
|
+
return printJson({ env, apiUrl, mcpUrl, mcpServer, skills });
|
|
16
|
+
info(`env: ${env ?? '(custom)'}`);
|
|
17
|
+
info(`api: ${apiUrl}`);
|
|
18
|
+
info(`mcp: ${mcpUrl} (${mcpServer})`);
|
|
19
|
+
info(`skills: ${skills}`);
|
|
20
|
+
if (!env)
|
|
21
|
+
info(' (custom apiUrl — `insta env use <name>` to switch to a named environment)');
|
|
22
|
+
}
|
|
23
|
+
export async function envUse(name) {
|
|
24
|
+
const want = name.trim().toLowerCase();
|
|
25
|
+
if (!isEnvName(want))
|
|
26
|
+
die(`unknown environment "${name}" — expected one of: ${ENV_NAMES.join(', ')}`);
|
|
27
|
+
const target = want;
|
|
28
|
+
const nextApi = ENVS[target].api;
|
|
29
|
+
// The PERSISTED config, deliberately not the override-resolved view — see readPersistedGlobal.
|
|
30
|
+
const stored = await readPersistedGlobal();
|
|
31
|
+
const from = envForApiUrl(stored.apiUrl);
|
|
32
|
+
// Compare normalised, so a stored trailing slash is recognised as the same environment (which is
|
|
33
|
+
// how envForApiUrl already treats it) instead of being rewritten as a "switch" that needlessly
|
|
34
|
+
// drops a perfectly good session.
|
|
35
|
+
if (normalizeUrl(stored.apiUrl) === normalizeUrl(nextApi)) {
|
|
36
|
+
info(`already on ${target} (${nextApi})`);
|
|
37
|
+
return;
|
|
38
|
+
}
|
|
39
|
+
// A real switch, so drop the stored session unconditionally. prod and staging are separate
|
|
40
|
+
// deployments: the old token cannot authenticate here, and keeping it is actively unsafe because
|
|
41
|
+
// api.ts's 401 path POSTs the refresh token to whatever apiUrl now resolves to, handing one
|
|
42
|
+
// deployment's credential to another. Same reasoning as the retired-host path in config.ts.
|
|
43
|
+
//
|
|
44
|
+
// "Any field" rather than accessToken alone: a config holding only a refreshToken (an interrupted
|
|
45
|
+
// login, a hand-edited file) would otherwise keep that token and post it to the new host.
|
|
46
|
+
const hadSession = !!(stored.accessToken || stored.refreshToken || stored.user);
|
|
47
|
+
const next = { ...stored, apiUrl: nextApi };
|
|
48
|
+
delete next.accessToken;
|
|
49
|
+
delete next.refreshToken;
|
|
50
|
+
delete next.user;
|
|
51
|
+
await writeGlobal(next);
|
|
52
|
+
info(`switched ${from ?? '(custom)'} → ${target}`);
|
|
53
|
+
info(` api: ${nextApi}`);
|
|
54
|
+
info(` mcp: ${ENVS[target].mcp} (registers as \`${mcpServerName(target)}\`)`);
|
|
55
|
+
if (hadSession)
|
|
56
|
+
info(' previous session dropped (separate deployment) — run `insta login --oauth github`');
|
|
57
|
+
// Switching the CLI does NOT re-point already-installed agents: their MCP registration and skill
|
|
58
|
+
// files were written for the previous environment and are keyed by a different server name, so
|
|
59
|
+
// they keep talking to it until setup is re-run. (The installer path is fine — install.sh runs
|
|
60
|
+
// `env use` before `setup agent`.)
|
|
61
|
+
info(' re-point this machine\'s agents at it with: insta setup agent');
|
|
62
|
+
}
|
|
63
|
+
//# sourceMappingURL=env.js.map
|
package/dist/commands/mcp.js
CHANGED
|
@@ -8,7 +8,7 @@ import { existsSync } from 'node:fs';
|
|
|
8
8
|
import os from 'node:os';
|
|
9
9
|
import path from 'node:path';
|
|
10
10
|
import { info } from '../util.js';
|
|
11
|
-
import {
|
|
11
|
+
import { MCP_SERVER_NAME, registerMcp, resolveMcpTarget } from './setup.js';
|
|
12
12
|
export const MCP_AGENT_TARGETS = ['cursor', 'codex', 'opencode', 'copilot', 'factory-droid'];
|
|
13
13
|
export function configPath(slug, home) {
|
|
14
14
|
switch (slug) {
|
|
@@ -26,7 +26,7 @@ export function detectAgents(home) {
|
|
|
26
26
|
}
|
|
27
27
|
// Merge our entry into existing JSON config. Returns null (skip, leave file alone) when the
|
|
28
28
|
// existing content isn't valid JSON — never clobber a config we can't parse.
|
|
29
|
-
export function renderJsonConfig(slug, existing, url) {
|
|
29
|
+
export function renderJsonConfig(slug, existing, url, name = MCP_SERVER_NAME) {
|
|
30
30
|
let root = {};
|
|
31
31
|
if (existing && existing.trim()) {
|
|
32
32
|
try {
|
|
@@ -40,28 +40,28 @@ export function renderJsonConfig(slug, existing, url) {
|
|
|
40
40
|
}
|
|
41
41
|
if (slug === 'opencode') {
|
|
42
42
|
// OpenCode: `mcp` key, `type: "remote"` schema (docs.opencode.ai).
|
|
43
|
-
root.mcp = { ...(root.mcp ?? {}), [
|
|
43
|
+
root.mcp = { ...(root.mcp ?? {}), [name]: { type: 'remote', url, enabled: true } };
|
|
44
44
|
root.$schema ??= 'https://opencode.ai/config.json';
|
|
45
45
|
}
|
|
46
46
|
else {
|
|
47
47
|
const entry = slug === 'cursor' ? { url } // Cursor auto-detects HTTP from `url`
|
|
48
48
|
: slug === 'copilot' ? { type: 'http', url, tools: ['*'] }
|
|
49
49
|
: { type: 'http', url, disabled: false }; // factory-droid
|
|
50
|
-
root.mcpServers = { ...(root.mcpServers ?? {}), [
|
|
50
|
+
root.mcpServers = { ...(root.mcpServers ?? {}), [name]: entry };
|
|
51
51
|
}
|
|
52
52
|
return JSON.stringify(root, null, 2) + '\n';
|
|
53
53
|
}
|
|
54
54
|
// Codex config is TOML. Appending a complete `[mcp_servers.<name>]` table is always valid at
|
|
55
55
|
// EOF, so we avoid a TOML parser: string-detect for idempotency, append for install.
|
|
56
|
-
export function renderCodexConfig(existing, url) {
|
|
56
|
+
export function renderCodexConfig(existing, url, name = MCP_SERVER_NAME) {
|
|
57
57
|
const base = existing ?? '';
|
|
58
|
-
if (base.includes(`[mcp_servers.${
|
|
58
|
+
if (base.includes(`[mcp_servers.${name}]`))
|
|
59
59
|
return null; // already configured
|
|
60
60
|
const sep = base.length && !base.endsWith('\n') ? '\n' : '';
|
|
61
|
-
return `${base}${sep}\n[mcp_servers.${
|
|
61
|
+
return `${base}${sep}\n[mcp_servers.${name}]\nurl = "${url}"\n`;
|
|
62
62
|
}
|
|
63
63
|
// Install for one agent. Returns 'installed' | 'already' | 'skipped' (unparseable config).
|
|
64
|
-
export async function installFor(slug, home, url) {
|
|
64
|
+
export async function installFor(slug, home, url, name = MCP_SERVER_NAME) {
|
|
65
65
|
const file = configPath(slug, home);
|
|
66
66
|
let existing = null;
|
|
67
67
|
try {
|
|
@@ -71,7 +71,7 @@ export async function installFor(slug, home, url) {
|
|
|
71
71
|
existing = null;
|
|
72
72
|
}
|
|
73
73
|
if (slug === 'codex') {
|
|
74
|
-
const next = renderCodexConfig(existing, url);
|
|
74
|
+
const next = renderCodexConfig(existing, url, name);
|
|
75
75
|
if (next === null)
|
|
76
76
|
return 'already';
|
|
77
77
|
await fs.mkdir(path.dirname(file), { recursive: true });
|
|
@@ -81,13 +81,13 @@ export async function installFor(slug, home, url) {
|
|
|
81
81
|
if (existing) {
|
|
82
82
|
try {
|
|
83
83
|
const root = JSON.parse(existing);
|
|
84
|
-
const entry = slug === 'opencode' ? root?.mcp?.[
|
|
84
|
+
const entry = slug === 'opencode' ? root?.mcp?.[name] : root?.mcpServers?.[name];
|
|
85
85
|
if (entry)
|
|
86
86
|
return 'already';
|
|
87
87
|
}
|
|
88
88
|
catch { /* fall through to renderJsonConfig, which refuses to clobber */ }
|
|
89
89
|
}
|
|
90
|
-
const next = renderJsonConfig(slug, existing, url);
|
|
90
|
+
const next = renderJsonConfig(slug, existing, url, name);
|
|
91
91
|
if (next === null)
|
|
92
92
|
return 'skipped';
|
|
93
93
|
await fs.mkdir(path.dirname(file), { recursive: true });
|
|
@@ -100,7 +100,7 @@ const AGENT_LABELS = {
|
|
|
100
100
|
// Configure every detected config-file agent (or one forced via `agent`). Returns the labels of
|
|
101
101
|
// agents now configured (installed or already present) for the caller's summary line.
|
|
102
102
|
export async function installAgentConfigs(agent, home = os.homedir()) {
|
|
103
|
-
const url =
|
|
103
|
+
const { name, url } = await resolveMcpTarget();
|
|
104
104
|
const targets = agent
|
|
105
105
|
? MCP_AGENT_TARGETS.includes(agent) ? [agent] : []
|
|
106
106
|
: detectAgents(home);
|
|
@@ -110,9 +110,9 @@ export async function installAgentConfigs(agent, home = os.homedir()) {
|
|
|
110
110
|
}
|
|
111
111
|
const done = [];
|
|
112
112
|
for (const slug of targets) {
|
|
113
|
-
const result = await installFor(slug, home, url);
|
|
113
|
+
const result = await installFor(slug, home, url, name);
|
|
114
114
|
if (result === 'skipped')
|
|
115
|
-
info(` ${AGENT_LABELS[slug]}: existing config at ${configPath(slug, home)} isn't valid JSON — add ${
|
|
115
|
+
info(` ${AGENT_LABELS[slug]}: existing config at ${configPath(slug, home)} isn't valid JSON — add ${name} manually`);
|
|
116
116
|
else
|
|
117
117
|
done.push(AGENT_LABELS[slug]);
|
|
118
118
|
}
|
package/dist/commands/metrics.js
CHANGED
|
@@ -77,10 +77,33 @@ export async function usage(opts) {
|
|
|
77
77
|
info(` ${pr.name}: $${Number(pr.totalCostUsd ?? 0).toFixed(4)}`);
|
|
78
78
|
}
|
|
79
79
|
}
|
|
80
|
+
// pure: platform path for a compute deploy-events request (used by `insta logs --deploy`).
|
|
81
|
+
export function deployEventsPath(projectId, opts) {
|
|
82
|
+
return `/projects/${projectId}/deploy-events${qs({ group: opts.group, branch: opts.branch, limit: opts.limit, instance: opts.instance })}`;
|
|
83
|
+
}
|
|
84
|
+
// pure: render one deploy event as a log-style line.
|
|
85
|
+
export function deployEventLine(ev) {
|
|
86
|
+
const inst = ev.instance ? ` (${ev.instance})` : '';
|
|
87
|
+
return `${ev.ts ?? ''} [${ev.origin ?? ''}] ${ev.type ?? ''}: ${ev.status ?? ''}${inst}`;
|
|
88
|
+
}
|
|
80
89
|
// insta logs <db|compute> [group]
|
|
81
90
|
export async function logs(component, group, opts) {
|
|
82
91
|
const api = await ApiClient.load();
|
|
83
92
|
const p = await requireProject();
|
|
93
|
+
if (opts.deploy) {
|
|
94
|
+
if (component !== 'compute')
|
|
95
|
+
return info('deploy events are only available for compute');
|
|
96
|
+
const res = await api.request('GET', deployEventsPath(p.projectId, { group, branch: opts.branch ?? p.branch, limit: opts.limit, instance: opts.instance }));
|
|
97
|
+
if (opts.json)
|
|
98
|
+
return printJson(res);
|
|
99
|
+
if (res.note)
|
|
100
|
+
info(`note: ${res.note}`);
|
|
101
|
+
if (!res.events?.length)
|
|
102
|
+
return info('(no deploy events)');
|
|
103
|
+
for (const ev of res.events)
|
|
104
|
+
info(deployEventLine(ev));
|
|
105
|
+
return;
|
|
106
|
+
}
|
|
84
107
|
const res = await api.request('GET', `/projects/${p.projectId}/logs${qs({ component, group, branch: opts.branch ?? p.branch, limit: opts.limit, region: opts.region, instance: opts.instance })}`);
|
|
85
108
|
if (opts.json)
|
|
86
109
|
return printJson(res);
|
package/dist/commands/setup.js
CHANGED
|
@@ -8,6 +8,8 @@
|
|
|
8
8
|
import { spawn } from 'node:child_process';
|
|
9
9
|
import os from 'node:os';
|
|
10
10
|
import { ApiClient } from '../api.js';
|
|
11
|
+
import { resolveEnv } from '../config.js';
|
|
12
|
+
import { DEFAULT_ENV, ENVS, mcpServerName } from '../env.js';
|
|
11
13
|
import { info } from '../util.js';
|
|
12
14
|
import { installAgentConfigs } from './mcp.js';
|
|
13
15
|
// The `skills` tool we shell out to prints a clack UI: a frame-by-frame clone spinner, an
|
|
@@ -91,10 +93,25 @@ const defaultRunner = (cmd, args) => new Promise((resolve) => {
|
|
|
91
93
|
});
|
|
92
94
|
// -g = user-level (machine-global); -a '*' = every agent dir the skills tool supports
|
|
93
95
|
// (Claude Code, Codex, Cursor, OpenCode, Copilot, …); --copy = real files, not cache symlinks.
|
|
94
|
-
|
|
96
|
+
// `spec` is the skill source for the resolved environment (`owner/repo` or `owner/repo@ref`), so a
|
|
97
|
+
// staging install reads the staging skill text rather than what's published on main.
|
|
98
|
+
export const setupArgs = (spec) => ['skills', 'add', spec, '-s', 'insta', '-a', '*', '-g', '-y', '--copy'];
|
|
99
|
+
/** Production's args. Kept as a named export because it is the installed-base default and is
|
|
100
|
+
* asserted directly by tests; runtime goes through `setupArgs(resolveEnv().skills)`. */
|
|
101
|
+
export const SETUP_ARGS = setupArgs(ENVS[DEFAULT_ENV].skills);
|
|
95
102
|
// ---- remote MCP registration ----
|
|
96
|
-
|
|
97
|
-
|
|
103
|
+
// Prod's name/URL, kept as named exports because they are the installed-base defaults and are
|
|
104
|
+
// asserted directly by tests. Everything at runtime goes through `resolveMcpTarget()` instead, so
|
|
105
|
+
// a staging install registers staging's MCP server under its own name rather than reusing prod's.
|
|
106
|
+
export const MCP_SERVER_NAME = mcpServerName(DEFAULT_ENV);
|
|
107
|
+
export const DEFAULT_MCP_URL = ENVS[DEFAULT_ENV].mcp;
|
|
108
|
+
/** The MCP server this machine should register, derived from the SAME resolved environment as the
|
|
109
|
+
* control-plane API. Returning name and url together is deliberate: they must never be chosen
|
|
110
|
+
* independently, or a staging machine ends up registering prod's URL under prod's name. */
|
|
111
|
+
export async function resolveMcpTarget() {
|
|
112
|
+
const { env, mcpUrl } = await resolveEnv();
|
|
113
|
+
return { name: mcpServerName(env ?? DEFAULT_ENV), url: mcpUrl };
|
|
114
|
+
}
|
|
98
115
|
const defaultMinter = async () => {
|
|
99
116
|
try {
|
|
100
117
|
const api = await ApiClient.load();
|
|
@@ -115,14 +132,14 @@ const defaultMinter = async () => {
|
|
|
115
132
|
// Idempotent — an existing registration is left alone. Best-effort: the skill install is the
|
|
116
133
|
// primary outcome; agents without an MCP registry are covered by the skill alone.
|
|
117
134
|
export async function registerMcp(run = defaultRunner, mint = defaultMinter, useToken = false) {
|
|
118
|
-
const url =
|
|
135
|
+
const { name, url } = await resolveMcpTarget();
|
|
119
136
|
if (!(await run('claude', ['--version'])).ok)
|
|
120
137
|
return; // no Claude Code on this machine
|
|
121
|
-
if ((await run('claude', ['mcp', 'get',
|
|
122
|
-
info(`✓ MCP — ${
|
|
138
|
+
if ((await run('claude', ['mcp', 'get', name])).ok) {
|
|
139
|
+
info(`✓ MCP — ${name} already registered with Claude Code`);
|
|
123
140
|
return;
|
|
124
141
|
}
|
|
125
|
-
const args = ['mcp', 'add', '--transport', 'http', '--scope', 'user',
|
|
142
|
+
const args = ['mcp', 'add', '--transport', 'http', '--scope', 'user', name, url];
|
|
126
143
|
if (useToken) {
|
|
127
144
|
const token = await mint();
|
|
128
145
|
if (!token) {
|
|
@@ -133,23 +150,29 @@ export async function registerMcp(run = defaultRunner, mint = defaultMinter, use
|
|
|
133
150
|
}
|
|
134
151
|
const res = await run('claude', args);
|
|
135
152
|
if (res.ok) {
|
|
136
|
-
info(`✓ MCP — ${
|
|
153
|
+
info(`✓ MCP — ${name} registered with Claude Code (\`claude mcp list\` to verify)`);
|
|
137
154
|
if (!useToken)
|
|
138
155
|
info(' first use: run `/mcp` in Claude Code and authorize in the browser (headless machines: `insta setup agent --mcp-token`)');
|
|
139
156
|
}
|
|
140
157
|
else {
|
|
141
|
-
info(` MCP registration failed — add manually:\n claude mcp add --transport http ${
|
|
158
|
+
info(` MCP registration failed — add manually:\n claude mcp add --transport http ${name} ${url}`);
|
|
142
159
|
}
|
|
143
160
|
}
|
|
144
161
|
export async function setupAgent(opts, run = defaultRunner, mint, installConfigs = installAgentConfigs) {
|
|
145
162
|
if (!opts.yes && !process.stdout.isTTY) {
|
|
146
163
|
info('non-interactive shell — assuming -y');
|
|
147
164
|
}
|
|
148
|
-
|
|
149
|
-
|
|
165
|
+
// One resolve for the whole step, so the skills and the MCP registration below cannot disagree
|
|
166
|
+
// about which environment this machine belongs to.
|
|
167
|
+
const { env, skills } = await resolveEnv();
|
|
168
|
+
const args = setupArgs(skills);
|
|
169
|
+
info(env && env !== DEFAULT_ENV
|
|
170
|
+
? `setting up coding-agent skills (${env}) …`
|
|
171
|
+
: 'setting up coding-agent skills …');
|
|
172
|
+
const res = await run('npx', args);
|
|
150
173
|
if (!res.ok) {
|
|
151
174
|
info(' skill install failed — install manually with:');
|
|
152
|
-
info(
|
|
175
|
+
info(` npx ${args.map((a) => (a === '*' ? '"*"' : a)).join(' ')}`);
|
|
153
176
|
// Surface the REAL error: the captured tail, minus the expected no-global-support noise.
|
|
154
177
|
const tail = (res.output ?? '')
|
|
155
178
|
.split('\n')
|
package/dist/config.js
CHANGED
|
@@ -2,6 +2,7 @@
|
|
|
2
2
|
import { homedir } from 'node:os';
|
|
3
3
|
import { dirname, join, resolve } from 'node:path';
|
|
4
4
|
import { mkdir, readFile, writeFile } from 'node:fs/promises';
|
|
5
|
+
import { DEFAULT_ENV, ENVS, envForApiUrl, envFromEnvVar, normalizeUrl } from './env.js';
|
|
5
6
|
const GLOBAL_DIR = join(homedir(), '.insta');
|
|
6
7
|
const GLOBAL_FILE = join(GLOBAL_DIR, 'config.json');
|
|
7
8
|
const PROJECT_DIR = '.insta';
|
|
@@ -9,17 +10,74 @@ const PROJECT_FILE = 'project.json';
|
|
|
9
10
|
// The cloud API default. Uses the instacloud.com brand domain (matches the agents.instacloud.com
|
|
10
11
|
// onboarding), NOT the legacy beta-api.insta.insforge.dev host — same backend, branded domain.
|
|
11
12
|
// Only affects fresh installs: a persisted apiUrl (from a prior login) or INSTA_API_URL wins below.
|
|
12
|
-
const DEFAULT_API =
|
|
13
|
+
const DEFAULT_API = ENVS[DEFAULT_ENV].api;
|
|
13
14
|
export async function readGlobal() {
|
|
14
|
-
//
|
|
15
|
-
//
|
|
15
|
+
// Precedence, most explicit first:
|
|
16
|
+
// 1. INSTA_API_URL — a literal URL. Overrides the persisted apiUrl, not just the default,
|
|
17
|
+
// otherwise the env var is silently ignored as soon as any login has written a config file.
|
|
18
|
+
// It also outranks INSTA_ENV: a hand-written URL is the more specific instruction, and it
|
|
19
|
+
// is the only way to reach a host no environment name covers (insta-oss, a preview).
|
|
20
|
+
// 2. INSTA_ENV — a named environment (see env.ts), resolved to its api host.
|
|
21
|
+
// 3. the persisted apiUrl, written by `insta login --env|--api-url` or `insta env use`.
|
|
22
|
+
// 4. DEFAULT_API.
|
|
16
23
|
const envApi = process.env.INSTA_API_URL;
|
|
24
|
+
const named = envFromEnvVar();
|
|
25
|
+
const override = envApi ?? (named ? ENVS[named].api : undefined);
|
|
17
26
|
try {
|
|
18
27
|
const parsed = JSON.parse(await readFile(GLOBAL_FILE, 'utf8'));
|
|
19
|
-
|
|
28
|
+
const persisted = parsed.apiUrl ?? DEFAULT_API;
|
|
29
|
+
// An override that points at a DIFFERENT deployment than the stored session was minted for
|
|
30
|
+
// must not carry that session along. `env use` already drops it on an explicit switch; without
|
|
31
|
+
// this, `INSTA_ENV=staging insta …` on a prod-logged-in machine sends prod's bearer to staging
|
|
32
|
+
// and then — on the 401 — POSTs prod's REFRESH token to staging's /auth/refresh (api.ts), which
|
|
33
|
+
// is the cross-deployment credential leak env.ts's header calls out as never allowed.
|
|
34
|
+
//
|
|
35
|
+
// In-memory only: the file keeps the real login, so unsetting the override restores it. A
|
|
36
|
+
// custom host (insta-oss, a preview) is treated the same way — its session is equally foreign.
|
|
37
|
+
if (override && normalizeUrl(override) !== normalizeUrl(persisted)) {
|
|
38
|
+
const scrubbed = { ...parsed, apiUrl: override };
|
|
39
|
+
delete scrubbed.accessToken;
|
|
40
|
+
delete scrubbed.refreshToken;
|
|
41
|
+
delete scrubbed.user;
|
|
42
|
+
return scrubbed;
|
|
43
|
+
}
|
|
44
|
+
return { ...parsed, apiUrl: override ?? persisted };
|
|
45
|
+
}
|
|
46
|
+
catch {
|
|
47
|
+
return { apiUrl: override ?? DEFAULT_API };
|
|
48
|
+
}
|
|
49
|
+
}
|
|
50
|
+
/** The environment the CLI is currently pointed at, plus everything derived from it. `env` is null
|
|
51
|
+
* when apiUrl is a custom host (insta-oss, a preview deployment) — deliberate, and left alone.
|
|
52
|
+
*
|
|
53
|
+
* API host, MCP host, and skill source are resolved from ONE environment on purpose: the failure
|
|
54
|
+
* mode of picking them independently is silent (a machine whose CLI talks to staging while its
|
|
55
|
+
* agents are wired to prod and reading prod's skill text). */
|
|
56
|
+
export async function resolveEnv() {
|
|
57
|
+
const { apiUrl } = await readGlobal();
|
|
58
|
+
const env = envForApiUrl(apiUrl);
|
|
59
|
+
const hosts = ENVS[env ?? DEFAULT_ENV];
|
|
60
|
+
// Each single-purpose env var still wins outright, for a self-hosted MCP / a tunnel / a skills
|
|
61
|
+
// fork. A custom apiUrl with none of them set falls back to the default environment, since
|
|
62
|
+
// there is nothing better to guess and it preserves today's behaviour.
|
|
63
|
+
const mcpUrl = process.env.INSTA_MCP_URL || hosts.mcp;
|
|
64
|
+
const skills = process.env.INSTA_SKILLS_REPO || hosts.skills;
|
|
65
|
+
return { apiUrl, env, mcpUrl, skills };
|
|
66
|
+
}
|
|
67
|
+
/** The config exactly as stored: no env-var overrides, no session scrubbing.
|
|
68
|
+
*
|
|
69
|
+
* `env use` must read this rather than `readGlobal()`. With INSTA_ENV set, `readGlobal()` already
|
|
70
|
+
* reports the override's host, so `env use <that same env>` would look like a no-op, print
|
|
71
|
+
* "already on X" and never write the file — leaving the next process (without the override in its
|
|
72
|
+
* environment) still pointed at the old one. Deciding "is this a real switch?" has to be done
|
|
73
|
+
* against what is persisted. */
|
|
74
|
+
export async function readPersistedGlobal() {
|
|
75
|
+
try {
|
|
76
|
+
const parsed = JSON.parse(await readFile(GLOBAL_FILE, 'utf8'));
|
|
77
|
+
return { ...parsed, apiUrl: parsed.apiUrl ?? DEFAULT_API };
|
|
20
78
|
}
|
|
21
79
|
catch {
|
|
22
|
-
return { apiUrl:
|
|
80
|
+
return { apiUrl: DEFAULT_API };
|
|
23
81
|
}
|
|
24
82
|
}
|
|
25
83
|
export async function writeGlobal(c) {
|
package/dist/ensure-skills.js
CHANGED
|
@@ -7,6 +7,8 @@
|
|
|
7
7
|
import { spawn } from 'node:child_process';
|
|
8
8
|
import { existsSync, readFileSync, writeFileSync } from 'node:fs';
|
|
9
9
|
import { join } from 'node:path';
|
|
10
|
+
import { resolveEnv } from './config.js';
|
|
11
|
+
import { DEFAULT_ENV, ENVS } from './env.js';
|
|
10
12
|
// Where `npx skills add` drops skills for the agents we pin below: Claude Code → .claude/skills/,
|
|
11
13
|
// Codex → .agents/skills/ (.github/skills/ is the third well-known dir). These are regenerable
|
|
12
14
|
// agent context, not the developer's source — keep them out of git.
|
|
@@ -28,8 +30,11 @@ const defaultRunner = (cmd, args, inherit = false) => new Promise((resolve) => {
|
|
|
28
30
|
// name the exact skills (-s …) so there's no skill picker; -y to skip the scope/confirm prompt;
|
|
29
31
|
// --copy to write real files (not symlinks into a transient npx cache).
|
|
30
32
|
const AGENT_FLAGS = ['-a', 'claude-code', '-a', 'codex', '-y', '--copy'];
|
|
31
|
-
|
|
32
|
-
|
|
33
|
+
// `instaSpec` is the insta skill source for the resolved environment (`owner/repo[@ref]`), so a
|
|
34
|
+
// project created against staging gets the staging skill text. The third-party stack skills are
|
|
35
|
+
// environment-independent — they document Neon/Tigris/Better Auth, not our control plane.
|
|
36
|
+
const skillTargets = (instaSpec) => [
|
|
37
|
+
{ label: 'insta', args: ['skills', 'add', instaSpec, '-s', 'insta', ...AGENT_FLAGS] },
|
|
33
38
|
{ label: 'neon-postgres', args: ['skills', 'add', 'neondatabase/agent-skills', '-s', 'neon-postgres', ...AGENT_FLAGS] },
|
|
34
39
|
{ label: 'tigris', args: ['skills', 'add', 'tigrisdata/skills',
|
|
35
40
|
'-s', 'tigris-object-operations', '-s', 'file-storage', '-s', 'tigris-sdk-guide',
|
|
@@ -40,6 +45,8 @@ const SKILLS = [
|
|
|
40
45
|
'-s', 'better-auth-best-practices', '-s', 'email-and-password-best-practices',
|
|
41
46
|
'-s', 'better-auth-security-best-practices', ...AGENT_FLAGS] },
|
|
42
47
|
];
|
|
48
|
+
/** Production's targets — kept for tests and as the fallback when no env resolves. */
|
|
49
|
+
const SKILLS = skillTargets(ENVS[DEFAULT_ENV].skills);
|
|
43
50
|
// Install all related skills. Production omits `run`/`print` → the real spawn + stdout; tests inject
|
|
44
51
|
// a fake runner and capture output. Continues past a per-skill failure so one bad repo doesn't skip
|
|
45
52
|
// the rest, and never throws.
|
|
@@ -47,8 +54,17 @@ export async function installSkills(deps) {
|
|
|
47
54
|
const run = deps.run ?? defaultRunner;
|
|
48
55
|
const print = deps.print ?? ((s) => process.stdout.write(s + '\n'));
|
|
49
56
|
try {
|
|
57
|
+
// Resolve once per call so the insta skill follows this machine's environment. Falls back to
|
|
58
|
+
// production's targets if anything about the resolve fails — a bad read must not skip the
|
|
59
|
+
// whole best-effort install.
|
|
60
|
+
let targets = SKILLS;
|
|
61
|
+
try {
|
|
62
|
+
const { skills } = await resolveEnv();
|
|
63
|
+
targets = skillTargets(skills);
|
|
64
|
+
}
|
|
65
|
+
catch { /* keep production defaults */ }
|
|
50
66
|
print(' installing related agent skills (insta, neon-postgres, tigris, better-auth) …');
|
|
51
|
-
for (const s of
|
|
67
|
+
for (const s of targets) {
|
|
52
68
|
// Don't stream: the `skills` tool's clack UI (clone spinner, banners) is noise. Run it
|
|
53
69
|
// silent (stdio 'ignore') and let the per-skill ✓/failed line below be the clean output —
|
|
54
70
|
// it appears as each skill finishes, so there's still live progress. (Also avoids the
|
package/dist/env.js
ADDED
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
export const ENVS = {
|
|
2
|
+
prod: {
|
|
3
|
+
api: 'https://api.instacloud.com',
|
|
4
|
+
mcp: 'https://mcp.instacloud.com/mcp',
|
|
5
|
+
skills: 'InsForge/insta-skills',
|
|
6
|
+
},
|
|
7
|
+
staging: {
|
|
8
|
+
api: 'https://api.staging.instacloud.com',
|
|
9
|
+
mcp: 'https://mcp.staging.instacloud.com/mcp',
|
|
10
|
+
skills: 'InsForge/insta-skills#devel',
|
|
11
|
+
},
|
|
12
|
+
};
|
|
13
|
+
export const DEFAULT_ENV = 'prod';
|
|
14
|
+
export const ENV_NAMES = Object.keys(ENVS);
|
|
15
|
+
export function isEnvName(v) {
|
|
16
|
+
return ENV_NAMES.includes(v);
|
|
17
|
+
}
|
|
18
|
+
/** Strip trailing slashes so 'https://x/' and 'https://x' compare equal. */
|
|
19
|
+
export const normalizeUrl = (url) => url.replace(/\/+$/, '');
|
|
20
|
+
/** The environment a persisted apiUrl belongs to, or null for a custom/self-hosted host
|
|
21
|
+
* (insta-oss on localhost, a preview deployment). null is not an error — it means "the user
|
|
22
|
+
* chose this URL deliberately", and callers must leave such a choice alone. */
|
|
23
|
+
export function envForApiUrl(apiUrl) {
|
|
24
|
+
const want = normalizeUrl(apiUrl);
|
|
25
|
+
return ENV_NAMES.find((n) => normalizeUrl(ENVS[n].api) === want) ?? null;
|
|
26
|
+
}
|
|
27
|
+
/** $INSTA_ENV, validated. An unrecognised value throws rather than falling back to prod: a
|
|
28
|
+
* typo'd `INSTA_ENV=stagng` that silently provisions real infrastructure in production is the
|
|
29
|
+
* worst possible outcome, and it would be invisible until the bill arrived. */
|
|
30
|
+
export function envFromEnvVar(raw = process.env.INSTA_ENV) {
|
|
31
|
+
if (raw === undefined)
|
|
32
|
+
return null;
|
|
33
|
+
const v = raw.trim().toLowerCase();
|
|
34
|
+
if (v === '')
|
|
35
|
+
return null;
|
|
36
|
+
if (!isEnvName(v)) {
|
|
37
|
+
throw new Error(`unknown INSTA_ENV "${raw}" — expected one of: ${ENV_NAMES.join(', ')}`);
|
|
38
|
+
}
|
|
39
|
+
return v;
|
|
40
|
+
}
|
|
41
|
+
/** MCP registration name for an environment.
|
|
42
|
+
*
|
|
43
|
+
* Prod keeps the bare `insta-cloud` name — it is the installed base, and renaming it would
|
|
44
|
+
* orphan every existing registration. Other environments get a suffix so they can coexist on
|
|
45
|
+
* one machine. This matters more than it looks: `registerMcp` treats an already-registered
|
|
46
|
+
* name as "nothing to do", and the config-file agents key their entry by name, so sharing one
|
|
47
|
+
* name across environments leaves a staging install silently wired to the prod MCP server
|
|
48
|
+
* while reporting success. */
|
|
49
|
+
export function mcpServerName(env) {
|
|
50
|
+
return env === DEFAULT_ENV ? 'insta-cloud' : `insta-cloud-${env}`;
|
|
51
|
+
}
|
|
52
|
+
//# sourceMappingURL=env.js.map
|
package/dist/index.js
CHANGED
|
@@ -4,6 +4,8 @@ import { Command } from 'commander';
|
|
|
4
4
|
import { ApiError } from './api.js';
|
|
5
5
|
import { die } from './util.js';
|
|
6
6
|
import * as auth from './commands/auth.js';
|
|
7
|
+
import * as envCmd_ from './commands/env.js';
|
|
8
|
+
import { ENV_NAMES } from './env.js';
|
|
7
9
|
import * as setup from './commands/setup.js';
|
|
8
10
|
import * as mcp from './commands/mcp.js';
|
|
9
11
|
import * as runCmd from './commands/run.js';
|
|
@@ -56,9 +58,16 @@ program.command('login').description('Log in with email + password, or --oauth <
|
|
|
56
58
|
.option('--password <password>', 'account password (else $INSTA_PASSWORD or prompt)')
|
|
57
59
|
.option('--oauth <provider>', 'browser OAuth login: github | google')
|
|
58
60
|
.option('--api-url <url>', 'control-plane API base URL')
|
|
61
|
+
.option('--env <name>', `deployment environment: ${ENV_NAMES.join(' | ')}`)
|
|
59
62
|
.action(guard((o) => auth.login(o)));
|
|
60
63
|
program.command('logout').description('Log out and clear local tokens').action(guard(() => auth.logout()));
|
|
61
64
|
program.command('status').description('Show login + linked project').option('--json').action(guard((o) => auth.status(o)));
|
|
65
|
+
// ---- environment (prod | staging) ----
|
|
66
|
+
const envCmd = program.command('env').description('Show or switch the deployment environment (prod | staging)');
|
|
67
|
+
envCmd.command('show', { isDefault: true }).description('Show the current environment and its hosts')
|
|
68
|
+
.option('--json').action(guard((o) => envCmd_.envShow(o)));
|
|
69
|
+
envCmd.command('use <name>').description(`Switch environment (${ENV_NAMES.join(' | ')}) — drops the stored session, which is deployment-specific`)
|
|
70
|
+
.action(guard((name) => envCmd_.envUse(name)));
|
|
62
71
|
// ---- run (per-request secret injection — nothing written to disk) ----
|
|
63
72
|
program.command('run <cmd> [args...]').description('Run a command with the branch credential bundle injected into its environment (no .env written)')
|
|
64
73
|
.option('--branch <b>', 'branch bundle to inject (default: linked branch)')
|
|
@@ -160,8 +169,8 @@ program.command('regions').description('List regions available for postgres/comp
|
|
|
160
169
|
program.command('metrics <target> [group]').description('Service metrics (target: db|compute)')
|
|
161
170
|
.option('--branch <b>').option('--from <unix>').option('--to <unix>').option('--step <s>').option('--json')
|
|
162
171
|
.action(guard((target, group, o) => obs.metrics(target, group, o)));
|
|
163
|
-
program.command('logs <target> [group]').description('Service runtime
|
|
164
|
-
.option('--branch <b>').option('--limit <n>').option('--region <r>').option('--instance <i>').option('--json')
|
|
172
|
+
program.command('logs <target> [group]').description('Service logs (runtime by default; --deploy = compute deploy events; target: db|compute)')
|
|
173
|
+
.option('--branch <b>').option('--limit <n>').option('--region <r>').option('--instance <i>').option('--deploy', 'show compute deploy events (machine lifecycle) instead of runtime logs').option('--json')
|
|
165
174
|
.action(guard((target, group, o) => obs.logs(target, group, o)));
|
|
166
175
|
program.command('usage').description('Usage for the current billing cycle by billing dimension (org by default; --proj for one project)')
|
|
167
176
|
.option('--from <unix>').option('--to <unix>').option('--proj [id]', 'show one project (the linked one, or a given id) instead of the whole org').option('--json')
|
package/dist/observe/install.js
CHANGED
|
@@ -58,8 +58,11 @@ function claudeEntry() {
|
|
|
58
58
|
// Claude Code executes `command` as ONE shell string with $CLAUDE_PROJECT_DIR in the env —
|
|
59
59
|
// there is no `args` field in its hooks schema, so a ${…} template in args reaches node
|
|
60
60
|
// verbatim and throws MODULE_NOT_FOUND after every tool call.
|
|
61
|
+
// .claude/settings.json is often committed while ./.insta stays local-only, so a fresh
|
|
62
|
+
// clone (cloud session, teammate) gets the hook without the script — no-op there.
|
|
63
|
+
const hook = '"$CLAUDE_PROJECT_DIR/.insta/observe/hook.js"';
|
|
61
64
|
return { matcher: '*', hooks: [{ type: 'command',
|
|
62
|
-
command:
|
|
65
|
+
command: `[ ! -f ${hook} ] || node ${hook}`, timeout: 15, _insta: MARKER }] };
|
|
63
66
|
}
|
|
64
67
|
function codexEntry(cwd) {
|
|
65
68
|
const abs = join(cwd, '.insta', 'observe', 'hook.js'); // Codex doesn't expand ${CLAUDE_PROJECT_DIR}; use an absolute path
|
package/package.json
CHANGED
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "insta",
|
|
3
|
-
"version": "0.0.
|
|
3
|
+
"version": "0.0.23-rc.1",
|
|
4
4
|
"type": "module",
|
|
5
|
-
"description": "InstaCloud CLI
|
|
5
|
+
"description": "InstaCloud CLI — a thin client of the platform control-plane API.",
|
|
6
6
|
"keywords": [
|
|
7
7
|
"insta",
|
|
8
8
|
"insforge",
|