@runtypelabs/cli 2.47.9 → 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 +1814 -1061
  3. package/package.json +8 -7
package/dist/index.js CHANGED
@@ -2,32 +2,32 @@
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";
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";
23
28
  import { access, chmod, copyFile, mkdir, open as open$1, readFile, realpath, rename, rm, stat, writeFile } from "node:fs/promises";
24
29
  import TextInput from "ink-text-input";
25
- import SelectInput from "ink-select-input";
26
30
  import { execSync, spawn, spawnSync } from "node:child_process";
27
- import * as crypto$2 from "node:crypto";
28
- import { createHash, randomBytes, randomUUID } from "node:crypto";
29
- import * as os$2 from "node:os";
30
- import os$1, { constants as constants$1 } from "node:os";
31
31
  import { AnimatedVariant, LoadingAnimation, StartupGridIconCompactLightInverted } from "@runtypelabs/terminal-animations";
32
32
  import { createJiti } from "jiti";
33
33
  import micromatch from "micromatch";
@@ -35,6 +35,7 @@ import { parse } from "yaml";
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.
@@ -12511,7 +12140,7 @@ const CREATOR_TO_PROVIDER = {
12511
12140
  "unknown": "openai"
12512
12141
  };
12513
12142
  //#endregion
12514
- //#region ../shared/dist/chunk-DVTXa1NO.mjs
12143
+ //#region ../shared/dist/chunk-ykhTBHKy.mjs
12515
12144
  /**
12516
12145
  * The reserved model-facing name of the agent state write tool
12517
12146
  * (`packages/runtime/src/engine/state-write-tool.ts`). The model is told to call
@@ -12915,7 +12544,7 @@ function durableCrawlBudget(plan) {
12915
12544
  };
12916
12545
  }
12917
12546
  //#endregion
12918
- //#region ../shared/dist/chunk-2PJvKrV8.mjs
12547
+ //#region ../shared/dist/chunk-BcuKxfZa.mjs
12919
12548
  /**
12920
12549
  * Unwraps a single {{variableName}} template to its bare variable name, preserving array/object identity instead of stringifying via the template engine.
12921
12550
  * Returns source unchanged when it is not a lone {{...}} wrapper.
@@ -13022,7 +12651,7 @@ strictObject({
13022
12651
  additionalProperties: boolean().optional()
13023
12652
  }).superRefine((schema, ctx) => addSemanticIssues(schema, ctx));
13024
12653
  //#endregion
13025
- //#region ../shared/dist/chunk-DLCjOb1c.mjs
12654
+ //#region ../shared/dist/chunk-DoQn4_pS.mjs
13026
12655
  /** The Tenancy Assurance Levels, in the order `tenancyAssuranceLevelSchema` is built from. */
