insta 0.0.68 → 0.0.70
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/README.md +10 -7
- package/dist/api.js +31 -11
- package/dist/commands/billing.js +11 -8
- package/dist/commands/branch.js +6 -1
- package/dist/commands/build.js +9 -8
- package/dist/commands/deploy.js +112 -12
- package/dist/commands/feedback.js +1 -1
- package/dist/commands/project.js +10 -1
- package/dist/commands/upgrade.js +1 -1
- package/dist/config.js +177 -9
- package/dist/deploy-archive.js +145 -0
- package/dist/env.js +2 -2
- package/dist/index.js +4 -4
- package/dist/pack-ignore.js +267 -0
- package/dist/pack.js +229 -0
- package/package.json +6 -4
package/README.md
CHANGED
|
@@ -16,7 +16,7 @@ Native binary, no Node required (macOS / Linux / WSL). Installs to `~/.insta/bin
|
|
|
16
16
|
verifies the download against `SHA256SUMS`:
|
|
17
17
|
|
|
18
18
|
```bash
|
|
19
|
-
curl -fsSL https://raw.githubusercontent.com/InsForge/
|
|
19
|
+
curl -fsSL https://raw.githubusercontent.com/InsForge/instacloud-cli/main/install.sh | sh
|
|
20
20
|
```
|
|
21
21
|
|
|
22
22
|
From npm:
|
|
@@ -48,7 +48,7 @@ On macOS/Linux without Node, the native-binary installer puts the `insta` CLI on
|
|
|
48
48
|
skill + MCP steps it then runs still need Node — the skills tool runs via npx). Never run it
|
|
49
49
|
on native Windows — PowerShell's `curl` alias and the WSL `bash` shim break it; use npx
|
|
50
50
|
above, or download `insta-windows-x64.exe` from the
|
|
51
|
-
[releases page](https://github.com/InsForge/
|
|
51
|
+
[releases page](https://github.com/InsForge/instacloud-cli/releases):
|
|
52
52
|
|
|
53
53
|
```bash
|
|
54
54
|
curl -fsSL agents.instacloud.com | sh
|
|
@@ -74,7 +74,9 @@ insta deploy .
|
|
|
74
74
|
opt-in, so you add only what you need. `secrets` writes the current branch's user-defined
|
|
75
75
|
secrets to `./.env` (the postgres connection string is read with `insta db url`). `deploy .`
|
|
76
76
|
builds the directory remotely and ships it to the branch's compute
|
|
77
|
-
service
|
|
77
|
+
service, with no local Docker. Whether it needs a `Dockerfile` depends on where the
|
|
78
|
+
service runs: on insta-compute it is optional, and a directory without one is built
|
|
79
|
+
by nixpacks on the build gateway; on Fly-backed services one is still required.
|
|
78
80
|
|
|
79
81
|
## Authentication
|
|
80
82
|
|
|
@@ -157,7 +159,7 @@ other, so switching environments drops the stored session and you log in again.
|
|
|
157
159
|
| control plane | `api.instacloud.com` | `api.staging.instacloud.com` |
|
|
158
160
|
| MCP server | `mcp.instacloud.com/mcp` | `mcp.staging.instacloud.com/mcp` |
|
|
159
161
|
| MCP registers as | `insta-cloud` | `insta-cloud-staging` |
|
|
160
|
-
| agent skills | `InsForge/
|
|
162
|
+
| agent skills | `InsForge/instacloud-skills` | `InsForge/instacloud-skills#devel` |
|
|
161
163
|
| CLI channel | latest stable release | newest prerelease, else stable |
|
|
162
164
|
|
|
163
165
|
```bash
|
|
@@ -180,7 +182,7 @@ That host is a CloudFront cache, so after a change to the installer it can serve
|
|
|
180
182
|
previous copy for up to about a day. This form is equivalent and always current:
|
|
181
183
|
|
|
182
184
|
```bash
|
|
183
|
-
curl -fsSL https://raw.githubusercontent.com/InsForge/
|
|
185
|
+
curl -fsSL https://raw.githubusercontent.com/InsForge/instacloud-cli/main/install.sh | sh -s -- --agents --staging
|
|
184
186
|
```
|
|
185
187
|
|
|
186
188
|
If the environment cannot be applied — an installed CLI older than 0.0.23 has no `insta
|
|
@@ -204,7 +206,7 @@ build never reaches a production installer.
|
|
|
204
206
|
## Commands
|
|
205
207
|
|
|
206
208
|
`insta --help` is the authoritative list. For flags, approval gates and plan limits, see the
|
|
207
|
-
[full command reference](https://github.com/InsForge/
|
|
209
|
+
[full command reference](https://github.com/InsForge/instacloud-skills/blob/main/insta/cli-reference.md).
|
|
208
210
|
|
|
209
211
|
| Command | What it covers |
|
|
210
212
|
|---|---|
|
|
@@ -238,6 +240,7 @@ build never reaches a production installer.
|
|
|
238
240
|
|---|---|
|
|
239
241
|
| `~/.insta/config.json` | API URL, access and refresh tokens, user, auto-update preference |
|
|
240
242
|
| `./.insta/project.json` | Project id, org id, current branch |
|
|
243
|
+
| `./.insta/link-plane.json` | The control-plane URL this machine linked against. Gitignored and per machine; a link made against a different control plane is refused rather than reused. The home directory is never a project |
|
|
241
244
|
|
|
242
245
|
| Variable | Effect |
|
|
243
246
|
|---|---|
|
|
@@ -252,7 +255,7 @@ build never reaches a production installer.
|
|
|
252
255
|
## Agent skills
|
|
253
256
|
|
|
254
257
|
The `insta` skill and its task guides live in
|
|
255
|
-
[InsForge/
|
|
258
|
+
[InsForge/instacloud-skills](https://github.com/InsForge/instacloud-skills). `insta setup agent`
|
|
256
259
|
installs it user-globally for every coding agent on the machine. `insta project create` and
|
|
257
260
|
`insta project link` additionally install the stack skills (Tigris, Better Auth) into the
|
|
258
261
|
project, along with the `insta observe` credential-audit hook. Postgres needs no stack
|
package/dist/api.js
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
// Thin API client over the platform control-plane. Handles bearer auth + one-shot refresh on 401.
|
|
2
2
|
// 2xx (including 202 approval_required) returns the parsed body; >=400 throws ApiError.
|
|
3
|
-
import { readGlobal, writeGlobal, readProject,
|
|
3
|
+
import { readGlobal, writeGlobal, readProject, persistAutoLink, resolveProjectLink, foreignLinkMessage } from './config.js';
|
|
4
4
|
import { autoResolveProject, promptChoice } from './resolve-project.js';
|
|
5
5
|
import { die } from './util.js';
|
|
6
6
|
import { USER_AGENT } from './version.js';
|
|
@@ -77,7 +77,7 @@ export class ApiClient {
|
|
|
77
77
|
async raw(method, path, body, auth, scope = {}) {
|
|
78
78
|
let r = await this.fetch(method, path, body, auth, scope);
|
|
79
79
|
if (r.status === 401 && auth && this.cfg.refreshToken) {
|
|
80
|
-
if (await this.refresh())
|
|
80
|
+
if (await this.refresh(scope.signal))
|
|
81
81
|
r = await this.fetch(method, path, body, auth, scope);
|
|
82
82
|
}
|
|
83
83
|
return r;
|
|
@@ -92,6 +92,7 @@ export class ApiClient {
|
|
|
92
92
|
method,
|
|
93
93
|
headers,
|
|
94
94
|
body: body === undefined ? undefined : JSON.stringify(body),
|
|
95
|
+
signal: scope.signal,
|
|
95
96
|
});
|
|
96
97
|
const text = await res.text();
|
|
97
98
|
let parsed = null;
|
|
@@ -103,9 +104,9 @@ export class ApiClient {
|
|
|
103
104
|
}
|
|
104
105
|
return { status: res.status, body: parsed };
|
|
105
106
|
}
|
|
106
|
-
async refresh() {
|
|
107
|
+
async refresh(signal) {
|
|
107
108
|
try {
|
|
108
|
-
const res = await this.fetch('POST', '/auth/refresh', { refreshToken: this.cfg.refreshToken }, false);
|
|
109
|
+
const res = await this.fetch('POST', '/auth/refresh', { refreshToken: this.cfg.refreshToken }, false, { signal });
|
|
109
110
|
if (res.status >= 400)
|
|
110
111
|
return false;
|
|
111
112
|
this.cfg.accessToken = res.body.accessToken;
|
|
@@ -113,7 +114,12 @@ export class ApiClient {
|
|
|
113
114
|
await this.persist();
|
|
114
115
|
return true;
|
|
115
116
|
}
|
|
116
|
-
catch {
|
|
117
|
+
catch (e) {
|
|
118
|
+
// Refresh belongs to the original request's time budget. Do not turn its cancellation
|
|
119
|
+
// into the earlier 401: the caller needs the abort to report a timeout or cancellation.
|
|
120
|
+
signal?.throwIfAborted();
|
|
121
|
+
if (e instanceof Error && (e.name === 'AbortError' || e.name === 'TimeoutError'))
|
|
122
|
+
throw e;
|
|
117
123
|
return false;
|
|
118
124
|
}
|
|
119
125
|
}
|
|
@@ -121,12 +127,20 @@ export class ApiClient {
|
|
|
121
127
|
// Resolve the linked project (./.insta/project.json), or null.
|
|
122
128
|
export async function linkedProject() { return readProject(); }
|
|
123
129
|
// Resolve the linked project or exit with guidance.
|
|
124
|
-
export async function requireProject() {
|
|
125
|
-
const
|
|
126
|
-
|
|
127
|
-
|
|
130
|
+
export async function requireProject(deps = {}) {
|
|
131
|
+
const r = await resolveProjectLink(deps.cwd);
|
|
132
|
+
// Fail CLOSED on a link made against another control plane. Treating it as "unlinked" sent this
|
|
133
|
+
// into auto-resolve, which with exactly one project on the new plane picks it without a prompt
|
|
134
|
+
// and SAVES — so a read-only command replaced the committed team binding, and pointing back
|
|
135
|
+
// flipped it again. Only an explicit `insta project link` may replace a link.
|
|
136
|
+
if (r?.foreign)
|
|
137
|
+
die(foreignLinkMessage(r.foreign));
|
|
138
|
+
if (r)
|
|
139
|
+
return r.link;
|
|
128
140
|
if (agentMode())
|
|
129
141
|
die('agent mode requires a linked project — run `insta setup agent --project <id>`');
|
|
142
|
+
if (deps.autoResolve)
|
|
143
|
+
return deps.autoResolve();
|
|
130
144
|
// One command, just works: unlinked ≠ error. Resolve the project (auto when there's one,
|
|
131
145
|
// one-keystroke picker when several) and persist the choice so this happens once per dir.
|
|
132
146
|
const api = await ApiClient.load();
|
|
@@ -137,10 +151,16 @@ export async function requireProject() {
|
|
|
137
151
|
listProjects: async () => (await api.request('GET', `/orgs/${orgId}/projects`)).projects,
|
|
138
152
|
promptChoice,
|
|
139
153
|
save: async (c) => {
|
|
140
|
-
await writeProject(c);
|
|
141
154
|
// stderr: this is a diagnostic that can precede ANY command's output — under --json,
|
|
142
155
|
// stdout must stay one parseable document.
|
|
143
|
-
|
|
156
|
+
if (await persistAutoLink(c, deps.cwd)) {
|
|
157
|
+
process.stderr.write(`auto-linked project ${c.projectId} → ./.insta/project.json\n`);
|
|
158
|
+
}
|
|
159
|
+
else {
|
|
160
|
+
// The home directory never holds a link (~/.insta is the global config): use the choice
|
|
161
|
+
// for this command instead of failing it after the picker has already run.
|
|
162
|
+
process.stderr.write(`using project ${c.projectId} for this command — not saving a link in the home directory; run inside a project directory to remember it\n`);
|
|
163
|
+
}
|
|
144
164
|
},
|
|
145
165
|
tty: !!process.stdin.isTTY && !!process.stderr.isTTY,
|
|
146
166
|
});
|
package/dist/commands/billing.js
CHANGED
|
@@ -14,17 +14,20 @@ export async function resolveOrgId(opts) {
|
|
|
14
14
|
export function billingLines(s, org) {
|
|
15
15
|
const t = s.totals;
|
|
16
16
|
const lines = [
|
|
17
|
-
`tier:
|
|
18
|
-
`status:
|
|
17
|
+
`tier: ${s.tier}`,
|
|
18
|
+
`status: ${s.billingStatus}`,
|
|
19
19
|
cycleLine(s.window),
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
`
|
|
24
|
-
`
|
|
20
|
+
// Two separate figures, never added together: the plan's allowance for this cycle, and the
|
|
21
|
+
// org's wallet. "included usage" is the name the console and the pricing page use for the
|
|
22
|
+
// former; "credits" now means the wallet alone.
|
|
23
|
+
`included usage: $${Number(t.includedUsd).toFixed(2)}`,
|
|
24
|
+
`used: $${Number(t.usedUsd).toFixed(4)}`,
|
|
25
|
+
`overage: $${Number(t.overageUsd).toFixed(4)}`,
|
|
26
|
+
`credits: $${Number(t.creditBalanceUsd).toFixed(2)}`,
|
|
27
|
+
`forecast: $${Number(t.forecastUsd).toFixed(4)} (predicted full cycle)`,
|
|
25
28
|
];
|
|
26
29
|
if (s.subscriptionStatus)
|
|
27
|
-
lines.push(`subscription:
|
|
30
|
+
lines.push(`subscription: ${s.subscriptionStatus}`);
|
|
28
31
|
if (s.billingStatus === 'suspended') {
|
|
29
32
|
// Four causes, five messages, and every one is a dead end for the others. Tier first: only a
|
|
30
33
|
// free org can spend a prepaid wallet, and waiting for the next cycle genuinely fixes that one.
|
package/dist/commands/branch.js
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { ApiClient, requireProject } from '../api.js';
|
|
2
|
-
import { writeProject } from '../config.js';
|
|
2
|
+
import { isHomeLinkTarget, writeProject } from '../config.js';
|
|
3
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();
|
|
@@ -20,6 +20,11 @@ export async function branchList(opts) {
|
|
|
20
20
|
info(`${b.is_default ? '*' : ' '} ${b.name} [${b.status}] ${b.id}`);
|
|
21
21
|
}
|
|
22
22
|
export async function branchSwitch(name, opts = {}) {
|
|
23
|
+
// A switch is remembered in ./.insta/project.json, which never lives in the home directory. Say so
|
|
24
|
+
// up front, instead of auto-resolving a project "for this command" and then refusing to save.
|
|
25
|
+
if (await isHomeLinkTarget()) {
|
|
26
|
+
die('can\'t switch branches in the home directory — the branch is remembered in ./.insta/project.json, and ~/.insta is the insta CLI\'s global config. Run this inside a project directory');
|
|
27
|
+
}
|
|
23
28
|
const api = await ApiClient.load();
|
|
24
29
|
const p = await requireProject();
|
|
25
30
|
const { branches } = await api.request('GET', `/projects/${p.projectId}/branches`);
|
package/dist/commands/build.js
CHANGED
|
@@ -155,21 +155,21 @@ export async function buildReport(dirArg, opts, deps) {
|
|
|
155
155
|
const envKeys = existsSync(envExample) ? envKeysFromDotEnvExample(readFileSync(envExample, 'utf8')) : [];
|
|
156
156
|
const checks = [];
|
|
157
157
|
// Only a Dockerfile IN the directory passes. A nixpacks-generated one is a real plan, but it is
|
|
158
|
-
// not
|
|
159
|
-
//
|
|
160
|
-
//
|
|
161
|
-
//
|
|
158
|
+
// not one this machine can hand to every target: on a Fly-backed service `insta deploy <dir>`
|
|
159
|
+
// builds the directory's own Dockerfile and dies without one. On insta-compute the gateway runs
|
|
160
|
+
// nixpacks itself, so the same directory ships. The check stays a non-pass because it cannot know
|
|
161
|
+
// the target, and the detail below says both halves rather than promising either.
|
|
162
162
|
checks.push(dockerfile.source === 'nixpacks'
|
|
163
163
|
? {
|
|
164
164
|
id: 'dockerfile',
|
|
165
165
|
severity: 'warning',
|
|
166
166
|
status: 'fail',
|
|
167
167
|
title: 'Dockerfile',
|
|
168
|
-
detail: `${dockerfileDetail} —
|
|
168
|
+
detail: `${dockerfileDetail} — \`insta deploy <dir>\` builds this on the gateway with nixpacks when the service runs on insta-compute, and needs your own Dockerfile on a Fly-backed one`,
|
|
169
169
|
// NOT "save the generated Dockerfile here": it is not standalone (it COPYs the
|
|
170
170
|
// .nixpacks/nixpkgs-<hash>.nix support files nixpacks writes beside it, which this
|
|
171
171
|
// directory does not have). The detected commands above are the reusable part.
|
|
172
|
-
nextAction: `
|
|
172
|
+
nextAction: `on insta-compute this deploys as-is (nixpacks builds it on the gateway). On a Fly-backed service, write a Dockerfile at ${userDockerfilePath} — the detected install/start commands above are the starting point — or connect the GitHub repo (\`insta compute connect-repo <owner/repo>\`)`,
|
|
173
173
|
}
|
|
174
174
|
: {
|
|
175
175
|
id: 'dockerfile',
|
|
@@ -225,8 +225,9 @@ export function renderReport(r, explain) {
|
|
|
225
225
|
const lines = [];
|
|
226
226
|
lines.push(`plan for ${r.dir}:`);
|
|
227
227
|
// The builder line is the first thing read (and the thing an agent scrapes), so it carries the
|
|
228
|
-
// lane caveat too — "builder: nixpacks" on its own reads as a promise
|
|
229
|
-
|
|
228
|
+
// lane caveat too — "builder: nixpacks" on its own reads as a promise that is only true on some
|
|
229
|
+
// targets. It has to say the SAME thing the detail below it says, or a scrape sees both claims.
|
|
230
|
+
const lane = r.plan.builder === 'nixpacks' ? ' — server-side: insta-compute builds this as-is, a Fly-backed service needs your own Dockerfile' : '';
|
|
230
231
|
lines.push(` builder: ${r.plan.builder ?? 'none'}${r.plan.providers.length ? ` (providers: ${r.plan.providers.join(', ')})` : ''}${lane}`);
|
|
231
232
|
if (r.plan.installCommand)
|
|
232
233
|
lines.push(` install: ${r.plan.installCommand}`);
|
package/dist/commands/deploy.js
CHANGED
|
@@ -3,14 +3,17 @@ import { existsSync, readFileSync } from 'node:fs';
|
|
|
3
3
|
import { ApiClient, ApiError, requireProject } from '../api.js';
|
|
4
4
|
import { info, die, printJson, handleApproval, renderNextActions, CliExit } from '../util.js';
|
|
5
5
|
import { flyctlBuildAndPush, ensureFlyctl, defaultBuildRunner, stderrBuildRunner } from '../flyctl-build.js';
|
|
6
|
+
import { packDirectory, windowsModeCaveat } from '../pack.js';
|
|
7
|
+
import { deployArchive, uploadArchive } from '../deploy-archive.js';
|
|
8
|
+
import { parsePort } from './services.js';
|
|
6
9
|
// With --json, stdout must carry exactly one JSON document (the deploy result), so every progress
|
|
7
10
|
// line moves to stderr.
|
|
8
11
|
const note = (opts) => (opts.json ? (m) => void process.stderr.write(m + '\n') : info);
|
|
9
12
|
// Map CLI options to the platform deploy request body. Pure, so it's unit-tested. --websocket is only
|
|
10
|
-
// sent when set (plain deploys unchanged).
|
|
11
|
-
export function deployRequestBody(
|
|
13
|
+
// sent when set (plain deploys unchanged). Exactly one of image | archive rides along.
|
|
14
|
+
export function deployRequestBody(source, branch, opts) {
|
|
12
15
|
return {
|
|
13
|
-
image,
|
|
16
|
+
image: source.image,
|
|
14
17
|
branch,
|
|
15
18
|
group: opts.group,
|
|
16
19
|
port: opts.port ? Number(opts.port) : undefined,
|
|
@@ -18,6 +21,82 @@ export function deployRequestBody(image, branch, opts) {
|
|
|
18
21
|
replaceSource: opts.replaceSource ? true : undefined,
|
|
19
22
|
};
|
|
20
23
|
}
|
|
24
|
+
// Ask the platform which lane serves this target, so the CLI stops knowing which provider backs
|
|
25
|
+
// its service. A 404 means the platform predates the contract — and because this is a GET, it
|
|
26
|
+
// 404s the same way for a human and an agent, which a POST would not.
|
|
27
|
+
async function discoverLane(api, projectId, branch, opts) {
|
|
28
|
+
const q = new URLSearchParams({ branch, ...(opts.group ? { group: opts.group } : {}) });
|
|
29
|
+
try {
|
|
30
|
+
const res = await api.rawRequest('GET', `/projects/${projectId}/source-build?${q}`);
|
|
31
|
+
// Validated, not cast. Every value other than the four we know silently fell through to the
|
|
32
|
+
// flyctl path below, so a server that grew a fifth lane would send this CLI down the wrong
|
|
33
|
+
// one and fail somewhere unrelated. An unknown answer is the server being ahead of us, and
|
|
34
|
+
// saying that is more useful than guessing.
|
|
35
|
+
const lane = (res.body ?? {});
|
|
36
|
+
const tag = lane.lane ?? '';
|
|
37
|
+
const known = ['flyctl', 'local-docker', 'archive', 'none'];
|
|
38
|
+
if (!known.includes(tag)) {
|
|
39
|
+
die(`this platform answered with a source-build lane this CLI does not know (${JSON.stringify(tag)}) — upgrade with \`insta upgrade\``);
|
|
40
|
+
}
|
|
41
|
+
// The tag alone is not the contract: each branch carries a payload this code then trusts.
|
|
42
|
+
// An `archive` with malformed limits fell back to local defaults, so the CLI would enforce
|
|
43
|
+
// caps the SERVER does not have, and a `none` with no reason died with `undefined`.
|
|
44
|
+
if (tag === 'archive') {
|
|
45
|
+
const l = lane.limits;
|
|
46
|
+
const positive = (v) => typeof v === 'number' && Number.isSafeInteger(v) && v > 0;
|
|
47
|
+
if (!l || !positive(l.maxArchiveBytes) || !positive(l.maxExtractedBytes) || !positive(l.maxFiles)) {
|
|
48
|
+
die('this platform offered the archive lane without usable size limits — upgrade with `insta upgrade`');
|
|
49
|
+
}
|
|
50
|
+
}
|
|
51
|
+
const reason = lane.reason;
|
|
52
|
+
if (tag === 'none' && (typeof reason !== 'string' || !reason.trim())) {
|
|
53
|
+
die('this platform refused a source build without saying why — upgrade with `insta upgrade`');
|
|
54
|
+
}
|
|
55
|
+
return lane;
|
|
56
|
+
}
|
|
57
|
+
catch (e) {
|
|
58
|
+
if (e instanceof ApiError && e.status === 404) {
|
|
59
|
+
// The route answered and the TARGET is what is missing: say so, or flyctl turns it into "no Dockerfile".
|
|
60
|
+
// Both ways out: the project has several groups and none is named, or it has none at all.
|
|
61
|
+
if (/compute group not found|branch not found/.test(e.message))
|
|
62
|
+
die(`${e.message} — name it with \`--group <name>\` (see \`insta services list\`), or add one: \`insta services add compute <name>\``);
|
|
63
|
+
return { lane: 'legacy' };
|
|
64
|
+
}
|
|
65
|
+
throw e;
|
|
66
|
+
}
|
|
67
|
+
}
|
|
68
|
+
// Turn a source directory into something deployable. Returns null when an approval is pending.
|
|
69
|
+
export async function prepareSource(api, projectId, dir, branch, opts, run = opts.json ? stderrBuildRunner : defaultBuildRunner, upload) {
|
|
70
|
+
const lane = await discoverLane(api, projectId, branch, opts);
|
|
71
|
+
if (lane.lane === 'none')
|
|
72
|
+
die(lane.reason);
|
|
73
|
+
if (lane.lane !== 'archive') {
|
|
74
|
+
// flyctl, local-docker and legacy all end in an image, and all three need a Dockerfile.
|
|
75
|
+
return { image: await buildFromSource(api, projectId, dir, branch, opts, run) };
|
|
76
|
+
}
|
|
77
|
+
const log = note(opts);
|
|
78
|
+
const absDir = resolve(process.cwd(), dir);
|
|
79
|
+
const caveat = windowsModeCaveat();
|
|
80
|
+
if (caveat)
|
|
81
|
+
log(caveat);
|
|
82
|
+
const packed = packDirectory(absDir, lane.limits);
|
|
83
|
+
log(`packed ${dir}: ${packed.files} files, ${packed.archive.length} bytes`);
|
|
84
|
+
const ref = await uploadArchive(api, projectId, packed, branch, opts, upload);
|
|
85
|
+
if (!ref)
|
|
86
|
+
return null;
|
|
87
|
+
log(`deploying ${packed.archive.length} bytes via the gateway (${ref.build.type})`);
|
|
88
|
+
// One gated call enqueues build+deploy as an operation; the wait is ours, one short poll at a
|
|
89
|
+
// time, because a platform request has to answer inside the ALB's 60s while a build runs minutes.
|
|
90
|
+
// A repo-connected service refuses this with a 409 the same way it refuses an image deploy, and
|
|
91
|
+
// the hint that names the FLAG rather than the API field lives here, beside the `/deploy` path.
|
|
92
|
+
const out = await deployArchive(api, projectId, ref, branch, opts, Date.now, undefined, log)
|
|
93
|
+
.catch((e) => { throw e instanceof ApiError && e.status === 409 ? new ApiError(e.status, repoConnectedHint(e.message), e.body) : e; });
|
|
94
|
+
if (!out)
|
|
95
|
+
return null;
|
|
96
|
+
if ('failed' in out)
|
|
97
|
+
die(out.failed);
|
|
98
|
+
return { deployed: out };
|
|
99
|
+
}
|
|
21
100
|
// A port mismatch is the #1 deploy mistake: the app boots "successfully" but the proxy routes to
|
|
22
101
|
// the wrong internal port and every request is refused. For source deploys the Dockerfile states
|
|
23
102
|
// the truth — use its (last) EXPOSE as the default instead of a blind 8080.
|
|
@@ -30,10 +109,10 @@ export function dockerfileExposedPort(dockerfile) {
|
|
|
30
109
|
}
|
|
31
110
|
return port;
|
|
32
111
|
}
|
|
33
|
-
//
|
|
34
|
-
//
|
|
35
|
-
//
|
|
36
|
-
//
|
|
112
|
+
// This message is for the target that still REQUIRES a Dockerfile: a Fly-backed service, where a
|
|
113
|
+
// directory deploy builds the Dockerfile in the directory and dies without one. On insta-compute
|
|
114
|
+
// the archive lane carries the directory to the gateway and nixpacks builds it, so this dead end is
|
|
115
|
+
// no longer universal. It names every way forward instead of the bare "add one".
|
|
37
116
|
//
|
|
38
117
|
// It deliberately does NOT say "save the Dockerfile `insta build --explain` prints": that file is
|
|
39
118
|
// not standalone — it COPYs `.nixpacks/nixpkgs-<hash>.nix` support files nixpacks writes beside it,
|
|
@@ -60,7 +139,16 @@ export async function deploy(dir, opts) {
|
|
|
60
139
|
const p = await requireProject();
|
|
61
140
|
const branch = opts.branch ?? p.branch;
|
|
62
141
|
const log = note(opts);
|
|
63
|
-
|
|
142
|
+
// Junk fails here, before a directory is packed and uploaded for a body the platform would only
|
|
143
|
+
// refuse: the same parser every other --port in this CLI runs. `Number()` alone sent NaN as
|
|
144
|
+
// `null` and let 0 or 70000 travel to the server.
|
|
145
|
+
let port;
|
|
146
|
+
try {
|
|
147
|
+
port = opts.port === undefined ? undefined : parsePort(opts.port);
|
|
148
|
+
}
|
|
149
|
+
catch (e) {
|
|
150
|
+
die(`--${e.message}`);
|
|
151
|
+
}
|
|
64
152
|
if (dir && port === undefined) {
|
|
65
153
|
const dockerfile = join(resolve(process.cwd(), dir), 'Dockerfile');
|
|
66
154
|
const exposed = existsSync(dockerfile) ? dockerfileExposedPort(readFileSync(dockerfile, 'utf8')) : undefined;
|
|
@@ -70,14 +158,26 @@ export async function deploy(dir, opts) {
|
|
|
70
158
|
}
|
|
71
159
|
}
|
|
72
160
|
const effOpts = { ...opts, port: port?.toString() };
|
|
73
|
-
const
|
|
74
|
-
|
|
161
|
+
const source = dir ? await prepareSource(api, p.projectId, dir, branch, effOpts) : { image: opts.image };
|
|
162
|
+
if (!source)
|
|
163
|
+
return; // an approval is pending; the user approves and re-runs
|
|
164
|
+
if ('deployed' in source) {
|
|
165
|
+
// The archive lane's operation already deployed. One output shape whichever lane ran, minus
|
|
166
|
+
// nextActions, which the operation read does not carry.
|
|
167
|
+
const d = source.deployed;
|
|
168
|
+
if (opts.json)
|
|
169
|
+
return printJson({ image: d.image, url: d.url, branch: d.branch, group: d.group, machineId: d.machineId });
|
|
170
|
+
info(`deployed ${d.image} -> ${d.url} (branch ${d.branch}, group ${d.group})`);
|
|
171
|
+
return;
|
|
172
|
+
}
|
|
173
|
+
const res = await api.rawRequest('POST', `/projects/${p.projectId}/deploy`, deployRequestBody(source, branch, effOpts))
|
|
75
174
|
.catch((e) => { throw e instanceof ApiError && e.status === 409 ? new ApiError(e.status, repoConnectedHint(e.message), e.body) : e; });
|
|
76
175
|
if (handleApproval(res, opts.json))
|
|
77
176
|
return;
|
|
177
|
+
const what = source.image;
|
|
78
178
|
if (opts.json)
|
|
79
|
-
return printJson({ image, ...res.body });
|
|
80
|
-
info(`deployed ${
|
|
179
|
+
return printJson({ image: source.image, ...res.body });
|
|
180
|
+
info(`deployed ${what} -> ${res.body.url} (branch ${res.body.branch}, group ${res.body.group})`);
|
|
81
181
|
renderNextActions(res.body.nextActions);
|
|
82
182
|
}
|
|
83
183
|
// The platform refuses an image deploy onto a repo-connected service and names the body field it
|
|
@@ -4,7 +4,7 @@
|
|
|
4
4
|
// server, the platform, the skills, the docs. Never for problems in the app the user is building.
|
|
5
5
|
//
|
|
6
6
|
// The backend is InstaCloud dogfooding itself: the "InstaCloud Agent Feedback" project runs the
|
|
7
|
-
// ingest service (InsForge/
|
|
7
|
+
// ingest service (InsForge/instacloud-feedback repo) on a postgres + compute pair. It is NOT the
|
|
8
8
|
// control-plane API on purpose — feedback must work logged-out, unlinked, and from insta-oss,
|
|
9
9
|
// and a control-plane outage is exactly when we most want reports to still arrive.
|
|
10
10
|
import { readFileSync, statSync } from 'node:fs';
|
package/dist/commands/project.js
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { homedir } from 'node:os';
|
|
2
2
|
import { agentMode, setupProjectAgentSession } from '../agent.js';
|
|
3
3
|
import { ApiClient, requireProject } from '../api.js';
|
|
4
|
-
import { writeProject } from '../config.js';
|
|
4
|
+
import { HOME_LINK_REFUSAL, isHomeLinkTarget, writeProject } from '../config.js';
|
|
5
5
|
import { info, die, printJson, handleApproval, renderNextActions } from '../util.js';
|
|
6
6
|
import { installObserve } from '../observe/install.js';
|
|
7
7
|
import { installRoot } from './observe.js';
|
|
@@ -73,6 +73,10 @@ export async function projectCreate(name, opts) {
|
|
|
73
73
|
info(' (or just ask your coding agent — it has the insta skill and will do this for you)');
|
|
74
74
|
return;
|
|
75
75
|
}
|
|
76
|
+
// Refuse the home directory BEFORE provisioning: refusing only inside writeProject created the
|
|
77
|
+
// project on the control plane and then failed, with no id printed under --json.
|
|
78
|
+
if (await isHomeLinkTarget())
|
|
79
|
+
die(HOME_LINK_REFUSAL);
|
|
76
80
|
const api = await ApiClient.load();
|
|
77
81
|
const orgId = await resolveOrg(api, opts.org);
|
|
78
82
|
const out = await api.request('POST', `/orgs/${orgId}/projects`, { name: resolved });
|
|
@@ -103,6 +107,11 @@ export async function projectList(opts) {
|
|
|
103
107
|
info(`${p.id} ${p.name} [${p.status}]`);
|
|
104
108
|
}
|
|
105
109
|
export async function projectLink(id, opts = {}) {
|
|
110
|
+
// Refuse the home directory BEFORE anything with side effects. In agent mode the session below is
|
|
111
|
+
// saved, and .gitignore edited, at the link root; refusing only inside writeProject left both
|
|
112
|
+
// behind in ~ after the command had already failed.
|
|
113
|
+
if (await isHomeLinkTarget())
|
|
114
|
+
die(HOME_LINK_REFUSAL);
|
|
106
115
|
const api = await ApiClient.load();
|
|
107
116
|
if (agentMode())
|
|
108
117
|
await setupProjectAgentSession(api, id);
|
package/dist/commands/upgrade.js
CHANGED
|
@@ -24,7 +24,7 @@ import { readFileSync, writeFileSync, mkdirSync } from 'node:fs';
|
|
|
24
24
|
import { readGlobal, writeGlobal } from '../config.js';
|
|
25
25
|
import { resolveSpawnable } from '../spawn.js';
|
|
26
26
|
import { info } from '../util.js';
|
|
27
|
-
const INSTALL_SH = 'https://raw.githubusercontent.com/InsForge/
|
|
27
|
+
const INSTALL_SH = 'https://raw.githubusercontent.com/InsForge/instacloud-cli/main/install.sh';
|
|
28
28
|
// The dist-tag document is the authoritative, tiny (~50 byte) answer for `latest`. The full
|
|
29
29
|
// `latest` manifest is the fallback if that route is ever unavailable.
|
|
30
30
|
const REGISTRY_DIST_TAGS = 'https://registry.npmjs.org/-/package/insta/dist-tags';
|