@qawolf/cli 1.24.0 → 1.26.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/cli.js CHANGED
@@ -8390,11 +8390,82 @@ function extractMissingPackage(text) {
8390
8390
  return specifier;
8391
8391
  }
8392
8392
 
8393
+ // src/core/pluralize.ts
8394
+ function pluralize(count, singular, plural = `${singular}s`) {
8395
+ return `${String(count)} ${count === 1 ? singular : plural}`;
8396
+ }
8397
+
8393
8398
  // src/core/formatSeconds.ts
8394
8399
  function formatSeconds(ms) {
8395
8400
  return `${Math.round(ms / 1000)}s`;
8396
8401
  }
8397
8402
 
8403
+ // src/core/messages/authErrors.ts
8404
+ var authErrorMessages = {
8405
+ identity: {
8406
+ invalidOrUnauthorized: "API key is invalid or unauthorized",
8407
+ unexpectedFormat: "Could not verify API key: unexpected response format",
8408
+ couldNotVerify: (detail, status) => `Could not verify API key: ${detail || `HTTP ${status}`}`,
8409
+ couldNotVerifyNetwork: (cause) => `Could not verify API key: ${cause}`,
8410
+ timedOut: (timeoutMs) => `Could not verify API key: the QA Wolf API did not answer within ${formatSeconds(timeoutMs)}.`
8411
+ },
8412
+ request: {
8413
+ rejected401: (noun) => `QA Wolf API rejected the${noun ? ` ${noun}` : ""} request (HTTP 401). Check your API key.`,
8414
+ rejected402: (noun) => `QA Wolf API refused the${noun ? ` ${noun}` : ""} request (HTTP 402): billing prevented it.`,
8415
+ rejected403: (noun) => `QA Wolf API rejected the${noun ? ` ${noun}` : ""} request (HTTP 403). Check that your API key has access to this environment.`,
8416
+ notFound404: (noun) => `QA Wolf API could not find ${noun ? `${noun} for that environment` : "that environment"} (HTTP 404). Check the --env value.`,
8417
+ failedWithStatus: (status, noun) => `QA Wolf API${noun ? ` ${noun}` : ""} request failed (HTTP ${status}).`,
8418
+ networkUnreachable: (baseUrl, noun) => `Could not reach the QA Wolf API at ${baseUrl}${noun ? ` to fetch ${noun}` : ""}. Check your network connection and QAWOLF_HOST_URL.`,
8419
+ timedOut: (timeoutMs, noun) => `The QA Wolf API${noun ? ` ${noun}` : ""} request timed out after ${formatSeconds(timeoutMs)}. The work may still be finishing on the platform.`,
8420
+ unexpectedResponse: (noun) => `Unexpected${noun ? ` ${noun}` : ""} response from the QA Wolf API.`
8421
+ },
8422
+ workos: {
8423
+ unexpectedResponse: "WorkOS returned an unexpected response",
8424
+ unexpectedResponseWithStatus: (status) => `WorkOS returned an unexpected response (HTTP ${status})`,
8425
+ unreachable: (detail) => `Could not reach WorkOS: ${detail}`,
8426
+ redirected: "WorkOS answered with a redirect, which the CLI does not follow for a sign-in request",
8427
+ noClientForSession: "This session names no WorkOS client"
8428
+ },
8429
+ bundle: {
8430
+ linkExpired: "The flow bundle download link has expired. Please run `qawolf flows pull` again to refresh.",
8431
+ failedWithStatus: (status) => `Could not download the flow bundle (HTTP ${status}).`,
8432
+ networkUnreachable: "Could not reach the flow bundle storage. Check your network connection and try again.",
8433
+ timedOut: (timeoutMs) => `Downloading the flow bundle stalled — no data arrived for ${formatSeconds(timeoutMs)}. Please try again.`,
8434
+ malformed: "The flow bundle download was malformed. Please run `qawolf flows pull` again."
8435
+ }
8436
+ };
8437
+
8438
+ // src/core/messages/whoami.ts
8439
+ var whoamiMessages = {
8440
+ source: (source) => `Source: ${source}`,
8441
+ authFailed: (source, error) => `Authentication failed (source: ${source}): ${error}`,
8442
+ authenticatedAs: (teamName, source) => `Authenticated as ${teamName} (source: ${source})`,
8443
+ teamNote: (input) => [
8444
+ `Workspace: ${input.team.name}`,
8445
+ `ID: ${input.team.id}`,
8446
+ input.team.slug ? `Slug: ${input.team.slug}` : undefined,
8447
+ input.teamUrl ? `URL: ${input.teamUrl}` : undefined,
8448
+ `Source: ${input.source}`
8449
+ ].filter((line) => Boolean(line)).join(`
8450
+ `),
8451
+ organizationNote: (input) => [
8452
+ `Organization: ${input.organization.name}`,
8453
+ `ID: ${input.organization.id}`,
8454
+ `Source: ${input.source}`
8455
+ ].join(`
8456
+ `),
8457
+ activeWorkspace: (workspaceId, found) => found ? `${found.workspace} (${found.organization})` : `${workspaceId} — no longer in reach; run 'qawolf auth switch'`,
8458
+ userNote: (input) => [
8459
+ `User: ${input.user.email}`,
8460
+ `ID: ${input.user.id}`,
8461
+ `Organization: ${input.organization.name}`,
8462
+ input.activeWorkspace ? `Workspace: ${input.activeWorkspace}` : undefined,
8463
+ input.organizations.length > 0 ? `Workspaces: ${input.organizations.map((o) => `${o.name}: ${o.workspaces.map((w) => w.name).join(", ")}`).join(" | ")}` : undefined,
8464
+ `Source: ${input.source}`
8465
+ ].filter((line) => Boolean(line)).join(`
8466
+ `)
8467
+ };
8468
+
8398
8469
  // src/core/messages/auth.ts
