@outerlayer/cli 0.2.0 → 0.4.1

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.
Files changed (141) hide show
  1. package/CHANGELOG.md +132 -0
  2. package/README.md +229 -60
  3. package/dist/agent-setup-IFH3FB5X.js +197 -0
  4. package/dist/build-TZUUTRGH.js +9 -0
  5. package/dist/build-info.json +1 -1
  6. package/dist/check-7MFH6HNC.js +94 -0
  7. package/dist/chunk-3TFPDVTI.js +94 -0
  8. package/dist/{chunk-4TNZMO7V.js → chunk-4C4THABW.js} +5566 -754
  9. package/dist/{chunk-R5KBGVII.js → chunk-5QRP3MVS.js} +30 -14
  10. package/dist/chunk-5Y3QQRUH.js +300 -0
  11. package/dist/chunk-774HEQL6.js +178 -0
  12. package/dist/{work-pr-cmd-AZQPWH4H.js → chunk-7WERLFVR.js} +9 -26
  13. package/dist/chunk-ABIWDUMS.js +104 -0
  14. package/dist/chunk-AUFG23AA.js +534 -0
  15. package/dist/chunk-B2G7JHHB.js +42 -0
  16. package/dist/chunk-BCJNHMZT.js +34 -0
  17. package/dist/chunk-BCLJSQCV.js +56 -0
  18. package/dist/chunk-BFESLKPP.js +61 -0
  19. package/dist/chunk-BJ3KTMDH.js +63 -0
  20. package/dist/chunk-BKO6JEZI.js +47 -0
  21. package/dist/chunk-BOLTI6LR.js +37 -0
  22. package/dist/chunk-CBPPJSAR.js +89 -0
  23. package/dist/chunk-DM3VFDS3.js +1461 -0
  24. package/dist/{chunk-65WXAANR.js → chunk-DVDEBNQJ.js} +17 -2
  25. package/dist/{chunk-TFUIDMOB.js → chunk-EABW6AJQ.js} +12 -1
  26. package/dist/{chunk-KFYJV2ZG.js → chunk-EMY4I27X.js} +1 -1
  27. package/dist/{chunk-NTTPJV35.js → chunk-F47JBIAW.js} +3 -1
  28. package/dist/chunk-F6GFZXZ3.js +60 -0
  29. package/dist/{chunk-YOOSBOKS.js → chunk-F7CU5ABH.js} +1 -1
  30. package/dist/{emit-cmd-TWSYYEZI.js → chunk-H5NCNVDA.js} +68 -17
  31. package/dist/chunk-HZRNMGLF.js +2233 -0
  32. package/dist/chunk-LF6MLJCY.js +38 -0
  33. package/dist/{chunk-U32VSRLO.js → chunk-LT5TZHYH.js} +95 -634
  34. package/dist/chunk-LYCDNMOH.js +83 -0
  35. package/dist/chunk-M5POMKKW.js +1051 -0
  36. package/dist/{mcp-install-cmd-FDQH6SEN.js → chunk-MQ3IPHIZ.js} +35 -18
  37. package/dist/{chunk-D77LS3UI.js → chunk-N5FOE5PS.js} +33 -4
  38. package/dist/chunk-N6LURUEF.js +64 -0
  39. package/dist/chunk-OGFGZQA3.js +23 -0
  40. package/dist/chunk-QDQEUVUF.js +9 -0
  41. package/dist/chunk-QOZKPLVJ.js +226 -0
  42. package/dist/chunk-QY22NZUW.js +1764 -0
  43. package/dist/chunk-RA3O54FC.js +30 -0
  44. package/dist/chunk-RFUPN6KX.js +71 -0
  45. package/dist/chunk-SY4GK4SP.js +55 -0
  46. package/dist/chunk-TBT347UY.js +41 -0
  47. package/dist/chunk-TWNWNS2Q.js +1084 -0
  48. package/dist/{chunk-NXLLURA4.js → chunk-UFFXNXLQ.js} +115 -71
  49. package/dist/chunk-USO2DKBD.js +421 -0
  50. package/dist/chunk-W4SQCOZU.js +206 -0
  51. package/dist/{chunk-Q6CL5THG.js → chunk-W5FVUZXV.js} +208 -61
  52. package/dist/chunk-WFJ65NUD.js +383 -0
  53. package/dist/{chunk-A3WLZX2F.js → chunk-WIOZAJ2W.js} +15 -2
  54. package/dist/chunk-WO2BXCTQ.js +83 -0
  55. package/dist/{chunk-I3ETLSNE.js → chunk-XCCLFVXM.js} +120 -11
  56. package/dist/{context-materialize-6WKT3RBQ.js → chunk-XDDW4FRS.js} +134 -26
  57. package/dist/chunk-Y7KJLXYN.js +232 -0
  58. package/dist/chunk-YFHIIH4B.js +102 -0
  59. package/dist/chunk-YYYXRJUW.js +101 -0
  60. package/dist/chunk-ZM2IMMYN.js +76 -0
  61. package/dist/chunk-ZMYPLFG3.js +71 -0
  62. package/dist/chunk-ZNA27WEV.js +470 -0
  63. package/dist/{cli-G7DILYJY.js → cli-W62AFRSW.js} +616 -878
  64. package/dist/{paths-D2VGWWFI.js → cli-build-K56DK4DS.js} +1 -1
  65. package/dist/config-XVJYZ4IQ.js +9 -0
  66. package/dist/connect-cmd-ZTJONP37.js +16 -0
  67. package/dist/context-adopt-UMA4O5NY.js +105 -0
  68. package/dist/context-materialize-G2FUFC72.js +19 -0
  69. package/dist/dist-44CQVWGT.js +6 -0
  70. package/dist/docker-NEGDLU6D.js +7 -0
  71. package/dist/doctor-53F2PYY7.js +43 -0
  72. package/dist/doctor-ZLGZYMDK.js +426 -0
  73. package/dist/{emit-artifact-cmd-UQVT3OKW.js → emit-artifact-cmd-AKZP7TMF.js} +87 -23
  74. package/dist/emit-cmd-DCERAU27.js +9 -0
  75. package/dist/emit-criteria-cmd-7MXQVCKD.js +166 -0
  76. package/dist/{emit-finding-cmd-FDOU2OPD.js → emit-finding-cmd-N2FTGCVC.js} +34 -31
  77. package/dist/{emit-result-cmd-H2T4C2K3.js → emit-result-cmd-4CPDBMN2.js} +34 -62
  78. package/dist/exec-client-4XGXV3XN.js +8 -0
  79. package/dist/guest-init-GENRHNP7.js +8 -0
  80. package/dist/hook-fast-EYENPK6F.js +9 -0
  81. package/dist/{hook-wrap-fast-KXYNX3AD.js → hook-wrap-fast-NPLHJXFK.js} +2 -2
  82. package/dist/host-key-KDYZ5ECC.js +7 -0
  83. package/dist/import-capture-cmd-AVZ4VIVJ.js +68 -0
  84. package/dist/{import-ruler-cmd-7LH2QOMG.js → import-ruler-cmd-GUHL6IY5.js} +1 -1
  85. package/dist/index.js +4 -4
  86. package/dist/init-55Y4LHWF.js +36 -0
  87. package/dist/init-XXEKWZQP.js +221 -0
  88. package/dist/init-cmd-WLST2F7G.js +135 -0
  89. package/dist/install-cmd-7WXOIFDO.js +55 -0
  90. package/dist/lima-XN65D7GN.js +55 -0
  91. package/dist/login-browser-7XGSV7MA.js +10 -0
  92. package/dist/{config-POF7DEQW.js → logout-cmd-QAUFDJ6J.js} +3 -1
  93. package/dist/{logs-TRNPQM42.js → logs-7RYUH5A6.js} +1 -1
  94. package/dist/loop-JFIBP4B7.js +43 -0
  95. package/dist/machine-ZBPT2J3R.js +35 -0
  96. package/dist/mcp-install-cmd-RLK4SO3N.js +9 -0
  97. package/dist/{mcp-serve-cmd-57EZZOTL.js → mcp-serve-cmd-N6UD2KBX.js} +20 -9
  98. package/dist/paths-OYKMVYJP.js +6 -0
  99. package/dist/{pidfile-PTW76F56.js → pidfile-UZRH774M.js} +2 -3
  100. package/dist/real-deps-SU24ZA2K.js +21 -0
  101. package/dist/relay-L76HDX72.js +46 -0
  102. package/dist/settings-J2652U5N.js +7 -0
  103. package/dist/starter-pack-LIZYMKYQ.js +10 -0
  104. package/dist/{status-I27IST4L.js → status-UJXWZRN5.js} +30 -13
  105. package/dist/{statusline-fast-3C5OXHDD.js → statusline-fast-SVTMBMD7.js} +3 -2
  106. package/dist/sync-cmd-4G4A7EVN.js +28 -0
  107. package/dist/version-BWM6VLDI.js +6 -0
  108. package/dist/{watch-3DTPJETH.js → watch-7SKJKGAA.js} +19 -8
  109. package/dist/{work-claim-cmd-5AUNCLS2.js → work-claim-cmd-5F7VZ42N.js} +34 -21
  110. package/dist/work-cmd-QAUZ7MTD.js +17 -0
  111. package/dist/work-comment-cmd-BNCSBTLH.js +106 -0
  112. package/dist/{work-launch-LT663PB3.js → work-launch-YI4CJDAP.js} +1 -1
  113. package/dist/work-open-pr-cmd-VFYZHIIW.js +156 -0
  114. package/dist/work-pr-cmd-PV32ZLSI.js +16 -0
  115. package/package.json +13 -3
  116. package/skill-pack/maintained/amend/SKILL.md +104 -0
  117. package/skill-pack/maintained/emitting-evidence/SKILL.md +108 -0
  118. package/skill-pack/maintained/emitting-evidence/references/agents-snippet.md +20 -0
  119. package/skill-pack/maintained/outerlayer/SKILL.md +55 -0
  120. package/skill-pack/maintained/reporting-findings/SKILL.md +148 -0
  121. package/skill-pack/template/build/SKILL.md +193 -0
  122. package/skill-pack/template/build/references/agent-briefs.md +243 -0
  123. package/skill-pack/template/build/references/criteria-judge.md +91 -0
  124. package/skill-pack/template/build/references/evidence.md +42 -0
  125. package/skill-pack/template/build/references/release.md +74 -0
  126. package/skill-pack/template/build/references/review-briefs.md +275 -0
  127. package/skill-pack/template/build/references/review-loop.md +158 -0
  128. package/skill-pack/template/build/scripts/record-criteria.mjs +235 -0
  129. package/skill-pack/template/spec/SKILL.md +84 -0
  130. package/skill-pack/template/writing-specs/SKILL.md +134 -0
  131. package/dist/chunk-DCNOXRMV.js +0 -589
  132. package/dist/chunk-JJP7YLMN.js +0 -25
  133. package/dist/chunk-OZ7C3XUE.js +0 -34
  134. package/dist/chunk-WQ6VGRGZ.js +0 -150
  135. package/dist/hook-fast-J5LCDHUJ.js +0 -8
  136. package/dist/import-capture-cmd-EUMBGIV3.js +0 -176
  137. package/dist/init-PTBITAUO.js +0 -103
  138. package/dist/login-cmd-IRX6LZT7.js +0 -62
  139. package/dist/loop-ASZRX3CZ.js +0 -648
  140. package/dist/sync-cmd-BBAUZ5JD.js +0 -17
  141. package/dist/work-cmd-2P4BVX47.js +0 -18
