ghostdock 0.0.17 → 0.0.19
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-reference.md +69 -51
- package/dist/cli.md +64 -7
- package/dist/index.js +2178 -1563
- package/package.json +1 -1
package/dist/index.js
CHANGED
|
@@ -606,7 +606,7 @@ var GENERATED_TOOL_CATALOG = [
|
|
|
606
606
|
"sandboxes",
|
|
607
607
|
"create"
|
|
608
608
|
],
|
|
609
|
-
description: "Create a sandbox from a snapshot, OCI image, Dockerfile or Compose files. Inline content only: for a directory or ZIP (Dockerfile COPY/ADD, Compose build files) use `ghostdock sandboxes build`. Snapshots and catalog images can require environment entries, URL ports and MCP names (requiredEnvironment / requiredUrls / requiredMcps); a missing environment entry fails with its names (missingEnvironment), and removing a required URL or MCP fails with them (missingUrls / missingMcps). Returns once the
|
|
609
|
+
description: "Create a sandbox from a snapshot, OCI image, Dockerfile or Compose files. Inline content only: for a directory or ZIP (Dockerfile COPY/ADD, Compose build files) use `ghostdock sandboxes build`. Snapshots and catalog images can require environment entries, URL ports and MCP names (requiredEnvironment / requiredUrls / requiredMcps); a missing environment entry fails with its names (missingEnvironment), and removing a required URL or MCP fails with them (missingUrls / missingMcps). Every source but a snapshot becomes a Compose project in the sandbox, run once with docker compose up -d; after that Docker is the truth. Returns once the project is up, with `boot`: how long the sandbox (VM) and the project's compose up took, each in ms, and the images it pulled. Idle-stop and auto-delete apply (defaults 20 minutes / 14 days). Read the sandboxes skill for sources, manifests, lifecycle and providers.",
|
|
610
610
|
inputSchema: {
|
|
611
611
|
$schema: "https://json-schema.org/draft/2020-12/schema",
|
|
612
612
|
type: "object",
|
|
@@ -744,7 +744,17 @@ var GENERATED_TOOL_CATALOG = [
|
|
|
744
744
|
additionalProperties: false
|
|
745
745
|
}
|
|
746
746
|
],
|
|
747
|
-
description: 'What the sandbox boots from. Required. "image" boots an org catalog row by id ({ id } from images_list / images_get — images_add OCI bookmarks, images_add_dockerfile, and images_add_compose rows) or any OCI ref ({ ref }); "dockerfile" builds inline Dockerfile text only (ghostdock.* LABEL lines parsed for manifest; this tool has no build directory, so COPY/ADD of local files fails — use the CLI `ghostdock sandboxes build --kind dockerfile` with a directory or ZIP); "compose"
|
|
747
|
+
description: 'What the sandbox boots from. Required. "image" boots an org catalog row by id ({ id } from images_list / images_get — images_add OCI bookmarks, images_add_dockerfile, and images_add_compose rows) or any OCI ref ({ ref }); "dockerfile" builds inline Dockerfile text only (ghostdock.* LABEL lines parsed for manifest; this tool has no build directory, so COPY/ADD of local files fails — use the CLI `ghostdock sandboxes build --kind dockerfile` with a directory or ZIP); "compose" runs the given inline files with docker compose up -d (must include docker-compose.yml/yaml or compose.yml/yaml; ghostdock.* service labels parsed for manifest). Every source but a snapshot becomes a Compose project in /srv/ghostdock/<sandbox name>, run once; an image is one service named after it, a Dockerfile the service app. A Compose project that needs extra files is not accepted here — use `ghostdock sandboxes build --kind compose`. "compose" also takes a published Compose project instead of files ({ reference: "oci://…", override? }): it runs through include:, the override merged into it. Image, dockerfile, and compose sources never appear in snapshots_list. Published static sites use sites_publish (CDN), not a snapshot.'
|
|
748
|
+
},
|
|
749
|
+
restart: {
|
|
750
|
+
description: "Restart policy of the service an image or a Dockerfile becomes, Docker's names: unless-stopped (default), always, on-failure, or no (it does not come back after the sandbox sleeps). Not for Compose: its files set restart:.",
|
|
751
|
+
type: "string",
|
|
752
|
+
enum: [
|
|
753
|
+
"no",
|
|
754
|
+
"always",
|
|
755
|
+
"unless-stopped",
|
|
756
|
+
"on-failure"
|
|
757
|
+
]
|
|
748
758
|
},
|
|
749
759
|
resources: {
|
|
750
760
|
type: "object",
|
|
@@ -769,9 +779,20 @@ var GENERATED_TOOL_CATALOG = [
|
|
|
769
779
|
description: "Minutes of inactivity before the platform worker idle-stops the sandbox. -1 means always on. 0 is rejected. Omit for the platform default. Provider-native timers are optional backup.",
|
|
770
780
|
type: "number"
|
|
771
781
|
},
|
|
772
|
-
|
|
773
|
-
description: "
|
|
774
|
-
type: "
|
|
782
|
+
lifecycle: {
|
|
783
|
+
description: "What may make the sandbox disappear on its own. ephemeral: every stop wipes the VM and its disk, and the next start runs its Compose project again (edits in the project folder kept, everything else gone; locks too). auto_delete (default): a stop keeps the disk, and the record goes autoDeleteAfterDays after the stop. keep: never removed automatically. Omit for the image label ghostdock.ephemeral, else auto_delete. ephemeral is chosen here only.",
|
|
784
|
+
type: "string",
|
|
785
|
+
enum: [
|
|
786
|
+
"ephemeral",
|
|
787
|
+
"auto_delete",
|
|
788
|
+
"keep"
|
|
789
|
+
]
|
|
790
|
+
},
|
|
791
|
+
autoDeleteAfterDays: {
|
|
792
|
+
description: "Days the sandbox stays stopped before the platform worker deletes the record (0–365; 0 deletes it when it stops). Omit for the platform default (14). Not with keep. A container a person locked holds it.",
|
|
793
|
+
type: "integer",
|
|
794
|
+
minimum: 0,
|
|
795
|
+
maximum: 365
|
|
775
796
|
},
|
|
776
797
|
sandboxProviderId: {
|
|
777
798
|
description: "Configured sandbox-provider instance id from providers_list (not the provider kind). Omit to use the organization default (the org choice, else the platform default); if that provider lacks a capability this create needs, the first listed provider that accepts new sandboxes and has it is used. When no provider fits, the call fails with a not-configured error.",
|
|
@@ -859,12 +880,8 @@ var GENERATED_TOOL_CATALOG = [
|
|
|
859
880
|
},
|
|
860
881
|
additionalProperties: false
|
|
861
882
|
},
|
|
862
|
-
ephemeral: {
|
|
863
|
-
description: "Mark the sandbox ephemeral: every stop destroys the machine and the next start boots fresh, while the record, URLs, and settings stay. Omit to use the image label ghostdock.ephemeral, or false when that label is absent. Does not change idle-stop or auto-delete.",
|
|
864
|
-
type: "boolean"
|
|
865
|
-
},
|
|
866
883
|
imageBase: {
|
|
867
|
-
description: "How the disk is made for one of Ghostdock's
|
|
884
|
+
description: "How the disk is made for one of Ghostdock's platform images: auto (default) clones the host's image base, a disk with the image already on it, when there is one; off always starts from an empty disk that pulls the image. For tests and comparisons; other images always start empty.",
|
|
868
885
|
type: "string",
|
|
869
886
|
enum: [
|
|
870
887
|
"auto",
|
|
@@ -1012,7 +1029,7 @@ var GENERATED_TOOL_CATALOG = [
|
|
|
1012
1029
|
"sandboxes",
|
|
1013
1030
|
"list"
|
|
1014
1031
|
],
|
|
1015
|
-
description: "List the sandboxes of an organization,
|
|
1032
|
+
description: "List the sandboxes of an organization, with their lifecycle and deadlines. Each has a name (e.g. quiet-harbor), unique in the organization, that every sandbox tool accepts as sandboxId.",
|
|
1016
1033
|
inputSchema: {
|
|
1017
1034
|
$schema: "https://json-schema.org/draft/2020-12/schema",
|
|
1018
1035
|
type: "object",
|
|
@@ -4515,7 +4532,7 @@ var GENERATED_TOOL_CATALOG = [
|
|
|
4515
4532
|
}
|
|
4516
4533
|
},
|
|
4517
4534
|
reference: {
|
|
4518
|
-
description: "A published Compose project instead of files, e.g. oci://docker.io/ghostdockhq/
|
|
4535
|
+
description: "A published Compose project instead of files, e.g. oci://docker.io/ghostdockhq/headless-compose:ubuntu-26.04.",
|
|
4519
4536
|
type: "string",
|
|
4520
4537
|
minLength: 1,
|
|
4521
4538
|
maxLength: 512,
|
|
@@ -4672,7 +4689,7 @@ var GENERATED_TOOL_CATALOG = [
|
|
|
4672
4689
|
cliArgs: [
|
|
4673
4690
|
"sandboxId"
|
|
4674
4691
|
],
|
|
4675
|
-
description: "Upload small files or a base64 ZIP to a sandbox. For directories or large ZIPs, use `ghostdock sandboxes files upload`.",
|
|
4692
|
+
description: "Upload small files or a base64 ZIP to a sandbox's container. A relative path lands in the container's start folder (its dir: the login user's home, else WORKDIR, else /), an absolute one where it says; the answer names the absolute paths written and that folder (root). For directories or large ZIPs, use `ghostdock sandboxes files upload`.",
|
|
4676
4693
|
inputSchema: {
|
|
4677
4694
|
$schema: "https://json-schema.org/draft/2020-12/schema",
|
|
4678
4695
|
type: "object",
|
|
@@ -4755,7 +4772,7 @@ var GENERATED_TOOL_CATALOG = [
|
|
|
4755
4772
|
cliArgs: [
|
|
4756
4773
|
"sandboxId"
|
|
4757
4774
|
],
|
|
4758
|
-
description: "List the files in a sandbox directory (
|
|
4775
|
+
description: "List the files in a sandbox directory. Default: the container's start folder (its dir).",
|
|
4759
4776
|
inputSchema: {
|
|
4760
4777
|
$schema: "https://json-schema.org/draft/2020-12/schema",
|
|
4761
4778
|
type: "object",
|
|
@@ -4778,7 +4795,7 @@ var GENERATED_TOOL_CATALOG = [
|
|
|
4778
4795
|
pattern: "^[a-zA-Z0-9][a-zA-Z0-9_.-]*$"
|
|
4779
4796
|
},
|
|
4780
4797
|
path: {
|
|
4781
|
-
description: "Directory to list
|
|
4798
|
+
description: "Directory to list: absolute, or relative to the container's start folder (its dir: the login user's home, else WORKDIR, else /). Defaults to that folder.",
|
|
4782
4799
|
type: "string",
|
|
4783
4800
|
minLength: 1
|
|
4784
4801
|
}
|
|
@@ -4866,7 +4883,7 @@ var GENERATED_TOOL_CATALOG = [
|
|
|
4866
4883
|
path: {
|
|
4867
4884
|
type: "string",
|
|
4868
4885
|
minLength: 1,
|
|
4869
|
-
description: "File path
|
|
4886
|
+
description: "File path: absolute, or relative to the container's start folder (for example src/App.vue)."
|
|
4870
4887
|
}
|
|
4871
4888
|
},
|
|
4872
4889
|
required: [
|
|
@@ -6082,7 +6099,7 @@ var GENERATED_TOOL_CATALOG = [
|
|
|
6082
6099
|
cliArgs: [
|
|
6083
6100
|
"sandboxId"
|
|
6084
6101
|
],
|
|
6085
|
-
description: "List the containers of a sandbox, running or not. Each with state, health, restart policy, ports, Compose project and service,
|
|
6102
|
+
description: "List the containers of a sandbox, running or not. Each with state, health, restart policy, ports, Compose project and service, its lock (locked, and whether a person or its ghostdock.lock label decided), ghostdock.* labels, and which one is the default. A stopped sandbox lists none (it is not started for this).",
|
|
6086
6103
|
inputSchema: {
|
|
6087
6104
|
$schema: "https://json-schema.org/draft/2020-12/schema",
|
|
6088
6105
|
type: "object",
|
|
@@ -6166,18 +6183,18 @@ var GENERATED_TOOL_CATALOG = [
|
|
|
6166
6183
|
}
|
|
6167
6184
|
},
|
|
6168
6185
|
{
|
|
6169
|
-
id: "
|
|
6170
|
-
name: "
|
|
6186
|
+
id: "sandboxes_containers_lock",
|
|
6187
|
+
name: "sandboxes_containers_lock",
|
|
6171
6188
|
cli: [
|
|
6172
6189
|
"sandboxes",
|
|
6173
6190
|
"containers",
|
|
6174
|
-
"
|
|
6191
|
+
"lock"
|
|
6175
6192
|
],
|
|
6176
6193
|
cliArgs: [
|
|
6177
6194
|
"sandboxId",
|
|
6178
6195
|
"container"
|
|
6179
6196
|
],
|
|
6180
|
-
description: "
|
|
6197
|
+
description: "Lock a container against removal. A locked container cannot be removed, renamed or recreated through Docker (docker rm, compose down, a compose up that would recreate it) until it is unlocked here; start, stop and restart still work. It also pauses the sandbox’s auto-delete. An ephemeral sandbox’s stop still wipes it. Never starts the sandbox: a stopped one gets the lock at its next start.",
|
|
6181
6198
|
inputSchema: {
|
|
6182
6199
|
$schema: "https://json-schema.org/draft/2020-12/schema",
|
|
6183
6200
|
type: "object",
|
|
@@ -6198,17 +6215,63 @@ var GENERATED_TOOL_CATALOG = [
|
|
|
6198
6215
|
maxLength: 255,
|
|
6199
6216
|
pattern: "^[a-zA-Z0-9][a-zA-Z0-9_.-]*$",
|
|
6200
6217
|
description: "Container, by name."
|
|
6218
|
+
}
|
|
6219
|
+
},
|
|
6220
|
+
required: [
|
|
6221
|
+
"organization",
|
|
6222
|
+
"sandboxId",
|
|
6223
|
+
"container"
|
|
6224
|
+
],
|
|
6225
|
+
additionalProperties: false
|
|
6226
|
+
},
|
|
6227
|
+
outputSchema: {
|
|
6228
|
+
$schema: "https://json-schema.org/draft/2020-12/schema",
|
|
6229
|
+
type: "object",
|
|
6230
|
+
propertyNames: {
|
|
6231
|
+
type: "string"
|
|
6232
|
+
},
|
|
6233
|
+
additionalProperties: {}
|
|
6234
|
+
}
|
|
6235
|
+
},
|
|
6236
|
+
{
|
|
6237
|
+
id: "sandboxes_containers_unlock",
|
|
6238
|
+
name: "sandboxes_containers_unlock",
|
|
6239
|
+
cli: [
|
|
6240
|
+
"sandboxes",
|
|
6241
|
+
"containers",
|
|
6242
|
+
"unlock"
|
|
6243
|
+
],
|
|
6244
|
+
cliArgs: [
|
|
6245
|
+
"sandboxId",
|
|
6246
|
+
"container"
|
|
6247
|
+
],
|
|
6248
|
+
description: "Unlock a container so Docker can remove or recreate it. Also one its image locked with the ghostdock.lock label. Never starts the sandbox: a stopped one gets it at its next start, and an overdue auto-delete it held goes ahead.",
|
|
6249
|
+
inputSchema: {
|
|
6250
|
+
$schema: "https://json-schema.org/draft/2020-12/schema",
|
|
6251
|
+
type: "object",
|
|
6252
|
+
properties: {
|
|
6253
|
+
organization: {
|
|
6254
|
+
type: "string",
|
|
6255
|
+
minLength: 1,
|
|
6256
|
+
description: "Organization id or slug, from organizations_list. Ids survive renames; slugs change when the organization is renamed."
|
|
6201
6257
|
},
|
|
6202
|
-
|
|
6203
|
-
type: "
|
|
6204
|
-
|
|
6258
|
+
sandboxId: {
|
|
6259
|
+
type: "string",
|
|
6260
|
+
minLength: 1,
|
|
6261
|
+
description: "Sandbox name (e.g. quiet-harbor) or id, from sandboxes_list or sandboxes_create. Names are unique within the organization."
|
|
6262
|
+
},
|
|
6263
|
+
container: {
|
|
6264
|
+
type: "string",
|
|
6265
|
+
minLength: 1,
|
|
6266
|
+
maxLength: 255,
|
|
6267
|
+
pattern: "^[a-zA-Z0-9][a-zA-Z0-9_.-]*$",
|
|
6268
|
+
description: "Container, by name."
|
|
6205
6269
|
}
|
|
6206
6270
|
},
|
|
6207
6271
|
required: [
|
|
6208
6272
|
"organization",
|
|
6209
6273
|
"sandboxId",
|
|
6210
|
-
"container"
|
|
6211
|
-
"protected"
|
|
6274
|
+
"container"
|
|
6212
6275
|
],
|
|
6213
6276
|
additionalProperties: false
|
|
6214
6277
|
},
|
|
@@ -6284,7 +6347,7 @@ var GENERATED_TOOL_CATALOG = [
|
|
|
6284
6347
|
"sandboxId",
|
|
6285
6348
|
"container"
|
|
6286
6349
|
],
|
|
6287
|
-
description: "Stop a container, as docker stop does. It gets its own stop signal, then a kill after its own stop timeout. It stays stopped while the sandbox sleeps and wakes, as Docker keeps a stopped container stopped, unless its restart policy is always. On a stopped sandbox it does nothing (note sandbox_stopped).
|
|
6350
|
+
description: "Stop a container, as docker stop does. It gets its own stop signal, then a kill after its own stop timeout. It stays stopped while the sandbox sleeps and wakes, as Docker keeps a stopped container stopped, unless its restart policy is always. On a stopped sandbox it does nothing (note sandbox_stopped). A lock does not block it. note says when it was the default container or the last one running: then SSH, commands and files have nowhere to land until one runs.",
|
|
6288
6351
|
inputSchema: {
|
|
6289
6352
|
$schema: "https://json-schema.org/draft/2020-12/schema",
|
|
6290
6353
|
type: "object",
|
|
@@ -6827,7 +6890,7 @@ var GENERATED_TOOL_CATALOG = [
|
|
|
6827
6890
|
pattern: "^[a-zA-Z0-9][a-zA-Z0-9_.-]*$"
|
|
6828
6891
|
},
|
|
6829
6892
|
sessionId: {
|
|
6830
|
-
description: "Id for the new session: a slug
|
|
6893
|
+
description: "Id for the new session: a slug. Omit to generate one.",
|
|
6831
6894
|
type: "string",
|
|
6832
6895
|
minLength: 1
|
|
6833
6896
|
}
|
|
@@ -6902,62 +6965,6 @@ var GENERATED_TOOL_CATALOG = [
|
|
|
6902
6965
|
additionalProperties: {}
|
|
6903
6966
|
}
|
|
6904
6967
|
},
|
|
6905
|
-
{
|
|
6906
|
-
id: "sandboxes_set_boot",
|
|
6907
|
-
name: "sandboxes_set_boot",
|
|
6908
|
-
cli: [
|
|
6909
|
-
"sandboxes",
|
|
6910
|
-
"set-boot"
|
|
6911
|
-
],
|
|
6912
|
-
cliArgs: [
|
|
6913
|
-
"sandboxId"
|
|
6914
|
-
],
|
|
6915
|
-
description: "Register a command that starts now and again after every restart. For example bun dev. On Compose it runs after the stack; on a Dockerfile it replaces the image CMD. Snapshots cannot register one. Read the sandbox-boot skill.",
|
|
6916
|
-
inputSchema: {
|
|
6917
|
-
$schema: "https://json-schema.org/draft/2020-12/schema",
|
|
6918
|
-
type: "object",
|
|
6919
|
-
properties: {
|
|
6920
|
-
organization: {
|
|
6921
|
-
type: "string",
|
|
6922
|
-
minLength: 1,
|
|
6923
|
-
description: "Organization id or slug, from organizations_list. Ids survive renames; slugs change when the organization is renamed."
|
|
6924
|
-
},
|
|
6925
|
-
sandboxId: {
|
|
6926
|
-
type: "string",
|
|
6927
|
-
minLength: 1,
|
|
6928
|
-
description: "Sandbox name (e.g. quiet-harbor) or id, from sandboxes_list or sandboxes_create. Names are unique within the organization."
|
|
6929
|
-
},
|
|
6930
|
-
command: {
|
|
6931
|
-
type: "string",
|
|
6932
|
-
minLength: 1,
|
|
6933
|
-
description: "Command to start, e.g. bun dev."
|
|
6934
|
-
},
|
|
6935
|
-
cwd: {
|
|
6936
|
-
description: "Directory to run it in. Default /.",
|
|
6937
|
-
type: "string",
|
|
6938
|
-
minLength: 1
|
|
6939
|
-
},
|
|
6940
|
-
start: {
|
|
6941
|
-
description: "Also start it now, not only after restarts. Default true.",
|
|
6942
|
-
type: "boolean"
|
|
6943
|
-
}
|
|
6944
|
-
},
|
|
6945
|
-
required: [
|
|
6946
|
-
"organization",
|
|
6947
|
-
"sandboxId",
|
|
6948
|
-
"command"
|
|
6949
|
-
],
|
|
6950
|
-
additionalProperties: false
|
|
6951
|
-
},
|
|
6952
|
-
outputSchema: {
|
|
6953
|
-
$schema: "https://json-schema.org/draft/2020-12/schema",
|
|
6954
|
-
type: "object",
|
|
6955
|
-
propertyNames: {
|
|
6956
|
-
type: "string"
|
|
6957
|
-
},
|
|
6958
|
-
additionalProperties: {}
|
|
6959
|
-
}
|
|
6960
|
-
},
|
|
6961
6968
|
{
|
|
6962
6969
|
id: "sandboxes_update",
|
|
6963
6970
|
name: "sandboxes_update",
|
|
@@ -6968,7 +6975,7 @@ var GENERATED_TOOL_CATALOG = [
|
|
|
6968
6975
|
cliArgs: [
|
|
6969
6976
|
"sandboxId"
|
|
6970
6977
|
],
|
|
6971
|
-
description: "Change a sandbox’s display name, idle-stop or
|
|
6978
|
+
description: "Change a sandbox’s display name, idle-stop interval or lifecycle. Omit a field to keep it; null resets it to the platform default. Ephemeral is chosen at create only.",
|
|
6972
6979
|
inputSchema: {
|
|
6973
6980
|
$schema: "https://json-schema.org/draft/2020-12/schema",
|
|
6974
6981
|
type: "object",
|
|
@@ -7005,11 +7012,21 @@ var GENERATED_TOOL_CATALOG = [
|
|
|
7005
7012
|
}
|
|
7006
7013
|
]
|
|
7007
7014
|
},
|
|
7008
|
-
|
|
7009
|
-
description: "
|
|
7015
|
+
lifecycle: {
|
|
7016
|
+
description: "auto_delete: the record goes autoDeleteAfterDays after the sandbox stops. keep: never removed automatically.",
|
|
7017
|
+
type: "string",
|
|
7018
|
+
enum: [
|
|
7019
|
+
"auto_delete",
|
|
7020
|
+
"keep"
|
|
7021
|
+
]
|
|
7022
|
+
},
|
|
7023
|
+
autoDeleteAfterDays: {
|
|
7024
|
+
description: "Days stopped before the record is deleted (0–365; 0 deletes it when it stops). Null writes the platform default. Not with keep.",
|
|
7010
7025
|
anyOf: [
|
|
7011
7026
|
{
|
|
7012
|
-
type: "
|
|
7027
|
+
type: "integer",
|
|
7028
|
+
minimum: 0,
|
|
7029
|
+
maximum: 365
|
|
7013
7030
|
},
|
|
7014
7031
|
{
|
|
7015
7032
|
type: "null"
|
|
@@ -7263,7 +7280,7 @@ var GENERATED_TOOL_CATALOG = [
|
|
|
7263
7280
|
cliArgs: [
|
|
7264
7281
|
"sandboxId"
|
|
7265
7282
|
],
|
|
7266
|
-
description: "Permanently delete a sandbox and its files.",
|
|
7283
|
+
description: "Permanently delete a sandbox and its files. Refused while it has locked containers (sandbox_has_locked_containers, naming them) unless deleteLocked is true.",
|
|
7267
7284
|
inputSchema: {
|
|
7268
7285
|
$schema: "https://json-schema.org/draft/2020-12/schema",
|
|
7269
7286
|
type: "object",
|
|
@@ -7277,6 +7294,10 @@ var GENERATED_TOOL_CATALOG = [
|
|
|
7277
7294
|
type: "string",
|
|
7278
7295
|
minLength: 1,
|
|
7279
7296
|
description: "Sandbox name (e.g. quiet-harbor) or id, from sandboxes_list or sandboxes_create. Names are unique within the organization."
|
|
7297
|
+
},
|
|
7298
|
+
deleteLocked: {
|
|
7299
|
+
description: "Delete it with its locked containers. Without it, a locked container refuses the delete.",
|
|
7300
|
+
type: "boolean"
|
|
7280
7301
|
}
|
|
7281
7302
|
},
|
|
7282
7303
|
required: [
|
|
@@ -7321,7 +7342,7 @@ var GENERATED_TOOL_CATALOG = [
|
|
|
7321
7342
|
minLength: 1
|
|
7322
7343
|
},
|
|
7323
7344
|
directory: {
|
|
7324
|
-
description: "Sandbox directory to upload when files/zip are omitted. Defaults to dist.",
|
|
7345
|
+
description: "Sandbox directory to upload when files/zip are omitted: absolute, or relative to the container's start folder (its dir). Defaults to dist there.",
|
|
7325
7346
|
type: "string",
|
|
7326
7347
|
minLength: 1
|
|
7327
7348
|
},
|
|
@@ -7695,9 +7716,20 @@ var GENERATED_TOOL_CATALOG = [
|
|
|
7695
7716
|
description: "Minutes of inactivity before the platform worker idle-stops the sandbox. -1 means always on. 0 is rejected. Omit for the platform default. Provider-native timers are optional backup.",
|
|
7696
7717
|
type: "number"
|
|
7697
7718
|
},
|
|
7698
|
-
|
|
7699
|
-
description: "
|
|
7700
|
-
type: "
|
|
7719
|
+
lifecycle: {
|
|
7720
|
+
description: "What may make the sandbox disappear on its own. ephemeral: every stop wipes the VM and its disk, and the next start runs its Compose project again (edits in the project folder kept, everything else gone; locks too). auto_delete (default): a stop keeps the disk, and the record goes autoDeleteAfterDays after the stop. keep: never removed automatically. Omit for the image label ghostdock.ephemeral, else auto_delete. ephemeral is chosen here only.",
|
|
7721
|
+
type: "string",
|
|
7722
|
+
enum: [
|
|
7723
|
+
"ephemeral",
|
|
7724
|
+
"auto_delete",
|
|
7725
|
+
"keep"
|
|
7726
|
+
]
|
|
7727
|
+
},
|
|
7728
|
+
autoDeleteAfterDays: {
|
|
7729
|
+
description: "Days the sandbox stays stopped before the platform worker deletes the record (0–365; 0 deletes it when it stops). Omit for the platform default (14). Not with keep. A container a person locked holds it.",
|
|
7730
|
+
type: "integer",
|
|
7731
|
+
minimum: 0,
|
|
7732
|
+
maximum: 365
|
|
7701
7733
|
},
|
|
7702
7734
|
sandboxProviderId: {
|
|
7703
7735
|
description: "Configured sandbox-provider instance id from providers_list (not the provider kind). Omit to use the organization default (the org choice, else the platform default); if that provider lacks a capability this create needs, the first listed provider that accepts new sandboxes and has it is used. When no provider fits, the call fails with a not-configured error.",
|
|
@@ -7785,10 +7817,6 @@ var GENERATED_TOOL_CATALOG = [
|
|
|
7785
7817
|
},
|
|
7786
7818
|
additionalProperties: false
|
|
7787
7819
|
},
|
|
7788
|
-
ephemeral: {
|
|
7789
|
-
description: "Mark the sandbox ephemeral: every stop destroys the machine and the next start boots fresh, while the record, URLs, and settings stay. Omit to use the image label ghostdock.ephemeral, or false when that label is absent. Does not change idle-stop or auto-delete.",
|
|
7790
|
-
type: "boolean"
|
|
7791
|
-
},
|
|
7792
7820
|
autoWake: {
|
|
7793
7821
|
description: "Same as urls[].autoWake on sandboxes_create. Default true for the minted URL (sandboxes_urls_create defaults false).",
|
|
7794
7822
|
type: "boolean"
|
|
@@ -8165,9 +8193,20 @@ var GENERATED_TOOL_CATALOG = [
|
|
|
8165
8193
|
description: "Minutes of inactivity before the platform worker idle-stops the sandbox. -1 means always on. 0 is rejected. Omit for the platform default. Provider-native timers are optional backup.",
|
|
8166
8194
|
type: "number"
|
|
8167
8195
|
},
|
|
8168
|
-
|
|
8169
|
-
description: "
|
|
8170
|
-
type: "
|
|
8196
|
+
lifecycle: {
|
|
8197
|
+
description: "What may make the sandbox disappear on its own. ephemeral: every stop wipes the VM and its disk, and the next start runs its Compose project again (edits in the project folder kept, everything else gone; locks too). auto_delete (default): a stop keeps the disk, and the record goes autoDeleteAfterDays after the stop. keep: never removed automatically. Omit for the image label ghostdock.ephemeral, else auto_delete. ephemeral is chosen here only.",
|
|
8198
|
+
type: "string",
|
|
8199
|
+
enum: [
|
|
8200
|
+
"ephemeral",
|
|
8201
|
+
"auto_delete",
|
|
8202
|
+
"keep"
|
|
8203
|
+
]
|
|
8204
|
+
},
|
|
8205
|
+
autoDeleteAfterDays: {
|
|
8206
|
+
description: "Days the sandbox stays stopped before the platform worker deletes the record (0–365; 0 deletes it when it stops). Omit for the platform default (14). Not with keep. A container a person locked holds it.",
|
|
8207
|
+
type: "integer",
|
|
8208
|
+
minimum: 0,
|
|
8209
|
+
maximum: 365
|
|
8171
8210
|
},
|
|
8172
8211
|
sandboxProviderId: {
|
|
8173
8212
|
description: "Configured sandbox-provider instance id from providers_list (not the provider kind). Omit to use the organization default (the org choice, else the platform default); if that provider lacks a capability this create needs, the first listed provider that accepts new sandboxes and has it is used. When no provider fits, the call fails with a not-configured error.",
|
|
@@ -8255,10 +8294,6 @@ var GENERATED_TOOL_CATALOG = [
|
|
|
8255
8294
|
},
|
|
8256
8295
|
additionalProperties: false
|
|
8257
8296
|
},
|
|
8258
|
-
ephemeral: {
|
|
8259
|
-
description: "Mark the sandbox ephemeral: every stop destroys the machine and the next start boots fresh, while the record, URLs, and settings stay. Omit to use the image label ghostdock.ephemeral, or false when that label is absent. Does not change idle-stop or auto-delete.",
|
|
8260
|
-
type: "boolean"
|
|
8261
|
-
},
|
|
8262
8297
|
autoWake: {
|
|
8263
8298
|
description: "Same as urls[].autoWake on sandboxes_create. Default true for the minted URL.",
|
|
8264
8299
|
type: "boolean"
|
|
@@ -9183,8 +9218,25 @@ function getCliName(argv = process.argv) {
|
|
|
9183
9218
|
const invoked = basename(entry);
|
|
9184
9219
|
return CLI_NAMES.find((name) => name === invoked) ?? "ghostdock";
|
|
9185
9220
|
}
|
|
9221
|
+
|
|
9222
|
+
// src/commands/docker.ts
|
|
9223
|
+
import { spawn as spawn2, spawnSync as spawnSync3 } from "node:child_process";
|
|
9224
|
+
|
|
9225
|
+
// ../../shared/lib/private-network.ts
|
|
9226
|
+
var ALL_BITS = (1n << 128n) - 1n;
|
|
9227
|
+
|
|
9228
|
+
// ../../shared/lib/docker-access.ts
|
|
9229
|
+
var DOCKER_CERTIFICATE_RENEW_DAYS = 10;
|
|
9230
|
+
var DOCKER_CLIENT_URI_PREFIX = {
|
|
9231
|
+
user: "urn:ghostdock:user:",
|
|
9232
|
+
device: "urn:ghostdock:device:",
|
|
9233
|
+
organization: "urn:ghostdock:org:"
|
|
9234
|
+
};
|
|
9235
|
+
function dockerContextName(orgSlug, sandboxName) {
|
|
9236
|
+
return `ghostdock-${orgSlug}-${sandboxName}`;
|
|
9237
|
+
}
|
|
9186
9238
|
// package.json
|
|
9187
|
-
var version = "0.0.
|
|
9239
|
+
var version = "0.0.19";
|
|
9188
9240
|
var package_default = {
|
|
9189
9241
|
name: "ghostdock",
|
|
9190
9242
|
description: "Ghostdock CLI — device login and agent tool invocation from the command line",
|
|
@@ -9452,11 +9504,15 @@ function deviceStateText(state, now = new Date) {
|
|
|
9452
9504
|
return "Not connected (logged out)";
|
|
9453
9505
|
if (state.status === "paused" && state.pauseReason) {
|
|
9454
9506
|
const text = state.pauseReason === "manual" ? "Paused by you" : `Paused: ${devicePauseReasonText(state.pauseReason, state)}`;
|
|
9455
|
-
|
|
9507
|
+
if (state.connected)
|
|
9508
|
+
return text;
|
|
9509
|
+
return state.connecting ? `${text}, connecting` : `Not connected (${text.toLowerCase()})`;
|
|
9456
9510
|
}
|
|
9457
9511
|
const left = state.pausesAt ? new Date(state.pausesAt).getTime() - now.getTime() : 0;
|
|
9458
9512
|
const active = `Active, pauses in ${formatRemaining(Math.max(left, 0) / 1000)}`;
|
|
9459
|
-
|
|
9513
|
+
if (state.connected)
|
|
9514
|
+
return active;
|
|
9515
|
+
return state.connecting ? `${active}, connecting` : `Not connected (armed, pauses in ${formatRemaining(Math.max(left, 0) / 1000)})`;
|
|
9460
9516
|
}
|
|
9461
9517
|
function formatRemaining(seconds) {
|
|
9462
9518
|
const minutes = Math.floor(seconds / 60);
|
|
@@ -9791,11 +9847,388 @@ async function authorizedRequest(path, init = {}) {
|
|
|
9791
9847
|
}
|
|
9792
9848
|
}
|
|
9793
9849
|
|
|
9794
|
-
// src/
|
|
9795
|
-
import {
|
|
9850
|
+
// src/docker/certificates.ts
|
|
9851
|
+
import { spawnSync } from "node:child_process";
|
|
9852
|
+
|
|
9853
|
+
// src/docker/context.ts
|
|
9854
|
+
function contextDescription(sandbox, orgSlug) {
|
|
9855
|
+
return `Ghostdock sandbox ${sandbox} (${orgSlug}; needs ${getCliName()} network connect)`;
|
|
9856
|
+
}
|
|
9857
|
+
function csvField(field) {
|
|
9858
|
+
return /[",\r\n]/.test(field) ? `"${field.replaceAll('"', '""')}"` : field;
|
|
9859
|
+
}
|
|
9860
|
+
function contextArgs(verb, spec) {
|
|
9861
|
+
const endpoint = [
|
|
9862
|
+
`host=${spec.host}`,
|
|
9863
|
+
`ca=${spec.files.ca}`,
|
|
9864
|
+
`cert=${spec.files.cert}`,
|
|
9865
|
+
`key=${spec.files.key}`
|
|
9866
|
+
].map(csvField).join(",");
|
|
9867
|
+
return ["context", verb, spec.name, "--description", spec.description, "--docker", endpoint];
|
|
9868
|
+
}
|
|
9869
|
+
function contextHost(exec, name) {
|
|
9870
|
+
const result = exec("docker", [
|
|
9871
|
+
"context",
|
|
9872
|
+
"inspect",
|
|
9873
|
+
"--format",
|
|
9874
|
+
"{{.Endpoints.docker.Host}}",
|
|
9875
|
+
name
|
|
9876
|
+
]);
|
|
9877
|
+
return result.status === 0 ? result.stdout.trim() : null;
|
|
9878
|
+
}
|
|
9879
|
+
function dockerFailure(args, result) {
|
|
9880
|
+
const said = result.stderr.trim() || result.stdout.trim();
|
|
9881
|
+
return new Error(`docker ${args.slice(0, 3).join(" ")} failed (exit ${result.status})${said ? `: ${said}` : "."}`);
|
|
9882
|
+
}
|
|
9883
|
+
function writeContext(exec, spec) {
|
|
9884
|
+
const created = contextHost(exec, spec.name) === null;
|
|
9885
|
+
const args = contextArgs(created ? "create" : "update", spec);
|
|
9886
|
+
const result = exec("docker", args);
|
|
9887
|
+
if (result.status !== 0)
|
|
9888
|
+
throw dockerFailure(args, result);
|
|
9889
|
+
return { created };
|
|
9890
|
+
}
|
|
9891
|
+
function useContext(exec, name) {
|
|
9892
|
+
const args = ["context", "use", name];
|
|
9893
|
+
const result = exec("docker", args);
|
|
9894
|
+
if (result.status !== 0)
|
|
9895
|
+
throw dockerFailure(args, result);
|
|
9896
|
+
}
|
|
9897
|
+
|
|
9898
|
+
// src/docker/csr.ts
|
|
9899
|
+
import { createPublicKey as createPublicKey2, sign } from "node:crypto";
|
|
9900
|
+
var TAG = {
|
|
9901
|
+
integer: 2,
|
|
9902
|
+
bitString: 3,
|
|
9903
|
+
utf8String: 12,
|
|
9904
|
+
sequence: 48,
|
|
9905
|
+
set: 49,
|
|
9906
|
+
context0: 160
|
|
9907
|
+
};
|
|
9908
|
+
var OID_COMMON_NAME = Buffer.from("0603550403", "hex");
|
|
9909
|
+
var OID_ECDSA_WITH_SHA256 = Buffer.from("06082a8648ce3d040302", "hex");
|
|
9910
|
+
function der(tag, content) {
|
|
9911
|
+
const length = content.length;
|
|
9912
|
+
if (length < 128)
|
|
9913
|
+
return Buffer.concat([Buffer.from([tag, length]), content]);
|
|
9914
|
+
const bytes = [];
|
|
9915
|
+
for (let rest = length;rest > 0; rest >>= 8)
|
|
9916
|
+
bytes.unshift(rest & 255);
|
|
9917
|
+
return Buffer.concat([Buffer.from([tag, 128 | bytes.length, ...bytes]), content]);
|
|
9918
|
+
}
|
|
9919
|
+
function csrSubject(deviceId) {
|
|
9920
|
+
return `ghostdock device ${deviceId}`;
|
|
9921
|
+
}
|
|
9922
|
+
function buildCsr(privateKey, deviceId) {
|
|
9923
|
+
const spki = createPublicKey2(privateKey).export({ type: "spki", format: "der" });
|
|
9924
|
+
const commonName = der(TAG.sequence, Buffer.concat([OID_COMMON_NAME, der(TAG.utf8String, Buffer.from(csrSubject(deviceId)))]));
|
|
9925
|
+
const subject = der(TAG.sequence, der(TAG.set, commonName));
|
|
9926
|
+
const info = der(TAG.sequence, Buffer.concat([
|
|
9927
|
+
der(TAG.integer, Buffer.from([0])),
|
|
9928
|
+
subject,
|
|
9929
|
+
spki,
|
|
9930
|
+
der(TAG.context0, Buffer.alloc(0))
|
|
9931
|
+
]));
|
|
9932
|
+
const signature = sign("sha256", info, { key: privateKey, dsaEncoding: "der" });
|
|
9933
|
+
return der(TAG.sequence, Buffer.concat([
|
|
9934
|
+
info,
|
|
9935
|
+
der(TAG.sequence, OID_ECDSA_WITH_SHA256),
|
|
9936
|
+
der(TAG.bitString, Buffer.concat([Buffer.from([0]), signature]))
|
|
9937
|
+
]));
|
|
9938
|
+
}
|
|
9939
|
+
function buildCsrPem(privateKey, deviceId) {
|
|
9940
|
+
const base64 = buildCsr(privateKey, deviceId).toString("base64");
|
|
9941
|
+
const lines = base64.match(/.{1,64}/g) ?? [];
|
|
9942
|
+
return `-----BEGIN CERTIFICATE REQUEST-----
|
|
9943
|
+
${lines.join(`
|
|
9944
|
+
`)}
|
|
9945
|
+
-----END CERTIFICATE REQUEST-----
|
|
9946
|
+
`;
|
|
9947
|
+
}
|
|
9948
|
+
|
|
9949
|
+
// src/docker/store.ts
|
|
9950
|
+
import {
|
|
9951
|
+
createPrivateKey as createPrivateKey2,
|
|
9952
|
+
generateKeyPairSync as generateKeyPairSync2,
|
|
9953
|
+
randomBytes,
|
|
9954
|
+
X509Certificate
|
|
9955
|
+
} from "node:crypto";
|
|
9956
|
+
import { chmod as chmod3, link, mkdir as mkdir3, readdir, readFile as readFile3, rename, unlink, writeFile as writeFile3 } from "node:fs/promises";
|
|
9796
9957
|
import { dirname as dirname3, join as join3 } from "node:path";
|
|
9958
|
+
function dockerDirFor(networkDir) {
|
|
9959
|
+
return join3(dirname3(networkDir), "docker");
|
|
9960
|
+
}
|
|
9961
|
+
function isOrganizationId(id) {
|
|
9962
|
+
return /^[A-Za-z0-9_-]{1,128}$/.test(id);
|
|
9963
|
+
}
|
|
9964
|
+
function organizationFiles(dockerDir, organizationId) {
|
|
9965
|
+
if (!isOrganizationId(organizationId)) {
|
|
9966
|
+
throw new Error(`Not an organization id: ${JSON.stringify(organizationId)}`);
|
|
9967
|
+
}
|
|
9968
|
+
const dir = join3(dockerDir, organizationId);
|
|
9969
|
+
return {
|
|
9970
|
+
dir,
|
|
9971
|
+
key: join3(dir, "key.pem"),
|
|
9972
|
+
cert: join3(dir, "cert.pem"),
|
|
9973
|
+
ca: join3(dir, "ca.pem"),
|
|
9974
|
+
meta: join3(dir, "certificate.json")
|
|
9975
|
+
};
|
|
9976
|
+
}
|
|
9977
|
+
async function listOrganizationIds(dockerDir) {
|
|
9978
|
+
try {
|
|
9979
|
+
const entries = await readdir(dockerDir, { withFileTypes: true });
|
|
9980
|
+
return entries.filter((entry) => entry.isDirectory() && isOrganizationId(entry.name)).map((entry) => entry.name).sort();
|
|
9981
|
+
} catch (err) {
|
|
9982
|
+
if (err.code === "ENOENT")
|
|
9983
|
+
return [];
|
|
9984
|
+
throw err;
|
|
9985
|
+
}
|
|
9986
|
+
}
|
|
9987
|
+
function tempPath(path) {
|
|
9988
|
+
return `${path}.${process.pid}.${randomBytes(4).toString("hex")}.tmp`;
|
|
9989
|
+
}
|
|
9990
|
+
async function writeFileAtomic(path, contents, mode) {
|
|
9991
|
+
await mkdir3(dirname3(path), { recursive: true, mode: 448 });
|
|
9992
|
+
const temp = tempPath(path);
|
|
9993
|
+
try {
|
|
9994
|
+
await writeFile3(temp, contents, { mode });
|
|
9995
|
+
await chmod3(temp, mode);
|
|
9996
|
+
await rename(temp, path);
|
|
9997
|
+
} catch (err) {
|
|
9998
|
+
await unlink(temp).catch(() => {});
|
|
9999
|
+
throw err;
|
|
10000
|
+
}
|
|
10001
|
+
}
|
|
10002
|
+
function isP256(key) {
|
|
10003
|
+
return key.asymmetricKeyType === "ec" && key.asymmetricKeyDetails?.namedCurve === "prime256v1";
|
|
10004
|
+
}
|
|
10005
|
+
async function loadOrCreateDockerKey(files) {
|
|
10006
|
+
const existing = await readDockerKey(files);
|
|
10007
|
+
if (existing)
|
|
10008
|
+
return existing;
|
|
10009
|
+
const { privateKey } = generateKeyPairSync2("ec", { namedCurve: "prime256v1" });
|
|
10010
|
+
const pem = privateKey.export({ type: "pkcs8", format: "pem" }).toString();
|
|
10011
|
+
await mkdir3(files.dir, { recursive: true, mode: 448 });
|
|
10012
|
+
const temp = tempPath(files.key);
|
|
10013
|
+
try {
|
|
10014
|
+
await writeFile3(temp, pem, { mode: 384 });
|
|
10015
|
+
await chmod3(temp, 384);
|
|
10016
|
+
await link(temp, files.key);
|
|
10017
|
+
} catch (err) {
|
|
10018
|
+
if (err.code !== "EEXIST")
|
|
10019
|
+
throw err;
|
|
10020
|
+
const created = await readDockerKey(files);
|
|
10021
|
+
if (created)
|
|
10022
|
+
return created;
|
|
10023
|
+
throw err;
|
|
10024
|
+
} finally {
|
|
10025
|
+
await unlink(temp).catch(() => {});
|
|
10026
|
+
}
|
|
10027
|
+
return privateKey;
|
|
10028
|
+
}
|
|
10029
|
+
async function readDockerKey(files) {
|
|
10030
|
+
let pem;
|
|
10031
|
+
try {
|
|
10032
|
+
pem = await readFile3(files.key, "utf8");
|
|
10033
|
+
} catch (err) {
|
|
10034
|
+
if (err.code === "ENOENT")
|
|
10035
|
+
return null;
|
|
10036
|
+
throw err;
|
|
10037
|
+
}
|
|
10038
|
+
const key = createPrivateKey2(pem);
|
|
10039
|
+
if (!isP256(key))
|
|
10040
|
+
throw new Error(`${files.key} is not an ECDSA P-256 key.`);
|
|
10041
|
+
return key;
|
|
10042
|
+
}
|
|
10043
|
+
function uriSan(certificate, prefix) {
|
|
10044
|
+
for (const name of (certificate.subjectAltName ?? "").split(/,\s*/)) {
|
|
10045
|
+
if (name.startsWith(`URI:${prefix}`))
|
|
10046
|
+
return name.slice(`URI:${prefix}`.length);
|
|
10047
|
+
}
|
|
10048
|
+
return null;
|
|
10049
|
+
}
|
|
10050
|
+
function parseClientCertificate(pem, key) {
|
|
10051
|
+
const certificate = new X509Certificate(pem);
|
|
10052
|
+
if (!certificate.checkPrivateKey(key)) {
|
|
10053
|
+
throw new Error("The certificate is not for this machine's Docker key.");
|
|
10054
|
+
}
|
|
10055
|
+
return {
|
|
10056
|
+
pem,
|
|
10057
|
+
serial: certificate.serialNumber.toLowerCase(),
|
|
10058
|
+
notAfter: new Date(certificate.validTo),
|
|
10059
|
+
deviceId: uriSan(certificate, DOCKER_CLIENT_URI_PREFIX.device),
|
|
10060
|
+
organizationId: uriSan(certificate, DOCKER_CLIENT_URI_PREFIX.organization)
|
|
10061
|
+
};
|
|
10062
|
+
}
|
|
10063
|
+
async function readClientCertificate(files, key) {
|
|
10064
|
+
try {
|
|
10065
|
+
const [pem] = await Promise.all([readFile3(files.cert, "utf8"), readFile3(files.ca, "utf8")]);
|
|
10066
|
+
return parseClientCertificate(pem, key);
|
|
10067
|
+
} catch {
|
|
10068
|
+
return null;
|
|
10069
|
+
}
|
|
10070
|
+
}
|
|
10071
|
+
var DAY_MS = 24 * 60 * 60 * 1000;
|
|
10072
|
+
function certificateDue(certificate, deviceId, now) {
|
|
10073
|
+
return certificate.deviceId !== deviceId || certificate.notAfter.getTime() - now.getTime() < DOCKER_CERTIFICATE_RENEW_DAYS * DAY_MS;
|
|
10074
|
+
}
|
|
10075
|
+
async function readCertificateMeta(files) {
|
|
10076
|
+
try {
|
|
10077
|
+
const parsed = JSON.parse(await readFile3(files.meta, "utf8"));
|
|
10078
|
+
if (typeof parsed.baseUrl === "string" && typeof parsed.organization?.id === "string" && typeof parsed.organization.slug === "string") {
|
|
10079
|
+
return {
|
|
10080
|
+
baseUrl: parsed.baseUrl,
|
|
10081
|
+
organization: { id: parsed.organization.id, slug: parsed.organization.slug },
|
|
10082
|
+
serial: typeof parsed.serial === "string" ? parsed.serial : "",
|
|
10083
|
+
notAfter: typeof parsed.notAfter === "string" ? parsed.notAfter : ""
|
|
10084
|
+
};
|
|
10085
|
+
}
|
|
10086
|
+
} catch {}
|
|
10087
|
+
return null;
|
|
10088
|
+
}
|
|
10089
|
+
async function writeIssuedCertificate(files, issued) {
|
|
10090
|
+
await writeFileAtomic(files.ca, issued.caPem, 420);
|
|
10091
|
+
await writeFileAtomic(files.cert, issued.certificatePem, 420);
|
|
10092
|
+
await writeFileAtomic(files.meta, `${JSON.stringify(issued.meta, null, 2)}
|
|
10093
|
+
`, 420);
|
|
10094
|
+
}
|
|
10095
|
+
function contextsPath(dockerDir) {
|
|
10096
|
+
return join3(dockerDir, "contexts.json");
|
|
10097
|
+
}
|
|
10098
|
+
async function readContexts(dockerDir) {
|
|
10099
|
+
try {
|
|
10100
|
+
const parsed = JSON.parse(await readFile3(contextsPath(dockerDir), "utf8"));
|
|
10101
|
+
return (parsed.contexts ?? []).filter((c) => typeof c?.name === "string" && typeof c.organizationId === "string" && typeof c.sandbox === "string");
|
|
10102
|
+
} catch {
|
|
10103
|
+
return [];
|
|
10104
|
+
}
|
|
10105
|
+
}
|
|
10106
|
+
async function writeContexts(dockerDir, contexts) {
|
|
10107
|
+
const unique = [...new Map(contexts.map((c) => [c.name, c])).values()].sort((a, b) => a.name.localeCompare(b.name));
|
|
10108
|
+
await writeFileAtomic(contextsPath(dockerDir), `${JSON.stringify({ contexts: unique }, null, 2)}
|
|
10109
|
+
`, 420);
|
|
10110
|
+
}
|
|
10111
|
+
async function recordContext(dockerDir, context) {
|
|
10112
|
+
const others = (await readContexts(dockerDir)).filter((c) => c.name !== context.name);
|
|
10113
|
+
await writeContexts(dockerDir, [...others, context]);
|
|
10114
|
+
}
|
|
10115
|
+
|
|
10116
|
+
// src/docker/certificates.ts
|
|
10117
|
+
function execDocker(command, args) {
|
|
10118
|
+
const result = spawnSync(command, args, { encoding: "utf8" });
|
|
10119
|
+
return {
|
|
10120
|
+
status: result.status ?? 1,
|
|
10121
|
+
stdout: result.stdout ?? "",
|
|
10122
|
+
stderr: result.stderr ?? result.error?.message ?? ""
|
|
10123
|
+
};
|
|
10124
|
+
}
|
|
10125
|
+
async function issueCertificate(input, request) {
|
|
10126
|
+
const body = {
|
|
10127
|
+
organization: input.organizationId,
|
|
10128
|
+
deviceId: input.deviceId,
|
|
10129
|
+
csrPem: buildCsrPem(input.key, input.deviceId)
|
|
10130
|
+
};
|
|
10131
|
+
const issued = await request("/api/v1/docker/certificates", {
|
|
10132
|
+
method: "POST",
|
|
10133
|
+
body: JSON.stringify(body)
|
|
10134
|
+
});
|
|
10135
|
+
const certificate = parseClientCertificate(issued.certificatePem, input.key);
|
|
10136
|
+
if (certificate.deviceId !== input.deviceId || issued.organization.id !== input.organizationId || certificate.organizationId !== null && certificate.organizationId !== input.organizationId) {
|
|
10137
|
+
throw new Error("The app sent a certificate for another device or organization: not stored.");
|
|
10138
|
+
}
|
|
10139
|
+
const meta = {
|
|
10140
|
+
baseUrl: input.baseUrl,
|
|
10141
|
+
organization: issued.organization,
|
|
10142
|
+
serial: certificate.serial,
|
|
10143
|
+
notAfter: certificate.notAfter.toISOString()
|
|
10144
|
+
};
|
|
10145
|
+
await writeIssuedCertificate(input.files, {
|
|
10146
|
+
certificatePem: issued.certificatePem,
|
|
10147
|
+
caPem: issued.caPem,
|
|
10148
|
+
meta
|
|
10149
|
+
});
|
|
10150
|
+
return { certificate, meta };
|
|
10151
|
+
}
|
|
10152
|
+
async function ensureCertificate(input, request) {
|
|
10153
|
+
const key = await loadOrCreateDockerKey(input.files);
|
|
10154
|
+
const stored = await readClientCertificate(input.files, key);
|
|
10155
|
+
if (stored && !certificateDue(stored, input.deviceId, input.now)) {
|
|
10156
|
+
return { certificate: stored, issued: false };
|
|
10157
|
+
}
|
|
10158
|
+
const { certificate } = await issueCertificate({ ...input, key }, request);
|
|
10159
|
+
return { certificate, issued: true };
|
|
10160
|
+
}
|
|
10161
|
+
function message(err) {
|
|
10162
|
+
return err instanceof Error ? err.message : String(err);
|
|
10163
|
+
}
|
|
10164
|
+
function day(date) {
|
|
10165
|
+
return date.toISOString().slice(0, 10);
|
|
10166
|
+
}
|
|
10167
|
+
async function renewDockerCertificates(deps, options = {}) {
|
|
10168
|
+
const exec = deps.exec ?? execDocker;
|
|
10169
|
+
const dockerDir = deps.dockerDir ?? dockerDirFor(deps.dir);
|
|
10170
|
+
const now = (deps.now ?? (() => new Date))();
|
|
10171
|
+
let organizationIds;
|
|
10172
|
+
let state;
|
|
10173
|
+
try {
|
|
10174
|
+
organizationIds = (await listOrganizationIds(dockerDir)).filter((id) => id !== options.except);
|
|
10175
|
+
state = organizationIds.length > 0 ? await readDeviceState(deps.dir) : null;
|
|
10176
|
+
} catch (err) {
|
|
10177
|
+
deps.notice(`Could not check the Docker certificates: ${message(err)}`);
|
|
10178
|
+
return;
|
|
10179
|
+
}
|
|
10180
|
+
if (!state || !deps.has("docker"))
|
|
10181
|
+
return;
|
|
10182
|
+
const deviceId = state.deviceId;
|
|
10183
|
+
let contexts = await readContexts(dockerDir);
|
|
10184
|
+
let contextsChanged = false;
|
|
10185
|
+
for (const organizationId of organizationIds) {
|
|
10186
|
+
const files = organizationFiles(dockerDir, organizationId);
|
|
10187
|
+
const meta = await readCertificateMeta(files);
|
|
10188
|
+
const label = meta?.organization.slug ?? organizationId;
|
|
10189
|
+
try {
|
|
10190
|
+
if (meta && meta.baseUrl !== state.baseUrl)
|
|
10191
|
+
continue;
|
|
10192
|
+
const key = await readDockerKey(files);
|
|
10193
|
+
const stored = key ? await readClientCertificate(files, key) : null;
|
|
10194
|
+
if (!key || !stored || !certificateDue(stored, deviceId, now))
|
|
10195
|
+
continue;
|
|
10196
|
+
const { certificate } = await issueCertificate({ files, key, organizationId, deviceId, baseUrl: state.baseUrl }, deps.request);
|
|
10197
|
+
const updated = [];
|
|
10198
|
+
for (const context of contexts.filter((c) => c.organizationId === organizationId)) {
|
|
10199
|
+
const host = contextHost(exec, context.name);
|
|
10200
|
+
if (host === null) {
|
|
10201
|
+
contexts = contexts.filter((c) => c.name !== context.name);
|
|
10202
|
+
contextsChanged = true;
|
|
10203
|
+
continue;
|
|
10204
|
+
}
|
|
10205
|
+
writeContext(exec, {
|
|
10206
|
+
name: context.name,
|
|
10207
|
+
description: contextDescription(context.sandbox, label),
|
|
10208
|
+
host,
|
|
10209
|
+
files
|
|
10210
|
+
});
|
|
10211
|
+
updated.push(context.name);
|
|
10212
|
+
}
|
|
10213
|
+
deps.notice(`Renewed the Docker certificate for ${label}, valid until ${day(certificate.notAfter)}${updated.length > 0 ? `; updated ${updated.join(", ")}` : ""}.`);
|
|
10214
|
+
} catch (err) {
|
|
10215
|
+
deps.notice(`Could not renew the Docker certificate for ${label} (${message(err)}). Run \`${getCliName()} docker context <sandbox> --org ${label}\` to try again.`);
|
|
10216
|
+
}
|
|
10217
|
+
}
|
|
10218
|
+
if (contextsChanged) {
|
|
10219
|
+
try {
|
|
10220
|
+
await writeContexts(dockerDir, contexts);
|
|
10221
|
+
} catch (err) {
|
|
10222
|
+
deps.notice(`Could not update ${dockerDir}/contexts.json: ${message(err)}`);
|
|
10223
|
+
}
|
|
10224
|
+
}
|
|
10225
|
+
}
|
|
10226
|
+
|
|
10227
|
+
// src/settings.ts
|
|
10228
|
+
import { mkdir as mkdir4, readFile as readFile4, writeFile as writeFile4 } from "node:fs/promises";
|
|
10229
|
+
import { dirname as dirname4, join as join4 } from "node:path";
|
|
9797
10230
|
function getSettingsPath() {
|
|
9798
|
-
return
|
|
10231
|
+
return join4(dirname4(getCredentialsPath()), "settings.json");
|
|
9799
10232
|
}
|
|
9800
10233
|
function isDefaultOrganization(value) {
|
|
9801
10234
|
if (!value || typeof value !== "object")
|
|
@@ -9805,15 +10238,15 @@ function isDefaultOrganization(value) {
|
|
|
9805
10238
|
}
|
|
9806
10239
|
async function readSettings(path = getSettingsPath()) {
|
|
9807
10240
|
try {
|
|
9808
|
-
const parsed = JSON.parse(await
|
|
10241
|
+
const parsed = JSON.parse(await readFile4(path, "utf8"));
|
|
9809
10242
|
return isDefaultOrganization(parsed.defaultOrganization) ? { defaultOrganization: parsed.defaultOrganization } : {};
|
|
9810
10243
|
} catch {
|
|
9811
10244
|
return {};
|
|
9812
10245
|
}
|
|
9813
10246
|
}
|
|
9814
10247
|
async function writeSettings(settings, path = getSettingsPath()) {
|
|
9815
|
-
await
|
|
9816
|
-
await
|
|
10248
|
+
await mkdir4(dirname4(path), { recursive: true });
|
|
10249
|
+
await writeFile4(path, `${JSON.stringify(settings, null, 2)}
|
|
9817
10250
|
`, "utf8");
|
|
9818
10251
|
}
|
|
9819
10252
|
|
|
@@ -9878,1588 +10311,1769 @@ async function resolveOrganizationId(flag) {
|
|
|
9878
10311
|
return (await findMyOrganization(await requireOrganizationRef(flag))).id;
|
|
9879
10312
|
}
|
|
9880
10313
|
|
|
9881
|
-
// src/commands/
|
|
9882
|
-
|
|
9883
|
-
|
|
9884
|
-
|
|
9885
|
-
|
|
9886
|
-
|
|
9887
|
-
|
|
9888
|
-
|
|
10314
|
+
// src/commands/network.ts
|
|
10315
|
+
import { spawn, spawnSync as spawnSync2 } from "node:child_process";
|
|
10316
|
+
import { existsSync as existsSync2 } from "node:fs";
|
|
10317
|
+
import { hostname } from "node:os";
|
|
10318
|
+
function defaultDeps() {
|
|
10319
|
+
return {
|
|
10320
|
+
request: authorizedRequest,
|
|
10321
|
+
run: (command, args) => new Promise((resolve, reject) => {
|
|
10322
|
+
const child = spawn(command, args, { stdio: "inherit" });
|
|
10323
|
+
child.on("error", reject);
|
|
10324
|
+
child.on("close", (code) => resolve(code ?? 1));
|
|
10325
|
+
}),
|
|
10326
|
+
output: (command, args) => {
|
|
10327
|
+
const result = spawnSync2(command, args, { encoding: "utf8" });
|
|
10328
|
+
return result.status === 0 ? result.stdout : "";
|
|
10329
|
+
},
|
|
10330
|
+
platform: process.platform,
|
|
10331
|
+
has: (command) => spawnSync2("sh", ["-c", `command -v ${command}`]).status === 0,
|
|
10332
|
+
dir: networkDir(),
|
|
10333
|
+
up: () => tunnelUp(),
|
|
10334
|
+
exists: existsSync2,
|
|
10335
|
+
log: console.log,
|
|
10336
|
+
error: printError,
|
|
10337
|
+
notice: printNotice
|
|
10338
|
+
};
|
|
9889
10339
|
}
|
|
9890
|
-
function
|
|
9891
|
-
return
|
|
9892
|
-
method: "POST",
|
|
9893
|
-
body: JSON.stringify(input)
|
|
9894
|
-
});
|
|
10340
|
+
function message2(err) {
|
|
10341
|
+
return err instanceof Error ? err.message : String(err);
|
|
9895
10342
|
}
|
|
9896
|
-
function
|
|
9897
|
-
|
|
9898
|
-
|
|
9899
|
-
|
|
9900
|
-
return `<${entry.valueLength} chars — read with \`${getCliName()} env get\`>`;
|
|
9901
|
-
}
|
|
9902
|
-
return entry.value ?? "";
|
|
10343
|
+
function connectCommandFor(provider) {
|
|
10344
|
+
const ref = provider.connectRef;
|
|
10345
|
+
const arg = /^[A-Za-z0-9._-]+$/.test(ref) ? ref : JSON.stringify(ref);
|
|
10346
|
+
return `${getCliName()} network connect ${arg}`;
|
|
9903
10347
|
}
|
|
9904
|
-
function
|
|
9905
|
-
const
|
|
9906
|
-
|
|
9907
|
-
|
|
9908
|
-
displayValue(entry),
|
|
9909
|
-
entry.comment ?? ""
|
|
9910
|
-
]);
|
|
9911
|
-
const header = ["NAME", "KIND", "VALUE", "COMMENT"];
|
|
9912
|
-
const widths = header.map((_, column) => Math.max(header[column]?.length ?? 0, ...rows.map((row) => row[column]?.length ?? 0)));
|
|
9913
|
-
const line = (cells) => cells.map((cell, column) => column === cells.length - 1 ? cell : cell.padEnd(widths[column] ?? 0)).join(" ").trimEnd();
|
|
9914
|
-
return [line(header), ...rows.map(line)].join(`
|
|
9915
|
-
`);
|
|
10348
|
+
async function wgQuick(deps, verb) {
|
|
10349
|
+
const args = ["wg-quick", verb, configPath(deps.dir)];
|
|
10350
|
+
deps.notice(`Running: sudo ${args.join(" ")}`);
|
|
10351
|
+
return deps.run("sudo", args);
|
|
9916
10352
|
}
|
|
9917
|
-
async function
|
|
9918
|
-
|
|
9919
|
-
|
|
9920
|
-
|
|
10353
|
+
async function runNetworkConnect(args = {}, deps = defaultDeps()) {
|
|
10354
|
+
suppressPausedNotice();
|
|
10355
|
+
if (!deps.has("wg-quick")) {
|
|
10356
|
+
deps.error(`wg-quick is not installed. ${installHint()}`);
|
|
10357
|
+
return 1;
|
|
9921
10358
|
}
|
|
9922
|
-
|
|
9923
|
-
|
|
9924
|
-
|
|
10359
|
+
let registered;
|
|
10360
|
+
let privateKey;
|
|
10361
|
+
const previous = await readDeviceState(deps.dir);
|
|
9925
10362
|
try {
|
|
9926
|
-
const
|
|
9927
|
-
|
|
10363
|
+
const organization = await requireOrganizationRef(args.org, deps.notice);
|
|
10364
|
+
const provider = args.provider?.trim();
|
|
10365
|
+
const key = await loadOrCreateKey(deps.dir);
|
|
10366
|
+
privateKey = key.privateKey;
|
|
10367
|
+
registered = await deps.request("/api/v1/network/devices", {
|
|
10368
|
+
method: "POST",
|
|
10369
|
+
body: JSON.stringify({
|
|
10370
|
+
publicKey: key.publicKey,
|
|
10371
|
+
name: hostname().slice(0, 64) || "device",
|
|
10372
|
+
organization,
|
|
10373
|
+
...provider ? { provider } : {}
|
|
10374
|
+
})
|
|
9928
10375
|
});
|
|
9929
|
-
if (args.json) {
|
|
9930
|
-
log(JSON.stringify(result.entries, null, 2));
|
|
9931
|
-
return 0;
|
|
9932
|
-
}
|
|
9933
|
-
if (result.entries.length === 0) {
|
|
9934
|
-
log(" No variables or secrets.");
|
|
9935
|
-
return 0;
|
|
9936
|
-
}
|
|
9937
|
-
log(formatTable(result.entries));
|
|
9938
|
-
return 0;
|
|
9939
10376
|
} catch (err) {
|
|
9940
|
-
|
|
10377
|
+
deps.error(message2(err));
|
|
10378
|
+
return 1;
|
|
9941
10379
|
}
|
|
9942
|
-
|
|
9943
|
-
|
|
10380
|
+
const dns = detectDnsMode();
|
|
10381
|
+
let config;
|
|
9944
10382
|
try {
|
|
9945
|
-
|
|
9946
|
-
organization: args.organization,
|
|
9947
|
-
name: args.name
|
|
9948
|
-
});
|
|
9949
|
-
log(args.json ? JSON.stringify(result.entry, null, 2) : result.entry.value ?? "");
|
|
9950
|
-
return 0;
|
|
10383
|
+
config = renderConfig({ privateKey, registered, dns });
|
|
9951
10384
|
} catch (err) {
|
|
9952
|
-
|
|
9953
|
-
}
|
|
9954
|
-
}
|
|
9955
|
-
function resolveKind(args) {
|
|
9956
|
-
if (args.kind !== undefined && args.kind !== "variable" && args.kind !== "secret") {
|
|
9957
|
-
return { error: `--kind must be "variable" or "secret", got "${args.kind}".` };
|
|
9958
|
-
}
|
|
9959
|
-
const fromKind = args.kind;
|
|
9960
|
-
if (args.secret && fromKind === "variable") {
|
|
9961
|
-
return { error: "Pass either --secret or --kind variable, not both." };
|
|
9962
|
-
}
|
|
9963
|
-
return { kind: fromKind ?? (args.secret ? "secret" : undefined) };
|
|
9964
|
-
}
|
|
9965
|
-
async function runEnvSet(args, log = console.log, error = printError, stdin = readStdin) {
|
|
9966
|
-
if (args.value !== undefined && args.valueStdin) {
|
|
9967
|
-
error("Pass either --value or --value-stdin, not both.");
|
|
9968
|
-
return 1;
|
|
9969
|
-
}
|
|
9970
|
-
if (args.value === undefined && !args.valueStdin) {
|
|
9971
|
-
error("Provide --value, or --value-stdin to read the value from stdin.");
|
|
10385
|
+
deps.error(message2(err));
|
|
9972
10386
|
return 1;
|
|
9973
10387
|
}
|
|
9974
|
-
const
|
|
9975
|
-
|
|
9976
|
-
|
|
9977
|
-
|
|
10388
|
+
const previousConfig = await readConfig(configPath(deps.dir));
|
|
10389
|
+
await writeConfig(config, configPath(deps.dir));
|
|
10390
|
+
await writeDeviceState({
|
|
10391
|
+
baseUrl: getBaseUrl(),
|
|
10392
|
+
deviceId: registered.device.id,
|
|
10393
|
+
address: registered.device.address ?? "",
|
|
10394
|
+
gatewayProvider: registered.provider,
|
|
10395
|
+
zones: [
|
|
10396
|
+
registered.network.zone,
|
|
10397
|
+
...registered.network.netZone ? [registered.network.netZone] : []
|
|
10398
|
+
]
|
|
10399
|
+
}, deps.dir);
|
|
10400
|
+
const wasUp = deps.up();
|
|
10401
|
+
if (wasUp && previousConfig === config) {
|
|
10402
|
+
const { device, network, provider, shortNameOrganizationChanged } = registered;
|
|
10403
|
+
printResult({
|
|
10404
|
+
device,
|
|
10405
|
+
network,
|
|
10406
|
+
provider,
|
|
10407
|
+
shortNameOrganizationChanged,
|
|
10408
|
+
interface: INTERFACE,
|
|
10409
|
+
dns,
|
|
10410
|
+
rearmed: true,
|
|
10411
|
+
pause: device.state
|
|
10412
|
+
}, () => [
|
|
10413
|
+
eventLine("done", rearmedLine(device.state)),
|
|
10414
|
+
...network.netZone && device.shortNameOrganization && shortNameOrganizationChanged ? clearingNotice(network.netCookieSite) : []
|
|
10415
|
+
], deps.log);
|
|
10416
|
+
printPauseHint(device.state, deps);
|
|
10417
|
+
await renewDockerCertificates(deps);
|
|
10418
|
+
return 0;
|
|
9978
10419
|
}
|
|
9979
|
-
|
|
9980
|
-
|
|
9981
|
-
|
|
9982
|
-
|
|
9983
|
-
|
|
9984
|
-
value,
|
|
9985
|
-
...resolved.kind ? { kind: resolved.kind } : {},
|
|
9986
|
-
...args.comment ? { comment: args.comment } : {}
|
|
9987
|
-
});
|
|
9988
|
-
if (args.json) {
|
|
9989
|
-
log(JSON.stringify(result, null, 2));
|
|
9990
|
-
return 0;
|
|
9991
|
-
}
|
|
9992
|
-
log(eventLine("done", `${result.created ? "Created" : "Updated"} ${result.entry.kind} ${result.entry.name}.`));
|
|
9993
|
-
if (result.previousKind === "secret") {
|
|
9994
|
-
log(eventLine("item", `Warning: ${result.entry.name} was a secret and is now a variable — its value is readable by everyone in the organization.`));
|
|
9995
|
-
} else if (result.previousKind === "variable") {
|
|
9996
|
-
log(eventLine("item", `${result.entry.name} was a variable and is now a secret; its value is no longer shown.`));
|
|
10420
|
+
if (wasUp) {
|
|
10421
|
+
const down = await wgQuick(deps, "down");
|
|
10422
|
+
if (down !== 0) {
|
|
10423
|
+
deps.error(`Could not take the old tunnel down (wg-quick exited ${down}).`);
|
|
10424
|
+
return 1;
|
|
9997
10425
|
}
|
|
9998
|
-
return 0;
|
|
9999
|
-
} catch (err) {
|
|
10000
|
-
return reportError(err, error);
|
|
10001
10426
|
}
|
|
10002
|
-
|
|
10003
|
-
|
|
10004
|
-
|
|
10005
|
-
|
|
10006
|
-
|
|
10007
|
-
name: args.name
|
|
10008
|
-
});
|
|
10009
|
-
log(eventLine("done", `Deleted ${args.name}.`));
|
|
10010
|
-
return 0;
|
|
10011
|
-
} catch (err) {
|
|
10012
|
-
return reportError(err, error);
|
|
10427
|
+
const ipv4Clashes = ipv4Clashes4(registered, deps);
|
|
10428
|
+
const code = await wgQuick(deps, "up");
|
|
10429
|
+
if (code !== 0) {
|
|
10430
|
+
deps.error(`wg-quick exited ${code}. The config is in ${configPath(deps.dir)}.`);
|
|
10431
|
+
return 1;
|
|
10013
10432
|
}
|
|
10433
|
+
const { device, network, provider, shortNameOrganizationChanged } = registered;
|
|
10434
|
+
const pinned = device.shortNameOrganization;
|
|
10435
|
+
const switched = wasUp && previous?.gatewayProvider && previous.gatewayProvider.id !== provider.id ? previous.gatewayProvider : null;
|
|
10436
|
+
printResult({
|
|
10437
|
+
device,
|
|
10438
|
+
network,
|
|
10439
|
+
provider,
|
|
10440
|
+
shortNameOrganizationChanged,
|
|
10441
|
+
interface: INTERFACE,
|
|
10442
|
+
dns,
|
|
10443
|
+
rearmed: false,
|
|
10444
|
+
pause: device.state
|
|
10445
|
+
}, () => [
|
|
10446
|
+
eventLine("done", `Connected through ${provider.name}. This machine is ${device.address} in the private network.`),
|
|
10447
|
+
...switched ? [
|
|
10448
|
+
` Replaced the connection through ${switched.name}: its sandboxes are out of reach now.`
|
|
10449
|
+
] : [],
|
|
10450
|
+
` Reachable: the sandboxes on ${provider.name}. One provider at a time.`,
|
|
10451
|
+
` Names: <sandbox>.<org>.${network.zone}`,
|
|
10452
|
+
...network.netZone ? [` In a browser: https://<sandbox>--<org>.${network.netZone}:<port>`] : [],
|
|
10453
|
+
...network.netZone && pinned ? [
|
|
10454
|
+
` Short names: https://<sandbox>.${network.netZone}:<port> for ${pinned.slug}'s sandboxes, on this machine.`,
|
|
10455
|
+
` Another organization: ${getCliName()} network connect --org <org>.`
|
|
10456
|
+
] : [],
|
|
10457
|
+
...network.netZone && pinned && shortNameOrganizationChanged ? clearingNotice(network.netCookieSite) : [],
|
|
10458
|
+
...network.ingress4 && !device.address4 ? [
|
|
10459
|
+
" No IPv4 address was free on this provider's host, so browser names work over IPv6 only:",
|
|
10460
|
+
" Chrome on a network without IPv6 cannot open them on this machine. Firefox and Safari",
|
|
10461
|
+
" can. Run connect again later: an address frees up when another device goes."
|
|
10462
|
+
] : [],
|
|
10463
|
+
...ipv4Clashes.map(({ address, owner }) => ` ${address} is already routed through ${owner} on this machine: browsers may not reach the private network's names over IPv4 while it is. Ask your Ghostdock admin about the IPv4 block.`),
|
|
10464
|
+
...dns === "none" ? [
|
|
10465
|
+
" No split DNS on this machine (no systemd-resolved): use the addresses from",
|
|
10466
|
+
` \`${getCliName()} network ls\`.`
|
|
10467
|
+
] : [],
|
|
10468
|
+
` → ${getCliName()} network ls`
|
|
10469
|
+
], deps.log);
|
|
10470
|
+
printPauseHint(device.state, deps);
|
|
10471
|
+
await renewDockerCertificates(deps);
|
|
10472
|
+
return 0;
|
|
10014
10473
|
}
|
|
10015
|
-
|
|
10016
|
-
|
|
10017
|
-
|
|
10018
|
-
|
|
10019
|
-
}
|
|
10020
|
-
var envCommand = defineCommand({
|
|
10021
|
-
meta: {
|
|
10022
|
-
name: "env",
|
|
10023
|
-
description: "Manage organization environment variables and secrets."
|
|
10024
|
-
},
|
|
10025
|
-
subCommands: {
|
|
10026
|
-
list: defineCommand({
|
|
10027
|
-
meta: { name: "list", description: "List variables and secrets." },
|
|
10028
|
-
args: {
|
|
10029
|
-
org: orgArg
|
|
10030
|
-
},
|
|
10031
|
-
async run({ args }) {
|
|
10032
|
-
process.exitCode = await withOrganization(args.org, (organization) => runEnvList({
|
|
10033
|
-
organization,
|
|
10034
|
-
json: isJsonOutput()
|
|
10035
|
-
}));
|
|
10036
|
-
}
|
|
10037
|
-
}),
|
|
10038
|
-
get: defineCommand({
|
|
10039
|
-
meta: {
|
|
10040
|
-
name: "get",
|
|
10041
|
-
description: "Print one variable value in full (secrets cannot be read)."
|
|
10042
|
-
},
|
|
10043
|
-
args: {
|
|
10044
|
-
name: nameArg,
|
|
10045
|
-
org: orgArg
|
|
10046
|
-
},
|
|
10047
|
-
async run({ args }) {
|
|
10048
|
-
process.exitCode = await withOrganization(args.org, (organization) => runEnvGet({
|
|
10049
|
-
organization,
|
|
10050
|
-
name: args.name,
|
|
10051
|
-
json: isJsonOutput()
|
|
10052
|
-
}));
|
|
10053
|
-
}
|
|
10054
|
-
}),
|
|
10055
|
-
set: defineCommand({
|
|
10056
|
-
meta: { name: "set", description: "Create or replace a variable or secret." },
|
|
10057
|
-
args: {
|
|
10058
|
-
name: nameArg,
|
|
10059
|
-
org: orgArg,
|
|
10060
|
-
value: { type: "string", description: "Value to store." },
|
|
10061
|
-
"value-stdin": {
|
|
10062
|
-
type: "boolean",
|
|
10063
|
-
description: "Read the value from stdin — keeps secrets out of shell history.",
|
|
10064
|
-
default: false
|
|
10065
|
-
},
|
|
10066
|
-
secret: {
|
|
10067
|
-
type: "boolean",
|
|
10068
|
-
description: "Shorthand for --kind secret. Required when creating a secret.",
|
|
10069
|
-
default: false
|
|
10070
|
-
},
|
|
10071
|
-
kind: {
|
|
10072
|
-
type: "string",
|
|
10073
|
-
description: "variable or secret. Required when creating; on an update, omit to keep the existing kind or pass the other to convert."
|
|
10074
|
-
},
|
|
10075
|
-
comment: { type: "string", description: "Note about what this value is for." }
|
|
10076
|
-
},
|
|
10077
|
-
async run({ args }) {
|
|
10078
|
-
process.exitCode = await withOrganization(args.org, (organization) => runEnvSet({
|
|
10079
|
-
organization,
|
|
10080
|
-
name: args.name,
|
|
10081
|
-
value: args.value,
|
|
10082
|
-
valueStdin: args["value-stdin"],
|
|
10083
|
-
secret: args.secret,
|
|
10084
|
-
kind: args.kind,
|
|
10085
|
-
comment: args.comment,
|
|
10086
|
-
json: isJsonOutput()
|
|
10087
|
-
}));
|
|
10088
|
-
}
|
|
10089
|
-
}),
|
|
10090
|
-
delete: defineCommand({
|
|
10091
|
-
meta: { name: "delete", description: "Delete a variable or secret." },
|
|
10092
|
-
args: {
|
|
10093
|
-
name: nameArg,
|
|
10094
|
-
org: orgArg
|
|
10095
|
-
},
|
|
10096
|
-
async run({ args }) {
|
|
10097
|
-
process.exitCode = await withOrganization(args.org, (organization) => runEnvDelete({
|
|
10098
|
-
organization,
|
|
10099
|
-
name: args.name
|
|
10100
|
-
}));
|
|
10101
|
-
}
|
|
10102
|
-
})
|
|
10103
|
-
}
|
|
10104
|
-
});
|
|
10105
|
-
|
|
10106
|
-
// src/container-picker.ts
|
|
10107
|
-
import { createInterface } from "node:readline/promises";
|
|
10108
|
-
function notRunningContainerOf(error) {
|
|
10109
|
-
if (!(error instanceof ApiError) || !error.data || typeof error.data !== "object")
|
|
10110
|
-
return null;
|
|
10111
|
-
const { code, params } = error.data;
|
|
10112
|
-
if (code !== "container_not_running")
|
|
10113
|
-
return null;
|
|
10114
|
-
const container = params && typeof params === "object" ? params : {};
|
|
10115
|
-
return typeof container.container === "string" && container.container ? container.container : null;
|
|
10474
|
+
function rearmedLine(state, now = new Date) {
|
|
10475
|
+
if (!state)
|
|
10476
|
+
return "Re-armed.";
|
|
10477
|
+
const until = clockTime(new Date(now.getTime() + state.maxSessionSeconds * 1000).toISOString(), now);
|
|
10478
|
+
return `Re-armed: network access for this device runs until ${until} (pauses after ${formatPauseLimit(state.idleTimeoutSeconds)} idle).`;
|
|
10116
10479
|
}
|
|
10117
|
-
function
|
|
10118
|
-
if (!
|
|
10119
|
-
return
|
|
10120
|
-
const
|
|
10121
|
-
|
|
10122
|
-
|
|
10123
|
-
const record = params && typeof params === "object" ? params : {};
|
|
10124
|
-
const running = typeof record.containers === "string" ? record.containers.split(",").map((name) => name.trim()).filter(Boolean) : [];
|
|
10125
|
-
if (running.length === 0)
|
|
10126
|
-
return null;
|
|
10127
|
-
return {
|
|
10128
|
-
running,
|
|
10129
|
-
...code === "container_default_missing" && typeof record.container === "string" ? { missingDefault: record.container } : {}
|
|
10130
|
-
};
|
|
10480
|
+
function printPauseHint(state, deps) {
|
|
10481
|
+
if (!state || isJsonOutput())
|
|
10482
|
+
return;
|
|
10483
|
+
const [first = "", ...rest] = pauseHint(state);
|
|
10484
|
+
deps.notice(["", bold2(first), ...rest].join(`
|
|
10485
|
+
`));
|
|
10131
10486
|
}
|
|
10132
|
-
|
|
10133
|
-
|
|
10134
|
-
|
|
10135
|
-
|
|
10136
|
-
|
|
10137
|
-
|
|
10487
|
+
function ipv4Clashes4(registered, deps) {
|
|
10488
|
+
const addresses = [registered.network.ingress4, registered.device.address4].filter((a) => typeof a === "string" && isIPv4Address(a));
|
|
10489
|
+
const clashes = [];
|
|
10490
|
+
for (const address of addresses) {
|
|
10491
|
+
const [command, args] = ipv4RouteCommand(deps.platform, address);
|
|
10492
|
+
const owner = ipv4RouteOwner(deps.platform, deps.output(command, args));
|
|
10493
|
+
if (owner)
|
|
10494
|
+
clashes.push({ address, owner });
|
|
10138
10495
|
}
|
|
10139
|
-
|
|
10140
|
-
|
|
10141
|
-
|
|
10142
|
-
|
|
10143
|
-
|
|
10144
|
-
|
|
10145
|
-
|
|
10146
|
-
|
|
10147
|
-
|
|
10148
|
-
|
|
10149
|
-
|
|
10150
|
-
|
|
10496
|
+
return clashes;
|
|
10497
|
+
}
|
|
10498
|
+
function clearingNotice(cookieSite) {
|
|
10499
|
+
const site = cookieSite?.site ?? "the net domain";
|
|
10500
|
+
return [
|
|
10501
|
+
" Short names meant another organization's sandboxes before. The first short-name site each",
|
|
10502
|
+
` browser opens now clears its data once: the site's storage, and every cookie of ${site}.`,
|
|
10503
|
+
` That signs the browser out of every site under ${site}${cookieSite?.includesApp ? ", the Ghostdock app included" : ""}.`,
|
|
10504
|
+
" Reload or close browser tabs opened on short names before this connect."
|
|
10505
|
+
];
|
|
10506
|
+
}
|
|
10507
|
+
async function staleResolverFiles(deps) {
|
|
10508
|
+
if (deps.platform !== "darwin")
|
|
10509
|
+
return [];
|
|
10510
|
+
const state = await readDeviceState(deps.dir);
|
|
10511
|
+
const zones = new Set([
|
|
10512
|
+
...configResolverZones(await readConfig(configPath(deps.dir))),
|
|
10513
|
+
...state?.zones ?? []
|
|
10514
|
+
]);
|
|
10515
|
+
return [...zones].map(resolverFile).filter((file) => deps.exists(file));
|
|
10516
|
+
}
|
|
10517
|
+
async function runNetworkDisconnect(deps = defaultDeps()) {
|
|
10518
|
+
const wasUp = deps.up();
|
|
10519
|
+
let failed = false;
|
|
10520
|
+
if (wasUp) {
|
|
10521
|
+
const code = await wgQuick(deps, "down");
|
|
10522
|
+
if (code !== 0) {
|
|
10523
|
+
deps.error(`wg-quick exited ${code}.`);
|
|
10524
|
+
failed = true;
|
|
10525
|
+
}
|
|
10151
10526
|
}
|
|
10152
|
-
|
|
10153
|
-
|
|
10154
|
-
|
|
10155
|
-
|
|
10156
|
-
const
|
|
10157
|
-
|
|
10158
|
-
|
|
10159
|
-
|
|
10527
|
+
const stale = await staleResolverFiles(deps);
|
|
10528
|
+
if (stale.length) {
|
|
10529
|
+
const args = ["rm", "-f", ...stale];
|
|
10530
|
+
deps.notice(`Removing split DNS left from an earlier connection. Running: sudo ${args.join(" ")}`);
|
|
10531
|
+
const code = await deps.run("sudo", args);
|
|
10532
|
+
if (code !== 0) {
|
|
10533
|
+
deps.error(`Could not remove ${stale.join(", ")} (rm exited ${code}).`);
|
|
10534
|
+
return 1;
|
|
10535
|
+
}
|
|
10160
10536
|
}
|
|
10161
|
-
|
|
10162
|
-
|
|
10163
|
-
|
|
10537
|
+
if (failed)
|
|
10538
|
+
return 1;
|
|
10539
|
+
printResult({ connected: false, removedSplitDns: stale }, () => [
|
|
10540
|
+
wasUp ? eventLine("done", "Disconnected.") : eventLine(stale.length ? "done" : "item", stale.length ? "Not connected; removed the leftover split DNS." : "Not connected.")
|
|
10541
|
+
], deps.log);
|
|
10542
|
+
return 0;
|
|
10543
|
+
}
|
|
10544
|
+
async function readConnection(deps) {
|
|
10545
|
+
const state = await readDeviceState(deps.dir);
|
|
10546
|
+
const up = deps.up();
|
|
10547
|
+
if (!state)
|
|
10548
|
+
return { state, up, device: null, asked: true, provider: null, staleMtu: null };
|
|
10164
10549
|
try {
|
|
10165
|
-
const
|
|
10166
|
-
|
|
10167
|
-
|
|
10168
|
-
|
|
10550
|
+
const { devices, mtu } = await deps.request("/api/v1/network/devices");
|
|
10551
|
+
const device = devices.find((d) => d.id === state.deviceId) ?? null;
|
|
10552
|
+
const config = await readConfigMtu(configPath(deps.dir));
|
|
10553
|
+
const staleMtu = config !== null && config !== mtu ? { config, current: mtu } : null;
|
|
10554
|
+
if (staleMtu) {
|
|
10555
|
+
deps.notice(`This machine's tunnel has MTU ${staleMtu.config}, the network now uses ${staleMtu.current}. ` + `Run \`${getCliName()} network connect\` again: until then, large packets other than TCP ` + "can be lost on the way.");
|
|
10556
|
+
}
|
|
10557
|
+
return {
|
|
10558
|
+
state,
|
|
10559
|
+
up,
|
|
10560
|
+
device,
|
|
10561
|
+
asked: true,
|
|
10562
|
+
provider: up ? device?.gatewayProvider ?? null : null,
|
|
10563
|
+
staleMtu
|
|
10564
|
+
};
|
|
10565
|
+
} catch (err) {
|
|
10566
|
+
deps.notice(`Could not ask the app about this device: ${message2(err)}`);
|
|
10567
|
+
return {
|
|
10568
|
+
state,
|
|
10569
|
+
up,
|
|
10570
|
+
device: null,
|
|
10571
|
+
asked: false,
|
|
10572
|
+
provider: up ? state.gatewayProvider : null,
|
|
10573
|
+
staleMtu: null
|
|
10574
|
+
};
|
|
10169
10575
|
}
|
|
10170
|
-
};
|
|
10171
|
-
function defaultContainerPickers() {
|
|
10172
|
-
return process.stdin.isTTY && process.stderr.isTTY ? {
|
|
10173
|
-
choose: chooseContainerOnTerminal,
|
|
10174
|
-
confirmDefault: confirmDefaultOnTerminal,
|
|
10175
|
-
confirmStart: confirmStartOnTerminal
|
|
10176
|
-
} : undefined;
|
|
10177
10576
|
}
|
|
10178
|
-
|
|
10179
|
-
|
|
10180
|
-
|
|
10181
|
-
|
|
10182
|
-
|
|
10183
|
-
|
|
10577
|
+
async function runNetworkStatus(deps = defaultDeps()) {
|
|
10578
|
+
const { state, up, device, asked, provider, staleMtu } = await readConnection(deps);
|
|
10579
|
+
const staleSplitDns = up ? [] : await staleResolverFiles(deps);
|
|
10580
|
+
for (const file of staleSplitDns) {
|
|
10581
|
+
deps.notice(`Split DNS from an earlier connection is still set (${file}): names of the private network will hang. Run: ${getCliName()} network disconnect`);
|
|
10582
|
+
}
|
|
10583
|
+
printResult({ connected: up, device, provider, staleMtu, staleSplitDns }, () => {
|
|
10584
|
+
if (!state) {
|
|
10585
|
+
return [eventLine("item", `Not set up. Run \`${getCliName()} network connect\`.`)];
|
|
10586
|
+
}
|
|
10587
|
+
const through = provider ? ` through ${provider.name}` : "";
|
|
10588
|
+
const lines = [
|
|
10589
|
+
eventLine("item", `${up ? `Connected${through}` : "Not connected"}: ${state.address}`)
|
|
10590
|
+
];
|
|
10591
|
+
if (device) {
|
|
10592
|
+
lines.push(` Device ${device.name}, valid until ${device.expiresAt.slice(0, 10)}.`);
|
|
10593
|
+
lines.push(` ${deviceStateText(device.state)}.`);
|
|
10594
|
+
if (device.shortNameOrganization) {
|
|
10595
|
+
lines.push(` Short names: ${device.shortNameOrganization.slug}'s sandboxes.`);
|
|
10596
|
+
}
|
|
10597
|
+
if (!device.gatewayProvider) {
|
|
10598
|
+
lines.push(` Its provider is gone, so no gateway serves it. Run \`${getCliName()} network connect\`.`);
|
|
10599
|
+
}
|
|
10600
|
+
} else if (asked) {
|
|
10601
|
+
lines.push(` The app no longer knows this device (removed, expired or logged out). Run \`${getCliName()} network connect\`.`);
|
|
10602
|
+
}
|
|
10603
|
+
if (provider)
|
|
10604
|
+
lines.push(` Reachable: the sandboxes on ${provider.name}.`);
|
|
10605
|
+
return lines;
|
|
10606
|
+
}, deps.log);
|
|
10607
|
+
return 0;
|
|
10184
10608
|
}
|
|
10185
|
-
function
|
|
10186
|
-
|
|
10187
|
-
|
|
10188
|
-
const started = now();
|
|
10189
|
-
const elapsed = () => formatElapsed(now() - started);
|
|
10190
|
-
if (isJsonOutput())
|
|
10191
|
-
return { stop: elapsed };
|
|
10192
|
-
const animate = options.animate ?? stream.isTTY === true;
|
|
10193
|
-
if (!animate) {
|
|
10194
|
-
stream.write(` ${SYMBOLS.progress} ${message}
|
|
10195
|
-
`);
|
|
10196
|
-
return { stop: elapsed };
|
|
10609
|
+
function unreachableNote(sandbox, connected) {
|
|
10610
|
+
if (!sandbox.provider.privateNetwork) {
|
|
10611
|
+
return `not reachable: ${sandbox.provider.name} has no private network yet`;
|
|
10197
10612
|
}
|
|
10198
|
-
|
|
10199
|
-
|
|
10200
|
-
|
|
10201
|
-
|
|
10202
|
-
|
|
10203
|
-
|
|
10204
|
-
|
|
10205
|
-
|
|
10206
|
-
|
|
10207
|
-
|
|
10208
|
-
|
|
10209
|
-
|
|
10210
|
-
|
|
10211
|
-
|
|
10212
|
-
|
|
10213
|
-
|
|
10214
|
-
|
|
10215
|
-
|
|
10216
|
-
return {
|
|
10217
|
-
stop() {
|
|
10218
|
-
if (!stopped) {
|
|
10219
|
-
stopped = true;
|
|
10220
|
-
clearInterval(timer);
|
|
10221
|
-
process.off("SIGINT", onInterrupt);
|
|
10222
|
-
restore();
|
|
10613
|
+
if (connected?.id === sandbox.provider.id)
|
|
10614
|
+
return null;
|
|
10615
|
+
return `not reachable: on ${sandbox.provider.name} → ${connectCommandFor(sandbox.provider)}`;
|
|
10616
|
+
}
|
|
10617
|
+
async function runNetworkLs(args, deps = defaultDeps()) {
|
|
10618
|
+
try {
|
|
10619
|
+
const organization = await requireOrganizationRef(args.org, deps.notice);
|
|
10620
|
+
const connection = await readConnection(deps);
|
|
10621
|
+
const deviceId = connection.device?.id;
|
|
10622
|
+
const listed = await deps.request(`/api/v1/network/organizations/${encodeURIComponent(organization)}/sandboxes${deviceId ? `?device=${encodeURIComponent(deviceId)}` : ""}`);
|
|
10623
|
+
const connected = connection.provider;
|
|
10624
|
+
const sandboxes = listed.sandboxes.map((sandbox) => ({
|
|
10625
|
+
...sandbox,
|
|
10626
|
+
reachable: unreachableNote(sandbox, connected) === null
|
|
10627
|
+
}));
|
|
10628
|
+
printResult({ ...listed, connectedProvider: connected, sandboxes }, () => {
|
|
10629
|
+
if (listed.sandboxes.length === 0) {
|
|
10630
|
+
return [eventLine("item", `No sandboxes in ${listed.organization.slug}.`)];
|
|
10223
10631
|
}
|
|
10224
|
-
|
|
10225
|
-
|
|
10226
|
-
|
|
10227
|
-
|
|
10228
|
-
|
|
10229
|
-
|
|
10230
|
-
|
|
10231
|
-
|
|
10232
|
-
|
|
10632
|
+
const width = Math.max(...listed.sandboxes.map((s) => s.name.length));
|
|
10633
|
+
const canonicalWidth = Math.max(...listed.sandboxes.map((s) => s.canonicalName.length));
|
|
10634
|
+
const httpsWidth = Math.max(...listed.sandboxes.map((s) => (s.httpsName?.length ?? 0) + 8));
|
|
10635
|
+
const shortWidth = Math.max(...listed.sandboxes.map((s) => (s.shortName?.length ?? 0) + 8));
|
|
10636
|
+
const lines = [
|
|
10637
|
+
connected ? eventLine("item", `Connected through ${connected.name}.`) : eventLine("item", `Not connected: nothing is reachable. Run \`${getCliName()} network connect [provider]\`.`)
|
|
10638
|
+
];
|
|
10639
|
+
for (const s of listed.sandboxes) {
|
|
10640
|
+
const note = unreachableNote(s, connected);
|
|
10641
|
+
lines.push([
|
|
10642
|
+
` ${s.name.padEnd(width)}`,
|
|
10643
|
+
(s.address ?? "-").padEnd(28),
|
|
10644
|
+
s.canonicalName.padEnd(canonicalWidth),
|
|
10645
|
+
...s.httpsName ? [`https://${s.httpsName}`.padEnd(httpsWidth)] : [],
|
|
10646
|
+
...s.shortName ? [`https://${s.shortName}`.padEnd(shortWidth)] : [],
|
|
10647
|
+
s.status,
|
|
10648
|
+
...note ? [`(${note})`] : []
|
|
10649
|
+
].join(" "));
|
|
10650
|
+
}
|
|
10651
|
+
return lines;
|
|
10652
|
+
}, deps.log);
|
|
10653
|
+
return 0;
|
|
10233
10654
|
} catch (err) {
|
|
10234
|
-
|
|
10235
|
-
|
|
10655
|
+
deps.error(message2(err));
|
|
10656
|
+
return 1;
|
|
10236
10657
|
}
|
|
10237
10658
|
}
|
|
10238
|
-
|
|
10239
|
-
// src/container-flow.ts
|
|
10240
|
-
async function withContainerPick(call, send, pickers, notice) {
|
|
10659
|
+
async function runNetworkDevices(deps = defaultDeps()) {
|
|
10241
10660
|
try {
|
|
10242
|
-
|
|
10661
|
+
const { devices } = await deps.request("/api/v1/network/devices");
|
|
10662
|
+
const own = (await readDeviceState(deps.dir))?.deviceId;
|
|
10663
|
+
printResult({ devices }, () => devices.length === 0 ? [eventLine("item", "No devices.")] : devices.map((d) => ` ${d.id === own ? "*" : " "} ${d.id} ${d.name} ${d.address} until ${d.expiresAt.slice(0, 10)} ${deviceStateText(d.state)}`), deps.log);
|
|
10664
|
+
return 0;
|
|
10243
10665
|
} catch (err) {
|
|
10244
|
-
|
|
10245
|
-
|
|
10246
|
-
throw err;
|
|
10247
|
-
const container = await pickers.choose(question);
|
|
10248
|
-
if (!container)
|
|
10249
|
-
throw err;
|
|
10250
|
-
call.setContainer(container);
|
|
10251
|
-
const result = await send();
|
|
10252
|
-
if (call.sandboxId !== undefined && await pickers.confirmDefault(container)) {
|
|
10253
|
-
try {
|
|
10254
|
-
await authorizedRequest("/api/v1/tools/sandboxes_default_container", {
|
|
10255
|
-
method: "POST",
|
|
10256
|
-
body: JSON.stringify({
|
|
10257
|
-
organization: call.organization,
|
|
10258
|
-
sandboxId: call.sandboxId,
|
|
10259
|
-
container
|
|
10260
|
-
})
|
|
10261
|
-
});
|
|
10262
|
-
notice(eventLine("done", `Default container: ${container}.`));
|
|
10263
|
-
} catch (err) {
|
|
10264
|
-
notice(eventLine("failed", `The default container was not set: ${err instanceof Error ? err.message : String(err)}`));
|
|
10265
|
-
}
|
|
10266
|
-
}
|
|
10267
|
-
return result;
|
|
10666
|
+
deps.error(message2(err));
|
|
10667
|
+
return 1;
|
|
10268
10668
|
}
|
|
10269
10669
|
}
|
|
10270
|
-
async function
|
|
10670
|
+
async function runNetworkDevicesRemove(args, deps = defaultDeps()) {
|
|
10271
10671
|
try {
|
|
10272
|
-
|
|
10672
|
+
await deps.request(`/api/v1/network/devices/${encodeURIComponent(args.id)}`, {
|
|
10673
|
+
method: "DELETE"
|
|
10674
|
+
});
|
|
10675
|
+
printResult({ removed: args.id }, () => [eventLine("done", `Removed device ${args.id}.`)], deps.log);
|
|
10676
|
+
return 0;
|
|
10273
10677
|
} catch (err) {
|
|
10274
|
-
|
|
10275
|
-
|
|
10276
|
-
throw err;
|
|
10277
|
-
if (!start && !(pickers && await pickers.confirmStart(container)))
|
|
10278
|
-
throw err;
|
|
10279
|
-
await withProgress("Starting container…", () => authorizedRequest("/api/v1/tools/sandboxes_containers_start", {
|
|
10280
|
-
method: "POST",
|
|
10281
|
-
body: JSON.stringify({
|
|
10282
|
-
organization: call.organization,
|
|
10283
|
-
sandboxId: call.sandboxId,
|
|
10284
|
-
container
|
|
10285
|
-
})
|
|
10286
|
-
}));
|
|
10287
|
-
notice(eventLine("done", `Started ${container}.`));
|
|
10288
|
-
return await send();
|
|
10678
|
+
deps.error(message2(err));
|
|
10679
|
+
return 1;
|
|
10289
10680
|
}
|
|
10290
10681
|
}
|
|
10291
|
-
|
|
10292
|
-
|
|
10293
|
-
|
|
10294
|
-
|
|
10295
|
-
|
|
10296
|
-
|
|
10297
|
-
|
|
10298
|
-
|
|
10299
|
-
|
|
10682
|
+
var connectCommand = defineCommand({
|
|
10683
|
+
meta: {
|
|
10684
|
+
name: "connect",
|
|
10685
|
+
description: "Put this machine into the private network through one provider: WireGuard and split DNS. The organization (--org, else the default) becomes this machine's short-name organization. Also re-arms a paused device: with the tunnel up and nothing changed, without sudo."
|
|
10686
|
+
},
|
|
10687
|
+
args: {
|
|
10688
|
+
provider: {
|
|
10689
|
+
type: "positional",
|
|
10690
|
+
required: false,
|
|
10691
|
+
description: "Sandbox provider name or id. Omit for the organization's default provider (else the platform default). Only its sandboxes are reachable; connecting through another replaces the connection."
|
|
10692
|
+
},
|
|
10693
|
+
org: orgArg
|
|
10694
|
+
},
|
|
10695
|
+
async run({ args }) {
|
|
10696
|
+
process.exitCode = await runNetworkConnect({ provider: args.provider, org: args.org });
|
|
10300
10697
|
}
|
|
10301
|
-
|
|
10302
|
-
|
|
10303
|
-
|
|
10304
|
-
|
|
10305
|
-
|
|
10306
|
-
|
|
10307
|
-
|
|
10308
|
-
|
|
10309
|
-
`),
|
|
10310
|
-
sleep: (ms) => new Promise((resolve) => setTimeout(resolve, ms)),
|
|
10311
|
-
stopped: () => stop
|
|
10312
|
-
};
|
|
10313
|
-
}
|
|
10314
|
-
async function runContainerLogs(input, options, deps = defaultLogsDeps(options.follow)) {
|
|
10315
|
-
const json = isJsonOutput();
|
|
10316
|
-
const color = colorEnabled(process.stdout);
|
|
10317
|
-
const print = (line) => {
|
|
10318
|
-
if (json) {
|
|
10319
|
-
deps.out(JSON.stringify(line));
|
|
10320
|
-
return;
|
|
10321
|
-
}
|
|
10322
|
-
const text = options.timestamps && line.time ? `${dim(line.time, color)} ${line.text}` : line.text;
|
|
10323
|
-
(line.stream === "stderr" ? deps.err : deps.out)(text);
|
|
10324
|
-
};
|
|
10325
|
-
let result = await deps.request(input);
|
|
10326
|
-
if (!result.running) {
|
|
10327
|
-
deps.err("The sandbox is stopped: it has no logs to show.");
|
|
10328
|
-
return 1;
|
|
10698
|
+
});
|
|
10699
|
+
var disconnectCommand = defineCommand({
|
|
10700
|
+
meta: {
|
|
10701
|
+
name: "disconnect",
|
|
10702
|
+
description: "Take this machine out of the private network, and remove split DNS an earlier connection left (macOS)."
|
|
10703
|
+
},
|
|
10704
|
+
async run() {
|
|
10705
|
+
process.exitCode = await runNetworkDisconnect();
|
|
10329
10706
|
}
|
|
10330
|
-
|
|
10331
|
-
|
|
10332
|
-
|
|
10333
|
-
|
|
10334
|
-
|
|
10335
|
-
|
|
10336
|
-
|
|
10337
|
-
|
|
10338
|
-
if (result.truncated && json) {
|
|
10339
|
-
deps.out(JSON.stringify({ truncated: true }));
|
|
10340
|
-
} else if (result.truncated) {
|
|
10341
|
-
deps.err(dim("(older lines left out: use --tail for more)", colorEnabled(process.stderr)));
|
|
10342
|
-
}
|
|
10343
|
-
return 0;
|
|
10344
|
-
}
|
|
10345
|
-
if (!result.truncated)
|
|
10346
|
-
await deps.sleep(FOLLOW_INTERVAL_MS);
|
|
10347
|
-
if (deps.stopped())
|
|
10348
|
-
return 0;
|
|
10349
|
-
const { tail: _tail, ...rest } = input;
|
|
10350
|
-
try {
|
|
10351
|
-
result = await deps.request(last ? { ...rest, after: last } : input);
|
|
10352
|
-
} catch (error) {
|
|
10353
|
-
deps.err(red(error instanceof Error ? error.message : String(error), colorEnabled(process.stderr)));
|
|
10354
|
-
return 1;
|
|
10355
|
-
}
|
|
10356
|
-
if (!result.running) {
|
|
10357
|
-
deps.err("The sandbox stopped.");
|
|
10358
|
-
return 0;
|
|
10359
|
-
}
|
|
10707
|
+
});
|
|
10708
|
+
var statusCommand = defineCommand({
|
|
10709
|
+
meta: {
|
|
10710
|
+
name: "status",
|
|
10711
|
+
description: "Whether this machine is connected, through which provider, and as which device."
|
|
10712
|
+
},
|
|
10713
|
+
async run() {
|
|
10714
|
+
process.exitCode = await runNetworkStatus();
|
|
10360
10715
|
}
|
|
10361
|
-
}
|
|
10362
|
-
|
|
10363
|
-
|
|
10364
|
-
|
|
10365
|
-
|
|
10366
|
-
|
|
10367
|
-
|
|
10368
|
-
|
|
10369
|
-
|
|
10370
|
-
|
|
10371
|
-
|
|
10372
|
-
|
|
10373
|
-
"
|
|
10374
|
-
|
|
10375
|
-
|
|
10376
|
-
|
|
10377
|
-
|
|
10378
|
-
|
|
10379
|
-
|
|
10380
|
-
|
|
10381
|
-
|
|
10382
|
-
|
|
10383
|
-
}
|
|
10384
|
-
|
|
10385
|
-
|
|
10386
|
-
|
|
10387
|
-
|
|
10388
|
-
return keys.find((key) => {
|
|
10389
|
-
const value = item[key];
|
|
10390
|
-
return typeof value === "string" && value.trim() !== "";
|
|
10391
|
-
});
|
|
10392
|
-
}
|
|
10393
|
-
function firstString(item, keys) {
|
|
10394
|
-
const key = firstKey(item, keys);
|
|
10395
|
-
return key ? item[key] : undefined;
|
|
10396
|
-
}
|
|
10397
|
-
function symbolForStatus(status) {
|
|
10398
|
-
const s = (status ?? "").toLowerCase();
|
|
10399
|
-
if (/(fail|error|denied)/.test(s))
|
|
10400
|
-
return "failed";
|
|
10401
|
-
if (/^(running|active|ready|deployed|published|online|verified|ok)$/.test(s))
|
|
10402
|
-
return "ready";
|
|
10403
|
-
if (/^(stopped|deleted|unpublished|vanished|archived|revoked|closed|killed|exited|dead|paused)$/.test(s)) {
|
|
10404
|
-
return "stopped";
|
|
10716
|
+
});
|
|
10717
|
+
var lsCommand = defineCommand({
|
|
10718
|
+
meta: {
|
|
10719
|
+
name: "ls",
|
|
10720
|
+
description: "Sandboxes of an organization with their private addresses and names, and which are reachable through the current connection."
|
|
10721
|
+
},
|
|
10722
|
+
args: { org: orgArg },
|
|
10723
|
+
async run({ args }) {
|
|
10724
|
+
process.exitCode = await runNetworkLs({ org: args.org });
|
|
10725
|
+
}
|
|
10726
|
+
});
|
|
10727
|
+
var devicesRemoveCommand = defineCommand({
|
|
10728
|
+
meta: { name: "remove", description: "Remove one of your devices from the private network." },
|
|
10729
|
+
args: {
|
|
10730
|
+
id: { type: "positional", description: "Device id, from `network devices`.", required: true }
|
|
10731
|
+
},
|
|
10732
|
+
async run({ args }) {
|
|
10733
|
+
process.exitCode = await runNetworkDevicesRemove({ id: args.id });
|
|
10734
|
+
}
|
|
10735
|
+
});
|
|
10736
|
+
var devicesCommand = defineCommand({
|
|
10737
|
+
meta: { name: "devices", description: "Your devices in the private network." },
|
|
10738
|
+
subCommands: { remove: devicesRemoveCommand },
|
|
10739
|
+
async run({ rawArgs }) {
|
|
10740
|
+
if (rawArgs[0] === "remove")
|
|
10741
|
+
return;
|
|
10742
|
+
process.exitCode = await runNetworkDevices();
|
|
10405
10743
|
}
|
|
10406
|
-
|
|
10407
|
-
|
|
10744
|
+
});
|
|
10745
|
+
var networkCommand = defineCommand({
|
|
10746
|
+
meta: {
|
|
10747
|
+
name: "network",
|
|
10748
|
+
description: "The private network: reach your sandboxes by address and name from this machine."
|
|
10749
|
+
},
|
|
10750
|
+
subCommands: {
|
|
10751
|
+
connect: connectCommand,
|
|
10752
|
+
disconnect: disconnectCommand,
|
|
10753
|
+
status: statusCommand,
|
|
10754
|
+
ls: lsCommand,
|
|
10755
|
+
devices: devicesCommand
|
|
10408
10756
|
}
|
|
10409
|
-
|
|
10757
|
+
});
|
|
10758
|
+
|
|
10759
|
+
// src/commands/docker.ts
|
|
10760
|
+
function defaultDeps2() {
|
|
10761
|
+
const dir = networkDir();
|
|
10762
|
+
return {
|
|
10763
|
+
request: authorizedRequest,
|
|
10764
|
+
run: (command, args) => new Promise((resolve, reject) => {
|
|
10765
|
+
const child = spawn2(command, args, { stdio: "inherit" });
|
|
10766
|
+
child.on("error", reject);
|
|
10767
|
+
child.on("close", (code) => resolve(code ?? 1));
|
|
10768
|
+
}),
|
|
10769
|
+
output: (command, args) => {
|
|
10770
|
+
const result = spawnSync3(command, args, { encoding: "utf8" });
|
|
10771
|
+
return result.status === 0 ? result.stdout : "";
|
|
10772
|
+
},
|
|
10773
|
+
platform: process.platform,
|
|
10774
|
+
has: (command) => spawnSync3("sh", ["-c", `command -v ${command}`]).status === 0,
|
|
10775
|
+
dir,
|
|
10776
|
+
up: () => tunnelUp(),
|
|
10777
|
+
log: console.log,
|
|
10778
|
+
error: printError,
|
|
10779
|
+
notice: printNotice,
|
|
10780
|
+
exec: execDocker,
|
|
10781
|
+
dockerDir: dockerDirFor(dir),
|
|
10782
|
+
now: () => new Date,
|
|
10783
|
+
env: process.env
|
|
10784
|
+
};
|
|
10410
10785
|
}
|
|
10411
|
-
function
|
|
10412
|
-
return
|
|
10786
|
+
function message3(err) {
|
|
10787
|
+
return err instanceof Error ? err.message : String(err);
|
|
10413
10788
|
}
|
|
10414
|
-
|
|
10415
|
-
|
|
10416
|
-
|
|
10789
|
+
var DNS_LABEL2 = /^[a-z0-9](?:[a-z0-9-]{0,61}[a-z0-9])?$/;
|
|
10790
|
+
function noDockerHostReason(sandbox, orgSlug) {
|
|
10791
|
+
if (!sandbox.provider.privateNetwork) {
|
|
10792
|
+
return `${sandbox.name} runs on ${sandbox.provider.name}, which has no private network yet: no Docker context reaches it.`;
|
|
10417
10793
|
}
|
|
10418
|
-
|
|
10419
|
-
|
|
10420
|
-
}
|
|
10421
|
-
|
|
10422
|
-
|
|
10423
|
-
}
|
|
10424
|
-
|
|
10425
|
-
|
|
10426
|
-
const id = keys.id ? item[keys.id] : undefined;
|
|
10427
|
-
const label = keys.label ? item[keys.label] : undefined;
|
|
10428
|
-
const status = firstString(item, STATUS_KEYS);
|
|
10429
|
-
const url = firstString(item, URL_KEYS);
|
|
10430
|
-
const parts = [label ?? id ?? "(unnamed)"];
|
|
10431
|
-
if (label && id && id !== label)
|
|
10432
|
-
parts.push(dim(shortId(id), color));
|
|
10433
|
-
if (status)
|
|
10434
|
-
parts.push(status);
|
|
10435
|
-
if (url)
|
|
10436
|
-
parts.push(`${SYMBOLS.url} ${url}`);
|
|
10437
|
-
return eventLine(symbolForStatus(status), parts.join(" "), color);
|
|
10794
|
+
if (!sandbox.address) {
|
|
10795
|
+
return `${sandbox.name} has no address in the private network yet (${sandbox.provider.name} has no host on it).`;
|
|
10796
|
+
}
|
|
10797
|
+
const notLabel = [sandbox.name, orgSlug].find((name) => !DNS_LABEL2.test(name));
|
|
10798
|
+
if (notLabel) {
|
|
10799
|
+
return `${sandbox.name} has no Docker engine over the network: "${notLabel}" is not a DNS label (lower-case letters, digits and hyphens), so its Docker engine stays closed.`;
|
|
10800
|
+
}
|
|
10801
|
+
return `${sandbox.name} has no Docker engine over the network.`;
|
|
10438
10802
|
}
|
|
10439
|
-
function
|
|
10440
|
-
|
|
10441
|
-
|
|
10442
|
-
|
|
10443
|
-
|
|
10444
|
-
|
|
10445
|
-
|
|
10446
|
-
if (
|
|
10447
|
-
|
|
10448
|
-
|
|
10803
|
+
function sandboxWarnings(sandbox, gateway, up) {
|
|
10804
|
+
const warnings = [];
|
|
10805
|
+
if (gateway?.id !== sandbox.provider.id) {
|
|
10806
|
+
warnings.push(`${sandbox.name} runs on ${sandbox.provider.name}, and this machine connects through ${gateway?.name ?? "no provider"}: Docker reaches it only after \`${connectCommandFor(sandbox.provider)}\`.`);
|
|
10807
|
+
} else if (!up) {
|
|
10808
|
+
warnings.push(`This machine is not connected to the private network: run \`${connectCommandFor(sandbox.provider)}\` before using the context.`);
|
|
10809
|
+
}
|
|
10810
|
+
if (sandbox.dockerDoor === "none") {
|
|
10811
|
+
warnings.push(`${sandbox.name}: this sandbox's base image is too old for Docker over the network; restart it to update.`);
|
|
10812
|
+
}
|
|
10813
|
+
return warnings;
|
|
10449
10814
|
}
|
|
10450
|
-
|
|
10451
|
-
|
|
10452
|
-
|
|
10453
|
-
|
|
10454
|
-
|
|
10455
|
-
|
|
10456
|
-
|
|
10457
|
-
|
|
10458
|
-
|
|
10459
|
-
|
|
10460
|
-
|
|
10815
|
+
var NO_DEVICE = () => `Run ${getCliName()} network connect first: the Docker context uses this machine's device.`;
|
|
10816
|
+
async function runDockerContext(args, deps = defaultDeps2()) {
|
|
10817
|
+
const state = await readDeviceStateFor(deps);
|
|
10818
|
+
if (!state) {
|
|
10819
|
+
deps.error(NO_DEVICE());
|
|
10820
|
+
return 1;
|
|
10821
|
+
}
|
|
10822
|
+
if (!deps.has("docker")) {
|
|
10823
|
+
deps.error("docker is not installed: the context is for Docker's CLI. Install Docker (Docker Desktop, OrbStack, or the docker CLI alone), then run this again.");
|
|
10824
|
+
return 1;
|
|
10825
|
+
}
|
|
10826
|
+
const use = args.use !== false;
|
|
10827
|
+
try {
|
|
10828
|
+
const organization = await requireOrganizationRef(args.org, deps.notice);
|
|
10829
|
+
const connection = await readConnection(deps);
|
|
10830
|
+
if (connection.asked && !connection.device) {
|
|
10831
|
+
deps.error(`The app no longer knows this machine's device (removed, expired or logged out). ${NO_DEVICE()}`);
|
|
10832
|
+
return 1;
|
|
10461
10833
|
}
|
|
10462
|
-
|
|
10463
|
-
|
|
10464
|
-
|
|
10465
|
-
|
|
10466
|
-
|
|
10834
|
+
const listed = await deps.request(`/api/v1/network/organizations/${encodeURIComponent(organization)}/sandboxes?device=${encodeURIComponent(state.deviceId)}`);
|
|
10835
|
+
const slug = listed.organization.slug;
|
|
10836
|
+
const wanted = args.sandbox.trim().toLowerCase();
|
|
10837
|
+
const sandbox = listed.sandboxes.find((s) => s.name.toLowerCase() === wanted);
|
|
10838
|
+
if (!sandbox) {
|
|
10839
|
+
deps.error(`No sandbox ${args.sandbox} in ${slug}. Run \`${getCliName()} network ls --org ${slug}\` for its sandboxes.`);
|
|
10840
|
+
return 1;
|
|
10841
|
+
}
|
|
10842
|
+
if (!sandbox.dockerHost) {
|
|
10843
|
+
deps.error(noDockerHostReason(sandbox, slug));
|
|
10844
|
+
return 1;
|
|
10845
|
+
}
|
|
10846
|
+
const gateway = connection.device?.gatewayProvider ?? connection.state?.gatewayProvider ?? null;
|
|
10847
|
+
for (const warning of sandboxWarnings(sandbox, gateway, connection.up))
|
|
10848
|
+
deps.notice(warning);
|
|
10849
|
+
const organizationId = listed.organization.id;
|
|
10850
|
+
if (!isOrganizationId(organizationId)) {
|
|
10851
|
+
throw new Error(`The app sent an organization id that is not one: ${organizationId}`);
|
|
10852
|
+
}
|
|
10853
|
+
const files = organizationFiles(deps.dockerDir, organizationId);
|
|
10854
|
+
const { certificate, issued } = await ensureCertificate({ files, organizationId, deviceId: state.deviceId, baseUrl: state.baseUrl, now: deps.now() }, deps.request);
|
|
10855
|
+
const name = dockerContextName(slug, sandbox.name);
|
|
10856
|
+
const { created } = writeContext(deps.exec, {
|
|
10857
|
+
name,
|
|
10858
|
+
description: contextDescription(sandbox.name, slug),
|
|
10859
|
+
host: sandbox.dockerHost,
|
|
10860
|
+
files
|
|
10861
|
+
});
|
|
10862
|
+
await recordContext(deps.dockerDir, { name, organizationId, sandbox: sandbox.name });
|
|
10863
|
+
if (use) {
|
|
10864
|
+
useContext(deps.exec, name);
|
|
10865
|
+
for (const variable of ["DOCKER_HOST", "DOCKER_CONTEXT"]) {
|
|
10866
|
+
if (deps.env[variable]) {
|
|
10867
|
+
deps.notice(`${variable} is set in this shell and wins over \`docker context use\`: unset it, or pass \`docker --context ${name}\`.`);
|
|
10868
|
+
}
|
|
10467
10869
|
}
|
|
10468
|
-
continue;
|
|
10469
10870
|
}
|
|
10470
|
-
|
|
10871
|
+
printResult({
|
|
10872
|
+
context: { name, host: sandbox.dockerHost, created, inUse: use },
|
|
10873
|
+
sandbox: {
|
|
10874
|
+
name: sandbox.name,
|
|
10875
|
+
canonicalName: sandbox.canonicalName,
|
|
10876
|
+
provider: sandbox.provider,
|
|
10877
|
+
dockerDoor: sandbox.dockerDoor
|
|
10878
|
+
},
|
|
10879
|
+
organization: listed.organization,
|
|
10880
|
+
certificate: {
|
|
10881
|
+
serial: certificate.serial,
|
|
10882
|
+
notAfter: certificate.notAfter.toISOString(),
|
|
10883
|
+
issued,
|
|
10884
|
+
files: { ca: files.ca, cert: files.cert, key: files.key }
|
|
10885
|
+
}
|
|
10886
|
+
}, () => [
|
|
10887
|
+
eventLine("done", `Docker context ${name}${use ? " (in use)" : ""}`),
|
|
10888
|
+
` ${sandbox.dockerHost}`,
|
|
10889
|
+
` Certificate for this device, valid until ${day(certificate.notAfter)}.`,
|
|
10890
|
+
` → ${use ? "docker ps" : `docker --context ${name} ps`}`
|
|
10891
|
+
], deps.log);
|
|
10892
|
+
await renewDockerCertificates(deps, { except: organizationId });
|
|
10893
|
+
return 0;
|
|
10894
|
+
} catch (err) {
|
|
10895
|
+
deps.error(message3(err));
|
|
10896
|
+
return 1;
|
|
10471
10897
|
}
|
|
10472
|
-
return lines;
|
|
10473
|
-
}
|
|
10474
|
-
function listLines(items, color) {
|
|
10475
|
-
if (items.length === 0)
|
|
10476
|
-
return [` ${dim("(none)", color)}`];
|
|
10477
|
-
return items.map((item) => isItem(item) ? itemLine(item, color) : eventLine("item", scalarText(item), color));
|
|
10478
10898
|
}
|
|
10479
|
-
function
|
|
10480
|
-
|
|
10899
|
+
async function readDeviceStateFor(deps) {
|
|
10900
|
+
const state = await readDeviceState(deps.dir);
|
|
10901
|
+
return state && state.baseUrl === getBaseUrl() ? state : null;
|
|
10481
10902
|
}
|
|
10482
|
-
var
|
|
10483
|
-
|
|
10484
|
-
|
|
10485
|
-
|
|
10486
|
-
}
|
|
10487
|
-
|
|
10488
|
-
|
|
10489
|
-
|
|
10490
|
-
|
|
10491
|
-
|
|
10492
|
-
|
|
10493
|
-
|
|
10494
|
-
|
|
10495
|
-
|
|
10496
|
-
|
|
10903
|
+
var contextCommand = defineCommand({
|
|
10904
|
+
meta: {
|
|
10905
|
+
name: "context",
|
|
10906
|
+
description: `Point Docker at a sandbox's Docker engine over the private network: plain \`docker\` and \`docker compose\` then run there. Creates (or updates) the Docker context ghostdock-<org>-<sandbox> for tcp://<sandbox>.<org>.ghostdock.internal:2376 with a client certificate of this machine's device, and makes it the current context. Needs \`${getCliName()} network connect\` first, and Docker's CLI. The certificate lives 30 days; \`network connect\` and this command renew it with 10 days left. An expired certificate fails Docker's TLS handshake with Docker's own error: run this command again. Docker's \`no such host\` on a ghostdock.internal address means this machine is not connected: run \`${getCliName()} network connect\`. After an organization rename the old name stops resolving: run it again to update the host.`
|
|
10907
|
+
},
|
|
10908
|
+
args: {
|
|
10909
|
+
sandbox: {
|
|
10910
|
+
type: "positional",
|
|
10911
|
+
required: true,
|
|
10912
|
+
description: "Sandbox name, from `network ls`."
|
|
10913
|
+
},
|
|
10914
|
+
org: orgArg,
|
|
10915
|
+
use: {
|
|
10916
|
+
type: "boolean",
|
|
10917
|
+
default: true,
|
|
10918
|
+
description: "Make it the current Docker context (`docker context use`). `--no-use` only creates or updates it.",
|
|
10919
|
+
negativeDescription: "Only create or update the context; then pass `docker --context <name>`."
|
|
10920
|
+
}
|
|
10921
|
+
},
|
|
10922
|
+
async run({ args }) {
|
|
10923
|
+
process.exitCode = await runDockerContext({
|
|
10924
|
+
sandbox: args.sandbox,
|
|
10925
|
+
org: args.org,
|
|
10926
|
+
use: args.use
|
|
10927
|
+
});
|
|
10497
10928
|
}
|
|
10498
|
-
|
|
10499
|
-
|
|
10500
|
-
|
|
10501
|
-
|
|
10502
|
-
|
|
10929
|
+
});
|
|
10930
|
+
var dockerCommand = defineCommand({
|
|
10931
|
+
meta: {
|
|
10932
|
+
name: "docker",
|
|
10933
|
+
description: "Docker over the private network: a sandbox's Docker engine from this machine."
|
|
10934
|
+
},
|
|
10935
|
+
subCommands: { context: contextCommand }
|
|
10936
|
+
});
|
|
10937
|
+
|
|
10938
|
+
// src/commands/env.ts
|
|
10939
|
+
function reportError(err, error) {
|
|
10940
|
+
if (err instanceof AuthRequiredError || err instanceof Error) {
|
|
10941
|
+
error(err.message);
|
|
10942
|
+
} else {
|
|
10943
|
+
error(String(err));
|
|
10503
10944
|
}
|
|
10504
|
-
return
|
|
10945
|
+
return 1;
|
|
10505
10946
|
}
|
|
10506
|
-
function
|
|
10507
|
-
|
|
10508
|
-
|
|
10509
|
-
|
|
10510
|
-
|
|
10511
|
-
if (!isItem(data))
|
|
10512
|
-
return [eventLine("item", scalarText(data), color)];
|
|
10513
|
-
const entries = Object.entries(data).filter(([, value]) => value !== undefined);
|
|
10514
|
-
if (entries.length === 0 || entries.length === 1 && data.ok === true) {
|
|
10515
|
-
return [eventLine("done", "Done.", color)];
|
|
10516
|
-
}
|
|
10517
|
-
const lists = entries.filter(([, value]) => Array.isArray(value) && (value.length === 0 || value.some(isItem)));
|
|
10518
|
-
if (lists.length === 1 && !firstString(data, ID_KEYS)) {
|
|
10519
|
-
const [listKey, list] = lists[0];
|
|
10520
|
-
const rest = fieldLines(data, color, new Set([listKey]));
|
|
10521
|
-
return [...rest, ...listLines(list, color)];
|
|
10522
|
-
}
|
|
10523
|
-
const { id: idKey, label: labelKey } = lineKeys(data);
|
|
10524
|
-
const used = [idKey, labelKey, firstKey(data, STATUS_KEYS), firstKey(data, URL_KEYS)].filter((key) => key !== undefined);
|
|
10525
|
-
const head = used.length > 0 ? [itemLine(data, color)] : [];
|
|
10526
|
-
const boot = bootLines(data, color);
|
|
10527
|
-
if ("boot" in data && (boot.length > 0 || data.boot === null))
|
|
10528
|
-
used.push("boot");
|
|
10529
|
-
return [...head, ...boot, ...fieldLines(data, color, new Set(used))];
|
|
10947
|
+
function invokeTool(tool, input) {
|
|
10948
|
+
return authorizedRequest(`/api/v1/tools/${tool}`, {
|
|
10949
|
+
method: "POST",
|
|
10950
|
+
body: JSON.stringify(input)
|
|
10951
|
+
});
|
|
10530
10952
|
}
|
|
10531
|
-
|
|
10532
|
-
|
|
10533
|
-
|
|
10534
|
-
|
|
10535
|
-
|
|
10536
|
-
if (oneLine.length <= maxLength) {
|
|
10537
|
-
return oneLine;
|
|
10953
|
+
function displayValue(entry) {
|
|
10954
|
+
if (entry.valueOmittedReason === "secret")
|
|
10955
|
+
return "<secret>";
|
|
10956
|
+
if (entry.valueOmittedReason === "too_large") {
|
|
10957
|
+
return `<${entry.valueLength} chars — read with \`${getCliName()} env get\`>`;
|
|
10538
10958
|
}
|
|
10539
|
-
|
|
10540
|
-
const slice = oneLine.slice(0, budget);
|
|
10541
|
-
const breakAt = slice.lastIndexOf(" ");
|
|
10542
|
-
const cut = breakAt >= Math.floor(budget * 0.6) ? slice.slice(0, breakAt) : slice;
|
|
10543
|
-
return `${cut.trimEnd()}…`;
|
|
10959
|
+
return entry.value ?? "";
|
|
10544
10960
|
}
|
|
10545
|
-
|
|
10546
|
-
|
|
10547
|
-
|
|
10548
|
-
|
|
10549
|
-
|
|
10550
|
-
|
|
10961
|
+
function formatTable(entries) {
|
|
10962
|
+
const rows = entries.map((entry) => [
|
|
10963
|
+
entry.name,
|
|
10964
|
+
entry.kind,
|
|
10965
|
+
displayValue(entry),
|
|
10966
|
+
entry.comment ?? ""
|
|
10967
|
+
]);
|
|
10968
|
+
const header = ["NAME", "KIND", "VALUE", "COMMENT"];
|
|
10969
|
+
const widths = header.map((_, column) => Math.max(header[column]?.length ?? 0, ...rows.map((row) => row[column]?.length ?? 0)));
|
|
10970
|
+
const line = (cells) => cells.map((cell, column) => column === cells.length - 1 ? cell : cell.padEnd(widths[column] ?? 0)).join(" ").trimEnd();
|
|
10971
|
+
return [line(header), ...rows.map(line)].join(`
|
|
10972
|
+
`);
|
|
10551
10973
|
}
|
|
10552
|
-
function
|
|
10553
|
-
|
|
10974
|
+
async function readStdin() {
|
|
10975
|
+
const chunks = [];
|
|
10976
|
+
for await (const chunk of process.stdin) {
|
|
10977
|
+
chunks.push(Buffer.from(chunk));
|
|
10978
|
+
}
|
|
10979
|
+
return Buffer.concat(chunks).toString("utf8").replace(/\r?\n$/, "");
|
|
10554
10980
|
}
|
|
10555
|
-
function
|
|
10556
|
-
|
|
10557
|
-
|
|
10558
|
-
|
|
10559
|
-
|
|
10560
|
-
|
|
10561
|
-
|
|
10562
|
-
|
|
10563
|
-
|
|
10981
|
+
async function runEnvList(args, log = console.log, error = printError) {
|
|
10982
|
+
try {
|
|
10983
|
+
const result = await invokeTool("env_list", {
|
|
10984
|
+
organization: args.organization
|
|
10985
|
+
});
|
|
10986
|
+
if (args.json) {
|
|
10987
|
+
log(JSON.stringify(result.entries, null, 2));
|
|
10988
|
+
return 0;
|
|
10989
|
+
}
|
|
10990
|
+
if (result.entries.length === 0) {
|
|
10991
|
+
log(" No variables or secrets.");
|
|
10992
|
+
return 0;
|
|
10564
10993
|
}
|
|
10994
|
+
log(formatTable(result.entries));
|
|
10995
|
+
return 0;
|
|
10996
|
+
} catch (err) {
|
|
10997
|
+
return reportError(err, error);
|
|
10565
10998
|
}
|
|
10566
|
-
return variants;
|
|
10567
10999
|
}
|
|
10568
|
-
function
|
|
10569
|
-
|
|
10570
|
-
|
|
10571
|
-
|
|
10572
|
-
|
|
10573
|
-
|
|
11000
|
+
async function runEnvGet(args, log = console.log, error = printError) {
|
|
11001
|
+
try {
|
|
11002
|
+
const result = await invokeTool("env_get", {
|
|
11003
|
+
organization: args.organization,
|
|
11004
|
+
name: args.name
|
|
11005
|
+
});
|
|
11006
|
+
log(args.json ? JSON.stringify(result.entry, null, 2) : result.entry.value ?? "");
|
|
11007
|
+
return 0;
|
|
11008
|
+
} catch (err) {
|
|
11009
|
+
return reportError(err, error);
|
|
10574
11010
|
}
|
|
10575
|
-
|
|
10576
|
-
|
|
10577
|
-
|
|
10578
|
-
|
|
10579
|
-
types.add(type);
|
|
11011
|
+
}
|
|
11012
|
+
function resolveKind(args) {
|
|
11013
|
+
if (args.kind !== undefined && args.kind !== "variable" && args.kind !== "secret") {
|
|
11014
|
+
return { error: `--kind must be "variable" or "secret", got "${args.kind}".` };
|
|
10580
11015
|
}
|
|
10581
|
-
|
|
10582
|
-
|
|
10583
|
-
|
|
10584
|
-
return "array";
|
|
10585
|
-
if (types.size === 1) {
|
|
10586
|
-
const [only] = types;
|
|
10587
|
-
return only;
|
|
11016
|
+
const fromKind = args.kind;
|
|
11017
|
+
if (args.secret && fromKind === "variable") {
|
|
11018
|
+
return { error: "Pass either --secret or --kind variable, not both." };
|
|
10588
11019
|
}
|
|
10589
|
-
return;
|
|
10590
|
-
}
|
|
10591
|
-
function allowsNull(prop) {
|
|
10592
|
-
if (prop.type === "null")
|
|
10593
|
-
return true;
|
|
10594
|
-
if (Array.isArray(prop.type) && prop.type.includes("null"))
|
|
10595
|
-
return true;
|
|
10596
|
-
return schemaVariants(prop).some(allowsNull);
|
|
11020
|
+
return { kind: fromKind ?? (args.secret ? "secret" : undefined) };
|
|
10597
11021
|
}
|
|
10598
|
-
function
|
|
10599
|
-
|
|
10600
|
-
|
|
10601
|
-
|
|
10602
|
-
for (const name of positionals) {
|
|
10603
|
-
const prop = properties[name];
|
|
10604
|
-
if (!prop)
|
|
10605
|
-
continue;
|
|
10606
|
-
const jsonType = primaryJsonType(prop);
|
|
10607
|
-
args[toFlagName(name)] = {
|
|
10608
|
-
type: "positional",
|
|
10609
|
-
description: describe(prop.description ?? name),
|
|
10610
|
-
required: true,
|
|
10611
|
-
valueHint: jsonType
|
|
10612
|
-
};
|
|
11022
|
+
async function runEnvSet(args, log = console.log, error = printError, stdin = readStdin) {
|
|
11023
|
+
if (args.value !== undefined && args.valueStdin) {
|
|
11024
|
+
error("Pass either --value or --value-stdin, not both.");
|
|
11025
|
+
return 1;
|
|
10613
11026
|
}
|
|
10614
|
-
|
|
10615
|
-
|
|
10616
|
-
|
|
10617
|
-
const jsonType = primaryJsonType(prop);
|
|
10618
|
-
const isBoolean = jsonType === "boolean";
|
|
10619
|
-
const isJsonValue = jsonType === "object" || jsonType === "array";
|
|
10620
|
-
const typeNote = isJsonValue ? ` (JSON ${jsonType})` : "";
|
|
10621
|
-
const isOrganization = name === "organization";
|
|
10622
|
-
const isRequired = required.has(name) && prop.default === undefined && !isOrganization;
|
|
10623
|
-
const defaultNote = isOrganization ? " Defaults to GHOSTDOCK_ORG, then `organizations use`." : "";
|
|
10624
|
-
args[toFlagName(name)] = isBoolean ? {
|
|
10625
|
-
type: "boolean",
|
|
10626
|
-
description: describe(`${prop.description ?? name}${typeNote}${defaultNote}`),
|
|
10627
|
-
required: isRequired
|
|
10628
|
-
} : {
|
|
10629
|
-
type: "string",
|
|
10630
|
-
description: describe(`${prop.description ?? name}${typeNote}${defaultNote}`),
|
|
10631
|
-
required: isRequired,
|
|
10632
|
-
valueHint: jsonType
|
|
10633
|
-
};
|
|
11027
|
+
if (args.value === undefined && !args.valueStdin) {
|
|
11028
|
+
error("Provide --value, or --value-stdin to read the value from stdin.");
|
|
11029
|
+
return 1;
|
|
10634
11030
|
}
|
|
10635
|
-
|
|
10636
|
-
|
|
10637
|
-
|
|
10638
|
-
|
|
10639
|
-
return null;
|
|
10640
|
-
const jsonType = primaryJsonType(prop);
|
|
10641
|
-
if (jsonType === "boolean") {
|
|
10642
|
-
return typeof raw === "boolean" ? raw : raw === "true";
|
|
11031
|
+
const resolved = resolveKind(args);
|
|
11032
|
+
if ("error" in resolved) {
|
|
11033
|
+
error(resolved.error);
|
|
11034
|
+
return 1;
|
|
10643
11035
|
}
|
|
10644
|
-
|
|
10645
|
-
|
|
10646
|
-
|
|
10647
|
-
|
|
11036
|
+
const value = args.valueStdin ? await stdin() : args.value ?? "";
|
|
11037
|
+
try {
|
|
11038
|
+
const result = await invokeTool("env_set", {
|
|
11039
|
+
organization: args.organization,
|
|
11040
|
+
name: args.name,
|
|
11041
|
+
value,
|
|
11042
|
+
...resolved.kind ? { kind: resolved.kind } : {},
|
|
11043
|
+
...args.comment ? { comment: args.comment } : {}
|
|
11044
|
+
});
|
|
11045
|
+
if (args.json) {
|
|
11046
|
+
log(JSON.stringify(result, null, 2));
|
|
11047
|
+
return 0;
|
|
10648
11048
|
}
|
|
10649
|
-
|
|
10650
|
-
|
|
10651
|
-
|
|
10652
|
-
if (
|
|
10653
|
-
|
|
10654
|
-
try {
|
|
10655
|
-
return JSON.parse(raw);
|
|
10656
|
-
} catch {
|
|
10657
|
-
throw new Error(`Invalid JSON for --${flagName}: ${String(raw)}`);
|
|
11049
|
+
log(eventLine("done", `${result.created ? "Created" : "Updated"} ${result.entry.kind} ${result.entry.name}.`));
|
|
11050
|
+
if (result.previousKind === "secret") {
|
|
11051
|
+
log(eventLine("item", `Warning: ${result.entry.name} was a secret and is now a variable — its value is readable by everyone in the organization.`));
|
|
11052
|
+
} else if (result.previousKind === "variable") {
|
|
11053
|
+
log(eventLine("item", `${result.entry.name} was a variable and is now a secret; its value is no longer shown.`));
|
|
10658
11054
|
}
|
|
11055
|
+
return 0;
|
|
11056
|
+
} catch (err) {
|
|
11057
|
+
return reportError(err, error);
|
|
10659
11058
|
}
|
|
10660
|
-
return raw;
|
|
10661
11059
|
}
|
|
10662
|
-
function
|
|
10663
|
-
|
|
10664
|
-
|
|
10665
|
-
|
|
10666
|
-
|
|
10667
|
-
|
|
10668
|
-
|
|
10669
|
-
|
|
10670
|
-
|
|
11060
|
+
async function runEnvDelete(args, log = console.log, error = printError) {
|
|
11061
|
+
try {
|
|
11062
|
+
await invokeTool("env_delete", {
|
|
11063
|
+
organization: args.organization,
|
|
11064
|
+
name: args.name
|
|
11065
|
+
});
|
|
11066
|
+
log(eventLine("done", `Deleted ${args.name}.`));
|
|
11067
|
+
return 0;
|
|
11068
|
+
} catch (err) {
|
|
11069
|
+
return reportError(err, error);
|
|
11070
|
+
}
|
|
11071
|
+
}
|
|
11072
|
+
var nameArg = {
|
|
11073
|
+
type: "positional",
|
|
11074
|
+
description: "Environment variable name.",
|
|
11075
|
+
required: true
|
|
11076
|
+
};
|
|
11077
|
+
var envCommand = defineCommand({
|
|
11078
|
+
meta: {
|
|
11079
|
+
name: "env",
|
|
11080
|
+
description: "Manage organization environment variables and secrets."
|
|
11081
|
+
},
|
|
11082
|
+
subCommands: {
|
|
11083
|
+
list: defineCommand({
|
|
11084
|
+
meta: { name: "list", description: "List variables and secrets." },
|
|
11085
|
+
args: {
|
|
11086
|
+
org: orgArg
|
|
11087
|
+
},
|
|
11088
|
+
async run({ args }) {
|
|
11089
|
+
process.exitCode = await withOrganization(args.org, (organization) => runEnvList({
|
|
11090
|
+
organization,
|
|
11091
|
+
json: isJsonOutput()
|
|
11092
|
+
}));
|
|
11093
|
+
}
|
|
11094
|
+
}),
|
|
11095
|
+
get: defineCommand({
|
|
11096
|
+
meta: {
|
|
11097
|
+
name: "get",
|
|
11098
|
+
description: "Print one variable value in full (secrets cannot be read)."
|
|
11099
|
+
},
|
|
11100
|
+
args: {
|
|
11101
|
+
name: nameArg,
|
|
11102
|
+
org: orgArg
|
|
11103
|
+
},
|
|
11104
|
+
async run({ args }) {
|
|
11105
|
+
process.exitCode = await withOrganization(args.org, (organization) => runEnvGet({
|
|
11106
|
+
organization,
|
|
11107
|
+
name: args.name,
|
|
11108
|
+
json: isJsonOutput()
|
|
11109
|
+
}));
|
|
11110
|
+
}
|
|
11111
|
+
}),
|
|
11112
|
+
set: defineCommand({
|
|
11113
|
+
meta: { name: "set", description: "Create or replace a variable or secret." },
|
|
11114
|
+
args: {
|
|
11115
|
+
name: nameArg,
|
|
11116
|
+
org: orgArg,
|
|
11117
|
+
value: { type: "string", description: "Value to store." },
|
|
11118
|
+
"value-stdin": {
|
|
11119
|
+
type: "boolean",
|
|
11120
|
+
description: "Read the value from stdin — keeps secrets out of shell history.",
|
|
11121
|
+
default: false
|
|
11122
|
+
},
|
|
11123
|
+
secret: {
|
|
11124
|
+
type: "boolean",
|
|
11125
|
+
description: "Shorthand for --kind secret. Required when creating a secret.",
|
|
11126
|
+
default: false
|
|
11127
|
+
},
|
|
11128
|
+
kind: {
|
|
11129
|
+
type: "string",
|
|
11130
|
+
description: "variable or secret. Required when creating; on an update, omit to keep the existing kind or pass the other to convert."
|
|
11131
|
+
},
|
|
11132
|
+
comment: { type: "string", description: "Note about what this value is for." }
|
|
11133
|
+
},
|
|
11134
|
+
async run({ args }) {
|
|
11135
|
+
process.exitCode = await withOrganization(args.org, (organization) => runEnvSet({
|
|
11136
|
+
organization,
|
|
11137
|
+
name: args.name,
|
|
11138
|
+
value: args.value,
|
|
11139
|
+
valueStdin: args["value-stdin"],
|
|
11140
|
+
secret: args.secret,
|
|
11141
|
+
kind: args.kind,
|
|
11142
|
+
comment: args.comment,
|
|
11143
|
+
json: isJsonOutput()
|
|
11144
|
+
}));
|
|
11145
|
+
}
|
|
11146
|
+
}),
|
|
11147
|
+
delete: defineCommand({
|
|
11148
|
+
meta: { name: "delete", description: "Delete a variable or secret." },
|
|
11149
|
+
args: {
|
|
11150
|
+
name: nameArg,
|
|
11151
|
+
org: orgArg
|
|
11152
|
+
},
|
|
11153
|
+
async run({ args }) {
|
|
11154
|
+
process.exitCode = await withOrganization(args.org, (organization) => runEnvDelete({
|
|
11155
|
+
organization,
|
|
11156
|
+
name: args.name
|
|
11157
|
+
}));
|
|
11158
|
+
}
|
|
11159
|
+
})
|
|
10671
11160
|
}
|
|
10672
|
-
|
|
10673
|
-
}
|
|
11161
|
+
});
|
|
10674
11162
|
|
|
10675
|
-
// src/
|
|
10676
|
-
|
|
10677
|
-
|
|
10678
|
-
|
|
10679
|
-
|
|
10680
|
-
|
|
10681
|
-
|
|
10682
|
-
|
|
10683
|
-
|
|
10684
|
-
|
|
10685
|
-
|
|
10686
|
-
|
|
10687
|
-
|
|
10688
|
-
|
|
10689
|
-
|
|
10690
|
-
|
|
10691
|
-
|
|
10692
|
-
|
|
10693
|
-
|
|
10694
|
-
|
|
10695
|
-
|
|
10696
|
-
|
|
10697
|
-
|
|
10698
|
-
|
|
10699
|
-
const call = {
|
|
10700
|
-
organization: typeof input.organization === "string" ? input.organization : undefined,
|
|
10701
|
-
sandboxId: typeof input.sandboxId === "string" ? input.sandboxId : undefined,
|
|
10702
|
-
canPick: !!properties && "container" in properties && !input.container,
|
|
10703
|
-
setContainer: (container) => {
|
|
10704
|
-
input.container = container;
|
|
10705
|
-
}
|
|
11163
|
+
// src/container-picker.ts
|
|
11164
|
+
import { createInterface } from "node:readline/promises";
|
|
11165
|
+
function notRunningContainerOf(error) {
|
|
11166
|
+
if (!(error instanceof ApiError) || !error.data || typeof error.data !== "object")
|
|
11167
|
+
return null;
|
|
11168
|
+
const { code, params } = error.data;
|
|
11169
|
+
if (code !== "container_not_running")
|
|
11170
|
+
return null;
|
|
11171
|
+
const container = params && typeof params === "object" ? params : {};
|
|
11172
|
+
return typeof container.container === "string" && container.container ? container.container : null;
|
|
11173
|
+
}
|
|
11174
|
+
function containerQuestionOf(error) {
|
|
11175
|
+
if (!(error instanceof ApiError) || !error.data || typeof error.data !== "object")
|
|
11176
|
+
return null;
|
|
11177
|
+
const { code, params } = error.data;
|
|
11178
|
+
if (code !== "container_choice_required" && code !== "container_default_missing")
|
|
11179
|
+
return null;
|
|
11180
|
+
const record = params && typeof params === "object" ? params : {};
|
|
11181
|
+
const running = typeof record.containers === "string" ? record.containers.split(",").map((name) => name.trim()).filter(Boolean) : [];
|
|
11182
|
+
if (running.length === 0)
|
|
11183
|
+
return null;
|
|
11184
|
+
return {
|
|
11185
|
+
running,
|
|
11186
|
+
...code === "container_default_missing" && typeof record.container === "string" ? { missingDefault: record.container } : {}
|
|
10706
11187
|
};
|
|
11188
|
+
}
|
|
11189
|
+
var say = (line) => process.stderr.write(`${line}
|
|
11190
|
+
`);
|
|
11191
|
+
var chooseContainerOnTerminal = async ({ running, missingDefault }) => {
|
|
11192
|
+
if (missingDefault) {
|
|
11193
|
+
say(`
|
|
11194
|
+
The default container ${missingDefault} is gone.`);
|
|
11195
|
+
}
|
|
11196
|
+
say(missingDefault ? "Which container?" : `
|
|
11197
|
+
Several containers are running. Which one?`);
|
|
11198
|
+
running.forEach((name, index) => {
|
|
11199
|
+
say(` ${index + 1}. ${name}`);
|
|
11200
|
+
});
|
|
11201
|
+
const prompt = createInterface({ input: process.stdin, output: process.stderr });
|
|
10707
11202
|
try {
|
|
10708
|
-
const
|
|
10709
|
-
|
|
10710
|
-
|
|
10711
|
-
|
|
10712
|
-
|
|
10713
|
-
let elapsed;
|
|
10714
|
-
const send = async () => {
|
|
10715
|
-
if (!waitMessage)
|
|
10716
|
-
return post();
|
|
10717
|
-
const timed = await withProgress(waitMessage, post);
|
|
10718
|
-
elapsed = timed.elapsed;
|
|
10719
|
-
return timed.value;
|
|
10720
|
-
};
|
|
10721
|
-
const result = await withContainerPick(call, () => withContainerStart(call, send, rawArgs.start === true, pickers, notice), pickers, notice);
|
|
10722
|
-
printResult(result, () => {
|
|
10723
|
-
const color = colorEnabled(process.stdout);
|
|
10724
|
-
const lines = renderResult(result, color);
|
|
10725
|
-
return elapsed === undefined || bootLines(result, color).length > 0 ? lines : withElapsed(lines, elapsed);
|
|
10726
|
-
}, log);
|
|
10727
|
-
stopNote(result, notice);
|
|
10728
|
-
return 0;
|
|
10729
|
-
} catch (err) {
|
|
10730
|
-
error(err instanceof Error ? err.message : `${tool.name} failed.`);
|
|
10731
|
-
return 1;
|
|
11203
|
+
const answer = (await prompt.question("Number (Enter to cancel): ")).trim();
|
|
11204
|
+
const index = Number.parseInt(answer, 10) - 1;
|
|
11205
|
+
return Number.isInteger(index) && index >= 0 && index < running.length ? running[index] ?? null : null;
|
|
11206
|
+
} finally {
|
|
11207
|
+
prompt.close();
|
|
10732
11208
|
}
|
|
10733
|
-
}
|
|
10734
|
-
|
|
10735
|
-
|
|
10736
|
-
|
|
10737
|
-
|
|
10738
|
-
|
|
10739
|
-
|
|
10740
|
-
|
|
10741
|
-
notice(`${container} is the sandbox's default container: SSH, commands and files ask which container until it runs again.`);
|
|
10742
|
-
} else if (note === "last_running") {
|
|
10743
|
-
notice("No container is running now: SSH, commands and files have none to land in until one runs.");
|
|
10744
|
-
} else if (note === "sandbox_stopped") {
|
|
10745
|
-
notice(`The sandbox is stopped, so ${container} is too: nothing was done.`);
|
|
11209
|
+
};
|
|
11210
|
+
var confirmDefaultOnTerminal = async (container) => {
|
|
11211
|
+
const prompt = createInterface({ input: process.stdin, output: process.stderr });
|
|
11212
|
+
try {
|
|
11213
|
+
const answer = (await prompt.question(`Make ${container} the default? [y/N] `)).trim();
|
|
11214
|
+
return /^y(es)?$/i.test(answer);
|
|
11215
|
+
} finally {
|
|
11216
|
+
prompt.close();
|
|
10746
11217
|
}
|
|
10747
|
-
}
|
|
10748
|
-
var PROGRESS_MESSAGES = {
|
|
10749
|
-
sandboxes_create: "Creating sandbox…",
|
|
10750
|
-
sandboxes_wake: "Waking sandbox…",
|
|
10751
|
-
sandboxes_stop: "Stopping sandbox…",
|
|
10752
|
-
sandboxes_delete: "Deleting sandbox…",
|
|
10753
|
-
sandboxes_resize: "Resizing sandbox…",
|
|
10754
|
-
sandboxes_resize_disk: "Resizing disk…",
|
|
10755
|
-
sandboxes_commands_run: "Running command…",
|
|
10756
|
-
sandboxes_containers_start: "Starting container…",
|
|
10757
|
-
sandboxes_containers_stop: "Stopping container…",
|
|
10758
|
-
sandboxes_containers_restart: "Restarting container…",
|
|
10759
|
-
snapshots_create: "Saving snapshot…",
|
|
10760
|
-
projects_redeploy: "Redeploying…",
|
|
10761
|
-
providers_verify_incus: "Checking the Incus host…"
|
|
10762
11218
|
};
|
|
10763
|
-
|
|
10764
|
-
const
|
|
10765
|
-
|
|
10766
|
-
|
|
10767
|
-
return
|
|
11219
|
+
var confirmStartOnTerminal = async (container) => {
|
|
11220
|
+
const prompt = createInterface({ input: process.stdin, output: process.stderr });
|
|
11221
|
+
try {
|
|
11222
|
+
const answer = (await prompt.question(`Container ${container} is not running. Start it? [Y/n] `)).trim();
|
|
11223
|
+
return answer === "" || /^y(es)?$/i.test(answer);
|
|
11224
|
+
} finally {
|
|
11225
|
+
prompt.close();
|
|
10768
11226
|
}
|
|
10769
|
-
|
|
11227
|
+
};
|
|
11228
|
+
function defaultContainerPickers() {
|
|
11229
|
+
return process.stdin.isTTY && process.stderr.isTTY ? {
|
|
11230
|
+
choose: chooseContainerOnTerminal,
|
|
11231
|
+
confirmDefault: confirmDefaultOnTerminal,
|
|
11232
|
+
confirmStart: confirmStartOnTerminal
|
|
11233
|
+
} : undefined;
|
|
10770
11234
|
}
|
|
10771
|
-
|
|
10772
|
-
|
|
10773
|
-
|
|
10774
|
-
|
|
10775
|
-
|
|
10776
|
-
|
|
10777
|
-
const commands = pattern ? oneLine.replace(pattern, (name) => paths.get(name)?.join(" ") ?? name) : oneLine;
|
|
10778
|
-
return commands.replace(/\bthe ([a-z][a-z0-9-]*) skill\b/g, (phrase, skill) => skills.includes(skill) ? `the ${skill} skill (\`${getCliName()} skills get ${skill}\`)` : phrase);
|
|
10779
|
-
};
|
|
11235
|
+
|
|
11236
|
+
// src/progress.ts
|
|
11237
|
+
var PROGRESS_FRAMES = ["▂▄▆", "▄▆█", "▆█▆", "█▆▄", "▆▄▂", "▄▂▄"];
|
|
11238
|
+
var FRAME_MS = 120;
|
|
11239
|
+
function formatElapsed(ms) {
|
|
11240
|
+
return ms < 1000 ? `${Math.round(ms)}ms` : `${(ms / 1000).toFixed(1)}s`;
|
|
10780
11241
|
}
|
|
10781
|
-
function
|
|
10782
|
-
const
|
|
10783
|
-
const
|
|
10784
|
-
|
|
10785
|
-
|
|
10786
|
-
|
|
10787
|
-
|
|
10788
|
-
|
|
10789
|
-
|
|
10790
|
-
|
|
10791
|
-
|
|
10792
|
-
|
|
10793
|
-
alias: "f",
|
|
10794
|
-
description: "Keep printing new lines until Ctrl-C."
|
|
10795
|
-
};
|
|
10796
|
-
args.timestamps = { type: "boolean", description: "Put each line's time in front of it." };
|
|
11242
|
+
function startProgress(message, options = {}) {
|
|
11243
|
+
const stream = options.stream ?? process.stderr;
|
|
11244
|
+
const now = options.now ?? Date.now;
|
|
11245
|
+
const started = now();
|
|
11246
|
+
const elapsed = () => formatElapsed(now() - started);
|
|
11247
|
+
if (isJsonOutput())
|
|
11248
|
+
return { stop: elapsed };
|
|
11249
|
+
const animate = options.animate ?? stream.isTTY === true;
|
|
11250
|
+
if (!animate) {
|
|
11251
|
+
stream.write(` ${SYMBOLS.progress} ${message}
|
|
11252
|
+
`);
|
|
11253
|
+
return { stop: elapsed };
|
|
10797
11254
|
}
|
|
10798
|
-
|
|
10799
|
-
|
|
10800
|
-
|
|
10801
|
-
|
|
10802
|
-
|
|
10803
|
-
|
|
10804
|
-
|
|
10805
|
-
|
|
10806
|
-
|
|
10807
|
-
|
|
10808
|
-
|
|
10809
|
-
|
|
10810
|
-
|
|
10811
|
-
|
|
10812
|
-
|
|
11255
|
+
const color = colorEnabled(stream);
|
|
11256
|
+
let frame = 0;
|
|
11257
|
+
const draw = () => {
|
|
11258
|
+
const bars = accent(PROGRESS_FRAMES[frame % PROGRESS_FRAMES.length] ?? SYMBOLS.progress, color);
|
|
11259
|
+
stream.write(`\r\x1B[2K ${bars} ${message} ${dim(elapsed(), color)}`);
|
|
11260
|
+
frame += 1;
|
|
11261
|
+
};
|
|
11262
|
+
const restore = () => stream.write("\r\x1B[2K\x1B[?25h");
|
|
11263
|
+
const onInterrupt = () => {
|
|
11264
|
+
restore();
|
|
11265
|
+
process.exit(130);
|
|
11266
|
+
};
|
|
11267
|
+
stream.write("\x1B[?25l");
|
|
11268
|
+
draw();
|
|
11269
|
+
const timer = setInterval(draw, FRAME_MS);
|
|
11270
|
+
timer.unref?.();
|
|
11271
|
+
process.once("SIGINT", onInterrupt);
|
|
11272
|
+
let stopped = false;
|
|
11273
|
+
return {
|
|
11274
|
+
stop() {
|
|
11275
|
+
if (!stopped) {
|
|
11276
|
+
stopped = true;
|
|
11277
|
+
clearInterval(timer);
|
|
11278
|
+
process.off("SIGINT", onInterrupt);
|
|
11279
|
+
restore();
|
|
10813
11280
|
}
|
|
10814
|
-
|
|
10815
|
-
|
|
10816
|
-
|
|
10817
|
-
}
|
|
10818
|
-
return commands;
|
|
10819
|
-
}
|
|
10820
|
-
|
|
10821
|
-
// src/commands/login.ts
|
|
10822
|
-
import { hostname } from "node:os";
|
|
10823
|
-
|
|
10824
|
-
// src/commands/login-organization.ts
|
|
10825
|
-
import { createInterface as createInterface2 } from "node:readline/promises";
|
|
10826
|
-
function label(organization) {
|
|
10827
|
-
return `${organization.name} (${organization.slug ?? organization.id})`;
|
|
11281
|
+
return elapsed();
|
|
11282
|
+
}
|
|
11283
|
+
};
|
|
10828
11284
|
}
|
|
10829
|
-
|
|
10830
|
-
|
|
10831
|
-
Which organization should commands use by default?`);
|
|
10832
|
-
organizations.forEach((organization, index) => {
|
|
10833
|
-
console.log(` ${index + 1}. ${label(organization)}`);
|
|
10834
|
-
});
|
|
10835
|
-
const prompt = createInterface2({ input: process.stdin, output: process.stdout });
|
|
11285
|
+
async function withProgress(message, work, options) {
|
|
11286
|
+
const progress = startProgress(message, options);
|
|
10836
11287
|
try {
|
|
10837
|
-
const
|
|
10838
|
-
|
|
10839
|
-
|
|
10840
|
-
|
|
10841
|
-
|
|
11288
|
+
const value = await work();
|
|
11289
|
+
return { value, elapsed: progress.stop() };
|
|
11290
|
+
} catch (err) {
|
|
11291
|
+
progress.stop();
|
|
11292
|
+
throw err;
|
|
10842
11293
|
}
|
|
10843
|
-
};
|
|
10844
|
-
function defaultChooser() {
|
|
10845
|
-
return process.stdin.isTTY && process.stdout.isTTY ? chooseOrganizationOnTerminal : undefined;
|
|
10846
11294
|
}
|
|
10847
|
-
|
|
10848
|
-
|
|
10849
|
-
|
|
11295
|
+
|
|
11296
|
+
// src/container-flow.ts
|
|
11297
|
+
async function withContainerPick(call, send, pickers, notice) {
|
|
10850
11298
|
try {
|
|
10851
|
-
|
|
10852
|
-
} catch {
|
|
10853
|
-
|
|
10854
|
-
|
|
10855
|
-
|
|
10856
|
-
|
|
10857
|
-
|
|
10858
|
-
|
|
10859
|
-
|
|
10860
|
-
|
|
10861
|
-
|
|
10862
|
-
|
|
10863
|
-
|
|
10864
|
-
|
|
10865
|
-
|
|
10866
|
-
|
|
10867
|
-
|
|
10868
|
-
|
|
10869
|
-
|
|
10870
|
-
|
|
11299
|
+
return await send();
|
|
11300
|
+
} catch (err) {
|
|
11301
|
+
const question = containerQuestionOf(err);
|
|
11302
|
+
if (!pickers || !question || !call.canPick)
|
|
11303
|
+
throw err;
|
|
11304
|
+
const container = await pickers.choose(question);
|
|
11305
|
+
if (!container)
|
|
11306
|
+
throw err;
|
|
11307
|
+
call.setContainer(container);
|
|
11308
|
+
const result = await send();
|
|
11309
|
+
if (call.sandboxId !== undefined && await pickers.confirmDefault(container)) {
|
|
11310
|
+
try {
|
|
11311
|
+
await authorizedRequest("/api/v1/tools/sandboxes_default_container", {
|
|
11312
|
+
method: "POST",
|
|
11313
|
+
body: JSON.stringify({
|
|
11314
|
+
organization: call.organization,
|
|
11315
|
+
sandboxId: call.sandboxId,
|
|
11316
|
+
container
|
|
11317
|
+
})
|
|
11318
|
+
});
|
|
11319
|
+
notice(eventLine("done", `Default container: ${container}.`));
|
|
11320
|
+
} catch (err) {
|
|
11321
|
+
notice(eventLine("failed", `The default container was not set: ${err instanceof Error ? err.message : String(err)}`));
|
|
11322
|
+
}
|
|
10871
11323
|
}
|
|
10872
|
-
|
|
10873
|
-
return 0;
|
|
10874
|
-
}
|
|
10875
|
-
const current = organizations.find((organization) => organization.id === settings.defaultOrganization?.id);
|
|
10876
|
-
if (current) {
|
|
10877
|
-
log(eventLine("item", `Default organization: ${label(current)} (unchanged).`));
|
|
10878
|
-
return 0;
|
|
10879
|
-
}
|
|
10880
|
-
if (settings.defaultOrganization) {
|
|
10881
|
-
delete settings.defaultOrganization;
|
|
10882
|
-
await writeSettings(settings);
|
|
10883
|
-
}
|
|
10884
|
-
const [only, ...others] = organizations;
|
|
10885
|
-
if (!only) {
|
|
10886
|
-
log(eventLine("item", "You do not belong to an organization yet."));
|
|
10887
|
-
return 0;
|
|
10888
|
-
}
|
|
10889
|
-
if (others.length === 0) {
|
|
10890
|
-
await save(only, " (your only organization)");
|
|
10891
|
-
return 0;
|
|
10892
|
-
}
|
|
10893
|
-
const index = choose ? await choose(organizations) : null;
|
|
10894
|
-
const chosen = index === null ? undefined : organizations[index];
|
|
10895
|
-
if (chosen) {
|
|
10896
|
-
await save(chosen, "");
|
|
10897
|
-
return 0;
|
|
11324
|
+
return result;
|
|
10898
11325
|
}
|
|
10899
|
-
log(eventLine("item", `You belong to ${organizations.length} organizations: ${organizations.map((organization) => organization.slug ?? organization.id).join(", ")}. Pick a default with \`${cli} organizations use <org>\`, or pass --org.`));
|
|
10900
|
-
return 0;
|
|
10901
11326
|
}
|
|
10902
|
-
|
|
10903
|
-
// src/commands/login.ts
|
|
10904
|
-
function defaultClientName() {
|
|
11327
|
+
async function withContainerStart(call, send, start, pickers, notice) {
|
|
10905
11328
|
try {
|
|
10906
|
-
|
|
10907
|
-
|
|
10908
|
-
|
|
10909
|
-
|
|
10910
|
-
|
|
11329
|
+
return await send();
|
|
11330
|
+
} catch (err) {
|
|
11331
|
+
const container = notRunningContainerOf(err);
|
|
11332
|
+
if (!container || call.sandboxId === undefined)
|
|
11333
|
+
throw err;
|
|
11334
|
+
if (!start && !(pickers && await pickers.confirmStart(container)))
|
|
11335
|
+
throw err;
|
|
11336
|
+
await withProgress("Starting container…", () => authorizedRequest("/api/v1/tools/sandboxes_containers_start", {
|
|
11337
|
+
method: "POST",
|
|
11338
|
+
body: JSON.stringify({
|
|
11339
|
+
organization: call.organization,
|
|
11340
|
+
sandboxId: call.sandboxId,
|
|
11341
|
+
container
|
|
11342
|
+
})
|
|
11343
|
+
}));
|
|
11344
|
+
notice(eventLine("done", `Started ${container}.`));
|
|
11345
|
+
return await send();
|
|
11346
|
+
}
|
|
10911
11347
|
}
|
|
10912
|
-
|
|
10913
|
-
|
|
11348
|
+
|
|
11349
|
+
// src/container-logs.ts
|
|
11350
|
+
var FOLLOW_INTERVAL_MS = 2000;
|
|
11351
|
+
function defaultLogsDeps(follow) {
|
|
11352
|
+
let stop = false;
|
|
11353
|
+
if (follow) {
|
|
11354
|
+
process.once("SIGINT", () => {
|
|
11355
|
+
stop = true;
|
|
11356
|
+
});
|
|
11357
|
+
}
|
|
10914
11358
|
return {
|
|
10915
|
-
|
|
10916
|
-
|
|
10917
|
-
|
|
11359
|
+
request: (input) => authorizedRequest("/api/v1/tools/sandboxes_containers_logs", {
|
|
11360
|
+
method: "POST",
|
|
11361
|
+
body: JSON.stringify(input)
|
|
11362
|
+
}),
|
|
11363
|
+
out: (line) => process.stdout.write(`${line}
|
|
11364
|
+
`),
|
|
11365
|
+
err: (line) => process.stderr.write(`${line}
|
|
11366
|
+
`),
|
|
11367
|
+
sleep: (ms) => new Promise((resolve) => setTimeout(resolve, ms)),
|
|
11368
|
+
stopped: () => stop
|
|
10918
11369
|
};
|
|
10919
11370
|
}
|
|
10920
|
-
function
|
|
10921
|
-
|
|
10922
|
-
|
|
10923
|
-
|
|
10924
|
-
|
|
10925
|
-
|
|
10926
|
-
|
|
10927
|
-
};
|
|
10928
|
-
if (args.code) {
|
|
10929
|
-
const result = await deviceToken({ pasteCode: args.code });
|
|
10930
|
-
if (result.status === "approved") {
|
|
10931
|
-
await writeCredentials(toCredentials(result));
|
|
10932
|
-
return loggedIn();
|
|
11371
|
+
async function runContainerLogs(input, options, deps = defaultLogsDeps(options.follow)) {
|
|
11372
|
+
const json = isJsonOutput();
|
|
11373
|
+
const color = colorEnabled(process.stdout);
|
|
11374
|
+
const print = (line) => {
|
|
11375
|
+
if (json) {
|
|
11376
|
+
deps.out(JSON.stringify(line));
|
|
11377
|
+
return;
|
|
10933
11378
|
}
|
|
10934
|
-
|
|
11379
|
+
const text = options.timestamps && line.time ? `${dim(line.time, color)} ${line.text}` : line.text;
|
|
11380
|
+
(line.stream === "stderr" ? deps.err : deps.out)(text);
|
|
11381
|
+
};
|
|
11382
|
+
let result = await deps.request(input);
|
|
11383
|
+
if (!result.running) {
|
|
11384
|
+
deps.err("The sandbox is stopped: it has no logs to show.");
|
|
10935
11385
|
return 1;
|
|
10936
11386
|
}
|
|
10937
|
-
|
|
10938
|
-
|
|
10939
|
-
|
|
10940
|
-
|
|
10941
|
-
|
|
10942
|
-
|
|
10943
|
-
|
|
10944
|
-
|
|
10945
|
-
|
|
10946
|
-
|
|
10947
|
-
|
|
10948
|
-
|
|
10949
|
-
try {
|
|
10950
|
-
const result = await deviceToken({ deviceCode: start.deviceCode });
|
|
10951
|
-
if (result.status === "approved") {
|
|
10952
|
-
progress.stop();
|
|
10953
|
-
await writeCredentials(toCredentials(result));
|
|
10954
|
-
return loggedIn();
|
|
10955
|
-
}
|
|
10956
|
-
} catch (err) {
|
|
10957
|
-
progress.stop();
|
|
10958
|
-
if (err instanceof ApiError && err.status === 400) {
|
|
10959
|
-
error(`Login failed: ${err.message}`);
|
|
10960
|
-
return 1;
|
|
11387
|
+
let last = null;
|
|
11388
|
+
for (;; ) {
|
|
11389
|
+
for (const line of result.lines) {
|
|
11390
|
+
print(line);
|
|
11391
|
+
if (line.time)
|
|
11392
|
+
last = line.time;
|
|
11393
|
+
}
|
|
11394
|
+
if (!options.follow) {
|
|
11395
|
+
if (result.truncated && json) {
|
|
11396
|
+
deps.out(JSON.stringify({ truncated: true }));
|
|
11397
|
+
} else if (result.truncated) {
|
|
11398
|
+
deps.err(dim("(older lines left out: use --tail for more)", colorEnabled(process.stderr)));
|
|
10961
11399
|
}
|
|
10962
|
-
|
|
11400
|
+
return 0;
|
|
10963
11401
|
}
|
|
10964
|
-
|
|
10965
|
-
|
|
10966
|
-
|
|
10967
|
-
|
|
10968
|
-
}
|
|
10969
|
-
|
|
10970
|
-
|
|
10971
|
-
|
|
10972
|
-
|
|
10973
|
-
|
|
10974
|
-
|
|
10975
|
-
|
|
10976
|
-
|
|
10977
|
-
|
|
10978
|
-
default: false
|
|
10979
|
-
},
|
|
10980
|
-
code: {
|
|
10981
|
-
type: "string",
|
|
10982
|
-
description: "Redeem the one-time paste code shown in the browser instead of polling."
|
|
10983
|
-
},
|
|
10984
|
-
timeout: {
|
|
10985
|
-
type: "string",
|
|
10986
|
-
description: "Max seconds to poll when using --wait (default 300)."
|
|
10987
|
-
},
|
|
10988
|
-
org: {
|
|
10989
|
-
type: "string",
|
|
10990
|
-
description: "Set this organization (id or slug) as the default once logged in."
|
|
11402
|
+
if (!result.truncated)
|
|
11403
|
+
await deps.sleep(FOLLOW_INTERVAL_MS);
|
|
11404
|
+
if (deps.stopped())
|
|
11405
|
+
return 0;
|
|
11406
|
+
const { tail: _tail, ...rest } = input;
|
|
11407
|
+
try {
|
|
11408
|
+
result = await deps.request(last ? { ...rest, after: last } : input);
|
|
11409
|
+
} catch (error) {
|
|
11410
|
+
deps.err(red(error instanceof Error ? error.message : String(error), colorEnabled(process.stderr)));
|
|
11411
|
+
return 1;
|
|
11412
|
+
}
|
|
11413
|
+
if (!result.running) {
|
|
11414
|
+
deps.err("The sandbox stopped.");
|
|
11415
|
+
return 0;
|
|
10991
11416
|
}
|
|
10992
|
-
},
|
|
10993
|
-
async run({ args }) {
|
|
10994
|
-
process.exitCode = await runLogin(args);
|
|
10995
11417
|
}
|
|
10996
|
-
}
|
|
11418
|
+
}
|
|
10997
11419
|
|
|
10998
|
-
// src/
|
|
10999
|
-
|
|
11000
|
-
|
|
11001
|
-
|
|
11002
|
-
|
|
11003
|
-
|
|
11004
|
-
|
|
11005
|
-
|
|
11006
|
-
|
|
11007
|
-
|
|
11420
|
+
// src/human.ts
|
|
11421
|
+
var ID_KEYS = [
|
|
11422
|
+
"sandboxId",
|
|
11423
|
+
"siteId",
|
|
11424
|
+
"projectId",
|
|
11425
|
+
"deploymentId",
|
|
11426
|
+
"imageId",
|
|
11427
|
+
"snapshotId",
|
|
11428
|
+
"urlId",
|
|
11429
|
+
"mcpId",
|
|
11430
|
+
"commandId",
|
|
11431
|
+
"sessionId",
|
|
11432
|
+
"sandboxProviderId",
|
|
11433
|
+
"id"
|
|
11434
|
+
];
|
|
11435
|
+
var LABEL_KEYS = ["displayName", "name", "title", "slug", "label", "path", "uri", "ref"];
|
|
11436
|
+
var STATUS_KEYS = ["status", "outcome", "state"];
|
|
11437
|
+
var URL_KEYS = ["url", "liveUrl", "publicUrl", "sshCommand"];
|
|
11438
|
+
function isItem(value) {
|
|
11439
|
+
return value !== null && typeof value === "object" && !Array.isArray(value);
|
|
11440
|
+
}
|
|
11441
|
+
function isScalar(value) {
|
|
11442
|
+
return typeof value === "string" || typeof value === "number" || typeof value === "boolean";
|
|
11443
|
+
}
|
|
11444
|
+
function firstKey(item, keys) {
|
|
11445
|
+
return keys.find((key) => {
|
|
11446
|
+
const value = item[key];
|
|
11447
|
+
return typeof value === "string" && value.trim() !== "";
|
|
11448
|
+
});
|
|
11449
|
+
}
|
|
11450
|
+
function firstString(item, keys) {
|
|
11451
|
+
const key = firstKey(item, keys);
|
|
11452
|
+
return key ? item[key] : undefined;
|
|
11453
|
+
}
|
|
11454
|
+
function symbolForStatus(status) {
|
|
11455
|
+
const s = (status ?? "").toLowerCase();
|
|
11456
|
+
if (/(fail|error|denied)/.test(s))
|
|
11457
|
+
return "failed";
|
|
11458
|
+
if (/^(running|active|ready|deployed|published|online|verified|ok)$/.test(s))
|
|
11459
|
+
return "ready";
|
|
11460
|
+
if (/^(stopped|deleted|unpublished|vanished|archived|revoked|closed|killed|exited|dead|paused)$/.test(s)) {
|
|
11461
|
+
return "stopped";
|
|
11008
11462
|
}
|
|
11009
|
-
|
|
11010
|
-
|
|
11011
|
-
|
|
11463
|
+
if (/(pending|starting|spawning|stopping|building|progress|awaiting|queued|creating|restarting|removing)/.test(s)) {
|
|
11464
|
+
return "progress";
|
|
11465
|
+
}
|
|
11466
|
+
return "item";
|
|
11012
11467
|
}
|
|
11013
|
-
|
|
11014
|
-
|
|
11015
|
-
|
|
11016
|
-
|
|
11017
|
-
|
|
11018
|
-
|
|
11019
|
-
process.exitCode = await runLogout();
|
|
11468
|
+
function isSandbox(item) {
|
|
11469
|
+
return typeof item.sandboxId === "string" && typeof item.name === "string" && !("id" in item);
|
|
11470
|
+
}
|
|
11471
|
+
function lineKeys(item) {
|
|
11472
|
+
if (isSandbox(item)) {
|
|
11473
|
+
return { id: "name", label: firstKey(item, ["displayName"]) };
|
|
11020
11474
|
}
|
|
11021
|
-
|
|
11022
|
-
|
|
11023
|
-
// src/commands/network.ts
|
|
11024
|
-
import { spawn, spawnSync } from "node:child_process";
|
|
11025
|
-
import { existsSync as existsSync2 } from "node:fs";
|
|
11026
|
-
import { hostname as hostname2 } from "node:os";
|
|
11027
|
-
function defaultDeps() {
|
|
11028
|
-
return {
|
|
11029
|
-
request: authorizedRequest,
|
|
11030
|
-
run: (command, args) => new Promise((resolve, reject) => {
|
|
11031
|
-
const child = spawn(command, args, { stdio: "inherit" });
|
|
11032
|
-
child.on("error", reject);
|
|
11033
|
-
child.on("close", (code) => resolve(code ?? 1));
|
|
11034
|
-
}),
|
|
11035
|
-
output: (command, args) => {
|
|
11036
|
-
const result = spawnSync(command, args, { encoding: "utf8" });
|
|
11037
|
-
return result.status === 0 ? result.stdout : "";
|
|
11038
|
-
},
|
|
11039
|
-
platform: process.platform,
|
|
11040
|
-
has: (command) => spawnSync("sh", ["-c", `command -v ${command}`]).status === 0,
|
|
11041
|
-
dir: networkDir(),
|
|
11042
|
-
up: () => tunnelUp(),
|
|
11043
|
-
exists: existsSync2,
|
|
11044
|
-
log: console.log,
|
|
11045
|
-
error: printError,
|
|
11046
|
-
notice: printNotice
|
|
11047
|
-
};
|
|
11475
|
+
const idKeys = "id" in item ? ID_KEYS.filter((key) => key !== "sandboxId") : ID_KEYS;
|
|
11476
|
+
return { id: firstKey(item, idKeys), label: firstKey(item, LABEL_KEYS) };
|
|
11048
11477
|
}
|
|
11049
|
-
function
|
|
11050
|
-
return
|
|
11478
|
+
function shortId(id) {
|
|
11479
|
+
return /^[0-9a-f]{64}$/.test(id) ? id.slice(0, 12) : id;
|
|
11051
11480
|
}
|
|
11052
|
-
function
|
|
11053
|
-
const
|
|
11054
|
-
const
|
|
11055
|
-
|
|
11481
|
+
function itemLine(item, color) {
|
|
11482
|
+
const keys = lineKeys(item);
|
|
11483
|
+
const id = keys.id ? item[keys.id] : undefined;
|
|
11484
|
+
const label = keys.label ? item[keys.label] : undefined;
|
|
11485
|
+
const status = firstString(item, STATUS_KEYS);
|
|
11486
|
+
const url = firstString(item, URL_KEYS);
|
|
11487
|
+
const parts = [label ?? id ?? "(unnamed)"];
|
|
11488
|
+
if (label && id && id !== label)
|
|
11489
|
+
parts.push(dim(shortId(id), color));
|
|
11490
|
+
if (status)
|
|
11491
|
+
parts.push(status);
|
|
11492
|
+
if (url)
|
|
11493
|
+
parts.push(`${SYMBOLS.url} ${url}`);
|
|
11494
|
+
return eventLine(symbolForStatus(status), parts.join(" "), color);
|
|
11056
11495
|
}
|
|
11057
|
-
|
|
11058
|
-
|
|
11059
|
-
|
|
11060
|
-
|
|
11496
|
+
function scalarText(value) {
|
|
11497
|
+
if (value === null)
|
|
11498
|
+
return "—";
|
|
11499
|
+
if (isScalar(value))
|
|
11500
|
+
return String(value);
|
|
11501
|
+
if (Array.isArray(value) && value.every(isScalar))
|
|
11502
|
+
return value.join(", ");
|
|
11503
|
+
if (Array.isArray(value))
|
|
11504
|
+
return `${value.length} item${value.length === 1 ? "" : "s"}`;
|
|
11505
|
+
return "…";
|
|
11061
11506
|
}
|
|
11062
|
-
|
|
11063
|
-
|
|
11064
|
-
|
|
11065
|
-
|
|
11066
|
-
|
|
11507
|
+
function fieldLines(item, color, skip) {
|
|
11508
|
+
const lines = [];
|
|
11509
|
+
for (const [key, value] of Object.entries(item)) {
|
|
11510
|
+
if (skip.has(key) || value === undefined)
|
|
11511
|
+
continue;
|
|
11512
|
+
if (Array.isArray(value) && value.some(isItem)) {
|
|
11513
|
+
lines.push(` ${dim(`${key}:`, color)}`);
|
|
11514
|
+
for (const entry of value) {
|
|
11515
|
+
lines.push(` ${isItem(entry) ? itemLine(entry, color) : eventLine("item", scalarText(entry), color)}`);
|
|
11516
|
+
}
|
|
11517
|
+
continue;
|
|
11518
|
+
}
|
|
11519
|
+
if (isItem(value)) {
|
|
11520
|
+
for (const [sub, subValue] of Object.entries(value)) {
|
|
11521
|
+
if (subValue === undefined || isItem(subValue))
|
|
11522
|
+
continue;
|
|
11523
|
+
lines.push(` ${dim(`${key}.${sub}:`, color)} ${scalarText(subValue)}`);
|
|
11524
|
+
}
|
|
11525
|
+
continue;
|
|
11526
|
+
}
|
|
11527
|
+
lines.push(` ${dim(`${key}:`, color)} ${scalarText(value)}`);
|
|
11067
11528
|
}
|
|
11068
|
-
|
|
11069
|
-
|
|
11070
|
-
|
|
11071
|
-
|
|
11072
|
-
|
|
11073
|
-
|
|
11074
|
-
|
|
11075
|
-
|
|
11076
|
-
|
|
11077
|
-
|
|
11078
|
-
|
|
11079
|
-
|
|
11080
|
-
|
|
11081
|
-
|
|
11082
|
-
|
|
11083
|
-
|
|
11084
|
-
|
|
11085
|
-
|
|
11086
|
-
|
|
11087
|
-
|
|
11529
|
+
return lines;
|
|
11530
|
+
}
|
|
11531
|
+
function listLines(items, color) {
|
|
11532
|
+
if (items.length === 0)
|
|
11533
|
+
return [` ${dim("(none)", color)}`];
|
|
11534
|
+
return items.map((item) => isItem(item) ? itemLine(item, color) : eventLine("item", scalarText(item), color));
|
|
11535
|
+
}
|
|
11536
|
+
function bootPhase(value) {
|
|
11537
|
+
return isItem(value) && typeof value.ms === "number" && Number.isFinite(value.ms) ? { ms: value.ms } : null;
|
|
11538
|
+
}
|
|
11539
|
+
var DISK_SOURCE_TEXT = {
|
|
11540
|
+
fresh: "image pulled",
|
|
11541
|
+
"image-base": "image already on disk",
|
|
11542
|
+
"user-snapshot": "from a snapshot"
|
|
11543
|
+
};
|
|
11544
|
+
function bootLines(data, color) {
|
|
11545
|
+
if (!isItem(data) || !isItem(data.boot))
|
|
11546
|
+
return [];
|
|
11547
|
+
const sandbox = bootPhase(data.boot.sandbox);
|
|
11548
|
+
if (!sandbox)
|
|
11549
|
+
return [];
|
|
11550
|
+
const lines = [eventLine("ready", `sandbox started in ${formatElapsed(sandbox.ms)}`, color)];
|
|
11551
|
+
const container = bootPhase(data.boot.container);
|
|
11552
|
+
if (container) {
|
|
11553
|
+
lines.push(eventLine("done", `container started in ${formatElapsed(container.ms)}`, color));
|
|
11088
11554
|
}
|
|
11089
|
-
const
|
|
11090
|
-
|
|
11091
|
-
|
|
11092
|
-
|
|
11093
|
-
|
|
11094
|
-
|
|
11095
|
-
|
|
11555
|
+
const provision = bootPhase(data.boot.provision);
|
|
11556
|
+
if (provision) {
|
|
11557
|
+
const disk = typeof data.boot.disk === "string" ? DISK_SOURCE_TEXT[data.boot.disk] : undefined;
|
|
11558
|
+
const how = disk ? ` (${disk})` : "";
|
|
11559
|
+
lines.push(eventLine("done", `ready in ${formatElapsed(provision.ms)}${how}`, color));
|
|
11560
|
+
}
|
|
11561
|
+
return lines;
|
|
11562
|
+
}
|
|
11563
|
+
function renderResult(data, color) {
|
|
11564
|
+
if (data === null || data === undefined)
|
|
11565
|
+
return [eventLine("done", "Done.", color)];
|
|
11566
|
+
if (Array.isArray(data))
|
|
11567
|
+
return listLines(data, color);
|
|
11568
|
+
if (!isItem(data))
|
|
11569
|
+
return [eventLine("item", scalarText(data), color)];
|
|
11570
|
+
const entries = Object.entries(data).filter(([, value]) => value !== undefined);
|
|
11571
|
+
if (entries.length === 0 || entries.length === 1 && data.ok === true) {
|
|
11572
|
+
return [eventLine("done", "Done.", color)];
|
|
11573
|
+
}
|
|
11574
|
+
const lists = entries.filter(([, value]) => Array.isArray(value) && (value.length === 0 || value.some(isItem)));
|
|
11575
|
+
if (lists.length === 1 && !firstString(data, ID_KEYS)) {
|
|
11576
|
+
const [listKey, list] = lists[0];
|
|
11577
|
+
const rest = fieldLines(data, color, new Set([listKey]));
|
|
11578
|
+
return [...rest, ...listLines(list, color)];
|
|
11579
|
+
}
|
|
11580
|
+
const { id: idKey, label: labelKey } = lineKeys(data);
|
|
11581
|
+
const used = [idKey, labelKey, firstKey(data, STATUS_KEYS), firstKey(data, URL_KEYS)].filter((key) => key !== undefined);
|
|
11582
|
+
const head = used.length > 0 ? [itemLine(data, color)] : [];
|
|
11583
|
+
const boot = bootLines(data, color);
|
|
11584
|
+
if ("boot" in data && (boot.length > 0 || data.boot === null))
|
|
11585
|
+
used.push("boot");
|
|
11586
|
+
return [...head, ...boot, ...fieldLines(data, color, new Set(used))];
|
|
11587
|
+
}
|
|
11588
|
+
|
|
11589
|
+
// src/cli-help-text.ts
|
|
11590
|
+
var CLI_HELP_DESCRIPTION_MAX_LENGTH = 80;
|
|
11591
|
+
function cliHelpDescription(description, maxLength = CLI_HELP_DESCRIPTION_MAX_LENGTH) {
|
|
11592
|
+
const oneLine = description.replace(/\s+/g, " ").trim();
|
|
11593
|
+
if (oneLine.length <= maxLength) {
|
|
11594
|
+
return oneLine;
|
|
11096
11595
|
}
|
|
11097
|
-
const
|
|
11098
|
-
|
|
11099
|
-
|
|
11100
|
-
|
|
11101
|
-
|
|
11102
|
-
|
|
11103
|
-
|
|
11104
|
-
|
|
11105
|
-
|
|
11106
|
-
|
|
11107
|
-
|
|
11108
|
-
|
|
11109
|
-
|
|
11110
|
-
|
|
11111
|
-
|
|
11112
|
-
|
|
11113
|
-
|
|
11114
|
-
|
|
11115
|
-
|
|
11116
|
-
|
|
11117
|
-
|
|
11118
|
-
|
|
11119
|
-
|
|
11120
|
-
|
|
11121
|
-
|
|
11122
|
-
|
|
11123
|
-
...network.netZone && device.shortNameOrganization && shortNameOrganizationChanged ? clearingNotice(network.netCookieSite) : []
|
|
11124
|
-
], deps.log);
|
|
11125
|
-
printPauseHint(device.state, deps);
|
|
11126
|
-
return 0;
|
|
11596
|
+
const budget = Math.max(1, maxLength - 1);
|
|
11597
|
+
const slice = oneLine.slice(0, budget);
|
|
11598
|
+
const breakAt = slice.lastIndexOf(" ");
|
|
11599
|
+
const cut = breakAt >= Math.floor(budget * 0.6) ? slice.slice(0, breakAt) : slice;
|
|
11600
|
+
return `${cut.trimEnd()}…`;
|
|
11601
|
+
}
|
|
11602
|
+
|
|
11603
|
+
// src/schema-flags.ts
|
|
11604
|
+
function toFlagName(propertyName) {
|
|
11605
|
+
if (propertyName === "organization")
|
|
11606
|
+
return "org";
|
|
11607
|
+
return propertyName.replace(/([KMGTP])iB(?![a-z])/g, "$1ib").replace(/([a-z0-9])([A-Z])/g, "$1-$2").replace(/[_\s]+/g, "-").toLowerCase();
|
|
11608
|
+
}
|
|
11609
|
+
function isJsonSchemaProperty(value) {
|
|
11610
|
+
return value !== null && typeof value === "object" && !Array.isArray(value);
|
|
11611
|
+
}
|
|
11612
|
+
function schemaVariants(prop) {
|
|
11613
|
+
const variants = [];
|
|
11614
|
+
for (const key of ["anyOf", "oneOf", "allOf"]) {
|
|
11615
|
+
const group = prop[key];
|
|
11616
|
+
if (!Array.isArray(group))
|
|
11617
|
+
continue;
|
|
11618
|
+
for (const item of group) {
|
|
11619
|
+
if (isJsonSchemaProperty(item))
|
|
11620
|
+
variants.push(item);
|
|
11621
|
+
}
|
|
11127
11622
|
}
|
|
11128
|
-
|
|
11129
|
-
|
|
11130
|
-
|
|
11131
|
-
|
|
11132
|
-
|
|
11623
|
+
return variants;
|
|
11624
|
+
}
|
|
11625
|
+
function primaryJsonType(prop) {
|
|
11626
|
+
if (prop.type !== undefined) {
|
|
11627
|
+
if (Array.isArray(prop.type)) {
|
|
11628
|
+
return prop.type.find((t) => t !== "null");
|
|
11133
11629
|
}
|
|
11630
|
+
return prop.type === "null" ? undefined : prop.type;
|
|
11134
11631
|
}
|
|
11135
|
-
const
|
|
11136
|
-
const
|
|
11137
|
-
|
|
11138
|
-
|
|
11139
|
-
|
|
11632
|
+
const types = new Set;
|
|
11633
|
+
for (const variant of schemaVariants(prop)) {
|
|
11634
|
+
const type = primaryJsonType(variant);
|
|
11635
|
+
if (type)
|
|
11636
|
+
types.add(type);
|
|
11140
11637
|
}
|
|
11141
|
-
|
|
11142
|
-
|
|
11143
|
-
|
|
11144
|
-
|
|
11145
|
-
|
|
11146
|
-
|
|
11147
|
-
|
|
11148
|
-
|
|
11149
|
-
|
|
11150
|
-
dns,
|
|
11151
|
-
rearmed: false,
|
|
11152
|
-
pause: device.state
|
|
11153
|
-
}, () => [
|
|
11154
|
-
eventLine("done", `Connected through ${provider.name}. This machine is ${device.address} in the private network.`),
|
|
11155
|
-
...switched ? [
|
|
11156
|
-
` Replaced the connection through ${switched.name}: its sandboxes are out of reach now.`
|
|
11157
|
-
] : [],
|
|
11158
|
-
` Reachable: the sandboxes on ${provider.name}. One provider at a time.`,
|
|
11159
|
-
` Names: <sandbox>.<org>.${network.zone}`,
|
|
11160
|
-
...network.netZone ? [` In a browser: https://<sandbox>--<org>.${network.netZone}:<port>`] : [],
|
|
11161
|
-
...network.netZone && pinned ? [
|
|
11162
|
-
` Short names: https://<sandbox>.${network.netZone}:<port> for ${pinned.slug}'s sandboxes, on this machine.`,
|
|
11163
|
-
` Another organization: ${getCliName()} network connect --org <org>.`
|
|
11164
|
-
] : [],
|
|
11165
|
-
...network.netZone && pinned && shortNameOrganizationChanged ? clearingNotice(network.netCookieSite) : [],
|
|
11166
|
-
...network.ingress4 && !device.address4 ? [
|
|
11167
|
-
" No IPv4 address was free on this provider's host, so browser names work over IPv6 only:",
|
|
11168
|
-
" Chrome on a network without IPv6 cannot open them on this machine. Firefox and Safari",
|
|
11169
|
-
" can. Run connect again later: an address frees up when another device goes."
|
|
11170
|
-
] : [],
|
|
11171
|
-
...ipv4Clashes.map(({ address, owner }) => ` ${address} is already routed through ${owner} on this machine: browsers may not reach the private network's names over IPv4 while it is. Ask your Ghostdock admin about the IPv4 block.`),
|
|
11172
|
-
...dns === "none" ? [
|
|
11173
|
-
" No split DNS on this machine (no systemd-resolved): use the addresses from",
|
|
11174
|
-
` \`${getCliName()} network ls\`.`
|
|
11175
|
-
] : [],
|
|
11176
|
-
` → ${getCliName()} network ls`
|
|
11177
|
-
], deps.log);
|
|
11178
|
-
printPauseHint(device.state, deps);
|
|
11179
|
-
return 0;
|
|
11180
|
-
}
|
|
11181
|
-
function rearmedLine(state, now = new Date) {
|
|
11182
|
-
if (!state)
|
|
11183
|
-
return "Re-armed.";
|
|
11184
|
-
const until = clockTime(new Date(now.getTime() + state.maxSessionSeconds * 1000).toISOString(), now);
|
|
11185
|
-
return `Re-armed: network access for this device runs until ${until} (pauses after ${formatPauseLimit(state.idleTimeoutSeconds)} idle).`;
|
|
11638
|
+
if (types.has("object"))
|
|
11639
|
+
return "object";
|
|
11640
|
+
if (types.has("array"))
|
|
11641
|
+
return "array";
|
|
11642
|
+
if (types.size === 1) {
|
|
11643
|
+
const [only] = types;
|
|
11644
|
+
return only;
|
|
11645
|
+
}
|
|
11646
|
+
return;
|
|
11186
11647
|
}
|
|
11187
|
-
function
|
|
11188
|
-
if (
|
|
11189
|
-
return;
|
|
11190
|
-
|
|
11191
|
-
|
|
11192
|
-
|
|
11648
|
+
function allowsNull(prop) {
|
|
11649
|
+
if (prop.type === "null")
|
|
11650
|
+
return true;
|
|
11651
|
+
if (Array.isArray(prop.type) && prop.type.includes("null"))
|
|
11652
|
+
return true;
|
|
11653
|
+
return schemaVariants(prop).some(allowsNull);
|
|
11193
11654
|
}
|
|
11194
|
-
function
|
|
11195
|
-
const
|
|
11196
|
-
const
|
|
11197
|
-
|
|
11198
|
-
|
|
11199
|
-
const
|
|
11200
|
-
if (
|
|
11201
|
-
|
|
11655
|
+
function buildToolArgs(schema, describe = cliHelpDescription, positionals = []) {
|
|
11656
|
+
const properties = schema.properties ?? {};
|
|
11657
|
+
const required = new Set(schema.required ?? []);
|
|
11658
|
+
const args = {};
|
|
11659
|
+
for (const name of positionals) {
|
|
11660
|
+
const prop = properties[name];
|
|
11661
|
+
if (!prop)
|
|
11662
|
+
continue;
|
|
11663
|
+
const jsonType = primaryJsonType(prop);
|
|
11664
|
+
args[toFlagName(name)] = {
|
|
11665
|
+
type: "positional",
|
|
11666
|
+
description: describe(prop.description ?? name),
|
|
11667
|
+
required: true,
|
|
11668
|
+
valueHint: jsonType
|
|
11669
|
+
};
|
|
11202
11670
|
}
|
|
11203
|
-
|
|
11671
|
+
for (const [name, prop] of Object.entries(properties)) {
|
|
11672
|
+
if (positionals.includes(name))
|
|
11673
|
+
continue;
|
|
11674
|
+
const jsonType = primaryJsonType(prop);
|
|
11675
|
+
const isBoolean = jsonType === "boolean";
|
|
11676
|
+
const isJsonValue = jsonType === "object" || jsonType === "array";
|
|
11677
|
+
const typeNote = isJsonValue ? ` (JSON ${jsonType})` : "";
|
|
11678
|
+
const isOrganization = name === "organization";
|
|
11679
|
+
const isRequired = required.has(name) && prop.default === undefined && !isOrganization;
|
|
11680
|
+
const defaultNote = isOrganization ? " Defaults to GHOSTDOCK_ORG, then `organizations use`." : "";
|
|
11681
|
+
args[toFlagName(name)] = isBoolean ? {
|
|
11682
|
+
type: "boolean",
|
|
11683
|
+
description: describe(`${prop.description ?? name}${typeNote}${defaultNote}`),
|
|
11684
|
+
required: isRequired
|
|
11685
|
+
} : {
|
|
11686
|
+
type: "string",
|
|
11687
|
+
description: describe(`${prop.description ?? name}${typeNote}${defaultNote}`),
|
|
11688
|
+
required: isRequired,
|
|
11689
|
+
valueHint: jsonType
|
|
11690
|
+
};
|
|
11691
|
+
}
|
|
11692
|
+
return args;
|
|
11204
11693
|
}
|
|
11205
|
-
function
|
|
11206
|
-
|
|
11207
|
-
|
|
11208
|
-
|
|
11209
|
-
|
|
11210
|
-
|
|
11211
|
-
|
|
11212
|
-
|
|
11694
|
+
function coerceFlagValue(raw, prop, flagName) {
|
|
11695
|
+
if (raw === "null" && allowsNull(prop))
|
|
11696
|
+
return null;
|
|
11697
|
+
const jsonType = primaryJsonType(prop);
|
|
11698
|
+
if (jsonType === "boolean") {
|
|
11699
|
+
return typeof raw === "boolean" ? raw : raw === "true";
|
|
11700
|
+
}
|
|
11701
|
+
if (jsonType === "number" || jsonType === "integer") {
|
|
11702
|
+
const num = Number(raw);
|
|
11703
|
+
if (Number.isNaN(num)) {
|
|
11704
|
+
throw new Error(`Invalid number for --${flagName}: ${String(raw)}`);
|
|
11705
|
+
}
|
|
11706
|
+
return num;
|
|
11707
|
+
}
|
|
11708
|
+
if (jsonType === "object" || jsonType === "array") {
|
|
11709
|
+
if (typeof raw !== "string")
|
|
11710
|
+
return raw;
|
|
11711
|
+
try {
|
|
11712
|
+
return JSON.parse(raw);
|
|
11713
|
+
} catch {
|
|
11714
|
+
throw new Error(`Invalid JSON for --${flagName}: ${String(raw)}`);
|
|
11715
|
+
}
|
|
11716
|
+
}
|
|
11717
|
+
return raw;
|
|
11213
11718
|
}
|
|
11214
|
-
|
|
11215
|
-
|
|
11216
|
-
|
|
11217
|
-
const
|
|
11218
|
-
|
|
11219
|
-
|
|
11220
|
-
|
|
11221
|
-
|
|
11222
|
-
|
|
11719
|
+
function buildToolInput(schema, rawArgs) {
|
|
11720
|
+
const properties = schema.properties ?? {};
|
|
11721
|
+
const input = {};
|
|
11722
|
+
for (const [name, prop] of Object.entries(properties)) {
|
|
11723
|
+
const flagName = toFlagName(name);
|
|
11724
|
+
const raw = rawArgs[flagName] ?? rawArgs[name];
|
|
11725
|
+
if (raw === undefined)
|
|
11726
|
+
continue;
|
|
11727
|
+
input[name] = coerceFlagValue(raw, prop, flagName);
|
|
11728
|
+
}
|
|
11729
|
+
return input;
|
|
11223
11730
|
}
|
|
11224
|
-
|
|
11225
|
-
|
|
11226
|
-
|
|
11227
|
-
|
|
11228
|
-
|
|
11229
|
-
|
|
11230
|
-
|
|
11231
|
-
|
|
11731
|
+
|
|
11732
|
+
// src/commands/invoke.ts
|
|
11733
|
+
async function runToolInvoke(tool, rawArgs, log = console.log, error = printError, notice = printNotice, pickers = defaultContainerPickers()) {
|
|
11734
|
+
const properties = tool.inputSchema.properties;
|
|
11735
|
+
let input;
|
|
11736
|
+
try {
|
|
11737
|
+
input = buildToolInput(tool.inputSchema, rawArgs);
|
|
11738
|
+
if (properties && "organization" in properties && input.organization === undefined) {
|
|
11739
|
+
input.organization = await requireOrganizationRef(undefined, notice);
|
|
11232
11740
|
}
|
|
11741
|
+
} catch (err) {
|
|
11742
|
+
error(err instanceof Error ? err.message : String(err));
|
|
11743
|
+
return 1;
|
|
11233
11744
|
}
|
|
11234
|
-
|
|
11235
|
-
|
|
11236
|
-
|
|
11237
|
-
|
|
11238
|
-
|
|
11239
|
-
|
|
11240
|
-
|
|
11745
|
+
if (tool.name === "sandboxes_containers_logs") {
|
|
11746
|
+
try {
|
|
11747
|
+
return await runContainerLogs(input, {
|
|
11748
|
+
follow: rawArgs.follow === true,
|
|
11749
|
+
timestamps: rawArgs.timestamps === true
|
|
11750
|
+
});
|
|
11751
|
+
} catch (err) {
|
|
11752
|
+
error(err instanceof Error ? err.message : `${tool.name} failed.`);
|
|
11241
11753
|
return 1;
|
|
11242
11754
|
}
|
|
11243
11755
|
}
|
|
11244
|
-
|
|
11245
|
-
|
|
11246
|
-
|
|
11247
|
-
|
|
11248
|
-
|
|
11249
|
-
|
|
11250
|
-
}
|
|
11251
|
-
async function readConnection(deps) {
|
|
11252
|
-
const state = await readDeviceState(deps.dir);
|
|
11253
|
-
const up = deps.up();
|
|
11254
|
-
if (!state)
|
|
11255
|
-
return { state, up, device: null, asked: true, provider: null, staleMtu: null };
|
|
11256
|
-
try {
|
|
11257
|
-
const { devices, mtu } = await deps.request("/api/v1/network/devices");
|
|
11258
|
-
const device = devices.find((d) => d.id === state.deviceId) ?? null;
|
|
11259
|
-
const config = await readConfigMtu(configPath(deps.dir));
|
|
11260
|
-
const staleMtu = config !== null && config !== mtu ? { config, current: mtu } : null;
|
|
11261
|
-
if (staleMtu) {
|
|
11262
|
-
deps.notice(`This machine's tunnel has MTU ${staleMtu.config}, the network now uses ${staleMtu.current}. ` + `Run \`${getCliName()} network connect\` again: until then, large packets other than TCP ` + "can be lost on the way.");
|
|
11756
|
+
const call = {
|
|
11757
|
+
organization: typeof input.organization === "string" ? input.organization : undefined,
|
|
11758
|
+
sandboxId: typeof input.sandboxId === "string" ? input.sandboxId : undefined,
|
|
11759
|
+
canPick: !!properties && "container" in properties && !input.container,
|
|
11760
|
+
setContainer: (container) => {
|
|
11761
|
+
input.container = container;
|
|
11263
11762
|
}
|
|
11264
|
-
|
|
11265
|
-
|
|
11266
|
-
|
|
11267
|
-
|
|
11268
|
-
|
|
11269
|
-
|
|
11270
|
-
|
|
11763
|
+
};
|
|
11764
|
+
try {
|
|
11765
|
+
const post = () => authorizedRequest(`/api/v1/tools/${tool.name}`, {
|
|
11766
|
+
method: "POST",
|
|
11767
|
+
body: JSON.stringify(input)
|
|
11768
|
+
});
|
|
11769
|
+
const waitMessage = PROGRESS_MESSAGES[tool.name];
|
|
11770
|
+
let elapsed;
|
|
11771
|
+
const send = async () => {
|
|
11772
|
+
if (!waitMessage)
|
|
11773
|
+
return post();
|
|
11774
|
+
const timed = await withProgress(waitMessage, post);
|
|
11775
|
+
elapsed = timed.elapsed;
|
|
11776
|
+
return timed.value;
|
|
11271
11777
|
};
|
|
11778
|
+
const result = await withContainerPick(call, () => withContainerStart(call, send, rawArgs.start === true, pickers, notice), pickers, notice);
|
|
11779
|
+
printResult(result, () => {
|
|
11780
|
+
const color = colorEnabled(process.stdout);
|
|
11781
|
+
const lines = renderResult(result, color);
|
|
11782
|
+
return elapsed === undefined || bootLines(result, color).length > 0 ? lines : withElapsed(lines, elapsed);
|
|
11783
|
+
}, log);
|
|
11784
|
+
stopNote(result, notice);
|
|
11785
|
+
return 0;
|
|
11272
11786
|
} catch (err) {
|
|
11273
|
-
|
|
11274
|
-
return
|
|
11275
|
-
state,
|
|
11276
|
-
up,
|
|
11277
|
-
device: null,
|
|
11278
|
-
asked: false,
|
|
11279
|
-
provider: up ? state.gatewayProvider : null,
|
|
11280
|
-
staleMtu: null
|
|
11281
|
-
};
|
|
11787
|
+
error(err instanceof Error ? err.message : `${tool.name} failed.`);
|
|
11788
|
+
return 1;
|
|
11282
11789
|
}
|
|
11283
11790
|
}
|
|
11284
|
-
|
|
11285
|
-
|
|
11286
|
-
|
|
11287
|
-
|
|
11288
|
-
|
|
11791
|
+
function stopNote(result, notice) {
|
|
11792
|
+
if (!result || typeof result !== "object")
|
|
11793
|
+
return;
|
|
11794
|
+
const { action, note, container } = result;
|
|
11795
|
+
if (action !== "stop" || typeof container !== "string")
|
|
11796
|
+
return;
|
|
11797
|
+
if (note === "default") {
|
|
11798
|
+
notice(`${container} is the sandbox's default container: SSH, commands and files ask which container until it runs again.`);
|
|
11799
|
+
} else if (note === "last_running") {
|
|
11800
|
+
notice("No container is running now: SSH, commands and files have none to land in until one runs.");
|
|
11801
|
+
} else if (note === "sandbox_stopped") {
|
|
11802
|
+
notice(`The sandbox is stopped, so ${container} is too: nothing was done.`);
|
|
11289
11803
|
}
|
|
11290
|
-
printResult({ connected: up, device, provider, staleMtu, staleSplitDns }, () => {
|
|
11291
|
-
if (!state) {
|
|
11292
|
-
return [eventLine("item", `Not set up. Run \`${getCliName()} network connect\`.`)];
|
|
11293
|
-
}
|
|
11294
|
-
const through = provider ? ` through ${provider.name}` : "";
|
|
11295
|
-
const lines = [
|
|
11296
|
-
eventLine("item", `${up ? `Connected${through}` : "Not connected"}: ${state.address}`)
|
|
11297
|
-
];
|
|
11298
|
-
if (device) {
|
|
11299
|
-
lines.push(` Device ${device.name}, valid until ${device.expiresAt.slice(0, 10)}.`);
|
|
11300
|
-
lines.push(` ${deviceStateText(device.state)}.`);
|
|
11301
|
-
if (device.shortNameOrganization) {
|
|
11302
|
-
lines.push(` Short names: ${device.shortNameOrganization.slug}'s sandboxes.`);
|
|
11303
|
-
}
|
|
11304
|
-
if (!device.gatewayProvider) {
|
|
11305
|
-
lines.push(` Its provider is gone, so no gateway serves it. Run \`${getCliName()} network connect\`.`);
|
|
11306
|
-
}
|
|
11307
|
-
} else if (asked) {
|
|
11308
|
-
lines.push(` The app no longer knows this device (removed, expired or logged out). Run \`${getCliName()} network connect\`.`);
|
|
11309
|
-
}
|
|
11310
|
-
if (provider)
|
|
11311
|
-
lines.push(` Reachable: the sandboxes on ${provider.name}.`);
|
|
11312
|
-
return lines;
|
|
11313
|
-
}, deps.log);
|
|
11314
|
-
return 0;
|
|
11315
11804
|
}
|
|
11316
|
-
|
|
11317
|
-
|
|
11318
|
-
|
|
11805
|
+
var PROGRESS_MESSAGES = {
|
|
11806
|
+
sandboxes_create: "Creating sandbox…",
|
|
11807
|
+
sandboxes_wake: "Waking sandbox…",
|
|
11808
|
+
sandboxes_stop: "Stopping sandbox…",
|
|
11809
|
+
sandboxes_delete: "Deleting sandbox…",
|
|
11810
|
+
sandboxes_resize: "Resizing sandbox…",
|
|
11811
|
+
sandboxes_resize_disk: "Resizing disk…",
|
|
11812
|
+
sandboxes_commands_run: "Running command…",
|
|
11813
|
+
sandboxes_containers_start: "Starting container…",
|
|
11814
|
+
sandboxes_containers_stop: "Stopping container…",
|
|
11815
|
+
sandboxes_containers_restart: "Restarting container…",
|
|
11816
|
+
snapshots_create: "Saving snapshot…",
|
|
11817
|
+
projects_redeploy: "Redeploying…",
|
|
11818
|
+
providers_verify_incus: "Checking the Incus host…"
|
|
11819
|
+
};
|
|
11820
|
+
function withElapsed(lines, elapsed) {
|
|
11821
|
+
const color = colorEnabled(process.stdout);
|
|
11822
|
+
const [first, ...rest] = lines;
|
|
11823
|
+
if (first === undefined || first.startsWith(" ")) {
|
|
11824
|
+
return [eventLine("done", `Done in ${elapsed}.`, color), ...lines];
|
|
11319
11825
|
}
|
|
11320
|
-
|
|
11321
|
-
return null;
|
|
11322
|
-
return `not reachable: on ${sandbox.provider.name} → ${connectCommandFor(sandbox.provider)}`;
|
|
11826
|
+
return [`${first} ${dim(`in ${elapsed}`, color)}`, ...rest];
|
|
11323
11827
|
}
|
|
11324
|
-
|
|
11325
|
-
|
|
11326
|
-
|
|
11327
|
-
|
|
11328
|
-
|
|
11329
|
-
const
|
|
11330
|
-
const
|
|
11331
|
-
|
|
11332
|
-
|
|
11333
|
-
|
|
11334
|
-
|
|
11335
|
-
|
|
11336
|
-
|
|
11337
|
-
|
|
11338
|
-
|
|
11339
|
-
|
|
11340
|
-
|
|
11341
|
-
|
|
11342
|
-
|
|
11343
|
-
|
|
11344
|
-
|
|
11345
|
-
|
|
11346
|
-
|
|
11347
|
-
|
|
11348
|
-
|
|
11349
|
-
|
|
11350
|
-
|
|
11351
|
-
|
|
11352
|
-
|
|
11353
|
-
|
|
11354
|
-
|
|
11355
|
-
|
|
11356
|
-
|
|
11828
|
+
function cliText(catalog, skills = BUILTIN_SKILL_NAMES) {
|
|
11829
|
+
const paths = new Map(catalog.filter((tool) => tool.cli).map((tool) => [tool.name, tool.cli]));
|
|
11830
|
+
const names = [...paths.keys()].sort((a, b) => b.length - a.length);
|
|
11831
|
+
const pattern = names.length > 0 ? new RegExp(`\\b(${names.join("|")})\\b`, "g") : null;
|
|
11832
|
+
return (text) => {
|
|
11833
|
+
const oneLine = text.replace(/\s+/g, " ").trim();
|
|
11834
|
+
const commands = pattern ? oneLine.replace(pattern, (name) => paths.get(name)?.join(" ") ?? name) : oneLine;
|
|
11835
|
+
return commands.replace(/\bthe ([a-z][a-z0-9-]*) skill\b/g, (phrase, skill) => skills.includes(skill) ? `the ${skill} skill (\`${getCliName()} skills get ${skill}\`)` : phrase);
|
|
11836
|
+
};
|
|
11837
|
+
}
|
|
11838
|
+
function cliOnlyArgs(tool) {
|
|
11839
|
+
const schema = tool.inputSchema;
|
|
11840
|
+
const args = {};
|
|
11841
|
+
if (schema.properties && "container" in schema.properties && !schema.required?.includes("container")) {
|
|
11842
|
+
args.start = {
|
|
11843
|
+
type: "boolean",
|
|
11844
|
+
description: "Start the container first when it is not running, without asking."
|
|
11845
|
+
};
|
|
11846
|
+
}
|
|
11847
|
+
if (tool.name === "sandboxes_containers_logs") {
|
|
11848
|
+
args.follow = {
|
|
11849
|
+
type: "boolean",
|
|
11850
|
+
alias: "f",
|
|
11851
|
+
description: "Keep printing new lines until Ctrl-C."
|
|
11852
|
+
};
|
|
11853
|
+
args.timestamps = { type: "boolean", description: "Put each line's time in front of it." };
|
|
11854
|
+
}
|
|
11855
|
+
return args;
|
|
11856
|
+
}
|
|
11857
|
+
var generatedToolCommands = new WeakSet;
|
|
11858
|
+
function createToolCommands(catalog) {
|
|
11859
|
+
const commands = {};
|
|
11860
|
+
const toCliText = cliText(catalog);
|
|
11861
|
+
for (const tool of catalog) {
|
|
11862
|
+
const command = defineCommand({
|
|
11863
|
+
meta: {
|
|
11864
|
+
name: tool.cli?.at(-1) ?? tool.name,
|
|
11865
|
+
description: toCliText(tool.description)
|
|
11866
|
+
},
|
|
11867
|
+
args: { ...buildToolArgs(tool.inputSchema, toCliText, tool.cliArgs), ...cliOnlyArgs(tool) },
|
|
11868
|
+
async run({ args }) {
|
|
11869
|
+
process.exitCode = await runToolInvoke(tool, args);
|
|
11357
11870
|
}
|
|
11358
|
-
|
|
11359
|
-
|
|
11360
|
-
|
|
11361
|
-
}
|
|
11362
|
-
|
|
11363
|
-
|
|
11871
|
+
});
|
|
11872
|
+
generatedToolCommands.add(command);
|
|
11873
|
+
commands[tool.name] = command;
|
|
11874
|
+
}
|
|
11875
|
+
return commands;
|
|
11876
|
+
}
|
|
11877
|
+
|
|
11878
|
+
// src/commands/login.ts
|
|
11879
|
+
import { hostname as hostname2 } from "node:os";
|
|
11880
|
+
|
|
11881
|
+
// src/commands/login-organization.ts
|
|
11882
|
+
import { createInterface as createInterface2 } from "node:readline/promises";
|
|
11883
|
+
function label(organization) {
|
|
11884
|
+
return `${organization.name} (${organization.slug ?? organization.id})`;
|
|
11885
|
+
}
|
|
11886
|
+
var chooseOrganizationOnTerminal = async (organizations) => {
|
|
11887
|
+
console.log(`
|
|
11888
|
+
Which organization should commands use by default?`);
|
|
11889
|
+
organizations.forEach((organization, index) => {
|
|
11890
|
+
console.log(` ${index + 1}. ${label(organization)}`);
|
|
11891
|
+
});
|
|
11892
|
+
const prompt = createInterface2({ input: process.stdin, output: process.stdout });
|
|
11893
|
+
try {
|
|
11894
|
+
const answer = (await prompt.question(`Number (Enter to skip): `)).trim();
|
|
11895
|
+
const index = Number.parseInt(answer, 10) - 1;
|
|
11896
|
+
return Number.isInteger(index) && index >= 0 && index < organizations.length ? index : null;
|
|
11897
|
+
} finally {
|
|
11898
|
+
prompt.close();
|
|
11364
11899
|
}
|
|
11900
|
+
};
|
|
11901
|
+
function defaultChooser() {
|
|
11902
|
+
return process.stdin.isTTY && process.stdout.isTTY ? chooseOrganizationOnTerminal : undefined;
|
|
11365
11903
|
}
|
|
11366
|
-
async function
|
|
11904
|
+
async function chooseDefaultOrganizationAfterLogin(org, log, choose = defaultChooser()) {
|
|
11905
|
+
const cli = getCliName();
|
|
11906
|
+
let organizations;
|
|
11367
11907
|
try {
|
|
11368
|
-
|
|
11369
|
-
|
|
11370
|
-
|
|
11908
|
+
({ organizations } = await authorizedRequest("/api/v1/tools/organizations_list", { method: "POST", body: "{}" }));
|
|
11909
|
+
} catch {
|
|
11910
|
+
organizations = undefined;
|
|
11911
|
+
}
|
|
11912
|
+
if (!Array.isArray(organizations)) {
|
|
11913
|
+
log(eventLine("item", `Pick a default organization with \`${cli} organizations use <org>\`.`));
|
|
11914
|
+
return org ? 1 : 0;
|
|
11915
|
+
}
|
|
11916
|
+
const settings = await readSettings();
|
|
11917
|
+
const save = async (organization, why) => {
|
|
11918
|
+
settings.defaultOrganization = { id: organization.id, slug: organization.slug };
|
|
11919
|
+
await writeSettings(settings);
|
|
11920
|
+
log(eventLine("done", `Default organization: ${label(organization)}${why}.`));
|
|
11921
|
+
};
|
|
11922
|
+
const ref = org?.trim();
|
|
11923
|
+
if (ref) {
|
|
11924
|
+
const match = organizations.find((organization) => organization.id === ref) ?? organizations.find((organization) => organization.slug === ref);
|
|
11925
|
+
if (!match) {
|
|
11926
|
+
log(eventLine("failed", `You are not a member of an organization "${ref}"; no default set.`));
|
|
11927
|
+
return 1;
|
|
11928
|
+
}
|
|
11929
|
+
await save(match, "");
|
|
11930
|
+
return 0;
|
|
11931
|
+
}
|
|
11932
|
+
const current = organizations.find((organization) => organization.id === settings.defaultOrganization?.id);
|
|
11933
|
+
if (current) {
|
|
11934
|
+
log(eventLine("item", `Default organization: ${label(current)} (unchanged).`));
|
|
11935
|
+
return 0;
|
|
11936
|
+
}
|
|
11937
|
+
if (settings.defaultOrganization) {
|
|
11938
|
+
delete settings.defaultOrganization;
|
|
11939
|
+
await writeSettings(settings);
|
|
11940
|
+
}
|
|
11941
|
+
const [only, ...others] = organizations;
|
|
11942
|
+
if (!only) {
|
|
11943
|
+
log(eventLine("item", "You do not belong to an organization yet."));
|
|
11944
|
+
return 0;
|
|
11945
|
+
}
|
|
11946
|
+
if (others.length === 0) {
|
|
11947
|
+
await save(only, " (your only organization)");
|
|
11948
|
+
return 0;
|
|
11949
|
+
}
|
|
11950
|
+
const index = choose ? await choose(organizations) : null;
|
|
11951
|
+
const chosen = index === null ? undefined : organizations[index];
|
|
11952
|
+
if (chosen) {
|
|
11953
|
+
await save(chosen, "");
|
|
11371
11954
|
return 0;
|
|
11372
|
-
} catch (err) {
|
|
11373
|
-
deps.error(message(err));
|
|
11374
|
-
return 1;
|
|
11375
11955
|
}
|
|
11956
|
+
log(eventLine("item", `You belong to ${organizations.length} organizations: ${organizations.map((organization) => organization.slug ?? organization.id).join(", ")}. Pick a default with \`${cli} organizations use <org>\`, or pass --org.`));
|
|
11957
|
+
return 0;
|
|
11376
11958
|
}
|
|
11377
|
-
|
|
11959
|
+
|
|
11960
|
+
// src/commands/login.ts
|
|
11961
|
+
function defaultClientName() {
|
|
11378
11962
|
try {
|
|
11379
|
-
|
|
11380
|
-
|
|
11381
|
-
|
|
11382
|
-
|
|
11383
|
-
|
|
11384
|
-
|
|
11385
|
-
|
|
11963
|
+
const host = hostname2().trim();
|
|
11964
|
+
if (host)
|
|
11965
|
+
return host.slice(0, 64);
|
|
11966
|
+
} catch {}
|
|
11967
|
+
return "ghostdock-cli";
|
|
11968
|
+
}
|
|
11969
|
+
var DEFAULT_TIMEOUT_SECONDS = 300;
|
|
11970
|
+
function toCredentials(result) {
|
|
11971
|
+
return {
|
|
11972
|
+
accessToken: result.accessToken,
|
|
11973
|
+
refreshToken: result.refreshToken,
|
|
11974
|
+
accessExpiresAt: new Date(Date.now() + result.expiresIn * 1000).toISOString()
|
|
11975
|
+
};
|
|
11976
|
+
}
|
|
11977
|
+
function sleep(ms) {
|
|
11978
|
+
return new Promise((resolve) => setTimeout(resolve, ms));
|
|
11979
|
+
}
|
|
11980
|
+
async function runLogin(args, log = console.log, choose = defaultChooser(), error = printError) {
|
|
11981
|
+
const loggedIn = async () => {
|
|
11982
|
+
log(eventLine("ready", "Logged in."));
|
|
11983
|
+
return chooseDefaultOrganizationAfterLogin(args.org, log, choose);
|
|
11984
|
+
};
|
|
11985
|
+
if (args.code) {
|
|
11986
|
+
const result = await deviceToken({ pasteCode: args.code });
|
|
11987
|
+
if (result.status === "approved") {
|
|
11988
|
+
await writeCredentials(toCredentials(result));
|
|
11989
|
+
return loggedIn();
|
|
11990
|
+
}
|
|
11991
|
+
error("That code has not been approved yet. Approve it in the browser, then run this again.");
|
|
11386
11992
|
return 1;
|
|
11387
11993
|
}
|
|
11994
|
+
const start = await deviceStart({ client_name: defaultClientName() });
|
|
11995
|
+
log(eventLine("url", `Open this URL to sign in: ${start.verificationUrl}`));
|
|
11996
|
+
log(` Pass --wait to finish here once you approve, or approve on another device and run \`${getCliName()} login --code <paste-code>\` with the code shown there.`);
|
|
11997
|
+
if (!args.wait) {
|
|
11998
|
+
return 0;
|
|
11999
|
+
}
|
|
12000
|
+
const timeoutSeconds = Number.parseInt(args.timeout ?? "", 10) || DEFAULT_TIMEOUT_SECONDS;
|
|
12001
|
+
const deadline = Date.now() + timeoutSeconds * 1000;
|
|
12002
|
+
const intervalMs = start.interval * 1000;
|
|
12003
|
+
const progress = startProgress("Waiting for approval in the browser…");
|
|
12004
|
+
while (Date.now() < deadline) {
|
|
12005
|
+
await sleep(intervalMs);
|
|
12006
|
+
try {
|
|
12007
|
+
const result = await deviceToken({ deviceCode: start.deviceCode });
|
|
12008
|
+
if (result.status === "approved") {
|
|
12009
|
+
progress.stop();
|
|
12010
|
+
await writeCredentials(toCredentials(result));
|
|
12011
|
+
return loggedIn();
|
|
12012
|
+
}
|
|
12013
|
+
} catch (err) {
|
|
12014
|
+
progress.stop();
|
|
12015
|
+
if (err instanceof ApiError && err.status === 400) {
|
|
12016
|
+
error(`Login failed: ${err.message}`);
|
|
12017
|
+
return 1;
|
|
12018
|
+
}
|
|
12019
|
+
throw err;
|
|
12020
|
+
}
|
|
12021
|
+
}
|
|
12022
|
+
progress.stop();
|
|
12023
|
+
error(`Timed out waiting for approval. Run \`${getCliName()} login\` again.`);
|
|
12024
|
+
return 1;
|
|
11388
12025
|
}
|
|
11389
|
-
var
|
|
12026
|
+
var loginCommand = defineCommand({
|
|
11390
12027
|
meta: {
|
|
11391
|
-
name: "
|
|
11392
|
-
description: "
|
|
12028
|
+
name: "login",
|
|
12029
|
+
description: "Sign in and store a device access token for this machine (~/.config/ghostdock/credentials.json)."
|
|
11393
12030
|
},
|
|
11394
12031
|
args: {
|
|
11395
|
-
|
|
11396
|
-
type: "
|
|
11397
|
-
|
|
11398
|
-
|
|
12032
|
+
wait: {
|
|
12033
|
+
type: "boolean",
|
|
12034
|
+
description: "Poll until the login is approved in the browser (default timeout 5 minutes).",
|
|
12035
|
+
default: false
|
|
11399
12036
|
},
|
|
11400
|
-
|
|
12037
|
+
code: {
|
|
12038
|
+
type: "string",
|
|
12039
|
+
description: "Redeem the one-time paste code shown in the browser instead of polling."
|
|
12040
|
+
},
|
|
12041
|
+
timeout: {
|
|
12042
|
+
type: "string",
|
|
12043
|
+
description: "Max seconds to poll when using --wait (default 300)."
|
|
12044
|
+
},
|
|
12045
|
+
org: {
|
|
12046
|
+
type: "string",
|
|
12047
|
+
description: "Set this organization (id or slug) as the default once logged in."
|
|
12048
|
+
}
|
|
11401
12049
|
},
|
|
11402
12050
|
async run({ args }) {
|
|
11403
|
-
process.exitCode = await
|
|
12051
|
+
process.exitCode = await runLogin(args);
|
|
11404
12052
|
}
|
|
11405
12053
|
});
|
|
11406
|
-
|
|
11407
|
-
|
|
11408
|
-
|
|
11409
|
-
|
|
11410
|
-
|
|
11411
|
-
|
|
11412
|
-
|
|
12054
|
+
|
|
12055
|
+
// src/commands/logout.ts
|
|
12056
|
+
async function runLogout(log = console.log) {
|
|
12057
|
+
const credentials = await readCredentials();
|
|
12058
|
+
if (credentials) {
|
|
12059
|
+
try {
|
|
12060
|
+
await deviceRevoke({
|
|
12061
|
+
accessToken: credentials.accessToken,
|
|
12062
|
+
refreshToken: credentials.refreshToken
|
|
12063
|
+
});
|
|
12064
|
+
} catch {}
|
|
11413
12065
|
}
|
|
11414
|
-
|
|
11415
|
-
|
|
12066
|
+
await clearCredentials();
|
|
12067
|
+
log(eventLine("stopped", "Logged out."));
|
|
12068
|
+
return 0;
|
|
12069
|
+
}
|
|
12070
|
+
var logoutCommand = defineCommand({
|
|
11416
12071
|
meta: {
|
|
11417
|
-
name: "
|
|
11418
|
-
description: "
|
|
12072
|
+
name: "logout",
|
|
12073
|
+
description: "Revoke the device session and remove stored credentials."
|
|
11419
12074
|
},
|
|
11420
12075
|
async run() {
|
|
11421
|
-
process.exitCode = await
|
|
11422
|
-
}
|
|
11423
|
-
});
|
|
11424
|
-
var lsCommand = defineCommand({
|
|
11425
|
-
meta: {
|
|
11426
|
-
name: "ls",
|
|
11427
|
-
description: "Sandboxes of an organization with their private addresses and names, and which are reachable through the current connection."
|
|
11428
|
-
},
|
|
11429
|
-
args: { org: orgArg },
|
|
11430
|
-
async run({ args }) {
|
|
11431
|
-
process.exitCode = await runNetworkLs({ org: args.org });
|
|
11432
|
-
}
|
|
11433
|
-
});
|
|
11434
|
-
var devicesRemoveCommand = defineCommand({
|
|
11435
|
-
meta: { name: "remove", description: "Remove one of your devices from the private network." },
|
|
11436
|
-
args: {
|
|
11437
|
-
id: { type: "positional", description: "Device id, from `network devices`.", required: true }
|
|
11438
|
-
},
|
|
11439
|
-
async run({ args }) {
|
|
11440
|
-
process.exitCode = await runNetworkDevicesRemove({ id: args.id });
|
|
11441
|
-
}
|
|
11442
|
-
});
|
|
11443
|
-
var devicesCommand = defineCommand({
|
|
11444
|
-
meta: { name: "devices", description: "Your devices in the private network." },
|
|
11445
|
-
subCommands: { remove: devicesRemoveCommand },
|
|
11446
|
-
async run({ rawArgs }) {
|
|
11447
|
-
if (rawArgs[0] === "remove")
|
|
11448
|
-
return;
|
|
11449
|
-
process.exitCode = await runNetworkDevices();
|
|
11450
|
-
}
|
|
11451
|
-
});
|
|
11452
|
-
var networkCommand = defineCommand({
|
|
11453
|
-
meta: {
|
|
11454
|
-
name: "network",
|
|
11455
|
-
description: "The private network: reach your sandboxes by address and name from this machine."
|
|
11456
|
-
},
|
|
11457
|
-
subCommands: {
|
|
11458
|
-
connect: connectCommand,
|
|
11459
|
-
disconnect: disconnectCommand,
|
|
11460
|
-
status: statusCommand,
|
|
11461
|
-
ls: lsCommand,
|
|
11462
|
-
devices: devicesCommand
|
|
12076
|
+
process.exitCode = await runLogout();
|
|
11463
12077
|
}
|
|
11464
12078
|
});
|
|
11465
12079
|
|
|
@@ -11538,8 +12152,8 @@ var organizationsCurrentCommand = defineCommand({
|
|
|
11538
12152
|
});
|
|
11539
12153
|
|
|
11540
12154
|
// src/commands/publish-auto.ts
|
|
11541
|
-
import { readdir, readFile as
|
|
11542
|
-
import { basename as basename2, join as
|
|
12155
|
+
import { readdir as readdir2, readFile as readFile5, stat } from "node:fs/promises";
|
|
12156
|
+
import { basename as basename2, join as join5, relative, sep } from "node:path";
|
|
11543
12157
|
function publishAutoLines(result) {
|
|
11544
12158
|
const [summaryLine, ...rest] = formatPublishAutoResult(result).split(`
|
|
11545
12159
|
`);
|
|
@@ -11616,23 +12230,23 @@ function formatAttemptedSpec(spec) {
|
|
|
11616
12230
|
async function collectPath(path) {
|
|
11617
12231
|
const pathStats = await stat(path);
|
|
11618
12232
|
if (pathStats.isFile()) {
|
|
11619
|
-
return [{ path: basename2(path), data: await
|
|
12233
|
+
return [{ path: basename2(path), data: await readFile5(path) }];
|
|
11620
12234
|
}
|
|
11621
12235
|
if (!pathStats.isDirectory()) {
|
|
11622
12236
|
throw new Error(`Publish path must be a file or directory: ${path}`);
|
|
11623
12237
|
}
|
|
11624
12238
|
const files = [];
|
|
11625
12239
|
async function visit(currentPath) {
|
|
11626
|
-
const entries = await
|
|
12240
|
+
const entries = await readdir2(currentPath, { withFileTypes: true });
|
|
11627
12241
|
entries.sort((a, b) => a.name.localeCompare(b.name));
|
|
11628
12242
|
for (const entry of entries) {
|
|
11629
|
-
const entryPath =
|
|
12243
|
+
const entryPath = join5(currentPath, entry.name);
|
|
11630
12244
|
if (entry.isDirectory()) {
|
|
11631
12245
|
await visit(entryPath);
|
|
11632
12246
|
} else if (entry.isFile()) {
|
|
11633
12247
|
files.push({
|
|
11634
12248
|
path: relative(path, entryPath).split(sep).join("/"),
|
|
11635
|
-
data: await
|
|
12249
|
+
data: await readFile5(entryPath)
|
|
11636
12250
|
});
|
|
11637
12251
|
}
|
|
11638
12252
|
}
|
|
@@ -11663,7 +12277,7 @@ async function runPublishAuto(options, log = console.log, error = printError) {
|
|
|
11663
12277
|
form.append("displayName", options.displayName);
|
|
11664
12278
|
appendOptional(form, "tag", options.tag);
|
|
11665
12279
|
appendOptional(form, "autoStopIntervalMinutes", options.autoStopIntervalMinutes);
|
|
11666
|
-
appendOptional(form, "
|
|
12280
|
+
appendOptional(form, "autoDeleteAfterDays", options.autoDeleteAfterDays);
|
|
11667
12281
|
appendOptional(form, "resources", options.resources);
|
|
11668
12282
|
appendOptional(form, "environment", options.environment);
|
|
11669
12283
|
appendOptional(form, "autoWake", options.autoWake);
|
|
@@ -11675,7 +12289,7 @@ async function runPublishAuto(options, log = console.log, error = printError) {
|
|
|
11675
12289
|
}
|
|
11676
12290
|
}
|
|
11677
12291
|
if (options.zip) {
|
|
11678
|
-
form.append("zip", new Blob([await
|
|
12292
|
+
form.append("zip", new Blob([await readFile5(options.zip)]), basename2(options.zip));
|
|
11679
12293
|
}
|
|
11680
12294
|
const { value: result } = await withProgress("Planning and deploying… (this can take a few minutes)", () => authorizedRequest(`/api/v1/organizations/${encodeURIComponent(options.organizationId)}/publish-auto`, { method: "POST", body: form }));
|
|
11681
12295
|
printResult(result, () => publishAutoLines(result), log);
|
|
@@ -11733,9 +12347,9 @@ var publishAutoCommand = defineCommand({
|
|
|
11733
12347
|
type: "string",
|
|
11734
12348
|
description: "Same as `sandboxes create`. Omit for the 20-minute default. Pass -1 to keep it running. 0 is rejected."
|
|
11735
12349
|
},
|
|
11736
|
-
"auto-delete-
|
|
12350
|
+
"auto-delete-after-days": {
|
|
11737
12351
|
type: "string",
|
|
11738
|
-
description: "Same as `sandboxes create
|
|
12352
|
+
description: "Same as `sandboxes create`: days stopped before the record is deleted (0–365). Omit for the 14-day default. Pass 0 to delete the record when it stops."
|
|
11739
12353
|
},
|
|
11740
12354
|
resources: {
|
|
11741
12355
|
type: "string",
|
|
@@ -11785,7 +12399,7 @@ var publishAutoCommand = defineCommand({
|
|
|
11785
12399
|
displayName: args["display-name"],
|
|
11786
12400
|
tag: args.tag,
|
|
11787
12401
|
autoStopIntervalMinutes: args["auto-stop-interval-minutes"],
|
|
11788
|
-
|
|
12402
|
+
autoDeleteAfterDays: args["auto-delete-after-days"],
|
|
11789
12403
|
resources: args.resources,
|
|
11790
12404
|
environment: args.environment,
|
|
11791
12405
|
autoWake: args["auto-wake"],
|
|
@@ -11798,28 +12412,28 @@ var publishAutoCommand = defineCommand({
|
|
|
11798
12412
|
});
|
|
11799
12413
|
|
|
11800
12414
|
// src/commands/publish-static.ts
|
|
11801
|
-
import { readdir as
|
|
11802
|
-
import { basename as basename3, join as
|
|
12415
|
+
import { readdir as readdir3, readFile as readFile6, stat as stat2 } from "node:fs/promises";
|
|
12416
|
+
import { basename as basename3, join as join6, relative as relative2, sep as sep2 } from "node:path";
|
|
11803
12417
|
async function collectPath2(path) {
|
|
11804
12418
|
const pathStats = await stat2(path);
|
|
11805
12419
|
if (pathStats.isFile()) {
|
|
11806
|
-
return [{ path: basename3(path), data: await
|
|
12420
|
+
return [{ path: basename3(path), data: await readFile6(path) }];
|
|
11807
12421
|
}
|
|
11808
12422
|
if (!pathStats.isDirectory()) {
|
|
11809
12423
|
throw new Error(`Publish path must be a file or directory: ${path}`);
|
|
11810
12424
|
}
|
|
11811
12425
|
const files = [];
|
|
11812
12426
|
async function visit(currentPath) {
|
|
11813
|
-
const entries = await
|
|
12427
|
+
const entries = await readdir3(currentPath, { withFileTypes: true });
|
|
11814
12428
|
entries.sort((a, b) => a.name.localeCompare(b.name));
|
|
11815
12429
|
for (const entry of entries) {
|
|
11816
|
-
const entryPath =
|
|
12430
|
+
const entryPath = join6(currentPath, entry.name);
|
|
11817
12431
|
if (entry.isDirectory()) {
|
|
11818
12432
|
await visit(entryPath);
|
|
11819
12433
|
} else if (entry.isFile()) {
|
|
11820
12434
|
files.push({
|
|
11821
12435
|
path: relative2(path, entryPath).split(sep2).join("/"),
|
|
11822
|
-
data: await
|
|
12436
|
+
data: await readFile6(entryPath)
|
|
11823
12437
|
});
|
|
11824
12438
|
}
|
|
11825
12439
|
}
|
|
@@ -11842,7 +12456,7 @@ async function runPublishStatic(options, log = console.log, error = printError)
|
|
|
11842
12456
|
}
|
|
11843
12457
|
}
|
|
11844
12458
|
if (options.zip) {
|
|
11845
|
-
form.append("zip", new Blob([await
|
|
12459
|
+
form.append("zip", new Blob([await readFile6(options.zip)]), basename3(options.zip));
|
|
11846
12460
|
}
|
|
11847
12461
|
const { value: result } = await withProgress("Publishing…", () => authorizedRequest(`/api/v1/organizations/${encodeURIComponent(options.organizationId)}/static-sites`, { method: "POST", body: form }));
|
|
11848
12462
|
printResult(result, () => [eventLine("url", result.url), eventLine("done", `Published site ${result.siteId}.`)], log);
|
|
@@ -11896,7 +12510,7 @@ var publishStaticCommand = defineCommand({
|
|
|
11896
12510
|
});
|
|
11897
12511
|
|
|
11898
12512
|
// src/commands/registry.ts
|
|
11899
|
-
import { spawn as
|
|
12513
|
+
import { spawn as spawn3 } from "node:child_process";
|
|
11900
12514
|
function reportError2(err, error) {
|
|
11901
12515
|
if (err instanceof AuthRequiredError || err instanceof Error) {
|
|
11902
12516
|
error(err.message);
|
|
@@ -11913,7 +12527,7 @@ function defaultInvoke(input) {
|
|
|
11913
12527
|
}
|
|
11914
12528
|
function dockerLogin(input) {
|
|
11915
12529
|
return new Promise((resolve, reject) => {
|
|
11916
|
-
const child =
|
|
12530
|
+
const child = spawn3("docker", ["login", input.host, "-u", input.username, "--password-stdin"], {
|
|
11917
12531
|
stdio: ["pipe", "inherit", "pipe"]
|
|
11918
12532
|
});
|
|
11919
12533
|
let stderr = "";
|
|
@@ -12011,8 +12625,8 @@ var registryCommand = defineCommand({
|
|
|
12011
12625
|
});
|
|
12012
12626
|
|
|
12013
12627
|
// src/commands/sandbox-create.ts
|
|
12014
|
-
import { readdir as
|
|
12015
|
-
import { basename as basename4, join as
|
|
12628
|
+
import { readdir as readdir4, readFile as readFile7, stat as stat3 } from "node:fs/promises";
|
|
12629
|
+
import { basename as basename4, join as join7, relative as relative3, sep as sep3 } from "node:path";
|
|
12016
12630
|
|
|
12017
12631
|
// ../../node_modules/fflate/esm/index.mjs
|
|
12018
12632
|
import { createRequire } from "module";
|
|
@@ -12665,16 +13279,16 @@ async function collectDirectory(path) {
|
|
|
12665
13279
|
}
|
|
12666
13280
|
const files = [];
|
|
12667
13281
|
async function visit(currentPath) {
|
|
12668
|
-
const entries = await
|
|
13282
|
+
const entries = await readdir4(currentPath, { withFileTypes: true });
|
|
12669
13283
|
entries.sort((a, b) => a.name.localeCompare(b.name));
|
|
12670
13284
|
for (const entry of entries) {
|
|
12671
|
-
const entryPath =
|
|
13285
|
+
const entryPath = join7(currentPath, entry.name);
|
|
12672
13286
|
if (entry.isDirectory()) {
|
|
12673
13287
|
await visit(entryPath);
|
|
12674
13288
|
} else if (entry.isFile()) {
|
|
12675
13289
|
files.push({
|
|
12676
13290
|
path: relative3(path, entryPath).split(sep3).join("/"),
|
|
12677
|
-
data: await
|
|
13291
|
+
data: await readFile7(entryPath)
|
|
12678
13292
|
});
|
|
12679
13293
|
}
|
|
12680
13294
|
}
|
|
@@ -12707,7 +13321,7 @@ async function runSandboxCreate(options, log = console.log, error = printError)
|
|
|
12707
13321
|
return 1;
|
|
12708
13322
|
}
|
|
12709
13323
|
try {
|
|
12710
|
-
const zipBytes = options.zip ? new Uint8Array(await
|
|
13324
|
+
const zipBytes = options.zip ? new Uint8Array(await readFile7(options.zip)) : zipDirectory(await collectDirectory(options.paths[0] ?? ""));
|
|
12711
13325
|
const form = new FormData;
|
|
12712
13326
|
form.append("kind", options.kind);
|
|
12713
13327
|
if (options.displayName)
|
|
@@ -12968,28 +13582,28 @@ var toolsCommand = defineCommand({
|
|
|
12968
13582
|
});
|
|
12969
13583
|
|
|
12970
13584
|
// src/commands/upload-files.ts
|
|
12971
|
-
import { readdir as
|
|
12972
|
-
import { basename as basename5, join as
|
|
13585
|
+
import { readdir as readdir5, readFile as readFile8, stat as stat4 } from "node:fs/promises";
|
|
13586
|
+
import { basename as basename5, join as join8, relative as relative4, sep as sep4 } from "node:path";
|
|
12973
13587
|
async function collectPath3(path) {
|
|
12974
13588
|
const pathStats = await stat4(path);
|
|
12975
13589
|
if (pathStats.isFile()) {
|
|
12976
|
-
return [{ path: basename5(path), data: await
|
|
13590
|
+
return [{ path: basename5(path), data: await readFile8(path) }];
|
|
12977
13591
|
}
|
|
12978
13592
|
if (!pathStats.isDirectory()) {
|
|
12979
13593
|
throw new Error(`Upload path must be a file or directory: ${path}`);
|
|
12980
13594
|
}
|
|
12981
13595
|
const files = [];
|
|
12982
13596
|
async function visit(currentPath) {
|
|
12983
|
-
const entries = await
|
|
13597
|
+
const entries = await readdir5(currentPath, { withFileTypes: true });
|
|
12984
13598
|
entries.sort((a, b) => a.name.localeCompare(b.name));
|
|
12985
13599
|
for (const entry of entries) {
|
|
12986
|
-
const entryPath =
|
|
13600
|
+
const entryPath = join8(currentPath, entry.name);
|
|
12987
13601
|
if (entry.isDirectory()) {
|
|
12988
13602
|
await visit(entryPath);
|
|
12989
13603
|
} else if (entry.isFile()) {
|
|
12990
13604
|
files.push({
|
|
12991
13605
|
path: relative4(path, entryPath).split(sep4).join("/"),
|
|
12992
|
-
data: await
|
|
13606
|
+
data: await readFile8(entryPath)
|
|
12993
13607
|
});
|
|
12994
13608
|
}
|
|
12995
13609
|
}
|
|
@@ -13011,7 +13625,7 @@ async function runSandboxUpload(options, log = console.log, error = printError,
|
|
|
13011
13625
|
}
|
|
13012
13626
|
}
|
|
13013
13627
|
if (options.zip) {
|
|
13014
|
-
form.append("zip", new Blob([await
|
|
13628
|
+
form.append("zip", new Blob([await readFile8(options.zip)]), basename5(options.zip));
|
|
13015
13629
|
}
|
|
13016
13630
|
let container = options.container;
|
|
13017
13631
|
const call = {
|
|
@@ -13032,7 +13646,7 @@ async function runSandboxUpload(options, log = console.log, error = printError,
|
|
|
13032
13646
|
const result = await withContainerPick(call, () => withContainerStart(call, send, options.start === true, pickers, notice), pickers, notice);
|
|
13033
13647
|
const count = `${result.written.length} file${result.written.length === 1 ? "" : "s"}`;
|
|
13034
13648
|
printResult(result, () => [
|
|
13035
|
-
eventLine("done", result.container ? `Uploaded ${count} to ${result.
|
|
13649
|
+
eventLine("done", result.container ? `Uploaded ${count} to ${result.root} in ${result.container} (${options.sandboxId}).` : `Uploaded ${count} to ${result.root} in ${options.sandboxId}.`)
|
|
13036
13650
|
], log);
|
|
13037
13651
|
return 0;
|
|
13038
13652
|
} catch (err) {
|
|
@@ -13047,7 +13661,7 @@ async function runSandboxUpload(options, log = console.log, error = printError,
|
|
|
13047
13661
|
var sandboxUploadCommand = defineCommand({
|
|
13048
13662
|
meta: {
|
|
13049
13663
|
name: "upload",
|
|
13050
|
-
description: "Upload local files or directories to a sandbox
|
|
13664
|
+
description: "Upload local files or directories to a sandbox's container, into its start folder (the login user's home, else WORKDIR, else /)."
|
|
13051
13665
|
},
|
|
13052
13666
|
args: {
|
|
13053
13667
|
"sandbox-id": {
|
|
@@ -13153,7 +13767,8 @@ var HANDWRITTEN_COMMANDS = [
|
|
|
13153
13767
|
[["sandboxes", "files", "upload"], sandboxUploadCommand],
|
|
13154
13768
|
[["sites", "publish"], publishStaticCommand],
|
|
13155
13769
|
[["projects", "publish"], publishAutoCommand],
|
|
13156
|
-
[["network"], networkCommand]
|
|
13770
|
+
[["network"], networkCommand],
|
|
13771
|
+
[["docker"], dockerCommand]
|
|
13157
13772
|
];
|
|
13158
13773
|
var HANDWRITTEN_PATHS = new Set(HANDWRITTEN_COMMANDS.map(([path]) => path.join(" ")));
|
|
13159
13774
|
function createMainCommand(catalog = OFFLINE_TOOL_CATALOG) {
|