@runtypelabs/cli 2.47.8 → 2.48.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 (3) hide show
  1. package/README.md +20 -7
  2. package/dist/index.js +1944 -1090
  3. package/package.json +9 -8
package/dist/index.js CHANGED
@@ -2,39 +2,40 @@
2
2
  import { createRequire } from "node:module";
3
3
  import { Command, InvalidArgumentError, Option } from "commander";
4
4
  import chalk from "chalk";
5
- import * as fs$2 from "node:fs";
5
+ import * as fs$1 from "node:fs";
6
6
  import fs, { closeSync, constants, existsSync, lstatSync, mkdirSync, openSync, readFileSync, readdirSync, realpathSync, renameSync, rmSync, statSync, writeFileSync } from "node:fs";
7
7
  import * as path$2 from "node:path";
8
8
  import path, { dirname, isAbsolute, join, relative, resolve, sep } from "node:path";
9
9
  import React, { memo, useCallback, useEffect, useMemo, useRef, useState } from "react";
10
10
  import { Box, Text, render, useApp, useInput, useStdout } from "ink";
11
- import open from "open";
12
- import crypto$1 from "crypto";
13
- import express from "express";
14
11
  import { ClientTokensNamespace, DEFAULT_STALL_STOP_AFTER, ENSURE_RELEASE_DEPLOY_CONFLICT_MESSAGE, FlowsNamespace, LEDGER_ARTIFACT_LINE_PREFIX, ProductsNamespace, RuntypeApiError, RuntypeClient, SkillsNamespace, SurfacesNamespace, ToolsNamespace, activateAgentPromotion, buildLedgerOffloadReference, compileWorkflowConfig, createClient, defaultWorkflow, defineFlow, defineFpo, defineProduct, defineSkill, defineSurface, defineTool, deployWorkflow, ensureDefaultWorkflowHooks, evaluateGeneratedRuntimeToolProposal, gameWorkflow, getDefaultPlanPath, isBuiltinToolExecuteResponse, isWorkflowHookRef, parseFinalBuffer, parseLedgerArtifactRelativePath, parseOffloadedOutputId, parseSSEChunk, prepareAgentPromotion, processStream, registerWorkflowHook, validateAgentPromotion } from "@runtypelabs/sdk";
15
12
  import Conf from "conf";
16
- import os, { tmpdir } from "os";
17
- import path$1, { delimiter, dirname as dirname$1, join as join$1, relative as relative$1, resolve as resolve$1, sep as sep$1 } from "path";
18
- import fs$1, { copyFileSync, existsSync as existsSync$1, mkdirSync as mkdirSync$1, readFileSync as readFileSync$1, readdirSync as readdirSync$1, realpathSync as realpathSync$1, unlinkSync, writeFileSync as writeFileSync$1 } from "fs";
13
+ import { AsyncLocalStorage } from "node:async_hooks";
14
+ import * as crypto$3 from "node:crypto";
15
+ import crypto$1, { createHash, randomBytes, randomUUID } from "node:crypto";
16
+ import * as os$1 from "node:os";
17
+ import os, { constants as constants$1 } from "node:os";
19
18
  import { fileURLToPath, pathToFileURL } from "node:url";
19
+ import open from "open";
20
+ import crypto$2 from "crypto";
21
+ import express from "express";
20
22
  import readline from "readline";
21
23
  import { ErrorDisplay, Spinner, StatusBar, StreamOutput, renderMarkdownToTerminal, theme } from "@runtypelabs/ink-components";
22
24
  import { Fragment, jsx, jsxs } from "react/jsx-runtime";
23
- import TextInput from "ink-text-input";
24
25
  import SelectInput from "ink-select-input";
26
+ import { copyFileSync, existsSync as existsSync$1, mkdirSync as mkdirSync$1, readFileSync as readFileSync$1, readdirSync as readdirSync$1, realpathSync as realpathSync$1, unlinkSync, writeFileSync as writeFileSync$1 } from "fs";
27
+ import path$1, { delimiter, dirname as dirname$1, join as join$1, relative as relative$1, resolve as resolve$1, sep as sep$1 } from "path";
28
+ import { access, chmod, copyFile, mkdir, open as open$1, readFile, realpath, rename, rm, stat, writeFile } from "node:fs/promises";
29
+ import TextInput from "ink-text-input";
25
30
  import { execSync, spawn, spawnSync } from "node:child_process";
26
- import * as crypto$2 from "node:crypto";
27
- import { createHash, randomBytes, randomUUID } from "node:crypto";
28
- import * as os$2 from "node:os";
29
- import os$1, { constants as constants$1 } from "node:os";
30
31
  import { AnimatedVariant, LoadingAnimation, StartupGridIconCompactLightInverted } from "@runtypelabs/terminal-animations";
31
32
  import { createJiti } from "jiti";
32
33
  import micromatch from "micromatch";
33
34
  import { parse } from "yaml";
34
- import { access, chmod, copyFile, mkdir, open as open$1, readFile, realpath, rename, rm, stat, writeFile } from "node:fs/promises";
35
35
  import { execFileSync, spawn as spawn$1 } from "child_process";
36
36
  import { createServer } from "node:http";
37
37
  import { createServer as createServer$1 } from "node:net";
38
+ import { tmpdir } from "os";
38
39
  import { zipSync } from "fflate";
39
40
  import { Client, StreamableHTTPClientTransport } from "@modelcontextprotocol/client";
40
41
  import WebSocket from "ws";
@@ -66,378 +67,6 @@ function loadEnv(path = resolve(process.cwd(), ".env")) {
66
67
  } catch {}
67
68
  }
68
69
  //#endregion
