@asteroidai/cli 1.9.11 → 1.9.27

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/asteroid.js CHANGED
@@ -124,7 +124,8 @@ var COMMANDS = [
124
124
  query: [],
125
125
  binary: false,
126
126
  readOnly: false,
127
- destructive: false
127
+ destructive: false,
128
+ capability: "run_execution"
128
129
  },
129
130
  {
130
131
  command: "context",
@@ -1030,7 +1031,8 @@ var COMMANDS = [
1030
1031
  ],
1031
1032
  files: [],
1032
1033
  credentialOrganization: false
1033
- }
1034
+ },
1035
+ capability: "run_execution"
1034
1036
  },
1035
1037
  {
1036
1038
  command: "execution resume",
@@ -1050,7 +1052,8 @@ var COMMANDS = [
1050
1052
  query: [],
1051
1053
  binary: false,
1052
1054
  readOnly: false,
1053
- destructive: false
1055
+ destructive: false,
1056
+ capability: "run_execution"
1054
1057
  },
1055
1058
  {
1056
1059
  command: "insight get",
@@ -1898,7 +1901,8 @@ var COMMANDS = [
1898
1901
  ],
1899
1902
  files: [],
1900
1903
  credentialOrganization: false
1901
- }
1904
+ },
1905
+ capability: "run_execution"
1902
1906
  },
1903
1907
  {
1904
1908
  command: "schedule delete",
@@ -1924,7 +1928,8 @@ var COMMANDS = [
1924
1928
  query: [],
1925
1929
  binary: false,
1926
1930
  readOnly: false,
1927
- destructive: true
1931
+ destructive: true,
1932
+ capability: "run_execution"
1928
1933
  },
1929
1934
  {
1930
1935
  command: "schedule get",
@@ -2105,7 +2110,8 @@ var COMMANDS = [
2105
2110
  ],
2106
2111
  files: [],
2107
2112
  credentialOrganization: false
2108
- }
2113
+ },
2114
+ capability: "run_execution"
2109
2115
  },
2110
2116
  {
2111
2117
  command: "scheduled-execution cancel",
@@ -2125,7 +2131,8 @@ var COMMANDS = [
2125
2131
  query: [],
2126
2132
  binary: false,
2127
2133
  readOnly: false,
2128
- destructive: false
2134
+ destructive: false,
2135
+ capability: "run_execution"
2129
2136
  },
2130
2137
  {
2131
2138
  command: "scheduled-execution create",
@@ -2193,7 +2200,8 @@ var COMMANDS = [
2193
2200
  ],
2194
2201
  files: [],
2195
2202
  credentialOrganization: false
2196
- }
2203
+ },
2204
+ capability: "run_execution"
2197
2205
  },
2198
2206
  {
2199
2207
  command: "scheduled-execution list",
@@ -2295,7 +2303,8 @@ var COMMANDS = [
2295
2303
  ],
2296
2304
  files: [],
2297
2305
  credentialOrganization: false
2298
- }
2306
+ },
2307
+ capability: "run_execution"
2299
2308
  },
2300
2309
  {
2301
2310
  command: "schema validate",
@@ -3435,7 +3444,8 @@ var COMMANDS = [
3435
3444
  ],
3436
3445
  files: [],
3437
3446
  credentialOrganization: false
3438
- }
3447
+ },
3448
+ capability: "run_execution"
3439
3449
  },
3440
3450
  {
3441
3451
  command: "workflow execute-sync",
@@ -3534,112 +3544,8 @@ var COMMANDS = [
3534
3544
  ],
3535
3545
  files: [],
3536
3546
  credentialOrganization: false
3537
- }
3538
- },
3539
- {
3540
- command: "workflow file list",
3541
- tool: "workflow_files_list",
3542
- title: "List workflow files",
3543
- description: "List the workflow's editable head as a directory of files: the graph rendered as structural files (settings.yaml, per-node instructions.md, scripts) alongside the workflow's own files. Those files are referenced by fileId unless contents=all is set. Pass paths to read named files only, or contents=none for a manifest with no bodies. The returned rev is what you pass as baseRev to write.",
3544
- method: "GET",
3545
- path: "/agents/v2/workflows/{workflowId}/head/files",
3546
- args: [
3547
- {
3548
- name: "workflowId",
3549
- flag: "workflow-id",
3550
- type: "string",
3551
- required: true
3552
- }
3553
- ],
3554
- query: [
3555
- {
3556
- name: "contents",
3557
- flag: "contents",
3558
- type: "string",
3559
- required: false,
3560
- description: "Which contents come inline: none (a pure manifest), structural (the default), or all (structural plus file bytes within the response budget).",
3561
- enum: ["none", "structural", "all"]
3562
- },
3563
- {
3564
- name: "paths",
3565
- flag: "paths",
3566
- type: "string[]",
3567
- required: false,
3568
- description: "Return only the files at these exact tree paths. A path that names nothing yields no entry. The inline budget applies to the filtered set, so a narrow read can inline files a whole-tree read would have to skip."
3569
- },
3570
- {
3571
- name: "variants",
3572
- flag: "variants",
3573
- type: "string",
3574
- required: false,
3575
- description: `Narrow which variants/<key>/ files the tree carries: "none" strips every variant, a comma-separated list keeps only those variant keys. Files outside variants/ are always kept, and the response's variantKeys still names every variant. Ignored unless the workflow has variant mode enabled; absent keeps every variant.`
3576
- },
3577
- {
3578
- name: "maxFileBytes",
3579
- flag: "max-file-bytes",
3580
- type: "integer",
3581
- required: false,
3582
- description: "Per-file ceiling, in bytes, for inlined file contents. Clamped to the server maximum. Files above the ceiling stay references."
3583
- },
3584
- {
3585
- name: "maxTotalBytes",
3586
- flag: "max-total-bytes",
3587
- type: "integer",
3588
- required: false,
3589
- description: "Ceiling, in bytes, on the total inlined file contents in one response. Clamped to the server maximum. Files are admitted in path order until the ceiling is reached; the rest stay references."
3590
- }
3591
- ],
3592
- binary: false,
3593
- readOnly: true,
3594
- destructive: false
3595
- },
3596
- {
3597
- command: "workflow file patch",
3598
- tool: "workflow_files_patch",
3599
- title: "Write workflow files",
3600
- description: "Write and delete files on the workflow's editable head in one patch. Structural files re-derive the graph; the workflow's own files go to its file store, you just write paths. Pass the rev from `workflow_files_list` as baseRev; a stale value is rejected with 409, so re-read and retry. Editing a published or already-executed version forks a new one instead of rewriting it. Each write carries `contentBase64`: base64-encode the file content, plain text is rejected with 400.",
3601
- method: "PATCH",
3602
- path: "/agents/v2/workflows/{workflowId}/head/files",
3603
- args: [
3604
- {
3605
- name: "workflowId",
3606
- flag: "workflow-id",
3607
- type: "string",
3608
- required: true
3609
- }
3610
- ],
3611
- query: [],
3612
- binary: false,
3613
- readOnly: false,
3614
- destructive: true,
3615
- body: {
3616
- required: true,
3617
- contentType: "application/json",
3618
- fields: [
3619
- {
3620
- name: "baseRev",
3621
- flag: "base-rev",
3622
- type: "integer",
3623
- required: true,
3624
- description: "The revision this edit is based on. Agent-file-only patches overlay the current head even when this is stale. A structural-file patch is rejected with 409 when it does not match the stored head revision."
3625
- },
3626
- {
3627
- name: "writes",
3628
- flag: "writes",
3629
- type: "json",
3630
- required: true,
3631
- description: "Files to upsert."
3632
- },
3633
- {
3634
- name: "deletes",
3635
- flag: "deletes",
3636
- type: "string[]",
3637
- required: true
3638
- }
3639
- ],
3640
- files: [],
3641
- credentialOrganization: false
3642
- }
3547
+ },
3548
+ capability: "run_execution"
3643
3549
  },
