@go-labs-sg/gosh 0.10.1 → 0.12.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +55 -0
- package/command-manifest.json +12 -0
- package/command-reference.md +12 -0
- package/dist/index.js +599 -146
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -2,6 +2,20 @@
|
|
|
2
2
|
|
|
3
3
|
`@go-labs-sg/gosh` provides the `gosh` binary for permission-aware GOSH organizer operations and agent automation. It calls the same tRPC procedures as the web app using the authorizing user's global, organization, and event permissions.
|
|
4
4
|
|
|
5
|
+
## First step: update before use
|
|
6
|
+
|
|
7
|
+
Before starting a task, **update to the latest published release first**, including when an AI agent runs this CLI:
|
|
8
|
+
|
|
9
|
+
```bash
|
|
10
|
+
bun add --global @go-labs-sg/gosh@latest
|
|
11
|
+
gosh version
|
|
12
|
+
gosh --help
|
|
13
|
+
```
|
|
14
|
+
|
|
15
|
+
Run this sequence once per task, not before every command. Re-read the updated help before choosing commands or flags. If updating fails, stop and report the failure rather than silently using an outdated CLI. If the user explicitly requested a pinned version or a development/staging build, preserve that version instead of replacing it with the public release.
|
|
16
|
+
|
|
17
|
+
New installation? Follow the runtime and installation instructions below to install Bun first, then return to this step.
|
|
18
|
+
|
|
5
19
|
## Runtime and installation
|
|
6
20
|
|
|
7
21
|
The CLI requires [Bun](https://bun.com/docs/installation) 1.4.0 or newer. npm remains the package distribution channel, while the installed `gosh` command runs under Bun.
|
|
@@ -41,6 +55,16 @@ bun add --global @go-labs-sg/gosh
|
|
|
41
55
|
gosh version
|
|
42
56
|
```
|
|
43
57
|
|
|
58
|
+
## Secure credential storage
|
|
59
|
+
|
|
60
|
+
`GO_LABS_CLI_CREDENTIAL_ACCOUNT` selects the native credential-store account for this CLI. The default is `default`, preserving existing production logins. The downloadable staging launcher sets it to `staging` so a staging login does not replace the production login; keep the same account value for login and subsequent commands. The launcher also sets `GO_LABS_CLI_SKIP_DOTENV=1`, which skips the CLI's explicit current-directory `.env` load while leaving normal production behavior unchanged.
|
|
61
|
+
|
|
62
|
+
Browser sessions use `Bun.secrets`: macOS Keychain, Windows Credential Manager, or Linux Secret Service. Linux requires libsecret, a reachable user D-Bus session, and an unlocked Secret Service collection. SSH and containers must provide those native prerequisites themselves; `--no-browser` only lets approval happen on another device. Denied D-Bus access is a host permissions/session issue, not something the CLI bypasses.
|
|
63
|
+
|
|
64
|
+
Before requesting approval, login verifies write/read/delete capability with a unique non-secret probe. Native operations have a 10-second process deadline and sanitized errors. Issued credentials are read back and verified; failed persistence attempts remote revocation and reports local and remote cleanup separately. There is no plaintext fallback or external credential-helper configuration.
|
|
65
|
+
|
|
66
|
+
See the [shared authentication contract](../../cli-auth/README.md) for recovery and verification limits.
|
|
67
|
+
|
|
44
68
|
## Authentication
|
|
45
69
|
|
|
46
70
|
```bash
|
|
@@ -227,6 +251,37 @@ measures mission work only: travel between stations and staff review time are
|
|
|
227
251
|
excluded. Existing leaderboard and results commands include the resulting
|
|
228
252
|
timing information.
|
|
229
253
|
|
|
254
|
+
### Artwork requirements (no login required)
|
|
255
|
+
|
|
256
|
+
Read the bundled upload policy before preparing station, mission, or branding artwork:
|
|
257
|
+
|
|
258
|
+
```bash
|
|
259
|
+
gosh assets requirements --type station
|
|
260
|
+
gosh assets requirements --type mission --format text
|
|
261
|
+
gosh assets requirements
|
|
262
|
+
```
|
|
263
|
+
|
|
264
|
+
The default output is the standard JSON envelope; `--format text` gives a readable brief.
|
|
265
|
+
Omitting `--type` lists every supported artwork type. `gosh commands`,
|
|
266
|
+
`gosh help assets requirements`, and the upload-command help expose this guidance too.
|
|
267
|
+
This command is local and read-only: it does not access credentials, contact the API,
|
|
268
|
+
or upload a file. API URL configuration is not used or validated for this command or
|
|
269
|
+
its help. Its policy comes from the installed CLI version, not a live deployment.
|
|
270
|
+
Update the CLI when newer policy definitions are published.
|
|
271
|
+
|
|
272
|
+
- Station and mission setup images: JPEG, PNG, WebP; at most **10 MiB (10,485,760 bytes)**.
|
|
273
|
+
- Branding images (logo, sponsor-logo, login-banner, map-marker, player-background):
|
|
274
|
+
the same formats; at most **5 MiB (5,242,880 bytes)**.
|
|
275
|
+
- Neither artwork upload policy enforces pixel dimensions or aspect ratio.
|
|
276
|
+
- **Recommendations, not requirements:** station artwork 1920 × 1080 (16:9),
|
|
277
|
+
mission artwork 1600 × 1000 (16:10). Participant cover containers can crop artwork;
|
|
278
|
+
the standard station card also becomes shorter on completion. Keep essential
|
|
279
|
+
text in station/mission text fields and important image content away from the edges.
|
|
280
|
+
- Logos, login banners, map markers, and player backgrounds have separate display
|
|
281
|
+
guidance; no exact canvas is prescribed for them. Preview on the relevant mobile surface.
|
|
282
|
+
|
|
283
|
+
These policies describe organizer artwork, not participant submission media or profile images.
|
|
284
|
+
|
|
230
285
|
### Local file uploads
|
|
231
286
|
|
|
232
287
|
Upload commands add the local file metadata to the supplied API input, request a permission-scoped signed URL, and upload the bytes directly to storage without sending the GOSH CLI access token to storage:
|
package/command-manifest.json
CHANGED
|
@@ -2947,5 +2947,17 @@
|
|
|
2947
2947
|
"slug",
|
|
2948
2948
|
"name"
|
|
2949
2949
|
]
|
|
2950
|
+
},
|
|
2951
|
+
{
|
|
2952
|
+
"path": ["assets", "requirements"],
|
|
2953
|
+
"operation": "local",
|
|
2954
|
+
"input": "none",
|
|
2955
|
+
"requiresAuth": false,
|
|
2956
|
+
"effects": [],
|
|
2957
|
+
"summary": "Show upload limits and separate artwork recommendations without login or network access.",
|
|
2958
|
+
"options": {
|
|
2959
|
+
"--type": "Select station, mission, logo, sponsor-logo, login-banner, map-marker, player-background; omit for all asset types.",
|
|
2960
|
+
"--format": "json (default, standard CLI envelope) or text."
|
|
2961
|
+
}
|
|
2950
2962
|
}
|
|
2951
2963
|
]
|
package/command-reference.md
CHANGED
|
@@ -953,6 +953,7 @@ Upload an event branding asset from a local file.
|
|
|
953
953
|
- Authentication: browser-authenticated CLI session required
|
|
954
954
|
- Input: `--input '<json>'` required
|
|
955
955
|
- Effects: state-change, external-write
|
|
956
|
+
- Artwork guidance: `gosh assets requirements` (bundled policy, no login required).
|
|
956
957
|
|
|
957
958
|
## `gosh upload setup-image`
|
|
958
959
|
|
|
@@ -961,6 +962,7 @@ Upload a station or mission setup image from a local file.
|
|
|
961
962
|
- Authentication: browser-authenticated CLI session required
|
|
962
963
|
- Input: `--input '<json>'` required
|
|
963
964
|
- Effects: state-change, external-write
|
|
965
|
+
- Artwork guidance: `gosh assets requirements` (bundled policy, no login required).
|
|
964
966
|
|
|
965
967
|
## `gosh upload profile-image`
|
|
966
968
|
|
|
@@ -1153,3 +1155,13 @@ Unban a user.
|
|
|
1153
1155
|
- Authentication: browser-authenticated CLI session required
|
|
1154
1156
|
- Input: `--input '<json>'` required
|
|
1155
1157
|
- Effects: state-change
|
|
1158
|
+
|
|
1159
|
+
## `gosh assets requirements`
|
|
1160
|
+
|
|
1161
|
+
Show upload limits and separate artwork recommendations without login or network access.
|
|
1162
|
+
|
|
1163
|
+
- Authentication: not required
|
|
1164
|
+
- Input: none
|
|
1165
|
+
- Effects: read-only
|
|
1166
|
+
- --type: Select station, mission, logo, sponsor-logo, login-banner, map-marker, player-background; omit for all asset types.
|
|
1167
|
+
- --format: json (default, standard CLI envelope) or text.
|
package/dist/index.js
CHANGED
|
@@ -468,7 +468,9 @@ var require_dist = __commonJS(function(exports, module) {
|
|
|
468
468
|
// src/load-env.ts
|
|
469
469
|
var import_dotenv = __toESM(require_dist(), 1);
|
|
470
470
|
import path from "path";
|
|
471
|
-
|
|
471
|
+
if (process.env.GO_LABS_CLI_SKIP_DOTENV !== "1") {
|
|
472
|
+
import_dotenv.config({ path: path.resolve(process.cwd(), ".env"), quiet: true });
|
|
473
|
+
}
|
|
472
474
|
|
|
473
475
|
// src/cli.ts
|
|
474
476
|
import { readFileSync } from "fs";
|
|
@@ -1486,6 +1488,8 @@ function datetime(args) {
|
|
|
1486
1488
|
}
|
|
1487
1489
|
var anyString = /^[\s\S]{0,}$/;
|
|
1488
1490
|
var number = /^-?\d+(?:\.\d+)?$/;
|
|
1491
|
+
var boolean = /^(?:true|false)$/i;
|
|
1492
|
+
var _null = /^null$/i;
|
|
1489
1493
|
var lowercase = /^[^A-Z]*$/;
|
|
1490
1494
|
var uppercase = /^[^a-z]*$/;
|
|
1491
1495
|
|
|
@@ -2367,6 +2371,43 @@ var $ZodNumberFormat = /* @__PURE__ */ $constructor("$ZodNumberFormat", (inst, d
|
|
|
2367
2371
|
$ZodCheckNumberFormat.init(inst, def);
|
|
2368
2372
|
$ZodNumber.init(inst, def);
|
|
2369
2373
|
});
|
|
2374
|
+
var $ZodBoolean = /* @__PURE__ */ $constructor("$ZodBoolean", (inst, def) => {
|
|
2375
|
+
$ZodType.init(inst, def);
|
|
2376
|
+
inst._zod.pattern = boolean;
|
|
2377
|
+
inst._zod.parse = (payload, _ctx) => {
|
|
2378
|
+
if (def.coerce)
|
|
2379
|
+
try {
|
|
2380
|
+
payload.value = Boolean(payload.value);
|
|
2381
|
+
} catch (_) {}
|
|
2382
|
+
const input = payload.value;
|
|
2383
|
+
if (typeof input === "boolean")
|
|
2384
|
+
return payload;
|
|
2385
|
+
payload.issues.push({
|
|
2386
|
+
expected: "boolean",
|
|
2387
|
+
code: "invalid_type",
|
|
2388
|
+
input,
|
|
2389
|
+
inst
|
|
2390
|
+
});
|
|
2391
|
+
return payload;
|
|
2392
|
+
};
|
|
2393
|
+
});
|
|
2394
|
+
var $ZodNull = /* @__PURE__ */ $constructor("$ZodNull", (inst, def) => {
|
|
2395
|
+
$ZodType.init(inst, def);
|
|
2396
|
+
inst._zod.pattern = _null;
|
|
2397
|
+
inst._zod.values = new Set([null]);
|
|
2398
|
+
inst._zod.parse = (payload, _ctx) => {
|
|
2399
|
+
const input = payload.value;
|
|
2400
|
+
if (input === null)
|
|
2401
|
+
return payload;
|
|
2402
|
+
payload.issues.push({
|
|
2403
|
+
expected: "null",
|
|
2404
|
+
code: "invalid_type",
|
|
2405
|
+
input,
|
|
2406
|
+
inst
|
|
2407
|
+
});
|
|
2408
|
+
return payload;
|
|
2409
|
+
};
|
|
2410
|
+
});
|
|
2370
2411
|
var $ZodUnknown = /* @__PURE__ */ $constructor("$ZodUnknown", (inst, def) => {
|
|
2371
2412
|
$ZodType.init(inst, def);
|
|
2372
2413
|
inst._zod.parse = (payload) => payload;
|
|
@@ -3904,6 +3945,18 @@ function _int(Class, params) {
|
|
|
3904
3945
|
...normalizeParams(params)
|
|
3905
3946
|
});
|
|
3906
3947
|
}
|
|
3948
|
+
function _boolean(Class, params) {
|
|
3949
|
+
return new Class({
|
|
3950
|
+
type: "boolean",
|
|
3951
|
+
...normalizeParams(params)
|
|
3952
|
+
});
|
|
3953
|
+
}
|
|
3954
|
+
function _null2(Class, params) {
|
|
3955
|
+
return new Class({
|
|
3956
|
+
type: "null",
|
|
3957
|
+
...normalizeParams(params)
|
|
3958
|
+
});
|
|
3959
|
+
}
|
|
3907
3960
|
function _unknown(Class) {
|
|
3908
3961
|
return new Class({
|
|
3909
3962
|
type: "unknown"
|
|
@@ -4786,6 +4839,18 @@ var numberProcessor = (schema, ctx, _json, params) => {
|
|
|
4786
4839
|
json.allOf = [...json.allOf ?? [], ...rest.map((m) => ({ multipleOf: m }))];
|
|
4787
4840
|
}
|
|
4788
4841
|
};
|
|
4842
|
+
var booleanProcessor = (_schema, _ctx, json, _params) => {
|
|
4843
|
+
json.type = "boolean";
|
|
4844
|
+
};
|
|
4845
|
+
var nullProcessor = (_schema, ctx, json, _params) => {
|
|
4846
|
+
if (ctx.target === "openapi-3.0") {
|
|
4847
|
+
json.type = "string";
|
|
4848
|
+
json.nullable = true;
|
|
4849
|
+
json.enum = [null];
|
|
4850
|
+
} else {
|
|
4851
|
+
json.type = "null";
|
|
4852
|
+
}
|
|
4853
|
+
};
|
|
4789
4854
|
var neverProcessor = (_schema, _ctx, json, _params) => {
|
|
4790
4855
|
json.not = {};
|
|
4791
4856
|
};
|
|
@@ -5580,6 +5645,22 @@ var ZodNumberFormat = /* @__PURE__ */ $constructor("ZodNumberFormat", (inst, def
|
|
|
5580
5645
|
function int(params) {
|
|
5581
5646
|
return _int(ZodNumberFormat, params);
|
|
5582
5647
|
}
|
|
5648
|
+
var ZodBoolean = /* @__PURE__ */ $constructor("ZodBoolean", (inst, def) => {
|
|
5649
|
+
$ZodBoolean.init(inst, def);
|
|
5650
|
+
ZodType.init(inst, def);
|
|
5651
|
+
inst._zod.processJSONSchema = (ctx, json, params) => booleanProcessor(inst, ctx, json, params);
|
|
5652
|
+
});
|
|
5653
|
+
function boolean2(params) {
|
|
5654
|
+
return _boolean(ZodBoolean, params);
|
|
5655
|
+
}
|
|
5656
|
+
var ZodNull = /* @__PURE__ */ $constructor("ZodNull", (inst, def) => {
|
|
5657
|
+
$ZodNull.init(inst, def);
|
|
5658
|
+
ZodType.init(inst, def);
|
|
5659
|
+
inst._zod.processJSONSchema = (ctx, json, params) => nullProcessor(inst, ctx, json, params);
|
|
5660
|
+
});
|
|
5661
|
+
function _null3(params) {
|
|
5662
|
+
return _null2(ZodNull, params);
|
|
5663
|
+
}
|
|
5583
5664
|
var ZodUnknown = /* @__PURE__ */ $constructor("ZodUnknown", (inst, def) => {
|
|
5584
5665
|
$ZodUnknown.init(inst, def);
|
|
5585
5666
|
ZodType.init(inst, def);
|
|
@@ -5998,6 +6079,9 @@ var storedCliCredentialsSchema = object({
|
|
|
5998
6079
|
session: cliSessionIdentitySchema
|
|
5999
6080
|
}).strict();
|
|
6000
6081
|
var cliProductSchema = _enum(cliProducts);
|
|
6082
|
+
// ../../cli-auth/src/client/secret-backend.ts
|
|
6083
|
+
import { execFile } from "child_process";
|
|
6084
|
+
|
|
6001
6085
|
// ../../cli-auth/src/client/runtime.ts
|
|
6002
6086
|
var MINIMUM_BUN_VERSION = [1, 4, 0];
|
|
6003
6087
|
var parseBunVersion = (version) => {
|
|
@@ -6023,27 +6107,145 @@ var assertBunRuntime = (runtime = globalThis.Bun) => {
|
|
|
6023
6107
|
}
|
|
6024
6108
|
};
|
|
6025
6109
|
|
|
6026
|
-
// ../../cli-auth/src/client/
|
|
6110
|
+
// ../../cli-auth/src/client/storage-error.ts
|
|
6111
|
+
var guidance = {
|
|
6112
|
+
"missing-library": "Install the runtime's native credential-store library and ensure your own native keyring/session is accessible and unlocked; Linux desktop storage requires libsecret AND an accessible, unlocked Secret Service.",
|
|
6113
|
+
"bus-unavailable": "The Linux user D-Bus session is absent or unreachable. --no-browser does not provide storage. Start or repair your own user session and native keyring before retrying.",
|
|
6114
|
+
"bus-denied": "Access to the Linux user bus was denied. Fix your own native keyring/session permissions or host policy; do not change bus/socket permissions or borrow another user's bus.",
|
|
6115
|
+
"service-unavailable": "The user bus is reachable but your own Secret Service is unavailable. Start or repair your own native keyring session; do not expose another user's store.",
|
|
6116
|
+
locked: "Unlock your own native credential collection/session before retrying. The CLI will not wait indefinitely for an unlock prompt.",
|
|
6117
|
+
timeout: "The native credential backend did not finish within 10 seconds and its process was terminated. Fix or unlock your own native keyring/session outside the CLI; the CLI will not bypass host permissions.",
|
|
6118
|
+
"invalid-response": "The native credential backend returned an invalid response. Check your own keyring/session and configuration; no credential output was disclosed.",
|
|
6119
|
+
"read-failed": "The native credential backend could not read credentials. Check your own keyring/session availability and permissions; this is not evidence that you are logged out.",
|
|
6120
|
+
"write-failed": "The native credential backend could not persist credentials. Check your own keyring/session write permissions and availability; no plaintext fallback is used.",
|
|
6121
|
+
"delete-failed": "The native credential backend could not delete its entry. Check your own keyring/session permissions and remove any non-secret preflight probe through that backend if necessary."
|
|
6122
|
+
};
|
|
6123
|
+
|
|
6027
6124
|
class CliCredentialStoreUnavailableError extends Error {
|
|
6028
6125
|
code = "CLI_CREDENTIAL_STORE_UNAVAILABLE";
|
|
6029
|
-
|
|
6030
|
-
|
|
6126
|
+
reason;
|
|
6127
|
+
constructor(product, reason = "read-failed") {
|
|
6128
|
+
super(`Secure credential storage is unavailable for the ${product} CLI [CLI_CREDENTIAL_STORE_UNAVAILABLE:${reason}]. ${guidance[reason]}`);
|
|
6031
6129
|
this.name = "CliCredentialStoreUnavailableError";
|
|
6032
|
-
|
|
6130
|
+
this.reason = reason;
|
|
6131
|
+
}
|
|
6132
|
+
}
|
|
6133
|
+
var classifyCredentialStoreFailure = (message, fallback) => {
|
|
6134
|
+
if (/libsecret.*(not found|not available|cannot open|load)|dlopen|shared (object|library).*not found/i.test(message))
|
|
6135
|
+
return "missing-library";
|
|
6136
|
+
if (/(d-?bus|user bus|session bus).*(denied|not permitted)|(?:denied|not permitted).*(d-?bus|user bus|session bus)/i.test(message))
|
|
6137
|
+
return "bus-denied";
|
|
6138
|
+
if (/locked|interaction.*not allowed|user.*cancel/i.test(message))
|
|
6139
|
+
return "locked";
|
|
6140
|
+
if (/ServiceUnknown|org\.freedesktop\.secrets.*(not provided|no owner|not available)|secret.?service.*(unavailable|not running)/i.test(message))
|
|
6141
|
+
return "service-unavailable";
|
|
6142
|
+
if (/d-?bus|user bus|session bus|autolaunch/i.test(message))
|
|
6143
|
+
return "bus-unavailable";
|
|
6144
|
+
return fallback;
|
|
6145
|
+
};
|
|
6146
|
+
|
|
6147
|
+
class CliCredentialPersistenceError extends Error {
|
|
6148
|
+
remoteRevoked;
|
|
6149
|
+
localRestored;
|
|
6150
|
+
phase;
|
|
6151
|
+
code = "CLI_CREDENTIAL_PERSISTENCE_FAILED";
|
|
6152
|
+
constructor(remoteRevoked, localRestored, phase) {
|
|
6153
|
+
super(`CLI credentials could not be saved after ${phase} [CLI_CREDENTIAL_PERSISTENCE_FAILED]. ${remoteRevoked ? "The newly issued session was revoked." : "Remote revocation could not be confirmed; revoke the session in the product's session management before retrying."} ${localRestored ? phase === "login" ? "The previous local credentials were preserved." : "The unusable rotated local credentials were removed; sign in again." : "Local cleanup could not be confirmed. Inspect your own native credential backend before retrying."}`);
|
|
6154
|
+
this.remoteRevoked = remoteRevoked;
|
|
6155
|
+
this.localRestored = localRestored;
|
|
6156
|
+
this.phase = phase;
|
|
6157
|
+
this.name = "CliCredentialPersistenceError";
|
|
6158
|
+
}
|
|
6159
|
+
}
|
|
6160
|
+
var getCliStorageError = (error) => {
|
|
6161
|
+
if (error instanceof CliCredentialStoreUnavailableError)
|
|
6162
|
+
return {
|
|
6163
|
+
code: error.code,
|
|
6164
|
+
message: error.message,
|
|
6165
|
+
details: { reason: error.reason }
|
|
6166
|
+
};
|
|
6167
|
+
if (error instanceof CliCredentialPersistenceError)
|
|
6168
|
+
return {
|
|
6169
|
+
code: error.code,
|
|
6170
|
+
message: error.message,
|
|
6171
|
+
details: {
|
|
6172
|
+
remoteRevoked: error.remoteRevoked,
|
|
6173
|
+
localRestored: error.localRestored,
|
|
6174
|
+
phase: error.phase
|
|
6175
|
+
}
|
|
6176
|
+
};
|
|
6177
|
+
return;
|
|
6178
|
+
};
|
|
6179
|
+
|
|
6180
|
+
// ../../cli-auth/src/client/secret-backend.ts
|
|
6181
|
+
var timeoutMs = 1e4;
|
|
6182
|
+
var maxBuffer = 64 * 1024;
|
|
6183
|
+
var runBackend = (product, executable, args, input, failure) => new Promise((resolve, reject) => {
|
|
6184
|
+
const child = execFile(executable, args, {
|
|
6185
|
+
timeout: timeoutMs,
|
|
6186
|
+
killSignal: "SIGKILL",
|
|
6187
|
+
maxBuffer,
|
|
6188
|
+
windowsHide: true
|
|
6189
|
+
}, (error, stdout, stderr) => {
|
|
6190
|
+
if (error) {
|
|
6191
|
+
const reason = error.code === "ERR_CHILD_PROCESS_STDIO_MAXBUFFER" ? "invalid-response" : error.killed ? "timeout" : classifyCredentialStoreFailure(`${stdout}
|
|
6192
|
+
${stderr}`, failure);
|
|
6193
|
+
reject(new CliCredentialStoreUnavailableError(product, reason));
|
|
6194
|
+
return;
|
|
6195
|
+
}
|
|
6196
|
+
resolve(stdout);
|
|
6197
|
+
});
|
|
6198
|
+
child.stdin?.on("error", () => {});
|
|
6199
|
+
child.stdin?.end(input);
|
|
6200
|
+
});
|
|
6201
|
+
var nativeWorker = `
|
|
6202
|
+
try {
|
|
6203
|
+
const { operation, input } = JSON.parse(await Bun.stdin.text());
|
|
6204
|
+
const value = await Bun.secrets[operation](input);
|
|
6205
|
+
process.stdout.write(JSON.stringify({ value: value ?? null }));
|
|
6206
|
+
} catch (error) {
|
|
6207
|
+
const message = error instanceof Error ? error.message : "Credential backend failed";
|
|
6208
|
+
// libsecret's bus transport errors can omit the D-Bus context.
|
|
6209
|
+
const context = process.platform === "linux" && /^Could not connect:/i.test(message) ? "D-Bus: " : "";
|
|
6210
|
+
process.stderr.write(context + message);
|
|
6211
|
+
process.exitCode = 1;
|
|
6033
6212
|
}
|
|
6034
|
-
|
|
6213
|
+
`;
|
|
6214
|
+
var nativeResponse = object({ value: union([string2(), boolean2(), _null3()]) }).strict();
|
|
6215
|
+
var createIsolatedBunSecrets = (product) => {
|
|
6216
|
+
const run = async (operation, input) => {
|
|
6217
|
+
assertBunRuntime();
|
|
6218
|
+
const output = await runBackend(product, process.execPath, ["--no-env-file", "--eval", nativeWorker], JSON.stringify({ operation, input }), operation === "set" ? "write-failed" : operation === "delete" ? "delete-failed" : "read-failed");
|
|
6219
|
+
try {
|
|
6220
|
+
return nativeResponse.parse(JSON.parse(output)).value;
|
|
6221
|
+
} catch {
|
|
6222
|
+
throw new CliCredentialStoreUnavailableError(product, "invalid-response");
|
|
6223
|
+
}
|
|
6224
|
+
};
|
|
6225
|
+
return {
|
|
6226
|
+
get: async (input) => {
|
|
6227
|
+
const value = await run("get", input);
|
|
6228
|
+
if (value === null || typeof value === "string")
|
|
6229
|
+
return value;
|
|
6230
|
+
throw new CliCredentialStoreUnavailableError(product, "invalid-response");
|
|
6231
|
+
},
|
|
6232
|
+
set: async (input) => {
|
|
6233
|
+
await run("set", input);
|
|
6234
|
+
},
|
|
6235
|
+
delete: async (input) => await run("delete", input) === true
|
|
6236
|
+
};
|
|
6237
|
+
};
|
|
6238
|
+
|
|
6239
|
+
// ../../cli-auth/src/client/credential-store.ts
|
|
6240
|
+
var runSecretOperation = async (product, operation, fallback = "read-failed") => {
|
|
6035
6241
|
try {
|
|
6036
6242
|
return await operation();
|
|
6037
6243
|
} catch (cause) {
|
|
6038
6244
|
if (cause instanceof CliCredentialStoreUnavailableError)
|
|
6039
6245
|
throw cause;
|
|
6040
|
-
throw new CliCredentialStoreUnavailableError(product,
|
|
6246
|
+
throw new CliCredentialStoreUnavailableError(product, classifyCredentialStoreFailure(cause instanceof Error ? cause.message : "", fallback));
|
|
6041
6247
|
}
|
|
6042
6248
|
};
|
|
6043
|
-
var defaultSecrets = () => {
|
|
6044
|
-
assertBunRuntime();
|
|
6045
|
-
return Bun.secrets;
|
|
6046
|
-
};
|
|
6047
6249
|
var refreshLockTimeoutMs = 5 * 60 * 1000;
|
|
6048
6250
|
var refreshLockRetryMs = 50;
|
|
6049
6251
|
var getCredentialLockDirectoryPath = () => {
|
|
@@ -6115,18 +6317,38 @@ var acquireRefreshLock = async (path) => {
|
|
|
6115
6317
|
}
|
|
6116
6318
|
throw new Error("Timed out waiting for another CLI process to refresh credentials. Retry the command.");
|
|
6117
6319
|
};
|
|
6320
|
+
var resolveCliCredentialAccount = (environment = process.env) => environment.GO_LABS_CLI_CREDENTIAL_ACCOUNT?.trim() || "default";
|
|
6118
6321
|
var createBunCredentialStore = ({
|
|
6119
|
-
account =
|
|
6322
|
+
account = resolveCliCredentialAccount(),
|
|
6120
6323
|
product,
|
|
6121
|
-
secrets =
|
|
6324
|
+
secrets = createIsolatedBunSecrets(product)
|
|
6122
6325
|
}) => {
|
|
6123
6326
|
const service = `go-labs-cli:${product}`;
|
|
6124
6327
|
return {
|
|
6328
|
+
preflight: async () => {
|
|
6329
|
+
const input = { name: `probe-${randomUUID()}`, service };
|
|
6330
|
+
const value = `go-labs-non-secret-probe:${randomUUID()}`;
|
|
6331
|
+
try {
|
|
6332
|
+
await runSecretOperation(product, () => secrets.set({ ...input, value }), "write-failed");
|
|
6333
|
+
if (await runSecretOperation(product, () => secrets.get(input)) !== value) {
|
|
6334
|
+
throw new CliCredentialStoreUnavailableError(product, "write-failed");
|
|
6335
|
+
}
|
|
6336
|
+
} catch (error) {
|
|
6337
|
+
await secrets.delete(input).catch(() => {
|
|
6338
|
+
return;
|
|
6339
|
+
});
|
|
6340
|
+
throw error;
|
|
6341
|
+
}
|
|
6342
|
+
await runSecretOperation(product, () => secrets.delete(input), "delete-failed");
|
|
6343
|
+
if (await runSecretOperation(product, () => secrets.get(input)) !== null) {
|
|
6344
|
+
throw new CliCredentialStoreUnavailableError(product, "delete-failed");
|
|
6345
|
+
}
|
|
6346
|
+
},
|
|
6125
6347
|
delete: async () => {
|
|
6126
|
-
await
|
|
6348
|
+
await runSecretOperation(product, () => secrets.delete({ name: account, service }), "delete-failed");
|
|
6127
6349
|
},
|
|
6128
6350
|
get: async () => {
|
|
6129
|
-
const value = await
|
|
6351
|
+
const value = await runSecretOperation(product, () => secrets.get({ name: account, service }));
|
|
6130
6352
|
if (value === null)
|
|
6131
6353
|
return null;
|
|
6132
6354
|
try {
|
|
@@ -6137,11 +6359,11 @@ var createBunCredentialStore = ({
|
|
|
6137
6359
|
},
|
|
6138
6360
|
set: async (credentials) => {
|
|
6139
6361
|
const parsed = storedCliCredentialsSchema.parse(credentials);
|
|
6140
|
-
await
|
|
6362
|
+
await runSecretOperation(product, () => secrets.set({
|
|
6141
6363
|
name: account,
|
|
6142
6364
|
service,
|
|
6143
6365
|
value: JSON.stringify(parsed)
|
|
6144
|
-
}));
|
|
6366
|
+
}), "write-failed");
|
|
6145
6367
|
},
|
|
6146
6368
|
withCredentialLock: async (operation) => {
|
|
6147
6369
|
const refreshLockPath = await getCredentialLockPath(product, account);
|
|
@@ -6162,6 +6384,7 @@ var createBunCredentialStore = ({
|
|
|
6162
6384
|
};
|
|
6163
6385
|
// ../../cli-auth/src/client/login.ts
|
|
6164
6386
|
import { hostname } from "os";
|
|
6387
|
+
import { isDeepStrictEqual } from "util";
|
|
6165
6388
|
var normalizeCliAuthIssuer = (value) => {
|
|
6166
6389
|
const url = new URL(value);
|
|
6167
6390
|
url.hash = "";
|
|
@@ -6224,12 +6447,15 @@ var openSystemBrowser = async (url) => {
|
|
|
6224
6447
|
"-NoProfile",
|
|
6225
6448
|
"-NonInteractive",
|
|
6226
6449
|
"-Command",
|
|
6227
|
-
"Start-Process -FilePath $
|
|
6228
|
-
url
|
|
6450
|
+
"Start-Process -FilePath $env:GO_LABS_CLI_VERIFICATION_URI -ErrorAction Stop"
|
|
6229
6451
|
] : ["xdg-open", url];
|
|
6230
6452
|
try {
|
|
6231
|
-
const
|
|
6232
|
-
|
|
6453
|
+
const browserProcess = Bun.spawn(command, {
|
|
6454
|
+
env: process.platform === "win32" ? { ...process.env, GO_LABS_CLI_VERIFICATION_URI: url } : process.env,
|
|
6455
|
+
stderr: "ignore",
|
|
6456
|
+
stdout: "ignore"
|
|
6457
|
+
});
|
|
6458
|
+
return await browserProcess.exited === 0;
|
|
6233
6459
|
} catch {
|
|
6234
6460
|
return false;
|
|
6235
6461
|
}
|
|
@@ -6247,6 +6473,8 @@ var loginWithBrowser = async ({
|
|
|
6247
6473
|
store
|
|
6248
6474
|
}) => {
|
|
6249
6475
|
const baseUrl = normalizeCliAuthIssuer(apiBaseUrl);
|
|
6476
|
+
if (store.preflight)
|
|
6477
|
+
await withCredentialLock(store, () => store.preflight?.() ?? Promise.resolve());
|
|
6250
6478
|
const pkce = createPkcePair();
|
|
6251
6479
|
const beginResponse = await postJson(fetchImplementation, `${baseUrl}/api/cli-auth/device`, {
|
|
6252
6480
|
clientName,
|
|
@@ -6274,7 +6502,19 @@ var loginWithBrowser = async ({
|
|
|
6274
6502
|
if (response.ok) {
|
|
6275
6503
|
const issued = tokenResponseSchema.parse(payload);
|
|
6276
6504
|
const credentials = toStoredCredentials(issued, baseUrl, now());
|
|
6277
|
-
|
|
6505
|
+
try {
|
|
6506
|
+
await withCredentialLock(store, () => persistIssuedCredentials({
|
|
6507
|
+
store,
|
|
6508
|
+
credentials,
|
|
6509
|
+
apiBaseUrl: baseUrl,
|
|
6510
|
+
fetch: fetchImplementation,
|
|
6511
|
+
phase: "login"
|
|
6512
|
+
}));
|
|
6513
|
+
} catch (error) {
|
|
6514
|
+
if (error instanceof CliCredentialPersistenceError)
|
|
6515
|
+
throw error;
|
|
6516
|
+
throw new CliCredentialPersistenceError(await revokeIssuedCredentials(fetchImplementation, baseUrl, credentials), false, "login");
|
|
6517
|
+
}
|
|
6278
6518
|
return credentials;
|
|
6279
6519
|
}
|
|
6280
6520
|
const error = deviceTokenErrorSchema.safeParse(payload);
|
|
@@ -6294,6 +6534,55 @@ var loginWithBrowser = async ({
|
|
|
6294
6534
|
}
|
|
6295
6535
|
throw new Error("CLI authentication expired. Run auth login again.");
|
|
6296
6536
|
};
|
|
6537
|
+
var revokeIssuedCredentials = async (fetchImplementation, apiBaseUrl, credentials) => {
|
|
6538
|
+
try {
|
|
6539
|
+
const response = await postJson(fetchImplementation, `${normalizeCliAuthIssuer(apiBaseUrl)}/api/cli-auth/revoke`, { refreshToken: credentials.refreshToken }, { signal: AbortSignal.timeout(refreshRequestTimeoutMs) });
|
|
6540
|
+
return response.ok;
|
|
6541
|
+
} catch {
|
|
6542
|
+
return false;
|
|
6543
|
+
}
|
|
6544
|
+
};
|
|
6545
|
+
var persistIssuedCredentials = async ({
|
|
6546
|
+
store,
|
|
6547
|
+
credentials,
|
|
6548
|
+
previous,
|
|
6549
|
+
apiBaseUrl,
|
|
6550
|
+
fetch: fetchImplementation,
|
|
6551
|
+
phase
|
|
6552
|
+
}) => {
|
|
6553
|
+
let prior = previous;
|
|
6554
|
+
try {
|
|
6555
|
+
if (phase === "login")
|
|
6556
|
+
prior = await store.get();
|
|
6557
|
+
await store.set(credentials);
|
|
6558
|
+
const saved = await store.get();
|
|
6559
|
+
if (!isDeepStrictEqual(saved, credentials))
|
|
6560
|
+
throw new Error("Credential write could not be verified");
|
|
6561
|
+
return;
|
|
6562
|
+
} catch {
|
|
6563
|
+
const remoteRevoked = await revokeIssuedCredentials(fetchImplementation, apiBaseUrl, credentials);
|
|
6564
|
+
let localRestored = false;
|
|
6565
|
+
try {
|
|
6566
|
+
const current = await store.get();
|
|
6567
|
+
if (phase === "login" && prior !== undefined) {
|
|
6568
|
+
if (isDeepStrictEqual(current, prior))
|
|
6569
|
+
localRestored = true;
|
|
6570
|
+
else if (current === null || current.refreshToken === credentials.refreshToken) {
|
|
6571
|
+
if (prior)
|
|
6572
|
+
await store.set(prior);
|
|
6573
|
+
else
|
|
6574
|
+
await store.delete();
|
|
6575
|
+
localRestored = isDeepStrictEqual(await store.get(), prior);
|
|
6576
|
+
}
|
|
6577
|
+
} else if (phase === "refresh" && (!current || current.refreshToken === credentials.refreshToken || current.refreshToken === prior?.refreshToken)) {
|
|
6578
|
+
if (current)
|
|
6579
|
+
await store.delete();
|
|
6580
|
+
localRestored = await store.get() === null;
|
|
6581
|
+
}
|
|
6582
|
+
} catch {}
|
|
6583
|
+
throw new CliCredentialPersistenceError(remoteRevoked, localRestored, phase);
|
|
6584
|
+
}
|
|
6585
|
+
};
|
|
6297
6586
|
var refreshes = new WeakMap;
|
|
6298
6587
|
var refreshRequestTimeoutMs = 30000;
|
|
6299
6588
|
var isInvalidGrantResponse = (payload) => {
|
|
@@ -6348,7 +6637,14 @@ var refreshAccessToken = async ({
|
|
|
6348
6637
|
throw new Error("Unable to refresh the CLI session because the API returned invalid credentials. Credentials were kept; retry the command.");
|
|
6349
6638
|
}
|
|
6350
6639
|
const stored = toStoredCredentials(issued, normalizeCliAuthIssuer(apiBaseUrl), now());
|
|
6351
|
-
await
|
|
6640
|
+
await persistIssuedCredentials({
|
|
6641
|
+
store,
|
|
6642
|
+
credentials: stored,
|
|
6643
|
+
previous: credentials,
|
|
6644
|
+
apiBaseUrl,
|
|
6645
|
+
fetch: fetchImplementation,
|
|
6646
|
+
phase: "refresh"
|
|
6647
|
+
});
|
|
6352
6648
|
return stored.accessToken;
|
|
6353
6649
|
};
|
|
6354
6650
|
var refreshWithCurrentCredentials = async ({
|
|
@@ -6420,126 +6716,64 @@ var logoutCliSession = async ({
|
|
|
6420
6716
|
return { remoteRevoked };
|
|
6421
6717
|
});
|
|
6422
6718
|
};
|
|
6423
|
-
// src/
|
|
6424
|
-
|
|
6425
|
-
|
|
6426
|
-
|
|
6427
|
-
|
|
6428
|
-
|
|
6429
|
-
|
|
6430
|
-
|
|
6431
|
-
|
|
6432
|
-
|
|
6433
|
-
|
|
6434
|
-
|
|
6435
|
-
|
|
6436
|
-
|
|
6437
|
-
|
|
6438
|
-
|
|
6439
|
-
|
|
6440
|
-
|
|
6441
|
-
|
|
6442
|
-
|
|
6443
|
-
|
|
6444
|
-
|
|
6445
|
-
|
|
6446
|
-
|
|
6447
|
-
|
|
6448
|
-
|
|
6449
|
-
|
|
6450
|
-
|
|
6451
|
-
|
|
6452
|
-
|
|
6453
|
-
|
|
6454
|
-
|
|
6455
|
-
|
|
6456
|
-
|
|
6457
|
-
|
|
6458
|
-
|
|
6459
|
-
|
|
6460
|
-
|
|
6461
|
-
|
|
6462
|
-
|
|
6463
|
-
|
|
6464
|
-
|
|
6465
|
-
|
|
6466
|
-
|
|
6467
|
-
|
|
6468
|
-
|
|
6469
|
-
if (this.closed)
|
|
6470
|
-
return;
|
|
6471
|
-
this.readyState = this.CONNECTING;
|
|
6472
|
-
this.abortController = new AbortController;
|
|
6473
|
-
try {
|
|
6474
|
-
const response = await fetch(this.url, {
|
|
6475
|
-
headers: {
|
|
6476
|
-
Accept: "text/event-stream",
|
|
6477
|
-
...this.init.headers,
|
|
6478
|
-
...this.lastEventId ? { "Last-Event-ID": this.lastEventId } : {}
|
|
6479
|
-
},
|
|
6480
|
-
signal: this.abortController.signal
|
|
6481
|
-
});
|
|
6482
|
-
if (!response.ok || !response.body) {
|
|
6483
|
-
throw new Error(`SSE connection failed with HTTP ${response.status}.`);
|
|
6484
|
-
}
|
|
6485
|
-
this.readyState = this.OPEN;
|
|
6486
|
-
this.dispatch("open", new Event("open"));
|
|
6487
|
-
await this.consume(response.body);
|
|
6488
|
-
} catch (error) {
|
|
6489
|
-
if (!this.closed) {
|
|
6490
|
-
this.dispatch("error", new ErrorEvent("error", {
|
|
6491
|
-
error: error instanceof Error ? error : new Error(String(error))
|
|
6492
|
-
}));
|
|
6493
|
-
}
|
|
6494
|
-
}
|
|
6495
|
-
if (!this.closed) {
|
|
6496
|
-
setTimeout(() => void this.connect(), this.retryMilliseconds);
|
|
6497
|
-
}
|
|
6498
|
-
}
|
|
6499
|
-
async consume(body) {
|
|
6500
|
-
const reader = body.getReader();
|
|
6501
|
-
const decoder = new TextDecoder;
|
|
6502
|
-
let pending = "";
|
|
6503
|
-
while (!this.closed) {
|
|
6504
|
-
const { done, value } = await reader.read();
|
|
6505
|
-
pending += decoder.decode(value, { stream: !done });
|
|
6506
|
-
const blocks = pending.split(/\r?\n\r?\n/);
|
|
6507
|
-
pending = blocks.pop() ?? "";
|
|
6508
|
-
for (const block of blocks)
|
|
6509
|
-
this.processBlock(block);
|
|
6510
|
-
if (done)
|
|
6511
|
-
break;
|
|
6512
|
-
}
|
|
6513
|
-
}
|
|
6514
|
-
processBlock(block) {
|
|
6515
|
-
let data = "";
|
|
6516
|
-
let eventType = "message";
|
|
6517
|
-
for (const line of block.split(/\r?\n/)) {
|
|
6518
|
-
if (!line || line.startsWith(":"))
|
|
6519
|
-
continue;
|
|
6520
|
-
const separator = line.indexOf(":");
|
|
6521
|
-
const field = separator === -1 ? line : line.slice(0, separator);
|
|
6522
|
-
const rawValue = separator === -1 ? "" : line.slice(separator + 1);
|
|
6523
|
-
const value = rawValue.startsWith(" ") ? rawValue.slice(1) : rawValue;
|
|
6524
|
-
if (field === "data")
|
|
6525
|
-
data += `${value}
|
|
6526
|
-
`;
|
|
6527
|
-
if (field === "event")
|
|
6528
|
-
eventType = value;
|
|
6529
|
-
if (field === "id")
|
|
6530
|
-
this.lastEventId = value;
|
|
6531
|
-
if (field === "retry" && /^\d+$/.test(value)) {
|
|
6532
|
-
this.retryMilliseconds = Number(value);
|
|
6533
|
-
}
|
|
6534
|
-
}
|
|
6535
|
-
if (!data)
|
|
6536
|
-
return;
|
|
6537
|
-
this.dispatch(eventType, new MessageEvent(eventType, {
|
|
6538
|
-
data: data.slice(0, -1),
|
|
6539
|
-
lastEventId: this.lastEventId
|
|
6540
|
-
}));
|
|
6719
|
+
// ../validators/src/asset-requirements.ts
|
|
6720
|
+
var goshImageContentTypes = ["image/jpeg", "image/png", "image/webp"];
|
|
6721
|
+
var goshImageAccept = goshImageContentTypes.join(",");
|
|
6722
|
+
var maxGoshSetupImageBytes = 10 * 1024 * 1024;
|
|
6723
|
+
var maxGoshBrandingImageBytes = 5 * 1024 * 1024;
|
|
6724
|
+
var goshAssetTypes = [
|
|
6725
|
+
"station",
|
|
6726
|
+
"mission",
|
|
6727
|
+
"logo",
|
|
6728
|
+
"sponsor-logo",
|
|
6729
|
+
"login-banner",
|
|
6730
|
+
"map-marker",
|
|
6731
|
+
"player-background"
|
|
6732
|
+
];
|
|
6733
|
+
var logoGuidance = {
|
|
6734
|
+
recommendedCanvas: null,
|
|
6735
|
+
display: "Logo containers use contain sizing; available space varies by surface.",
|
|
6736
|
+
artworkAdvice: "Use PNG or WebP for transparency. Keep the logo legible at small sizes; there is no prescribed canvas."
|
|
6737
|
+
};
|
|
6738
|
+
var guidance2 = {
|
|
6739
|
+
station: {
|
|
6740
|
+
recommendedCanvas: { width: 1920, height: 1080, aspectRatio: "16:9" },
|
|
6741
|
+
display: "The standard event-home station card uses a 16:9 cover crop and a shorter crop when completed. Other station surfaces differ.",
|
|
6742
|
+
artworkAdvice: "Keep important text and logos away from the edges. Prefer putting essential instructions in station text rather than the image."
|
|
6743
|
+
},
|
|
6744
|
+
mission: {
|
|
6745
|
+
recommendedCanvas: { width: 1600, height: 1000, aspectRatio: "16:10" },
|
|
6746
|
+
display: "Participant mission cards and mission artwork use 16:10 cover crops.",
|
|
6747
|
+
artworkAdvice: "Keep important content away from the edges. Use mission text for essential instructions."
|
|
6748
|
+
},
|
|
6749
|
+
logo: logoGuidance,
|
|
6750
|
+
"sponsor-logo": logoGuidance,
|
|
6751
|
+
"login-banner": {
|
|
6752
|
+
recommendedCanvas: null,
|
|
6753
|
+
display: "The login banner uses full container width and automatic height; there is no prescribed upload ratio.",
|
|
6754
|
+
artworkAdvice: "Preview in the mobile login card. No exact canvas is prescribed; do not assume the station's 16:9 recommendation applies."
|
|
6755
|
+
},
|
|
6756
|
+
"map-marker": {
|
|
6757
|
+
recommendedCanvas: null,
|
|
6758
|
+
display: "Custom markers appear in small map-marker containers.",
|
|
6759
|
+
artworkAdvice: "Use a simple, legible icon and PNG or WebP for transparency. Preview on the participant map."
|
|
6760
|
+
},
|
|
6761
|
+
"player-background": {
|
|
6762
|
+
recommendedCanvas: null,
|
|
6763
|
+
display: "Participant backgrounds use centered cover sizing and can crop differently with the viewport.",
|
|
6764
|
+
artworkAdvice: "Use decorative artwork without essential text. Check both portrait and landscape previews."
|
|
6541
6765
|
}
|
|
6542
|
-
}
|
|
6766
|
+
};
|
|
6767
|
+
var getGoshAssetRequirements = (type) => ({
|
|
6768
|
+
type,
|
|
6769
|
+
requirements: {
|
|
6770
|
+
acceptedContentTypes: goshImageContentTypes,
|
|
6771
|
+
maxBytes: type === "station" || type === "mission" ? maxGoshSetupImageBytes : maxGoshBrandingImageBytes,
|
|
6772
|
+
dimensions: null,
|
|
6773
|
+
aspectRatio: null
|
|
6774
|
+
},
|
|
6775
|
+
recommendations: guidance2[type]
|
|
6776
|
+
});
|
|
6543
6777
|
|
|
6544
6778
|
// src/runtime/index.ts
|
|
6545
6779
|
import { createInterface } from "readline/promises";
|
|
@@ -6631,6 +6865,9 @@ var sanitizeValue = (value, seen = new WeakSet) => {
|
|
|
6631
6865
|
]));
|
|
6632
6866
|
};
|
|
6633
6867
|
var normalizeError = (error) => {
|
|
6868
|
+
const storageError = getCliStorageError(error);
|
|
6869
|
+
if (storageError)
|
|
6870
|
+
return storageError;
|
|
6634
6871
|
if (error instanceof CliError) {
|
|
6635
6872
|
return {
|
|
6636
6873
|
code: error.code,
|
|
@@ -6776,6 +7013,168 @@ var createProcessRuntime = () => ({
|
|
|
6776
7013
|
}
|
|
6777
7014
|
});
|
|
6778
7015
|
|
|
7016
|
+
// src/features/asset-requirements.ts
|
|
7017
|
+
var assetRequirementsCommand = {
|
|
7018
|
+
path: ["assets", "requirements"],
|
|
7019
|
+
operation: "local",
|
|
7020
|
+
input: "none",
|
|
7021
|
+
requiresAuth: false,
|
|
7022
|
+
effects: [],
|
|
7023
|
+
summary: "Show upload limits and separate artwork recommendations without login or network access.",
|
|
7024
|
+
options: {
|
|
7025
|
+
"--type": `Select ${goshAssetTypes.join(", ")}; omit for all asset types.`,
|
|
7026
|
+
"--format": "json (default, standard CLI envelope) or text."
|
|
7027
|
+
}
|
|
7028
|
+
};
|
|
7029
|
+
var getAssetRequirementsOutput = (type) => {
|
|
7030
|
+
const selectedTypes = type !== undefined ? goshAssetTypes.filter((candidate) => candidate === type) : goshAssetTypes;
|
|
7031
|
+
if (selectedTypes.length === 0) {
|
|
7032
|
+
throw new CliError("USAGE", `Unknown asset type: ${type}. Choose ${goshAssetTypes.join(", ")}.`);
|
|
7033
|
+
}
|
|
7034
|
+
return {
|
|
7035
|
+
provenance: "Bundled CLI policy; not a live-server verification. Update the CLI to obtain newer policy definitions.",
|
|
7036
|
+
assets: selectedTypes.map(getGoshAssetRequirements)
|
|
7037
|
+
};
|
|
7038
|
+
};
|
|
7039
|
+
var formatAssetRequirementsText = (output) => [
|
|
7040
|
+
output.provenance,
|
|
7041
|
+
...output.assets.map(({ type, requirements, recommendations }) => {
|
|
7042
|
+
const canvas = recommendations.recommendedCanvas;
|
|
7043
|
+
return [
|
|
7044
|
+
`
|
|
7045
|
+
${type}`,
|
|
7046
|
+
`Required: JPEG, PNG, WebP; maximum ${requirements.maxBytes} bytes (${requirements.maxBytes / (1024 * 1024)} MiB).`,
|
|
7047
|
+
"Enforced pixel dimensions / aspect ratio: none.",
|
|
7048
|
+
`Recommended canvas (not required): ${canvas ? `${canvas.width} \xD7 ${canvas.height} px (${canvas.aspectRatio})` : "not prescribed"}.`,
|
|
7049
|
+
`Display: ${recommendations.display}`,
|
|
7050
|
+
`Artwork advice: ${recommendations.artworkAdvice}`
|
|
7051
|
+
].join(`
|
|
7052
|
+
`);
|
|
7053
|
+
})
|
|
7054
|
+
].join(`
|
|
7055
|
+
`);
|
|
7056
|
+
|
|
7057
|
+
// src/api-client.ts
|
|
7058
|
+
import {
|
|
7059
|
+
createTRPCProxyClient,
|
|
7060
|
+
httpBatchLink,
|
|
7061
|
+
httpSubscriptionLink,
|
|
7062
|
+
loggerLink,
|
|
7063
|
+
splitLink
|
|
7064
|
+
} from "@trpc/client";
|
|
7065
|
+
import SuperJSON from "superjson";
|
|
7066
|
+
|
|
7067
|
+
// src/features/authenticated-event-source.ts
|
|
7068
|
+
class AuthenticatedEventSource {
|
|
7069
|
+
url;
|
|
7070
|
+
init;
|
|
7071
|
+
CLOSED = 2;
|
|
7072
|
+
CONNECTING = 0;
|
|
7073
|
+
OPEN = 1;
|
|
7074
|
+
readyState = this.CONNECTING;
|
|
7075
|
+
abortController = null;
|
|
7076
|
+
closed = false;
|
|
7077
|
+
lastEventId = "";
|
|
7078
|
+
listeners = new Map;
|
|
7079
|
+
retryMilliseconds = 1000;
|
|
7080
|
+
constructor(url, init = {}) {
|
|
7081
|
+
this.url = url;
|
|
7082
|
+
this.init = init;
|
|
7083
|
+
this.connect();
|
|
7084
|
+
}
|
|
7085
|
+
addEventListener(type, listener) {
|
|
7086
|
+
const listeners = this.listeners.get(type) ?? new Set;
|
|
7087
|
+
listeners.add(listener);
|
|
7088
|
+
this.listeners.set(type, listeners);
|
|
7089
|
+
}
|
|
7090
|
+
removeEventListener(type, listener) {
|
|
7091
|
+
this.listeners.get(type)?.delete(listener);
|
|
7092
|
+
}
|
|
7093
|
+
close = () => {
|
|
7094
|
+
this.closed = true;
|
|
7095
|
+
this.readyState = this.CLOSED;
|
|
7096
|
+
this.abortController?.abort();
|
|
7097
|
+
};
|
|
7098
|
+
dispatch(type, event) {
|
|
7099
|
+
for (const listener of this.listeners.get(type) ?? [])
|
|
7100
|
+
listener(event);
|
|
7101
|
+
}
|
|
7102
|
+
async connect() {
|
|
7103
|
+
if (this.closed)
|
|
7104
|
+
return;
|
|
7105
|
+
this.readyState = this.CONNECTING;
|
|
7106
|
+
this.abortController = new AbortController;
|
|
7107
|
+
try {
|
|
7108
|
+
const response = await fetch(this.url, {
|
|
7109
|
+
headers: {
|
|
7110
|
+
Accept: "text/event-stream",
|
|
7111
|
+
...this.init.headers,
|
|
7112
|
+
...this.lastEventId ? { "Last-Event-ID": this.lastEventId } : {}
|
|
7113
|
+
},
|
|
7114
|
+
signal: this.abortController.signal
|
|
7115
|
+
});
|
|
7116
|
+
if (!response.ok || !response.body) {
|
|
7117
|
+
throw new Error(`SSE connection failed with HTTP ${response.status}.`);
|
|
7118
|
+
}
|
|
7119
|
+
this.readyState = this.OPEN;
|
|
7120
|
+
this.dispatch("open", new Event("open"));
|
|
7121
|
+
await this.consume(response.body);
|
|
7122
|
+
} catch (error) {
|
|
7123
|
+
if (!this.closed) {
|
|
7124
|
+
this.dispatch("error", new ErrorEvent("error", {
|
|
7125
|
+
error: error instanceof Error ? error : new Error(String(error))
|
|
7126
|
+
}));
|
|
7127
|
+
}
|
|
7128
|
+
}
|
|
7129
|
+
if (!this.closed) {
|
|
7130
|
+
setTimeout(() => void this.connect(), this.retryMilliseconds);
|
|
7131
|
+
}
|
|
7132
|
+
}
|
|
7133
|
+
async consume(body) {
|
|
7134
|
+
const reader = body.getReader();
|
|
7135
|
+
const decoder = new TextDecoder;
|
|
7136
|
+
let pending = "";
|
|
7137
|
+
while (!this.closed) {
|
|
7138
|
+
const { done, value } = await reader.read();
|
|
7139
|
+
pending += decoder.decode(value, { stream: !done });
|
|
7140
|
+
const blocks = pending.split(/\r?\n\r?\n/);
|
|
7141
|
+
pending = blocks.pop() ?? "";
|
|
7142
|
+
for (const block of blocks)
|
|
7143
|
+
this.processBlock(block);
|
|
7144
|
+
if (done)
|
|
7145
|
+
break;
|
|
7146
|
+
}
|
|
7147
|
+
}
|
|
7148
|
+
processBlock(block) {
|
|
7149
|
+
let data = "";
|
|
7150
|
+
let eventType = "message";
|
|
7151
|
+
for (const line of block.split(/\r?\n/)) {
|
|
7152
|
+
if (!line || line.startsWith(":"))
|
|
7153
|
+
continue;
|
|
7154
|
+
const separator = line.indexOf(":");
|
|
7155
|
+
const field = separator === -1 ? line : line.slice(0, separator);
|
|
7156
|
+
const rawValue = separator === -1 ? "" : line.slice(separator + 1);
|
|
7157
|
+
const value = rawValue.startsWith(" ") ? rawValue.slice(1) : rawValue;
|
|
7158
|
+
if (field === "data")
|
|
7159
|
+
data += `${value}
|
|
7160
|
+
`;
|
|
7161
|
+
if (field === "event")
|
|
7162
|
+
eventType = value;
|
|
7163
|
+
if (field === "id")
|
|
7164
|
+
this.lastEventId = value;
|
|
7165
|
+
if (field === "retry" && /^\d+$/.test(value)) {
|
|
7166
|
+
this.retryMilliseconds = Number(value);
|
|
7167
|
+
}
|
|
7168
|
+
}
|
|
7169
|
+
if (!data)
|
|
7170
|
+
return;
|
|
7171
|
+
this.dispatch(eventType, new MessageEvent(eventType, {
|
|
7172
|
+
data: data.slice(0, -1),
|
|
7173
|
+
lastEventId: this.lastEventId
|
|
7174
|
+
}));
|
|
7175
|
+
}
|
|
7176
|
+
}
|
|
7177
|
+
|
|
6779
7178
|
// src/config.ts
|
|
6780
7179
|
var DEFAULT_GOSH_API_BASE_URL = "https://app.gosh.sg";
|
|
6781
7180
|
var normalizeApiBaseUrl = (value) => {
|
|
@@ -7929,10 +8328,14 @@ var allowedFlags = new Set([
|
|
|
7929
8328
|
"input",
|
|
7930
8329
|
"no-browser",
|
|
7931
8330
|
"quiet",
|
|
7932
|
-
"target"
|
|
8331
|
+
"target",
|
|
8332
|
+
"type",
|
|
8333
|
+
"format"
|
|
7933
8334
|
]);
|
|
7934
8335
|
var commandName = (command) => command.path.join(" ");
|
|
8336
|
+
var updateBeforeUse = "First step before each task: update to the latest published release with 'bun add --global @go-labs-sg/gosh@latest', then run 'gosh version' and 'gosh --help' before using commands. Update once per task. If updating fails, stop and report it. Preserve an explicitly requested pinned or development/staging version.";
|
|
7935
8337
|
var commandHelp = (command) => ({
|
|
8338
|
+
beforeUse: updateBeforeUse,
|
|
7936
8339
|
effects: command.effects,
|
|
7937
8340
|
input: command.input === "json" ? "Pass the exact tRPC input object with --input '<json>'." : "This command does not accept --input.",
|
|
7938
8341
|
path: command.path,
|
|
@@ -7942,9 +8345,14 @@ var commandHelp = (command) => ({
|
|
|
7942
8345
|
target: "Pass --target with the exact resource being changed."
|
|
7943
8346
|
},
|
|
7944
8347
|
summary: command.summary,
|
|
8348
|
+
...command.upload === "setup-image" || command.upload === "branding" ? {
|
|
8349
|
+
assetRequirements: goshAssetTypes.filter((type) => command.upload === "setup-image" ? type === "station" || type === "mission" : type !== "station" && type !== "mission").map(getGoshAssetRequirements),
|
|
8350
|
+
assetRequirementsCommand: "gosh assets requirements"
|
|
8351
|
+
} : {},
|
|
7945
8352
|
usage: `gosh ${commandName(command)}${command.input === "json" ? " --input '<json>'" : ""}`
|
|
7946
8353
|
});
|
|
7947
8354
|
var rootHelp = {
|
|
8355
|
+
beforeUse: updateBeforeUse,
|
|
7948
8356
|
authentication: {
|
|
7949
8357
|
instructions: "Run 'gosh auth login'. Approve the device in your browser; credentials are stored through Bun.secrets."
|
|
7950
8358
|
},
|
|
@@ -7952,6 +8360,7 @@ var rootHelp = {
|
|
|
7952
8360
|
"gosh help [command path]",
|
|
7953
8361
|
"gosh commands",
|
|
7954
8362
|
"gosh version",
|
|
8363
|
+
"gosh assets requirements [--type station] [--format text]",
|
|
7955
8364
|
"gosh <command path> [--input '<json>']"
|
|
7956
8365
|
],
|
|
7957
8366
|
globalOptions: {
|
|
@@ -7987,6 +8396,16 @@ var emitHelp = (runtime, apiBaseUrl, positional) => {
|
|
|
7987
8396
|
command: "help"
|
|
7988
8397
|
}, rootHelp);
|
|
7989
8398
|
}
|
|
8399
|
+
if (positional.join(" ") === "assets requirements") {
|
|
8400
|
+
return emitSuccess(runtime, {
|
|
8401
|
+
apiBaseUrl,
|
|
8402
|
+
cliVersion: GOSH_CLI_VERSION,
|
|
8403
|
+
command: "help assets requirements"
|
|
8404
|
+
}, {
|
|
8405
|
+
...assetRequirementsCommand,
|
|
8406
|
+
usage: "gosh assets requirements [--type station] [--format json|text]"
|
|
8407
|
+
});
|
|
8408
|
+
}
|
|
7990
8409
|
const resolved = findCommand(positional);
|
|
7991
8410
|
if (!resolved || resolved.consumed !== positional.length) {
|
|
7992
8411
|
throw new CliError("USAGE", `Unknown GOSH command: ${positional.join(" ")}`);
|
|
@@ -8003,8 +8422,14 @@ var runGoshCli = async (argv, dependencies) => {
|
|
|
8003
8422
|
let apiBaseUrl = DEFAULT_GOSH_API_BASE_URL;
|
|
8004
8423
|
try {
|
|
8005
8424
|
validateFlags(flags);
|
|
8006
|
-
apiBaseUrl = resolveApiBaseUrl(dependencies.runtime, flags);
|
|
8007
8425
|
const [first, ...rest] = positional;
|
|
8426
|
+
const isAssetRequirements = (first === "help" ? rest : positional).join(" ") === "assets requirements";
|
|
8427
|
+
if (!isAssetRequirements) {
|
|
8428
|
+
apiBaseUrl = resolveApiBaseUrl(dependencies.runtime, flags);
|
|
8429
|
+
}
|
|
8430
|
+
if (!isAssetRequirements && (flags.type !== undefined || flags.format !== undefined)) {
|
|
8431
|
+
throw new CliError("USAGE", "--type and --format are only supported by assets requirements.");
|
|
8432
|
+
}
|
|
8008
8433
|
if (first === undefined || first === "help" || flags.help === true) {
|
|
8009
8434
|
return emitHelp(dependencies.runtime, apiBaseUrl, first === "help" ? rest : positional);
|
|
8010
8435
|
}
|
|
@@ -8026,9 +8451,37 @@ var runGoshCli = async (argv, dependencies) => {
|
|
|
8026
8451
|
apiBaseUrl,
|
|
8027
8452
|
cliVersion: GOSH_CLI_VERSION,
|
|
8028
8453
|
command: "commands"
|
|
8029
|
-
}, commandManifest);
|
|
8454
|
+
}, [...commandManifest, assetRequirementsCommand]);
|
|
8455
|
+
}
|
|
8456
|
+
if (isAssetRequirements && first !== "help") {
|
|
8457
|
+
for (const flag of Object.keys(flags)) {
|
|
8458
|
+
if (!["type", "format", "api-url", "debug", "quiet", "help"].includes(flag)) {
|
|
8459
|
+
throw new CliError("USAGE", `assets requirements does not accept --${flag}.`);
|
|
8460
|
+
}
|
|
8461
|
+
}
|
|
8462
|
+
if (flags.type === true || flags.format === true) {
|
|
8463
|
+
throw new CliError("USAGE", "--type and --format require values.");
|
|
8464
|
+
}
|
|
8465
|
+
const format = getStringFlag(flags, "format") ?? "json";
|
|
8466
|
+
if (format !== "json" && format !== "text") {
|
|
8467
|
+
throw new CliError("USAGE", "--format must be json or text.");
|
|
8468
|
+
}
|
|
8469
|
+
const output = getAssetRequirementsOutput(getStringFlag(flags, "type"));
|
|
8470
|
+
if (format === "text") {
|
|
8471
|
+
dependencies.runtime.stdout.write(`${formatAssetRequirementsText(output)}
|
|
8472
|
+
`);
|
|
8473
|
+
return 0;
|
|
8474
|
+
}
|
|
8475
|
+
return emitSuccess(dependencies.runtime, {
|
|
8476
|
+
apiBaseUrl,
|
|
8477
|
+
cliVersion: GOSH_CLI_VERSION,
|
|
8478
|
+
command: "assets requirements"
|
|
8479
|
+
}, output);
|
|
8030
8480
|
}
|
|
8031
|
-
const credentialStore = dependencies.credentialStore ?? createBunCredentialStore({
|
|
8481
|
+
const credentialStore = dependencies.credentialStore ?? createBunCredentialStore({
|
|
8482
|
+
account: resolveCliCredentialAccount(dependencies.runtime.env),
|
|
8483
|
+
product: "gosh"
|
|
8484
|
+
});
|
|
8032
8485
|
if (first === "auth" && rest[0] === "login" && rest.length === 1) {
|
|
8033
8486
|
const credentials = await loginWithBrowser({
|
|
8034
8487
|
apiBaseUrl,
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@go-labs-sg/gosh",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.12.0",
|
|
4
4
|
"description": "GOSH CLI for organizers and AI agents — manage organizations, events, teams, stations, missions, templates, and live operations through the GOSH API.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "./dist/index.js",
|