insta 0.0.69 → 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 +1 -0
- package/dist/api.js +21 -7
- package/dist/commands/billing.js +11 -8
- package/dist/commands/branch.js +6 -1
- package/dist/commands/project.js +10 -1
- package/dist/config.js +177 -9
- package/dist/index.js +2 -2
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -240,6 +240,7 @@ build never reaches a production installer.
|
|
|
240
240
|
|---|---|
|
|
241
241
|
| `~/.insta/config.json` | API URL, access and refresh tokens, user, auto-update preference |
|
|
242
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 |
|
|
243
244
|
|
|
244
245
|
| Variable | Effect |
|
|
245
246
|
|---|---|
|
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';
|
|
@@ -127,12 +127,20 @@ export class ApiClient {
|
|
|
127
127
|
// Resolve the linked project (./.insta/project.json), or null.
|
|
128
128
|
export async function linkedProject() { return readProject(); }
|
|
129
129
|
// Resolve the linked project or exit with guidance.
|
|
130
|
-
export async function requireProject() {
|
|
131
|
-
const
|
|
132
|
-
|
|
133
|
-
|
|
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;
|
|
134
140
|
if (agentMode())
|
|
135
141
|
die('agent mode requires a linked project — run `insta setup agent --project <id>`');
|
|
142
|
+
if (deps.autoResolve)
|
|
143
|
+
return deps.autoResolve();
|
|
136
144
|
// One command, just works: unlinked ≠ error. Resolve the project (auto when there's one,
|
|
137
145
|
// one-keystroke picker when several) and persist the choice so this happens once per dir.
|
|
138
146
|
const api = await ApiClient.load();
|
|
@@ -143,10 +151,16 @@ export async function requireProject() {
|
|
|
143
151
|
listProjects: async () => (await api.request('GET', `/orgs/${orgId}/projects`)).projects,
|
|
144
152
|
promptChoice,
|
|
145
153
|
save: async (c) => {
|
|
146
|
-
await writeProject(c);
|
|
147
154
|
// stderr: this is a diagnostic that can precede ANY command's output — under --json,
|
|
148
155
|
// stdout must stay one parseable document.
|
|
149
|
-
|
|
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
|
+
}
|
|
150
164
|
},
|
|
151
165
|
tty: !!process.stdin.isTTY && !!process.stderr.isTTY,
|
|
152
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/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/config.js
CHANGED
|
@@ -1,13 +1,21 @@
|
|
|
1
1
|
// CLI config: global (~/.insta/config.json: api url + tokens) and per-project (./.insta/project.json).
|
|
2
2
|
import { homedir } from 'node:os';
|
|
3
3
|
import { dirname, join, resolve } from 'node:path';
|
|
4
|
-
import {
|
|
4
|
+
import { realpathSync } from 'node:fs';
|
|
5
|
+
import { chmod, mkdir, readFile, writeFile } from 'node:fs/promises';
|
|
5
6
|
import { ensureGitignore } from './gitignore.js';
|
|
7
|
+
import { die } from './util.js';
|
|
6
8
|
import { DEFAULT_ENV, ENVS, envForApiUrl, envFromEnvVar, normalizeUrl } from './env.js';
|
|
7
9
|
const GLOBAL_DIR = join(homedir(), '.insta');
|
|
8
10
|
const GLOBAL_FILE = join(GLOBAL_DIR, 'config.json');
|
|
9
11
|
const PROJECT_DIR = '.insta';
|
|
10
12
|
const PROJECT_FILE = 'project.json';
|
|
13
|
+
// Machine-local, gitignored: which project this machine linked here, and on which control plane.
|
|
14
|
+
// It is NOT in project.json because that file is the team's committed binding — a URL chosen by one
|
|
15
|
+
// machine (a staging switch, a local box, a transient INSTA_API_URL) would make every teammate's
|
|
16
|
+
// CLI treat the shared link as foreign. It records the project id too, because project.json is
|
|
17
|
+
// committed and this file is not: a pull or checkout can replace the project underneath it.
|
|
18
|
+
const LINK_PLANE_FILE = 'link-plane.json';
|
|
11
19
|
// The cloud API default. Uses the instacloud.com brand domain (matches the agents.instacloud.com
|
|
12
20
|
// onboarding), NOT the legacy beta-api.insta.insforge.dev host — same backend, branded domain.
|
|
13
21
|
// Only affects fresh installs: a persisted apiUrl (from a prior login) or INSTA_API_URL wins below.
|
|
@@ -85,11 +93,46 @@ export async function writeGlobal(c) {
|
|
|
85
93
|
await mkdir(GLOBAL_DIR, { recursive: true });
|
|
86
94
|
await writeFile(GLOBAL_FILE, JSON.stringify(c, null, 2));
|
|
87
95
|
}
|
|
96
|
+
/** The home directory is never a project root. `~/.insta/` is this CLI's GLOBAL config directory,
|
|
97
|
+
* so a project.json there is not a project link: honouring one made every directory under the home
|
|
98
|
+
* dir inherit it, and `insta project link` run anywhere below home silently overwrote it. */
|
|
99
|
+
function isHomeDir(dir) {
|
|
100
|
+
return canonicalDir(dir) === canonicalDir(homedir());
|
|
101
|
+
}
|
|
102
|
+
/** A directory's filesystem identity rather than its spelling: the native real path, which resolves
|
|
103
|
+
* symlinks and returns the on-disk case on case-insensitive filesystems (macOS, Windows). Comparing
|
|
104
|
+
* spellings let a symlinked or differently-cased path to home through the home check. A path that
|
|
105
|
+
* doesn't exist has no identity to resolve, so it falls back to its resolved spelling. */
|
|
106
|
+
function canonicalDir(dir) {
|
|
107
|
+
try {
|
|
108
|
+
return realpathSync.native(resolve(dir));
|
|
109
|
+
}
|
|
110
|
+
catch {
|
|
111
|
+
return resolve(dir);
|
|
112
|
+
}
|
|
113
|
+
}
|
|
114
|
+
export const HOME_LINK_REFUSAL = 'refusing to link the home directory — ~/.insta is the insta CLI\'s global config, not a project. Run this inside a project directory';
|
|
115
|
+
/** Where a link written from `cwd` lands: the existing project root, or `cwd` itself. */
|
|
116
|
+
async function linkTarget(cwd) {
|
|
117
|
+
return (await findProjectRoot(cwd)) ?? resolve(cwd);
|
|
118
|
+
}
|
|
119
|
+
/** True when a link written from `cwd` would land in the home directory, which never holds one.
|
|
120
|
+
* Lets a caller refuse BEFORE it does anything else with side effects. */
|
|
121
|
+
export async function isHomeLinkTarget(cwd = process.cwd()) {
|
|
122
|
+
return isHomeDir(await linkTarget(cwd));
|
|
123
|
+
}
|
|
88
124
|
/** Git-style ancestor lookup: the nearest directory at-or-above `cwd` containing
|
|
89
|
-
* .insta/project.json — so "link once" works from any subdirectory of the project.
|
|
125
|
+
* .insta/project.json — so "link once" works from any subdirectory of the project. The search
|
|
126
|
+
* stops at the home directory (see isHomeDir): neither home nor anything above it is a project
|
|
127
|
+
* root for a directory inside home. */
|
|
90
128
|
export async function findProjectRoot(cwd = process.cwd()) {
|
|
91
129
|
let dir = resolve(cwd);
|
|
92
130
|
for (;;) {
|
|
131
|
+
// Stop AT the home directory rather than skipping it: a link above home is not a project for
|
|
132
|
+
// home or anything below it, and climbing past home let `insta project link` run in ~ resolve
|
|
133
|
+
// to, and overwrite, an ancestor's link after the home-directory check had passed.
|
|
134
|
+
if (isHomeDir(dir))
|
|
135
|
+
return null;
|
|
93
136
|
try {
|
|
94
137
|
await readFile(join(dir, PROJECT_DIR, PROJECT_FILE), 'utf8');
|
|
95
138
|
return dir;
|
|
@@ -101,32 +144,157 @@ export async function findProjectRoot(cwd = process.cwd()) {
|
|
|
101
144
|
dir = parent;
|
|
102
145
|
}
|
|
103
146
|
}
|
|
104
|
-
|
|
147
|
+
/** The link that applies to `cwd`, and whether it was made against a DIFFERENT control plane. A
|
|
148
|
+
* project id means nothing on another control plane (cloud, staging and every insta-oss box each
|
|
149
|
+
* have their own), and the CLI used to reuse a link against whatever API it was pointed at. */
|
|
150
|
+
export async function resolveProjectLink(cwd = process.cwd()) {
|
|
105
151
|
// Linkless targeting (CI / one-offs / agents): INSTA_PROJECT_ID resolves the project with no
|
|
106
152
|
// link file, and beats one when both exist — an explicit parameter outranks ambient state.
|
|
107
153
|
if (process.env.INSTA_PROJECT_ID) {
|
|
108
154
|
return {
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
155
|
+
link: {
|
|
156
|
+
projectId: process.env.INSTA_PROJECT_ID,
|
|
157
|
+
orgId: process.env.INSTA_ORG_ID ?? '',
|
|
158
|
+
branch: process.env.INSTA_BRANCH ?? 'main',
|
|
159
|
+
},
|
|
112
160
|
};
|
|
113
161
|
}
|
|
114
162
|
const root = await findProjectRoot(cwd);
|
|
115
163
|
if (!root)
|
|
116
164
|
return null;
|
|
165
|
+
let link;
|
|
166
|
+
try {
|
|
167
|
+
link = JSON.parse(await readFile(join(root, PROJECT_DIR, PROJECT_FILE), 'utf8'));
|
|
168
|
+
}
|
|
169
|
+
catch {
|
|
170
|
+
return null;
|
|
171
|
+
}
|
|
172
|
+
// No sidecar — a link from before this existed, or a teammate who just cloned — resolves as it
|
|
173
|
+
// always did: there is nothing to say which control plane it belongs to.
|
|
174
|
+
const record = await readLinkPlane(root);
|
|
175
|
+
if (record) {
|
|
176
|
+
const current = safeUrl((await readGlobal()).apiUrl);
|
|
177
|
+
const base = {
|
|
178
|
+
file: join(root, PROJECT_DIR, PROJECT_FILE), projectId: String(link.projectId),
|
|
179
|
+
linkedProjectId: record.projectId, linkedApiUrl: record.apiUrl, currentApiUrl: current,
|
|
180
|
+
};
|
|
181
|
+
// The record vouches for the project it was written for, and only that one. A record for
|
|
182
|
+
// another project says nothing about this one: trusting it would send this project to the
|
|
183
|
+
// control plane of the project it replaced, or refuse it for a reason that is not true. Fail
|
|
184
|
+
// closed with the real reason instead.
|
|
185
|
+
if (record.projectId !== base.projectId)
|
|
186
|
+
return { link, foreign: { reason: 'changed', ...base } };
|
|
187
|
+
if (normalizeUrl(record.apiUrl) !== normalizeUrl(current))
|
|
188
|
+
return { link, foreign: { reason: 'plane', ...base } };
|
|
189
|
+
}
|
|
190
|
+
return { link };
|
|
191
|
+
}
|
|
192
|
+
/** The link for this control plane, or null. A foreign link is NOT returned (it names a project
|
|
193
|
+
* that does not exist here); callers that must act on a project use requireProject, which stops
|
|
194
|
+
* with guidance instead of treating a foreign link as "unlinked". */
|
|
195
|
+
export async function readProject(cwd = process.cwd()) {
|
|
196
|
+
const r = await resolveProjectLink(cwd);
|
|
197
|
+
if (!r)
|
|
198
|
+
return null;
|
|
199
|
+
if (r.foreign) {
|
|
200
|
+
if (!warnedForeignLinks.has(r.foreign.file)) {
|
|
201
|
+
warnedForeignLinks.add(r.foreign.file);
|
|
202
|
+
process.stderr.write(`note: ${foreignLinkMessage(r.foreign)}\n`);
|
|
203
|
+
}
|
|
204
|
+
return null;
|
|
205
|
+
}
|
|
206
|
+
return r.link;
|
|
207
|
+
}
|
|
208
|
+
const warnedForeignLinks = new Set();
|
|
209
|
+
export function foreignLinkMessage(f) {
|
|
210
|
+
// Every field can come from a file (a committed project.json, a copied or hand-edited record),
|
|
211
|
+
// so all of it is stripped of control characters before it reaches a terminal.
|
|
212
|
+
const file = safeText(f.file);
|
|
213
|
+
const id = safeText(f.projectId);
|
|
214
|
+
const linked = safeUrl(f.linkedApiUrl);
|
|
215
|
+
const current = safeUrl(f.currentApiUrl);
|
|
216
|
+
if (f.reason === 'changed') {
|
|
217
|
+
return `${file} now links project ${id}, but this machine linked project ${safeText(f.linkedProjectId)} there, on ${linked}. `
|
|
218
|
+
+ `The link changed since (a pull or checkout), so its control plane is unknown. `
|
|
219
|
+
+ `Confirm it for ${current} with \`insta project link ${id}\`.`;
|
|
220
|
+
}
|
|
221
|
+
return `${file} links project ${id} on ${linked}, but the CLI is pointed at ${current}. `
|
|
222
|
+
+ `Link this directory for ${current} with \`insta project link <id>\`, or point the CLI back at ${linked}.`;
|
|
223
|
+
}
|
|
224
|
+
async function readLinkPlane(root) {
|
|
117
225
|
try {
|
|
118
|
-
|
|
226
|
+
const raw = JSON.parse(await readFile(join(root, PROJECT_DIR, LINK_PLANE_FILE), 'utf8'));
|
|
227
|
+
// Validated, not cast: a malformed record is ignored rather than crashing every command. A
|
|
228
|
+
// record with no project id cannot say which project it vouches for, so it is ignored too.
|
|
229
|
+
if (!raw || typeof raw.projectId !== 'string' || !raw.projectId || typeof raw.apiUrl !== 'string' || !raw.apiUrl)
|
|
230
|
+
return null;
|
|
231
|
+
// Untrusted input (it may have been copied or edited), so sanitized before it is compared or printed.
|
|
232
|
+
return { projectId: raw.projectId, apiUrl: safeUrl(raw.apiUrl) };
|
|
119
233
|
}
|
|
120
234
|
catch {
|
|
121
235
|
return null;
|
|
122
236
|
}
|
|
123
237
|
}
|
|
238
|
+
/** Text safe to echo to a terminal: C0 and C1 control characters and DEL removed — ESC and the
|
|
239
|
+
* single-byte C1 introducers (U+009B CSI among them) alike, so no escape sequence survives. */
|
|
240
|
+
function safeText(text) {
|
|
241
|
+
return String(text).replace(/[\u0000-\u001f\u007f-\u009f]/g, '');
|
|
242
|
+
}
|
|
243
|
+
/** A control-plane URL safe to persist and to print: control characters removed, surrounding
|
|
244
|
+
* whitespace trimmed, and any userinfo (INSTA_API_URL may carry credentials) removed.
|
|
245
|
+
*
|
|
246
|
+
* Userinfo is removed with the same WHATWG parser fetch uses, not with a pattern. That parser is
|
|
247
|
+
* lenient in ways a pattern keeps missing: it accepts leading whitespace, `\` for `/`, and a
|
|
248
|
+
* missing `//` on special schemes, and each of those defeated an anchored pattern while fetch
|
|
249
|
+
* would still have sent the credentials. A value the parser rejects is never requested, but it can
|
|
250
|
+
* still be stored or printed, so everything up to its last `@` is dropped. The same goes for a
|
|
251
|
+
* value that parses under any other scheme: `user:token@host` parses as scheme `user:` with the
|
|
252
|
+
* credentials in its path, where clearing username and password removes nothing. */
|
|
253
|
+
export function safeUrl(url) {
|
|
254
|
+
const text = safeText(url).trim();
|
|
255
|
+
const pastLastAt = () => (text.includes('@') ? text.slice(text.lastIndexOf('@') + 1) : text);
|
|
256
|
+
let parsed;
|
|
257
|
+
try {
|
|
258
|
+
parsed = new URL(text);
|
|
259
|
+
}
|
|
260
|
+
catch {
|
|
261
|
+
return pastLastAt();
|
|
262
|
+
}
|
|
263
|
+
if (parsed.protocol !== 'http:' && parsed.protocol !== 'https:')
|
|
264
|
+
return pastLastAt();
|
|
265
|
+
if (!parsed.username && !parsed.password)
|
|
266
|
+
return text;
|
|
267
|
+
parsed.username = '';
|
|
268
|
+
parsed.password = '';
|
|
269
|
+
return parsed.href;
|
|
270
|
+
}
|
|
124
271
|
/** Writes to the existing project root when inside a linked project (branch switches from a
|
|
125
|
-
* subdirectory must not mint a nested link); a fresh `link` in an unlinked tree writes to cwd.
|
|
272
|
+
* subdirectory must not mint a nested link); a fresh `link` in an unlinked tree writes to cwd.
|
|
273
|
+
* Records the control plane in the machine-local sidecar beside it. Never writes into the home
|
|
274
|
+
* directory: `~/.insta/` is the global config, not a project. */
|
|
126
275
|
export async function writeProject(c, cwd = process.cwd()) {
|
|
127
|
-
const target =
|
|
276
|
+
const target = await linkTarget(cwd);
|
|
277
|
+
if (isHomeDir(target))
|
|
278
|
+
die(HOME_LINK_REFUSAL);
|
|
279
|
+
const { apiUrl } = await readGlobal();
|
|
128
280
|
await mkdir(join(target, PROJECT_DIR), { recursive: true });
|
|
129
281
|
ensureGitignore(target, ['.insta/agent-session.json'], '# Local agent credentials');
|
|
282
|
+
ensureGitignore(target, [`.insta/${LINK_PLANE_FILE}`], '# Local: the control plane this machine linked against');
|
|
130
283
|
await writeFile(join(target, PROJECT_DIR, PROJECT_FILE), JSON.stringify(c, null, 2));
|
|
284
|
+
// Owner-only, like agent-session.json: it can name a private self-hosted box. writeFile's mode
|
|
285
|
+
// applies only when it creates the file, so an existing record is chmod-ed too.
|
|
286
|
+
const record = join(target, PROJECT_DIR, LINK_PLANE_FILE);
|
|
287
|
+
await writeFile(record, JSON.stringify({ projectId: c.projectId, apiUrl: safeUrl(apiUrl) }, null, 2), { mode: 0o600 });
|
|
288
|
+
await chmod(record, 0o600);
|
|
289
|
+
}
|
|
290
|
+
/** Save a link that auto-resolution chose. Unlike an explicit `insta project link`, the command it
|
|
291
|
+
* was resolved for must still run: in the home directory the choice is used for this command and
|
|
292
|
+
* simply not remembered, rather than showing the picker and then failing the command. Returns
|
|
293
|
+
* whether the link was saved. */
|
|
294
|
+
export async function persistAutoLink(c, cwd = process.cwd()) {
|
|
295
|
+
if (await isHomeLinkTarget(cwd))
|
|
296
|
+
return false;
|
|
297
|
+
await writeProject(c, cwd);
|
|
298
|
+
return true;
|
|
131
299
|
}
|
|
132
300
|
//# sourceMappingURL=config.js.map
|
package/dist/index.js
CHANGED
|
@@ -149,7 +149,7 @@ svc.command('add [type] [name]').description('Provision a service on demand (ass
|
|
|
149
149
|
.option('--port <n>', 'compute only: port the image listens on (default 8080)')
|
|
150
150
|
.option('--always-on', 'compute only: create as always-on — never scales to zero (the default for new compute services; all plans; billing is actual usage either way)')
|
|
151
151
|
.option('--no-always-on', 'compute only: create as scale-to-zero — idle machines suspend and wake on the next request')
|
|
152
|
-
.option('--volume <gi>', 'compute only: attach a persistent /data volume of this many whole Gi (also attachable later: `insta compute volume <name> --size <gi
|
|
152
|
+
.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 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')
|
|
153
153
|
.option('--json')
|
|
154
154
|
.action(guard(async (type, name, o) => {
|
|
155
155
|
const a = await resolveServiceArgs(type, name, serviceArgsDeps(o.json), o);
|
|
@@ -270,7 +270,7 @@ compute.command('watch-paths [service]').description("Show or change which paths
|
|
|
270
270
|
.option('--json').option('--branch <branch>', 'branch (default: current)').action(guard((service, o) => githubCmd.computeWatchPaths(service, o)));
|
|
271
271
|
compute.command('disconnect-repo [service]').description('Disconnect the GitHub repository from a compute service. The service keeps running its current image; pushes no longer deploy it, and its build history stays')
|
|
272
272
|
.option('--json').option('--branch <branch>', 'branch (default: current)').action(guard((service, o) => githubCmd.computeDisconnectRepo(service, o)));
|
|
273
|
-
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
|
|
273
|
+
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 up to its own plan cap — 10Gi free, 50Gi paid by default — which is also what a disk with no size named is born at; a size above the free cap is paid; 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")
|
|
274
274
|
.option('--size <gi>', 'new size in whole Gi, e.g. 10 (must be ≥ the current size)')
|
|
275
275
|
.option('--delete', 'destroy the volume and ALL its data (irreversible; download anything you need first)')
|
|
276
276
|
.option('--json').option('--branch <branch>', 'branch (default: current)').action(guard((service, o) => computeCmd.computeVolume(service, o)));
|