@@ -1,2 +0,2 @@ | ||
| import { ApiClient, requireProject } from '../api.js'; | ||
| import { ApiClient, ApiError, requireProject } from '../api.js'; | ||
| import { info, printJson, handleApproval } from '../util.js'; | ||
@@ -119,3 +119,3 @@ import { resolveComputeServiceId, q, parseVolumeGib } from './services.js'; | ||
| } | ||
| // ---- volume (the persistent /data disk; attach any time, grow-only, never detach) ---- | ||
| // ---- volume (the persistent /data disk; attach any time, grow-only, deletable; never detach) ---- | ||
| // Render the volume read. Pure, exported for tests (mirrors serviceListLine). Every plan may view; | ||
@@ -130,3 +130,3 @@ // only growth is paid — that gate is the backend's to enforce, so nothing here pre-blocks. | ||
| `compute ${name}: volume ${volume.sizeGib}Gi at ${volume.mountPath} (plan max ${cap.volumeGib}Gi)`, | ||
| ' billing is actual data stored — the size is a cap, not a price; grow with --size (grow-only)', | ||
| ' billing is actual data stored — the size is a cap, not a price; grow with --size (grow-only), delete with --delete (destroys the data)', | ||
| ]; | ||
@@ -143,7 +143,32 @@ } | ||
| } | ||
| // Show, attach, or grow a compute service's /data volume. No --size: a safe read (size + mount | ||
| // path + the plan cap). --size: PUT .../volume — attaches when no volume exists, grows otherwise. | ||
| // The paid/cap/machine-count gates all belong to the backend, whose 403/400 messages carry the | ||
| // upgrade hints and must reach the user verbatim (the guard prints ApiError messages as-is). | ||
| // Render the DELETE result. Pure, exported for tests. Deleting is the only way off the volume | ||
| // path (there is no detach), so the line says what came back with it: the two constraints the | ||
| // volume imposed. | ||
| export function volumeDeleteLine(name) { | ||
| return `compute ${name}: volume deleted — the disk and its data are gone; suspend fast-wake and scale-out are back`; | ||
| } | ||
| // Map a DELETE .../volume failure. Pure, exported for tests (r2d2 review rounds 1+2: this is the | ||
| // close-call branch worth pinning). An older backend has no DELETE route, and what its 404 looks | ||
| // like depends on who answered: the real platform (Fastify, no custom notFound handler) sends its | ||
| // default body {"message":"Route DELETE:/… not found","error":"Not Found"} → ApiError message | ||
| // "Not Found"; a proxy or bodyless 404 leaves ApiError's own "HTTP 404" fallback. BOTH are the | ||
| // generic route-miss shape and mean version skew, not a bug — parroting them would send the user | ||
| // hunting the wrong thing. A backend that HAS the route names the real problem in a DOMAIN | ||
| // message ("this service has no volume", …), which must flow verbatim, 404 or not. | ||
| const GENERIC_404 = /^(HTTP 404|Not Found)$/i; | ||
| export function volumeDeleteError(e) { | ||
| if (e instanceof ApiError && e.status === 404 && GENERIC_404.test(e.message.trim())) { | ||
| return new Error('this backend does not support volume delete yet — update the platform, or delete the service to remove its volume'); | ||
| } | ||
| return e; | ||
| } | ||
| // Show, attach, grow, or delete a compute service's /data volume. No flag: a safe read (size + | ||
| // mount path + the plan cap). --size: PUT .../volume — attaches when no volume exists, grows | ||
| // otherwise. --delete: DELETE .../volume — destroys the disk and its data immediately (no detach, | ||
| // no undo; billing stops now). The paid/cap/machine-count gates all belong to the backend, whose | ||
| // 403/400 messages carry the upgrade hints and must reach the user verbatim (the guard prints | ||
| // ApiError messages as-is). | ||
| export async function computeVolume(serviceName, opts) { | ||
| if (opts.delete && opts.size) | ||
| throw new Error('--delete cannot be combined with --size (one changes the volume, the other destroys it)'); | ||
| const api = await ApiClient.load(); | ||
@@ -154,2 +179,17 @@ const p = await requireProject(); | ||
| const id = resolveComputeServiceId(services, serviceName); | ||
| if (opts.delete) { | ||
| let res; | ||
| try { | ||
| res = await api.rawRequest('DELETE', `/projects/${p.projectId}/services/${id}/volume`); | ||
| } | ||
| catch (e) { | ||
| throw volumeDeleteError(e); | ||
| } | ||
| if (handleApproval(res)) | ||
| return; | ||
| if (opts.json) | ||
| return printJson(res.body); | ||
| info(volumeDeleteLine(res.body.service?.name ?? serviceName ?? id)); | ||
| return; | ||
| } | ||
| if (!opts.size) { | ||
@@ -156,0 +196,0 @@ const r = await api.request('GET', `/projects/${p.projectId}/services/${id}/volume`); |
+18
-0
@@ -75,2 +75,20 @@ // Build a source directory into an image and push it to Fly's registry, using a short-lived, | ||
| } | ||
| if (process.platform === 'linux') { | ||
| // A fresh Linux machine (CI containers included — insta-e2e run 31284364163) has no flyctl | ||
| // and no brew; without this branch `insta deploy <dir>` dead-ends on a hand-install of a | ||
| // third-party CLI. Official installer, pinned into ~/.fly; the current process extends its | ||
| // own PATH because the installer's shell-profile edit can't reach an already-running process. | ||
| info('flyctl not found — installing to ~/.fly (one-time)…'); | ||
| const flyHome = `${process.env.HOME ?? '~'}/.fly`; | ||
| const installed = await ok('sh', ['-c', `curl -fsSL https://fly.io/install.sh | FLYCTL_INSTALL="${flyHome}" sh`], true); | ||
| if (installed) { | ||
| process.env.PATH = `${process.env.PATH}:${flyHome}/bin`; | ||
| if (await ok('flyctl', ['version'])) { | ||
| info('flyctl installed ✓'); | ||
| return; | ||
| } | ||
| } | ||
| info('flyctl install failed — install manually: https://fly.io/docs/flyctl/install/'); | ||
| return; | ||
| } | ||
| info('flyctl (fly CLI) not found — install it to deploy from source: https://fly.io/docs/flyctl/install/'); | ||
@@ -77,0 +95,0 @@ } |
+2
-1
@@ -174,4 +174,5 @@ #!/usr/bin/env node | ||
| .option('--json').option('--branch <branch>', 'branch (default: current)').action(guard((mode, service, o) => computeCmd.computeAlwaysOn(mode, service, o))); | ||
| compute.command('volume [service]').description("Show, attach, or grow a compute service's persistent /data volume. No --size: print size, mount path, and the plan cap (any plan). --size on a volumeless service ATTACHES one (any plan at the default 1Gi; larger is paid and plan-capped; the disk mounts at /data on the next deploy); on a volume-bearing one it grows (paid plans; grow-only — a provisioned disk cannot shrink). Billing is actual data stored — the size is a cap, not a price") | ||
| compute.command('volume [service]').description("Show, attach, grow, or delete a compute service's persistent /data volume. No flag: print size, mount path, and the plan cap (any plan). --size on a volumeless service ATTACHES one (any plan at the default 1Gi; larger is paid and plan-capped; the disk mounts at /data on the next deploy); on a volume-bearing one it grows (paid plans; grow-only — a provisioned disk cannot shrink). --delete DESTROYS the disk and ALL its data immediately (no detach, no undo; billing stops now, and suspend fast-wake + scale-out return). Billing is actual data stored — the size is a cap, not a price") | ||
| .option('--size <gi>', 'new size in whole Gi, e.g. 10 (must be ≥ the current size)') | ||
| .option('--delete', 'destroy the volume and ALL its data (irreversible; download anything you need first)') | ||
| .option('--json').option('--branch <branch>', 'branch (default: current)').action(guard((service, o) => computeCmd.computeVolume(service, o))); | ||
@@ -178,0 +179,0 @@ // ---- db (postgres service controls) ---- |
+1
-1
| { | ||
| "name": "insta", | ||
| "version": "0.0.30", | ||
| "version": "0.0.31", | ||
| "type": "module", | ||
@@ -5,0 +5,0 @@ "description": "InstaCloud CLI — a thin client of the platform control-plane API.", |
Environment variable access
Supply chain riskPackage accesses environment variables, which may be a sign of credential stuffing or data theft.
Found 2 instances
URL strings
Supply chain riskPackage contains fragments of external URLs or IP addresses, which the package may be accessing at runtime.
URL strings
Supply chain riskPackage contains fragments of external URLs or IP addresses, which the package may be accessing at runtime.
208192
1.79%3542
1.72%40
8.11%