insta 0.0.9 → 0.0.11
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/api.js +27 -5
- package/dist/commands/run.js +40 -0
- package/dist/commands/secrets.js +16 -0
- package/dist/commands/setup.js +33 -3
- package/dist/config.js +9 -0
- package/dist/index.js +6 -0
- package/dist/resolve-project.js +45 -0
- package/package.json +1 -1
package/dist/api.js
CHANGED
|
@@ -1,7 +1,8 @@
|
|
|
1
1
|
// Thin API client over the platform control-plane. Handles bearer auth + one-shot refresh on 401.
|
|
2
2
|
// 2xx (including 202 approval_required) returns the parsed body; >=400 throws ApiError.
|
|
3
|
-
import { readGlobal, writeGlobal, readProject } from './config.js';
|
|
4
|
-
import {
|
|
3
|
+
import { readGlobal, writeGlobal, readProject, writeProject } from './config.js';
|
|
4
|
+
import { autoResolveProject, promptChoice } from './resolve-project.js';
|
|
5
|
+
import { die, info } from './util.js';
|
|
5
6
|
export class ApiError extends Error {
|
|
6
7
|
status;
|
|
7
8
|
constructor(status, msg) {
|
|
@@ -92,8 +93,29 @@ export async function linkedProject() { return readProject(); }
|
|
|
92
93
|
// Resolve the linked project or exit with guidance.
|
|
93
94
|
export async function requireProject() {
|
|
94
95
|
const p = await readProject();
|
|
95
|
-
if (
|
|
96
|
-
|
|
97
|
-
|
|
96
|
+
if (p)
|
|
97
|
+
return p;
|
|
98
|
+
// One command, just works: unlinked ≠ error. Resolve the project (auto when there's one,
|
|
99
|
+
// one-keystroke picker when several) and persist the choice so this happens once per dir.
|
|
100
|
+
const api = await ApiClient.load();
|
|
101
|
+
try {
|
|
102
|
+
const orgs = (await api.request('GET', '/orgs')).orgs;
|
|
103
|
+
const orgId = orgs[0]?.id ?? 'local';
|
|
104
|
+
return await autoResolveProject(orgId, {
|
|
105
|
+
listProjects: async () => (await api.request('GET', `/orgs/${orgId}/projects`)).projects,
|
|
106
|
+
promptChoice,
|
|
107
|
+
save: async (c) => {
|
|
108
|
+
await writeProject(c);
|
|
109
|
+
info(`auto-linked project ${c.projectId} → ./.insta/project.json`);
|
|
110
|
+
},
|
|
111
|
+
tty: !!process.stdin.isTTY && !!process.stderr.isTTY,
|
|
112
|
+
});
|
|
113
|
+
}
|
|
114
|
+
catch (e) {
|
|
115
|
+
if (e instanceof ApiError && e.status === 401) {
|
|
116
|
+
die('not logged in — run `insta login --oauth github` (cloud) or point INSTA_API_URL at your insta-oss daemon');
|
|
117
|
+
}
|
|
118
|
+
die(e instanceof Error ? e.message : String(e));
|
|
119
|
+
}
|
|
98
120
|
}
|
|
99
121
|
//# sourceMappingURL=api.js.map
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
// `insta run -- <cmd>` — the Railway model for credentials: fetch the branch's secret bundle
|
|
2
|
+
// per invocation and inject it into the CHILD PROCESS ENVIRONMENT only. Nothing is written to
|
|
3
|
+
// disk, so there is no .env to leak, stale-out, or commit. The bundle exists exactly as long
|
|
4
|
+
// as the process does.
|
|
5
|
+
import { spawn } from 'node:child_process';
|
|
6
|
+
import { ApiClient, requireProject } from '../api.js';
|
|
7
|
+
import { die, info } from '../util.js';
|
|
8
|
+
/** Core, dependency-injected for tests: spawn cmd with the bundle in env, return its exit code. */
|
|
9
|
+
export async function runWithSecrets(cmd, args, deps) {
|
|
10
|
+
const bundle = await deps.fetchBundle();
|
|
11
|
+
return await new Promise((resolve, reject) => {
|
|
12
|
+
const child = (deps.spawnImpl ?? spawn)(cmd, args, {
|
|
13
|
+
stdio: 'inherit',
|
|
14
|
+
cwd: deps.cwd,
|
|
15
|
+
env: { ...process.env, ...bundle },
|
|
16
|
+
});
|
|
17
|
+
child.on('error', reject);
|
|
18
|
+
child.on('close', (code) => resolve(code ?? 1));
|
|
19
|
+
});
|
|
20
|
+
}
|
|
21
|
+
export async function run(cmdAndArgs, opts) {
|
|
22
|
+
const [cmd, ...rest] = cmdAndArgs;
|
|
23
|
+
if (!cmd)
|
|
24
|
+
die('usage: insta run [--branch <b>] -- <command> [args…]');
|
|
25
|
+
const api = await ApiClient.load();
|
|
26
|
+
const p = await requireProject();
|
|
27
|
+
const branch = opts.branch ?? p.branch;
|
|
28
|
+
const code = await runWithSecrets(cmd, rest, {
|
|
29
|
+
fetchBundle: async () => {
|
|
30
|
+
const res = await api.rawRequest('GET', `/projects/${p.projectId}/secrets?branch=${encodeURIComponent(branch)}`);
|
|
31
|
+
if (res.status === 202) {
|
|
32
|
+
die(`secrets.read requires approval — run: insta approvals approve ${res.body.approvalId}, then re-run`);
|
|
33
|
+
}
|
|
34
|
+
info(`running with ${Object.keys(res.body.secrets).length} injected secrets (branch ${branch}) — nothing written to disk`);
|
|
35
|
+
return res.body.secrets;
|
|
36
|
+
},
|
|
37
|
+
});
|
|
38
|
+
process.exit(code);
|
|
39
|
+
}
|
|
40
|
+
//# sourceMappingURL=run.js.map
|
package/dist/commands/secrets.js
CHANGED
|
@@ -1,4 +1,6 @@
|
|
|
1
1
|
import { writeFile } from 'node:fs/promises';
|
|
2
|
+
import { appendFileSync, existsSync, readFileSync } from 'node:fs';
|
|
3
|
+
import { join } from 'node:path';
|
|
2
4
|
import { ApiClient, requireProject } from '../api.js';
|
|
3
5
|
import { info, printJson, serializeEnv, handleApproval, die } from '../util.js';
|
|
4
6
|
function q(branch) {
|
|
@@ -22,6 +24,9 @@ export async function secrets(opts) {
|
|
|
22
24
|
const out = opts.output ?? '.env';
|
|
23
25
|
await writeFile(out, serializeEnv(bundle));
|
|
24
26
|
info(`wrote ${Object.keys(bundle).length} secrets to ${out} (branch ${branch})`);
|
|
27
|
+
if (ensureIgnored(process.cwd(), out))
|
|
28
|
+
info(` .gitignore += ${out} (credentials must never be committed)`);
|
|
29
|
+
info(' tip: `insta run -- <cmd>` injects these per-run with nothing written to disk');
|
|
25
30
|
}
|
|
26
31
|
export async function secretsList(opts) {
|
|
27
32
|
const api = await ApiClient.load();
|
|
@@ -62,4 +67,15 @@ export async function secretsUnset(name, opts) {
|
|
|
62
67
|
return;
|
|
63
68
|
info(`unset ${name} (${opts.branch ? `branch ${opts.branch}` : 'project-wide'})`);
|
|
64
69
|
}
|
|
70
|
+
/** Gitignore the env file we just wrote (git repos only; idempotent). Returns true if added. */
|
|
71
|
+
export function ensureIgnored(cwd, name) {
|
|
72
|
+
if (!existsSync(join(cwd, '.git')))
|
|
73
|
+
return false;
|
|
74
|
+
const gi = join(cwd, '.gitignore');
|
|
75
|
+
const current = existsSync(gi) ? readFileSync(gi, 'utf8') : '';
|
|
76
|
+
if (current.split('\n').some((l) => l.trim() === name))
|
|
77
|
+
return false;
|
|
78
|
+
appendFileSync(gi, (current.endsWith('\n') || current === '' ? '' : '\n') + name + '\n');
|
|
79
|
+
return true;
|
|
80
|
+
}
|
|
65
81
|
//# sourceMappingURL=secrets.js.map
|
package/dist/commands/setup.js
CHANGED
|
@@ -7,11 +7,41 @@
|
|
|
7
7
|
// project doubles as its stack manifest — that install happens on `project create|link`.
|
|
8
8
|
import { spawn } from 'node:child_process';
|
|
9
9
|
import { info } from '../util.js';
|
|
10
|
+
// The skills tool reports agents that don't support user-global installs (Eve, PromptScript, …)
|
|
11
|
+
// as red failures. That's expected reality, not a problem with THIS machine — showing red in the
|
|
12
|
+
// first 30s of onboarding erodes trust (Railway's installer reads all-green for a reason).
|
|
13
|
+
// Classify each output line: drop that expected noise, keep everything real.
|
|
14
|
+
export function classifyInstallLine(line) {
|
|
15
|
+
if (/does not support global skill installation/.test(line))
|
|
16
|
+
return 'skip';
|
|
17
|
+
if (/Failed to install \d+/.test(line))
|
|
18
|
+
return 'skip';
|
|
19
|
+
return 'keep';
|
|
20
|
+
}
|
|
10
21
|
const defaultRunner = (cmd, args) => new Promise((resolve) => {
|
|
11
|
-
const env = { ...process.env, AI_AGENT: process.env.AI_AGENT || 'insta' };
|
|
12
|
-
const p = spawn(cmd, args, { stdio: 'inherit', env });
|
|
22
|
+
const env = { ...process.env, AI_AGENT: process.env.AI_AGENT || 'insta', FORCE_COLOR: '1' };
|
|
23
|
+
const p = spawn(cmd, args, { stdio: ['inherit', 'pipe', 'pipe'], env });
|
|
24
|
+
let skipped = 0;
|
|
25
|
+
const sift = (chunk) => {
|
|
26
|
+
for (const line of chunk.toString().split('\n')) {
|
|
27
|
+
// strip ANSI before classifying; print the original line to preserve the tool's styling
|
|
28
|
+
const plain = line.replace(/\x1b\[[0-9;]*m/g, '');
|
|
29
|
+
if (classifyInstallLine(plain) === 'skip') {
|
|
30
|
+
skipped++;
|
|
31
|
+
continue;
|
|
32
|
+
}
|
|
33
|
+
if (line.trim())
|
|
34
|
+
process.stdout.write(line + '\n');
|
|
35
|
+
}
|
|
36
|
+
};
|
|
37
|
+
p.stdout?.on('data', sift);
|
|
38
|
+
p.stderr?.on('data', sift);
|
|
13
39
|
p.on('error', () => resolve({ ok: false }));
|
|
14
|
-
p.on('close', (code) =>
|
|
40
|
+
p.on('close', (code) => {
|
|
41
|
+
if (skipped)
|
|
42
|
+
info(` (skipped ${skipped >= 3 ? 'some' : skipped} agent targets that don't support user-global installs — expected)`);
|
|
43
|
+
resolve({ ok: code === 0 });
|
|
44
|
+
});
|
|
15
45
|
});
|
|
16
46
|
// -g = user-level (machine-global); -a '*' = every agent dir the skills tool supports
|
|
17
47
|
// (Claude Code, Codex, Cursor, OpenCode, Copilot, …); --copy = real files, not cache symlinks.
|
package/dist/config.js
CHANGED
|
@@ -40,6 +40,15 @@ export async function findProjectRoot(cwd = process.cwd()) {
|
|
|
40
40
|
}
|
|
41
41
|
}
|
|
42
42
|
export async function readProject(cwd = process.cwd()) {
|
|
43
|
+
// Linkless targeting (CI / one-offs / agents): INSTA_PROJECT_ID resolves the project with no
|
|
44
|
+
// link file, and beats one when both exist — an explicit parameter outranks ambient state.
|
|
45
|
+
if (process.env.INSTA_PROJECT_ID) {
|
|
46
|
+
return {
|
|
47
|
+
projectId: process.env.INSTA_PROJECT_ID,
|
|
48
|
+
orgId: process.env.INSTA_ORG_ID ?? '',
|
|
49
|
+
branch: process.env.INSTA_BRANCH ?? 'main',
|
|
50
|
+
};
|
|
51
|
+
}
|
|
43
52
|
const root = await findProjectRoot(cwd);
|
|
44
53
|
if (!root)
|
|
45
54
|
return null;
|
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 runCmd from './commands/run.js';
|
|
8
9
|
import * as org from './commands/org.js';
|
|
9
10
|
import * as project from './commands/project.js';
|
|
10
11
|
import * as branch from './commands/branch.js';
|
|
@@ -56,6 +57,11 @@ program.command('login').description('Log in with email + password, or --oauth <
|
|
|
56
57
|
.action(guard((o) => auth.login(o)));
|
|
57
58
|
program.command('logout').description('Log out and clear local tokens').action(guard(() => auth.logout()));
|
|
58
59
|
program.command('status').description('Show login + linked project').option('--json').action(guard((o) => auth.status(o)));
|
|
60
|
+
// ---- run (per-request secret injection — nothing written to disk) ----
|
|
61
|
+
program.command('run <cmd> [args...]').description('Run a command with the branch credential bundle injected into its environment (no .env written)')
|
|
62
|
+
.option('--branch <b>', 'branch bundle to inject (default: linked branch)')
|
|
63
|
+
.passThroughOptions().allowUnknownOption()
|
|
64
|
+
.action(guard((cmd, args, o) => runCmd.run([cmd, ...(args ?? [])], o)));
|
|
59
65
|
// ---- agent setup (the `curl … | sh --agents` target) ----
|
|
60
66
|
const setupCmd = program.command('setup').description('Set up this machine for InstaCloud agent workflows');
|
|
61
67
|
setupCmd.command('agent').description('Install the insta skill user-globally for all coding agents')
|
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
// "One command, just works": when a command needs a project and the directory isn't linked
|
|
2
|
+
// (and no INSTA_PROJECT_ID is set), resolve it instead of lecturing about `project link` —
|
|
3
|
+
// one project auto-selects silently, several get a one-keystroke picker, and either way the
|
|
4
|
+
// choice is SAVED so this happens at most once per directory. (Railway prompts every unlinked
|
|
5
|
+
// machine; we persist the answer and, via the committed link file, share it with the team.)
|
|
6
|
+
import { createInterface } from 'node:readline/promises';
|
|
7
|
+
export async function autoResolveProject(orgId, deps) {
|
|
8
|
+
const projects = await deps.listProjects();
|
|
9
|
+
if (projects.length === 0) {
|
|
10
|
+
throw new Error('no projects yet — start one with `insta project create <name>`');
|
|
11
|
+
}
|
|
12
|
+
let picked;
|
|
13
|
+
if (projects.length === 1) {
|
|
14
|
+
picked = projects[0];
|
|
15
|
+
}
|
|
16
|
+
else if (deps.tty) {
|
|
17
|
+
picked = await deps.promptChoice(projects);
|
|
18
|
+
}
|
|
19
|
+
else {
|
|
20
|
+
const list = projects.map((p) => ` ${p.id} ${p.name}`).join('\n');
|
|
21
|
+
throw new Error(`several projects and no terminal to pick — set INSTA_PROJECT_ID or run \`insta project link <id>\`:\n${list}`);
|
|
22
|
+
}
|
|
23
|
+
const cfg = { projectId: picked.id, orgId, branch: 'main' };
|
|
24
|
+
await deps.save(cfg);
|
|
25
|
+
return cfg;
|
|
26
|
+
}
|
|
27
|
+
/** Real picker: numbered list on stderr, one line of input. Dependency-free. */
|
|
28
|
+
export async function promptChoice(items) {
|
|
29
|
+
process.stderr.write('pick a project:\n');
|
|
30
|
+
items.forEach((p, i) => process.stderr.write(` ${i + 1}) ${p.name} (${p.id})\n`));
|
|
31
|
+
const rl = createInterface({ input: process.stdin, output: process.stderr });
|
|
32
|
+
try {
|
|
33
|
+
for (;;) {
|
|
34
|
+
const answer = (await rl.question(`1-${items.length} > `)).trim();
|
|
35
|
+
const idx = Number(answer) - 1;
|
|
36
|
+
const hit = items[idx];
|
|
37
|
+
if (hit)
|
|
38
|
+
return hit;
|
|
39
|
+
}
|
|
40
|
+
}
|
|
41
|
+
finally {
|
|
42
|
+
rl.close();
|
|
43
|
+
}
|
|
44
|
+
}
|
|
45
|
+
//# sourceMappingURL=resolve-project.js.map
|