13027
12656
  const TENANCY_ASSURANCE_LEVELS = [
13028
12657
  "anonymous",
@@ -25176,7 +24805,7 @@ const AgentInputSchema = object({
25176
24805
  durability: agentDurabilityConfigSchema.optional(),
25177
24806
  tenancyStrategy: agentRuntimeConfigSchema.shape.tenancyStrategy
25178
24807
  }).superRefine((val, ctx) => {
25179
- 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;
25180
24809
  if (unsupported) {
25181
24810
  const [path, message] = unsupported;
25182
24811
  ctx.addIssue({
@@ -26043,7 +25672,10 @@ object({
26043
25672
  metering: _enum(["active", "legacy-untracked"]),
26044
25673
  sleepPolicy: sandboxDeploySleepPolicySchema.optional(),
26045
25674
  autoStopInterval: number$1().optional(),
26046
- 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.")
26047
25679
  });
26048
25680
  Object.freeze({
26049
25681
  /** Automatic attempts per incident, counted in OPERATIONS rather than requests. */
@@ -26578,97 +26210,550 @@ object({
26578
26210
  diagnosticId: string().max(64)
26579
26211
  });
26580
26212
  //#endregion
26581
- //#region src/auth/credential-store.ts
26582
- var credential_store_exports = /* @__PURE__ */ __exportAll({ CredentialStore: () => CredentialStore });
26583
- const CREDENTIALS_DIR = path$1.join(os.homedir(), ".runtype");
26584
- const CREDENTIALS_PATH = path$1.join(CREDENTIALS_DIR, "credentials.json");
26585
- function createConf(encryptionKey) {
26586
- return new Conf({
26587
- projectName: "runtype-cli",
26588
- projectSuffix: "",
26589
- configName: "credentials",
26590
- encryptionKey,
26591
- cwd: CREDENTIALS_DIR
26592
- });
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");
26593
26217
  }
26594
- function isCorruptStoreError$1(error) {
26595
- if (error instanceof SyntaxError) return true;
26596
- const message = error instanceof Error ? error.message : String(error);
26597
- 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;
26598
26229
  }
26599
- var CredentialStore = class {
26600
- config;
26601
- encryptionKey;
26602
- constructor() {
26603
- 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() {
26604
26288
  try {
26605
- this.config = createConf(this.encryptionKey);
26606
- } catch (error) {
26607
- if (isCorruptStoreError$1(error)) {
26608
- try {
26609
- if (fs$1.existsSync(CREDENTIALS_PATH)) fs$1.unlinkSync(CREDENTIALS_PATH);
26610
- } catch {}
26611
- this.config = createConf(this.encryptionKey);
26612
- } else throw error;
26289
+ const parsed = JSON.parse(fs.readFileSync(this.filePath, "utf8"));
26290
+ return parsed && typeof parsed === "object" ? parsed : {};
26291
+ } catch {
26292
+ return {};
26613
26293
  }
26614
26294
  }
26615
- /**
26616
- * Replace the entire backing store in one file write.
26617
- * Conf's store setter is typed read-only, hence the cast to assign it.
26618
- * 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.
26619
- */
26620
- replaceStore(data) {
26621
- 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 });
26622
26312
  }
26623
- getMachineKey() {
26624
- const hostname = os.hostname();
26625
- const username = os.userInfo().username;
26626
- return crypto$1.createHash("sha256").update(`runtype-cli-${hostname}-${username}`).digest("hex").substring(0, 32);
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
+ });
26491
+ }
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`;
26627
26510
  }
26628
26511
  /**
26629
- * Store credentials, optionally together with agent-auth claim state, in one write.
26630
- * Passing claimState here keeps the credential and claim token atomic, avoiding an orphaned pre-claim key.
26631
- * 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.
26632
26514
  */
26633
- async saveCredentials(credentials, claimState) {
26634
- const configData = {
26635
- apiKey: this.encrypt(credentials.apiKey),
26636
- userId: credentials.userId,
26637
- createdAt: (/* @__PURE__ */ new Date()).toISOString(),
26638
- 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: []
26639
26590
  };
26640
- if (credentials.orgId !== void 0) configData.orgId = credentials.orgId;
26641
- if (credentials.apiUrl !== void 0) configData.apiUrl = credentials.apiUrl;
26642
- if (claimState) Object.assign(configData, this.claimStateToConfig(claimState));
26643
- this.replaceStore(configData);
26644
- }
26645
- claimStateToConfig(state) {
26646
- const data = {
26647
- claimToken: this.encrypt(state.claimToken),
26648
- 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
+ }
26649
26701
  };
26650
- if (state.claimEmail !== void 0) data.claimEmail = state.claimEmail;
26651
- if (state.credentialExpires !== void 0) data.credentialExpires = state.credentialExpires;
26652
- if (state.claimTokenExpires !== void 0) data.claimTokenExpires = state.claimTokenExpires;
26653
- if (state.apiUrl !== void 0) data.claimApiUrl = state.apiUrl;
26654
- return data;
26655
26702
  }
26656
26703
  async getApiKey() {
26657
- const encrypted = this.config.get("apiKey");
26658
- if (!encrypted) return null;
26659
- this.config.set("lastUsed", (/* @__PURE__ */ new Date()).toISOString());
26660
- return this.decrypt(encrypted);
26704
+ return (await this.getCredentials())?.apiKey ?? null;
26661
26705
  }
26662
26706
  async getCredentials() {
26663
- const stored = this.config.store;
26664
- if (!stored.apiKey) return null;
26665
- return {
26666
- ...stored,
26667
- 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
26668
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;
26669
26737
  }
26738
+ /** Remove the active login (logout); other stored logins are untouched. */
26670
26739
  async clearCredentials() {
26671
- 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
+ };
26672
26757
  }
26673
26758
  /**
26674
26759
  * Persist agent-auth claim ceremony state alongside the pre-claim credential.
@@ -26676,54 +26761,129 @@ var CredentialStore = class {
26676
26761
  * The claim token is encrypted like the API key since it can mint the post-claim credential.
26677
26762
  */
26678
26763
  async saveClaimState(state) {
26679
- this.config.set(this.claimStateToConfig(state));
26680
- }
26681
- async getClaimState() {
26682
- const claimToken = this.config.get("claimToken");
26683
- const registrationId = this.config.get("registrationId");
26684
- if (!claimToken || !registrationId) return null;
26685
- return {
26686
- claimToken: this.decrypt(claimToken),
26687
- registrationId,
26688
- claimEmail: this.config.get("claimEmail"),
26689
- credentialExpires: this.config.get("credentialExpires"),
26690
- claimTokenExpires: this.config.get("claimTokenExpires"),
26691
- apiUrl: this.config.get("claimApiUrl")
26692
- };
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();
26693
26769
  }
26694
26770
  async clearClaimState() {
26695
- const store = { ...this.config.store };
26696
- delete store.claimToken;
26697
- delete store.registrationId;
26698
- delete store.claimEmail;
26699
- delete store.credentialExpires;
26700
- delete store.claimTokenExpires;
26701
- delete store.claimApiUrl;
26702
- this.replaceStore(store);
26771
+ const account = await this.pendingClaimAccount();
26772
+ if (!account) return;
26773
+ await this.accounts.setClaim(account.id, null);
26774
+ resetEffectiveContext();
26703
26775
  }
26704
- async hasCredentials() {
26705
- return !!this.config.get("apiKey");
26706
- }
26707
- encrypt(text) {
26708
- const algorithm = "aes-256-cbc";
26709
- const key = Buffer.from(this.encryptionKey);
26710
- const iv = crypto$1.randomBytes(16);
26711
- const cipher = crypto$1.createCipheriv(algorithm, key, iv);
26712
- let encrypted = cipher.update(text, "utf8", "hex");
26713
- encrypted += cipher.final("hex");
26714
- return iv.toString("hex") + ":" + encrypted;
26715
- }
26716
- decrypt(text) {
26717
- const algorithm = "aes-256-cbc";
26718
- const key = Buffer.from(this.encryptionKey);
26719
- const [ivHex, encrypted] = text.split(":");
26720
- const iv = Buffer.from(ivHex, "hex");
26721
- const decipher = crypto$1.createDecipheriv(algorithm, key, iv);
26722
- let decrypted = decipher.update(encrypted, "hex", "utf8");
26723
- decrypted += decipher.final("utf8");
26724
- 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";
26725
26807
  }
26726
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
+ }
26727
26887
  //#endregion
26728
26888
  //#region src/config/effective-context.ts
26729
26889
  var CliContextError = class extends Error {
@@ -26744,11 +26904,15 @@ function getCliConfig() {
26744
26904
  projectName: "runtype-cli",
26745
26905
  projectSuffix: "",
26746
26906
  configName: "config",
26747
- cwd: path$1.join(os.homedir(), ".runtype")
26907
+ cwd: getRuntypeHomeDir()
26748
26908
  });
26749
26909
  }
26750
26910
  function setInvocationOptions(options) {
26751
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() {
26752
26916
  effectiveContext = void 0;
26753
26917
  identityCheck = void 0;
26754
26918
  verifiedIdentity = void 0;
@@ -26767,11 +26931,12 @@ function normalizeApiUrl(value) {
26767
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.");
26768
26932
  return url.toString().replace(/\/+$/, "");
26769
26933
  }
26770
- /** API target precedence: explicit flag > environment > saved config > credential endpoint > production. */
26934
+ /** API target precedence: flag > environment > project link > saved config > credential endpoint > production. */
26771
26935
  function resolveApiTarget(values) {
26772
26936
  const [apiUrlSource, value] = [
26773
26937
  ["flag", values.flag],
26774
26938
  ["environment", values.environment],
26939
+ ["project", values.project],
26775
26940
  ["config", values.config],
26776
26941
  ["credential", values.credential],
26777
26942
  ["default", "https://api.runtype.com"]
@@ -26781,36 +26946,83 @@ function resolveApiTarget(values) {
26781
26946
  apiUrlSource
26782
26947
  };
26783
26948
  }
26784
- function resolveTarget(apiUrl, credentials) {
26785
- if (apiUrl && invocationOptions.apiUrl && normalizeApiUrl(apiUrl) !== normalizeApiUrl(invocationOptions.apiUrl)) throw new CliContextError("Conflicting explicit --api-url values; choose one API target.");
26786
- return resolveApiTarget({
26787
- flag: apiUrl || invocationOptions.apiUrl,
26788
- environment: process.env.RUNTYPE_API_URL,
26789
- config: getCliConfig().get("apiUrl"),
26790
- credential: credentials?.apiUrl
26791
- });
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
+ };
26792
27018
  }
26793
27019
  function getEffectiveApiUrl() {
26794
- return effectiveContext?.apiUrl ?? resolveTarget().apiUrl;
27020
+ return effectiveContext?.apiUrl ?? resolveApiTarget(explicitApiTarget(invocationOptions.apiUrl).target).apiUrl;
26795
27021
  }
26796
27022
  async function loadEffectiveContext(apiUrl) {
26797
- if (!effectiveContext) {
26798
- const credentials = await new CredentialStore().getCredentials();
26799
- const envKey = process.env.RUNTYPE_API_KEY;
26800
- effectiveContext = {
26801
- ...resolveTarget(apiUrl, credentials),
26802
- credentials,
26803
- apiKey: envKey || credentials?.apiKey,
26804
- credentialSource: envKey ? "environment" : credentials?.apiKey ? "stored" : "none"
26805
- };
26806
- } else if (apiUrl) {
26807
- const target = resolveTarget(apiUrl, effectiveContext.credentials);
26808
- if (target.apiUrl !== effectiveContext.apiUrl) effectiveContext = {
26809
- ...effectiveContext,
26810
- ...target
26811
- };
26812
- else effectiveContext.apiUrlSource = target.apiUrlSource;
26813
- }
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";
26814
27026
  return effectiveContext;
26815
27027
  }
26816
27028
  function useStoredCredential(context) {
@@ -26851,7 +27063,11 @@ function redactEffectiveContext(context) {
26851
27063
  apiUrlSource: context.apiUrlSource,
26852
27064
  credentialSource: context.credentialSource,
26853
27065
  orgId: storedOrgApplies ? context.credentials?.orgId ?? null : null,
26854
- 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
26855
27071
  };
26856
27072
  }
26857
27073
  //#endregion
@@ -27013,6 +27229,437 @@ var ApiKeyManager = class {
27013
27229
  }
27014
27230
  };
27015
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
27016
27663
  //#region src/lib/ensure-auth.ts
27017
27664
  const verifiedUsers = /* @__PURE__ */ new WeakMap();
27018
27665
  async function getAuthenticatedUser() {
@@ -27081,7 +27728,7 @@ async function ensureAuth(options) {
27081
27728
  }
27082
27729
  if (options?.interactive === false || process.stdin.isTTY !== true || invocation.noInput || strictIdentity) {
27083
27730
  if (options?.exitOnFailure === false) return null;
27084
- const message = "Authentication required. Run \"runtype auth login\" first or set RUNTYPE_API_KEY.";
27731
+ const message = authRequiredMessage(context);
27085
27732
  console.error(invocation.json ? JSON.stringify({
27086
27733
  success: false,
27087
27734
  error: {
@@ -27092,39 +27739,39 @@ async function ensureAuth(options) {
27092
27739
  process.exit(1);
27093
27740
  return null;
27094
27741
  }
27095
- console.log(chalk.yellow("\nAuthentication required."));
27742
+ console.log(chalk.yellow(`\n${authRequiredMessage(context)}`));
27096
27743
  if (!await promptConfirm$2("Would you like to log in now?", { default: true })) {
27097
27744
  console.log(chalk.gray("Run \"runtype auth login\" when you're ready."));
27098
27745
  if (options?.exitOnFailure !== false) process.exit(1);
27099
27746
  return null;
27100
27747
  }
27101
- const apiKey = await handleBrowserLogin(new CredentialStore(), context.apiUrl);
27748
+ const apiKey = await handleBrowserLogin(context.apiUrl);
27102
27749
  if (apiKey) {
27103
27750
  context.apiKey = apiKey;
27104
27751
  context.credentialSource = "stored";
27105
27752
  }
27106
27753
  return apiKey;
27107
27754
  }
27108
- 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) {
27109
27762
  console.log(chalk.gray("Opening browser for authentication..."));
27110
27763
  try {
27111
- const oauth = new OAuthManager({
27112
- clerkPublishableKey: getClerkPublishableKey(),
27113
- dashboardUrl: getDashboardUrl()
27114
- });
27115
- console.log(chalk.gray("Waiting for browser authentication..."));
27116
- const sessionToken = await oauth.authenticate("sign-in");
27117
- console.log(chalk.gray("Getting API key..."));
27118
- const { key, userId, orgId } = await new ApiKeyManager().exchangeSessionForApiKey(sessionToken, apiUrl || getApiUrl());
27119
- console.log(chalk.gray("Storing credentials..."));
27120
- await store.saveCredentials({
27121
- apiKey: key,
27122
- userId,
27123
- orgId,
27124
- apiUrl: apiUrl || getApiUrl()
27764
+ const login = await browserLogin({
27765
+ apiUrl,
27766
+ onProgress: (label) => console.log(chalk.gray(label))
27125
27767
  });
27768
+ const warning = orgMismatchWarning(login);
27769
+ if (warning) {
27770
+ console.log(chalk.yellow(warning));
27771
+ return null;
27772
+ }
27126
27773
  console.log(chalk.green("Logged in successfully!"));
27127
- return key;
27774
+ return login.apiKey;
27128
27775
  } catch (error) {
27129
27776
  const message = error instanceof Error ? error.message : "Unknown error";
27130
27777
  console.log(chalk.red("Login failed"));
@@ -27355,6 +28002,25 @@ function MutationResult({ loading, loadingLabel = "Processing...", success, succ
27355
28002
  return null;
27356
28003
  }
27357
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
27358
28024
  //#region src/auth/agent-auth-client.ts
27359
28025
  /**
27360
28026
  * Protocol error with the spec error code and an agent-actionable hint.
@@ -27609,7 +28275,7 @@ async function registerAction(options) {
27609
28275
  claimTokenExpires: registration.claimTokenExpires,
27610
28276
  apiUrl
27611
28277
  };
27612
- await store.saveCredentials({
28278
+ const registered = await store.saveCredentials({
27613
28279
  apiKey: registration.credential,
27614
28280
  userId: identity.userId,
27615
28281
  orgId: identity.orgId,
@@ -27627,7 +28293,8 @@ async function registerAction(options) {
27627
28293
  const claim = await initiateClaim(registration.claimToken, options.email, apiUrl);
27628
28294
  await store.saveClaimState({
27629
28295
  ...claimState,
27630
- claimEmail: options.email
28296
+ claimEmail: options.email,
28297
+ accountId: registered.id
27631
28298
  });
27632
28299
  printJson({
27633
28300
  ...base,
@@ -27708,13 +28375,15 @@ async function verifyAction(code, options) {
27708
28375
  try {
27709
28376
  identity = await fetchIdentity(completed.credential, apiUrl);
27710
28377
  } catch {}
27711
- await store.saveCredentials({
28378
+ const preClaimAccountId = claimState.accountId;
28379
+ const wasDefault = (await store.getDefaultAccount())?.id === preClaimAccountId;
28380
+ const saved = await store.saveCredentials({
27712
28381
  apiKey: completed.credential,
27713
28382
  userId: identity.userId,
27714
28383
  orgId: identity.orgId,
27715
28384
  apiUrl
27716
- });
27717
- await store.clearClaimState();
28385
+ }, void 0, { makeDefault: wasDefault });
28386
+ if (preClaimAccountId && preClaimAccountId !== saved.id) await store.removeAccount(preClaimAccountId);
27718
28387
  printJson({
27719
28388
  status: "authenticated",
27720
28389
  accountState: "full",
@@ -27763,149 +28432,122 @@ async function getBillingInfo(apiKey, apiUrl) {
27763
28432
  return null;
27764
28433
  }
27765
28434
  }
27766
- 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) => {
27767
- await loadEffectiveContext(options.apiUrl);
27768
- refuseForcedTtyLogin(options);
27769
- if (!isTTY(options)) {
27770
- console.error(JSON.stringify(NON_TTY_LOGIN_GUIDANCE, null, 2));
27771
- process.exit(1);
27772
- }
28435
+ /** Run one async step under the shared spinner/result chrome. */
28436
+ async function renderTask(loadingLabel, task) {
27773
28437
  const App = () => {
27774
28438
  const [loading, setLoading] = useState(true);
27775
- const [loadingLabel, setLoadingLabel] = useState("Opening browser for authentication...");
28439
+ const [label, setLabel] = useState(loadingLabel);
27776
28440
  const [success, setSuccess] = useState(null);
27777
28441
  const [error, setError] = useState(null);
28442
+ const [outcome, setOutcome] = useState(null);
27778
28443
  useEffect(() => {
27779
- const run = async () => {
27780
- try {
27781
- const oauth = new OAuthManager({
27782
- clerkPublishableKey: getClerkPublishableKey(),
27783
- dashboardUrl: getDashboardUrl()
27784
- });
27785
- setLoadingLabel("Waiting for browser authentication...");
27786
- const sessionToken = await oauth.authenticate("sign-in");
27787
- setLoadingLabel("Creating API key...");
27788
- const { key, userId, orgId } = await new ApiKeyManager().exchangeSessionForApiKey(sessionToken, options.apiUrl || getApiUrl());
27789
- setLoadingLabel("Storing credentials securely...");
27790
- await new CredentialStore().saveCredentials({
27791
- apiKey: key,
27792
- userId,
27793
- orgId,
27794
- apiUrl: options.apiUrl || getApiUrl()
27795
- });
27796
- setSuccess(true);
27797
- setLoading(false);
27798
- } catch (err) {
27799
- setError(err instanceof Error ? err : new Error(String(err)));
27800
- setSuccess(false);
27801
- setLoading(false);
27802
- }
27803
- };
27804
- 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));
27805
28451
  }, []);
27806
28452
  return React.createElement(MutationResult, {
27807
28453
  loading,
27808
- loadingLabel,
28454
+ loadingLabel: label,
27809
28455
  success,
27810
- successMessage: "Authentication successful! Welcome to Runtype!",
28456
+ successMessage: outcome?.message ?? "",
27811
28457
  error,
27812
- result: React.createElement(EntityCard, { fields: [{
27813
- label: "Next",
27814
- value: "runtype flows list"
27815
- }] })
28458
+ result: outcome?.result
27816
28459
  });
27817
28460
  };
27818
28461
  const { waitUntilExit } = render(React.createElement(App));
27819
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
+ })));
27820
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
+ }
27821
28532
  async function loginAction(options) {
27822
28533
  await loadEffectiveContext(options.apiUrl);
27823
- const store = new CredentialStore();
27824
28534
  if (options.apiKey) {
28535
+ const apiKey = options.apiKey;
27825
28536
  if (!isTTY(options)) {
28537
+ let login;
27826
28538
  try {
27827
- const apiKeyManager = new ApiKeyManager();
27828
- if (!await apiKeyManager.validateApiKey(options.apiKey, options.apiUrl)) {
27829
- console.error(JSON.stringify({
27830
- status: "error",
27831
- error: "Invalid API key"
27832
- }, null, 2));
27833
- process.exit(1);
27834
- }
27835
- const userData = await apiKeyManager.getCurrentUser(options.apiKey, options.apiUrl);
27836
- await store.saveCredentials({
27837
- apiKey: options.apiKey,
27838
- userId: userData.userId,
27839
- orgId: userData.orgId ?? void 0,
27840
- apiUrl: options.apiUrl || getApiUrl()
27841
- });
27842
- console.log(JSON.stringify({
27843
- status: "authenticated",
27844
- userId: userData.userId,
27845
- next: "Continue with setup: runtype install-mcp (then confirm with: runtype auth whoami)"
27846
- }, null, 2));
28539
+ login = await apiKeyLogin(apiKey, { apiUrl: options.apiUrl });
27847
28540
  } catch (error) {
27848
- if (error instanceof ApiKeyValidationUnavailableError) {
27849
- console.error(JSON.stringify({
27850
- status: "error",
27851
- code: "api_unreachable",
27852
- error: error.message,
27853
- next: API_UNREACHABLE_NEXT
27854
- }, null, 2));
27855
- process.exit(1);
27856
- }
27857
- const message = error instanceof Error ? error.message : "Unknown error";
27858
- console.error(JSON.stringify({
27859
- status: "error",
27860
- error: message
27861
- }, null, 2));
27862
- process.exit(1);
28541
+ failApiKeyLogin(error);
28542
+ return;
27863
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));
27864
28548
  return;
27865
28549
  }
27866
- const App = () => {
27867
- const [loading, setLoading] = useState(true);
27868
- const [success, setSuccess] = useState(null);
27869
- const [error, setError] = useState(null);
27870
- const [userId, setUserId] = useState(null);
27871
- useEffect(() => {
27872
- const run = async () => {
27873
- try {
27874
- const apiKeyManager = new ApiKeyManager();
27875
- if (!await apiKeyManager.validateApiKey(options.apiKey, options.apiUrl)) {
27876
- setError(/* @__PURE__ */ new Error("Invalid API key"));
27877
- setSuccess(false);
27878
- setLoading(false);
27879
- return;
27880
- }
27881
- const userData = await apiKeyManager.getCurrentUser(options.apiKey, options.apiUrl);
27882
- await store.saveCredentials({
27883
- apiKey: options.apiKey,
27884
- userId: userData.userId,
27885
- orgId: userData.orgId ?? void 0,
27886
- apiUrl: options.apiUrl || getApiUrl()
27887
- });
27888
- setUserId(userData.userId);
27889
- setSuccess(true);
27890
- setLoading(false);
27891
- } catch (err) {
27892
- setError(err instanceof Error ? err : new Error(String(err)));
27893
- setSuccess(false);
27894
- setLoading(false);
27895
- }
27896
- };
27897
- run();
27898
- }, []);
27899
- return React.createElement(MutationResult, {
27900
- loading,
27901
- loadingLabel: "Validating API key...",
27902
- success,
27903
- successMessage: `Logged in successfully!${userId ? ` User ID: ${userId}` : ""}`,
27904
- error
27905
- });
27906
- };
27907
- const { waitUntilExit } = render(React.createElement(App));
27908
- await waitUntilExit();
28550
+ await renderTask("Validating API key...", async () => loginOutcome(await apiKeyLogin(apiKey, { apiUrl: options.apiUrl })));
27909
28551
  return;
27910
28552
  }
27911
28553
  refuseForcedTtyLogin(options);
@@ -27913,73 +28555,54 @@ async function loginAction(options) {
27913
28555
  console.error(JSON.stringify(NON_TTY_LOGIN_GUIDANCE, null, 2));
27914
28556
  process.exit(1);
27915
28557
  }
27916
- const App = () => {
27917
- const [loading, setLoading] = useState(true);
27918
- const [loadingLabel, setLoadingLabel] = useState("Initializing authentication...");
27919
- const [success, setSuccess] = useState(null);
27920
- const [error, setError] = useState(null);
27921
- const [userId, setUserId] = useState(null);
27922
- useEffect(() => {
27923
- const run = async () => {
27924
- try {
27925
- const oauth = new OAuthManager({
27926
- clerkPublishableKey: getClerkPublishableKey(),
27927
- dashboardUrl: options.dashboardUrl || getDashboardUrl()
27928
- });
27929
- setLoadingLabel("Waiting for browser authentication...");
27930
- const sessionToken = await oauth.authenticate("sign-in");
27931
- setLoadingLabel("Getting API key...");
27932
- const { key, userId: uid, orgId } = await new ApiKeyManager().exchangeSessionForApiKey(sessionToken, options.apiUrl || getApiUrl());
27933
- setLoadingLabel("Storing credentials...");
27934
- await store.saveCredentials({
27935
- apiKey: key,
27936
- userId: uid,
27937
- orgId,
27938
- apiUrl: options.apiUrl || getApiUrl()
27939
- });
27940
- setUserId(uid);
27941
- setSuccess(true);
27942
- setLoading(false);
27943
- } catch (err) {
27944
- setError(err instanceof Error ? err : new Error(String(err)));
27945
- setSuccess(false);
27946
- setLoading(false);
27947
- }
27948
- };
27949
- run();
27950
- }, []);
27951
- return React.createElement(MutationResult, {
27952
- loading,
27953
- loadingLabel,
27954
- success,
27955
- successMessage: `Logged in successfully!${userId ? ` User ID: ${userId}` : ""}`,
27956
- error
27957
- });
27958
- };
27959
- const { waitUntilExit } = render(React.createElement(App));
27960
- 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
+ }
27961
28573
  }
27962
- 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);
27963
- 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);
27964
- 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) => {
27965
28575
  const store = new CredentialStore();
27966
28576
  if (!isTTY(options)) {
27967
28577
  try {
27968
- if (!await store.hasCredentials()) {
28578
+ const context = await loadEffectiveContext();
28579
+ const storedLogins = (await store.listAccounts()).length;
28580
+ const credentials = context.credentials;
28581
+ if (!credentials) {
27969
28582
  console.log(JSON.stringify({
27970
28583
  status: "unauthenticated",
27971
- 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>."
27972
28589
  }, null, 2));
27973
28590
  return;
27974
28591
  }
27975
- const credentials = await store.getCredentials();
27976
- const claimState = await store.getClaimState();
28592
+ const pendingClaim = await store.getClaimState();
28593
+ const claimState = pendingClaim?.accountId === credentials.accountId ? pendingClaim : null;
27977
28594
  console.log(JSON.stringify({
27978
28595
  status: "authenticated",
27979
28596
  accountState: claimState ? "temporary" : "full",
27980
- userId: credentials?.userId,
27981
- apiUrl: credentials?.apiUrl || "default",
27982
- 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." } : {},
27983
28606
  ...claimState ? {
27984
28607
  registrationId: claimState.registrationId,
27985
28608
  credentialExpires: claimState.credentialExpires,
@@ -27996,74 +28619,158 @@ authCommand.command("status").description("Show authentication status").option("
27996
28619
  }
27997
28620
  return;
27998
28621
  }
27999
- const App = () => {
28000
- const [loading, setLoading] = useState(true);
28001
- const [success, setSuccess] = useState(null);
28002
- const [error, setError] = useState(null);
28003
- const [resultNode, setResultNode] = useState(void 0);
28004
- useEffect(() => {
28005
- const run = async () => {
28006
- try {
28007
- if (!await store.hasCredentials()) {
28008
- setResultNode(React.createElement(EntityCard, { fields: [{
28009
- label: "Status",
28010
- value: "Not authenticated",
28011
- color: "yellow"
28012
- }, {
28013
- label: "Hint",
28014
- value: "Run runtype auth login"
28015
- }] }));
28016
- setSuccess(true);
28017
- setLoading(false);
28018
- return;
28019
- }
28020
- const credentials = await store.getCredentials();
28021
- if (credentials) setResultNode(React.createElement(EntityCard, { fields: [
28022
- {
28023
- label: "Status",
28024
- value: "Authenticated",
28025
- color: "green"
28026
- },
28027
- {
28028
- label: "User ID",
28029
- value: credentials.userId
28030
- },
28031
- {
28032
- label: "API URL",
28033
- value: credentials.apiUrl || "default"
28034
- },
28035
- {
28036
- label: "API Key",
28037
- value: credentials.apiKey ? credentials.apiKey.substring(0, 12) + "..." : "missing"
28038
- },
28039
- {
28040
- label: "Org ID",
28041
- value: credentials.orgId || "none"
28042
- }
28043
- ] }));
28044
- setSuccess(true);
28045
- setLoading(false);
28046
- } catch (err) {
28047
- setError(err instanceof Error ? err : new Error(String(err)));
28048
- setSuccess(false);
28049
- 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"
28050
28645
  }
28051
- };
28052
- run();
28053
- }, []);
28054
- return React.createElement(MutationResult, {
28055
- loading,
28056
- loadingLabel: "Checking authentication...",
28057
- success,
28058
- successMessage: "Auth Status",
28059
- error,
28060
- result: resultNode
28061
- });
28062
- };
28063
- const { waitUntilExit } = render(React.createElement(App));
28064
- 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
+ });
28065
28692
  });
28066
- authCommand.command("export-key").description("Print the stored API key to stdout (for use in scripts and .env files)").action(async () => {
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));
28772
+ });
28773
+ authCommand.command("export-key").description("Print the active login's API key to stdout (for use in scripts and .env files)").action(async () => {
28067
28774
  const apiKey = await new CredentialStore().getApiKey();
28068
28775
  if (!apiKey) {
28069
28776
  console.error("Not authenticated. Run `runtype auth login` first.");
@@ -28073,52 +28780,43 @@ authCommand.command("export-key").description("Print the stored API key to stdou
28073
28780
  process.stdout.write(apiKey);
28074
28781
  });
28075
28782
  registerAgentAuthCommands(authCommand);
28076
- 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) => {
28077
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
+ };
28078
28799
  if (!isTTY(options)) {
28079
- if (!await store.hasCredentials()) {
28800
+ const { removed } = await logout();
28801
+ if (removed.length === 0) {
28080
28802
  console.log(JSON.stringify({ status: "no_credentials" }, null, 2));
28081
28803
  return;
28082
28804
  }
28083
- await store.clearCredentials();
28084
- 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));
28085
28813
  return;
28086
28814
  }
28087
- const App = () => {
28088
- const [loading, setLoading] = useState(true);
28089
- const [success, setSuccess] = useState(null);
28090
- const [error, setError] = useState(null);
28091
- const [msg, setMsg] = useState("Logged out successfully");
28092
- useEffect(() => {
28093
- const run = async () => {
28094
- try {
28095
- if (!await store.hasCredentials()) {
28096
- setMsg("No stored credentials found");
28097
- setSuccess(true);
28098
- setLoading(false);
28099
- return;
28100
- }
28101
- await store.clearCredentials();
28102
- setSuccess(true);
28103
- setLoading(false);
28104
- } catch (err) {
28105
- setError(err instanceof Error ? err : new Error(String(err)));
28106
- setSuccess(false);
28107
- setLoading(false);
28108
- }
28109
- };
28110
- run();
28111
- }, []);
28112
- return React.createElement(MutationResult, {
28113
- loading,
28114
- loadingLabel: "Logging out...",
28115
- success,
28116
- successMessage: msg,
28117
- error
28118
- });
28119
- };
28120
- const { waitUntilExit } = render(React.createElement(App));
28121
- 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
+ });
28122
28820
  });
28123
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) => {
28124
28822
  const apiKey = await ensureAuth({
@@ -28132,10 +28830,16 @@ authCommand.command("whoami").description("Display current user information").op
28132
28830
  apiUrl: context.apiUrl
28133
28831
  };
28134
28832
  if (!credentials || !credentials.apiKey) {
28135
- 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));
28136
28840
  else {
28137
- console.log(chalk.yellow("Not logged in"));
28138
- 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");
28139
28843
  }
28140
28844
  return;
28141
28845
  }
@@ -28147,6 +28851,9 @@ authCommand.command("whoami").description("Display current user information").op
28147
28851
  userId: user.userId,
28148
28852
  orgId: user.orgId,
28149
28853
  apiUrl: credentials.apiUrl || getApiUrl(),
28854
+ credentialSource: context.credentialSource,
28855
+ accountSource: context.accountSource,
28856
+ projectLink: context.projectLink?.path ?? null,
28150
28857
  billing: billingInfo
28151
28858
  }, null, 2));
28152
28859
  } catch (error) {
@@ -28159,81 +28866,142 @@ authCommand.command("whoami").description("Display current user information").op
28159
28866
  }
28160
28867
  return;
28161
28868
  }
28162
- const App = () => {
28163
- const [loading, setLoading] = useState(true);
28164
- const [success, setSuccess] = useState(null);
28165
- const [error, setError] = useState(null);
28166
- const [resultNode, setResultNode] = useState(void 0);
28167
- useEffect(() => {
28168
- const run = async () => {
28169
- try {
28170
- const user = await getAuthenticatedUser();
28171
- const billingInfo = await getBillingInfo(credentials.apiKey, credentials.apiUrl);
28172
- const fields = [
28173
- {
28174
- label: "User ID",
28175
- value: user.userId
28176
- },
28177
- {
28178
- label: "Organization",
28179
- value: user.orgId || "Personal"
28180
- },
28181
- {
28182
- label: "API Key",
28183
- value: credentials.apiKey.substring(0, 10) + "..."
28184
- },
28185
- {
28186
- label: "API URL",
28187
- value: credentials.apiUrl || getApiUrl()
28188
- },
28189
- {
28190
- label: "Created",
28191
- value: credentials.createdAt
28192
- },
28193
- {
28194
- label: "Last Used",
28195
- value: credentials.lastUsed
28196
- }
28197
- ];
28198
- if (billingInfo) {
28199
- const plan = billingInfo.planName || billingInfo.plan;
28200
- if (plan) fields.push({
28201
- label: "Plan",
28202
- value: plan,
28203
- color: "green"
28204
- });
28205
- const usage = billingInfo.usage;
28206
- if (usage) {
28207
- const used = usage.executionsToday ?? usage.executionsThisMonth;
28208
- const value = usage.executionsThisMonthAvailable === false || typeof used !== "number" ? "unavailable" : used.toLocaleString();
28209
- fields.push({
28210
- label: "Executions today",
28211
- value
28212
- });
28213
- }
28214
- }
28215
- setResultNode(React.createElement(EntityCard, { fields }));
28216
- setSuccess(true);
28217
- setLoading(false);
28218
- } catch (err) {
28219
- setError(err instanceof Error ? err : new Error(String(err)));
28220
- setSuccess(false);
28221
- setLoading(false);
28222
- }
28223
- };
28224
- run();
28225
- }, []);
28226
- return React.createElement(MutationResult, {
28227
- loading,
28228
- loadingLabel: "Fetching user information...",
28229
- success,
28230
- successMessage: "Current User",
28231
- error,
28232
- result: resultNode
28233
- });
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
28234
28946
  };
28235
- const { waitUntilExit } = render(React.createElement(App));
28236
- 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."));
28237
29005
  });
28238
29006
  //#endregion
28239
29007
  //#region src/lib/file-utils.ts
@@ -31499,6 +32267,9 @@ configCommand.command("context").description("Show the effective API target and
31499
32267
  console.log(`API URL: ${context.apiUrl} (${context.apiUrlSource})`);
31500
32268
  console.log(`Credential source: ${context.credentialSource}`);
31501
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}`);
31502
32273
  });
31503
32274
  configCommand.command("get [key]").description("Get configuration value").action((key) => {
31504
32275
  const config = getCliConfig();
@@ -34200,21 +34971,16 @@ function InitApp({ apiUrl, onComplete }) {
34200
34971
  loadingLabel: storedKeyNotice ? `${storedKeyNotice} Opening browser to sign in instead...` : "Opening browser to sign in..."
34201
34972
  }));
34202
34973
  try {
34203
- const sessionToken = await new OAuthManager({
34204
- clerkPublishableKey: getClerkPublishableKey(),
34205
- dashboardUrl: getDashboardUrl()
34206
- }).authenticate("sign-in");
34207
- setState((s) => ({
34208
- ...s,
34209
- loadingLabel: "Exchanging credentials..."
34210
- }));
34211
- const { key, userId, orgId } = await new ApiKeyManager().exchangeSessionForApiKey(sessionToken, apiUrl || getApiUrl());
34212
- await store.saveCredentials({
34213
- apiKey: key,
34214
- userId,
34215
- orgId,
34216
- apiUrl: apiUrl || getApiUrl()
34974
+ const login = await browserLogin({
34975
+ apiUrl,
34976
+ onProgress: (label) => setState((s) => ({
34977
+ ...s,
34978
+ loadingLabel: label
34979
+ }))
34217
34980
  });
34981
+ const mismatch = orgMismatchWarning(login);
34982
+ if (mismatch) throw new Error(mismatch);
34983
+ const { apiKey: key, userId, orgId } = login;
34218
34984
  setState((s) => ({
34219
34985
  ...s,
34220
34986
  apiKey: key,
@@ -37759,7 +38525,7 @@ function applyMaxLines(raw, maxLines) {
37759
38525
  function readFileContent(filePath, snapshots, liveTools, maxLines = 5e3) {
37760
38526
  try {
37761
38527
  return {
37762
- ...applyMaxLines(fs$2.readFileSync(filePath, "utf-8"), maxLines),
38528
+ ...applyMaxLines(fs$1.readFileSync(filePath, "utf-8"), maxLines),
37763
38529
  source: "disk"
37764
38530
  };
37765
38531
  } catch {}
@@ -38064,7 +38830,7 @@ const deployLifecycleParameters = {
38064
38830
  description: "Runtype destruction backstop. Omitted: Daytona 10m, Runtype Sandbox unlimited. Provider sleep/loss can end availability earlier."
38065
38831
  },
38066
38832
  sleepPolicy: {
38067
- description: "Idle compute policy, independent of retention. Legacy Runtype Sandbox supports idle only.",
38833
+ description: "Idle compute policy, independent of retention.",
38068
38834
  oneOf: [{
38069
38835
  type: "object",
38070
38836
  properties: { mode: { const: "never" } },
@@ -38187,7 +38953,7 @@ function createDeploySandboxLocalTool(client, debugMode) {
38187
38953
  let activeCfSandboxId;
38188
38954
  function createDeployCfSandboxLocalTool(client, debugMode) {
38189
38955
  return {
38190
- 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.",
38191
38957
  parametersSchema: {
38192
38958
  type: "object",
38193
38959
  properties: {
@@ -38196,6 +38962,10 @@ function createDeployCfSandboxLocalTool(client, debugMode) {
38196
38962
  description: "Code to deploy (main entry point file)"
38197
38963
  },
38198
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
+ },
38199
38969
  packageJson: {
38200
38970
  type: "object",
38201
38971
  description: "package.json content as a JSON object, e.g. { \"dependencies\": { \"hono\": \"^4.0.0\" } }"
@@ -38289,14 +39059,14 @@ function createEventLogWriter(stateFilePath, initialSessionIndex) {
38289
39059
  let stream = null;
38290
39060
  function ensureDir() {
38291
39061
  if (!dirCreated) {
38292
- fs$2.mkdirSync(eventsDir, { recursive: true });
39062
+ fs$1.mkdirSync(eventsDir, { recursive: true });
38293
39063
  dirCreated = true;
38294
39064
  }
38295
39065
  }
38296
39066
  function ensureStream() {
38297
39067
  if (!stream || stream.destroyed) {
38298
39068
  ensureDir();
38299
- stream = fs$2.createWriteStream(currentPath, { flags: "a" });
39069
+ stream = fs$1.createWriteStream(currentPath, { flags: "a" });
38300
39070
  stream.on("error", () => {});
38301
39071
  }
38302
39072
  }
@@ -38325,8 +39095,8 @@ function createEventLogWriter(stateFilePath, initialSessionIndex) {
38325
39095
  * Returns an empty array if the file doesn't exist. */
38326
39096
  function readSessionEventLog(stateFilePath, sessionIndex) {
38327
39097
  const logPath = sessionEventLogPath(stateFilePath, sessionIndex);
38328
- if (!fs$2.existsSync(logPath)) return [];
38329
- const content = fs$2.readFileSync(logPath, "utf-8");
39098
+ if (!fs$1.existsSync(logPath)) return [];
39099
+ const content = fs$1.readFileSync(logPath, "utf-8");
38330
39100
  const events = [];
38331
39101
  for (const line of content.split("\n")) {
38332
39102
  if (!line.trim()) continue;
@@ -38361,7 +39131,7 @@ function isTreeLogEntry(value) {
38361
39131
  function loadTreeLog(filePath) {
38362
39132
  let raw;
38363
39133
  try {
38364
- raw = fs$2.readFileSync(filePath, "utf-8");
39134
+ raw = fs$1.readFileSync(filePath, "utf-8");
38365
39135
  } catch {
38366
39136
  return null;
38367
39137
  }
@@ -38398,8 +39168,8 @@ function loadTreeLog(filePath) {
38398
39168
  };
38399
39169
  }
38400
39170
  function appendLine(log, entry) {
38401
- fs$2.mkdirSync(path$2.dirname(log.filePath), { recursive: true });
38402
- 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");
38403
39173
  log.entries.push(entry);
38404
39174
  if (entry.type !== "head") {
38405
39175
  log.byId.set(entry.id, entry);
@@ -38464,7 +39234,7 @@ function appendArtifact(log, payload) {
38464
39234
  type: "artifact",
38465
39235
  payload: {
38466
39236
  ...payload,
38467
- host: payload.host || os$2.hostname()
39237
+ host: payload.host || os$1.hostname()
38468
39238
  }
38469
39239
  };
38470
39240
  appendLine(log, entry);
@@ -41540,7 +42310,7 @@ function MarathonApp({ taskName, agentId, model, sandbox, goal, initialSessionCo
41540
42310
  setShowSessionMenu(false);
41541
42311
  try {
41542
42312
  let json;
41543
- 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");
41544
42314
  else json = JSON.stringify({
41545
42315
  sessionSnapshots,
41546
42316
  content: state.content,
@@ -41563,8 +42333,8 @@ function MarathonApp({ taskName, agentId, model, sandbox, goal, initialSessionCo
41563
42333
  setShowSessionMenu(false);
41564
42334
  try {
41565
42335
  let json;
41566
- if (stateFilePath && fs$2.existsSync(stateFilePath)) {
41567
- const raw = fs$2.readFileSync(stateFilePath, "utf-8");
42336
+ if (stateFilePath && fs$1.existsSync(stateFilePath)) {
42337
+ const raw = fs$1.readFileSync(stateFilePath, "utf-8");
41568
42338
  const parsed = JSON.parse(raw);
41569
42339
  json = JSON.stringify(compactMarathonStateJsonForContextExport(parsed), null, 2);
41570
42340
  } else {
@@ -41590,7 +42360,7 @@ function MarathonApp({ taskName, agentId, model, sandbox, goal, initialSessionCo
41590
42360
  ]);
41591
42361
  const handleOpenStateFile = useCallback(() => {
41592
42362
  setShowSessionMenu(false);
41593
- if (stateFilePath && fs$2.existsSync(stateFilePath)) {
42363
+ if (stateFilePath && fs$1.existsSync(stateFilePath)) {
41594
42364
  open(stateFilePath);
41595
42365
  showFlash("Opening state file...");
41596
42366
  } else showFlash("No state file yet");
@@ -41985,7 +42755,7 @@ function MarathonApp({ taskName, agentId, model, sandbox, goal, initialSessionCo
41985
42755
  const file = detailFile ?? trackedFiles[fileCursor];
41986
42756
  if (file) {
41987
42757
  const filePath = path$2.resolve(file.path);
41988
- if (fs$2.existsSync(filePath)) {
42758
+ if (fs$1.existsSync(filePath)) {
41989
42759
  open(filePath);
41990
42760
  showFlash(`Opening ${file.path}`);
41991
42761
  } else showFlash("File not found on disk");
@@ -42500,7 +43270,7 @@ function MarathonApp({ taskName, agentId, model, sandbox, goal, initialSessionCo
42500
43270
  onOpenDashboard: handleOpenDashboard,
42501
43271
  onClose: () => setShowSessionMenu(false),
42502
43272
  hasDashboard: !!agentPageUrl,
42503
- hasStateFile: !!stateFilePath && fs$2.existsSync(stateFilePath)
43273
+ hasStateFile: !!stateFilePath && fs$1.existsSync(stateFilePath)
42504
43274
  })
42505
43275
  }),
42506
43276
  showUpgradeModal && upgradePrompt && /* @__PURE__ */ jsx(Box, {
@@ -43041,28 +43811,6 @@ function setMarathonTitle() {
43041
43811
  setTerminalTitle(`Runtype: Marathon (${getFolderName()})`);
43042
43812
  }
43043
43813
  //#endregion
43044
- //#region src/config/paths.ts
43045
- /** Root state directory: RUNTYPE_STATE_DIR or ~/.runtype */
43046
- function getRuntypeHomeDir() {
43047
- return process.env.RUNTYPE_STATE_DIR || path$2.join(os$2.homedir(), ".runtype");
43048
- }
43049
- /** Per-project state dir: ~/.runtype/projects/{hash}/ */
43050
- function getProjectStateDir(projectDir) {
43051
- const dir = projectDir || process.cwd();
43052
- const hash = crypto$2.createHash("sha256").update(dir).digest("hex").slice(0, 12);
43053
- const stateDir = path$2.join(getRuntypeHomeDir(), "projects", hash);
43054
- const breadcrumb = path$2.join(stateDir, "project-path.txt");
43055
- if (!fs$2.existsSync(breadcrumb)) {
43056
- fs$2.mkdirSync(stateDir, { recursive: true });
43057
- fs$2.writeFileSync(breadcrumb, dir);
43058
- }
43059
- return stateDir;
43060
- }
43061
- /** Marathon state dir: ~/.runtype/projects/{hash}/marathons/ */
43062
- function getMarathonStateDir(projectDir) {
43063
- return path$2.join(getProjectStateDir(projectDir), "marathons");
43064
- }
43065
- //#endregion
43066
43814
  //#region src/marathon/checkpoint.ts
43067
43815
  function defaultStateDir$2() {
43068
43816
  return getMarathonStateDir();
@@ -43083,14 +43831,14 @@ function ensureMarathonFileCheckpoint(taskName, targetPath, stateDir, stateFileP
43083
43831
  const normalizedTargetPath = path$2.resolve(targetPath);
43084
43832
  const relativeTargetPath = path$2.relative(process.cwd(), normalizedTargetPath);
43085
43833
  if (!relativeTargetPath || relativeTargetPath.startsWith("..")) return void 0;
43086
- if (!fs$2.existsSync(normalizedTargetPath)) return void 0;
43087
- 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;
43088
43836
  if (relativeTargetPath.replace(/\\/g, "/").startsWith(".runtype/")) return void 0;
43089
43837
  if (normalizedTargetPath.startsWith(getRuntypeHomeDir() + path$2.sep)) return void 0;
43090
43838
  const checkpointPath = resolveMarathonCheckpointPath(taskName, normalizedTargetPath, stateDir, stateFilePath);
43091
- if (fs$2.existsSync(checkpointPath)) return checkpointPath;
43092
- fs$2.mkdirSync(path$2.dirname(checkpointPath), { recursive: true });
43093
- 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);
43094
43842
  return checkpointPath;
43095
43843
  }
43096
43844
  function restoreMarathonFileCheckpoint(taskName, targetPath, stateDir, stateFilePath) {
@@ -43101,13 +43849,13 @@ function restoreMarathonFileCheckpoint(taskName, targetPath, stateDir, stateFile
43101
43849
  error: `Cannot restore checkpoint outside the current repository: ${normalizedTargetPath}`
43102
43850
  };
43103
43851
  const checkpointPath = resolveMarathonCheckpointPath(taskName, normalizedTargetPath, stateDir, stateFilePath);
43104
- if (!fs$2.existsSync(checkpointPath)) return {
43852
+ if (!fs$1.existsSync(checkpointPath)) return {
43105
43853
  restored: false,
43106
43854
  checkpointPath,
43107
43855
  error: `No checkpoint found for ${normalizedTargetPath}`
43108
43856
  };
43109
- fs$2.mkdirSync(path$2.dirname(normalizedTargetPath), { recursive: true });
43110
- fs$2.copyFileSync(checkpointPath, normalizedTargetPath);
43857
+ fs$1.mkdirSync(path$2.dirname(normalizedTargetPath), { recursive: true });
43858
+ fs$1.copyFileSync(checkpointPath, normalizedTargetPath);
43111
43859
  return {
43112
43860
  restored: true,
43113
43861
  checkpointPath
@@ -43637,7 +44385,7 @@ function normalizeMarathonStatePath(candidatePath) {
43637
44385
  }
43638
44386
  function marathonStatePathExists(candidatePath) {
43639
44387
  if (!candidatePath) return false;
43640
- return fs$2.existsSync(path$2.resolve(candidatePath));
44388
+ return fs$1.existsSync(path$2.resolve(candidatePath));
43641
44389
  }
43642
44390
  function isMarathonArtifactStatePath(candidatePath) {
43643
44391
  if (!candidatePath) return false;
@@ -43798,7 +44546,7 @@ function sanitizeLoadedMarathonState(state) {
43798
44546
  }
43799
44547
  function loadState(filePath) {
43800
44548
  try {
43801
- const raw = fs$2.readFileSync(filePath, "utf-8");
44549
+ const raw = fs$1.readFileSync(filePath, "utf-8");
43802
44550
  return sanitizeLoadedMarathonState(JSON.parse(raw));
43803
44551
  } catch {
43804
44552
  return null;
@@ -43806,7 +44554,7 @@ function loadState(filePath) {
43806
44554
  }
43807
44555
  function saveState(filePath, state, options) {
43808
44556
  const dir = path$2.dirname(filePath);
43809
- fs$2.mkdirSync(dir, { recursive: true });
44557
+ fs$1.mkdirSync(dir, { recursive: true });
43810
44558
  const stateToWrite = options?.stripSnapshotEvents && state.sessionSnapshots?.length ? {
43811
44559
  ...state,
43812
44560
  sessionSnapshots: state.sessionSnapshots.map((snapshot) => ({
@@ -43814,7 +44562,7 @@ function saveState(filePath, state, options) {
43814
44562
  rawEvents: []
43815
44563
  }))
43816
44564
  } : state;
43817
- fs$2.writeFileSync(filePath, JSON.stringify(stateToWrite, null, 2));
44565
+ fs$1.writeFileSync(filePath, JSON.stringify(stateToWrite, null, 2));
43818
44566
  syncTreeLogWithState(filePath, state);
43819
44567
  }
43820
44568
  function extractRunTaskResumeState(state) {
@@ -43860,11 +44608,11 @@ function offsetNewContextCompactionSummaries(summaries, existingSummaryIds, sess
43860
44608
  function findStateFile(name, stateDir) {
43861
44609
  if (stateDir) return stateFilePath(name, stateDir);
43862
44610
  const homePath = stateFilePath(name, getMarathonStateDir());
43863
- if (fs$2.existsSync(homePath)) return homePath;
44611
+ if (fs$1.existsSync(homePath)) return homePath;
43864
44612
  const oldMarathonPath = stateFilePath(name, path$2.join(process.cwd(), ".runtype", "marathons"));
43865
- if (fs$2.existsSync(oldMarathonPath)) return oldMarathonPath;
44613
+ if (fs$1.existsSync(oldMarathonPath)) return oldMarathonPath;
43866
44614
  const oldTaskPath = stateFilePath(name, path$2.join(process.cwd(), ".runtype", "tasks"));
43867
- if (fs$2.existsSync(oldTaskPath)) return oldTaskPath;
44615
+ if (fs$1.existsSync(oldTaskPath)) return oldTaskPath;
43868
44616
  return homePath;
43869
44617
  }
43870
44618
  function findLatestStateFile(stateDir) {
@@ -43875,11 +44623,11 @@ function findLatestStateFile(stateDir) {
43875
44623
  ];
43876
44624
  let latest = null;
43877
44625
  for (const dir of dirs) {
43878
- if (!fs$2.existsSync(dir)) continue;
43879
- 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"));
43880
44628
  for (const file of files) {
43881
44629
  const fullPath = path$2.join(dir, file);
43882
- const stat = fs$2.statSync(fullPath);
44630
+ const stat = fs$1.statSync(fullPath);
43883
44631
  if (!latest || stat.mtimeMs > latest.mtime) latest = {
43884
44632
  name: file.replace(".json", ""),
43885
44633
  filePath: fullPath,
@@ -43963,7 +44711,7 @@ async function resolveMarathonStateResolution(options, taskName, filePath, promp
43963
44711
  };
43964
44712
  }
43965
44713
  if (options.fresh) return { action: "fresh" };
43966
- if (!fs$2.existsSync(filePath)) return { action: "fresh" };
44714
+ if (!fs$1.existsSync(filePath)) return { action: "fresh" };
43967
44715
  if (!allowPrompt) throw new Error(`Local marathon state already exists at ${filePath}. Use --resume to continue it or --fresh to start over.`);
43968
44716
  const choice = await promptForChoice({
43969
44717
  reason: "existing-state",
@@ -44243,9 +44991,9 @@ function shouldIgnoreRepoEntry(entryPath) {
44243
44991
  function safeReadTextFile(filePath) {
44244
44992
  try {
44245
44993
  if (isSensitivePath(normalizeToolPath(filePath).replace(/\\/g, "/"))) return null;
44246
- const stat = fs$2.statSync(filePath);
44994
+ const stat = fs$1.statSync(filePath);
44247
44995
  if (!stat.isFile() || stat.size > 1048576) return null;
44248
- const buffer = fs$2.readFileSync(filePath);
44996
+ const buffer = fs$1.readFileSync(filePath);
44249
44997
  if (buffer.includes(0)) return null;
44250
44998
  return buffer.toString("utf-8");
44251
44999
  } catch {
@@ -44253,14 +45001,14 @@ function safeReadTextFile(filePath) {
44253
45001
  }
44254
45002
  }
44255
45003
  function walkRepo(startPath, visitor) {
44256
- if (!fs$2.existsSync(startPath) || !fs$2.statSync(startPath).isDirectory()) return;
45004
+ if (!fs$1.existsSync(startPath) || !fs$1.statSync(startPath).isDirectory()) return;
44257
45005
  const stack = [startPath];
44258
45006
  while (stack.length > 0) {
44259
45007
  const currentDir = stack.pop();
44260
45008
  if (!currentDir) continue;
44261
45009
  let entries;
44262
45010
  try {
44263
- entries = fs$2.readdirSync(currentDir, { withFileTypes: true });
45011
+ entries = fs$1.readdirSync(currentDir, { withFileTypes: true });
44264
45012
  } catch {
44265
45013
  continue;
44266
45014
  }
@@ -44300,7 +45048,7 @@ function buildTree(dirPath, maxDepth, depth = 0) {
44300
45048
  if (depth > maxDepth || shouldIgnoreRepoEntry(dirPath)) return [];
44301
45049
  let entries;
44302
45050
  try {
44303
- entries = fs$2.readdirSync(dirPath, { withFileTypes: true });
45051
+ entries = fs$1.readdirSync(dirPath, { withFileTypes: true });
44304
45052
  } catch (error) {
44305
45053
  const message = error instanceof Error ? error.message : String(error);
44306
45054
  return [`${" ".repeat(depth)}[error reading ${normalizeToolPath(dirPath)}: ${message}]`];
@@ -44337,8 +45085,8 @@ function resolveCanonicalToolPath(toolPath, allowMissing) {
44337
45085
  let remainingSegments = absolutePath.slice(canonicalPath.length).split(path$2.sep);
44338
45086
  let symlinksFollowed = 0;
44339
45087
  try {
44340
- if (fs$2.existsSync(absolutePath)) return {
44341
- canonicalPath: fs$2.realpathSync.native(absolutePath),
45088
+ if (fs$1.existsSync(absolutePath)) return {
45089
+ canonicalPath: fs$1.realpathSync.native(absolutePath),
44342
45090
  exists: true
44343
45091
  };
44344
45092
  if (!allowMissing) return null;
@@ -44350,25 +45098,25 @@ function resolveCanonicalToolPath(toolPath, allowMissing) {
44350
45098
  continue;
44351
45099
  }
44352
45100
  const candidatePath = path$2.join(canonicalPath, segment);
44353
- const entry = fs$2.lstatSync(candidatePath, { throwIfNoEntry: false });
45101
+ const entry = fs$1.lstatSync(candidatePath, { throwIfNoEntry: false });
44354
45102
  if (!entry) {
44355
45103
  canonicalPath = candidatePath;
44356
45104
  continue;
44357
45105
  }
44358
45106
  if (entry.isSymbolicLink()) {
44359
45107
  if (++symlinksFollowed > MAX_TOOL_PATH_SYMLINKS) return null;
44360
- const target = fs$2.readlinkSync(candidatePath);
45108
+ const target = fs$1.readlinkSync(candidatePath);
44361
45109
  const targetRoot = path$2.isAbsolute(target) ? path$2.parse(target).root : "";
44362
45110
  if (targetRoot) canonicalPath = targetRoot;
44363
45111
  remainingSegments = [...target.slice(targetRoot.length).split(path$2.sep), ...remainingSegments];
44364
45112
  continue;
44365
45113
  }
44366
45114
  if (remainingSegments.length > 0 && !entry.isDirectory()) return null;
44367
- canonicalPath = fs$2.realpathSync.native(candidatePath);
45115
+ canonicalPath = fs$1.realpathSync.native(candidatePath);
44368
45116
  }
44369
45117
  return {
44370
45118
  canonicalPath,
44371
- exists: fs$2.existsSync(canonicalPath)
45119
+ exists: fs$1.existsSync(canonicalPath)
44372
45120
  };
44373
45121
  } catch {
44374
45122
  return null;
@@ -44394,7 +45142,7 @@ function resolveToolPath(toolPath, options = {}) {
44394
45142
  ok: false,
44395
45143
  error: `Path does not exist: ${requestedPath}`
44396
45144
  };
44397
- const workspaceRoot = fs$2.realpathSync.native(process.cwd());
45145
+ const workspaceRoot = fs$1.realpathSync.native(process.cwd());
44398
45146
  const extraRoots = (options.allowedRoots || []).map((rootPath) => canonicalizeAllowedRoot(rootPath));
44399
45147
  const matchedRoot = [...extraRoots, workspaceRoot].find((rootPath) => isPathWithinRoot(resolved.canonicalPath, rootPath));
44400
45148
  if (!matchedRoot) return {
@@ -44415,7 +45163,7 @@ function resolveToolPath(toolPath, options = {}) {
44415
45163
  }
44416
45164
  }
44417
45165
  if (resolved.exists) {
44418
- const stat = fs$2.statSync(resolved.canonicalPath);
45166
+ const stat = fs$1.statSync(resolved.canonicalPath);
44419
45167
  if (options.requireFile && !stat.isFile()) return {
44420
45168
  ok: false,
44421
45169
  error: `Path is not a file: ${requestedPath}`
@@ -44463,7 +45211,7 @@ function createDefaultLocalTools(context) {
44463
45211
  allowedRoots: allowedReadRoots
44464
45212
  });
44465
45213
  if (!resolvedPath.ok) return `Error: ${resolvedPath.error}`;
44466
- return fs$2.readFileSync(resolvedPath.resolvedPath, "utf-8");
45214
+ return fs$1.readFileSync(resolvedPath.resolvedPath, "utf-8");
44467
45215
  }
44468
45216
  },
44469
45217
  write_file: {
@@ -44486,10 +45234,10 @@ function createDefaultLocalTools(context) {
44486
45234
  const resolvedPath = resolveToolPath(String(args.path || ""), { allowMissing: true });
44487
45235
  if (!resolvedPath.ok) return `Error: ${resolvedPath.error}`;
44488
45236
  const content = String(args.content || "");
44489
- 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 || "")}`;
44490
45238
  const dir = path$2.dirname(resolvedPath.resolvedPath);
44491
- fs$2.mkdirSync(dir, { recursive: true });
44492
- fs$2.writeFileSync(resolvedPath.resolvedPath, content);
45239
+ fs$1.mkdirSync(dir, { recursive: true });
45240
+ fs$1.writeFileSync(resolvedPath.resolvedPath, content);
44493
45241
  return "ok";
44494
45242
  }
44495
45243
  },
@@ -44505,7 +45253,7 @@ function createDefaultLocalTools(context) {
44505
45253
  execute: async (args) => {
44506
45254
  const resolvedPath = resolveToolPath(String(args.path || "."), { requireDirectory: true });
44507
45255
  if (!resolvedPath.ok) return `Error: ${resolvedPath.error}`;
44508
- 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");
44509
45257
  }
44510
45258
  },
44511
45259
  search_repo: {
@@ -44669,8 +45417,8 @@ function createCheckpointedWriteFileTool(taskName, stateDir, planPath, stateFile
44669
45417
  const content = String(args.content || "");
44670
45418
  ensureMarathonFileCheckpoint(taskName, resolvedPath.resolvedPath, stateDir, stateFilePath);
44671
45419
  const dir = path$2.dirname(resolvedPath.resolvedPath);
44672
- fs$2.mkdirSync(dir, { recursive: true });
44673
- fs$2.writeFileSync(resolvedPath.resolvedPath, content);
45420
+ fs$1.mkdirSync(dir, { recursive: true });
45421
+ fs$1.writeFileSync(resolvedPath.resolvedPath, content);
44674
45422
  const ext = path$2.extname(resolvedPath.resolvedPath).toLowerCase();
44675
45423
  if (ext === ".js" || ext === ".jsx") {
44676
45424
  const syntaxError = quickJsSyntaxCheck(content);
@@ -44746,7 +45494,7 @@ function createEditFileTool(taskName, stateDir, planPath, stateFilePath) {
44746
45494
  if (oldString === newString) return "Error: old_string and new_string are identical";
44747
45495
  let content;
44748
45496
  try {
44749
- content = fs$2.readFileSync(resolvedPath.resolvedPath, "utf-8");
45497
+ content = fs$1.readFileSync(resolvedPath.resolvedPath, "utf-8");
44750
45498
  } catch {
44751
45499
  return `Error: Could not read file: ${String(args.path || "")}`;
44752
45500
  }
@@ -44757,7 +45505,7 @@ function createEditFileTool(taskName, stateDir, planPath, stateFilePath) {
44757
45505
  }
44758
45506
  ensureMarathonFileCheckpoint(taskName, resolvedPath.resolvedPath, stateDir, stateFilePath);
44759
45507
  const updated = replaceAll ? content.split(oldString).join(newString) : content.replace(oldString, newString);
44760
- fs$2.writeFileSync(resolvedPath.resolvedPath, updated);
45508
+ fs$1.writeFileSync(resolvedPath.resolvedPath, updated);
44761
45509
  const ext = path$2.extname(resolvedPath.resolvedPath).toLowerCase();
44762
45510
  if (ext === ".js" || ext === ".jsx") {
44763
45511
  const syntaxError = quickJsSyntaxCheck(updated);
@@ -44809,13 +45557,13 @@ function createReadOffloadedOutputTool(taskName, stateDir, stateFilePath) {
44809
45557
  };
44810
45558
  const idEmbeddedSha256 = /^art_([0-9a-f]{64})$/.exec(outputId)?.[1];
44811
45559
  const outputPath = path$2.join(getOffloadedOutputDir(taskName, stateDir, stateFilePath), `${outputId}.txt`);
44812
- if (!fs$2.existsSync(outputPath) || !fs$2.statSync(outputPath).isFile()) {
45560
+ if (!fs$1.existsSync(outputPath) || !fs$1.statSync(outputPath).isFile()) {
44813
45561
  const resolvedStateFilePath = getStateFilePathForTask(taskName, stateDir, stateFilePath);
44814
45562
  const log = loadTreeLog(treeLogPathForStateFile(resolvedStateFilePath));
44815
45563
  const artifact = log ? findArtifactPayload(log, outputId) : void 0;
44816
45564
  if (artifact) {
44817
45565
  const ledgerPath = path$2.resolve(path$2.dirname(treeLogPathForStateFile(resolvedStateFilePath)), artifact.relativePath);
44818
- 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);
44819
45567
  return [
44820
45568
  `Error: offloaded output recorded in the context ledger but missing locally: ${outputId}`,
44821
45569
  artifact.host ? `Stored on host: ${artifact.host}` : void 0,
@@ -44824,7 +45572,7 @@ function createReadOffloadedOutputTool(taskName, stateDir, stateFilePath) {
44824
45572
  }
44825
45573
  return `Error: offloaded output not found: ${outputId}`;
44826
45574
  }
44827
- return verifyContent(fs$2.readFileSync(outputPath, "utf-8"), idEmbeddedSha256);
45575
+ return verifyContent(fs$1.readFileSync(outputPath, "utf-8"), idEmbeddedSha256);
44828
45576
  }
44829
45577
  };
44830
45578
  }
@@ -45008,7 +45756,7 @@ function resolveLedgerToolResult(tool, stateFilePath, log) {
45008
45756
  if (!artifact) return result;
45009
45757
  const artifactPath = path$2.resolve(path$2.dirname(treeLogPathForStateFile(stateFilePath)), artifact.relativePath);
45010
45758
  try {
45011
- 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");
45012
45760
  } catch {}
45013
45761
  return result;
45014
45762
  }
@@ -45222,8 +45970,8 @@ function offloadToolOutput(taskName, toolName, output, options = {}, stateDir) {
45222
45970
  const dir = artifactStoreDirForStateFile(stateFilePath);
45223
45971
  const fileName = `${outputId}.txt`;
45224
45972
  const filePath = path$2.join(dir, fileName);
45225
- fs$2.mkdirSync(dir, { recursive: true });
45226
- 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");
45227
45975
  recordOffloadedArtifact({
45228
45976
  taskName,
45229
45977
  toolName,
@@ -45373,14 +46121,14 @@ const RULES_DIR = ".marathon/rules";
45373
46121
  function loadRules(cwd) {
45374
46122
  const baseCwd = cwd || process.cwd();
45375
46123
  const rulesDir = path$2.resolve(baseCwd, RULES_DIR);
45376
- if (!fs$2.existsSync(rulesDir)) return [];
46124
+ if (!fs$1.existsSync(rulesDir)) return [];
45377
46125
  const rules = [];
45378
46126
  try {
45379
- const entries = fs$2.readdirSync(rulesDir).filter((f) => f.endsWith(".md"));
46127
+ const entries = fs$1.readdirSync(rulesDir).filter((f) => f.endsWith(".md"));
45380
46128
  for (const entry of entries) {
45381
46129
  const filePath = path$2.join(rulesDir, entry);
45382
46130
  try {
45383
- const parsed = parseRuleFile(fs$2.readFileSync(filePath, "utf-8"));
46131
+ const parsed = parseRuleFile(fs$1.readFileSync(filePath, "utf-8"));
45384
46132
  rules.push({
45385
46133
  ...parsed,
45386
46134
  filePath
@@ -45516,7 +46264,7 @@ function resolveErrorHandlingForPhase(phase, cliFallbackModel, milestoneFallback
45516
46264
  //#region src/marathon/playbook-loader.ts
45517
46265
  const PLAYBOOKS_DIR = ".runtype/marathons/playbooks";
45518
46266
  function getCandidatePaths(nameOrPath, cwd) {
45519
- const home = os$2.homedir();
46267
+ const home = os$1.homedir();
45520
46268
  return [
45521
46269
  path$2.resolve(cwd, nameOrPath),
45522
46270
  path$2.resolve(cwd, PLAYBOOKS_DIR, `${nameOrPath}.yaml`),
@@ -45556,7 +46304,7 @@ async function loadModulePlaybook(filePath) {
45556
46304
  return config;
45557
46305
  }
45558
46306
  function parsePlaybookFile(filePath) {
45559
- const raw = fs$2.readFileSync(filePath, "utf-8");
46307
+ const raw = fs$1.readFileSync(filePath, "utf-8");
45560
46308
  if (path$2.extname(filePath).toLowerCase() === ".json") return JSON.parse(raw);
45561
46309
  return parse(raw);
45562
46310
  }
@@ -45618,7 +46366,7 @@ async function loadPlaybookPlugins(plugins, playbookFilePath, playbookName) {
45618
46366
  const resolved = path$2.resolve(baseDir, plugin);
45619
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.`);
45620
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.`);
45621
- 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}.`);
45622
46370
  const mod = await import(pathToFileURL(resolved).href);
45623
46371
  if (typeof mod.default !== "function") throw new Error(`Playbook '${playbookName}': plugin "${plugin}" must export a default function: export default ({ registerWorkflowHook }) => { ... }`);
45624
46372
  await mod.default({ registerWorkflowHook });
@@ -45651,7 +46399,7 @@ function toWorkflowConfig(config) {
45651
46399
  async function loadPlaybook(nameOrPath, cwd) {
45652
46400
  const candidates = getCandidatePaths(nameOrPath, cwd || process.cwd());
45653
46401
  for (const candidate of candidates) {
45654
- if (!fs$2.existsSync(candidate) || fs$2.statSync(candidate).isDirectory()) continue;
46402
+ if (!fs$1.existsSync(candidate) || fs$1.statSync(candidate).isDirectory()) continue;
45655
46403
  const ext = path$2.extname(candidate).toLowerCase();
45656
46404
  const config = MODULE_PLAYBOOK_EXTENSIONS.has(ext) ? await loadModulePlaybook(candidate) : parsePlaybookFile(candidate);
45657
46405
  validatePlaybook(config, candidate);
@@ -46043,7 +46791,7 @@ async function taskAction(agent, options) {
46043
46791
  forcedResumeFilePath = stateResolution.filePath;
46044
46792
  } else {
46045
46793
  resumeRequested = false;
46046
- if (options.fresh && fs$2.existsSync(filePath)) {
46794
+ if (options.fresh && fs$1.existsSync(filePath)) {
46047
46795
  if (useStartupShell) setStartupStatus("starting fresh");
46048
46796
  else console.log(chalk.gray(`Starting fresh and ignoring saved local state at ${filePath}`));
46049
46797
  }
@@ -48877,17 +49625,17 @@ const deployCommand = new Command("deploy").description("Export an agent and sca
48877
49625
  }
48878
49626
  slugSet.add(slug);
48879
49627
  }
48880
- fs$2.mkdirSync(path$2.join(outDir, "agents"), { recursive: true });
48881
- 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 });
48882
49630
  for (const { name, def } of agentDefs) {
48883
49631
  const filename = `${slugify(name)}.json`;
48884
- 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));
48885
49633
  console.log(` Wrote agents/${filename}`);
48886
49634
  }
48887
49635
  const agentNames = agentDefs.map((a) => a.name);
48888
49636
  let monorepoRoot = process.cwd();
48889
49637
  for (let i = 0; i < 8; i++) {
48890
- 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;
48891
49639
  const parent = path$2.dirname(monorepoRoot);
48892
49640
  if (parent === monorepoRoot) break;
48893
49641
  monorepoRoot = parent;
@@ -48903,35 +49651,35 @@ const deployCommand = new Command("deploy").description("Export an agent and sca
48903
49651
  const secretNames = Array.from(allSecrets);
48904
49652
  const optionalSettings = Array.from(allOptionalSettings.values());
48905
49653
  if (target === "cloudflare") {
48906
- fs$2.mkdirSync(path$2.join(outDir, "src"), { recursive: true });
48907
- fs$2.writeFileSync(path$2.join(outDir, "src", "index.ts"), workerIndexTs(agentSlugs));
48908
- fs$2.writeFileSync(path$2.join(outDir, "wrangler.toml"), wranglerToml(projectName));
48909
- fs$2.writeFileSync(path$2.join(outDir, "package.json"), workerPackageJson(projectName, "workspace:*"));
48910
- 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());
48911
49659
  const setupPath = path$2.join(outDir, "setup.sh");
48912
- fs$2.writeFileSync(setupPath, workerSetupSh(monorepoRoot));
48913
- fs$2.chmodSync(setupPath, 493);
48914
- 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));
48915
49663
  } else if (target === "vercel") {
48916
- fs$2.mkdirSync(path$2.join(outDir, "api"), { recursive: true });
48917
- fs$2.writeFileSync(path$2.join(outDir, "api", "[[...route]].ts"), vercelRouteTs(agentSlugs));
48918
- fs$2.writeFileSync(path$2.join(outDir, "vercel.json"), vercelJson());
48919
- fs$2.writeFileSync(path$2.join(outDir, "package.json"), vercelPackageJson(projectName, "workspace:*"));
48920
- 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());
48921
49669
  const setupPath = path$2.join(outDir, "setup.sh");
48922
- fs$2.writeFileSync(setupPath, vercelSetupSh(monorepoRoot));
48923
- fs$2.chmodSync(setupPath, 493);
48924
- 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));
48925
49673
  } else {
48926
- fs$2.writeFileSync(path$2.join(outDir, "server.ts"), serverTs());
48927
- fs$2.writeFileSync(path$2.join(outDir, "Dockerfile"), dockerfile());
48928
- fs$2.writeFileSync(path$2.join(outDir, ".dockerignore"), dockerignore());
48929
- fs$2.writeFileSync(path$2.join(outDir, "tsconfig.json"), tsconfigJson());
48930
- 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:*"));
48931
49679
  const setupPath = path$2.join(outDir, "setup.sh");
48932
- fs$2.writeFileSync(setupPath, setupSh(monorepoRoot));
48933
- fs$2.chmodSync(setupPath, 493);
48934
- 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));
48935
49683
  }
48936
49684
  console.log("");
48937
49685
  console.log(chalk.green(`✓ Scaffold written to ${outDir}`));
@@ -49000,13 +49748,13 @@ const agentExportRuntimeCommand = new Command("export-runtime").description("Exp
49000
49748
  if (options.format === "json") {
49001
49749
  const exported = await client.agents.exportRuntime(id, selector.params);
49002
49750
  const body = `${JSON.stringify(exported, null, 2)}\n`;
49003
- 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);
49004
49752
  else process.stdout.write(body);
49005
49753
  return;
49006
49754
  }
49007
49755
  const bundle = await client.agents.exportRuntimeBundle(id, selector.params);
49008
49756
  const outPath = path$2.resolve(options.output ?? bundle.fileName);
49009
- fs$2.writeFileSync(outPath, bundle.bytes);
49757
+ fs$1.writeFileSync(outPath, bundle.bytes);
49010
49758
  console.log(JSON.stringify({
49011
49759
  path: outPath,
49012
49760
  bytes: bundle.bytes.byteLength,
@@ -56536,7 +57284,7 @@ npm install -g @runtypelabs/cli
56536
57284
  runtype auth status
56537
57285
  \`\`\`
56538
57286
 
56539
- 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.
56540
57288
 
56541
57289
  ### 2. Sign the user up from the terminal
56542
57290
 
@@ -56625,7 +57373,9 @@ The signup commands wrap the agent auth protocol documented at \`https://runtype
56625
57373
  - \`runtype auth claim <email>\` — re-send or change the email for a pending signup
56626
57374
  - \`runtype auth verify <code>\` — finish signup with the 6-digit code
56627
57375
  - \`runtype auth login --api-key <key>\` — privately store an existing credential; not an account-creation command
56628
- - \`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)
56629
57379
  - \`runtype auth whoami --no-tty\` — verify the effective credential (environment key first, then stored login)
56630
57380
 
56631
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.
@@ -58497,7 +59247,7 @@ function readIdentity(value) {
58497
59247
  function resolveDeviceListenName(requested) {
58498
59248
  const explicit = requested?.trim();
58499
59249
  if (explicit) return explicit.slice(0, 120);
58500
- const hostname = os$1.hostname().trim();
59250
+ const hostname = os.hostname().trim();
58501
59251
  if (!hostname) return "This computer";
58502
59252
  return hostname.slice(0, 120);
58503
59253
  }
@@ -58517,8 +59267,8 @@ async function signDeviceListenChallenge(identity, input) {
58517
59267
  });
58518
59268
  }
58519
59269
  function deviceListenEncryptionKey() {
58520
- const hostname = os$1.hostname();
58521
- const username = os$1.userInfo().username;
59270
+ const hostname = os.hostname();
59271
+ const username = os.userInfo().username;
58522
59272
  return createHash("sha256").update(`runtype-device-listen-${hostname}-${username}`).digest("hex");
58523
59273
  }
58524
59274
  function isCorruptStoreError(error) {
@@ -59072,8 +59822,8 @@ var MacOSDeviceProvider = class {
59072
59822
  constructor(options = {}) {
59073
59823
  this.platform = options.platform ?? process.platform;
59074
59824
  this.architecture = options.architecture ?? process.arch;
59075
- this.operatingSystemRelease = options.operatingSystemRelease ?? os$1.release();
59076
- this.homeDirectory = options.homeDirectory ?? os$1.homedir();
59825
+ this.operatingSystemRelease = options.operatingSystemRelease ?? os.release();
59826
+ this.homeDirectory = options.homeDirectory ?? os.homedir();
59077
59827
  this.workingDirectory = options.workingDirectory ?? process.cwd();
59078
59828
  this.environment = options.environment ?? process.env;
59079
59829
  this.sidecarTimeoutMs = options.sidecarTimeoutMs ?? DEFAULT_SIDECAR_TIMEOUT_MS;
@@ -59775,6 +60525,7 @@ function registerGlobalOptionForwarding(program) {
59775
60525
  const options = actionCommand.optsWithGlobals();
59776
60526
  const invocation = {
59777
60527
  apiUrl: apiUrls.at(-1),
60528
+ org: typeof options.org === "string" && options.org ? options.org : void 0,
59778
60529
  expectApiUrl: options.expectApiUrl,
59779
60530
  expectOrg: options.expectOrg,
59780
60531
  noInput: options.input === false || options.noInput === true,
@@ -59800,10 +60551,12 @@ function registerGlobalOptionForwarding(program) {
59800
60551
  //#region src/index.ts
59801
60552
  loadEnv();
59802
60553
  const program = new Command();
59803
- 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());
59804
60555
  program.addCommand(initCommand);
59805
60556
  program.addCommand(loginCommand);
59806
60557
  program.addCommand(authCommand);
60558
+ program.addCommand(linkCommand);
60559
+ program.addCommand(unlinkCommand);
59807
60560
  program.addCommand(flowsCommand);
59808
60561
  program.addCommand(recordsCommand);
59809
60562
  program.addCommand(collectionsCommand);
@@ -59902,7 +60655,7 @@ run().catch((error) => {
59902
60655
  }
59903
60656
  });
59904
60657
  async function handleNoCommand() {
59905
- if (!await new CredentialStore().hasCredentials()) {
60658
+ if (!((await new CredentialStore().listAccounts()).length > 0) && !process.env.RUNTYPE_API_KEY) {
59906
60659
  console.log(chalk.cyan("\nWelcome to Runtype CLI!\n"));
59907
60660
  console.log("It looks like this is your first time. Run the setup wizard:");
59908
60661
  console.log(` ${chalk.green("runtype init")}\n`);