insta 0.0.1 → 0.0.3

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,44 @@
1
+ import { resolve, join } from 'node:path';
2
+ import { existsSync } from 'node:fs';
3
+ import { ApiClient, requireProject } from '../api.js';
4
+ import { info, die, handleApproval } from '../util.js';
5
+ import { flyctlBuildAndPush, ensureFlyctl } from '../flyctl-build.js';
6
+ // Deploy either a prebuilt image (`--image`) or a source directory (positional `<dir>`, built
7
+ // remotely on Fly and pushed with a short-lived platform-minted token). Exactly one mode.
8
+ export async function deploy(dir, opts) {
9
+ if (dir && opts.image)
10
+ die('pick one: a source <dir> OR --image <url>, not both');
11
+ if (!dir && !opts.image)
12
+ die('usage: insta deploy <dir> | --image <url> [--branch <b>] [--group <g>] [--port <n>]');
13
+ const api = await ApiClient.load();
14
+ const p = await requireProject();
15
+ const branch = opts.branch ?? p.branch;
16
+ const image = dir ? await buildFromSource(api, p.projectId, dir, branch, opts) : opts.image;
17
+ const res = await api.rawRequest('POST', `/projects/${p.projectId}/deploy`, {
18
+ image,
19
+ branch,
20
+ group: opts.group,
21
+ port: opts.port ? Number(opts.port) : undefined,
22
+ });
23
+ if (handleApproval(res))
24
+ return;
25
+ info(`deployed ${image} -> ${res.body.url} (branch ${res.body.branch}, group ${res.body.group})`);
26
+ }
27
+ // Source mode: mint a scoped Fly deploy token from the platform, then build+push <dir> (needs a
28
+ // Dockerfile) with flyctl's remote builder, returning the pushed image ref to deploy.
29
+ async function buildFromSource(api, projectId, dir, branch, opts) {
30
+ const absDir = resolve(process.cwd(), dir);
31
+ if (!existsSync(join(absDir, 'Dockerfile')))
32
+ die(`no Dockerfile at ${join(absDir, 'Dockerfile')} — add one, or use --image <url>`);
33
+ await ensureFlyctl();
34
+ const port = opts.port ? Number(opts.port) : 8080;
35
+ const tok = await api.rawRequest('POST', `/projects/${projectId}/deploy-token`, { branch, group: opts.group });
36
+ if (handleApproval(tok))
37
+ die('deploy requires approval — get it approved, then re-run');
38
+ const { token, flyApp } = tok.body;
39
+ info(`building ${dir} for ${flyApp} (remote builder)…`);
40
+ const { imageRef } = await flyctlBuildAndPush({ dir: absDir, flyApp, imageLabel: `insta-${Date.now()}`, token, port });
41
+ info(` pushed ${imageRef}`);
42
+ return imageRef;
43
+ }
44
+ //# sourceMappingURL=deploy.js.map
@@ -0,0 +1,55 @@
1
+ import { ApiClient, requireProject } from '../api.js';
2
+ import { info, printJson } from '../util.js';
3
+ export async function events(opts) {
4
+ const api = await ApiClient.load();
5
+ const p = await requireProject();
6
+ const qs = [];
7
+ if (opts.branch)
8
+ qs.push(`branch=${encodeURIComponent(opts.branch)}`);
9
+ if (opts.limit)
10
+ qs.push(`limit=${opts.limit}`);
11
+ const { events } = await api.request('GET', `/projects/${p.projectId}/events${qs.length ? `?${qs.join('&')}` : ''}`);
12
+ if (opts.json)
13
+ return printJson(events);
14
+ for (const e of events)
15
+ info(`${e.created_at} [${e.source}] ${e.kind}`);
16
+ }
17
+ export async function approvalsList(opts) {
18
+ const api = await ApiClient.load();
19
+ const p = await requireProject();
20
+ const { approvals } = await api.request('GET', `/projects/${p.projectId}/approvals${opts.status ? `?status=${opts.status}` : ''}`);
21
+ if (opts.json)
22
+ return printJson(approvals);
23
+ if (!approvals.length)
24
+ return info('(no approvals)');
25
+ for (const a of approvals)
26
+ info(`${a.id} ${a.action} [${a.status}] ${a.requested_at}`);
27
+ }
28
+ export async function approvalsApprove(id, opts) {
29
+ const api = await ApiClient.load();
30
+ const p = await requireProject();
31
+ const out = await api.request('POST', `/projects/${p.projectId}/approvals/${id}/approve`, { always: !!opts.always });
32
+ info(`approved ${out.approval.action} (${id})${opts.always ? ' — policy set to allow' : ''}`);
33
+ }
34
+ export async function approvalsDeny(id) {
35
+ const api = await ApiClient.load();
36
+ const p = await requireProject();
37
+ const out = await api.request('POST', `/projects/${p.projectId}/approvals/${id}/deny`);
38
+ info(`denied ${out.approval.action} (${id})`);
39
+ }
40
+ export async function policyGet(opts) {
41
+ const api = await ApiClient.load();
42
+ const p = await requireProject();
43
+ const { policy } = await api.request('GET', `/projects/${p.projectId}/policy`);
44
+ if (opts.json)
45
+ return printJson(policy);
46
+ for (const [action, decision] of Object.entries(policy))
47
+ info(`${action}: ${decision}`);
48
+ }
49
+ export async function policySet(action, decision) {
50
+ const api = await ApiClient.load();
51
+ const p = await requireProject();
52
+ await api.request('PUT', `/projects/${p.projectId}/policy/${action}`, { decision });
53
+ info(`policy ${action} = ${decision}`);
54
+ }
55
+ //# sourceMappingURL=govern.js.map
@@ -0,0 +1,20 @@
1
+ import { ApiClient, requireProject } from '../api.js';
2
+ import { info, printJson } from '../util.js';
3
+ // Agent-legible view of each environment's databases / storage / compute.
4
+ export async function manifest(opts) {
5
+ const api = await ApiClient.load();
6
+ const p = await requireProject();
7
+ const detail = await api.request('GET', `/projects/${p.projectId}`);
8
+ if (opts.json)
9
+ return printJson(detail);
10
+ info(`project ${detail.project.name} (${detail.project.id}) [${detail.project.status}]`);
11
+ for (const b of detail.branches) {
12
+ info(` branch ${b.name}${b.is_default ? ' *' : ''} [${b.status}]`);
13
+ const rs = detail.resources.filter((r) => r.branchId === b.id || (b.is_default && r.branchId === null));
14
+ for (const r of rs) {
15
+ const where = r.ref?.url ?? r.ref?.bucket ?? r.ref?.neonProjectId ?? '';
16
+ info(` - ${r.kind}${r.name ? `(${r.name})` : ''} ${where} [${r.status}]`);
17
+ }
18
+ }
19
+ }
20
+ //# sourceMappingURL=manifest.js.map
@@ -0,0 +1,88 @@
1
+ import { ApiClient, requireProject } from '../api.js';
2
+ import { info, printJson } from '../util.js';
3
+ function qs(params) {
4
+ const u = new URLSearchParams();
5
+ for (const [k, v] of Object.entries(params))
6
+ if (v !== undefined && v !== '')
7
+ u.set(k, String(v));
8
+ const s = u.toString();
9
+ return s ? `?${s}` : '';
10
+ }
11
+ // insta metrics <db|compute> [group]
12
+ export async function metrics(component, group, opts) {
13
+ const api = await ApiClient.load();
14
+ const p = await requireProject();
15
+ const res = await api.request('GET', `/projects/${p.projectId}/metrics${qs({ component, group, branch: opts.branch ?? p.branch, from: opts.from, to: opts.to, step: opts.step })}`);
16
+ if (opts.json)
17
+ return printJson(res);
18
+ if (res.note)
19
+ info(`note: ${res.note}`);
20
+ if (!res.series?.length)
21
+ return info('(no series)');
22
+ for (const s of res.series) {
23
+ const last = s.points?.[s.points.length - 1];
24
+ info(`${s.name}${s.unit ? ` (${s.unit})` : ''}: ${last ? last[1] : 'n/a'} [${s.points?.length ?? 0} points]`);
25
+ }
26
+ }
27
+ // Customer-facing name for each internal billing dimension (the platform stores RAM as `ram`).
28
+ const DIMENSION_LABEL = { ram: 'memory' };
29
+ // Window line. Defaults to the current billing cycle; `to` is the exclusive next-cycle start, so
30
+ // show the inclusive last day (to − 1 day) — e.g. an org created on the 5th reads "…-05 → …next-04".
31
+ function cycleLine(res) {
32
+ const day = (sec) => new Date(sec * 1000).toISOString().slice(0, 10);
33
+ return `billing cycle ${day(res.from)} → ${day(res.to - 86400)}`;
34
+ }
35
+ function printDimensions(dims) {
36
+ for (const d of dims) {
37
+ const label = DIMENSION_LABEL[d.dimension] ?? d.dimension;
38
+ const cost = d.costUsd != null ? ` ($${Number(d.costUsd).toFixed(4)})` : '';
39
+ info(`${label}: ${d.quantity} ${d.unit}${cost}`);
40
+ }
41
+ }
42
+ // insta usage — usage across the 5 billing dimensions (cpu/memory/volume/egress/storage) for the
43
+ // current billing cycle. Shows the whole ORG by default (with a per-project breakdown); pass --proj
44
+ // [id] for a single project (the linked one, or a given id). Billed dimensions, not raw fly/neon meters.
45
+ export async function usage(opts) {
46
+ const api = await ApiClient.load();
47
+ const p = await requireProject();
48
+ if (opts.proj !== undefined && opts.proj !== false) {
49
+ const projectId = typeof opts.proj === 'string' ? opts.proj : p.projectId;
50
+ const res = await api.request('GET', `/projects/${projectId}/usage${qs({ from: opts.from, to: opts.to })}`);
51
+ if (opts.json)
52
+ return printJson(res);
53
+ info(cycleLine(res));
54
+ if (!res.dimensions?.length)
55
+ return info('(no usage recorded)');
56
+ printDimensions(res.dimensions);
57
+ return info(`total: $${Number(res.totalCostUsd ?? 0).toFixed(4)}`);
58
+ }
59
+ // Default: the whole org (the linked project's org), with a per-project cost breakdown.
60
+ const res = await api.request('GET', `/orgs/${p.orgId}/usage${qs({ from: opts.from, to: opts.to })}`);
61
+ if (opts.json)
62
+ return printJson(res);
63
+ info(cycleLine(res));
64
+ if (!res.org?.dimensions?.length)
65
+ return info('(no usage recorded)');
66
+ printDimensions(res.org.dimensions);
67
+ info(`total: $${Number(res.org.totalCostUsd ?? 0).toFixed(4)}`);
68
+ if (res.projects?.length) {
69
+ info('by project:');
70
+ for (const pr of res.projects)
71
+ info(` ${pr.name}: $${Number(pr.totalCostUsd ?? 0).toFixed(4)}`);
72
+ }
73
+ }
74
+ // insta logs <db|compute> [group]
75
+ export async function logs(component, group, opts) {
76
+ const api = await ApiClient.load();
77
+ const p = await requireProject();
78
+ 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 })}`);
79
+ if (opts.json)
80
+ return printJson(res);
81
+ if (res.note)
82
+ info(`note: ${res.note}`);
83
+ if (!res.lines?.length)
84
+ return info('(no logs)');
85
+ for (const l of res.lines)
86
+ info(`${l.ts} ${(l.level ?? '').padEnd(5)} ${l.message}`);
87
+ }
88
+ //# sourceMappingURL=metrics.js.map
@@ -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