insta 0.0.1 → 0.0.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,58 @@
1
+ // `insta observe` — the local credential-audit hook. install wires a PostToolUse hook into the
2
+ // agent harness; report renders the local audit; sync uploads findings into the project timeline
3
+ // (idempotent via a stable dedup key, matching the platform's audit-event ingest).
4
+ import { readFile } from 'node:fs/promises';
5
+ import { join } from 'node:path';
6
+ import { installObserve, uninstallObserve } from '../observe/install.js';
7
+ import { renderReport } from '../observe/report.js';
8
+ import { ApiClient, requireProject } from '../api.js';
9
+ import { info, printJson } from '../util.js';
10
+ async function readAudit() {
11
+ try {
12
+ const txt = await readFile(join(process.cwd(), '.insta', 'audit.jsonl'), 'utf8');
13
+ return txt.split('\n').filter(Boolean).map((l) => JSON.parse(l));
14
+ }
15
+ catch {
16
+ return [];
17
+ }
18
+ }
19
+ function dedupKey(r) {
20
+ return `${r.ts}|${r.fingerprint}|${r.surface}|${r.sink}|${r.kind}`;
21
+ }
22
+ function* chunk(a, n) {
23
+ for (let i = 0; i < a.length; i += n)
24
+ yield a.slice(i, i + n);
25
+ }
26
+ export async function observeInstall() {
27
+ const res = installObserve({ cwd: process.cwd() });
28
+ info(`installed observe hook (claude: ${res.claude}, codex: ${res.codex}) → ./.insta/observe`);
29
+ info('it scans agent tool-use for credential exposure; findings append to ./.insta/audit.jsonl');
30
+ info('run `insta observe report` to review, `insta observe sync` to upload to the project timeline');
31
+ }
32
+ export async function observeUninstall() {
33
+ uninstallObserve(process.cwd());
34
+ info('uninstalled observe hook');
35
+ }
36
+ export async function observeReport(opts) {
37
+ const rows = await readAudit();
38
+ if (opts.json)
39
+ return printJson(rows);
40
+ info(renderReport(rows));
41
+ }
42
+ export async function observeSync() {
43
+ const api = await ApiClient.load();
44
+ const p = await requireProject();
45
+ const rows = await readAudit();
46
+ if (!rows.length)
47
+ return info('nothing to sync (./.insta/audit.jsonl is empty)');
48
+ const events = rows.map((r) => ({ source: 'agent', kind: `cred.${r.kind ?? 'touch'}`, branchId: null, dedupKey: dedupKey(r), payload: r }));
49
+ let recorded = 0;
50
+ let skipped = 0;
51
+ for (const c of chunk(events, 200)) {
52
+ const out = await api.request('POST', `/projects/${p.projectId}/events`, { events: c });
53
+ recorded += out.recorded;
54
+ skipped += out.skipped;
55
+ }
56
+ info(`synced ${rows.length} finding(s) → recorded ${recorded}, skipped ${skipped} (already uploaded)`);
57
+ }
58
+ //# sourceMappingURL=observe.js.map
@@ -0,0 +1,16 @@
1
+ import { ApiClient } from '../api.js';
2
+ import { info, printJson } from '../util.js';
3
+ export async function orgList(opts) {
4
+ const api = await ApiClient.load();
5
+ const { orgs } = await api.request('GET', '/orgs');
6
+ if (opts.json)
7
+ return printJson(orgs);
8
+ for (const o of orgs)
9
+ info(`${o.id} ${o.name}${o.is_personal ? ' (personal)' : ''} [${o.role}]`);
10
+ }
11
+ export async function orgCreate(name) {
12
+ const api = await ApiClient.load();
13
+ const { org } = await api.request('POST', '/orgs', { name });
14
+ info(`created org ${org.id} (${org.name})`);
15
+ }
16
+ //# sourceMappingURL=org.js.map
@@ -0,0 +1,61 @@
1
+ import { ApiClient, requireProject } from '../api.js';
2
+ import { writeProject } from '../config.js';
3
+ import { info, die, printJson, handleApproval } from '../util.js';
4
+ import { installObserve } from '../observe/install.js';
5
+ import { installSkills } from '../ensure-skills.js';
6
+ // Best-effort: wire the credential-audit hook into the project (no-op if assets aren't built).
7
+ function tryInstallObserve() {
8
+ try {
9
+ const r = installObserve({ cwd: process.cwd() });
10
+ if (r.claude || r.codex)
11
+ info(' installed observe hook (credential audit) → ./.insta/observe');
12
+ }
13
+ catch { /* assets missing (dev/unbuilt) — skip silently */ }
14
+ }
15
+ async function resolveOrg(api, given) {
16
+ if (given)
17
+ return given;
18
+ const { orgs } = await api.request('GET', '/orgs');
19
+ if (!orgs.length)
20
+ die('no org found — run `insta org create <name>`');
21
+ return orgs[0].id;
22
+ }
23
+ export async function projectCreate(name, opts) {
24
+ const api = await ApiClient.load();
25
+ const orgId = await resolveOrg(api, opts.org);
26
+ const out = await api.request('POST', `/orgs/${orgId}/projects`, { name });
27
+ await writeProject({ projectId: out.project.id, orgId, branch: out.defaultBranch.name });
28
+ info(`created project ${out.project.id} (${name})`);
29
+ info(` resources: ${out.resources.map((r) => r.kind).join(', ')}`);
30
+ info(` linked ./.insta/project.json (branch ${out.defaultBranch.name})`);
31
+ tryInstallObserve();
32
+ await installSkills({ cwd: process.cwd() });
33
+ }
34
+ export async function projectList(opts) {
35
+ const api = await ApiClient.load();
36
+ const orgId = await resolveOrg(api, opts.org);
37
+ const { projects } = await api.request('GET', `/orgs/${orgId}/projects`);
38
+ if (opts.json)
39
+ return printJson(projects);
40
+ if (!projects.length)
41
+ return info('(no projects)');
42
+ for (const p of projects)
43
+ info(`${p.id} ${p.name} [${p.status}]`);
44
+ }
45
+ export async function projectLink(id) {
46
+ const api = await ApiClient.load();
47
+ const { project } = await api.request('GET', `/projects/${id}`);
48
+ await writeProject({ projectId: project.id, orgId: project.org_id, branch: 'main' });
49
+ info(`linked project ${project.id} (${project.name})`);
50
+ tryInstallObserve();
51
+ await installSkills({ cwd: process.cwd() });
52
+ }
53
+ export async function projectDelete(opts) {
54
+ const api = await ApiClient.load();
55
+ const projectId = opts.project ?? (await requireProject()).projectId;
56
+ const res = await api.rawRequest('DELETE', `/projects/${projectId}`);
57
+ if (handleApproval(res))
58
+ return;
59
+ info(`deleted project ${projectId}`);
60
+ }
61
+ //# sourceMappingURL=project.js.map
@@ -0,0 +1,36 @@
1
+ import { writeFile } from 'node:fs/promises';
2
+ import { ApiClient, requireProject } from '../api.js';
3
+ import { info, printJson, serializeEnv, handleApproval } from '../util.js';
4
+ function q(branch) {
5
+ return branch ? `?branch=${encodeURIComponent(branch)}` : '';
6
+ }
7
+ // Fetch the credential bundle (the secret seam) and write it to .env (or print).
8
+ export async function secrets(opts) {
9
+ const api = await ApiClient.load();
10
+ const p = await requireProject();
11
+ const branch = opts.branch ?? p.branch;
12
+ const res = await api.rawRequest('GET', `/projects/${p.projectId}/secrets${q(branch)}`);
13
+ if (handleApproval(res))
14
+ return;
15
+ const bundle = res.body.secrets;
16
+ if (opts.json)
17
+ return printJson(bundle);
18
+ if (opts.print) {
19
+ process.stdout.write(serializeEnv(bundle));
20
+ return;
21
+ }
22
+ const out = opts.output ?? '.env';
23
+ await writeFile(out, serializeEnv(bundle));
24
+ info(`wrote ${Object.keys(bundle).length} secrets to ${out} (branch ${branch})`);
25
+ }
26
+ export async function secretsList(opts) {
27
+ const api = await ApiClient.load();
28
+ const p = await requireProject();
29
+ const branch = opts.branch ?? p.branch;
30
+ const res = await api.rawRequest('GET', `/projects/${p.projectId}/secrets${q(branch)}`);
31
+ if (handleApproval(res))
32
+ return;
33
+ for (const name of Object.keys(res.body.secrets))
34
+ info(name);
35
+ }
36
+ //# sourceMappingURL=secrets.js.map
@@ -0,0 +1,89 @@
1
+ // `insta services` — manage a project's opt-in services (postgres | storage | compute).
2
+ import { ApiClient, requireProject } from '../api.js';
3
+ import { info, printJson, handleApproval } from '../util.js';
4
+ export const SERVICE_TYPES = ['postgres', 'storage', 'compute'];
5
+ // ---- pure, unit-tested helpers (throw plain Errors; the CLI guard turns them into clean output) ----
6
+ // Validate a service-type argument against the allowed set for a command.
7
+ export function assertType(type, allowed = SERVICE_TYPES) {
8
+ if (!allowed.includes(type))
9
+ throw new Error(`type must be ${allowed.join('|')}`);
10
+ }
11
+ // Parse a positive-integer machine count.
12
+ export function parseCount(raw) {
13
+ const n = Number(raw);
14
+ if (!Number.isInteger(n) || n < 1)
15
+ throw new Error(`count must be a positive integer, got: ${raw}`);
16
+ return n;
17
+ }
18
+ // Resolve a service id from a `services list` result by (type, name).
19
+ export function resolveServiceId(services, type, name) {
20
+ const svc = services.find((s) => s.type === type && s.name === name);
21
+ if (!svc)
22
+ throw new Error(`service not found: ${type} ${name}`);
23
+ return svc.id;
24
+ }
25
+ // ---- commands ----
26
+ export async function servicesAdd(type, name) {
27
+ assertType(type);
28
+ const api = await ApiClient.load();
29
+ const p = await requireProject();
30
+ const res = await api.rawRequest('POST', `/projects/${p.projectId}/services`, { type, name });
31
+ if (handleApproval(res))
32
+ return;
33
+ const svc = res.body.service;
34
+ info(`added ${type} service ${name} (${svc.id})${svc.domain ? ` — ${svc.domain}` : ''}`);
35
+ }
36
+ export async function servicesList(opts) {
37
+ const api = await ApiClient.load();
38
+ const p = await requireProject();
39
+ const { services } = await api.request('GET', `/projects/${p.projectId}/services`);
40
+ if (opts.json)
41
+ return printJson(services);
42
+ if (!services.length)
43
+ return info('(no services — add one with `insta services add <postgres|storage|compute> <name>`)');
44
+ for (const s of services) {
45
+ const extra = s.type === 'compute' ? ` x${s.machine_count}` : '';
46
+ info(`${s.type}/${s.name} [${s.status}]${extra}${s.domain ? ` ${s.domain}` : ''} ${s.id}`);
47
+ }
48
+ }
49
+ export async function servicesRemove(type, name) {
50
+ assertType(type);
51
+ const api = await ApiClient.load();
52
+ const p = await requireProject();
53
+ const { services } = await api.request('GET', `/projects/${p.projectId}/services`);
54
+ const id = resolveServiceId(services, type, name);
55
+ const res = await api.rawRequest('DELETE', `/projects/${p.projectId}/services/${id}`);
56
+ if (handleApproval(res))
57
+ return;
58
+ info(`removed ${type} service ${name}`);
59
+ }
60
+ // insta services scale compute <name> <number> [region]
61
+ export async function servicesScale(type, name, number, region, _opts) {
62
+ assertType(type, ['compute']);
63
+ const machineCount = parseCount(number);
64
+ const api = await ApiClient.load();
65
+ const p = await requireProject();
66
+ const { services } = await api.request('GET', `/projects/${p.projectId}/services`);
67
+ const id = resolveServiceId(services, type, name);
68
+ const res = await api.rawRequest('POST', `/projects/${p.projectId}/services/${id}/scale`, { machineCount, region });
69
+ if (handleApproval(res))
70
+ return;
71
+ if (_opts.json)
72
+ return printJson(res.body.service);
73
+ info(`scaled compute ${name} to ${machineCount} machine(s)${region ? ` in ${region}` : ''}`);
74
+ }
75
+ // insta services upgrade <compute|postgres> <name> <new-spec>
76
+ export async function servicesUpgrade(type, name, spec, _opts) {
77
+ assertType(type, ['compute', 'postgres']);
78
+ const api = await ApiClient.load();
79
+ const p = await requireProject();
80
+ const { services } = await api.request('GET', `/projects/${p.projectId}/services`);
81
+ const id = resolveServiceId(services, type, name);
82
+ const res = await api.rawRequest('POST', `/projects/${p.projectId}/services/${id}/upgrade`, { spec });
83
+ if (handleApproval(res))
84
+ return;
85
+ if (_opts.json)
86
+ return printJson(res.body.service);
87
+ info(`upgraded ${type} ${name} to ${spec}`);
88
+ }
89
+ //# sourceMappingURL=services.js.map
package/dist/config.js ADDED
@@ -0,0 +1,35 @@
1
+ // CLI config: global (~/.insta/config.json: api url + tokens) and per-project (./.insta/project.json).
2
+ import { homedir } from 'node:os';
3
+ import { join } from 'node:path';
4
+ import { mkdir, readFile, writeFile } from 'node:fs/promises';
5
+ const GLOBAL_DIR = join(homedir(), '.insta');
6
+ const GLOBAL_FILE = join(GLOBAL_DIR, 'config.json');
7
+ const PROJECT_DIR = '.insta';
8
+ const PROJECT_FILE = 'project.json';
9
+ const DEFAULT_API = process.env.INSTA_API_URL ?? 'https://beta-api.insta.insforge.dev';
10
+ export async function readGlobal() {
11
+ try {
12
+ const parsed = JSON.parse(await readFile(GLOBAL_FILE, 'utf8'));
13
+ return { ...parsed, apiUrl: parsed.apiUrl ?? DEFAULT_API };
14
+ }
15
+ catch {
16
+ return { apiUrl: DEFAULT_API };
17
+ }
18
+ }
19
+ export async function writeGlobal(c) {
20
+ await mkdir(GLOBAL_DIR, { recursive: true });
21
+ await writeFile(GLOBAL_FILE, JSON.stringify(c, null, 2));
22
+ }
23
+ export async function readProject(cwd = process.cwd()) {
24
+ try {
25
+ return JSON.parse(await readFile(join(cwd, PROJECT_DIR, PROJECT_FILE), 'utf8'));
26
+ }
27
+ catch {
28
+ return null;
29
+ }
30
+ }
31
+ export async function writeProject(c, cwd = process.cwd()) {
32
+ await mkdir(join(cwd, PROJECT_DIR), { recursive: true });
33
+ await writeFile(join(cwd, PROJECT_DIR, PROJECT_FILE), JSON.stringify(c, null, 2));
34
+ }
35
+ //# sourceMappingURL=config.js.map
@@ -0,0 +1,70 @@
1
+ // Install the related agent skills into a linked project so the developer's coding agent has
2
+ // context for what InstaCloud provisions — the `insta` CLI itself plus the three services you build
3
+ // directly against (Neon Postgres, Tigris storage, Better Auth). Runs `npx skills add`
4
+ // (vercel-labs/skills) fully non-interactively. Best-effort: a failure (offline, npx missing, a
5
+ // repo moved) prints a manual fallback and never blocks or fails the host command — same contract
6
+ // as the observe-hook install.
7
+ import { spawn } from 'node:child_process';
8
+ import { existsSync, readFileSync, writeFileSync } from 'node:fs';
9
+ import { join } from 'node:path';
10
+ // Where `npx skills add` drops skills for the agents we pin below: Claude Code → .claude/skills/,
11
+ // Codex → .agents/skills/ (.github/skills/ is the third well-known dir). These are regenerable
12
+ // agent context, not the developer's source — keep them out of git.
13
+ const SKILL_DIRS = ['.claude/skills/', '.agents/skills/', '.github/skills/'];
14
+ const defaultRunner = (cmd, args, inherit = false) => new Promise((resolve) => {
15
+ const p = spawn(cmd, args, { stdio: inherit ? 'inherit' : 'ignore' });
16
+ p.on('error', () => resolve({ ok: false })); // e.g. npx not on PATH
17
+ p.on('close', (code) => resolve({ ok: code === 0 }));
18
+ });
19
+ // `npx skills add` is non-interactive only when we remove every prompt: pin the target agents
20
+ // (-a claude-code -a codex) so it neither asks which agent nor fans out to every known agent dir;
21
+ // name the exact skills (-s …) so there's no skill picker; -y to skip the scope/confirm prompt;
22
+ // --copy to write real files (not symlinks into a transient npx cache).
23
+ const AGENT_FLAGS = ['-a', 'claude-code', '-a', 'codex', '-y', '--copy'];
24
+ const SKILLS = [
25
+ { label: 'insta', args: ['skills', 'add', 'InsForge/insta-skills', '-s', 'insta', ...AGENT_FLAGS] },
26
+ { label: 'neon-postgres', args: ['skills', 'add', 'neondatabase/agent-skills', '-s', 'neon-postgres', ...AGENT_FLAGS] },
27
+ { label: 'tigris', args: ['skills', 'add', 'tigrisdata/skills',
28
+ '-s', 'tigris-object-operations', '-s', 'file-storage', '-s', 'tigris-sdk-guide',
29
+ '-s', 'tigris-security-access-control', '-s', 'tigris-image-optimization',
30
+ '-s', 'tigris-s3-migration', '-s', 'tigris-static-assets', '-s', 'tigris-agent-kit',
31
+ ...AGENT_FLAGS] },
32
+ { label: 'better-auth', args: ['skills', 'add', 'better-auth/skills',
33
+ '-s', 'better-auth-best-practices', '-s', 'email-and-password-best-practices',
34
+ '-s', 'better-auth-security-best-practices', ...AGENT_FLAGS] },
35
+ ];
36
+ // Install all related skills. Production omits `run`/`print` → the real spawn + stdout; tests inject
37
+ // a fake runner and capture output. Continues past a per-skill failure so one bad repo doesn't skip
38
+ // the rest, and never throws.
39
+ export async function installSkills(deps) {
40
+ const run = deps.run ?? defaultRunner;
41
+ const print = deps.print ?? ((s) => process.stdout.write(s + '\n'));
42
+ try {
43
+ print(' installing related agent skills (insta, neon-postgres, tigris, better-auth) via `npx skills add` …');
44
+ for (const s of SKILLS) {
45
+ const r = await run('npx', s.args, true); // streamed so the user sees download progress
46
+ print(r.ok ? ` ${s.label} ✓` : ` ${s.label} failed — add manually: npx ${s.args.join(' ')}`);
47
+ }
48
+ const added = ensureGitignore(deps.cwd, SKILL_DIRS);
49
+ if (added.length)
50
+ print(` .gitignore += ${added.join(', ')}`);
51
+ }
52
+ catch {
53
+ /* best-effort convenience — never block the host command */
54
+ }
55
+ }
56
+ // Append any missing entries to the project's ./.gitignore (creating it if absent). Idempotent:
57
+ // entries already present are left alone. Returns the entries it added.
58
+ export function ensureGitignore(cwd, entries) {
59
+ const p = join(cwd, '.gitignore');
60
+ const existing = existsSync(p) ? readFileSync(p, 'utf8') : '';
61
+ const have = new Set(existing.split('\n').map((l) => l.trim()));
62
+ const missing = entries.filter((e) => !have.has(e));
63
+ if (missing.length === 0)
64
+ return [];
65
+ const prefix = existing && !existing.endsWith('\n') ? '\n' : '';
66
+ const comment = '# InstaCloud: agent skills installed by `npx skills add` (regenerable, not source)';
67
+ writeFileSync(p, existing + `${prefix}\n${comment}\n${missing.join('\n')}\n`);
68
+ return missing;
69
+ }
70
+ //# sourceMappingURL=ensure-skills.js.map
@@ -0,0 +1,79 @@
1
+ // Build a source directory into an image and push it to Fly's registry, using a short-lived,
2
+ // app-scoped deploy token minted by the platform (the CLI never holds a standing Fly credential).
3
+ // Shells out to `flyctl deploy --build-only --push` (remote builder). Ported from firth.
4
+ import { spawn } from 'node:child_process';
5
+ import { existsSync, writeFileSync, unlinkSync } from 'node:fs';
6
+ import { join } from 'node:path';
7
+ import { info } from './util.js';
8
+ // Spawn flyctl, tee its output to the user (so they see buildkit progress) AND capture it for digest
9
+ // parsing.
10
+ export const defaultBuildRunner = (cmd, args, opts) => new Promise((resolve) => {
11
+ const child = spawn(cmd, args, { cwd: opts.cwd, env: opts.env, stdio: ['inherit', 'pipe', 'pipe'] });
12
+ let output = '';
13
+ child.stdout?.on('data', (b) => { const s = b.toString(); output += s; process.stdout.write(s); });
14
+ child.stderr?.on('data', (b) => { const s = b.toString(); output += s; process.stderr.write(s); });
15
+ child.on('error', (err) => resolve({ code: -1, output: `${output}\n${err.message}` }));
16
+ child.on('close', (code) => resolve({ code: code ?? -1, output }));
17
+ });
18
+ // buildkit prints "pushing manifest for registry.fly.io/<app>:<label>@sha256:<digest>" on push.
19
+ // Pin to the digest — the bare tag races on Fly's registry (MANIFEST_UNKNOWN); the digest always resolves.
20
+ export function parseImageDigest(output, flyApp) {
21
+ const m = output.match(/pushing manifest for registry\.fly\.io\/[^\s]+@(sha256:[0-9a-f]+)/);
22
+ return m ? `registry.fly.io/${flyApp}@${m[1]}` : null;
23
+ }
24
+ // Write a minimal stub fly.toml iff the dir has none (flyctl needs app config; a fresh app has no
25
+ // machines to derive it from). Region is build-config only — `--build-only` places no machines.
26
+ // Returns a cleanup callback the caller MUST run in finally; leaves a user's own fly.toml alone.
27
+ function ensureFlyTomlStub(dir, flyApp, port) {
28
+ const path = join(dir, 'fly.toml');
29
+ if (existsSync(path))
30
+ return () => { };
31
+ const stub = `# Auto-generated by insta for source deploy. Safe to delete.\n` +
32
+ `app = "${flyApp}"\n` +
33
+ `primary_region = "iad"\n\n` +
34
+ `[build]\n\n` +
35
+ `[http_service]\n` +
36
+ ` internal_port = ${port}\n` +
37
+ ` force_https = true\n`;
38
+ writeFileSync(path, stub, 'utf8');
39
+ return () => { try {
40
+ unlinkSync(path);
41
+ }
42
+ catch { /* best-effort */ } };
43
+ }
44
+ export async function flyctlBuildAndPush(opts, run = defaultBuildRunner) {
45
+ const cleanup = ensureFlyTomlStub(opts.dir, opts.flyApp, opts.port);
46
+ try {
47
+ const { code, output } = await run('flyctl', ['deploy', '--remote-only', '--build-only', '--push', '--app', opts.flyApp, '--image-label', opts.imageLabel, '--no-cache'], { cwd: opts.dir, env: { ...process.env, FLY_API_TOKEN: opts.token } });
48
+ if (code !== 0)
49
+ throw new Error(`flyctl deploy --build-only failed (exit ${code}). See output above.`);
50
+ const imageRef = parseImageDigest(output, opts.flyApp);
51
+ if (!imageRef)
52
+ throw new Error('flyctl build succeeded but no "pushing manifest" digest line was found — cannot determine the image. Re-run with FLY_LOG_LEVEL=debug.');
53
+ return { imageRef };
54
+ }
55
+ finally {
56
+ cleanup();
57
+ }
58
+ }
59
+ // Best-effort: ensure the fly CLI is available (needed for source-directory deploys). Never blocks —
60
+ // if it can't be installed the subsequent build surfaces a clear error.
61
+ export async function ensureFlyctl() {
62
+ const ok = (cmd, args, inherit = false) => new Promise((resolve) => {
63
+ const p = spawn(cmd, args, { stdio: inherit ? 'inherit' : 'ignore' });
64
+ p.on('error', () => resolve(false));
65
+ p.on('close', (code) => resolve(code === 0));
66
+ });
67
+ try {
68
+ if (await ok('flyctl', ['version']))
69
+ return;
70
+ if (process.platform === 'darwin' && (await ok('brew', ['--version']))) {
71
+ info('flyctl not found — installing with `brew install flyctl` (one-time)…');
72
+ info((await ok('brew', ['install', 'flyctl'], true)) ? 'flyctl installed ✓' : 'flyctl install failed — install manually: https://fly.io/docs/flyctl/install/');
73
+ return;
74
+ }
75
+ info('flyctl (fly CLI) not found — install it to deploy from source: https://fly.io/docs/flyctl/install/');
76
+ }
77
+ catch { /* best-effort convenience */ }
78
+ }
79
+ //# sourceMappingURL=flyctl-build.js.map
package/dist/index.js ADDED
@@ -0,0 +1,132 @@
1
+ #!/usr/bin/env node
2
+ import { readFileSync } from 'node:fs';
3
+ import { Command } from 'commander';
4
+ import { ApiError } from './api.js';
5
+ import { die } from './util.js';
6
+ import * as auth from './commands/auth.js';
7
+ import * as org from './commands/org.js';
8
+ import * as project from './commands/project.js';
9
+ import * as branch from './commands/branch.js';
10
+ import * as services from './commands/services.js';
11
+ import * as secretsCmd from './commands/secrets.js';
12
+ import { deploy } from './commands/deploy.js';
13
+ import * as computeCmd from './commands/compute.js';
14
+ import { manifest } from './commands/manifest.js';
15
+ import * as govern from './commands/govern.js';
16
+ import * as observe from './commands/observe.js';
17
+ import * as obs from './commands/metrics.js';
18
+ import { billing, billingUpgrade, billingPortal } from './commands/billing.js';
19
+ function onError(e) {
20
+ if (e instanceof ApiError)
21
+ die(`${e.message} (HTTP ${e.status})`);
22
+ die(e instanceof Error ? e.message : String(e));
23
+ }
24
+ // Wrap an async action so rejections surface as clean CLI errors.
25
+ const guard = (fn) => (...a) => fn(...a).then(() => undefined).catch(onError);
26
+ const program = new Command();
27
+ // Version resolution: INSTA_CLI_VERSION (baked into the standalone binary via bun build --define) →
28
+ // the installed package.json (npm/node — ../package.json sits beside dist/) → 0.0.0.
29
+ function resolveVersion() {
30
+ if (process.env.INSTA_CLI_VERSION)
31
+ return process.env.INSTA_CLI_VERSION;
32
+ try {
33
+ return JSON.parse(readFileSync(new URL('../package.json', import.meta.url), 'utf8')).version;
34
+ }
35
+ catch {
36
+ return '0.0.0';
37
+ }
38
+ }
39
+ program.name('insta').description('InstaCloud CLI — manage projects, branches, secrets, deploys').version(resolveVersion());
40
+ // ---- auth ----
41
+ program.command('login').description('Log in with email + password, or --oauth <github|google> (browser)')
42
+ .option('--email <email>', 'account email')
43
+ .option('--password <password>', 'account password (else $INSTA_PASSWORD or prompt)')
44
+ .option('--oauth <provider>', 'browser OAuth login: github | google')
45
+ .option('--api-url <url>', 'control-plane API base URL')
46
+ .action(guard((o) => auth.login(o)));
47
+ program.command('logout').description('Log out and clear local tokens').action(guard(() => auth.logout()));
48
+ program.command('status').description('Show login + linked project').option('--json').action(guard((o) => auth.status(o)));
49
+ // ---- org ----
50
+ const orgCmd = program.command('org').description('Manage organizations');
51
+ orgCmd.command('list').option('--json').action(guard((o) => org.orgList(o)));
52
+ orgCmd.command('create <name>').action(guard((name) => org.orgCreate(name)));
53
+ // ---- project ----
54
+ const pj = program.command('project').description('Manage projects');
55
+ pj.command('create <name>').option('--org <id>', 'org to create under (default: personal)').action(guard((name, o) => project.projectCreate(name, o)));
56
+ pj.command('list').option('--org <id>').option('--json').action(guard((o) => project.projectList(o)));
57
+ pj.command('link <id>').description('Link a project to this directory').action(guard((id) => project.projectLink(id)));
58
+ pj.command('delete').option('--project <id>').action(guard((o) => project.projectDelete(o)));
59
+ // ---- branch ----
60
+ const br = program.command('branch').description('Manage branch environments');
61
+ br.command('create <name>').option('--from <branch>', 'parent branch (default: current)').action(guard((name, o) => branch.branchCreate(name, o)));
62
+ br.command('list').option('--json').action(guard((o) => branch.branchList(o)));
63
+ br.command('switch <name>').action(guard((name) => branch.branchSwitch(name)));
64
+ br.command('delete <name>').action(guard((name) => branch.branchDelete(name)));
65
+ // ---- services (opt-in postgres/storage/compute) ----
66
+ const svc = program.command('services').alias('svc').description('Manage project services (postgres|storage|compute)');
67
+ svc.command('add <type> <name>').description('Provision a service on demand (assigns a default domain for postgres/compute)')
68
+ .action(guard((type, name) => services.servicesAdd(type, name)));
69
+ svc.command('list').option('--json').action(guard((o) => services.servicesList(o)));
70
+ svc.command('remove <type> <name>').description('Remove a service and destroy its resources')
71
+ .action(guard((type, name) => services.servicesRemove(type, name)));
72
+ svc.command('scale <type> <name> <number> [region]').description('Set a compute service machine count (paid plans only)')
73
+ .option('--json').action(guard((type, name, number, region, o) => services.servicesScale(type, name, number, region, o)));
74
+ svc.command('upgrade <type> <name> <spec>').description('Change a compute/postgres service spec (paid plans only)')
75
+ .option('--json').action(guard((type, name, spec, o) => services.servicesUpgrade(type, name, spec, o)));
76
+ // ---- secrets (seam) ----
77
+ const sec = program.command('secrets').description('Fetch the credential bundle (secret seam) into .env')
78
+ .option('--branch <branch>').option('-o, --output <file>', 'output file (default .env)').option('--print', 'print instead of writing').option('--json')
79
+ .action(guard((o) => secretsCmd.secrets(o)));
80
+ sec.command('list').description('List secret names only').option('--branch <branch>').action(guard((o) => secretsCmd.secretsList(o)));
81
+ // ---- deploy ----
82
+ program.command('deploy [dir]').description('Deploy a source directory (built remotely on Fly) or a prebuilt --image to a branch compute group')
83
+ .option('--image <url>', 'prebuilt container image to deploy (instead of a source dir)').option('--branch <b>').option('--group <g>').option('--port <p>')
84
+ .action(guard((dir, o) => deploy(dir, o)));
85
+ // ---- compute custom domains (bring your own domain → Fly cert + routing) ----
86
+ const compute = program.command('compute').description('Compute custom domains (bring your own domain)');
87
+ compute.command('set-domain <host>').description('Attach a custom domain to a branch compute service (gated: deploy)')
88
+ .option('--branch <b>').option('--group <g>').option('--json').action(guard((host, o) => computeCmd.setDomain(host, o)));
89
+ compute.command('check-domain <host>').description("Show a custom domain's cert status + required DNS records")
90
+ .option('--branch <b>').option('--group <g>').option('--json').action(guard((host, o) => computeCmd.checkDomain(host, o)));
91
+ compute.command('remove-domain <host>').description('Detach a custom domain (gated: deploy)')
92
+ .option('--branch <b>').option('--group <g>').action(guard((host, o) => computeCmd.removeDomain(host, o)));
93
+ // ---- manifest ----
94
+ program.command('manifest').description('Print an agent-legible view of the project environments').option('--json').action(guard((o) => manifest(o)));
95
+ // ---- observability ----
96
+ program.command('metrics <target> [group]').description('Service metrics (target: db|compute)')
97
+ .option('--branch <b>').option('--from <unix>').option('--to <unix>').option('--step <s>').option('--json')
98
+ .action(guard((target, group, o) => obs.metrics(target, group, o)));
99
+ program.command('logs <target> [group]').description('Service runtime logs (target: db|compute)')
100
+ .option('--branch <b>').option('--limit <n>').option('--region <r>').option('--instance <i>').option('--json')
101
+ .action(guard((target, group, o) => obs.logs(target, group, o)));
102
+ program.command('usage').description('Usage for the current billing cycle by billing dimension (org by default; --proj for one project)')
103
+ .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')
104
+ .action(guard((o) => obs.usage(o)));
105
+ const bill = program.command('billing').description('Current billing cycle summary (tier / credit / used / overage)')
106
+ .option('--org <id>', 'target org (default: linked project\'s org)').option('--json')
107
+ .action(guard((o) => billing(o)));
108
+ bill.command('upgrade <tier>').description('Subscribe the org to a paid tier (pro|enterprise) via Stripe Checkout')
109
+ .option('--org <id>').option('--no-open', 'print the URL instead of opening a browser').option('--json')
110
+ .action(guard((tier, o) => billingUpgrade(tier, o)));
111
+ bill.command('portal').description('Open the Stripe Customer Portal (change plan / card / cancel)')
112
+ .option('--org <id>').option('--no-open', 'print the URL instead of opening a browser').option('--json')
113
+ .action(guard((o) => billingPortal(o)));
114
+ // ---- events (audit timeline) ----
115
+ program.command('events').description('Show the audit + agent-event timeline').option('--branch <b>').option('--limit <n>').option('--json').action(guard((o) => govern.events(o)));
116
+ // ---- approvals ----
117
+ const ap = program.command('approvals').description('Governance approvals (HITL)');
118
+ ap.command('list').option('--status <s>', 'pending|granted|denied|consumed').option('--json').action(guard((o) => govern.approvalsList(o)));
119
+ ap.command('approve <id>').option('--always', 'also set the policy to allow').action(guard((id, o) => govern.approvalsApprove(id, o)));
120
+ ap.command('deny <id>').action(guard((id) => govern.approvalsDeny(id)));
121
+ // ---- observe (local credential audit) ----
122
+ const ob = program.command('observe').description('Local credential-audit hook');
123
+ ob.command('install').description('Install the PostToolUse hook into this project').action(guard(() => observe.observeInstall()));
124
+ ob.command('uninstall').action(guard(() => observe.observeUninstall()));
125
+ ob.command('report').description('Render the local credential audit').option('--json').action(guard((o) => observe.observeReport(o)));
126
+ ob.command('sync').description('Upload findings into the project timeline').action(guard(() => observe.observeSync()));
127
+ // ---- policy ----
128
+ const pol = program.command('policy').description('Governance policy');
129
+ pol.command('get').option('--json').action(guard((o) => govern.policyGet(o)));
130
+ pol.command('set <action> <decision>').description('action: secrets.read|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)));
131
+ program.parseAsync(process.argv);
132
+ //# sourceMappingURL=index.js.map
@@ -0,0 +1,62 @@
1
+ // PostToolUse hook: reads a tool-use event on stdin (Claude Code / Codex), scans every string
2
+ // surface for credential exposure, and appends findings to ./.insta/audit.jsonl. Ported from firth.
3
+ import { appendFileSync, mkdirSync } from 'node:fs';
4
+ import { dirname, join, resolve } from 'node:path';
5
+ import { fileURLToPath, pathToFileURL } from 'node:url';
6
+ import { scanEvent } from './scanner.js';
7
+ export function recordFindings(event, baseDir) {
8
+ const instaDir = join(baseDir, '.insta');
9
+ const instaAbs = resolve(instaDir);
10
+ const selfDir = dirname(fileURLToPath(import.meta.url));
11
+ const ignore = (p) => {
12
+ let ap;
13
+ try {
14
+ ap = resolve(p);
15
+ }
16
+ catch {
17
+ return false;
18
+ }
19
+ return ap.startsWith(instaAbs) || ap.startsWith(selfDir);
20
+ };
21
+ const findings = scanEvent(event, { ignorePath: ignore });
22
+ if (findings.length === 0)
23
+ return 0;
24
+ const common = {
25
+ ts: new Date().toISOString(),
26
+ session_id: event.session_id ?? null,
27
+ tool: event.tool_name ?? null,
28
+ cwd: event.cwd ?? null,
29
+ };
30
+ mkdirSync(instaDir, { recursive: true });
31
+ const lines = findings.map((f) => JSON.stringify({ ...common, ...f })).join('\n') + '\n';
32
+ appendFileSync(join(instaDir, 'audit.jsonl'), lines, 'utf8');
33
+ return findings.length;
34
+ }
35
+ async function readStdin() {
36
+ const chunks = [];
37
+ for await (const c of process.stdin)
38
+ chunks.push(c);
39
+ return Buffer.concat(chunks).toString('utf8');
40
+ }
41
+ export async function main() {
42
+ let event;
43
+ try {
44
+ event = JSON.parse(await readStdin());
45
+ }
46
+ catch {
47
+ process.exit(0);
48
+ }
49
+ const base = process.env.CLAUDE_PROJECT_DIR || event.cwd || '.';
50
+ try {
51
+ recordFindings(event, base);
52
+ }
53
+ catch (e) {
54
+ process.stderr.write(`insta-observe: ${e instanceof Error ? e.message : e}\n`);
55
+ }
56
+ process.exit(0);
57
+ }
58
+ // Run only when executed directly (the materialized .insta/observe/hook.js entry).
59
+ // Imported by tests → guard is false → main() does not run.
60
+ if (process.argv[1] && import.meta.url === pathToFileURL(process.argv[1]).href)
61
+ void main();
62
+ //# sourceMappingURL=hook.js.map