insta 0.0.11 → 0.0.13
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 +1 -1
- package/dist/commands/branch.js +2 -1
- package/dist/commands/compute.js +26 -0
- package/dist/commands/deploy.js +2 -1
- package/dist/commands/project.js +30 -3
- package/dist/commands/services.js +17 -1
- package/dist/index.js +11 -3
- package/dist/util.js +25 -0
- package/package.json +1 -1
package/dist/api.js
CHANGED
|
@@ -55,7 +55,7 @@ export class ApiClient {
|
|
|
55
55
|
return r;
|
|
56
56
|
}
|
|
57
57
|
async fetch(method, path, body, auth) {
|
|
58
|
-
const headers = { 'Content-Type': 'application/json' };
|
|
58
|
+
const headers = { 'Content-Type': 'application/json', 'Insta-Hints': '1' };
|
|
59
59
|
if (auth && this.cfg.accessToken)
|
|
60
60
|
headers.Authorization = `Bearer ${this.cfg.accessToken}`;
|
|
61
61
|
const res = await fetch(this.apiUrl + path, {
|
package/dist/commands/branch.js
CHANGED
|
@@ -1,11 +1,12 @@
|
|
|
1
1
|
import { ApiClient, requireProject } from '../api.js';
|
|
2
2
|
import { writeProject } from '../config.js';
|
|
3
|
-
import { info, die, printJson, handleApproval } from '../util.js';
|
|
3
|
+
import { info, die, printJson, handleApproval, renderNextActions } from '../util.js';
|
|
4
4
|
export async function branchCreate(name, opts) {
|
|
5
5
|
const api = await ApiClient.load();
|
|
6
6
|
const p = await requireProject();
|
|
7
7
|
const out = await api.request('POST', `/projects/${p.projectId}/branches`, { name, from: opts.from ?? p.branch });
|
|
8
8
|
info(`created branch ${out.branch.name} (${out.branch.id})`);
|
|
9
|
+
renderNextActions(out.nextActions);
|
|
9
10
|
}
|
|
10
11
|
export async function branchList(opts) {
|
|
11
12
|
const api = await ApiClient.load();
|
package/dist/commands/compute.js
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { ApiClient, requireProject } from '../api.js';
|
|
2
2
|
import { info, printJson, handleApproval } from '../util.js';
|
|
3
|
+
import { resolveComputeServiceId } from './services.js';
|
|
3
4
|
// Attach a developer-owned custom domain to a branch's compute service. Fly issues the cert + routes
|
|
4
5
|
// it; the platform returns the DNS records to set in your OWN zone.
|
|
5
6
|
export async function setDomain(host, opts) {
|
|
@@ -42,4 +43,29 @@ function printDomain(r, json) {
|
|
|
42
43
|
if (!r.configured)
|
|
43
44
|
info(' once DNS propagates, Fly issues the cert — re-check with `insta compute check-domain`');
|
|
44
45
|
}
|
|
46
|
+
async function lifecycle(verb, serviceName, opts) {
|
|
47
|
+
const api = await ApiClient.load();
|
|
48
|
+
const p = await requireProject();
|
|
49
|
+
const { services } = await api.request('GET', `/projects/${p.projectId}/services`);
|
|
50
|
+
const id = resolveComputeServiceId(services, serviceName);
|
|
51
|
+
const res = await api.rawRequest('POST', `/projects/${p.projectId}/services/${id}/${verb}`);
|
|
52
|
+
if (handleApproval(res))
|
|
53
|
+
return;
|
|
54
|
+
if (opts.json)
|
|
55
|
+
return printJson(res.body);
|
|
56
|
+
info(`compute ${res.body.service?.name ?? id}: ${verb} → desired=${res.body.service?.desired_state} (live: ${res.body.state})`);
|
|
57
|
+
}
|
|
58
|
+
export const computeStart = (service, opts) => lifecycle('start', service, opts);
|
|
59
|
+
export const computeStop = (service, opts) => lifecycle('stop', service, opts);
|
|
60
|
+
export const computeSuspend = (service, opts) => lifecycle('suspend', service, opts);
|
|
61
|
+
export async function computeStatus(serviceName, opts) {
|
|
62
|
+
const api = await ApiClient.load();
|
|
63
|
+
const p = await requireProject();
|
|
64
|
+
const { services } = await api.request('GET', `/projects/${p.projectId}/services`);
|
|
65
|
+
const id = resolveComputeServiceId(services, serviceName);
|
|
66
|
+
const r = await api.request('GET', `/projects/${p.projectId}/services/${id}/state`);
|
|
67
|
+
if (opts.json)
|
|
68
|
+
return printJson(r);
|
|
69
|
+
info(`compute ${serviceName ?? id}: desired=${r.desiredState} live=${r.state}`);
|
|
70
|
+
}
|
|
45
71
|
//# sourceMappingURL=compute.js.map
|
package/dist/commands/deploy.js
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { resolve, join } from 'node:path';
|
|
2
2
|
import { existsSync, readFileSync } from 'node:fs';
|
|
3
3
|
import { ApiClient, requireProject } from '../api.js';
|
|
4
|
-
import { info, die, handleApproval } from '../util.js';
|
|
4
|
+
import { info, die, handleApproval, renderNextActions } from '../util.js';
|
|
5
5
|
import { flyctlBuildAndPush, ensureFlyctl } from '../flyctl-build.js';
|
|
6
6
|
// Map CLI options to the platform deploy request body. Pure, so it's unit-tested. --websocket is only
|
|
7
7
|
// sent when set (plain deploys unchanged).
|
|
@@ -51,6 +51,7 @@ export async function deploy(dir, opts) {
|
|
|
51
51
|
if (handleApproval(res))
|
|
52
52
|
return;
|
|
53
53
|
info(`deployed ${image} -> ${res.body.url} (branch ${res.body.branch}, group ${res.body.group})`);
|
|
54
|
+
renderNextActions(res.body.nextActions);
|
|
54
55
|
}
|
|
55
56
|
// Source mode: mint a scoped Fly deploy token from the platform, then build+push <dir> (needs a
|
|
56
57
|
// Dockerfile) with flyctl's remote builder, returning the pushed image ref to deploy.
|
package/dist/commands/project.js
CHANGED
|
@@ -1,8 +1,19 @@
|
|
|
1
|
+
import { createInterface } from 'node:readline/promises';
|
|
1
2
|
import { ApiClient, requireProject } from '../api.js';
|
|
2
3
|
import { writeProject } from '../config.js';
|
|
3
|
-
import { info, die, printJson, handleApproval } from '../util.js';
|
|
4
|
+
import { info, die, printJson, handleApproval, renderNextActions } from '../util.js';
|
|
4
5
|
import { installObserve } from '../observe/install.js';
|
|
5
6
|
import { installSkills } from '../ensure-skills.js';
|
|
7
|
+
// Interactive name prompt (stderr, so piped stdout stays clean). Enter accepts the default.
|
|
8
|
+
async function promptName(question, def) {
|
|
9
|
+
const rl = createInterface({ input: process.stdin, output: process.stderr });
|
|
10
|
+
try {
|
|
11
|
+
return (await rl.question(`${question} [${def}]: `)).trim();
|
|
12
|
+
}
|
|
13
|
+
finally {
|
|
14
|
+
rl.close();
|
|
15
|
+
}
|
|
16
|
+
}
|
|
6
17
|
// Best-effort: wire the credential-audit hook into the project (no-op if assets aren't built).
|
|
7
18
|
function tryInstallObserve() {
|
|
8
19
|
try {
|
|
@@ -20,14 +31,30 @@ async function resolveOrg(api, given) {
|
|
|
20
31
|
die('no org found — run `insta org create <name>`');
|
|
21
32
|
return orgs[0].id;
|
|
22
33
|
}
|
|
34
|
+
/** A valid project name from a raw string: lowercase, non-alnum → hyphen, trimmed. */
|
|
35
|
+
export function slugifyName(raw) {
|
|
36
|
+
return raw.toLowerCase().replace(/[^a-z0-9]+/g, '-').replace(/^-+|-+$/g, '').slice(0, 40);
|
|
37
|
+
}
|
|
38
|
+
/** Name resolution so the recommended one-liner (`insta project create`, no arg) is paste-and-run:
|
|
39
|
+
* explicit arg wins; else prompt with the cwd basename as default (TTY); else use the basename. */
|
|
40
|
+
export async function resolveProjectName(nameArg, cwd = process.cwd(), prompt) {
|
|
41
|
+
const fromDir = slugifyName(cwd.split('/').filter(Boolean).pop() ?? 'app') || 'app';
|
|
42
|
+
if (nameArg)
|
|
43
|
+
return slugifyName(nameArg);
|
|
44
|
+
if (prompt && process.stdin.isTTY)
|
|
45
|
+
return slugifyName((await prompt('project name', fromDir)) || fromDir) || fromDir;
|
|
46
|
+
return fromDir;
|
|
47
|
+
}
|
|
23
48
|
export async function projectCreate(name, opts) {
|
|
24
49
|
const api = await ApiClient.load();
|
|
25
50
|
const orgId = await resolveOrg(api, opts.org);
|
|
26
|
-
const
|
|
51
|
+
const resolved = await resolveProjectName(name, process.cwd(), promptName);
|
|
52
|
+
const out = await api.request('POST', `/orgs/${orgId}/projects`, { name: resolved });
|
|
27
53
|
await writeProject({ projectId: out.project.id, orgId, branch: out.defaultBranch.name });
|
|
28
|
-
info(`created project ${out.project.id} (${
|
|
54
|
+
info(`created project ${out.project.id} (${resolved})`);
|
|
29
55
|
info(` resources: ${out.resources.map((r) => r.kind).join(', ')}`);
|
|
30
56
|
info(` linked ./.insta/project.json (branch ${out.defaultBranch.name})`);
|
|
57
|
+
renderNextActions(out.nextActions);
|
|
31
58
|
tryInstallObserve();
|
|
32
59
|
await installSkills({ cwd: process.cwd() });
|
|
33
60
|
}
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
// `insta services` — manage a project's opt-in services (postgres | storage | compute).
|
|
2
2
|
import { ApiClient, requireProject } from '../api.js';
|
|
3
|
-
import { info, printJson, handleApproval } from '../util.js';
|
|
3
|
+
import { info, printJson, handleApproval, renderNextActions } from '../util.js';
|
|
4
4
|
export const SERVICE_TYPES = ['postgres', 'storage', 'compute'];
|
|
5
5
|
// ---- pure, unit-tested helpers (throw plain Errors; the CLI guard turns them into clean output) ----
|
|
6
6
|
// Validate a service-type argument against the allowed set for a command.
|
|
@@ -22,6 +22,21 @@ export function resolveServiceId(services, type, name) {
|
|
|
22
22
|
throw new Error(`service not found: ${type} ${name}`);
|
|
23
23
|
return svc.id;
|
|
24
24
|
}
|
|
25
|
+
// Resolve a compute service id: by name, or the sole compute service when name is omitted.
|
|
26
|
+
export function resolveComputeServiceId(services, name) {
|
|
27
|
+
const compute = services.filter((s) => s.type === 'compute');
|
|
28
|
+
if (name) {
|
|
29
|
+
const svc = compute.find((s) => s.name === name);
|
|
30
|
+
if (!svc)
|
|
31
|
+
throw new Error(`compute service not found: ${name}`);
|
|
32
|
+
return svc.id;
|
|
33
|
+
}
|
|
34
|
+
if (compute.length === 0)
|
|
35
|
+
throw new Error('no compute service in this project (add one with `insta services add compute <name>`)');
|
|
36
|
+
if (compute.length > 1)
|
|
37
|
+
throw new Error(`multiple compute services — specify one: ${compute.map((s) => s.name).join(', ')}`);
|
|
38
|
+
return compute[0].id;
|
|
39
|
+
}
|
|
25
40
|
// ---- commands ----
|
|
26
41
|
export async function servicesAdd(type, name) {
|
|
27
42
|
assertType(type);
|
|
@@ -32,6 +47,7 @@ export async function servicesAdd(type, name) {
|
|
|
32
47
|
return;
|
|
33
48
|
const svc = res.body.service;
|
|
34
49
|
info(`added ${type} service ${name} (${svc.id})${svc.domain ? ` — ${svc.domain}` : ''}`);
|
|
50
|
+
renderNextActions(res.body.nextActions);
|
|
35
51
|
}
|
|
36
52
|
export async function servicesList(opts) {
|
|
37
53
|
const api = await ApiClient.load();
|
package/dist/index.js
CHANGED
|
@@ -73,7 +73,7 @@ orgCmd.command('list').option('--json').action(guard((o) => org.orgList(o)));
|
|
|
73
73
|
orgCmd.command('create <name>').action(guard((name) => org.orgCreate(name)));
|
|
74
74
|
// ---- project ----
|
|
75
75
|
const pj = program.command('project').description('Manage projects');
|
|
76
|
-
pj.command('create
|
|
76
|
+
pj.command('create [name]').option('--org <id>', 'org to create under (default: personal)').action(guard((name, o) => project.projectCreate(name, o)));
|
|
77
77
|
pj.command('list').option('--org <id>').option('--json').action(guard((o) => project.projectList(o)));
|
|
78
78
|
pj.command('link <id>').description('Link a project to this directory').action(guard((id) => project.projectLink(id)));
|
|
79
79
|
pj.command('delete').option('--project <id>').action(guard((o) => project.projectDelete(o)));
|
|
@@ -108,14 +108,22 @@ program.command('deploy [dir]').description('Deploy a source directory (built re
|
|
|
108
108
|
.option('--image <url>', 'prebuilt container image to deploy (instead of a source dir)').option('--branch <b>').option('--group <g>').option('--port <p>')
|
|
109
109
|
.option('--websocket', 'run a WebSocket app (larger guest + connection-based concurrency)')
|
|
110
110
|
.action(guard((dir, o) => deploy(dir, o)));
|
|
111
|
-
// ---- compute
|
|
112
|
-
const compute = program.command('compute').description('
|
|
111
|
+
// ---- compute (lifecycle control + custom domains) ----
|
|
112
|
+
const compute = program.command('compute').description('Control compute lifecycle (start/stop/suspend/status) + custom domains');
|
|
113
113
|
compute.command('set-domain <host>').description('Attach a custom domain to a branch compute service (gated: deploy)')
|
|
114
114
|
.option('--branch <b>').option('--group <g>').option('--json').action(guard((host, o) => computeCmd.setDomain(host, o)));
|
|
115
115
|
compute.command('check-domain <host>').description("Show a custom domain's cert status + required DNS records")
|
|
116
116
|
.option('--branch <b>').option('--group <g>').option('--json').action(guard((host, o) => computeCmd.checkDomain(host, o)));
|
|
117
117
|
compute.command('remove-domain <host>').description('Detach a custom domain (gated: deploy)')
|
|
118
118
|
.option('--branch <b>').option('--group <g>').action(guard((host, o) => computeCmd.removeDomain(host, o)));
|
|
119
|
+
compute.command('start [service]').description('Bring a compute service online (persistent — re-enables auto-wake)')
|
|
120
|
+
.option('--json').action(guard((service, o) => computeCmd.computeStart(service, o)));
|
|
121
|
+
compute.command('stop [service]').description('Take a compute service offline; traffic will NOT wake it until `start`')
|
|
122
|
+
.option('--json').action(guard((service, o) => computeCmd.computeStop(service, o)));
|
|
123
|
+
compute.command('suspend [service]').description('Suspend a compute service (RAM snapshot); stays down until `start`')
|
|
124
|
+
.option('--json').action(guard((service, o) => computeCmd.computeSuspend(service, o)));
|
|
125
|
+
compute.command('status [service]').description("Show a compute service's desired vs. live state")
|
|
126
|
+
.option('--json').action(guard((service, o) => computeCmd.computeStatus(service, o)));
|
|
119
127
|
// ---- manifest ----
|
|
120
128
|
program.command('manifest').description('Print an agent-legible view of the project environments').option('--json').action(guard((o) => manifest(o)));
|
|
121
129
|
// ---- observability ----
|
package/dist/util.js
CHANGED
|
@@ -34,6 +34,31 @@ export function handleApproval(res) {
|
|
|
34
34
|
}
|
|
35
35
|
return false;
|
|
36
36
|
}
|
|
37
|
+
// Neutral op → an `insta` command string. Unknown ops fall back to reason-only (no crash).
|
|
38
|
+
const OP_COMMAND = {
|
|
39
|
+
'service.add': (a) => `insta services add ${a.type ?? '<type>'} ${a.name ?? '<name>'}`,
|
|
40
|
+
deploy: (a) => `insta deploy${a.branch ? ` --branch ${a.branch}` : ''}`,
|
|
41
|
+
'secrets.set': (a) => `insta secrets set ${a.name ?? '<NAME>'} ${a.value ?? '<value>'}`,
|
|
42
|
+
metrics: (a) => `insta metrics ${a.target ?? 'compute'}`,
|
|
43
|
+
logs: (a) => `insta logs ${a.target ?? 'compute'}`,
|
|
44
|
+
'approvals.approve': (a) => `insta approvals approve ${a.approvalId ?? '<id>'}`,
|
|
45
|
+
};
|
|
46
|
+
// Pure — builds the printable lines (unit-tested). Empty input → [].
|
|
47
|
+
export function nextActionsLines(actions) {
|
|
48
|
+
if (!actions || actions.length === 0)
|
|
49
|
+
return [];
|
|
50
|
+
const lines = ['Next:'];
|
|
51
|
+
for (const a of actions) {
|
|
52
|
+
const cmd = OP_COMMAND[a.op]?.(a.args ?? {});
|
|
53
|
+
const gated = a.gated ? ' [needs approval]' : '';
|
|
54
|
+
lines.push(cmd ? ` • ${a.reason} → ${cmd}${gated}` : ` • ${a.reason}${gated}`);
|
|
55
|
+
}
|
|
56
|
+
return lines;
|
|
57
|
+
}
|
|
58
|
+
export function renderNextActions(actions) {
|
|
59
|
+
for (const line of nextActionsLines(actions))
|
|
60
|
+
info(line);
|
|
61
|
+
}
|
|
37
62
|
// Serialize a credential bundle to .env text. All values are double-quoted (connection strings
|
|
38
63
|
// contain special chars); backslashes and quotes are escaped so dotenv parsers read them back exactly.
|
|
39
64
|
export function serializeEnv(bundle) {
|