3644
3550
  {
3645
3551
  command: "workflow get",
@@ -3687,7 +3593,8 @@ var COMMANDS = [
3687
3593
  query: [],
3688
3594
  binary: false,
3689
3595
  readOnly: false,
3690
- destructive: false
3596
+ destructive: false,
3597
+ capability: "publish"
3691
3598
  },
3692
3599
  {
3693
3600
  command: "workflow head revert",
@@ -3813,6 +3720,113 @@ var COMMANDS = [
3813
3720
  readOnly: true,
3814
3721
  destructive: false
3815
3722
  },
3723
+ {
3724
+ command: "workflow pull",
3725
+ tool: "workflow_files_list",
3726
+ title: "List workflow files",
3727
+ description: "List the workflow's editable head as a directory of files: the graph rendered as structural files (settings.yaml, per-node instructions.md, scripts) alongside the workflow's own files. Those files are referenced by fileId unless contents=all is set. Pass paths to read named files only, or contents=none for a manifest with no bodies. The returned rev is what you pass as baseRev to write.",
3728
+ method: "GET",
3729
+ path: "/agents/v2/workflows/{workflowId}/head/files",
3730
+ args: [
3731
+ {
3732
+ name: "workflowId",
3733
+ flag: "workflow-id",
3734
+ type: "string",
3735
+ required: true
3736
+ }
3737
+ ],
3738
+ query: [
3739
+ {
3740
+ name: "contents",
3741
+ flag: "contents",
3742
+ type: "string",
3743
+ required: false,
3744
+ description: "Which contents come inline: none (a pure manifest), structural (the default), or all (structural plus file bytes within the response budget).",
3745
+ enum: ["none", "structural", "all"]
3746
+ },
3747
+ {
3748
+ name: "paths",
3749
+ flag: "paths",
3750
+ type: "string[]",
3751
+ required: false,
3752
+ description: "Return only the files at these exact tree paths. A path that names nothing yields no entry. The inline budget applies to the filtered set, so a narrow read can inline files a whole-tree read would have to skip."
3753
+ },
3754
+ {
3755
+ name: "variants",
3756
+ flag: "variants",
3757
+ type: "string",
3758
+ required: false,
3759
+ description: `Narrow which variants/<key>/ files the tree carries: "none" strips every variant, a comma-separated list keeps only those variant keys. Files outside variants/ are always kept, and the response's variantKeys still names every variant. Ignored unless the workflow has variant mode enabled; absent keeps every variant.`
3760
+ },
3761
+ {
3762
+ name: "maxFileBytes",
3763
+ flag: "max-file-bytes",
3764
+ type: "integer",
3765
+ required: false,
3766
+ description: "Per-file ceiling, in bytes, for inlined file contents. Clamped to the server maximum. Files above the ceiling stay references."
3767
+ },
3768
+ {
3769
+ name: "maxTotalBytes",
3770
+ flag: "max-total-bytes",
3771
+ type: "integer",
3772
+ required: false,
3773
+ description: "Ceiling, in bytes, on the total inlined file contents in one response. Clamped to the server maximum. Files are admitted in path order until the ceiling is reached; the rest stay references."
3774
+ }
3775
+ ],
3776
+ binary: false,
3777
+ readOnly: true,
3778
+ destructive: false,
3779
+ files: "pull"
3780
+ },
3781
+ {
3782
+ command: "workflow push",
3783
+ tool: "workflow_files_patch",
3784
+ title: "Write workflow files",
3785
+ description: "Write and delete files on the workflow's editable head in one patch. Structural files re-derive the graph; the workflow's own files go to its file store, you just write paths. Pass the rev from `workflow_files_list` as baseRev; a stale value is rejected with 409, so re-read and retry. Editing a published or already-executed version forks a new one instead of rewriting it. Each write carries `contentBase64`: base64-encode the file content, plain text is rejected with 400.",
3786
+ method: "PATCH",
3787
+ path: "/agents/v2/workflows/{workflowId}/head/files",
3788
+ args: [
3789
+ {
3790
+ name: "workflowId",
3791
+ flag: "workflow-id",
3792
+ type: "string",
3793
+ required: true
3794
+ }
3795
+ ],
3796
+ query: [],
3797
+ binary: false,
3798
+ readOnly: false,
3799
+ destructive: true,
3800
+ body: {
3801
+ required: true,
3802
+ contentType: "application/json",
3803
+ fields: [
3804
+ {
3805
+ name: "baseRev",
3806
+ flag: "base-rev",
3807
+ type: "integer",
3808
+ required: true,
3809
+ description: "The revision this edit is based on. Agent-file-only patches overlay the current head even when this is stale. A structural-file patch is rejected with 409 when it does not match the stored head revision."
3810
+ },
3811
+ {
3812
+ name: "writes",
3813
+ flag: "writes",
3814
+ type: "json",
3815
+ required: true,
3816
+ description: "Files to upsert."
3817
+ },
3818
+ {
3819
+ name: "deletes",
3820
+ flag: "deletes",
3821
+ type: "string[]",
3822
+ required: true
3823
+ }
3824
+ ],
3825
+ files: [],
3826
+ credentialOrganization: false
3827
+ },
3828
+ files: "push"
3829
+ },
3816
3830
  {
3817
3831
  command: "workflow rename",
3818
3832
  tool: "workflow_rename",
@@ -4215,78 +4229,16 @@ var COMMANDS = [
4215
4229
  ],
4216
4230
  files: [],
4217
4231
  credentialOrganization: false
4218
- }
4232
+ },
4233
+ capability: "run_execution"
4219
4234
  },