69
- //#region src/auth/callback-server.ts
70
- var CallbackServer = class {
71
- server = null;
72
- app;
73
- codeResolve;
74
- codeReject;
75
- codePromise;
76
- expectedState;
77
- constructor() {
78
- this.app = express();
79
- this.codePromise = new Promise((resolve, reject) => {
80
- this.codeResolve = resolve;
81
- this.codeReject = reject;
82
- });
83
- this.app.get("/callback", (req, res) => {
84
- const { token, code, state, error } = req.query;
85
- if (typeof state !== "string" || state !== this.expectedState) {
86
- res.status(400).send(this.errorHTML("Invalid authentication state. Continue the login you started in the CLI."));
87
- return;
88
- }
89
- const values = [
90
- token,
91
- code,
92
- error
93
- ].filter((value) => value !== void 0);
94
- if (values.length !== 1 || typeof values[0] !== "string" || values[0].length === 0) {
95
- res.status(400).send(this.errorHTML("Invalid authentication callback"));
96
- return;
97
- }
98
- if (typeof error === "string") {
99
- res.send(this.errorHTML(error));
100
- this.codeReject(new Error(error));
101
- setTimeout(() => this.stop(), 1e3);
102
- return;
103
- }
104
- res.send(this.successHTML());
105
- this.codeResolve(values[0]);
106
- setTimeout(() => this.stop(), 1e3);
107
- });
108
- this.app.get("/health", (_req, res) => {
109
- res.json({ status: "ok" });
110
- });
111
- }
112
- /** Set expected state parameter for OAuth2 CSRF validation */
113
- setExpectedState(state) {
114
- if (!state) throw new Error("Authentication requires a nonempty expected state");
115
- if (this.server) throw new Error("Expected state must be registered before listening");
116
- this.expectedState = state;
117
- }
118
- async start(port = 8765) {
119
- if (!this.expectedState) throw new Error("Authentication requires an expected state before listening");
120
- return new Promise((resolveStart, rejectStart) => {
121
- this.server = this.app.listen(port, "127.0.0.1", () => {
122
- const address = this.server?.address();
123
- const actualPort = address && typeof address === "object" && "port" in address ? address.port : port;
124
- console.log(`Callback server listening on port ${actualPort}`);
125
- resolveStart({
126
- actualPort,
127
- codePromise: this.codePromise
128
- });
129
- });
130
- this.server.on("error", (error) => {
131
- if (error.code === "EADDRINUSE") {
132
- console.log(`Port ${port} is in use, trying ${port + 1}...`);
133
- this.server = null;
134
- this.start(port + 1).then(resolveStart).catch(rejectStart);
135
- } else rejectStart(error);
136
- });
137
- });
138
- }
139
- stop() {
140
- if (this.server) {
141
- const s = this.server;
142
- this.server = null;
143
- s.unref();
144
- if (typeof s.closeAllConnections === "function") s.closeAllConnections();
145
- s.close();
146
- }
147
- }
148
- successHTML() {
149
- return `<!DOCTYPE html>
150
- <html lang="en">
151
- <head>
152
- <meta charset="utf-8">
153
- <title>Authentication Successful</title>
154
- <style>
155
- * { margin: 0; padding: 0; box-sizing: border-box; }
156
- body {
157
- font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;
158
- height: 100vh;
159
- overflow: hidden;
160
- background: #0a0a0a;
161
- }
162
- .grid-bg {
163
- position: absolute;
164
- inset: 0;
165
- z-index: 0;
166
- overflow: hidden;
167
- }
168
- .grid-bg .gradient {
169
- position: absolute;
170
- inset: 0;
171
- background: linear-gradient(to bottom, #0a0a0a 0%, #171717 50%, #262626 100%);
172
- }
173
- @keyframes gridMove {
174
- 0% { background-position-y: 0; }
175
- 100% { background-position-y: 30px; }
176
- }
177
- .grid-bg .grid-vertical-wrap {
178
- position: absolute;
179
- left: 0;
180
- right: 0;
181
- bottom: 0;
182
- top: 40%;
183
- transform-origin: top center;
184
- transform: perspective(500px) rotateX(60deg);
185
- }
186
- .grid-bg .grid-vertical-wrap .fade {
187
- position: absolute;
188
- inset: 0;
189
- background: linear-gradient(to bottom, transparent 0%, rgba(255,255,255,0.02) 100%);
190
- }
191
- .grid-bg .grid-vertical-wrap .lines {
192
- position: absolute;
193
- inset: 0;
194
- background-image: repeating-linear-gradient(
195
- 90deg,
196
- transparent 0,
197
- transparent 49px,
198
- rgba(255,255,255,0.08) 49px,
199
- rgba(255,255,255,0.08) 50px
200
- );
201
- background-size: 50px 100%;
202
- }
203
- .grid-bg .grid-horizontal {
204
- position: absolute;
205
- left: 0;
206
- right: 0;
207
- bottom: 0;
208
- top: 40%;
209
- transform-origin: top center;
210
- transform: perspective(500px) rotateX(60deg);
211
- background-image: repeating-linear-gradient(
212
- 0deg,
213
- transparent 0,
214
- transparent 29px,
215
- rgba(255,255,255,0.06) 29px,
216
- rgba(255,255,255,0.06) 30px
217
- );
218
- background-size: 100% 30px;
219
- animation: gridMove 2s linear infinite;
220
- }
221
- .card-wrap {
222
- position: absolute;
223
- left: 50%;
224
- top: 50%;
225
- transform: translate(-50%, -50%);
226
- z-index: 10;
227
- width: 100%;
228
- max-width: 400px;
229
- perspective: 1400px;
230
- animation: cardIn 0.4s ease-out;
231
- }
232
- @keyframes cardIn {
233
- from { opacity: 0; transform: translate(-50%, -50%) translateY(16px); }
234
- to { opacity: 1; transform: translate(-50%, -50%) translateY(0); }
235
- }
236
- .card-inner {
237
- position: relative;
238
- }
239
- .card-layer-back,
240
- .card-layer-mid {
241
- position: absolute;
242
- inset: 0;
243
- border-radius: 12px;
244
- border: 1px solid #e5e7eb;
245
- pointer-events: none;
246
- }
247
- .card-layer-back {
248
- background: rgba(0, 0, 0, 0.08);
249
- transform: translate3d(0, -18px, -36px) rotateX(6deg);
250
- }
251
- .card-layer-mid {
252
- background: rgba(0, 0, 0, 0.05);
253
- transform: translate3d(0, -10px, -20px) rotateX(3deg);
254
- }
255
- .card-front {
256
- position: relative;
257
- border-radius: 12px;
258
- border: 1px solid rgba(255, 255, 255, 1);
259
- background: rgba(0, 0, 0, .5);
260
- backdrop-filter: blur(10px);
261
- -webkit-backdrop-filter: blur(10px);
262
- padding: 2rem 2.5rem;
263
- text-align: center;
264
- box-shadow: 0 1px 3px rgba(0, 0, 0, 0.2);
265
- transform: rotateX(-10deg) translateZ(0);
266
- transform-style: preserve-3d;
267
- }
268
- .card-front .section-title {
269
- color: #ffffff;
270
- font-size: 0.8125rem;
271
- font-weight: 600;
272
- letter-spacing: 0.05em;
273
- text-transform: uppercase;
274
- margin-bottom: 1rem;
275
- }
276
- .card-front .flow-arrow {
277
- color: rgba(255, 255, 255, 0.7);
278
- font-size: 1rem;
279
- margin-bottom: 1rem;
280
- line-height: 1;
281
- }
282
- .card-front .message {
283
- color: #ffffff;
284
- font-size: 0.75rem;
285
- font-weight: 100;
286
- margin-bottom: 0.5rem;
287
- font-family: monospace;
288
- }
289
- .card-front .progress {
290
- color: rgba(255, 255, 255, 0.75);
291
- font-size: 0.8125rem;
292
- }
293
- </style>
294
- </head>
295
- <body>
296
- <div class="grid-bg">
297
- <div class="gradient"></div>
298
- <div class="grid-vertical-wrap">
299
- <div class="fade"></div>
300
- <div class="lines"></div>
301
- </div>
302
- <div class="grid-horizontal"></div>
303
- </div>
304
- <div id="card-wrap" class="card-wrap">
305
- <div class="card-inner">
306
- <div class="card-layer-back" aria-hidden="true"></div>
307
- <div class="card-layer-mid" aria-hidden="true"></div>
308
- <div id="card" class="card-front">
309
- <div class="section-title">Authentication Successful</div>
310
- <div class="flow-arrow" aria-hidden="true">↓</div>
311
- <p class="message">You can close this window and return to your terminal.</p>
312
- <p class="progress" id="progress">Closing automatically in 3 seconds…</p>
313
- </div>
314
- </div>
315
- </div>
316
- <script>
317
- setTimeout(function() {
318
- window.close();
319
- var prog = document.getElementById('progress');
320
- if (prog) prog.textContent = 'You can now close this window.';
321
- var card = document.getElementById('card');
322
- if (card) card.innerHTML = '<div class="section-title">Runtype is Ready</div><p class="message">You can now close this window</p>';
323
- }, 3000);
324
- <\/script>
325
- </body>
326
- </html>`;
327
- }
328
- escapeHtml(str) {
329
- return str.replace(/&/g, "&amp;").replace(/</g, "&lt;").replace(/>/g, "&gt;").replace(/"/g, "&quot;").replace(/'/g, "&#39;");
330
- }
331
- errorHTML(error) {
332
- return `
333
- <!DOCTYPE html>
334
- <html>
335
- <head>
336
- <title>Authentication Failed</title>
337
- <style>
338
- * {
339
- margin: 0;
340
- padding: 0;
341
- box-sizing: border-box;
342
- }
343
- body {
344
- font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, 'Helvetica Neue', Arial, sans-serif;
345
- display: flex;
346
- align-items: center;
347
- justify-content: center;
348
- height: 100vh;
349
- background: linear-gradient(135deg, #ef4444 0%, #dc2626 100%);
350
- }
351
- .container {
352
- background: white;
353
- padding: 3rem;
354
- border-radius: 12px;
355
- text-align: center;
356
- box-shadow: 0 20px 40px rgba(0,0,0,0.1);
357
- max-width: 400px;
358
- }
359
- .error-icon {
360
- color: #ef4444;
361
- font-size: 64px;
362
- margin-bottom: 1rem;
363
- }
364
- h1 {
365
- color: #1f2937;
366
- margin-bottom: 0.5rem;
367
- font-size: 1.5rem;
368
- }
369
- p {
370
- color: #6b7280;
371
- font-size: 1rem;
372
- margin-bottom: 1rem;
373
- }
374
- .error-details {
375
- background: #fef2f2;
376
- color: #991b1b;
377
- padding: 0.75rem;
378
- border-radius: 6px;
379
- font-size: 0.875rem;
380
- font-family: monospace;
381
- }
382
- </style>
383
- </head>
384
- <body>
385
- <div class="container">
386
- <div class="error-icon">✕</div>
387
- <h1>Authentication Failed</h1>
388
- <p>There was an error during authentication.</p>
389
- <div class="error-details">${this.escapeHtml(error)}</div>
390
- </div>
391
- </body>
392
- </html>
393
- `;
394
- }
395
- };
396
- //#endregion
397
- //#region src/auth/oauth-manager.ts
398
- var OAuthManager = class {
399
- clerkPublishableKey;
400
- dashboardUrl;
401
- constructor(config) {
402
- this.clerkPublishableKey = config.clerkPublishableKey;
403
- this.dashboardUrl = config.dashboardUrl;
404
- }
405
- async authenticate(mode) {
406
- const state = crypto$1.randomBytes(32).toString("base64url");
407
- const codeVerifier = crypto$1.randomBytes(32).toString("base64url");
408
- const codeChallenge = crypto$1.createHash("sha256").update(codeVerifier).digest("base64url");
409
- const callbackServer = new CallbackServer();
410
- callbackServer.setExpectedState(state);
411
- const requestedPort = await this.findAvailablePort();
412
- const { actualPort, codePromise } = await callbackServer.start(requestedPort);
413
- const redirectUri = `http://127.0.0.1:${actualPort}/callback`;
414
- const authUrl = new URL(`${this.dashboardUrl.replace(/\/$/, "")}/${mode}`);
415
- authUrl.searchParams.set("redirect_uri", redirectUri);
416
- authUrl.searchParams.set("cli", "true");
417
- authUrl.searchParams.set("state", state);
418
- authUrl.searchParams.set("code_challenge", codeChallenge);
419
- authUrl.searchParams.set("code_challenge_method", "S256");
420
- authUrl.searchParams.set("publishable_key", this.clerkPublishableKey);
421
- console.log("🌐 Opening browser for authentication...");
422
- console.log(`If the browser doesn't open, visit: ${authUrl}`);
423
- try {
424
- console.log("⏳ Waiting for authentication...");
425
- const [code] = await Promise.all([codePromise, open(authUrl.toString()).catch(() => {
426
- console.log("Failed to open browser automatically. Please visit the URL above.");
427
- })]);
428
- return await this.exchangeCodeForSession(code, codeVerifier, redirectUri);
429
- } finally {
430
- callbackServer.stop();
431
- }
432
- }
433
- async findAvailablePort() {
434
- return 8765;
435
- }
436
- async exchangeCodeForSession(code, codeVerifier, redirectUri) {
437
- return code;
438
- }
439
- };
440
- //#endregion
441
70
  //#region ../shared/dist/chunk-1Tgh2C1u.mjs
442
71
  /**
443
72
  * Get nested property value using dot notation and bracket notation.
@@ -11528,7 +11157,7 @@ function isDataConnectionToolId(toolId) {
11528
11157
  return toolId.startsWith("dc:");
11529
11158
  }
11530
11159
  //#endregion
11531
- //#region ../shared/dist/chunk-CN8mv6N6.mjs
11160
+ //#region ../shared/dist/chunk-InaIcoUA.mjs
11532
11161
  /**
11533
11162
  * Maps model families to provider-specific model IDs.
11534
11163
  * Use for routing: family + provider → exact model ID
@@ -11551,6 +11180,11 @@ const MODEL_FAMILY_PROVIDER_IDS = {
11551
11180
  },
11552
11181
  "claude-haiku-4-5-20251001": { "anthropic": "claude-haiku-4-5-20251001" },
11553
11182
  "claude-haiku-4.5": { "vercel": "anthropic/claude-haiku-4.5" },
11183
+ "claude-haiku-5-5": {
11184
+ "anthropic": "claude-haiku-5-5",
11185
+ "vercel": "anthropic/claude-haiku-5.5"
11186
+ },
11187
+ "claude-haiku-5.5": { "vercel": "anthropic/claude-haiku-5.5" },
11554
11188
  "claude-opus-4": { "vercel": "anthropic/claude-opus-4" },
11555
11189
  "claude-opus-4-5": {
11556
11190
  "anthropic": "claude-opus-4-5-20251101",
@@ -11613,12 +11247,14 @@ const MODEL_FAMILY_PROVIDER_IDS = {
11613
11247
  "deep-research-max-preview-04-2026": { "google": "deep-research-max-preview-04-2026" },
11614
11248
  "deep-research-preview-04-2026": { "google": "deep-research-preview-04-2026" },
11615
11249
  "deepseek-v4-1-flash": { "vercel": "deepseek/deepseek-v4.1-flash" },
11250
+ "deepseek-v4-1-flash-fast": { "vercel": "deepseek/deepseek-v4.1-flash-fast" },
11616
11251
  "deepseek-v4-flash": { "vercel": "deepseek/deepseek-v4-flash" },
11617
11252
  "deepseek-v4-flash-0731": { "vercel": "deepseek/deepseek-v4-flash-0731" },
11618
11253
  "deepseek-v4-flash-vision-exp": { "vercel": "deepseek/deepseek-v4-flash-vision-exp" },
11619
11254
  "deepseek-v4-pro": { "vercel": "deepseek/deepseek-v4-pro" },
11620
11255
  "deepseek-v4-pro-0813": { "vercel": "deepseek/deepseek-v4-pro-0813" },
11621
11256
  "deepseek-v4.1-flash": { "vercel": "deepseek/deepseek-v4.1-flash" },
11257
+ "deepseek-v4.1-flash-fast": { "vercel": "deepseek/deepseek-v4.1-flash-fast" },
11622
11258
  "ember-1": { "vercel": "fireworks/ember-1" },
11623
11259
  "fugu-max": { "vercel": "sakana/fugu-max" },
11624
11260
  "fugu-ultra": { "vercel": "sakana/fugu-ultra-v2" },
@@ -11746,6 +11382,8 @@ const MODEL_FAMILY_PROVIDER_IDS = {
11746
11382
  "gemini-3.8-live-extended-thinking": { "vercel": "google/gemini-3.8-live-extended-thinking" },
11747
11383
  "gemini-embedding-001": { "google": "gemini-embedding-001" },
11748
11384
  "gemini-embedding-2": { "google": "gemini-embedding-2" },
11385
+ "gemini-nano-banana-2-1": { "vercel": "google/gemini-nano-banana-2.1" },
11386
+ "gemini-nano-banana-2.1": { "vercel": "google/gemini-nano-banana-2.1" },
11749
11387
  "gemini-omni-flash": { "vercel": "google/gemini-omni-flash-preview" },
11750
11388
  "gemini-omni-flash-preview": { "vercel": "google/gemini-omni-flash-preview" },
11751
11389
  "gemma-4-26b-a4b-it": {
@@ -12056,11 +11694,10 @@ const MODEL_FAMILY_PROVIDER_IDS = {
12056
11694
  "inkling": { "vercel": "thinkingmachines/inkling" },
12057
11695
  "inkling-small": { "vercel": "thinkingmachines/inkling-small" },
12058
11696
  "interfaze-beta": { "vercel": "interfaze/interfaze-beta" },
12059
- "kimi-k2": { "vercel": "moonshotai/kimi-k2-thinking" },
11697
+ "kimi-k2": { "vercel": "moonshotai/kimi-k2" },
12060
11698
  "kimi-k2-6": { "vercel": "moonshotai/kimi-k2.6" },
12061
11699
  "kimi-k2-7-code": { "vercel": "moonshotai/kimi-k2.7-code" },
12062
11700
  "kimi-k2-7-code-highspeed": { "vercel": "moonshotai/kimi-k2.7-code-highspeed" },
12063
- "kimi-k2-thinking": { "vercel": "moonshotai/kimi-k2-thinking" },
12064
11701
  "kimi-k2.6": { "vercel": "moonshotai/kimi-k2.6" },
12065
11702
  "kimi-k2.7-code": { "vercel": "moonshotai/kimi-k2.7-code" },
12066
11703
  "kimi-k2.7-code-highspeed": { "vercel": "moonshotai/kimi-k2.7-code-highspeed" },
@@ -12072,13 +11709,11 @@ const MODEL_FAMILY_PROVIDER_IDS = {
12072
11709
  "laguna-s-2.1-free": { "vercel": "poolside/laguna-s-2.1-free" },
12073
11710
  "ling-3-0-flash": { "vercel": "inclusionai/ling-3.0-flash" },
12074
11711
  "ling-3-0-flash-sante": { "vercel": "inclusionai/ling-3.0-flash-sante" },
12075
- "ling-3-0-flash-sante-free": { "vercel": "inclusionai/ling-3.0-flash-sante-free" },
12076
11712
  "ling-3-0-flash-vl": { "vercel": "inclusionai/ling-3.0-flash-vl" },
12077
11713
  "ling-3-1-flash": { "vercel": "inclusionai/ling-3.1-flash" },
12078
11714
  "ling-3-1-flash-free": { "vercel": "inclusionai/ling-3.1-flash-free" },
12079
11715
  "ling-3.0-flash": { "vercel": "inclusionai/ling-3.0-flash" },
12080
11716
  "ling-3.0-flash-sante": { "vercel": "inclusionai/ling-3.0-flash-sante" },
12081
- "ling-3.0-flash-sante-free": { "vercel": "inclusionai/ling-3.0-flash-sante-free" },
12082
11717
  "ling-3.0-flash-vl": { "vercel": "inclusionai/ling-3.0-flash-vl" },
12083
11718
  "ling-3.1-flash": { "vercel": "inclusionai/ling-3.1-flash" },
12084
11719
  "ling-3.1-flash-free": { "vercel": "inclusionai/ling-3.1-flash-free" },
@@ -12117,6 +11752,7 @@ const MODEL_FAMILY_PROVIDER_IDS = {
12117
11752
  "ministral-3b": { "vercel": "mistral/ministral-3b" },
12118
11753
  "ministral-8b": { "vercel": "mistral/ministral-8b" },
12119
11754
  "mistral-large-3": { "vercel": "mistral/mistral-large-3" },
11755
+ "mistral-large-4": { "vercel": "mistral/mistral-large-4" },
12120
11756
  "mistral-medium-3-5": { "vercel": "mistral/mistral-medium-3.5" },
12121
11757
  "mistral-medium-3.5": { "vercel": "mistral/mistral-medium-3.5" },
12122
11758
  "mistral-small": { "vercel": "mistral/mistral-small" },
@@ -12228,9 +11864,7 @@ const MODEL_FAMILY_PROVIDER_IDS = {
12228
11864
  "seed-2-1-turbo": { "vercel": "bytedance/seed-2.1-turbo" },
12229
11865
  "seed-2.1-turbo": { "vercel": "bytedance/seed-2.1-turbo" },
12230
11866
  "sonar": { "vercel": "perplexity/sonar" },
12231
- "step-3-5-flash": { "vercel": "stepfun/step-3.5-flash" },
12232
11867
  "step-3-7-flash": { "vercel": "stepfun/step-3.7-flash" },
12233
- "step-3.5-flash": { "vercel": "stepfun/step-3.5-flash" },
12234
11868
  "step-3.7-flash": { "vercel": "stepfun/step-3.7-flash" },
12235
11869
  "step-5": { "vercel": "stepfun/step-5-preview" },
12236
11870
  "step-5-preview": { "vercel": "stepfun/step-5-preview" },
@@ -12322,7 +11956,10 @@ const QWEN_3_8_27B_IDS = [
12322
11956
  "claude-sonnet-5",
12323
11957
  "anthropic/claude-sonnet-5",
12324
11958
  "claude-haiku-4.5",
12325
- "anthropic/claude-haiku-4.5"
11959
+ "anthropic/claude-haiku-4.5",
11960
+ "claude-haiku-5-5",
11961
+ "claude-haiku-5.5",
11962
+ "anthropic/claude-haiku-5.5"
12326
11963
  ]) }), {
12327
11964
  ...workersAiPrimary(DEEPSEEK_V4_FLASH_IDS),
12328
11965
  ...vercelPrimary(QWEN_3_8_27B_IDS, ["mixlayer"]),
@@ -12385,6 +12022,14 @@ const MANUAL_PROVIDER_MAP_OVERRIDES = {
12385
12022
  "vercel": "anthropic/claude-haiku-4.5",
12386
12023
  "anthropic": "claude-haiku-4-5-20251001"
12387
12024
  },
12025
+ "claude-haiku-5.5": {
12026
+ "vercel": "anthropic/claude-haiku-5.5",
12027
+ "anthropic": "claude-haiku-5-5"
12028
+ },
12029
+ "anthropic/claude-haiku-5.5": {
12030
+ "vercel": "anthropic/claude-haiku-5.5",
12031
+ "anthropic": "claude-haiku-5-5"
12032
+ },
12388
12033
  ...workersAiAliases("@cf/zai-org/glm-5.3-flash", GLM_5_3_FLASH_IDS),
12389
12034
  "qwen/qwen3.8-27b": {
12390
12035
  "mixlayer": "qwen/qwen3.8-27b",
@@ -12495,7 +12140,7 @@ const CREATOR_TO_PROVIDER = {
12495
12140
  "unknown": "openai"
12496
12141
  };
12497
12142
  //#endregion
12498
- //#region ../shared/dist/chunk-EZRDnysg.mjs
12143
+ //#region ../shared/dist/chunk-ykhTBHKy.mjs
12499
12144
  /**
12500
12145
  * The reserved model-facing name of the agent state write tool
12501
12146
  * (`packages/runtime/src/engine/state-write-tool.ts`). The model is told to call
@@ -12899,7 +12544,7 @@ function durableCrawlBudget(plan) {
12899
12544
  };
12900
12545
  }
12901
12546
  //#endregion
12902
- //#region ../shared/dist/chunk-2PJvKrV8.mjs
12547
+ //#region ../shared/dist/chunk-BcuKxfZa.mjs
12903
12548
  /**
12904
12549
  * Unwraps a single {{variableName}} template to its bare variable name, preserving array/object identity instead of stringifying via the template engine.
12905
12550
  * Returns source unchanged when it is not a lone {{...}} wrapper.
@@ -13005,6 +12650,14 @@ strictObject({
13005
12650
  required: array(fieldNameSchema).optional(),
13006
12651
  additionalProperties: boolean().optional()
13007
12652
  }).superRefine((schema, ctx) => addSemanticIssues(schema, ctx));
12653
+ //#endregion
12654
+ //#region ../shared/dist/chunk-DoQn4_pS.mjs
12655
+ /** The Tenancy Assurance Levels, in the order `tenancyAssuranceLevelSchema` is built from. */
12656
+ const TENANCY_ASSURANCE_LEVELS = [
12657
+ "anonymous",
12658
+ "asserted",
12659
+ "verified"
12660
+ ];
13008
12661
  const FLOW_STEP_TYPES = ["prompt", ...[
13009
12662
  "crawl",
13010
12663
  "fetch-url",
@@ -13737,19 +13390,68 @@ object({
13737
13390
  model: string().min(1).optional(),
13738
13391
  calibration: judgmentCalibrationSchema.optional()
13739
13392
  });
13740
- Object.freeze({ "typesafe-ai/jev": Object.freeze({
13741
- id: "typesafe-ai/jev",
13742
- name: "Jev",
13743
- description: "TypeSafe AI System One evaluation model. Accepts shared state and typed questions, returning choices, scores, and boolean probabilities. Limits: 64,000 tokens total per request; 32,000 tokens for the state plus the longest question. No streaming or output-token limit.",
13744
- contextWindow: 32e3,
13745
- supportedSpecifications: Object.freeze(["v4"]),
13746
- pricing: Object.freeze({
13747
- inputPerToken: "0.000000042",
13748
- outputPerToken: "0",
13749
- inputPer1MTokens: .042,
13750
- outputPer1MTokens: 0
13393
+ Object.freeze({
13394
+ "convaiinnovations/laya": Object.freeze({
13395
+ id: "convaiinnovations/laya",
13396
+ name: "Laya",
13397
+ description: "Laya is Convai Innovations’ System One evaluation model for fast, structured decisions. It evaluates shared state against choice, score, and yes/no questions and returns typed answers with probabilities in a single forward pass. It supports classification, routing, guardrail checks, and rubric-based assessment without generating free-form text.",
13398
+ contextWindow: 8192,
13399
+ supportedSpecifications: Object.freeze(["v4"]),
13400
+ pricing: Object.freeze({
13401
+ inputPerToken: "0",
13402
+ outputPerToken: "0",
13403
+ inputPer1MTokens: 0,
13404
+ outputPer1MTokens: 0
13405
+ })
13406
+ }),
13407
+ "convaiinnovations/laya-free": Object.freeze({
13408
+ id: "convaiinnovations/laya-free",
13409
+ name: "Laya (Free)",
13410
+ description: "Laya is Convai Innovations’ System One evaluation model for fast, structured decisions. It evaluates shared state against choice, score, and yes/no questions and returns typed answers with probabilities in a single forward pass. It supports classification, routing, guardrail checks, and rubric-based assessment without generating free-form text.",
13411
+ contextWindow: 8192,
13412
+ supportedSpecifications: Object.freeze(["v4"]),
13413
+ pricing: Object.freeze({
13414
+ inputPerToken: "0",
13415
+ outputPerToken: "0",
13416
+ inputPer1MTokens: 0,
13417
+ outputPer1MTokens: 0
13418
+ })
13419
+ }),
13420
+ "liquid/d1": Object.freeze({
13421
+ id: "liquid/d1",
13422
+ name: "Liquid d1",
13423
+ description: "Liquid d1 is a decision model for classification, routing, and scoring. It evaluates shared state against typed Choice, Score, and yes/no questions and returns structured answers with calibrated probabilities through the TypeSafe-compatible System One API.",
13424
+ contextWindow: 65536,
13425
+ supportedSpecifications: Object.freeze(["v4"]),
13426
+ pricing: Object.freeze({
13427
+ inputPerToken: "0.00000004",
13428
+ outputPerToken: "0",
13429
+ inputPer1MTokens: .04,
13430
+ outputPer1MTokens: 0
13431
+ })
13432
+ }),
13433
+ "openai/gpt-6-luna-decisions": Object.freeze({
13434
+ id: "openai/gpt-6-luna-decisions",
13435
+ name: "GPT-6 Luna Decisions",
13436
+ description: "GPT-6 Luna Decisions evaluates classification and scoring questions against shared text and image input using OpenAI's Decisions API. It supports predicate, choice, and score questions with probabilities and confidence values.",
13437
+ contextWindow: 105e4,
13438
+ supportedSpecifications: Object.freeze(["v4"]),
13439
+ pricing: null
13440
+ }),
13441
+ "typesafe-ai/jev": Object.freeze({
13442
+ id: "typesafe-ai/jev",
13443
+ name: "Jev",
13444
+ description: "TypeSafe AI System One decision model. Accepts shared state and typed questions, returning choices, scores, and boolean probabilities. Limits: 64,000 tokens total per request; 32,000 tokens for the state plus the longest question. No streaming or output-token limit.",
13445
+ contextWindow: 32e3,
13446
+ supportedSpecifications: Object.freeze(["v4"]),
13447
+ pricing: Object.freeze({
13448
+ inputPerToken: "0.000000042",
13449
+ outputPerToken: "0",
13450
+ inputPer1MTokens: .042,
13451
+ outputPer1MTokens: 0
13452
+ })
13751
13453
  })
13752
- }) });
13454
+ });
13753
13455
  //#endregion
13754
13456
  //#region ../shared/dist/chunk-CIi5jW2t.mjs
13755
13457
  /**
@@ -15266,11 +14968,7 @@ const agentStateChannelSchema = object({
15266
14968
  * Tenant vs End User). The vocabulary the trust boundary grades into and a
15267
14969
  * strategy's assurance floor is expressed in. `CONTEXT.md` → "Assurance level".
15268
14970
  */
15269
- const tenancyAssuranceLevelSchema = _enum([
15270
- "anonymous",
15271
- "asserted",
15272
- "verified"
15273
- ]);
14971
+ const tenancyAssuranceLevelSchema = _enum(TENANCY_ASSURANCE_LEVELS);
15274
14972
  /**
15275
14973
  * Named archetype preset for a Tenancy Strategy (ADR 0012): the elicitable, auditable unit.
15276
14974
  * internal: Builder-Org internal tooling, product-scoped data, no tenant/end-user required.
@@ -23079,6 +22777,7 @@ const agentExternalConfigSchema = object({
23079
22777
  auth: record(string(), any()).optional(),
23080
22778
  timeout: number$1().positive().optional(),
23081
22779
  retryCount: number$1().int().nonnegative().optional(),
22780
+ identityAssertion: boolean().optional(),
23082
22781
  skillOrchestration: union([_enum(["delegate", "managed"]), boolean()]).optional()
23083
22782
  });
23084
22783
  /**
@@ -25106,7 +24805,7 @@ const AgentInputSchema = object({
25106
24805
  durability: agentDurabilityConfigSchema.optional(),
25107
24806
  tenancyStrategy: agentRuntimeConfigSchema.shape.tenancyStrategy
25108
24807
  }).superRefine((val, ctx) => {
25109
- const unsupported = val.tenancyStrategy !== void 0 ? [["tenancyStrategy"], "tenancyStrategy is server-authored; save it on an agent"] : val.voice !== void 0 ? [["voice"], "voice is unsupported by agent dispatch"] : Object.keys(val.temporal ?? {}).some((key) => key !== "timezone") ? [["temporal"], "Only temporal.timezone is supported by agent dispatch"] : void 0;
24808
+ const unsupported = val.tenancyStrategy !== void 0 ? [["tenancyStrategy"], "tenancyStrategy is server-authored; save it on an agent"] : val.voice !== void 0 ? [["voice"], "voice is unsupported by agent dispatch"] : void 0;
25110
24809
  if (unsupported) {
25111
24810
  const [path, message] = unsupported;
25112
24811
  ctx.addIssue({
@@ -25973,7 +25672,10 @@ object({
25973
25672
  metering: _enum(["active", "legacy-untracked"]),
25974
25673
  sleepPolicy: sandboxDeploySleepPolicySchema.optional(),
25975
25674
  autoStopInterval: number$1().optional(),
25976
- autoDeleteInterval: number$1().optional()
25675
+ autoDeleteInterval: number$1().optional(),
25676
+ containerEpoch: number$1().int().optional().describe("Runtype Sandbox: the container epoch the preview URL is bound to; a new epoch mints a new URL."),
25677
+ freshContainer: boolean().optional().describe("Runtype Sandbox: true when this deploy started a new container rather than reusing a running one."),
25678
+ previewUrlChanged: boolean().optional().describe("Runtype Sandbox: true when the preview URL differs from the one the previous successful deploy to this sandbox returned. Re-register any saved copy of the old URL. False for a first deploy.")
25977
25679
  });
25978
25680
  Object.freeze({
25979
25681
  /** Automatic attempts per incident, counted in OPERATIONS rather than requests. */
@@ -26508,97 +26210,550 @@ object({
26508
26210
  diagnosticId: string().max(64)
26509
26211
  });
26510
26212
  //#endregion
26511
- //#region src/auth/credential-store.ts
26512
- var credential_store_exports = /* @__PURE__ */ __exportAll({ CredentialStore: () => CredentialStore });
26513
- const CREDENTIALS_DIR = path$1.join(os.homedir(), ".runtype");
26514
- const CREDENTIALS_PATH = path$1.join(CREDENTIALS_DIR, "credentials.json");
26515
- function createConf(encryptionKey) {
26516
- return new Conf({
26517
- projectName: "runtype-cli",
26518
- projectSuffix: "",
26519
- configName: "credentials",
26520
- encryptionKey,
26521
- cwd: CREDENTIALS_DIR
26522
- });
26213
+ //#region src/config/paths.ts
26214
+ /** Root state directory: RUNTYPE_STATE_DIR or ~/.runtype */
26215
+ function getRuntypeHomeDir() {
26216
+ return process.env.RUNTYPE_STATE_DIR || path$2.join(os$1.homedir(), ".runtype");
26523
26217
  }
26524
- function isCorruptStoreError$1(error) {
26525
- if (error instanceof SyntaxError) return true;
26526
- const message = error instanceof Error ? error.message : String(error);
26527
- return typeof message === "string" && message.includes("JSON");
26218
+ /** Per-project state dir: ~/.runtype/projects/{hash}/ */
26219
+ function getProjectStateDir(projectDir) {
26220
+ const dir = projectDir || process.cwd();
26221
+ const hash = crypto$3.createHash("sha256").update(dir).digest("hex").slice(0, 12);
26222
+ const stateDir = path$2.join(getRuntypeHomeDir(), "projects", hash);
26223
+ const breadcrumb = path$2.join(stateDir, "project-path.txt");
26224
+ if (!fs$1.existsSync(breadcrumb)) {
26225
+ fs$1.mkdirSync(stateDir, { recursive: true });
26226
+ fs$1.writeFileSync(breadcrumb, dir);
26227
+ }
26228
+ return stateDir;
26528
26229
  }
26529
- var CredentialStore = class {
26530
- config;
26531
- encryptionKey;
26532
- constructor() {
26533
- this.encryptionKey = this.getMachineKey();
26230
+ /** Marathon state dir: ~/.runtype/projects/{hash}/marathons/ */
26231
+ function getMarathonStateDir(projectDir) {
26232
+ return path$2.join(getProjectStateDir(projectDir), "marathons");
26233
+ }
26234
+ //#endregion
26235
+ //#region src/auth/secret-backend.ts
26236
+ const KEYCHAIN_SERVICE = "runtype-cli";
26237
+ let keyringModule;
26238
+ /** Load the native keyring binding lazily; a missing prebuilt binary means "no keychain", not a crash. */
26239
+ function loadKeyring() {
26240
+ keyringModule ??= import("@napi-rs/keyring").then((mod) => mod.AsyncEntry).catch(() => null);
26241
+ return keyringModule;
26242
+ }
26243
+ var KeychainSecretBackend = class {
26244
+ kind = "keychain";
26245
+ async entry(name) {
26246
+ const AsyncEntry = await loadKeyring();
26247
+ if (!AsyncEntry) throw new Error("OS keychain is not available on this platform.");
26248
+ return new AsyncEntry(KEYCHAIN_SERVICE, name);
26249
+ }
26250
+ async get(name) {
26251
+ return await (await this.entry(name)).getPassword() ?? null;
26252
+ }
26253
+ async set(name, value) {
26254
+ await (await this.entry(name)).setPassword(value);
26255
+ }
26256
+ async delete(name) {
26257
+ await (await this.entry(name)).deleteCredential();
26258
+ }
26259
+ };
26260
+ /**
26261
+ * Fallback for hosts with no usable OS keychain (headless Linux, containers, CI).
26262
+ * Values are AES-256-CBC encrypted with a host/user-derived key: obfuscation at rest, not secrecy
26263
+ * from someone who can read the file. The file is written 0600 inside a 0700 directory.
26264
+ */
26265
+ var FileSecretBackend = class {
26266
+ filePath;
26267
+ kind = "file";
26268
+ constructor(filePath = path.join(getRuntypeHomeDir(), "secrets.json")) {
26269
+ this.filePath = filePath;
26270
+ }
26271
+ async get(name) {
26272
+ const encrypted = this.read()[name];
26273
+ return encrypted ? decryptWithMachineKey(encrypted) : null;
26274
+ }
26275
+ async set(name, value) {
26276
+ this.write({
26277
+ ...this.read(),
26278
+ [name]: encryptWithMachineKey(value)
26279
+ });
26280
+ }
26281
+ async delete(name) {
26282
+ const secrets = this.read();
26283
+ if (!(name in secrets)) return;
26284
+ delete secrets[name];
26285
+ this.write(secrets);
26286
+ }
26287
+ read() {
26534
26288
  try {
26535
- this.config = createConf(this.encryptionKey);
26536
- } catch (error) {
26537
- if (isCorruptStoreError$1(error)) {
26538
- try {
26539
- if (fs$1.existsSync(CREDENTIALS_PATH)) fs$1.unlinkSync(CREDENTIALS_PATH);
26540
- } catch {}
26541
- this.config = createConf(this.encryptionKey);
26542
- } else throw error;
26289
+ const parsed = JSON.parse(fs.readFileSync(this.filePath, "utf8"));
26290
+ return parsed && typeof parsed === "object" ? parsed : {};
26291
+ } catch {
26292
+ return {};
26543
26293
  }
26544
26294
  }
26545
- /**
26546
- * Replace the entire backing store in one file write.
26547
- * Conf's store setter is typed read-only, hence the cast to assign it.
26548
- * Single-write replacement is load-bearing: a multi-write sequence could lose the only copy of a fresh credential after the server revokes the previous key.
26549
- */
26550
- replaceStore(data) {
26551
- this.config.store = data;
26295
+ write(secrets) {
26296
+ writePrivateJsonFile(this.filePath, secrets);
26297
+ }
26298
+ };
26299
+ /** Write JSON atomically (temp file + rename) with owner-only permissions. */
26300
+ function writePrivateJsonFile(filePath, data) {
26301
+ const directory = path.dirname(filePath);
26302
+ fs.mkdirSync(directory, {
26303
+ recursive: true,
26304
+ mode: 448
26305
+ });
26306
+ const temporaryPath = path.join(directory, `.${path.basename(filePath)}.${crypto$1.randomUUID()}.tmp`);
26307
+ try {
26308
+ fs.writeFileSync(temporaryPath, `${JSON.stringify(data, null, 2)}\n`, { mode: 384 });
26309
+ fs.renameSync(temporaryPath, filePath);
26310
+ } finally {
26311
+ fs.rmSync(temporaryPath, { force: true });
26312
+ }
26313
+ }
26314
+ function getMachineKey() {
26315
+ return crypto$1.createHash("sha256").update(`runtype-cli-${os.hostname()}-${os.userInfo().username}`).digest("hex").substring(0, 32);
26316
+ }
26317
+ function encryptWithMachineKey(text) {
26318
+ const iv = crypto$1.randomBytes(16);
26319
+ const cipher = crypto$1.createCipheriv("aes-256-cbc", Buffer.from(getMachineKey()), iv);
26320
+ return `${iv.toString("hex")}:${cipher.update(text, "utf8", "hex")}${cipher.final("hex")}`;
26321
+ }
26322
+ function decryptWithMachineKey(text) {
26323
+ const [ivHex, encrypted] = text.split(":");
26324
+ const decipher = crypto$1.createDecipheriv("aes-256-cbc", Buffer.from(getMachineKey()), Buffer.from(ivHex, "hex"));
26325
+ return `${decipher.update(encrypted, "hex", "utf8")}${decipher.final("utf8")}`;
26326
+ }
26327
+ /** `RUNTYPE_CREDENTIAL_STORE=file|keychain` pins the backend; anything else is `auto`. */
26328
+ function getSecretStorePreference() {
26329
+ const value = process.env.RUNTYPE_CREDENTIAL_STORE?.trim().toLowerCase();
26330
+ return value === "file" || value === "keychain" ? value : "auto";
26331
+ }
26332
+ //#endregion
26333
+ //#region src/auth/account-store.ts
26334
+ const LAST_USED_RESOLUTION_MS = 6e4;
26335
+ /** Canonical API base used for account identity; mirrors normalizeApiUrl for valid URLs. */
26336
+ function canonicalApiUrl(value) {
26337
+ try {
26338
+ return new URL(value).toString().replace(/\/+$/, "");
26339
+ } catch {
26340
+ return value.replace(/\/+$/, "");
26341
+ }
26342
+ }
26343
+ function accountIdFor(apiUrl, orgId, userId) {
26344
+ return `${canonicalApiUrl(apiUrl)}#${orgId ?? `user:${userId}`}`;
26345
+ }
26346
+ function recency(account) {
26347
+ return Date.parse(account.lastUsedAt ?? account.createdAt) || 0;
26348
+ }
26349
+ function mostRecent(accounts) {
26350
+ return [...accounts].sort((a, b) => recency(b) - recency(a))[0] ?? null;
26351
+ }
26352
+ /**
26353
+ * Pick the stored login for an invocation. A requested organization only ever matches that
26354
+ * organization: a project linked to org A must never silently run with org B's credential.
26355
+ * Without one, the default login wins when it targets the explicit API URL (if any).
26356
+ */
26357
+ function selectAccount(accounts, options) {
26358
+ const apiUrl = options.apiUrl ? canonicalApiUrl(options.apiUrl) : void 0;
26359
+ const onTarget = apiUrl ? accounts.filter((account) => account.apiUrl === apiUrl) : accounts;
26360
+ if (options.orgId) return mostRecent(onTarget.filter((account) => account.orgId === options.orgId));
26361
+ return onTarget.find((account) => account.id === options.defaultAccountId) ?? mostRecent(onTarget);
26362
+ }
26363
+ const LOCK_STALE_MS = 3e4;
26364
+ const LOCK_TIMEOUT_MS = 1e4;
26365
+ const heldLocks = new AsyncLocalStorage();
26366
+ /** Multi-account login store: one org-scoped credential per (API URL, organization). */
26367
+ var AccountStore = class {
26368
+ filePath;
26369
+ lockPath;
26370
+ keychain;
26371
+ fileBackend;
26372
+ legacyCredentialsDir;
26373
+ secretPrefix;
26374
+ constructor(options = {}) {
26375
+ const homeDir = path.resolve(options.homeDir ?? getRuntypeHomeDir());
26376
+ this.filePath = path.join(homeDir, "accounts.json");
26377
+ this.lockPath = `${this.filePath}.lock`;
26378
+ this.keychain = options.keychain ?? new KeychainSecretBackend();
26379
+ this.fileBackend = options.file ?? new FileSecretBackend(path.join(homeDir, "secrets.json"));
26380
+ const defaultHome = path.join(os.homedir(), ".runtype");
26381
+ const isDefaultHome = homeDir === defaultHome;
26382
+ this.legacyCredentialsDir = options.legacyCredentialsDir ?? (isDefaultHome ? defaultHome : null);
26383
+ this.secretPrefix = isDefaultHome ? "" : `${crypto$1.createHash("sha256").update(homeDir).digest("hex").slice(0, 12)}/`;
26384
+ }
26385
+ async list() {
26386
+ return (await this.load()).accounts;
26387
+ }
26388
+ async get(id) {
26389
+ return (await this.list()).find((account) => account.id === id) ?? null;
26390
+ }
26391
+ async getDefault() {
26392
+ const data = await this.load();
26393
+ return data.accounts.find((account) => account.id === data.defaultAccountId) ?? null;
26394
+ }
26395
+ async setDefault(id) {
26396
+ await this.withLock(async () => {
26397
+ const data = await this.load();
26398
+ if (id !== null && !data.accounts.some((account) => account.id === id)) throw new Error(`No stored login with id ${id}.`);
26399
+ this.persist({
26400
+ ...data,
26401
+ defaultAccountId: id
26402
+ });
26403
+ });
26404
+ }
26405
+ /** Store (or replace) the login for this credential's org, secret first so metadata never points at nothing. */
26406
+ async save(input, options = {}) {
26407
+ return this.withLock(async () => {
26408
+ const apiUrl = canonicalApiUrl(input.apiUrl);
26409
+ const orgId = input.orgId ?? null;
26410
+ const id = accountIdFor(apiUrl, orgId, input.userId);
26411
+ const backend = await this.writeSecret(this.apiKeySecretName(id), input.apiKey);
26412
+ const { claim: claimInput } = options;
26413
+ let claim;
26414
+ if (claimInput) {
26415
+ const { claimToken, ...claimMetadata } = claimInput;
26416
+ await backend.set(this.claimTokenSecretName(id), claimToken);
26417
+ claim = claimMetadata;
26418
+ } else await this.deleteSecret(backend, this.claimTokenSecretName(id));
26419
+ const data = await this.load();
26420
+ const previous = data.accounts.find((account) => account.id === id);
26421
+ if (previous && previous.secretStore !== backend.kind) {
26422
+ await this.deleteSecret(this.backendFor(previous), this.apiKeySecretName(id));
26423
+ await this.deleteSecret(this.backendFor(previous), this.claimTokenSecretName(id));
26424
+ }
26425
+ const now = (/* @__PURE__ */ new Date()).toISOString();
26426
+ const account = {
26427
+ id,
26428
+ apiUrl,
26429
+ orgId,
26430
+ userId: input.userId,
26431
+ secretStore: backend.kind,
26432
+ createdAt: now,
26433
+ lastUsedAt: now,
26434
+ ...claim ? { claim } : {}
26435
+ };
26436
+ const accounts = [...data.accounts.filter((existing) => existing.id !== id), account];
26437
+ const makeDefault = options.makeDefault ?? !data.defaultAccountId;
26438
+ this.persist({
26439
+ version: 1,
26440
+ accounts,
26441
+ defaultAccountId: makeDefault ? id : data.defaultAccountId
26442
+ });
26443
+ return account;
26444
+ });
26445
+ }
26446
+ async getApiKey(account) {
26447
+ return this.backendFor(account).get(this.apiKeySecretName(account.id));
26448
+ }
26449
+ async getClaimToken(account) {
26450
+ if (!account.claim) return null;
26451
+ return this.backendFor(account).get(this.claimTokenSecretName(account.id));
26452
+ }
26453
+ async setClaim(id, claim) {
26454
+ await this.withLock(async () => {
26455
+ const data = await this.load();
26456
+ const account = data.accounts.find((existing) => existing.id === id);
26457
+ if (!account) throw new Error(`No stored login with id ${id}.`);
26458
+ const backend = this.backendFor(account);
26459
+ let nextClaim;
26460
+ if (claim) {
26461
+ const { claimToken, ...metadata } = claim;
26462
+ if (claimToken) await backend.set(this.claimTokenSecretName(id), claimToken);
26463
+ nextClaim = metadata;
26464
+ } else await this.deleteSecret(backend, this.claimTokenSecretName(id));
26465
+ const { claim: _previousClaim, ...rest } = account;
26466
+ const updated = nextClaim ? {
26467
+ ...rest,
26468
+ claim: nextClaim
26469
+ } : rest;
26470
+ this.persist({
26471
+ ...data,
26472
+ accounts: data.accounts.map((existing) => existing.id === id ? updated : existing)
26473
+ });
26474
+ });
26475
+ }
26476
+ async remove(id) {
26477
+ return this.withLock(async () => {
26478
+ const data = await this.load();
26479
+ const account = data.accounts.find((existing) => existing.id === id);
26480
+ if (!account) return false;
26481
+ const backend = this.backendFor(account);
26482
+ await this.deleteSecret(backend, this.apiKeySecretName(id));
26483
+ await this.deleteSecret(backend, this.claimTokenSecretName(id));
26484
+ this.persist({
26485
+ version: 1,
26486
+ accounts: data.accounts.filter((existing) => existing.id !== id),
26487
+ defaultAccountId: data.defaultAccountId === id ? null : data.defaultAccountId
26488
+ });
26489
+ return true;
26490
+ });
26552
26491
  }
26553
- getMachineKey() {
26554
- const hostname = os.hostname();
26555
- const username = os.userInfo().username;
26556
- return crypto$1.createHash("sha256").update(`runtype-cli-${hostname}-${username}`).digest("hex").substring(0, 32);
26492
+ async touch(id) {
26493
+ const current = (await this.load()).accounts.find((account) => account.id === id);
26494
+ if (!current) return;
26495
+ const last = current.lastUsedAt ? Date.parse(current.lastUsedAt) : 0;
26496
+ if (Date.now() - last < LAST_USED_RESOLUTION_MS) return;
26497
+ await this.withLock(async () => {
26498
+ const data = await this.load();
26499
+ const account = data.accounts.find((existing) => existing.id === id);
26500
+ if (!account) return;
26501
+ account.lastUsedAt = (/* @__PURE__ */ new Date()).toISOString();
26502
+ this.persist(data);
26503
+ });
26504
+ }
26505
+ apiKeySecretName(accountId) {
26506
+ return `${this.secretPrefix}${accountId}:api-key`;
26507
+ }
26508
+ claimTokenSecretName(accountId) {
26509
+ return `${this.secretPrefix}${accountId}:claim-token`;
26557
26510
  }
26558
26511
  /**
26559
- * Store credentials, optionally together with agent-auth claim state, in one write.
26560
- * Passing claimState here keeps the credential and claim token atomic, avoiding an orphaned pre-claim key.
26561
- * Storing credentials WITHOUT claimState invalidates any in-flight claim ceremony.
26512
+ * Serialize read-modify-write of accounts.json (and secrets.json) across CLI processes with an
26513
+ * exclusive lock file. Re-entrant along one async call chain so a locked operation may call another.
26562
26514
  */
26563
- async saveCredentials(credentials, claimState) {
26564
- const configData = {
26565
- apiKey: this.encrypt(credentials.apiKey),
26566
- userId: credentials.userId,
26567
- createdAt: (/* @__PURE__ */ new Date()).toISOString(),
26568
- lastUsed: (/* @__PURE__ */ new Date()).toISOString()
26515
+ async withLock(operation) {
26516
+ const held = heldLocks.getStore() ?? /* @__PURE__ */ new Set();
26517
+ if (held.has(this.lockPath)) return operation();
26518
+ fs.mkdirSync(path.dirname(this.lockPath), {
26519
+ recursive: true,
26520
+ mode: 448
26521
+ });
26522
+ const deadline = Date.now() + LOCK_TIMEOUT_MS;
26523
+ for (;;) try {
26524
+ fs.closeSync(fs.openSync(this.lockPath, "wx", 384));
26525
+ break;
26526
+ } catch (error) {
26527
+ if (error.code !== "EEXIST") throw error;
26528
+ if (this.lockIsStale()) {
26529
+ fs.rmSync(this.lockPath, { force: true });
26530
+ continue;
26531
+ }
26532
+ if (Date.now() > deadline) throw new Error(`Timed out waiting for another runtype process to finish updating logins (${this.lockPath}).`, { cause: error });
26533
+ await new Promise((resolve) => setTimeout(resolve, 25 + Math.random() * 50));
26534
+ }
26535
+ try {
26536
+ return await heldLocks.run(/* @__PURE__ */ new Set([...held, this.lockPath]), operation);
26537
+ } finally {
26538
+ fs.rmSync(this.lockPath, { force: true });
26539
+ }
26540
+ }
26541
+ lockIsStale() {
26542
+ try {
26543
+ return Date.now() - fs.statSync(this.lockPath).mtimeMs > LOCK_STALE_MS;
26544
+ } catch {
26545
+ return false;
26546
+ }
26547
+ }
26548
+ backendFor(account) {
26549
+ return account.secretStore === "keychain" ? this.keychain : this.fileBackend;
26550
+ }
26551
+ async writeSecret(name, value) {
26552
+ const preference = getSecretStorePreference();
26553
+ if (preference === "file") {
26554
+ await this.fileBackend.set(name, value);
26555
+ return this.fileBackend;
26556
+ }
26557
+ try {
26558
+ await this.keychain.set(name, value);
26559
+ return this.keychain;
26560
+ } catch (error) {
26561
+ if (preference === "keychain") throw error;
26562
+ await this.fileBackend.set(name, value);
26563
+ return this.fileBackend;
26564
+ }
26565
+ }
26566
+ async deleteSecret(backend, name) {
26567
+ try {
26568
+ await backend.delete(name);
26569
+ } catch {}
26570
+ }
26571
+ readFile() {
26572
+ let raw;
26573
+ try {
26574
+ raw = fs.readFileSync(this.filePath, "utf8");
26575
+ } catch {
26576
+ return null;
26577
+ }
26578
+ try {
26579
+ const parsed = JSON.parse(raw);
26580
+ if (parsed?.version === 1 && Array.isArray(parsed.accounts)) return {
26581
+ version: 1,
26582
+ defaultAccountId: parsed.defaultAccountId ?? null,
26583
+ accounts: parsed.accounts
26584
+ };
26585
+ } catch {}
26586
+ return {
26587
+ version: 1,
26588
+ defaultAccountId: null,
26589
+ accounts: []
26569
26590
  };
26570
- if (credentials.orgId !== void 0) configData.orgId = credentials.orgId;
26571
- if (credentials.apiUrl !== void 0) configData.apiUrl = credentials.apiUrl;
26572
- if (claimState) Object.assign(configData, this.claimStateToConfig(claimState));
26573
- this.replaceStore(configData);
26574
- }
26575
- claimStateToConfig(state) {
26576
- const data = {
26577
- claimToken: this.encrypt(state.claimToken),
26578
- registrationId: state.registrationId
26591
+ }
26592
+ async load() {
26593
+ const existing = this.readFile();
26594
+ if (existing) return existing;
26595
+ const empty = {
26596
+ version: 1,
26597
+ defaultAccountId: null,
26598
+ accounts: []
26599
+ };
26600
+ const legacy = this.readLegacyCredentials();
26601
+ if (!legacy) return empty;
26602
+ return this.withLock(async () => {
26603
+ const raced = this.readFile();
26604
+ if (raced) return raced;
26605
+ this.persist(empty);
26606
+ try {
26607
+ await this.save(legacy.account, {
26608
+ claim: legacy.claim,
26609
+ makeDefault: true
26610
+ });
26611
+ } catch {
26612
+ fs.rmSync(this.filePath, { force: true });
26613
+ return empty;
26614
+ }
26615
+ return this.readFile() ?? empty;
26616
+ });
26617
+ }
26618
+ readLegacyCredentials() {
26619
+ if (!this.legacyCredentialsDir) return null;
26620
+ if (!fs.existsSync(path.join(this.legacyCredentialsDir, "credentials.json"))) return null;
26621
+ try {
26622
+ const store = new Conf({
26623
+ projectName: "runtype-cli",
26624
+ projectSuffix: "",
26625
+ configName: "credentials",
26626
+ encryptionKey: getMachineKey(),
26627
+ cwd: this.legacyCredentialsDir
26628
+ }).store;
26629
+ if (!store.apiKey || !store.userId) return null;
26630
+ const apiKey = decryptWithMachineKey(store.apiKey);
26631
+ const claim = store.claimToken && store.registrationId ? {
26632
+ claimToken: decryptWithMachineKey(store.claimToken),
26633
+ registrationId: store.registrationId,
26634
+ claimEmail: store.claimEmail,
26635
+ credentialExpires: store.credentialExpires,
26636
+ claimTokenExpires: store.claimTokenExpires,
26637
+ apiUrl: store.claimApiUrl
26638
+ } : void 0;
26639
+ return {
26640
+ account: {
26641
+ apiKey,
26642
+ userId: store.userId,
26643
+ orgId: store.orgId ?? null,
26644
+ apiUrl: store.apiUrl || "https://api.runtype.com"
26645
+ },
26646
+ claim
26647
+ };
26648
+ } catch {
26649
+ return null;
26650
+ }
26651
+ }
26652
+ persist(data) {
26653
+ writePrivateJsonFile(this.filePath, data);
26654
+ }
26655
+ };
26656
+ //#endregion
26657
+ //#region src/auth/credential-store.ts
26658
+ var credential_store_exports = /* @__PURE__ */ __exportAll({ CredentialStore: () => CredentialStore });
26659
+ /**
26660
+ * Credential access for CLI commands. "Active" methods act on the login this invocation
26661
+ * resolved to (`--org` > `RUNTYPE_ORG` > `.runtype/project.json` > default login); the
26662
+ * account methods manage every stored login. Secrets live in the OS keychain when available.
26663
+ */
26664
+ var CredentialStore = class {
26665
+ accounts;
26666
+ constructor(accounts = new AccountStore()) {
26667
+ this.accounts = accounts;
26668
+ }
26669
+ async resolveCredentials(selection) {
26670
+ const account = selectAccount(await this.accounts.list(), {
26671
+ ...selection,
26672
+ defaultAccountId: (await this.accounts.getDefault())?.id ?? null
26673
+ });
26674
+ if (!account) return null;
26675
+ let apiKey;
26676
+ try {
26677
+ apiKey = await this.accounts.getApiKey(account);
26678
+ } catch (error) {
26679
+ return {
26680
+ account,
26681
+ credentials: null,
26682
+ readError: error instanceof Error ? error.message : String(error)
26683
+ };
26684
+ }
26685
+ if (!apiKey) return {
26686
+ account,
26687
+ credentials: null
26688
+ };
26689
+ await this.accounts.touch(account.id).catch(() => void 0);
26690
+ return {
26691
+ account,
26692
+ credentials: {
26693
+ accountId: account.id,
26694
+ apiKey,
26695
+ userId: account.userId,
26696
+ ...account.orgId ? { orgId: account.orgId } : {},
26697
+ apiUrl: account.apiUrl,
26698
+ createdAt: account.createdAt,
26699
+ lastUsed: account.lastUsedAt
26700
+ }
26579
26701
  };
26580
- if (state.claimEmail !== void 0) data.claimEmail = state.claimEmail;
26581
- if (state.credentialExpires !== void 0) data.credentialExpires = state.credentialExpires;
26582
- if (state.claimTokenExpires !== void 0) data.claimTokenExpires = state.claimTokenExpires;
26583
- if (state.apiUrl !== void 0) data.claimApiUrl = state.apiUrl;
26584
- return data;
26585
26702
  }
26586
26703
  async getApiKey() {
26587
- const encrypted = this.config.get("apiKey");
26588
- if (!encrypted) return null;
26589
- this.config.set("lastUsed", (/* @__PURE__ */ new Date()).toISOString());
26590
- return this.decrypt(encrypted);
26704
+ return (await this.getCredentials())?.apiKey ?? null;
26591
26705
  }
26592
26706
  async getCredentials() {
26593
- const stored = this.config.store;
26594
- if (!stored.apiKey) return null;
26595
- return {
26596
- ...stored,
26597
- apiKey: this.decrypt(stored.apiKey)
26707
+ return (await loadEffectiveContext()).credentials;
26708
+ }
26709
+ async hasCredentials() {
26710
+ return await this.getCredentials() !== null;
26711
+ }
26712
+ /**
26713
+ * Store a login for the credential's organization, replacing any earlier login for the same
26714
+ * (API URL, org). Passing claimState stores the pre-claim credential and its claim token
26715
+ * together; storing without it ends any in-flight claim ceremony for that login.
26716
+ */
26717
+ async saveCredentials(credentials, claimState, options = {}) {
26718
+ const context = await loadEffectiveContext();
26719
+ const input = {
26720
+ apiKey: credentials.apiKey,
26721
+ userId: credentials.userId,
26722
+ orgId: credentials.orgId ?? null,
26723
+ apiUrl: credentials.apiUrl ?? context.apiUrl
26598
26724
  };
26725
+ const hasDefault = await this.accounts.getDefault() !== null;
26726
+ const makeDefault = options.makeDefault ?? (!context.projectLink || !hasDefault);
26727
+ const claim = claimState ? {
26728
+ ...claimState,
26729
+ accountId: void 0
26730
+ } : void 0;
26731
+ const account = await this.accounts.save(input, {
26732
+ claim,
26733
+ makeDefault
26734
+ });
26735
+ resetEffectiveContext();
26736
+ return account;
26599
26737
  }
26738
+ /** Remove the active login (logout); other stored logins are untouched. */
26600
26739
  async clearCredentials() {
26601
- this.config.clear();
26740
+ const active = (await loadEffectiveContext()).account;
26741
+ if (!active) return null;
26742
+ await this.accounts.remove(active.id);
26743
+ resetEffectiveContext();
26744
+ return active;
26745
+ }
26746
+ /** Pending claim on the active login, else the most recent login with one. */
26747
+ async getClaimState() {
26748
+ const account = await this.pendingClaimAccount();
26749
+ if (!account?.claim) return null;
26750
+ const claimToken = await this.accounts.getClaimToken(account);
26751
+ if (!claimToken) return null;
26752
+ return {
26753
+ ...account.claim,
26754
+ claimToken,
26755
+ accountId: account.id
26756
+ };
26602
26757
  }
26603
26758
  /**
26604
26759
  * Persist agent-auth claim ceremony state alongside the pre-claim credential.
@@ -26606,54 +26761,129 @@ var CredentialStore = class {
26606
26761
  * The claim token is encrypted like the API key since it can mint the post-claim credential.
26607
26762
  */
26608
26763
  async saveClaimState(state) {
26609
- this.config.set(this.claimStateToConfig(state));
26610
- }
26611
- async getClaimState() {
26612
- const claimToken = this.config.get("claimToken");
26613
- const registrationId = this.config.get("registrationId");
26614
- if (!claimToken || !registrationId) return null;
26615
- return {
26616
- claimToken: this.decrypt(claimToken),
26617
- registrationId,
26618
- claimEmail: this.config.get("claimEmail"),
26619
- credentialExpires: this.config.get("credentialExpires"),
26620
- claimTokenExpires: this.config.get("claimTokenExpires"),
26621
- apiUrl: this.config.get("claimApiUrl")
26622
- };
26764
+ const { accountId, ...claim } = state;
26765
+ const targetId = accountId ?? (await this.pendingClaimAccount())?.id ?? (await loadEffectiveContext()).account?.id;
26766
+ if (!targetId) throw new Error("No stored login to attach the claim to.");
26767
+ await this.accounts.setClaim(targetId, claim);
26768
+ resetEffectiveContext();
26623
26769
  }
26624
26770
  async clearClaimState() {
26625
- const store = { ...this.config.store };
26626
- delete store.claimToken;
26627
- delete store.registrationId;
26628
- delete store.claimEmail;
26629
- delete store.credentialExpires;
26630
- delete store.claimTokenExpires;
26631
- delete store.claimApiUrl;
26632
- this.replaceStore(store);
26771
+ const account = await this.pendingClaimAccount();
26772
+ if (!account) return;
26773
+ await this.accounts.setClaim(account.id, null);
26774
+ resetEffectiveContext();
26633
26775
  }
26634
- async hasCredentials() {
26635
- return !!this.config.get("apiKey");
26636
- }
26637
- encrypt(text) {
26638
- const algorithm = "aes-256-cbc";
26639
- const key = Buffer.from(this.encryptionKey);
26640
- const iv = crypto$1.randomBytes(16);
26641
- const cipher = crypto$1.createCipheriv(algorithm, key, iv);
26642
- let encrypted = cipher.update(text, "utf8", "hex");
26643
- encrypted += cipher.final("hex");
26644
- return iv.toString("hex") + ":" + encrypted;
26645
- }
26646
- decrypt(text) {
26647
- const algorithm = "aes-256-cbc";
26648
- const key = Buffer.from(this.encryptionKey);
26649
- const [ivHex, encrypted] = text.split(":");
26650
- const iv = Buffer.from(ivHex, "hex");
26651
- const decipher = crypto$1.createDecipheriv(algorithm, key, iv);
26652
- let decrypted = decipher.update(encrypted, "hex", "utf8");
26653
- decrypted += decipher.final("utf8");
26654
- return decrypted;
26776
+ async listAccounts() {
26777
+ return this.accounts.list();
26778
+ }
26779
+ async getDefaultAccount() {
26780
+ return this.accounts.getDefault();
26781
+ }
26782
+ async setDefaultAccount(id) {
26783
+ await this.accounts.setDefault(id);
26784
+ resetEffectiveContext();
26785
+ }
26786
+ async removeAccount(id) {
26787
+ const removed = await this.accounts.remove(id);
26788
+ resetEffectiveContext();
26789
+ return removed;
26790
+ }
26791
+ async pendingClaimAccount() {
26792
+ const active = (await loadEffectiveContext()).account;
26793
+ if (active?.claim) return active;
26794
+ return (await this.accounts.list()).filter((account) => account.claim).sort((a, b) => b.createdAt.localeCompare(a.createdAt))[0] ?? null;
26795
+ }
26796
+ };
26797
+ //#endregion
26798
+ //#region src/config/project-link.ts
26799
+ const PROJECT_LINK_DIR = ".runtype";
26800
+ const PROJECT_LINK_FILENAME = "project.json";
26801
+ var ProjectLinkError = class extends Error {
26802
+ path;
26803
+ constructor(message, path) {
26804
+ super(message);
26805
+ this.path = path;
26806
+ this.name = "ProjectLinkError";
26655
26807
  }
26656
26808
  };
26809
+ const FIRST_PARTY_API_HOST_SUFFIXES = [
26810
+ ".runtype.com",
26811
+ ".runtype-staging.com",
26812
+ ".runtype-preview.com",
26813
+ ".runtype.workers.dev"
26814
+ ];
26815
+ /** A committed link may only pin HTTPS Runtype-operated hosts; local or other APIs need --api-url. */
26816
+ function isFirstPartyApiUrl(value) {
26817
+ let url;
26818
+ try {
26819
+ url = new URL(value);
26820
+ } catch {
26821
+ return false;
26822
+ }
26823
+ const hostname = url.hostname.toLowerCase();
26824
+ return url.protocol === "https:" && (hostname === "runtype.com" || FIRST_PARTY_API_HOST_SUFFIXES.some((suffix) => hostname.endsWith(suffix)));
26825
+ }
26826
+ function projectLinkPath(directory) {
26827
+ return path.join(directory, PROJECT_LINK_DIR, PROJECT_LINK_FILENAME);
26828
+ }
26829
+ function parseProjectLink(raw, filePath) {
26830
+ let parsed;
26831
+ try {
26832
+ parsed = JSON.parse(raw);
26833
+ } catch {
26834
+ throw new ProjectLinkError(`${filePath} is not valid JSON.`, filePath);
26835
+ }
26836
+ const record = parsed && typeof parsed === "object" ? parsed : {};
26837
+ if (typeof record.orgId !== "string" || record.orgId.trim() === "") throw new ProjectLinkError(`${filePath} must contain a non-empty "orgId" string.`, filePath);
26838
+ if (record.apiUrl !== void 0 && typeof record.apiUrl !== "string") throw new ProjectLinkError(`${filePath} has a non-string "apiUrl".`, filePath);
26839
+ return {
26840
+ orgId: record.orgId.trim(),
26841
+ ...typeof record.apiUrl === "string" && record.apiUrl ? { apiUrl: record.apiUrl } : {}
26842
+ };
26843
+ }
26844
+ /** Walk up from `startDir` to the filesystem root and return the nearest link, if any. */
26845
+ function findProjectLink(startDir = process.cwd()) {
26846
+ if (process.env.RUNTYPE_IGNORE_PROJECT_LINK === "1") return null;
26847
+ let directory = path.resolve(startDir);
26848
+ for (;;) {
26849
+ const filePath = projectLinkPath(directory);
26850
+ let raw = null;
26851
+ try {
26852
+ raw = fs.readFileSync(filePath, "utf8");
26853
+ } catch {
26854
+ raw = null;
26855
+ }
26856
+ if (raw !== null) return {
26857
+ ...parseProjectLink(raw, filePath),
26858
+ path: filePath,
26859
+ root: directory
26860
+ };
26861
+ const parent = path.dirname(directory);
26862
+ if (parent === directory) return null;
26863
+ directory = parent;
26864
+ }
26865
+ }
26866
+ function writeProjectLink(directory, link) {
26867
+ const filePath = projectLinkPath(directory);
26868
+ fs.mkdirSync(path.dirname(filePath), { recursive: true });
26869
+ const content = {
26870
+ orgId: link.orgId,
26871
+ ...link.apiUrl ? { apiUrl: link.apiUrl } : {}
26872
+ };
26873
+ fs.writeFileSync(filePath, `${JSON.stringify(content, null, 2)}\n`);
26874
+ return filePath;
26875
+ }
26876
+ /** Remove `project.json`, and the `.runtype/` folder only when nothing else lives in it. */
26877
+ function removeProjectLink(directory) {
26878
+ const filePath = projectLinkPath(directory);
26879
+ if (!fs.existsSync(filePath)) return false;
26880
+ fs.rmSync(filePath);
26881
+ const folder = path.dirname(filePath);
26882
+ try {
26883
+ if (fs.readdirSync(folder).length === 0) fs.rmdirSync(folder);
26884
+ } catch {}
26885
+ return true;
26886
+ }
26657
26887
  //#endregion
26658
26888
  //#region src/config/effective-context.ts
26659
26889
  var CliContextError = class extends Error {
@@ -26674,11 +26904,15 @@ function getCliConfig() {
26674
26904
  projectName: "runtype-cli",
26675
26905
  projectSuffix: "",
26676
26906
  configName: "config",
26677
- cwd: path$1.join(os.homedir(), ".runtype")
26907
+ cwd: getRuntypeHomeDir()
26678
26908
  });
26679
26909
  }
26680
26910
  function setInvocationOptions(options) {
26681
26911
  invocationOptions = options;
26912
+ resetEffectiveContext();
26913
+ }
26914
+ /** Drop the memoized context so the next read sees logins saved or removed in this process. */
26915
+ function resetEffectiveContext() {
26682
26916
  effectiveContext = void 0;
26683
26917
  identityCheck = void 0;
26684
26918
  verifiedIdentity = void 0;
@@ -26697,11 +26931,12 @@ function normalizeApiUrl(value) {
26697
26931
  if (!["http:", "https:"].includes(url.protocol) || url.username || url.password || url.search || url.hash) throw new CliContextError("API URL must use HTTP(S) without credentials, query, or fragment.");
26698
26932
  return url.toString().replace(/\/+$/, "");
26699
26933
  }
26700
- /** API target precedence: explicit flag > environment > saved config > credential endpoint > production. */
26934
+ /** API target precedence: flag > environment > project link > saved config > credential endpoint > production. */
26701
26935
  function resolveApiTarget(values) {
26702
26936
  const [apiUrlSource, value] = [
26703
26937
  ["flag", values.flag],
26704
26938
  ["environment", values.environment],
26939
+ ["project", values.project],
26705
26940
  ["config", values.config],
26706
26941
  ["credential", values.credential],
26707
26942
  ["default", "https://api.runtype.com"]
@@ -26711,36 +26946,83 @@ function resolveApiTarget(values) {
26711
26946
  apiUrlSource
26712
26947
  };
26713
26948
  }
26714
- function resolveTarget(apiUrl, credentials) {
26715
- if (apiUrl && invocationOptions.apiUrl && normalizeApiUrl(apiUrl) !== normalizeApiUrl(invocationOptions.apiUrl)) throw new CliContextError("Conflicting explicit --api-url values; choose one API target.");
26716
- return resolveApiTarget({
26717
- flag: apiUrl || invocationOptions.apiUrl,
26718
- environment: process.env.RUNTYPE_API_URL,
26719
- config: getCliConfig().get("apiUrl"),
26720
- credential: credentials?.apiUrl
26721
- });
26949
+ function readProjectLink() {
26950
+ try {
26951
+ return findProjectLink();
26952
+ } catch (error) {
26953
+ if (error instanceof ProjectLinkError) throw new CliContextError(error.message);
26954
+ throw error;
26955
+ }
26956
+ }
26957
+ function requestedOrganization(link) {
26958
+ if (invocationOptions.org) return {
26959
+ orgId: invocationOptions.org,
26960
+ source: "flag"
26961
+ };
26962
+ const environmentOrg = process.env.RUNTYPE_ORG?.trim();
26963
+ if (environmentOrg) return {
26964
+ orgId: environmentOrg,
26965
+ source: "environment"
26966
+ };
26967
+ if (link) return {
26968
+ orgId: link.orgId,
26969
+ source: "project"
26970
+ };
26971
+ return { source: "default" };
26972
+ }
26973
+ /**
26974
+ * Every invocation-scoped API target source, in precedence order. A project link's apiUrl only
26975
+ * counts when no flag or environment value outranks it, and must name a Runtype-operated host:
26976
+ * the file is committed, so an arbitrary host would receive credentials from anyone running here.
26977
+ */
26978
+ function explicitApiTarget(flag) {
26979
+ const projectLink = readProjectLink();
26980
+ const environment = process.env.RUNTYPE_API_URL;
26981
+ const project = projectLink?.apiUrl;
26982
+ if (projectLink && project && !flag && !environment && !isFirstPartyApiUrl(project)) throw new CliContextError(`${projectLink.path} pins apiUrl ${project}, which is not a Runtype API host. Pass --api-url or set RUNTYPE_API_URL to use it deliberately.`);
26983
+ return {
26984
+ projectLink,
26985
+ target: {
26986
+ flag,
26987
+ environment,
26988
+ project,
26989
+ config: getCliConfig().get("apiUrl")
26990
+ }
26991
+ };
26992
+ }
26993
+ async function buildContext(apiUrlOverride) {
26994
+ if (apiUrlOverride && invocationOptions.apiUrl && normalizeApiUrl(apiUrlOverride) !== normalizeApiUrl(invocationOptions.apiUrl)) throw new CliContextError("Conflicting explicit --api-url values; choose one API target.");
26995
+ const { projectLink, target: explicitTarget } = explicitApiTarget(apiUrlOverride || invocationOptions.apiUrl);
26996
+ const explicitApiUrl = Object.values(explicitTarget).some(Boolean) ? resolveApiTarget(explicitTarget).apiUrl : void 0;
26997
+ const { orgId: requestedOrgId, source: accountSource } = requestedOrganization(projectLink);
26998
+ const envKey = process.env.RUNTYPE_API_KEY;
26999
+ const login = await new CredentialStore().resolveCredentials({
27000
+ orgId: requestedOrgId,
27001
+ apiUrl: explicitApiUrl
27002
+ });
27003
+ const credentials = login?.credentials ?? null;
27004
+ return {
27005
+ ...resolveApiTarget({
27006
+ ...explicitTarget,
27007
+ credential: login?.account.apiUrl
27008
+ }),
27009
+ credentials,
27010
+ account: login?.account ?? null,
27011
+ ...login?.readError ? { credentialError: login.readError } : {},
27012
+ apiKey: envKey || credentials?.apiKey,
27013
+ credentialSource: envKey ? "environment" : credentials ? "stored" : "none",
27014
+ ...requestedOrgId ? { requestedOrgId } : {},
27015
+ accountSource,
27016
+ projectLink
27017
+ };
26722
27018
  }
26723
27019
  function getEffectiveApiUrl() {
26724
- return effectiveContext?.apiUrl ?? resolveTarget().apiUrl;
27020
+ return effectiveContext?.apiUrl ?? resolveApiTarget(explicitApiTarget(invocationOptions.apiUrl).target).apiUrl;
26725
27021
  }
26726
27022
  async function loadEffectiveContext(apiUrl) {
26727
- if (!effectiveContext) {
26728
- const credentials = await new CredentialStore().getCredentials();
26729
- const envKey = process.env.RUNTYPE_API_KEY;
26730
- effectiveContext = {
26731
- ...resolveTarget(apiUrl, credentials),
26732
- credentials,
26733
- apiKey: envKey || credentials?.apiKey,
26734
- credentialSource: envKey ? "environment" : credentials?.apiKey ? "stored" : "none"
26735
- };
26736
- } else if (apiUrl) {
26737
- const target = resolveTarget(apiUrl, effectiveContext.credentials);
26738
- if (target.apiUrl !== effectiveContext.apiUrl) effectiveContext = {
26739
- ...effectiveContext,
26740
- ...target
26741
- };
26742
- else effectiveContext.apiUrlSource = target.apiUrlSource;
26743
- }
27023
+ if (!effectiveContext) effectiveContext = await buildContext(apiUrl);
27024
+ else if (apiUrl && normalizeApiUrl(apiUrl) !== effectiveContext.apiUrl) effectiveContext = await buildContext(apiUrl);
27025
+ else if (apiUrl) effectiveContext.apiUrlSource = "flag";
26744
27026
  return effectiveContext;
26745
27027
  }
26746
27028
  function useStoredCredential(context) {
@@ -26781,7 +27063,11 @@ function redactEffectiveContext(context) {
26781
27063
  apiUrlSource: context.apiUrlSource,
26782
27064
  credentialSource: context.credentialSource,
26783
27065
  orgId: storedOrgApplies ? context.credentials?.orgId ?? null : null,
26784
- orgVerified: false
27066
+ orgVerified: false,
27067
+ accountSource: context.accountSource,
27068
+ requestedOrgId: context.requestedOrgId ?? null,
27069
+ credentialStore: context.credentialSource === "stored" ? context.account?.secretStore ?? null : null,
27070
+ projectLink: context.projectLink?.path ?? null
26785
27071
  };
26786
27072
  }
26787
27073
  //#endregion
@@ -26943,6 +27229,437 @@ var ApiKeyManager = class {
26943
27229
  }
26944
27230
  };
26945
27231
  //#endregion
27232
+ //#region src/auth/callback-server.ts
27233
+ var CallbackServer = class {
27234
+ server = null;
27235
+ app;
27236
+ codeResolve;
27237
+ codeReject;
27238
+ codePromise;
27239
+ expectedState;
27240
+ constructor() {
27241
+ this.app = express();
27242
+ this.codePromise = new Promise((resolve, reject) => {
27243
+ this.codeResolve = resolve;
27244
+ this.codeReject = reject;
27245
+ });
27246
+ this.app.get("/callback", (req, res) => {
27247
+ const { token, code, state, error } = req.query;
27248
+ if (typeof state !== "string" || state !== this.expectedState) {
27249
+ res.status(400).send(this.errorHTML("Invalid authentication state. Continue the login you started in the CLI."));
27250
+ return;
27251
+ }
27252
+ const values = [
27253
+ token,
27254
+ code,
27255
+ error
27256
+ ].filter((value) => value !== void 0);
27257
+ if (values.length !== 1 || typeof values[0] !== "string" || values[0].length === 0) {
27258
+ res.status(400).send(this.errorHTML("Invalid authentication callback"));
27259
+ return;
27260
+ }
27261
+ if (typeof error === "string") {
27262
+ res.send(this.errorHTML(error));
27263
+ this.codeReject(new Error(error));
27264
+ setTimeout(() => this.stop(), 1e3);
27265
+ return;
27266
+ }
27267
+ res.send(this.successHTML());
27268
+ this.codeResolve(values[0]);
27269
+ setTimeout(() => this.stop(), 1e3);
27270
+ });
27271
+ this.app.get("/health", (_req, res) => {
27272
+ res.json({ status: "ok" });
27273
+ });
27274
+ }
27275
+ /** Set expected state parameter for OAuth2 CSRF validation */
27276
+ setExpectedState(state) {
27277
+ if (!state) throw new Error("Authentication requires a nonempty expected state");
27278
+ if (this.server) throw new Error("Expected state must be registered before listening");
27279
+ this.expectedState = state;
27280
+ }
27281
+ async start(port = 8765) {
27282
+ if (!this.expectedState) throw new Error("Authentication requires an expected state before listening");
27283
+ return new Promise((resolveStart, rejectStart) => {
27284
+ this.server = this.app.listen(port, "127.0.0.1", () => {
27285
+ const address = this.server?.address();
27286
+ const actualPort = address && typeof address === "object" && "port" in address ? address.port : port;
27287
+ console.log(`Callback server listening on port ${actualPort}`);
27288
+ resolveStart({
27289
+ actualPort,
27290
+ codePromise: this.codePromise
27291
+ });
27292
+ });
27293
+ this.server.on("error", (error) => {
27294
+ if (error.code === "EADDRINUSE") {
27295
+ console.log(`Port ${port} is in use, trying ${port + 1}...`);
27296
+ this.server = null;
27297
+ this.start(port + 1).then(resolveStart).catch(rejectStart);
27298
+ } else rejectStart(error);
27299
+ });
27300
+ });
27301
+ }
27302
+ stop() {
27303
+ if (this.server) {
27304
+ const s = this.server;
27305
+ this.server = null;
27306
+ s.unref();
27307
+ if (typeof s.closeAllConnections === "function") s.closeAllConnections();
27308
+ s.close();
27309
+ }
27310
+ }
27311
+ successHTML() {
27312
+ return `<!DOCTYPE html>
27313
+ <html lang="en">
27314
+ <head>
27315
+ <meta charset="utf-8">
27316
+ <title>Authentication Successful</title>
27317
+ <style>
27318
+ * { margin: 0; padding: 0; box-sizing: border-box; }
27319
+ body {
27320
+ font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;
27321
+ height: 100vh;
27322
+ overflow: hidden;
27323
+ background: #0a0a0a;
27324
+ }
27325
+ .grid-bg {
27326
+ position: absolute;
27327
+ inset: 0;
27328
+ z-index: 0;
27329
+ overflow: hidden;
27330
+ }
27331
+ .grid-bg .gradient {
27332
+ position: absolute;
27333
+ inset: 0;
27334
+ background: linear-gradient(to bottom, #0a0a0a 0%, #171717 50%, #262626 100%);
27335
+ }
27336
+ @keyframes gridMove {
27337
+ 0% { background-position-y: 0; }
27338
+ 100% { background-position-y: 30px; }
27339
+ }
27340
+ .grid-bg .grid-vertical-wrap {
27341
+ position: absolute;
27342
+ left: 0;
27343
+ right: 0;
27344
+ bottom: 0;
27345
+ top: 40%;
27346
+ transform-origin: top center;
27347
+ transform: perspective(500px) rotateX(60deg);
27348
+ }
27349
+ .grid-bg .grid-vertical-wrap .fade {
27350
+ position: absolute;
27351
+ inset: 0;
27352
+ background: linear-gradient(to bottom, transparent 0%, rgba(255,255,255,0.02) 100%);
27353
+ }
27354
+ .grid-bg .grid-vertical-wrap .lines {
27355
+ position: absolute;
27356
+ inset: 0;
27357
+ background-image: repeating-linear-gradient(
27358
+ 90deg,
27359
+ transparent 0,
27360
+ transparent 49px,
27361
+ rgba(255,255,255,0.08) 49px,
27362
+ rgba(255,255,255,0.08) 50px
27363
+ );
27364
+ background-size: 50px 100%;
27365
+ }
27366
+ .grid-bg .grid-horizontal {
27367
+ position: absolute;
27368
+ left: 0;
27369
+ right: 0;
27370
+ bottom: 0;
27371
+ top: 40%;
27372
+ transform-origin: top center;
27373
+ transform: perspective(500px) rotateX(60deg);
27374
+ background-image: repeating-linear-gradient(
27375
+ 0deg,
27376
+ transparent 0,
27377
+ transparent 29px,
27378
+ rgba(255,255,255,0.06) 29px,
27379
+ rgba(255,255,255,0.06) 30px
27380
+ );
27381
+ background-size: 100% 30px;
27382
+ animation: gridMove 2s linear infinite;
27383
+ }
27384
+ .card-wrap {
27385
+ position: absolute;
27386
+ left: 50%;
27387
+ top: 50%;
27388
+ transform: translate(-50%, -50%);
27389
+ z-index: 10;
27390
+ width: 100%;
27391
+ max-width: 400px;
27392
+ perspective: 1400px;
27393
+ animation: cardIn 0.4s ease-out;
27394
+ }
27395
+ @keyframes cardIn {
27396
+ from { opacity: 0; transform: translate(-50%, -50%) translateY(16px); }
27397
+ to { opacity: 1; transform: translate(-50%, -50%) translateY(0); }
27398
+ }
27399
+ .card-inner {
27400
+ position: relative;
27401
+ }
27402
+ .card-layer-back,
27403
+ .card-layer-mid {
27404
+ position: absolute;
27405
+ inset: 0;
27406
+ border-radius: 12px;
27407
+ border: 1px solid #e5e7eb;
27408
+ pointer-events: none;
27409
+ }
27410
+ .card-layer-back {
27411
+ background: rgba(0, 0, 0, 0.08);
27412
+ transform: translate3d(0, -18px, -36px) rotateX(6deg);
27413
+ }
27414
+ .card-layer-mid {
27415
+ background: rgba(0, 0, 0, 0.05);
27416
+ transform: translate3d(0, -10px, -20px) rotateX(3deg);
27417
+ }
27418
+ .card-front {
27419
+ position: relative;
27420
+ border-radius: 12px;
27421
+ border: 1px solid rgba(255, 255, 255, 1);
27422
+ background: rgba(0, 0, 0, .5);
27423
+ backdrop-filter: blur(10px);
27424
+ -webkit-backdrop-filter: blur(10px);
27425
+ padding: 2rem 2.5rem;
27426
+ text-align: center;
27427
+ box-shadow: 0 1px 3px rgba(0, 0, 0, 0.2);
27428
+ transform: rotateX(-10deg) translateZ(0);
27429
+ transform-style: preserve-3d;
27430
+ }
27431
+ .card-front .section-title {
27432
+ color: #ffffff;
27433
+ font-size: 0.8125rem;
27434
+ font-weight: 600;
27435
+ letter-spacing: 0.05em;
27436
+ text-transform: uppercase;
27437
+ margin-bottom: 1rem;
27438
+ }
27439
+ .card-front .flow-arrow {
27440
+ color: rgba(255, 255, 255, 0.7);
27441
+ font-size: 1rem;
27442
+ margin-bottom: 1rem;
27443
+ line-height: 1;
27444
+ }
27445
+ .card-front .message {
27446
+ color: #ffffff;
27447
+ font-size: 0.75rem;
27448
+ font-weight: 100;
27449
+ margin-bottom: 0.5rem;
27450
+ font-family: monospace;
27451
+ }
27452
+ .card-front .progress {
27453
+ color: rgba(255, 255, 255, 0.75);
27454
+ font-size: 0.8125rem;
27455
+ }
27456
+ </style>
27457
+ </head>
27458
+ <body>
27459
+ <div class="grid-bg">
27460
+ <div class="gradient"></div>
27461
+ <div class="grid-vertical-wrap">
27462
+ <div class="fade"></div>
27463
+ <div class="lines"></div>
27464
+ </div>
27465
+ <div class="grid-horizontal"></div>
27466
+ </div>
27467
+ <div id="card-wrap" class="card-wrap">
27468
+ <div class="card-inner">
27469
+ <div class="card-layer-back" aria-hidden="true"></div>
27470
+ <div class="card-layer-mid" aria-hidden="true"></div>
27471
+ <div id="card" class="card-front">
27472
+ <div class="section-title">Authentication Successful</div>
27473
+ <div class="flow-arrow" aria-hidden="true">↓</div>
27474
+ <p class="message">You can close this window and return to your terminal.</p>
27475
+ <p class="progress" id="progress">Closing automatically in 3 seconds…</p>
27476
+ </div>
27477
+ </div>
27478
+ </div>
27479
+ <script>
27480
+ setTimeout(function() {
27481
+ window.close();
27482
+ var prog = document.getElementById('progress');
27483
+ if (prog) prog.textContent = 'You can now close this window.';
27484
+ var card = document.getElementById('card');
27485
+ if (card) card.innerHTML = '<div class="section-title">Runtype is Ready</div><p class="message">You can now close this window</p>';
27486
+ }, 3000);
27487
+ <\/script>
27488
+ </body>
27489
+ </html>`;
27490
+ }
27491
+ escapeHtml(str) {
27492
+ return str.replace(/&/g, "&amp;").replace(/</g, "&lt;").replace(/>/g, "&gt;").replace(/"/g, "&quot;").replace(/'/g, "&#39;");
27493
+ }
27494
+ errorHTML(error) {
27495
+ return `
27496
+ <!DOCTYPE html>
27497
+ <html>
27498
+ <head>
27499
+ <title>Authentication Failed</title>
27500
+ <style>
27501
+ * {
27502
+ margin: 0;
27503
+ padding: 0;
27504
+ box-sizing: border-box;
27505
+ }
27506
+ body {
27507
+ font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, 'Helvetica Neue', Arial, sans-serif;
27508
+ display: flex;
27509
+ align-items: center;
27510
+ justify-content: center;
27511
+ height: 100vh;
27512
+ background: linear-gradient(135deg, #ef4444 0%, #dc2626 100%);
27513
+ }
27514
+ .container {
27515
+ background: white;
27516
+ padding: 3rem;
27517
+ border-radius: 12px;
27518
+ text-align: center;
27519
+ box-shadow: 0 20px 40px rgba(0,0,0,0.1);
27520
+ max-width: 400px;
27521
+ }
27522
+ .error-icon {
27523
+ color: #ef4444;
27524
+ font-size: 64px;
27525
+ margin-bottom: 1rem;
27526
+ }
27527
+ h1 {
27528
+ color: #1f2937;
27529
+ margin-bottom: 0.5rem;
27530
+ font-size: 1.5rem;
27531
+ }
27532
+ p {
27533
+ color: #6b7280;
27534
+ font-size: 1rem;
27535
+ margin-bottom: 1rem;
27536
+ }
27537
+ .error-details {
27538
+ background: #fef2f2;
27539
+ color: #991b1b;
27540
+ padding: 0.75rem;
27541
+ border-radius: 6px;
27542
+ font-size: 0.875rem;
27543
+ font-family: monospace;
27544
+ }
27545
+ </style>
27546
+ </head>
27547
+ <body>
27548
+ <div class="container">
27549
+ <div class="error-icon">✕</div>
27550
+ <h1>Authentication Failed</h1>
27551
+ <p>There was an error during authentication.</p>
27552
+ <div class="error-details">${this.escapeHtml(error)}</div>
27553
+ </div>
27554
+ </body>
27555
+ </html>
27556
+ `;
27557
+ }
27558
+ };
27559
+ //#endregion
27560
+ //#region src/auth/oauth-manager.ts
27561
+ var OAuthManager = class {
27562
+ clerkPublishableKey;
27563
+ dashboardUrl;
27564
+ constructor(config) {
27565
+ this.clerkPublishableKey = config.clerkPublishableKey;
27566
+ this.dashboardUrl = config.dashboardUrl;
27567
+ }
27568
+ /** `orgId` preselects that organization on the dashboard's account picker; the user can still choose another. */
27569
+ async authenticate(mode, options = {}) {
27570
+ const state = crypto$2.randomBytes(32).toString("base64url");
27571
+ const codeVerifier = crypto$2.randomBytes(32).toString("base64url");
27572
+ const codeChallenge = crypto$2.createHash("sha256").update(codeVerifier).digest("base64url");
27573
+ const callbackServer = new CallbackServer();
27574
+ callbackServer.setExpectedState(state);
27575
+ const requestedPort = await this.findAvailablePort();
27576
+ const { actualPort, codePromise } = await callbackServer.start(requestedPort);
27577
+ const redirectUri = `http://127.0.0.1:${actualPort}/callback`;
27578
+ const authUrl = new URL(`${this.dashboardUrl.replace(/\/$/, "")}/${mode}`);
27579
+ authUrl.searchParams.set("redirect_uri", redirectUri);
27580
+ authUrl.searchParams.set("cli", "true");
27581
+ authUrl.searchParams.set("state", state);
27582
+ authUrl.searchParams.set("code_challenge", codeChallenge);
27583
+ authUrl.searchParams.set("code_challenge_method", "S256");
27584
+ authUrl.searchParams.set("publishable_key", this.clerkPublishableKey);
27585
+ if (options.orgId) authUrl.searchParams.set("org", options.orgId);
27586
+ console.log("🌐 Opening browser for authentication...");
27587
+ console.log(`If the browser doesn't open, visit: ${authUrl}`);
27588
+ try {
27589
+ console.log("⏳ Waiting for authentication...");
27590
+ const [code] = await Promise.all([codePromise, open(authUrl.toString()).catch(() => {
27591
+ console.log("Failed to open browser automatically. Please visit the URL above.");
27592
+ })]);
27593
+ return await this.exchangeCodeForSession(code, codeVerifier, redirectUri);
27594
+ } finally {
27595
+ callbackServer.stop();
27596
+ }
27597
+ }
27598
+ async findAvailablePort() {
27599
+ return 8765;
27600
+ }
27601
+ async exchangeCodeForSession(code, codeVerifier, redirectUri) {
27602
+ return code;
27603
+ }
27604
+ };
27605
+ //#endregion
27606
+ //#region src/auth/login-flow.ts
27607
+ var InvalidApiKeyError = class extends Error {
27608
+ constructor() {
27609
+ super("Invalid API key");
27610
+ this.name = "InvalidApiKeyError";
27611
+ }
27612
+ };
27613
+ async function storeLogin(credentials) {
27614
+ const context = await loadEffectiveContext(credentials.apiUrl);
27615
+ const store = new CredentialStore();
27616
+ const account = await store.saveCredentials(credentials);
27617
+ const isDefault = (await store.getDefaultAccount())?.id === account.id;
27618
+ return {
27619
+ ...credentials,
27620
+ account,
27621
+ isDefault,
27622
+ ...context.requestedOrgId ? { requestedOrgId: context.requestedOrgId } : {}
27623
+ };
27624
+ }
27625
+ /** Browser (Clerk) login: mint an org-owned CLI key for the organization the user authorizes. */
27626
+ async function browserLogin(options = {}) {
27627
+ const context = await loadEffectiveContext(options.apiUrl);
27628
+ const oauth = new OAuthManager({
27629
+ clerkPublishableKey: getClerkPublishableKey(),
27630
+ dashboardUrl: options.dashboardUrl || getDashboardUrl()
27631
+ });
27632
+ options.onProgress?.("Waiting for browser authentication...");
27633
+ const sessionToken = await oauth.authenticate("sign-in", { orgId: context.requestedOrgId });
27634
+ options.onProgress?.("Creating API key...");
27635
+ const { key, userId, orgId } = await new ApiKeyManager().exchangeSessionForApiKey(sessionToken, context.apiUrl);
27636
+ options.onProgress?.("Storing credentials securely...");
27637
+ return storeLogin({
27638
+ apiKey: key,
27639
+ userId,
27640
+ orgId,
27641
+ apiUrl: context.apiUrl
27642
+ });
27643
+ }
27644
+ /** Store an existing API key as a login for whichever organization owns it. */
27645
+ async function apiKeyLogin(apiKey, options = {}) {
27646
+ const context = await loadEffectiveContext(options.apiUrl);
27647
+ const manager = new ApiKeyManager();
27648
+ if (!await manager.validateApiKey(apiKey, context.apiUrl)) throw new InvalidApiKeyError();
27649
+ const user = await manager.getCurrentUser(apiKey, context.apiUrl);
27650
+ return storeLogin({
27651
+ apiKey,
27652
+ userId: user.userId,
27653
+ orgId: user.orgId ?? void 0,
27654
+ apiUrl: context.apiUrl
27655
+ });
27656
+ }
27657
+ /** Non-null when the user authorized a different organization than the one this directory targets. */
27658
+ function orgMismatchWarning(login) {
27659
+ if (!login.requestedOrgId || login.requestedOrgId === login.orgId) return null;
27660
+ return `Logged in to ${login.orgId ?? "a personal account"}, but this invocation targets ${login.requestedOrgId}. Commands here will keep asking for a login to ${login.requestedOrgId}; run "runtype login" again and pick that organization, or update the link with "runtype link ${login.orgId ?? ""}".`;
27661
+ }
27662
+ //#endregion
26946
27663
  //#region src/lib/ensure-auth.ts
26947
27664
  const verifiedUsers = /* @__PURE__ */ new WeakMap();
26948
27665
  async function getAuthenticatedUser() {
@@ -27011,7 +27728,7 @@ async function ensureAuth(options) {
27011
27728
  }
27012
27729
  if (options?.interactive === false || process.stdin.isTTY !== true || invocation.noInput || strictIdentity) {
27013
27730
  if (options?.exitOnFailure === false) return null;
27014
- const message = "Authentication required. Run \"runtype auth login\" first or set RUNTYPE_API_KEY.";
27731
+ const message = authRequiredMessage(context);
27015
27732
  console.error(invocation.json ? JSON.stringify({
27016
27733
  success: false,
27017
27734
  error: {
@@ -27022,39 +27739,39 @@ async function ensureAuth(options) {
27022
27739
  process.exit(1);
27023
27740
  return null;
27024
27741
  }
27025
- console.log(chalk.yellow("\nAuthentication required."));
27742
+ console.log(chalk.yellow(`\n${authRequiredMessage(context)}`));
27026
27743
  if (!await promptConfirm$2("Would you like to log in now?", { default: true })) {
27027
27744
  console.log(chalk.gray("Run \"runtype auth login\" when you're ready."));
27028
27745
  if (options?.exitOnFailure !== false) process.exit(1);
27029
27746
  return null;
27030
27747
  }
27031
- const apiKey = await handleBrowserLogin(new CredentialStore(), context.apiUrl);
27748
+ const apiKey = await handleBrowserLogin(context.apiUrl);
27032
27749
  if (apiKey) {
27033
27750
  context.apiKey = apiKey;
27034
27751
  context.credentialSource = "stored";
27035
27752
  }
27036
27753
  return apiKey;
27037
27754
  }
27038
- async function handleBrowserLogin(store, apiUrl) {
27755
+ /** Name the organization the invocation needs, so a linked project never reads as "logged out". */
27756
+ function authRequiredMessage(context) {
27757
+ if (context.credentialError && context.account) return `The stored login for ${context.account.orgId ?? "your personal account"} could not be read (${context.credentialError}). Log in again with "runtype login" (or set RUNTYPE_CREDENTIAL_STORE=file first if the OS keychain is unavailable here), or set RUNTYPE_API_KEY.`;
27758
+ if (context.requestedOrgId) return `${context.accountSource === "project" && context.projectLink ? `This directory is linked to ${context.requestedOrgId} (${context.projectLink.path})` : `This command targets ${context.requestedOrgId}`}, but there is no stored login for it on ${context.apiUrl}. Run "runtype login" and authorize that organization, or set RUNTYPE_API_KEY.`;
27759
+ return "Authentication required. Run \"runtype auth login\" first or set RUNTYPE_API_KEY.";
27760
+ }
27761
+ async function handleBrowserLogin(apiUrl) {
27039
27762
  console.log(chalk.gray("Opening browser for authentication..."));
27040
27763
  try {
27041
- const oauth = new OAuthManager({
27042
- clerkPublishableKey: getClerkPublishableKey(),
27043
- dashboardUrl: getDashboardUrl()
27044
- });
27045
- console.log(chalk.gray("Waiting for browser authentication..."));
27046
- const sessionToken = await oauth.authenticate("sign-in");
27047
- console.log(chalk.gray("Getting API key..."));
27048
- const { key, userId, orgId } = await new ApiKeyManager().exchangeSessionForApiKey(sessionToken, apiUrl || getApiUrl());
27049
- console.log(chalk.gray("Storing credentials..."));
27050
- await store.saveCredentials({
27051
- apiKey: key,
27052
- userId,
27053
- orgId,
27054
- apiUrl: apiUrl || getApiUrl()
27764
+ const login = await browserLogin({
27765
+ apiUrl,
27766
+ onProgress: (label) => console.log(chalk.gray(label))
27055
27767
  });
27768
+ const warning = orgMismatchWarning(login);
27769
+ if (warning) {
27770
+ console.log(chalk.yellow(warning));
27771
+ return null;
27772
+ }
27056
27773
  console.log(chalk.green("Logged in successfully!"));
27057
- return key;
27774
+ return login.apiKey;
27058
27775
  } catch (error) {
27059
27776
  const message = error instanceof Error ? error.message : "Unknown error";
27060
27777
  console.log(chalk.red("Login failed"));
@@ -27285,6 +28002,25 @@ function MutationResult({ loading, loadingLabel = "Processing...", success, succ
27285
28002
  return null;
27286
28003
  }
27287
28004
  //#endregion
28005
+ //#region src/ink/shared/account-picker.ts
28006
+ /** Interactive picker over stored logins; resolves undefined if the user exits without choosing. */
28007
+ async function promptForAccount(accounts) {
28008
+ let chosen;
28009
+ const { waitUntilExit, unmount } = render(React.createElement(SelectInput, {
28010
+ items: accounts.map((account) => ({
28011
+ key: account.id,
28012
+ label: `${account.orgId ?? "Personal"} ${account.apiUrl}`,
28013
+ value: account.id
28014
+ })),
28015
+ onSelect: (item) => {
28016
+ chosen = accounts.find((account) => account.id === item.value);
28017
+ unmount();
28018
+ }
28019
+ }));
28020
+ await waitUntilExit();
28021
+ return chosen;
28022
+ }
28023
+ //#endregion
27288
28024
  //#region src/auth/agent-auth-client.ts
27289
28025
  /**
27290
28026
  * Protocol error with the spec error code and an agent-actionable hint.
@@ -27539,7 +28275,7 @@ async function registerAction(options) {
27539
28275
  claimTokenExpires: registration.claimTokenExpires,
27540
28276
  apiUrl
27541
28277
  };
27542
- await store.saveCredentials({
28278
+ const registered = await store.saveCredentials({
27543
28279
  apiKey: registration.credential,
27544
28280
  userId: identity.userId,
27545
28281
  orgId: identity.orgId,
@@ -27557,7 +28293,8 @@ async function registerAction(options) {
27557
28293
  const claim = await initiateClaim(registration.claimToken, options.email, apiUrl);
27558
28294
  await store.saveClaimState({
27559
28295
  ...claimState,
27560
- claimEmail: options.email
28296
+ claimEmail: options.email,
28297
+ accountId: registered.id
27561
28298
  });
27562
28299
  printJson({
27563
28300
  ...base,
@@ -27638,13 +28375,15 @@ async function verifyAction(code, options) {
27638
28375
  try {
27639
28376
  identity = await fetchIdentity(completed.credential, apiUrl);
27640
28377
  } catch {}
27641
- await store.saveCredentials({
28378
+ const preClaimAccountId = claimState.accountId;
28379
+ const wasDefault = (await store.getDefaultAccount())?.id === preClaimAccountId;
28380
+ const saved = await store.saveCredentials({
27642
28381
  apiKey: completed.credential,
27643
28382
  userId: identity.userId,
27644
28383
  orgId: identity.orgId,
27645
28384
  apiUrl
27646
- });
27647
- await store.clearClaimState();
28385
+ }, void 0, { makeDefault: wasDefault });
28386
+ if (preClaimAccountId && preClaimAccountId !== saved.id) await store.removeAccount(preClaimAccountId);
27648
28387
  printJson({
27649
28388
  status: "authenticated",
27650
28389
  accountState: "full",
@@ -27693,149 +28432,122 @@ async function getBillingInfo(apiKey, apiUrl) {
27693
28432
  return null;
27694
28433
  }
27695
28434
  }
27696
- authCommand.command("signup").description("Create a new Runtype account (alias for \"auth login\")").option("--api-url <url>", "Custom API URL").option("--tty", "Force TTY mode").option("--no-tty", "Force non-TTY mode").action(async (options) => {
27697
- await loadEffectiveContext(options.apiUrl);
27698
- refuseForcedTtyLogin(options);
27699
- if (!isTTY(options)) {
27700
- console.error(JSON.stringify(NON_TTY_LOGIN_GUIDANCE, null, 2));
27701
- process.exit(1);
27702
- }
28435
+ /** Run one async step under the shared spinner/result chrome. */
28436
+ async function renderTask(loadingLabel, task) {
27703
28437
  const App = () => {
27704
28438
  const [loading, setLoading] = useState(true);
27705
- const [loadingLabel, setLoadingLabel] = useState("Opening browser for authentication...");
28439
+ const [label, setLabel] = useState(loadingLabel);
27706
28440
  const [success, setSuccess] = useState(null);
27707
28441
  const [error, setError] = useState(null);
28442
+ const [outcome, setOutcome] = useState(null);
27708
28443
  useEffect(() => {
27709
- const run = async () => {
27710
- try {
27711
- const oauth = new OAuthManager({
27712
- clerkPublishableKey: getClerkPublishableKey(),
27713
- dashboardUrl: getDashboardUrl()
27714
- });
27715
- setLoadingLabel("Waiting for browser authentication...");
27716
- const sessionToken = await oauth.authenticate("sign-in");
27717
- setLoadingLabel("Creating API key...");
27718
- const { key, userId, orgId } = await new ApiKeyManager().exchangeSessionForApiKey(sessionToken, options.apiUrl || getApiUrl());
27719
- setLoadingLabel("Storing credentials securely...");
27720
- await new CredentialStore().saveCredentials({
27721
- apiKey: key,
27722
- userId,
27723
- orgId,
27724
- apiUrl: options.apiUrl || getApiUrl()
27725
- });
27726
- setSuccess(true);
27727
- setLoading(false);
27728
- } catch (err) {
27729
- setError(err instanceof Error ? err : new Error(String(err)));
27730
- setSuccess(false);
27731
- setLoading(false);
27732
- }
27733
- };
27734
- run();
28444
+ task(setLabel).then((value) => {
28445
+ setOutcome(value);
28446
+ setSuccess(true);
28447
+ }).catch((err) => {
28448
+ setError(err instanceof Error ? err : new Error(String(err)));
28449
+ setSuccess(false);
28450
+ }).finally(() => setLoading(false));
27735
28451
  }, []);
27736
28452
  return React.createElement(MutationResult, {
27737
28453
  loading,
27738
- loadingLabel,
28454
+ loadingLabel: label,
27739
28455
  success,
27740
- successMessage: "Authentication successful! Welcome to Runtype!",
28456
+ successMessage: outcome?.message ?? "",
27741
28457
  error,
27742
- result: React.createElement(EntityCard, { fields: [{
27743
- label: "Next",
27744
- value: "runtype flows list"
27745
- }] })
28458
+ result: outcome?.result
27746
28459
  });
27747
28460
  };
27748
28461
  const { waitUntilExit } = render(React.createElement(App));
27749
28462
  await waitUntilExit();
28463
+ }
28464
+ function loginFields(login) {
28465
+ return [
28466
+ {
28467
+ label: "Organization",
28468
+ value: login.orgId ?? "Personal"
28469
+ },
28470
+ {
28471
+ label: "User ID",
28472
+ value: login.userId
28473
+ },
28474
+ {
28475
+ label: "API URL",
28476
+ value: login.apiUrl
28477
+ },
28478
+ {
28479
+ label: "Default login",
28480
+ value: login.isDefault ? "yes" : "no (run: runtype auth switch)"
28481
+ }
28482
+ ];
28483
+ }
28484
+ function loginJson(login) {
28485
+ const warning = orgMismatchWarning(login);
28486
+ return {
28487
+ status: "authenticated",
28488
+ userId: login.userId,
28489
+ orgId: login.orgId ?? null,
28490
+ apiUrl: login.apiUrl,
28491
+ accountId: login.account.id,
28492
+ isDefault: login.isDefault,
28493
+ secretStore: login.account.secretStore,
28494
+ ...warning ? { warning } : {}
28495
+ };
28496
+ }
28497
+ function loginOutcome(login) {
28498
+ return {
28499
+ message: orgMismatchWarning(login) ?? `Logged in to ${login.orgId ?? "your personal account"}.`,
28500
+ result: React.createElement(EntityCard, { fields: loginFields(login) })
28501
+ };
28502
+ }
28503
+ authCommand.command("signup").description("Create a new Runtype account (alias for \"auth login\")").option("--api-url <url>", "Custom API URL").option("--tty", "Force TTY mode").option("--no-tty", "Force non-TTY mode").action(async (options) => {
28504
+ await loadEffectiveContext(options.apiUrl);
28505
+ refuseForcedTtyLogin(options);
28506
+ if (!isTTY(options)) {
28507
+ console.error(JSON.stringify(NON_TTY_LOGIN_GUIDANCE, null, 2));
28508
+ process.exit(1);
28509
+ }
28510
+ await renderTask("Opening browser for authentication...", async (setLabel) => loginOutcome(await browserLogin({
28511
+ ...options,
28512
+ onProgress: setLabel
28513
+ })));
27750
28514
  });
28515
+ function failApiKeyLogin(error) {
28516
+ if (error instanceof ApiKeyValidationUnavailableError) {
28517
+ console.error(JSON.stringify({
28518
+ status: "error",
28519
+ code: "api_unreachable",
28520
+ error: error.message,
28521
+ next: API_UNREACHABLE_NEXT
28522
+ }, null, 2));
28523
+ process.exit(1);
28524
+ }
28525
+ const message = error instanceof Error ? error.message : "Unknown error";
28526
+ console.error(JSON.stringify({
28527
+ status: "error",
28528
+ error: message
28529
+ }, null, 2));
28530
+ process.exit(1);
28531
+ }
27751
28532
  async function loginAction(options) {
27752
28533
  await loadEffectiveContext(options.apiUrl);
27753
- const store = new CredentialStore();
27754
28534
  if (options.apiKey) {
28535
+ const apiKey = options.apiKey;
27755
28536
  if (!isTTY(options)) {
28537
+ let login;
27756
28538
  try {
27757
- const apiKeyManager = new ApiKeyManager();
27758
- if (!await apiKeyManager.validateApiKey(options.apiKey, options.apiUrl)) {
27759
- console.error(JSON.stringify({
27760
- status: "error",
27761
- error: "Invalid API key"
27762
- }, null, 2));
27763
- process.exit(1);
27764
- }
27765
- const userData = await apiKeyManager.getCurrentUser(options.apiKey, options.apiUrl);
27766
- await store.saveCredentials({
27767
- apiKey: options.apiKey,
27768
- userId: userData.userId,
27769
- orgId: userData.orgId ?? void 0,
27770
- apiUrl: options.apiUrl || getApiUrl()
27771
- });
27772
- console.log(JSON.stringify({
27773
- status: "authenticated",
27774
- userId: userData.userId,
27775
- next: "Continue with setup: runtype install-mcp (then confirm with: runtype auth whoami)"
27776
- }, null, 2));
28539
+ login = await apiKeyLogin(apiKey, { apiUrl: options.apiUrl });
27777
28540
  } catch (error) {
27778
- if (error instanceof ApiKeyValidationUnavailableError) {
27779
- console.error(JSON.stringify({
27780
- status: "error",
27781
- code: "api_unreachable",
27782
- error: error.message,
27783
- next: API_UNREACHABLE_NEXT
27784
- }, null, 2));
27785
- process.exit(1);
27786
- }
27787
- const message = error instanceof Error ? error.message : "Unknown error";
27788
- console.error(JSON.stringify({
27789
- status: "error",
27790
- error: message
27791
- }, null, 2));
27792
- process.exit(1);
28541
+ failApiKeyLogin(error);
28542
+ return;
27793
28543
  }
28544
+ console.log(JSON.stringify({
28545
+ ...loginJson(login),
28546
+ next: "Continue with setup: runtype install-mcp (then confirm with: runtype auth whoami)"
28547
+ }, null, 2));
27794
28548
  return;
27795
28549
  }
27796
- const App = () => {
27797
- const [loading, setLoading] = useState(true);
27798
- const [success, setSuccess] = useState(null);
27799
- const [error, setError] = useState(null);
27800
- const [userId, setUserId] = useState(null);
27801
- useEffect(() => {
27802
- const run = async () => {
27803
- try {
27804
- const apiKeyManager = new ApiKeyManager();
27805
- if (!await apiKeyManager.validateApiKey(options.apiKey, options.apiUrl)) {
27806
- setError(/* @__PURE__ */ new Error("Invalid API key"));
27807
- setSuccess(false);
27808
- setLoading(false);
27809
- return;
27810
- }
27811
- const userData = await apiKeyManager.getCurrentUser(options.apiKey, options.apiUrl);
27812
- await store.saveCredentials({
27813
- apiKey: options.apiKey,
27814
- userId: userData.userId,
27815
- orgId: userData.orgId ?? void 0,
27816
- apiUrl: options.apiUrl || getApiUrl()
27817
- });
27818
- setUserId(userData.userId);
27819
- setSuccess(true);
27820
- setLoading(false);
27821
- } catch (err) {
27822
- setError(err instanceof Error ? err : new Error(String(err)));
27823
- setSuccess(false);
27824
- setLoading(false);
27825
- }
27826
- };
27827
- run();
27828
- }, []);
27829
- return React.createElement(MutationResult, {
27830
- loading,
27831
- loadingLabel: "Validating API key...",
27832
- success,
27833
- successMessage: `Logged in successfully!${userId ? ` User ID: ${userId}` : ""}`,
27834
- error
27835
- });
27836
- };
27837
- const { waitUntilExit } = render(React.createElement(App));
27838
- await waitUntilExit();
28550
+ await renderTask("Validating API key...", async () => loginOutcome(await apiKeyLogin(apiKey, { apiUrl: options.apiUrl })));
27839
28551
  return;
27840
28552
  }
27841
28553
  refuseForcedTtyLogin(options);
@@ -27843,73 +28555,54 @@ async function loginAction(options) {
27843
28555
  console.error(JSON.stringify(NON_TTY_LOGIN_GUIDANCE, null, 2));
27844
28556
  process.exit(1);
27845
28557
  }
27846
- const App = () => {
27847
- const [loading, setLoading] = useState(true);
27848
- const [loadingLabel, setLoadingLabel] = useState("Initializing authentication...");
27849
- const [success, setSuccess] = useState(null);
27850
- const [error, setError] = useState(null);
27851
- const [userId, setUserId] = useState(null);
27852
- useEffect(() => {
27853
- const run = async () => {
27854
- try {
27855
- const oauth = new OAuthManager({
27856
- clerkPublishableKey: getClerkPublishableKey(),
27857
- dashboardUrl: options.dashboardUrl || getDashboardUrl()
27858
- });
27859
- setLoadingLabel("Waiting for browser authentication...");
27860
- const sessionToken = await oauth.authenticate("sign-in");
27861
- setLoadingLabel("Getting API key...");
27862
- const { key, userId: uid, orgId } = await new ApiKeyManager().exchangeSessionForApiKey(sessionToken, options.apiUrl || getApiUrl());
27863
- setLoadingLabel("Storing credentials...");
27864
- await store.saveCredentials({
27865
- apiKey: key,
27866
- userId: uid,
27867
- orgId,
27868
- apiUrl: options.apiUrl || getApiUrl()
27869
- });
27870
- setUserId(uid);
27871
- setSuccess(true);
27872
- setLoading(false);
27873
- } catch (err) {
27874
- setError(err instanceof Error ? err : new Error(String(err)));
27875
- setSuccess(false);
27876
- setLoading(false);
27877
- }
27878
- };
27879
- run();
27880
- }, []);
27881
- return React.createElement(MutationResult, {
27882
- loading,
27883
- loadingLabel,
27884
- success,
27885
- successMessage: `Logged in successfully!${userId ? ` User ID: ${userId}` : ""}`,
27886
- error
27887
- });
27888
- };
27889
- const { waitUntilExit } = render(React.createElement(App));
27890
- await waitUntilExit();
28558
+ await renderTask("Initializing authentication...", async (setLabel) => loginOutcome(await browserLogin({
28559
+ ...options,
28560
+ onProgress: setLabel
28561
+ })));
28562
+ }
28563
+ const LOGIN_DESCRIPTION = "Log in to an organization (adds a login; the target org comes from --org, RUNTYPE_ORG, or .runtype/project.json)";
28564
+ authCommand.command("login").description(LOGIN_DESCRIPTION).option("--api-url <url>", "Custom API URL").option("--api-key <key>", "Use API key directly").option("--tty", "Force TTY mode").option("--no-tty", "Force non-TTY mode").action(loginAction);
28565
+ const loginCommand = new Command("login").description(LOGIN_DESCRIPTION).option("--api-url <url>", "Custom API URL").option("--api-key <key>", "Use API key directly").option("--tty", "Force TTY mode").option("--no-tty", "Force non-TTY mode").action(loginAction);
28566
+ function describeAccountSource(context) {
28567
+ switch (context.accountSource) {
28568
+ case "flag": return "--org flag";
28569
+ case "environment": return "RUNTYPE_ORG";
28570
+ case "project": return context.projectLink ? `project link (${context.projectLink.path})` : "project link";
28571
+ default: return "default login";
28572
+ }
27891
28573
  }
27892
- authCommand.command("login").description("Login to existing account").option("--api-url <url>", "Custom API URL").option("--api-key <key>", "Use API key directly").option("--tty", "Force TTY mode").option("--no-tty", "Force non-TTY mode").action(loginAction);
27893
- const loginCommand = new Command("login").description("Log in to Runtype").option("--api-url <url>", "Custom API URL").option("--api-key <key>", "Use API key directly").option("--tty", "Force TTY mode").option("--no-tty", "Force non-TTY mode").action(loginAction);
27894
- authCommand.command("status").description("Show authentication status").option("--tty", "Force TTY mode").option("--no-tty", "Force non-TTY mode").action(async (options) => {
28574
+ authCommand.command("status").description("Show which login this directory uses").option("--tty", "Force TTY mode").option("--no-tty", "Force non-TTY mode").action(async (options) => {
27895
28575
  const store = new CredentialStore();
27896
28576
  if (!isTTY(options)) {
27897
28577
  try {
27898
- if (!await store.hasCredentials()) {
28578
+ const context = await loadEffectiveContext();
28579
+ const storedLogins = (await store.listAccounts()).length;
28580
+ const credentials = context.credentials;
28581
+ if (!credentials) {
27899
28582
  console.log(JSON.stringify({
27900
28583
  status: "unauthenticated",
27901
- next: "New account: runtype auth register --email <email>. Existing account: runtype auth login --api-key <key>."
28584
+ requestedOrgId: context.requestedOrgId ?? null,
28585
+ accountSource: context.accountSource,
28586
+ projectLink: context.projectLink?.path ?? null,
28587
+ storedLogins,
28588
+ next: context.requestedOrgId ? authRequiredMessage(context) : "New account: runtype auth register --email <email>. Existing account: runtype auth login --api-key <key>."
27902
28589
  }, null, 2));
27903
28590
  return;
27904
28591
  }
27905
- const credentials = await store.getCredentials();
27906
- const claimState = await store.getClaimState();
28592
+ const pendingClaim = await store.getClaimState();
28593
+ const claimState = pendingClaim?.accountId === credentials.accountId ? pendingClaim : null;
27907
28594
  console.log(JSON.stringify({
27908
28595
  status: "authenticated",
27909
28596
  accountState: claimState ? "temporary" : "full",
27910
- userId: credentials?.userId,
27911
- apiUrl: credentials?.apiUrl || "default",
27912
- orgId: credentials?.orgId || null,
28597
+ userId: credentials.userId,
28598
+ apiUrl: credentials.apiUrl,
28599
+ orgId: credentials.orgId || null,
28600
+ accountId: credentials.accountId,
28601
+ accountSource: context.accountSource,
28602
+ projectLink: context.projectLink?.path ?? null,
28603
+ secretStore: context.account?.secretStore ?? null,
28604
+ storedLogins,
28605
+ ...context.credentialSource === "environment" ? { note: "RUNTYPE_API_KEY is set and takes precedence over the stored login." } : {},
27913
28606
  ...claimState ? {
27914
28607
  registrationId: claimState.registrationId,
27915
28608
  credentialExpires: claimState.credentialExpires,
@@ -27926,74 +28619,158 @@ authCommand.command("status").description("Show authentication status").option("
27926
28619
  }
27927
28620
  return;
27928
28621
  }
27929
- const App = () => {
27930
- const [loading, setLoading] = useState(true);
27931
- const [success, setSuccess] = useState(null);
27932
- const [error, setError] = useState(null);
27933
- const [resultNode, setResultNode] = useState(void 0);
27934
- useEffect(() => {
27935
- const run = async () => {
27936
- try {
27937
- if (!await store.hasCredentials()) {
27938
- setResultNode(React.createElement(EntityCard, { fields: [{
27939
- label: "Status",
27940
- value: "Not authenticated",
27941
- color: "yellow"
27942
- }, {
27943
- label: "Hint",
27944
- value: "Run runtype auth login"
27945
- }] }));
27946
- setSuccess(true);
27947
- setLoading(false);
27948
- return;
27949
- }
27950
- const credentials = await store.getCredentials();
27951
- if (credentials) setResultNode(React.createElement(EntityCard, { fields: [
27952
- {
27953
- label: "Status",
27954
- value: "Authenticated",
27955
- color: "green"
27956
- },
27957
- {
27958
- label: "User ID",
27959
- value: credentials.userId
27960
- },
27961
- {
27962
- label: "API URL",
27963
- value: credentials.apiUrl || "default"
27964
- },
27965
- {
27966
- label: "API Key",
27967
- value: credentials.apiKey ? credentials.apiKey.substring(0, 12) + "..." : "missing"
27968
- },
27969
- {
27970
- label: "Org ID",
27971
- value: credentials.orgId || "none"
27972
- }
27973
- ] }));
27974
- setSuccess(true);
27975
- setLoading(false);
27976
- } catch (err) {
27977
- setError(err instanceof Error ? err : new Error(String(err)));
27978
- setSuccess(false);
27979
- setLoading(false);
28622
+ await renderTask("Checking authentication...", async () => {
28623
+ const context = await loadEffectiveContext();
28624
+ const storedLogins = (await store.listAccounts()).length;
28625
+ const credentials = context.credentials;
28626
+ if (!credentials) return {
28627
+ message: "Auth Status",
28628
+ result: React.createElement(EntityCard, { fields: [
28629
+ {
28630
+ label: "Status",
28631
+ value: "Not authenticated",
28632
+ color: "yellow"
28633
+ },
28634
+ {
28635
+ label: "Selected by",
28636
+ value: describeAccountSource(context)
28637
+ },
28638
+ {
28639
+ label: "Stored logins",
28640
+ value: String(storedLogins)
28641
+ },
28642
+ {
28643
+ label: "Hint",
28644
+ value: context.requestedOrgId ? authRequiredMessage(context) : "Run runtype auth login"
27980
28645
  }
27981
- };
27982
- run();
27983
- }, []);
27984
- return React.createElement(MutationResult, {
27985
- loading,
27986
- loadingLabel: "Checking authentication...",
27987
- success,
27988
- successMessage: "Auth Status",
27989
- error,
27990
- result: resultNode
27991
- });
27992
- };
27993
- const { waitUntilExit } = render(React.createElement(App));
27994
- await waitUntilExit();
28646
+ ] })
28647
+ };
28648
+ return {
28649
+ message: "Auth Status",
28650
+ result: React.createElement(EntityCard, { fields: [
28651
+ {
28652
+ label: "Status",
28653
+ value: "Authenticated",
28654
+ color: "green"
28655
+ },
28656
+ {
28657
+ label: "Organization",
28658
+ value: credentials.orgId || "Personal"
28659
+ },
28660
+ {
28661
+ label: "User ID",
28662
+ value: credentials.userId
28663
+ },
28664
+ {
28665
+ label: "API URL",
28666
+ value: credentials.apiUrl
28667
+ },
28668
+ {
28669
+ label: "API Key",
28670
+ value: credentials.apiKey.substring(0, 12) + "..."
28671
+ },
28672
+ {
28673
+ label: "Selected by",
28674
+ value: describeAccountSource(context)
28675
+ },
28676
+ {
28677
+ label: "Stored in",
28678
+ value: context.account?.secretStore === "file" ? "credential file" : "OS keychain"
28679
+ },
28680
+ {
28681
+ label: "Stored logins",
28682
+ value: String(storedLogins)
28683
+ },
28684
+ ...context.credentialSource === "environment" ? [{
28685
+ label: "Note",
28686
+ value: "RUNTYPE_API_KEY overrides the stored login",
28687
+ color: "yellow"
28688
+ }] : []
28689
+ ] })
28690
+ };
28691
+ });
28692
+ });
28693
+ authCommand.command("accounts").alias("list").description("List stored logins (one per organization and API URL)").option("--json", "Output as JSON").option("--tty", "Force TTY mode").option("--no-tty", "Force non-TTY mode").action(async (options) => {
28694
+ const store = new CredentialStore();
28695
+ const [accounts, defaultAccount, context] = await Promise.all([
28696
+ store.listAccounts(),
28697
+ store.getDefaultAccount(),
28698
+ loadEffectiveContext()
28699
+ ]);
28700
+ const rows = accounts.map((account) => ({
28701
+ accountId: account.id,
28702
+ orgId: account.orgId,
28703
+ userId: account.userId,
28704
+ apiUrl: account.apiUrl,
28705
+ isDefault: account.id === defaultAccount?.id,
28706
+ isActive: account.id === context.account?.id,
28707
+ secretStore: account.secretStore,
28708
+ pendingClaim: Boolean(account.claim),
28709
+ lastUsedAt: account.lastUsedAt ?? null
28710
+ }));
28711
+ if (!isTTY(options) || options.json) {
28712
+ console.log(JSON.stringify({ accounts: rows }, null, 2));
28713
+ return;
28714
+ }
28715
+ if (rows.length === 0) {
28716
+ console.log(chalk.yellow("No stored logins. Run: runtype login"));
28717
+ return;
28718
+ }
28719
+ for (const row of rows) {
28720
+ const marker = row.isActive ? chalk.green("*") : " ";
28721
+ const tags = [row.isDefault ? "default" : null, row.pendingClaim ? "unclaimed" : null].filter(Boolean).join(", ");
28722
+ console.log(`${marker} ${chalk.bold(row.orgId ?? "Personal")} ${chalk.gray(row.apiUrl)}${tags ? ` ${chalk.cyan(`(${tags})`)}` : ""}`);
28723
+ }
28724
+ console.log(chalk.gray("\n* = login used in this directory"));
28725
+ });
28726
+ /** Resolve an `auth switch`/`logout` argument (org id or account id) to exactly one stored login. */
28727
+ async function findAccountArgument(store, value, apiUrl) {
28728
+ const accounts = await store.listAccounts();
28729
+ const byId = accounts.find((account) => account.id === value);
28730
+ if (byId) return byId;
28731
+ const target = apiUrl ? normalizeApiUrl(apiUrl) : void 0;
28732
+ const matches = accounts.filter((account) => account.orgId === value && (!target || account.apiUrl === target));
28733
+ if (matches.length === 1) return matches[0];
28734
+ if (matches.length > 1) throw new CliContextError(`${value} has logins on several API URLs (${matches.map((m) => m.apiUrl).join(", ")}); pass --api-url to pick one.`);
28735
+ throw new CliContextError(`No stored login for ${value}. Run: runtype login --org ${value}`);
28736
+ }
28737
+ authCommand.command("switch [org]").description("Set the default login used outside linked projects").option("--api-url <url>", "Disambiguate when the organization has logins on several API URLs").option("--tty", "Force TTY mode").option("--no-tty", "Force non-TTY mode").action(async (org, options) => {
28738
+ const store = new CredentialStore();
28739
+ const accounts = await store.listAccounts();
28740
+ if (accounts.length === 0) {
28741
+ console.error(chalk.yellow("No stored logins. Run: runtype login"));
28742
+ process.exit(1);
28743
+ return;
28744
+ }
28745
+ let account;
28746
+ if (org) account = await findAccountArgument(store, org, options.apiUrl ?? getInvocationOptions().apiUrl);
28747
+ else if (!isTTY(options)) {
28748
+ console.error(JSON.stringify({
28749
+ status: "error",
28750
+ error: "Pass the organization to switch to.",
28751
+ next: "runtype auth accounts --json, then: runtype auth switch <orgId>"
28752
+ }));
28753
+ process.exit(1);
28754
+ return;
28755
+ } else account = await promptForAccount(accounts);
28756
+ if (!account) return;
28757
+ await store.setDefaultAccount(account.id);
28758
+ const context = await loadEffectiveContext();
28759
+ const shadowed = context.accountSource !== "default" && context.account?.id !== account.id ? `This directory still uses ${context.requestedOrgId} (${describeAccountSource(context)}).` : null;
28760
+ if (!isTTY(options)) {
28761
+ console.log(JSON.stringify({
28762
+ status: "switched",
28763
+ accountId: account.id,
28764
+ orgId: account.orgId,
28765
+ apiUrl: account.apiUrl,
28766
+ ...shadowed ? { note: shadowed } : {}
28767
+ }, null, 2));
28768
+ return;
28769
+ }
28770
+ console.log(chalk.green(`Default login is now ${account.orgId ?? "Personal"} (${account.apiUrl}).`));
28771
+ if (shadowed) console.log(chalk.yellow(shadowed));
27995
28772
  });
27996
- authCommand.command("export-key").description("Print the stored API key to stdout (for use in scripts and .env files)").action(async () => {
28773
+ authCommand.command("export-key").description("Print the active login's API key to stdout (for use in scripts and .env files)").action(async () => {
27997
28774
  const apiKey = await new CredentialStore().getApiKey();
27998
28775
  if (!apiKey) {
27999
28776
  console.error("Not authenticated. Run `runtype auth login` first.");
@@ -28003,52 +28780,43 @@ authCommand.command("export-key").description("Print the stored API key to stdou
28003
28780
  process.stdout.write(apiKey);
28004
28781
  });
28005
28782
  registerAgentAuthCommands(authCommand);
28006
- authCommand.command("logout").description("Remove stored credentials").option("--tty", "Force TTY mode").option("--no-tty", "Force non-TTY mode").action(async (options) => {
28783
+ authCommand.command("logout [org]").description("Remove the login this directory uses (or the named org's login, or --all)").option("--all", "Remove every stored login").option("--api-url <url>", "Disambiguate when the organization has logins on several API URLs").option("--tty", "Force TTY mode").option("--no-tty", "Force non-TTY mode").action(async (org, options) => {
28007
28784
  const store = new CredentialStore();
28785
+ const logout = async () => {
28786
+ if (options.all) {
28787
+ const accounts = await store.listAccounts();
28788
+ for (const account of accounts) await store.removeAccount(account.id);
28789
+ return { removed: accounts };
28790
+ }
28791
+ if (org) {
28792
+ const account = await findAccountArgument(store, org, options.apiUrl ?? getInvocationOptions().apiUrl);
28793
+ await store.removeAccount(account.id);
28794
+ return { removed: [account] };
28795
+ }
28796
+ const removed = await store.clearCredentials();
28797
+ return { removed: removed ? [removed] : [] };
28798
+ };
28008
28799
  if (!isTTY(options)) {
28009
- if (!await store.hasCredentials()) {
28800
+ const { removed } = await logout();
28801
+ if (removed.length === 0) {
28010
28802
  console.log(JSON.stringify({ status: "no_credentials" }, null, 2));
28011
28803
  return;
28012
28804
  }
28013
- await store.clearCredentials();
28014
- console.log(JSON.stringify({ status: "logged_out" }, null, 2));
28805
+ console.log(JSON.stringify({
28806
+ status: "logged_out",
28807
+ removed: removed.map((account) => ({
28808
+ orgId: account.orgId,
28809
+ apiUrl: account.apiUrl
28810
+ })),
28811
+ remainingLogins: (await store.listAccounts()).length
28812
+ }, null, 2));
28015
28813
  return;
28016
28814
  }
28017
- const App = () => {
28018
- const [loading, setLoading] = useState(true);
28019
- const [success, setSuccess] = useState(null);
28020
- const [error, setError] = useState(null);
28021
- const [msg, setMsg] = useState("Logged out successfully");
28022
- useEffect(() => {
28023
- const run = async () => {
28024
- try {
28025
- if (!await store.hasCredentials()) {
28026
- setMsg("No stored credentials found");
28027
- setSuccess(true);
28028
- setLoading(false);
28029
- return;
28030
- }
28031
- await store.clearCredentials();
28032
- setSuccess(true);
28033
- setLoading(false);
28034
- } catch (err) {
28035
- setError(err instanceof Error ? err : new Error(String(err)));
28036
- setSuccess(false);
28037
- setLoading(false);
28038
- }
28039
- };
28040
- run();
28041
- }, []);
28042
- return React.createElement(MutationResult, {
28043
- loading,
28044
- loadingLabel: "Logging out...",
28045
- success,
28046
- successMessage: msg,
28047
- error
28048
- });
28049
- };
28050
- const { waitUntilExit } = render(React.createElement(App));
28051
- await waitUntilExit();
28815
+ await renderTask("Logging out...", async () => {
28816
+ const { removed } = await logout();
28817
+ if (removed.length === 0) return { message: "No stored credentials found" };
28818
+ return { message: `Logged out of ${removed.map((account) => account.orgId ?? "Personal").join(", ")}` };
28819
+ });
28052
28820
  });
28053
28821
  authCommand.command("whoami").description("Display current user information").option("--json", "Output as JSON").option("--tty", "Force TTY mode").option("--no-tty", "Force non-TTY mode").action(async (options) => {
28054
28822
  const apiKey = await ensureAuth({
@@ -28062,10 +28830,16 @@ authCommand.command("whoami").description("Display current user information").op
28062
28830
  apiUrl: context.apiUrl
28063
28831
  };
28064
28832
  if (!credentials || !credentials.apiKey) {
28065
- if (!isTTY(options) || options.json) console.log(JSON.stringify({ status: "unauthenticated" }, null, 2));
28833
+ if (!isTTY(options) || options.json) console.log(JSON.stringify({
28834
+ status: "unauthenticated",
28835
+ ...context.requestedOrgId ? {
28836
+ requestedOrgId: context.requestedOrgId,
28837
+ next: authRequiredMessage(context)
28838
+ } : {}
28839
+ }, null, 2));
28066
28840
  else {
28067
- console.log(chalk.yellow("Not logged in"));
28068
- console.log("Run: runtype auth login");
28841
+ console.log(chalk.yellow(context.requestedOrgId ? authRequiredMessage(context) : "Not logged in"));
28842
+ if (!context.requestedOrgId) console.log("Run: runtype auth login");
28069
28843
  }
28070
28844
  return;
28071
28845
  }
@@ -28077,6 +28851,9 @@ authCommand.command("whoami").description("Display current user information").op
28077
28851
  userId: user.userId,
28078
28852
  orgId: user.orgId,
28079
28853
  apiUrl: credentials.apiUrl || getApiUrl(),
28854
+ credentialSource: context.credentialSource,
28855
+ accountSource: context.accountSource,
28856
+ projectLink: context.projectLink?.path ?? null,
28080
28857
  billing: billingInfo
28081
28858
  }, null, 2));
28082
28859
  } catch (error) {
@@ -28089,81 +28866,142 @@ authCommand.command("whoami").description("Display current user information").op
28089
28866
  }
28090
28867
  return;
28091
28868
  }
28092
- const App = () => {
28093
- const [loading, setLoading] = useState(true);
28094
- const [success, setSuccess] = useState(null);
28095
- const [error, setError] = useState(null);
28096
- const [resultNode, setResultNode] = useState(void 0);
28097
- useEffect(() => {
28098
- const run = async () => {
28099
- try {
28100
- const user = await getAuthenticatedUser();
28101
- const billingInfo = await getBillingInfo(credentials.apiKey, credentials.apiUrl);
28102
- const fields = [
28103
- {
28104
- label: "User ID",
28105
- value: user.userId
28106
- },
28107
- {
28108
- label: "Organization",
28109
- value: user.orgId || "Personal"
28110
- },
28111
- {
28112
- label: "API Key",
28113
- value: credentials.apiKey.substring(0, 10) + "..."
28114
- },
28115
- {
28116
- label: "API URL",
28117
- value: credentials.apiUrl || getApiUrl()
28118
- },
28119
- {
28120
- label: "Created",
28121
- value: credentials.createdAt
28122
- },
28123
- {
28124
- label: "Last Used",
28125
- value: credentials.lastUsed
28126
- }
28127
- ];
28128
- if (billingInfo) {
28129
- const plan = billingInfo.planName || billingInfo.plan;
28130
- if (plan) fields.push({
28131
- label: "Plan",
28132
- value: plan,
28133
- color: "green"
28134
- });
28135
- const usage = billingInfo.usage;
28136
- if (usage) {
28137
- const used = usage.executionsToday ?? usage.executionsThisMonth;
28138
- const value = usage.executionsThisMonthAvailable === false || typeof used !== "number" ? "unavailable" : used.toLocaleString();
28139
- fields.push({
28140
- label: "Executions today",
28141
- value
28142
- });
28143
- }
28144
- }
28145
- setResultNode(React.createElement(EntityCard, { fields }));
28146
- setSuccess(true);
28147
- setLoading(false);
28148
- } catch (err) {
28149
- setError(err instanceof Error ? err : new Error(String(err)));
28150
- setSuccess(false);
28151
- setLoading(false);
28152
- }
28153
- };
28154
- run();
28155
- }, []);
28156
- return React.createElement(MutationResult, {
28157
- loading,
28158
- loadingLabel: "Fetching user information...",
28159
- success,
28160
- successMessage: "Current User",
28161
- error,
28162
- result: resultNode
28163
- });
28869
+ await renderTask("Fetching user information...", async () => {
28870
+ const user = await getAuthenticatedUser();
28871
+ const billingInfo = await getBillingInfo(credentials.apiKey, credentials.apiUrl);
28872
+ const fields = [
28873
+ {
28874
+ label: "User ID",
28875
+ value: user.userId
28876
+ },
28877
+ {
28878
+ label: "Organization",
28879
+ value: user.orgId || "Personal"
28880
+ },
28881
+ {
28882
+ label: "API Key",
28883
+ value: credentials.apiKey.substring(0, 10) + "..."
28884
+ },
28885
+ {
28886
+ label: "API URL",
28887
+ value: credentials.apiUrl || getApiUrl()
28888
+ },
28889
+ {
28890
+ label: "Selected by",
28891
+ value: context.credentialSource === "environment" ? "RUNTYPE_API_KEY" : describeAccountSource(context)
28892
+ },
28893
+ {
28894
+ label: "Created",
28895
+ value: credentials.createdAt
28896
+ },
28897
+ {
28898
+ label: "Last Used",
28899
+ value: credentials.lastUsed
28900
+ }
28901
+ ];
28902
+ if (billingInfo) {
28903
+ const plan = billingInfo.planName || billingInfo.plan;
28904
+ if (plan) fields.push({
28905
+ label: "Plan",
28906
+ value: plan,
28907
+ color: "green"
28908
+ });
28909
+ const usage = billingInfo.usage;
28910
+ if (usage) {
28911
+ const used = usage.executionsToday ?? usage.executionsThisMonth;
28912
+ const value = usage.executionsThisMonthAvailable === false || typeof used !== "number" ? "unavailable" : used.toLocaleString();
28913
+ fields.push({
28914
+ label: "Executions today",
28915
+ value
28916
+ });
28917
+ }
28918
+ }
28919
+ return {
28920
+ message: "Current User",
28921
+ result: React.createElement(EntityCard, { fields })
28922
+ };
28923
+ });
28924
+ });
28925
+ //#endregion
28926
+ //#region src/commands/link.ts
28927
+ const PRODUCTION_API_URL = "https://api.runtype.com";
28928
+ async function chooseOrganization(orgArgument, options) {
28929
+ if (orgArgument) return { orgId: orgArgument };
28930
+ const accounts = (await new CredentialStore().listAccounts()).filter((account) => account.orgId);
28931
+ if (accounts.length > 1 && isTTY(options) && !options.json) {
28932
+ console.log(chalk.cyan("Which organization should this directory use?"));
28933
+ const chosen = await promptForAccount(accounts);
28934
+ if (!chosen?.orgId) throw new CliContextError("No organization selected.");
28935
+ return {
28936
+ orgId: chosen.orgId,
28937
+ apiUrl: chosen.apiUrl
28938
+ };
28939
+ }
28940
+ const context = await loadEffectiveContext();
28941
+ const orgId = context.credentials?.orgId ?? (accounts.length === 1 ? accounts[0].orgId : null);
28942
+ if (!orgId) throw new CliContextError("Pass the organization to link (runtype link <orgId>), or log in first with: runtype login");
28943
+ return {
28944
+ orgId,
28945
+ apiUrl: context.credentials?.orgId ? context.credentials.apiUrl : accounts[0]?.apiUrl
28164
28946
  };
28165
- const { waitUntilExit } = render(React.createElement(App));
28166
- await waitUntilExit();
28947
+ }
28948
+ const linkCommand = new Command("link").description("Link this directory to an organization: writes .runtype/project.json (no secrets; commit it)").argument("[orgId]", "Organization ID (defaults to a stored login)").option("--api-url <url>", "Also pin the API URL for this directory").option("--dir <path>", "Directory to link (default: current directory)").option("--force", "Replace an existing link in that directory").option("--json", "Output as JSON").option("--tty", "Force TTY mode").option("--no-tty", "Force non-TTY mode").action(async (orgArgument, options) => {
28949
+ const directory = path.resolve(options.dir ?? process.cwd());
28950
+ const existingPath = projectLinkPath(directory);
28951
+ let existingApiUrl;
28952
+ if (fs.existsSync(existingPath) && !options.force) {
28953
+ let existingOrg = null;
28954
+ try {
28955
+ const existing = parseProjectLink(fs.readFileSync(existingPath, "utf8"), existingPath);
28956
+ existingOrg = existing.orgId;
28957
+ existingApiUrl = existing.apiUrl;
28958
+ } catch {
28959
+ existingOrg = null;
28960
+ }
28961
+ if (existingOrg !== orgArgument || !orgArgument) throw new CliContextError(`${existingPath} already links ${existingOrg ?? "an unreadable organization"}. Pass --force to replace it.`);
28962
+ }
28963
+ const chosen = await chooseOrganization(orgArgument, options);
28964
+ const candidateApiUrl = options.apiUrl ?? getInvocationOptions().apiUrl ?? existingApiUrl ?? chosen.apiUrl;
28965
+ const apiUrl = candidateApiUrl && normalizeApiUrl(candidateApiUrl) !== PRODUCTION_API_URL ? normalizeApiUrl(candidateApiUrl) : void 0;
28966
+ if (apiUrl && !isFirstPartyApiUrl(apiUrl)) throw new CliContextError(`${apiUrl} is not a Runtype API host, so it cannot be pinned in a committed link. Use --api-url or RUNTYPE_API_URL for it instead.`);
28967
+ const filePath = writeProjectLink(directory, {
28968
+ orgId: chosen.orgId,
28969
+ apiUrl
28970
+ });
28971
+ const hasLogin = (await new CredentialStore().listAccounts()).some((account) => account.orgId === chosen.orgId && account.apiUrl === (apiUrl ?? account.apiUrl));
28972
+ const next = hasLogin ? null : `No stored login for ${chosen.orgId} yet. Run: runtype login`;
28973
+ if (options.json || !isTTY(options)) {
28974
+ console.log(JSON.stringify({
28975
+ status: "linked",
28976
+ path: filePath,
28977
+ orgId: chosen.orgId,
28978
+ apiUrl: apiUrl ?? null,
28979
+ hasLogin,
28980
+ ...next ? { next } : {}
28981
+ }, null, 2));
28982
+ return;
28983
+ }
28984
+ console.log(chalk.green(`Linked ${directory} to ${chosen.orgId}${apiUrl ? ` (${apiUrl})` : ""}.`));
28985
+ console.log(chalk.gray(`Wrote ${filePath}. It holds no secrets; commit it so teammates use the same org.`));
28986
+ if (next) console.log(chalk.yellow(next));
28987
+ });
28988
+ const unlinkCommand = new Command("unlink").description("Remove the nearest .runtype/project.json so commands use the default login again").option("--json", "Output as JSON").option("--tty", "Force TTY mode").option("--no-tty", "Force non-TTY mode").action(async (options) => {
28989
+ let root = null;
28990
+ try {
28991
+ root = findProjectLink()?.root ?? null;
28992
+ } catch (error) {
28993
+ if (!(error instanceof ProjectLinkError)) throw error;
28994
+ root = path.dirname(path.dirname(error.path));
28995
+ }
28996
+ const removedPath = root && removeProjectLink(root) ? projectLinkPath(root) : null;
28997
+ if (options.json || !isTTY(options)) {
28998
+ console.log(JSON.stringify(removedPath ? {
28999
+ status: "unlinked",
29000
+ path: removedPath
29001
+ } : { status: "not_linked" }, null, 2));
29002
+ return;
29003
+ }
29004
+ console.log(removedPath ? chalk.green(`Removed ${removedPath}. Commands here now use the default login.`) : chalk.yellow("No .runtype/project.json found in this directory or its parents."));
28167
29005
  });
28168
29006
  //#endregion
28169
29007
  //#region src/lib/file-utils.ts
@@ -31429,6 +32267,9 @@ configCommand.command("context").description("Show the effective API target and
31429
32267
  console.log(`API URL: ${context.apiUrl} (${context.apiUrlSource})`);
31430
32268
  console.log(`Credential source: ${context.credentialSource}`);
31431
32269
  console.log(`Organization: ${context.orgId ?? "unknown"} (not verified; use auth whoami)`);
32270
+ console.log(`Login selected by: ${context.accountSource}${context.requestedOrgId ? ` (${context.requestedOrgId})` : ""}`);
32271
+ if (context.projectLink) console.log(`Project link: ${context.projectLink}`);
32272
+ if (context.credentialStore) console.log(`Credential stored in: ${context.credentialStore}`);
31432
32273
  });
31433
32274
  configCommand.command("get [key]").description("Get configuration value").action((key) => {
31434
32275
  const config = getCliConfig();
@@ -32832,6 +33673,32 @@ integrationsCommand.command("get <id>").description("Get integration details, in
32832
33673
  });
32833
33674
  //#endregion
32834
33675
  //#region src/commands/surfaces.ts
33676
+ const DELIVERY_ID_PATTERN = /^[A-Za-z0-9._:-]{1,128}$/;
33677
+ async function readStdin$1() {
33678
+ let value = "";
33679
+ process.stdin.setEncoding("utf8");
33680
+ for await (const chunk of process.stdin) value += chunk;
33681
+ return value;
33682
+ }
33683
+ /** The `--payload` object, read from a file or from stdin when the source is `-`. */
33684
+ async function readPayloadObject(source) {
33685
+ const label = source === "-" ? "stdin" : source;
33686
+ const raw = source === "-" ? await readStdin$1() : await readFile(source, "utf8");
33687
+ let parsed;
33688
+ try {
33689
+ parsed = JSON.parse(raw);
33690
+ } catch {
33691
+ throw new Error(`--payload: invalid JSON in ${label}`);
33692
+ }
33693
+ if (typeof parsed !== "object" || parsed === null || Array.isArray(parsed)) throw new Error(`--payload: ${label} must contain a JSON object`);
33694
+ return parsed;
33695
+ }
33696
+ /** `Attempt 1: HTTP 202, run exec_1 (route: event_route opened)` */
33697
+ function formatDeliveryAttempt(attempt) {
33698
+ const outcome = attempt.deduplicated ? ", deduplicated" : attempt.executionId ? `, run ${attempt.executionId}` : "";
33699
+ const route = attempt.route ? ` (route: ${[attempt.route.matchedBy, attempt.route.pattern].filter(Boolean).join(" ")})` : "";
33700
+ return `Attempt ${attempt.attempt}: HTTP ${attempt.status}${outcome}${route}`;
33701
+ }
32835
33702
  function parsePositiveInteger$1(value, option) {
32836
33703
  const parsed = Number(value);
32837
33704
  if (!Number.isInteger(parsed) || parsed < 1) throw new Error(`${option} must be a positive integer`);
@@ -33076,19 +33943,23 @@ surfacesCommand.command("get <id>").description("Get surface details including i
33076
33943
  const { waitUntilExit } = render(React.createElement(App));
33077
33944
  await waitUntilExit();
33078
33945
  });
33079
- surfacesCommand.command("test <id>").description("Send a provider fixture through a surface’s real ingress").addOption(new Option("--product <productId>", "Deprecated; surface IDs resolve their product").hideHelp()).requiredOption("--fixture <fixture>", "Fixture name, such as github.pull_request.opened or github.pull_request.merged_plan").requiredOption("--repo <owner/repository>", "GitHub repository in owner/repository format").requiredOption("--pr <number>", "Pull request number").option("--repeat <number>", "Deliver the same provider event more than once", "1").option("--planning-path <path>", "Planning-document directory for merged-plan fixtures").option("--json", "Output as JSON").action(async (id, options) => {
33946
+ surfacesCommand.command("test <id>").description("Send a provider fixture through a surface’s real ingress").addOption(new Option("--product <productId>", "Deprecated; surface IDs resolve their product").hideHelp()).requiredOption("--fixture <fixture>", "Fixture name, such as github.pull_request.opened or github.pull_request.merged_plan").requiredOption("--repo <owner/repository>", "GitHub repository in owner/repository format").requiredOption("--pr <number>", "Pull request number").option("--repeat <number>", "Deliver the same provider event more than once", "1").option("--planning-path <path>", "Planning-document directory for merged-plan fixtures").option("--delivery-id <id>", "Reuse a previous delivery ID (X-GitHub-Delivery), to check the surface handles a repeat once").option("--payload <file>", "Send and sign this JSON object instead of the built fixture; - reads it from stdin").option("--json", "Output as JSON").action(async (id, options) => {
33080
33947
  const apiKey = await ensureAuth();
33081
33948
  if (!apiKey) return;
33082
33949
  try {
33083
33950
  const pullRequest = parsePositiveInteger$1(options.pr, "--pr");
33084
33951
  const repeat = parsePositiveInteger$1(options.repeat, "--repeat");
33085
33952
  if (repeat > 3) throw new Error("--repeat must not exceed 3");
33953
+ if (options.deliveryId !== void 0 && !DELIVERY_ID_PATTERN.test(options.deliveryId)) throw new Error("--delivery-id must be 1-128 letters, digits, dots, underscores, colons or hyphens");
33954
+ const payload = options.payload !== void 0 ? await readPayloadObject(options.payload) : void 0;
33086
33955
  const result = await createCliClient(apiKey).post(`/surfaces/${encodeURIComponent(id)}/test-deliveries`, {
33087
33956
  fixture: options.fixture,
33088
33957
  repository: options.repo,
33089
33958
  pullRequest,
33090
33959
  repeat,
33091
- ...options.planningPath ? { planningPath: options.planningPath } : {}
33960
+ ...options.planningPath ? { planningPath: options.planningPath } : {},
33961
+ ...options.deliveryId ? { deliveryId: options.deliveryId } : {},
33962
+ ...payload ? { payload } : {}
33092
33963
  });
33093
33964
  if (options.json) {
33094
33965
  printJson(result);
@@ -33099,7 +33970,7 @@ surfacesCommand.command("test <id>").description("Send a provider fixture throug
33099
33970
  console.log(` Fixture: ${result.fixture}`);
33100
33971
  console.log(` Source: ${result.repository}#${result.pullRequest}`);
33101
33972
  console.log(` Delivery: ${result.deliveryId}`);
33102
- for (const attempt of result.attempts) console.log(` Attempt ${attempt.attempt}: HTTP ${attempt.status}`);
33973
+ for (const attempt of result.attempts) console.log(` ${formatDeliveryAttempt(attempt)}`);
33103
33974
  } catch (error) {
33104
33975
  const message = error instanceof Error ? error.message : "Unknown error";
33105
33976
  console.error(chalk.red(`Failed to test surface: ${message}`));
@@ -34100,21 +34971,16 @@ function InitApp({ apiUrl, onComplete }) {
34100
34971
  loadingLabel: storedKeyNotice ? `${storedKeyNotice} Opening browser to sign in instead...` : "Opening browser to sign in..."
34101
34972
  }));
34102
34973
  try {
34103
- const sessionToken = await new OAuthManager({
34104
- clerkPublishableKey: getClerkPublishableKey(),
34105
- dashboardUrl: getDashboardUrl()
34106
- }).authenticate("sign-in");
34107
- setState((s) => ({
34108
- ...s,
34109
- loadingLabel: "Exchanging credentials..."
34110
- }));
34111
- const { key, userId, orgId } = await new ApiKeyManager().exchangeSessionForApiKey(sessionToken, apiUrl || getApiUrl());
34112
- await store.saveCredentials({
34113
- apiKey: key,
34114
- userId,
34115
- orgId,
34116
- apiUrl: apiUrl || getApiUrl()
34974
+ const login = await browserLogin({
34975
+ apiUrl,
34976
+ onProgress: (label) => setState((s) => ({
34977
+ ...s,
34978
+ loadingLabel: label
34979
+ }))
34117
34980
  });
34981
+ const mismatch = orgMismatchWarning(login);
34982
+ if (mismatch) throw new Error(mismatch);
34983
+ const { apiKey: key, userId, orgId } = login;
34118
34984
  setState((s) => ({
34119
34985
  ...s,
34120
34986
  apiKey: key,
@@ -37659,7 +38525,7 @@ function applyMaxLines(raw, maxLines) {
37659
38525
  function readFileContent(filePath, snapshots, liveTools, maxLines = 5e3) {
37660
38526
  try {
37661
38527
  return {
37662
- ...applyMaxLines(fs$2.readFileSync(filePath, "utf-8"), maxLines),
38528
+ ...applyMaxLines(fs$1.readFileSync(filePath, "utf-8"), maxLines),
37663
38529
  source: "disk"
37664
38530
  };
37665
38531
  } catch {}
@@ -37964,7 +38830,7 @@ const deployLifecycleParameters = {
37964
38830
  description: "Runtype destruction backstop. Omitted: Daytona 10m, Runtype Sandbox unlimited. Provider sleep/loss can end availability earlier."
37965
38831
  },
37966
38832
  sleepPolicy: {
37967
- description: "Idle compute policy, independent of retention. Legacy Runtype Sandbox supports idle only.",
38833
+ description: "Idle compute policy, independent of retention.",
37968
38834
  oneOf: [{
37969
38835
  type: "object",
37970
38836
  properties: { mode: { const: "never" } },
@@ -38087,7 +38953,7 @@ function createDeploySandboxLocalTool(client, debugMode) {
38087
38953
  let activeCfSandboxId;
38088
38954
  function createDeployCfSandboxLocalTool(client, debugMode) {
38089
38955
  return {
38090
- description: "Deploy a live Runtype Sandbox server preview. Omitted retention preserves the unlimited legacy backstop; explicit lifecycle policy requires rollout enablement. Provider sleep/loss can end availability earlier; the URL is not durable hosting.",
38956
+ description: "Deploy a live Runtype Sandbox server preview. Omitted retention preserves the unlimited legacy backstop; explicit lifecycle policy requires rollout enablement. Provider sleep/loss can end availability earlier; the URL is not durable hosting. A redeploy stops the previous server; when effectivePolicy.previewUrlChanged is true, re-register any saved copy of the URL.",
38091
38957
  parametersSchema: {
38092
38958
  type: "object",
38093
38959
  properties: {
@@ -38096,6 +38962,10 @@ function createDeployCfSandboxLocalTool(client, debugMode) {
38096
38962
  description: "Code to deploy (main entry point file)"
38097
38963
  },
38098
38964
  ...deployLifecycleParameters,
38965
+ sleepPolicy: {
38966
+ ...deployLifecycleParameters.sleepPolicy,
38967
+ description: "Idle sleep, or { mode: \"never\" } to keep the container running until the retention deadline. \"never\" requires retention 10m, 1h or 24h."
38968
+ },
38099
38969
  packageJson: {
38100
38970
  type: "object",
38101
38971
  description: "package.json content as a JSON object, e.g. { \"dependencies\": { \"hono\": \"^4.0.0\" } }"
@@ -38189,14 +39059,14 @@ function createEventLogWriter(stateFilePath, initialSessionIndex) {
38189
39059
  let stream = null;
38190
39060
  function ensureDir() {
38191
39061
  if (!dirCreated) {
38192
- fs$2.mkdirSync(eventsDir, { recursive: true });
39062
+ fs$1.mkdirSync(eventsDir, { recursive: true });
38193
39063
  dirCreated = true;
38194
39064
  }
38195
39065
  }
38196
39066
  function ensureStream() {
38197
39067
  if (!stream || stream.destroyed) {
38198
39068
  ensureDir();
38199
- stream = fs$2.createWriteStream(currentPath, { flags: "a" });
39069
+ stream = fs$1.createWriteStream(currentPath, { flags: "a" });
38200
39070
  stream.on("error", () => {});
38201
39071
  }
38202
39072
  }
@@ -38225,8 +39095,8 @@ function createEventLogWriter(stateFilePath, initialSessionIndex) {
38225
39095
  * Returns an empty array if the file doesn't exist. */
38226
39096
  function readSessionEventLog(stateFilePath, sessionIndex) {
38227
39097
  const logPath = sessionEventLogPath(stateFilePath, sessionIndex);
38228
- if (!fs$2.existsSync(logPath)) return [];
38229
- const content = fs$2.readFileSync(logPath, "utf-8");
39098
+ if (!fs$1.existsSync(logPath)) return [];
39099
+ const content = fs$1.readFileSync(logPath, "utf-8");
38230
39100
  const events = [];
38231
39101
  for (const line of content.split("\n")) {
38232
39102
  if (!line.trim()) continue;
@@ -38261,7 +39131,7 @@ function isTreeLogEntry(value) {
38261
39131
  function loadTreeLog(filePath) {
38262
39132
  let raw;
38263
39133
  try {
38264
- raw = fs$2.readFileSync(filePath, "utf-8");
39134
+ raw = fs$1.readFileSync(filePath, "utf-8");
38265
39135
  } catch {
38266
39136
  return null;
38267
39137
  }
@@ -38298,8 +39168,8 @@ function loadTreeLog(filePath) {
38298
39168
  };
38299
39169
  }
38300
39170
  function appendLine(log, entry) {
38301
- fs$2.mkdirSync(path$2.dirname(log.filePath), { recursive: true });
38302
- fs$2.appendFileSync(log.filePath, JSON.stringify(entry) + "\n");
39171
+ fs$1.mkdirSync(path$2.dirname(log.filePath), { recursive: true });
39172
+ fs$1.appendFileSync(log.filePath, JSON.stringify(entry) + "\n");
38303
39173
  log.entries.push(entry);
38304
39174
  if (entry.type !== "head") {
38305
39175
  log.byId.set(entry.id, entry);
@@ -38364,7 +39234,7 @@ function appendArtifact(log, payload) {
38364
39234
  type: "artifact",
38365
39235
  payload: {
38366
39236
  ...payload,
38367
- host: payload.host || os$2.hostname()
39237
+ host: payload.host || os$1.hostname()
38368
39238
  }
38369
39239
  };
38370
39240
  appendLine(log, entry);
@@ -41440,7 +42310,7 @@ function MarathonApp({ taskName, agentId, model, sandbox, goal, initialSessionCo
41440
42310
  setShowSessionMenu(false);
41441
42311
  try {
41442
42312
  let json;
41443
- if (stateFilePath && fs$2.existsSync(stateFilePath)) json = fs$2.readFileSync(stateFilePath, "utf-8");
42313
+ if (stateFilePath && fs$1.existsSync(stateFilePath)) json = fs$1.readFileSync(stateFilePath, "utf-8");
41444
42314
  else json = JSON.stringify({
41445
42315
  sessionSnapshots,
41446
42316
  content: state.content,
@@ -41463,8 +42333,8 @@ function MarathonApp({ taskName, agentId, model, sandbox, goal, initialSessionCo
41463
42333
  setShowSessionMenu(false);
41464
42334
  try {
41465
42335
  let json;
41466
- if (stateFilePath && fs$2.existsSync(stateFilePath)) {
41467
- const raw = fs$2.readFileSync(stateFilePath, "utf-8");
42336
+ if (stateFilePath && fs$1.existsSync(stateFilePath)) {
42337
+ const raw = fs$1.readFileSync(stateFilePath, "utf-8");
41468
42338
  const parsed = JSON.parse(raw);
41469
42339
  json = JSON.stringify(compactMarathonStateJsonForContextExport(parsed), null, 2);
41470
42340
  } else {
@@ -41490,7 +42360,7 @@ function MarathonApp({ taskName, agentId, model, sandbox, goal, initialSessionCo
41490
42360
  ]);
41491
42361
  const handleOpenStateFile = useCallback(() => {
41492
42362
  setShowSessionMenu(false);
41493
- if (stateFilePath && fs$2.existsSync(stateFilePath)) {
42363
+ if (stateFilePath && fs$1.existsSync(stateFilePath)) {
41494
42364
  open(stateFilePath);
41495
42365
  showFlash("Opening state file...");
41496
42366
  } else showFlash("No state file yet");
@@ -41885,7 +42755,7 @@ function MarathonApp({ taskName, agentId, model, sandbox, goal, initialSessionCo
41885
42755
  const file = detailFile ?? trackedFiles[fileCursor];
41886
42756
  if (file) {
41887
42757
  const filePath = path$2.resolve(file.path);
41888
- if (fs$2.existsSync(filePath)) {
42758
+ if (fs$1.existsSync(filePath)) {
41889
42759
  open(filePath);
41890
42760
  showFlash(`Opening ${file.path}`);
41891
42761
  } else showFlash("File not found on disk");
@@ -42400,7 +43270,7 @@ function MarathonApp({ taskName, agentId, model, sandbox, goal, initialSessionCo
42400
43270
  onOpenDashboard: handleOpenDashboard,
42401
43271
  onClose: () => setShowSessionMenu(false),
42402
43272
  hasDashboard: !!agentPageUrl,
42403
- hasStateFile: !!stateFilePath && fs$2.existsSync(stateFilePath)
43273
+ hasStateFile: !!stateFilePath && fs$1.existsSync(stateFilePath)
42404
43274
  })
42405
43275
  }),
42406
43276
  showUpgradeModal && upgradePrompt && /* @__PURE__ */ jsx(Box, {
@@ -42941,28 +43811,6 @@ function setMarathonTitle() {
42941
43811
  setTerminalTitle(`Runtype: Marathon (${getFolderName()})`);
42942
43812
  }
42943
43813
  //#endregion
42944
- //#region src/config/paths.ts
42945
- /** Root state directory: RUNTYPE_STATE_DIR or ~/.runtype */
42946
- function getRuntypeHomeDir() {
42947
- return process.env.RUNTYPE_STATE_DIR || path$2.join(os$2.homedir(), ".runtype");
42948
- }
42949
- /** Per-project state dir: ~/.runtype/projects/{hash}/ */
42950
- function getProjectStateDir(projectDir) {
42951
- const dir = projectDir || process.cwd();
42952
- const hash = crypto$2.createHash("sha256").update(dir).digest("hex").slice(0, 12);
42953
- const stateDir = path$2.join(getRuntypeHomeDir(), "projects", hash);
42954
- const breadcrumb = path$2.join(stateDir, "project-path.txt");
42955
- if (!fs$2.existsSync(breadcrumb)) {
42956
- fs$2.mkdirSync(stateDir, { recursive: true });
42957
- fs$2.writeFileSync(breadcrumb, dir);
42958
- }
42959
- return stateDir;
42960
- }
42961
- /** Marathon state dir: ~/.runtype/projects/{hash}/marathons/ */
42962
- function getMarathonStateDir(projectDir) {
42963
- return path$2.join(getProjectStateDir(projectDir), "marathons");
42964
- }
42965
- //#endregion
42966
43814
  //#region src/marathon/checkpoint.ts
42967
43815
  function defaultStateDir$2() {
42968
43816
  return getMarathonStateDir();
@@ -42983,14 +43831,14 @@ function ensureMarathonFileCheckpoint(taskName, targetPath, stateDir, stateFileP
42983
43831
  const normalizedTargetPath = path$2.resolve(targetPath);
42984
43832
  const relativeTargetPath = path$2.relative(process.cwd(), normalizedTargetPath);
42985
43833
  if (!relativeTargetPath || relativeTargetPath.startsWith("..")) return void 0;
42986
- if (!fs$2.existsSync(normalizedTargetPath)) return void 0;
42987
- if (!fs$2.statSync(normalizedTargetPath).isFile()) return void 0;
43834
+ if (!fs$1.existsSync(normalizedTargetPath)) return void 0;
43835
+ if (!fs$1.statSync(normalizedTargetPath).isFile()) return void 0;
42988
43836
  if (relativeTargetPath.replace(/\\/g, "/").startsWith(".runtype/")) return void 0;
42989
43837
  if (normalizedTargetPath.startsWith(getRuntypeHomeDir() + path$2.sep)) return void 0;
42990
43838
  const checkpointPath = resolveMarathonCheckpointPath(taskName, normalizedTargetPath, stateDir, stateFilePath);
42991
- if (fs$2.existsSync(checkpointPath)) return checkpointPath;
42992
- fs$2.mkdirSync(path$2.dirname(checkpointPath), { recursive: true });
42993
- fs$2.copyFileSync(normalizedTargetPath, checkpointPath);
43839
+ if (fs$1.existsSync(checkpointPath)) return checkpointPath;
43840
+ fs$1.mkdirSync(path$2.dirname(checkpointPath), { recursive: true });
43841
+ fs$1.copyFileSync(normalizedTargetPath, checkpointPath);
42994
43842
  return checkpointPath;
42995
43843
  }
42996
43844
  function restoreMarathonFileCheckpoint(taskName, targetPath, stateDir, stateFilePath) {
@@ -43001,13 +43849,13 @@ function restoreMarathonFileCheckpoint(taskName, targetPath, stateDir, stateFile
43001
43849
  error: `Cannot restore checkpoint outside the current repository: ${normalizedTargetPath}`
43002
43850
  };
43003
43851
  const checkpointPath = resolveMarathonCheckpointPath(taskName, normalizedTargetPath, stateDir, stateFilePath);
43004
- if (!fs$2.existsSync(checkpointPath)) return {
43852
+ if (!fs$1.existsSync(checkpointPath)) return {
43005
43853
  restored: false,
43006
43854
  checkpointPath,
43007
43855
  error: `No checkpoint found for ${normalizedTargetPath}`
43008
43856
  };
43009
- fs$2.mkdirSync(path$2.dirname(normalizedTargetPath), { recursive: true });
43010
- fs$2.copyFileSync(checkpointPath, normalizedTargetPath);
43857
+ fs$1.mkdirSync(path$2.dirname(normalizedTargetPath), { recursive: true });
43858
+ fs$1.copyFileSync(checkpointPath, normalizedTargetPath);
43011
43859
  return {
43012
43860
  restored: true,
43013
43861
  checkpointPath
@@ -43537,7 +44385,7 @@ function normalizeMarathonStatePath(candidatePath) {
43537
44385
  }
43538
44386
  function marathonStatePathExists(candidatePath) {
43539
44387
  if (!candidatePath) return false;
43540
- return fs$2.existsSync(path$2.resolve(candidatePath));
44388
+ return fs$1.existsSync(path$2.resolve(candidatePath));
43541
44389
  }
43542
44390
  function isMarathonArtifactStatePath(candidatePath) {
43543
44391
  if (!candidatePath) return false;
@@ -43698,7 +44546,7 @@ function sanitizeLoadedMarathonState(state) {
43698
44546
  }
43699
44547
  function loadState(filePath) {
43700
44548
  try {
43701
- const raw = fs$2.readFileSync(filePath, "utf-8");
44549
+ const raw = fs$1.readFileSync(filePath, "utf-8");
43702
44550
  return sanitizeLoadedMarathonState(JSON.parse(raw));
43703
44551
  } catch {
43704
44552
  return null;
@@ -43706,7 +44554,7 @@ function loadState(filePath) {
43706
44554
  }
43707
44555
  function saveState(filePath, state, options) {
43708
44556
  const dir = path$2.dirname(filePath);
43709
- fs$2.mkdirSync(dir, { recursive: true });
44557
+ fs$1.mkdirSync(dir, { recursive: true });
43710
44558
  const stateToWrite = options?.stripSnapshotEvents && state.sessionSnapshots?.length ? {
43711
44559
  ...state,
43712
44560
  sessionSnapshots: state.sessionSnapshots.map((snapshot) => ({
@@ -43714,7 +44562,7 @@ function saveState(filePath, state, options) {
43714
44562
  rawEvents: []
43715
44563
  }))
43716
44564
  } : state;
43717
- fs$2.writeFileSync(filePath, JSON.stringify(stateToWrite, null, 2));
44565
+ fs$1.writeFileSync(filePath, JSON.stringify(stateToWrite, null, 2));
43718
44566
  syncTreeLogWithState(filePath, state);
43719
44567
  }
43720
44568
  function extractRunTaskResumeState(state) {
@@ -43760,11 +44608,11 @@ function offsetNewContextCompactionSummaries(summaries, existingSummaryIds, sess
43760
44608
  function findStateFile(name, stateDir) {
43761
44609
  if (stateDir) return stateFilePath(name, stateDir);
43762
44610
  const homePath = stateFilePath(name, getMarathonStateDir());
43763
- if (fs$2.existsSync(homePath)) return homePath;
44611
+ if (fs$1.existsSync(homePath)) return homePath;
43764
44612
  const oldMarathonPath = stateFilePath(name, path$2.join(process.cwd(), ".runtype", "marathons"));
43765
- if (fs$2.existsSync(oldMarathonPath)) return oldMarathonPath;
44613
+ if (fs$1.existsSync(oldMarathonPath)) return oldMarathonPath;
43766
44614
  const oldTaskPath = stateFilePath(name, path$2.join(process.cwd(), ".runtype", "tasks"));
43767
- if (fs$2.existsSync(oldTaskPath)) return oldTaskPath;
44615
+ if (fs$1.existsSync(oldTaskPath)) return oldTaskPath;
43768
44616
  return homePath;
43769
44617
  }
43770
44618
  function findLatestStateFile(stateDir) {
@@ -43775,11 +44623,11 @@ function findLatestStateFile(stateDir) {
43775
44623
  ];
43776
44624
  let latest = null;
43777
44625
  for (const dir of dirs) {
43778
- if (!fs$2.existsSync(dir)) continue;
43779
- const files = fs$2.readdirSync(dir).filter((f) => f.endsWith(".json"));
44626
+ if (!fs$1.existsSync(dir)) continue;
44627
+ const files = fs$1.readdirSync(dir).filter((f) => f.endsWith(".json"));
43780
44628
  for (const file of files) {
43781
44629
  const fullPath = path$2.join(dir, file);
43782
- const stat = fs$2.statSync(fullPath);
44630
+ const stat = fs$1.statSync(fullPath);
43783
44631
  if (!latest || stat.mtimeMs > latest.mtime) latest = {
43784
44632
  name: file.replace(".json", ""),
43785
44633
  filePath: fullPath,
@@ -43863,7 +44711,7 @@ async function resolveMarathonStateResolution(options, taskName, filePath, promp
43863
44711
  };
43864
44712
  }
43865
44713
  if (options.fresh) return { action: "fresh" };
43866
- if (!fs$2.existsSync(filePath)) return { action: "fresh" };
44714
+ if (!fs$1.existsSync(filePath)) return { action: "fresh" };
43867
44715
  if (!allowPrompt) throw new Error(`Local marathon state already exists at ${filePath}. Use --resume to continue it or --fresh to start over.`);
43868
44716
  const choice = await promptForChoice({
43869
44717
  reason: "existing-state",
@@ -44143,9 +44991,9 @@ function shouldIgnoreRepoEntry(entryPath) {
44143
44991
  function safeReadTextFile(filePath) {
44144
44992
  try {
44145
44993
  if (isSensitivePath(normalizeToolPath(filePath).replace(/\\/g, "/"))) return null;
44146
- const stat = fs$2.statSync(filePath);
44994
+ const stat = fs$1.statSync(filePath);
44147
44995
  if (!stat.isFile() || stat.size > 1048576) return null;
44148
- const buffer = fs$2.readFileSync(filePath);
44996
+ const buffer = fs$1.readFileSync(filePath);
44149
44997
  if (buffer.includes(0)) return null;
44150
44998
  return buffer.toString("utf-8");
44151
44999
  } catch {
@@ -44153,14 +45001,14 @@ function safeReadTextFile(filePath) {
44153
45001
  }
44154
45002
  }
44155
45003
  function walkRepo(startPath, visitor) {
44156
- if (!fs$2.existsSync(startPath) || !fs$2.statSync(startPath).isDirectory()) return;
45004
+ if (!fs$1.existsSync(startPath) || !fs$1.statSync(startPath).isDirectory()) return;
44157
45005
  const stack = [startPath];
44158
45006
  while (stack.length > 0) {
44159
45007
  const currentDir = stack.pop();
44160
45008
  if (!currentDir) continue;
44161
45009
  let entries;
44162
45010
  try {
44163
- entries = fs$2.readdirSync(currentDir, { withFileTypes: true });
45011
+ entries = fs$1.readdirSync(currentDir, { withFileTypes: true });
44164
45012
  } catch {
44165
45013
  continue;
44166
45014
  }
@@ -44200,7 +45048,7 @@ function buildTree(dirPath, maxDepth, depth = 0) {
44200
45048
  if (depth > maxDepth || shouldIgnoreRepoEntry(dirPath)) return [];
44201
45049
  let entries;
44202
45050
  try {
44203
- entries = fs$2.readdirSync(dirPath, { withFileTypes: true });
45051
+ entries = fs$1.readdirSync(dirPath, { withFileTypes: true });
44204
45052
  } catch (error) {
44205
45053
  const message = error instanceof Error ? error.message : String(error);
44206
45054
  return [`${" ".repeat(depth)}[error reading ${normalizeToolPath(dirPath)}: ${message}]`];
@@ -44237,8 +45085,8 @@ function resolveCanonicalToolPath(toolPath, allowMissing) {
44237
45085
  let remainingSegments = absolutePath.slice(canonicalPath.length).split(path$2.sep);
44238
45086
  let symlinksFollowed = 0;
44239
45087
  try {
44240
- if (fs$2.existsSync(absolutePath)) return {
44241
- canonicalPath: fs$2.realpathSync.native(absolutePath),
45088
+ if (fs$1.existsSync(absolutePath)) return {
45089
+ canonicalPath: fs$1.realpathSync.native(absolutePath),
44242
45090
  exists: true
44243
45091
  };
44244
45092
  if (!allowMissing) return null;
@@ -44250,25 +45098,25 @@ function resolveCanonicalToolPath(toolPath, allowMissing) {
44250
45098
  continue;
44251
45099
  }
44252
45100
  const candidatePath = path$2.join(canonicalPath, segment);
44253
- const entry = fs$2.lstatSync(candidatePath, { throwIfNoEntry: false });
45101
+ const entry = fs$1.lstatSync(candidatePath, { throwIfNoEntry: false });
44254
45102
  if (!entry) {
44255
45103
  canonicalPath = candidatePath;
44256
45104
  continue;
44257
45105
  }
44258
45106
  if (entry.isSymbolicLink()) {
44259
45107
  if (++symlinksFollowed > MAX_TOOL_PATH_SYMLINKS) return null;
44260
- const target = fs$2.readlinkSync(candidatePath);
45108
+ const target = fs$1.readlinkSync(candidatePath);
44261
45109
  const targetRoot = path$2.isAbsolute(target) ? path$2.parse(target).root : "";
44262
45110
  if (targetRoot) canonicalPath = targetRoot;
44263
45111
  remainingSegments = [...target.slice(targetRoot.length).split(path$2.sep), ...remainingSegments];
44264
45112
  continue;
44265
45113
  }
44266
45114
  if (remainingSegments.length > 0 && !entry.isDirectory()) return null;
44267
- canonicalPath = fs$2.realpathSync.native(candidatePath);
45115
+ canonicalPath = fs$1.realpathSync.native(candidatePath);
44268
45116
  }
44269
45117
  return {
44270
45118
  canonicalPath,
44271
- exists: fs$2.existsSync(canonicalPath)
45119
+ exists: fs$1.existsSync(canonicalPath)
44272
45120
  };
44273
45121
  } catch {
44274
45122
  return null;
@@ -44294,7 +45142,7 @@ function resolveToolPath(toolPath, options = {}) {
44294
45142
  ok: false,
44295
45143
  error: `Path does not exist: ${requestedPath}`
44296
45144
  };
44297
- const workspaceRoot = fs$2.realpathSync.native(process.cwd());
45145
+ const workspaceRoot = fs$1.realpathSync.native(process.cwd());
44298
45146
  const extraRoots = (options.allowedRoots || []).map((rootPath) => canonicalizeAllowedRoot(rootPath));
44299
45147
  const matchedRoot = [...extraRoots, workspaceRoot].find((rootPath) => isPathWithinRoot(resolved.canonicalPath, rootPath));
44300
45148
  if (!matchedRoot) return {
@@ -44315,7 +45163,7 @@ function resolveToolPath(toolPath, options = {}) {
44315
45163
  }
44316
45164
  }
44317
45165
  if (resolved.exists) {
44318
- const stat = fs$2.statSync(resolved.canonicalPath);
45166
+ const stat = fs$1.statSync(resolved.canonicalPath);
44319
45167
  if (options.requireFile && !stat.isFile()) return {
44320
45168
  ok: false,
44321
45169
  error: `Path is not a file: ${requestedPath}`
@@ -44363,7 +45211,7 @@ function createDefaultLocalTools(context) {
44363
45211
  allowedRoots: allowedReadRoots
44364
45212
  });
44365
45213
  if (!resolvedPath.ok) return `Error: ${resolvedPath.error}`;
44366
- return fs$2.readFileSync(resolvedPath.resolvedPath, "utf-8");
45214
+ return fs$1.readFileSync(resolvedPath.resolvedPath, "utf-8");
44367
45215
  }
44368
45216
  },
44369
45217
  write_file: {
@@ -44386,10 +45234,10 @@ function createDefaultLocalTools(context) {
44386
45234
  const resolvedPath = resolveToolPath(String(args.path || ""), { allowMissing: true });
44387
45235
  if (!resolvedPath.ok) return `Error: ${resolvedPath.error}`;
44388
45236
  const content = String(args.content || "");
44389
- if (fs$2.existsSync(resolvedPath.resolvedPath) && fs$2.statSync(resolvedPath.resolvedPath).isDirectory()) return `Error: Path is a directory: ${String(args.path || "")}`;
45237
+ if (fs$1.existsSync(resolvedPath.resolvedPath) && fs$1.statSync(resolvedPath.resolvedPath).isDirectory()) return `Error: Path is a directory: ${String(args.path || "")}`;
44390
45238
  const dir = path$2.dirname(resolvedPath.resolvedPath);
44391
- fs$2.mkdirSync(dir, { recursive: true });
44392
- fs$2.writeFileSync(resolvedPath.resolvedPath, content);
45239
+ fs$1.mkdirSync(dir, { recursive: true });
45240
+ fs$1.writeFileSync(resolvedPath.resolvedPath, content);
44393
45241
  return "ok";
44394
45242
  }
44395
45243
  },
@@ -44405,7 +45253,7 @@ function createDefaultLocalTools(context) {
44405
45253
  execute: async (args) => {
44406
45254
  const resolvedPath = resolveToolPath(String(args.path || "."), { requireDirectory: true });
44407
45255
  if (!resolvedPath.ok) return `Error: ${resolvedPath.error}`;
44408
- return fs$2.readdirSync(resolvedPath.resolvedPath, { withFileTypes: true }).filter((entry) => !shouldIgnoreRepoEntry(path$2.join(resolvedPath.resolvedPath, entry.name))).map((entry) => entry.name).join("\n");
45256
+ return fs$1.readdirSync(resolvedPath.resolvedPath, { withFileTypes: true }).filter((entry) => !shouldIgnoreRepoEntry(path$2.join(resolvedPath.resolvedPath, entry.name))).map((entry) => entry.name).join("\n");
44409
45257
  }
44410
45258
  },
44411
45259
  search_repo: {
@@ -44569,8 +45417,8 @@ function createCheckpointedWriteFileTool(taskName, stateDir, planPath, stateFile
44569
45417
  const content = String(args.content || "");
44570
45418
  ensureMarathonFileCheckpoint(taskName, resolvedPath.resolvedPath, stateDir, stateFilePath);
44571
45419
  const dir = path$2.dirname(resolvedPath.resolvedPath);
44572
- fs$2.mkdirSync(dir, { recursive: true });
44573
- fs$2.writeFileSync(resolvedPath.resolvedPath, content);
45420
+ fs$1.mkdirSync(dir, { recursive: true });
45421
+ fs$1.writeFileSync(resolvedPath.resolvedPath, content);
44574
45422
  const ext = path$2.extname(resolvedPath.resolvedPath).toLowerCase();
44575
45423
  if (ext === ".js" || ext === ".jsx") {
44576
45424
  const syntaxError = quickJsSyntaxCheck(content);
@@ -44646,7 +45494,7 @@ function createEditFileTool(taskName, stateDir, planPath, stateFilePath) {
44646
45494
  if (oldString === newString) return "Error: old_string and new_string are identical";
44647
45495
  let content;
44648
45496
  try {
44649
- content = fs$2.readFileSync(resolvedPath.resolvedPath, "utf-8");
45497
+ content = fs$1.readFileSync(resolvedPath.resolvedPath, "utf-8");
44650
45498
  } catch {
44651
45499
  return `Error: Could not read file: ${String(args.path || "")}`;
44652
45500
  }
@@ -44657,7 +45505,7 @@ function createEditFileTool(taskName, stateDir, planPath, stateFilePath) {
44657
45505
  }
44658
45506
  ensureMarathonFileCheckpoint(taskName, resolvedPath.resolvedPath, stateDir, stateFilePath);
44659
45507
  const updated = replaceAll ? content.split(oldString).join(newString) : content.replace(oldString, newString);
44660
- fs$2.writeFileSync(resolvedPath.resolvedPath, updated);
45508
+ fs$1.writeFileSync(resolvedPath.resolvedPath, updated);
44661
45509
  const ext = path$2.extname(resolvedPath.resolvedPath).toLowerCase();
44662
45510
  if (ext === ".js" || ext === ".jsx") {
44663
45511
  const syntaxError = quickJsSyntaxCheck(updated);
@@ -44709,13 +45557,13 @@ function createReadOffloadedOutputTool(taskName, stateDir, stateFilePath) {
44709
45557
  };
44710
45558
  const idEmbeddedSha256 = /^art_([0-9a-f]{64})$/.exec(outputId)?.[1];
44711
45559
  const outputPath = path$2.join(getOffloadedOutputDir(taskName, stateDir, stateFilePath), `${outputId}.txt`);
44712
- if (!fs$2.existsSync(outputPath) || !fs$2.statSync(outputPath).isFile()) {
45560
+ if (!fs$1.existsSync(outputPath) || !fs$1.statSync(outputPath).isFile()) {
44713
45561
  const resolvedStateFilePath = getStateFilePathForTask(taskName, stateDir, stateFilePath);
44714
45562
  const log = loadTreeLog(treeLogPathForStateFile(resolvedStateFilePath));
44715
45563
  const artifact = log ? findArtifactPayload(log, outputId) : void 0;
44716
45564
  if (artifact) {
44717
45565
  const ledgerPath = path$2.resolve(path$2.dirname(treeLogPathForStateFile(resolvedStateFilePath)), artifact.relativePath);
44718
- if (fs$2.existsSync(ledgerPath) && fs$2.statSync(ledgerPath).isFile()) return verifyContent(fs$2.readFileSync(ledgerPath, "utf-8"), artifact.sha256);
45566
+ if (fs$1.existsSync(ledgerPath) && fs$1.statSync(ledgerPath).isFile()) return verifyContent(fs$1.readFileSync(ledgerPath, "utf-8"), artifact.sha256);
44719
45567
  return [
44720
45568
  `Error: offloaded output recorded in the context ledger but missing locally: ${outputId}`,
44721
45569
  artifact.host ? `Stored on host: ${artifact.host}` : void 0,
@@ -44724,7 +45572,7 @@ function createReadOffloadedOutputTool(taskName, stateDir, stateFilePath) {
44724
45572
  }
44725
45573
  return `Error: offloaded output not found: ${outputId}`;
44726
45574
  }
44727
- return verifyContent(fs$2.readFileSync(outputPath, "utf-8"), idEmbeddedSha256);
45575
+ return verifyContent(fs$1.readFileSync(outputPath, "utf-8"), idEmbeddedSha256);
44728
45576
  }
44729
45577
  };
44730
45578
  }
@@ -44908,7 +45756,7 @@ function resolveLedgerToolResult(tool, stateFilePath, log) {
44908
45756
  if (!artifact) return result;
44909
45757
  const artifactPath = path$2.resolve(path$2.dirname(treeLogPathForStateFile(stateFilePath)), artifact.relativePath);
44910
45758
  try {
44911
- if (fs$2.existsSync(artifactPath) && fs$2.statSync(artifactPath).isFile()) return fs$2.readFileSync(artifactPath, "utf-8");
45759
+ if (fs$1.existsSync(artifactPath) && fs$1.statSync(artifactPath).isFile()) return fs$1.readFileSync(artifactPath, "utf-8");
44912
45760
  } catch {}
44913
45761
  return result;
44914
45762
  }
@@ -45122,8 +45970,8 @@ function offloadToolOutput(taskName, toolName, output, options = {}, stateDir) {
45122
45970
  const dir = artifactStoreDirForStateFile(stateFilePath);
45123
45971
  const fileName = `${outputId}.txt`;
45124
45972
  const filePath = path$2.join(dir, fileName);
45125
- fs$2.mkdirSync(dir, { recursive: true });
45126
- if (!fs$2.existsSync(filePath)) fs$2.writeFileSync(filePath, output, "utf-8");
45973
+ fs$1.mkdirSync(dir, { recursive: true });
45974
+ if (!fs$1.existsSync(filePath)) fs$1.writeFileSync(filePath, output, "utf-8");
45127
45975
  recordOffloadedArtifact({
45128
45976
  taskName,
45129
45977
  toolName,
@@ -45273,14 +46121,14 @@ const RULES_DIR = ".marathon/rules";
45273
46121
  function loadRules(cwd) {
45274
46122
  const baseCwd = cwd || process.cwd();
45275
46123
  const rulesDir = path$2.resolve(baseCwd, RULES_DIR);
45276
- if (!fs$2.existsSync(rulesDir)) return [];
46124
+ if (!fs$1.existsSync(rulesDir)) return [];
45277
46125
  const rules = [];
45278
46126
  try {
45279
- const entries = fs$2.readdirSync(rulesDir).filter((f) => f.endsWith(".md"));
46127
+ const entries = fs$1.readdirSync(rulesDir).filter((f) => f.endsWith(".md"));
45280
46128
  for (const entry of entries) {
45281
46129
  const filePath = path$2.join(rulesDir, entry);
45282
46130
  try {
45283
- const parsed = parseRuleFile(fs$2.readFileSync(filePath, "utf-8"));
46131
+ const parsed = parseRuleFile(fs$1.readFileSync(filePath, "utf-8"));
45284
46132
  rules.push({
45285
46133
  ...parsed,
45286
46134
  filePath
@@ -45416,7 +46264,7 @@ function resolveErrorHandlingForPhase(phase, cliFallbackModel, milestoneFallback
45416
46264
  //#region src/marathon/playbook-loader.ts
45417
46265
  const PLAYBOOKS_DIR = ".runtype/marathons/playbooks";
45418
46266
  function getCandidatePaths(nameOrPath, cwd) {
45419
- const home = os$2.homedir();
46267
+ const home = os$1.homedir();
45420
46268
  return [
45421
46269
  path$2.resolve(cwd, nameOrPath),
45422
46270
  path$2.resolve(cwd, PLAYBOOKS_DIR, `${nameOrPath}.yaml`),
@@ -45456,7 +46304,7 @@ async function loadModulePlaybook(filePath) {
45456
46304
  return config;
45457
46305
  }
45458
46306
  function parsePlaybookFile(filePath) {
45459
- const raw = fs$2.readFileSync(filePath, "utf-8");
46307
+ const raw = fs$1.readFileSync(filePath, "utf-8");
45460
46308
  if (path$2.extname(filePath).toLowerCase() === ".json") return JSON.parse(raw);
45461
46309
  return parse(raw);
45462
46310
  }
@@ -45518,7 +46366,7 @@ async function loadPlaybookPlugins(plugins, playbookFilePath, playbookName) {
45518
46366
  const resolved = path$2.resolve(baseDir, plugin);
45519
46367
  if (resolved !== baseDir && !resolved.startsWith(baseDir + path$2.sep)) throw new Error(`Playbook '${playbookName}': plugin "${plugin}" resolves outside the playbook's directory. Keep plugin modules next to the playbook.`);
45520
46368
  if (!PLUGIN_EXTENSIONS.has(path$2.extname(resolved).toLowerCase())) throw new Error(`Playbook '${playbookName}': plugin "${plugin}" must be a .js, .mjs, or .cjs module.`);
45521
- if (!fs$2.existsSync(resolved)) throw new Error(`Playbook '${playbookName}': plugin "${plugin}" not found at ${resolved}.`);
46369
+ if (!fs$1.existsSync(resolved)) throw new Error(`Playbook '${playbookName}': plugin "${plugin}" not found at ${resolved}.`);
45522
46370
  const mod = await import(pathToFileURL(resolved).href);
45523
46371
  if (typeof mod.default !== "function") throw new Error(`Playbook '${playbookName}': plugin "${plugin}" must export a default function: export default ({ registerWorkflowHook }) => { ... }`);
45524
46372
  await mod.default({ registerWorkflowHook });
@@ -45551,7 +46399,7 @@ function toWorkflowConfig(config) {
45551
46399
  async function loadPlaybook(nameOrPath, cwd) {
45552
46400
  const candidates = getCandidatePaths(nameOrPath, cwd || process.cwd());
45553
46401
  for (const candidate of candidates) {
45554
- if (!fs$2.existsSync(candidate) || fs$2.statSync(candidate).isDirectory()) continue;
46402
+ if (!fs$1.existsSync(candidate) || fs$1.statSync(candidate).isDirectory()) continue;
45555
46403
  const ext = path$2.extname(candidate).toLowerCase();
45556
46404
  const config = MODULE_PLAYBOOK_EXTENSIONS.has(ext) ? await loadModulePlaybook(candidate) : parsePlaybookFile(candidate);
45557
46405
  validatePlaybook(config, candidate);
@@ -45943,7 +46791,7 @@ async function taskAction(agent, options) {
45943
46791
  forcedResumeFilePath = stateResolution.filePath;
45944
46792
  } else {
45945
46793
  resumeRequested = false;
45946
- if (options.fresh && fs$2.existsSync(filePath)) {
46794
+ if (options.fresh && fs$1.existsSync(filePath)) {
45947
46795
  if (useStartupShell) setStartupStatus("starting fresh");
45948
46796
  else console.log(chalk.gray(`Starting fresh and ignoring saved local state at ${filePath}`));
45949
46797
  }
@@ -48777,17 +49625,17 @@ const deployCommand = new Command("deploy").description("Export an agent and sca
48777
49625
  }
48778
49626
  slugSet.add(slug);
48779
49627
  }
48780
- fs$2.mkdirSync(path$2.join(outDir, "agents"), { recursive: true });
48781
- fs$2.mkdirSync(path$2.join(outDir, "packages"), { recursive: true });
49628
+ fs$1.mkdirSync(path$2.join(outDir, "agents"), { recursive: true });
49629
+ fs$1.mkdirSync(path$2.join(outDir, "packages"), { recursive: true });
48782
49630
  for (const { name, def } of agentDefs) {
48783
49631
  const filename = `${slugify(name)}.json`;
48784
- fs$2.writeFileSync(path$2.join(outDir, "agents", filename), JSON.stringify(def, null, 2));
49632
+ fs$1.writeFileSync(path$2.join(outDir, "agents", filename), JSON.stringify(def, null, 2));
48785
49633
  console.log(` Wrote agents/${filename}`);
48786
49634
  }
48787
49635
  const agentNames = agentDefs.map((a) => a.name);
48788
49636
  let monorepoRoot = process.cwd();
48789
49637
  for (let i = 0; i < 8; i++) {
48790
- if (fs$2.existsSync(path$2.join(monorepoRoot, "pnpm-workspace.yaml"))) break;
49638
+ if (fs$1.existsSync(path$2.join(monorepoRoot, "pnpm-workspace.yaml"))) break;
48791
49639
  const parent = path$2.dirname(monorepoRoot);
48792
49640
  if (parent === monorepoRoot) break;
48793
49641
  monorepoRoot = parent;
@@ -48803,35 +49651,35 @@ const deployCommand = new Command("deploy").description("Export an agent and sca
48803
49651
  const secretNames = Array.from(allSecrets);
48804
49652
  const optionalSettings = Array.from(allOptionalSettings.values());
48805
49653
  if (target === "cloudflare") {
48806
- fs$2.mkdirSync(path$2.join(outDir, "src"), { recursive: true });
48807
- fs$2.writeFileSync(path$2.join(outDir, "src", "index.ts"), workerIndexTs(agentSlugs));
48808
- fs$2.writeFileSync(path$2.join(outDir, "wrangler.toml"), wranglerToml(projectName));
48809
- fs$2.writeFileSync(path$2.join(outDir, "package.json"), workerPackageJson(projectName, "workspace:*"));
48810
- fs$2.writeFileSync(path$2.join(outDir, "tsconfig.json"), workerTsconfigJson());
49654
+ fs$1.mkdirSync(path$2.join(outDir, "src"), { recursive: true });
49655
+ fs$1.writeFileSync(path$2.join(outDir, "src", "index.ts"), workerIndexTs(agentSlugs));
49656
+ fs$1.writeFileSync(path$2.join(outDir, "wrangler.toml"), wranglerToml(projectName));
49657
+ fs$1.writeFileSync(path$2.join(outDir, "package.json"), workerPackageJson(projectName, "workspace:*"));
49658
+ fs$1.writeFileSync(path$2.join(outDir, "tsconfig.json"), workerTsconfigJson());
48811
49659
  const setupPath = path$2.join(outDir, "setup.sh");
48812
- fs$2.writeFileSync(setupPath, workerSetupSh(monorepoRoot));
48813
- fs$2.chmodSync(setupPath, 493);
48814
- fs$2.writeFileSync(path$2.join(outDir, "README.md"), workerReadme(agentNames, secretNames, optionalSettings));
49660
+ fs$1.writeFileSync(setupPath, workerSetupSh(monorepoRoot));
49661
+ fs$1.chmodSync(setupPath, 493);
49662
+ fs$1.writeFileSync(path$2.join(outDir, "README.md"), workerReadme(agentNames, secretNames, optionalSettings));
48815
49663
  } else if (target === "vercel") {
48816
- fs$2.mkdirSync(path$2.join(outDir, "api"), { recursive: true });
48817
- fs$2.writeFileSync(path$2.join(outDir, "api", "[[...route]].ts"), vercelRouteTs(agentSlugs));
48818
- fs$2.writeFileSync(path$2.join(outDir, "vercel.json"), vercelJson());
48819
- fs$2.writeFileSync(path$2.join(outDir, "package.json"), vercelPackageJson(projectName, "workspace:*"));
48820
- fs$2.writeFileSync(path$2.join(outDir, "tsconfig.json"), vercelTsconfigJson());
49664
+ fs$1.mkdirSync(path$2.join(outDir, "api"), { recursive: true });
49665
+ fs$1.writeFileSync(path$2.join(outDir, "api", "[[...route]].ts"), vercelRouteTs(agentSlugs));
49666
+ fs$1.writeFileSync(path$2.join(outDir, "vercel.json"), vercelJson());
49667
+ fs$1.writeFileSync(path$2.join(outDir, "package.json"), vercelPackageJson(projectName, "workspace:*"));
49668
+ fs$1.writeFileSync(path$2.join(outDir, "tsconfig.json"), vercelTsconfigJson());
48821
49669
  const setupPath = path$2.join(outDir, "setup.sh");
48822
- fs$2.writeFileSync(setupPath, vercelSetupSh(monorepoRoot));
48823
- fs$2.chmodSync(setupPath, 493);
48824
- fs$2.writeFileSync(path$2.join(outDir, "README.md"), vercelReadme(agentNames, secretNames, optionalSettings));
49670
+ fs$1.writeFileSync(setupPath, vercelSetupSh(monorepoRoot));
49671
+ fs$1.chmodSync(setupPath, 493);
49672
+ fs$1.writeFileSync(path$2.join(outDir, "README.md"), vercelReadme(agentNames, secretNames, optionalSettings));
48825
49673
  } else {
48826
- fs$2.writeFileSync(path$2.join(outDir, "server.ts"), serverTs());
48827
- fs$2.writeFileSync(path$2.join(outDir, "Dockerfile"), dockerfile());
48828
- fs$2.writeFileSync(path$2.join(outDir, ".dockerignore"), dockerignore());
48829
- fs$2.writeFileSync(path$2.join(outDir, "tsconfig.json"), tsconfigJson());
48830
- fs$2.writeFileSync(path$2.join(outDir, "package.json"), packageJson(projectName, "workspace:*"));
49674
+ fs$1.writeFileSync(path$2.join(outDir, "server.ts"), serverTs());
49675
+ fs$1.writeFileSync(path$2.join(outDir, "Dockerfile"), dockerfile());
49676
+ fs$1.writeFileSync(path$2.join(outDir, ".dockerignore"), dockerignore());
49677
+ fs$1.writeFileSync(path$2.join(outDir, "tsconfig.json"), tsconfigJson());
49678
+ fs$1.writeFileSync(path$2.join(outDir, "package.json"), packageJson(projectName, "workspace:*"));
48831
49679
  const setupPath = path$2.join(outDir, "setup.sh");
48832
- fs$2.writeFileSync(setupPath, setupSh(monorepoRoot));
48833
- fs$2.chmodSync(setupPath, 493);
48834
- fs$2.writeFileSync(path$2.join(outDir, "README.md"), readme(agentNames, secretNames, optionalSettings));
49680
+ fs$1.writeFileSync(setupPath, setupSh(monorepoRoot));
49681
+ fs$1.chmodSync(setupPath, 493);
49682
+ fs$1.writeFileSync(path$2.join(outDir, "README.md"), readme(agentNames, secretNames, optionalSettings));
48835
49683
  }
48836
49684
  console.log("");
48837
49685
  console.log(chalk.green(`✓ Scaffold written to ${outDir}`));
@@ -48900,13 +49748,13 @@ const agentExportRuntimeCommand = new Command("export-runtime").description("Exp
48900
49748
  if (options.format === "json") {
48901
49749
  const exported = await client.agents.exportRuntime(id, selector.params);
48902
49750
  const body = `${JSON.stringify(exported, null, 2)}\n`;
48903
- if (options.output) fs$2.writeFileSync(path$2.resolve(options.output), body);
49751
+ if (options.output) fs$1.writeFileSync(path$2.resolve(options.output), body);
48904
49752
  else process.stdout.write(body);
48905
49753
  return;
48906
49754
  }
48907
49755
  const bundle = await client.agents.exportRuntimeBundle(id, selector.params);
48908
49756
  const outPath = path$2.resolve(options.output ?? bundle.fileName);
48909
- fs$2.writeFileSync(outPath, bundle.bytes);
49757
+ fs$1.writeFileSync(outPath, bundle.bytes);
48910
49758
  console.log(JSON.stringify({
48911
49759
  path: outPath,
48912
49760
  bytes: bundle.bytes.byteLength,
@@ -56436,7 +57284,7 @@ npm install -g @runtypelabs/cli
56436
57284
  runtype auth status
56437
57285
  \`\`\`
56438
57286
 
56439
- Reuse an installed CLI rather than reinstalling it. If status reports \`authenticated\` with \`accountState: "full"\`, skip to step 3. If it reports a pending signup, follow its \`next\` command instead of registering again. If \`RUNTYPE_API_KEY\` is configured, verify it with \`runtype auth whoami --no-tty\` and reuse that account before following stored signup state: \`auth status\` only inspects stored credentials. Never print the key.
57287
+ Reuse an installed CLI rather than reinstalling it. If status reports \`authenticated\` with \`accountState: "full"\`, skip to step 3. If it reports a pending signup, follow its \`next\` command instead of registering again. If it reports \`unauthenticated\` with a non-null \`requestedOrgId\`, this directory is linked to an organization (\`.runtype/project.json\`, \`--org\`, or \`RUNTYPE_ORG\`) that has no stored login: do not register a new account. Have the user privately run \`runtype auth login --api-key <key>\` with a key from that organization (or \`runtype login\` in their own terminal), then verify with \`runtype auth whoami --no-tty\`. If the link looks wrong, confirm with the user before \`runtype unlink\`. If \`RUNTYPE_API_KEY\` is configured, verify it with \`runtype auth whoami --no-tty\` and reuse that account before following stored signup state: \`auth status\` only inspects stored credentials. Never print the key.
56440
57288
 
56441
57289
  ### 2. Sign the user up from the terminal
56442
57290
 
@@ -56525,7 +57373,9 @@ The signup commands wrap the agent auth protocol documented at \`https://runtype
56525
57373
  - \`runtype auth claim <email>\` — re-send or change the email for a pending signup
56526
57374
  - \`runtype auth verify <code>\` — finish signup with the 6-digit code
56527
57375
  - \`runtype auth login --api-key <key>\` — privately store an existing credential; not an account-creation command
56528
- - \`runtype auth status\` — inspect stored login and pending signup state
57376
+ - \`runtype auth status\` — inspect the login this directory uses, linked-org state, and pending signup state
57377
+ - \`runtype auth accounts\` — list stored logins (one per organization)
57378
+ - \`runtype link <orgId>\` — pin the current project to an organization (writes \`.runtype/project.json\`, no secrets)
56529
57379
  - \`runtype auth whoami --no-tty\` — verify the effective credential (environment key first, then stored login)
56530
57380
 
56531
57381
  A user-supplied \`RUNTYPE_API_KEY\` environment variable also authenticates API-backed CLI commands without a stored login; \`auth status\` still describes only the stored state. Never print a credential into the transcript or write one into a committed file.
@@ -58293,7 +59143,8 @@ const FORWARDED_REQUEST_HEADERS = /* @__PURE__ */ new Set([
58293
59143
  "authorization",
58294
59144
  "a2a-version",
58295
59145
  "content-type",
58296
- "x-api-key"
59146
+ "x-api-key",
59147
+ "x-runtype-identity"
58297
59148
  ]);
58298
59149
  const BLOCKED_AUTH_HEADERS = /* @__PURE__ */ new Set([
58299
59150
  "host",
@@ -58396,7 +59247,7 @@ function readIdentity(value) {
58396
59247
  function resolveDeviceListenName(requested) {
58397
59248
  const explicit = requested?.trim();
58398
59249
  if (explicit) return explicit.slice(0, 120);
58399
- const hostname = os$1.hostname().trim();
59250
+ const hostname = os.hostname().trim();
58400
59251
  if (!hostname) return "This computer";
58401
59252
  return hostname.slice(0, 120);
58402
59253
  }
@@ -58416,8 +59267,8 @@ async function signDeviceListenChallenge(identity, input) {
58416
59267
  });
58417
59268
  }
58418
59269
  function deviceListenEncryptionKey() {
58419
- const hostname = os$1.hostname();
58420
- const username = os$1.userInfo().username;
59270
+ const hostname = os.hostname();
59271
+ const username = os.userInfo().username;
58421
59272
  return createHash("sha256").update(`runtype-device-listen-${hostname}-${username}`).digest("hex");
58422
59273
  }
58423
59274
  function isCorruptStoreError(error) {
@@ -58971,8 +59822,8 @@ var MacOSDeviceProvider = class {
58971
59822
  constructor(options = {}) {
58972
59823
  this.platform = options.platform ?? process.platform;
58973
59824
  this.architecture = options.architecture ?? process.arch;
58974
- this.operatingSystemRelease = options.operatingSystemRelease ?? os$1.release();
58975
- this.homeDirectory = options.homeDirectory ?? os$1.homedir();
59825
+ this.operatingSystemRelease = options.operatingSystemRelease ?? os.release();
59826
+ this.homeDirectory = options.homeDirectory ?? os.homedir();
58976
59827
  this.workingDirectory = options.workingDirectory ?? process.cwd();
58977
59828
  this.environment = options.environment ?? process.env;
58978
59829
  this.sidecarTimeoutMs = options.sidecarTimeoutMs ?? DEFAULT_SIDECAR_TIMEOUT_MS;
@@ -59674,6 +60525,7 @@ function registerGlobalOptionForwarding(program) {
59674
60525
  const options = actionCommand.optsWithGlobals();
59675
60526
  const invocation = {
59676
60527
  apiUrl: apiUrls.at(-1),
60528
+ org: typeof options.org === "string" && options.org ? options.org : void 0,
59677
60529
  expectApiUrl: options.expectApiUrl,
59678
60530
  expectOrg: options.expectOrg,
59679
60531
  noInput: options.input === false || options.noInput === true,
@@ -59699,10 +60551,12 @@ function registerGlobalOptionForwarding(program) {
59699
60551
  //#region src/index.ts
59700
60552
  loadEnv();
59701
60553
  const program = new Command();
59702
- program.name("runtype").description("CLI for Runtype AI Platform").version(getCliVersion()).option("-v, --verbose", "Enable verbose output").option("--api-url <url>", "API URL (flag > env > saved config > credential endpoint > default)").option("--expect-api-url <url>", "Refuse actions unless the effective API URL matches").option("--expect-org <id>", "Require authenticated organization ID before API actions").option("--no-input", "Disable prompts; destructive delete/revoke/reset requires --yes").option("-y, --yes", "Explicitly consent to destructive commands in --no-input mode").option("--json", "Output as JSON").action(() => program.outputHelp());
60554
+ program.name("runtype").description("CLI for Runtype AI Platform").version(getCliVersion()).option("-v, --verbose", "Enable verbose output").option("--api-url <url>", "API URL (flag > env > project link > saved config > credential endpoint > default)").option("--org <id>", "Use the stored login for this organization (overrides RUNTYPE_ORG and .runtype/project.json)").option("--expect-api-url <url>", "Refuse actions unless the effective API URL matches").option("--expect-org <id>", "Require authenticated organization ID before API actions").option("--no-input", "Disable prompts; destructive delete/revoke/reset requires --yes").option("-y, --yes", "Explicitly consent to destructive commands in --no-input mode").option("--json", "Output as JSON").action(() => program.outputHelp());
59703
60555
  program.addCommand(initCommand);
59704
60556
  program.addCommand(loginCommand);
59705
60557
  program.addCommand(authCommand);
60558
+ program.addCommand(linkCommand);
60559
+ program.addCommand(unlinkCommand);
59706
60560
  program.addCommand(flowsCommand);
59707
60561
  program.addCommand(recordsCommand);
59708
60562
  program.addCommand(collectionsCommand);
@@ -59801,7 +60655,7 @@ run().catch((error) => {
59801
60655
  }
59802
60656
  });
59803
60657
  async function handleNoCommand() {
59804
- if (!await new CredentialStore().hasCredentials()) {
60658
+ if (!((await new CredentialStore().listAccounts()).length > 0) && !process.env.RUNTYPE_API_KEY) {
59805
60659
  console.log(chalk.cyan("\nWelcome to Runtype CLI!\n"));
59806
60660
  console.log("It looks like this is your first time. Run the setup wizard:");
59807
60661
  console.log(` ${chalk.green("runtype init")}\n`);