8399
8470
  var authMessages = {
8400
8471
  title: "QA Wolf Authentication",
@@ -8412,7 +8483,47 @@ var authMessages = {
8412
8483
  whoamiFailed: "Not authenticated",
8413
8484
  login: {
8414
8485
  nonInteractive: "auth login requires an interactive terminal. Set the QAWOLF_API_KEY environment variable for CI authentication.",
8415
- reAuthPrompt: "You are already authenticated. Re-authenticate?"
8486
+ reAuthPrompt: "You are already authenticated. Re-authenticate?",
8487
+ chooseMethod: "How do you want to sign in?",
8488
+ methodBrowser: "Browser",
8489
+ methodBrowserHint: "Sign in with your QA Wolf account",
8490
+ methodApiKey: "API key",
8491
+ methodApiKeyHint: "Paste a team key — needed for flows pull and flows run",
8492
+ apiKeyPrecedence: {
8493
+ env: "QAWOLF_API_KEY is set, and an API key takes precedence over a browser session. Commands continue to use that key until you unset the variable.",
8494
+ stored: "A stored API key takes precedence over a browser session. Commands continue to use that key until you run 'qawolf auth logout' and sign in again."
8495
+ }
8496
+ },
8497
+ device: {
8498
+ unavailable: "This QA Wolf deployment does not offer browser sign-in. Run 'qawolf auth login' again and choose 'API key'.",
8499
+ configUnreachable: "Could not ask this QA Wolf deployment whether it offers browser sign-in. Check your connection, then try again.",
8500
+ confirmCode: (userCode) => `Your code is ${userCode}`,
8501
+ visitUrl: (url) => `Confirm it at ${url}`,
8502
+ visitUrlPlain: (url) => `Or go to ${url} and enter the code`,
8503
+ openFailed: (url) => `Could not open a browser automatically. Open ${url} yourself to continue.`,
8504
+ waiting: "Waiting for you to finish in the browser",
8505
+ signedIn: (email) => `Signed in as ${email}.`,
8506
+ failed: {
8507
+ "access-denied": "The sign-in request was rejected.",
8508
+ expired: "The sign-in request expired. Run 'qawolf auth login' to retry.",
8509
+ timeout: "The sign-in request timed out. Run 'qawolf auth login' to retry.",
8510
+ network: "Could not reach WorkOS to complete sign-in.",
8511
+ unavailable: "Could not start browser sign-in.",
8512
+ cancelled: "Sign-in cancelled."
8513
+ }
8514
+ },
8515
+ workspace: {
8516
+ chooseOrganization: "Which organization do you want to work in?",
8517
+ choose: "Which workspace do you want to use?",
8518
+ workspaceCount: (count) => pluralize(count, "workspace"),
8519
+ working: (organization, workspace) => `Working in ${workspace} (${organization}).`,
8520
+ none: "This account reaches no organizations yet.",
8521
+ cancelled: "Workspace not changed.",
8522
+ notSignedIn: "Workspace switching needs a browser sign-in. Run 'qawolf auth login' and choose Browser.",
8523
+ sessionExpired: "Your session could not be renewed. Run 'qawolf auth login' to sign in again.",
8524
+ nonInteractive: "auth switch needs an interactive terminal, or set QAWOLF_WORKSPACE to name a workspace.",
8525
+ sessionChanged: "The stored sign-in changed while you made your choice. Run the command again.",
8526
+ saveFailed: (detail) => `Could not save the workspace choice: ${detail}`
8416
8527
  },
8417
8528
  logout: {
8418
8529
  title: "Log Out",
@@ -8424,58 +8535,8 @@ var authMessages = {
8424
8535
  success: "Logged out successfully.",
8425
8536
  cancelled: "Logout cancelled."
8426
8537
  },
8427
- errors: {
8428
- identity: {
8429
- invalidOrUnauthorized: "API key is invalid or unauthorized",
8430
- unexpectedFormat: "Could not verify API key: unexpected response format",
8431
- couldNotVerify: (detail, status) => `Could not verify API key: ${detail || `HTTP ${status}`}`,
8432
- couldNotVerifyNetwork: (cause) => `Could not verify API key: ${cause}`,
8433
- timedOut: (timeoutMs) => `Could not verify API key: the QA Wolf API did not answer within ${formatSeconds(timeoutMs)}.`
8434
- },
8435
- request: {
8436
- rejected401: (noun) => `QA Wolf API rejected the${noun ? ` ${noun}` : ""} request (HTTP 401). Check your API key.`,
8437
- rejected402: (noun) => `QA Wolf API refused the${noun ? ` ${noun}` : ""} request (HTTP 402): billing prevented it.`,
8438
- rejected403: (noun) => `QA Wolf API rejected the${noun ? ` ${noun}` : ""} request (HTTP 403). Check that your API key has access to this environment.`,
8439
- notFound404: (noun) => `QA Wolf API could not find ${noun ? `${noun} for that environment` : "that environment"} (HTTP 404). Check the --env value.`,
8440
- failedWithStatus: (status, noun) => `QA Wolf API${noun ? ` ${noun}` : ""} request failed (HTTP ${status}).`,
8441
- networkUnreachable: (baseUrl, noun) => `Could not reach the QA Wolf API at ${baseUrl}${noun ? ` to fetch ${noun}` : ""}. Check your network connection and QAWOLF_HOST_URL.`,
8442
- timedOut: (timeoutMs, noun) => `The QA Wolf API${noun ? ` ${noun}` : ""} request timed out after ${formatSeconds(timeoutMs)}. The work may still be finishing on the platform.`,
8443
- unexpectedResponse: (noun) => `Unexpected${noun ? ` ${noun}` : ""} response from the QA Wolf API.`
8444
- },
8445
- bundle: {
8446
- linkExpired: "The flow bundle download link has expired. Please run `qawolf flows pull` again to refresh.",
8447
- failedWithStatus: (status) => `Could not download the flow bundle (HTTP ${status}).`,
8448
- networkUnreachable: "Could not reach the flow bundle storage. Check your network connection and try again.",
8449
- timedOut: (timeoutMs) => `Downloading the flow bundle stalled — no data arrived for ${formatSeconds(timeoutMs)}. Please try again.`,
8450
- malformed: "The flow bundle download was malformed. Please run `qawolf flows pull` again."
8451
- }
8452
- },
8453
- whoami: {
8454
- source: (source) => `Source: ${source}`,
8455
- authFailed: (source, error) => `Authentication failed (source: ${source}): ${error}`,
8456
- authenticatedAs: (teamName, source) => `Authenticated as ${teamName} (source: ${source})`,
8457
- teamNote: (input) => [
8458
- `Team: ${input.team.name}`,
8459
- `ID: ${input.team.id}`,
8460
- input.team.slug ? `Slug: ${input.team.slug}` : undefined,
8461
- input.teamUrl ? `URL: ${input.teamUrl}` : undefined,
8462
- `Source: ${input.source}`
8463
- ].filter((line) => Boolean(line)).join(`
8464
- `),
8465
- organizationNote: (input) => [
8466
- `Organization: ${input.organization.name}`,
8467
- `ID: ${input.organization.id}`,
8468
- `Source: ${input.source}`
8469
- ].join(`
8470
- `),
8471
- userNote: (input) => [
8472
- `User: ${input.user.email}`,
8473
- `ID: ${input.user.id}`,
8474
- `Organization: ${input.organization.name}`,
8475
- `Source: ${input.source}`
8476
- ].join(`
8477
- `)
8478
- }
8538
+ errors: authErrorMessages,
8539
+ whoami: whoamiMessages
8479
8540
  };
8480
8541
  // src/core/messages/toolNotFound.ts
8481
8542
  var installRemedy = "Run `qawolf install` to install the runtime dependencies.";
@@ -8568,11 +8629,6 @@ var environmentsMessages = {
8568
8629
  usingPulledEnv: (env) => `Could not reach the platform to resolve environment ${env}; using the copy pulled into .qawolf/${env}.`,
8569
8630
  tooManyPages: (pages) => `Stopped listing environments after ${pages} pages. Pass --env explicitly.`
8570
8631
  };
8571
- // src/core/pluralize.ts
8572
- function pluralize(count, singular, plural = `${singular}s`) {
8573
- return `${String(count)} ${count === 1 ? singular : plural}`;
8574
- }
8575
-
8576
8632
  // src/core/messages/flows.ts
8577
8633
  var flowsMessages = {
8578
8634
  title: "Flows",
@@ -9027,25 +9083,32 @@ function exit(code, message, proc = process) {
9027
9083
  }
9028
9084
  return proc.exit(code);
9029
9085
  }
9030
- function flushAndExit(code, proc = process, scheduleBackstop = (fn) => {
9031
- setTimeout(fn, 2000).unref();
9086
+ var backstopMs = 2000;
9087
+ function exitWhenIdle(code, proc = process, scheduleBackstop = (fn) => {
9088
+ setTimeout(fn, backstopMs).unref();
9032
9089
  }) {
9033
- let exited = false;
9034
- const exitOnce = () => {
9035
- if (exited)
9036
- return;
9037
- exited = true;
9038
- proc.exit(code);
9039
- };
9040
- let pending = 2;
9041
- const onFlushed = () => {
9042
- pending -= 1;
9043
- if (pending === 0)
9044
- exitOnce();
9090
+ proc.exitCode = code;
9091
+ scheduleBackstop(() => proc.exit(code));
9092
+ }
9093
+ var signalExitCodes = {
9094
+ SIGINT: 130,
9095
+ SIGTERM: 143
9096
+ };
9097
+ var signalGraceMs = 150;
9098
+ function scheduleSignalGrace(fn) {
9099
+ return setTimeout(fn, signalGraceMs);
9100
+ }
9101
+ function createSignalExit(deps) {
9102
+ const proc = deps.proc ?? process;
9103
+ const scheduleGrace = deps.scheduleGrace ?? scheduleSignalGrace;
9104
+ let signalled = false;
9105
+ return (signal) => () => {
9106
+ const code = signalExitCodes[signal];
9107
+ if (signalled)
9108
+ proc.exit(code);
9109
+ signalled = true;
9110
+ deps.shutdown(signal).catch(() => {}).finally(() => exitWhenIdle(code, proc, scheduleGrace));
9045
9111
  };
9046
- proc.stdout.write("", onFlushed);
9047
- proc.stderr.write("", onFlushed);
9048
- scheduleBackstop(exitOnce);
9049
9112
  }
9050
9113
 
9051
9114
  // src/shell/fs.ts
@@ -9161,43 +9224,30 @@ function resolveHostUrl(env) {
9161
9224
  }
9162
9225
 
9163
9226
  // src/domains/auth/resolve.ts
9164
- import { Entry as Entry3 } from "@napi-rs/keyring";
9165
-
9166
- // src/domains/auth/store/delete.ts
9167
- import { join as join2 } from "node:path";
9168
- import { Entry } from "@napi-rs/keyring";
9169
-
9170
- // src/domains/auth/store/constants.ts
9171
- var service = "qawolf-cli";
9172
- var account = "api-key";
9173
- var credentialsFile = "credentials.json";
9227
+ import { Entry as Entry5 } from "@napi-rs/keyring";
9174
9228
 
9175
- // src/domains/auth/store/delete.ts
9176
- function deleteFromKeychain() {
9177
- try {
9178
- new Entry(service, account).deletePassword();
9179
- return "deleted";
9180
- } catch {
9181
- return "unavailable";
9182
- }
9183
- }
9184
- async function deleteFromFile(configDir, fs) {
9229
+ // src/core/deviceAuth/tokenExpiry.ts
9230
+ function readAccessTokenExpiry(accessToken) {
9231
+ const segments = accessToken.split(".");
9232
+ if (segments.length !== 3)
9233
+ return;
9234
+ const [, payload] = segments;
9235
+ if (!payload)
9236
+ return;
9237
+ let claims;
9185
9238
  try {
9186
- await fs.unlink(join2(configDir, credentialsFile));
9187
- return "deleted";
9239
+ claims = JSON.parse(Buffer.from(payload, "base64url").toString("utf8"));
9188
9240
  } catch {
9189
- return "not-found";
9241
+ return;
9190
9242
  }
9243
+ if (typeof claims !== "object" || claims === null)
9244
+ return;
9245
+ const exp = claims.exp;
9246
+ if (typeof exp !== "number" || !Number.isFinite(exp))
9247
+ return;
9248
+ const expiresAtMs = exp * 1000;
9249
+ return Number.isFinite(expiresAtMs) ? expiresAtMs : undefined;
9191
9250
  }
9192
- async function deleteApiKey(configDir, fs) {
9193
- const [keychain, file] = await Promise.all([
9194
- Promise.resolve(deleteFromKeychain()),
9195
- deleteFromFile(configDir, fs)
9196
- ]);
9197
- return { keychain, file };
9198
- }
9199
- // src/domains/auth/store/load.ts
9200
- import { join as join3 } from "node:path";
9201
9251
 
9202
9252
  // node_modules/zod/v4/core/core.js
9203
9253
  var _a;
@@ -14424,10 +14474,229 @@ function date4(params) {
14424
14474
 
14425
14475
  // node_modules/zod/v4/classic/external.js
14426
14476
  config(en_default());
14477
+ // src/shell/workos/types.ts
14478
+ var defaultWorkosBaseUrl = "https://api.workos.com";
14479
+ var defaultIntervalSec = 5;
14480
+ var deviceAuthorizationBody = object({
14481
+ device_code: string2().min(1),
14482
+ user_code: string2().min(1),
14483
+ verification_uri: string2().min(1),
14484
+ verification_uri_complete: string2().min(1).optional(),
14485
+ expires_in: number2().int().positive(),
14486
+ interval: number2().int().positive().optional()
14487
+ });
14488
+ var deviceTokenBody = object({
14489
+ access_token: string2().min(1),
14490
+ refresh_token: string2().min(1),
14491
+ user: object({ email: string2().min(1) }),
14492
+ organization_id: string2().min(1).optional()
14493
+ });
14494
+ var oauthErrorBody = object({
14495
+ error: string2().min(1),
14496
+ error_description: string2().min(1).optional()
14497
+ });
14498
+ var authenticationErrorBody = object({
14499
+ code: string2().min(1),
14500
+ message: string2().min(1).optional()
14501
+ });
14502
+
14503
+ // src/shell/workos/send.ts
14504
+ var timeoutMs = 15000;
14505
+ var unexpectedResponse = authErrorMessages.workos.unexpectedResponse;
14506
+ function isTransientStatus(status) {
14507
+ return status >= 500 || status === 429 || status === 408;
14508
+ }
14509
+ async function sendWorkosRequest(url, init, fetchFn) {
14510
+ let response;
14511
+ try {
14512
+ response = await fetchFn(url, {
14513
+ method: "POST",
14514
+ headers: init.headers,
14515
+ body: init.body,
14516
+ redirect: "manual",
14517
+ signal: AbortSignal.timeout(timeoutMs)
14518
+ });
14519
+ } catch (err) {
14520
+ return {
14521
+ kind: "failure",
14522
+ detail: authErrorMessages.workos.unreachable(errorMessage(err)),
14523
+ retryable: true
14524
+ };
14525
+ }
14526
+ if (response.status >= 300 && response.status < 400) {
14527
+ return {
14528
+ kind: "failure",
14529
+ detail: authErrorMessages.workos.redirected,
14530
+ retryable: false
14531
+ };
14532
+ }
14533
+ let json;
14534
+ try {
14535
+ json = await response.json();
14536
+ } catch {
14537
+ return {
14538
+ kind: "failure",
14539
+ detail: authErrorMessages.workos.unexpectedResponseWithStatus(response.status),
14540
+ retryable: response.ok || isTransientStatus(response.status)
14541
+ };
14542
+ }
14543
+ if (response.ok)
14544
+ return { kind: "json", json };
14545
+ if (isTransientStatus(response.status)) {
14546
+ return {
14547
+ kind: "failure",
14548
+ detail: authErrorMessages.workos.unexpectedResponseWithStatus(response.status),
14549
+ retryable: true
14550
+ };
14551
+ }
14552
+ const parsed = oauthErrorBody.safeParse(json);
14553
+ if (parsed.success) {
14554
+ return {
14555
+ kind: "oauth-error",
14556
+ code: parsed.data.error,
14557
+ description: parsed.data.error_description
14558
+ };
14559
+ }
14560
+ const authError = authenticationErrorBody.safeParse(json);
14561
+ if (authError.success) {
14562
+ return {
14563
+ kind: "oauth-error",
14564
+ code: authError.data.code,
14565
+ description: authError.data.message
14566
+ };
14567
+ }
14568
+ return {
14569
+ kind: "failure",
14570
+ detail: authErrorMessages.workos.unexpectedResponseWithStatus(response.status),
14571
+ retryable: false
14572
+ };
14573
+ }
14574
+
14575
+ // src/shell/workos/refreshAccessToken.ts
14576
+ async function refreshAccessToken(refreshToken, organizationId, deps) {
14577
+ const params = new URLSearchParams({
14578
+ grant_type: "refresh_token",
14579
+ refresh_token: refreshToken,
14580
+ client_id: deps.clientId
14581
+ });
14582
+ if (organizationId)
14583
+ params.set("organization_id", organizationId);
14584
+ const outcome = await sendWorkosRequest(`${deps.baseUrl}/user_management/authenticate`, {
14585
+ headers: { "content-type": "application/x-www-form-urlencoded" },
14586
+ body: params.toString()
14587
+ }, deps.fetch);
14588
+ if (outcome.kind === "failure") {
14589
+ return { ok: false, error: outcome.detail, retryable: outcome.retryable };
14590
+ }
14591
+ if (outcome.kind === "oauth-error") {
14592
+ return {
14593
+ ok: false,
14594
+ error: outcome.description ?? outcome.code,
14595
+ retryable: false
14596
+ };
14597
+ }
14598
+ const parsed = deviceTokenBody.safeParse(outcome.json);
14599
+ if (!parsed.success) {
14600
+ return { ok: false, error: unexpectedResponse, retryable: false };
14601
+ }
14602
+ return {
14603
+ ok: true,
14604
+ value: {
14605
+ accessToken: parsed.data.access_token,
14606
+ refreshToken: parsed.data.refresh_token,
14607
+ expiresAt: readAccessTokenExpiry(parsed.data.access_token),
14608
+ email: parsed.data.user.email,
14609
+ organizationId: parsed.data.organization_id
14610
+ }
14611
+ };
14612
+ }
14613
+
14614
+ // src/shell/workos/config.ts
14615
+ function resolveWorkosConfig(publishedClientId) {
14616
+ const clientId = publishedClientId?.trim();
14617
+ if (!clientId)
14618
+ return { configured: false };
14619
+ return { configured: true, clientId, baseUrl: defaultWorkosBaseUrl };
14620
+ }
14621
+
14622
+ // src/domains/auth/store/delete.ts
14623
+ import { join as join2 } from "node:path";
14624
+ import { Entry } from "@napi-rs/keyring";
14625
+
14626
+ // src/domains/auth/store/constants.ts
14627
+ var service = "qawolf-cli";
14628
+ var account = "api-key";
14629
+ var credentialsFile = "credentials.json";
14630
+ var tokensAccount = "oauth-tokens";
14631
+ var tokensFile = "tokens.json";
14632
+
14633
+ // src/domains/auth/store/delete.ts
14634
+ function deleteFromKeychain() {
14635
+ try {
14636
+ new Entry(service, account).deletePassword();
14637
+ return "deleted";
14638
+ } catch {
14639
+ return "unavailable";
14640
+ }
14641
+ }
14642
+ async function deleteFromFile(configDir, fs) {
14643
+ try {
14644
+ await fs.unlink(join2(configDir, credentialsFile));
14645
+ return "deleted";
14646
+ } catch (err) {
14647
+ if (isNoEntError(err))
14648
+ return "not-found";
14649
+ throw err;
14650
+ }
14651
+ }
14652
+ async function deleteApiKey(configDir, fs) {
14653
+ const [keychain, file] = await Promise.all([
14654
+ Promise.resolve(deleteFromKeychain()),
14655
+ deleteFromFile(configDir, fs)
14656
+ ]);
14657
+ return { keychain, file };
14658
+ }
14659
+ // src/domains/auth/store/hasStoredCredentials.ts
14660
+ import { join as join3 } from "node:path";
14661
+ import { Entry as Entry2 } from "@napi-rs/keyring";
14662
+ async function fileExists(path, fs) {
14663
+ try {
14664
+ await fs.readFile(path);
14665
+ return true;
14666
+ } catch (err) {
14667
+ return !isNoEntError(err);
14668
+ }
14669
+ }
14670
+ function keychainHolds(entryAccount) {
14671
+ try {
14672
+ return Boolean(new Entry2(service, entryAccount).getPassword());
14673
+ } catch {
14674
+ return false;
14675
+ }
14676
+ }
14677
+ async function hasStoredCredentials(configDir, fs) {
14678
+ const [apiKeyFile, tokenFile] = await Promise.all([
14679
+ fileExists(join3(configDir, credentialsFile), fs),
14680
+ fileExists(join3(configDir, tokensFile), fs)
14681
+ ]);
14682
+ return apiKeyFile || tokenFile || keychainHolds(account) || keychainHolds(tokensAccount);
14683
+ }
14684
+ // src/domains/auth/store/load.ts
14685
+ import { join as join4 } from "node:path";
14686
+
14427
14687
  // src/domains/auth/store/types.ts
14428
14688
  var credentialsFileSchema = object({
14429
14689
  apiKey: string2().min(1)
14430
14690
  });
14691
+ var oauthTokensSchema = object({
14692
+ accessToken: string2().min(1),
14693
+ refreshToken: string2().min(1),
14694
+ expiresAt: number2().int().optional(),
14695
+ email: string2().min(1),
14696
+ organizationId: string2().min(1).optional(),
14697
+ workspaceId: string2().min(1).optional(),
14698
+ clientId: string2().min(1).optional()
14699
+ });
14431
14700
 
14432
14701
  // src/domains/auth/store/load.ts
14433
14702
  async function loadApiKey(configDir, deps) {
@@ -14441,7 +14710,7 @@ async function loadApiKey(configDir, deps) {
14441
14710
  errors.keychain = errorMessage(err);
14442
14711
  }
14443
14712
  try {
14444
- const content = await deps.fs.readFile(join3(configDir, credentialsFile));
14713
+ const content = await deps.fs.readFile(join4(configDir, credentialsFile));
14445
14714
  const parsed = credentialsFileSchema.safeParse(JSON.parse(content));
14446
14715
  if (parsed.success) {
14447
14716
  return { found: true, key: parsed.data.apiKey, source: "file" };
@@ -14453,16 +14722,16 @@ async function loadApiKey(configDir, deps) {
14453
14722
  return { found: false, errors };
14454
14723
  }
14455
14724
  // src/domains/auth/store/save.ts
14456
- import { join as join4 } from "node:path";
14457
- import { Entry as Entry2 } from "@napi-rs/keyring";
14725
+ import { join as join5 } from "node:path";
14726
+ import { Entry as Entry3 } from "@napi-rs/keyring";
14458
14727
  async function saveToFile(configDir, key, fs) {
14459
14728
  const payload = { apiKey: key };
14460
14729
  await fs.mkdir(configDir, { recursive: true, mode: 448 });
14461
- await fs.writeFile(join4(configDir, credentialsFile), JSON.stringify(payload, undefined, 2), { mode: 384 });
14730
+ await fs.writeFile(join5(configDir, credentialsFile), JSON.stringify(payload, undefined, 2), { mode: 384 });
14462
14731
  }
14463
14732
  async function saveApiKey(configDir, key, fs) {
14464
14733
  try {
14465
- const entry = new Entry2(service, account);
14734
+ const entry = new Entry3(service, account);
14466
14735
  entry.setPassword(key);
14467
14736
  return { stored: "keychain" };
14468
14737
  } catch (err) {
@@ -14470,10 +14739,159 @@ async function saveApiKey(configDir, key, fs) {
14470
14739
  return { stored: "file", keychainError: errorMessage(err) };
14471
14740
  }
14472
14741
  }
14742
+ // src/domains/auth/store/loadTokens.ts
14743
+ import { join as join6 } from "node:path";
14744
+ var invalidPayload = "Invalid stored token format";
14745
+ function parseTokens(raw) {
14746
+ const parsed = oauthTokensSchema.safeParse(JSON.parse(raw));
14747
+ if (!parsed.success)
14748
+ return;
14749
+ return {
14750
+ accessToken: parsed.data.accessToken,
14751
+ refreshToken: parsed.data.refreshToken,
14752
+ expiresAt: parsed.data.expiresAt,
14753
+ email: parsed.data.email,
14754
+ organizationId: parsed.data.organizationId,
14755
+ workspaceId: parsed.data.workspaceId,
14756
+ clientId: parsed.data.clientId
14757
+ };
14758
+ }
14759
+ async function loadTokens(configDir, deps) {
14760
+ const errors = {};
14761
+ try {
14762
+ const raw = new deps.EntryClass(service, tokensAccount).getPassword();
14763
+ if (raw) {
14764
+ const tokens = parseTokens(raw);
14765
+ if (tokens)
14766
+ return { found: true, tokens, source: "keychain" };
14767
+ errors.keychain = invalidPayload;
14768
+ }
14769
+ } catch (err) {
14770
+ errors.keychain = errorMessage(err);
14771
+ }
14772
+ try {
14773
+ const raw = await deps.fs.readFile(join6(configDir, tokensFile));
14774
+ const tokens = parseTokens(raw);
14775
+ if (tokens)
14776
+ return { found: true, tokens, source: "file" };
14777
+ errors.file = invalidPayload;
14778
+ } catch (err) {
14779
+ errors.file = errorMessage(err);
14780
+ }
14781
+ return { found: false, errors };
14782
+ }
14783
+
14784
+ // src/domains/auth/store/saveTokens.ts
14785
+ import { randomUUID } from "node:crypto";
14786
+ import { join as join7 } from "node:path";
14787
+ import { Entry as Entry4 } from "@napi-rs/keyring";
14788
+ async function saveToFile2(configDir, tokens, fs) {
14789
+ await fs.mkdir(configDir, { recursive: true, mode: 448 });
14790
+ const target = join7(configDir, tokensFile);
14791
+ const staging = `${target}.${randomUUID()}.tmp`;
14792
+ try {
14793
+ await fs.writeFile(staging, JSON.stringify(tokens, undefined, 2), {
14794
+ mode: 384
14795
+ });
14796
+ await fs.rename(staging, target);
14797
+ } catch (err) {
14798
+ await fs.unlink(staging).catch(() => {});
14799
+ throw err;
14800
+ }
14801
+ }
14802
+ async function saveTokens(configDir, tokens, fs) {
14803
+ try {
14804
+ new Entry4(service, tokensAccount).setPassword(JSON.stringify(tokens));
14805
+ return { stored: "keychain" };
14806
+ } catch (err) {
14807
+ try {
14808
+ new Entry4(service, tokensAccount).deletePassword();
14809
+ } catch {}
14810
+ await saveToFile2(configDir, tokens, fs);
14811
+ return { stored: "file", keychainError: errorMessage(err) };
14812
+ }
14813
+ }
14814
+
14815
+ // src/domains/auth/resolveOauthToken.ts
14816
+ var expiryMarginMs = 30000;
14817
+ async function resolveOauthToken(configDir, deps) {
14818
+ const stored = await deps.loadTokens(configDir);
14819
+ if (!stored.found)
14820
+ return;
14821
+ const { tokens } = stored;
14822
+ const expiresAt = tokens.expiresAt;
14823
+ const isFresh = expiresAt !== undefined && expiresAt - expiryMarginMs > deps.now();
14824
+ if (isFresh) {
14825
+ return {
14826
+ key: tokens.accessToken,
14827
+ email: tokens.email,
14828
+ workspaceId: tokens.workspaceId
14829
+ };
14830
+ }
14831
+ const refreshed = await deps.refreshTokens({
14832
+ refreshToken: tokens.refreshToken,
14833
+ organizationId: tokens.organizationId,
14834
+ clientId: tokens.clientId
14835
+ });
14836
+ if (!refreshed.ok) {
14837
+ if (refreshed.retryable && expiresAt !== undefined && expiresAt > deps.now()) {
14838
+ return {
14839
+ key: tokens.accessToken,
14840
+ email: tokens.email,
14841
+ workspaceId: tokens.workspaceId
14842
+ };
14843
+ }
14844
+ const current = await deps.loadTokens(configDir);
14845
+ if (current.found && current.tokens.refreshToken !== tokens.refreshToken && current.tokens.email === tokens.email && current.tokens.organizationId === tokens.organizationId && current.tokens.clientId === tokens.clientId && current.tokens.expiresAt !== undefined && current.tokens.expiresAt > deps.now()) {
14846
+ return {
14847
+ key: current.tokens.accessToken,
14848
+ email: current.tokens.email,
14849
+ workspaceId: current.tokens.workspaceId
14850
+ };
14851
+ }
14852
+ return;
14853
+ }
14854
+ try {
14855
+ await deps.saveTokens(configDir, {
14856
+ ...refreshed.value,
14857
+ workspaceId: tokens.workspaceId,
14858
+ clientId: tokens.clientId
14859
+ });
14860
+ } catch {}
14861
+ return {
14862
+ key: refreshed.value.accessToken,
14863
+ email: refreshed.value.email,
14864
+ workspaceId: tokens.workspaceId
14865
+ };
14866
+ }
14867
+
14473
14868
  // src/domains/auth/resolve.ts
14869
+ function makeOauthDeps(fs) {
14870
+ return {
14871
+ loadTokens: (configDir) => loadTokens(configDir, { EntryClass: Entry5, fs }),
14872
+ refreshTokens: async ({ refreshToken, organizationId, clientId }) => {
14873
+ const config = resolveWorkosConfig(clientId);
14874
+ if (!config.configured) {
14875
+ return {
14876
+ ok: false,
14877
+ error: authErrorMessages.workos.noClientForSession,
14878
+ retryable: false
14879
+ };
14880
+ }
14881
+ return refreshAccessToken(refreshToken, organizationId, {
14882
+ fetch: globalThis.fetch,
14883
+ baseUrl: config.baseUrl,
14884
+ clientId: config.clientId
14885
+ });
14886
+ },
14887
+ saveTokens: (configDir, tokens) => saveTokens(configDir, tokens, fs),
14888
+ now: () => Date.now()
14889
+ };
14890
+ }
14474
14891
  function makeDefaultDeps(fs) {
14475
14892
  return {
14476
- loadApiKey: (configDir) => loadApiKey(configDir, { EntryClass: Entry3, fs }),
14893
+ loadApiKey: (configDir) => loadApiKey(configDir, { EntryClass: Entry5, fs }),
14894
+ resolveOauth: (configDir) => resolveOauthToken(configDir, makeOauthDeps(fs)),
14477
14895
  env: process.env
14478
14896
  };
14479
14897
  }
@@ -14481,11 +14899,19 @@ async function resolveApiKey(configDir, fs, deps) {
14481
14899
  const resolvedDeps = deps ?? makeDefaultDeps(fs);
14482
14900
  const envKey = resolvedDeps.env["QAWOLF_API_KEY"];
14483
14901
  if (envKey?.trim()) {
14484
- return { key: envKey.trim(), source: "env" };
14902
+ return { key: envKey.trim(), source: "env", workspaceId: undefined };
14485
14903
  }
14486
14904
  const stored = await resolvedDeps.loadApiKey(configDir);
14487
14905
  if (stored.found) {
14488
- return { key: stored.key, source: stored.source };
14906
+ return { key: stored.key, source: stored.source, workspaceId: undefined };
14907
+ }
14908
+ const oauth = await resolvedDeps.resolveOauth(configDir);
14909
+ if (oauth) {
14910
+ return {
14911
+ key: oauth.key,
14912
+ source: "browser",
14913
+ workspaceId: oauth.workspaceId
14914
+ };
14489
14915
  }
14490
14916
  return;
14491
14917
  }
@@ -14507,9 +14933,9 @@ async function validateApiKey(deps) {
14507
14933
  // src/shell/logger.ts
14508
14934
  var import_pino = __toESM(require_pino(), 1);
14509
14935
  import { mkdirSync as mkdirSync2, openSync } from "node:fs";
14510
- import { dirname, join as join5 } from "node:path";
14936
+ import { dirname, join as join8 } from "node:path";
14511
14937
  function defaultLogPath() {
14512
- return join5(envPaths("qawolf", { suffix: "" }).log, "cli.log");
14938
+ return join8(envPaths("qawolf", { suffix: "" }).log, "cli.log");
14513
14939
  }
14514
14940
  function resolveStderrLevel(env, verbose) {
14515
14941
  if (verbose)
@@ -14577,9 +15003,179 @@ function createLoggingSystem(opts) {
14577
15003
  };
14578
15004
  }
14579
15005
 
14580
- // src/shell/platform/parseErrorBody.ts
14581
- var defaultMaxLength = 1024;
14582
- function asRecord(value) {
15006
+ // src/core/publicApi/flagKind.ts
15007
+ function flagKind(schema) {
15008
+ if (schema.type === "string")
15009
+ return { ok: true, kind: "string" };
15010
+ if (schema.type === "number" || schema.type === "integer") {
15011
+ return { ok: true, kind: "number" };
15012
+ }
15013
+ if (schema.type === "boolean")
15014
+ return { ok: true, kind: "boolean" };
15015
+ if (schema.type === "array") {
15016
+ if (schema.items?.type !== "string") {
15017
+ return {
15018
+ ok: false,
15019
+ reason: "only string arrays can be expressed as flags"
15020
+ };
15021
+ }
15022
+ return { ok: true, kind: "string-array" };
15023
+ }
15024
+ if (schema.type === "object") {
15025
+ if (schema.properties) {
15026
+ return {
15027
+ ok: false,
15028
+ reason: "nested objects cannot be expressed as flags"
15029
+ };
15030
+ }
15031
+ if (typeof schema.additionalProperties === "object" && schema.additionalProperties.type === "string") {
15032
+ return { ok: true, kind: "key-value-record" };
15033
+ }
15034
+ return { ok: false, reason: "nested objects cannot be expressed as flags" };
15035
+ }
15036
+ return {
15037
+ ok: false,
15038
+ reason: `type ${schema.type ?? "unknown"} cannot be expressed as a flag`
15039
+ };
15040
+ }
15041
+
15042
+ // src/core/publicApi/objectShape.ts
15043
+ var asObjectShape = (schema) => schema.type === "object" && schema.properties ? { properties: schema.properties, required: new Set(schema.required) } : undefined;
15044
+ function mergeIntersection(members) {
15045
+ const shape = { properties: {}, required: new Set };
15046
+ for (const member of members) {
15047
+ const memberShape = asObjectShape(member);
15048
+ if (!memberShape) {
15049
+ return {
15050
+ ok: false,
15051
+ field: "",
15052
+ reason: "intersection members must be object schemas"
15053
+ };
15054
+ }
15055
+ for (const [field, fieldSchema] of Object.entries(memberShape.properties)) {
15056
+ if (field in shape.properties) {
15057
+ return {
15058
+ ok: false,
15059
+ field,
15060
+ reason: "field appears in multiple intersection members"
15061
+ };
15062
+ }
15063
+ shape.properties[field] = fieldSchema;
15064
+ if (memberShape.required.has(field))
15065
+ shape.required.add(field);
15066
+ }
15067
+ }
15068
+ return { ok: true, shape };
15069
+ }
15070
+ function mergeUnion(branches) {
15071
+ const shapes = [];
15072
+ for (const branch of branches) {
15073
+ const branchShape = asObjectShape(branch);
15074
+ if (!branchShape) {
15075
+ return {
15076
+ ok: false,
15077
+ field: "",
15078
+ reason: "union branches must be object schemas"
15079
+ };
15080
+ }
15081
+ shapes.push(branchShape);
15082
+ }
15083
+ const shape = { properties: {}, required: new Set };
15084
+ for (const branchShape of shapes) {
15085
+ for (const [field, fieldSchema] of Object.entries(branchShape.properties)) {
15086
+ const existing = shape.properties[field];
15087
+ if (!existing) {
15088
+ shape.properties[field] = { ...fieldSchema };
15089
+ continue;
15090
+ }
15091
+ const kinds = [flagKind(existing), flagKind(fieldSchema)];
15092
+ if (kinds[0]?.ok && kinds[1]?.ok) {
15093
+ if (kinds[0].kind !== kinds[1].kind) {
15094
+ return {
15095
+ ok: false,
15096
+ field,
15097
+ reason: "field has conflicting types across union branches"
15098
+ };
15099
+ }
15100
+ } else if (JSON.stringify({ ...existing, description: undefined }) !== JSON.stringify({ ...fieldSchema, description: undefined })) {
15101
+ return {
15102
+ ok: false,
15103
+ field,
15104
+ reason: "field has conflicting object schemas across union branches"
15105
+ };
15106
+ }
15107
+ if (!existing.description && fieldSchema.description) {
15108
+ existing.description = fieldSchema.description;
15109
+ }
15110
+ }
15111
+ }
15112
+ const discriminator = Object.keys(shape.properties).find((field) => {
15113
+ const values = shapes.map((branch) => branch.properties[field]?.const);
15114
+ return values.every((value) => typeof value === "string") && new Set(values).size === shapes.length && shapes.every((branch) => branch.required.has(field));
15115
+ });
15116
+ for (const [field, fieldSchema] of Object.entries(shape.properties)) {
15117
+ const occurrences = shapes.filter((branch) => (field in branch.properties));
15118
+ if (occurrences.length === shapes.length && occurrences.every((branch) => branch.required.has(field))) {
15119
+ shape.required.add(field);
15120
+ }
15121
+ if (field === discriminator) {
15122
+ const values = shapes.map((branch) => branch.properties[field]?.const);
15123
+ fieldSchema.description = `One of: ${values.join(", ")}`;
15124
+ }
15125
+ }
15126
+ return { ok: true, shape };
15127
+ }
15128
+ function toObjectShape(schema) {
15129
+ if (schema.allOf)
15130
+ return mergeIntersection(schema.allOf);
15131
+ const branches = schema.oneOf ?? schema.anyOf;
15132
+ if (branches)
15133
+ return mergeUnion(branches);
15134
+ const shape = asObjectShape(schema);
15135
+ if (shape)
15136
+ return { ok: true, shape };
15137
+ return {
15138
+ ok: false,
15139
+ field: "",
15140
+ reason: "contract input must be an object schema"
15141
+ };
15142
+ }
15143
+
15144
+ // src/shell/platform/applyWorkspaceId.ts
15145
+ var acceptsWorkspace = new WeakMap;
15146
+ function takesWorkspaceId(inputSchema) {
15147
+ const cached = acceptsWorkspace.get(inputSchema);
15148
+ if (cached !== undefined)
15149
+ return cached;
15150
+ let accepted = false;
15151
+ try {
15152
+ const jsonSchema = toJSONSchema(inputSchema, {
15153
+ io: "input"
15154
+ });
15155
+ const shape = toObjectShape(jsonSchema);
15156
+ accepted = shape.ok && "workspaceId" in shape.shape.properties;
15157
+ } catch {
15158
+ accepted = false;
15159
+ }
15160
+ acceptsWorkspace.set(inputSchema, accepted);
15161
+ return accepted;
15162
+ }
15163
+ function applyWorkspaceId(inputSchema, input, workspaceId) {
15164
+ if (!workspaceId)
15165
+ return input;
15166
+ if (typeof input !== "object" || input === null)
15167
+ return input;
15168
+ const existing = input.workspaceId;
15169
+ if (existing !== undefined)
15170
+ return input;
15171
+ if (!takesWorkspaceId(inputSchema))
15172
+ return input;
15173
+ return { ...input, workspaceId };
15174
+ }
15175
+
15176
+ // src/shell/platform/parseErrorBody.ts
15177
+ var defaultMaxLength = 1024;
15178
+ function asRecord(value) {
14583
15179
  return typeof value === "object" && value !== null && !Array.isArray(value) ? value : undefined;
14584
15180
  }
14585
15181
  function asString(value) {
@@ -14743,7 +15339,7 @@ function callPublicApi(trpc, contract, input, options) {
14743
15339
  }
14744
15340
  function makeCallPublicApiMethod(trpc, deps, readBackoffMs) {
14745
15341
  return async (contract, input, options) => requestWithRetry({
14746
- call: () => callPublicApi(trpc, contract, input, options),
15342
+ call: () => callPublicApi(trpc, contract, applyWorkspaceId(contract.input, input, deps.workspaceId), options),
14747
15343
  backoffMs: contract.kind === "read" ? readBackoffMs : [],
14748
15344
  describe: (err) => describeRequestError(err, deps.baseUrl, contract.name),
14749
15345
  sleep: deps.sleep
@@ -15646,7 +16242,7 @@ function summarize(error) {
15646
16242
  // src/shell/platform/downloadBundle.ts
15647
16243
  import { randomBytes } from "node:crypto";
15648
16244
  import { tmpdir as tmpdir2 } from "node:os";
15649
- import { join as join6 } from "node:path";
16245
+ import { join as join9 } from "node:path";
15650
16246
 
15651
16247
  // src/shell/platform/fetchSignedUrl.ts
15652
16248
  var defaultStallTimeoutMs = 30000;
@@ -15734,7 +16330,7 @@ async function fetchSignedUrl(args, deps = { fetch: globalThis.fetch }) {
15734
16330
 
15735
16331
  // src/shell/platform/downloadBundle.ts
15736
16332
  async function downloadBundle(signedUrl, deps) {
15737
- const tmpArchive = join6(tmpdir2(), `qawolf-pull-${randomBytes(8).toString("hex")}.tar.gz`);
16333
+ const tmpArchive = join9(tmpdir2(), `qawolf-pull-${randomBytes(8).toString("hex")}.tar.gz`);
15738
16334
  const result = await fetchSignedUrl({ url: signedUrl, dest: tmpArchive }, { fetch: deps.fetch, fs: deps.fs });
15739
16335
  if (!result.ok) {
15740
16336
  await deps.fs.unlink(tmpArchive).catch(() => {});
@@ -15795,6 +16391,10 @@ var makeEmailResourceSchema = () => resource({
15795
16391
  replyTo: array(namedAddressSchema).optional(),
15796
16392
  text: string2().optional().describe("The plain text body.")
15797
16393
  }, { urlFieldDescription: emailUrlFieldDescription });
16394
+ var makeEmailAddressResourceSchema = () => resource({
16395
+ address: string2().describe("The inbox address. Mail sent to any plus-suffixed form, such as inbox+abc@example.com, lands in the same inbox."),
16396
+ isDefault: boolean2().describe("Whether this is the workspace's default inbox address.")
16397
+ }, { urlFieldDescription: emailUrlFieldDescription });
15798
16398
 
15799
16399
  // node_modules/@qawolf/api-contracts/dist/v1/identity/organization.js
15800
16400
  var identityOrganization = object({
@@ -15838,6 +16438,10 @@ var identityResponse = union([
15838
16438
  user: never().optional()
15839
16439
  }).transform(({ organization }) => ({ organization }))
15840
16440
  ]);
16441
+ // node_modules/@qawolf/api-contracts/dist/v1/identity/organizations.js
16442
+ var identityOrganizationsResponse = object({
16443
+ organizations: array(identityOrganization)
16444
+ });
15841
16445
  // node_modules/@qawolf/api-contracts/dist/v1/ids.js
15842
16446
  var defaultIdSchemas = {
15843
16447
  aiTask: string2().min(1).describe("The id of the AI task."),
@@ -16014,6 +16618,8 @@ var runnerNameSchema = _enum([
16014
16618
  "android",
16015
16619
  "ios"
16016
16620
  ]);
16621
+ var runnerWorkspaceRequirement = "Required when the credential is not bound to a single workspace, such as a user API key or an OAuth connection whose organization owns several.";
16622
+ var runnerWorkspaceIdDescription = `The workspace the runner belongs to. ${runnerWorkspaceRequirement}`;
16017
16623
  var runnerIdSchema = string2().min(1).max(63).regex(/^[a-z0-9]([a-z0-9-]*[a-z0-9])?$/, "A runner id may contain only lowercase letters, digits and dashes, and must start and end with a letter or digit.");
16018
16624
  var makeRunnerSchema = () => object({
16019
16625
  gpuAccelerated: boolean2().describe("Whether the runner was placed on GPU hardware. Chosen by QA Wolf from the team's configuration and what the fleet can provide, not requested."),
@@ -16041,10 +16647,11 @@ var inspectRequestSchema = discriminatedUnion("what", [
16041
16647
  what: literal("variable")
16042
16648
  })
16043
16649
  ]);
16044
- var makeInspectOnRunnerContract = () => {
16650
+ var makeInspectOnRunnerContract = (ids) => {
16045
16651
  const input = object({
16046
16652
  id: runnerIdSchema.describe("Id of the runner to inspect."),
16047
- request: inspectRequestSchema
16653
+ request: inspectRequestSchema,
16654
+ workspaceId: ids.workspace.optional().describe(runnerWorkspaceIdDescription)
16048
16655
  });
16049
16656
  const output = discriminatedUnion("outcome", [
16050
16657
  object({
@@ -16191,10 +16798,11 @@ var inspectMobileAnswerSchema = discriminatedUnion("what", [
16191
16798
  what: literal("elements")
16192
16799
  })
16193
16800
  ]);
16194
- var makeInspectMobileOnRunnerContract = () => {
16801
+ var makeInspectMobileOnRunnerContract = (ids) => {
16195
16802
  const input = object({
16196
16803
  id: runnerIdSchema.describe("Id of the runner to inspect."),
16197
- request: inspectMobileRequestSchema
16804
+ request: inspectMobileRequestSchema,
16805
+ workspaceId: ids.workspace.optional().describe(runnerWorkspaceIdDescription)
16198
16806
  });
16199
16807
  const output = discriminatedUnion("outcome", [
16200
16808
  inspectMobileAnswerSchema,
@@ -16278,20 +16886,26 @@ var screenFailureReasons = [
16278
16886
  var screenNotReadyDescription = "`screen-not-ready` if the runner has a screen that cannot serve this instant. Retry in a second or two: the desktop restarts when a run changes the display size, and it serves one see-or-act request at a time, so a screenshot or action already in flight is the usual reason. Do not submit a run to clear this — a run may restart the display and discard what is on it.";
16279
16887
  var screenNeedsARunDescription = "`screen-needs-a-run` if the runner's virtual desktop has never started. Waiting will not change this and retrying is pointless — call `runner.runFlow` with a flow that opens a browser, then ask for the screen again. On a runner image with a browser that has never run anything, any `runner.performAction` also starts the browser itself. Evaluating a snippet does not start the desktop.";
16280
16888
  var runnerHasNoScreenDescription = "`runner-has-no-screen` if this runner is not one that runs a browser on a virtual desktop. Nothing about it can be seen or driven, and retrying will never help — launch a `playwright` runner instead.";
16889
+ var screenshotImageSchema = string2().min(1).describe("The screenshot, as a base64-encoded JPEG. Decode it and write the bytes to a `.jpg` file — writing this string to the file leaves you with base64 text rather than an image.");
16281
16890
 
16282
16891
  // node_modules/@qawolf/api-contracts/dist/v1/runner/performAction.js
16283
16892
  var unreachableDescription = "`runner-unreachable` if the runner could not be reached: it may still be starting, it may have terminated after inactivity, or it may have stopped answering mid-action. This does not mean the action was not performed — take a screenshot before repeating it.";
16284
16893
  var notSupportedOnMobileDescription = "`action-not-supported-on-mobile` if the runner is a mobile device and this action has no touchscreen equivalent: `double_click`, `scroll`, `move`, `keypress` and `navigate`, and a `click` whose `button` is not `left`, are all pointer-device concepts a touchscreen has nothing to offer for. `click` taps, `drag` swipes between its path's first and last point, and `type` types into whatever the last tap focused.";
16894
+ var withScreenshotDescription = "With `withScreenshot: true`, the answer also carries `imageJpegBase64`: a screenshot taken after the action, once the screen has changed from just before it or half a second has passed, whichever comes first. One call instead of `runner.performAction` followed by `runner.takeScreenshot`, with no fixed wait in between. Anything animating on the page counts as a change. The action itself is performed exactly as without the option; if the screen could serve no frame after it — a display restart, or a `navigate` on a desktop that is still starting — the action's result comes back without `imageJpegBase64`, so take a screenshot separately rather than repeating the action. On a mobile runner the screenshot is the device's own, taken right after a performed action.";
16285
16895
  var maxActionErrorMessageLength = 1000;
16286
- var makePerformActionOnRunnerContract = () => {
16896
+ var makePerformActionOnRunnerContract = (ids) => {
16287
16897
  const input = object({
16288
16898
  action: browserActionSchema.describe("The action to perform. Coordinates are pixels on the screenshot's own coordinate space."),
16289
- id: runnerIdSchema.describe("Id of the runner to act on.")
16899
+ id: runnerIdSchema.describe("Id of the runner to act on."),
16900
+ withScreenshot: boolean2().optional().describe("Also answer with a screenshot taken after the action, once the screen has changed or half a second has passed. Saves the separate `runner.takeScreenshot` call and its fixed wait."),
16901
+ workspaceId: ids.workspace.optional().describe(runnerWorkspaceIdDescription)
16290
16902
  });
16903
+ const screenshotAfterAction = screenshotImageSchema.optional();
16291
16904
  const failure = discriminatedUnion("failureReason", [
16292
16905
  object({
16293
16906
  errorMessage: string2().max(maxActionErrorMessageLength).describe("What stopped the action from taking effect."),
16294
16907
  failureReason: literal("action-failed"),
16908
+ imageJpegBase64: screenshotAfterAction,
16295
16909
  outcome: literal("failure")
16296
16910
  }),
16297
16911
  makeRunnerFailureSchema([
@@ -16301,11 +16915,14 @@ var makePerformActionOnRunnerContract = () => {
16301
16915
  ])
16302
16916
  ]);
16303
16917
  const output = discriminatedUnion("outcome", [
16304
- object({ outcome: literal("success") }),
16918
+ object({
16919
+ imageJpegBase64: screenshotAfterAction,
16920
+ outcome: literal("success")
16921
+ }),
16305
16922
  failure
16306
16923
  ]);
16307
16924
  return {
16308
- description: `Perform one raw browser action on an interactive runner: click, double_click, move, drag, scroll, keypress, type, or navigate. Coordinates are whole pixels on the runner's virtual desktop, in the same space as \`runner.takeScreenshot\`. One action per request, and the runner serves one at a time. The action shapes follow the computer-use vocabulary, minus \`screenshot\` (use \`runner.takeScreenshot\`) and \`wait\` (delay on the caller's side). A success means the action took effect. \`action-failed\`, with a reason, if it reached the runner and did not take effect. On a runner image with a browser, the first action on a runner that has never run anything starts its browser and waits for it, so it can take up to a minute to answer — no \`runner.runFlow\` is needed before acting; if the browser is still starting when the wait runs out, the answer is \`screen-not-ready\` and retrying converges. On a mobile runner, the same \`screen-needs-a-run\` and \`screen-not-ready\` outcomes mean no Appium session has started yet, or it did not answer this instant; \`runner.runFlow\` is what starts one, same as a browser. \`screen-needs-a-run\` if the browser could not be started that way — usually a runner whose runs all finished without starting its desktop; call \`runner.runFlow\` with a flow that opens a browser. ${screenNotReadyDescription} A \`navigate\` does not go through the screen, so a screen that is not ready does not stop it. ${runnerHasNoScreenDescription} ${notSupportedOnMobileDescription} ${unreachableDescription}`,
16925
+ description: `Perform one raw browser action on an interactive runner: click, double_click, move, drag, scroll, keypress, type, or navigate. Coordinates are whole pixels on the runner's virtual desktop, in the same space as \`runner.takeScreenshot\`. One action per request, and the runner serves one at a time. The action shapes follow the computer-use vocabulary, minus \`screenshot\` (use \`runner.takeScreenshot\`) and \`wait\` (delay on the caller's side). A success means the action took effect. \`action-failed\`, with a reason, if it reached the runner and did not take effect. On a runner image with a browser, the first action on a runner that has never run anything starts its browser and waits for it, so it can take up to a minute to answer — no \`runner.runFlow\` is needed before acting; if the browser is still starting when the wait runs out, the answer is \`screen-not-ready\` and retrying converges. On a mobile runner, the same \`screen-needs-a-run\` and \`screen-not-ready\` outcomes mean no Appium session has started yet, or it did not answer this instant; \`runner.runFlow\` is what starts one, same as a browser. \`screen-needs-a-run\` if the browser could not be started that way — usually a runner whose runs all finished without starting its desktop; call \`runner.runFlow\` with a flow that opens a browser. ${screenNotReadyDescription} A \`navigate\` does not go through the screen, so a screen that is not ready does not stop it. ${runnerHasNoScreenDescription} ${notSupportedOnMobileDescription} ${withScreenshotDescription} ${unreachableDescription}`,
16309
16926
  input,
16310
16927
  kind: "write",
16311
16928
  name: "runner.performAction",
@@ -16331,9 +16948,11 @@ var makeAgentGetContract = (ids) => {
16331
16948
  replies: array(agentReplySchema).describe("Everything the AI has said so far, oldest first. A reply carrying choices is a question the work is blocked on."),
16332
16949
  sessionId: ids.chatSession,
16333
16950
  status: agentSessionStatusSchema
16334
- }, { urlFieldDescription: "Absolute URL of the session in the QA Wolf app." });
16951
+ }, {
16952
+ urlFieldDescription: "Absolute URL of the live session in the QA Wolf app."
16953
+ });
16335
16954
  return {
16336
- description: 'Read what the QA Wolf AI has said and whether it is still working. Poll this after agent.send until the status settles. A status of "waiting-for-you" means the last reply is a question the work is blocked on, and answering it with agent.send is what unblocks it. Replies accumulate, so a caller that polls repeatedly sees the earlier ones again.',
16955
+ description: 'Monitor a QA Wolf AI session by reading its status and replies. After agent.send, share the returned session URL before monitoring. Wait 30 to 60 seconds between checks; do not call this in a tight loop. Replies accumulate, so compare them with what you have already seen. Continue monitoring silently when the status and replies are unchanged; do not narrate waiting, announce the next check, or ask whether to keep monitoring. Report only substantive new progress, questions, blockers, or the final outcome. A status of "waiting-for-you" means the last reply is a question the work is blocked on, and answering it with agent.send is what unblocks it. Surface an explicit request for user input even if the status still says "working". Include the session URL when reporting a blocker or final outcome. On "completed", stop status checks and verify the requested result before claiming success. For new flows, validation, publication in the target environment, and readiness are separate checks; a Git push or final reply does not prove the flow is active. If every requested result is verified but status remains "working", report the mismatch and stop monitoring. Stop on "failed" or "cancelled" and report any confirmed partial result.',
16337
16956
  input,
16338
16957
  kind: "read",
16339
16958
  name: "agent.get",
@@ -16352,9 +16971,11 @@ var makeAgentSendContract = (ids) => {
16352
16971
  const output = resource({
16353
16972
  sessionId: ids.chatSession.describe("Follow the work by passing this to agent.get."),
16354
16973
  status: agentSessionStatusSchema
16355
- }, { urlFieldDescription: "Absolute URL of the session in the QA Wolf app." });
16974
+ }, {
16975
+ urlFieldDescription: "Absolute URL of the live session in the QA Wolf app."
16976
+ });
16356
16977
  return {
16357
- description: "Ask the QA Wolf AI to do a piece of work in plain language, such as covering a user journey, investigating a failing run, or fixing a broken flow. This is the one verb that starts work from nothing: every other write acts on a flow, run or issue that already exists. Answers as soon as the request is accepted, with the id to follow it by, because the work runs for minutes to tens of minutes. Poll agent.get for progress, and send here again to answer a question or add context to work already running.",
16978
+ description: "Start or continue work with the QA Wolf AI and return a live session URL to share with the user. Use it to cover a user journey, investigate a failing run, or fix a broken flow. This is the one verb that starts work from nothing: every other write acts on a flow, run or issue that already exists. Returns sessionId, status, and url as soon as the request is accepted; work can take minutes to tens of minutes. After each send, make the next action a normal user-visible assistant message containing the exact returned url, before any tool call or wait. Tool output and internal reasoning do not count as sharing the link. Do not run a timer or monitoring call alongside this send. Acceptance does not mean the work is complete. Then monitor the session with agent.get, reporting new progress, blockers, and the final outcome rather than unchanged status. Send here again to answer a question or add context to the same session.",
16358
16979
  input,
16359
16980
  kind: "write",
16360
16981
  name: "agent.send",
@@ -16457,10 +17078,7 @@ var makeListEmailAddressesContract = (ids) => {
16457
17078
  ...makePaginationInputFields({ defaultLimit: 20, maxLimit: 50 })
16458
17079
  });
16459
17080
  const output = object({
16460
- addresses: array(resource({
16461
- address: string2().describe("The inbox address. Mail sent to any plus-suffixed form, such as inbox+abc@example.com, lands in the same inbox."),
16462
- isDefault: boolean2().describe("Whether this is the workspace's default inbox address.")
16463
- }, { urlFieldDescription: emailUrlFieldDescription })).describe("The workspace's permanent inbox addresses, alphabetical."),
17081
+ addresses: array(makeEmailAddressResourceSchema()).describe("The workspace's permanent inbox addresses, alphabetical."),
16464
17082
  nextCursor: nextCursorSchema
16465
17083
  });
16466
17084
  return {
@@ -16472,6 +17090,28 @@ var makeListEmailAddressesContract = (ids) => {
16472
17090
  };
16473
17091
  };
16474
17092
 
17093
+ // node_modules/@qawolf/api-contracts/dist/v1/email/registerAddress.js
17094
+ var hasNoPlusSuffix = (address) => {
17095
+ const [localPart] = address.split("@");
17096
+ return localPart !== undefined && !localPart.includes("+");
17097
+ };
17098
+ var makeRegisterEmailAddressContract = (ids) => {
17099
+ const input = object({
17100
+ address: email2().refine(hasNoPlusSuffix, {
17101
+ message: "Register the base address. Mail to a plus-suffixed form lands in the base inbox."
17102
+ }).describe("The inbox address to register. Its domain must be one QA Wolf serves for the workspace."),
17103
+ workspaceId: ids.workspace.optional().describe("The workspace to register the address in. Required when authenticating with an organization or user API key.")
17104
+ });
17105
+ const output = makeEmailAddressResourceSchema();
17106
+ return {
17107
+ description: "Register an inbox address for the workspace. Registering an address the workspace already has changes nothing. A refusal names the domains the workspace can use.",
17108
+ input,
17109
+ kind: "write",
17110
+ name: "email.registerAddress",
17111
+ output
17112
+ };
17113
+ };
17114
+
16475
17115
  // node_modules/@qawolf/api-contracts/dist/v1/email/send.js
16476
17116
  var recipientsSchema = array(email2()).max(50);
16477
17117
  var maxAttachments = 10;
@@ -17112,14 +17752,31 @@ var makeReattemptRunContract = (ids) => {
17112
17752
  };
17113
17753
  };
17114
17754
 
17755
+ // node_modules/@qawolf/api-contracts/dist/v1/run/stop.js
17756
+ var makeStopRunContract = (ids) => {
17757
+ const input = object({ runId: ids.run });
17758
+ const output = resource({
17759
+ runId: ids.run,
17760
+ status: _enum(["accepted", "already-finished", "execution-not-found"]).describe('"accepted" means stopping was requested; poll run.get for the final result. "already-finished" means the run has already finished and its result is unchanged. "execution-not-found" means the run exists but no execution was found to stop; it may still be starting or may no longer be available. No stop request was accepted in that case.')
17761
+ }, { urlFieldDescription: "Absolute URL of the run page." });
17762
+ return {
17763
+ description: "Stop a run, including its queued flows and automatic retries. Stopping is asynchronous. Repeated requests are safe, and finished runs keep their results. A run that is still being created returns not found; retry once run.get returns the run. If run.get returns a different runId, use that ID. Poll run.get for results.",
17764
+ input,
17765
+ kind: "write",
17766
+ name: "run.stop",
17767
+ output
17768
+ };
17769
+ };
17770
+
17115
17771
  // node_modules/@qawolf/api-contracts/dist/v1/runner/evaluateSnippet.js
17116
17772
  var maxSnippetCodeLength = 64 * 1024;
17117
- var makeEvaluateSnippetOnRunnerContract = () => {
17773
+ var makeEvaluateSnippetOnRunnerContract = (ids) => {
17118
17774
  const input = object({
17119
17775
  code: string2().min(1).max(maxSnippetCodeLength).describe("The code to evaluate against the runner's live page, in the scope of `filePath` when one is given."),
17120
17776
  filePath: runFilePathSchema.optional().describe("Path of the file the snippet is evaluated in the scope of. Omit to evaluate a snippet that imports nothing of yours."),
17121
17777
  files: runFilesSchema.optional().describe("The file named by `filePath` and everything it imports. A runner holds no copy of your project, so a snippet's scope has to travel with it."),
17122
- id: runnerIdSchema.describe("Id of the runner to evaluate the snippet on.")
17778
+ id: runnerIdSchema.describe("Id of the runner to evaluate the snippet on."),
17779
+ workspaceId: ids.workspace.optional().describe(runnerWorkspaceIdDescription)
17123
17780
  }).refine((request) => request.filePath === undefined || Object.hasOwn(request.files ?? {}, request.filePath), {
17124
17781
  error: "A snippet naming a file must carry that file in files.",
17125
17782
  path: ["files"]
@@ -17145,9 +17802,10 @@ var makeEvaluateSnippetOnRunnerContract = () => {
17145
17802
  };
17146
17803
 
17147
17804
  // node_modules/@qawolf/api-contracts/dist/v1/runner/get.js
17148
- var makeGetRunnerContract = () => {
17805
+ var makeGetRunnerContract = (ids) => {
17149
17806
  const input = object({
17150
- id: runnerIdSchema.describe("Id of the runner to look up.")
17807
+ id: runnerIdSchema.describe("Id of the runner to look up."),
17808
+ workspaceId: ids.workspace.optional().describe(runnerWorkspaceIdDescription)
17151
17809
  });
17152
17810
  const output = object({
17153
17811
  id: runnerIdSchema.describe("Id of the runner the call addressed."),
@@ -17165,10 +17823,11 @@ var makeGetRunnerContract = () => {
17165
17823
 
17166
17824
  // node_modules/@qawolf/api-contracts/dist/v1/runner/highlightSelector.js
17167
17825
  var maxHighlightSelectorLength = 2000;
17168
- var makeHighlightSelectorOnRunnerContract = () => {
17826
+ var makeHighlightSelectorOnRunnerContract = (ids) => {
17169
17827
  const input = object({
17170
17828
  id: runnerIdSchema.describe("Id of the runner to highlight on."),
17171
- selector: string2().max(maxHighlightSelectorLength).describe("The selector to highlight. An empty string clears whatever is highlighted.")
17829
+ selector: string2().max(maxHighlightSelectorLength).describe("The selector to highlight. An empty string clears whatever is highlighted."),
17830
+ workspaceId: ids.workspace.optional().describe(runnerWorkspaceIdDescription)
17172
17831
  });
17173
17832
  const output = discriminatedUnion("outcome", [
17174
17833
  object({
@@ -17203,12 +17862,13 @@ var makeHighlightSelectorOnRunnerContract = () => {
17203
17862
  // node_modules/@qawolf/api-contracts/dist/v1/runner/importPackage.js
17204
17863
  var maxPackageNameLength = 214;
17205
17864
  var maxPackageVersionLength = 256;
17206
- var makeImportPackageOnRunnerContract = () => {
17865
+ var makeImportPackageOnRunnerContract = (ids) => {
17207
17866
  const input = object({
17208
17867
  id: runnerIdSchema.describe("Id of the runner to install into."),
17209
17868
  npmDependencies: record(string2(), string2()).describe("The run's current npm dependencies, so the install resolves against them."),
17210
17869
  packageName: string2().min(1).max(maxPackageNameLength),
17211
- packageVersion: string2().min(1).max(maxPackageVersionLength)
17870
+ packageVersion: string2().min(1).max(maxPackageVersionLength),
17871
+ workspaceId: ids.workspace.optional().describe(runnerWorkspaceIdDescription)
17212
17872
  });
17213
17873
  const output = discriminatedUnion("outcome", [
17214
17874
  object({ outcome: literal("success") }),
@@ -17228,10 +17888,11 @@ var makeImportPackageOnRunnerContract = () => {
17228
17888
  };
17229
17889
 
17230
17890
  // node_modules/@qawolf/api-contracts/dist/v1/runner/index.js
17231
- var makeLaunchRunnerContract = () => {
17891
+ var makeLaunchRunnerContract = (ids) => {
17232
17892
  const input = object({
17233
17893
  id: runnerIdSchema.describe("Id to launch the runner under, chosen by the caller. Launching the same id again returns the runner already running under it."),
17234
- runnerName: runnerNameSchema.optional().describe("The runner family to launch. Defaults to the standard web family, playwright.")
17894
+ runnerName: runnerNameSchema.optional().describe("The runner family to launch. Defaults to the standard web family, playwright."),
17895
+ workspaceId: ids.workspace.optional().describe(`The workspace to launch the runner in. ${runnerWorkspaceRequirement}`)
17235
17896
  });
17236
17897
  const output = makeRunnerSchema().extend({
17237
17898
  alreadyRunning: boolean2().describe("True when a runner was already running under this id and was returned unchanged, so no second runner was started and nothing new is billed. Not an error either way."),
@@ -17245,9 +17906,10 @@ var makeLaunchRunnerContract = () => {
17245
17906
  output
17246
17907
  };
17247
17908
  };
17248
- var makeTerminateRunnerContract = () => {
17909
+ var makeTerminateRunnerContract = (ids) => {
17249
17910
  const input = object({
17250
- id: runnerIdSchema.describe("Id of the runner to terminate.")
17911
+ id: runnerIdSchema.describe("Id of the runner to terminate."),
17912
+ workspaceId: ids.workspace.optional().describe(runnerWorkspaceIdDescription)
17251
17913
  });
17252
17914
  const output = object({
17253
17915
  id: runnerIdSchema.describe("Id of the runner the call addressed."),
@@ -17264,14 +17926,16 @@ var makeTerminateRunnerContract = () => {
17264
17926
  };
17265
17927
 
17266
17928
  // node_modules/@qawolf/api-contracts/dist/v1/runner/list.js
17267
- var makeListRunnersContract = () => {
17268
- const input = object({});
17929
+ var makeListRunnersContract = (ids) => {
17930
+ const input = object({
17931
+ workspaceId: ids.workspace.optional().describe(`The workspace whose runners to list. ${runnerWorkspaceRequirement}`)
17932
+ });
17269
17933
  const output = object({
17270
17934
  outcome: literal("success"),
17271
17935
  runners: array(makeRunnerSchema()).describe("The runners running right now, in no particular order. Each entry carries the same fields a launch reports, so any of them can be addressed like a runner you launched yourself.")
17272
17936
  });
17273
17937
  return {
17274
- description: "List the runners running on the caller's team right now. Takes no arguments: the API key already names the team. A runner that was terminated, or that terminated on its own after inactivity, is not listed, because the run system keeps no record of a runner once it is gone. This is a lookup: it never starts a runner, and it does not reset a runner's inactivity clock the way reading its journal does. It is how to find a runner launched from another machine or in an earlier session, when the id it was launched under is no longer at hand.",
17938
+ description: "List the runners running on the caller's team right now. A team API key already names the team and sends nothing; a credential bound to no single workspace names one in `workspaceId`. A runner that was terminated, or that terminated on its own after inactivity, is not listed, because the run system keeps no record of a runner once it is gone. This is a lookup: it never starts a runner, and it does not reset a runner's inactivity clock the way reading its journal does. It is how to find a runner launched from another machine or in an earlier session, when the id it was launched under is no longer at hand.",
17275
17939
  input,
17276
17940
  kind: "read",
17277
17941
  name: "runner.list",
@@ -17281,11 +17945,12 @@ var makeListRunnersContract = () => {
17281
17945
 
17282
17946
  // node_modules/@qawolf/api-contracts/dist/v1/runner/promoteSnapshot.js
17283
17947
  var maxSnapshotPathLength = 1024;
17284
- var makePromoteSnapshotOnRunnerContract = () => {
17948
+ var makePromoteSnapshotOnRunnerContract = (ids) => {
17285
17949
  const input = object({
17286
17950
  baselinePath: string2().min(1).max(maxSnapshotPathLength).describe("The baseline to replace, as named in the image diff."),
17287
17951
  id: runnerIdSchema.describe("Id of the runner holding the screenshot."),
17288
- screenshotPath: string2().min(1).max(maxSnapshotPathLength).describe("The screenshot to promote, as named in the image diff.")
17952
+ screenshotPath: string2().min(1).max(maxSnapshotPathLength).describe("The screenshot to promote, as named in the image diff."),
17953
+ workspaceId: ids.workspace.optional().describe(runnerWorkspaceIdDescription)
17289
17954
  });
17290
17955
  const output = discriminatedUnion("outcome", [
17291
17956
  object({ outcome: literal("success") }),
@@ -17308,10 +17973,11 @@ var makePromoteSnapshotOnRunnerContract = () => {
17308
17973
  };
17309
17974
 
17310
17975
  // node_modules/@qawolf/api-contracts/dist/v1/runner/readJournal.js
17311
- var makeReadRunnerJournalContract = () => {
17976
+ var makeReadRunnerJournalContract = (ids) => {
17312
17977
  const input = readJournalRequestSchema.extend({
17313
17978
  id: runnerIdSchema.describe("Id of the runner to read the journal of."),
17314
- stream: journalStreamSchema.describe(`The stream to read. QA Wolf writes ${knownJournalStreams.join(", ")}; an unwritten stream reads as empty rather than as an error.`)
17979
+ stream: journalStreamSchema.describe(`The stream to read. QA Wolf writes ${knownJournalStreams.join(", ")}; an unwritten stream reads as empty rather than as an error.`),
17980
+ workspaceId: ids.workspace.optional().describe(runnerWorkspaceIdDescription)
17315
17981
  });
17316
17982
  const output = discriminatedUnion("outcome", [
17317
17983
  readJournalResponseSchema.extend({ outcome: literal("success") }),
@@ -17346,7 +18012,8 @@ var makeRunFlowOnRunnerContract = (ids) => {
17346
18012
  files: runFilesSchema.describe("Every file the run needs keyed by its path — the flow file, everything it imports, package.json and tsconfig.json. A runner holds no copy of your project, so what runs is exactly what is sent here."),
17347
18013
  id: runnerIdSchema.describe("Id of the runner to run the flow on."),
17348
18014
  selection: runSelectionSchema.optional().describe("Run only these lines, inside the page the runner is already on, instead of running the whole flow from a fresh browser. Omit to run the whole entry point."),
17349
- unchangedFiles: unchangedFilesSchema.optional().describe("Files this runner already holds from an earlier run, keyed by path with a hex SHA-256 of the content it should hold. Send this to make `files` only what changed. The runner refuses with `needs-full-sync` if it holds none of a referenced path, so it never runs something other than what was asked for. Omit it to send every file.")
18015
+ unchangedFiles: unchangedFilesSchema.optional().describe("Files this runner already holds from an earlier run, keyed by path with a hex SHA-256 of the content it should hold. Send this to make `files` only what changed. The runner refuses with `needs-full-sync` if it holds none of a referenced path, so it never runs something other than what was asked for. Omit it to send every file."),
18016
+ workspaceId: ids.workspace.optional().describe(runnerWorkspaceIdDescription)
17350
18017
  }).refine((request) => request.env === undefined || request.environmentId === undefined, {
17351
18018
  error: "Send env or environmentId, not both. An environment's variables are taken as they are stored, so there is no order in which the two would combine.",
17352
18019
  path: ["environmentId"]
@@ -17398,9 +18065,10 @@ var makeRunFlowOnRunnerContract = (ids) => {
17398
18065
  };
17399
18066
 
17400
18067
  // node_modules/@qawolf/api-contracts/dist/v1/runner/stopRun.js
17401
- var makeStopRunOnRunnerContract = () => {
18068
+ var makeStopRunOnRunnerContract = (ids) => {
17402
18069
  const input = object({
17403
- id: runnerIdSchema.describe("Id of the runner to stop the run on.")
18070
+ id: runnerIdSchema.describe("Id of the runner to stop the run on."),
18071
+ workspaceId: ids.workspace.optional().describe(runnerWorkspaceIdDescription)
17404
18072
  });
17405
18073
  const output = discriminatedUnion("outcome", [
17406
18074
  object({
@@ -17419,13 +18087,14 @@ var makeStopRunOnRunnerContract = () => {
17419
18087
  };
17420
18088
 
17421
18089
  // node_modules/@qawolf/api-contracts/dist/v1/runner/takeScreenshot.js
17422
- var makeTakeScreenshotOnRunnerContract = () => {
18090
+ var makeTakeScreenshotOnRunnerContract = (ids) => {
17423
18091
  const input = object({
17424
- id: runnerIdSchema.describe("Id of the runner to take a screenshot of.")
18092
+ id: runnerIdSchema.describe("Id of the runner to take a screenshot of."),
18093
+ workspaceId: ids.workspace.optional().describe(runnerWorkspaceIdDescription)
17425
18094
  });
17426
18095
  const output = discriminatedUnion("outcome", [
17427
18096
  object({
17428
- imageJpegBase64: string2().min(1).describe("The screenshot, as a base64-encoded JPEG. Decode it and write the bytes to a `.jpg` file — writing this string to the file leaves you with base64 text rather than an image."),
18097
+ imageJpegBase64: screenshotImageSchema,
17429
18098
  outcome: literal("success")
17430
18099
  }),
17431
18100
  makeRunnerFailureSchema([
@@ -17479,6 +18148,7 @@ var makeContractsV1 = (ids) => {
17479
18148
  get: makeGetEmailContract(resolvedIds),
17480
18149
  getAttachment: makeGetEmailAttachmentContract(resolvedIds),
17481
18150
  listAddresses: makeListEmailAddressesContract(resolvedIds),
18151
+ registerAddress: makeRegisterEmailAddressContract(resolvedIds),
17482
18152
  send: makeSendEmailContract(resolvedIds)
17483
18153
  },
17484
18154
  environment: {
@@ -17510,24 +18180,25 @@ var makeContractsV1 = (ids) => {
17510
18180
  diagnose: makeDiagnoseRunContract(resolvedIds),
17511
18181
  find: makeFindRunsContract(resolvedIds),
17512
18182
  get: makeGetRunContract(resolvedIds),
17513
- reattempt: makeReattemptRunContract(resolvedIds)
18183
+ reattempt: makeReattemptRunContract(resolvedIds),
18184
+ stop: makeStopRunContract(resolvedIds)
17514
18185
  },
17515
18186
  runner: {
17516
- evaluateSnippet: makeEvaluateSnippetOnRunnerContract(),
17517
- get: makeGetRunnerContract(),
17518
- highlightSelector: makeHighlightSelectorOnRunnerContract(),
17519
- importPackage: makeImportPackageOnRunnerContract(),
17520
- inspect: makeInspectOnRunnerContract(),
17521
- inspectMobile: makeInspectMobileOnRunnerContract(),
17522
- launch: makeLaunchRunnerContract(),
17523
- list: makeListRunnersContract(),
17524
- performAction: makePerformActionOnRunnerContract(),
17525
- promoteSnapshot: makePromoteSnapshotOnRunnerContract(),
17526
- readJournal: makeReadRunnerJournalContract(),
18187
+ evaluateSnippet: makeEvaluateSnippetOnRunnerContract(resolvedIds),
18188
+ get: makeGetRunnerContract(resolvedIds),
18189
+ highlightSelector: makeHighlightSelectorOnRunnerContract(resolvedIds),
18190
+ importPackage: makeImportPackageOnRunnerContract(resolvedIds),
18191
+ inspect: makeInspectOnRunnerContract(resolvedIds),
18192
+ inspectMobile: makeInspectMobileOnRunnerContract(resolvedIds),
18193
+ launch: makeLaunchRunnerContract(resolvedIds),
18194
+ list: makeListRunnersContract(resolvedIds),
18195
+ performAction: makePerformActionOnRunnerContract(resolvedIds),
18196
+ promoteSnapshot: makePromoteSnapshotOnRunnerContract(resolvedIds),
18197
+ readJournal: makeReadRunnerJournalContract(resolvedIds),
17527
18198
  runFlow: makeRunFlowOnRunnerContract(resolvedIds),
17528
- stopRun: makeStopRunOnRunnerContract(),
17529
- takeScreenshot: makeTakeScreenshotOnRunnerContract(),
17530
- terminate: makeTerminateRunnerContract()
18199
+ stopRun: makeStopRunOnRunnerContract(resolvedIds),
18200
+ takeScreenshot: makeTakeScreenshotOnRunnerContract(resolvedIds),
18201
+ terminate: makeTerminateRunnerContract(resolvedIds)
17531
18202
  },
17532
18203
  tag: {
17533
18204
  create: makeCreateTagContract(resolvedIds),
@@ -17537,19 +18208,70 @@ var makeContractsV1 = (ids) => {
17537
18208
  };
17538
18209
  var publicContractsV1 = makeContractsV1(defaultIdSchemas);
17539
18210
 
18211
+ // src/shell/platform/organizations.ts
18212
+ function parseOrganizationsResponse(json) {
18213
+ const parsed = identityOrganizationsResponse.safeParse(json);
18214
+ return parsed.success ? parsed.data.organizations : undefined;
18215
+ }
18216
+
18217
+ // src/shell/platform/getAccessibleOrganizations.ts
18218
+ var timeoutMs2 = 1e4;
18219
+ async function getAccessibleOrganizations(apiKey, deps) {
18220
+ const url = `${deps.baseUrl}/api/v0/identity/organizations`;
18221
+ let response;
18222
+ try {
18223
+ response = await deps.fetch(url, {
18224
+ headers: { Authorization: `Bearer ${apiKey}` },
18225
+ signal: AbortSignal.timeout(timeoutMs2)
18226
+ });
18227
+ } catch (error) {
18228
+ if (isTimeoutError(error)) {
18229
+ return { ok: false, error: { kind: "timeout", timeoutMs: timeoutMs2 } };
18230
+ }
18231
+ return { ok: false, error: { kind: "network", cause: toError(error) } };
18232
+ }
18233
+ if (!response.ok) {
18234
+ const body = await response.text().catch(() => "");
18235
+ return {
18236
+ ok: false,
18237
+ error: { kind: "http", status: response.status, body }
18238
+ };
18239
+ }
18240
+ let json;
18241
+ try {
18242
+ json = await response.json();
18243
+ } catch (error) {
18244
+ if (isTimeoutError(error)) {
18245
+ return { ok: false, error: { kind: "timeout", timeoutMs: timeoutMs2 } };
18246
+ }
18247
+ json = undefined;
18248
+ }
18249
+ const organizations = parseOrganizationsResponse(json);
18250
+ if (!organizations) {
18251
+ return {
18252
+ ok: false,
18253
+ error: {
18254
+ kind: "parse",
18255
+ cause: Error("organizations response did not match the contract")
18256
+ }
18257
+ };
18258
+ }
18259
+ return { ok: true, data: organizations };
18260
+ }
18261
+
17540
18262
  // src/shell/platform/getIdentity.ts
17541
- var timeoutMs = 1e4;
18263
+ var timeoutMs3 = 1e4;
17542
18264
  async function getIdentity(apiKey, deps) {
17543
18265
  const url = `${deps.baseUrl}/api/v0/identity`;
17544
18266
  let response;
17545
18267
  try {
17546
18268
  response = await deps.fetch(url, {
17547
18269
  headers: { Authorization: `Bearer ${apiKey}` },
17548
- signal: AbortSignal.timeout(timeoutMs)
18270
+ signal: AbortSignal.timeout(timeoutMs3)
17549
18271
  });
17550
18272
  } catch (error) {
17551
18273
  if (isTimeoutError(error)) {
17552
- return { ok: false, error: { kind: "timeout", timeoutMs } };
18274
+ return { ok: false, error: { kind: "timeout", timeoutMs: timeoutMs3 } };
17553
18275
  }
17554
18276
  return {
17555
18277
  ok: false,
@@ -17568,7 +18290,7 @@ async function getIdentity(apiKey, deps) {
17568
18290
  json = await response.json();
17569
18291
  } catch (error) {
17570
18292
  if (isTimeoutError(error)) {
17571
- return { ok: false, error: { kind: "timeout", timeoutMs } };
18293
+ return { ok: false, error: { kind: "timeout", timeoutMs: timeoutMs3 } };
17572
18294
  }
17573
18295
  json = undefined;
17574
18296
  }
@@ -17579,6 +18301,24 @@ async function getIdentity(apiKey, deps) {
17579
18301
  return { ok: true, data: parsed.data };
17580
18302
  }
17581
18303
 
18304
+ // src/shell/platform/identityMethods.ts
18305
+ function createIdentityMethods(apiKey, deps, backoffMs) {
18306
+ return {
18307
+ getIdentity: () => requestWithRetry({
18308
+ call: () => getIdentity(apiKey, deps),
18309
+ backoffMs,
18310
+ describe: describeIdentityError,
18311
+ sleep: deps.sleep
18312
+ }),
18313
+ getAccessibleOrganizations: () => requestWithRetry({
18314
+ call: () => getAccessibleOrganizations(apiKey, deps),
18315
+ backoffMs,
18316
+ describe: describeIdentityError,
18317
+ sleep: deps.sleep
18318
+ })
18319
+ };
18320
+ }
18321
+
17582
18322
  // src/shell/platform/types.ts
17583
18323
  var environmentListItemSchema = object({
17584
18324
  envId: string2(),
@@ -17706,7 +18446,7 @@ function isUnsafeSegment(segment) {
17706
18446
 
17707
18447
  // src/shell/platform/teamStorageAssetSnapshot.ts
17708
18448
  import { randomBytes as randomBytes2 } from "node:crypto";
17709
- import { join as join7, sep as sep2 } from "node:path";
18449
+ import { join as join10, sep as sep2 } from "node:path";
17710
18450
  function mintAssetSnapshotPath(assetsAbs, label) {
17711
18451
  return `${assetsAbs}.${label}-${randomBytes2(8).toString("hex")}`;
17712
18452
  }
@@ -17756,9 +18496,9 @@ function expectedEntries(filePaths) {
17756
18496
  return entries;
17757
18497
  }
17758
18498
  async function collectEntries(root, relDir, out, fs) {
17759
- const current = relDir ? join7(root, relDir) : root;
18499
+ const current = relDir ? join10(root, relDir) : root;
17760
18500
  for (const entry of await fs.readdirWithTypes(current)) {
17761
- const relPath = relDir ? join7(relDir, entry.name) : entry.name;
18501
+ const relPath = relDir ? join10(relDir, entry.name) : entry.name;
17762
18502
  out.add(relPath);
17763
18503
  if (entry.isDirectory()) {
17764
18504
  await collectEntries(root, relPath, out, fs);
@@ -17772,7 +18512,7 @@ function setsEqual(left, right) {
17772
18512
  }
17773
18513
 
17774
18514
  // src/shell/platform/teamStorageAssetReuse.ts
17775
- import { join as join8 } from "node:path";
18515
+ import { join as join11 } from "node:path";
17776
18516
  async function reusableAssetPaths(assetsAbs, safeFiles, manifest, fs) {
17777
18517
  const reusable = new Set;
17778
18518
  for (const { file, relativePath } of safeFiles) {
@@ -17788,20 +18528,20 @@ async function canReuseAsset(assetsAbs, relativePath, file, manifest, fs) {
17788
18528
  const previous = manifest.get(relativePath);
17789
18529
  if (previous?.etag !== file.etag)
17790
18530
  return false;
17791
- return fs.pathExists(join8(assetsAbs, relativePath));
18531
+ return fs.pathExists(join11(assetsAbs, relativePath));
17792
18532
  }
17793
18533
 
17794
18534
  // src/shell/platform/writeAssetSnapshot.ts
17795
- import { dirname as dirname3, join as join9 } from "node:path";
18535
+ import { dirname as dirname3, join as join12 } from "node:path";
17796
18536
  async function writeAssetSnapshot(args) {
17797
18537
  let downloadedCount = 0;
17798
18538
  const total = args.safeFiles.filter(({ relativePath }) => !args.reusable.has(relativePath)).length;
17799
18539
  await args.deps.fs.mkdir(args.tmpAssets, { recursive: true });
17800
18540
  for (const { file, relativePath } of args.safeFiles) {
17801
- const dest = join9(args.tmpAssets, relativePath);
18541
+ const dest = join12(args.tmpAssets, relativePath);
17802
18542
  await args.deps.fs.mkdir(dirname3(dest), { recursive: true });
17803
18543
  if (args.reusable.has(relativePath)) {
17804
- await args.deps.fs.copyFile(join9(args.assetsAbs, relativePath), dest);
18544
+ await args.deps.fs.copyFile(join12(args.assetsAbs, relativePath), dest);
17805
18545
  continue;
17806
18546
  }
17807
18547
  args.deps.onProgress?.({ current: downloadedCount + 1, total });
@@ -17861,6 +18601,34 @@ async function writeTeamStorageAssets(args, deps) {
17861
18601
  }
17862
18602
  }
17863
18603
 
18604
+ // src/shell/platform/teamStorageMethods.ts
18605
+ function createTeamStorageMethods(trpc, deps, fs, getIdentity) {
18606
+ async function list() {
18607
+ if (deps.workspaceId !== undefined) {
18608
+ return listTeamStorageFiles(trpc, { teamId: deps.workspaceId }, deps);
18609
+ }
18610
+ const identity = await getIdentity();
18611
+ if (!identity.ok)
18612
+ return identity;
18613
+ if (!("team" in identity.value)) {
18614
+ return {
18615
+ ok: false,
18616
+ error: flowsMessages.pull.teamStorageRequiresTeamKey
18617
+ };
18618
+ }
18619
+ return listTeamStorageFiles(trpc, { teamId: identity.value.team.id }, deps);
18620
+ }
18621
+ return {
18622
+ listTeamStorageFiles: list,
18623
+ async syncTeamStorageAssets(assetsAbs, opts) {
18624
+ const files = await list();
18625
+ if (!files.ok)
18626
+ return files;
18627
+ return downloadTeamStorageAssets({ assetsAbs, files: files.value }, { fetch: deps.fetch, fs, onProgress: opts?.onProgress });
18628
+ }
18629
+ };
18630
+ }
18631
+
17864
18632
  // src/shell/platform/createPlatformClient.ts
17865
18633
  var requestBackoffMs2 = [500, 1500];
17866
18634
  function createPlatformClient(apiKey, deps) {
@@ -17877,15 +18645,10 @@ function createPlatformClient(apiKey, deps) {
17877
18645
  return result;
17878
18646
  return { ok: true, value: { signedUrl: result.value.url } };
17879
18647
  }
18648
+ const identityMethods = createIdentityMethods(apiKey, deps, requestBackoffMs2);
17880
18649
  return {
17881
- async getIdentity() {
17882
- return requestWithRetry({
17883
- call: () => getIdentity(apiKey, deps),
17884
- backoffMs: requestBackoffMs2,
17885
- describe: describeIdentityError,
17886
- sleep: deps.sleep
17887
- });
17888
- },
18650
+ ...identityMethods,
18651
+ ...createTeamStorageMethods(trpc, deps, fs, identityMethods.getIdentity),
17889
18652
  getFlowsBundleUrl: getFlowsBundleUrlImpl,
17890
18653
  callPublicApi: makeCallPublicApiMethod(trpc, deps, requestBackoffMs2),
17891
18654
  async getEnvVars(envId) {
@@ -17899,24 +18662,6 @@ function createPlatformClient(apiKey, deps) {
17899
18662
  return result;
17900
18663
  return { ok: true, value: result.value.environmentVariables };
17901
18664
  },
17902
- async listTeamStorageFiles() {
17903
- const identity = await this.getIdentity();
17904
- if (!identity.ok)
17905
- return identity;
17906
- if (!("team" in identity.value)) {
17907
- return {
17908
- ok: false,
17909
- error: flowsMessages.pull.teamStorageRequiresTeamKey
17910
- };
17911
- }
17912
- return listTeamStorageFiles(trpc, { teamId: identity.value.team.id }, deps);
17913
- },
17914
- async syncTeamStorageAssets(assetsAbs, opts) {
17915
- const files = await this.listTeamStorageFiles();
17916
- if (!files.ok)
17917
- return files;
17918
- return downloadTeamStorageAssets({ assetsAbs, files: files.value }, { fetch: deps.fetch, fs, onProgress: opts?.onProgress });
17919
- },
17920
18665
  async downloadBundle(envId) {
17921
18666
  const urlResult = await getFlowsBundleUrlImpl(envId);
17922
18667
  if (!urlResult.ok)
@@ -18584,8 +19329,88 @@ class V {
18584
19329
  }
18585
19330
  }
18586
19331
  }
18587
- class r extends V {
18588
- get cursor() {
19332
+ function p$1(l, e) {
19333
+ if (l === undefined || e.length === 0)
19334
+ return 0;
19335
+ const i = e.findIndex((s) => s.value === l);
19336
+ return i !== -1 ? i : 0;
19337
+ }
19338
+ function g(l, e) {
19339
+ return (e.label ?? String(e.value)).toLowerCase().includes(l.toLowerCase());
19340
+ }
19341
+ function m2(l, e) {
19342
+ if (e)
19343
+ return l ? e : e[0];
19344
+ }
19345
+ var T$1 = class T extends V {
19346
+ filteredOptions;
19347
+ multiple;
19348
+ isNavigating = false;
19349
+ selectedValues = [];
19350
+ focusedValue;
19351
+ #e = 0;
19352
+ #s = "";
19353
+ #t;
19354
+ #i;
19355
+ #n;
19356
+ get cursor() {
19357
+ return this.#e;
19358
+ }
19359
+ get userInputWithCursor() {
19360
+ if (!this.userInput)
19361
+ return styleText(["inverse", "hidden"], "_");
19362
+ if (this._cursor >= this.userInput.length)
19363
+ return `${this.userInput}█`;
19364
+ const e = this.userInput.slice(0, this._cursor), [t, ...i] = this.userInput.slice(this._cursor);
19365
+ return `${e}${styleText("inverse", t)}${i.join("")}`;
19366
+ }
19367
+ get options() {
19368
+ return typeof this.#i == "function" ? this.#i() : this.#i;
19369
+ }
19370
+ constructor(e) {
19371
+ super(e), this.#i = e.options, this.#n = e.placeholder;
19372
+ const t = this.options;
19373
+ this.filteredOptions = [...t], this.multiple = e.multiple === true, this.#t = typeof e.options == "function" ? e.filter : e.filter ?? g;
19374
+ let i;
19375
+ if (e.initialValue && Array.isArray(e.initialValue) ? this.multiple ? i = e.initialValue : i = e.initialValue.slice(0, 1) : !this.multiple && this.options.length > 0 && (i = [this.options[0].value]), i)
19376
+ for (const s of i) {
19377
+ const n = t.findIndex((o) => o.value === s);
19378
+ n !== -1 && (this.toggleSelected(s), this.#e = n);
19379
+ }
19380
+ this.focusedValue = this.options[this.#e]?.value, this.on("key", (s, n) => this.#l(s, n)), this.on("userInput", (s) => this.#u(s));
19381
+ }
19382
+ _isActionKey(e, t) {
19383
+ return e === "\t" || this.multiple && this.isNavigating && t.name === "space" && e !== undefined && e !== "";
19384
+ }
19385
+ #l(e, t) {
19386
+ const i = t.name === "up", s = t.name === "down", n = t.name === "return", o = this.userInput === "" || this.userInput === "\t", u = this.#n, h = this.options, f = u !== undefined && u !== "" && h.some((r) => !r.disabled && (this.#t ? this.#t(u, r) : true));
19387
+ if (t.name === "tab" && o && f) {
19388
+ this.userInput === "\t" && this._clearUserInput(), this._setUserInput(u, true), this.isNavigating = false;
19389
+ return;
19390
+ }
19391
+ i || s ? (this.#e = findCursor(this.#e, i ? -1 : 1, this.filteredOptions), this.focusedValue = this.filteredOptions[this.#e]?.value, this.multiple || (this.selectedValues = [this.focusedValue]), this.isNavigating = true) : n ? this.value = m2(this.multiple, this.selectedValues) : this.multiple ? this.focusedValue !== undefined && (t.name === "tab" || this.isNavigating && t.name === "space") ? this.toggleSelected(this.focusedValue) : this.isNavigating = false : (this.focusedValue && (this.selectedValues = [this.focusedValue]), this.isNavigating = false);
19392
+ }
19393
+ deselectAll() {
19394
+ this.selectedValues = [];
19395
+ }
19396
+ toggleSelected(e) {
19397
+ this.filteredOptions.length !== 0 && (this.multiple ? this.selectedValues.includes(e) ? this.selectedValues = this.selectedValues.filter((t) => t !== e) : this.selectedValues = [...this.selectedValues, e] : this.selectedValues = [e]);
19398
+ }
19399
+ #u(e) {
19400
+ if (e !== this.#s) {
19401
+ this.#s = e;
19402
+ const t = this.options;
19403
+ e && this.#t ? this.filteredOptions = t.filter((n) => this.#t?.(e, n)) : this.filteredOptions = [...t];
19404
+ const i = p$1(this.focusedValue, this.filteredOptions);
19405
+ this.#e = findCursor(i, 0, this.filteredOptions);
19406
+ const s = this.filteredOptions[this.#e];
19407
+ s && !s.disabled ? this.focusedValue = s.value : this.focusedValue = undefined, this.multiple || (this.focusedValue !== undefined ? this.toggleSelected(this.focusedValue) : this.deselectAll());
19408
+ }
19409
+ }
19410
+ };
19411
+
19412
+ class r extends V {
19413
+ get cursor() {
18589
19414
  return this.value ? 0 : 1;
18590
19415
  }
18591
19416
  get _value() {
@@ -18770,6 +19595,89 @@ var limitOptions = ({
18770
19595
  b.push(n);
18771
19596
  return u && b.push(C), b;
18772
19597
  };
19598
+ function P(t) {
19599
+ return t.label ?? String(t.value ?? "");
19600
+ }
19601
+ function E(t, c) {
19602
+ if (!t)
19603
+ return true;
19604
+ const n = (c.label ?? String(c.value ?? "")).toLowerCase(), i = (c.hint ?? "").toLowerCase(), l = String(c.value).toLowerCase(), o = t.toLowerCase();
19605
+ return n.includes(o) || i.includes(o) || l.includes(o);
19606
+ }
19607
+ function N(t, c) {
19608
+ const n = [];
19609
+ for (const i of c)
19610
+ t.includes(i.value) && n.push(i);
19611
+ return n;
19612
+ }
19613
+ var autocomplete = (t) => new T$1({
19614
+ options: t.options,
19615
+ initialValue: t.initialValue ? [t.initialValue] : undefined,
19616
+ initialUserInput: t.initialUserInput,
19617
+ placeholder: t.placeholder,
19618
+ filter: t.filter ?? ((n, i) => E(n, i)),
19619
+ signal: t.signal,
19620
+ input: t.input,
19621
+ output: t.output,
19622
+ validate: t.validate,
19623
+ render() {
19624
+ const n = t.withGuide ?? settings.withGuide, i = n ? [`${styleText2("gray", S_BAR)}`, `${symbol(this.state)} ${t.message}`] : [`${symbol(this.state)} ${t.message}`], l = this.userInput, o = this.options, m = t.placeholder, p = l === "" && m !== undefined, $ = (r, s) => {
19625
+ const a = P(r), u = r.hint && r.value === this.focusedValue ? styleText2("dim", ` (${r.hint})`) : "";
19626
+ switch (s) {
19627
+ case "active":
19628
+ return `${styleText2("green", S_RADIO_ACTIVE)} ${a}${u}`;
19629
+ case "inactive":
19630
+ return `${styleText2("dim", S_RADIO_INACTIVE)} ${styleText2("dim", a)}`;
19631
+ case "disabled":
19632
+ return `${styleText2("gray", S_RADIO_INACTIVE)} ${styleText2(["strikethrough", "gray"], a)}`;
19633
+ }
19634
+ };
19635
+ switch (this.state) {
19636
+ case "submit": {
19637
+ const r = N(this.selectedValues, o), s = r.length > 0 ? ` ${styleText2("dim", r.map(P).join(", "))}` : "", a = n ? styleText2("gray", S_BAR) : "";
19638
+ return `${i.join(`
19639
+ `)}
19640
+ ${a}${s}`;
19641
+ }
19642
+ case "cancel": {
19643
+ const r = l ? ` ${styleText2(["strikethrough", "dim"], l)}` : "", s = n ? styleText2("gray", S_BAR) : "";
19644
+ return `${i.join(`
19645
+ `)}
19646
+ ${s}${r}`;
19647
+ }
19648
+ default: {
19649
+ const r = this.state === "error" ? "yellow" : "cyan", s = n ? `${styleText2(r, S_BAR)} ` : "", a = n ? styleText2(r, S_BAR_END) : "";
19650
+ let u = "";
19651
+ if (this.isNavigating || p) {
19652
+ const d = p ? m : l;
19653
+ u = d !== "" ? ` ${styleText2("dim", d)}` : "";
19654
+ } else
19655
+ u = ` ${this.userInputWithCursor}`;
19656
+ const V = this.filteredOptions.length !== o.length ? styleText2("dim", ` (${this.filteredOptions.length} match${this.filteredOptions.length === 1 ? "" : "es"})`) : "", y = this.filteredOptions.length === 0 && l ? [`${s}${styleText2("yellow", "No matches found")}`] : [], b = this.state === "error" ? [`${s}${styleText2("yellow", this.error)}`] : [];
19657
+ n && i.push(`${s.trimEnd()}`), i.push(`${s}${styleText2("dim", "Search:")}${u}${V}`, ...y, ...b);
19658
+ const v = [
19659
+ `${styleText2("dim", "↑/↓")} to select`,
19660
+ `${styleText2("dim", "Enter:")} confirm`,
19661
+ `${styleText2("dim", "Type:")} to search`
19662
+ ], g = [`${s}${v.join(" • ")}`, a], O = this.filteredOptions.length === 0 ? [] : limitOptions({
19663
+ cursor: this.cursor,
19664
+ options: this.filteredOptions,
19665
+ columnPadding: n ? 3 : 0,
19666
+ rowPadding: i.length + g.length,
19667
+ style: (d, f) => $(d, d.disabled ? "disabled" : f ? "active" : "inactive"),
19668
+ maxItems: t.maxItems,
19669
+ output: t.output
19670
+ });
19671
+ return [
19672
+ ...i,
19673
+ ...O.map((d) => `${s}${d}`),
19674
+ ...g
19675
+ ].join(`
19676
+ `);
19677
+ }
19678
+ }
19679
+ }
19680
+ }).prompt();
18773
19681
  var confirm = (i) => {
18774
19682
  const a = i.active ?? "Yes", s = i.inactive ?? "No";
18775
19683
  return new r({
@@ -19200,6 +20108,7 @@ function createStyledClack() {
19200
20108
  cancel,
19201
20109
  confirm,
19202
20110
  password,
20111
+ autocomplete,
19203
20112
  select,
19204
20113
  isCancel,
19205
20114
  spinner,
@@ -19459,11 +20368,50 @@ function createPassword({
19459
20368
  };
19460
20369
  }
19461
20370
 
20371
+ // src/core/textSearch.ts
20372
+ function normalizeForSearch(value) {
20373
+ return value.toLowerCase().replace(/[-_\s]+/g, " ").trim();
20374
+ }
20375
+ function matchesSearchTerm(search, fields) {
20376
+ const needle = normalizeForSearch(search);
20377
+ if (!needle)
20378
+ return true;
20379
+ return fields.some((field) => field !== undefined && normalizeForSearch(field).includes(needle));
20380
+ }
20381
+ function createSearchIndex(items, fieldsOf) {
20382
+ const indexed = new Map;
20383
+ for (const item of items) {
20384
+ indexed.set(item, fieldsOf(item).flatMap((field) => field === undefined ? [] : [normalizeForSearch(field)]));
20385
+ }
20386
+ let lastSearch;
20387
+ let needle = "";
20388
+ return (search, item) => {
20389
+ if (search !== lastSearch) {
20390
+ lastSearch = search;
20391
+ needle = normalizeForSearch(search);
20392
+ }
20393
+ if (!needle)
20394
+ return true;
20395
+ const fields = indexed.get(item);
20396
+ return fields ? fields.some((field) => field.includes(needle)) : matchesSearchTerm(search, fieldsOf(item));
20397
+ };
20398
+ }
20399
+
19462
20400
  // src/shell/ui/renderers/select.ts
20401
+ var searchThreshold = 8;
20402
+ var searchPlaceholder = "Type to filter";
20403
+ function searchableFields(option) {
20404
+ return [option.label, option.hint, option.value];
20405
+ }
19463
20406
  function createSelect({ mode, clack }) {
19464
20407
  return async (message, options) => {
19465
20408
  assertHumanMode(mode, "select");
19466
- const value = await clack.select({ message, options: [...options] });
20409
+ const value = options.length > searchThreshold ? await clack.autocomplete({
20410
+ message,
20411
+ options: [...options],
20412
+ placeholder: searchPlaceholder,
20413
+ filter: createSearchIndex([...options], searchableFields)
20414
+ }) : await clack.select({ message, options: [...options] });
19467
20415
  if (clack.isCancel(value))
19468
20416
  return { ok: false };
19469
20417
  return { ok: true, value };
@@ -19511,12 +20459,14 @@ function createVerboseContext(outputMode, isVerbose) {
19511
20459
 
19512
20460
  // src/shell/npmRegistry.ts
19513
20461
  var registryUrl = "https://registry.npmjs.org";
19514
- var timeoutMs2 = 3000;
20462
+ var timeoutMs4 = 3000;
19515
20463
  async function fetchLatestVersion(packageName, deps = {}) {
19516
20464
  const fetchFn = deps.fetchFn ?? globalThis.fetch;
20465
+ const deadline = AbortSignal.timeout(timeoutMs4);
20466
+ const signal = deps.signal ? AbortSignal.any([deps.signal, deadline]) : deadline;
19517
20467
  try {
19518
20468
  const response = await fetchFn(`${registryUrl}/${packageName}/latest`, {
19519
- signal: AbortSignal.timeout(timeoutMs2)
20469
+ signal
19520
20470
  });
19521
20471
  if (!response.ok)
19522
20472
  return;
@@ -19531,7 +20481,7 @@ async function fetchLatestVersion(packageName, deps = {}) {
19531
20481
  }
19532
20482
 
19533
20483
  // src/domains/updateCheck/updateCheck.ts
19534
- import { join as join10 } from "node:path";
20484
+ import { join as join13 } from "node:path";
19535
20485
 
19536
20486
  // src/core/version.ts
19537
20487
  function parseRelease(version) {
@@ -19565,19 +20515,21 @@ function startUpdateCheck(deps) {
19565
20515
  if (deps.env["QAWOLF_NO_UPDATE_CHECK"]) {
19566
20516
  return noopNotifier;
19567
20517
  }
20518
+ const controller = new AbortController;
19568
20519
  let latest;
19569
- deps.fetchLatestVersion().then((version) => {
20520
+ deps.fetchLatestVersion(controller.signal).then((version) => {
19570
20521
  latest = version;
19571
20522
  }, () => {
19572
20523
  return;
19573
20524
  });
19574
20525
  return {
19575
20526
  async notifyIfOutdated() {
20527
+ controller.abort();
19576
20528
  if (latest === undefined)
19577
20529
  return;
19578
20530
  if (!isNewerVersion(deps.currentVersion, latest))
19579
20531
  return;
19580
- const noticePath = join10(deps.configDir, noticeFile);
20532
+ const noticePath = join13(deps.configDir, noticeFile);
19581
20533
  try {
19582
20534
  const notified = (await deps.fs.readFile(noticePath)).trim();
19583
20535
  if (notified === latest)
@@ -19599,7 +20551,7 @@ function startUpdateCheck(deps) {
19599
20551
  // package.json
19600
20552
  var package_default = {
19601
20553
  name: "@qawolf/cli",
19602
- version: "1.24.0",
20554
+ version: "1.26.0",
19603
20555
  description: "Run and manage QA Wolf flows from the terminal, CI, or an AI agent",
19604
20556
  keywords: [
19605
20557
  "automation",
@@ -19670,7 +20622,7 @@ var package_default = {
19670
20622
  "@clack/prompts": "1.5.1",
19671
20623
  "@napi-rs/keyring": "1.3.0",
19672
20624
  "@oxc-node/core": "0.1.0",
19673
- "@qawolf/api-contracts": "0.47.0",
20625
+ "@qawolf/api-contracts": "0.52.0",
19674
20626
  "@qawolf/emails": "1.1.1",
19675
20627
  "@qawolf/flow-targets": "1.0.0",
19676
20628
  "@qawolf/flows": "0.1.4",
@@ -19736,7 +20688,7 @@ function buildBaseContext(command, signals) {
19736
20688
  currentVersion: package_default.version,
19737
20689
  configDir: getConfigDir(),
19738
20690
  fs,
19739
- fetchLatestVersion: () => fetchLatestVersion(package_default.name),
20691
+ fetchLatestVersion: (signal) => fetchLatestVersion(package_default.name, { signal }),
19740
20692
  renderNotice: (body, title) => ui.note(body, title)
19741
20693
  });
19742
20694
  return {
@@ -19789,7 +20741,12 @@ function withAuthContext(signals, fn, deps = {}) {
19789
20741
  await updateNotifier.notifyIfOutdated();
19790
20742
  return;
19791
20743
  }
19792
- const platformClient = (deps.createPlatform ?? createPlatformClient)(resolved.key, { baseUrl: apiBaseUrl, fetch: globalThis.fetch, logger: ctx.log("trpc") });
20744
+ const platformClient = (deps.createPlatform ?? createPlatformClient)(resolved.key, {
20745
+ baseUrl: apiBaseUrl,
20746
+ fetch: globalThis.fetch,
20747
+ logger: ctx.log("trpc"),
20748
+ workspaceId: resolved.workspaceId
20749
+ });
19793
20750
  try {
19794
20751
  const result = await fn({
19795
20752
  ...ctx,
@@ -19810,28 +20767,15 @@ function withAuthContext(signals, fn, deps = {}) {
19810
20767
  };
19811
20768
  }
19812
20769
 
19813
- // src/commands/auth/login.ts
19814
- async function handleLogin(ctx) {
19815
- if (ctx.ui.mode !== "human") {
19816
- ctx.ui.error(authMessages.login.nonInteractive);
19817
- return { error: "non-interactive" };
19818
- }
19819
- const existing = await resolveApiKey(ctx.configDir, ctx.fs);
19820
- if (existing) {
19821
- const reauth = await ctx.ui.confirm(authMessages.login.reAuthPrompt);
19822
- if (!reauth.ok || !reauth.value) {
19823
- ctx.ui.info(authMessages.alreadyConfigured);
19824
- return;
19825
- }
19826
- }
19827
- ctx.ui.gap();
19828
- ctx.ui.intro(authMessages.title);
20770
+ // src/commands/auth/loginApiKey.ts
20771
+ async function loginWithApiKey(ctx) {
19829
20772
  const result = await ctx.ui.password(authMessages.promptApiKey, "Set QAWOLF_API_KEY to authenticate in non-interactive environments.");
19830
20773
  if (!result.ok) {
19831
20774
  ctx.ui.cancel(authMessages.cancelled);
19832
20775
  return;
19833
20776
  }
19834
- if (!result.value.trim()) {
20777
+ const apiKey = result.value.trim();
20778
+ if (!apiKey) {
19835
20779
  ctx.ui.cancel(authMessages.cancelled);
19836
20780
  return;
19837
20781
  }
@@ -19840,7 +20784,7 @@ async function handleLogin(ctx) {
19840
20784
  message: authMessages.verifying,
19841
20785
  task: async () => {
19842
20786
  const v = await validateApiKey({
19843
- platformClient: createPlatformClient(result.value, {
20787
+ platformClient: createPlatformClient(apiKey, {
19844
20788
  baseUrl: ctx.apiBaseUrl,
19845
20789
  fetch: globalThis.fetch
19846
20790
  })
@@ -19851,7 +20795,7 @@ async function handleLogin(ctx) {
19851
20795
  },
19852
20796
  {
19853
20797
  message: authMessages.storing,
19854
- task: async () => saveApiKey(ctx.configDir, result.value, ctx.fs)
20798
+ task: async () => saveApiKey(ctx.configDir, apiKey, ctx.fs)
19855
20799
  }
19856
20800
  ], ([, saveResult]) => {
19857
20801
  return saveResult.stored === "file" ? authMessages.storedFile : authMessages.storedKeychain;
@@ -19859,14 +20803,632 @@ async function handleLogin(ctx) {
19859
20803
  ctx.ui.outro(authMessages.outroSuccess);
19860
20804
  }
19861
20805
 
20806
+ // src/core/deviceAuth/pollState.ts
20807
+ var slowDownIncrementMs = 5000;
20808
+ var unreachableBackoffFactor = 2;
20809
+ function capToLifetime(delayMs, state, nowMs) {
20810
+ return Math.min(delayMs, Math.max(1, state.deadlineMs - nowMs));
20811
+ }
20812
+ function nextPollStep(state, response, nowMs) {
20813
+ if (response.kind === "tokens") {
20814
+ return { action: "done", tokens: response.tokens };
20815
+ }
20816
+ if (nowMs > state.deadlineMs) {
20817
+ return { action: "fail", reason: "timeout", detail: undefined };
20818
+ }
20819
+ switch (response.kind) {
20820
+ case "pending":
20821
+ return {
20822
+ action: "poll",
20823
+ delayMs: capToLifetime(state.intervalMs, state, nowMs),
20824
+ state
20825
+ };
20826
+ case "slow-down": {
20827
+ const slowed = {
20828
+ intervalMs: state.intervalMs + slowDownIncrementMs,
20829
+ deadlineMs: state.deadlineMs
20830
+ };
20831
+ return {
20832
+ action: "poll",
20833
+ delayMs: capToLifetime(slowed.intervalMs, slowed, nowMs),
20834
+ state: slowed
20835
+ };
20836
+ }
20837
+ case "denied":
20838
+ return { action: "fail", reason: "access-denied", detail: undefined };
20839
+ case "expired":
20840
+ return { action: "fail", reason: "expired", detail: undefined };
20841
+ case "unreachable": {
20842
+ const backedOff = {
20843
+ intervalMs: state.intervalMs * unreachableBackoffFactor,
20844
+ deadlineMs: state.deadlineMs
20845
+ };
20846
+ return {
20847
+ action: "poll",
20848
+ delayMs: capToLifetime(backedOff.intervalMs, backedOff, nowMs),
20849
+ state: backedOff
20850
+ };
20851
+ }
20852
+ case "error":
20853
+ return { action: "fail", reason: "network", detail: response.detail };
20854
+ }
20855
+ }
20856
+
20857
+ // src/domains/auth/deviceLogin.ts
20858
+ var cancelled = {
20859
+ ok: false,
20860
+ reason: "cancelled",
20861
+ detail: undefined
20862
+ };
20863
+ async function deviceLogin(deps) {
20864
+ if (deps.isCancelled())
20865
+ return cancelled;
20866
+ const authorization = await deps.requestAuthorization();
20867
+ if (!authorization.ok) {
20868
+ return { ok: false, reason: "unavailable", detail: authorization.error };
20869
+ }
20870
+ await deps.onPrompt(authorization.value);
20871
+ let state = {
20872
+ intervalMs: authorization.value.intervalSec * 1000,
20873
+ deadlineMs: deps.now() + authorization.value.expiresInSec * 1000
20874
+ };
20875
+ for (;; ) {
20876
+ if (deps.isCancelled())
20877
+ return cancelled;
20878
+ const response = await deps.pollToken(authorization.value.deviceCode);
20879
+ const step = nextPollStep(state, response, deps.now());
20880
+ if (step.action === "done")
20881
+ return { ok: true, tokens: step.tokens };
20882
+ if (step.action === "fail") {
20883
+ return { ok: false, reason: step.reason, detail: step.detail };
20884
+ }
20885
+ state = step.state;
20886
+ await deps.sleep(step.delayMs);
20887
+ }
20888
+ }
20889
+
20890
+ // src/shell/openBrowser.ts
20891
+ var launchTimeoutMs = 2000;
20892
+ function launcher(url, platform) {
20893
+ if (platform === "darwin")
20894
+ return { cmd: "open", args: [url] };
20895
+ if (platform === "win32") {
20896
+ return { cmd: "rundll32", args: ["url.dll,FileProtocolHandler", url] };
20897
+ }
20898
+ return { cmd: "xdg-open", args: [url] };
20899
+ }
20900
+ async function openBrowser(url, deps) {
20901
+ let parsed;
20902
+ try {
20903
+ parsed = new URL(url);
20904
+ } catch {
20905
+ return false;
20906
+ }
20907
+ if (parsed.protocol !== "https:" && parsed.protocol !== "http:") {
20908
+ return false;
20909
+ }
20910
+ const { cmd, args } = launcher(url, deps.platform);
20911
+ const launched = deps.spawn(cmd, args, { platform: deps.platform }).then((result) => result.exitCode === 0).catch(() => false);
20912
+ return Promise.race([launched, deps.sleep(launchTimeoutMs).then(() => true)]);
20913
+ }
20914
+
20915
+ // src/shell/spawn.ts
20916
+ import { spawn } from "node:child_process";
20917
+
20918
+ // src/core/cmdEscape.ts
20919
+ var metaChars = /([()\][%!^"`<>&|;, *?])/g;
20920
+ function escapeCommand(command) {
20921
+ return command.replace(metaChars, "^$1");
20922
+ }
20923
+ function escapeArgument(arg) {
20924
+ const escaped = arg.replace(/(?=(\\+?)?)\1"/g, "$1$1\\\"").replace(/(?=(\\+?)?)\1$/, "$1$1");
20925
+ return `"${escaped}"`.replace(metaChars, "^$1").replace(metaChars, "^$1");
20926
+ }
20927
+
20928
+ // src/shell/spawn.ts
20929
+ function buildSpawnCommand(cmd, args, platform, env) {
20930
+ const options = {};
20931
+ if (env)
20932
+ options.env = env;
20933
+ if (platform !== "win32" || !/\.(cmd|bat)$/i.test(cmd)) {
20934
+ return { cmd, args, options };
20935
+ }
20936
+ const line = [escapeCommand(cmd), ...args.map(escapeArgument)].join(" ");
20937
+ return {
20938
+ cmd: process.env["ComSpec"] ?? "cmd.exe",
20939
+ args: ["/d", "/s", "/c", `"${line}"`],
20940
+ options: { ...options, windowsVerbatimArguments: true }
20941
+ };
20942
+ }
20943
+ var defaultSpawn = (cmd, args, opts) => new Promise((resolve) => {
20944
+ const env = opts.env ? { ...process.env, ...opts.env } : undefined;
20945
+ const built = buildSpawnCommand(cmd, args, opts.platform, env);
20946
+ const child = spawn(built.cmd, built.args, built.options);
20947
+ let stdout = "";
20948
+ let stderr = "";
20949
+ child.stdout?.on("data", (chunk) => {
20950
+ stdout += String(chunk);
20951
+ });
20952
+ child.stderr?.on("data", (chunk) => {
20953
+ stderr += String(chunk);
20954
+ });
20955
+ if (opts?.stdin !== undefined) {
20956
+ child.stdin?.write(opts.stdin);
20957
+ child.stdin?.end();
20958
+ }
20959
+ child.on("error", (err) => resolve({ exitCode: -1, stdout, stderr: stderr || err.message }));
20960
+ child.on("close", (code) => resolve({ exitCode: code ?? -1, stdout, stderr }));
20961
+ });
20962
+
20963
+ // src/shell/platform/getAuthConfig.ts
20964
+ var timeoutMs5 = 1e4;
20965
+ var authConfigBody = object({
20966
+ workOsClientId: string2().trim().min(1)
20967
+ });
20968
+ async function getAuthConfig(deps) {
20969
+ let response;
20970
+ try {
20971
+ response = await deps.fetch(`${deps.baseUrl}/api/v0/auth/config`, {
20972
+ signal: AbortSignal.timeout(timeoutMs5)
20973
+ });
20974
+ } catch (err) {
20975
+ return { kind: "unreachable", detail: errorMessage(err) };
20976
+ }
20977
+ if (response.status === 404)
20978
+ return { kind: "unconfigured" };
20979
+ if (!response.ok) {
20980
+ return { kind: "unreachable", detail: `HTTP ${response.status}` };
20981
+ }
20982
+ let json;
20983
+ try {
20984
+ json = await response.json();
20985
+ } catch (err) {
20986
+ return { kind: "unreachable", detail: errorMessage(err) };
20987
+ }
20988
+ const parsed = authConfigBody.safeParse(json);
20989
+ return parsed.success ? { kind: "configured", clientId: parsed.data.workOsClientId } : { kind: "unconfigured" };
20990
+ }
20991
+
20992
+ // src/shell/workos/pollDeviceToken.ts
20993
+ var deviceCodeGrantType = "urn:ietf:params:oauth:grant-type:device_code";
20994
+ async function pollDeviceToken(deviceCode, deps) {
20995
+ const outcome = await sendWorkosRequest(`${deps.baseUrl}/user_management/authenticate`, {
20996
+ headers: { "content-type": "application/x-www-form-urlencoded" },
20997
+ body: new URLSearchParams({
20998
+ grant_type: deviceCodeGrantType,
20999
+ device_code: deviceCode,
21000
+ client_id: deps.clientId
21001
+ }).toString()
21002
+ }, deps.fetch);
21003
+ if (outcome.kind === "failure") {
21004
+ return outcome.retryable ? { kind: "unreachable", detail: outcome.detail } : { kind: "error", detail: outcome.detail };
21005
+ }
21006
+ if (outcome.kind === "oauth-error") {
21007
+ switch (outcome.code) {
21008
+ case "authorization_pending":
21009
+ return { kind: "pending" };
21010
+ case "slow_down":
21011
+ return { kind: "slow-down" };
21012
+ case "access_denied":
21013
+ return { kind: "denied" };
21014
+ case "expired_token":
21015
+ case "invalid_grant":
21016
+ return { kind: "expired" };
21017
+ default:
21018
+ return {
21019
+ kind: "error",
21020
+ detail: outcome.description ?? outcome.code
21021
+ };
21022
+ }
21023
+ }
21024
+ const parsed = deviceTokenBody.safeParse(outcome.json);
21025
+ if (!parsed.success) {
21026
+ return { kind: "error", detail: unexpectedResponse };
21027
+ }
21028
+ return {
21029
+ kind: "tokens",
21030
+ tokens: {
21031
+ accessToken: parsed.data.access_token,
21032
+ refreshToken: parsed.data.refresh_token,
21033
+ expiresAt: readAccessTokenExpiry(parsed.data.access_token),
21034
+ email: parsed.data.user.email,
21035
+ organizationId: parsed.data.organization_id
21036
+ }
21037
+ };
21038
+ }
21039
+
21040
+ // src/shell/workos/requestDeviceAuthorization.ts
21041
+ async function requestDeviceAuthorization(deps) {
21042
+ const outcome = await sendWorkosRequest(`${deps.baseUrl}/user_management/authorize/device`, {
21043
+ headers: { "content-type": "application/x-www-form-urlencoded" },
21044
+ body: new URLSearchParams({ client_id: deps.clientId }).toString()
21045
+ }, deps.fetch);
21046
+ if (outcome.kind === "failure") {
21047
+ return { ok: false, error: outcome.detail, retryable: outcome.retryable };
21048
+ }
21049
+ if (outcome.kind === "oauth-error") {
21050
+ return {
21051
+ ok: false,
21052
+ error: outcome.description ?? outcome.code,
21053
+ retryable: false
21054
+ };
21055
+ }
21056
+ const parsed = deviceAuthorizationBody.safeParse(outcome.json);
21057
+ if (!parsed.success) {
21058
+ return { ok: false, error: unexpectedResponse, retryable: false };
21059
+ }
21060
+ return {
21061
+ ok: true,
21062
+ value: {
21063
+ deviceCode: parsed.data.device_code,
21064
+ userCode: parsed.data.user_code,
21065
+ verificationUri: parsed.data.verification_uri,
21066
+ verificationUriComplete: parsed.data.verification_uri_complete,
21067
+ expiresInSec: parsed.data.expires_in,
21068
+ intervalSec: parsed.data.interval ?? defaultIntervalSec
21069
+ }
21070
+ };
21071
+ }
21072
+
21073
+ // src/domains/auth/nameMatching.ts
21074
+ function matches(candidate, wanted) {
21075
+ return candidate.toLowerCase() === wanted.trim().toLowerCase();
21076
+ }
21077
+ function findOrganization(organizations, wanted) {
21078
+ return organizations.find((organization) => matches(organization.name, wanted) || matches(organization.id, wanted) || matches(organization.workOsOrganizationId, wanted));
21079
+ }
21080
+ function findWorkspace(workspaces, wanted) {
21081
+ return workspaces.find((workspace) => matches(workspace.name, wanted) || matches(workspace.id, wanted) || workspace.slug !== undefined && matches(workspace.slug, wanted));
21082
+ }
21083
+ function findWorkspaceAcross(organizations, wanted) {
21084
+ const hits = organizations.flatMap((organization) => {
21085
+ const workspace = findWorkspace(organization.workspaces, wanted);
21086
+ return workspace ? [{ organization, workspace }] : [];
21087
+ });
21088
+ const [only] = hits;
21089
+ if (!only)
21090
+ return { kind: "none" };
21091
+ if (hits.length > 1) {
21092
+ return {
21093
+ kind: "ambiguous",
21094
+ organizations: hits.map((h) => h.organization)
21095
+ };
21096
+ }
21097
+ return {
21098
+ kind: "found",
21099
+ organization: only.organization,
21100
+ workspace: only.workspace
21101
+ };
21102
+ }
21103
+ function nameList(items) {
21104
+ return items.map((item) => item.slug ?? item.name).join(", ");
21105
+ }
21106
+
21107
+ // src/domains/auth/selectWorkspace.ts
21108
+ async function selectWorkspace(deps) {
21109
+ const { organizations } = deps;
21110
+ if (organizations.length === 0)
21111
+ return { outcome: "none" };
21112
+ let organization;
21113
+ if (deps.preferredOrganization) {
21114
+ organization = findOrganization(organizations, deps.preferredOrganization);
21115
+ if (!organization) {
21116
+ return {
21117
+ outcome: "failed",
21118
+ error: `No organization matches '${deps.preferredOrganization}'. Available: ${nameList(organizations)}.`
21119
+ };
21120
+ }
21121
+ } else if (organizations.length === 1) {
21122
+ organization = organizations[0];
21123
+ } else if (deps.preferredWorkspace) {
21124
+ const match = findWorkspaceAcross(organizations, deps.preferredWorkspace);
21125
+ if (match.kind === "none") {
21126
+ return {
21127
+ outcome: "failed",
21128
+ error: `No workspace matches '${deps.preferredWorkspace}'. Available: ${nameList(organizations.flatMap((o) => o.workspaces))}.`
21129
+ };
21130
+ }
21131
+ if (match.kind === "ambiguous") {
21132
+ return {
21133
+ outcome: "failed",
21134
+ error: `More than one organization has a workspace matching '${deps.preferredWorkspace}': ${nameList(match.organizations)}. Set QAWOLF_ORGANIZATION to choose between them.`
21135
+ };
21136
+ }
21137
+ organization = match.organization;
21138
+ } else {
21139
+ organization = await deps.chooseOrganization(organizations);
21140
+ if (!organization)
21141
+ return { outcome: "cancelled" };
21142
+ }
21143
+ if (!organization)
21144
+ throw Error("organization was not settled");
21145
+ const { workspaces } = organization;
21146
+ if (workspaces.length === 0) {
21147
+ return {
21148
+ outcome: "failed",
21149
+ error: `Organization '${organization.name}' has no workspaces.`
21150
+ };
21151
+ }
21152
+ let workspace;
21153
+ if (deps.preferredWorkspace) {
21154
+ workspace = findWorkspace(workspaces, deps.preferredWorkspace);
21155
+ if (!workspace) {
21156
+ return {
21157
+ outcome: "failed",
21158
+ error: `No workspace matches '${deps.preferredWorkspace}' in ${organization.name}. Available: ${nameList(workspaces)}.`
21159
+ };
21160
+ }
21161
+ } else if (workspaces.length === 1) {
21162
+ workspace = workspaces[0];
21163
+ } else {
21164
+ workspace = await deps.chooseWorkspace(workspaces);
21165
+ if (!workspace)
21166
+ return { outcome: "cancelled" };
21167
+ }
21168
+ if (!workspace)
21169
+ throw Error("workspace was not settled");
21170
+ try {
21171
+ await deps.saveWorkspace(workspace.id);
21172
+ } catch (err) {
21173
+ return {
21174
+ outcome: "failed",
21175
+ error: authMessages.workspace.saveFailed(errorMessage(err))
21176
+ };
21177
+ }
21178
+ return { outcome: "selected", organization, workspace };
21179
+ }
21180
+
21181
+ // src/domains/auth/store/saveWorkspaceId.ts
21182
+ import { Entry as Entry6 } from "@napi-rs/keyring";
21183
+ function makeSaveWorkspaceIdDeps(configDir, fs) {
21184
+ return {
21185
+ loadTokens: () => loadTokens(configDir, { EntryClass: Entry6, fs }),
21186
+ saveTokens: (session) => saveTokens(configDir, session, fs)
21187
+ };
21188
+ }
21189
+ async function saveWorkspaceId(session, workspaceId, deps) {
21190
+ const current = await deps.loadTokens();
21191
+ if (!current.found || !isSameSession(current.tokens, session)) {
21192
+ throw Error(authMessages.workspace.sessionChanged);
21193
+ }
21194
+ return deps.saveTokens({ ...current.tokens, workspaceId });
21195
+ }
21196
+ function isSameSession(stored, captured) {
21197
+ return stored.email === captured.email && stored.organizationId === captured.organizationId;
21198
+ }
21199
+
21200
+ // src/commands/auth/chooseWorkspace.ts
21201
+ async function chooseWorkspace(ctx, args) {
21202
+ const client = createPlatformClient(args.session.accessToken, {
21203
+ baseUrl: ctx.apiBaseUrl,
21204
+ fetch: globalThis.fetch,
21205
+ logger: ctx.log("trpc")
21206
+ });
21207
+ const [identity, discovered] = await Promise.all([
21208
+ client.getIdentity(),
21209
+ client.getAccessibleOrganizations()
21210
+ ]);
21211
+ let organizations;
21212
+ if (discovered.ok) {
21213
+ organizations = discovered.value;
21214
+ } else if (identity.ok) {
21215
+ organizations = "user" in identity.value ? identity.value.organizations : [];
21216
+ } else {
21217
+ return { outcome: "failed", error: identity.error };
21218
+ }
21219
+ return selectWorkspace({
21220
+ organizations,
21221
+ preferredOrganization: args.env["QAWOLF_ORGANIZATION"]?.trim(),
21222
+ preferredWorkspace: args.env["QAWOLF_WORKSPACE"]?.trim(),
21223
+ chooseOrganization: async (organizations) => {
21224
+ if (ctx.ui.mode !== "human")
21225
+ return;
21226
+ const picked = await ctx.ui.select(authMessages.workspace.chooseOrganization, organizations.map((organization) => ({
21227
+ value: organization.workOsOrganizationId,
21228
+ label: organization.name,
21229
+ hint: authMessages.workspace.workspaceCount(organization.workspaces.length)
21230
+ })));
21231
+ if (!picked.ok)
21232
+ return;
21233
+ return organizations.find((organization) => organization.workOsOrganizationId === picked.value);
21234
+ },
21235
+ chooseWorkspace: async (workspaces) => {
21236
+ if (ctx.ui.mode !== "human")
21237
+ return;
21238
+ const picked = await ctx.ui.select(authMessages.workspace.choose, workspaces.map((workspace) => ({
21239
+ value: workspace.id,
21240
+ label: workspace.name,
21241
+ ...workspace.slug ? { hint: workspace.slug } : {}
21242
+ })));
21243
+ if (!picked.ok)
21244
+ return;
21245
+ return workspaces.find((workspace) => workspace.id === picked.value);
21246
+ },
21247
+ saveWorkspace: (workspaceId) => saveWorkspaceId(args.session, workspaceId, makeSaveWorkspaceIdDeps(ctx.configDir, ctx.fs))
21248
+ });
21249
+ }
21250
+ function reportWorkspace(ctx, result) {
21251
+ switch (result.outcome) {
21252
+ case "selected":
21253
+ ctx.ui.info(authMessages.workspace.working(result.organization.name, result.workspace.name));
21254
+ return;
21255
+ case "none":
21256
+ ctx.ui.info(authMessages.workspace.none);
21257
+ return;
21258
+ case "cancelled":
21259
+ if (ctx.ui.mode !== "human") {
21260
+ return { error: authMessages.workspace.nonInteractive };
21261
+ }
21262
+ ctx.ui.info(authMessages.workspace.cancelled);
21263
+ return;
21264
+ case "failed":
21265
+ return { error: result.error };
21266
+ }
21267
+ }
21268
+
21269
+ // src/commands/auth/loginDevice.ts
21270
+ async function loginWithDevice(ctx, deps = {}) {
21271
+ const authConfig = await getAuthConfig({
21272
+ baseUrl: ctx.apiBaseUrl,
21273
+ fetch: globalThis.fetch
21274
+ });
21275
+ if (authConfig.kind === "unreachable") {
21276
+ ctx.log("auth").debug(`auth config unreachable: ${authConfig.detail}`);
21277
+ return {
21278
+ error: authMessages.device.configUnreachable,
21279
+ errorBody: authConfig.detail
21280
+ };
21281
+ }
21282
+ const config = resolveWorkosConfig(authConfig.kind === "configured" ? authConfig.clientId : undefined);
21283
+ if (!config.configured) {
21284
+ return { error: authMessages.device.unavailable };
21285
+ }
21286
+ const workos = {
21287
+ fetch: globalThis.fetch,
21288
+ baseUrl: config.baseUrl,
21289
+ clientId: config.clientId
21290
+ };
21291
+ let cancelled = false;
21292
+ const unregister = ctx.signals.register(() => {
21293
+ cancelled = true;
21294
+ });
21295
+ const showCode = async (authorization) => {
21296
+ const url = authorization.verificationUriComplete ?? authorization.verificationUri;
21297
+ ctx.ui.note([
21298
+ authMessages.device.confirmCode(authorization.userCode),
21299
+ authMessages.device.visitUrl(url),
21300
+ url === authorization.verificationUri ? undefined : authMessages.device.visitUrlPlain(authorization.verificationUri)
21301
+ ].filter((line) => Boolean(line)).join(`
21302
+ `), authMessages.title);
21303
+ const opened = await openBrowser(url, {
21304
+ sleep,
21305
+ spawn: defaultSpawn,
21306
+ platform: deps.platform ?? process.platform
21307
+ });
21308
+ if (!opened)
21309
+ ctx.ui.info(authMessages.device.openFailed(url));
21310
+ ctx.ui.step(authMessages.device.waiting);
21311
+ };
21312
+ try {
21313
+ const result = await deviceLogin({
21314
+ requestAuthorization: () => requestDeviceAuthorization(workos),
21315
+ pollToken: (deviceCode) => pollDeviceToken(deviceCode, workos),
21316
+ onPrompt: showCode,
21317
+ sleep,
21318
+ now: () => Date.now(),
21319
+ isCancelled: () => cancelled
21320
+ });
21321
+ if (!result.ok) {
21322
+ return {
21323
+ error: authMessages.device.failed[result.reason],
21324
+ ...result.detail ? { errorBody: result.detail } : {}
21325
+ };
21326
+ }
21327
+ const session = {
21328
+ ...result.tokens,
21329
+ workspaceId: undefined,
21330
+ clientId: config.clientId
21331
+ };
21332
+ await saveTokens(ctx.configDir, session, ctx.fs);
21333
+ const workspace = await chooseWorkspace(ctx, {
21334
+ session,
21335
+ env: deps.env ?? process.env
21336
+ });
21337
+ const failure = reportWorkspace(ctx, workspace);
21338
+ if (failure)
21339
+ return failure;
21340
+ ctx.ui.outro(authMessages.device.signedIn(result.tokens.email));
21341
+ return;
21342
+ } finally {
21343
+ unregister();
21344
+ }
21345
+ }
21346
+
21347
+ // src/commands/auth/login.ts
21348
+ var browserMethod = "browser";
21349
+ async function handleLogin(ctx, deps = {}) {
21350
+ if (ctx.ui.mode !== "human") {
21351
+ ctx.ui.error(authMessages.login.nonInteractive);
21352
+ return { error: "non-interactive" };
21353
+ }
21354
+ const resolveApiKey2 = deps.resolveApiKey ?? resolveApiKey;
21355
+ const existing = await resolveApiKey2(ctx.configDir, ctx.fs);
21356
+ if (existing) {
21357
+ const reauth = await ctx.ui.confirm(authMessages.login.reAuthPrompt);
21358
+ if (!reauth.ok || !reauth.value) {
21359
+ ctx.ui.info(authMessages.alreadyConfigured);
21360
+ return;
21361
+ }
21362
+ }
21363
+ ctx.ui.gap();
21364
+ ctx.ui.intro(authMessages.title);
21365
+ const method = await ctx.ui.select(authMessages.login.chooseMethod, [
21366
+ {
21367
+ value: browserMethod,
21368
+ label: authMessages.login.methodBrowser,
21369
+ hint: authMessages.login.methodBrowserHint
21370
+ },
21371
+ {
21372
+ value: "api-key",
21373
+ label: authMessages.login.methodApiKey,
21374
+ hint: authMessages.login.methodApiKeyHint
21375
+ }
21376
+ ]);
21377
+ if (!method.ok) {
21378
+ ctx.ui.cancel(authMessages.cancelled);
21379
+ return;
21380
+ }
21381
+ if (method.value !== browserMethod) {
21382
+ return (deps.loginWithApiKey ?? loginWithApiKey)(ctx);
21383
+ }
21384
+ if (existing && existing.source !== "browser") {
21385
+ ctx.ui.warn(existing.source === "env" ? authMessages.login.apiKeyPrecedence.env : authMessages.login.apiKeyPrecedence.stored);
21386
+ }
21387
+ return (deps.loginWithDevice ?? loginWithDevice)(ctx);
21388
+ }
21389
+
21390
+ // src/domains/auth/store/deleteTokens.ts
21391
+ import { join as join14 } from "node:path";
21392
+ import { Entry as Entry7 } from "@napi-rs/keyring";
21393
+ function deleteFromKeychain2() {
21394
+ try {
21395
+ new Entry7(service, tokensAccount).deletePassword();
21396
+ return "deleted";
21397
+ } catch {
21398
+ return "unavailable";
21399
+ }
21400
+ }
21401
+ async function deleteFromFile2(configDir, fs) {
21402
+ try {
21403
+ await fs.unlink(join14(configDir, tokensFile));
21404
+ return "deleted";
21405
+ } catch (err) {
21406
+ if (isNoEntError(err))
21407
+ return "not-found";
21408
+ throw err;
21409
+ }
21410
+ }
21411
+ async function deleteTokens(configDir, fs) {
21412
+ const [keychain, file] = await Promise.all([
21413
+ Promise.resolve(deleteFromKeychain2()),
21414
+ deleteFromFile2(configDir, fs)
21415
+ ]);
21416
+ return { keychain, file };
21417
+ }
21418
+
19862
21419
  // src/commands/auth/logout.ts
19863
- async function handleLogout(ctx) {
19864
- const resolved = await resolveApiKey(ctx.configDir, ctx.fs);
19865
- if (!resolved) {
21420
+ async function handleLogout(ctx, deps = {}) {
21421
+ const hasStoredCredentials2 = deps.hasStoredCredentials ?? hasStoredCredentials;
21422
+ const deleteApiKey2 = deps.deleteApiKey ?? deleteApiKey;
21423
+ const deleteTokens2 = deps.deleteTokens ?? deleteTokens;
21424
+ const env = deps.env ?? process.env;
21425
+ const envKey = env["QAWOLF_API_KEY"]?.trim();
21426
+ const stored = await hasStoredCredentials2(ctx.configDir, ctx.fs);
21427
+ if (!envKey && !stored) {
19866
21428
  ctx.ui.info(authMessages.logout.notAuthenticated);
19867
21429
  return;
19868
21430
  }
19869
- if (resolved.source === "env") {
21431
+ if (envKey) {
19870
21432
  ctx.ui.warn(authMessages.logout.envVarWarning);
19871
21433
  }
19872
21434
  if (ctx.ui.mode === "human") {
@@ -19881,7 +21443,12 @@ async function handleLogout(ctx) {
19881
21443
  await ctx.ui.withProgress([
19882
21444
  {
19883
21445
  message: authMessages.logout.deleting,
19884
- task: () => deleteApiKey(ctx.configDir, ctx.fs)
21446
+ task: async () => {
21447
+ await Promise.all([
21448
+ deleteApiKey2(ctx.configDir, ctx.fs),
21449
+ deleteTokens2(ctx.configDir, ctx.fs)
21450
+ ]);
21451
+ }
19885
21452
  }
19886
21453
  ], () => authMessages.logout.credentialsRemoved);
19887
21454
  if (ctx.ui.mode === "human") {
@@ -19891,6 +21458,76 @@ async function handleLogout(ctx) {
19891
21458
  }
19892
21459
  }
19893
21460
 
21461
+ // src/domains/auth/refreshStoredSession.ts
21462
+ import { Entry as Entry8 } from "@napi-rs/keyring";
21463
+ function makeDefaultDeps2(fs) {
21464
+ return {
21465
+ loadTokens: (configDir) => loadTokens(configDir, { EntryClass: Entry8, fs }),
21466
+ resolveOauth: (configDir) => resolveOauthToken(configDir, makeOauthDeps(fs))
21467
+ };
21468
+ }
21469
+ async function refreshStoredSession(configDir, fs, deps) {
21470
+ const resolvedDeps = deps ?? makeDefaultDeps2(fs);
21471
+ const before = await resolvedDeps.loadTokens(configDir);
21472
+ if (!before.found)
21473
+ return { kind: "not-signed-in" };
21474
+ const renewed = await resolvedDeps.resolveOauth(configDir);
21475
+ if (!renewed)
21476
+ return { kind: "refresh-failed" };
21477
+ const after = await resolvedDeps.loadTokens(configDir);
21478
+ if (!after.found)
21479
+ return { kind: "refresh-failed" };
21480
+ if (after.tokens.accessToken !== renewed.key) {
21481
+ return { kind: "refresh-failed" };
21482
+ }
21483
+ return { kind: "session", session: after.tokens };
21484
+ }
21485
+
21486
+ // src/commands/auth/switchWorkspace.ts
21487
+ async function handleSwitchWorkspace(ctx, deps = {}) {
21488
+ const env = deps.env ?? process.env;
21489
+ const refreshStoredSession2 = deps.refreshStoredSession ?? refreshStoredSession;
21490
+ const preferred = env["QAWOLF_WORKSPACE"]?.trim() || env["QAWOLF_ORGANIZATION"]?.trim();
21491
+ if (ctx.ui.mode !== "human" && !preferred) {
21492
+ return { error: authMessages.workspace.nonInteractive };
21493
+ }
21494
+ const stored = await refreshStoredSession2(ctx.configDir, ctx.fs);
21495
+ if (stored.kind === "not-signed-in") {
21496
+ return { error: authMessages.workspace.notSignedIn };
21497
+ }
21498
+ if (stored.kind === "refresh-failed") {
21499
+ return { error: authMessages.workspace.sessionExpired };
21500
+ }
21501
+ ctx.ui.gap();
21502
+ ctx.ui.intro(authMessages.title);
21503
+ const result = await chooseWorkspace(ctx, {
21504
+ session: stored.session,
21505
+ env
21506
+ });
21507
+ const failure = reportWorkspace(ctx, result);
21508
+ if (failure)
21509
+ return failure;
21510
+ ctx.ui.outro(authMessages.outroReady);
21511
+ return;
21512
+ }
21513
+
21514
+ // src/commands/auth/whoamiTeam.ts
21515
+ function reportTeamIdentity(ctx, value, source) {
21516
+ const { team } = value;
21517
+ const teamUrl = team.slug ? new URL("/" + encodeURIComponent(team.slug), ctx.apiBaseUrl).toString() : undefined;
21518
+ if (ctx.ui.mode === "human") {
21519
+ ctx.ui.note(authMessages.whoami.teamNote({ team, teamUrl, source }), authMessages.whoamiAuthenticated);
21520
+ ctx.ui.outro(authMessages.outroReady);
21521
+ } else {
21522
+ ctx.ui.output({
21523
+ authenticated: true,
21524
+ source,
21525
+ team,
21526
+ teamUrl
21527
+ }, authMessages.whoami.authenticatedAs(team.name, source));
21528
+ }
21529
+ }
21530
+
19894
21531
  // src/commands/auth/whoami.ts
19895
21532
  async function handleWhoami(ctx, deps = {}) {
19896
21533
  ctx.ui.gap();
@@ -19932,11 +21569,19 @@ async function handleWhoami(ctx, deps = {}) {
19932
21569
  const { value } = identity;
19933
21570
  if ("user" in value) {
19934
21571
  const { organization, user } = value;
21572
+ const found = value.organizations.flatMap((candidate) => candidate.workspaces.map((workspace) => ({
21573
+ organization: candidate.name,
21574
+ workspace: workspace.name,
21575
+ id: workspace.id
21576
+ }))).find((entry) => entry.id === resolved.workspaceId);
21577
+ const activeWorkspace = resolved.workspaceId ? authMessages.whoami.activeWorkspace(resolved.workspaceId, found) : undefined;
19935
21578
  if (ctx.ui.mode === "human") {
19936
21579
  ctx.ui.note(authMessages.whoami.userNote({
19937
21580
  organization,
19938
21581
  source: resolved.source,
19939
- user
21582
+ user,
21583
+ activeWorkspace,
21584
+ organizations: value.organizations
19940
21585
  }), authMessages.whoamiAuthenticated);
19941
21586
  ctx.ui.outro(authMessages.outroReady);
19942
21587
  } else {
@@ -19944,7 +21589,9 @@ async function handleWhoami(ctx, deps = {}) {
19944
21589
  authenticated: true,
19945
21590
  organization,
19946
21591
  source: resolved.source,
19947
- user
21592
+ user,
21593
+ workspaceId: resolved.workspaceId,
21594
+ organizations: value.organizations
19948
21595
  }, authMessages.whoami.authenticatedAs(user.email, resolved.source));
19949
21596
  }
19950
21597
  return;
@@ -19966,26 +21613,15 @@ async function handleWhoami(ctx, deps = {}) {
19966
21613
  }
19967
21614
  return;
19968
21615
  }
19969
- const { team } = value;
19970
- const teamUrl = team.slug ? new URL("/" + encodeURIComponent(team.slug), ctx.apiBaseUrl).toString() : undefined;
19971
- if (ctx.ui.mode === "human") {
19972
- ctx.ui.note(authMessages.whoami.teamNote({ team, teamUrl, source: resolved.source }), authMessages.whoamiAuthenticated);
19973
- ctx.ui.outro(authMessages.outroReady);
19974
- } else {
19975
- ctx.ui.output({
19976
- authenticated: true,
19977
- source: resolved.source,
19978
- team,
19979
- teamUrl
19980
- }, authMessages.whoami.authenticatedAs(team.name, resolved.source));
19981
- }
21616
+ reportTeamIdentity(ctx, value, resolved.source);
19982
21617
  }
19983
21618
 
19984
21619
  // src/commands/auth/index.ts
19985
21620
  function registerAuthCommand(program, signals) {
19986
21621
  const auth = program.command("auth").description("Manage authentication with QA Wolf");
19987
- declareCommandKind(auth.command("login"), "local").description("Authenticate with your QA Wolf API key").action(withContext(signals, handleLogin));
21622
+ declareCommandKind(auth.command("login"), "local").description("Authenticate with QA Wolf in a browser or with an API key").action(withContext(signals, handleLogin));
19988
21623
  declareCommandKind(auth.command("logout"), "local").description("Remove stored credentials").action(withContext(signals, handleLogout));
21624
+ declareCommandKind(auth.command("switch"), "local").description("Choose which workspace to work in").action(withContext(signals, handleSwitchWorkspace));
19989
21625
  declareCommandKind(auth.command("whoami"), "read").description("Show authentication status").action(withContext(signals, handleWhoami));
19990
21626
  }
19991
21627
 
@@ -20035,11 +21671,11 @@ async function* batchMap(items, fn, size) {
20035
21671
  }
20036
21672
 
20037
21673
  // src/core/nodeModulesBins.ts
20038
- import { join as join11 } from "node:path";
21674
+ import { join as join15 } from "node:path";
20039
21675
  function nodeModulesBinCandidates(envDir, name, platform) {
20040
- const binDir = join11(envDir, "node_modules", ".bin");
21676
+ const binDir = join15(envDir, "node_modules", ".bin");
20041
21677
  const names = platform === "win32" ? [`${name}.cmd`, `${name}.exe`] : [name];
20042
- return names.map((shim) => join11(binDir, shim));
21678
+ return names.map((shim) => join15(binDir, shim));
20043
21679
  }
20044
21680
  function appiumCliCandidates(envDir, platform) {
20045
21681
  return nodeModulesBinCandidates(envDir, "appium", platform);
@@ -20096,21 +21732,21 @@ async function checkUiautomator2(spawn, appiumBin, platform) {
20096
21732
  }
20097
21733
 
20098
21734
  // src/core/androidBins.ts
20099
- import { join as join12 } from "node:path";
21735
+ import { join as join16 } from "node:path";
20100
21736
  function withExeSuffix(name, platform) {
20101
21737
  return platform === "win32" ? `${name}.exe` : name;
20102
21738
  }
20103
21739
  function emulatorBin(home, platform) {
20104
21740
  const name = withExeSuffix("emulator", platform);
20105
- return home ? join12(home, "emulator", name) : name;
21741
+ return home ? join16(home, "emulator", name) : name;
20106
21742
  }
20107
21743
  function adbBin(home, platform) {
20108
21744
  const name = withExeSuffix("adb", platform);
20109
- return home ? join12(home, "platform-tools", name) : name;
21745
+ return home ? join16(home, "platform-tools", name) : name;
20110
21746
  }
20111
21747
  function cmdlineToolsBin(home, name, platform) {
20112
21748
  const file = platform === "win32" ? `${name}.bat` : name;
20113
- return join12(home, "cmdline-tools", "latest", "bin", file);
21749
+ return join16(home, "cmdline-tools", "latest", "bin", file);
20114
21750
  }
20115
21751
  function sdkManagerBin(home, platform) {
20116
21752
  return cmdlineToolsBin(home, "sdkmanager", platform);
@@ -20407,9 +22043,9 @@ async function checkNpmRegistry(deps) {
20407
22043
  }
20408
22044
 
20409
22045
  // src/core/playwrightCli.ts
20410
- import { join as join13 } from "node:path";
22046
+ import { join as join17 } from "node:path";
20411
22047
  function playwrightCliJsPath(envDir) {
20412
- return join13(envDir, "node_modules", "playwright", "cli.js");
22048
+ return join17(envDir, "node_modules", "playwright", "cli.js");
20413
22049
  }
20414
22050
  function playwrightCliInvocation(args) {
20415
22051
  return {
@@ -20559,11 +22195,11 @@ function renderResults(ui, results) {
20559
22195
  }
20560
22196
 
20561
22197
  // src/domains/flows/ensureDeps.ts
20562
- import { dirname as dirname4, join as join14 } from "node:path";
22198
+ import { dirname as dirname4, join as join18 } from "node:path";
20563
22199
  function findEnvDir(flowPath, fs) {
20564
22200
  let dir = dirname4(flowPath);
20565
22201
  while (true) {
20566
- if (fs.existsSync(join14(dir, "package.json")))
22202
+ if (fs.existsSync(join18(dir, "package.json")))
20567
22203
  return dir;
20568
22204
  const parent = dirname4(dir);
20569
22205
  if (parent === dir)
@@ -20589,7 +22225,7 @@ function resolveProjectDirSafe(files, fs) {
20589
22225
  }
20590
22226
 
20591
22227
  // src/domains/flows/expand.ts
20592
- import { join as join15, resolve as resolve3 } from "node:path";
22228
+ import { join as join19, resolve as resolve3 } from "node:path";
20593
22229
 
20594
22230
  // node_modules/tinyglobby/dist/index.mjs
20595
22231
  import { readdir, readdirSync, realpath, realpathSync, stat, statSync } from "fs";
@@ -21578,11 +23214,11 @@ function makePeekFlowMeta(fs) {
21578
23214
  };
21579
23215
  }
21580
23216
  async function resolveGlobRoots(cwd, fs) {
21581
- const qawolfPath = join15(cwd, ".qawolf");
23217
+ const qawolfPath = join19(cwd, ".qawolf");
21582
23218
  let envDirs = [];
21583
23219
  try {
21584
23220
  const entries = await fs.readdirWithTypes(qawolfPath);
21585
- envDirs = entries.filter((e) => e.isDirectory()).map((e) => join15(qawolfPath, e.name));
23221
+ envDirs = entries.filter((e) => e.isDirectory()).map((e) => join19(qawolfPath, e.name));
21586
23222
  } catch {}
21587
23223
  return [cwd, ...envDirs];
21588
23224
  }
@@ -21608,11 +23244,11 @@ async function expandPatterns(patterns, cwd, logger, fs) {
21608
23244
  }
21609
23245
 
21610
23246
  // src/domains/runtimeEnv/clearRuntimeEnv.ts
21611
- import { join as join17 } from "node:path";
23247
+ import { join as join21 } from "node:path";
21612
23248
 
21613
23249
  // src/domains/runtimeEnv/managedEnvDir.ts
21614
23250
  import { createHash } from "node:crypto";
21615
- import { join as join16, resolve as resolve4 } from "node:path";
23251
+ import { join as join20, resolve as resolve4 } from "node:path";
21616
23252
 
21617
23253
  // src/domains/runtimeEnv/pinnedPackages.ts
21618
23254
  var pinnedPackages = [
@@ -21643,19 +23279,19 @@ function managedEnvBaseDir() {
21643
23279
  const override = process.env["QAWOLF_RUNTIME_DIR"]?.trim();
21644
23280
  if (override)
21645
23281
  return resolve4(override);
21646
- return join16(getDataDir(), "runtime");
23282
+ return join20(getDataDir(), "runtime");
21647
23283
  }
21648
23284
  function runStagingRoot() {
21649
23285
  return `${managedEnvBaseDir()}-runs`;
21650
23286
  }
21651
23287
  function managedEnvDir() {
21652
- return join16(managedEnvBaseDir(), managedEnvHash());
23288
+ return join20(managedEnvBaseDir(), managedEnvHash());
21653
23289
  }
21654
23290
  async function scaffoldManagedEnv(dir, fs) {
21655
23291
  await fs.mkdir(dir, { recursive: true });
21656
23292
  const dependencies = Object.fromEntries(pinnedPackages.map(({ name, version }) => [name, version]));
21657
- await fs.writeFile(join16(dir, "package.json"), JSON.stringify({ name: "qawolf-runtime", private: true, dependencies }, undefined, 2));
21658
- await fs.writeFile(join16(dir, ".npmrc"), `@qawolf:registry=https://registry.npmjs.org/
23293
+ await fs.writeFile(join20(dir, "package.json"), JSON.stringify({ name: "qawolf-runtime", private: true, dependencies }, undefined, 2));
23294
+ await fs.writeFile(join20(dir, ".npmrc"), `@qawolf:registry=https://registry.npmjs.org/
21659
23295
  `);
21660
23296
  }
21661
23297
 
@@ -21677,67 +23313,19 @@ async function isManagedRuntimeBase(dir, fs) {
21677
23313
  for (const entry of entries) {
21678
23314
  if (!entry.isDirectory())
21679
23315
  return false;
21680
- if (!await hasRuntimeSentinel(join17(dir, entry.name), fs))
23316
+ if (!await hasRuntimeSentinel(join21(dir, entry.name), fs))
21681
23317
  return false;
21682
23318
  }
21683
23319
  return true;
21684
23320
  }
21685
23321
  async function hasRuntimeSentinel(hashDir, fs) {
21686
23322
  try {
21687
- const pkg = JSON.parse(await fs.readFile(join17(hashDir, "package.json")));
23323
+ const pkg = JSON.parse(await fs.readFile(join21(hashDir, "package.json")));
21688
23324
  return pkg.name === runtimeSentinel;
21689
23325
  } catch {
21690
23326
  return false;
21691
23327
  }
21692
23328
  }
21693
- // src/shell/spawn.ts
21694
- import { spawn } from "node:child_process";
21695
-
21696
- // src/core/cmdEscape.ts
21697
- var metaChars = /([()\][%!^"`<>&|;, *?])/g;
21698
- function escapeCommand(command) {
21699
- return command.replace(metaChars, "^$1");
21700
- }
21701
- function escapeArgument(arg) {
21702
- const escaped = arg.replace(/(?=(\\+?)?)\1"/g, "$1$1\\\"").replace(/(?=(\\+?)?)\1$/, "$1$1");
21703
- return `"${escaped}"`.replace(metaChars, "^$1").replace(metaChars, "^$1");
21704
- }
21705
-
21706
- // src/shell/spawn.ts
21707
- function buildSpawnCommand(cmd, args, platform, env) {
21708
- const options = {};
21709
- if (env)
21710
- options.env = env;
21711
- if (platform !== "win32" || !/\.(cmd|bat)$/i.test(cmd)) {
21712
- return { cmd, args, options };
21713
- }
21714
- const line = [escapeCommand(cmd), ...args.map(escapeArgument)].join(" ");
21715
- return {
21716
- cmd: process.env["ComSpec"] ?? "cmd.exe",
21717
- args: ["/d", "/s", "/c", `"${line}"`],
21718
- options: { ...options, windowsVerbatimArguments: true }
21719
- };
21720
- }
21721
- var defaultSpawn = (cmd, args, opts) => new Promise((resolve) => {
21722
- const env = opts.env ? { ...process.env, ...opts.env } : undefined;
21723
- const built = buildSpawnCommand(cmd, args, opts.platform, env);
21724
- const child = spawn(built.cmd, built.args, built.options);
21725
- let stdout = "";
21726
- let stderr = "";
21727
- child.stdout?.on("data", (chunk) => {
21728
- stdout += String(chunk);
21729
- });
21730
- child.stderr?.on("data", (chunk) => {
21731
- stderr += String(chunk);
21732
- });
21733
- if (opts?.stdin !== undefined) {
21734
- child.stdin?.write(opts.stdin);
21735
- child.stdin?.end();
21736
- }
21737
- child.on("error", (err) => resolve({ exitCode: -1, stdout, stderr: stderr || err.message }));
21738
- child.on("close", (code) => resolve({ exitCode: code ?? -1, stdout, stderr }));
21739
- });
21740
-
21741
23329
  // src/domains/runtimeEnv/npmInstall.ts
21742
23330
  function buildNpmInstallSpawn(cwd, platform) {
21743
23331
  const built = buildSpawnCommand(resolveNpmCommand(platform), ["install", "--legacy-peer-deps"], platform, undefined);
@@ -21758,10 +23346,10 @@ function spawnNpmInstall(cwd, spawn2 = spawn) {
21758
23346
  }
21759
23347
 
21760
23348
  // src/domains/runtimeEnv/resolvePinned.ts
21761
- import { join as join18 } from "node:path";
23349
+ import { join as join22 } from "node:path";
21762
23350
  function readInstalledVersion(dir, pkgName, fs) {
21763
23351
  try {
21764
- const pkgPath = join18(dir, "node_modules", ...pkgName.split("/"), "package.json");
23352
+ const pkgPath = join22(dir, "node_modules", ...pkgName.split("/"), "package.json");
21765
23353
  const raw = JSON.parse(fs.readFileSync(pkgPath));
21766
23354
  const { version } = raw;
21767
23355
  return typeof version === "string" ? version : undefined;
@@ -21809,10 +23397,10 @@ function allPinnedResolved(dir, fs, platform) {
21809
23397
  }
21810
23398
 
21811
23399
  // src/domains/runtimeEnv/shimDeps.ts
21812
- import { join as join20 } from "node:path";
23400
+ import { join as join24 } from "node:path";
21813
23401
 
21814
23402
  // src/shell/resolveExport.ts
21815
- import { join as join19 } from "node:path";
23403
+ import { join as join23 } from "node:path";
21816
23404
  function pickPath(entry, preference) {
21817
23405
  if (typeof entry === "string")
21818
23406
  return entry;
@@ -21844,28 +23432,28 @@ function resolveFromEnvDir(envDir, specifier, preference = "esm", fs = makeDefau
21844
23432
  const pkgName = parts.slice(0, pkgPartCount).join("/");
21845
23433
  const rest = parts.slice(pkgPartCount);
21846
23434
  const subpath = rest.length > 0 ? "./" + rest.join("/") : ".";
21847
- const pkgDir = join19(envDir, "node_modules", ...pkgName.split("/"));
23435
+ const pkgDir = join23(envDir, "node_modules", ...pkgName.split("/"));
21848
23436
  let pkg;
21849
23437
  try {
21850
- pkg = JSON.parse(fs.readFileSync(join19(pkgDir, "package.json")));
23438
+ pkg = JSON.parse(fs.readFileSync(join23(pkgDir, "package.json")));
21851
23439
  } catch (err) {
21852
23440
  if (isNoEntError(err))
21853
- throw new Error(`Package '${pkgName}' not found in ${join19(envDir, "node_modules")}`, { cause: err });
23441
+ throw new Error(`Package '${pkgName}' not found in ${join23(envDir, "node_modules")}`, { cause: err });
21854
23442
  throw err;
21855
23443
  }
21856
23444
  const exportsMap = normalizeExports(pkg.exports);
21857
23445
  const entry = exportsMap?.[subpath];
21858
23446
  const relative = pickPath(entry, preference) ?? (subpath === "." ? pkg.module ?? pkg.main : undefined);
21859
23447
  if (!relative) {
21860
- throw new Error(`No entry for "${subpath}" in exports of ${join19(pkgDir, "package.json")}`);
23448
+ throw new Error(`No entry for "${subpath}" in exports of ${join23(pkgDir, "package.json")}`);
21861
23449
  }
21862
- return join19(pkgDir, relative);
23450
+ return join23(pkgDir, relative);
21863
23451
  }
21864
23452
 
21865
23453
  // src/domains/runtimeEnv/shimDeps.ts
21866
23454
  function readShimMarker(shimDir, fs) {
21867
23455
  try {
21868
- const pkg = JSON.parse(fs.readFileSync(join20(shimDir, "package.json")));
23456
+ const pkg = JSON.parse(fs.readFileSync(join24(shimDir, "package.json")));
21869
23457
  if (pkg._qawolf_format) {
21870
23458
  return {
21871
23459
  _qawolf_version: pkg._qawolf_version ?? "",
@@ -21881,11 +23469,11 @@ function resolveBunBuild(bunBuild) {
21881
23469
  return bunBuild === false ? undefined : { build: bunBuild };
21882
23470
  }
21883
23471
  async function removeStaleShims(flowsDir, flowsDeps, fs) {
21884
- const shimsDir = join20(flowsDir, "node_modules");
23472
+ const shimsDir = join24(flowsDir, "node_modules");
21885
23473
  if (!fs.existsSync(shimsDir))
21886
23474
  return;
21887
23475
  for (const dep of flowsDeps) {
21888
- const shimDepDir = join20(shimsDir, ...dep.split("/"));
23476
+ const shimDepDir = join24(shimsDir, ...dep.split("/"));
21889
23477
  if (readShimMarker(shimDepDir, fs)) {
21890
23478
  await fs.rm(shimDepDir, { recursive: true, force: true });
21891
23479
  }
@@ -21894,17 +23482,17 @@ async function removeStaleShims(flowsDir, flowsDeps, fs) {
21894
23482
  async function buildDepShim(args) {
21895
23483
  const { envDir, flowsDir, dep, fs, build } = args;
21896
23484
  const depParts = dep.split("/");
21897
- const depDir = join20(envDir, "node_modules", ...depParts);
23485
+ const depDir = join24(envDir, "node_modules", ...depParts);
21898
23486
  if (!fs.existsSync(depDir))
21899
23487
  return;
21900
23488
  let depVersion;
21901
23489
  try {
21902
- const pkg = JSON.parse(fs.readFileSync(join20(depDir, "package.json")));
23490
+ const pkg = JSON.parse(fs.readFileSync(join24(depDir, "package.json")));
21903
23491
  depVersion = pkg.version ?? "unknown";
21904
23492
  } catch {
21905
23493
  depVersion = "unknown";
21906
23494
  }
21907
- const shimDir = join20(flowsDir, "node_modules", ...depParts);
23495
+ const shimDir = join24(flowsDir, "node_modules", ...depParts);
21908
23496
  if (fs.existsSync(shimDir)) {
21909
23497
  const marker = readShimMarker(shimDir, fs);
21910
23498
  if (!marker)
@@ -21931,20 +23519,20 @@ async function buildDepShim(args) {
21931
23519
  }
21932
23520
  const shimCode = await output.text();
21933
23521
  await fs.mkdir(shimDir, { recursive: true });
21934
- await fs.writeFile(join20(shimDir, "package.json"), JSON.stringify({
23522
+ await fs.writeFile(join24(shimDir, "package.json"), JSON.stringify({
21935
23523
  name: dep,
21936
23524
  _qawolf_version: depVersion,
21937
23525
  _qawolf_format: "bun-build-v1"
21938
23526
  }));
21939
- await fs.writeFile(join20(shimDir, "index.js"), shimCode);
23527
+ await fs.writeFile(join24(shimDir, "index.js"), shimCode);
21940
23528
  }
21941
23529
  async function shimFlowsDeps(envDir, fs = makeDefaultFs(), bunBuild) {
21942
- const flowsDir = join20(envDir, "node_modules", "@qawolf", "flows");
23530
+ const flowsDir = join24(envDir, "node_modules", "@qawolf", "flows");
21943
23531
  if (!fs.existsSync(flowsDir))
21944
23532
  return;
21945
23533
  let flowsDeps;
21946
23534
  try {
21947
- const flowsPkg = JSON.parse(fs.readFileSync(join20(flowsDir, "package.json")));
23535
+ const flowsPkg = JSON.parse(fs.readFileSync(join24(flowsDir, "package.json")));
21948
23536
  flowsDeps = Object.keys(flowsPkg.dependencies ?? {});
21949
23537
  } catch {
21950
23538
  return;
@@ -22125,11 +23713,11 @@ function collectValue(value, previous) {
22125
23713
  }
22126
23714
 
22127
23715
  // src/shell/manifest/pulledEnv.ts
22128
- import { join as join22 } from "node:path";
23716
+ import { join as join26 } from "node:path";
22129
23717
 
22130
23718
  // src/shell/manifest/io.ts
22131
23719
  import { createHash as createHash2 } from "node:crypto";
22132
- import { join as join21 } from "node:path";
23720
+ import { join as join25 } from "node:path";
22133
23721
  var manifestFilename = ".manifest.json";
22134
23722
  var flowEntrySchema = object({
22135
23723
  path: string2(),
@@ -22151,7 +23739,7 @@ var manifestSchema = object({
22151
23739
  async function readManifest(envDir, fs = makeDefaultFs()) {
22152
23740
  let raw;
22153
23741
  try {
22154
- raw = await fs.readFile(join21(envDir, manifestFilename));
23742
+ raw = await fs.readFile(join25(envDir, manifestFilename));
22155
23743
  } catch (err) {
22156
23744
  if (isNoEntError(err))
22157
23745
  return "missing";
@@ -22185,7 +23773,7 @@ async function readManifest(envDir, fs = makeDefaultFs()) {
22185
23773
  }
22186
23774
  async function writeManifest(envDir, manifest, fs = makeDefaultFs()) {
22187
23775
  const body = JSON.stringify(manifest, undefined, 2);
22188
- await fs.writeFile(join21(envDir, manifestFilename), `${body}
23776
+ await fs.writeFile(join25(envDir, manifestFilename), `${body}
22189
23777
  `);
22190
23778
  }
22191
23779
  function hashFile(absPath, fs = makeDefaultFs()) {
@@ -22211,7 +23799,7 @@ async function findPulledEnv(ref, cwd, fs = makeDefaultFs()) {
22211
23799
  return;
22212
23800
  }
22213
23801
  async function listPulledEnvDirs(cwd, fs = makeDefaultFs()) {
22214
- const root = join22(cwd, ".qawolf");
23802
+ const root = join26(cwd, ".qawolf");
22215
23803
  let entries;
22216
23804
  try {
22217
23805
  entries = await fs.readdirWithTypes(root);
@@ -22224,7 +23812,7 @@ async function listPulledEnvDirs(cwd, fs = makeDefaultFs()) {
22224
23812
  for (const entry of entries) {
22225
23813
  if (!entry.isDirectory())
22226
23814
  continue;
22227
- const envDir = join22(root, entry.name);
23815
+ const envDir = join26(root, entry.name);
22228
23816
  const manifest = await readManifest(envDir, fs);
22229
23817
  if (typeof manifest === "string")
22230
23818
  continue;
@@ -22278,9 +23866,6 @@ async function readEnvLabels(files, readEnvLabel) {
22278
23866
  }
22279
23867
 
22280
23868
  // src/core/suggestNearMiss.ts
22281
- function normalize3(value) {
22282
- return value.toLowerCase().replace(/[-_\s]+/g, " ").trim();
22283
- }
22284
23869
  function editDistance(a, b) {
22285
23870
  let row = Array.from({ length: b.length + 1 }, (_, j) => j);
22286
23871
  for (let i = 1;i <= a.length; i += 1) {
@@ -22302,14 +23887,14 @@ function suggestNearMiss(input, candidates) {
22302
23887
  if (candidates.includes(input))
22303
23888
  return;
22304
23889
  const sorted = [...candidates].sort();
22305
- const normalizedInput = normalize3(input);
22306
- const presentationMatch = sorted.find((candidate) => normalize3(candidate) === normalizedInput);
23890
+ const normalizedInput = normalizeForSearch(input);
23891
+ const presentationMatch = sorted.find((candidate) => normalizeForSearch(candidate) === normalizedInput);
22307
23892
  if (presentationMatch !== undefined)
22308
23893
  return presentationMatch;
22309
23894
  let best;
22310
23895
  let bestDistance = Number.POSITIVE_INFINITY;
22311
23896
  for (const candidate of sorted) {
22312
- const distance = editDistance(normalizedInput, normalize3(candidate));
23897
+ const distance = editDistance(normalizedInput, normalizeForSearch(candidate));
22313
23898
  if (distance < bestDistance) {
22314
23899
  bestDistance = distance;
22315
23900
  best = candidate;
@@ -22619,7 +24204,7 @@ async function flowsListRemote(ctx, pattern, options) {
22619
24204
  }
22620
24205
 
22621
24206
  // src/domains/flows/pull/handler.ts
22622
- import { dirname as dirname8, join as join27, resolve as resolve8 } from "node:path";
24207
+ import { dirname as dirname8, join as join31, resolve as resolve8 } from "node:path";
22623
24208
 
22624
24209
  // src/domains/flows/pull/fetchPhase.ts
22625
24210
  async function fetchTags(ctx, envId) {
@@ -22763,7 +24348,7 @@ async function checkSafety(args) {
22763
24348
  }
22764
24349
 
22765
24350
  // src/domains/flows/pull/reportPull.ts
22766
- import { join as join23 } from "node:path";
24351
+ import { join as join27 } from "node:path";
22767
24352
  function reportPullResult(ui, args) {
22768
24353
  if (ui.mode !== "json")
22769
24354
  return;
@@ -22778,12 +24363,12 @@ function reportPullResult(ui, args) {
22778
24363
  assetDownloadedCount: args.assets.downloadedCount,
22779
24364
  assetReusedCount: args.assets.reusedCount,
22780
24365
  assetSkippedCount: args.assets.skippedCount,
22781
- manifestPath: join23(args.stage.envDir, manifestFilename)
24366
+ manifestPath: join27(args.stage.envDir, manifestFilename)
22782
24367
  }, "");
22783
24368
  }
22784
24369
 
22785
24370
  // src/domains/flows/pull/bundle.ts
22786
- import { join as join24, relative as relative6 } from "node:path";
24371
+ import { join as join28, relative as relative6 } from "node:path";
22787
24372
  async function flattenSingleWrapper(dir, fs = makeDefaultFs()) {
22788
24373
  const entries = await fs.readdir(dir);
22789
24374
  if (entries.length !== 1)
@@ -22791,12 +24376,12 @@ async function flattenSingleWrapper(dir, fs = makeDefaultFs()) {
22791
24376
  const innerName = entries[0];
22792
24377
  if (!innerName)
22793
24378
  return;
22794
- const inner = join24(dir, innerName);
24379
+ const inner = join28(dir, innerName);
22795
24380
  const innerStat = await fs.stat(inner);
22796
24381
  if (!innerStat.isDirectory())
22797
24382
  return;
22798
24383
  for (const e of await fs.readdir(inner)) {
22799
- await fs.rename(join24(inner, e), join24(dir, e));
24384
+ await fs.rename(join28(inner, e), join28(dir, e));
22800
24385
  }
22801
24386
  const leftovers = await fs.readdir(inner);
22802
24387
  if (leftovers.length > 0) {
@@ -22815,7 +24400,7 @@ async function buildManifest(args, fs = makeDefaultFs()) {
22815
24400
  const flowPaths = await walkForFlows(args.bundleDir, fs);
22816
24401
  const flows = await Promise.all(flowPaths.map(async (rel) => ({
22817
24402
  path: toPosix(rel),
22818
- contentHash: await hashFile(join24(args.bundleDir, rel), fs),
24403
+ contentHash: await hashFile(join28(args.bundleDir, rel), fs),
22819
24404
  tags: args.tags?.byPath.get(toPosix(rel))
22820
24405
  })));
22821
24406
  return {
@@ -22839,7 +24424,7 @@ async function walkForFlows(root, fs) {
22839
24424
  async function walk(current, root, out, fs) {
22840
24425
  const entries = await fs.readdirWithTypes(current);
22841
24426
  for (const e of entries) {
22842
- const abs = join24(current, e.name);
24427
+ const abs = join28(current, e.name);
22843
24428
  if (e.isDirectory()) {
22844
24429
  await walk(abs, root, out, fs);
22845
24430
  } else if (e.isFile() && flowExtensions.some((ext) => e.name.endsWith(ext))) {
@@ -22852,11 +24437,11 @@ async function sampleQawolfCommittedAt(bundleDir, fs = makeDefaultFs()) {
22852
24437
  const sample = flowPaths[0];
22853
24438
  if (!sample)
22854
24439
  return;
22855
- return (await fs.stat(join24(bundleDir, sample))).mtime.toISOString();
24440
+ return (await fs.stat(join28(bundleDir, sample))).mtime.toISOString();
22856
24441
  }
22857
24442
 
22858
24443
  // src/domains/flows/pull/applyTeamStorageRewrite.ts
22859
- import { join as join25, relative as relative7 } from "node:path";
24444
+ import { join as join29, relative as relative7 } from "node:path";
22860
24445
 
22861
24446
  // src/domains/flows/pull/rewriteTeamStorage.ts
22862
24447
  var runnerMountPrefix = "/home/wolf/team-storage/";
@@ -22892,7 +24477,7 @@ function isFlowFile(name) {
22892
24477
  async function walk2(dir, out, fs) {
22893
24478
  const entries = await fs.readdirWithTypes(dir);
22894
24479
  for (const e of entries) {
22895
- const abs = join25(dir, e.name);
24480
+ const abs = join29(dir, e.name);
22896
24481
  if (e.isDirectory()) {
22897
24482
  await walk2(abs, out, fs);
22898
24483
  } else if (e.isFile() && isSourceFile(e.name)) {
@@ -22920,7 +24505,7 @@ async function applyTeamStorageRewrite(rootDir, fs = makeDefaultFs()) {
22920
24505
  }
22921
24506
 
22922
24507
  // src/domains/flows/pull/envVars.ts
22923
- import { join as join26 } from "node:path";
24508
+ import { join as join30 } from "node:path";
22924
24509
 
22925
24510
  // src/core/dotenv.ts
22926
24511
  var bareKeyPattern = /^[A-Za-z_][A-Za-z0-9_]*$/;
@@ -22985,7 +24570,7 @@ function unquote(escaped) {
22985
24570
  async function writeEnvFile(envDir, vars, fs = makeDefaultFs()) {
22986
24571
  if (Object.keys(vars).length === 0)
22987
24572
  return;
22988
- await fs.writeFile(join26(envDir, ".env"), serializeDotenv(vars), {
24573
+ await fs.writeFile(join30(envDir, ".env"), serializeDotenv(vars), {
22989
24574
  mode: 384
22990
24575
  });
22991
24576
  }
@@ -23049,11 +24634,11 @@ var Ae = Symbol("flush");
23049
24634
  var As = Symbol("flushChunk");
23050
24635
  var z = Symbol("encoding");
23051
24636
  var Mt = Symbol("decoder");
23052
- var g = Symbol("flowing");
24637
+ var g2 = Symbol("flowing");
23053
24638
  var Qt = Symbol("paused");
23054
24639
  var Bt = Symbol("resume");
23055
24640
  var b = Symbol("buffer");
23056
- var N = Symbol("pipes");
24641
+ var N2 = Symbol("pipes");
23057
24642
  var _ = Symbol("bufferLength");
23058
24643
  var bi = Symbol("bufferPush");
23059
24644
  var Ie = Symbol("bufferShift");
@@ -23101,9 +24686,9 @@ var Li = class extends Fe {
23101
24686
  var Xr = (s) => !!s.objectMode;
23102
24687
  var qr = (s) => !s.objectMode && !!s.encoding && s.encoding !== "buffer";
23103
24688
  var A2 = class extends Di {
23104
- [g] = false;
24689
+ [g2] = false;
23105
24690
  [Qt] = false;
23106
- [N] = [];
24691
+ [N2] = [];
23107
24692
  [b] = [];
23108
24693
  [L];
23109
24694
  [z];
@@ -23126,7 +24711,7 @@ var A2 = class extends Di {
23126
24711
  let e = t[0] || {};
23127
24712
  if (super(), e.objectMode && typeof e.encoding == "string")
23128
24713
  throw new TypeError("Encoding and objectMode may not be used together");
23129
- Xr(e) ? (this[L] = true, this[z] = null) : qr(e) ? (this[z] = e.encoding, this[L] = false) : (this[L] = false, this[z] = null), this[Z] = !!e.async, this[Mt] = this[z] ? new Hr(this[z]) : null, e && e.debugExposeBuffer === true && Object.defineProperty(this, "buffer", { get: () => this[b] }), e && e.debugExposePipes === true && Object.defineProperty(this, "pipes", { get: () => this[N] });
24714
+ Xr(e) ? (this[L] = true, this[z] = null) : qr(e) ? (this[z] = e.encoding, this[L] = false) : (this[L] = false, this[z] = null), this[Z] = !!e.async, this[Mt] = this[z] ? new Hr(this[z]) : null, e && e.debugExposeBuffer === true && Object.defineProperty(this, "buffer", { get: () => this[b] }), e && e.debugExposePipes === true && Object.defineProperty(this, "pipes", { get: () => this[N2] });
23130
24715
  let { signal: i } = e;
23131
24716
  i && (this[Jt] = i, i.aborted ? this[xi]() : i.addEventListener("abort", () => this[xi]()));
23132
24717
  }
@@ -23178,7 +24763,7 @@ var A2 = class extends Di {
23178
24763
  else if (typeof t != "string")
23179
24764
  throw new Error("Non-contiguous data written to non-objectMode stream");
23180
24765
  }
23181
- return this[L] ? (this[g] && this[_] !== 0 && this[Ae](true), this[g] ? this.emit("data", t) : this[bi](t), this[_] !== 0 && this.emit("readable"), i && r(i), this[g]) : t.length ? (typeof t == "string" && !(e === this[z] && !this[Mt]?.lastNeed) && (t = Buffer.from(t, e)), Buffer.isBuffer(t) && this[z] && (t = this[Mt].write(t)), this[g] && this[_] !== 0 && this[Ae](true), this[g] ? this.emit("data", t) : this[bi](t), this[_] !== 0 && this.emit("readable"), i && r(i), this[g]) : (this[_] !== 0 && this.emit("readable"), i && r(i), this[g]);
24766
+ return this[L] ? (this[g2] && this[_] !== 0 && this[Ae](true), this[g2] ? this.emit("data", t) : this[bi](t), this[_] !== 0 && this.emit("readable"), i && r(i), this[g2]) : t.length ? (typeof t == "string" && !(e === this[z] && !this[Mt]?.lastNeed) && (t = Buffer.from(t, e)), Buffer.isBuffer(t) && this[z] && (t = this[Mt].write(t)), this[g2] && this[_] !== 0 && this[Ae](true), this[g2] ? this.emit("data", t) : this[bi](t), this[_] !== 0 && this.emit("readable"), i && r(i), this[g2]) : (this[_] !== 0 && this.emit("readable"), i && r(i), this[g2]);
23182
24767
  }
23183
24768
  read(t) {
23184
24769
  if (this[S])
@@ -23199,22 +24784,22 @@ var A2 = class extends Di {
23199
24784
  return this.emit("data", e), !this[b].length && !this[Q] && this.emit("drain"), e;
23200
24785
  }
23201
24786
  end(t, e, i) {
23202
- return typeof t == "function" && (i = t, t = undefined), typeof e == "function" && (i = e, e = "utf8"), t !== undefined && this.write(t, e), i && this.once("end", i), this[Q] = true, this.writable = false, (this[g] || !this[Qt]) && this[J](), this;
24787
+ return typeof t == "function" && (i = t, t = undefined), typeof e == "function" && (i = e, e = "utf8"), t !== undefined && this.write(t, e), i && this.once("end", i), this[Q] = true, this.writable = false, (this[g2] || !this[Qt]) && this[J](), this;
23203
24788
  }
23204
24789
  [Bt]() {
23205
- this[S] || (!this[Rt] && !this[N].length && (this[C2] = true), this[Qt] = false, this[g] = true, this.emit("resume"), this[b].length ? this[Ae]() : this[Q] ? this[J]() : this.emit("drain"));
24790
+ this[S] || (!this[Rt] && !this[N2].length && (this[C2] = true), this[Qt] = false, this[g2] = true, this.emit("resume"), this[b].length ? this[Ae]() : this[Q] ? this[J]() : this.emit("drain"));
23206
24791
  }
23207
24792
  resume() {
23208
24793
  return this[Bt]();
23209
24794
  }
23210
24795
  pause() {
23211
- this[g] = false, this[Qt] = true, this[C2] = false;
24796
+ this[g2] = false, this[Qt] = true, this[C2] = false;
23212
24797
  }
23213
24798
  get destroyed() {
23214
24799
  return this[S];
23215
24800
  }
23216
24801
  get flowing() {
23217
- return this[g];
24802
+ return this[g2];
23218
24803
  }
23219
24804
  get paused() {
23220
24805
  return this[Qt];
@@ -23232,18 +24817,18 @@ var A2 = class extends Di {
23232
24817
  !t && !this[b].length && !this[Q] && this.emit("drain");
23233
24818
  }
23234
24819
  [As](t) {
23235
- return this.emit("data", t), this[g];
24820
+ return this.emit("data", t), this[g2];
23236
24821
  }
23237
24822
  pipe(t, e) {
23238
24823
  if (this[S])
23239
24824
  return t;
23240
24825
  this[C2] = false;
23241
24826
  let i = this[nt];
23242
- return e = e || {}, t === Ds.stdout || t === Ds.stderr ? e.end = false : e.end = e.end !== false, e.proxyErrors = !!e.proxyErrors, i ? e.end && t.end() : (this[N].push(e.proxyErrors ? new Li(this, t, e) : new Fe(this, t, e)), this[Z] ? jt(() => this[Bt]()) : this[Bt]()), t;
24827
+ return e = e || {}, t === Ds.stdout || t === Ds.stderr ? e.end = false : e.end = e.end !== false, e.proxyErrors = !!e.proxyErrors, i ? e.end && t.end() : (this[N2].push(e.proxyErrors ? new Li(this, t, e) : new Fe(this, t, e)), this[Z] ? jt(() => this[Bt]()) : this[Bt]()), t;
23243
24828
  }
23244
24829
  unpipe(t) {
23245
- let e = this[N].find((i) => i.dest === t);
23246
- e && (this[N].length === 1 ? (this[g] && this[Rt] === 0 && (this[g] = false), this[N] = []) : this[N].splice(this[N].indexOf(e), 1), e.unpipe());
24830
+ let e = this[N2].find((i) => i.dest === t);
24831
+ e && (this[N2].length === 1 ? (this[g2] && this[Rt] === 0 && (this[g2] = false), this[N2] = []) : this[N2].splice(this[N2].indexOf(e), 1), e.unpipe());
23247
24832
  }
23248
24833
  addListener(t, e) {
23249
24834
  return this.on(t, e);
@@ -23251,7 +24836,7 @@ var A2 = class extends Di {
23251
24836
  on(t, e) {
23252
24837
  let i = super.on(t, e);
23253
24838
  if (t === "data")
23254
- this[C2] = false, this[Rt]++, !this[N].length && !this[g] && this[Bt]();
24839
+ this[C2] = false, this[Rt]++, !this[N2].length && !this[g2] && this[Bt]();
23255
24840
  else if (t === "readable" && this[_] !== 0)
23256
24841
  super.emit("readable");
23257
24842
  else if (Kr(t) && this[nt])
@@ -23267,11 +24852,11 @@ var A2 = class extends Di {
23267
24852
  }
23268
24853
  off(t, e) {
23269
24854
  let i = super.off(t, e);
23270
- return t === "data" && (this[Rt] = this.listeners("data").length, this[Rt] === 0 && !this[C2] && !this[N].length && (this[g] = false)), i;
24855
+ return t === "data" && (this[Rt] = this.listeners("data").length, this[Rt] === 0 && !this[C2] && !this[N2].length && (this[g2] = false)), i;
23271
24856
  }
23272
24857
  removeAllListeners(t) {
23273
24858
  let e = super.removeAllListeners(t);
23274
- return (t === "data" || t === undefined) && (this[Rt] = 0, !this[C2] && !this[N].length && (this[g] = false)), e;
24859
+ return (t === "data" || t === undefined) && (this[Rt] = 0, !this[C2] && !this[N2].length && (this[g2] = false)), e;
23275
24860
  }
23276
24861
  get emittedEnd() {
23277
24862
  return this[nt];
@@ -23307,7 +24892,7 @@ var A2 = class extends Di {
23307
24892
  return this[J](), r;
23308
24893
  }
23309
24894
  [Oi](t) {
23310
- for (let i of this[N])
24895
+ for (let i of this[N2])
23311
24896
  i.dest.write(t) === false && this.pause();
23312
24897
  let e = this[C2] ? false : super.emit("data", t);
23313
24898
  return this[J](), e;
@@ -23319,12 +24904,12 @@ var A2 = class extends Di {
23319
24904
  if (this[Mt]) {
23320
24905
  let e = this[Mt].end();
23321
24906
  if (e) {
23322
- for (let i of this[N])
24907
+ for (let i of this[N2])
23323
24908
  i.dest.write(e);
23324
24909
  this[C2] || super.emit("data", e);
23325
24910
  }
23326
24911
  }
23327
- for (let e of this[N])
24912
+ for (let e of this[N2])
23328
24913
  e.end();
23329
24914
  let t = super.emit("end");
23330
24915
  return this.removeAllListeners("end"), t;
@@ -23400,7 +24985,7 @@ var Jr = I.writev;
23400
24985
  var ht = Symbol("_autoClose");
23401
24986
  var H = Symbol("_close");
23402
24987
  var te = Symbol("_ended");
23403
- var m2 = Symbol("_fd");
24988
+ var m3 = Symbol("_fd");
23404
24989
  var Ni = Symbol("_finished");
23405
24990
  var tt = Symbol("_flags");
23406
24991
  var Ai = Symbol("_flush");
@@ -23427,7 +25012,7 @@ var Me = Symbol("_defaultFlag");
23427
25012
  var bt = Symbol("_errored");
23428
25013
  var _t = class extends A2 {
23429
25014
  [bt] = false;
23430
- [m2];
25015
+ [m3];
23431
25016
  [U2];
23432
25017
  [Ci];
23433
25018
  [j] = false;
@@ -23437,10 +25022,10 @@ var _t = class extends A2 {
23437
25022
  constructor(t, e) {
23438
25023
  if (e = e || {}, super(e), this.readable = true, this.writable = false, typeof t != "string")
23439
25024
  throw new TypeError("path must be a string");
23440
- this[bt] = false, this[m2] = typeof e.fd == "number" ? e.fd : undefined, this[U2] = t, this[Ci] = e.readSize || 16 * 1024 * 1024, this[j] = false, this[Fi] = typeof e.size == "number" ? e.size : 1 / 0, this[ee] = this[Fi], this[ht] = typeof e.autoClose == "boolean" ? e.autoClose : true, typeof this[m2] == "number" ? this[zt]() : this[at]();
25025
+ this[bt] = false, this[m3] = typeof e.fd == "number" ? e.fd : undefined, this[U2] = t, this[Ci] = e.readSize || 16 * 1024 * 1024, this[j] = false, this[Fi] = typeof e.size == "number" ? e.size : 1 / 0, this[ee] = this[Fi], this[ht] = typeof e.autoClose == "boolean" ? e.autoClose : true, typeof this[m3] == "number" ? this[zt]() : this[at]();
23441
25026
  }
23442
25027
  get fd() {
23443
- return this[m2];
25028
+ return this[m3];
23444
25029
  }
23445
25030
  get path() {
23446
25031
  return this[U2];
@@ -23455,7 +25040,7 @@ var _t = class extends A2 {
23455
25040
  I.open(this[U2], "r", (t, e) => this[Ht](t, e));
23456
25041
  }
23457
25042
  [Ht](t, e) {
23458
- t ? this[Ut](t) : (this[m2] = e, this.emit("open", e), this[zt]());
25043
+ t ? this[Ut](t) : (this[m3] = e, this.emit("open", e), this[zt]());
23459
25044
  }
23460
25045
  [vi]() {
23461
25046
  return Buffer.allocUnsafe(Math.min(this[Ci], this[ee]));
@@ -23466,16 +25051,16 @@ var _t = class extends A2 {
23466
25051
  let t = this[vi]();
23467
25052
  if (t.length === 0)
23468
25053
  return process.nextTick(() => this[Ii](null, 0, t));
23469
- I.read(this[m2], t, 0, t.length, null, (e, i, r) => this[Ii](e, i, r));
25054
+ I.read(this[m3], t, 0, t.length, null, (e, i, r) => this[Ii](e, i, r));
23470
25055
  }
23471
25056
  }
23472
25057
  [Ii](t, e, i) {
23473
25058
  this[j] = false, t ? this[Ut](t) : this[ki](e, i) && this[zt]();
23474
25059
  }
23475
25060
  [H]() {
23476
- if (this[ht] && typeof this[m2] == "number") {
23477
- let t = this[m2];
23478
- this[m2] = undefined, I.close(t, (e) => e ? this.emit("error", e) : this.emit("close"));
25061
+ if (this[ht] && typeof this[m3] == "number") {
25062
+ let t = this[m3];
25063
+ this[m3] = undefined, I.close(t, (e) => e ? this.emit("error", e) : this.emit("close"));
23479
25064
  }
23480
25065
  }
23481
25066
  [Ut](t) {
@@ -23491,7 +25076,7 @@ var _t = class extends A2 {
23491
25076
  case "finish":
23492
25077
  return false;
23493
25078
  case "drain":
23494
- return typeof this[m2] == "number" && this[zt](), false;
25079
+ return typeof this[m3] == "number" && this[zt](), false;
23495
25080
  case "error":
23496
25081
  return this[bt] ? false : (this[bt] = true, super.emit(t, ...e));
23497
25082
  default:
@@ -23514,7 +25099,7 @@ var Be = class extends _t {
23514
25099
  if (!this[j]) {
23515
25100
  this[j] = true;
23516
25101
  do {
23517
- let e = this[vi](), i = e.length === 0 ? 0 : I.readSync(this[m2], e, 0, e.length, null);
25102
+ let e = this[vi](), i = e.length === 0 ? 0 : I.readSync(this[m3], e, 0, e.length, null);
23518
25103
  if (!this[ki](i, e))
23519
25104
  break;
23520
25105
  } while (true);
@@ -23526,9 +25111,9 @@ var Be = class extends _t {
23526
25111
  }
23527
25112
  }
23528
25113
  [H]() {
23529
- if (this[ht] && typeof this[m2] == "number") {
23530
- let t = this[m2];
23531
- this[m2] = undefined, I.closeSync(t), this.emit("close");
25114
+ if (this[ht] && typeof this[m3] == "number") {
25115
+ let t = this[m3];
25116
+ this[m3] = undefined, I.closeSync(t), this.emit("close");
23532
25117
  }
23533
25118
  }
23534
25119
  };
@@ -23543,15 +25128,15 @@ var et = class extends Qr {
23543
25128
  [U2];
23544
25129
  [ie];
23545
25130
  [ht];
23546
- [m2];
25131
+ [m3];
23547
25132
  [Me];
23548
25133
  [tt];
23549
25134
  [Ni] = false;
23550
25135
  [ot];
23551
25136
  constructor(t, e) {
23552
- e = e || {}, super(e), this[U2] = t, this[m2] = typeof e.fd == "number" ? e.fd : undefined, this[ie] = e.mode === undefined ? 438 : e.mode, this[ot] = typeof e.start == "number" ? e.start : undefined, this[ht] = typeof e.autoClose == "boolean" ? e.autoClose : true;
25137
+ e = e || {}, super(e), this[U2] = t, this[m3] = typeof e.fd == "number" ? e.fd : undefined, this[ie] = e.mode === undefined ? 438 : e.mode, this[ot] = typeof e.start == "number" ? e.start : undefined, this[ht] = typeof e.autoClose == "boolean" ? e.autoClose : true;
23553
25138
  let i = this[ot] !== undefined ? "r+" : "w";
23554
- this[Me] = e.flags === undefined, this[tt] = e.flags === undefined ? i : e.flags, this[m2] === undefined && this[at]();
25139
+ this[Me] = e.flags === undefined, this[tt] = e.flags === undefined ? i : e.flags, this[m3] === undefined && this[at]();
23555
25140
  }
23556
25141
  emit(t, ...e) {
23557
25142
  if (t === "error") {
@@ -23562,7 +25147,7 @@ var et = class extends Qr {
23562
25147
  return super.emit(t, ...e);
23563
25148
  }
23564
25149
  get fd() {
23565
- return this[m2];
25150
+ return this[m3];
23566
25151
  }
23567
25152
  get path() {
23568
25153
  return this[U2];
@@ -23574,16 +25159,16 @@ var et = class extends Qr {
23574
25159
  I.open(this[U2], this[tt], this[ie], (t, e) => this[Ht](t, e));
23575
25160
  }
23576
25161
  [Ht](t, e) {
23577
- this[Me] && this[tt] === "r+" && t && t.code === "ENOENT" ? (this[tt] = "w", this[at]()) : t ? this[Ut](t) : (this[m2] = e, this.emit("open", e), this[gt] || this[Ai]());
25162
+ this[Me] && this[tt] === "r+" && t && t.code === "ENOENT" ? (this[tt] = "w", this[at]()) : t ? this[Ut](t) : (this[m3] = e, this.emit("open", e), this[gt] || this[Ai]());
23578
25163
  }
23579
25164
  end(t, e) {
23580
- return t && this.write(t, e), this[te] = true, !this[gt] && !this[Y].length && typeof this[m2] == "number" && this[Pt](null, 0), this;
25165
+ return t && this.write(t, e), this[te] = true, !this[gt] && !this[Y].length && typeof this[m3] == "number" && this[Pt](null, 0), this;
23581
25166
  }
23582
25167
  write(t, e) {
23583
- return typeof t == "string" && (t = Buffer.from(t, e)), this[te] ? (this.emit("error", new Error("write() after end()")), false) : this[m2] === undefined || this[gt] || this[Y].length ? (this[Y].push(t), this[ke] = true, false) : (this[gt] = true, this[ve](t), true);
25168
+ return typeof t == "string" && (t = Buffer.from(t, e)), this[te] ? (this.emit("error", new Error("write() after end()")), false) : this[m3] === undefined || this[gt] || this[Y].length ? (this[Y].push(t), this[ke] = true, false) : (this[gt] = true, this[ve](t), true);
23584
25169
  }
23585
25170
  [ve](t) {
23586
- I.write(this[m2], t, 0, t.length, this[ot], (e, i) => this[Pt](e, i));
25171
+ I.write(this[m3], t, 0, t.length, this[ot], (e, i) => this[Pt](e, i));
23587
25172
  }
23588
25173
  [Pt](t, e) {
23589
25174
  t ? this[Ut](t) : (this[ot] !== undefined && typeof e == "number" && (this[ot] += e), this[Y].length ? this[Ai]() : (this[gt] = false, this[te] && !this[Ni] ? (this[Ni] = true, this[H](), this.emit("finish")) : this[ke] && (this[ke] = false, this.emit("drain"))));
@@ -23595,13 +25180,13 @@ var et = class extends Qr {
23595
25180
  this[ve](this[Y].pop());
23596
25181
  else {
23597
25182
  let t = this[Y];
23598
- this[Y] = [], Jr(this[m2], t, this[ot], (e, i) => this[Pt](e, i));
25183
+ this[Y] = [], Jr(this[m3], t, this[ot], (e, i) => this[Pt](e, i));
23599
25184
  }
23600
25185
  }
23601
25186
  [H]() {
23602
- if (this[ht] && typeof this[m2] == "number") {
23603
- let t = this[m2];
23604
- this[m2] = undefined, I.close(t, (e) => e ? this.emit("error", e) : this.emit("close"));
25187
+ if (this[ht] && typeof this[m3] == "number") {
25188
+ let t = this[m3];
25189
+ this[m3] = undefined, I.close(t, (e) => e ? this.emit("error", e) : this.emit("close"));
23605
25190
  }
23606
25191
  }
23607
25192
  };
@@ -23621,15 +25206,15 @@ var Wt = class extends et {
23621
25206
  this[Ht](null, t);
23622
25207
  }
23623
25208
  [H]() {
23624
- if (this[ht] && typeof this[m2] == "number") {
23625
- let t = this[m2];
23626
- this[m2] = undefined, I.closeSync(t), this.emit("close");
25209
+ if (this[ht] && typeof this[m3] == "number") {
25210
+ let t = this[m3];
25211
+ this[m3] = undefined, I.closeSync(t), this.emit("close");
23627
25212
  }
23628
25213
  }
23629
25214
  [ve](t) {
23630
25215
  let e = true;
23631
25216
  try {
23632
- this[Pt](null, I.writeSync(this[m2], t, 0, t.length, this[ot])), e = false;
25217
+ this[Pt](null, I.writeSync(this[m3], t, 0, t.length, this[ot])), e = false;
23633
25218
  } finally {
23634
25219
  if (e)
23635
25220
  try {
@@ -25607,7 +27192,7 @@ var Dr = Symbol("onEntry");
25607
27192
  var _s = Symbol("checkFs");
25608
27193
  var Nr = Symbol("checkFs2");
25609
27194
  var Os = Symbol("isReusable");
25610
- var P = Symbol("makeFs");
27195
+ var P2 = Symbol("makeFs");
25611
27196
  var Ts = Symbol("file");
25612
27197
  var xs = Symbol("directory");
25613
27198
  var gi = Symbol("link");
@@ -25895,23 +27480,23 @@ var Xt = class extends rt {
25895
27480
  return;
25896
27481
  }
25897
27482
  if (h || this[Os](t, a))
25898
- return this[P](null, t, i);
27483
+ return this[P2](null, t, i);
25899
27484
  if (a.isDirectory()) {
25900
27485
  if (t.type === "Directory") {
25901
- let l = this.chmod && t.mode && (a.mode & 4095) !== t.mode, c = (d) => this[P](d ?? null, t, i);
27486
+ let l = this.chmod && t.mode && (a.mode & 4095) !== t.mode, c = (d) => this[P2](d ?? null, t, i);
25902
27487
  return l ? u3.chmod(String(t.absolute), Number(t.mode), c) : c();
25903
27488
  }
25904
27489
  if (t.absolute !== this.cwd)
25905
- return u3.rmdir(String(t.absolute), (l) => this[P](l ?? null, t, i));
27490
+ return u3.rmdir(String(t.absolute), (l) => this[P2](l ?? null, t, i));
25906
27491
  }
25907
27492
  if (t.absolute === this.cwd)
25908
- return this[P](null, t, i);
25909
- mo(String(t.absolute), (l) => this[P](l ?? null, t, i));
27493
+ return this[P2](null, t, i);
27494
+ mo(String(t.absolute), (l) => this[P2](l ?? null, t, i));
25910
27495
  });
25911
27496
  };
25912
27497
  this[Oe] ? n() : r();
25913
27498
  }
25914
- [P](t, e, i) {
27499
+ [P2](t, e, i) {
25915
27500
  if (t) {
25916
27501
  this[O](t, e), i();
25917
27502
  return;
@@ -25945,8 +27530,8 @@ var ye = (s) => {
25945
27530
  };
25946
27531
  var xe = class extends Xt {
25947
27532
  sync = true;
25948
- [P](t, e) {
25949
- return super[P](t, e, () => {});
27533
+ [P2](t, e) {
27534
+ return super[P2](t, e, () => {});
25950
27535
  }
25951
27536
  [_s](t) {
25952
27537
  if (!this[Oe]) {
@@ -25967,19 +27552,19 @@ var xe = class extends Xt {
25967
27552
  if (i && (this.keep || this.newer && i.mtime > (t.mtime ?? i.mtime)))
25968
27553
  return this[Ls](t);
25969
27554
  if (e || this[Os](t, i))
25970
- return this[P](null, t);
27555
+ return this[P2](null, t);
25971
27556
  if (i.isDirectory()) {
25972
27557
  if (t.type === "Directory") {
25973
27558
  let o = this.chmod && t.mode && (i.mode & 4095) !== t.mode, [h] = o ? ye(() => {
25974
27559
  u3.chmodSync(String(t.absolute), Number(t.mode));
25975
27560
  }) : [];
25976
- return this[P](h, t);
27561
+ return this[P2](h, t);
25977
27562
  }
25978
27563
  let [n] = ye(() => u3.rmdirSync(String(t.absolute)));
25979
- this[P](n, t);
27564
+ this[P2](n, t);
25980
27565
  }
25981
27566
  let [r] = t.absolute === this.cwd ? [] : ye(() => po(String(t.absolute)));
25982
- this[P](r, t);
27567
+ this[P2](r, t);
25983
27568
  }
25984
27569
  [Ts](t, e) {
25985
27570
  let i = typeof t.mode == "number" ? t.mode & 4095 : this.fmode, r = (h) => {
@@ -26427,8 +28012,8 @@ async function handleFlowsPull(ctx, opts, deps) {
26427
28012
  ctx.ui.error(validation.error);
26428
28013
  return { error: validation.error };
26429
28014
  }
26430
- const destAbs = resolve8(opts.out ?? join27(".qawolf", opts.env));
26431
- const assetsAbs = join27(dirname8(destAbs), "assets");
28015
+ const destAbs = resolve8(opts.out ?? join31(".qawolf", opts.env));
28016
+ const assetsAbs = join31(dirname8(destAbs), "assets");
26432
28017
  await resolvedDeps.fs.mkdir(assetsAbs, { recursive: true });
26433
28018
  const yes = opts.yes ?? false;
26434
28019
  let archive;
@@ -27303,15 +28888,15 @@ async function defaultRunWebFlowDeps(envDir = process.cwd(), signals) {
27303
28888
 
27304
28889
  // src/domains/runtimeEnv/prepareRunDir.ts
27305
28890
  import { createHash as createHash4 } from "node:crypto";
27306
- import { join as join35, resolve as resolve9 } from "node:path";
28891
+ import { join as join39, resolve as resolve9 } from "node:path";
27307
28892
 
27308
28893
  // src/domains/runtimeEnv/execSubpathImports.ts
27309
- import { join as join28 } from "node:path";
28894
+ import { join as join32 } from "node:path";
27310
28895
  var flowSubpathImports = { "#playwright": "playwright" };
27311
28896
  var packageJsonSchema = looseObject({ imports: record(string2(), unknown()).catch({}) }).catch({ imports: {} });
27312
28897
  async function writeExecSubpathImports(args) {
27313
28898
  const { execDir, fs } = args;
27314
- const pkgPath = join28(execDir, "package.json");
28899
+ const pkgPath = join32(execDir, "package.json");
27315
28900
  const pkg = packageJsonSchema.parse(await readPackageJson(pkgPath, fs));
27316
28901
  const merged = { ...pkg, imports: { ...pkg.imports, ...flowSubpathImports } };
27317
28902
  await fs.writeFile(pkgPath, JSON.stringify(merged, undefined, 2));
@@ -27333,10 +28918,10 @@ async function readPackageJson(pkgPath, fs) {
27333
28918
  }
27334
28919
 
27335
28920
  // src/domains/runtimeEnv/innerHop.ts
27336
- import { join as join30 } from "node:path";
28921
+ import { join as join34 } from "node:path";
27337
28922
 
27338
28923
  // src/domains/runtimeEnv/linkPinnedPackages.ts
27339
- import { dirname as dirname9, join as join29 } from "node:path";
28924
+ import { dirname as dirname9, join as join33 } from "node:path";
27340
28925
 
27341
28926
  // src/domains/runtimeEnv/symlinkDir.ts
27342
28927
  import { symlink } from "node:fs/promises";
@@ -27348,16 +28933,16 @@ async function createDirSymlink(source, target) {
27348
28933
  // src/domains/runtimeEnv/linkPinnedPackages.ts
27349
28934
  async function linkPinnedPackages(args) {
27350
28935
  const { depsRoot, nodeModulesDir, fs } = args;
27351
- const managedModules = join29(depsRoot, "node_modules");
28936
+ const managedModules = join33(depsRoot, "node_modules");
27352
28937
  await fs.mkdir(nodeModulesDir, { recursive: true });
27353
28938
  for (const { name } of pinnedPackages) {
27354
28939
  const segments = name.split("/");
27355
- const target = join29(nodeModulesDir, ...segments);
28940
+ const target = join33(nodeModulesDir, ...segments);
27356
28941
  if (segments.length > 1) {
27357
28942
  await fs.mkdir(dirname9(target), { recursive: true });
27358
28943
  }
27359
28944
  await fs.rm(target, { recursive: true, force: true });
27360
- await createDirSymlink(join29(managedModules, ...segments), target);
28945
+ await createDirSymlink(join33(managedModules, ...segments), target);
27361
28946
  }
27362
28947
  }
27363
28948
 
@@ -27366,18 +28951,18 @@ async function populateInnerHop(args) {
27366
28951
  const { depsRoot, execDir, fs } = args;
27367
28952
  await linkPinnedPackages({
27368
28953
  depsRoot,
27369
- nodeModulesDir: join30(execDir, "node_modules"),
28954
+ nodeModulesDir: join34(execDir, "node_modules"),
27370
28955
  fs
27371
28956
  });
27372
28957
  }
27373
28958
 
27374
28959
  // src/domains/runtimeEnv/outerHop.ts
27375
28960
  import { lstat as lstat2 } from "node:fs/promises";
27376
- import { dirname as dirname11, join as join32 } from "node:path";
28961
+ import { dirname as dirname11, join as join36 } from "node:path";
27377
28962
 
27378
28963
  // src/domains/runtimeEnv/carryOverPackages.ts
27379
28964
  import { lstat } from "node:fs/promises";
27380
- import { dirname as dirname10, join as join31 } from "node:path";
28965
+ import { dirname as dirname10, join as join35 } from "node:path";
27381
28966
 
27382
28967
  // src/domains/runtimeEnv/carryOverPlan.ts
27383
28968
  var executorPackageNames = new Set(pinnedPackages.map((p) => p.name));
@@ -27399,12 +28984,12 @@ async function carryOverPackages(args) {
27399
28984
  const linked = [];
27400
28985
  for (const name of names) {
27401
28986
  const segments = name.split("/");
27402
- const target = join31(nodeModulesDir, ...segments);
28987
+ const target = join35(nodeModulesDir, ...segments);
27403
28988
  if (await pathOccupied(target))
27404
28989
  continue;
27405
28990
  if (segments.length > 1)
27406
28991
  await fs.mkdir(dirname10(target), { recursive: true });
27407
- await createDirSymlink(join31(projectModulesDir, ...segments), target);
28992
+ await createDirSymlink(join35(projectModulesDir, ...segments), target);
27408
28993
  linked.push(name);
27409
28994
  }
27410
28995
  return linked;
@@ -27427,18 +29012,18 @@ async function listPackageNames(dir, fs) {
27427
29012
  const names = [];
27428
29013
  for (const entry of entries) {
27429
29014
  if (!entry.startsWith("@")) {
27430
- if (fs.existsSync(join31(dir, entry, "package.json")))
29015
+ if (fs.existsSync(join35(dir, entry, "package.json")))
27431
29016
  names.push(entry);
27432
29017
  continue;
27433
29018
  }
27434
29019
  let scoped;
27435
29020
  try {
27436
- scoped = await fs.readdir(join31(dir, entry));
29021
+ scoped = await fs.readdir(join35(dir, entry));
27437
29022
  } catch {
27438
29023
  continue;
27439
29024
  }
27440
29025
  for (const inner of scoped) {
27441
- if (fs.existsSync(join31(dir, entry, inner, "package.json"))) {
29026
+ if (fs.existsSync(join35(dir, entry, inner, "package.json"))) {
27442
29027
  names.push(`${entry}/${inner}`);
27443
29028
  }
27444
29029
  }
@@ -27456,7 +29041,7 @@ async function populateOuterHop(args) {
27456
29041
  const depNames = Object.keys(installableDeps);
27457
29042
  const { satisfying, rejected } = await findSatisfyingNodeModulesDir(projectDir, depNames, fs);
27458
29043
  if (satisfying !== undefined) {
27459
- await createDirSymlink(satisfying, join32(runDir, "node_modules"));
29044
+ await createDirSymlink(satisfying, join36(runDir, "node_modules"));
27460
29045
  return { mode: "symlink", nodeModulesDir: satisfying };
27461
29046
  }
27462
29047
  if (depNames.length === 0)
@@ -27465,12 +29050,12 @@ async function populateOuterHop(args) {
27465
29050
  await installOuterDeps(runDir, installableDeps, fs, args.install ?? spawnNpmInstall);
27466
29051
  await linkPinnedPackages({
27467
29052
  depsRoot: args.depsRoot,
27468
- nodeModulesDir: join32(runDir, "node_modules"),
29053
+ nodeModulesDir: join36(runDir, "node_modules"),
27469
29054
  fs
27470
29055
  });
27471
29056
  const carriedOver = await carryOverPackages({
27472
- projectModulesDir: join32(projectDir, "node_modules"),
27473
- nodeModulesDir: join32(runDir, "node_modules"),
29057
+ projectModulesDir: join36(projectDir, "node_modules"),
29058
+ nodeModulesDir: join36(runDir, "node_modules"),
27474
29059
  fs
27475
29060
  });
27476
29061
  return { mode: "install", depCount: depNames.length, rejected, carriedOver };
@@ -27479,10 +29064,10 @@ async function findSatisfyingNodeModulesDir(startDir, depNames, fs) {
27479
29064
  const rejected = [];
27480
29065
  let dir = startDir;
27481
29066
  while (true) {
27482
- const candidate = join32(dir, "node_modules");
29067
+ const candidate = join36(dir, "node_modules");
27483
29068
  const stats = await lstatSafe(candidate);
27484
29069
  if (stats !== undefined && stats.isDirectory() && !stats.isSymbolicLink()) {
27485
- const missing = depNames.filter((name) => !fs.existsSync(join32(candidate, name, "package.json")));
29070
+ const missing = depNames.filter((name) => !fs.existsSync(join36(candidate, name, "package.json")));
27486
29071
  if (missing.length === 0)
27487
29072
  return { satisfying: candidate, rejected };
27488
29073
  rejected.push({ dir: candidate, missing });
@@ -27503,7 +29088,7 @@ async function lstatSafe(path) {
27503
29088
  async function readInstallableDeps(projectDir, fs) {
27504
29089
  let content;
27505
29090
  try {
27506
- content = await fs.readFile(join32(projectDir, "package.json"));
29091
+ content = await fs.readFile(join36(projectDir, "package.json"));
27507
29092
  } catch {
27508
29093
  return {};
27509
29094
  }
@@ -27521,8 +29106,8 @@ async function readInstallableDeps(projectDir, fs) {
27521
29106
  return Object.fromEntries(Object.entries(rawDeps).filter(([name]) => !executorPackageNames2.has(name)).filter((entry) => typeof entry[1] === "string"));
27522
29107
  }
27523
29108
  async function installOuterDeps(runDir, deps, fs, install) {
27524
- await fs.writeFile(join32(runDir, "package.json"), JSON.stringify({ name: "qawolf-run", private: true, dependencies: deps }, undefined, 2));
27525
- await fs.writeFile(join32(runDir, ".npmrc"), `@qawolf:registry=https://registry.npmjs.org/
29109
+ await fs.writeFile(join36(runDir, "package.json"), JSON.stringify({ name: "qawolf-run", private: true, dependencies: deps }, undefined, 2));
29110
+ await fs.writeFile(join36(runDir, ".npmrc"), `@qawolf:registry=https://registry.npmjs.org/
27526
29111
  `);
27527
29112
  const result = await install(runDir);
27528
29113
  if (result.exitCode !== 0) {
@@ -27532,14 +29117,14 @@ async function installOuterDeps(runDir, deps, fs, install) {
27532
29117
 
27533
29118
  // src/domains/runtimeEnv/stageFlowFiles.ts
27534
29119
  import { createHash as createHash3 } from "node:crypto";
27535
- import { basename as basename6, dirname as dirname12, join as join34, sep as sep4 } from "node:path";
29120
+ import { basename as basename6, dirname as dirname12, join as join38, sep as sep4 } from "node:path";
27536
29121
 
27537
29122
  // src/shell/copyDir.ts
27538
29123
  import { cp, readdir as readdir2 } from "node:fs/promises";
27539
- import { basename as basename5, join as join33 } from "node:path";
29124
+ import { basename as basename5, join as join37 } from "node:path";
27540
29125
  async function copyDirExcluding(source, destination, excludedNames) {
27541
29126
  const entries = await readdir2(source, { withFileTypes: true });
27542
- await Promise.all(entries.filter((entry) => !excludedNames.has(entry.name)).map((entry) => cp(join33(source, entry.name), join33(destination, entry.name), {
29127
+ await Promise.all(entries.filter((entry) => !excludedNames.has(entry.name)).map((entry) => cp(join37(source, entry.name), join37(destination, entry.name), {
27543
29128
  recursive: true,
27544
29129
  filter: (path) => !excludedNames.has(basename5(path))
27545
29130
  })));
@@ -27556,14 +29141,14 @@ async function stageFlowFiles(args) {
27556
29141
  if (files.length > 1) {
27557
29142
  return Promise.all(files.map((f) => stageOneFile(f, execDir, fs)));
27558
29143
  }
27559
- await Promise.all(files.map((f) => fs.copyFile(f, join34(execDir, basename6(f)))));
27560
- return files.map((f) => join34(execDir, basename6(f)));
29144
+ await Promise.all(files.map((f) => fs.copyFile(f, join38(execDir, basename6(f)))));
29145
+ return files.map((f) => join38(execDir, basename6(f)));
27561
29146
  }
27562
29147
  async function stageOneFile(file, execDir, fs) {
27563
29148
  const dirHash = createHash3("sha256").update(dirname12(file)).digest("hex").slice(0, 8);
27564
- const subDir = join34(execDir, dirHash);
29149
+ const subDir = join38(execDir, dirHash);
27565
29150
  await fs.mkdir(subDir, { recursive: true });
27566
- const dest = join34(subDir, basename6(file));
29151
+ const dest = join38(subDir, basename6(file));
27567
29152
  await fs.copyFile(file, dest);
27568
29153
  return dest;
27569
29154
  }
@@ -27571,7 +29156,7 @@ function remapPath(file, projectDir, execDir) {
27571
29156
  if (file === projectDir)
27572
29157
  return execDir;
27573
29158
  if (file.startsWith(projectDir + sep4)) {
27574
- return join34(execDir, file.slice(projectDir.length + 1));
29159
+ return join38(execDir, file.slice(projectDir.length + 1));
27575
29160
  }
27576
29161
  return;
27577
29162
  }
@@ -27581,10 +29166,10 @@ async function prepareRunDir(args) {
27581
29166
  const { files, projectDir, depsRoot, runRoot } = args;
27582
29167
  const fs = args.fs ?? makeDefaultFs();
27583
29168
  const runId = buildRunId(projectDir, files);
27584
- const runDir = join35(runRoot, runId);
29169
+ const runDir = join39(runRoot, runId);
27585
29170
  await fs.rm(runDir, { recursive: true, force: true });
27586
29171
  await fs.mkdir(runDir, { recursive: true });
27587
- const execDir = join35(runDir, "exec");
29172
+ const execDir = join39(runDir, "exec");
27588
29173
  await fs.mkdir(execDir, { recursive: true });
27589
29174
  await populateInnerHop({ depsRoot, execDir, fs });
27590
29175
  const stagedFiles = await stageFlowFiles({ files, projectDir, execDir, fs });
@@ -27615,17 +29200,17 @@ function buildRunId(projectDir, files) {
27615
29200
  }
27616
29201
 
27617
29202
  // src/shell/testkit.ts
27618
- import { join as join36 } from "node:path";
29203
+ import { join as join40 } from "node:path";
27619
29204
  function notAvailableLocally(name) {
27620
29205
  throw new Error(runnerMessages.notAvailableLocally(name));
27621
29206
  }
27622
29207
  async function loadSdkDeps(cwd) {
27623
- const testkitDir = join36(cwd, "node_modules", "@qawolf", "testkit");
29208
+ const testkitDir = join40(cwd, "node_modules", "@qawolf", "testkit");
27624
29209
  try {
27625
29210
  const clientMod = await importFromPath(resolveFromEnvDir(cwd, "@qawolf/testkit/client"));
27626
29211
  if (typeof clientMod.createTestkitClient !== "function")
27627
29212
  throw new Error("createTestkitClient is not a function");
27628
- const scopeMod = await importFromPath(join36(testkitDir, "dist", "clientScope.js"));
29213
+ const scopeMod = await importFromPath(join40(testkitDir, "dist", "clientScope.js"));
27629
29214
  if (typeof scopeMod.configureTestkitClient !== "function")
27630
29215
  throw new Error("configureTestkitClient is not a function");
27631
29216
  return {
@@ -27683,9 +29268,9 @@ async function registerLazyEmailsClient(getInbox, cwd, deps) {
27683
29268
  }
27684
29269
 
27685
29270
  // src/commands/flows/envVarDeps.ts
27686
- import { join as join37 } from "node:path";
29271
+ import { join as join41 } from "node:path";
27687
29272
  function envFilePath(envDir) {
27688
- return join37(envDir, ".env");
29273
+ return join41(envDir, ".env");
27689
29274
  }
27690
29275
  async function readEnv(envDir, fs) {
27691
29276
  try {
@@ -27809,7 +29394,7 @@ async function selectRunByTag(args) {
27809
29394
  }
27810
29395
 
27811
29396
  // src/commands/flows/runStagedFlows.ts
27812
- import { join as join41 } from "node:path";
29397
+ import { join as join45 } from "node:path";
27813
29398
 
27814
29399
  // src/shell/appium/createAppiumServer.ts
27815
29400
  import { existsSync as existsSync2 } from "node:fs";
@@ -29115,14 +30700,14 @@ async function runWebFlow({
29115
30700
 
29116
30701
  // src/shell/embeddedWorkerCli.ts
29117
30702
  import { existsSync as existsSync3, mkdirSync as mkdirSync3, readFileSync as readFileSync2, writeFileSync as writeFileSync2 } from "node:fs";
29118
- import { basename as basename7, join as join38 } from "node:path";
30703
+ import { basename as basename7, join as join42 } from "node:path";
29119
30704
  var embeddedCliPathEnv = "QAWOLF_EMBEDDED_CLI_PATH";
29120
30705
  function extractEmbeddedWorkerCli() {
29121
30706
  const assetPath = process.env[embeddedCliPathEnv];
29122
30707
  if (assetPath === undefined || assetPath === "")
29123
30708
  return;
29124
- const dir = join38(getDataDir(), "worker");
29125
- const dest = join38(dir, basename7(assetPath));
30709
+ const dir = join42(getDataDir(), "worker");
30710
+ const dest = join42(dir, basename7(assetPath));
29126
30711
  if (existsSync3(dest))
29127
30712
  return dest;
29128
30713
  mkdirSync3(dir, { recursive: true });
@@ -29360,7 +30945,7 @@ ${styleText3("dim", "─".repeat(width))}
29360
30945
  };
29361
30946
  }
29362
30947
  // src/shell/reporter/createJUnitReporter.ts
29363
- import { join as join39 } from "node:path";
30948
+ import { join as join43 } from "node:path";
29364
30949
 
29365
30950
  // src/shell/reporter/junitXml.ts
29366
30951
  function escapeXml(str) {
@@ -29399,7 +30984,7 @@ ${suites}`;
29399
30984
 
29400
30985
  // src/shell/reporter/createJUnitReporter.ts
29401
30986
  function resolveJUnitOutputPath(junit, outputDir) {
29402
- return typeof junit === "string" && junit.trim() !== "" ? junit : join39(outputDir, "junit-report.xml");
30987
+ return typeof junit === "string" && junit.trim() !== "" ? junit : join43(outputDir, "junit-report.xml");
29403
30988
  }
29404
30989
  function createJUnitReporter(deps) {
29405
30990
  const flows = [];
@@ -29490,11 +31075,11 @@ function buildFlowsRunDeps(args) {
29490
31075
 
29491
31076
  // src/commands/flows/loadEnvFile.ts
29492
31077
  import { readFile } from "node:fs/promises";
29493
- import { join as join40 } from "node:path";
31078
+ import { join as join44 } from "node:path";
29494
31079
  async function loadEnvFile(envDir) {
29495
31080
  let content;
29496
31081
  try {
29497
- content = await readFile(join40(envDir, ".env"), "utf8");
31082
+ content = await readFile(join44(envDir, ".env"), "utf8");
29498
31083
  } catch (err) {
29499
31084
  if (isNoEntError(err))
29500
31085
  return;
@@ -29539,7 +31124,7 @@ async function runStagedFlows(args) {
29539
31124
  for (const candidate of staged.outerHop.rejected) {
29540
31125
  log.debug(runnerMessages.outerHopCandidateRejected(candidate.dir, candidate.missing));
29541
31126
  }
29542
- const ownModules = projectDir === undefined ? undefined : join41(projectDir, "node_modules");
31127
+ const ownModules = projectDir === undefined ? undefined : join45(projectDir, "node_modules");
29543
31128
  const own = staged.outerHop.rejected.find((c) => c.dir === ownModules);
29544
31129
  if (own !== undefined) {
29545
31130
  ctx.ui.warn(runnerMessages.outerHopFallbackNotice(own.dir, own.missing));
@@ -29574,7 +31159,7 @@ async function runStagedFlows(args) {
29574
31159
  }
29575
31160
 
29576
31161
  // src/commands/flows/runDefaults.ts
29577
- function makeDefaultDeps2(fs) {
31162
+ function makeDefaultDeps3(fs) {
29578
31163
  return {
29579
31164
  expandPatterns: (patterns, cwd, logger) => expandPatterns(patterns, cwd, logger, fs),
29580
31165
  resolveDepsRoot: (args) => resolveDepsRoot({ ...args, fs }),
@@ -29586,7 +31171,7 @@ function makeDefaultDeps2(fs) {
29586
31171
  };
29587
31172
  }
29588
31173
  async function handleFlowsRun(ctx, pattern, flags, deps, selectors = { tags: [] }, allEnvs = false) {
29589
- const resolvedDeps = deps ?? makeDefaultDeps2(ctx.fs);
31174
+ const resolvedDeps = deps ?? makeDefaultDeps3(ctx.fs);
29590
31175
  const cwd = process.cwd();
29591
31176
  const expandedFiles = await resolvedDeps.expandPatterns(buildPatternArgs(pattern), cwd, ctx.log("flows"));
29592
31177
  ctx.log("flows").debug(`discovered ${pluralize(expandedFiles.length, "flow")}`);
@@ -29635,7 +31220,7 @@ async function chooseEnv(ctx, files, allEnvs) {
29635
31220
  }
29636
31221
 
29637
31222
  // src/commands/flows/hybridRunDefaults.ts
29638
- import { join as join42, resolve as resolve10 } from "node:path";
31223
+ import { join as join46, resolve as resolve10 } from "node:path";
29639
31224
 
29640
31225
  // src/core/selectFlowFiles.ts
29641
31226
  function selectFlowFiles(args) {
@@ -29740,7 +31325,7 @@ async function handleHybridFlowsRun(ctx, pattern, flags, options = {}) {
29740
31325
  if (validation !== "ok") {
29741
31326
  return { error: validation.error, exitCode: 2 };
29742
31327
  }
29743
- const envDir = resolve10(join42(".qawolf", flags.env));
31328
+ const envDir = resolve10(join46(".qawolf", flags.env));
29744
31329
  const patternArgs = buildPatternArgs(pattern);
29745
31330
  const globFlows = () => resolvedDeps.expandPatterns(patternArgs, envDir, ctx.log("flows"));
29746
31331
  let files = await globFlows();
@@ -29936,12 +31521,12 @@ function registerFlowsCommand(program, signals, deps = { withResolvedEnv }) {
29936
31521
  }
29937
31522
 
29938
31523
  // src/domains/init/init.ts
29939
- import { dirname as dirname14, join as join44, relative as relative9 } from "node:path";
31524
+ import { dirname as dirname14, join as join48, relative as relative9 } from "node:path";
29940
31525
 
29941
31526
  // src/domains/init/ensurePackageJson.ts
29942
- import { join as join43 } from "node:path";
31527
+ import { join as join47 } from "node:path";
29943
31528
  async function ensurePackageJson(ctx, yes, deps) {
29944
- const pkgPath = join43(deps.cwd, "package.json");
31529
+ const pkgPath = join47(deps.cwd, "package.json");
29945
31530
  if (!await deps.fs.pathExists(pkgPath)) {
29946
31531
  const confirmed = await ctx.ui.confirm(initMessages.createPackageJsonPrompt, { yes });
29947
31532
  if (!confirmed.ok || !confirmed.value) {
@@ -30067,9 +31652,9 @@ function makeDefaultInitDeps() {
30067
31652
  async function handleInit(ctx, opts, deps = makeDefaultInitDeps()) {
30068
31653
  ctx.ui.gap();
30069
31654
  ctx.ui.intro(initMessages.title);
30070
- const configPath = join44(deps.cwd, "qawolf.config.ts");
30071
- const flowPath = join44(deps.cwd, "src", "flows", "example.flow.ts");
30072
- const gitignorePath = join44(deps.cwd, ".qawolf", ".gitignore");
31655
+ const configPath = join48(deps.cwd, "qawolf.config.ts");
31656
+ const flowPath = join48(deps.cwd, "src", "flows", "example.flow.ts");
31657
+ const gitignorePath = join48(deps.cwd, ".qawolf", ".gitignore");
30073
31658
  await writeWithPrompt(ctx, configPath, qawolfConfigTs, opts.yes, deps);
30074
31659
  await writeWithPrompt(ctx, flowPath, exampleFlowTs, opts.yes, deps);
30075
31660
  await writeWithPrompt(ctx, gitignorePath, qawolfGitignore, opts.yes, deps);
@@ -30107,7 +31692,7 @@ Examples:
30107
31692
  import { parseExecutionTarget as parseExecutionTarget4 } from "@qawolf/flow-targets";
30108
31693
 
30109
31694
  // src/domains/install/android/avd.ts
30110
- import { join as join45 } from "node:path";
31695
+ import { join as join49 } from "node:path";
30111
31696
  async function installAvds(ctx, specs, deps) {
30112
31697
  const sdkManagerPath = sdkManagerBin(deps.androidHome, deps.platform);
30113
31698
  const avdManagerPath = avdManagerBin(deps.androidHome, deps.platform);
@@ -30117,7 +31702,7 @@ async function installAvds(ctx, specs, deps) {
30117
31702
  if (versionResult.exitCode !== 0) {
30118
31703
  throw new Error(installMessages.android.sdkmanagerNotFound(sdkManagerPath));
30119
31704
  }
30120
- const licenseFile = join45(deps.androidHome, "licenses", "android-sdk-license");
31705
+ const licenseFile = join49(deps.androidHome, "licenses", "android-sdk-license");
30121
31706
  if (deps.checkExists(licenseFile)) {
30122
31707
  ctx.ui.info(installMessages.android.licensesAlreadyAccepted);
30123
31708
  } else {
@@ -30132,7 +31717,7 @@ async function installAvds(ctx, specs, deps) {
30132
31717
  for (const spec of specs) {
30133
31718
  if (installedImages.has(spec.systemImage))
30134
31719
  continue;
30135
- const systemImg = join45(deps.androidHome, ...spec.systemImage.split(";"), "system.img");
31720
+ const systemImg = join49(deps.androidHome, ...spec.systemImage.split(";"), "system.img");
30136
31721
  if (deps.checkExists(systemImg)) {
30137
31722
  ctx.ui.info(installMessages.android.systemImageAlreadyInstalled(spec.systemImage));
30138
31723
  installedImages.add(spec.systemImage);
@@ -30430,42 +32015,6 @@ Examples:
30430
32015
  });
30431
32016
  }
30432
32017
 
30433
- // src/core/publicApi/flagKind.ts
30434
- function flagKind(schema) {
30435
- if (schema.type === "string")
30436
- return { ok: true, kind: "string" };
30437
- if (schema.type === "number" || schema.type === "integer") {
30438
- return { ok: true, kind: "number" };
30439
- }
30440
- if (schema.type === "boolean")
30441
- return { ok: true, kind: "boolean" };
30442
- if (schema.type === "array") {
30443
- if (schema.items?.type !== "string") {
30444
- return {
30445
- ok: false,
30446
- reason: "only string arrays can be expressed as flags"
30447
- };
30448
- }
30449
- return { ok: true, kind: "string-array" };
30450
- }
30451
- if (schema.type === "object") {
30452
- if (schema.properties) {
30453
- return {
30454
- ok: false,
30455
- reason: "nested objects cannot be expressed as flags"
30456
- };
30457
- }
30458
- if (typeof schema.additionalProperties === "object" && schema.additionalProperties.type === "string") {
30459
- return { ok: true, kind: "key-value-record" };
30460
- }
30461
- return { ok: false, reason: "nested objects cannot be expressed as flags" };
30462
- }
30463
- return {
30464
- ok: false,
30465
- reason: `type ${schema.type ?? "unknown"} cannot be expressed as a flag`
30466
- };
30467
- }
30468
-
30469
32018
  // src/core/publicApi/flagNames.ts
30470
32019
  var kebabCase = (segment) => segment.replace(/[A-Z]/g, (char) => `-${char.toLowerCase()}`);
30471
32020
  var capitalize = (segment) => `${segment.slice(0, 1).toUpperCase()}${segment.slice(1)}`;
@@ -30484,108 +32033,6 @@ function flagUsage(path, kind) {
30484
32033
  return `${name} <value>`;
30485
32034
  }
30486
32035
 
30487
- // src/core/publicApi/objectShape.ts
30488
- var asObjectShape = (schema) => schema.type === "object" && schema.properties ? { properties: schema.properties, required: new Set(schema.required) } : undefined;
30489
- function mergeIntersection(members) {
30490
- const shape = { properties: {}, required: new Set };
30491
- for (const member of members) {
30492
- const memberShape = asObjectShape(member);
30493
- if (!memberShape) {
30494
- return {
30495
- ok: false,
30496
- field: "",
30497
- reason: "intersection members must be object schemas"
30498
- };
30499
- }
30500
- for (const [field, fieldSchema] of Object.entries(memberShape.properties)) {
30501
- if (field in shape.properties) {
30502
- return {
30503
- ok: false,
30504
- field,
30505
- reason: "field appears in multiple intersection members"
30506
- };
30507
- }
30508
- shape.properties[field] = fieldSchema;
30509
- if (memberShape.required.has(field))
30510
- shape.required.add(field);
30511
- }
30512
- }
30513
- return { ok: true, shape };
30514
- }
30515
- function mergeUnion(branches) {
30516
- const shapes = [];
30517
- for (const branch of branches) {
30518
- const branchShape = asObjectShape(branch);
30519
- if (!branchShape) {
30520
- return {
30521
- ok: false,
30522
- field: "",
30523
- reason: "union branches must be object schemas"
30524
- };
30525
- }
30526
- shapes.push(branchShape);
30527
- }
30528
- const shape = { properties: {}, required: new Set };
30529
- for (const branchShape of shapes) {
30530
- for (const [field, fieldSchema] of Object.entries(branchShape.properties)) {
30531
- const existing = shape.properties[field];
30532
- if (!existing) {
30533
- shape.properties[field] = { ...fieldSchema };
30534
- continue;
30535
- }
30536
- const kinds = [flagKind(existing), flagKind(fieldSchema)];
30537
- if (kinds[0]?.ok && kinds[1]?.ok) {
30538
- if (kinds[0].kind !== kinds[1].kind) {
30539
- return {
30540
- ok: false,
30541
- field,
30542
- reason: "field has conflicting types across union branches"
30543
- };
30544
- }
30545
- } else if (JSON.stringify({ ...existing, description: undefined }) !== JSON.stringify({ ...fieldSchema, description: undefined })) {
30546
- return {
30547
- ok: false,
30548
- field,
30549
- reason: "field has conflicting object schemas across union branches"
30550
- };
30551
- }
30552
- if (!existing.description && fieldSchema.description) {
30553
- existing.description = fieldSchema.description;
30554
- }
30555
- }
30556
- }
30557
- const discriminator = Object.keys(shape.properties).find((field) => {
30558
- const values = shapes.map((branch) => branch.properties[field]?.const);
30559
- return values.every((value) => typeof value === "string") && new Set(values).size === shapes.length && shapes.every((branch) => branch.required.has(field));
30560
- });
30561
- for (const [field, fieldSchema] of Object.entries(shape.properties)) {
30562
- const occurrences = shapes.filter((branch) => (field in branch.properties));
30563
- if (occurrences.length === shapes.length && occurrences.every((branch) => branch.required.has(field))) {
30564
- shape.required.add(field);
30565
- }
30566
- if (field === discriminator) {
30567
- const values = shapes.map((branch) => branch.properties[field]?.const);
30568
- fieldSchema.description = `One of: ${values.join(", ")}`;
30569
- }
30570
- }
30571
- return { ok: true, shape };
30572
- }
30573
- function toObjectShape(schema) {
30574
- if (schema.allOf)
30575
- return mergeIntersection(schema.allOf);
30576
- const branches = schema.oneOf ?? schema.anyOf;
30577
- if (branches)
30578
- return mergeUnion(branches);
30579
- const shape = asObjectShape(schema);
30580
- if (shape)
30581
- return { ok: true, shape };
30582
- return {
30583
- ok: false,
30584
- field: "",
30585
- reason: "contract input must be an object schema"
30586
- };
30587
- }
30588
-
30589
32036
  // src/core/publicApi/flagSpecs.ts
30590
32037
  function describeFlag(schema) {
30591
32038
  const description = schema.description ?? "";
@@ -31039,7 +32486,7 @@ function describeFailure(failureReason) {
31039
32486
  }
31040
32487
 
31041
32488
  // src/shell/interactiveRunner/collectRunFiles.ts
31042
- import { join as join47 } from "node:path";
32489
+ import { join as join51 } from "node:path";
31043
32490
 
31044
32491
  // src/core/interactiveRunner/getImports.ts
31045
32492
  function getImports(options) {
@@ -31078,7 +32525,7 @@ function isPathAlias(importPath, paths) {
31078
32525
  }
31079
32526
 
31080
32527
  // src/core/interactiveRunner/resolveImportPath.ts
31081
- import { dirname as dirname15, join as join46, normalize as normalize4 } from "node:path/posix";
32528
+ import { dirname as dirname15, join as join50, normalize as normalize3 } from "node:path/posix";
31082
32529
 
31083
32530
  // src/core/interactiveRunner/tsconfigPaths.ts
31084
32531
  function isRecord4(value) {
@@ -31131,7 +32578,7 @@ function candidatesForExplicitExtension(options) {
31131
32578
  }
31132
32579
  function resolveImportPath(options) {
31133
32580
  const aliasTarget = resolvePathAlias(options.importPath, options.tsconfigPaths);
31134
- const resolvedPath = aliasTarget === undefined ? normalize4(join46(dirname15(options.importingFilePath), options.importPath)) : normalize4(aliasTarget);
32581
+ const resolvedPath = aliasTarget === undefined ? normalize3(join50(dirname15(options.importingFilePath), options.importPath)) : normalize3(aliasTarget);
31135
32582
  const hasExplicitExtension = supportedExtensions.some((extension) => options.importPath.endsWith(extension));
31136
32583
  const candidates = hasExplicitExtension ? candidatesForExplicitExtension({
31137
32584
  importPath: options.importPath,
@@ -31155,7 +32602,7 @@ function resolveImportPath(options) {
31155
32602
  var tsconfigPath = "tsconfig.json";
31156
32603
  async function collectRunFiles(options) {
31157
32604
  const held = await listShippablePaths(options);
31158
- const readFile = (path) => options.fs.readFile(join47(options.cwd, path));
32605
+ const readFile = (path) => options.fs.readFile(join51(options.cwd, path));
31159
32606
  const tsconfigContent = held.has(tsconfigPath) ? await readFile(tsconfigPath).catch(() => {
31160
32607
  return;
31161
32608
  }) : undefined;
@@ -31232,13 +32679,13 @@ async function walkImportGraph(options) {
31232
32679
  }
31233
32680
 
31234
32681
  // src/shell/interactiveRunner/makeRunnerId.ts
31235
- import { randomUUID } from "node:crypto";
32682
+ import { randomUUID as randomUUID2 } from "node:crypto";
31236
32683
  function makeRunnerId() {
31237
- return `cli-${randomUUID().replaceAll("-", "").slice(0, 12)}`;
32684
+ return `cli-${randomUUID2().replaceAll("-", "").slice(0, 12)}`;
31238
32685
  }
31239
32686
 
31240
32687
  // src/shell/interactiveRunner/runFilesManifest.ts
31241
- import { join as join48 } from "node:path";
32688
+ import { join as join52 } from "node:path";
31242
32689
  var manifestFileName = "runner-files.json";
31243
32690
  var manifestSchema2 = object({
31244
32691
  files: array(object({ contentHash: string2(), path: string2() })),
@@ -31246,8 +32693,8 @@ var manifestSchema2 = object({
31246
32693
  version: literal(1)
31247
32694
  });
31248
32695
  function makeRunFilesManifestStore(options) {
31249
- const directory = join48(options.cwd, qawolfDir);
31250
- const path = join48(directory, manifestFileName);
32696
+ const directory = join52(options.cwd, qawolfDir);
32697
+ const path = join52(directory, manifestFileName);
31251
32698
  let pendingWrites = 0;
31252
32699
  return {
31253
32700
  async read() {
@@ -31277,7 +32724,7 @@ function parseJson(text) {
31277
32724
  }
31278
32725
 
31279
32726
  // src/shell/interactiveRunner/runnerStore.ts
31280
- import { join as join49 } from "node:path";
32727
+ import { join as join53 } from "node:path";
31281
32728
  var storeFileName = "runner.json";
31282
32729
  var runnersDirName = "runners";
31283
32730
  var storedRunnerSchema = object({
@@ -31296,11 +32743,11 @@ function parseJson2(text) {
31296
32743
  }
31297
32744
  var pendingWrites = 0;
31298
32745
  function makeRunnerStore(options) {
31299
- const directory = join49(options.cwd, qawolfDir);
31300
- const path = join49(directory, storeFileName);
31301
- const runnersDir = join49(directory, runnersDirName);
32746
+ const directory = join53(options.cwd, qawolfDir);
32747
+ const path = join53(directory, storeFileName);
32748
+ const runnersDir = join53(directory, runnersDirName);
31302
32749
  const nextPendingPath = (target) => `${target}.${process.pid}.${++pendingWrites}.tmp`;
31303
- const runnerPath = (runnerId) => join49(runnersDir, `${encodeURIComponent(runnerId)}.json`);
32750
+ const runnerPath = (runnerId) => join53(runnersDir, `${encodeURIComponent(runnerId)}.json`);
31304
32751
  const writeAtomically = async (target, contents) => {
31305
32752
  const pendingPath = nextPendingPath(target);
31306
32753
  await options.fs.writeFile(pendingPath, `${JSON.stringify(contents, undefined, 2)}
@@ -31337,7 +32784,7 @@ function makeRunnerStore(options) {
31337
32784
  readDefaultRunnerId,
31338
32785
  async readRunners() {
31339
32786
  const runners = await Promise.all((await readRunnerFileNames()).map(async (name) => {
31340
- const contents = await options.fs.readFile(join49(runnersDir, name)).catch(() => {
32787
+ const contents = await options.fs.readFile(join53(runnersDir, name)).catch(() => {
31341
32788
  return;
31342
32789
  });
31343
32790
  if (contents === undefined)
@@ -31356,7 +32803,7 @@ function makeRunnerStore(options) {
31356
32803
  await Promise.all(runnerIds.map((runnerId) => options.fs.rm(runnerPath(runnerId), { force: true })));
31357
32804
  const names = await readRunnerFileNames();
31358
32805
  await Promise.all(names.map(async (name) => {
31359
- const runnerFile = join49(runnersDir, name);
32806
+ const runnerFile = join53(runnersDir, name);
31360
32807
  const contents = await options.fs.readFile(runnerFile).catch(() => {
31361
32808
  return;
31362
32809
  });
@@ -31440,7 +32887,7 @@ function registerRunnerHighlightSelectorCommand(runner, signals) {
31440
32887
  }
31441
32888
 
31442
32889
  // src/domains/interactiveRunner/importPackage.ts
31443
- import { join as join50 } from "node:path";
32890
+ import { join as join54 } from "node:path";
31444
32891
 
31445
32892
  // src/core/interactiveRunner/npmDependencies.ts
31446
32893
  var sectionsInPrecedenceOrder = ["dependencies", "devDependencies"];
@@ -31481,7 +32928,7 @@ function readNpmDependencies(packageJsonContent) {
31481
32928
  // src/domains/interactiveRunner/importPackage.ts
31482
32929
  var defaultPackageVersion = "latest";
31483
32930
  async function handleRunnerImportPackage(ctx, options, deps) {
31484
- const content = await deps.readFile(join50(deps.cwd, runPackageJsonPath)).catch(() => {
32931
+ const content = await deps.readFile(join54(deps.cwd, runPackageJsonPath)).catch(() => {
31485
32932
  return;
31486
32933
  });
31487
32934
  if (content === undefined) {
@@ -33188,21 +34635,12 @@ function createProgram({
33188
34635
 
33189
34636
  // src/main.ts
33190
34637
  var signals = createSignalRegistry();
33191
- var forced = false;
33192
- var onSignal = (sig) => () => {
33193
- const code = sig === "SIGINT" ? 130 : 143;
33194
- if (forced)
33195
- process.exit(code);
33196
- forced = true;
33197
- signals.shutdown(sig).finally(() => {
33198
- process.exit(code);
33199
- });
33200
- };
34638
+ var onSignal = createSignalExit({ shutdown: (sig) => signals.shutdown(sig) });
33201
34639
  process.on("SIGINT", onSignal("SIGINT"));
33202
34640
  process.on("SIGTERM", onSignal("SIGTERM"));
33203
34641
  createProgram({ signals }).parseAsync().catch(() => {
33204
34642
  if (process.exitCode === undefined)
33205
34643
  process.exitCode = 1;
33206
- }).finally(() => flushAndExit(typeof process.exitCode === "number" ? process.exitCode : 0));
34644
+ }).finally(() => exitWhenIdle(typeof process.exitCode === "number" ? process.exitCode : 0));
33207
34645
 
33208
- //# debugId=599EFC31E23F426D64756E2164756E21
34646
+ //# debugId=D197CB13AEBF087464756E2164756E21