4220
4235
  {
4221
- command: "workflow version file list",
4222
- tool: "workflow_version_files_list",
4223
- title: "List workflow version files",
4224
- description: "List a version's files, like `workflow_files_list` does for the editable head. Versions are immutable, so this is read-only.",
4236
+ command: "workflow version file url",
4237
+ tool: "workflow_version_file_signed_url",
4238
+ title: "Get workflow version file signed URL",
4239
+ description: "Get a short-lived signed URL for downloading one of a workflow version's files directly from storage. Prefer this over a redirect-based download when reading a file's contents.",
4225
4240
  method: "GET",
4226
- path: "/agents/v2/workflows/{workflowId}/versions/{versionId}/files",
4227
- args: [
4228
- {
4229
- name: "workflowId",
4230
- flag: "workflow-id",
4231
- type: "string",
4232
- required: true
4233
- },
4234
- {
4235
- name: "versionId",
4236
- flag: "version-id",
4237
- type: "string",
4238
- required: true
4239
- }
4240
- ],
4241
- query: [
4242
- {
4243
- name: "contents",
4244
- flag: "contents",
4245
- type: "string",
4246
- required: false,
4247
- description: "Which contents come inline: none (a pure manifest), structural (the default), or all (structural plus file bytes within the response budget).",
4248
- enum: ["none", "structural", "all"]
4249
- },
4250
- {
4251
- name: "paths",
4252
- flag: "paths",
4253
- type: "string[]",
4254
- required: false,
4255
- description: "Return only the files at these exact tree paths. A path that names nothing yields no entry. The inline budget applies to the filtered set, so a narrow read can inline files a whole-tree read would have to skip."
4256
- },
4257
- {
4258
- name: "variants",
4259
- flag: "variants",
4260
- type: "string",
4261
- required: false,
4262
- description: `Narrow which variants/<key>/ files the tree carries: "none" strips every variant, a comma-separated list keeps only those variant keys. Files outside variants/ are always kept, and the response's variantKeys still names every variant. Ignored unless the workflow has variant mode enabled; absent keeps every variant.`
4263
- },
4264
- {
4265
- name: "maxFileBytes",
4266
- flag: "max-file-bytes",
4267
- type: "integer",
4268
- required: false,
4269
- description: "Per-file ceiling, in bytes, for inlined file contents. Clamped to the server maximum. Files above the ceiling stay references."
4270
- },
4271
- {
4272
- name: "maxTotalBytes",
4273
- flag: "max-total-bytes",
4274
- type: "integer",
4275
- required: false,
4276
- description: "Ceiling, in bytes, on the total inlined file contents in one response. Clamped to the server maximum. Files are admitted in path order until the ceiling is reached; the rest stay references."
4277
- }
4278
- ],
4279
- binary: false,
4280
- readOnly: true,
4281
- destructive: false
4282
- },
4283
- {
4284
- command: "workflow version file url",
4285
- tool: "workflow_version_file_signed_url",
4286
- title: "Get workflow version file signed URL",
4287
- description: "Get a short-lived signed URL for downloading one of a workflow version's files directly from storage. Prefer this over a redirect-based download when reading a file's contents.",
4288
- method: "GET",
4289
- path: "/agents/v2/workflows/{workflowId}/versions/{versionId}/files/{fileId}/signed-url",
4241
+ path: "/agents/v2/workflows/{workflowId}/versions/{versionId}/files/{fileId}/signed-url",
4290
4242
  args: [
4291
4243
  {
4292
4244
  name: "workflowId",
@@ -4434,7 +4386,72 @@ var COMMANDS = [
4434
4386
  query: [],
4435
4387
  binary: false,
4436
4388
  readOnly: false,
4437
- destructive: false
4389
+ destructive: false,
4390
+ capability: "publish"
4391
+ },
4392
+ {
4393
+ command: "workflow version pull",
4394
+ tool: "workflow_version_files_list",
4395
+ title: "List workflow version files",
4396
+ description: "List a version's files, like `workflow_files_list` does for the editable head. Versions are immutable, so this is read-only.",
4397
+ method: "GET",
4398
+ path: "/agents/v2/workflows/{workflowId}/versions/{versionId}/files",
4399
+ args: [
4400
+ {
4401
+ name: "workflowId",
4402
+ flag: "workflow-id",
4403
+ type: "string",
4404
+ required: true
4405
+ },
4406
+ {
4407
+ name: "versionId",
4408
+ flag: "version-id",
4409
+ type: "string",
4410
+ required: true
4411
+ }
4412
+ ],
4413
+ query: [
4414
+ {
4415
+ name: "contents",
4416
+ flag: "contents",
4417
+ type: "string",
4418
+ required: false,
4419
+ description: "Which contents come inline: none (a pure manifest), structural (the default), or all (structural plus file bytes within the response budget).",
4420
+ enum: ["none", "structural", "all"]
4421
+ },
4422
+ {
4423
+ name: "paths",
4424
+ flag: "paths",
4425
+ type: "string[]",
4426
+ required: false,
4427
+ description: "Return only the files at these exact tree paths. A path that names nothing yields no entry. The inline budget applies to the filtered set, so a narrow read can inline files a whole-tree read would have to skip."
4428
+ },
4429
+ {
4430
+ name: "variants",
4431
+ flag: "variants",
4432
+ type: "string",
4433
+ required: false,
4434
+ description: `Narrow which variants/<key>/ files the tree carries: "none" strips every variant, a comma-separated list keeps only those variant keys. Files outside variants/ are always kept, and the response's variantKeys still names every variant. Ignored unless the workflow has variant mode enabled; absent keeps every variant.`
4435
+ },
4436
+ {
4437
+ name: "maxFileBytes",
4438
+ flag: "max-file-bytes",
4439
+ type: "integer",
4440
+ required: false,
4441
+ description: "Per-file ceiling, in bytes, for inlined file contents. Clamped to the server maximum. Files above the ceiling stay references."
4442
+ },
4443
+ {
4444
+ name: "maxTotalBytes",
4445
+ flag: "max-total-bytes",
4446
+ type: "integer",
4447
+ required: false,
4448
+ description: "Ceiling, in bytes, on the total inlined file contents in one response. Clamped to the server maximum. Files are admitted in path order until the ceiling is reached; the rest stay references."
4449
+ }
4450
+ ],
4451
+ binary: false,
4452
+ readOnly: true,
4453
+ destructive: false,
4454
+ files: "pull"
4438
4455
  },
4439
4456
  {
4440
4457
  command: "workflow version validate",
@@ -4510,11 +4527,6 @@ var COMMANDS = [
4510
4527
  function option(name, type, description, extra = {}) {
4511
4528
  return { name, flag: name, type, required: false, description, ...extra };
4512
4529
  }
4513
- var DIR = option(
4514
- "dir",
4515
- "string",
4516
- "Write here instead of `<workflow-name>/...` under the current directory."
4517
- );
4518
4530
  var local = {
4519
4531
  method: "GET",
4520
4532
  path: "",
@@ -4523,46 +4535,6 @@ var local = {
4523
4535
  local: true
4524
4536
  };
4525
4537
  var SYNC_COMMANDS = [
4526
- {
4527
- ...local,
4528
- command: "workflow pull",
4529
- tool: "workflow_pull",
4530
- title: "Pull a workflow to disk",
4531
- description: "Write a workflow's editable head to `<workflow-name>/head/` as files: settings, per-node instructions and scripts, and the workflow's own files. Pass --version-id for a pinned version, written read-only to `<workflow-name>/.versions/`. Edit the head files, then run `asteroid workflow push` from that folder.",
4532
- readOnly: true,
4533
- args: [
4534
- {
4535
- name: "workflowId",
4536
- flag: "workflow-id",
4537
- type: "string",
4538
- required: true
4539
- }
4540
- ],
4541
- query: [
4542
- option(
4543
- "version-id",
4544
- "string",
4545
- "A workflow version ID. Omit for the editable head."
4546
- ),
4547
- DIR
4548
- ]
4549
- },
4550
- {
4551
- ...local,
4552
- command: "workflow push",
4553
- tool: "workflow_push",
4554
- title: "Push local edits to a workflow head",
4555
- description: "Send the edits in a pulled head folder to the workflow's editable head: changed and new files are written, removed files are deleted. Fails if the head changed since the pull; pull again and reapply. Push does not publish.",
4556
- readOnly: false,
4557
- args: [],
4558
- query: [
4559
- option(
4560
- "dir",
4561
- "string",
4562
- "The pulled head folder. Defaults to the current directory."
4563
- )
4564
- ]
4565
- },
4566
4538
  {
4567
4539
  ...local,
4568
4540
  command: "execution pull",
@@ -4644,8 +4616,8 @@ var HIDDEN = /^agent[A-Z]|Url$/;
4644
4616
  var MAX_COLUMNS = 6;
4645
4617
  var MAX_CELL = 60;
4646
4618
  var CONTROL = /\x1b\[[0-?]*[ -/]*[@-~]|\x1b\][^\x07\x1b]*(?:\x07|\x1b\\)?|\x1b[@-_]?|[\x00-\x08\x0b-\x1f\x7f-\x9f]/g;
4647
- function clean(text2) {
4648
- return text2.replace(CONTROL, "");
4619
+ function clean(text3) {
4620
+ return text3.replace(CONTROL, "");
4649
4621
  }
4650
4622
  function isObject(value2) {
4651
4623
  return typeof value2 === "object" && value2 !== null && !Array.isArray(value2);
@@ -4653,8 +4625,8 @@ function isObject(value2) {
4653
4625
  function isScalar(value2) {
4654
4626
  return value2 === null || ["string", "number", "boolean"].includes(typeof value2);
4655
4627
  }
4656
- function indent(text2) {
4657
- return text2.split("\n").map((line2) => ` ${line2}`).join("\n");
4628
+ function indent(text3) {
4629
+ return text3.split("\n").map((line2) => ` ${line2}`).join("\n");
4658
4630
  }
4659
4631
  function text(value2) {
4660
4632
  return clean(isScalar(value2) ? String(value2) : JSON.stringify(value2));
@@ -4835,7 +4807,7 @@ function orgLabel(org) {
4835
4807
  }
4836
4808
 
4837
4809
  // src/program.ts
4838
- import { Command, CommanderError } from "commander";
4810
+ import { Command, CommanderError, Option as Flag } from "commander";
4839
4811
 
4840
4812
  // src/args.ts
4841
4813
  var UsageError = class extends Error {
@@ -4866,7 +4838,17 @@ var BUILTINS = [
4866
4838
  usage: "asteroid use [org]",
4867
4839
  arg: "org",
4868
4840
  description: "Make an organization, by id or name, the default for later commands. Without one, pick from a list.",
4869
- args: 1
4841
+ args: 1,
4842
+ hiddenOptions: [
4843
+ [
4844
+ "--reason <why>",
4845
+ "Staff only. Why you need access to an organization you are not a member of, such as a ticket id. It is audited. Asked for in a terminal."
4846
+ ],
4847
+ [
4848
+ "--exit",
4849
+ "Staff only. Revoke every staff token now and go back to your own organization."
4850
+ ]
4851
+ ]
4870
4852
  },
4871
4853
  {
4872
4854
  command: "complete",
@@ -4903,8 +4885,8 @@ function collect(value2, previous) {
4903
4885
  return [...previous ?? [], value2];
4904
4886
  }
4905
4887
  function optionText(option2) {
4906
- const text2 = option2.description?.replace(/\s+/g, " ").trim() ?? "";
4907
- const parts = [text2 && !/[.!?]$/.test(text2) ? `${text2}.` : text2];
4888
+ const text3 = option2.description?.replace(/\s+/g, " ").trim() ?? "";
4889
+ const parts = [text3 && !/[.!?]$/.test(text3) ? `${text3}.` : text3];
4908
4890
  if (option2.enum) parts.push(`One of: ${option2.enum.join(", ")}.`);
4909
4891
  if (option2.type.endsWith("[]")) parts.push("Repeatable.");
4910
4892
  if (option2.required) parts.push("Required.");
@@ -4952,6 +4934,16 @@ function leafOptions(leaf, command) {
4952
4934
  for (const file of command.body?.files ?? []) {
4953
4935
  leaf.option(`--${file} <path>`, "File to upload. Repeatable.", collect);
4954
4936
  }
4937
+ if (command.files === "pull")
4938
+ leaf.option(
4939
+ "--out <dir>",
4940
+ "Write here instead of ./<workflow-name>/head (a version: ./<workflow-name>/.versions/...). --json prints the file listing instead."
4941
+ );
4942
+ if (command.files === "push")
4943
+ leaf.option(
4944
+ "--from <dir>",
4945
+ "The pulled head folder to push. Defaults to the current folder."
4946
+ );
4955
4947
  }
4956
4948
  function toParsed(leaf, positionals, dotted) {
4957
4949
  const globals = leaf.optsWithGlobals();
@@ -4979,12 +4971,14 @@ function defineCommands(root, options, dotted = /* @__PURE__ */ new Map(), onInv
4979
4971
  "after",
4980
4972
  "\nRun `asteroid --version` to print the CLI version."
4981
4973
  );
4982
- for (const [flags, text2] of GLOBAL_FLAGS) {
4983
- if (!flags.startsWith("-h")) root.option(flags, text2);
4974
+ for (const [flags, text3] of GLOBAL_FLAGS) {
4975
+ if (!flags.startsWith("-h")) root.option(flags, text3);
4984
4976
  }
4985
4977
  for (const builtin of options.builtins ?? []) {
4986
4978
  const leaf = root.command(builtin.command).description(builtin.description);
4987
4979
  if (builtin.arg) leaf.argument(`[${builtin.arg}]`);
4980
+ for (const [flags, text3] of builtin.hiddenOptions ?? [])
4981
+ leaf.addOption(new Flag(flags, text3).hideHelp());
4988
4982
  leaf.action((...args) => {
4989
4983
  const cmd = args.at(-1);
4990
4984
  onInvocation({
@@ -5073,8 +5067,8 @@ async function parseInvocation(argv2, options) {
5073
5067
  }
5074
5068
  const { argv: rest, dotted } = splitDotted(argv2);
5075
5069
  const root = new Command("asteroid").exitOverride().helpCommand(false).configureOutput({
5076
- writeOut: (text2) => stdout += text2,
5077
- writeErr: (text2) => stderr += text2
5070
+ writeOut: (text3) => stdout += text3,
5071
+ writeErr: (text3) => stderr += text3
5078
5072
  });
5079
5073
  defineCommands(root, options, dotted, (invocation) => {
5080
5074
  result = invocation;
@@ -5228,11 +5222,12 @@ function tokenClaims(token) {
5228
5222
  var SERVICE = "asteroid-cli";
5229
5223
  var LOCK_STALE_MS = 3e4;
5230
5224
  var ACCOUNT = "refresh-token";
5231
- async function keyring() {
5225
+ var STAFF_ACCOUNT = "staff-tokens";
5226
+ async function keyring(account = ACCOUNT) {
5232
5227
  if (process.env.ASTEROID_NO_KEYCHAIN) return void 0;
5233
5228
  try {
5234
5229
  const { Entry } = await import("@napi-rs/keyring");
5235
- const entry = new Entry(SERVICE, ACCOUNT);
5230
+ const entry = new Entry(SERVICE, account);
5236
5231
  entry.getPassword();
5237
5232
  return {
5238
5233
  get: () => entry.getPassword() ?? null,
@@ -5272,6 +5267,9 @@ var CredentialStore = class {
5272
5267
  get tokenFile() {
5273
5268
  return join2(this.env.configDir, "tokens.json");
5274
5269
  }
5270
+ get staffFile() {
5271
+ return join2(this.env.configDir, "staff-tokens.json");
5272
+ }
5275
5273
  async readLogin() {
5276
5274
  const ring = await keyring();
5277
5275
  const raw = ring ? tryGet(ring).value : null;
@@ -5300,22 +5298,95 @@ var CredentialStore = class {
5300
5298
  const cleared = current.ok && (!current.value || trySet(ring, null));
5301
5299
  rmSync(this.loginFile, { force: true });
5302
5300
  rmSync(this.tokenFile, { force: true });
5303
- return cleared;
5301
+ const staffCleared = await this.withStaffLock(
5302
+ () => this.writeStaffTokens({})
5303
+ );
5304
+ return staffCleared && cleared;
5305
+ }
5306
+ /** Every staff token held, expired ones included. */
5307
+ async staffTokens() {
5308
+ const ring = await keyring(STAFF_ACCOUNT);
5309
+ const raw = ring ? tryGet(ring).value : null;
5310
+ const tokens = raw ? JSON.parse(raw) : readJson(this.staffFile, {});
5311
+ return Object.values(tokens);
5312
+ }
5313
+ /** The staff token for an org, matched by WorkOS id, id or name. */
5314
+ async staffToken(ref) {
5315
+ const lower = ref.toLowerCase();
5316
+ return (await this.staffTokens()).find(
5317
+ ({ org }) => org.workosId.toLowerCase() === lower || org.id?.toLowerCase() === lower || org.name?.toLowerCase() === lower
5318
+ );
5319
+ }
5320
+ /** Saves a staff token and drops expired ones. `replaces` is dropped too. */
5321
+ async saveStaffToken(token, replaces) {
5322
+ await this.withStaffLock(async () => {
5323
+ const tokens = {};
5324
+ for (const held of await this.staffTokens())
5325
+ if (held.expiresAt > Date.now() && held.grantId !== replaces)
5326
+ tokens[held.org.workosId] = held;
5327
+ tokens[token.org.workosId] = token;
5328
+ if (!await this.writeStaffTokens(tokens))
5329
+ throw new WorkosError(
5330
+ "keychain",
5331
+ "could not save the staff token in the OS keychain. Unlock it and try again."
5332
+ );
5333
+ });
5334
+ }
5335
+ async dropStaffToken(grantId) {
5336
+ await this.withStaffLock(async () => {
5337
+ const tokens = {};
5338
+ for (const held of await this.staffTokens())
5339
+ if (held.grantId !== grantId) tokens[held.org.workosId] = held;
5340
+ if (!await this.writeStaffTokens(tokens))
5341
+ throw new WorkosError(
5342
+ "keychain",
5343
+ "could not remove the staff token from the OS keychain. Unlock it and try again."
5344
+ );
5345
+ });
5346
+ }
5347
+ async writeStaffTokens(tokens) {
5348
+ const empty = Object.keys(tokens).length === 0;
5349
+ const ring = await keyring(STAFF_ACCOUNT);
5350
+ if (ring && trySet(ring, empty ? null : JSON.stringify(tokens))) {
5351
+ rmSync(this.staffFile, { force: true });
5352
+ return true;
5353
+ }
5354
+ const stale = ring ? tryGet(ring) : { ok: true, value: null };
5355
+ if (!stale.ok || stale.value && !trySet(ring, null)) return false;
5356
+ if (empty) rmSync(this.staffFile, { force: true });
5357
+ else writePrivateJson(this.staffFile, tokens);
5358
+ return true;
5304
5359
  }
5305
5360
  async withRefreshLock(work) {
5361
+ return this.withLock(
5362
+ "refresh",
5363
+ "another asteroid command is refreshing the sign-in.",
5364
+ work
5365
+ );
5366
+ }
5367
+ /** Two commands at once must not lose a grant they could still revoke. */
5368
+ async withStaffLock(work) {
5369
+ return this.withLock(
5370
+ "staff",
5371
+ "another asteroid command is saving staff access.",
5372
+ work
5373
+ );
5374
+ }
5375
+ async withLock(name, busy, work) {
5306
5376
  mkdirSync2(this.env.configDir, { recursive: true, mode: 448 });
5307
- const lockfilePath = join2(this.env.configDir, "refresh.lock");
5377
+ const lockfilePath = join2(this.env.configDir, `${name}.lock`);
5308
5378
  let release;
5309
5379
  try {
5310
- release = await lockfile.lock(this.env.configDir, {
5380
+ release = await lockfile.lock(join2(this.env.configDir, name), {
5311
5381
  lockfilePath,
5382
+ realpath: false,
5312
5383
  stale: LOCK_STALE_MS,
5313
5384
  retries: { retries: 60, minTimeout: 100, maxTimeout: 500 }
5314
5385
  });
5315
5386
  } catch {
5316
5387
  throw new WorkosError(
5317
5388
  "locked",
5318
- `another asteroid command is refreshing the sign-in. Try again, or delete ${lockfilePath} if none is running.`
5389
+ `${busy} Try again, or delete ${lockfilePath} if none is running.`
5319
5390
  );
5320
5391
  }
5321
5392
  try {
@@ -5342,8 +5413,25 @@ var CredentialStore = class {
5342
5413
  // src/auth/credentials.ts
5343
5414
  var AuthError = class extends Error {
5344
5415
  };
5416
+ var NotMemberError = class extends AuthError {
5417
+ };
5345
5418
  var API_KEY_OVERRIDES = "ASTEROID_API_KEY is set, so the CLI uses that key and ignores `asteroid login`.";
5346
5419
  var UNSET_API_KEY = "Run `unset ASTEROID_API_KEY` to use your login instead.";
5420
+ function isStaffLogin(login2) {
5421
+ return login2?.email?.toLowerCase().endsWith("@asteroid.ai") ?? false;
5422
+ }
5423
+ function staffHint(ref, staff) {
5424
+ return staff ? ` Asteroid staff: run \`asteroid use ${clean(ref)} --reason <why>\`.` : "";
5425
+ }
5426
+ function staffAuth(held) {
5427
+ return {
5428
+ kind: "bearer",
5429
+ headers: { Authorization: `Bearer ${held.token}` },
5430
+ org: held.org,
5431
+ expiresAt: held.expiresAt,
5432
+ staff: { grantId: held.grantId, reason: held.reason }
5433
+ };
5434
+ }
5347
5435
  function orgFromToken(token) {
5348
5436
  const claims = tokenClaims(token);
5349
5437
  if (!claims.org_id) return void 0;
@@ -5370,7 +5458,7 @@ function bearer(token, expiresAt) {
5370
5458
  };
5371
5459
  }
5372
5460
  var SSO_OR_MFA = /^(sso_required|mfa_.*|organization_authentication_methods_required)$/;
5373
- function refreshError(error, org) {
5461
+ function refreshError(error, org, login2) {
5374
5462
  if (error.code === "invalid_grant") {
5375
5463
  return new AuthError("your sign-in has expired. Run `asteroid login`.");
5376
5464
  }
@@ -5380,8 +5468,8 @@ function refreshError(error, org) {
5380
5468
  );
5381
5469
  }
5382
5470
  if (error.code === "organization_membership_not_found") {
5383
- return new AuthError(
5384
- `you are not a member of ${orgLabel(org)}. Staff access to other organizations is not available yet.`
5471
+ return new NotMemberError(
5472
+ `you are not a member of ${orgLabel(org)}.${staffHint(org.id ?? org.workosId, isStaffLogin(login2))}`
5385
5473
  );
5386
5474
  }
5387
5475
  return new AuthError(
@@ -5392,15 +5480,24 @@ async function resolveAuth(env, config, orgRef, fetchImpl = fetch) {
5392
5480
  if (env.apiKey)
5393
5481
  return { kind: "api-key", headers: { "X-Api-Key": env.apiKey } };
5394
5482
  const store = new CredentialStore(env);
5395
- if (!await store.readLogin()) {
5483
+ const login2 = await store.readLogin();
5484
+ if (!login2) {
5396
5485
  throw new AuthError(
5397
5486
  "not signed in. Run `asteroid login`, or set ASTEROID_API_KEY."
5398
5487
  );
5399
5488
  }
5489
+ const target = orgRef ?? config.org?.workosId;
5490
+ const held = target ? await store.staffToken(target) : void 0;
5491
+ if (held) {
5492
+ if (held.expiresAt - 3e4 > Date.now()) return staffAuth(held);
5493
+ throw new AuthError(
5494
+ `your staff access to ${orgLabel(held.org)} has expired.${staffHint(held.org.id ?? held.org.workosId, true)}`
5495
+ );
5496
+ }
5400
5497
  const org = orgRef ? findOrg(orgRef, config.orgs ?? []) : config.org;
5401
5498
  if (orgRef && !org) {
5402
5499
  throw new AuthError(
5403
- `unknown organization ${orgRef}. Run \`asteroid orgs\` to list yours.`
5500
+ `unknown organization ${clean(orgRef)}. Run \`asteroid orgs\` to list yours.${staffHint(orgRef, isStaffLogin(login2))}`
5404
5501
  );
5405
5502
  }
5406
5503
  if (!org) {
@@ -5413,8 +5510,8 @@ async function resolveAuth(env, config, orgRef, fetchImpl = fetch) {
5413
5510
  return store.withRefreshLock(async () => {
5414
5511
  const fresh = store.cachedToken(org.workosId);
5415
5512
  if (fresh) return bearer(fresh.accessToken, fresh.expiresAt);
5416
- const login2 = await store.readLogin();
5417
- if (!login2) {
5513
+ const login3 = await store.readLogin();
5514
+ if (!login3) {
5418
5515
  throw new AuthError("not signed in. Run `asteroid login`.");
5419
5516
  }
5420
5517
  try {
@@ -5424,10 +5521,10 @@ async function resolveAuth(env, config, orgRef, fetchImpl = fetch) {
5424
5521
  apiUrl: env.workosApiUrl,
5425
5522
  fetch: fetchImpl
5426
5523
  },
5427
- login2.refreshToken,
5524
+ login3.refreshToken,
5428
5525
  org.workosId
5429
5526
  );
5430
- await store.writeLogin({ ...login2, refreshToken: session2.refreshToken });
5527
+ await store.writeLogin({ ...login3, refreshToken: session2.refreshToken });
5431
5528
  const expiresAt = (tokenClaims(session2.accessToken).exp ?? 0) * 1e3 || Date.now() + 6e4;
5432
5529
  const auth = bearer(session2.accessToken, expiresAt);
5433
5530
  store.cacheToken(auth.org?.workosId ?? org.workosId, {
@@ -5437,7 +5534,7 @@ async function resolveAuth(env, config, orgRef, fetchImpl = fetch) {
5437
5534
  rememberOrg(env, config, auth.org);
5438
5535
  return auth;
5439
5536
  } catch (error) {
5440
- if (error instanceof WorkosError) throw refreshError(error, org);
5537
+ if (error instanceof WorkosError) throw refreshError(error, org, login3);
5441
5538
  throw error;
5442
5539
  }
5443
5540
  });
@@ -5453,7 +5550,7 @@ var ApiError = class extends Error {
5453
5550
  status;
5454
5551
  body;
5455
5552
  };
5456
- var VERSION = "1.9.11";
5553
+ var VERSION = "1.9.27";
5457
5554
  function problem(status, body, fallback) {
5458
5555
  if (body && typeof body === "object") {
5459
5556
  const record2 = body;
@@ -5513,14 +5610,14 @@ async function send(apiUrl, auth, request, binary, fetchImpl = fetch) {
5513
5610
  }
5514
5611
  const contentType = response.headers.get("content-type") ?? "";
5515
5612
  if (!response.ok) {
5516
- const text2 = await response.text();
5517
- let parsed = text2;
5613
+ const text3 = await response.text();
5614
+ let parsed = text3;
5518
5615
  try {
5519
- parsed = JSON.parse(text2);
5616
+ parsed = JSON.parse(text3);
5520
5617
  } catch {
5521
- parsed = text2;
5618
+ parsed = text3;
5522
5619
  }
5523
- let message = problem(response.status, parsed, text2.slice(0, 200));
5620
+ let message = problem(response.status, parsed, text3.slice(0, 200));
5524
5621
  const sentKey = "X-Api-Key" in hopHeaders;
5525
5622
  if (sentKey && response.status === 401)
5526
5623
  message = `ASTEROID_API_KEY was rejected: ${message}. Check the key. ${UNSET_API_KEY}`;
@@ -5632,11 +5729,21 @@ async function orgs(deps, flagOrg, json) {
5632
5729
  deps.fetch
5633
5730
  );
5634
5731
  const list = loadConfig(deps.env).orgs ?? [];
5732
+ const staff = (await new CredentialStore(deps.env).staffTokens()).filter(
5733
+ (held) => held.expiresAt > Date.now()
5734
+ );
5635
5735
  const active = auth.org?.workosId;
5636
5736
  if (json) {
5637
5737
  deps.stdout(
5638
5738
  `${JSON.stringify(
5639
- list.map((org) => ({ ...org, active: org.workosId === active })),
5739
+ [
5740
+ ...list.map((org) => ({ ...org, active: org.workosId === active })),
5741
+ ...staff.map((held) => ({
5742
+ ...held.org,
5743
+ active: held.org.workosId === active,
5744
+ staff: { reason: held.reason, expiresAt: held.expiresAt }
5745
+ }))
5746
+ ],
5640
5747
  null,
5641
5748
  2
5642
5749
  )}
@@ -5647,13 +5754,204 @@ async function orgs(deps, flagOrg, json) {
5647
5754
  for (const org of list)
5648
5755
  deps.stdout(`${org.workosId === active ? "*" : " "} ${orgLabel(org)}
5649
5756
  `);
5757
+ for (const held of staff)
5758
+ deps.stdout(
5759
+ `${held.org.workosId === active ? "*" : " "} ${orgLabel(held.org)} staff access, ${timeLeft(held.expiresAt)} left
5760
+ `
5761
+ );
5650
5762
  return 0;
5651
5763
  }
5652
- async function use(deps, ref) {
5764
+ function timeLeft(expiresAt) {
5765
+ const minutes = Math.max(0, Math.round((expiresAt - Date.now()) / 6e4));
5766
+ return minutes >= 60 ? `${Math.floor(minutes / 60)}h${String(minutes % 60).padStart(2, "0")}m` : `${minutes}m`;
5767
+ }
5768
+ function isMember(config, org) {
5769
+ return (config.orgs ?? []).some((o) => o.workosId === org.workosId);
5770
+ }
5771
+ function homeOrg(config) {
5772
+ const org = config.org && isMember(config, config.org) ? config.org : config.orgs?.[0];
5773
+ if (!org)
5774
+ throw new AuthError(
5775
+ "staff access needs a sign-in to one of your own organizations. Run `asteroid login`."
5776
+ );
5777
+ return org;
5778
+ }
5779
+ var STAFF_TOKEN_PATH = "/users/v2/staff-tokens";
5780
+ async function mintStaffToken(deps, config, ref, reason) {
5781
+ const home = await resolveAuth(
5782
+ deps.env,
5783
+ config,
5784
+ homeOrg(config).workosId,
5785
+ deps.fetch
5786
+ );
5787
+ let minted;
5788
+ try {
5789
+ const response = await send(
5790
+ deps.env.apiUrl,
5791
+ home,
5792
+ {
5793
+ method: "POST",
5794
+ path: STAFF_TOKEN_PATH,
5795
+ query: new URLSearchParams(),
5796
+ json: { organization: ref, reason }
5797
+ },
5798
+ false,
5799
+ deps.fetch
5800
+ );
5801
+ minted = response.json;
5802
+ } catch (error) {
5803
+ if (error instanceof ApiError && error.status === 403)
5804
+ throw new AuthError(
5805
+ `you are not a member of ${clean(ref)}, and staff access is for Asteroid staff only.`
5806
+ );
5807
+ if (error instanceof ApiError && error.status === 404)
5808
+ throw new UsageError(
5809
+ `unknown organization ${clean(ref)}. Pass its id or its WorkOS id (org_...).`
5810
+ );
5811
+ throw error;
5812
+ }
5813
+ return {
5814
+ token: minted.token,
5815
+ grantId: minted.grantId,
5816
+ org: {
5817
+ workosId: minted.workosOrganizationId ?? minted.organizationId,
5818
+ id: minted.organizationId,
5819
+ name: minted.organizationName
5820
+ },
5821
+ reason: minted.reason,
5822
+ expiresAt: Date.parse(minted.expiresAt)
5823
+ };
5824
+ }
5825
+ async function revokeStaffToken(deps, home, held) {
5826
+ try {
5827
+ await send(
5828
+ deps.env.apiUrl,
5829
+ home,
5830
+ {
5831
+ method: "DELETE",
5832
+ path: `${STAFF_TOKEN_PATH}/${encodeURIComponent(held.grantId)}`,
5833
+ query: new URLSearchParams()
5834
+ },
5835
+ false,
5836
+ deps.fetch
5837
+ );
5838
+ return true;
5839
+ } catch (error) {
5840
+ if (error instanceof ApiError && error.status === 404) return true;
5841
+ deps.stderr(
5842
+ `Could not revoke staff access to ${orgLabel(held.org)}: ${clean(error instanceof Error ? error.message : String(error))}. It ends at ${new Date(held.expiresAt).toISOString()}.
5843
+ `
5844
+ );
5845
+ return false;
5846
+ }
5847
+ }
5848
+ async function exitStaff(deps) {
5849
+ const store = new CredentialStore(deps.env);
5850
+ const held = await store.staffTokens();
5851
+ const config = loadConfig(deps.env);
5852
+ if (held.length === 0) {
5853
+ deps.stderr("No staff access to leave.\n");
5854
+ return 0;
5855
+ }
5856
+ const home = await resolveAuth(
5857
+ deps.env,
5858
+ config,
5859
+ homeOrg(config).workosId,
5860
+ deps.fetch
5861
+ );
5862
+ const left = [];
5863
+ const kept = [];
5864
+ for (const token of held) {
5865
+ const ended = token.expiresAt <= Date.now() || await revokeStaffToken(deps, home, token);
5866
+ if (!ended) {
5867
+ kept.push(token);
5868
+ continue;
5869
+ }
5870
+ await store.dropStaffToken(token.grantId);
5871
+ left.push(token);
5872
+ }
5873
+ const back = held.find((t) => t.from)?.from ?? homeOrg(config);
5874
+ saveConfig(deps.env, { ...loadConfig(deps.env), org: back });
5875
+ if (left.length > 0)
5876
+ deps.stderr(
5877
+ `Left staff access to ${left.map((t) => orgLabel(t.org)).join(", ")}.
5878
+ `
5879
+ );
5880
+ if (kept.length > 0)
5881
+ deps.stderr(
5882
+ `Still holding staff access to ${kept.map((t) => orgLabel(t.org)).join(", ")}. Run \`asteroid use --exit\` again to retry.
5883
+ `
5884
+ );
5885
+ deps.stderr(`Now using ${orgLabel(back)}.
5886
+ `);
5887
+ return kept.length === 0 ? 0 : 1;
5888
+ }
5889
+ async function askReason(deps, org) {
5890
+ if (!deps.ui) return void 0;
5891
+ const reason = await deps.ui.text(
5892
+ `You are not a member of ${clean(org)}. Why do you need staff access? This is audited.`
5893
+ );
5894
+ return reason?.trim() || void 0;
5895
+ }
5896
+ async function useAsStaff(deps, config, ref, reason, staff) {
5897
+ const store = new CredentialStore(deps.env);
5898
+ const held = await store.staffToken(ref);
5899
+ if (held && held.expiresAt - 3e4 > Date.now() && !reason) {
5900
+ saveConfig(deps.env, { ...config, org: held.org });
5901
+ deps.stderr(
5902
+ `Now using ${orgLabel(held.org)} with staff access, ${timeLeft(held.expiresAt)} left.
5903
+ `
5904
+ );
5905
+ return 0;
5906
+ }
5907
+ if (!reason && staff) reason = await askReason(deps, ref);
5908
+ if (!reason)
5909
+ throw new UsageError(
5910
+ `you are not a member of ${clean(ref)}.${staffHint(ref, staff)}${staff ? " The reason is audited." : ""}`
5911
+ );
5912
+ const minted = await mintStaffToken(
5913
+ deps,
5914
+ config,
5915
+ held?.org.id ?? ref,
5916
+ reason
5917
+ );
5918
+ const previous = config.org ? isMember(config, config.org) ? config.org : (await store.staffToken(config.org.workosId))?.from : void 0;
5919
+ try {
5920
+ await store.saveStaffToken(
5921
+ { ...minted, from: previous ?? held?.from },
5922
+ held?.grantId
5923
+ );
5924
+ } catch (error) {
5925
+ const home = await resolveAuth(
5926
+ deps.env,
5927
+ config,
5928
+ homeOrg(config).workosId,
5929
+ deps.fetch
5930
+ );
5931
+ await revokeStaffToken(deps, home, minted);
5932
+ throw error;
5933
+ }
5934
+ saveConfig(deps.env, { ...loadConfig(deps.env), org: minted.org });
5935
+ deps.stderr(
5936
+ `Staff access to ${orgLabel(minted.org)} for ${timeLeft(minted.expiresAt)}, until ${new Date(minted.expiresAt).toLocaleTimeString()}. Reason: ${clean(minted.reason)}. This is audited. Run \`asteroid use --exit\` to leave.
5937
+ `
5938
+ );
5939
+ return 0;
5940
+ }
5941
+ async function use(deps, ref, options = {}) {
5653
5942
  if (deps.env.apiKey)
5654
5943
  throw new UsageError(
5655
5944
  `${API_KEY_OVERRIDES} \`use\` only switches the login's organization. ${UNSET_API_KEY}`
5656
5945
  );
5946
+ if (options.exit) {
5947
+ if (ref || options.reason)
5948
+ throw new UsageError("usage: asteroid use --exit");
5949
+ return exitStaff(deps);
5950
+ }
5951
+ if (options.reason?.startsWith("-"))
5952
+ throw new UsageError(
5953
+ `--reason needs a value, not ${clean(options.reason)}. Give the reason without a leading -.`
5954
+ );
5657
5955
  const config = loadConfig(deps.env);
5658
5956
  if (!ref && deps.ui && (config.orgs?.length ?? 0) > 0) {
5659
5957
  ref = await deps.ui.select(
@@ -5666,12 +5964,20 @@ async function use(deps, ref) {
5666
5964
  if (!ref) return 1;
5667
5965
  }
5668
5966
  if (!ref) throw new UsageError("usage: asteroid use <org>");
5967
+ const login2 = await new CredentialStore(deps.env).readLogin();
5968
+ if (!login2) throw new AuthError("not signed in. Run `asteroid login`.");
5969
+ const staff = isStaffLogin(login2);
5669
5970
  const org = findOrg(ref, config.orgs ?? []);
5670
- if (!org)
5671
- throw new UsageError(
5672
- `unknown organization ${ref}. Run \`asteroid orgs\` to list yours.`
5673
- );
5674
- const auth = await resolveAuth(deps.env, config, org.workosId, deps.fetch);
5971
+ if (!org || !isMember(config, org) && options.reason)
5972
+ return useAsStaff(deps, config, ref, options.reason, staff);
5973
+ let auth;
5974
+ try {
5975
+ auth = await resolveAuth(deps.env, config, org.workosId, deps.fetch);
5976
+ } catch (error) {
5977
+ if (error instanceof NotMemberError)
5978
+ return useAsStaff(deps, config, ref, options.reason, staff);
5979
+ throw error;
5980
+ }
5675
5981
  const updated = loadConfig(deps.env);
5676
5982
  updated.org = updated.orgs?.find((o) => o.workosId === org.workosId) ?? auth.org ?? org;
5677
5983
  saveConfig(deps.env, updated);
@@ -5703,6 +6009,7 @@ async function whoami(deps, flagOrg, json) {
5703
6009
  );
5704
6010
  return 0;
5705
6011
  }
6012
+ const grant = body.staffGrant;
5706
6013
  const person = body.person;
5707
6014
  const orgId = body.organizationId;
5708
6015
  const organization = auth.org ? orgLabel(auth.org) : orgId ?? "none";
@@ -5711,9 +6018,10 @@ async function whoami(deps, flagOrg, json) {
5711
6018
  as: person?.email ?? body.id,
5712
6019
  kind: body.kind,
5713
6020
  credential: body.credential,
5714
- source: auth.kind === "api-key" ? "ASTEROID_API_KEY" : "asteroid login",
6021
+ source: auth.kind === "api-key" ? "ASTEROID_API_KEY" : auth.staff ? "asteroid use (staff access)" : "asteroid login",
5715
6022
  organization,
5716
- ...minutes === void 0 ? {} : { tokenExpiresIn: `${minutes}m` }
6023
+ ...grant ? { staffReason: grant.reason } : {},
6024
+ ...auth.expiresAt === void 0 ? {} : { tokenExpiresIn: timeLeft(auth.expiresAt) }
5717
6025
  })}
5718
6026
  `
5719
6027
  );
@@ -6107,8 +6415,8 @@ function executionDir(options, execution) {
6107
6415
  );
6108
6416
  return join3(root, "executions", executionDirName(execution.id));
6109
6417
  }
6110
- async function writeText(sync, path, text2) {
6111
- await sync.files.write(path, Buffer.from(text2, "utf8"));
6418
+ async function writeText(sync, path, text3) {
6419
+ await sync.files.write(path, Buffer.from(text3, "utf8"));
6112
6420
  }
6113
6421
  async function writeActivities(sync, dir, execution) {
6114
6422
  try {
@@ -6308,16 +6616,10 @@ function nodeFiles(cwd) {
6308
6616
 
6309
6617
  // src/sync/workflow-pull.ts
6310
6618
  var CONCURRENCY = 16;
6311
- function treeRequest(options) {
6312
- const base = `/agents/v2/workflows/${options.workflowId}`;
6313
- const path = options.versionId ? `${base}/versions/${options.versionId}/files` : `${base}/head/files`;
6314
- return get(path, { contents: "all" });
6315
- }
6316
- function pullDir(options, tree) {
6317
- if (options.dir) return options.dir;
6318
- const root = workflowRoot(options.root, tree.workflowName);
6319
- if (!options.versionId) return join3(root, "head");
6320
- return join3(root, ".versions", workflowDirName(tree.versionId, tree.version));
6619
+ function pullDir(root, tree, head) {
6620
+ const base = workflowRoot(root, tree.workflowName);
6621
+ if (head) return join3(base, "head");
6622
+ return join3(base, ".versions", workflowDirName(tree.versionId, tree.version));
6321
6623
  }
6322
6624
  async function agentFileBytes(sync, workflowId, tree, entry) {
6323
6625
  const path = `/agents/v2/workflows/${workflowId}/versions/${tree.versionId}/files/${entry.fileId}/signed-url`;
@@ -6362,13 +6664,19 @@ async function removeStale(sync, dir, keep) {
6362
6664
  }
6363
6665
  }
6364
6666
  async function writeManifest(sync, dir, manifest) {
6365
- const text2 = `${JSON.stringify(manifest, null, 2)}
6667
+ const text3 = `${JSON.stringify(manifest, null, 2)}
6366
6668
  `;
6367
- await sync.files.write(join3(dir, MANIFEST), Buffer.from(text2, "utf8"));
6669
+ await sync.files.write(join3(dir, MANIFEST), Buffer.from(text3, "utf8"));
6368
6670
  }
6369
6671
  async function pullWorkflow(sync, options) {
6370
- const tree = await sync.remote.json(treeRequest(options));
6371
- const dir = pullDir(options, tree);
6672
+ const request = {
6673
+ ...options.request,
6674
+ query: new URLSearchParams(options.request.query)
6675
+ };
6676
+ request.query.set("contents", "all");
6677
+ const tree = await sync.remote.json(request);
6678
+ const head = request.path.endsWith("/head/files");
6679
+ const dir = options.dir ?? pullDir(options.root ?? "", tree, head);
6372
6680
  const { hashes, skipped, shared } = await writeEntries(
6373
6681
  sync,
6374
6682
  options,
@@ -6380,7 +6688,7 @@ async function pullWorkflow(sync, options) {
6380
6688
  await writeManifest(sync, dir, {
6381
6689
  workflowId: options.workflowId,
6382
6690
  versionId: tree.versionId,
6383
- head: !options.versionId,
6691
+ head,
6384
6692
  rev: tree.rev,
6385
6693
  files: hashes
6386
6694
  });
@@ -6392,6 +6700,7 @@ async function pullWorkflow(sync, options) {
6392
6700
  version: tree.version,
6393
6701
  rev: tree.rev,
6394
6702
  dir,
6703
+ head,
6395
6704
  written: Object.keys(hashes).length,
6396
6705
  shared,
6397
6706
  skipped
@@ -6406,7 +6715,8 @@ function formatWorkflowPull(result) {
6406
6715
  `Written to: ${result.dir}/`,
6407
6716
  files,
6408
6717
  result.skipped.length > 0 ? `Skipped:
6409
- ${result.skipped.map((s) => ` - ${s}`).join("\n")}` : null
6718
+ ${result.skipped.map((s) => ` - ${s}`).join("\n")}` : null,
6719
+ result.head ? `Edit the files, then run \`asteroid workflow push\` from ${result.dir}/.` : "This is a pinned version. It is read-only."
6410
6720
  ].filter(Boolean).join("\n");
6411
6721
  }
6412
6722
 
@@ -6488,8 +6798,13 @@ async function refresh2(sync, dir, manifest, pushed) {
6488
6798
  await writeManifest(sync, dir, next);
6489
6799
  return next;
6490
6800
  }
6491
- async function pushWorkflow(sync, dir) {
6801
+ async function pushWorkflow(sync, dir, workflowId) {
6492
6802
  const manifest = await readManifest(sync, dir);
6803
+ if (workflowId !== void 0 && manifest.workflowId !== workflowId) {
6804
+ throw new UsageError(
6805
+ `${dir} holds workflow ${manifest.workflowId}, not ${workflowId}.`
6806
+ );
6807
+ }
6493
6808
  const { writes, deletes } = await localChanges(sync, dir, manifest);
6494
6809
  const result = {
6495
6810
  workflowId: manifest.workflowId,
@@ -6550,42 +6865,64 @@ function executionOptions(parsed) {
6550
6865
  dir: dir ? () => dir : void 0
6551
6866
  };
6552
6867
  }
6868
+ function withoutFlag(parsed, name) {
6869
+ const flags = new Map(parsed.flags);
6870
+ flags.delete(name);
6871
+ return { ...parsed, flags };
6872
+ }
6873
+ function runsLocally(command, parsed) {
6874
+ if (command.local) return true;
6875
+ if (command.files === "pull") return !parsed.json;
6876
+ if (command.files === "push") return parsed.body === void 0;
6877
+ return false;
6878
+ }
6879
+ var PARTIAL = ["paths", "variants"];
6880
+ function rejectPartial(parsed) {
6881
+ const used = PARTIAL.filter((name) => parsed.flags.has(name));
6882
+ if (used.length === 0) return;
6883
+ throw new UsageError(
6884
+ `pull writes the whole tree, so it cannot take ${used.map((name) => `--${name}`).join(" or ")}. Add --json to list those files instead.`
6885
+ );
6886
+ }
6887
+ async function pull(command, parsed, sync, deps) {
6888
+ rejectPartial(parsed);
6889
+ const result = await pullWorkflow(sync, {
6890
+ workflowId: parsed.positionals[0],
6891
+ request: await buildRequest(command, withoutFlag(parsed, "out"), {
6892
+ io: deps.io
6893
+ }),
6894
+ dir: flag(parsed, "out"),
6895
+ root: deps.root ?? "",
6896
+ ...deps.workflowPull
6897
+ });
6898
+ return { result, text: formatWorkflowPull(result) };
6899
+ }
6553
6900
  async function execute(command, parsed, sync, deps) {
6554
- const root = deps.root ?? "";
6555
- switch (command.command) {
6556
- case "workflow pull": {
6557
- const result = await pullWorkflow(sync, {
6558
- workflowId: parsed.positionals[0],
6559
- versionId: flag(parsed, "version-id"),
6560
- dir: flag(parsed, "dir"),
6561
- root,
6562
- ...deps.workflowPull
6563
- });
6564
- return { result, text: formatWorkflowPull(result) };
6565
- }
6566
- case "workflow push": {
6567
- const result = await pushWorkflow(sync, flag(parsed, "dir") ?? ".");
6568
- return { result, text: formatWorkflowPush(result) };
6569
- }
6570
- case "execution pull": {
6571
- const result = await pullExecution(sync, {
6572
- ...executionOptions(parsed),
6573
- root,
6574
- ...deps.executionPull
6575
- });
6576
- return { result, text: formatExecutionPull(result) };
6577
- }
6578
- default:
6579
- throw new UsageError(`unknown command ${command.command}`);
6901
+ if (command.files === "pull") return pull(command, parsed, sync, deps);
6902
+ if (command.files === "push") {
6903
+ const result2 = await pushWorkflow(
6904
+ sync,
6905
+ flag(parsed, "from") ?? ".",
6906
+ parsed.positionals[0] || void 0
6907
+ );
6908
+ return { result: result2, text: formatWorkflowPush(result2) };
6580
6909
  }
6910
+ if (command.command !== "execution pull")
6911
+ throw new UsageError(`unknown command ${command.command}`);
6912
+ const result = await pullExecution(sync, {
6913
+ ...executionOptions(parsed),
6914
+ root: deps.root ?? "",
6915
+ ...deps.executionPull
6916
+ });
6917
+ return { result, text: formatExecutionPull(result) };
6581
6918
  }
6582
6919
  async function runSync(command, parsed, deps) {
6583
6920
  const sync = {
6584
6921
  remote: deps.remote,
6585
6922
  files: deps.files ?? nodeFiles(deps.cwd)
6586
6923
  };
6587
- const { result, text: text2 } = await execute(command, parsed, sync, deps);
6588
- deps.stdout(`${parsed.json ? JSON.stringify(result, null, 2) : text2}
6924
+ const { result, text: text3 } = await execute(command, parsed, sync, deps);
6925
+ deps.stdout(`${parsed.json ? JSON.stringify(result, null, 2) : text3}
6589
6926
  `);
6590
6927
  return 0;
6591
6928
  }
@@ -6613,7 +6950,10 @@ async function runBuiltin(name, parsed, deps) {
6613
6950
  "run `asteroid complete <shell>` to print a completion script"
6614
6951
  );
6615
6952
  case "use":
6616
- return use(deps, parsed.positionals[0]);
6953
+ return use(deps, parsed.positionals[0], {
6954
+ reason: parsed.flags.get("reason")?.[0],
6955
+ exit: parsed.flags.has("exit")
6956
+ });
6617
6957
  default:
6618
6958
  return whoami(deps, parsed.org, parsed.json);
6619
6959
  }
@@ -6642,7 +6982,8 @@ async function authFor(parsed, deps) {
6642
6982
  function announce(command, auth, deps) {
6643
6983
  if (command.readOnly) return;
6644
6984
  const org = auth.kind === "api-key" ? "the API key's organization" : auth.org ? orgLabel(auth.org) : "your default organization";
6645
- deps.stderr(`Acting in ${org}.
6985
+ const staff = auth.staff ? ` with staff access (${clean(auth.staff.reason)})` : "";
6986
+ deps.stderr(`Acting in ${org}${staff}.
6646
6987
  `);
6647
6988
  }
6648
6989
  async function runCommand(command, parsed, deps) {
@@ -6710,7 +7051,9 @@ async function run(argv2, deps) {
6710
7051
  commands: ALL_COMMANDS,
6711
7052
  builtins: BUILTINS,
6712
7053
  version: VERSION,
6713
- description: DESCRIPTION
7054
+ description: DESCRIPTION,
7055
+ defaultArg: (command, arg) => command.files === "push" && arg.name === "workflowId" ? "" : void 0,
7056
+ defaultArgNote: "<workflow-id> defaults to the workflow the pushed folder was pulled from."
6714
7057
  });
6715
7058
  if (invocation.kind === "exit") {
6716
7059
  if (invocation.stdout) deps.stdout(invocation.stdout);
@@ -6720,7 +7063,7 @@ async function run(argv2, deps) {
6720
7063
  if (invocation.kind === "builtin") {
6721
7064
  return await runBuiltin(invocation.name, invocation.parsed, deps);
6722
7065
  }
6723
- if (invocation.command.local) {
7066
+ if (runsLocally(invocation.command, invocation.parsed)) {
6724
7067
  const auth = await authFor(invocation.parsed, deps);
6725
7068
  announce(invocation.command, auth, deps);
6726
7069
  return await runSync(invocation.command, invocation.parsed, {
@@ -6756,6 +7099,10 @@ var clackUi = {
6756
7099
  const value2 = await clack.select({ message, options: choices });
6757
7100
  return clack.isCancel(value2) ? void 0 : value2;
6758
7101
  },
7102
+ async text(message) {
7103
+ const value2 = await clack.text({ message });
7104
+ return clack.isCancel(value2) ? void 0 : value2;
7105
+ },
6759
7106
  async wait(message, done, work) {
6760
7107
  const spinner2 = clack.spinner();
6761
7108
  spinner2.start(message);
@@ -6790,8 +7137,8 @@ var code = await run(argv, {
6790
7137
  env: readEnv(),
6791
7138
  cwd: process.cwd(),
6792
7139
  fetch,
6793
- stdout: (text2) => process.stdout.write(text2),
6794
- stderr: (text2) => process.stderr.write(text2),
7140
+ stdout: (text3) => process.stdout.write(text3),
7141
+ stderr: (text3) => process.stderr.write(text3),
6795
7142
  writeBytes: (bytes) => process.stdout.write(bytes),
6796
7143
  ui: process.stdin.isTTY && process.stderr.isTTY ? clackUi : void 0,
6797
7144
  stdoutIsTTY: process.stdout.isTTY === true,