insta 0.1.13 → 0.1.15
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/commands/deploy.js +1 -4
- package/dist/commands/services.js +11 -11
- package/dist/deploy-archive.js +1 -1
- package/dist/index.js +5 -5
- package/dist/pack.js +5 -26
- package/dist/resolve-service.js +3 -3
- package/package.json +1 -1
package/dist/commands/deploy.js
CHANGED
|
@@ -147,12 +147,9 @@ export async function deploy(dir, opts) {
|
|
|
147
147
|
const p = await requireProject();
|
|
148
148
|
const branch = opts.branch ?? p.branch;
|
|
149
149
|
const log = note(opts);
|
|
150
|
-
// Junk fails here, before a directory is packed and uploaded for a body the platform would only
|
|
151
|
-
// refuse: the same parser every other --port in this CLI runs. `Number()` alone sent NaN as
|
|
152
|
-
// `null` and let 0 or 70000 travel to the server.
|
|
153
150
|
let port;
|
|
154
151
|
try {
|
|
155
|
-
port = opts.port === undefined ? undefined : parsePort(opts.port);
|
|
152
|
+
port = opts.port === undefined ? undefined : parsePort(opts.port, { allowZero: true });
|
|
156
153
|
}
|
|
157
154
|
catch (e) {
|
|
158
155
|
die(`--${e.message}`);
|
|
@@ -47,14 +47,13 @@ export function parseCount(raw) {
|
|
|
47
47
|
}
|
|
48
48
|
return n;
|
|
49
49
|
}
|
|
50
|
-
//
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
export function parsePort(raw) {
|
|
50
|
+
// Keep TCP-only callers strict; port 0 is reserved for explicitly opted-in compute workers.
|
|
51
|
+
export function parsePort(raw, opts = {}) {
|
|
52
|
+
const min = opts.allowZero ? 0 : 1;
|
|
54
53
|
const m = /^\s*(\d+)\s*$/.exec(raw);
|
|
55
54
|
const n = m ? Number(m[1]) : NaN;
|
|
56
|
-
if (!Number.isInteger(n) || n <
|
|
57
|
-
throw new Error(`port must be an integer between
|
|
55
|
+
if (!Number.isInteger(n) || n < min || n > 65535)
|
|
56
|
+
throw new Error(`port must be an integer between ${min} and 65535, got: ${raw}`);
|
|
58
57
|
return n;
|
|
59
58
|
}
|
|
60
59
|
// Parse a volume size in whole Gi: "10" or "10Gi" (suffix case-insensitive — unlike the db
|
|
@@ -112,10 +111,11 @@ function defaultDatabasePort(type) {
|
|
|
112
111
|
export function servicesAddRequestBody(type, name, branch, opts) {
|
|
113
112
|
return {
|
|
114
113
|
type, name, ...(branch ? { branch } : {}), public: !!opts.public,
|
|
115
|
-
...(opts.image ? { image: opts.image } : {}), ...(opts.port ? { port: parsePort(opts.port) } : {}),
|
|
114
|
+
...(opts.image ? { image: opts.image } : {}), ...(opts.port !== undefined ? { port: parsePort(opts.port, { allowZero: type === 'compute' }) } : {}),
|
|
116
115
|
...(opts.region ? { region: opts.region } : {}),
|
|
117
|
-
// Sent whenever the flag was given, false included:
|
|
118
|
-
//
|
|
116
|
+
// Sent whenever the flag was given, false included: the birth default is platform config
|
|
117
|
+
// (INSTA_COMPUTE_ALWAYS_ON_DEFAULT, scale-to-zero unless set), so an explicit --no-always-on
|
|
118
|
+
// must reach the API as false to hold whatever that config says. Omitted means the platform default.
|
|
119
119
|
...(opts.alwaysOn !== undefined ? { alwaysOn: opts.alwaysOn } : {}),
|
|
120
120
|
...(opts.volume !== undefined ? { volumeGib: parseVolumeGib(opts.volume) } : {}),
|
|
121
121
|
...(opts.mountPath !== undefined ? { volumeMountPath: opts.mountPath } : {}),
|
|
@@ -130,10 +130,10 @@ export async function servicesAdd(type, name, opts = {}) {
|
|
|
130
130
|
throw new Error('--region is not valid for storage services');
|
|
131
131
|
if (opts.image && type !== 'compute')
|
|
132
132
|
throw new Error('--image is only valid for compute services');
|
|
133
|
-
if (opts.port) {
|
|
133
|
+
if (opts.port !== undefined) {
|
|
134
134
|
if (type !== 'compute')
|
|
135
135
|
throw new Error('--port is only valid for compute services');
|
|
136
|
-
parsePort(opts.port
|
|
136
|
+
parsePort(opts.port, { allowZero: true });
|
|
137
137
|
}
|
|
138
138
|
// Presence, not truthiness: `--no-always-on` is an explicit false and is just as compute-only.
|
|
139
139
|
if (opts.alwaysOn !== undefined && type !== 'compute')
|
package/dist/deploy-archive.js
CHANGED
|
@@ -121,7 +121,7 @@ export async function deployArchive(api, projectId, ref, branch, opts, now = Dat
|
|
|
121
121
|
if (state === 'live') {
|
|
122
122
|
const image = res.body?.imageRef;
|
|
123
123
|
const url = res.body?.url;
|
|
124
|
-
if (typeof image !== 'string' || !image || typeof url !== 'string' || !url) {
|
|
124
|
+
if (typeof image !== 'string' || !image || typeof url !== 'string' || (!url && opts.port !== '0')) {
|
|
125
125
|
throw new Error('the deploy finished but the platform returned no image or URL for it — check `insta status`');
|
|
126
126
|
}
|
|
127
127
|
const optionalString = (field, v) => {
|
package/dist/index.js
CHANGED
|
@@ -153,9 +153,9 @@ svc.command('add [type] [name]').description('Provision a service on demand (com
|
|
|
153
153
|
.option('--region <region>', 'region for postgres/compute/managed databases, e.g. us-east (see `insta config regions`)')
|
|
154
154
|
.option('--public', 'storage only: serve the bucket with anonymous public-read (default private)')
|
|
155
155
|
.option('--image <url>', 'compute only: run this container image at creation')
|
|
156
|
-
.option('--port <n>', 'compute only: port the image listens on (default 8080)')
|
|
157
|
-
.option('--always-on', 'compute only: create as always-on — never scales to zero (
|
|
158
|
-
.option('--no-always-on', 'compute only: create as scale-to-zero — idle machines suspend and wake on the next request')
|
|
156
|
+
.option('--port <n>', 'compute only: port the image listens on (default 8080; 0 for a worker without a routed port)')
|
|
157
|
+
.option('--always-on', 'compute only: create as always-on — never scales to zero (all plans; billing is actual usage either way). A worker (--port 0) is always-on regardless')
|
|
158
|
+
.option('--no-always-on', 'compute only: create as scale-to-zero — idle machines suspend and wake on the next request (the default for new compute services; refused with --port 0, a worker cannot scale to zero)')
|
|
159
159
|
.option('--mount-path <path>', 'compute only: container mount path for a new volume (requires --volume; default /data)')
|
|
160
160
|
.option('--volume <gi>', 'compute only: attach a persistent volume of this many whole Gi (also attachable later: `insta compute volume <name> --size <gi>`). Any plan may attach up to its own plan cap (10Gi free, 50Gi paid by default; the bare `insta compute volume <name>` read prints it as plan max); a size above the free cap is paid. Volume services keep 1 machine and stop (cold wake) instead of suspend when idle')
|
|
161
161
|
.option('--pg-version <major>', 'postgres only: the Postgres major version to create the database on — 16, 17 or 18 (default 16). Fixed for the database\'s lifetime; branches keep it')
|
|
@@ -379,7 +379,7 @@ compute.command('scale [count] [service]').description('Set a compute service sa
|
|
|
379
379
|
compute.command('limits [service]').description("Show or set a compute service's resource ceiling (any plan within the free cap; raising above it needs a paid plan). --memory is the dial; cpu derives from it unless --cpu is given. Billing is actual usage — the ceiling caps what the app may burn, it is not a price")
|
|
380
380
|
.option('--memory <size>', 'memory ceiling, e.g. 512mb or 1gb').option('--cpu <n>', 'vCPU ceiling override (provider sizes: 1, 2, 4, 6, 8)')
|
|
381
381
|
.option('--json').option('--branch <branch>', 'branch (default: current)').action(guard((service, o) => computeCmd.computeLimits(service, o)));
|
|
382
|
-
compute.command('always-on <mode> [service]').description('Set a compute service always-on (mode: on|off). on = machines never scale to zero (the default for new compute services
|
|
382
|
+
compute.command('always-on <mode> [service]').description('Set a compute service always-on (mode: on|off). on = machines never scale to zero; off = scale-to-zero (the default for new compute services; refused on a worker, port 0). All plans; billing is actual usage either way')
|
|
383
383
|
.option('--json').option('--branch <branch>', 'branch (default: current)').action(guard((mode, service, o) => computeCmd.computeAlwaysOn(mode, service, o)));
|
|
384
384
|
compute.command('ssh [service]')
|
|
385
385
|
.description("Issue a short-lived SSH certificate for a compute service and print the command that uses it -- this command does NOT open the session itself, it makes `ssh` work. `--setup` does the one-time work: it generates a dedicated key under ~/.insta/ssh (your existing keys are never touched), has the platform sign a SHORT-LIVED certificate for it, adds one @cert-authority line to ~/.ssh/known_hosts so every region is trusted without per-node fingerprint prompts, and writes an ssh_config block AT THE TOP of ~/.ssh/config giving each compute service the alias `<service>.insta`. After that it is plain `ssh api.insta`, scp and -L: the block renews that alias's certificate for you while OpenSSH parses the config. Needs an interactive login -- API keys are refused; use `insta compute exec` for one-shot commands from CI")
|
|
@@ -515,7 +515,7 @@ buildCmd.command('logs <build-id>').description('Read source-build output for a
|
|
|
515
515
|
.action(guard((id, opts) => buildLogs(id, opts)));
|
|
516
516
|
// ---- deploy ----
|
|
517
517
|
program.command('deploy [dir]').description('Deploy a source directory (built remotely; on insta-compute a Dockerfile is optional and nixpacks detects the runtime) or a prebuilt --image to a branch compute group')
|
|
518
|
-
.option('--image <url>', 'prebuilt container image to deploy (instead of a source dir)').option('--branch <b>').option('--group <g>').option('--port <p>')
|
|
518
|
+
.option('--image <url>', 'prebuilt container image to deploy (instead of a source dir)').option('--branch <b>').option('--group <g>').option('--port <p>', 'port the app listens on (0 for a worker without a routed port)')
|
|
519
519
|
.option('--websocket', 'run a WebSocket app (larger guest + connection-based concurrency)')
|
|
520
520
|
.option('--replace-source', 'the service deploys from a connected GitHub repo: switch it to this image and remove the repo connection (admin); without it such a deploy is refused')
|
|
521
521
|
.option('--json', 'print the deploy result as JSON (build progress goes to stderr)')
|
package/dist/pack.js
CHANGED
|
@@ -109,31 +109,8 @@ function walk(root, rel, out, links, ig, files, flavour, cap = Infinity) {
|
|
|
109
109
|
}
|
|
110
110
|
}
|
|
111
111
|
}
|
|
112
|
-
//
|
|
113
|
-
|
|
114
|
-
// into an archive that promises none. Two guards, and neither is a full one on its own:
|
|
115
|
-
//
|
|
116
|
-
// O_NOFOLLOW refuses when the final component is a symlink AT OPEN TIME, closing the swap the
|
|
117
|
-
// walk cannot see. Undefined on Windows, where it degrades to the check below.
|
|
118
|
-
//
|
|
119
|
-
// fstat on the OPEN HANDLE must still describe the file the walk measured: same inode, same
|
|
120
|
-
// device, same size. Its job is the tar's own consistency -- a file rewritten to a different
|
|
121
|
-
// length mid-pack would otherwise produce a header whose count disagrees with its payload.
|
|
122
|
-
//
|
|
123
|
-
// Two things neither closes, and both are stated rather than implied away:
|
|
124
|
-
//
|
|
125
|
-
// An ANCESTOR directory swapped for a symlink. Node exposes no openat, so resolving each
|
|
126
|
-
// component against a directory handle is not available here.
|
|
127
|
-
//
|
|
128
|
-
// A same-size plain file deleted and recreated. Measured on linux rather than assumed: the
|
|
129
|
-
// inode is REUSED and mtimeNs/ctimeNs are byte-identical for a delete+create inside one
|
|
130
|
-
// timestamp tick, so no stat-based identity can see it. It is also the least interesting case
|
|
131
|
-
// -- the symlink promise still holds, the tar stays well formed because the length did not
|
|
132
|
-
// move, and the archive simply carries a slightly newer copy of a file the caller owns.
|
|
133
|
-
//
|
|
134
|
-
// The residual on both is narrow: someone able to rewrite files and directories inside the tree
|
|
135
|
-
// being packed can already put any bytes they like into it by writing them.
|
|
136
|
-
export function readEntry(abs, e) {
|
|
112
|
+
// O_NOFOLLOW closes final-component symlink swaps only on platforms that expose it.
|
|
113
|
+
export function readEntry(abs, e, platform = process.platform) {
|
|
137
114
|
const noFollow = constants.O_NOFOLLOW ?? 0;
|
|
138
115
|
let fd;
|
|
139
116
|
try {
|
|
@@ -147,8 +124,10 @@ export function readEntry(abs, e) {
|
|
|
147
124
|
}
|
|
148
125
|
try {
|
|
149
126
|
const st = fstatSync(fd);
|
|
127
|
+
// Some Windows libuv versions omit lstat's device ID; inode and size must still match.
|
|
128
|
+
const missingDevice = platform === 'win32' && (e.dev === 0 || e.dev === 0n);
|
|
150
129
|
const same = st.isFile() && st.size === e.size
|
|
151
|
-
&& (e.ino === undefined || st.ino === e.ino) && (e.dev === undefined || st.dev === e.dev);
|
|
130
|
+
&& (e.ino === undefined || st.ino === e.ino) && (e.dev === undefined || missingDevice || st.dev === e.dev);
|
|
152
131
|
if (!same)
|
|
153
132
|
throw new Error(`${e.path} changed while packing — re-run the deploy`);
|
|
154
133
|
return readFileSync(fd);
|
package/dist/resolve-service.js
CHANGED
|
@@ -70,7 +70,7 @@ export async function resolveServiceArgs(type, name, deps, given = {}) {
|
|
|
70
70
|
throw new Error(missingArgsMessage(type));
|
|
71
71
|
// A bad --port is a typo in the command, not an answer: fail before asking anything.
|
|
72
72
|
if (given.port !== undefined)
|
|
73
|
-
parsePort(given.port);
|
|
73
|
+
parsePort(given.port, { allowZero: !type || type === 'compute' });
|
|
74
74
|
const kind = type
|
|
75
75
|
? SERVICE_KINDS.find((k) => k.type === type && !k.needsImage)
|
|
76
76
|
: await deps.selectKind(SERVICE_KINDS);
|
|
@@ -133,12 +133,12 @@ export async function promptServiceName(kind, suggested) {
|
|
|
133
133
|
}
|
|
134
134
|
export async function promptPort(fallback) {
|
|
135
135
|
const answer = await clack.text({
|
|
136
|
-
message: 'Port the image listens on:',
|
|
136
|
+
message: 'Port the image listens on (0 for a worker):',
|
|
137
137
|
initialValue: fallback,
|
|
138
138
|
// The rule the command enforces, so the prompt and a --port can never disagree.
|
|
139
139
|
validate: (v) => {
|
|
140
140
|
try {
|
|
141
|
-
parsePort(v.trim());
|
|
141
|
+
parsePort(v.trim(), { allowZero: true });
|
|
142
142
|
return undefined;
|
|
143
143
|
}
|
|
144
144
|
catch (e) {
|