Sign In

insta

Package Overview
Dependencies
Maintainers
3
Versions
35
Alerts
File Explorer

Advanced tools

Socket logo

Install Socket

Detect and block malicious and high-risk dependencies

Install

insta - npm Package Compare versions

Comparing version
0.0.31
to
0.0.32
+161
dist/resolve-service.js
// `insta services add` with no type (or no name): the kinds are otherwise only discoverable by
// guessing wrong and reading `type must be postgres|storage|compute`, so missing arguments answer
// "what can I add?" instead. The list mirrors the dashboard's Add Service menu (frontend
// `add-service-button.tsx`) — Docker Image sits BESIDE Empty Service, not under it, because
// picking an image is a different intent rather than a compute flag. An agent gets the same list
// as an error, because nothing was created and a silent exit 0 would read as success.
import * as clack from '@clack/prompts';
import { SERVICE_TYPES, assertServiceName, parsePort } from './commands/services.js';
// Same order, labels and default names as the dashboard's Add Service menu. Github Repo is left
// out: the platform has no repo path yet, so a CLI entry could only say "coming soon".
export const SERVICE_KINDS = [
{ id: 'image', label: 'Docker Image', type: 'compute', hint: 'run an existing container image', needsImage: true },
{ id: 'postgres', label: 'Postgres', type: 'postgres', hint: 'relational DB, usable as soon as it is added', defaultName: 'main-db' },
{ id: 'storage', label: 'Storage', type: 'storage', hint: 'S3-compatible bucket, private by default', defaultName: 'assets' },
{ id: 'compute', label: 'Empty Service', type: 'compute', hint: 'an app to deploy code to (empty until `insta deploy`)', defaultName: 'compute' },
];
// The platform's own default; the dialog prefills the same number.
export const DEFAULT_IMAGE_PORT = '8080';
/** Registry refs aren't URLs — quietly strip a pasted scheme prefix (mirrors the dashboard). */
export function normalizeImageRef(raw) {
return raw.trim().replace(/^https?:\/\//, '');
}
/**
* Name from an image ref: last path segment, sans tag/digest, kebab-safe (the dashboard's rule).
* Also capped at the 39 chars `assertServiceName` allows — a suggestion the user cannot accept
* unchanged is worse than none.
*/
export function suggestServiceName(ref) {
const last = ref.split('@')[0].split('/').pop() ?? '';
return last
.split(':')[0]
.toLowerCase()
.replace(/[^a-z0-9-]+/g, '-')
.replace(/^-+|-+$/g, '')
.slice(0, 39)
.replace(/-+$/g, '');
}
/** The non-interactive command for a kind — what an agent should run instead of being asked. */
export function kindCommand(k) {
if (k.needsImage)
return `insta services add compute <name> --image <ref> --port <n>`;
return `insta services add ${k.type} ${k.defaultName}`;
}
/** The kind list, one line each — what a terminal picks from and an agent reads. */
export function serviceKindLines() {
return SERVICE_KINDS.map((k) => ` ${k.label.padEnd(14)} ${kindCommand(k)}`);
}
/** What to say when there is no terminal to ask: the missing half, and how to supply it. */
export function missingArgsMessage(type) {
// A bare type names the plain kind, never Docker Image — that one is reached with --image.
const known = SERVICE_KINDS.find((k) => k.type === type && !k.needsImage);
if (known)
return `name the service: ${kindCommand(known)}`;
return ['what to add:', ...serviceKindLines()].join('\n');
}
/**
* Fill in whatever `insta services add` was not given. An unknown type passes straight through so
* `assertType` — not this — reports it, keeping one wording for a bad type everywhere. Flags that
* were already supplied are never asked for again.
*/
export async function resolveServiceArgs(type, name, deps, given = {}) {
if (type && name)
return { type, name };
if (type && !SERVICE_TYPES.includes(type))
return { type, name: name ?? '' };
if (!deps.tty)
throw new Error(missingArgsMessage(type));
// A bad --port is a typo in the command, not an answer: fail before asking anything.
if (given.port !== undefined)
parsePort(given.port);
const kind = type
? SERVICE_KINDS.find((k) => k.type === type && !k.needsImage)
: await deps.selectKind(SERVICE_KINDS);
if (!kind)
return { type: type, name: name ?? '' };
if (!kind.needsImage) {
return { type: kind.type, name: name ?? (await deps.askName(kind, kind.defaultName ?? '')) };
}
// The prompt validates a typed ref; a --image that normalizes away would slip past it and
// provision a plain empty compute instead (servicesAddRequestBody drops a falsy image).
const image = normalizeImageRef(given.image ?? (await deps.askImage()));
if (!image)
throw new Error('an image reference is required');
return {
type: kind.type,
name: name ?? (await deps.askName(kind, suggestServiceName(image))),
image,
port: given.port ?? (await deps.askPort(DEFAULT_IMAGE_PORT)),
};
}
/** Real prompts (clack, as the InsForge CLI's `create`); cancelling exits without provisioning. */
export async function promptServiceKind(kinds) {
const picked = await clack.select({
message: 'What do you want to add?',
options: kinds.map((k) => ({ value: k.id, label: k.label, hint: k.hint })),
});
if (clack.isCancel(picked))
process.exit(0);
// Resolve against the list that was displayed — a subset must not fall through to the registry.
return kinds.find((k) => k.id === picked);
}
export async function promptImageRef() {
const answer = await clack.text({
message: 'Image reference:',
placeholder: 'nginx:latest',
validate: (v) => (normalizeImageRef(v) ? undefined : 'an image reference is required'),
});
if (clack.isCancel(answer))
process.exit(0);
return answer;
}
export async function promptServiceName(kind, suggested) {
const answer = await clack.text({
message: `Name this ${kind.type} service:`,
initialValue: suggested,
// The same rule the command enforces, reported before Enter rather than after a round trip.
validate: (v) => {
try {
assertServiceName(v.trim());
return undefined;
}
catch (e) {
return e.message;
}
},
});
if (clack.isCancel(answer))
process.exit(0);
return answer.trim();
}
export async function promptPort(fallback) {
const answer = await clack.text({
message: 'Port the image listens on:',
initialValue: fallback,
// The rule the command enforces, so the prompt and a --port can never disagree.
validate: (v) => {
try {
parsePort(v.trim());
return undefined;
}
catch (e) {
return e.message;
}
},
});
if (clack.isCancel(answer))
process.exit(0);
return answer.trim();
}
/** Prompts on a real terminal only — an agent's stdin is not one, and must never block. */
export function serviceArgsDeps(json) {
return {
selectKind: promptServiceKind,
askImage: promptImageRef,
askName: promptServiceName,
askPort: promptPort,
// --json asked for parseable output, so a caller that happens to own a TTY still gets the error.
tty: !json && !!process.stdin.isTTY && !!process.stdout.isTTY,
};
}
//# sourceMappingURL=resolve-service.js.map
+12
-8

@@ -7,4 +7,5 @@ import { ApiClient, ApiError, requireProject } from '../api.js';

// actual usage). Thin wrapper over PATCH /database/settings {scaleToZero} — insta-db-backed
// postgres only; Neon-backed services manage their own autosuspend and the platform returns an
// error for them.
// postgres only. Legacy Neon path: Neon is no longer used by any environment (postgres is 100%
// insta-db) and this code is retained, not live — Neon-backed services managed their own
// autosuspend and the platform returned an error for them.
export async function dbAlwaysOn(mode, opts) {

@@ -57,3 +58,4 @@ if (mode !== 'on' && mode !== 'off')

// The platform answers a provider-shaped 502 for services with no manageable instance
// (Neon-backed): a soft case, not a failure. Everything else stays an error — an expired
// (the legacy Neon path — Neon is no longer used by any environment; this branch is retained,
// not live): a soft case, not a failure. Everything else stays an error — an expired
// token must not render as "no ceiling set" — but wrapped so the user sees what failed.

@@ -83,3 +85,3 @@ if (e instanceof ApiError && e.status === 502)

if (read.kind === 'no-instance') {
info(`postgres ${opts.group ?? 'default'}: no manageable instance (Neon-backed services manage their own resources)`);
info(`postgres ${opts.group ?? 'default'}: no manageable instance (this service manages its own resources)`);
return;

@@ -167,5 +169,7 @@ }

// hit rate, database size. Read-only. insta-db-backed: a suspended instance answers from the
// provider's control plane (shown as "(suspended)" with structural zeros), never dialed.
// Neon-backed: the platform reads over a direct SQL connection, so a one-shot call may wake a
// suspended endpoint — acceptable for an explicit command, which is why nothing here polls.
// provider's control plane (shown as "(suspended)" with structural zeros), never dialed. That is
// every environment today — the Neon-backed contrast below is historical: Neon is no longer used
// anywhere, and the code that handled it is retained, not live. Neon-backed: the platform read
// over a direct SQL connection, so a one-shot call could wake a suspended endpoint — acceptable
// for an explicit command, which is why nothing here polls.
export async function dbStats(opts) {

@@ -214,3 +218,3 @@ const api = await ApiClient.load();

if (read.kind === 'no-instance') {
info(`postgres ${opts.group ?? 'default'}: no manageable instance (Neon-backed services manage their own storage)`);
info(`postgres ${opts.group ?? 'default'}: no manageable instance (this service manages its own storage)`);
return;

@@ -217,0 +221,0 @@ }

+3
-1

@@ -100,3 +100,5 @@ import { ApiClient, requireProject } from '../api.js';

// current billing cycle. Shows the whole ORG by default (with a per-project breakdown); pass --proj
// [id] for a single project (the linked one, or a given id). Billed dimensions, not raw fly/neon meters.
// [id] for a single project (the linked one, or a given id). Billed dimensions, not raw provider
// meters. (Historical: those were fly/neon meters — Neon is no longer used by any environment,
// though the adapter code is retained, not live.)
export async function usage(opts) {

@@ -103,0 +105,0 @@ const api = await ApiClient.load();

@@ -26,2 +26,12 @@ // `insta services` — manage a project's opt-in services (postgres | storage | compute).

}
// Parse a TCP port. Junk fails here rather than reaching the API as NaN (the parseCpu lesson).
// Decimal digits only, as parseVolumeGib: `Number()` alone would quietly read 0x1f90 as 8080 and
// 1e3 as 1000, and a port written in hex is a typo worth reporting, not one worth honouring.
export function parsePort(raw) {
const m = /^\s*(\d+)\s*$/.exec(raw);
const n = m ? Number(m[1]) : NaN;
if (!Number.isInteger(n) || n < 1 || n > 65535)
throw new Error(`port must be an integer between 1 and 65535, got: ${raw}`);
return n;
}
// Parse a volume size in whole Gi: "10" or "10Gi" (suffix case-insensitive — unlike the db

@@ -68,3 +78,3 @@ // quantity strings this is not a provider pass-through; the wire value is an integer). Volumes

type, name, ...(branch ? { branch } : {}), public: !!opts.public,
...(opts.image ? { image: opts.image } : {}), ...(opts.port ? { port: Number(opts.port) } : {}),
...(opts.image ? { image: opts.image } : {}), ...(opts.port ? { port: parsePort(opts.port) } : {}),
...(opts.region ? { region: opts.region } : {}),

@@ -83,4 +93,7 @@ ...(opts.alwaysOn ? { alwaysOn: true } : {}),

throw new Error('--image is only valid for compute services');
if (opts.port && type !== 'compute')
throw new Error('--port is only valid for compute services');
if (opts.port) {
if (type !== 'compute')
throw new Error('--port is only valid for compute services');
parsePort(opts.port); // junk fails here, before any config/network access
}
if (opts.alwaysOn && type !== 'compute')

@@ -99,2 +112,4 @@ throw new Error('--always-on is only valid for compute services (for postgres, use `insta db always-on on` after creation)');

return;
if (opts.json)
return printJson(res.body.service);
const svc = res.body.service;

@@ -101,0 +116,0 @@ const access = svc.type === 'storage' ? ` [${svc.public ? 'public' : 'private'}]` : '';

@@ -16,2 +16,3 @@ #!/usr/bin/env node

import * as services from './commands/services.js';
import { resolveServiceArgs, serviceArgsDeps } from './resolve-service.js';
import * as regions from './commands/regions.js';

@@ -112,3 +113,6 @@ import * as secretsCmd from './commands/secrets.js';

const svc = program.command('services').alias('svc').description('Manage project services (postgres|storage|compute)');
svc.command('add <type> <name>').description('Provision a service on demand (assigns a default domain for postgres/compute)')
// [type] [name] are optional so the command can answer "what can I add?" — a terminal is walked
// through the dashboard's Add Service kinds, anything else gets that list back as an error
// (resolve-service.ts). Picking Docker Image also fills in --image/--port from the answers.
svc.command('add [type] [name]').description('Provision a service on demand (assigns a default domain for postgres/compute); with no type/name, a terminal picks from the service kinds')
.option('--branch <branch>', 'target branch (default: current)')

@@ -121,3 +125,7 @@ .option('--region <region>', 'region for postgres/compute, e.g. us-east (see `insta regions`)')

.option('--volume <gi>', 'compute only: attach a persistent /data volume of this many whole Gi (also attachable later: `insta compute volume <name> --size <gi>`; any plan may attach at the default 1; larger sizes are paid and plan-capped). Volume services keep 1 machine and stop (cold wake) instead of suspend when idle')
.action(guard((type, name, o) => services.servicesAdd(type, name, o)));
.option('--json')
.action(guard(async (type, name, o) => {
const a = await resolveServiceArgs(type, name, serviceArgsDeps(o.json), o);
return services.servicesAdd(a.type, a.name, { ...o, image: a.image ?? o.image, port: a.port ?? o.port });
}));
svc.command('list').option('--json').option('--branch <branch>', 'branch (default: current)')

@@ -135,3 +143,3 @@ .action(guard((o) => services.servicesList(o)));

.option('--json').option('--branch <branch>', 'branch (default: current)').action(guard((type, name, number, region, o) => services.servicesScale(type, name, number, region, o)));
svc.command('upgrade <type> <name> <spec>').description('Change a compute/postgres service spec (paid plans only)')
svc.command('upgrade <type> <name> <spec>').description('Change a compute service spec (paid plans only). Postgres upgrades are rejected by the platform — use `insta db limits` instead')
.option('--json').option('--branch <branch>', 'branch (default: current)').action(guard((type, name, spec, o) => services.servicesUpgrade(type, name, spec, o)));

@@ -138,0 +146,0 @@ svc.command('secrets <type> <name>').description("List a service's secret names")

{
"name": "insta",
"version": "0.0.31",
"version": "0.0.32",
"type": "module",

@@ -45,2 +45,3 @@ "description": "InstaCloud CLI — a thin client of the platform control-plane API.",

"dependencies": {
"@clack/prompts": "^0.9.1",
"commander": "^12.1.0"

@@ -47,0 +48,0 @@ },