@@ -0,0 +1,421 @@
1
+ #!/usr/bin/env node
2
+ import { createRequire } from 'node:module';
3
+ import { revokeTokenOnDashboard } from './chunk-N6LURUEF.js';
4
+ import { C } from './chunk-YRYMNBGQ.js';
5
+ import { readCloudConfig, writeCloudConfig, cloudConfigPath } from './chunk-EABW6AJQ.js';
6
+ import { homeFromEnv } from './chunk-VNVZDWO3.js';
7
+ import { generateKeyPairSync, privateDecrypt, constants, createPrivateKey } from 'crypto';
8
+ import { spawn } from 'child_process';
9
+ import { fstatSync, existsSync, readdirSync, readFileSync, rmSync, mkdirSync, chmodSync, writeFileSync } from 'fs';
10
+ import { hostname } from 'os';
11
+ import { join } from 'path';
12
+
13
+ createRequire(import.meta.url);
14
+
15
+ // src/login-cmd.ts
16
+ var LoginError = class extends Error {
17
+ };
18
+ function runLogin(opts) {
19
+ const apiKey = opts.apiKey.trim();
20
+ if (apiKey.length === 0) throw new LoginError("no API key given \u2014 pipe it on stdin, or run outerlayer login with no key on stdin to sign in by browser");
21
+ if (/\s/.test(apiKey)) throw new LoginError("the API key must be a single token with no spaces or line breaks");
22
+ const url = opts.url?.trim() || void 0;
23
+ const appId = opts.appId?.trim() || void 0;
24
+ const home = opts.home ?? homeFromEnv();
25
+ writeCloudConfig(
26
+ {
27
+ apiKey,
28
+ ...url ? { url } : {},
29
+ ...appId ? { appId } : {}
30
+ },
31
+ home
32
+ );
33
+ const saved = readCloudConfig(home);
34
+ return { path: cloudConfigPath(home), url: saved.url, appId: saved.appId };
35
+ }
36
+
37
+ // src/login-browser.ts
38
+ var DEFAULT_DASHBOARD = "https://app.outerlayer.ai";
39
+ var EXIT_WAITING = 75;
40
+ var EXIT_USAGE = 2;
41
+ var PIPED_KEY_NOTICE_MS = 1e3;
42
+ var PIPED_KEY_GIVE_UP_MS = 3e4;
43
+ var REQUEST_TIMEOUT_MS = 15e3;
44
+ var MAX_POLL_FAILURES = 5;
45
+ var DEFAULT_POLL_INTERVAL_S = 3;
46
+ var MACHINE_NAME_MAX = 255;
47
+ function realLoginDeps() {
48
+ return {
49
+ fetchImpl: fetch,
50
+ now: () => /* @__PURE__ */ new Date(),
51
+ sleep: (ms) => new Promise((resolve) => setTimeout(resolve, ms)),
52
+ openBrowser: openInBrowser,
53
+ stdin: process.stdin,
54
+ stdoutIsTTY: Boolean(process.stdout.isTTY),
55
+ stdout: process.stdout,
56
+ stderr: process.stderr,
57
+ hostname: hostname
58
+ };
59
+ }
60
+ function openInBrowser(url) {
61
+ return new Promise((resolve) => {
62
+ const [command, args] = process.platform === "darwin" ? ["open", [url]] : process.platform === "win32" ? ["cmd", ["/c", "start", "", url]] : ["xdg-open", [url]];
63
+ try {
64
+ const child = spawn(command, args, { stdio: "ignore", detached: true });
65
+ child.on("error", () => resolve());
66
+ child.unref();
67
+ resolve();
68
+ } catch {
69
+ resolve();
70
+ }
71
+ });
72
+ }
73
+ function sanitizeMachineName(raw) {
74
+ const cleaned = raw.replace(/[^\w\s\-().]/g, "").trim();
75
+ return (cleaned.length > 0 ? cleaned : "machine").slice(0, MACHINE_NAME_MAX);
76
+ }
77
+ function stdinKind(stdin) {
78
+ if (stdin.isTTY) return "terminal";
79
+ const fd = stdin.fd;
80
+ if (typeof fd !== "number") return "pipe";
81
+ try {
82
+ const stat = fstatSync(fd);
83
+ return stat.isFile() || stat.isCharacterDevice() ? "file" : "pipe";
84
+ } catch {
85
+ return "pipe";
86
+ }
87
+ }
88
+ function readPipedKey(stdin, opts = {}) {
89
+ const kind = opts.kind ?? stdinKind(stdin);
90
+ if (kind === "terminal") return Promise.resolve(null);
91
+ const noticeMs = opts.noticeMs ?? PIPED_KEY_NOTICE_MS;
92
+ const giveUpMs = opts.giveUpMs ?? PIPED_KEY_GIVE_UP_MS;
93
+ return new Promise((resolve) => {
94
+ const chunks = [];
95
+ let noticeTimer;
96
+ let giveUpTimer;
97
+ const stopTimers = () => {
98
+ clearTimeout(noticeTimer);
99
+ clearTimeout(giveUpTimer);
100
+ };
101
+ const finish = (value) => {
102
+ stopTimers();
103
+ stdin.off("data", onData);
104
+ stdin.off("end", onEnd);
105
+ stdin.off("error", onEnd);
106
+ stdin.off("close", onEnd);
107
+ if (value === null) stdin.pause();
108
+ resolve(value);
109
+ };
110
+ const onData = (chunk) => {
111
+ stopTimers();
112
+ chunks.push(typeof chunk === "string" ? Buffer.from(chunk) : chunk);
113
+ };
114
+ const onEnd = () => {
115
+ const text = Buffer.concat(chunks).toString("utf8").trim();
116
+ finish(text.length > 0 ? text : null);
117
+ };
118
+ if (kind === "pipe") {
119
+ noticeTimer = setTimeout(() => opts.onWaiting?.(), noticeMs);
120
+ giveUpTimer = setTimeout(() => finish(null), giveUpMs);
121
+ }
122
+ stdin.on("data", onData);
123
+ stdin.once("end", onEnd);
124
+ stdin.once("error", onEnd);
125
+ stdin.once("close", onEnd);
126
+ });
127
+ }
128
+ function pendingDir(home) {
129
+ return join(home, ".outerlayer", "login-pending");
130
+ }
131
+ function isPlainId(id) {
132
+ return /^[\w-]{1,80}$/.test(id);
133
+ }
134
+ function pendingPath(home, id) {
135
+ return join(pendingDir(home), `${id}.json`);
136
+ }
137
+ function writePending(home, pending) {
138
+ const dir = pendingDir(home);
139
+ mkdirSync(dir, { recursive: true, mode: 448 });
140
+ chmodSync(dir, 448);
141
+ writeFileSync(pendingPath(home, pending.id), JSON.stringify(pending, null, 2) + "\n", { mode: 384 });
142
+ chmodSync(pendingPath(home, pending.id), 384);
143
+ }
144
+ function readPending(home, id) {
145
+ if (!isPlainId(id)) return null;
146
+ try {
147
+ return JSON.parse(readFileSync(pendingPath(home, id), "utf8"));
148
+ } catch {
149
+ return null;
150
+ }
151
+ }
152
+ function deletePending(home, id) {
153
+ if (isPlainId(id)) rmSync(pendingPath(home, id), { force: true });
154
+ }
155
+ function purgeExpiredPending(home, now, keepId) {
156
+ const dir = pendingDir(home);
157
+ if (!existsSync(dir)) return;
158
+ for (const name of readdirSync(dir)) {
159
+ if (!name.endsWith(".json") || name === `${keepId}.json`) continue;
160
+ try {
161
+ const parsed = JSON.parse(readFileSync(join(dir, name), "utf8"));
162
+ if (typeof parsed.expires_at === "string" && Date.parse(parsed.expires_at) <= now.getTime()) {
163
+ rmSync(join(dir, name), { force: true });
164
+ }
165
+ } catch {
166
+ }
167
+ }
168
+ }
169
+ function asRecord(value) {
170
+ return typeof value === "object" && value !== null ? value : {};
171
+ }
172
+ async function startLogin(deps, dashboard, publicKey, machineName) {
173
+ let res;
174
+ try {
175
+ res = await deps.fetchImpl(`${dashboard}/api/cli/login`, {
176
+ method: "POST",
177
+ headers: { "content-type": "application/json" },
178
+ body: JSON.stringify({ public_key: publicKey, machine_name: machineName, kind: "account" }),
179
+ signal: AbortSignal.timeout(REQUEST_TIMEOUT_MS)
180
+ });
181
+ } catch (err) {
182
+ throw new LoginError(`could not reach ${dashboard}: ${err instanceof Error ? err.message : String(err)}`);
183
+ }
184
+ if (res.status === 429) {
185
+ const wait = Number(res.headers.get("retry-after"));
186
+ throw new LoginError(
187
+ `${dashboard} is limiting logins from this address` + (wait > 0 ? `; try again in ${wait} seconds` : "; try again shortly")
188
+ );
189
+ }
190
+ const body = asRecord(await res.json().catch(() => null));
191
+ if (!res.ok) {
192
+ const reason = typeof body.message === "string" ? `: ${body.message}` : "";
193
+ throw new LoginError(`${dashboard} refused to start a login (HTTP ${res.status})${reason}`);
194
+ }
195
+ if (typeof body.id !== "string" || typeof body.code !== "string" || typeof body.expires_at !== "string") {
196
+ throw new LoginError(`${dashboard} answered with something that is not a login; is --dashboard the dashboard's address?`);
197
+ }
198
+ const interval = typeof body.interval === "number" && body.interval > 0 ? body.interval : DEFAULT_POLL_INTERVAL_S;
199
+ return { id: body.id, code: body.code, interval, expiresAt: body.expires_at };
200
+ }
201
+ async function pollLogin(deps, dashboard, id) {
202
+ let res;
203
+ try {
204
+ res = await deps.fetchImpl(`${dashboard}/api/cli/login/${encodeURIComponent(id)}`, {
205
+ method: "GET",
206
+ signal: AbortSignal.timeout(REQUEST_TIMEOUT_MS)
207
+ });
208
+ } catch (err) {
209
+ return { kind: "unavailable", message: err instanceof Error ? err.message : String(err) };
210
+ }
211
+ if (res.status === 410) return { kind: "expired" };
212
+ if (res.status === 429) return { kind: "slow", retryAfterS: Math.max(1, Number(res.headers.get("retry-after")) || DEFAULT_POLL_INTERVAL_S) };
213
+ if (res.status === 202) return { kind: "pending" };
214
+ if (res.status >= 500) return { kind: "unavailable", message: `HTTP ${res.status}` };
215
+ if (!res.ok) return { kind: "error", message: `HTTP ${res.status}` };
216
+ const body = asRecord(await res.json().catch(() => null));
217
+ if (body.status === "pending") return { kind: "pending" };
218
+ if (body.status === "denied") return { kind: "denied" };
219
+ if (body.status === "approved" && typeof body.encrypted_token === "string" && typeof body.gateway_url === "string" && typeof body.email === "string") {
220
+ return { kind: "approved", encryptedToken: body.encrypted_token, gatewayUrl: body.gateway_url, email: body.email };
221
+ }
222
+ return { kind: "error", message: "unexpected answer" };
223
+ }
224
+ function decryptToken(privateKeyPem, encryptedToken) {
225
+ try {
226
+ return privateDecrypt(
227
+ { key: createPrivateKey(privateKeyPem), oaepHash: "sha256", padding: constants.RSA_PKCS1_OAEP_PADDING },
228
+ Buffer.from(encryptedToken, "base64")
229
+ ).toString("utf8");
230
+ } catch {
231
+ throw new LoginError("could not decrypt the token the dashboard sent; run outerlayer login again");
232
+ }
233
+ }
234
+ function normalizeDashboard(raw) {
235
+ let url;
236
+ try {
237
+ url = new URL(raw);
238
+ } catch {
239
+ throw new LoginError(`--dashboard must be an address such as ${DEFAULT_DASHBOARD}`);
240
+ }
241
+ if (url.protocol !== "http:" && url.protocol !== "https:") throw new LoginError("--dashboard must start with http:// or https://");
242
+ return `${url.origin}${url.pathname.replace(/\/+$/, "")}`;
243
+ }
244
+ function approveLink(dashboard, code) {
245
+ return `${dashboard}/cli/login?code=${encodeURIComponent(code)}`;
246
+ }
247
+ function waitingDocument(link, code, id) {
248
+ return JSON.stringify({ status: "waiting", url: link, code, check: `outerlayer login --check ${id}` }) + "\n";
249
+ }
250
+ async function finishApproved(flags, deps, home, dashboard, approved, privateKeyPem) {
251
+ const accountToken = decryptToken(privateKeyPem, approved.encryptedToken);
252
+ const before = readCloudConfig(home);
253
+ try {
254
+ writeCloudConfig({ accountToken, url: approved.gatewayUrl, dashboard }, home);
255
+ } catch (err) {
256
+ const revoked = await revokeTokenOnDashboard(deps.fetchImpl, dashboard, accountToken);
257
+ return fail(
258
+ deps,
259
+ `could not save the login: ${err instanceof Error ? err.message : String(err)}. ` + (revoked === "revoked" ? "The token was revoked; run outerlayer login again." : "The token could not be revoked; revoke it on the CLI logins list at /profile/security.")
260
+ );
261
+ }
262
+ if (before.accountToken && before.accountToken !== accountToken) {
263
+ await revokeTokenOnDashboard(deps.fetchImpl, before.dashboard ?? dashboard, before.accountToken);
264
+ }
265
+ const path = cloudConfigPath(home);
266
+ if (flags.json) {
267
+ deps.stdout.write(JSON.stringify({ path, url: approved.gatewayUrl, email: approved.email }) + "\n");
268
+ return 0;
269
+ }
270
+ deps.stdout.write(
271
+ `${C.green}\u2713${C.reset} Logged in as ${approved.email}. Saved the login to ${path}
272
+ Next: run \`outerlayer connect\` to choose a factory.
273
+ `
274
+ );
275
+ if (before.apiKey) {
276
+ deps.stdout.write("A factory key is also saved on this machine, so commands here keep using the key, not this login.\n");
277
+ }
278
+ return 0;
279
+ }
280
+ function fail(deps, message) {
281
+ deps.stderr.write(`${C.red}\u2717${C.reset} ${message}
282
+ `);
283
+ return 1;
284
+ }
285
+ async function runPastedKey(flags, deps, key, home) {
286
+ const result = runLogin({ apiKey: key, url: flags.url, appId: flags.appId, home });
287
+ if (flags.json) {
288
+ deps.stdout.write(JSON.stringify(result) + "\n");
289
+ return 0;
290
+ }
291
+ deps.stdout.write(
292
+ `${C.green}\u2713${C.reset} Saved credentials to ${result.path}` + (result.url ? ` for ${result.url}` : "") + (result.appId ? ` (app ${result.appId})` : "") + "\n"
293
+ );
294
+ return 0;
295
+ }
296
+ async function runCheck(flags, deps, home, id) {
297
+ const pending = readPending(home, id);
298
+ if (!pending) return fail(deps, `no pending login ${id}; run outerlayer login to start one`);
299
+ if (Date.parse(pending.expires_at) <= deps.now().getTime()) {
300
+ deletePending(home, id);
301
+ return fail(deps, "the login link expired; run outerlayer login again");
302
+ }
303
+ const result = await pollLogin(deps, pending.dashboard, pending.id);
304
+ switch (result.kind) {
305
+ case "pending":
306
+ case "slow":
307
+ deps.stdout.write(waitingDocument(pending.url, pending.code, pending.id));
308
+ return EXIT_WAITING;
309
+ case "denied":
310
+ deletePending(home, id);
311
+ return fail(deps, "the login was denied");
312
+ case "expired":
313
+ deletePending(home, id);
314
+ return fail(deps, "the login link expired; run outerlayer login again");
315
+ case "unavailable":
316
+ deps.stderr.write(`could not check the login yet: ${result.message}
317
+ `);
318
+ deps.stdout.write(waitingDocument(pending.url, pending.code, pending.id));
319
+ return EXIT_WAITING;
320
+ case "error":
321
+ return fail(deps, `could not check the login: ${result.message}`);
322
+ case "approved": {
323
+ try {
324
+ return await finishApproved(flags, deps, home, pending.dashboard, result, pending.private_key);
325
+ } finally {
326
+ deletePending(home, id);
327
+ }
328
+ }
329
+ }
330
+ }
331
+ async function runBrowserLogin(flags, deps, home) {
332
+ const dashboard = normalizeDashboard(flags.dashboard ?? DEFAULT_DASHBOARD);
333
+ const { publicKey, privateKey } = generateKeyPairSync("rsa", {
334
+ modulusLength: 2048,
335
+ publicKeyEncoding: { type: "spki", format: "der" },
336
+ privateKeyEncoding: { type: "pkcs8", format: "pem" }
337
+ });
338
+ const started = await startLogin(deps, dashboard, publicKey.toString("base64"), sanitizeMachineName(deps.hostname()));
339
+ const link = approveLink(dashboard, started.code);
340
+ if (!deps.stdoutIsTTY) {
341
+ writePending(home, {
342
+ id: started.id,
343
+ dashboard,
344
+ url: link,
345
+ code: started.code,
346
+ expires_at: started.expiresAt,
347
+ private_key: privateKey
348
+ });
349
+ deps.stdout.write(waitingDocument(link, started.code, started.id));
350
+ return EXIT_WAITING;
351
+ }
352
+ deps.stderr.write(
353
+ `
354
+ Open this link to sign in:
355
+ ${link}
356
+
357
+ Check that the code it shows is ${started.code}.
358
+ Waiting for approval...
359
+ `
360
+ );
361
+ if (flags.browser) await deps.openBrowser(link);
362
+ const expiresAt = Date.parse(started.expiresAt);
363
+ let failures = 0;
364
+ for (; ; ) {
365
+ const result = await pollLogin(deps, dashboard, started.id);
366
+ let waitS = started.interval;
367
+ switch (result.kind) {
368
+ case "approved":
369
+ return await finishApproved(flags, deps, home, dashboard, result, privateKey);
370
+ case "denied":
371
+ return fail(deps, "the login was denied");
372
+ case "expired":
373
+ return fail(deps, "the login link expired; run outerlayer login again");
374
+ case "unavailable":
375
+ case "error":
376
+ failures += 1;
377
+ if (failures >= MAX_POLL_FAILURES) return fail(deps, `could not reach ${dashboard}: ${result.message}`);
378
+ break;
379
+ case "slow":
380
+ failures = 0;
381
+ waitS = result.retryAfterS;
382
+ break;
383
+ case "pending":
384
+ failures = 0;
385
+ break;
386
+ }
387
+ const untilExpiryMs = Math.max(0, expiresAt - deps.now().getTime());
388
+ await deps.sleep(Math.min(waitS * 1e3, untilExpiryMs));
389
+ if (deps.now().getTime() >= expiresAt) return fail(deps, "the login link expired; run outerlayer login again");
390
+ }
391
+ }
392
+ async function runLoginCommand(flags, deps) {
393
+ const home = deps.home ?? homeFromEnv();
394
+ try {
395
+ if (flags.check !== void 0) {
396
+ purgeExpiredPending(home, deps.now(), flags.check);
397
+ return await runCheck(flags, deps, home, flags.check);
398
+ }
399
+ const piped = await readPipedKey(deps.stdin, {
400
+ onWaiting: () => deps.stderr.write("login is waiting for an API key on stdin; close stdin or pass nothing to sign in by browser instead\n")
401
+ });
402
+ if (piped !== null) return await runPastedKey(flags, deps, piped, home);
403
+ if (flags.url !== void 0 || flags.appId !== void 0) {
404
+ deps.stderr.write(
405
+ `${C.red}\u2717${C.reset} --url and --app-id apply only to a piped key; browser login takes its gateway from the dashboard, and --dashboard <address> picks the dashboard
406
+ `
407
+ );
408
+ return EXIT_USAGE;
409
+ }
410
+ if (flags.noInput) {
411
+ return fail(deps, 'no API key on stdin and --no-input forbids prompting \u2014 pipe the key: echo "$KEY" | outerlayer login');
412
+ }
413
+ purgeExpiredPending(home, deps.now());
414
+ return await runBrowserLogin(flags, deps, home);
415
+ } catch (err) {
416
+ if (err instanceof LoginError) return fail(deps, err.message);
417
+ throw err;
418
+ }
419
+ }
420
+
421
+ export { DEFAULT_DASHBOARD, readPipedKey, realLoginDeps, runLoginCommand, sanitizeMachineName, stdinKind };
@@ -0,0 +1,206 @@
1
+ #!/usr/bin/env node
2
+ import { createRequire } from 'node:module';
3
+ import { EXEC_PORT } from './chunk-B2G7JHHB.js';
4
+ import { createConnection } from 'net';
5
+
6
+ createRequire(import.meta.url);
7
+
8
+ // src/runner/microvm/exec-protocol.ts
9
+ var FRAME = { request: 1, stdin: 2, stdout: 3, stderr: 4, exit: 5, kill: 6 };
10
+ var MAX_PAYLOAD = 16 * 1024 * 1024;
11
+ var CHUNK_BYTES = 64 * 1024;
12
+ var ProtocolError = class extends Error {
13
+ };
14
+ var TYPES = new Set(Object.values(FRAME));
15
+ var HEADER_BYTES = 5;
16
+ function encodeFrame(type, payload) {
17
+ const body = typeof payload === "string" ? Buffer.from(payload, "utf8") : payload;
18
+ if (body.length > MAX_PAYLOAD) throw new ProtocolError(`a frame's payload is ${body.length} bytes, past the ${MAX_PAYLOAD} limit`);
19
+ const header = Buffer.alloc(HEADER_BYTES);
20
+ header.writeUInt8(type, 0);
21
+ header.writeUInt32BE(body.length, 1);
22
+ return Buffer.concat([header, body]);
23
+ }
24
+ var FrameDecoder = class {
25
+ constructor() {
26
+ this.pending = Buffer.alloc(0);
27
+ }
28
+ push(chunk) {
29
+ this.pending = this.pending.length === 0 ? chunk : Buffer.concat([this.pending, chunk]);
30
+ const frames = [];
31
+ while (this.pending.length >= HEADER_BYTES) {
32
+ const type = this.pending.readUInt8(0);
33
+ const length = this.pending.readUInt32BE(1);
34
+ if (!TYPES.has(type)) throw new ProtocolError(`a frame has type ${type}, which no side sends`);
35
+ if (length > MAX_PAYLOAD) throw new ProtocolError(`a frame announces ${length} bytes, past the ${MAX_PAYLOAD} limit`);
36
+ if (this.pending.length < HEADER_BYTES + length) break;
37
+ frames.push({ type, payload: Buffer.from(this.pending.subarray(HEADER_BYTES, HEADER_BYTES + length)) });
38
+ this.pending = this.pending.subarray(HEADER_BYTES + length);
39
+ }
40
+ return frames;
41
+ }
42
+ };
43
+ function encodeRequest(request) {
44
+ const { argv, user, cwd, env } = request;
45
+ return Buffer.from(JSON.stringify({ argv, user, cwd, env }), "utf8");
46
+ }
47
+ function decodeExit(payload) {
48
+ if (payload.length !== 4) throw new ProtocolError(`an exit frame holds ${payload.length} bytes, not 4`);
49
+ return payload.readInt32BE(0);
50
+ }
51
+
52
+ // src/runner/microvm/exec-client.ts
53
+ var ExecConnectionError = class extends Error {
54
+ };
55
+ var HANDSHAKE_LIMIT = 256;
56
+ var HANDSHAKE_TIMEOUT_MS = 3e4;
57
+ var CAPTURE_LIMIT_BYTES = 8 * 1024 * 1024;
58
+ function runExec(socketPath, request, io = {}) {
59
+ return new Promise((resolve, reject) => {
60
+ const socket = createConnection(socketPath);
61
+ const decoder = new FrameDecoder();
62
+ let greeted = false;
63
+ let greeting = Buffer.alloc(0);
64
+ let settled = false;
65
+ let killed = false;
66
+ let stdinIterator;
67
+ const handshakeTimer = setTimeout(() => finish(new ExecConnectionError("the exec agent did not answer the connection in time")), HANDSHAKE_TIMEOUT_MS);
68
+ const finish = (outcome) => {
69
+ if (settled) return;
70
+ settled = true;
71
+ clearTimeout(handshakeTimer);
72
+ io.signal?.removeEventListener("abort", kill);
73
+ void stdinIterator?.return?.();
74
+ socket.destroy();
75
+ if (outcome instanceof Error) reject(outcome);
76
+ else resolve(outcome);
77
+ };
78
+ const send = (type, payload) => socket.destroyed ? true : socket.write(encodeFrame(type, payload));
79
+ function kill() {
80
+ if (killed || !greeted) return;
81
+ killed = true;
82
+ send(FRAME.kill, Buffer.alloc(0));
83
+ }
84
+ const pumpStdin = async () => {
85
+ const stdin = io.stdin;
86
+ const chunks = (buffer) => Array.from({ length: Math.ceil(buffer.length / CHUNK_BYTES) }, (_unused, i) => buffer.subarray(i * CHUNK_BYTES, (i + 1) * CHUNK_BYTES));
87
+ try {
88
+ if (stdin === void 0) {
89
+ } else if (typeof stdin === "string" || Buffer.isBuffer(stdin)) {
90
+ for (const chunk of chunks(Buffer.from(stdin))) send(FRAME.stdin, chunk);
91
+ } else {
92
+ stdinIterator = stdin.iterator({ destroyOnReturn: false });
93
+ for (; ; ) {
94
+ const next = await stdinIterator.next();
95
+ if (next.done === true || settled) break;
96
+ for (const chunk of chunks(Buffer.from(next.value))) {
97
+ if (!send(FRAME.stdin, chunk)) await new Promise((done) => socket.once("drain", () => done()));
98
+ }
99
+ }
100
+ }
101
+ } catch {
102
+ }
103
+ send(FRAME.stdin, Buffer.alloc(0));
104
+ };
105
+ const onFrames = (chunk) => {
106
+ let frames;
107
+ try {
108
+ frames = decoder.push(chunk);
109
+ } catch (err) {
110
+ finish(new ExecConnectionError(`the guest's exec agent sent a bad frame: ${err instanceof Error ? err.message : String(err)}`));
111
+ return;
112
+ }
113
+ for (const frame of frames) {
114
+ if (frame.type === FRAME.stdout) io.onStdout?.(frame.payload);
115
+ else if (frame.type === FRAME.stderr) io.onStderr?.(frame.payload);
116
+ else if (frame.type === FRAME.exit) {
117
+ try {
118
+ finish({ status: decodeExit(frame.payload) });
119
+ } catch (err) {
120
+ finish(new ExecConnectionError(`the guest's exec agent sent a bad exit frame: ${err instanceof Error ? err.message : String(err)}`));
121
+ }
122
+ return;
123
+ }
124
+ }
125
+ };
126
+ socket.on("connect", () => socket.write(`CONNECT ${EXEC_PORT}
127
+ `));
128
+ socket.on("data", (chunk) => {
129
+ if (greeted) {
130
+ onFrames(chunk);
131
+ return;
132
+ }
133
+ greeting = Buffer.concat([greeting, chunk]);
134
+ const newline = greeting.indexOf(10);
135
+ if (newline === -1) {
136
+ if (greeting.length > HANDSHAKE_LIMIT) finish(new ExecConnectionError("the vsock device's answer to CONNECT was not a line"));
137
+ return;
138
+ }
139
+ const line = greeting.subarray(0, newline).toString("utf8");
140
+ if (!/^OK\b/.test(line)) {
141
+ finish(new ExecConnectionError(`the vsock device refused the connection to the exec agent: ${line}`));
142
+ return;
143
+ }
144
+ greeted = true;
145
+ clearTimeout(handshakeTimer);
146
+ send(FRAME.request, encodeRequest(request));
147
+ if (io.signal?.aborted === true) kill();
148
+ void pumpStdin();
149
+ const rest = greeting.subarray(newline + 1);
150
+ if (rest.length > 0) onFrames(rest);
151
+ });
152
+ socket.on("error", (err) => finish(new ExecConnectionError(`the exec agent at ${socketPath} could not be reached: ${err.message}`)));
153
+ socket.on("close", () => finish(new ExecConnectionError("the connection to the exec agent ended before the command did")));
154
+ io.signal?.addEventListener("abort", kill, { once: true });
155
+ });
156
+ }
157
+ async function execCapture(socketPath, request, io = {}) {
158
+ const stdout = [];
159
+ const stderr = [];
160
+ const sizes = { stdout: 0, stderr: 0 };
161
+ const keep = (into, stream, chunk) => {
162
+ const room = CAPTURE_LIMIT_BYTES - sizes[stream];
163
+ if (room <= 0) return;
164
+ const kept = chunk.length > room ? chunk.subarray(0, room) : chunk;
165
+ sizes[stream] += kept.length;
166
+ into.push(kept);
167
+ };
168
+ const { status } = await runExec(socketPath, request, { ...io, onStdout: (chunk) => keep(stdout, "stdout", chunk), onStderr: (chunk) => keep(stderr, "stderr", chunk) });
169
+ return { status, stdout: Buffer.concat(stdout).toString("utf8"), stderr: Buffer.concat(stderr).toString("utf8") };
170
+ }
171
+ async function waitForAgent(socketPath, options) {
172
+ for (let attempt = 0; attempt < options.attempts; attempt++) {
173
+ try {
174
+ await execCapture(socketPath, { argv: ["true"], user: "root", cwd: "/", env: {} });
175
+ return true;
176
+ } catch {
177
+ await options.sleep(options.delayMs);
178
+ }
179
+ }
180
+ return false;
181
+ }
182
+ function vmExecArgs(options) {
183
+ return [
184
+ "--socket",
185
+ options.socket,
186
+ "--user",
187
+ options.user,
188
+ "--cwd",
189
+ options.cwd,
190
+ ...Object.keys(options.env).sort().flatMap((name) => ["--env", `${name}=${options.env[name]}`]),
191
+ ...[...options.pass].sort().flatMap((name) => ["--pass", name]),
192
+ "--",
193
+ ...options.argv
194
+ ];
195
+ }
196
+ async function runVmExec(options, io) {
197
+ const env = { ...options.env };
198
+ for (const name of options.pass) {
199
+ const value = io.env[name];
200
+ if (value !== void 0) env[name] = value;
201
+ }
202
+ const { status } = await runExec(options.socket, { argv: options.argv, user: options.user, cwd: options.cwd, env }, { stdin: io.stdin, onStdout: io.stdout, onStderr: io.stderr, signal: io.signal });
203
+ return status;
204
+ }
205
+
206
+ export { CAPTURE_LIMIT_BYTES, ExecConnectionError, HANDSHAKE_TIMEOUT_MS, execCapture, runExec, runVmExec, vmExecArgs, waitForAgent };