@qawolf/cli 1.23.0 → 1.24.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
@@ -8800,7 +8800,8 @@ var lifecycleMessages = {
8800
8800
 
8801
8801
  // src/core/messages/interactiveRunner/list.ts
8802
8802
  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.",
8803
+ noRunners: "Your team has no runner running. Launch one with qawolf runner launch.",
8804
+ noRunnersHere: "This directory has no runner running. Launch one with qawolf runner launch, or drop --here to see the whole team's runners.",
8804
8805
  runnerCount: (count) => count === 1 ? "1 runner" : `${String(count)} runners`,
8805
8806
  title: "Runners"
8806
8807
  };
@@ -13779,6 +13780,9 @@ var ZodEmail = /* @__PURE__ */ $constructor("ZodEmail", (inst, def) => {
13779
13780
  $ZodEmail.init(inst, def);
13780
13781
  ZodStringFormat.init(inst, def);
13781
13782
  });
13783
+ function email2(params) {
13784
+ return _email(ZodEmail, params);
13785
+ }
13782
13786
  var ZodGUID = /* @__PURE__ */ $constructor("ZodGUID", (inst, def) => {
13783
13787
  $ZodGUID.init(inst, def);
13784
13788
  ZodStringFormat.init(inst, def);
@@ -13842,6 +13846,9 @@ var ZodBase64 = /* @__PURE__ */ $constructor("ZodBase64", (inst, def) => {
13842
13846
  $ZodBase64.init(inst, def);
13843
13847
  ZodStringFormat.init(inst, def);
13844
13848
  });
13849
+ function base642(params) {
13850
+ return _base64(ZodBase64, params);
13851
+ }
13845
13852
  var ZodBase64URL = /* @__PURE__ */ $constructor("ZodBase64URL", (inst, def) => {
13846
13853
  $ZodBase64URL.init(inst, def);
13847
13854
  ZodStringFormat.init(inst, def);
@@ -15751,6 +15758,44 @@ var agentReplySchema = object({
15751
15758
  text: string2().describe("What the AI said.")
15752
15759
  });
15753
15760
 
15761
+ // node_modules/@qawolf/api-contracts/dist/v1/resource.js
15762
+ function resource(fields, { urlFieldDescription = "Absolute URL." } = {}) {
15763
+ return object({
15764
+ ...fields,
15765
+ url: url().describe(urlFieldDescription)
15766
+ });
15767
+ }
15768
+
15769
+ // node_modules/@qawolf/api-contracts/dist/v1/email/resource.js
15770
+ var emailDirectionSchema = _enum(["inbound", "outbound"]);
15771
+ var namedAddressSchema = object({
15772
+ address: string2().describe("The email address."),
15773
+ name: string2().optional().describe("The display name, when the message carried one.")
15774
+ });
15775
+ var emailUrlFieldDescription = "Absolute URL of the workspace. The inbox opens from the app header.";
15776
+ var makeEmailSummaryFields = () => ({
15777
+ createdAt: exports_iso.datetime({ offset: true }).describe("When QA Wolf received or sent the email."),
15778
+ direction: emailDirectionSchema.describe("inbound for mail the workspace received, outbound for mail it sent."),
15779
+ from: namedAddressSchema,
15780
+ id: string2().min(1).describe("The id of the email. Pass it to email.get to read the body."),
15781
+ subject: string2().optional(),
15782
+ to: array(namedAddressSchema)
15783
+ });
15784
+ var makeEmailResourceSchema = () => resource({
15785
+ ...makeEmailSummaryFields(),
15786
+ attachments: array(object({
15787
+ fileName: string2(),
15788
+ 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."),
15789
+ type: string2().optional().describe("The MIME type.")
15790
+ })).optional().describe("Attachment names, positions, and types. Read the content with email.getAttachment."),
15791
+ bcc: array(namedAddressSchema).optional(),
15792
+ cc: array(namedAddressSchema).optional(),
15793
+ html: string2().optional().describe("The HTML body."),
15794
+ 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."),
15795
+ replyTo: array(namedAddressSchema).optional(),
15796
+ text: string2().optional().describe("The plain text body.")
15797
+ }, { urlFieldDescription: emailUrlFieldDescription });
15798
+
15754
15799
  // node_modules/@qawolf/api-contracts/dist/v1/identity/organization.js
15755
15800
  var identityOrganization = object({
15756
15801
  id: string2(),
@@ -15806,14 +15851,6 @@ var defaultIdSchemas = {
15806
15851
  workspace: string2().min(1).describe("The id of the workspace.")
15807
15852
  };
15808
15853
 
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
15854
  // node_modules/@qawolf/api-contracts/dist/v1/run/flowSelection.js
15818
15855
  var makeFlowSelectionSchema = (ids) => object({
15819
15856
  flowIds: array(ids.flow).default([]),
@@ -16343,6 +16380,131 @@ var makeAutomateContract = (ids) => {
16343
16380
  };
16344
16381
  };
16345
16382
 
16383
+ // node_modules/@qawolf/api-contracts/dist/v1/pagination.js
16384
+ var makePaginationInputFields = (limits) => ({
16385
+ cursor: string2().min(1).optional().describe("Opaque cursor copied from the previous response's nextCursor. Omit to fetch the first page."),
16386
+ limit: number2().int().min(1).max(limits.maxLimit).default(limits.defaultLimit).describe(`Maximum rows per page. Defaults to ${limits.defaultLimit}, capped at ${limits.maxLimit}.`)
16387
+ });
16388
+ 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.");
16389
+
16390
+ // node_modules/@qawolf/api-contracts/dist/v1/email/find.js
16391
+ var makeFindEmailsContract = (ids) => {
16392
+ const input = object({
16393
+ 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."),
16394
+ 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."),
16395
+ direction: emailDirectionSchema.default("inbound").describe("inbound lists the mail the workspace received: its inbox. outbound lists the mail it sent."),
16396
+ workspaceId: ids.workspace.optional().describe("The workspace whose emails to list. Required when authenticating with an organization or user API key."),
16397
+ ...makePaginationInputFields({ defaultLimit: 20, maxLimit: 50 })
16398
+ });
16399
+ const output = object({
16400
+ emails: array(resource(makeEmailSummaryFields(), {
16401
+ urlFieldDescription: emailUrlFieldDescription
16402
+ })).describe("The workspace's emails, newest first. Bodies are available via email.get."),
16403
+ nextCursor: nextCursorSchema
16404
+ });
16405
+ return {
16406
+ description: "List the workspace's inbox, or its sent mail, newest first. Read a message body with email.get.",
16407
+ input,
16408
+ kind: "read",
16409
+ name: "email.find",
16410
+ output
16411
+ };
16412
+ };
16413
+
16414
+ // node_modules/@qawolf/api-contracts/dist/v1/email/get.js
16415
+ var makeGetEmailContract = (ids) => {
16416
+ const input = object({
16417
+ id: string2().min(1).describe("The id of the email, from email.find."),
16418
+ workspaceId: ids.workspace.optional().describe("The workspace the email belongs to. Required when authenticating with an organization or user API key.")
16419
+ });
16420
+ const output = makeEmailResourceSchema();
16421
+ return {
16422
+ 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.",
16423
+ input,
16424
+ kind: "read",
16425
+ name: "email.get",
16426
+ output
16427
+ };
16428
+ };
16429
+
16430
+ // node_modules/@qawolf/api-contracts/dist/v1/email/getAttachment.js
16431
+ var makeGetEmailAttachmentContract = (ids) => {
16432
+ const input = object({
16433
+ fileName: string2().min(1).optional().describe("The attachment's file name, as email.get lists it. The first attachment with that name is returned."),
16434
+ id: string2().min(1).describe("The id of the email, from email.find."),
16435
+ 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."),
16436
+ workspaceId: ids.workspace.optional().describe("The workspace the email belongs to. Required when authenticating with an organization or user API key.")
16437
+ }).refine((input) => input.fileName === undefined !== (input.index === undefined), { message: "Provide fileName or index, not both." });
16438
+ const output = resource({
16439
+ contentBase64: string2().describe("The file content, base64 encoded."),
16440
+ fileName: string2(),
16441
+ index: number2().int().min(0).describe("The position of the attachment in the email, from 0."),
16442
+ type: string2().optional().describe("The MIME type.")
16443
+ }, { urlFieldDescription: emailUrlFieldDescription });
16444
+ return {
16445
+ description: "Read one attachment of a workspace email as base64 content, by file name or by position. email.get lists both.",
16446
+ input,
16447
+ kind: "read",
16448
+ name: "email.getAttachment",
16449
+ output
16450
+ };
16451
+ };
16452
+
16453
+ // node_modules/@qawolf/api-contracts/dist/v1/email/listAddresses.js
16454
+ var makeListEmailAddressesContract = (ids) => {
16455
+ const input = object({
16456
+ workspaceId: ids.workspace.optional().describe("The workspace whose inbox addresses to list. Required when authenticating with an organization or user API key."),
16457
+ ...makePaginationInputFields({ defaultLimit: 20, maxLimit: 50 })
16458
+ });
16459
+ const output = object({
16460
+ addresses: array(resource({
16461
+ address: string2().describe("The inbox address. Mail sent to any plus-suffixed form, such as inbox+abc@example.com, lands in the same inbox."),
16462
+ isDefault: boolean2().describe("Whether this is the workspace's default inbox address.")
16463
+ }, { urlFieldDescription: emailUrlFieldDescription })).describe("The workspace's permanent inbox addresses, alphabetical."),
16464
+ nextCursor: nextCursorSchema
16465
+ });
16466
+ return {
16467
+ 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.",
16468
+ input,
16469
+ kind: "read",
16470
+ name: "email.listAddresses",
16471
+ output
16472
+ };
16473
+ };
16474
+
16475
+ // node_modules/@qawolf/api-contracts/dist/v1/email/send.js
16476
+ var recipientsSchema = array(email2()).max(50);
16477
+ var maxAttachments = 10;
16478
+ var maxEmailAttachmentsBase64Length = 30 * 1024 * 1024;
16479
+ var attachmentsBase64Length = (attachments) => Object.values(attachments).reduce((total, content) => total + content.length, 0);
16480
+ var makeSendEmailContract = (ids) => {
16481
+ const input = object({
16482
+ 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."),
16483
+ bcc: recipientsSchema.optional(),
16484
+ cc: recipientsSchema.optional(),
16485
+ 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."),
16486
+ html: string2().optional().describe("The HTML body."),
16487
+ 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."),
16488
+ replyTo: recipientsSchema.optional().describe("Where replies should go instead of the from address."),
16489
+ subject: string2().min(1),
16490
+ text: string2().optional().describe("The plain text body."),
16491
+ to: recipientsSchema.min(1),
16492
+ workspaceId: ids.workspace.optional().describe("The workspace to send from. Required when authenticating with an organization or user API key.")
16493
+ }).refine((email) => email.html || email.text, {
16494
+ message: "Provide text, html, or both."
16495
+ });
16496
+ const output = resource(makeEmailSummaryFields(), {
16497
+ urlFieldDescription: emailUrlFieldDescription
16498
+ });
16499
+ return {
16500
+ 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.",
16501
+ input,
16502
+ kind: "write",
16503
+ name: "email.send",
16504
+ output
16505
+ };
16506
+ };
16507
+
16346
16508
  // node_modules/@qawolf/api-contracts/dist/v1/environment/deleteVariable.js
16347
16509
  var makeDeleteEnvironmentVariableContract = (ids) => {
16348
16510
  const input = object({
@@ -16361,13 +16523,6 @@ var makeDeleteEnvironmentVariableContract = (ids) => {
16361
16523
  };
16362
16524
  };
16363
16525
 
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
16526
  // node_modules/@qawolf/api-contracts/dist/v1/environment/resource.js
16372
16527
  var makeEnvironmentResourceSchema = (ids) => resource({
16373
16528
  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 +16694,27 @@ var makeCreateTagContract = (ids) => {
16539
16694
  };
16540
16695
  };
16541
16696
 
16697
+ // node_modules/@qawolf/api-contracts/dist/v1/flow/skippedFlows.js
16698
+ var makeSkippedFlowsSchema = (ids, { description, reason }) => array(object({
16699
+ flowId: ids.flow,
16700
+ name: string2().describe("The name of the flow."),
16701
+ reason
16702
+ })).describe(description);
16703
+
16542
16704
  // node_modules/@qawolf/api-contracts/dist/v1/flow/addTag.js
16543
16705
  var makeAddTagToFlowsContract = (ids) => {
16544
16706
  const input = object({
16545
16707
  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
16708
  tagName: string2().trim().min(1).describe("The name of an existing tag on the caller's team.")
16547
16709
  });
16548
- const output = makeTagResourceSchema();
16710
+ const output = makeTagResourceSchema().extend({
16711
+ skippedFlows: makeSkippedFlowsSchema(ids, {
16712
+ description: "Requested flows that already carried the tag, so nothing changed for them.",
16713
+ reason: _enum(["already-tagged"]).describe('Why the flow was skipped: "already-tagged" means the flow already carried the tag.')
16714
+ })
16715
+ });
16549
16716
  return {
16550
- description: "Assign an existing tag to the selected flows. Create tags with tag.create.",
16717
+ 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
16718
  input,
16552
16719
  kind: "write",
16553
16720
  name: "flow.addTag",
@@ -16587,6 +16754,27 @@ var makeListFlowsContract = (ids) => {
16587
16754
  };
16588
16755
  };
16589
16756
 
16757
+ // node_modules/@qawolf/api-contracts/dist/v1/flow/removeTag.js
16758
+ var makeRemoveTagFromFlowsContract = (ids) => {
16759
+ const input = object({
16760
+ 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."),
16761
+ tagName: string2().trim().min(1).describe("The name of an existing tag on the caller's team.")
16762
+ });
16763
+ const output = makeTagResourceSchema().extend({
16764
+ skippedFlows: makeSkippedFlowsSchema(ids, {
16765
+ description: "Requested flows that did not carry the tag, so nothing changed for them.",
16766
+ reason: _enum(["not-tagged"]).describe('Why the flow was skipped: "not-tagged" means the flow did not carry the tag.')
16767
+ })
16768
+ });
16769
+ return {
16770
+ 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.",
16771
+ input,
16772
+ kind: "write",
16773
+ name: "flow.removeTag",
16774
+ output
16775
+ };
16776
+ };
16777
+
16590
16778
  // node_modules/@qawolf/api-contracts/dist/v1/flow/update.js
16591
16779
  var makeUpdateFlowContract = (ids) => {
16592
16780
  const input = object({
@@ -16731,6 +16919,24 @@ var makeCreateIssueContract = (ids) => {
16731
16919
  };
16732
16920
  };
16733
16921
 
16922
+ // node_modules/@qawolf/api-contracts/dist/v1/issue/removeFlows.js
16923
+ var makeRemoveFlowsFromIssueContract = (ids) => {
16924
+ const input = object({
16925
+ 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."),
16926
+ issueId: ids.issue
16927
+ });
16928
+ const output = object({
16929
+ issue: makeIssueResourceSchema(ids)
16930
+ });
16931
+ return {
16932
+ 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.",
16933
+ input,
16934
+ kind: "write",
16935
+ name: "issue.removeFlows",
16936
+ output
16937
+ };
16938
+ };
16939
+
16734
16940
  // node_modules/@qawolf/api-contracts/dist/v1/issue/update.js
16735
16941
  var makeUpdateIssueContract = (ids) => {
16736
16942
  const descriptionSchema = string2().describe("The issue description as plain text; each line becomes a paragraph. Markdown is not parsed.");
@@ -17057,6 +17263,22 @@ var makeTerminateRunnerContract = () => {
17057
17263
  };
17058
17264
  };
17059
17265
 
17266
+ // node_modules/@qawolf/api-contracts/dist/v1/runner/list.js
17267
+ var makeListRunnersContract = () => {
17268
+ const input = object({});
17269
+ const output = object({
17270
+ outcome: literal("success"),
17271
+ runners: array(makeRunnerSchema()).describe("The runners running right now, in no particular order. Each entry carries the same fields a launch reports, so any of them can be addressed like a runner you launched yourself.")
17272
+ });
17273
+ return {
17274
+ description: "List the runners running on the caller's team right now. Takes no arguments: the API key already names the team. A runner that was terminated, or that terminated on its own after inactivity, is not listed, because the run system keeps no record of a runner once it is gone. This is a lookup: it never starts a runner, and it does not reset a runner's inactivity clock the way reading its journal does. It is how to find a runner launched from another machine or in an earlier session, when the id it was launched under is no longer at hand.",
17275
+ input,
17276
+ kind: "read",
17277
+ name: "runner.list",
17278
+ output
17279
+ };
17280
+ };
17281
+
17060
17282
  // node_modules/@qawolf/api-contracts/dist/v1/runner/promoteSnapshot.js
17061
17283
  var maxSnapshotPathLength = 1024;
17062
17284
  var makePromoteSnapshotOnRunnerContract = () => {
@@ -17252,6 +17474,13 @@ var makeContractsV1 = (ids) => {
17252
17474
  send: makeAgentSendContract(resolvedIds)
17253
17475
  },
17254
17476
  automate: makeAutomateContract(resolvedIds),
17477
+ email: {
17478
+ find: makeFindEmailsContract(resolvedIds),
17479
+ get: makeGetEmailContract(resolvedIds),
17480
+ getAttachment: makeGetEmailAttachmentContract(resolvedIds),
17481
+ listAddresses: makeListEmailAddressesContract(resolvedIds),
17482
+ send: makeSendEmailContract(resolvedIds)
17483
+ },
17255
17484
  environment: {
17256
17485
  create: makeCreateEnvironmentContract(resolvedIds),
17257
17486
  deleteVariable: makeDeleteEnvironmentVariableContract(resolvedIds),
@@ -17265,6 +17494,7 @@ var makeContractsV1 = (ids) => {
17265
17494
  flow: {
17266
17495
  addTag: makeAddTagToFlowsContract(resolvedIds),
17267
17496
  list: makeListFlowsContract(resolvedIds),
17497
+ removeTag: makeRemoveTagFromFlowsContract(resolvedIds),
17268
17498
  update: makeUpdateFlowContract(resolvedIds)
17269
17499
  },
17270
17500
  issue: {
@@ -17272,6 +17502,7 @@ var makeContractsV1 = (ids) => {
17272
17502
  create: makeCreateIssueContract(resolvedIds),
17273
17503
  find: makeFindIssuesContract(resolvedIds),
17274
17504
  get: makeGetIssueContract(resolvedIds),
17505
+ removeFlows: makeRemoveFlowsFromIssueContract(resolvedIds),
17275
17506
  update: makeUpdateIssueContract(resolvedIds)
17276
17507
  },
17277
17508
  run: {
@@ -17289,6 +17520,7 @@ var makeContractsV1 = (ids) => {
17289
17520
  inspect: makeInspectOnRunnerContract(),
17290
17521
  inspectMobile: makeInspectMobileOnRunnerContract(),
17291
17522
  launch: makeLaunchRunnerContract(),
17523
+ list: makeListRunnersContract(),
17292
17524
  performAction: makePerformActionOnRunnerContract(),
17293
17525
  promoteSnapshot: makePromoteSnapshotOnRunnerContract(),
17294
17526
  readJournal: makeReadRunnerJournalContract(),
@@ -19367,7 +19599,7 @@ function startUpdateCheck(deps) {
19367
19599
  // package.json
19368
19600
  var package_default = {
19369
19601
  name: "@qawolf/cli",
19370
- version: "1.23.0",
19602
+ version: "1.24.0",
19371
19603
  description: "Run and manage QA Wolf flows from the terminal, CI, or an AI agent",
19372
19604
  keywords: [
19373
19605
  "automation",
@@ -19438,7 +19670,7 @@ var package_default = {
19438
19670
  "@clack/prompts": "1.5.1",
19439
19671
  "@napi-rs/keyring": "1.3.0",
19440
19672
  "@oxc-node/core": "0.1.0",
19441
- "@qawolf/api-contracts": "0.40.0",
19673
+ "@qawolf/api-contracts": "0.47.0",
19442
19674
  "@qawolf/emails": "1.1.1",
19443
19675
  "@qawolf/flow-targets": "1.0.0",
19444
19676
  "@qawolf/flows": "0.1.4",
@@ -30548,6 +30780,7 @@ var skippedContractNames = new Set([
30548
30780
  "runner.inspect",
30549
30781
  "runner.inspectMobile",
30550
30782
  "runner.launch",
30783
+ "runner.list",
30551
30784
  "runner.performAction",
30552
30785
  "runner.promoteSnapshot",
30553
30786
  "runner.readJournal",
@@ -32052,68 +32285,56 @@ async function handleRunnerLaunch(ctx, options, deps) {
32052
32285
  // src/domains/interactiveRunner/list.ts
32053
32286
  var columns = [
32054
32287
  { header: "id", value: (row) => row.id },
32055
- { header: "family", value: (row) => row.runnerName ?? "" },
32288
+ { header: "family", value: (row) => row.runnerName },
32289
+ { header: "launched here", value: (row) => row.launchedHere ? "yes" : "" },
32056
32290
  { header: "default", value: (row) => row.isDefault ? "yes" : "" }
32057
32291
  ];
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 };
32292
+ function rank(item) {
32293
+ if (item.isDefault)
32294
+ return 0;
32295
+ return item.launchedHere ? 1 : 2;
32071
32296
  }
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 };
32297
+ async function readDefaultRunnerId(deps) {
32298
+ const environmentValue = deps.env[runnerIdEnvironmentVariable]?.trim();
32299
+ if (environmentValue)
32300
+ return environmentValue;
32301
+ return deps.store.readDefaultRunnerId();
32077
32302
  }
32078
32303
  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
- }
32304
+ const listed = await ctx.platformClient.callPublicApi(publicContractsV1.runner.list, {}, runnerCallOptions);
32305
+ if (!listed.ok)
32306
+ return { ...failureFields(listed), ok: false };
32307
+ const defaultRunnerId = await readDefaultRunnerId(deps);
32308
+ const runningIds = new Set(listed.value.runners.map((runner) => runner.id));
32309
+ const held = await deps.store.readRunners();
32310
+ const gone = held.filter((runner) => !runningIds.has(runner.id)).map((runner) => runner.id);
32090
32311
  await deps.store.dropRunners(gone).catch(() => {
32091
32312
  return;
32092
32313
  });
32314
+ const heldIds = new Set(held.map((runner) => runner.id));
32315
+ const items = listed.value.runners.map((runner) => ({
32316
+ id: runner.id,
32317
+ isDefault: runner.id === defaultRunnerId,
32318
+ launchedHere: heldIds.has(runner.id),
32319
+ runnerName: runner.runnerName
32320
+ }));
32093
32321
  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
- })),
32322
+ items: items.sort((left, right) => rank(left) - rank(right) || left.id.localeCompare(right.id)),
32102
32323
  ok: true
32103
32324
  };
32104
32325
  }
32105
- async function handleRunnerList(ctx, deps) {
32326
+ async function handleRunnerList(ctx, options, deps) {
32106
32327
  const listed = await listRunners(ctx, deps);
32107
32328
  if (!listed.ok) {
32108
32329
  return { ...failureFields(listed), exitCode: exitCodes.network };
32109
32330
  }
32110
- const items = listed.items;
32331
+ const items = options.here ? listed.items.filter((item) => item.launchedHere) : listed.items;
32111
32332
  if (ctx.ui.mode === "json") {
32112
32333
  ctx.ui.json(items);
32113
32334
  return;
32114
32335
  }
32115
32336
  if (items.length === 0) {
32116
- ctx.ui.info(interactiveRunnerMessages.noRunners);
32337
+ ctx.ui.info(options.here ? interactiveRunnerMessages.noRunnersHere : interactiveRunnerMessages.noRunners);
32117
32338
  return;
32118
32339
  }
32119
32340
  if (ctx.ui.mode === "agent") {
@@ -32175,6 +32396,7 @@ Examples:
32175
32396
  var listExamples2 = `
32176
32397
  Examples:
32177
32398
  $ qawolf runner list
32399
+ $ qawolf runner list --here
32178
32400
  $ qawolf runner list --json`;
32179
32401
  var keepaliveExamples = `
32180
32402
  Examples:
@@ -32182,7 +32404,7 @@ Examples:
32182
32404
  $ qawolf runner keepalive --runner ci`;
32183
32405
  function registerRunnerLifecycleCommands(runner, signals) {
32184
32406
  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));
32407
+ 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
32408
  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
32409
  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
32410
  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 +33205,4 @@ createProgram({ signals }).parseAsync().catch(() => {
32983
33205
  process.exitCode = 1;
32984
33206
  }).finally(() => flushAndExit(typeof process.exitCode === "number" ? process.exitCode : 0));
32985
33207
 
32986
- //# debugId=C63692CA002E50C264756E2164756E21
33208
+ //# debugId=599EFC31E23F426D64756E2164756E21
@@ -6102,6 +6102,9 @@ var ZodEmail = /* @__PURE__ */ $constructor("ZodEmail", (inst, def) => {
6102
6102
  $ZodEmail.init(inst, def);
6103
6103
  ZodStringFormat.init(inst, def);
6104
6104
  });
6105
+ function email2(params) {
6106
+ return _email(ZodEmail, params);
6107
+ }
6105
6108
  var ZodGUID = /* @__PURE__ */ $constructor("ZodGUID", (inst, def) => {
6106
6109
  $ZodGUID.init(inst, def);
6107
6110
  ZodStringFormat.init(inst, def);
@@ -6165,6 +6168,9 @@ var ZodBase64 = /* @__PURE__ */ $constructor("ZodBase64", (inst, def) => {
6165
6168
  $ZodBase64.init(inst, def);
6166
6169
  ZodStringFormat.init(inst, def);
6167
6170
  });
6171
+ function base642(params) {
6172
+ return _base64(ZodBase64, params);
6173
+ }
6168
6174
  var ZodBase64URL = /* @__PURE__ */ $constructor("ZodBase64URL", (inst, def) => {
6169
6175
  $ZodBase64URL.init(inst, def);
6170
6176
  ZodStringFormat.init(inst, def);
@@ -6747,6 +6753,44 @@ var agentReplySchema = object({
6747
6753
  text: string2().describe("What the AI said.")
6748
6754
  });
6749
6755
 
6756
+ // node_modules/@qawolf/api-contracts/dist/v1/resource.js
6757
+ function resource(fields, { urlFieldDescription = "Absolute URL." } = {}) {
6758
+ return object({
6759
+ ...fields,
6760
+ url: url().describe(urlFieldDescription)
6761
+ });
6762
+ }
6763
+
6764
+ // node_modules/@qawolf/api-contracts/dist/v1/email/resource.js
6765
+ var emailDirectionSchema = _enum(["inbound", "outbound"]);
6766
+ var namedAddressSchema = object({
6767
+ address: string2().describe("The email address."),
6768
+ name: string2().optional().describe("The display name, when the message carried one.")
6769
+ });
6770
+ var emailUrlFieldDescription = "Absolute URL of the workspace. The inbox opens from the app header.";
6771
+ var makeEmailSummaryFields = () => ({
6772
+ createdAt: exports_iso.datetime({ offset: true }).describe("When QA Wolf received or sent the email."),
6773
+ direction: emailDirectionSchema.describe("inbound for mail the workspace received, outbound for mail it sent."),
6774
+ from: namedAddressSchema,
6775
+ id: string2().min(1).describe("The id of the email. Pass it to email.get to read the body."),
6776
+ subject: string2().optional(),
6777
+ to: array(namedAddressSchema)
6778
+ });
6779
+ var makeEmailResourceSchema = () => resource({
6780
+ ...makeEmailSummaryFields(),
6781
+ attachments: array(object({
6782
+ fileName: string2(),
6783
+ 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."),
6784
+ type: string2().optional().describe("The MIME type.")
6785
+ })).optional().describe("Attachment names, positions, and types. Read the content with email.getAttachment."),
6786
+ bcc: array(namedAddressSchema).optional(),
6787
+ cc: array(namedAddressSchema).optional(),
6788
+ html: string2().optional().describe("The HTML body."),
6789
+ 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."),
6790
+ replyTo: array(namedAddressSchema).optional(),
6791
+ text: string2().optional().describe("The plain text body.")
6792
+ }, { urlFieldDescription: emailUrlFieldDescription });
6793
+
6750
6794
  // node_modules/@qawolf/api-contracts/dist/v1/identity/organization.js
6751
6795
  var identityOrganization = object({
6752
6796
  id: string2(),
@@ -6802,14 +6846,6 @@ var defaultIdSchemas = {
6802
6846
  workspace: string2().min(1).describe("The id of the workspace.")
6803
6847
  };
6804
6848
 
6805
- // node_modules/@qawolf/api-contracts/dist/v1/resource.js
6806
- function resource(fields, { urlFieldDescription = "Absolute URL." } = {}) {
6807
- return object({
6808
- ...fields,
6809
- url: url().describe(urlFieldDescription)
6810
- });
6811
- }
6812
-
6813
6849
  // node_modules/@qawolf/api-contracts/dist/v1/run/flowSelection.js
6814
6850
  var makeFlowSelectionSchema = (ids) => object({
6815
6851
  flowIds: array(ids.flow).default([]),
@@ -7344,6 +7380,131 @@ var makeAutomateContract = (ids) => {
7344
7380
  };
7345
7381
  };
7346
7382
 
7383
+ // node_modules/@qawolf/api-contracts/dist/v1/pagination.js
7384
+ var makePaginationInputFields = (limits) => ({
7385
+ cursor: string2().min(1).optional().describe("Opaque cursor copied from the previous response's nextCursor. Omit to fetch the first page."),
7386
+ limit: number2().int().min(1).max(limits.maxLimit).default(limits.defaultLimit).describe(`Maximum rows per page. Defaults to ${limits.defaultLimit}, capped at ${limits.maxLimit}.`)
7387
+ });
7388
+ 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.");
7389
+
7390
+ // node_modules/@qawolf/api-contracts/dist/v1/email/find.js
7391
+ var makeFindEmailsContract = (ids) => {
7392
+ const input = object({
7393
+ 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."),
7394
+ 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."),
7395
+ direction: emailDirectionSchema.default("inbound").describe("inbound lists the mail the workspace received: its inbox. outbound lists the mail it sent."),
7396
+ workspaceId: ids.workspace.optional().describe("The workspace whose emails to list. Required when authenticating with an organization or user API key."),
7397
+ ...makePaginationInputFields({ defaultLimit: 20, maxLimit: 50 })
7398
+ });
7399
+ const output = object({
7400
+ emails: array(resource(makeEmailSummaryFields(), {
7401
+ urlFieldDescription: emailUrlFieldDescription
7402
+ })).describe("The workspace's emails, newest first. Bodies are available via email.get."),
7403
+ nextCursor: nextCursorSchema
7404
+ });
7405
+ return {
7406
+ description: "List the workspace's inbox, or its sent mail, newest first. Read a message body with email.get.",
7407
+ input,
7408
+ kind: "read",
7409
+ name: "email.find",
7410
+ output
7411
+ };
7412
+ };
7413
+
7414
+ // node_modules/@qawolf/api-contracts/dist/v1/email/get.js
7415
+ var makeGetEmailContract = (ids) => {
7416
+ const input = object({
7417
+ id: string2().min(1).describe("The id of the email, from email.find."),
7418
+ workspaceId: ids.workspace.optional().describe("The workspace the email belongs to. Required when authenticating with an organization or user API key.")
7419
+ });
7420
+ const output = makeEmailResourceSchema();
7421
+ return {
7422
+ 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.",
7423
+ input,
7424
+ kind: "read",
7425
+ name: "email.get",
7426
+ output
7427
+ };
7428
+ };
7429
+
7430
+ // node_modules/@qawolf/api-contracts/dist/v1/email/getAttachment.js
7431
+ var makeGetEmailAttachmentContract = (ids) => {
7432
+ const input = object({
7433
+ fileName: string2().min(1).optional().describe("The attachment's file name, as email.get lists it. The first attachment with that name is returned."),
7434
+ id: string2().min(1).describe("The id of the email, from email.find."),
7435
+ 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."),
7436
+ workspaceId: ids.workspace.optional().describe("The workspace the email belongs to. Required when authenticating with an organization or user API key.")
7437
+ }).refine((input) => input.fileName === undefined !== (input.index === undefined), { message: "Provide fileName or index, not both." });
7438
+ const output = resource({
7439
+ contentBase64: string2().describe("The file content, base64 encoded."),
7440
+ fileName: string2(),
7441
+ index: number2().int().min(0).describe("The position of the attachment in the email, from 0."),
7442
+ type: string2().optional().describe("The MIME type.")
7443
+ }, { urlFieldDescription: emailUrlFieldDescription });
7444
+ return {
7445
+ description: "Read one attachment of a workspace email as base64 content, by file name or by position. email.get lists both.",
7446
+ input,
7447
+ kind: "read",
7448
+ name: "email.getAttachment",
7449
+ output
7450
+ };
7451
+ };
7452
+
7453
+ // node_modules/@qawolf/api-contracts/dist/v1/email/listAddresses.js
7454
+ var makeListEmailAddressesContract = (ids) => {
7455
+ const input = object({
7456
+ workspaceId: ids.workspace.optional().describe("The workspace whose inbox addresses to list. Required when authenticating with an organization or user API key."),
7457
+ ...makePaginationInputFields({ defaultLimit: 20, maxLimit: 50 })
7458
+ });
7459
+ const output = object({
7460
+ addresses: array(resource({
7461
+ address: string2().describe("The inbox address. Mail sent to any plus-suffixed form, such as inbox+abc@example.com, lands in the same inbox."),
7462
+ isDefault: boolean2().describe("Whether this is the workspace's default inbox address.")
7463
+ }, { urlFieldDescription: emailUrlFieldDescription })).describe("The workspace's permanent inbox addresses, alphabetical."),
7464
+ nextCursor: nextCursorSchema
7465
+ });
7466
+ return {
7467
+ 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.",
7468
+ input,
7469
+ kind: "read",
7470
+ name: "email.listAddresses",
7471
+ output
7472
+ };
7473
+ };
7474
+
7475
+ // node_modules/@qawolf/api-contracts/dist/v1/email/send.js
7476
+ var recipientsSchema = array(email2()).max(50);
7477
+ var maxAttachments = 10;
7478
+ var maxEmailAttachmentsBase64Length = 30 * 1024 * 1024;
7479
+ var attachmentsBase64Length = (attachments) => Object.values(attachments).reduce((total, content) => total + content.length, 0);
7480
+ var makeSendEmailContract = (ids) => {
7481
+ const input = object({
7482
+ 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."),
7483
+ bcc: recipientsSchema.optional(),
7484
+ cc: recipientsSchema.optional(),
7485
+ 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."),
7486
+ html: string2().optional().describe("The HTML body."),
7487
+ 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."),
7488
+ replyTo: recipientsSchema.optional().describe("Where replies should go instead of the from address."),
7489
+ subject: string2().min(1),
7490
+ text: string2().optional().describe("The plain text body."),
7491
+ to: recipientsSchema.min(1),
7492
+ workspaceId: ids.workspace.optional().describe("The workspace to send from. Required when authenticating with an organization or user API key.")
7493
+ }).refine((email) => email.html || email.text, {
7494
+ message: "Provide text, html, or both."
7495
+ });
7496
+ const output = resource(makeEmailSummaryFields(), {
7497
+ urlFieldDescription: emailUrlFieldDescription
7498
+ });
7499
+ return {
7500
+ 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.",
7501
+ input,
7502
+ kind: "write",
7503
+ name: "email.send",
7504
+ output
7505
+ };
7506
+ };
7507
+
7347
7508
  // node_modules/@qawolf/api-contracts/dist/v1/environment/deleteVariable.js
7348
7509
  var makeDeleteEnvironmentVariableContract = (ids) => {
7349
7510
  const input = object({
@@ -7362,13 +7523,6 @@ var makeDeleteEnvironmentVariableContract = (ids) => {
7362
7523
  };
7363
7524
  };
7364
7525
 
7365
- // node_modules/@qawolf/api-contracts/dist/v1/pagination.js
7366
- var makePaginationInputFields = (limits) => ({
7367
- cursor: string2().min(1).optional().describe("Opaque cursor copied from the previous response's nextCursor. Omit to fetch the first page."),
7368
- limit: number2().int().min(1).max(limits.maxLimit).default(limits.defaultLimit).describe(`Maximum rows per page. Defaults to ${limits.defaultLimit}, capped at ${limits.maxLimit}.`)
7369
- });
7370
- 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.");
7371
-
7372
7526
  // node_modules/@qawolf/api-contracts/dist/v1/environment/resource.js
7373
7527
  var makeEnvironmentResourceSchema = (ids) => resource({
7374
7528
  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."),
@@ -7540,15 +7694,27 @@ var makeCreateTagContract = (ids) => {
7540
7694
  };
7541
7695
  };
7542
7696
 
7697
+ // node_modules/@qawolf/api-contracts/dist/v1/flow/skippedFlows.js
7698
+ var makeSkippedFlowsSchema = (ids, { description, reason }) => array(object({
7699
+ flowId: ids.flow,
7700
+ name: string2().describe("The name of the flow."),
7701
+ reason
7702
+ })).describe(description);
7703
+
7543
7704
  // node_modules/@qawolf/api-contracts/dist/v1/flow/addTag.js
7544
7705
  var makeAddTagToFlowsContract = (ids) => {
7545
7706
  const input = object({
7546
7707
  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."),
7547
7708
  tagName: string2().trim().min(1).describe("The name of an existing tag on the caller's team.")
7548
7709
  });
7549
- const output = makeTagResourceSchema();
7710
+ const output = makeTagResourceSchema().extend({
7711
+ skippedFlows: makeSkippedFlowsSchema(ids, {
7712
+ description: "Requested flows that already carried the tag, so nothing changed for them.",
7713
+ reason: _enum(["already-tagged"]).describe('Why the flow was skipped: "already-tagged" means the flow already carried the tag.')
7714
+ })
7715
+ });
7550
7716
  return {
7551
- description: "Assign an existing tag to the selected flows. Create tags with tag.create.",
7717
+ description: "Assign an existing tag to the selected flows. Create tags with tag.create. Flows that already carry the tag are reported in skippedFlows.",
7552
7718
  input,
7553
7719
  kind: "write",
7554
7720
  name: "flow.addTag",
@@ -7588,6 +7754,27 @@ var makeListFlowsContract = (ids) => {
7588
7754
  };
7589
7755
  };
7590
7756
 
7757
+ // node_modules/@qawolf/api-contracts/dist/v1/flow/removeTag.js
7758
+ var makeRemoveTagFromFlowsContract = (ids) => {
7759
+ const input = object({
7760
+ 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."),
7761
+ tagName: string2().trim().min(1).describe("The name of an existing tag on the caller's team.")
7762
+ });
7763
+ const output = makeTagResourceSchema().extend({
7764
+ skippedFlows: makeSkippedFlowsSchema(ids, {
7765
+ description: "Requested flows that did not carry the tag, so nothing changed for them.",
7766
+ reason: _enum(["not-tagged"]).describe('Why the flow was skipped: "not-tagged" means the flow did not carry the tag.')
7767
+ })
7768
+ });
7769
+ return {
7770
+ 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.",
7771
+ input,
7772
+ kind: "write",
7773
+ name: "flow.removeTag",
7774
+ output
7775
+ };
7776
+ };
7777
+
7591
7778
  // node_modules/@qawolf/api-contracts/dist/v1/flow/update.js
7592
7779
  var makeUpdateFlowContract = (ids) => {
7593
7780
  const input = object({
@@ -7732,6 +7919,24 @@ var makeCreateIssueContract = (ids) => {
7732
7919
  };
7733
7920
  };
7734
7921
 
7922
+ // node_modules/@qawolf/api-contracts/dist/v1/issue/removeFlows.js
7923
+ var makeRemoveFlowsFromIssueContract = (ids) => {
7924
+ const input = object({
7925
+ 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."),
7926
+ issueId: ids.issue
7927
+ });
7928
+ const output = object({
7929
+ issue: makeIssueResourceSchema(ids)
7930
+ });
7931
+ return {
7932
+ 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.",
7933
+ input,
7934
+ kind: "write",
7935
+ name: "issue.removeFlows",
7936
+ output
7937
+ };
7938
+ };
7939
+
7735
7940
  // node_modules/@qawolf/api-contracts/dist/v1/issue/update.js
7736
7941
  var makeUpdateIssueContract = (ids) => {
7737
7942
  const descriptionSchema = string2().describe("The issue description as plain text; each line becomes a paragraph. Markdown is not parsed.");
@@ -8058,6 +8263,22 @@ var makeTerminateRunnerContract = () => {
8058
8263
  };
8059
8264
  };
8060
8265
 
8266
+ // node_modules/@qawolf/api-contracts/dist/v1/runner/list.js
8267
+ var makeListRunnersContract = () => {
8268
+ const input = object({});
8269
+ const output = object({
8270
+ outcome: literal("success"),
8271
+ 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.")
8272
+ });
8273
+ return {
8274
+ 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.",
8275
+ input,
8276
+ kind: "read",
8277
+ name: "runner.list",
8278
+ output
8279
+ };
8280
+ };
8281
+
8061
8282
  // node_modules/@qawolf/api-contracts/dist/v1/runner/promoteSnapshot.js
8062
8283
  var maxSnapshotPathLength = 1024;
8063
8284
  var makePromoteSnapshotOnRunnerContract = () => {
@@ -8253,6 +8474,13 @@ var makeContractsV1 = (ids) => {
8253
8474
  send: makeAgentSendContract(resolvedIds)
8254
8475
  },
8255
8476
  automate: makeAutomateContract(resolvedIds),
8477
+ email: {
8478
+ find: makeFindEmailsContract(resolvedIds),
8479
+ get: makeGetEmailContract(resolvedIds),
8480
+ getAttachment: makeGetEmailAttachmentContract(resolvedIds),
8481
+ listAddresses: makeListEmailAddressesContract(resolvedIds),
8482
+ send: makeSendEmailContract(resolvedIds)
8483
+ },
8256
8484
  environment: {
8257
8485
  create: makeCreateEnvironmentContract(resolvedIds),
8258
8486
  deleteVariable: makeDeleteEnvironmentVariableContract(resolvedIds),
@@ -8266,6 +8494,7 @@ var makeContractsV1 = (ids) => {
8266
8494
  flow: {
8267
8495
  addTag: makeAddTagToFlowsContract(resolvedIds),
8268
8496
  list: makeListFlowsContract(resolvedIds),
8497
+ removeTag: makeRemoveTagFromFlowsContract(resolvedIds),
8269
8498
  update: makeUpdateFlowContract(resolvedIds)
8270
8499
  },
8271
8500
  issue: {
@@ -8273,6 +8502,7 @@ var makeContractsV1 = (ids) => {
8273
8502
  create: makeCreateIssueContract(resolvedIds),
8274
8503
  find: makeFindIssuesContract(resolvedIds),
8275
8504
  get: makeGetIssueContract(resolvedIds),
8505
+ removeFlows: makeRemoveFlowsFromIssueContract(resolvedIds),
8276
8506
  update: makeUpdateIssueContract(resolvedIds)
8277
8507
  },
8278
8508
  run: {
@@ -8290,6 +8520,7 @@ var makeContractsV1 = (ids) => {
8290
8520
  inspect: makeInspectOnRunnerContract(),
8291
8521
  inspectMobile: makeInspectMobileOnRunnerContract(),
8292
8522
  launch: makeLaunchRunnerContract(),
8523
+ list: makeListRunnersContract(),
8293
8524
  performAction: makePerformActionOnRunnerContract(),
8294
8525
  promoteSnapshot: makePromoteSnapshotOnRunnerContract(),
8295
8526
  readJournal: makeReadRunnerJournalContract(),
@@ -8306,20 +8537,6 @@ var makeContractsV1 = (ids) => {
8306
8537
  };
8307
8538
  var publicContractsV1 = makeContractsV1(defaultIdSchemas);
8308
8539
 
8309
- // src/core/batchMap.ts
8310
- async function* batchMap(items, fn, size) {
8311
- if (size < 1) {
8312
- throw new Error("batchMap size must be greater than 0");
8313
- }
8314
- for (let i = 0;i < items.length; i += size) {
8315
- const batch = items.slice(i, i + size);
8316
- const results = await Promise.all(batch.map(fn));
8317
- for (const result of results) {
8318
- yield result;
8319
- }
8320
- }
8321
- }
8322
-
8323
8540
  // src/core/formatSeconds.ts
8324
8541
  function formatSeconds(ms) {
8325
8542
  return `${Math.round(ms / 1000)}s`;
@@ -8628,7 +8845,8 @@ var lifecycleMessages = {
8628
8845
 
8629
8846
  // src/core/messages/interactiveRunner/list.ts
8630
8847
  var listMessages = {
8631
- noRunners: "This directory has no runner running. Launch one with qawolf runner launch, or pass --runner to address a runner started elsewhere.",
8848
+ noRunners: "Your team has no runner running. Launch one with qawolf runner launch.",
8849
+ noRunnersHere: "This directory has no runner running. Launch one with qawolf runner launch, or drop --here to see the whole team's runners.",
8632
8850
  runnerCount: (count) => count === 1 ? "1 runner" : `${String(count)} runners`,
8633
8851
  title: "Runners"
8634
8852
  };
@@ -8776,50 +8994,37 @@ var runnerCallOptions = { timeoutMs: 90000 };
8776
8994
  var runnerIdEnvironmentVariable = "QAWOLF_RUNNER_ID";
8777
8995
 
8778
8996
  // src/domains/interactiveRunner/list.ts
8779
- var probeBatchSize = 8;
8780
- async function readCandidates(deps) {
8781
- const held = await deps.store.readRunners();
8782
- const storedDefault = await deps.store.readDefaultRunnerId();
8783
- const environmentValue = deps.env[runnerIdEnvironmentVariable]?.trim();
8784
- const fromEnvironment = environmentValue ? environmentValue : undefined;
8785
- const candidates = [...held];
8786
- for (const id of [storedDefault, fromEnvironment]) {
8787
- if (id !== undefined && !candidates.some((runner) => runner.id === id)) {
8788
- candidates.push({ id });
8789
- }
8790
- }
8791
- return { candidates, defaultRunnerId: fromEnvironment ?? storedDefault };
8997
+ function rank(item) {
8998
+ if (item.isDefault)
8999
+ return 0;
9000
+ return item.launchedHere ? 1 : 2;
8792
9001
  }
8793
- async function probeRunner(ctx, runner) {
8794
- const result = await ctx.platformClient.callPublicApi(publicContractsV1.runner.get, { id: runner.id }, runnerCallOptions);
8795
- if (!result.ok)
8796
- return { ...failureFields(result), ok: false };
8797
- return { ok: true, runner, running: result.value.running };
9002
+ async function readDefaultRunnerId(deps) {
9003
+ const environmentValue = deps.env[runnerIdEnvironmentVariable]?.trim();
9004
+ if (environmentValue)
9005
+ return environmentValue;
9006
+ return deps.store.readDefaultRunnerId();
8798
9007
  }
8799
9008
  async function listRunners(ctx, deps) {
8800
- const { candidates, defaultRunnerId } = await readCandidates(deps);
8801
- const running = [];
8802
- const gone = [];
8803
- for await (const probe of batchMap(candidates, (runner) => probeRunner(ctx, runner), probeBatchSize)) {
8804
- if (!probe.ok)
8805
- return { ...failureFields(probe), ok: false };
8806
- if (probe.running)
8807
- running.push(probe.runner);
8808
- else
8809
- gone.push(probe.runner.id);
8810
- }
9009
+ const listed = await ctx.platformClient.callPublicApi(publicContractsV1.runner.list, {}, runnerCallOptions);
9010
+ if (!listed.ok)
9011
+ return { ...failureFields(listed), ok: false };
9012
+ const defaultRunnerId = await readDefaultRunnerId(deps);
9013
+ const runningIds = new Set(listed.value.runners.map((runner) => runner.id));
9014
+ const held = await deps.store.readRunners();
9015
+ const gone = held.filter((runner) => !runningIds.has(runner.id)).map((runner) => runner.id);
8811
9016
  await deps.store.dropRunners(gone).catch(() => {
8812
9017
  return;
8813
9018
  });
9019
+ const heldIds = new Set(held.map((runner) => runner.id));
9020
+ const items = listed.value.runners.map((runner) => ({
9021
+ id: runner.id,
9022
+ isDefault: runner.id === defaultRunnerId,
9023
+ launchedHere: heldIds.has(runner.id),
9024
+ runnerName: runner.runnerName
9025
+ }));
8814
9026
  return {
8815
- items: [
8816
- ...running.filter((runner) => runner.id === defaultRunnerId),
8817
- ...running.filter((runner) => runner.id !== defaultRunnerId)
8818
- ].map((runner) => ({
8819
- id: runner.id,
8820
- isDefault: runner.id === defaultRunnerId,
8821
- runnerName: runner.runnerName
8822
- })),
9027
+ items: items.sort((left, right) => rank(left) - rank(right) || left.id.localeCompare(right.id)),
8823
9028
  ok: true
8824
9029
  };
8825
9030
  }
@@ -12394,4 +12599,4 @@ export {
12394
12599
  createRunnerSdk
12395
12600
  };
12396
12601
 
12397
- //# debugId=57D8825DED309B1264756E2164756E21
12602
+ //# debugId=231C026DA9D7E0DD64756E2164756E21
@@ -108,11 +108,15 @@ export type SubmittedRun = {
108
108
  fileSync: FileSync;
109
109
  runId: string;
110
110
  };
111
- /** Runners this working directory launched, which the API has no notion of. */
111
+ /**
112
+ * A runner running on the team. `isDefault` and `launchedHere` are this working
113
+ * directory's own knowledge, which the API has no notion of.
114
+ */
112
115
  export type ListedRunner = {
113
116
  id: string;
114
117
  isDefault: boolean;
115
- runnerName: string | undefined;
118
+ launchedHere: boolean;
119
+ runnerName: string;
116
120
  };
117
121
  export type KeptAlive = {
118
122
  id: string;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@qawolf/cli",
3
- "version": "1.23.0",
3
+ "version": "1.24.0",
4
4
  "description": "Run and manage QA Wolf flows from the terminal, CI, or an AI agent",
5
5
  "keywords": [
6
6
  "automation",
@@ -71,7 +71,7 @@
71
71
  "@clack/prompts": "1.5.1",
72
72
  "@napi-rs/keyring": "1.3.0",
73
73
  "@oxc-node/core": "0.1.0",
74
- "@qawolf/api-contracts": "0.40.0",
74
+ "@qawolf/api-contracts": "0.47.0",
75
75
  "@qawolf/emails": "1.1.1",
76
76
  "@qawolf/flow-targets": "1.0.0",
77
77
  "@qawolf/flows": "0.1.4",
@@ -132,6 +132,11 @@ that `url`; never guess a route and never send a repository link in its place.
132
132
  | `qawolf auth whoami` | read | Show authentication status |
133
133
  | `qawolf automate` | write | Request automation for draft flows. First create a named local .flow.ts draft for every requested journey that does not already have a matching draft; never reuse a generic starter or placeholder. Each new draft must start with a JSDoc Goal: description, import flow from @qawolf/flows/web, and use export default flow(...); a comment-only file or direct test(...) call is not a valid draft. Commit and push all changes with Git to publish them, then list remote drafts to resolve every selected ID. Do not use patch to create or rename a selected flow. Finally make one automation request containing all requested flow IDs. |
134
134
  | `qawolf doctor` | local | Diagnose problems running flows locally |
135
+ | `qawolf email find` | read | List the workspace's inbox, or its sent mail, newest first. Read a message body with email.get. |
136
+ | `qawolf email get` | read | 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. |
137
+ | `qawolf email getAttachment` | read | Read one attachment of a workspace email as base64 content, by file name or by position. email.get lists both. |
138
+ | `qawolf email listAddresses` | read | 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. |
139
+ | `qawolf email send` | write | 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. |
135
140
  | `qawolf environment create` | write | Create an environment on the caller's team and return it in the environment.get shape. |
136
141
  | `qawolf environment deleteVariable` | write | Remove one environment variable by name. Succeeds whether or not the variable existed. |
137
142
  | `qawolf environment find` | read | List the team's environments, newest first. |
@@ -140,7 +145,8 @@ that `url`; never guess a route and never send a repository link in its place.
140
145
  | `qawolf environment listVariableNames` | read | Use this to answer which QA Wolf environment variables are available to test code. Returns names only; values never leave the server. |
141
146
  | `qawolf environment setVariable` | write | Create or replace an environment variable. If the user asks to create one for "my email" without naming it, use DEFAULT_EMAIL. The value is never returned. |
142
147
  | `qawolf environment update` | write | Update an environment owned by the caller's team and return it in the environment.get shape. Omitted fields remain unchanged. |
143
- | `qawolf flow addTag` | write | Assign an existing tag to the selected flows. Create tags with tag.create. |
148
+ | `qawolf flow addTag` | write | Assign an existing tag to the selected flows. Create tags with tag.create. Flows that already carry the tag are reported in skippedFlows. |
149
+ | `qawolf flow removeTag` | write | 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. |
144
150
  | `qawolf flow update` | write | Move a flow between draft and active readiness. The other statuses shown in the app are derived and cannot be set. |
145
151
  | `qawolf flows list` | local (read with --remote) | List flows matching [pattern] from the local project, or from a QA Wolf environment with --remote |
146
152
  | `qawolf flows pull` | read | Download an environment's flows into the local .qawolf/<env>/ cache |
@@ -154,6 +160,7 @@ that `url`; never guess a route and never send a repository link in its place.
154
160
  | `qawolf issue create` | write | Create a bug or coverage request issue for the caller's team. Maintenance issues cannot be created through the public API. |
155
161
  | `qawolf issue find` | read | List the team's bug reports, maintenance reports, or coverage requests, newest first. |
156
162
  | `qawolf issue get` | read | Get an issue by id. |
163
+ | `qawolf issue removeFlows` | write | 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. |
157
164
  | `qawolf issue update` | write | Update an issue owned by the caller's team. Omitted fields remain unchanged. |
158
165
  | `qawolf run create` | write | Create a run for the selected flows and/or tags in an environment. |
159
166
  | `qawolf run diagnose` | write | Diagnose failed flows in a run as reproductions of a bug or maintenance report owned by the caller's team. The issue's type selects the diagnosis. Each flow must have failed in the run. A flow that is already diagnosed moves to this issue. The diagnosis appears on the run, and the reproduction appears under the issue's reproductions. Coverage requests cannot be diagnosed; use issue.addFlows to cover flows instead. |
@@ -174,7 +181,7 @@ that `url`; never guess a route and never send a repository link in its place.
174
181
  | `qawolf runner inspect variable` | read | Print a top-level variable's value from the running workflow |
175
182
  | `qawolf runner keepalive` | read | Reset a runner's inactivity clock, for a caller that pauses between actions |
176
183
  | `qawolf runner launch` | write | Launch an interactive runner and make it this directory's default |
177
- | `qawolf runner list` | read | List the runners this directory holds that are still running |
184
+ | `qawolf runner list` | read | List the runners running on your team |
178
185
  | `qawolf runner promote-snapshot` | write | Accept a run's screenshot as the new baseline for an image diff, on the runner that produced it |
179
186
  | `qawolf runner run` | write | Run a flow on an interactive runner, shipping the flow and what it imports |
180
187
  | `qawolf runner screenshot` | read | Save a JPEG of an interactive runner's screen to a file |