@qawolf/cli 1.23.0 → 1.25.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",
@@ -8800,7 +8856,8 @@ var lifecycleMessages = {
8800
8856
 
8801
8857
  // src/core/messages/interactiveRunner/list.ts
8802
8858
  var listMessages = {
8803
- noRunners: "This directory has no runner running. Launch one with qawolf runner launch, or pass --runner to address a runner started elsewhere.",
8859
+ noRunners: "Your team has no runner running. Launch one with qawolf runner launch.",
8860
+ noRunnersHere: "This directory has no runner running. Launch one with qawolf runner launch, or drop --here to see the whole team's runners.",
8804
8861
  runnerCount: (count) => count === 1 ? "1 runner" : `${String(count)} runners`,
8805
8862
  title: "Runners"
8806
8863
  };
@@ -9160,43 +9217,30 @@ function resolveHostUrl(env) {
9160
9217
  }
9161
9218
 
9162
9219
  // src/domains/auth/resolve.ts
9163
- import { Entry as Entry3 } from "@napi-rs/keyring";
9164
-
9165
- // src/domains/auth/store/delete.ts
9166
- import { join as join2 } from "node:path";
9167
- import { Entry } from "@napi-rs/keyring";
9168
-
9169
- // src/domains/auth/store/constants.ts
9170
- var service = "qawolf-cli";
9171
- var account = "api-key";
9172
- var credentialsFile = "credentials.json";
9220
+ import { Entry as Entry5 } from "@napi-rs/keyring";
9173
9221
 
9174
- // src/domains/auth/store/delete.ts
9175
- function deleteFromKeychain() {
9176
- try {
9177
- new Entry(service, account).deletePassword();
9178
- return "deleted";
9179
- } catch {
9180
- return "unavailable";
9181
- }
9182
- }
9183
- async function deleteFromFile(configDir, fs) {
9222
+ // src/core/deviceAuth/tokenExpiry.ts
9223
+ function readAccessTokenExpiry(accessToken) {
9224
+ const segments = accessToken.split(".");
9225
+ if (segments.length !== 3)
9226
+ return;
9227
+ const [, payload] = segments;
9228
+ if (!payload)
9229
+ return;
9230
+ let claims;
9184
9231
  try {
9185
- await fs.unlink(join2(configDir, credentialsFile));
9186
- return "deleted";
9232
+ claims = JSON.parse(Buffer.from(payload, "base64url").toString("utf8"));
9187
9233
  } catch {
9188
- return "not-found";
9234
+ return;
9189
9235
  }
9236
+ if (typeof claims !== "object" || claims === null)
9237
+ return;
9238
+ const exp = claims.exp;
9239
+ if (typeof exp !== "number" || !Number.isFinite(exp))
9240
+ return;
9241
+ const expiresAtMs = exp * 1000;
9242
+ return Number.isFinite(expiresAtMs) ? expiresAtMs : undefined;
9190
9243
  }
9191
- async function deleteApiKey(configDir, fs) {
9192
- const [keychain, file] = await Promise.all([
9193
- Promise.resolve(deleteFromKeychain()),
9194
- deleteFromFile(configDir, fs)
9195
- ]);
9196
- return { keychain, file };
9197
- }
9198
- // src/domains/auth/store/load.ts
9199
- import { join as join3 } from "node:path";
9200
9244
 
9201
9245
  // node_modules/zod/v4/core/core.js
9202
9246
  var _a;
@@ -13779,6 +13823,9 @@ var ZodEmail = /* @__PURE__ */ $constructor("ZodEmail", (inst, def) => {
13779
13823
  $ZodEmail.init(inst, def);
13780
13824
  ZodStringFormat.init(inst, def);
13781
13825
  });
13826
+ function email2(params) {
13827
+ return _email(ZodEmail, params);
13828
+ }
13782
13829
  var ZodGUID = /* @__PURE__ */ $constructor("ZodGUID", (inst, def) => {
13783
13830
  $ZodGUID.init(inst, def);
13784
13831
  ZodStringFormat.init(inst, def);
@@ -13842,6 +13889,9 @@ var ZodBase64 = /* @__PURE__ */ $constructor("ZodBase64", (inst, def) => {
13842
13889
  $ZodBase64.init(inst, def);
13843
13890
  ZodStringFormat.init(inst, def);
13844
13891
  });
13892
+ function base642(params) {
13893
+ return _base64(ZodBase64, params);
13894
+ }
13845
13895
  var ZodBase64URL = /* @__PURE__ */ $constructor("ZodBase64URL", (inst, def) => {
13846
13896
  $ZodBase64URL.init(inst, def);
13847
13897
  ZodStringFormat.init(inst, def);
@@ -14417,10 +14467,229 @@ function date4(params) {
14417
14467
 
14418
14468
  // node_modules/zod/v4/classic/external.js
14419
14469
  config(en_default());
14470
+ // src/shell/workos/types.ts
14471
+ var defaultWorkosBaseUrl = "https://api.workos.com";
14472
+ var defaultIntervalSec = 5;
14473
+ var deviceAuthorizationBody = object({
14474
+ device_code: string2().min(1),
14475
+ user_code: string2().min(1),
14476
+ verification_uri: string2().min(1),
14477
+ verification_uri_complete: string2().min(1).optional(),
14478
+ expires_in: number2().int().positive(),
14479
+ interval: number2().int().positive().optional()
14480
+ });
14481
+ var deviceTokenBody = object({
14482
+ access_token: string2().min(1),
14483
+ refresh_token: string2().min(1),
14484
+ user: object({ email: string2().min(1) }),
14485
+ organization_id: string2().min(1).optional()
14486
+ });
14487
+ var oauthErrorBody = object({
14488
+ error: string2().min(1),
14489
+ error_description: string2().min(1).optional()
14490
+ });
14491
+ var authenticationErrorBody = object({
14492
+ code: string2().min(1),
14493
+ message: string2().min(1).optional()
14494
+ });
14495
+
14496
+ // src/shell/workos/send.ts
14497
+ var timeoutMs = 15000;
14498
+ var unexpectedResponse = authErrorMessages.workos.unexpectedResponse;
14499
+ function isTransientStatus(status) {
14500
+ return status >= 500 || status === 429 || status === 408;
14501
+ }
14502
+ async function sendWorkosRequest(url, init, fetchFn) {
14503
+ let response;
14504
+ try {
14505
+ response = await fetchFn(url, {
14506
+ method: "POST",
14507
+ headers: init.headers,
14508
+ body: init.body,
14509
+ redirect: "manual",
14510
+ signal: AbortSignal.timeout(timeoutMs)
14511
+ });
14512
+ } catch (err) {
14513
+ return {
14514
+ kind: "failure",
14515
+ detail: authErrorMessages.workos.unreachable(errorMessage(err)),
14516
+ retryable: true
14517
+ };
14518
+ }
14519
+ if (response.status >= 300 && response.status < 400) {
14520
+ return {
14521
+ kind: "failure",
14522
+ detail: authErrorMessages.workos.redirected,
14523
+ retryable: false
14524
+ };
14525
+ }
14526
+ let json;
14527
+ try {
14528
+ json = await response.json();
14529
+ } catch {
14530
+ return {
14531
+ kind: "failure",
14532
+ detail: authErrorMessages.workos.unexpectedResponseWithStatus(response.status),
14533
+ retryable: response.ok || isTransientStatus(response.status)
14534
+ };
14535
+ }
14536
+ if (response.ok)
14537
+ return { kind: "json", json };
14538
+ if (isTransientStatus(response.status)) {
14539
+ return {
14540
+ kind: "failure",
14541
+ detail: authErrorMessages.workos.unexpectedResponseWithStatus(response.status),
14542
+ retryable: true
14543
+ };
14544
+ }
14545
+ const parsed = oauthErrorBody.safeParse(json);
14546
+ if (parsed.success) {
14547
+ return {
14548
+ kind: "oauth-error",
14549
+ code: parsed.data.error,
14550
+ description: parsed.data.error_description
14551
+ };
14552
+ }
14553
+ const authError = authenticationErrorBody.safeParse(json);
14554
+ if (authError.success) {
14555
+ return {
14556
+ kind: "oauth-error",
14557
+ code: authError.data.code,
14558
+ description: authError.data.message
14559
+ };
14560
+ }
14561
+ return {
14562
+ kind: "failure",
14563
+ detail: authErrorMessages.workos.unexpectedResponseWithStatus(response.status),
14564
+ retryable: false
14565
+ };
14566
+ }
14567
+
14568
+ // src/shell/workos/refreshAccessToken.ts
14569
+ async function refreshAccessToken(refreshToken, organizationId, deps) {
14570
+ const params = new URLSearchParams({
14571
+ grant_type: "refresh_token",
14572
+ refresh_token: refreshToken,
14573
+ client_id: deps.clientId
14574
+ });
14575
+ if (organizationId)
14576
+ params.set("organization_id", organizationId);
14577
+ const outcome = await sendWorkosRequest(`${deps.baseUrl}/user_management/authenticate`, {
14578
+ headers: { "content-type": "application/x-www-form-urlencoded" },
14579
+ body: params.toString()
14580
+ }, deps.fetch);
14581
+ if (outcome.kind === "failure") {
14582
+ return { ok: false, error: outcome.detail, retryable: outcome.retryable };
14583
+ }
14584
+ if (outcome.kind === "oauth-error") {
14585
+ return {
14586
+ ok: false,
14587
+ error: outcome.description ?? outcome.code,
14588
+ retryable: false
14589
+ };
14590
+ }
14591
+ const parsed = deviceTokenBody.safeParse(outcome.json);
14592
+ if (!parsed.success) {
14593
+ return { ok: false, error: unexpectedResponse, retryable: false };
14594
+ }
14595
+ return {
14596
+ ok: true,
14597
+ value: {
14598
+ accessToken: parsed.data.access_token,
14599
+ refreshToken: parsed.data.refresh_token,
14600
+ expiresAt: readAccessTokenExpiry(parsed.data.access_token),
14601
+ email: parsed.data.user.email,
14602
+ organizationId: parsed.data.organization_id
14603
+ }
14604
+ };
14605
+ }
14606
+
14607
+ // src/shell/workos/config.ts
14608
+ function resolveWorkosConfig(publishedClientId) {
14609
+ const clientId = publishedClientId?.trim();
14610
+ if (!clientId)
14611
+ return { configured: false };
14612
+ return { configured: true, clientId, baseUrl: defaultWorkosBaseUrl };
14613
+ }
14614
+
14615
+ // src/domains/auth/store/delete.ts
14616
+ import { join as join2 } from "node:path";
14617
+ import { Entry } from "@napi-rs/keyring";
14618
+
14619
+ // src/domains/auth/store/constants.ts
14620
+ var service = "qawolf-cli";
14621
+ var account = "api-key";
14622
+ var credentialsFile = "credentials.json";
14623
+ var tokensAccount = "oauth-tokens";
14624
+ var tokensFile = "tokens.json";
14625
+
14626
+ // src/domains/auth/store/delete.ts
14627
+ function deleteFromKeychain() {
14628
+ try {
14629
+ new Entry(service, account).deletePassword();
14630
+ return "deleted";
14631
+ } catch {
14632
+ return "unavailable";
14633
+ }
14634
+ }
14635
+ async function deleteFromFile(configDir, fs) {
14636
+ try {
14637
+ await fs.unlink(join2(configDir, credentialsFile));
14638
+ return "deleted";
14639
+ } catch (err) {
14640
+ if (isNoEntError(err))
14641
+ return "not-found";
14642
+ throw err;
14643
+ }
14644
+ }
14645
+ async function deleteApiKey(configDir, fs) {
14646
+ const [keychain, file] = await Promise.all([
14647
+ Promise.resolve(deleteFromKeychain()),
14648
+ deleteFromFile(configDir, fs)
14649
+ ]);
14650
+ return { keychain, file };
14651
+ }
14652
+ // src/domains/auth/store/hasStoredCredentials.ts
14653
+ import { join as join3 } from "node:path";
14654
+ import { Entry as Entry2 } from "@napi-rs/keyring";
14655
+ async function fileExists(path, fs) {
14656
+ try {
14657
+ await fs.readFile(path);
14658
+ return true;
14659
+ } catch (err) {
14660
+ return !isNoEntError(err);
14661
+ }
14662
+ }
14663
+ function keychainHolds(entryAccount) {
14664
+ try {
14665
+ return Boolean(new Entry2(service, entryAccount).getPassword());
14666
+ } catch {
14667
+ return false;
14668
+ }
14669
+ }
14670
+ async function hasStoredCredentials(configDir, fs) {
14671
+ const [apiKeyFile, tokenFile] = await Promise.all([
14672
+ fileExists(join3(configDir, credentialsFile), fs),
14673
+ fileExists(join3(configDir, tokensFile), fs)
14674
+ ]);
14675
+ return apiKeyFile || tokenFile || keychainHolds(account) || keychainHolds(tokensAccount);
14676
+ }
14677
+ // src/domains/auth/store/load.ts
14678
+ import { join as join4 } from "node:path";
14679
+
14420
14680
  // src/domains/auth/store/types.ts
14421
14681
  var credentialsFileSchema = object({
14422
14682
  apiKey: string2().min(1)
14423
14683
  });
14684
+ var oauthTokensSchema = object({
14685
+ accessToken: string2().min(1),
14686
+ refreshToken: string2().min(1),
14687
+ expiresAt: number2().int().optional(),
14688
+ email: string2().min(1),
14689
+ organizationId: string2().min(1).optional(),
14690
+ workspaceId: string2().min(1).optional(),
14691
+ clientId: string2().min(1).optional()
14692
+ });
14424
14693
 
14425
14694
  // src/domains/auth/store/load.ts
14426
14695
  async function loadApiKey(configDir, deps) {
@@ -14434,7 +14703,7 @@ async function loadApiKey(configDir, deps) {
14434
14703
  errors.keychain = errorMessage(err);
14435
14704
  }
14436
14705
  try {
14437
- const content = await deps.fs.readFile(join3(configDir, credentialsFile));
14706
+ const content = await deps.fs.readFile(join4(configDir, credentialsFile));
14438
14707
  const parsed = credentialsFileSchema.safeParse(JSON.parse(content));
14439
14708
  if (parsed.success) {
14440
14709
  return { found: true, key: parsed.data.apiKey, source: "file" };
@@ -14446,16 +14715,16 @@ async function loadApiKey(configDir, deps) {
14446
14715
  return { found: false, errors };
14447
14716
  }
14448
14717
  // src/domains/auth/store/save.ts
14449
- import { join as join4 } from "node:path";
14450
- import { Entry as Entry2 } from "@napi-rs/keyring";
14718
+ import { join as join5 } from "node:path";
14719
+ import { Entry as Entry3 } from "@napi-rs/keyring";
14451
14720
  async function saveToFile(configDir, key, fs) {
14452
14721
  const payload = { apiKey: key };
14453
14722
  await fs.mkdir(configDir, { recursive: true, mode: 448 });
14454
- await fs.writeFile(join4(configDir, credentialsFile), JSON.stringify(payload, undefined, 2), { mode: 384 });
14723
+ await fs.writeFile(join5(configDir, credentialsFile), JSON.stringify(payload, undefined, 2), { mode: 384 });
14455
14724
  }
14456
14725
  async function saveApiKey(configDir, key, fs) {
14457
14726
  try {
14458
- const entry = new Entry2(service, account);
14727
+ const entry = new Entry3(service, account);
14459
14728
  entry.setPassword(key);
14460
14729
  return { stored: "keychain" };
14461
14730
  } catch (err) {
@@ -14463,10 +14732,159 @@ async function saveApiKey(configDir, key, fs) {
14463
14732
  return { stored: "file", keychainError: errorMessage(err) };
14464
14733
  }
14465
14734
  }
14735
+ // src/domains/auth/store/loadTokens.ts
14736
+ import { join as join6 } from "node:path";
14737
+ var invalidPayload = "Invalid stored token format";
14738
+ function parseTokens(raw) {
14739
+ const parsed = oauthTokensSchema.safeParse(JSON.parse(raw));
14740
+ if (!parsed.success)
14741
+ return;
14742
+ return {
14743
+ accessToken: parsed.data.accessToken,
14744
+ refreshToken: parsed.data.refreshToken,
14745
+ expiresAt: parsed.data.expiresAt,
14746
+ email: parsed.data.email,
14747
+ organizationId: parsed.data.organizationId,
14748
+ workspaceId: parsed.data.workspaceId,
14749
+ clientId: parsed.data.clientId
14750
+ };
14751
+ }
14752
+ async function loadTokens(configDir, deps) {
14753
+ const errors = {};
14754
+ try {
14755
+ const raw = new deps.EntryClass(service, tokensAccount).getPassword();
14756
+ if (raw) {
14757
+ const tokens = parseTokens(raw);
14758
+ if (tokens)
14759
+ return { found: true, tokens, source: "keychain" };
14760
+ errors.keychain = invalidPayload;
14761
+ }
14762
+ } catch (err) {
14763
+ errors.keychain = errorMessage(err);
14764
+ }
14765
+ try {
14766
+ const raw = await deps.fs.readFile(join6(configDir, tokensFile));
14767
+ const tokens = parseTokens(raw);
14768
+ if (tokens)
14769
+ return { found: true, tokens, source: "file" };
14770
+ errors.file = invalidPayload;
14771
+ } catch (err) {
14772
+ errors.file = errorMessage(err);
14773
+ }
14774
+ return { found: false, errors };
14775
+ }
14776
+
14777
+ // src/domains/auth/store/saveTokens.ts
14778
+ import { randomUUID } from "node:crypto";
14779
+ import { join as join7 } from "node:path";
14780
+ import { Entry as Entry4 } from "@napi-rs/keyring";
14781
+ async function saveToFile2(configDir, tokens, fs) {
14782
+ await fs.mkdir(configDir, { recursive: true, mode: 448 });
14783
+ const target = join7(configDir, tokensFile);
14784
+ const staging = `${target}.${randomUUID()}.tmp`;
14785
+ try {
14786
+ await fs.writeFile(staging, JSON.stringify(tokens, undefined, 2), {
14787
+ mode: 384
14788
+ });
14789
+ await fs.rename(staging, target);
14790
+ } catch (err) {
14791
+ await fs.unlink(staging).catch(() => {});
14792
+ throw err;
14793
+ }
14794
+ }
14795
+ async function saveTokens(configDir, tokens, fs) {
14796
+ try {
14797
+ new Entry4(service, tokensAccount).setPassword(JSON.stringify(tokens));
14798
+ return { stored: "keychain" };
14799
+ } catch (err) {
14800
+ try {
14801
+ new Entry4(service, tokensAccount).deletePassword();
14802
+ } catch {}
14803
+ await saveToFile2(configDir, tokens, fs);
14804
+ return { stored: "file", keychainError: errorMessage(err) };
14805
+ }
14806
+ }
14807
+
14808
+ // src/domains/auth/resolveOauthToken.ts
14809
+ var expiryMarginMs = 30000;
14810
+ async function resolveOauthToken(configDir, deps) {
14811
+ const stored = await deps.loadTokens(configDir);
14812
+ if (!stored.found)
14813
+ return;
14814
+ const { tokens } = stored;
14815
+ const expiresAt = tokens.expiresAt;
14816
+ const isFresh = expiresAt !== undefined && expiresAt - expiryMarginMs > deps.now();
14817
+ if (isFresh) {
14818
+ return {
14819
+ key: tokens.accessToken,
14820
+ email: tokens.email,
14821
+ workspaceId: tokens.workspaceId
14822
+ };
14823
+ }
14824
+ const refreshed = await deps.refreshTokens({
14825
+ refreshToken: tokens.refreshToken,
14826
+ organizationId: tokens.organizationId,
14827
+ clientId: tokens.clientId
14828
+ });
14829
+ if (!refreshed.ok) {
14830
+ if (refreshed.retryable && expiresAt !== undefined && expiresAt > deps.now()) {
14831
+ return {
14832
+ key: tokens.accessToken,
14833
+ email: tokens.email,
14834
+ workspaceId: tokens.workspaceId
14835
+ };
14836
+ }
14837
+ const current = await deps.loadTokens(configDir);
14838
+ 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()) {
14839
+ return {
14840
+ key: current.tokens.accessToken,
14841
+ email: current.tokens.email,
14842
+ workspaceId: current.tokens.workspaceId
14843
+ };
14844
+ }
14845
+ return;
14846
+ }
14847
+ try {
14848
+ await deps.saveTokens(configDir, {
14849
+ ...refreshed.value,
14850
+ workspaceId: tokens.workspaceId,
14851
+ clientId: tokens.clientId
14852
+ });
14853
+ } catch {}
14854
+ return {
14855
+ key: refreshed.value.accessToken,
14856
+ email: refreshed.value.email,
14857
+ workspaceId: tokens.workspaceId
14858
+ };
14859
+ }
14860
+
14466
14861
  // src/domains/auth/resolve.ts
14862
+ function makeOauthDeps(fs) {
14863
+ return {
14864
+ loadTokens: (configDir) => loadTokens(configDir, { EntryClass: Entry5, fs }),
14865
+ refreshTokens: async ({ refreshToken, organizationId, clientId }) => {
14866
+ const config = resolveWorkosConfig(clientId);
14867
+ if (!config.configured) {
14868
+ return {
14869
+ ok: false,
14870
+ error: authErrorMessages.workos.noClientForSession,
14871
+ retryable: false
14872
+ };
14873
+ }
14874
+ return refreshAccessToken(refreshToken, organizationId, {
14875
+ fetch: globalThis.fetch,
14876
+ baseUrl: config.baseUrl,
14877
+ clientId: config.clientId
14878
+ });
14879
+ },
14880
+ saveTokens: (configDir, tokens) => saveTokens(configDir, tokens, fs),
14881
+ now: () => Date.now()
14882
+ };
14883
+ }
14467
14884
  function makeDefaultDeps(fs) {
14468
14885
  return {
14469
- loadApiKey: (configDir) => loadApiKey(configDir, { EntryClass: Entry3, fs }),
14886
+ loadApiKey: (configDir) => loadApiKey(configDir, { EntryClass: Entry5, fs }),
14887
+ resolveOauth: (configDir) => resolveOauthToken(configDir, makeOauthDeps(fs)),
14470
14888
  env: process.env
14471
14889
  };
14472
14890
  }
@@ -14474,11 +14892,19 @@ async function resolveApiKey(configDir, fs, deps) {
14474
14892
  const resolvedDeps = deps ?? makeDefaultDeps(fs);
14475
14893
  const envKey = resolvedDeps.env["QAWOLF_API_KEY"];
14476
14894
  if (envKey?.trim()) {
14477
- return { key: envKey.trim(), source: "env" };
14895
+ return { key: envKey.trim(), source: "env", workspaceId: undefined };
14478
14896
  }
14479
14897
  const stored = await resolvedDeps.loadApiKey(configDir);
14480
14898
  if (stored.found) {
14481
- return { key: stored.key, source: stored.source };
14899
+ return { key: stored.key, source: stored.source, workspaceId: undefined };
14900
+ }
14901
+ const oauth = await resolvedDeps.resolveOauth(configDir);
14902
+ if (oauth) {
14903
+ return {
14904
+ key: oauth.key,
14905
+ source: "browser",
14906
+ workspaceId: oauth.workspaceId
14907
+ };
14482
14908
  }
14483
14909
  return;
14484
14910
  }
@@ -14500,9 +14926,9 @@ async function validateApiKey(deps) {
14500
14926
  // src/shell/logger.ts
14501
14927
  var import_pino = __toESM(require_pino(), 1);
14502
14928
  import { mkdirSync as mkdirSync2, openSync } from "node:fs";
14503
- import { dirname, join as join5 } from "node:path";
14929
+ import { dirname, join as join8 } from "node:path";
14504
14930
  function defaultLogPath() {
14505
- return join5(envPaths("qawolf", { suffix: "" }).log, "cli.log");
14931
+ return join8(envPaths("qawolf", { suffix: "" }).log, "cli.log");
14506
14932
  }
14507
14933
  function resolveStderrLevel(env, verbose) {
14508
14934
  if (verbose)
@@ -14570,12 +14996,182 @@ function createLoggingSystem(opts) {
14570
14996
  };
14571
14997
  }
14572
14998
 
14573
- // src/shell/platform/parseErrorBody.ts
14574
- var defaultMaxLength = 1024;
14575
- function asRecord(value) {
14576
- return typeof value === "object" && value !== null && !Array.isArray(value) ? value : undefined;
14577
- }
14578
- function asString(value) {
14999
+ // src/core/publicApi/flagKind.ts
15000
+ function flagKind(schema) {
15001
+ if (schema.type === "string")
15002
+ return { ok: true, kind: "string" };
15003
+ if (schema.type === "number" || schema.type === "integer") {
15004
+ return { ok: true, kind: "number" };
15005
+ }
15006
+ if (schema.type === "boolean")
15007
+ return { ok: true, kind: "boolean" };
15008
+ if (schema.type === "array") {
15009
+ if (schema.items?.type !== "string") {
15010
+ return {
15011
+ ok: false,
15012
+ reason: "only string arrays can be expressed as flags"
15013
+ };
15014
+ }
15015
+ return { ok: true, kind: "string-array" };
15016
+ }
15017
+ if (schema.type === "object") {
15018
+ if (schema.properties) {
15019
+ return {
15020
+ ok: false,
15021
+ reason: "nested objects cannot be expressed as flags"
15022
+ };
15023
+ }
15024
+ if (typeof schema.additionalProperties === "object" && schema.additionalProperties.type === "string") {
15025
+ return { ok: true, kind: "key-value-record" };
15026
+ }
15027
+ return { ok: false, reason: "nested objects cannot be expressed as flags" };
15028
+ }
15029
+ return {
15030
+ ok: false,
15031
+ reason: `type ${schema.type ?? "unknown"} cannot be expressed as a flag`
15032
+ };
15033
+ }
15034
+
15035
+ // src/core/publicApi/objectShape.ts
15036
+ var asObjectShape = (schema) => schema.type === "object" && schema.properties ? { properties: schema.properties, required: new Set(schema.required) } : undefined;
15037
+ function mergeIntersection(members) {
15038
+ const shape = { properties: {}, required: new Set };
15039
+ for (const member of members) {
15040
+ const memberShape = asObjectShape(member);
15041
+ if (!memberShape) {
15042
+ return {
15043
+ ok: false,
15044
+ field: "",
15045
+ reason: "intersection members must be object schemas"
15046
+ };
15047
+ }
15048
+ for (const [field, fieldSchema] of Object.entries(memberShape.properties)) {
15049
+ if (field in shape.properties) {
15050
+ return {
15051
+ ok: false,
15052
+ field,
15053
+ reason: "field appears in multiple intersection members"
15054
+ };
15055
+ }
15056
+ shape.properties[field] = fieldSchema;
15057
+ if (memberShape.required.has(field))
15058
+ shape.required.add(field);
15059
+ }
15060
+ }
15061
+ return { ok: true, shape };
15062
+ }
15063
+ function mergeUnion(branches) {
15064
+ const shapes = [];
15065
+ for (const branch of branches) {
15066
+ const branchShape = asObjectShape(branch);
15067
+ if (!branchShape) {
15068
+ return {
15069
+ ok: false,
15070
+ field: "",
15071
+ reason: "union branches must be object schemas"
15072
+ };
15073
+ }
15074
+ shapes.push(branchShape);
15075
+ }
15076
+ const shape = { properties: {}, required: new Set };
15077
+ for (const branchShape of shapes) {
15078
+ for (const [field, fieldSchema] of Object.entries(branchShape.properties)) {
15079
+ const existing = shape.properties[field];
15080
+ if (!existing) {
15081
+ shape.properties[field] = { ...fieldSchema };
15082
+ continue;
15083
+ }
15084
+ const kinds = [flagKind(existing), flagKind(fieldSchema)];
15085
+ if (kinds[0]?.ok && kinds[1]?.ok) {
15086
+ if (kinds[0].kind !== kinds[1].kind) {
15087
+ return {
15088
+ ok: false,
15089
+ field,
15090
+ reason: "field has conflicting types across union branches"
15091
+ };
15092
+ }
15093
+ } else if (JSON.stringify({ ...existing, description: undefined }) !== JSON.stringify({ ...fieldSchema, description: undefined })) {
15094
+ return {
15095
+ ok: false,
15096
+ field,
15097
+ reason: "field has conflicting object schemas across union branches"
15098
+ };
15099
+ }
15100
+ if (!existing.description && fieldSchema.description) {
15101
+ existing.description = fieldSchema.description;
15102
+ }
15103
+ }
15104
+ }
15105
+ const discriminator = Object.keys(shape.properties).find((field) => {
15106
+ const values = shapes.map((branch) => branch.properties[field]?.const);
15107
+ return values.every((value) => typeof value === "string") && new Set(values).size === shapes.length && shapes.every((branch) => branch.required.has(field));
15108
+ });
15109
+ for (const [field, fieldSchema] of Object.entries(shape.properties)) {
15110
+ const occurrences = shapes.filter((branch) => (field in branch.properties));
15111
+ if (occurrences.length === shapes.length && occurrences.every((branch) => branch.required.has(field))) {
15112
+ shape.required.add(field);
15113
+ }
15114
+ if (field === discriminator) {
15115
+ const values = shapes.map((branch) => branch.properties[field]?.const);
15116
+ fieldSchema.description = `One of: ${values.join(", ")}`;
15117
+ }
15118
+ }
15119
+ return { ok: true, shape };
15120
+ }
15121
+ function toObjectShape(schema) {
15122
+ if (schema.allOf)
15123
+ return mergeIntersection(schema.allOf);
15124
+ const branches = schema.oneOf ?? schema.anyOf;
15125
+ if (branches)
15126
+ return mergeUnion(branches);
15127
+ const shape = asObjectShape(schema);
15128
+ if (shape)
15129
+ return { ok: true, shape };
15130
+ return {
15131
+ ok: false,
15132
+ field: "",
15133
+ reason: "contract input must be an object schema"
15134
+ };
15135
+ }
15136
+
15137
+ // src/shell/platform/applyWorkspaceId.ts
15138
+ var acceptsWorkspace = new WeakMap;
15139
+ function takesWorkspaceId(inputSchema) {
15140
+ const cached = acceptsWorkspace.get(inputSchema);
15141
+ if (cached !== undefined)
15142
+ return cached;
15143
+ let accepted = false;
15144
+ try {
15145
+ const jsonSchema = toJSONSchema(inputSchema, {
15146
+ io: "input"
15147
+ });
15148
+ const shape = toObjectShape(jsonSchema);
15149
+ accepted = shape.ok && "workspaceId" in shape.shape.properties;
15150
+ } catch {
15151
+ accepted = false;
15152
+ }
15153
+ acceptsWorkspace.set(inputSchema, accepted);
15154
+ return accepted;
15155
+ }
15156
+ function applyWorkspaceId(inputSchema, input, workspaceId) {
15157
+ if (!workspaceId)
15158
+ return input;
15159
+ if (typeof input !== "object" || input === null)
15160
+ return input;
15161
+ const existing = input.workspaceId;
15162
+ if (existing !== undefined)
15163
+ return input;
15164
+ if (!takesWorkspaceId(inputSchema))
15165
+ return input;
15166
+ return { ...input, workspaceId };
15167
+ }
15168
+
15169
+ // src/shell/platform/parseErrorBody.ts
15170
+ var defaultMaxLength = 1024;
15171
+ function asRecord(value) {
15172
+ return typeof value === "object" && value !== null && !Array.isArray(value) ? value : undefined;
15173
+ }
15174
+ function asString(value) {
14579
15175
  return typeof value === "string" ? value : undefined;
14580
15176
  }
14581
15177
  function extractMessage(parsed) {
@@ -14736,7 +15332,7 @@ function callPublicApi(trpc, contract, input, options) {
14736
15332
  }
14737
15333
  function makeCallPublicApiMethod(trpc, deps, readBackoffMs) {
14738
15334
  return async (contract, input, options) => requestWithRetry({
14739
- call: () => callPublicApi(trpc, contract, input, options),
15335
+ call: () => callPublicApi(trpc, contract, applyWorkspaceId(contract.input, input, deps.workspaceId), options),
14740
15336
  backoffMs: contract.kind === "read" ? readBackoffMs : [],
14741
15337
  describe: (err) => describeRequestError(err, deps.baseUrl, contract.name),
14742
15338
  sleep: deps.sleep
@@ -15639,7 +16235,7 @@ function summarize(error) {
15639
16235
  // src/shell/platform/downloadBundle.ts
15640
16236
  import { randomBytes } from "node:crypto";
15641
16237
  import { tmpdir as tmpdir2 } from "node:os";
15642
- import { join as join6 } from "node:path";
16238
+ import { join as join9 } from "node:path";
15643
16239
 
15644
16240
  // src/shell/platform/fetchSignedUrl.ts
15645
16241
  var defaultStallTimeoutMs = 30000;
@@ -15727,7 +16323,7 @@ async function fetchSignedUrl(args, deps = { fetch: globalThis.fetch }) {
15727
16323
 
15728
16324
  // src/shell/platform/downloadBundle.ts
15729
16325
  async function downloadBundle(signedUrl, deps) {
15730
- const tmpArchive = join6(tmpdir2(), `qawolf-pull-${randomBytes(8).toString("hex")}.tar.gz`);
16326
+ const tmpArchive = join9(tmpdir2(), `qawolf-pull-${randomBytes(8).toString("hex")}.tar.gz`);
15731
16327
  const result = await fetchSignedUrl({ url: signedUrl, dest: tmpArchive }, { fetch: deps.fetch, fs: deps.fs });
15732
16328
  if (!result.ok) {
15733
16329
  await deps.fs.unlink(tmpArchive).catch(() => {});
@@ -15751,6 +16347,44 @@ var agentReplySchema = object({
15751
16347
  text: string2().describe("What the AI said.")
15752
16348
  });
15753
16349
 
16350
+ // node_modules/@qawolf/api-contracts/dist/v1/resource.js
16351
+ function resource(fields, { urlFieldDescription = "Absolute URL." } = {}) {
16352
+ return object({
16353
+ ...fields,
16354
+ url: url().describe(urlFieldDescription)
16355
+ });
16356
+ }
16357
+
16358
+ // node_modules/@qawolf/api-contracts/dist/v1/email/resource.js
16359
+ var emailDirectionSchema = _enum(["inbound", "outbound"]);
16360
+ var namedAddressSchema = object({
16361
+ address: string2().describe("The email address."),
16362
+ name: string2().optional().describe("The display name, when the message carried one.")
16363
+ });
16364
+ var emailUrlFieldDescription = "Absolute URL of the workspace. The inbox opens from the app header.";
16365
+ var makeEmailSummaryFields = () => ({
16366
+ createdAt: exports_iso.datetime({ offset: true }).describe("When QA Wolf received or sent the email."),
16367
+ direction: emailDirectionSchema.describe("inbound for mail the workspace received, outbound for mail it sent."),
16368
+ from: namedAddressSchema,
16369
+ id: string2().min(1).describe("The id of the email. Pass it to email.get to read the body."),
16370
+ subject: string2().optional(),
16371
+ to: array(namedAddressSchema)
16372
+ });
16373
+ var makeEmailResourceSchema = () => resource({
16374
+ ...makeEmailSummaryFields(),
16375
+ attachments: array(object({
16376
+ fileName: string2(),
16377
+ index: number2().int().min(0).describe("The position of the attachment in the email, from 0. Pass it to email.getAttachment when file names repeat."),
16378
+ type: string2().optional().describe("The MIME type.")
16379
+ })).optional().describe("Attachment names, positions, and types. Read the content with email.getAttachment."),
16380
+ bcc: array(namedAddressSchema).optional(),
16381
+ cc: array(namedAddressSchema).optional(),
16382
+ html: string2().optional().describe("The HTML body."),
16383
+ links: array(string2()).describe("The links of the email, in order: HTML anchors, then images, then bare URLs of the plain text body. Follow one to confirm a sign-up or reset a password."),
16384
+ replyTo: array(namedAddressSchema).optional(),
16385
+ text: string2().optional().describe("The plain text body.")
16386
+ }, { urlFieldDescription: emailUrlFieldDescription });
16387
+
15754
16388
  // node_modules/@qawolf/api-contracts/dist/v1/identity/organization.js
15755
16389
  var identityOrganization = object({
15756
16390
  id: string2(),
@@ -15793,6 +16427,10 @@ var identityResponse = union([
15793
16427
  user: never().optional()
15794
16428
  }).transform(({ organization }) => ({ organization }))
15795
16429
  ]);
16430
+ // node_modules/@qawolf/api-contracts/dist/v1/identity/organizations.js
16431
+ var identityOrganizationsResponse = object({
16432
+ organizations: array(identityOrganization)
16433
+ });
15796
16434
  // node_modules/@qawolf/api-contracts/dist/v1/ids.js
15797
16435
  var defaultIdSchemas = {
15798
16436
  aiTask: string2().min(1).describe("The id of the AI task."),
@@ -15806,14 +16444,6 @@ var defaultIdSchemas = {
15806
16444
  workspace: string2().min(1).describe("The id of the workspace.")
15807
16445
  };
15808
16446
 
15809
- // node_modules/@qawolf/api-contracts/dist/v1/resource.js
15810
- function resource(fields, { urlFieldDescription = "Absolute URL." } = {}) {
15811
- return object({
15812
- ...fields,
15813
- url: url().describe(urlFieldDescription)
15814
- });
15815
- }
15816
-
15817
16447
  // node_modules/@qawolf/api-contracts/dist/v1/run/flowSelection.js
15818
16448
  var makeFlowSelectionSchema = (ids) => object({
15819
16449
  flowIds: array(ids.flow).default([]),
@@ -16343,6 +16973,131 @@ var makeAutomateContract = (ids) => {
16343
16973
  };
16344
16974
  };
16345
16975
 
16976
+ // node_modules/@qawolf/api-contracts/dist/v1/pagination.js
16977
+ var makePaginationInputFields = (limits) => ({
16978
+ cursor: string2().min(1).optional().describe("Opaque cursor copied from the previous response's nextCursor. Omit to fetch the first page."),
16979
+ limit: number2().int().min(1).max(limits.maxLimit).default(limits.defaultLimit).describe(`Maximum rows per page. Defaults to ${limits.defaultLimit}, capped at ${limits.maxLimit}.`)
16980
+ });
16981
+ var nextCursorSchema = string2().min(1).optional().describe("Repeat the request with this value as cursor to fetch the next page. Absent when there are no further results.");
16982
+
16983
+ // node_modules/@qawolf/api-contracts/dist/v1/email/find.js
16984
+ var makeFindEmailsContract = (ids) => {
16985
+ const input = object({
16986
+ address: email2().optional().describe("Only emails delivered to this exact address (inbound) or sent from it (outbound). Mail to a plus-suffixed address such as inbox+abc@example.com is found by that full address, not by inbox@example.com."),
16987
+ createdAfter: exports_iso.datetime({ offset: true }).optional().describe("Only include emails received or sent after this instant, e.g. since a flow submitted a sign-up form."),
16988
+ direction: emailDirectionSchema.default("inbound").describe("inbound lists the mail the workspace received: its inbox. outbound lists the mail it sent."),
16989
+ workspaceId: ids.workspace.optional().describe("The workspace whose emails to list. Required when authenticating with an organization or user API key."),
16990
+ ...makePaginationInputFields({ defaultLimit: 20, maxLimit: 50 })
16991
+ });
16992
+ const output = object({
16993
+ emails: array(resource(makeEmailSummaryFields(), {
16994
+ urlFieldDescription: emailUrlFieldDescription
16995
+ })).describe("The workspace's emails, newest first. Bodies are available via email.get."),
16996
+ nextCursor: nextCursorSchema
16997
+ });
16998
+ return {
16999
+ description: "List the workspace's inbox, or its sent mail, newest first. Read a message body with email.get.",
17000
+ input,
17001
+ kind: "read",
17002
+ name: "email.find",
17003
+ output
17004
+ };
17005
+ };
17006
+
17007
+ // node_modules/@qawolf/api-contracts/dist/v1/email/get.js
17008
+ var makeGetEmailContract = (ids) => {
17009
+ const input = object({
17010
+ id: string2().min(1).describe("The id of the email, from email.find."),
17011
+ workspaceId: ids.workspace.optional().describe("The workspace the email belongs to. Required when authenticating with an organization or user API key.")
17012
+ });
17013
+ const output = makeEmailResourceSchema();
17014
+ return {
17015
+ description: "Read one email of the workspace, with its plain text and HTML bodies. Use it to pull a sign-in code or a verification link out of a message.",
17016
+ input,
17017
+ kind: "read",
17018
+ name: "email.get",
17019
+ output
17020
+ };
17021
+ };
17022
+
17023
+ // node_modules/@qawolf/api-contracts/dist/v1/email/getAttachment.js
17024
+ var makeGetEmailAttachmentContract = (ids) => {
17025
+ const input = object({
17026
+ fileName: string2().min(1).optional().describe("The attachment's file name, as email.get lists it. The first attachment with that name is returned."),
17027
+ id: string2().min(1).describe("The id of the email, from email.find."),
17028
+ index: number2().int().min(0).optional().describe("The attachment's position in the email, from 0, as email.get lists it. Use it when file names repeat."),
17029
+ workspaceId: ids.workspace.optional().describe("The workspace the email belongs to. Required when authenticating with an organization or user API key.")
17030
+ }).refine((input) => input.fileName === undefined !== (input.index === undefined), { message: "Provide fileName or index, not both." });
17031
+ const output = resource({
17032
+ contentBase64: string2().describe("The file content, base64 encoded."),
17033
+ fileName: string2(),
17034
+ index: number2().int().min(0).describe("The position of the attachment in the email, from 0."),
17035
+ type: string2().optional().describe("The MIME type.")
17036
+ }, { urlFieldDescription: emailUrlFieldDescription });
17037
+ return {
17038
+ description: "Read one attachment of a workspace email as base64 content, by file name or by position. email.get lists both.",
17039
+ input,
17040
+ kind: "read",
17041
+ name: "email.getAttachment",
17042
+ output
17043
+ };
17044
+ };
17045
+
17046
+ // node_modules/@qawolf/api-contracts/dist/v1/email/listAddresses.js
17047
+ var makeListEmailAddressesContract = (ids) => {
17048
+ const input = object({
17049
+ workspaceId: ids.workspace.optional().describe("The workspace whose inbox addresses to list. Required when authenticating with an organization or user API key."),
17050
+ ...makePaginationInputFields({ defaultLimit: 20, maxLimit: 50 })
17051
+ });
17052
+ const output = object({
17053
+ addresses: array(resource({
17054
+ address: string2().describe("The inbox address. Mail sent to any plus-suffixed form, such as inbox+abc@example.com, lands in the same inbox."),
17055
+ isDefault: boolean2().describe("Whether this is the workspace's default inbox address.")
17056
+ }, { urlFieldDescription: emailUrlFieldDescription })).describe("The workspace's permanent inbox addresses, alphabetical."),
17057
+ nextCursor: nextCursorSchema
17058
+ });
17059
+ return {
17060
+ description: "List the workspace's inbox addresses, alphabetical. A flow can sign up with a plus-suffixed form of any of them, and email.find reads what arrives.",
17061
+ input,
17062
+ kind: "read",
17063
+ name: "email.listAddresses",
17064
+ output
17065
+ };
17066
+ };
17067
+
17068
+ // node_modules/@qawolf/api-contracts/dist/v1/email/send.js
17069
+ var recipientsSchema = array(email2()).max(50);
17070
+ var maxAttachments = 10;
17071
+ var maxEmailAttachmentsBase64Length = 30 * 1024 * 1024;
17072
+ var attachmentsBase64Length = (attachments) => Object.values(attachments).reduce((total, content) => total + content.length, 0);
17073
+ var makeSendEmailContract = (ids) => {
17074
+ const input = object({
17075
+ attachments: record(string2().min(1), base642()).refine((attachments) => Object.keys(attachments).length <= maxAttachments, { message: `Send at most ${maxAttachments} attachments.` }).refine((attachments) => attachmentsBase64Length(attachments) <= maxEmailAttachmentsBase64Length, { message: "Send at most 30 MB of attachments." }).optional().describe("Files to attach, as file name to base64 content, at most 30 MB in total. The MIME type comes from the file name's extension."),
17076
+ bcc: recipientsSchema.optional(),
17077
+ cc: recipientsSchema.optional(),
17078
+ from: email2().optional().describe("The workspace inbox address to send from, or a plus-suffixed form of one. Defaults to the workspace's default inbox address. See email.listAddresses."),
17079
+ html: string2().optional().describe("The HTML body."),
17080
+ inReplyTo: string2().min(1).optional().describe("The id of an email of the workspace to reply to, from email.find. The reply threads under that message."),
17081
+ replyTo: recipientsSchema.optional().describe("Where replies should go instead of the from address."),
17082
+ subject: string2().min(1),
17083
+ text: string2().optional().describe("The plain text body."),
17084
+ to: recipientsSchema.min(1),
17085
+ workspaceId: ids.workspace.optional().describe("The workspace to send from. Required when authenticating with an organization or user API key.")
17086
+ }).refine((email) => email.html || email.text, {
17087
+ message: "Provide text, html, or both."
17088
+ });
17089
+ const output = resource(makeEmailSummaryFields(), {
17090
+ urlFieldDescription: emailUrlFieldDescription
17091
+ });
17092
+ return {
17093
+ description: "Send an email from one of the workspace's inbox addresses, for example to exercise a flow that reacts to incoming mail. Returns the sent email; read it back with email.get.",
17094
+ input,
17095
+ kind: "write",
17096
+ name: "email.send",
17097
+ output
17098
+ };
17099
+ };
17100
+
16346
17101
  // node_modules/@qawolf/api-contracts/dist/v1/environment/deleteVariable.js
16347
17102
  var makeDeleteEnvironmentVariableContract = (ids) => {
16348
17103
  const input = object({
@@ -16361,13 +17116,6 @@ var makeDeleteEnvironmentVariableContract = (ids) => {
16361
17116
  };
16362
17117
  };
16363
17118
 
16364
- // node_modules/@qawolf/api-contracts/dist/v1/pagination.js
16365
- var makePaginationInputFields = (limits) => ({
16366
- cursor: string2().min(1).optional().describe("Opaque cursor copied from the previous response's nextCursor. Omit to fetch the first page."),
16367
- limit: number2().int().min(1).max(limits.maxLimit).default(limits.defaultLimit).describe(`Maximum rows per page. Defaults to ${limits.defaultLimit}, capped at ${limits.maxLimit}.`)
16368
- });
16369
- var nextCursorSchema = string2().min(1).optional().describe("Repeat the request with this value as cursor to fetch the next page. Absent when there are no further results.");
16370
-
16371
17119
  // node_modules/@qawolf/api-contracts/dist/v1/environment/resource.js
16372
17120
  var makeEnvironmentResourceSchema = (ids) => resource({
16373
17121
  alias: string2().optional().describe("The environment's alias: a team-unique slug that team API keys can use in place of the id. Terminated environments have none."),
@@ -16539,15 +17287,27 @@ var makeCreateTagContract = (ids) => {
16539
17287
  };
16540
17288
  };
16541
17289
 
17290
+ // node_modules/@qawolf/api-contracts/dist/v1/flow/skippedFlows.js
17291
+ var makeSkippedFlowsSchema = (ids, { description, reason }) => array(object({
17292
+ flowId: ids.flow,
17293
+ name: string2().describe("The name of the flow."),
17294
+ reason
17295
+ })).describe(description);
17296
+
16542
17297
  // node_modules/@qawolf/api-contracts/dist/v1/flow/addTag.js
16543
17298
  var makeAddTagToFlowsContract = (ids) => {
16544
17299
  const input = object({
16545
17300
  flowIds: array(ids.flow).min(1).max(300).refine((flowIds) => new Set(flowIds).size === flowIds.length, "flowIds must be unique").describe("The flows that receive the tag."),
16546
17301
  tagName: string2().trim().min(1).describe("The name of an existing tag on the caller's team.")
16547
17302
  });
16548
- const output = makeTagResourceSchema();
17303
+ const output = makeTagResourceSchema().extend({
17304
+ skippedFlows: makeSkippedFlowsSchema(ids, {
17305
+ description: "Requested flows that already carried the tag, so nothing changed for them.",
17306
+ reason: _enum(["already-tagged"]).describe('Why the flow was skipped: "already-tagged" means the flow already carried the tag.')
17307
+ })
17308
+ });
16549
17309
  return {
16550
- description: "Assign an existing tag to the selected flows. Create tags with tag.create.",
17310
+ description: "Assign an existing tag to the selected flows. Create tags with tag.create. Flows that already carry the tag are reported in skippedFlows.",
16551
17311
  input,
16552
17312
  kind: "write",
16553
17313
  name: "flow.addTag",
@@ -16587,6 +17347,27 @@ var makeListFlowsContract = (ids) => {
16587
17347
  };
16588
17348
  };
16589
17349
 
17350
+ // node_modules/@qawolf/api-contracts/dist/v1/flow/removeTag.js
17351
+ var makeRemoveTagFromFlowsContract = (ids) => {
17352
+ const input = object({
17353
+ flowIds: array(ids.flow).min(1).max(300).refine((flowIds) => new Set(flowIds).size === flowIds.length, "flowIds must be unique").describe("The flows that lose the tag."),
17354
+ tagName: string2().trim().min(1).describe("The name of an existing tag on the caller's team.")
17355
+ });
17356
+ const output = makeTagResourceSchema().extend({
17357
+ skippedFlows: makeSkippedFlowsSchema(ids, {
17358
+ description: "Requested flows that did not carry the tag, so nothing changed for them.",
17359
+ reason: _enum(["not-tagged"]).describe('Why the flow was skipped: "not-tagged" means the flow did not carry the tag.')
17360
+ })
17361
+ });
17362
+ return {
17363
+ description: "Remove a tag from the selected flows. Succeeds whether or not each flow carried the tag; the flows that did not are reported in skippedFlows.",
17364
+ input,
17365
+ kind: "write",
17366
+ name: "flow.removeTag",
17367
+ output
17368
+ };
17369
+ };
17370
+
16590
17371
  // node_modules/@qawolf/api-contracts/dist/v1/flow/update.js
16591
17372
  var makeUpdateFlowContract = (ids) => {
16592
17373
  const input = object({
@@ -16731,9 +17512,27 @@ var makeCreateIssueContract = (ids) => {
16731
17512
  };
16732
17513
  };
16733
17514
 
16734
- // node_modules/@qawolf/api-contracts/dist/v1/issue/update.js
16735
- var makeUpdateIssueContract = (ids) => {
16736
- const descriptionSchema = string2().describe("The issue description as plain text; each line becomes a paragraph. Markdown is not parsed.");
17515
+ // node_modules/@qawolf/api-contracts/dist/v1/issue/removeFlows.js
17516
+ var makeRemoveFlowsFromIssueContract = (ids) => {
17517
+ const input = object({
17518
+ flowIds: array(ids.flow).min(1).max(300).refine((flowIds) => new Set(flowIds).size === flowIds.length, "flowIds must be unique").describe("The flows the coverage request no longer covers."),
17519
+ issueId: ids.issue
17520
+ });
17521
+ const output = object({
17522
+ issue: makeIssueResourceSchema(ids)
17523
+ });
17524
+ return {
17525
+ description: "Remove flows from a coverage request owned by the caller's team. " + "Flows the request does not cover are left alone. " + "Bug and maintenance reports link to flows through the runs that reproduce them, so their flows cannot be set directly.",
17526
+ input,
17527
+ kind: "write",
17528
+ name: "issue.removeFlows",
17529
+ output
17530
+ };
17531
+ };
17532
+
17533
+ // node_modules/@qawolf/api-contracts/dist/v1/issue/update.js
17534
+ var makeUpdateIssueContract = (ids) => {
17535
+ const descriptionSchema = string2().describe("The issue description as plain text; each line becomes a paragraph. Markdown is not parsed.");
16737
17536
  const nameSchema = string2().trim().min(1).max(255);
16738
17537
  const input = object({
16739
17538
  description: descriptionSchema.optional(),
@@ -17057,6 +17856,22 @@ var makeTerminateRunnerContract = () => {
17057
17856
  };
17058
17857
  };
17059
17858
 
17859
+ // node_modules/@qawolf/api-contracts/dist/v1/runner/list.js
17860
+ var makeListRunnersContract = () => {
17861
+ const input = object({});
17862
+ const output = object({
17863
+ outcome: literal("success"),
17864
+ 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.")
17865
+ });
17866
+ return {
17867
+ 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.",
17868
+ input,
17869
+ kind: "read",
17870
+ name: "runner.list",
17871
+ output
17872
+ };
17873
+ };
17874
+
17060
17875
  // node_modules/@qawolf/api-contracts/dist/v1/runner/promoteSnapshot.js
17061
17876
  var maxSnapshotPathLength = 1024;
17062
17877
  var makePromoteSnapshotOnRunnerContract = () => {
@@ -17252,6 +18067,13 @@ var makeContractsV1 = (ids) => {
17252
18067
  send: makeAgentSendContract(resolvedIds)
17253
18068
  },
17254
18069
  automate: makeAutomateContract(resolvedIds),
18070
+ email: {
18071
+ find: makeFindEmailsContract(resolvedIds),
18072
+ get: makeGetEmailContract(resolvedIds),
18073
+ getAttachment: makeGetEmailAttachmentContract(resolvedIds),
18074
+ listAddresses: makeListEmailAddressesContract(resolvedIds),
18075
+ send: makeSendEmailContract(resolvedIds)
18076
+ },
17255
18077
  environment: {
17256
18078
  create: makeCreateEnvironmentContract(resolvedIds),
17257
18079
  deleteVariable: makeDeleteEnvironmentVariableContract(resolvedIds),
@@ -17265,6 +18087,7 @@ var makeContractsV1 = (ids) => {
17265
18087
  flow: {
17266
18088
  addTag: makeAddTagToFlowsContract(resolvedIds),
17267
18089
  list: makeListFlowsContract(resolvedIds),
18090
+ removeTag: makeRemoveTagFromFlowsContract(resolvedIds),
17268
18091
  update: makeUpdateFlowContract(resolvedIds)
17269
18092
  },
17270
18093
  issue: {
@@ -17272,6 +18095,7 @@ var makeContractsV1 = (ids) => {
17272
18095
  create: makeCreateIssueContract(resolvedIds),
17273
18096
  find: makeFindIssuesContract(resolvedIds),
17274
18097
  get: makeGetIssueContract(resolvedIds),
18098
+ removeFlows: makeRemoveFlowsFromIssueContract(resolvedIds),
17275
18099
  update: makeUpdateIssueContract(resolvedIds)
17276
18100
  },
17277
18101
  run: {
@@ -17289,6 +18113,7 @@ var makeContractsV1 = (ids) => {
17289
18113
  inspect: makeInspectOnRunnerContract(),
17290
18114
  inspectMobile: makeInspectMobileOnRunnerContract(),
17291
18115
  launch: makeLaunchRunnerContract(),
18116
+ list: makeListRunnersContract(),
17292
18117
  performAction: makePerformActionOnRunnerContract(),
17293
18118
  promoteSnapshot: makePromoteSnapshotOnRunnerContract(),
17294
18119
  readJournal: makeReadRunnerJournalContract(),
@@ -17305,19 +18130,70 @@ var makeContractsV1 = (ids) => {
17305
18130
  };
17306
18131
  var publicContractsV1 = makeContractsV1(defaultIdSchemas);
17307
18132
 
18133
+ // src/shell/platform/organizations.ts
18134
+ function parseOrganizationsResponse(json) {
18135
+ const parsed = identityOrganizationsResponse.safeParse(json);
18136
+ return parsed.success ? parsed.data.organizations : undefined;
18137
+ }
18138
+
18139
+ // src/shell/platform/getAccessibleOrganizations.ts
18140
+ var timeoutMs2 = 1e4;
18141
+ async function getAccessibleOrganizations(apiKey, deps) {
18142
+ const url = `${deps.baseUrl}/api/v0/identity/organizations`;
18143
+ let response;
18144
+ try {
18145
+ response = await deps.fetch(url, {
18146
+ headers: { Authorization: `Bearer ${apiKey}` },
18147
+ signal: AbortSignal.timeout(timeoutMs2)
18148
+ });
18149
+ } catch (error) {
18150
+ if (isTimeoutError(error)) {
18151
+ return { ok: false, error: { kind: "timeout", timeoutMs: timeoutMs2 } };
18152
+ }
18153
+ return { ok: false, error: { kind: "network", cause: toError(error) } };
18154
+ }
18155
+ if (!response.ok) {
18156
+ const body = await response.text().catch(() => "");
18157
+ return {
18158
+ ok: false,
18159
+ error: { kind: "http", status: response.status, body }
18160
+ };
18161
+ }
18162
+ let json;
18163
+ try {
18164
+ json = await response.json();
18165
+ } catch (error) {
18166
+ if (isTimeoutError(error)) {
18167
+ return { ok: false, error: { kind: "timeout", timeoutMs: timeoutMs2 } };
18168
+ }
18169
+ json = undefined;
18170
+ }
18171
+ const organizations = parseOrganizationsResponse(json);
18172
+ if (!organizations) {
18173
+ return {
18174
+ ok: false,
18175
+ error: {
18176
+ kind: "parse",
18177
+ cause: Error("organizations response did not match the contract")
18178
+ }
18179
+ };
18180
+ }
18181
+ return { ok: true, data: organizations };
18182
+ }
18183
+
17308
18184
  // src/shell/platform/getIdentity.ts
17309
- var timeoutMs = 1e4;
18185
+ var timeoutMs3 = 1e4;
17310
18186
  async function getIdentity(apiKey, deps) {
17311
18187
  const url = `${deps.baseUrl}/api/v0/identity`;
17312
18188
  let response;
17313
18189
  try {
17314
18190
  response = await deps.fetch(url, {
17315
18191
  headers: { Authorization: `Bearer ${apiKey}` },
17316
- signal: AbortSignal.timeout(timeoutMs)
18192
+ signal: AbortSignal.timeout(timeoutMs3)
17317
18193
  });
17318
18194
  } catch (error) {
17319
18195
  if (isTimeoutError(error)) {
17320
- return { ok: false, error: { kind: "timeout", timeoutMs } };
18196
+ return { ok: false, error: { kind: "timeout", timeoutMs: timeoutMs3 } };
17321
18197
  }
17322
18198
  return {
17323
18199
  ok: false,
@@ -17336,7 +18212,7 @@ async function getIdentity(apiKey, deps) {
17336
18212
  json = await response.json();
17337
18213
  } catch (error) {
17338
18214
  if (isTimeoutError(error)) {
17339
- return { ok: false, error: { kind: "timeout", timeoutMs } };
18215
+ return { ok: false, error: { kind: "timeout", timeoutMs: timeoutMs3 } };
17340
18216
  }
17341
18217
  json = undefined;
17342
18218
  }
@@ -17347,6 +18223,24 @@ async function getIdentity(apiKey, deps) {
17347
18223
  return { ok: true, data: parsed.data };
17348
18224
  }
17349
18225
 
18226
+ // src/shell/platform/identityMethods.ts
18227
+ function createIdentityMethods(apiKey, deps, backoffMs) {
18228
+ return {
18229
+ getIdentity: () => requestWithRetry({
18230
+ call: () => getIdentity(apiKey, deps),
18231
+ backoffMs,
18232
+ describe: describeIdentityError,
18233
+ sleep: deps.sleep
18234
+ }),
18235
+ getAccessibleOrganizations: () => requestWithRetry({
18236
+ call: () => getAccessibleOrganizations(apiKey, deps),
18237
+ backoffMs,
18238
+ describe: describeIdentityError,
18239
+ sleep: deps.sleep
18240
+ })
18241
+ };
18242
+ }
18243
+
17350
18244
  // src/shell/platform/types.ts
17351
18245
  var environmentListItemSchema = object({
17352
18246
  envId: string2(),
@@ -17474,7 +18368,7 @@ function isUnsafeSegment(segment) {
17474
18368
 
17475
18369
  // src/shell/platform/teamStorageAssetSnapshot.ts
17476
18370
  import { randomBytes as randomBytes2 } from "node:crypto";
17477
- import { join as join7, sep as sep2 } from "node:path";
18371
+ import { join as join10, sep as sep2 } from "node:path";
17478
18372
  function mintAssetSnapshotPath(assetsAbs, label) {
17479
18373
  return `${assetsAbs}.${label}-${randomBytes2(8).toString("hex")}`;
17480
18374
  }
@@ -17524,9 +18418,9 @@ function expectedEntries(filePaths) {
17524
18418
  return entries;
17525
18419
  }
17526
18420
  async function collectEntries(root, relDir, out, fs) {
17527
- const current = relDir ? join7(root, relDir) : root;
18421
+ const current = relDir ? join10(root, relDir) : root;
17528
18422
  for (const entry of await fs.readdirWithTypes(current)) {
17529
- const relPath = relDir ? join7(relDir, entry.name) : entry.name;
18423
+ const relPath = relDir ? join10(relDir, entry.name) : entry.name;
17530
18424
  out.add(relPath);
17531
18425
  if (entry.isDirectory()) {
17532
18426
  await collectEntries(root, relPath, out, fs);
@@ -17540,7 +18434,7 @@ function setsEqual(left, right) {
17540
18434
  }
17541
18435
 
17542
18436
  // src/shell/platform/teamStorageAssetReuse.ts
17543
- import { join as join8 } from "node:path";
18437
+ import { join as join11 } from "node:path";
17544
18438
  async function reusableAssetPaths(assetsAbs, safeFiles, manifest, fs) {
17545
18439
  const reusable = new Set;
17546
18440
  for (const { file, relativePath } of safeFiles) {
@@ -17556,20 +18450,20 @@ async function canReuseAsset(assetsAbs, relativePath, file, manifest, fs) {
17556
18450
  const previous = manifest.get(relativePath);
17557
18451
  if (previous?.etag !== file.etag)
17558
18452
  return false;
17559
- return fs.pathExists(join8(assetsAbs, relativePath));
18453
+ return fs.pathExists(join11(assetsAbs, relativePath));
17560
18454
  }
17561
18455
 
17562
18456
  // src/shell/platform/writeAssetSnapshot.ts
17563
- import { dirname as dirname3, join as join9 } from "node:path";
18457
+ import { dirname as dirname3, join as join12 } from "node:path";
17564
18458
  async function writeAssetSnapshot(args) {
17565
18459
  let downloadedCount = 0;
17566
18460
  const total = args.safeFiles.filter(({ relativePath }) => !args.reusable.has(relativePath)).length;
17567
18461
  await args.deps.fs.mkdir(args.tmpAssets, { recursive: true });
17568
18462
  for (const { file, relativePath } of args.safeFiles) {
17569
- const dest = join9(args.tmpAssets, relativePath);
18463
+ const dest = join12(args.tmpAssets, relativePath);
17570
18464
  await args.deps.fs.mkdir(dirname3(dest), { recursive: true });
17571
18465
  if (args.reusable.has(relativePath)) {
17572
- await args.deps.fs.copyFile(join9(args.assetsAbs, relativePath), dest);
18466
+ await args.deps.fs.copyFile(join12(args.assetsAbs, relativePath), dest);
17573
18467
  continue;
17574
18468
  }
17575
18469
  args.deps.onProgress?.({ current: downloadedCount + 1, total });
@@ -17646,14 +18540,7 @@ function createPlatformClient(apiKey, deps) {
17646
18540
  return { ok: true, value: { signedUrl: result.value.url } };
17647
18541
  }
17648
18542
  return {
17649
- async getIdentity() {
17650
- return requestWithRetry({
17651
- call: () => getIdentity(apiKey, deps),
17652
- backoffMs: requestBackoffMs2,
17653
- describe: describeIdentityError,
17654
- sleep: deps.sleep
17655
- });
17656
- },
18543
+ ...createIdentityMethods(apiKey, deps, requestBackoffMs2),
17657
18544
  getFlowsBundleUrl: getFlowsBundleUrlImpl,
17658
18545
  callPublicApi: makeCallPublicApiMethod(trpc, deps, requestBackoffMs2),
17659
18546
  async getEnvVars(envId) {
@@ -18352,6 +19239,86 @@ class V {
18352
19239
  }
18353
19240
  }
18354
19241
  }
19242
+ function p$1(l, e) {
19243
+ if (l === undefined || e.length === 0)
19244
+ return 0;
19245
+ const i = e.findIndex((s) => s.value === l);
19246
+ return i !== -1 ? i : 0;
19247
+ }
19248
+ function g(l, e) {
19249
+ return (e.label ?? String(e.value)).toLowerCase().includes(l.toLowerCase());
19250
+ }
19251
+ function m2(l, e) {
19252
+ if (e)
19253
+ return l ? e : e[0];
19254
+ }
19255
+ var T$1 = class T extends V {
19256
+ filteredOptions;
19257
+ multiple;
19258
+ isNavigating = false;
19259
+ selectedValues = [];
19260
+ focusedValue;
19261
+ #e = 0;
19262
+ #s = "";
19263
+ #t;
19264
+ #i;
19265
+ #n;
19266
+ get cursor() {
19267
+ return this.#e;
19268
+ }
19269
+ get userInputWithCursor() {
19270
+ if (!this.userInput)
19271
+ return styleText(["inverse", "hidden"], "_");
19272
+ if (this._cursor >= this.userInput.length)
19273
+ return `${this.userInput}█`;
19274
+ const e = this.userInput.slice(0, this._cursor), [t, ...i] = this.userInput.slice(this._cursor);
19275
+ return `${e}${styleText("inverse", t)}${i.join("")}`;
19276
+ }
19277
+ get options() {
19278
+ return typeof this.#i == "function" ? this.#i() : this.#i;
19279
+ }
19280
+ constructor(e) {
19281
+ super(e), this.#i = e.options, this.#n = e.placeholder;
19282
+ const t = this.options;
19283
+ this.filteredOptions = [...t], this.multiple = e.multiple === true, this.#t = typeof e.options == "function" ? e.filter : e.filter ?? g;
19284
+ let i;
19285
+ 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)
19286
+ for (const s of i) {
19287
+ const n = t.findIndex((o) => o.value === s);
19288
+ n !== -1 && (this.toggleSelected(s), this.#e = n);
19289
+ }
19290
+ this.focusedValue = this.options[this.#e]?.value, this.on("key", (s, n) => this.#l(s, n)), this.on("userInput", (s) => this.#u(s));
19291
+ }
19292
+ _isActionKey(e, t) {
19293
+ return e === "\t" || this.multiple && this.isNavigating && t.name === "space" && e !== undefined && e !== "";
19294
+ }
19295
+ #l(e, t) {
19296
+ 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));
19297
+ if (t.name === "tab" && o && f) {
19298
+ this.userInput === "\t" && this._clearUserInput(), this._setUserInput(u, true), this.isNavigating = false;
19299
+ return;
19300
+ }
19301
+ 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);
19302
+ }
19303
+ deselectAll() {
19304
+ this.selectedValues = [];
19305
+ }
19306
+ toggleSelected(e) {
19307
+ 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]);
19308
+ }
19309
+ #u(e) {
19310
+ if (e !== this.#s) {
19311
+ this.#s = e;
19312
+ const t = this.options;
19313
+ e && this.#t ? this.filteredOptions = t.filter((n) => this.#t?.(e, n)) : this.filteredOptions = [...t];
19314
+ const i = p$1(this.focusedValue, this.filteredOptions);
19315
+ this.#e = findCursor(i, 0, this.filteredOptions);
19316
+ const s = this.filteredOptions[this.#e];
19317
+ s && !s.disabled ? this.focusedValue = s.value : this.focusedValue = undefined, this.multiple || (this.focusedValue !== undefined ? this.toggleSelected(this.focusedValue) : this.deselectAll());
19318
+ }
19319
+ }
19320
+ };
19321
+
18355
19322
  class r extends V {
18356
19323
  get cursor() {
18357
19324
  return this.value ? 0 : 1;
@@ -18538,6 +19505,89 @@ var limitOptions = ({
18538
19505
  b.push(n);
18539
19506
  return u && b.push(C), b;
18540
19507
  };
19508
+ function P(t) {
19509
+ return t.label ?? String(t.value ?? "");
19510
+ }
19511
+ function E(t, c) {
19512
+ if (!t)
19513
+ return true;
19514
+ const n = (c.label ?? String(c.value ?? "")).toLowerCase(), i = (c.hint ?? "").toLowerCase(), l = String(c.value).toLowerCase(), o = t.toLowerCase();
19515
+ return n.includes(o) || i.includes(o) || l.includes(o);
19516
+ }
19517
+ function N(t, c) {
19518
+ const n = [];
19519
+ for (const i of c)
19520
+ t.includes(i.value) && n.push(i);
19521
+ return n;
19522
+ }
19523
+ var autocomplete = (t) => new T$1({
19524
+ options: t.options,
19525
+ initialValue: t.initialValue ? [t.initialValue] : undefined,
19526
+ initialUserInput: t.initialUserInput,
19527
+ placeholder: t.placeholder,
19528
+ filter: t.filter ?? ((n, i) => E(n, i)),
19529
+ signal: t.signal,
19530
+ input: t.input,
19531
+ output: t.output,
19532
+ validate: t.validate,
19533
+ render() {
19534
+ 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) => {
19535
+ const a = P(r), u = r.hint && r.value === this.focusedValue ? styleText2("dim", ` (${r.hint})`) : "";
19536
+ switch (s) {
19537
+ case "active":
19538
+ return `${styleText2("green", S_RADIO_ACTIVE)} ${a}${u}`;
19539
+ case "inactive":
19540
+ return `${styleText2("dim", S_RADIO_INACTIVE)} ${styleText2("dim", a)}`;
19541
+ case "disabled":
19542
+ return `${styleText2("gray", S_RADIO_INACTIVE)} ${styleText2(["strikethrough", "gray"], a)}`;
19543
+ }
19544
+ };
19545
+ switch (this.state) {
19546
+ case "submit": {
19547
+ const r = N(this.selectedValues, o), s = r.length > 0 ? ` ${styleText2("dim", r.map(P).join(", "))}` : "", a = n ? styleText2("gray", S_BAR) : "";
19548
+ return `${i.join(`
19549
+ `)}
19550
+ ${a}${s}`;
19551
+ }
19552
+ case "cancel": {
19553
+ const r = l ? ` ${styleText2(["strikethrough", "dim"], l)}` : "", s = n ? styleText2("gray", S_BAR) : "";
19554
+ return `${i.join(`
19555
+ `)}
19556
+ ${s}${r}`;
19557
+ }
19558
+ default: {
19559
+ const r = this.state === "error" ? "yellow" : "cyan", s = n ? `${styleText2(r, S_BAR)} ` : "", a = n ? styleText2(r, S_BAR_END) : "";
19560
+ let u = "";
19561
+ if (this.isNavigating || p) {
19562
+ const d = p ? m : l;
19563
+ u = d !== "" ? ` ${styleText2("dim", d)}` : "";
19564
+ } else
19565
+ u = ` ${this.userInputWithCursor}`;
19566
+ 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)}`] : [];
19567
+ n && i.push(`${s.trimEnd()}`), i.push(`${s}${styleText2("dim", "Search:")}${u}${V}`, ...y, ...b);
19568
+ const v = [
19569
+ `${styleText2("dim", "↑/↓")} to select`,
19570
+ `${styleText2("dim", "Enter:")} confirm`,
19571
+ `${styleText2("dim", "Type:")} to search`
19572
+ ], g = [`${s}${v.join(" • ")}`, a], O = this.filteredOptions.length === 0 ? [] : limitOptions({
19573
+ cursor: this.cursor,
19574
+ options: this.filteredOptions,
19575
+ columnPadding: n ? 3 : 0,
19576
+ rowPadding: i.length + g.length,
19577
+ style: (d, f) => $(d, d.disabled ? "disabled" : f ? "active" : "inactive"),
19578
+ maxItems: t.maxItems,
19579
+ output: t.output
19580
+ });
19581
+ return [
19582
+ ...i,
19583
+ ...O.map((d) => `${s}${d}`),
19584
+ ...g
19585
+ ].join(`
19586
+ `);
19587
+ }
19588
+ }
19589
+ }
19590
+ }).prompt();
18541
19591
  var confirm = (i) => {
18542
19592
  const a = i.active ?? "Yes", s = i.inactive ?? "No";
18543
19593
  return new r({
@@ -18968,6 +20018,7 @@ function createStyledClack() {
18968
20018
  cancel,
18969
20019
  confirm,
18970
20020
  password,
20021
+ autocomplete,
18971
20022
  select,
18972
20023
  isCancel,
18973
20024
  spinner,
@@ -19227,11 +20278,50 @@ function createPassword({
19227
20278
  };
19228
20279
  }
19229
20280
 
20281
+ // src/core/textSearch.ts
20282
+ function normalizeForSearch(value) {
20283
+ return value.toLowerCase().replace(/[-_\s]+/g, " ").trim();
20284
+ }
20285
+ function matchesSearchTerm(search, fields) {
20286
+ const needle = normalizeForSearch(search);
20287
+ if (!needle)
20288
+ return true;
20289
+ return fields.some((field) => field !== undefined && normalizeForSearch(field).includes(needle));
20290
+ }
20291
+ function createSearchIndex(items, fieldsOf) {
20292
+ const indexed = new Map;
20293
+ for (const item of items) {
20294
+ indexed.set(item, fieldsOf(item).flatMap((field) => field === undefined ? [] : [normalizeForSearch(field)]));
20295
+ }
20296
+ let lastSearch;
20297
+ let needle = "";
20298
+ return (search, item) => {
20299
+ if (search !== lastSearch) {
20300
+ lastSearch = search;
20301
+ needle = normalizeForSearch(search);
20302
+ }
20303
+ if (!needle)
20304
+ return true;
20305
+ const fields = indexed.get(item);
20306
+ return fields ? fields.some((field) => field.includes(needle)) : matchesSearchTerm(search, fieldsOf(item));
20307
+ };
20308
+ }
20309
+
19230
20310
  // src/shell/ui/renderers/select.ts
20311
+ var searchThreshold = 8;
20312
+ var searchPlaceholder = "Type to filter";
20313
+ function searchableFields(option) {
20314
+ return [option.label, option.hint, option.value];
20315
+ }
19231
20316
  function createSelect({ mode, clack }) {
19232
20317
  return async (message, options) => {
19233
20318
  assertHumanMode(mode, "select");
19234
- const value = await clack.select({ message, options: [...options] });
20319
+ const value = options.length > searchThreshold ? await clack.autocomplete({
20320
+ message,
20321
+ options: [...options],
20322
+ placeholder: searchPlaceholder,
20323
+ filter: createSearchIndex([...options], searchableFields)
20324
+ }) : await clack.select({ message, options: [...options] });
19235
20325
  if (clack.isCancel(value))
19236
20326
  return { ok: false };
19237
20327
  return { ok: true, value };
@@ -19279,12 +20369,12 @@ function createVerboseContext(outputMode, isVerbose) {
19279
20369
 
19280
20370
  // src/shell/npmRegistry.ts
19281
20371
  var registryUrl = "https://registry.npmjs.org";
19282
- var timeoutMs2 = 3000;
20372
+ var timeoutMs4 = 3000;
19283
20373
  async function fetchLatestVersion(packageName, deps = {}) {
19284
20374
  const fetchFn = deps.fetchFn ?? globalThis.fetch;
19285
20375
  try {
19286
20376
  const response = await fetchFn(`${registryUrl}/${packageName}/latest`, {
19287
- signal: AbortSignal.timeout(timeoutMs2)
20377
+ signal: AbortSignal.timeout(timeoutMs4)
19288
20378
  });
19289
20379
  if (!response.ok)
19290
20380
  return;
@@ -19299,7 +20389,7 @@ async function fetchLatestVersion(packageName, deps = {}) {
19299
20389
  }
19300
20390
 
19301
20391
  // src/domains/updateCheck/updateCheck.ts
19302
- import { join as join10 } from "node:path";
20392
+ import { join as join13 } from "node:path";
19303
20393
 
19304
20394
  // src/core/version.ts
19305
20395
  function parseRelease(version) {
@@ -19345,7 +20435,7 @@ function startUpdateCheck(deps) {
19345
20435
  return;
19346
20436
  if (!isNewerVersion(deps.currentVersion, latest))
19347
20437
  return;
19348
- const noticePath = join10(deps.configDir, noticeFile);
20438
+ const noticePath = join13(deps.configDir, noticeFile);
19349
20439
  try {
19350
20440
  const notified = (await deps.fs.readFile(noticePath)).trim();
19351
20441
  if (notified === latest)
@@ -19367,7 +20457,7 @@ function startUpdateCheck(deps) {
19367
20457
  // package.json
19368
20458
  var package_default = {
19369
20459
  name: "@qawolf/cli",
19370
- version: "1.23.0",
20460
+ version: "1.25.0",
19371
20461
  description: "Run and manage QA Wolf flows from the terminal, CI, or an AI agent",
19372
20462
  keywords: [
19373
20463
  "automation",
@@ -19438,7 +20528,7 @@ var package_default = {
19438
20528
  "@clack/prompts": "1.5.1",
19439
20529
  "@napi-rs/keyring": "1.3.0",
19440
20530
  "@oxc-node/core": "0.1.0",
19441
- "@qawolf/api-contracts": "0.40.0",
20531
+ "@qawolf/api-contracts": "0.47.0",
19442
20532
  "@qawolf/emails": "1.1.1",
19443
20533
  "@qawolf/flow-targets": "1.0.0",
19444
20534
  "@qawolf/flows": "0.1.4",
@@ -19557,7 +20647,12 @@ function withAuthContext(signals, fn, deps = {}) {
19557
20647
  await updateNotifier.notifyIfOutdated();
19558
20648
  return;
19559
20649
  }
19560
- const platformClient = (deps.createPlatform ?? createPlatformClient)(resolved.key, { baseUrl: apiBaseUrl, fetch: globalThis.fetch, logger: ctx.log("trpc") });
20650
+ const platformClient = (deps.createPlatform ?? createPlatformClient)(resolved.key, {
20651
+ baseUrl: apiBaseUrl,
20652
+ fetch: globalThis.fetch,
20653
+ logger: ctx.log("trpc"),
20654
+ workspaceId: resolved.workspaceId
20655
+ });
19561
20656
  try {
19562
20657
  const result = await fn({
19563
20658
  ...ctx,
@@ -19578,28 +20673,15 @@ function withAuthContext(signals, fn, deps = {}) {
19578
20673
  };
19579
20674
  }
19580
20675
 
19581
- // src/commands/auth/login.ts
19582
- async function handleLogin(ctx) {
19583
- if (ctx.ui.mode !== "human") {
19584
- ctx.ui.error(authMessages.login.nonInteractive);
19585
- return { error: "non-interactive" };
19586
- }
19587
- const existing = await resolveApiKey(ctx.configDir, ctx.fs);
19588
- if (existing) {
19589
- const reauth = await ctx.ui.confirm(authMessages.login.reAuthPrompt);
19590
- if (!reauth.ok || !reauth.value) {
19591
- ctx.ui.info(authMessages.alreadyConfigured);
19592
- return;
19593
- }
19594
- }
19595
- ctx.ui.gap();
19596
- ctx.ui.intro(authMessages.title);
20676
+ // src/commands/auth/loginApiKey.ts
20677
+ async function loginWithApiKey(ctx) {
19597
20678
  const result = await ctx.ui.password(authMessages.promptApiKey, "Set QAWOLF_API_KEY to authenticate in non-interactive environments.");
19598
20679
  if (!result.ok) {
19599
20680
  ctx.ui.cancel(authMessages.cancelled);
19600
20681
  return;
19601
20682
  }
19602
- if (!result.value.trim()) {
20683
+ const apiKey = result.value.trim();
20684
+ if (!apiKey) {
19603
20685
  ctx.ui.cancel(authMessages.cancelled);
19604
20686
  return;
19605
20687
  }
@@ -19608,7 +20690,7 @@ async function handleLogin(ctx) {
19608
20690
  message: authMessages.verifying,
19609
20691
  task: async () => {
19610
20692
  const v = await validateApiKey({
19611
- platformClient: createPlatformClient(result.value, {
20693
+ platformClient: createPlatformClient(apiKey, {
19612
20694
  baseUrl: ctx.apiBaseUrl,
19613
20695
  fetch: globalThis.fetch
19614
20696
  })
@@ -19619,7 +20701,7 @@ async function handleLogin(ctx) {
19619
20701
  },
19620
20702
  {
19621
20703
  message: authMessages.storing,
19622
- task: async () => saveApiKey(ctx.configDir, result.value, ctx.fs)
20704
+ task: async () => saveApiKey(ctx.configDir, apiKey, ctx.fs)
19623
20705
  }
19624
20706
  ], ([, saveResult]) => {
19625
20707
  return saveResult.stored === "file" ? authMessages.storedFile : authMessages.storedKeychain;
@@ -19627,14 +20709,632 @@ async function handleLogin(ctx) {
19627
20709
  ctx.ui.outro(authMessages.outroSuccess);
19628
20710
  }
19629
20711
 
20712
+ // src/core/deviceAuth/pollState.ts
20713
+ var slowDownIncrementMs = 5000;
20714
+ var unreachableBackoffFactor = 2;
20715
+ function capToLifetime(delayMs, state, nowMs) {
20716
+ return Math.min(delayMs, Math.max(1, state.deadlineMs - nowMs));
20717
+ }
20718
+ function nextPollStep(state, response, nowMs) {
20719
+ if (response.kind === "tokens") {
20720
+ return { action: "done", tokens: response.tokens };
20721
+ }
20722
+ if (nowMs > state.deadlineMs) {
20723
+ return { action: "fail", reason: "timeout", detail: undefined };
20724
+ }
20725
+ switch (response.kind) {
20726
+ case "pending":
20727
+ return {
20728
+ action: "poll",
20729
+ delayMs: capToLifetime(state.intervalMs, state, nowMs),
20730
+ state
20731
+ };
20732
+ case "slow-down": {
20733
+ const slowed = {
20734
+ intervalMs: state.intervalMs + slowDownIncrementMs,
20735
+ deadlineMs: state.deadlineMs
20736
+ };
20737
+ return {
20738
+ action: "poll",
20739
+ delayMs: capToLifetime(slowed.intervalMs, slowed, nowMs),
20740
+ state: slowed
20741
+ };
20742
+ }
20743
+ case "denied":
20744
+ return { action: "fail", reason: "access-denied", detail: undefined };
20745
+ case "expired":
20746
+ return { action: "fail", reason: "expired", detail: undefined };
20747
+ case "unreachable": {
20748
+ const backedOff = {
20749
+ intervalMs: state.intervalMs * unreachableBackoffFactor,
20750
+ deadlineMs: state.deadlineMs
20751
+ };
20752
+ return {
20753
+ action: "poll",
20754
+ delayMs: capToLifetime(backedOff.intervalMs, backedOff, nowMs),
20755
+ state: backedOff
20756
+ };
20757
+ }
20758
+ case "error":
20759
+ return { action: "fail", reason: "network", detail: response.detail };
20760
+ }
20761
+ }
20762
+
20763
+ // src/domains/auth/deviceLogin.ts
20764
+ var cancelled = {
20765
+ ok: false,
20766
+ reason: "cancelled",
20767
+ detail: undefined
20768
+ };
20769
+ async function deviceLogin(deps) {
20770
+ if (deps.isCancelled())
20771
+ return cancelled;
20772
+ const authorization = await deps.requestAuthorization();
20773
+ if (!authorization.ok) {
20774
+ return { ok: false, reason: "unavailable", detail: authorization.error };
20775
+ }
20776
+ await deps.onPrompt(authorization.value);
20777
+ let state = {
20778
+ intervalMs: authorization.value.intervalSec * 1000,
20779
+ deadlineMs: deps.now() + authorization.value.expiresInSec * 1000
20780
+ };
20781
+ for (;; ) {
20782
+ if (deps.isCancelled())
20783
+ return cancelled;
20784
+ const response = await deps.pollToken(authorization.value.deviceCode);
20785
+ const step = nextPollStep(state, response, deps.now());
20786
+ if (step.action === "done")
20787
+ return { ok: true, tokens: step.tokens };
20788
+ if (step.action === "fail") {
20789
+ return { ok: false, reason: step.reason, detail: step.detail };
20790
+ }
20791
+ state = step.state;
20792
+ await deps.sleep(step.delayMs);
20793
+ }
20794
+ }
20795
+
20796
+ // src/shell/openBrowser.ts
20797
+ var launchTimeoutMs = 2000;
20798
+ function launcher(url, platform) {
20799
+ if (platform === "darwin")
20800
+ return { cmd: "open", args: [url] };
20801
+ if (platform === "win32") {
20802
+ return { cmd: "rundll32", args: ["url.dll,FileProtocolHandler", url] };
20803
+ }
20804
+ return { cmd: "xdg-open", args: [url] };
20805
+ }
20806
+ async function openBrowser(url, deps) {
20807
+ let parsed;
20808
+ try {
20809
+ parsed = new URL(url);
20810
+ } catch {
20811
+ return false;
20812
+ }
20813
+ if (parsed.protocol !== "https:" && parsed.protocol !== "http:") {
20814
+ return false;
20815
+ }
20816
+ const { cmd, args } = launcher(url, deps.platform);
20817
+ const launched = deps.spawn(cmd, args, { platform: deps.platform }).then((result) => result.exitCode === 0).catch(() => false);
20818
+ return Promise.race([launched, deps.sleep(launchTimeoutMs).then(() => true)]);
20819
+ }
20820
+
20821
+ // src/shell/spawn.ts
20822
+ import { spawn } from "node:child_process";
20823
+
20824
+ // src/core/cmdEscape.ts
20825
+ var metaChars = /([()\][%!^"`<>&|;, *?])/g;
20826
+ function escapeCommand(command) {
20827
+ return command.replace(metaChars, "^$1");
20828
+ }
20829
+ function escapeArgument(arg) {
20830
+ const escaped = arg.replace(/(?=(\\+?)?)\1"/g, "$1$1\\\"").replace(/(?=(\\+?)?)\1$/, "$1$1");
20831
+ return `"${escaped}"`.replace(metaChars, "^$1").replace(metaChars, "^$1");
20832
+ }
20833
+
20834
+ // src/shell/spawn.ts
20835
+ function buildSpawnCommand(cmd, args, platform, env) {
20836
+ const options = {};
20837
+ if (env)
20838
+ options.env = env;
20839
+ if (platform !== "win32" || !/\.(cmd|bat)$/i.test(cmd)) {
20840
+ return { cmd, args, options };
20841
+ }
20842
+ const line = [escapeCommand(cmd), ...args.map(escapeArgument)].join(" ");
20843
+ return {
20844
+ cmd: process.env["ComSpec"] ?? "cmd.exe",
20845
+ args: ["/d", "/s", "/c", `"${line}"`],
20846
+ options: { ...options, windowsVerbatimArguments: true }
20847
+ };
20848
+ }
20849
+ var defaultSpawn = (cmd, args, opts) => new Promise((resolve) => {
20850
+ const env = opts.env ? { ...process.env, ...opts.env } : undefined;
20851
+ const built = buildSpawnCommand(cmd, args, opts.platform, env);
20852
+ const child = spawn(built.cmd, built.args, built.options);
20853
+ let stdout = "";
20854
+ let stderr = "";
20855
+ child.stdout?.on("data", (chunk) => {
20856
+ stdout += String(chunk);
20857
+ });
20858
+ child.stderr?.on("data", (chunk) => {
20859
+ stderr += String(chunk);
20860
+ });
20861
+ if (opts?.stdin !== undefined) {
20862
+ child.stdin?.write(opts.stdin);
20863
+ child.stdin?.end();
20864
+ }
20865
+ child.on("error", (err) => resolve({ exitCode: -1, stdout, stderr: stderr || err.message }));
20866
+ child.on("close", (code) => resolve({ exitCode: code ?? -1, stdout, stderr }));
20867
+ });
20868
+
20869
+ // src/shell/platform/getAuthConfig.ts
20870
+ var timeoutMs5 = 1e4;
20871
+ var authConfigBody = object({
20872
+ workOsClientId: string2().trim().min(1)
20873
+ });
20874
+ async function getAuthConfig(deps) {
20875
+ let response;
20876
+ try {
20877
+ response = await deps.fetch(`${deps.baseUrl}/api/v0/auth/config`, {
20878
+ signal: AbortSignal.timeout(timeoutMs5)
20879
+ });
20880
+ } catch (err) {
20881
+ return { kind: "unreachable", detail: errorMessage(err) };
20882
+ }
20883
+ if (response.status === 404)
20884
+ return { kind: "unconfigured" };
20885
+ if (!response.ok) {
20886
+ return { kind: "unreachable", detail: `HTTP ${response.status}` };
20887
+ }
20888
+ let json;
20889
+ try {
20890
+ json = await response.json();
20891
+ } catch (err) {
20892
+ return { kind: "unreachable", detail: errorMessage(err) };
20893
+ }
20894
+ const parsed = authConfigBody.safeParse(json);
20895
+ return parsed.success ? { kind: "configured", clientId: parsed.data.workOsClientId } : { kind: "unconfigured" };
20896
+ }
20897
+
20898
+ // src/shell/workos/pollDeviceToken.ts
20899
+ var deviceCodeGrantType = "urn:ietf:params:oauth:grant-type:device_code";
20900
+ async function pollDeviceToken(deviceCode, deps) {
20901
+ const outcome = await sendWorkosRequest(`${deps.baseUrl}/user_management/authenticate`, {
20902
+ headers: { "content-type": "application/x-www-form-urlencoded" },
20903
+ body: new URLSearchParams({
20904
+ grant_type: deviceCodeGrantType,
20905
+ device_code: deviceCode,
20906
+ client_id: deps.clientId
20907
+ }).toString()
20908
+ }, deps.fetch);
20909
+ if (outcome.kind === "failure") {
20910
+ return outcome.retryable ? { kind: "unreachable", detail: outcome.detail } : { kind: "error", detail: outcome.detail };
20911
+ }
20912
+ if (outcome.kind === "oauth-error") {
20913
+ switch (outcome.code) {
20914
+ case "authorization_pending":
20915
+ return { kind: "pending" };
20916
+ case "slow_down":
20917
+ return { kind: "slow-down" };
20918
+ case "access_denied":
20919
+ return { kind: "denied" };
20920
+ case "expired_token":
20921
+ case "invalid_grant":
20922
+ return { kind: "expired" };
20923
+ default:
20924
+ return {
20925
+ kind: "error",
20926
+ detail: outcome.description ?? outcome.code
20927
+ };
20928
+ }
20929
+ }
20930
+ const parsed = deviceTokenBody.safeParse(outcome.json);
20931
+ if (!parsed.success) {
20932
+ return { kind: "error", detail: unexpectedResponse };
20933
+ }
20934
+ return {
20935
+ kind: "tokens",
20936
+ tokens: {
20937
+ accessToken: parsed.data.access_token,
20938
+ refreshToken: parsed.data.refresh_token,
20939
+ expiresAt: readAccessTokenExpiry(parsed.data.access_token),
20940
+ email: parsed.data.user.email,
20941
+ organizationId: parsed.data.organization_id
20942
+ }
20943
+ };
20944
+ }
20945
+
20946
+ // src/shell/workos/requestDeviceAuthorization.ts
20947
+ async function requestDeviceAuthorization(deps) {
20948
+ const outcome = await sendWorkosRequest(`${deps.baseUrl}/user_management/authorize/device`, {
20949
+ headers: { "content-type": "application/x-www-form-urlencoded" },
20950
+ body: new URLSearchParams({ client_id: deps.clientId }).toString()
20951
+ }, deps.fetch);
20952
+ if (outcome.kind === "failure") {
20953
+ return { ok: false, error: outcome.detail, retryable: outcome.retryable };
20954
+ }
20955
+ if (outcome.kind === "oauth-error") {
20956
+ return {
20957
+ ok: false,
20958
+ error: outcome.description ?? outcome.code,
20959
+ retryable: false
20960
+ };
20961
+ }
20962
+ const parsed = deviceAuthorizationBody.safeParse(outcome.json);
20963
+ if (!parsed.success) {
20964
+ return { ok: false, error: unexpectedResponse, retryable: false };
20965
+ }
20966
+ return {
20967
+ ok: true,
20968
+ value: {
20969
+ deviceCode: parsed.data.device_code,
20970
+ userCode: parsed.data.user_code,
20971
+ verificationUri: parsed.data.verification_uri,
20972
+ verificationUriComplete: parsed.data.verification_uri_complete,
20973
+ expiresInSec: parsed.data.expires_in,
20974
+ intervalSec: parsed.data.interval ?? defaultIntervalSec
20975
+ }
20976
+ };
20977
+ }
20978
+
20979
+ // src/domains/auth/nameMatching.ts
20980
+ function matches(candidate, wanted) {
20981
+ return candidate.toLowerCase() === wanted.trim().toLowerCase();
20982
+ }
20983
+ function findOrganization(organizations, wanted) {
20984
+ return organizations.find((organization) => matches(organization.name, wanted) || matches(organization.id, wanted) || matches(organization.workOsOrganizationId, wanted));
20985
+ }
20986
+ function findWorkspace(workspaces, wanted) {
20987
+ return workspaces.find((workspace) => matches(workspace.name, wanted) || matches(workspace.id, wanted) || workspace.slug !== undefined && matches(workspace.slug, wanted));
20988
+ }
20989
+ function findWorkspaceAcross(organizations, wanted) {
20990
+ const hits = organizations.flatMap((organization) => {
20991
+ const workspace = findWorkspace(organization.workspaces, wanted);
20992
+ return workspace ? [{ organization, workspace }] : [];
20993
+ });
20994
+ const [only] = hits;
20995
+ if (!only)
20996
+ return { kind: "none" };
20997
+ if (hits.length > 1) {
20998
+ return {
20999
+ kind: "ambiguous",
21000
+ organizations: hits.map((h) => h.organization)
21001
+ };
21002
+ }
21003
+ return {
21004
+ kind: "found",
21005
+ organization: only.organization,
21006
+ workspace: only.workspace
21007
+ };
21008
+ }
21009
+ function nameList(items) {
21010
+ return items.map((item) => item.slug ?? item.name).join(", ");
21011
+ }
21012
+
21013
+ // src/domains/auth/selectWorkspace.ts
21014
+ async function selectWorkspace(deps) {
21015
+ const { organizations } = deps;
21016
+ if (organizations.length === 0)
21017
+ return { outcome: "none" };
21018
+ let organization;
21019
+ if (deps.preferredOrganization) {
21020
+ organization = findOrganization(organizations, deps.preferredOrganization);
21021
+ if (!organization) {
21022
+ return {
21023
+ outcome: "failed",
21024
+ error: `No organization matches '${deps.preferredOrganization}'. Available: ${nameList(organizations)}.`
21025
+ };
21026
+ }
21027
+ } else if (organizations.length === 1) {
21028
+ organization = organizations[0];
21029
+ } else if (deps.preferredWorkspace) {
21030
+ const match = findWorkspaceAcross(organizations, deps.preferredWorkspace);
21031
+ if (match.kind === "none") {
21032
+ return {
21033
+ outcome: "failed",
21034
+ error: `No workspace matches '${deps.preferredWorkspace}'. Available: ${nameList(organizations.flatMap((o) => o.workspaces))}.`
21035
+ };
21036
+ }
21037
+ if (match.kind === "ambiguous") {
21038
+ return {
21039
+ outcome: "failed",
21040
+ error: `More than one organization has a workspace matching '${deps.preferredWorkspace}': ${nameList(match.organizations)}. Set QAWOLF_ORGANIZATION to choose between them.`
21041
+ };
21042
+ }
21043
+ organization = match.organization;
21044
+ } else {
21045
+ organization = await deps.chooseOrganization(organizations);
21046
+ if (!organization)
21047
+ return { outcome: "cancelled" };
21048
+ }
21049
+ if (!organization)
21050
+ throw Error("organization was not settled");
21051
+ const { workspaces } = organization;
21052
+ if (workspaces.length === 0) {
21053
+ return {
21054
+ outcome: "failed",
21055
+ error: `Organization '${organization.name}' has no workspaces.`
21056
+ };
21057
+ }
21058
+ let workspace;
21059
+ if (deps.preferredWorkspace) {
21060
+ workspace = findWorkspace(workspaces, deps.preferredWorkspace);
21061
+ if (!workspace) {
21062
+ return {
21063
+ outcome: "failed",
21064
+ error: `No workspace matches '${deps.preferredWorkspace}' in ${organization.name}. Available: ${nameList(workspaces)}.`
21065
+ };
21066
+ }
21067
+ } else if (workspaces.length === 1) {
21068
+ workspace = workspaces[0];
21069
+ } else {
21070
+ workspace = await deps.chooseWorkspace(workspaces);
21071
+ if (!workspace)
21072
+ return { outcome: "cancelled" };
21073
+ }
21074
+ if (!workspace)
21075
+ throw Error("workspace was not settled");
21076
+ try {
21077
+ await deps.saveWorkspace(workspace.id);
21078
+ } catch (err) {
21079
+ return {
21080
+ outcome: "failed",
21081
+ error: authMessages.workspace.saveFailed(errorMessage(err))
21082
+ };
21083
+ }
21084
+ return { outcome: "selected", organization, workspace };
21085
+ }
21086
+
21087
+ // src/domains/auth/store/saveWorkspaceId.ts
21088
+ import { Entry as Entry6 } from "@napi-rs/keyring";
21089
+ function makeSaveWorkspaceIdDeps(configDir, fs) {
21090
+ return {
21091
+ loadTokens: () => loadTokens(configDir, { EntryClass: Entry6, fs }),
21092
+ saveTokens: (session) => saveTokens(configDir, session, fs)
21093
+ };
21094
+ }
21095
+ async function saveWorkspaceId(session, workspaceId, deps) {
21096
+ const current = await deps.loadTokens();
21097
+ if (!current.found || !isSameSession(current.tokens, session)) {
21098
+ throw Error(authMessages.workspace.sessionChanged);
21099
+ }
21100
+ return deps.saveTokens({ ...current.tokens, workspaceId });
21101
+ }
21102
+ function isSameSession(stored, captured) {
21103
+ return stored.email === captured.email && stored.organizationId === captured.organizationId;
21104
+ }
21105
+
21106
+ // src/commands/auth/chooseWorkspace.ts
21107
+ async function chooseWorkspace(ctx, args) {
21108
+ const client = createPlatformClient(args.session.accessToken, {
21109
+ baseUrl: ctx.apiBaseUrl,
21110
+ fetch: globalThis.fetch,
21111
+ logger: ctx.log("trpc")
21112
+ });
21113
+ const [identity, discovered] = await Promise.all([
21114
+ client.getIdentity(),
21115
+ client.getAccessibleOrganizations()
21116
+ ]);
21117
+ let organizations;
21118
+ if (discovered.ok) {
21119
+ organizations = discovered.value;
21120
+ } else if (identity.ok) {
21121
+ organizations = "user" in identity.value ? identity.value.organizations : [];
21122
+ } else {
21123
+ return { outcome: "failed", error: identity.error };
21124
+ }
21125
+ return selectWorkspace({
21126
+ organizations,
21127
+ preferredOrganization: args.env["QAWOLF_ORGANIZATION"]?.trim(),
21128
+ preferredWorkspace: args.env["QAWOLF_WORKSPACE"]?.trim(),
21129
+ chooseOrganization: async (organizations) => {
21130
+ if (ctx.ui.mode !== "human")
21131
+ return;
21132
+ const picked = await ctx.ui.select(authMessages.workspace.chooseOrganization, organizations.map((organization) => ({
21133
+ value: organization.workOsOrganizationId,
21134
+ label: organization.name,
21135
+ hint: authMessages.workspace.workspaceCount(organization.workspaces.length)
21136
+ })));
21137
+ if (!picked.ok)
21138
+ return;
21139
+ return organizations.find((organization) => organization.workOsOrganizationId === picked.value);
21140
+ },
21141
+ chooseWorkspace: async (workspaces) => {
21142
+ if (ctx.ui.mode !== "human")
21143
+ return;
21144
+ const picked = await ctx.ui.select(authMessages.workspace.choose, workspaces.map((workspace) => ({
21145
+ value: workspace.id,
21146
+ label: workspace.name,
21147
+ ...workspace.slug ? { hint: workspace.slug } : {}
21148
+ })));
21149
+ if (!picked.ok)
21150
+ return;
21151
+ return workspaces.find((workspace) => workspace.id === picked.value);
21152
+ },
21153
+ saveWorkspace: (workspaceId) => saveWorkspaceId(args.session, workspaceId, makeSaveWorkspaceIdDeps(ctx.configDir, ctx.fs))
21154
+ });
21155
+ }
21156
+ function reportWorkspace(ctx, result) {
21157
+ switch (result.outcome) {
21158
+ case "selected":
21159
+ ctx.ui.info(authMessages.workspace.working(result.organization.name, result.workspace.name));
21160
+ return;
21161
+ case "none":
21162
+ ctx.ui.info(authMessages.workspace.none);
21163
+ return;
21164
+ case "cancelled":
21165
+ if (ctx.ui.mode !== "human") {
21166
+ return { error: authMessages.workspace.nonInteractive };
21167
+ }
21168
+ ctx.ui.info(authMessages.workspace.cancelled);
21169
+ return;
21170
+ case "failed":
21171
+ return { error: result.error };
21172
+ }
21173
+ }
21174
+
21175
+ // src/commands/auth/loginDevice.ts
21176
+ async function loginWithDevice(ctx, deps = {}) {
21177
+ const authConfig = await getAuthConfig({
21178
+ baseUrl: ctx.apiBaseUrl,
21179
+ fetch: globalThis.fetch
21180
+ });
21181
+ if (authConfig.kind === "unreachable") {
21182
+ ctx.log("auth").debug(`auth config unreachable: ${authConfig.detail}`);
21183
+ return {
21184
+ error: authMessages.device.configUnreachable,
21185
+ errorBody: authConfig.detail
21186
+ };
21187
+ }
21188
+ const config = resolveWorkosConfig(authConfig.kind === "configured" ? authConfig.clientId : undefined);
21189
+ if (!config.configured) {
21190
+ return { error: authMessages.device.unavailable };
21191
+ }
21192
+ const workos = {
21193
+ fetch: globalThis.fetch,
21194
+ baseUrl: config.baseUrl,
21195
+ clientId: config.clientId
21196
+ };
21197
+ let cancelled = false;
21198
+ const unregister = ctx.signals.register(() => {
21199
+ cancelled = true;
21200
+ });
21201
+ const showCode = async (authorization) => {
21202
+ const url = authorization.verificationUriComplete ?? authorization.verificationUri;
21203
+ ctx.ui.note([
21204
+ authMessages.device.confirmCode(authorization.userCode),
21205
+ authMessages.device.visitUrl(url),
21206
+ url === authorization.verificationUri ? undefined : authMessages.device.visitUrlPlain(authorization.verificationUri)
21207
+ ].filter((line) => Boolean(line)).join(`
21208
+ `), authMessages.title);
21209
+ const opened = await openBrowser(url, {
21210
+ sleep,
21211
+ spawn: defaultSpawn,
21212
+ platform: deps.platform ?? process.platform
21213
+ });
21214
+ if (!opened)
21215
+ ctx.ui.info(authMessages.device.openFailed(url));
21216
+ ctx.ui.step(authMessages.device.waiting);
21217
+ };
21218
+ try {
21219
+ const result = await deviceLogin({
21220
+ requestAuthorization: () => requestDeviceAuthorization(workos),
21221
+ pollToken: (deviceCode) => pollDeviceToken(deviceCode, workos),
21222
+ onPrompt: showCode,
21223
+ sleep,
21224
+ now: () => Date.now(),
21225
+ isCancelled: () => cancelled
21226
+ });
21227
+ if (!result.ok) {
21228
+ return {
21229
+ error: authMessages.device.failed[result.reason],
21230
+ ...result.detail ? { errorBody: result.detail } : {}
21231
+ };
21232
+ }
21233
+ const session = {
21234
+ ...result.tokens,
21235
+ workspaceId: undefined,
21236
+ clientId: config.clientId
21237
+ };
21238
+ await saveTokens(ctx.configDir, session, ctx.fs);
21239
+ const workspace = await chooseWorkspace(ctx, {
21240
+ session,
21241
+ env: deps.env ?? process.env
21242
+ });
21243
+ const failure = reportWorkspace(ctx, workspace);
21244
+ if (failure)
21245
+ return failure;
21246
+ ctx.ui.outro(authMessages.device.signedIn(result.tokens.email));
21247
+ return;
21248
+ } finally {
21249
+ unregister();
21250
+ }
21251
+ }
21252
+
21253
+ // src/commands/auth/login.ts
21254
+ var browserMethod = "browser";
21255
+ async function handleLogin(ctx, deps = {}) {
21256
+ if (ctx.ui.mode !== "human") {
21257
+ ctx.ui.error(authMessages.login.nonInteractive);
21258
+ return { error: "non-interactive" };
21259
+ }
21260
+ const resolveApiKey2 = deps.resolveApiKey ?? resolveApiKey;
21261
+ const existing = await resolveApiKey2(ctx.configDir, ctx.fs);
21262
+ if (existing) {
21263
+ const reauth = await ctx.ui.confirm(authMessages.login.reAuthPrompt);
21264
+ if (!reauth.ok || !reauth.value) {
21265
+ ctx.ui.info(authMessages.alreadyConfigured);
21266
+ return;
21267
+ }
21268
+ }
21269
+ ctx.ui.gap();
21270
+ ctx.ui.intro(authMessages.title);
21271
+ const method = await ctx.ui.select(authMessages.login.chooseMethod, [
21272
+ {
21273
+ value: browserMethod,
21274
+ label: authMessages.login.methodBrowser,
21275
+ hint: authMessages.login.methodBrowserHint
21276
+ },
21277
+ {
21278
+ value: "api-key",
21279
+ label: authMessages.login.methodApiKey,
21280
+ hint: authMessages.login.methodApiKeyHint
21281
+ }
21282
+ ]);
21283
+ if (!method.ok) {
21284
+ ctx.ui.cancel(authMessages.cancelled);
21285
+ return;
21286
+ }
21287
+ if (method.value !== browserMethod) {
21288
+ return (deps.loginWithApiKey ?? loginWithApiKey)(ctx);
21289
+ }
21290
+ if (existing && existing.source !== "browser") {
21291
+ ctx.ui.warn(existing.source === "env" ? authMessages.login.apiKeyPrecedence.env : authMessages.login.apiKeyPrecedence.stored);
21292
+ }
21293
+ return (deps.loginWithDevice ?? loginWithDevice)(ctx);
21294
+ }
21295
+
21296
+ // src/domains/auth/store/deleteTokens.ts
21297
+ import { join as join14 } from "node:path";
21298
+ import { Entry as Entry7 } from "@napi-rs/keyring";
21299
+ function deleteFromKeychain2() {
21300
+ try {
21301
+ new Entry7(service, tokensAccount).deletePassword();
21302
+ return "deleted";
21303
+ } catch {
21304
+ return "unavailable";
21305
+ }
21306
+ }
21307
+ async function deleteFromFile2(configDir, fs) {
21308
+ try {
21309
+ await fs.unlink(join14(configDir, tokensFile));
21310
+ return "deleted";
21311
+ } catch (err) {
21312
+ if (isNoEntError(err))
21313
+ return "not-found";
21314
+ throw err;
21315
+ }
21316
+ }
21317
+ async function deleteTokens(configDir, fs) {
21318
+ const [keychain, file] = await Promise.all([
21319
+ Promise.resolve(deleteFromKeychain2()),
21320
+ deleteFromFile2(configDir, fs)
21321
+ ]);
21322
+ return { keychain, file };
21323
+ }
21324
+
19630
21325
  // src/commands/auth/logout.ts
19631
- async function handleLogout(ctx) {
19632
- const resolved = await resolveApiKey(ctx.configDir, ctx.fs);
19633
- if (!resolved) {
21326
+ async function handleLogout(ctx, deps = {}) {
21327
+ const hasStoredCredentials2 = deps.hasStoredCredentials ?? hasStoredCredentials;
21328
+ const deleteApiKey2 = deps.deleteApiKey ?? deleteApiKey;
21329
+ const deleteTokens2 = deps.deleteTokens ?? deleteTokens;
21330
+ const env = deps.env ?? process.env;
21331
+ const envKey = env["QAWOLF_API_KEY"]?.trim();
21332
+ const stored = await hasStoredCredentials2(ctx.configDir, ctx.fs);
21333
+ if (!envKey && !stored) {
19634
21334
  ctx.ui.info(authMessages.logout.notAuthenticated);
19635
21335
  return;
19636
21336
  }
19637
- if (resolved.source === "env") {
21337
+ if (envKey) {
19638
21338
  ctx.ui.warn(authMessages.logout.envVarWarning);
19639
21339
  }
19640
21340
  if (ctx.ui.mode === "human") {
@@ -19649,13 +21349,88 @@ async function handleLogout(ctx) {
19649
21349
  await ctx.ui.withProgress([
19650
21350
  {
19651
21351
  message: authMessages.logout.deleting,
19652
- task: () => deleteApiKey(ctx.configDir, ctx.fs)
21352
+ task: async () => {
21353
+ await Promise.all([
21354
+ deleteApiKey2(ctx.configDir, ctx.fs),
21355
+ deleteTokens2(ctx.configDir, ctx.fs)
21356
+ ]);
21357
+ }
19653
21358
  }
19654
21359
  ], () => authMessages.logout.credentialsRemoved);
19655
21360
  if (ctx.ui.mode === "human") {
19656
- ctx.ui.outro(authMessages.logout.success);
21361
+ ctx.ui.outro(authMessages.logout.success);
21362
+ } else {
21363
+ ctx.ui.output({ loggedOut: true }, authMessages.logout.success);
21364
+ }
21365
+ }
21366
+
21367
+ // src/domains/auth/refreshStoredSession.ts
21368
+ import { Entry as Entry8 } from "@napi-rs/keyring";
21369
+ function makeDefaultDeps2(fs) {
21370
+ return {
21371
+ loadTokens: (configDir) => loadTokens(configDir, { EntryClass: Entry8, fs }),
21372
+ resolveOauth: (configDir) => resolveOauthToken(configDir, makeOauthDeps(fs))
21373
+ };
21374
+ }
21375
+ async function refreshStoredSession(configDir, fs, deps) {
21376
+ const resolvedDeps = deps ?? makeDefaultDeps2(fs);
21377
+ const before = await resolvedDeps.loadTokens(configDir);
21378
+ if (!before.found)
21379
+ return { kind: "not-signed-in" };
21380
+ const renewed = await resolvedDeps.resolveOauth(configDir);
21381
+ if (!renewed)
21382
+ return { kind: "refresh-failed" };
21383
+ const after = await resolvedDeps.loadTokens(configDir);
21384
+ if (!after.found)
21385
+ return { kind: "refresh-failed" };
21386
+ if (after.tokens.accessToken !== renewed.key) {
21387
+ return { kind: "refresh-failed" };
21388
+ }
21389
+ return { kind: "session", session: after.tokens };
21390
+ }
21391
+
21392
+ // src/commands/auth/switchWorkspace.ts
21393
+ async function handleSwitchWorkspace(ctx, deps = {}) {
21394
+ const env = deps.env ?? process.env;
21395
+ const refreshStoredSession2 = deps.refreshStoredSession ?? refreshStoredSession;
21396
+ const preferred = env["QAWOLF_WORKSPACE"]?.trim() || env["QAWOLF_ORGANIZATION"]?.trim();
21397
+ if (ctx.ui.mode !== "human" && !preferred) {
21398
+ return { error: authMessages.workspace.nonInteractive };
21399
+ }
21400
+ const stored = await refreshStoredSession2(ctx.configDir, ctx.fs);
21401
+ if (stored.kind === "not-signed-in") {
21402
+ return { error: authMessages.workspace.notSignedIn };
21403
+ }
21404
+ if (stored.kind === "refresh-failed") {
21405
+ return { error: authMessages.workspace.sessionExpired };
21406
+ }
21407
+ ctx.ui.gap();
21408
+ ctx.ui.intro(authMessages.title);
21409
+ const result = await chooseWorkspace(ctx, {
21410
+ session: stored.session,
21411
+ env
21412
+ });
21413
+ const failure = reportWorkspace(ctx, result);
21414
+ if (failure)
21415
+ return failure;
21416
+ ctx.ui.outro(authMessages.outroReady);
21417
+ return;
21418
+ }
21419
+
21420
+ // src/commands/auth/whoamiTeam.ts
21421
+ function reportTeamIdentity(ctx, value, source) {
21422
+ const { team } = value;
21423
+ const teamUrl = team.slug ? new URL("/" + encodeURIComponent(team.slug), ctx.apiBaseUrl).toString() : undefined;
21424
+ if (ctx.ui.mode === "human") {
21425
+ ctx.ui.note(authMessages.whoami.teamNote({ team, teamUrl, source }), authMessages.whoamiAuthenticated);
21426
+ ctx.ui.outro(authMessages.outroReady);
19657
21427
  } else {
19658
- ctx.ui.output({ loggedOut: true }, authMessages.logout.success);
21428
+ ctx.ui.output({
21429
+ authenticated: true,
21430
+ source,
21431
+ team,
21432
+ teamUrl
21433
+ }, authMessages.whoami.authenticatedAs(team.name, source));
19659
21434
  }
19660
21435
  }
19661
21436
 
@@ -19700,11 +21475,19 @@ async function handleWhoami(ctx, deps = {}) {
19700
21475
  const { value } = identity;
19701
21476
  if ("user" in value) {
19702
21477
  const { organization, user } = value;
21478
+ const found = value.organizations.flatMap((candidate) => candidate.workspaces.map((workspace) => ({
21479
+ organization: candidate.name,
21480
+ workspace: workspace.name,
21481
+ id: workspace.id
21482
+ }))).find((entry) => entry.id === resolved.workspaceId);
21483
+ const activeWorkspace = resolved.workspaceId ? authMessages.whoami.activeWorkspace(resolved.workspaceId, found) : undefined;
19703
21484
  if (ctx.ui.mode === "human") {
19704
21485
  ctx.ui.note(authMessages.whoami.userNote({
19705
21486
  organization,
19706
21487
  source: resolved.source,
19707
- user
21488
+ user,
21489
+ activeWorkspace,
21490
+ organizations: value.organizations
19708
21491
  }), authMessages.whoamiAuthenticated);
19709
21492
  ctx.ui.outro(authMessages.outroReady);
19710
21493
  } else {
@@ -19712,7 +21495,9 @@ async function handleWhoami(ctx, deps = {}) {
19712
21495
  authenticated: true,
19713
21496
  organization,
19714
21497
  source: resolved.source,
19715
- user
21498
+ user,
21499
+ workspaceId: resolved.workspaceId,
21500
+ organizations: value.organizations
19716
21501
  }, authMessages.whoami.authenticatedAs(user.email, resolved.source));
19717
21502
  }
19718
21503
  return;
@@ -19734,26 +21519,15 @@ async function handleWhoami(ctx, deps = {}) {
19734
21519
  }
19735
21520
  return;
19736
21521
  }
19737
- const { team } = value;
19738
- const teamUrl = team.slug ? new URL("/" + encodeURIComponent(team.slug), ctx.apiBaseUrl).toString() : undefined;
19739
- if (ctx.ui.mode === "human") {
19740
- ctx.ui.note(authMessages.whoami.teamNote({ team, teamUrl, source: resolved.source }), authMessages.whoamiAuthenticated);
19741
- ctx.ui.outro(authMessages.outroReady);
19742
- } else {
19743
- ctx.ui.output({
19744
- authenticated: true,
19745
- source: resolved.source,
19746
- team,
19747
- teamUrl
19748
- }, authMessages.whoami.authenticatedAs(team.name, resolved.source));
19749
- }
21522
+ reportTeamIdentity(ctx, value, resolved.source);
19750
21523
  }
19751
21524
 
19752
21525
  // src/commands/auth/index.ts
19753
21526
  function registerAuthCommand(program, signals) {
19754
21527
  const auth = program.command("auth").description("Manage authentication with QA Wolf");
19755
- declareCommandKind(auth.command("login"), "local").description("Authenticate with your QA Wolf API key").action(withContext(signals, handleLogin));
21528
+ declareCommandKind(auth.command("login"), "local").description("Authenticate with QA Wolf in a browser or with an API key").action(withContext(signals, handleLogin));
19756
21529
  declareCommandKind(auth.command("logout"), "local").description("Remove stored credentials").action(withContext(signals, handleLogout));
21530
+ declareCommandKind(auth.command("switch"), "local").description("Choose which workspace to work in").action(withContext(signals, handleSwitchWorkspace));
19757
21531
  declareCommandKind(auth.command("whoami"), "read").description("Show authentication status").action(withContext(signals, handleWhoami));
19758
21532
  }
19759
21533
 
@@ -19803,11 +21577,11 @@ async function* batchMap(items, fn, size) {
19803
21577
  }
19804
21578
 
19805
21579
  // src/core/nodeModulesBins.ts
19806
- import { join as join11 } from "node:path";
21580
+ import { join as join15 } from "node:path";
19807
21581
  function nodeModulesBinCandidates(envDir, name, platform) {
19808
- const binDir = join11(envDir, "node_modules", ".bin");
21582
+ const binDir = join15(envDir, "node_modules", ".bin");
19809
21583
  const names = platform === "win32" ? [`${name}.cmd`, `${name}.exe`] : [name];
19810
- return names.map((shim) => join11(binDir, shim));
21584
+ return names.map((shim) => join15(binDir, shim));
19811
21585
  }
19812
21586
  function appiumCliCandidates(envDir, platform) {
19813
21587
  return nodeModulesBinCandidates(envDir, "appium", platform);
@@ -19864,21 +21638,21 @@ async function checkUiautomator2(spawn, appiumBin, platform) {
19864
21638
  }
19865
21639
 
19866
21640
  // src/core/androidBins.ts
19867
- import { join as join12 } from "node:path";
21641
+ import { join as join16 } from "node:path";
19868
21642
  function withExeSuffix(name, platform) {
19869
21643
  return platform === "win32" ? `${name}.exe` : name;
19870
21644
  }
19871
21645
  function emulatorBin(home, platform) {
19872
21646
  const name = withExeSuffix("emulator", platform);
19873
- return home ? join12(home, "emulator", name) : name;
21647
+ return home ? join16(home, "emulator", name) : name;
19874
21648
  }
19875
21649
  function adbBin(home, platform) {
19876
21650
  const name = withExeSuffix("adb", platform);
19877
- return home ? join12(home, "platform-tools", name) : name;
21651
+ return home ? join16(home, "platform-tools", name) : name;
19878
21652
  }
19879
21653
  function cmdlineToolsBin(home, name, platform) {
19880
21654
  const file = platform === "win32" ? `${name}.bat` : name;
19881
- return join12(home, "cmdline-tools", "latest", "bin", file);
21655
+ return join16(home, "cmdline-tools", "latest", "bin", file);
19882
21656
  }
19883
21657
  function sdkManagerBin(home, platform) {
19884
21658
  return cmdlineToolsBin(home, "sdkmanager", platform);
@@ -20175,9 +21949,9 @@ async function checkNpmRegistry(deps) {
20175
21949
  }
20176
21950
 
20177
21951
  // src/core/playwrightCli.ts
20178
- import { join as join13 } from "node:path";
21952
+ import { join as join17 } from "node:path";
20179
21953
  function playwrightCliJsPath(envDir) {
20180
- return join13(envDir, "node_modules", "playwright", "cli.js");
21954
+ return join17(envDir, "node_modules", "playwright", "cli.js");
20181
21955
  }
20182
21956
  function playwrightCliInvocation(args) {
20183
21957
  return {
@@ -20327,11 +22101,11 @@ function renderResults(ui, results) {
20327
22101
  }
20328
22102
 
20329
22103
  // src/domains/flows/ensureDeps.ts
20330
- import { dirname as dirname4, join as join14 } from "node:path";
22104
+ import { dirname as dirname4, join as join18 } from "node:path";
20331
22105
  function findEnvDir(flowPath, fs) {
20332
22106
  let dir = dirname4(flowPath);
20333
22107
  while (true) {
20334
- if (fs.existsSync(join14(dir, "package.json")))
22108
+ if (fs.existsSync(join18(dir, "package.json")))
20335
22109
  return dir;
20336
22110
  const parent = dirname4(dir);
20337
22111
  if (parent === dir)
@@ -20357,7 +22131,7 @@ function resolveProjectDirSafe(files, fs) {
20357
22131
  }
20358
22132
 
20359
22133
  // src/domains/flows/expand.ts
20360
- import { join as join15, resolve as resolve3 } from "node:path";
22134
+ import { join as join19, resolve as resolve3 } from "node:path";
20361
22135
 
20362
22136
  // node_modules/tinyglobby/dist/index.mjs
20363
22137
  import { readdir, readdirSync, realpath, realpathSync, stat, statSync } from "fs";
@@ -21346,11 +23120,11 @@ function makePeekFlowMeta(fs) {
21346
23120
  };
21347
23121
  }
21348
23122
  async function resolveGlobRoots(cwd, fs) {
21349
- const qawolfPath = join15(cwd, ".qawolf");
23123
+ const qawolfPath = join19(cwd, ".qawolf");
21350
23124
  let envDirs = [];
21351
23125
  try {
21352
23126
  const entries = await fs.readdirWithTypes(qawolfPath);
21353
- envDirs = entries.filter((e) => e.isDirectory()).map((e) => join15(qawolfPath, e.name));
23127
+ envDirs = entries.filter((e) => e.isDirectory()).map((e) => join19(qawolfPath, e.name));
21354
23128
  } catch {}
21355
23129
  return [cwd, ...envDirs];
21356
23130
  }
@@ -21376,11 +23150,11 @@ async function expandPatterns(patterns, cwd, logger, fs) {
21376
23150
  }
21377
23151
 
21378
23152
  // src/domains/runtimeEnv/clearRuntimeEnv.ts
21379
- import { join as join17 } from "node:path";
23153
+ import { join as join21 } from "node:path";
21380
23154
 
21381
23155
  // src/domains/runtimeEnv/managedEnvDir.ts
21382
23156
  import { createHash } from "node:crypto";
21383
- import { join as join16, resolve as resolve4 } from "node:path";
23157
+ import { join as join20, resolve as resolve4 } from "node:path";
21384
23158
 
21385
23159
  // src/domains/runtimeEnv/pinnedPackages.ts
21386
23160
  var pinnedPackages = [
@@ -21411,19 +23185,19 @@ function managedEnvBaseDir() {
21411
23185
  const override = process.env["QAWOLF_RUNTIME_DIR"]?.trim();
21412
23186
  if (override)
21413
23187
  return resolve4(override);
21414
- return join16(getDataDir(), "runtime");
23188
+ return join20(getDataDir(), "runtime");
21415
23189
  }
21416
23190
  function runStagingRoot() {
21417
23191
  return `${managedEnvBaseDir()}-runs`;
21418
23192
  }
21419
23193
  function managedEnvDir() {
21420
- return join16(managedEnvBaseDir(), managedEnvHash());
23194
+ return join20(managedEnvBaseDir(), managedEnvHash());
21421
23195
  }
21422
23196
  async function scaffoldManagedEnv(dir, fs) {
21423
23197
  await fs.mkdir(dir, { recursive: true });
21424
23198
  const dependencies = Object.fromEntries(pinnedPackages.map(({ name, version }) => [name, version]));
21425
- await fs.writeFile(join16(dir, "package.json"), JSON.stringify({ name: "qawolf-runtime", private: true, dependencies }, undefined, 2));
21426
- await fs.writeFile(join16(dir, ".npmrc"), `@qawolf:registry=https://registry.npmjs.org/
23199
+ await fs.writeFile(join20(dir, "package.json"), JSON.stringify({ name: "qawolf-runtime", private: true, dependencies }, undefined, 2));
23200
+ await fs.writeFile(join20(dir, ".npmrc"), `@qawolf:registry=https://registry.npmjs.org/
21427
23201
  `);
21428
23202
  }
21429
23203
 
@@ -21445,67 +23219,19 @@ async function isManagedRuntimeBase(dir, fs) {
21445
23219
  for (const entry of entries) {
21446
23220
  if (!entry.isDirectory())
21447
23221
  return false;
21448
- if (!await hasRuntimeSentinel(join17(dir, entry.name), fs))
23222
+ if (!await hasRuntimeSentinel(join21(dir, entry.name), fs))
21449
23223
  return false;
21450
23224
  }
21451
23225
  return true;
21452
23226
  }
21453
23227
  async function hasRuntimeSentinel(hashDir, fs) {
21454
23228
  try {
21455
- const pkg = JSON.parse(await fs.readFile(join17(hashDir, "package.json")));
23229
+ const pkg = JSON.parse(await fs.readFile(join21(hashDir, "package.json")));
21456
23230
  return pkg.name === runtimeSentinel;
21457
23231
  } catch {
21458
23232
  return false;
21459
23233
  }
21460
23234
  }
21461
- // src/shell/spawn.ts
21462
- import { spawn } from "node:child_process";
21463
-
21464
- // src/core/cmdEscape.ts
21465
- var metaChars = /([()\][%!^"`<>&|;, *?])/g;
21466
- function escapeCommand(command) {
21467
- return command.replace(metaChars, "^$1");
21468
- }
21469
- function escapeArgument(arg) {
21470
- const escaped = arg.replace(/(?=(\\+?)?)\1"/g, "$1$1\\\"").replace(/(?=(\\+?)?)\1$/, "$1$1");
21471
- return `"${escaped}"`.replace(metaChars, "^$1").replace(metaChars, "^$1");
21472
- }
21473
-
21474
- // src/shell/spawn.ts
21475
- function buildSpawnCommand(cmd, args, platform, env) {
21476
- const options = {};
21477
- if (env)
21478
- options.env = env;
21479
- if (platform !== "win32" || !/\.(cmd|bat)$/i.test(cmd)) {
21480
- return { cmd, args, options };
21481
- }
21482
- const line = [escapeCommand(cmd), ...args.map(escapeArgument)].join(" ");
21483
- return {
21484
- cmd: process.env["ComSpec"] ?? "cmd.exe",
21485
- args: ["/d", "/s", "/c", `"${line}"`],
21486
- options: { ...options, windowsVerbatimArguments: true }
21487
- };
21488
- }
21489
- var defaultSpawn = (cmd, args, opts) => new Promise((resolve) => {
21490
- const env = opts.env ? { ...process.env, ...opts.env } : undefined;
21491
- const built = buildSpawnCommand(cmd, args, opts.platform, env);
21492
- const child = spawn(built.cmd, built.args, built.options);
21493
- let stdout = "";
21494
- let stderr = "";
21495
- child.stdout?.on("data", (chunk) => {
21496
- stdout += String(chunk);
21497
- });
21498
- child.stderr?.on("data", (chunk) => {
21499
- stderr += String(chunk);
21500
- });
21501
- if (opts?.stdin !== undefined) {
21502
- child.stdin?.write(opts.stdin);
21503
- child.stdin?.end();
21504
- }
21505
- child.on("error", (err) => resolve({ exitCode: -1, stdout, stderr: stderr || err.message }));
21506
- child.on("close", (code) => resolve({ exitCode: code ?? -1, stdout, stderr }));
21507
- });
21508
-
21509
23235
  // src/domains/runtimeEnv/npmInstall.ts
21510
23236
  function buildNpmInstallSpawn(cwd, platform) {
21511
23237
  const built = buildSpawnCommand(resolveNpmCommand(platform), ["install", "--legacy-peer-deps"], platform, undefined);
@@ -21526,10 +23252,10 @@ function spawnNpmInstall(cwd, spawn2 = spawn) {
21526
23252
  }
21527
23253
 
21528
23254
  // src/domains/runtimeEnv/resolvePinned.ts
21529
- import { join as join18 } from "node:path";
23255
+ import { join as join22 } from "node:path";
21530
23256
  function readInstalledVersion(dir, pkgName, fs) {
21531
23257
  try {
21532
- const pkgPath = join18(dir, "node_modules", ...pkgName.split("/"), "package.json");
23258
+ const pkgPath = join22(dir, "node_modules", ...pkgName.split("/"), "package.json");
21533
23259
  const raw = JSON.parse(fs.readFileSync(pkgPath));
21534
23260
  const { version } = raw;
21535
23261
  return typeof version === "string" ? version : undefined;
@@ -21577,10 +23303,10 @@ function allPinnedResolved(dir, fs, platform) {
21577
23303
  }
21578
23304
 
21579
23305
  // src/domains/runtimeEnv/shimDeps.ts
21580
- import { join as join20 } from "node:path";
23306
+ import { join as join24 } from "node:path";
21581
23307
 
21582
23308
  // src/shell/resolveExport.ts
21583
- import { join as join19 } from "node:path";
23309
+ import { join as join23 } from "node:path";
21584
23310
  function pickPath(entry, preference) {
21585
23311
  if (typeof entry === "string")
21586
23312
  return entry;
@@ -21612,28 +23338,28 @@ function resolveFromEnvDir(envDir, specifier, preference = "esm", fs = makeDefau
21612
23338
  const pkgName = parts.slice(0, pkgPartCount).join("/");
21613
23339
  const rest = parts.slice(pkgPartCount);
21614
23340
  const subpath = rest.length > 0 ? "./" + rest.join("/") : ".";
21615
- const pkgDir = join19(envDir, "node_modules", ...pkgName.split("/"));
23341
+ const pkgDir = join23(envDir, "node_modules", ...pkgName.split("/"));
21616
23342
  let pkg;
21617
23343
  try {
21618
- pkg = JSON.parse(fs.readFileSync(join19(pkgDir, "package.json")));
23344
+ pkg = JSON.parse(fs.readFileSync(join23(pkgDir, "package.json")));
21619
23345
  } catch (err) {
21620
23346
  if (isNoEntError(err))
21621
- throw new Error(`Package '${pkgName}' not found in ${join19(envDir, "node_modules")}`, { cause: err });
23347
+ throw new Error(`Package '${pkgName}' not found in ${join23(envDir, "node_modules")}`, { cause: err });
21622
23348
  throw err;
21623
23349
  }
21624
23350
  const exportsMap = normalizeExports(pkg.exports);
21625
23351
  const entry = exportsMap?.[subpath];
21626
23352
  const relative = pickPath(entry, preference) ?? (subpath === "." ? pkg.module ?? pkg.main : undefined);
21627
23353
  if (!relative) {
21628
- throw new Error(`No entry for "${subpath}" in exports of ${join19(pkgDir, "package.json")}`);
23354
+ throw new Error(`No entry for "${subpath}" in exports of ${join23(pkgDir, "package.json")}`);
21629
23355
  }
21630
- return join19(pkgDir, relative);
23356
+ return join23(pkgDir, relative);
21631
23357
  }
21632
23358
 
21633
23359
  // src/domains/runtimeEnv/shimDeps.ts
21634
23360
  function readShimMarker(shimDir, fs) {
21635
23361
  try {
21636
- const pkg = JSON.parse(fs.readFileSync(join20(shimDir, "package.json")));
23362
+ const pkg = JSON.parse(fs.readFileSync(join24(shimDir, "package.json")));
21637
23363
  if (pkg._qawolf_format) {
21638
23364
  return {
21639
23365
  _qawolf_version: pkg._qawolf_version ?? "",
@@ -21649,11 +23375,11 @@ function resolveBunBuild(bunBuild) {
21649
23375
  return bunBuild === false ? undefined : { build: bunBuild };
21650
23376
  }
21651
23377
  async function removeStaleShims(flowsDir, flowsDeps, fs) {
21652
- const shimsDir = join20(flowsDir, "node_modules");
23378
+ const shimsDir = join24(flowsDir, "node_modules");
21653
23379
  if (!fs.existsSync(shimsDir))
21654
23380
  return;
21655
23381
  for (const dep of flowsDeps) {
21656
- const shimDepDir = join20(shimsDir, ...dep.split("/"));
23382
+ const shimDepDir = join24(shimsDir, ...dep.split("/"));
21657
23383
  if (readShimMarker(shimDepDir, fs)) {
21658
23384
  await fs.rm(shimDepDir, { recursive: true, force: true });
21659
23385
  }
@@ -21662,17 +23388,17 @@ async function removeStaleShims(flowsDir, flowsDeps, fs) {
21662
23388
  async function buildDepShim(args) {
21663
23389
  const { envDir, flowsDir, dep, fs, build } = args;
21664
23390
  const depParts = dep.split("/");
21665
- const depDir = join20(envDir, "node_modules", ...depParts);
23391
+ const depDir = join24(envDir, "node_modules", ...depParts);
21666
23392
  if (!fs.existsSync(depDir))
21667
23393
  return;
21668
23394
  let depVersion;
21669
23395
  try {
21670
- const pkg = JSON.parse(fs.readFileSync(join20(depDir, "package.json")));
23396
+ const pkg = JSON.parse(fs.readFileSync(join24(depDir, "package.json")));
21671
23397
  depVersion = pkg.version ?? "unknown";
21672
23398
  } catch {
21673
23399
  depVersion = "unknown";
21674
23400
  }
21675
- const shimDir = join20(flowsDir, "node_modules", ...depParts);
23401
+ const shimDir = join24(flowsDir, "node_modules", ...depParts);
21676
23402
  if (fs.existsSync(shimDir)) {
21677
23403
  const marker = readShimMarker(shimDir, fs);
21678
23404
  if (!marker)
@@ -21699,20 +23425,20 @@ async function buildDepShim(args) {
21699
23425
  }
21700
23426
  const shimCode = await output.text();
21701
23427
  await fs.mkdir(shimDir, { recursive: true });
21702
- await fs.writeFile(join20(shimDir, "package.json"), JSON.stringify({
23428
+ await fs.writeFile(join24(shimDir, "package.json"), JSON.stringify({
21703
23429
  name: dep,
21704
23430
  _qawolf_version: depVersion,
21705
23431
  _qawolf_format: "bun-build-v1"
21706
23432
  }));
21707
- await fs.writeFile(join20(shimDir, "index.js"), shimCode);
23433
+ await fs.writeFile(join24(shimDir, "index.js"), shimCode);
21708
23434
  }
21709
23435
  async function shimFlowsDeps(envDir, fs = makeDefaultFs(), bunBuild) {
21710
- const flowsDir = join20(envDir, "node_modules", "@qawolf", "flows");
23436
+ const flowsDir = join24(envDir, "node_modules", "@qawolf", "flows");
21711
23437
  if (!fs.existsSync(flowsDir))
21712
23438
  return;
21713
23439
  let flowsDeps;
21714
23440
  try {
21715
- const flowsPkg = JSON.parse(fs.readFileSync(join20(flowsDir, "package.json")));
23441
+ const flowsPkg = JSON.parse(fs.readFileSync(join24(flowsDir, "package.json")));
21716
23442
  flowsDeps = Object.keys(flowsPkg.dependencies ?? {});
21717
23443
  } catch {
21718
23444
  return;
@@ -21893,11 +23619,11 @@ function collectValue(value, previous) {
21893
23619
  }
21894
23620
 
21895
23621
  // src/shell/manifest/pulledEnv.ts
21896
- import { join as join22 } from "node:path";
23622
+ import { join as join26 } from "node:path";
21897
23623
 
21898
23624
  // src/shell/manifest/io.ts
21899
23625
  import { createHash as createHash2 } from "node:crypto";
21900
- import { join as join21 } from "node:path";
23626
+ import { join as join25 } from "node:path";
21901
23627
  var manifestFilename = ".manifest.json";
21902
23628
  var flowEntrySchema = object({
21903
23629
  path: string2(),
@@ -21919,7 +23645,7 @@ var manifestSchema = object({
21919
23645
  async function readManifest(envDir, fs = makeDefaultFs()) {
21920
23646
  let raw;
21921
23647
  try {
21922
- raw = await fs.readFile(join21(envDir, manifestFilename));
23648
+ raw = await fs.readFile(join25(envDir, manifestFilename));
21923
23649
  } catch (err) {
21924
23650
  if (isNoEntError(err))
21925
23651
  return "missing";
@@ -21953,7 +23679,7 @@ async function readManifest(envDir, fs = makeDefaultFs()) {
21953
23679
  }
21954
23680
  async function writeManifest(envDir, manifest, fs = makeDefaultFs()) {
21955
23681
  const body = JSON.stringify(manifest, undefined, 2);
21956
- await fs.writeFile(join21(envDir, manifestFilename), `${body}
23682
+ await fs.writeFile(join25(envDir, manifestFilename), `${body}
21957
23683
  `);
21958
23684
  }
21959
23685
  function hashFile(absPath, fs = makeDefaultFs()) {
@@ -21979,7 +23705,7 @@ async function findPulledEnv(ref, cwd, fs = makeDefaultFs()) {
21979
23705
  return;
21980
23706
  }
21981
23707
  async function listPulledEnvDirs(cwd, fs = makeDefaultFs()) {
21982
- const root = join22(cwd, ".qawolf");
23708
+ const root = join26(cwd, ".qawolf");
21983
23709
  let entries;
21984
23710
  try {
21985
23711
  entries = await fs.readdirWithTypes(root);
@@ -21992,7 +23718,7 @@ async function listPulledEnvDirs(cwd, fs = makeDefaultFs()) {
21992
23718
  for (const entry of entries) {
21993
23719
  if (!entry.isDirectory())
21994
23720
  continue;
21995
- const envDir = join22(root, entry.name);
23721
+ const envDir = join26(root, entry.name);
21996
23722
  const manifest = await readManifest(envDir, fs);
21997
23723
  if (typeof manifest === "string")
21998
23724
  continue;
@@ -22046,9 +23772,6 @@ async function readEnvLabels(files, readEnvLabel) {
22046
23772
  }
22047
23773
 
22048
23774
  // src/core/suggestNearMiss.ts
22049
- function normalize3(value) {
22050
- return value.toLowerCase().replace(/[-_\s]+/g, " ").trim();
22051
- }
22052
23775
  function editDistance(a, b) {
22053
23776
  let row = Array.from({ length: b.length + 1 }, (_, j) => j);
22054
23777
  for (let i = 1;i <= a.length; i += 1) {
@@ -22070,14 +23793,14 @@ function suggestNearMiss(input, candidates) {
22070
23793
  if (candidates.includes(input))
22071
23794
  return;
22072
23795
  const sorted = [...candidates].sort();
22073
- const normalizedInput = normalize3(input);
22074
- const presentationMatch = sorted.find((candidate) => normalize3(candidate) === normalizedInput);
23796
+ const normalizedInput = normalizeForSearch(input);
23797
+ const presentationMatch = sorted.find((candidate) => normalizeForSearch(candidate) === normalizedInput);
22075
23798
  if (presentationMatch !== undefined)
22076
23799
  return presentationMatch;
22077
23800
  let best;
22078
23801
  let bestDistance = Number.POSITIVE_INFINITY;
22079
23802
  for (const candidate of sorted) {
22080
- const distance = editDistance(normalizedInput, normalize3(candidate));
23803
+ const distance = editDistance(normalizedInput, normalizeForSearch(candidate));
22081
23804
  if (distance < bestDistance) {
22082
23805
  bestDistance = distance;
22083
23806
  best = candidate;
@@ -22387,7 +24110,7 @@ async function flowsListRemote(ctx, pattern, options) {
22387
24110
  }
22388
24111
 
22389
24112
  // src/domains/flows/pull/handler.ts
22390
- import { dirname as dirname8, join as join27, resolve as resolve8 } from "node:path";
24113
+ import { dirname as dirname8, join as join31, resolve as resolve8 } from "node:path";
22391
24114
 
22392
24115
  // src/domains/flows/pull/fetchPhase.ts
22393
24116
  async function fetchTags(ctx, envId) {
@@ -22531,7 +24254,7 @@ async function checkSafety(args) {
22531
24254
  }
22532
24255
 
22533
24256
  // src/domains/flows/pull/reportPull.ts
22534
- import { join as join23 } from "node:path";
24257
+ import { join as join27 } from "node:path";
22535
24258
  function reportPullResult(ui, args) {
22536
24259
  if (ui.mode !== "json")
22537
24260
  return;
@@ -22546,12 +24269,12 @@ function reportPullResult(ui, args) {
22546
24269
  assetDownloadedCount: args.assets.downloadedCount,
22547
24270
  assetReusedCount: args.assets.reusedCount,
22548
24271
  assetSkippedCount: args.assets.skippedCount,
22549
- manifestPath: join23(args.stage.envDir, manifestFilename)
24272
+ manifestPath: join27(args.stage.envDir, manifestFilename)
22550
24273
  }, "");
22551
24274
  }
22552
24275
 
22553
24276
  // src/domains/flows/pull/bundle.ts
22554
- import { join as join24, relative as relative6 } from "node:path";
24277
+ import { join as join28, relative as relative6 } from "node:path";
22555
24278
  async function flattenSingleWrapper(dir, fs = makeDefaultFs()) {
22556
24279
  const entries = await fs.readdir(dir);
22557
24280
  if (entries.length !== 1)
@@ -22559,12 +24282,12 @@ async function flattenSingleWrapper(dir, fs = makeDefaultFs()) {
22559
24282
  const innerName = entries[0];
22560
24283
  if (!innerName)
22561
24284
  return;
22562
- const inner = join24(dir, innerName);
24285
+ const inner = join28(dir, innerName);
22563
24286
  const innerStat = await fs.stat(inner);
22564
24287
  if (!innerStat.isDirectory())
22565
24288
  return;
22566
24289
  for (const e of await fs.readdir(inner)) {
22567
- await fs.rename(join24(inner, e), join24(dir, e));
24290
+ await fs.rename(join28(inner, e), join28(dir, e));
22568
24291
  }
22569
24292
  const leftovers = await fs.readdir(inner);
22570
24293
  if (leftovers.length > 0) {
@@ -22583,7 +24306,7 @@ async function buildManifest(args, fs = makeDefaultFs()) {
22583
24306
  const flowPaths = await walkForFlows(args.bundleDir, fs);
22584
24307
  const flows = await Promise.all(flowPaths.map(async (rel) => ({
22585
24308
  path: toPosix(rel),
22586
- contentHash: await hashFile(join24(args.bundleDir, rel), fs),
24309
+ contentHash: await hashFile(join28(args.bundleDir, rel), fs),
22587
24310
  tags: args.tags?.byPath.get(toPosix(rel))
22588
24311
  })));
22589
24312
  return {
@@ -22607,7 +24330,7 @@ async function walkForFlows(root, fs) {
22607
24330
  async function walk(current, root, out, fs) {
22608
24331
  const entries = await fs.readdirWithTypes(current);
22609
24332
  for (const e of entries) {
22610
- const abs = join24(current, e.name);
24333
+ const abs = join28(current, e.name);
22611
24334
  if (e.isDirectory()) {
22612
24335
  await walk(abs, root, out, fs);
22613
24336
  } else if (e.isFile() && flowExtensions.some((ext) => e.name.endsWith(ext))) {
@@ -22620,11 +24343,11 @@ async function sampleQawolfCommittedAt(bundleDir, fs = makeDefaultFs()) {
22620
24343
  const sample = flowPaths[0];
22621
24344
  if (!sample)
22622
24345
  return;
22623
- return (await fs.stat(join24(bundleDir, sample))).mtime.toISOString();
24346
+ return (await fs.stat(join28(bundleDir, sample))).mtime.toISOString();
22624
24347
  }
22625
24348
 
22626
24349
  // src/domains/flows/pull/applyTeamStorageRewrite.ts
22627
- import { join as join25, relative as relative7 } from "node:path";
24350
+ import { join as join29, relative as relative7 } from "node:path";
22628
24351
 
22629
24352
  // src/domains/flows/pull/rewriteTeamStorage.ts
22630
24353
  var runnerMountPrefix = "/home/wolf/team-storage/";
@@ -22660,7 +24383,7 @@ function isFlowFile(name) {
22660
24383
  async function walk2(dir, out, fs) {
22661
24384
  const entries = await fs.readdirWithTypes(dir);
22662
24385
  for (const e of entries) {
22663
- const abs = join25(dir, e.name);
24386
+ const abs = join29(dir, e.name);
22664
24387
  if (e.isDirectory()) {
22665
24388
  await walk2(abs, out, fs);
22666
24389
  } else if (e.isFile() && isSourceFile(e.name)) {
@@ -22688,7 +24411,7 @@ async function applyTeamStorageRewrite(rootDir, fs = makeDefaultFs()) {
22688
24411
  }
22689
24412
 
22690
24413
  // src/domains/flows/pull/envVars.ts
22691
- import { join as join26 } from "node:path";
24414
+ import { join as join30 } from "node:path";
22692
24415
 
22693
24416
  // src/core/dotenv.ts
22694
24417
  var bareKeyPattern = /^[A-Za-z_][A-Za-z0-9_]*$/;
@@ -22753,7 +24476,7 @@ function unquote(escaped) {
22753
24476
  async function writeEnvFile(envDir, vars, fs = makeDefaultFs()) {
22754
24477
  if (Object.keys(vars).length === 0)
22755
24478
  return;
22756
- await fs.writeFile(join26(envDir, ".env"), serializeDotenv(vars), {
24479
+ await fs.writeFile(join30(envDir, ".env"), serializeDotenv(vars), {
22757
24480
  mode: 384
22758
24481
  });
22759
24482
  }
@@ -22817,11 +24540,11 @@ var Ae = Symbol("flush");
22817
24540
  var As = Symbol("flushChunk");
22818
24541
  var z = Symbol("encoding");
22819
24542
  var Mt = Symbol("decoder");
22820
- var g = Symbol("flowing");
24543
+ var g2 = Symbol("flowing");
22821
24544
  var Qt = Symbol("paused");
22822
24545
  var Bt = Symbol("resume");
22823
24546
  var b = Symbol("buffer");
22824
- var N = Symbol("pipes");
24547
+ var N2 = Symbol("pipes");
22825
24548
  var _ = Symbol("bufferLength");
22826
24549
  var bi = Symbol("bufferPush");
22827
24550
  var Ie = Symbol("bufferShift");
@@ -22869,9 +24592,9 @@ var Li = class extends Fe {
22869
24592
  var Xr = (s) => !!s.objectMode;
22870
24593
  var qr = (s) => !s.objectMode && !!s.encoding && s.encoding !== "buffer";
22871
24594
  var A2 = class extends Di {
22872
- [g] = false;
24595
+ [g2] = false;
22873
24596
  [Qt] = false;
22874
- [N] = [];
24597
+ [N2] = [];
22875
24598
  [b] = [];
22876
24599
  [L];
22877
24600
  [z];
@@ -22894,7 +24617,7 @@ var A2 = class extends Di {
22894
24617
  let e = t[0] || {};
22895
24618
  if (super(), e.objectMode && typeof e.encoding == "string")
22896
24619
  throw new TypeError("Encoding and objectMode may not be used together");
22897
- 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] });
24620
+ 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] });
22898
24621
  let { signal: i } = e;
22899
24622
  i && (this[Jt] = i, i.aborted ? this[xi]() : i.addEventListener("abort", () => this[xi]()));
22900
24623
  }
@@ -22946,7 +24669,7 @@ var A2 = class extends Di {
22946
24669
  else if (typeof t != "string")
22947
24670
  throw new Error("Non-contiguous data written to non-objectMode stream");
22948
24671
  }
22949
- 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]);
24672
+ 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]);
22950
24673
  }
22951
24674
  read(t) {
22952
24675
  if (this[S])
@@ -22967,22 +24690,22 @@ var A2 = class extends Di {
22967
24690
  return this.emit("data", e), !this[b].length && !this[Q] && this.emit("drain"), e;
22968
24691
  }
22969
24692
  end(t, e, i) {
22970
- 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;
24693
+ 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;
22971
24694
  }
22972
24695
  [Bt]() {
22973
- 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"));
24696
+ 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"));
22974
24697
  }
22975
24698
  resume() {
22976
24699
  return this[Bt]();
22977
24700
  }
22978
24701
  pause() {
22979
- this[g] = false, this[Qt] = true, this[C2] = false;
24702
+ this[g2] = false, this[Qt] = true, this[C2] = false;
22980
24703
  }
22981
24704
  get destroyed() {
22982
24705
  return this[S];
22983
24706
  }
22984
24707
  get flowing() {
22985
- return this[g];
24708
+ return this[g2];
22986
24709
  }
22987
24710
  get paused() {
22988
24711
  return this[Qt];
@@ -23000,18 +24723,18 @@ var A2 = class extends Di {
23000
24723
  !t && !this[b].length && !this[Q] && this.emit("drain");
23001
24724
  }
23002
24725
  [As](t) {
23003
- return this.emit("data", t), this[g];
24726
+ return this.emit("data", t), this[g2];
23004
24727
  }
23005
24728
  pipe(t, e) {
23006
24729
  if (this[S])
23007
24730
  return t;
23008
24731
  this[C2] = false;
23009
24732
  let i = this[nt];
23010
- 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;
24733
+ 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;
23011
24734
  }
23012
24735
  unpipe(t) {
23013
- let e = this[N].find((i) => i.dest === t);
23014
- 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());
24736
+ let e = this[N2].find((i) => i.dest === t);
24737
+ 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());
23015
24738
  }
23016
24739
  addListener(t, e) {
23017
24740
  return this.on(t, e);
@@ -23019,7 +24742,7 @@ var A2 = class extends Di {
23019
24742
  on(t, e) {
23020
24743
  let i = super.on(t, e);
23021
24744
  if (t === "data")
23022
- this[C2] = false, this[Rt]++, !this[N].length && !this[g] && this[Bt]();
24745
+ this[C2] = false, this[Rt]++, !this[N2].length && !this[g2] && this[Bt]();
23023
24746
  else if (t === "readable" && this[_] !== 0)
23024
24747
  super.emit("readable");
23025
24748
  else if (Kr(t) && this[nt])
@@ -23035,11 +24758,11 @@ var A2 = class extends Di {
23035
24758
  }
23036
24759
  off(t, e) {
23037
24760
  let i = super.off(t, e);
23038
- return t === "data" && (this[Rt] = this.listeners("data").length, this[Rt] === 0 && !this[C2] && !this[N].length && (this[g] = false)), i;
24761
+ return t === "data" && (this[Rt] = this.listeners("data").length, this[Rt] === 0 && !this[C2] && !this[N2].length && (this[g2] = false)), i;
23039
24762
  }
23040
24763
  removeAllListeners(t) {
23041
24764
  let e = super.removeAllListeners(t);
23042
- return (t === "data" || t === undefined) && (this[Rt] = 0, !this[C2] && !this[N].length && (this[g] = false)), e;
24765
+ return (t === "data" || t === undefined) && (this[Rt] = 0, !this[C2] && !this[N2].length && (this[g2] = false)), e;
23043
24766
  }
23044
24767
  get emittedEnd() {
23045
24768
  return this[nt];
@@ -23075,7 +24798,7 @@ var A2 = class extends Di {
23075
24798
  return this[J](), r;
23076
24799
  }
23077
24800
  [Oi](t) {
23078
- for (let i of this[N])
24801
+ for (let i of this[N2])
23079
24802
  i.dest.write(t) === false && this.pause();
23080
24803
  let e = this[C2] ? false : super.emit("data", t);
23081
24804
  return this[J](), e;
@@ -23087,12 +24810,12 @@ var A2 = class extends Di {
23087
24810
  if (this[Mt]) {
23088
24811
  let e = this[Mt].end();
23089
24812
  if (e) {
23090
- for (let i of this[N])
24813
+ for (let i of this[N2])
23091
24814
  i.dest.write(e);
23092
24815
  this[C2] || super.emit("data", e);
23093
24816
  }
23094
24817
  }
23095
- for (let e of this[N])
24818
+ for (let e of this[N2])
23096
24819
  e.end();
23097
24820
  let t = super.emit("end");
23098
24821
  return this.removeAllListeners("end"), t;
@@ -23168,7 +24891,7 @@ var Jr = I.writev;
23168
24891
  var ht = Symbol("_autoClose");
23169
24892
  var H = Symbol("_close");
23170
24893
  var te = Symbol("_ended");
23171
- var m2 = Symbol("_fd");
24894
+ var m3 = Symbol("_fd");
23172
24895
  var Ni = Symbol("_finished");
23173
24896
  var tt = Symbol("_flags");
23174
24897
  var Ai = Symbol("_flush");
@@ -23195,7 +24918,7 @@ var Me = Symbol("_defaultFlag");
23195
24918
  var bt = Symbol("_errored");
23196
24919
  var _t = class extends A2 {
23197
24920
  [bt] = false;
23198
- [m2];
24921
+ [m3];
23199
24922
  [U2];
23200
24923
  [Ci];
23201
24924
  [j] = false;
@@ -23205,10 +24928,10 @@ var _t = class extends A2 {
23205
24928
  constructor(t, e) {
23206
24929
  if (e = e || {}, super(e), this.readable = true, this.writable = false, typeof t != "string")
23207
24930
  throw new TypeError("path must be a string");
23208
- 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]();
24931
+ 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]();
23209
24932
  }
23210
24933
  get fd() {
23211
- return this[m2];
24934
+ return this[m3];
23212
24935
  }
23213
24936
  get path() {
23214
24937
  return this[U2];
@@ -23223,7 +24946,7 @@ var _t = class extends A2 {
23223
24946
  I.open(this[U2], "r", (t, e) => this[Ht](t, e));
23224
24947
  }
23225
24948
  [Ht](t, e) {
23226
- t ? this[Ut](t) : (this[m2] = e, this.emit("open", e), this[zt]());
24949
+ t ? this[Ut](t) : (this[m3] = e, this.emit("open", e), this[zt]());
23227
24950
  }
23228
24951
  [vi]() {
23229
24952
  return Buffer.allocUnsafe(Math.min(this[Ci], this[ee]));
@@ -23234,16 +24957,16 @@ var _t = class extends A2 {
23234
24957
  let t = this[vi]();
23235
24958
  if (t.length === 0)
23236
24959
  return process.nextTick(() => this[Ii](null, 0, t));
23237
- I.read(this[m2], t, 0, t.length, null, (e, i, r) => this[Ii](e, i, r));
24960
+ I.read(this[m3], t, 0, t.length, null, (e, i, r) => this[Ii](e, i, r));
23238
24961
  }
23239
24962
  }
23240
24963
  [Ii](t, e, i) {
23241
24964
  this[j] = false, t ? this[Ut](t) : this[ki](e, i) && this[zt]();
23242
24965
  }
23243
24966
  [H]() {
23244
- if (this[ht] && typeof this[m2] == "number") {
23245
- let t = this[m2];
23246
- this[m2] = undefined, I.close(t, (e) => e ? this.emit("error", e) : this.emit("close"));
24967
+ if (this[ht] && typeof this[m3] == "number") {
24968
+ let t = this[m3];
24969
+ this[m3] = undefined, I.close(t, (e) => e ? this.emit("error", e) : this.emit("close"));
23247
24970
  }
23248
24971
  }
23249
24972
  [Ut](t) {
@@ -23259,7 +24982,7 @@ var _t = class extends A2 {
23259
24982
  case "finish":
23260
24983
  return false;
23261
24984
  case "drain":
23262
- return typeof this[m2] == "number" && this[zt](), false;
24985
+ return typeof this[m3] == "number" && this[zt](), false;
23263
24986
  case "error":
23264
24987
  return this[bt] ? false : (this[bt] = true, super.emit(t, ...e));
23265
24988
  default:
@@ -23282,7 +25005,7 @@ var Be = class extends _t {
23282
25005
  if (!this[j]) {
23283
25006
  this[j] = true;
23284
25007
  do {
23285
- let e = this[vi](), i = e.length === 0 ? 0 : I.readSync(this[m2], e, 0, e.length, null);
25008
+ let e = this[vi](), i = e.length === 0 ? 0 : I.readSync(this[m3], e, 0, e.length, null);
23286
25009
  if (!this[ki](i, e))
23287
25010
  break;
23288
25011
  } while (true);
@@ -23294,9 +25017,9 @@ var Be = class extends _t {
23294
25017
  }
23295
25018
  }
23296
25019
  [H]() {
23297
- if (this[ht] && typeof this[m2] == "number") {
23298
- let t = this[m2];
23299
- this[m2] = undefined, I.closeSync(t), this.emit("close");
25020
+ if (this[ht] && typeof this[m3] == "number") {
25021
+ let t = this[m3];
25022
+ this[m3] = undefined, I.closeSync(t), this.emit("close");
23300
25023
  }
23301
25024
  }
23302
25025
  };
@@ -23311,15 +25034,15 @@ var et = class extends Qr {
23311
25034
  [U2];
23312
25035
  [ie];
23313
25036
  [ht];
23314
- [m2];
25037
+ [m3];
23315
25038
  [Me];
23316
25039
  [tt];
23317
25040
  [Ni] = false;
23318
25041
  [ot];
23319
25042
  constructor(t, e) {
23320
- 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;
25043
+ 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;
23321
25044
  let i = this[ot] !== undefined ? "r+" : "w";
23322
- this[Me] = e.flags === undefined, this[tt] = e.flags === undefined ? i : e.flags, this[m2] === undefined && this[at]();
25045
+ this[Me] = e.flags === undefined, this[tt] = e.flags === undefined ? i : e.flags, this[m3] === undefined && this[at]();
23323
25046
  }
23324
25047
  emit(t, ...e) {
23325
25048
  if (t === "error") {
@@ -23330,7 +25053,7 @@ var et = class extends Qr {
23330
25053
  return super.emit(t, ...e);
23331
25054
  }
23332
25055
  get fd() {
23333
- return this[m2];
25056
+ return this[m3];
23334
25057
  }
23335
25058
  get path() {
23336
25059
  return this[U2];
@@ -23342,16 +25065,16 @@ var et = class extends Qr {
23342
25065
  I.open(this[U2], this[tt], this[ie], (t, e) => this[Ht](t, e));
23343
25066
  }
23344
25067
  [Ht](t, e) {
23345
- 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]());
25068
+ 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]());
23346
25069
  }
23347
25070
  end(t, e) {
23348
- return t && this.write(t, e), this[te] = true, !this[gt] && !this[Y].length && typeof this[m2] == "number" && this[Pt](null, 0), this;
25071
+ return t && this.write(t, e), this[te] = true, !this[gt] && !this[Y].length && typeof this[m3] == "number" && this[Pt](null, 0), this;
23349
25072
  }
23350
25073
  write(t, e) {
23351
- 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);
25074
+ 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);
23352
25075
  }
23353
25076
  [ve](t) {
23354
- I.write(this[m2], t, 0, t.length, this[ot], (e, i) => this[Pt](e, i));
25077
+ I.write(this[m3], t, 0, t.length, this[ot], (e, i) => this[Pt](e, i));
23355
25078
  }
23356
25079
  [Pt](t, e) {
23357
25080
  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"))));
@@ -23363,13 +25086,13 @@ var et = class extends Qr {
23363
25086
  this[ve](this[Y].pop());
23364
25087
  else {
23365
25088
  let t = this[Y];
23366
- this[Y] = [], Jr(this[m2], t, this[ot], (e, i) => this[Pt](e, i));
25089
+ this[Y] = [], Jr(this[m3], t, this[ot], (e, i) => this[Pt](e, i));
23367
25090
  }
23368
25091
  }
23369
25092
  [H]() {
23370
- if (this[ht] && typeof this[m2] == "number") {
23371
- let t = this[m2];
23372
- this[m2] = undefined, I.close(t, (e) => e ? this.emit("error", e) : this.emit("close"));
25093
+ if (this[ht] && typeof this[m3] == "number") {
25094
+ let t = this[m3];
25095
+ this[m3] = undefined, I.close(t, (e) => e ? this.emit("error", e) : this.emit("close"));
23373
25096
  }
23374
25097
  }
23375
25098
  };
@@ -23389,15 +25112,15 @@ var Wt = class extends et {
23389
25112
  this[Ht](null, t);
23390
25113
  }
23391
25114
  [H]() {
23392
- if (this[ht] && typeof this[m2] == "number") {
23393
- let t = this[m2];
23394
- this[m2] = undefined, I.closeSync(t), this.emit("close");
25115
+ if (this[ht] && typeof this[m3] == "number") {
25116
+ let t = this[m3];
25117
+ this[m3] = undefined, I.closeSync(t), this.emit("close");
23395
25118
  }
23396
25119
  }
23397
25120
  [ve](t) {
23398
25121
  let e = true;
23399
25122
  try {
23400
- this[Pt](null, I.writeSync(this[m2], t, 0, t.length, this[ot])), e = false;
25123
+ this[Pt](null, I.writeSync(this[m3], t, 0, t.length, this[ot])), e = false;
23401
25124
  } finally {
23402
25125
  if (e)
23403
25126
  try {
@@ -25375,7 +27098,7 @@ var Dr = Symbol("onEntry");
25375
27098
  var _s = Symbol("checkFs");
25376
27099
  var Nr = Symbol("checkFs2");
25377
27100
  var Os = Symbol("isReusable");
25378
- var P = Symbol("makeFs");
27101
+ var P2 = Symbol("makeFs");
25379
27102
  var Ts = Symbol("file");
25380
27103
  var xs = Symbol("directory");
25381
27104
  var gi = Symbol("link");
@@ -25663,23 +27386,23 @@ var Xt = class extends rt {
25663
27386
  return;
25664
27387
  }
25665
27388
  if (h || this[Os](t, a))
25666
- return this[P](null, t, i);
27389
+ return this[P2](null, t, i);
25667
27390
  if (a.isDirectory()) {
25668
27391
  if (t.type === "Directory") {
25669
- let l = this.chmod && t.mode && (a.mode & 4095) !== t.mode, c = (d) => this[P](d ?? null, t, i);
27392
+ let l = this.chmod && t.mode && (a.mode & 4095) !== t.mode, c = (d) => this[P2](d ?? null, t, i);
25670
27393
  return l ? u3.chmod(String(t.absolute), Number(t.mode), c) : c();
25671
27394
  }
25672
27395
  if (t.absolute !== this.cwd)
25673
- return u3.rmdir(String(t.absolute), (l) => this[P](l ?? null, t, i));
27396
+ return u3.rmdir(String(t.absolute), (l) => this[P2](l ?? null, t, i));
25674
27397
  }
25675
27398
  if (t.absolute === this.cwd)
25676
- return this[P](null, t, i);
25677
- mo(String(t.absolute), (l) => this[P](l ?? null, t, i));
27399
+ return this[P2](null, t, i);
27400
+ mo(String(t.absolute), (l) => this[P2](l ?? null, t, i));
25678
27401
  });
25679
27402
  };
25680
27403
  this[Oe] ? n() : r();
25681
27404
  }
25682
- [P](t, e, i) {
27405
+ [P2](t, e, i) {
25683
27406
  if (t) {
25684
27407
  this[O](t, e), i();
25685
27408
  return;
@@ -25713,8 +27436,8 @@ var ye = (s) => {
25713
27436
  };
25714
27437
  var xe = class extends Xt {
25715
27438
  sync = true;
25716
- [P](t, e) {
25717
- return super[P](t, e, () => {});
27439
+ [P2](t, e) {
27440
+ return super[P2](t, e, () => {});
25718
27441
  }
25719
27442
  [_s](t) {
25720
27443
  if (!this[Oe]) {
@@ -25735,19 +27458,19 @@ var xe = class extends Xt {
25735
27458
  if (i && (this.keep || this.newer && i.mtime > (t.mtime ?? i.mtime)))
25736
27459
  return this[Ls](t);
25737
27460
  if (e || this[Os](t, i))
25738
- return this[P](null, t);
27461
+ return this[P2](null, t);
25739
27462
  if (i.isDirectory()) {
25740
27463
  if (t.type === "Directory") {
25741
27464
  let o = this.chmod && t.mode && (i.mode & 4095) !== t.mode, [h] = o ? ye(() => {
25742
27465
  u3.chmodSync(String(t.absolute), Number(t.mode));
25743
27466
  }) : [];
25744
- return this[P](h, t);
27467
+ return this[P2](h, t);
25745
27468
  }
25746
27469
  let [n] = ye(() => u3.rmdirSync(String(t.absolute)));
25747
- this[P](n, t);
27470
+ this[P2](n, t);
25748
27471
  }
25749
27472
  let [r] = t.absolute === this.cwd ? [] : ye(() => po(String(t.absolute)));
25750
- this[P](r, t);
27473
+ this[P2](r, t);
25751
27474
  }
25752
27475
  [Ts](t, e) {
25753
27476
  let i = typeof t.mode == "number" ? t.mode & 4095 : this.fmode, r = (h) => {
@@ -26195,8 +27918,8 @@ async function handleFlowsPull(ctx, opts, deps) {
26195
27918
  ctx.ui.error(validation.error);
26196
27919
  return { error: validation.error };
26197
27920
  }
26198
- const destAbs = resolve8(opts.out ?? join27(".qawolf", opts.env));
26199
- const assetsAbs = join27(dirname8(destAbs), "assets");
27921
+ const destAbs = resolve8(opts.out ?? join31(".qawolf", opts.env));
27922
+ const assetsAbs = join31(dirname8(destAbs), "assets");
26200
27923
  await resolvedDeps.fs.mkdir(assetsAbs, { recursive: true });
26201
27924
  const yes = opts.yes ?? false;
26202
27925
  let archive;
@@ -27071,15 +28794,15 @@ async function defaultRunWebFlowDeps(envDir = process.cwd(), signals) {
27071
28794
 
27072
28795
  // src/domains/runtimeEnv/prepareRunDir.ts
27073
28796
  import { createHash as createHash4 } from "node:crypto";
27074
- import { join as join35, resolve as resolve9 } from "node:path";
28797
+ import { join as join39, resolve as resolve9 } from "node:path";
27075
28798
 
27076
28799
  // src/domains/runtimeEnv/execSubpathImports.ts
27077
- import { join as join28 } from "node:path";
28800
+ import { join as join32 } from "node:path";
27078
28801
  var flowSubpathImports = { "#playwright": "playwright" };
27079
28802
  var packageJsonSchema = looseObject({ imports: record(string2(), unknown()).catch({}) }).catch({ imports: {} });
27080
28803
  async function writeExecSubpathImports(args) {
27081
28804
  const { execDir, fs } = args;
27082
- const pkgPath = join28(execDir, "package.json");
28805
+ const pkgPath = join32(execDir, "package.json");
27083
28806
  const pkg = packageJsonSchema.parse(await readPackageJson(pkgPath, fs));
27084
28807
  const merged = { ...pkg, imports: { ...pkg.imports, ...flowSubpathImports } };
27085
28808
  await fs.writeFile(pkgPath, JSON.stringify(merged, undefined, 2));
@@ -27101,10 +28824,10 @@ async function readPackageJson(pkgPath, fs) {
27101
28824
  }
27102
28825
 
27103
28826
  // src/domains/runtimeEnv/innerHop.ts
27104
- import { join as join30 } from "node:path";
28827
+ import { join as join34 } from "node:path";
27105
28828
 
27106
28829
  // src/domains/runtimeEnv/linkPinnedPackages.ts
27107
- import { dirname as dirname9, join as join29 } from "node:path";
28830
+ import { dirname as dirname9, join as join33 } from "node:path";
27108
28831
 
27109
28832
  // src/domains/runtimeEnv/symlinkDir.ts
27110
28833
  import { symlink } from "node:fs/promises";
@@ -27116,16 +28839,16 @@ async function createDirSymlink(source, target) {
27116
28839
  // src/domains/runtimeEnv/linkPinnedPackages.ts
27117
28840
  async function linkPinnedPackages(args) {
27118
28841
  const { depsRoot, nodeModulesDir, fs } = args;
27119
- const managedModules = join29(depsRoot, "node_modules");
28842
+ const managedModules = join33(depsRoot, "node_modules");
27120
28843
  await fs.mkdir(nodeModulesDir, { recursive: true });
27121
28844
  for (const { name } of pinnedPackages) {
27122
28845
  const segments = name.split("/");
27123
- const target = join29(nodeModulesDir, ...segments);
28846
+ const target = join33(nodeModulesDir, ...segments);
27124
28847
  if (segments.length > 1) {
27125
28848
  await fs.mkdir(dirname9(target), { recursive: true });
27126
28849
  }
27127
28850
  await fs.rm(target, { recursive: true, force: true });
27128
- await createDirSymlink(join29(managedModules, ...segments), target);
28851
+ await createDirSymlink(join33(managedModules, ...segments), target);
27129
28852
  }
27130
28853
  }
27131
28854
 
@@ -27134,18 +28857,18 @@ async function populateInnerHop(args) {
27134
28857
  const { depsRoot, execDir, fs } = args;
27135
28858
  await linkPinnedPackages({
27136
28859
  depsRoot,
27137
- nodeModulesDir: join30(execDir, "node_modules"),
28860
+ nodeModulesDir: join34(execDir, "node_modules"),
27138
28861
  fs
27139
28862
  });
27140
28863
  }
27141
28864
 
27142
28865
  // src/domains/runtimeEnv/outerHop.ts
27143
28866
  import { lstat as lstat2 } from "node:fs/promises";
27144
- import { dirname as dirname11, join as join32 } from "node:path";
28867
+ import { dirname as dirname11, join as join36 } from "node:path";
27145
28868
 
27146
28869
  // src/domains/runtimeEnv/carryOverPackages.ts
27147
28870
  import { lstat } from "node:fs/promises";
27148
- import { dirname as dirname10, join as join31 } from "node:path";
28871
+ import { dirname as dirname10, join as join35 } from "node:path";
27149
28872
 
27150
28873
  // src/domains/runtimeEnv/carryOverPlan.ts
27151
28874
  var executorPackageNames = new Set(pinnedPackages.map((p) => p.name));
@@ -27167,12 +28890,12 @@ async function carryOverPackages(args) {
27167
28890
  const linked = [];
27168
28891
  for (const name of names) {
27169
28892
  const segments = name.split("/");
27170
- const target = join31(nodeModulesDir, ...segments);
28893
+ const target = join35(nodeModulesDir, ...segments);
27171
28894
  if (await pathOccupied(target))
27172
28895
  continue;
27173
28896
  if (segments.length > 1)
27174
28897
  await fs.mkdir(dirname10(target), { recursive: true });
27175
- await createDirSymlink(join31(projectModulesDir, ...segments), target);
28898
+ await createDirSymlink(join35(projectModulesDir, ...segments), target);
27176
28899
  linked.push(name);
27177
28900
  }
27178
28901
  return linked;
@@ -27195,18 +28918,18 @@ async function listPackageNames(dir, fs) {
27195
28918
  const names = [];
27196
28919
  for (const entry of entries) {
27197
28920
  if (!entry.startsWith("@")) {
27198
- if (fs.existsSync(join31(dir, entry, "package.json")))
28921
+ if (fs.existsSync(join35(dir, entry, "package.json")))
27199
28922
  names.push(entry);
27200
28923
  continue;
27201
28924
  }
27202
28925
  let scoped;
27203
28926
  try {
27204
- scoped = await fs.readdir(join31(dir, entry));
28927
+ scoped = await fs.readdir(join35(dir, entry));
27205
28928
  } catch {
27206
28929
  continue;
27207
28930
  }
27208
28931
  for (const inner of scoped) {
27209
- if (fs.existsSync(join31(dir, entry, inner, "package.json"))) {
28932
+ if (fs.existsSync(join35(dir, entry, inner, "package.json"))) {
27210
28933
  names.push(`${entry}/${inner}`);
27211
28934
  }
27212
28935
  }
@@ -27224,7 +28947,7 @@ async function populateOuterHop(args) {
27224
28947
  const depNames = Object.keys(installableDeps);
27225
28948
  const { satisfying, rejected } = await findSatisfyingNodeModulesDir(projectDir, depNames, fs);
27226
28949
  if (satisfying !== undefined) {
27227
- await createDirSymlink(satisfying, join32(runDir, "node_modules"));
28950
+ await createDirSymlink(satisfying, join36(runDir, "node_modules"));
27228
28951
  return { mode: "symlink", nodeModulesDir: satisfying };
27229
28952
  }
27230
28953
  if (depNames.length === 0)
@@ -27233,12 +28956,12 @@ async function populateOuterHop(args) {
27233
28956
  await installOuterDeps(runDir, installableDeps, fs, args.install ?? spawnNpmInstall);
27234
28957
  await linkPinnedPackages({
27235
28958
  depsRoot: args.depsRoot,
27236
- nodeModulesDir: join32(runDir, "node_modules"),
28959
+ nodeModulesDir: join36(runDir, "node_modules"),
27237
28960
  fs
27238
28961
  });
27239
28962
  const carriedOver = await carryOverPackages({
27240
- projectModulesDir: join32(projectDir, "node_modules"),
27241
- nodeModulesDir: join32(runDir, "node_modules"),
28963
+ projectModulesDir: join36(projectDir, "node_modules"),
28964
+ nodeModulesDir: join36(runDir, "node_modules"),
27242
28965
  fs
27243
28966
  });
27244
28967
  return { mode: "install", depCount: depNames.length, rejected, carriedOver };
@@ -27247,10 +28970,10 @@ async function findSatisfyingNodeModulesDir(startDir, depNames, fs) {
27247
28970
  const rejected = [];
27248
28971
  let dir = startDir;
27249
28972
  while (true) {
27250
- const candidate = join32(dir, "node_modules");
28973
+ const candidate = join36(dir, "node_modules");
27251
28974
  const stats = await lstatSafe(candidate);
27252
28975
  if (stats !== undefined && stats.isDirectory() && !stats.isSymbolicLink()) {
27253
- const missing = depNames.filter((name) => !fs.existsSync(join32(candidate, name, "package.json")));
28976
+ const missing = depNames.filter((name) => !fs.existsSync(join36(candidate, name, "package.json")));
27254
28977
  if (missing.length === 0)
27255
28978
  return { satisfying: candidate, rejected };
27256
28979
  rejected.push({ dir: candidate, missing });
@@ -27271,7 +28994,7 @@ async function lstatSafe(path) {
27271
28994
  async function readInstallableDeps(projectDir, fs) {
27272
28995
  let content;
27273
28996
  try {
27274
- content = await fs.readFile(join32(projectDir, "package.json"));
28997
+ content = await fs.readFile(join36(projectDir, "package.json"));
27275
28998
  } catch {
27276
28999
  return {};
27277
29000
  }
@@ -27289,8 +29012,8 @@ async function readInstallableDeps(projectDir, fs) {
27289
29012
  return Object.fromEntries(Object.entries(rawDeps).filter(([name]) => !executorPackageNames2.has(name)).filter((entry) => typeof entry[1] === "string"));
27290
29013
  }
27291
29014
  async function installOuterDeps(runDir, deps, fs, install) {
27292
- await fs.writeFile(join32(runDir, "package.json"), JSON.stringify({ name: "qawolf-run", private: true, dependencies: deps }, undefined, 2));
27293
- await fs.writeFile(join32(runDir, ".npmrc"), `@qawolf:registry=https://registry.npmjs.org/
29015
+ await fs.writeFile(join36(runDir, "package.json"), JSON.stringify({ name: "qawolf-run", private: true, dependencies: deps }, undefined, 2));
29016
+ await fs.writeFile(join36(runDir, ".npmrc"), `@qawolf:registry=https://registry.npmjs.org/
27294
29017
  `);
27295
29018
  const result = await install(runDir);
27296
29019
  if (result.exitCode !== 0) {
@@ -27300,14 +29023,14 @@ async function installOuterDeps(runDir, deps, fs, install) {
27300
29023
 
27301
29024
  // src/domains/runtimeEnv/stageFlowFiles.ts
27302
29025
  import { createHash as createHash3 } from "node:crypto";
27303
- import { basename as basename6, dirname as dirname12, join as join34, sep as sep4 } from "node:path";
29026
+ import { basename as basename6, dirname as dirname12, join as join38, sep as sep4 } from "node:path";
27304
29027
 
27305
29028
  // src/shell/copyDir.ts
27306
29029
  import { cp, readdir as readdir2 } from "node:fs/promises";
27307
- import { basename as basename5, join as join33 } from "node:path";
29030
+ import { basename as basename5, join as join37 } from "node:path";
27308
29031
  async function copyDirExcluding(source, destination, excludedNames) {
27309
29032
  const entries = await readdir2(source, { withFileTypes: true });
27310
- await Promise.all(entries.filter((entry) => !excludedNames.has(entry.name)).map((entry) => cp(join33(source, entry.name), join33(destination, entry.name), {
29033
+ await Promise.all(entries.filter((entry) => !excludedNames.has(entry.name)).map((entry) => cp(join37(source, entry.name), join37(destination, entry.name), {
27311
29034
  recursive: true,
27312
29035
  filter: (path) => !excludedNames.has(basename5(path))
27313
29036
  })));
@@ -27324,14 +29047,14 @@ async function stageFlowFiles(args) {
27324
29047
  if (files.length > 1) {
27325
29048
  return Promise.all(files.map((f) => stageOneFile(f, execDir, fs)));
27326
29049
  }
27327
- await Promise.all(files.map((f) => fs.copyFile(f, join34(execDir, basename6(f)))));
27328
- return files.map((f) => join34(execDir, basename6(f)));
29050
+ await Promise.all(files.map((f) => fs.copyFile(f, join38(execDir, basename6(f)))));
29051
+ return files.map((f) => join38(execDir, basename6(f)));
27329
29052
  }
27330
29053
  async function stageOneFile(file, execDir, fs) {
27331
29054
  const dirHash = createHash3("sha256").update(dirname12(file)).digest("hex").slice(0, 8);
27332
- const subDir = join34(execDir, dirHash);
29055
+ const subDir = join38(execDir, dirHash);
27333
29056
  await fs.mkdir(subDir, { recursive: true });
27334
- const dest = join34(subDir, basename6(file));
29057
+ const dest = join38(subDir, basename6(file));
27335
29058
  await fs.copyFile(file, dest);
27336
29059
  return dest;
27337
29060
  }
@@ -27339,7 +29062,7 @@ function remapPath(file, projectDir, execDir) {
27339
29062
  if (file === projectDir)
27340
29063
  return execDir;
27341
29064
  if (file.startsWith(projectDir + sep4)) {
27342
- return join34(execDir, file.slice(projectDir.length + 1));
29065
+ return join38(execDir, file.slice(projectDir.length + 1));
27343
29066
  }
27344
29067
  return;
27345
29068
  }
@@ -27349,10 +29072,10 @@ async function prepareRunDir(args) {
27349
29072
  const { files, projectDir, depsRoot, runRoot } = args;
27350
29073
  const fs = args.fs ?? makeDefaultFs();
27351
29074
  const runId = buildRunId(projectDir, files);
27352
- const runDir = join35(runRoot, runId);
29075
+ const runDir = join39(runRoot, runId);
27353
29076
  await fs.rm(runDir, { recursive: true, force: true });
27354
29077
  await fs.mkdir(runDir, { recursive: true });
27355
- const execDir = join35(runDir, "exec");
29078
+ const execDir = join39(runDir, "exec");
27356
29079
  await fs.mkdir(execDir, { recursive: true });
27357
29080
  await populateInnerHop({ depsRoot, execDir, fs });
27358
29081
  const stagedFiles = await stageFlowFiles({ files, projectDir, execDir, fs });
@@ -27383,17 +29106,17 @@ function buildRunId(projectDir, files) {
27383
29106
  }
27384
29107
 
27385
29108
  // src/shell/testkit.ts
27386
- import { join as join36 } from "node:path";
29109
+ import { join as join40 } from "node:path";
27387
29110
  function notAvailableLocally(name) {
27388
29111
  throw new Error(runnerMessages.notAvailableLocally(name));
27389
29112
  }
27390
29113
  async function loadSdkDeps(cwd) {
27391
- const testkitDir = join36(cwd, "node_modules", "@qawolf", "testkit");
29114
+ const testkitDir = join40(cwd, "node_modules", "@qawolf", "testkit");
27392
29115
  try {
27393
29116
  const clientMod = await importFromPath(resolveFromEnvDir(cwd, "@qawolf/testkit/client"));
27394
29117
  if (typeof clientMod.createTestkitClient !== "function")
27395
29118
  throw new Error("createTestkitClient is not a function");
27396
- const scopeMod = await importFromPath(join36(testkitDir, "dist", "clientScope.js"));
29119
+ const scopeMod = await importFromPath(join40(testkitDir, "dist", "clientScope.js"));
27397
29120
  if (typeof scopeMod.configureTestkitClient !== "function")
27398
29121
  throw new Error("configureTestkitClient is not a function");
27399
29122
  return {
@@ -27451,9 +29174,9 @@ async function registerLazyEmailsClient(getInbox, cwd, deps) {
27451
29174
  }
27452
29175
 
27453
29176
  // src/commands/flows/envVarDeps.ts
27454
- import { join as join37 } from "node:path";
29177
+ import { join as join41 } from "node:path";
27455
29178
  function envFilePath(envDir) {
27456
- return join37(envDir, ".env");
29179
+ return join41(envDir, ".env");
27457
29180
  }
27458
29181
  async function readEnv(envDir, fs) {
27459
29182
  try {
@@ -27577,7 +29300,7 @@ async function selectRunByTag(args) {
27577
29300
  }
27578
29301
 
27579
29302
  // src/commands/flows/runStagedFlows.ts
27580
- import { join as join41 } from "node:path";
29303
+ import { join as join45 } from "node:path";
27581
29304
 
27582
29305
  // src/shell/appium/createAppiumServer.ts
27583
29306
  import { existsSync as existsSync2 } from "node:fs";
@@ -28883,14 +30606,14 @@ async function runWebFlow({
28883
30606
 
28884
30607
  // src/shell/embeddedWorkerCli.ts
28885
30608
  import { existsSync as existsSync3, mkdirSync as mkdirSync3, readFileSync as readFileSync2, writeFileSync as writeFileSync2 } from "node:fs";
28886
- import { basename as basename7, join as join38 } from "node:path";
30609
+ import { basename as basename7, join as join42 } from "node:path";
28887
30610
  var embeddedCliPathEnv = "QAWOLF_EMBEDDED_CLI_PATH";
28888
30611
  function extractEmbeddedWorkerCli() {
28889
30612
  const assetPath = process.env[embeddedCliPathEnv];
28890
30613
  if (assetPath === undefined || assetPath === "")
28891
30614
  return;
28892
- const dir = join38(getDataDir(), "worker");
28893
- const dest = join38(dir, basename7(assetPath));
30615
+ const dir = join42(getDataDir(), "worker");
30616
+ const dest = join42(dir, basename7(assetPath));
28894
30617
  if (existsSync3(dest))
28895
30618
  return dest;
28896
30619
  mkdirSync3(dir, { recursive: true });
@@ -29128,7 +30851,7 @@ ${styleText3("dim", "─".repeat(width))}
29128
30851
  };
29129
30852
  }
29130
30853
  // src/shell/reporter/createJUnitReporter.ts
29131
- import { join as join39 } from "node:path";
30854
+ import { join as join43 } from "node:path";
29132
30855
 
29133
30856
  // src/shell/reporter/junitXml.ts
29134
30857
  function escapeXml(str) {
@@ -29167,7 +30890,7 @@ ${suites}`;
29167
30890
 
29168
30891
  // src/shell/reporter/createJUnitReporter.ts
29169
30892
  function resolveJUnitOutputPath(junit, outputDir) {
29170
- return typeof junit === "string" && junit.trim() !== "" ? junit : join39(outputDir, "junit-report.xml");
30893
+ return typeof junit === "string" && junit.trim() !== "" ? junit : join43(outputDir, "junit-report.xml");
29171
30894
  }
29172
30895
  function createJUnitReporter(deps) {
29173
30896
  const flows = [];
@@ -29258,11 +30981,11 @@ function buildFlowsRunDeps(args) {
29258
30981
 
29259
30982
  // src/commands/flows/loadEnvFile.ts
29260
30983
  import { readFile } from "node:fs/promises";
29261
- import { join as join40 } from "node:path";
30984
+ import { join as join44 } from "node:path";
29262
30985
  async function loadEnvFile(envDir) {
29263
30986
  let content;
29264
30987
  try {
29265
- content = await readFile(join40(envDir, ".env"), "utf8");
30988
+ content = await readFile(join44(envDir, ".env"), "utf8");
29266
30989
  } catch (err) {
29267
30990
  if (isNoEntError(err))
29268
30991
  return;
@@ -29307,7 +31030,7 @@ async function runStagedFlows(args) {
29307
31030
  for (const candidate of staged.outerHop.rejected) {
29308
31031
  log.debug(runnerMessages.outerHopCandidateRejected(candidate.dir, candidate.missing));
29309
31032
  }
29310
- const ownModules = projectDir === undefined ? undefined : join41(projectDir, "node_modules");
31033
+ const ownModules = projectDir === undefined ? undefined : join45(projectDir, "node_modules");
29311
31034
  const own = staged.outerHop.rejected.find((c) => c.dir === ownModules);
29312
31035
  if (own !== undefined) {
29313
31036
  ctx.ui.warn(runnerMessages.outerHopFallbackNotice(own.dir, own.missing));
@@ -29342,7 +31065,7 @@ async function runStagedFlows(args) {
29342
31065
  }
29343
31066
 
29344
31067
  // src/commands/flows/runDefaults.ts
29345
- function makeDefaultDeps2(fs) {
31068
+ function makeDefaultDeps3(fs) {
29346
31069
  return {
29347
31070
  expandPatterns: (patterns, cwd, logger) => expandPatterns(patterns, cwd, logger, fs),
29348
31071
  resolveDepsRoot: (args) => resolveDepsRoot({ ...args, fs }),
@@ -29354,7 +31077,7 @@ function makeDefaultDeps2(fs) {
29354
31077
  };
29355
31078
  }
29356
31079
  async function handleFlowsRun(ctx, pattern, flags, deps, selectors = { tags: [] }, allEnvs = false) {
29357
- const resolvedDeps = deps ?? makeDefaultDeps2(ctx.fs);
31080
+ const resolvedDeps = deps ?? makeDefaultDeps3(ctx.fs);
29358
31081
  const cwd = process.cwd();
29359
31082
  const expandedFiles = await resolvedDeps.expandPatterns(buildPatternArgs(pattern), cwd, ctx.log("flows"));
29360
31083
  ctx.log("flows").debug(`discovered ${pluralize(expandedFiles.length, "flow")}`);
@@ -29403,7 +31126,7 @@ async function chooseEnv(ctx, files, allEnvs) {
29403
31126
  }
29404
31127
 
29405
31128
  // src/commands/flows/hybridRunDefaults.ts
29406
- import { join as join42, resolve as resolve10 } from "node:path";
31129
+ import { join as join46, resolve as resolve10 } from "node:path";
29407
31130
 
29408
31131
  // src/core/selectFlowFiles.ts
29409
31132
  function selectFlowFiles(args) {
@@ -29508,7 +31231,7 @@ async function handleHybridFlowsRun(ctx, pattern, flags, options = {}) {
29508
31231
  if (validation !== "ok") {
29509
31232
  return { error: validation.error, exitCode: 2 };
29510
31233
  }
29511
- const envDir = resolve10(join42(".qawolf", flags.env));
31234
+ const envDir = resolve10(join46(".qawolf", flags.env));
29512
31235
  const patternArgs = buildPatternArgs(pattern);
29513
31236
  const globFlows = () => resolvedDeps.expandPatterns(patternArgs, envDir, ctx.log("flows"));
29514
31237
  let files = await globFlows();
@@ -29704,12 +31427,12 @@ function registerFlowsCommand(program, signals, deps = { withResolvedEnv }) {
29704
31427
  }
29705
31428
 
29706
31429
  // src/domains/init/init.ts
29707
- import { dirname as dirname14, join as join44, relative as relative9 } from "node:path";
31430
+ import { dirname as dirname14, join as join48, relative as relative9 } from "node:path";
29708
31431
 
29709
31432
  // src/domains/init/ensurePackageJson.ts
29710
- import { join as join43 } from "node:path";
31433
+ import { join as join47 } from "node:path";
29711
31434
  async function ensurePackageJson(ctx, yes, deps) {
29712
- const pkgPath = join43(deps.cwd, "package.json");
31435
+ const pkgPath = join47(deps.cwd, "package.json");
29713
31436
  if (!await deps.fs.pathExists(pkgPath)) {
29714
31437
  const confirmed = await ctx.ui.confirm(initMessages.createPackageJsonPrompt, { yes });
29715
31438
  if (!confirmed.ok || !confirmed.value) {
@@ -29835,9 +31558,9 @@ function makeDefaultInitDeps() {
29835
31558
  async function handleInit(ctx, opts, deps = makeDefaultInitDeps()) {
29836
31559
  ctx.ui.gap();
29837
31560
  ctx.ui.intro(initMessages.title);
29838
- const configPath = join44(deps.cwd, "qawolf.config.ts");
29839
- const flowPath = join44(deps.cwd, "src", "flows", "example.flow.ts");
29840
- const gitignorePath = join44(deps.cwd, ".qawolf", ".gitignore");
31561
+ const configPath = join48(deps.cwd, "qawolf.config.ts");
31562
+ const flowPath = join48(deps.cwd, "src", "flows", "example.flow.ts");
31563
+ const gitignorePath = join48(deps.cwd, ".qawolf", ".gitignore");
29841
31564
  await writeWithPrompt(ctx, configPath, qawolfConfigTs, opts.yes, deps);
29842
31565
  await writeWithPrompt(ctx, flowPath, exampleFlowTs, opts.yes, deps);
29843
31566
  await writeWithPrompt(ctx, gitignorePath, qawolfGitignore, opts.yes, deps);
@@ -29875,7 +31598,7 @@ Examples:
29875
31598
  import { parseExecutionTarget as parseExecutionTarget4 } from "@qawolf/flow-targets";
29876
31599
 
29877
31600
  // src/domains/install/android/avd.ts
29878
- import { join as join45 } from "node:path";
31601
+ import { join as join49 } from "node:path";
29879
31602
  async function installAvds(ctx, specs, deps) {
29880
31603
  const sdkManagerPath = sdkManagerBin(deps.androidHome, deps.platform);
29881
31604
  const avdManagerPath = avdManagerBin(deps.androidHome, deps.platform);
@@ -29885,7 +31608,7 @@ async function installAvds(ctx, specs, deps) {
29885
31608
  if (versionResult.exitCode !== 0) {
29886
31609
  throw new Error(installMessages.android.sdkmanagerNotFound(sdkManagerPath));
29887
31610
  }
29888
- const licenseFile = join45(deps.androidHome, "licenses", "android-sdk-license");
31611
+ const licenseFile = join49(deps.androidHome, "licenses", "android-sdk-license");
29889
31612
  if (deps.checkExists(licenseFile)) {
29890
31613
  ctx.ui.info(installMessages.android.licensesAlreadyAccepted);
29891
31614
  } else {
@@ -29900,7 +31623,7 @@ async function installAvds(ctx, specs, deps) {
29900
31623
  for (const spec of specs) {
29901
31624
  if (installedImages.has(spec.systemImage))
29902
31625
  continue;
29903
- const systemImg = join45(deps.androidHome, ...spec.systemImage.split(";"), "system.img");
31626
+ const systemImg = join49(deps.androidHome, ...spec.systemImage.split(";"), "system.img");
29904
31627
  if (deps.checkExists(systemImg)) {
29905
31628
  ctx.ui.info(installMessages.android.systemImageAlreadyInstalled(spec.systemImage));
29906
31629
  installedImages.add(spec.systemImage);
@@ -30198,42 +31921,6 @@ Examples:
30198
31921
  });
30199
31922
  }
30200
31923
 
30201
- // src/core/publicApi/flagKind.ts
30202
- function flagKind(schema) {
30203
- if (schema.type === "string")
30204
- return { ok: true, kind: "string" };
30205
- if (schema.type === "number" || schema.type === "integer") {
30206
- return { ok: true, kind: "number" };
30207
- }
30208
- if (schema.type === "boolean")
30209
- return { ok: true, kind: "boolean" };
30210
- if (schema.type === "array") {
30211
- if (schema.items?.type !== "string") {
30212
- return {
30213
- ok: false,
30214
- reason: "only string arrays can be expressed as flags"
30215
- };
30216
- }
30217
- return { ok: true, kind: "string-array" };
30218
- }
30219
- if (schema.type === "object") {
30220
- if (schema.properties) {
30221
- return {
30222
- ok: false,
30223
- reason: "nested objects cannot be expressed as flags"
30224
- };
30225
- }
30226
- if (typeof schema.additionalProperties === "object" && schema.additionalProperties.type === "string") {
30227
- return { ok: true, kind: "key-value-record" };
30228
- }
30229
- return { ok: false, reason: "nested objects cannot be expressed as flags" };
30230
- }
30231
- return {
30232
- ok: false,
30233
- reason: `type ${schema.type ?? "unknown"} cannot be expressed as a flag`
30234
- };
30235
- }
30236
-
30237
31924
  // src/core/publicApi/flagNames.ts
30238
31925
  var kebabCase = (segment) => segment.replace(/[A-Z]/g, (char) => `-${char.toLowerCase()}`);
30239
31926
  var capitalize = (segment) => `${segment.slice(0, 1).toUpperCase()}${segment.slice(1)}`;
@@ -30252,108 +31939,6 @@ function flagUsage(path, kind) {
30252
31939
  return `${name} <value>`;
30253
31940
  }
30254
31941
 
30255
- // src/core/publicApi/objectShape.ts
30256
- var asObjectShape = (schema) => schema.type === "object" && schema.properties ? { properties: schema.properties, required: new Set(schema.required) } : undefined;
30257
- function mergeIntersection(members) {
30258
- const shape = { properties: {}, required: new Set };
30259
- for (const member of members) {
30260
- const memberShape = asObjectShape(member);
30261
- if (!memberShape) {
30262
- return {
30263
- ok: false,
30264
- field: "",
30265
- reason: "intersection members must be object schemas"
30266
- };
30267
- }
30268
- for (const [field, fieldSchema] of Object.entries(memberShape.properties)) {
30269
- if (field in shape.properties) {
30270
- return {
30271
- ok: false,
30272
- field,
30273
- reason: "field appears in multiple intersection members"
30274
- };
30275
- }
30276
- shape.properties[field] = fieldSchema;
30277
- if (memberShape.required.has(field))
30278
- shape.required.add(field);
30279
- }
30280
- }
30281
- return { ok: true, shape };
30282
- }
30283
- function mergeUnion(branches) {
30284
- const shapes = [];
30285
- for (const branch of branches) {
30286
- const branchShape = asObjectShape(branch);
30287
- if (!branchShape) {
30288
- return {
30289
- ok: false,
30290
- field: "",
30291
- reason: "union branches must be object schemas"
30292
- };
30293
- }
30294
- shapes.push(branchShape);
30295
- }
30296
- const shape = { properties: {}, required: new Set };
30297
- for (const branchShape of shapes) {
30298
- for (const [field, fieldSchema] of Object.entries(branchShape.properties)) {
30299
- const existing = shape.properties[field];
30300
- if (!existing) {
30301
- shape.properties[field] = { ...fieldSchema };
30302
- continue;
30303
- }
30304
- const kinds = [flagKind(existing), flagKind(fieldSchema)];
30305
- if (kinds[0]?.ok && kinds[1]?.ok) {
30306
- if (kinds[0].kind !== kinds[1].kind) {
30307
- return {
30308
- ok: false,
30309
- field,
30310
- reason: "field has conflicting types across union branches"
30311
- };
30312
- }
30313
- } else if (JSON.stringify({ ...existing, description: undefined }) !== JSON.stringify({ ...fieldSchema, description: undefined })) {
30314
- return {
30315
- ok: false,
30316
- field,
30317
- reason: "field has conflicting object schemas across union branches"
30318
- };
30319
- }
30320
- if (!existing.description && fieldSchema.description) {
30321
- existing.description = fieldSchema.description;
30322
- }
30323
- }
30324
- }
30325
- const discriminator = Object.keys(shape.properties).find((field) => {
30326
- const values = shapes.map((branch) => branch.properties[field]?.const);
30327
- return values.every((value) => typeof value === "string") && new Set(values).size === shapes.length && shapes.every((branch) => branch.required.has(field));
30328
- });
30329
- for (const [field, fieldSchema] of Object.entries(shape.properties)) {
30330
- const occurrences = shapes.filter((branch) => (field in branch.properties));
30331
- if (occurrences.length === shapes.length && occurrences.every((branch) => branch.required.has(field))) {
30332
- shape.required.add(field);
30333
- }
30334
- if (field === discriminator) {
30335
- const values = shapes.map((branch) => branch.properties[field]?.const);
30336
- fieldSchema.description = `One of: ${values.join(", ")}`;
30337
- }
30338
- }
30339
- return { ok: true, shape };
30340
- }
30341
- function toObjectShape(schema) {
30342
- if (schema.allOf)
30343
- return mergeIntersection(schema.allOf);
30344
- const branches = schema.oneOf ?? schema.anyOf;
30345
- if (branches)
30346
- return mergeUnion(branches);
30347
- const shape = asObjectShape(schema);
30348
- if (shape)
30349
- return { ok: true, shape };
30350
- return {
30351
- ok: false,
30352
- field: "",
30353
- reason: "contract input must be an object schema"
30354
- };
30355
- }
30356
-
30357
31942
  // src/core/publicApi/flagSpecs.ts
30358
31943
  function describeFlag(schema) {
30359
31944
  const description = schema.description ?? "";
@@ -30548,6 +32133,7 @@ var skippedContractNames = new Set([
30548
32133
  "runner.inspect",
30549
32134
  "runner.inspectMobile",
30550
32135
  "runner.launch",
32136
+ "runner.list",
30551
32137
  "runner.performAction",
30552
32138
  "runner.promoteSnapshot",
30553
32139
  "runner.readJournal",
@@ -30806,7 +32392,7 @@ function describeFailure(failureReason) {
30806
32392
  }
30807
32393
 
30808
32394
  // src/shell/interactiveRunner/collectRunFiles.ts
30809
- import { join as join47 } from "node:path";
32395
+ import { join as join51 } from "node:path";
30810
32396
 
30811
32397
  // src/core/interactiveRunner/getImports.ts
30812
32398
  function getImports(options) {
@@ -30845,7 +32431,7 @@ function isPathAlias(importPath, paths) {
30845
32431
  }
30846
32432
 
30847
32433
  // src/core/interactiveRunner/resolveImportPath.ts
30848
- import { dirname as dirname15, join as join46, normalize as normalize4 } from "node:path/posix";
32434
+ import { dirname as dirname15, join as join50, normalize as normalize3 } from "node:path/posix";
30849
32435
 
30850
32436
  // src/core/interactiveRunner/tsconfigPaths.ts
30851
32437
  function isRecord4(value) {
@@ -30898,7 +32484,7 @@ function candidatesForExplicitExtension(options) {
30898
32484
  }
30899
32485
  function resolveImportPath(options) {
30900
32486
  const aliasTarget = resolvePathAlias(options.importPath, options.tsconfigPaths);
30901
- const resolvedPath = aliasTarget === undefined ? normalize4(join46(dirname15(options.importingFilePath), options.importPath)) : normalize4(aliasTarget);
32487
+ const resolvedPath = aliasTarget === undefined ? normalize3(join50(dirname15(options.importingFilePath), options.importPath)) : normalize3(aliasTarget);
30902
32488
  const hasExplicitExtension = supportedExtensions.some((extension) => options.importPath.endsWith(extension));
30903
32489
  const candidates = hasExplicitExtension ? candidatesForExplicitExtension({
30904
32490
  importPath: options.importPath,
@@ -30922,7 +32508,7 @@ function resolveImportPath(options) {
30922
32508
  var tsconfigPath = "tsconfig.json";
30923
32509
  async function collectRunFiles(options) {
30924
32510
  const held = await listShippablePaths(options);
30925
- const readFile = (path) => options.fs.readFile(join47(options.cwd, path));
32511
+ const readFile = (path) => options.fs.readFile(join51(options.cwd, path));
30926
32512
  const tsconfigContent = held.has(tsconfigPath) ? await readFile(tsconfigPath).catch(() => {
30927
32513
  return;
30928
32514
  }) : undefined;
@@ -30999,13 +32585,13 @@ async function walkImportGraph(options) {
30999
32585
  }
31000
32586
 
31001
32587
  // src/shell/interactiveRunner/makeRunnerId.ts
31002
- import { randomUUID } from "node:crypto";
32588
+ import { randomUUID as randomUUID2 } from "node:crypto";
31003
32589
  function makeRunnerId() {
31004
- return `cli-${randomUUID().replaceAll("-", "").slice(0, 12)}`;
32590
+ return `cli-${randomUUID2().replaceAll("-", "").slice(0, 12)}`;
31005
32591
  }
31006
32592
 
31007
32593
  // src/shell/interactiveRunner/runFilesManifest.ts
31008
- import { join as join48 } from "node:path";
32594
+ import { join as join52 } from "node:path";
31009
32595
  var manifestFileName = "runner-files.json";
31010
32596
  var manifestSchema2 = object({
31011
32597
  files: array(object({ contentHash: string2(), path: string2() })),
@@ -31013,8 +32599,8 @@ var manifestSchema2 = object({
31013
32599
  version: literal(1)
31014
32600
  });
31015
32601
  function makeRunFilesManifestStore(options) {
31016
- const directory = join48(options.cwd, qawolfDir);
31017
- const path = join48(directory, manifestFileName);
32602
+ const directory = join52(options.cwd, qawolfDir);
32603
+ const path = join52(directory, manifestFileName);
31018
32604
  let pendingWrites = 0;
31019
32605
  return {
31020
32606
  async read() {
@@ -31044,7 +32630,7 @@ function parseJson(text) {
31044
32630
  }
31045
32631
 
31046
32632
  // src/shell/interactiveRunner/runnerStore.ts
31047
- import { join as join49 } from "node:path";
32633
+ import { join as join53 } from "node:path";
31048
32634
  var storeFileName = "runner.json";
31049
32635
  var runnersDirName = "runners";
31050
32636
  var storedRunnerSchema = object({
@@ -31063,11 +32649,11 @@ function parseJson2(text) {
31063
32649
  }
31064
32650
  var pendingWrites = 0;
31065
32651
  function makeRunnerStore(options) {
31066
- const directory = join49(options.cwd, qawolfDir);
31067
- const path = join49(directory, storeFileName);
31068
- const runnersDir = join49(directory, runnersDirName);
32652
+ const directory = join53(options.cwd, qawolfDir);
32653
+ const path = join53(directory, storeFileName);
32654
+ const runnersDir = join53(directory, runnersDirName);
31069
32655
  const nextPendingPath = (target) => `${target}.${process.pid}.${++pendingWrites}.tmp`;
31070
- const runnerPath = (runnerId) => join49(runnersDir, `${encodeURIComponent(runnerId)}.json`);
32656
+ const runnerPath = (runnerId) => join53(runnersDir, `${encodeURIComponent(runnerId)}.json`);
31071
32657
  const writeAtomically = async (target, contents) => {
31072
32658
  const pendingPath = nextPendingPath(target);
31073
32659
  await options.fs.writeFile(pendingPath, `${JSON.stringify(contents, undefined, 2)}
@@ -31104,7 +32690,7 @@ function makeRunnerStore(options) {
31104
32690
  readDefaultRunnerId,
31105
32691
  async readRunners() {
31106
32692
  const runners = await Promise.all((await readRunnerFileNames()).map(async (name) => {
31107
- const contents = await options.fs.readFile(join49(runnersDir, name)).catch(() => {
32693
+ const contents = await options.fs.readFile(join53(runnersDir, name)).catch(() => {
31108
32694
  return;
31109
32695
  });
31110
32696
  if (contents === undefined)
@@ -31123,7 +32709,7 @@ function makeRunnerStore(options) {
31123
32709
  await Promise.all(runnerIds.map((runnerId) => options.fs.rm(runnerPath(runnerId), { force: true })));
31124
32710
  const names = await readRunnerFileNames();
31125
32711
  await Promise.all(names.map(async (name) => {
31126
- const runnerFile = join49(runnersDir, name);
32712
+ const runnerFile = join53(runnersDir, name);
31127
32713
  const contents = await options.fs.readFile(runnerFile).catch(() => {
31128
32714
  return;
31129
32715
  });
@@ -31207,7 +32793,7 @@ function registerRunnerHighlightSelectorCommand(runner, signals) {
31207
32793
  }
31208
32794
 
31209
32795
  // src/domains/interactiveRunner/importPackage.ts
31210
- import { join as join50 } from "node:path";
32796
+ import { join as join54 } from "node:path";
31211
32797
 
31212
32798
  // src/core/interactiveRunner/npmDependencies.ts
31213
32799
  var sectionsInPrecedenceOrder = ["dependencies", "devDependencies"];
@@ -31248,7 +32834,7 @@ function readNpmDependencies(packageJsonContent) {
31248
32834
  // src/domains/interactiveRunner/importPackage.ts
31249
32835
  var defaultPackageVersion = "latest";
31250
32836
  async function handleRunnerImportPackage(ctx, options, deps) {
31251
- const content = await deps.readFile(join50(deps.cwd, runPackageJsonPath)).catch(() => {
32837
+ const content = await deps.readFile(join54(deps.cwd, runPackageJsonPath)).catch(() => {
31252
32838
  return;
31253
32839
  });
31254
32840
  if (content === undefined) {
@@ -32052,68 +33638,56 @@ async function handleRunnerLaunch(ctx, options, deps) {
32052
33638
  // src/domains/interactiveRunner/list.ts
32053
33639
  var columns = [
32054
33640
  { header: "id", value: (row) => row.id },
32055
- { header: "family", value: (row) => row.runnerName ?? "" },
33641
+ { header: "family", value: (row) => row.runnerName },
33642
+ { header: "launched here", value: (row) => row.launchedHere ? "yes" : "" },
32056
33643
  { header: "default", value: (row) => row.isDefault ? "yes" : "" }
32057
33644
  ];
32058
- var probeBatchSize = 8;
32059
- async function readCandidates(deps) {
32060
- const held = await deps.store.readRunners();
32061
- const storedDefault = await deps.store.readDefaultRunnerId();
32062
- const environmentValue = deps.env[runnerIdEnvironmentVariable]?.trim();
32063
- const fromEnvironment = environmentValue ? environmentValue : undefined;
32064
- const candidates = [...held];
32065
- for (const id of [storedDefault, fromEnvironment]) {
32066
- if (id !== undefined && !candidates.some((runner) => runner.id === id)) {
32067
- candidates.push({ id });
32068
- }
32069
- }
32070
- return { candidates, defaultRunnerId: fromEnvironment ?? storedDefault };
33645
+ function rank(item) {
33646
+ if (item.isDefault)
33647
+ return 0;
33648
+ return item.launchedHere ? 1 : 2;
32071
33649
  }
32072
- async function probeRunner(ctx, runner) {
32073
- const result = await ctx.platformClient.callPublicApi(publicContractsV1.runner.get, { id: runner.id }, runnerCallOptions);
32074
- if (!result.ok)
32075
- return { ...failureFields(result), ok: false };
32076
- return { ok: true, runner, running: result.value.running };
33650
+ async function readDefaultRunnerId(deps) {
33651
+ const environmentValue = deps.env[runnerIdEnvironmentVariable]?.trim();
33652
+ if (environmentValue)
33653
+ return environmentValue;
33654
+ return deps.store.readDefaultRunnerId();
32077
33655
  }
32078
33656
  async function listRunners(ctx, deps) {
32079
- const { candidates, defaultRunnerId } = await readCandidates(deps);
32080
- const running = [];
32081
- const gone = [];
32082
- for await (const probe of batchMap(candidates, (runner) => probeRunner(ctx, runner), probeBatchSize)) {
32083
- if (!probe.ok)
32084
- return { ...failureFields(probe), ok: false };
32085
- if (probe.running)
32086
- running.push(probe.runner);
32087
- else
32088
- gone.push(probe.runner.id);
32089
- }
33657
+ const listed = await ctx.platformClient.callPublicApi(publicContractsV1.runner.list, {}, runnerCallOptions);
33658
+ if (!listed.ok)
33659
+ return { ...failureFields(listed), ok: false };
33660
+ const defaultRunnerId = await readDefaultRunnerId(deps);
33661
+ const runningIds = new Set(listed.value.runners.map((runner) => runner.id));
33662
+ const held = await deps.store.readRunners();
33663
+ const gone = held.filter((runner) => !runningIds.has(runner.id)).map((runner) => runner.id);
32090
33664
  await deps.store.dropRunners(gone).catch(() => {
32091
33665
  return;
32092
33666
  });
33667
+ const heldIds = new Set(held.map((runner) => runner.id));
33668
+ const items = listed.value.runners.map((runner) => ({
33669
+ id: runner.id,
33670
+ isDefault: runner.id === defaultRunnerId,
33671
+ launchedHere: heldIds.has(runner.id),
33672
+ runnerName: runner.runnerName
33673
+ }));
32093
33674
  return {
32094
- items: [
32095
- ...running.filter((runner) => runner.id === defaultRunnerId),
32096
- ...running.filter((runner) => runner.id !== defaultRunnerId)
32097
- ].map((runner) => ({
32098
- id: runner.id,
32099
- isDefault: runner.id === defaultRunnerId,
32100
- runnerName: runner.runnerName
32101
- })),
33675
+ items: items.sort((left, right) => rank(left) - rank(right) || left.id.localeCompare(right.id)),
32102
33676
  ok: true
32103
33677
  };
32104
33678
  }
32105
- async function handleRunnerList(ctx, deps) {
33679
+ async function handleRunnerList(ctx, options, deps) {
32106
33680
  const listed = await listRunners(ctx, deps);
32107
33681
  if (!listed.ok) {
32108
33682
  return { ...failureFields(listed), exitCode: exitCodes.network };
32109
33683
  }
32110
- const items = listed.items;
33684
+ const items = options.here ? listed.items.filter((item) => item.launchedHere) : listed.items;
32111
33685
  if (ctx.ui.mode === "json") {
32112
33686
  ctx.ui.json(items);
32113
33687
  return;
32114
33688
  }
32115
33689
  if (items.length === 0) {
32116
- ctx.ui.info(interactiveRunnerMessages.noRunners);
33690
+ ctx.ui.info(options.here ? interactiveRunnerMessages.noRunnersHere : interactiveRunnerMessages.noRunners);
32117
33691
  return;
32118
33692
  }
32119
33693
  if (ctx.ui.mode === "agent") {
@@ -32175,6 +33749,7 @@ Examples:
32175
33749
  var listExamples2 = `
32176
33750
  Examples:
32177
33751
  $ qawolf runner list
33752
+ $ qawolf runner list --here
32178
33753
  $ qawolf runner list --json`;
32179
33754
  var keepaliveExamples = `
32180
33755
  Examples:
@@ -32182,7 +33757,7 @@ Examples:
32182
33757
  $ qawolf runner keepalive --runner ci`;
32183
33758
  function registerRunnerLifecycleCommands(runner, signals) {
32184
33759
  declareCommandKind(runner.command("launch"), "write").description("Launch an interactive runner and make it this directory's default").option("--id <id>", "Id to launch under. Relaunching an id attaches to that runner").option("--name <family>", "Runner family to run, e.g. playwright").addHelpText("after", launchExamples).action((opts, command) => withAuthContext(signals, (ctx) => handleRunnerLaunch(ctx, { id: opts.id, name: opts.name }, runnerDeps(ctx)))(opts, command));
32185
- declareCommandKind(runner.command("list"), "read").description("List the runners this directory holds that are still running").addHelpText("after", listExamples2).action((opts, command) => withAuthContext(signals, (ctx) => handleRunnerList(ctx, runnerDeps(ctx)))(opts, command));
33760
+ declareCommandKind(runner.command("list"), "read").description("List the runners running on your team").option("--here", "Only the runners this directory launched").addHelpText("after", listExamples2).action((opts, command) => withAuthContext(signals, (ctx) => handleRunnerList(ctx, { here: opts.here === true }, runnerDeps(ctx)))(opts, command));
32186
33761
  declareCommandKind(runner.command("terminate"), "write").description("End an interactive runner, and the pod it runs on with it").option("--runner <id>", runnerFlagDescription).action((opts, command) => withAuthContext(signals, (ctx) => handleRunnerTerminate(ctx, { runner: opts.runner }, runnerDeps(ctx)))(opts, command));
32187
33762
  declareCommandKind(runner.command("stop-run"), "write").description("Stop what a runner is currently executing, leaving the runner up").option("--runner <id>", runnerFlagDescription).action((opts, command) => withAuthContext(signals, (ctx) => handleRunnerStopRun(ctx, { runner: opts.runner }, runnerDeps(ctx)))(opts, command));
32188
33763
  declareCommandKind(runner.command("keepalive"), "read").description("Reset a runner's inactivity clock, for a caller that pauses between actions").option("--runner <id>", runnerFlagDescription).addHelpText("after", keepaliveExamples).action((opts, command) => withAuthContext(signals, (ctx) => handleRunnerKeepalive(ctx, { runner: opts.runner }, runnerDeps(ctx)))(opts, command));
@@ -32983,4 +34558,4 @@ createProgram({ signals }).parseAsync().catch(() => {
32983
34558
  process.exitCode = 1;
32984
34559
  }).finally(() => flushAndExit(typeof process.exitCode === "number" ? process.exitCode : 0));
32985
34560
 
32986
- //# debugId=C63692CA002E50C264756E2164756E21
34561
+ //# debugId=DA8D4A9E373CFE5C64756E2164756E21