insta 0.0.25 → 0.0.27
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 +5 -4
- package/dist/commands/auth.js +78 -2
- package/dist/commands/setup.js +1 -1
- package/dist/ensure-skills.js +7 -7
- package/dist/index.js +2 -1
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -79,8 +79,8 @@ plane's address, not the CLI's loopback address.
|
|
|
79
79
|
### Services are branch-scoped
|
|
80
80
|
|
|
81
81
|
A project holds services (`postgres`, `storage`, `compute`) and each branch owns its own
|
|
82
|
-
set. `insta branch create feature-x` forks the parent's services: a
|
|
83
|
-
Postgres, a copy-on-write bucket per storage, a clone of every compute service. From there
|
|
82
|
+
set. `insta branch create feature-x` forks the parent's services: a copy-on-write database
|
|
83
|
+
branch per Postgres, a copy-on-write bucket per storage, a clone of every compute service. From there
|
|
84
84
|
the two branches diverge independently. A project is capped at 10 branches.
|
|
85
85
|
|
|
86
86
|
### Credentials come from the secret seam, not a file you maintain
|
|
@@ -207,8 +207,9 @@ build never reaches a production installer.
|
|
|
207
207
|
The `insta` skill and its task guides live in
|
|
208
208
|
[InsForge/insta-skills](https://github.com/InsForge/insta-skills). `insta setup agent`
|
|
209
209
|
installs it user-globally for every coding agent on the machine. `insta project create` and
|
|
210
|
-
`insta project link` additionally install the stack skills (
|
|
211
|
-
|
|
210
|
+
`insta project link` additionally install the stack skills (Tigris, Better Auth) into the
|
|
211
|
+
project, along with the `insta observe` credential-audit hook. Postgres needs no stack
|
|
212
|
+
skill — it's plain Postgres, reached directly via `DATABASE_URL`.
|
|
212
213
|
|
|
213
214
|
## Contributing
|
|
214
215
|
|
package/dist/commands/auth.js
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { createServer } from 'node:http';
|
|
2
2
|
import { randomBytes } from 'node:crypto';
|
|
3
|
-
import { ApiClient, linkedProject } from '../api.js';
|
|
3
|
+
import { ApiClient, ApiError, linkedProject } from '../api.js';
|
|
4
4
|
import { ENVS, ENV_NAMES, envForApiUrl, isEnvName } from '../env.js';
|
|
5
5
|
import { info, die, printJson, promptPassword, openUrl } from '../util.js';
|
|
6
6
|
/** --api-url and --env both set the target host; --api-url wins (more specific), matching the
|
|
@@ -17,6 +17,8 @@ function targetApiUrl(opts) {
|
|
|
17
17
|
return ENVS[want].api;
|
|
18
18
|
}
|
|
19
19
|
export async function login(opts) {
|
|
20
|
+
if (opts.device)
|
|
21
|
+
return loginDevice(opts);
|
|
20
22
|
if (opts.oauth)
|
|
21
23
|
return loginOauth(opts.oauth, opts);
|
|
22
24
|
const api = await ApiClient.load();
|
|
@@ -24,7 +26,7 @@ export async function login(opts) {
|
|
|
24
26
|
if (target)
|
|
25
27
|
api.setApiUrl(target);
|
|
26
28
|
if (!opts.email)
|
|
27
|
-
die('--email is required (or use --oauth <github|google
|
|
29
|
+
die('--email is required (or use --oauth <github|google>; on a headless machine, --device)');
|
|
28
30
|
const password = opts.password ?? process.env.INSTA_PASSWORD ?? (await promptPassword());
|
|
29
31
|
const res = await api.request('POST', '/auth/login', { email: opts.email, password }, { auth: false });
|
|
30
32
|
api.setSession(res, res.user);
|
|
@@ -47,6 +49,80 @@ export async function loginOauth(provider, opts) {
|
|
|
47
49
|
await api.persist();
|
|
48
50
|
info(`logged in as ${me.user.email ?? me.user.id} @ ${api.apiUrl}`);
|
|
49
51
|
}
|
|
52
|
+
// RFC 8628 device authorization — login from a machine with no usable browser (VM, SSH box, CI
|
|
53
|
+
// container). The loopback --oauth flow can never work there: its callback targets 127.0.0.1 on
|
|
54
|
+
// THIS machine. Here the roles invert — we mint a code, print a link the human opens on ANY
|
|
55
|
+
// device, and poll the platform until they approve in the console.
|
|
56
|
+
export async function loginDevice(opts) {
|
|
57
|
+
const api = await ApiClient.load();
|
|
58
|
+
const target = targetApiUrl(opts);
|
|
59
|
+
if (target)
|
|
60
|
+
api.setApiUrl(target);
|
|
61
|
+
const token = await deviceGrant((path, body) => api.request('POST', path, body, { auth: false }));
|
|
62
|
+
api.setSession({ accessToken: token, refreshToken: token });
|
|
63
|
+
const me = await api.request('GET', '/me');
|
|
64
|
+
api.setSession({ accessToken: token, refreshToken: token }, me.user);
|
|
65
|
+
await api.persist();
|
|
66
|
+
info(`logged in as ${me.user.email ?? me.user.id} @ ${api.apiUrl}`);
|
|
67
|
+
}
|
|
68
|
+
const sleepSeconds = (s) => new Promise((r) => setTimeout(r, s * 1000));
|
|
69
|
+
// Drives the device grant against the platform's Better Auth mount (/api/auth/device*) and
|
|
70
|
+
// returns the approved session token. Injectable poster + wait keep this testable without a
|
|
71
|
+
// network or real timers. Poll errors arrive as ApiError with the OAuth error code as message.
|
|
72
|
+
export async function deviceGrant(post, wait = sleepSeconds) {
|
|
73
|
+
const start = (await post('/api/auth/device/code', { client_id: 'insta-cli' }));
|
|
74
|
+
// A missing/garbage expires_in must fail loudly here — carried into the deadline arithmetic it
|
|
75
|
+
// becomes NaN, every `Date.now() < deadline` is false, and login dies as a bogus instant expiry.
|
|
76
|
+
// Cap the lifetime too: a huge-but-finite value (Number.MAX_VALUE) overflows the ms conversion
|
|
77
|
+
// to Infinity and would otherwise pin the CLI polling forever.
|
|
78
|
+
const expiresIn = Number(start.expires_in);
|
|
79
|
+
if (!Number.isFinite(expiresIn) || expiresIn <= 0) {
|
|
80
|
+
throw new Error('malformed device authorization response (missing expires_in) — is the platform up to date?');
|
|
81
|
+
}
|
|
82
|
+
const lifetime = Math.min(expiresIn, 3600); // no device code sensibly outlives an hour
|
|
83
|
+
info('to log in, open this link in a browser on any device:');
|
|
84
|
+
info(` ${start.verification_uri_complete ?? start.verification_uri}`);
|
|
85
|
+
info(`and check it shows this code: ${start.user_code}`);
|
|
86
|
+
info(`waiting for approval… (expires in ${Math.round(lifetime / 60)}m, ctrl-c to abort)`);
|
|
87
|
+
// Absent OR non-finite interval = the RFC 8628 §3.2 default 5s: NaN would fire the timer
|
|
88
|
+
// instantly and Infinity gets truncated to ~1ms by Node — both hot-poll the token endpoint.
|
|
89
|
+
const rawInterval = Number(start.interval);
|
|
90
|
+
let interval = Number.isFinite(rawInterval) ? Math.max(rawInterval, 1) : 5;
|
|
91
|
+
const deadline = Date.now() + lifetime * 1000;
|
|
92
|
+
while (Date.now() < deadline) {
|
|
93
|
+
await wait(interval);
|
|
94
|
+
let grant = null;
|
|
95
|
+
try {
|
|
96
|
+
grant = (await post('/api/auth/device/token', {
|
|
97
|
+
grant_type: 'urn:ietf:params:oauth:grant-type:device_code',
|
|
98
|
+
device_code: start.device_code,
|
|
99
|
+
client_id: 'insta-cli',
|
|
100
|
+
}));
|
|
101
|
+
}
|
|
102
|
+
catch (e) {
|
|
103
|
+
if (!(e instanceof ApiError))
|
|
104
|
+
continue; // transport blip (dropped SSH/CI link) — keep polling until deadline
|
|
105
|
+
const code = e.message;
|
|
106
|
+
if (code === 'authorization_pending')
|
|
107
|
+
continue;
|
|
108
|
+
if (code === 'slow_down') {
|
|
109
|
+
interval += 5;
|
|
110
|
+
continue;
|
|
111
|
+
} // RFC 8628 §3.5: back off by 5s
|
|
112
|
+
if (code === 'expired_token')
|
|
113
|
+
break;
|
|
114
|
+
if (code === 'access_denied')
|
|
115
|
+
throw new Error('login request was denied in the console');
|
|
116
|
+
throw e; // a definite API-level error (invalid_grant, …) — not retryable
|
|
117
|
+
}
|
|
118
|
+
// Validated OUTSIDE the try: a 200 without a token is a malformed response that must fail
|
|
119
|
+
// loudly, not be mistaken for a transport blip and retried into an empty stored session.
|
|
120
|
+
if (!grant?.access_token)
|
|
121
|
+
throw new Error('malformed token response (missing access_token)');
|
|
122
|
+
return grant.access_token;
|
|
123
|
+
}
|
|
124
|
+
throw new Error('device login expired before it was approved — run `insta login --device` again');
|
|
125
|
+
}
|
|
50
126
|
// Start a loopback server, open the browser at the platform bridge, and await the token.
|
|
51
127
|
function browserOauth(apiUrl, provider) {
|
|
52
128
|
return new Promise((resolve, reject) => {
|
package/dist/commands/setup.js
CHANGED
|
@@ -3,7 +3,7 @@
|
|
|
3
3
|
// every agent the skills tool knows: the skill is pure product knowledge with brand-gated
|
|
4
4
|
// triggers — no project state in it (the project binding is carried by ./.insta/project.json
|
|
5
5
|
// at command time), so one machine-level copy is strictly better than per-project copies.
|
|
6
|
-
// Stack skills (
|
|
6
|
+
// Stack skills (tigris/better-auth) intentionally stay per-project: their presence in a
|
|
7
7
|
// project doubles as its stack manifest — that install happens on `project create|link`.
|
|
8
8
|
import { spawn } from 'node:child_process';
|
|
9
9
|
import os from 'node:os';
|
package/dist/ensure-skills.js
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
// Install the related agent skills into a linked project so the developer's coding agent has
|
|
2
|
-
// context for what InstaCloud provisions — the `insta` CLI itself plus the
|
|
3
|
-
// directly against (
|
|
2
|
+
// context for what InstaCloud provisions — the `insta` CLI itself plus the services you build
|
|
3
|
+
// directly against (Tigris storage, Better Auth). Postgres gets no vendor skill: instadb is
|
|
4
|
+
// plain Postgres, and agents talk to it directly via DATABASE_URL. Runs `npx skills add`
|
|
4
5
|
// (vercel-labs/skills) fully non-interactively. Best-effort: a failure (offline, npx missing, a
|
|
5
6
|
// repo moved) prints a manual fallback and never blocks or fails the host command — same contract
|
|
6
7
|
// as the observe-hook install.
|
|
@@ -13,8 +14,8 @@ import { DEFAULT_ENV, ENVS } from './env.js';
|
|
|
13
14
|
// Codex → .agents/skills/ (.github/skills/ is the third well-known dir). These are regenerable
|
|
14
15
|
// agent context, not the developer's source — keep them out of git.
|
|
15
16
|
const SKILL_DIRS = ['.claude/skills/', '.agents/skills/', '.github/skills/'];
|
|
16
|
-
// `skills` prints a full-screen ASCII banner at the top of every `add`, so our
|
|
17
|
-
// would stack
|
|
17
|
+
// `skills` prints a full-screen ASCII banner at the top of every `add`, so our 3 invocations
|
|
18
|
+
// would stack 3 banners. It skips the banner when it detects an agent driving it rather than a
|
|
18
19
|
// human — AI_AGENT is its first-checked signal (any non-empty value ⇒ agent mode). Setting it is
|
|
19
20
|
// honest here (this IS programmatic, not an interactive prompt) and, because we already pin the
|
|
20
21
|
// agents/skills/-y, has no effect on the install beyond quieting the banner. Preserve a caller's
|
|
@@ -32,10 +33,9 @@ const defaultRunner = (cmd, args, inherit = false) => new Promise((resolve) => {
|
|
|
32
33
|
const AGENT_FLAGS = ['-a', 'claude-code', '-a', 'codex', '-y', '--copy'];
|
|
33
34
|
// `instaSpec` is the insta skill source for the resolved environment (`owner/repo[@ref]`), so a
|
|
34
35
|
// project created against staging gets the staging skill text. The third-party stack skills are
|
|
35
|
-
// environment-independent — they document
|
|
36
|
+
// environment-independent — they document Tigris/Better Auth, not our control plane.
|
|
36
37
|
const skillTargets = (instaSpec) => [
|
|
37
38
|
{ label: 'insta', args: ['skills', 'add', instaSpec, '-s', 'insta', ...AGENT_FLAGS] },
|
|
38
|
-
{ label: 'neon-postgres', args: ['skills', 'add', 'neondatabase/agent-skills', '-s', 'neon-postgres', ...AGENT_FLAGS] },
|
|
39
39
|
{ label: 'tigris', args: ['skills', 'add', 'tigrisdata/skills',
|
|
40
40
|
'-s', 'tigris-object-operations', '-s', 'file-storage', '-s', 'tigris-sdk-guide',
|
|
41
41
|
'-s', 'tigris-security-access-control', '-s', 'tigris-image-optimization',
|
|
@@ -63,7 +63,7 @@ export async function installSkills(deps) {
|
|
|
63
63
|
targets = skillTargets(skills);
|
|
64
64
|
}
|
|
65
65
|
catch { /* keep production defaults */ }
|
|
66
|
-
print(' installing related agent skills (insta,
|
|
66
|
+
print(' installing related agent skills (insta, tigris, better-auth) …');
|
|
67
67
|
for (const s of targets) {
|
|
68
68
|
// Don't stream: the `skills` tool's clack UI (clone spinner, banners) is noise. Run it
|
|
69
69
|
// silent (stdio 'ignore') and let the per-skill ✓/failed line below be the clean output —
|
package/dist/index.js
CHANGED
|
@@ -54,10 +54,11 @@ function resolveVersion() {
|
|
|
54
54
|
}
|
|
55
55
|
program.name('insta').description('InstaCloud CLI — manage projects, branches, secrets, deploys').version(resolveVersion());
|
|
56
56
|
// ---- auth ----
|
|
57
|
-
program.command('login').description('Log in with email + password,
|
|
57
|
+
program.command('login').description('Log in with email + password, --oauth <github|google> (browser), or --device (headless)')
|
|
58
58
|
.option('--email <email>', 'account email')
|
|
59
59
|
.option('--password <password>', 'account password (else $INSTA_PASSWORD or prompt)')
|
|
60
60
|
.option('--oauth <provider>', 'browser OAuth login: github | google')
|
|
61
|
+
.option('--device', 'device-code login: approve from a browser on any other machine (VMs, SSH, CI)')
|
|
61
62
|
.option('--api-url <url>', 'control-plane API base URL')
|
|
62
63
|
.option('--env <name>', `deployment environment: ${ENV_NAMES.join(' | ')}`)
|
|
63
64
|
.action(guard((o) => auth.login(o)));
|