@go-labs-sg/gosh 0.11.0 → 0.13.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 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.
@@ -83,7 +97,7 @@ Commands use `--input '<json>'` for the exact validated tRPC input object. This
83
97
 
84
98
  `gosh event list-page` returns `{ items, page, pageSize, pageCount, totalCount }`. It defaults to page 1 with 25 results, accepts page sizes from 1 to 100, and supports server-side `search` and `status` filters. Archived events are excluded unless an explicit status is supplied. Request subsequent pages explicitly; the CLI does not automatically fetch every page. The existing `gosh event list` command and its array response remain unchanged.
85
99
 
86
- The command registry exposes the supported GOSH tRPC procedures across organizer and global-admin operations, organization invitations and membership, player workflows, public results/login procedures, uploads, and realtime subscriptions. Update the registry alongside API changes, then run `bun run generate:commands` to regenerate the manifest, reference, and typed operations.
100
+ The command registry exposes the supported GOSH tRPC procedures across organizer and global-admin operations, organization invitations and membership, player workflows, public results/login procedures, uploads, and realtime subscriptions. Update the registry alongside API changes, then run `bun --filter @go-labs-sg/gosh generate:commands` from the repository root to regenerate the manifest, reference, and typed operations.
87
101
 
88
102
  ### Templates and organization scope
89
103
 
@@ -237,6 +251,37 @@ measures mission work only: travel between stations and staff review time are
237
251
  excluded. Existing leaderboard and results commands include the resulting
238
252
  timing information.
239
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
+
240
285
  ### Local file uploads
241
286
 
242
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:
@@ -313,6 +358,20 @@ Without the allow flags, an interactive terminal prints a workflow preview and r
313
358
 
314
359
  One-shot commands write one JSON envelope to stdout. Watch commands write newline-delimited JSON envelopes. Failures and opt-in `--debug` diagnostics go to stderr, and known secrets, payloads, join codes, share tokens, and signed URLs are redacted. `--quiet` or `-q` suppresses diagnostics.
315
360
 
361
+ ## Monorepo development
362
+
363
+ Follow the [development workflow](../../../docs/development-workflow.md#required-cli-coverage). App/API changes affecting this CLI must update its commands, types/input-output contracts, help/docs and verification in the same change. Use the existing authenticated product API rather than CLI-only business logic. Presentation-only changes do not require CLI edits; record why when no CLI update was needed.
364
+
365
+ From the repository root after isolated-worktree initialization:
366
+
367
+ ```bash
368
+ bun --filter @go-labs-sg/gosh typecheck
369
+ bun --filter @go-labs-sg/gosh build
370
+ bun run packages/gosh/cli/src/index.ts help
371
+ ```
372
+
373
+ Building is not publication or permission to mutate live data. Use focused temporary business-behavior tests and the final pre-push gate from the shared workflow. Its existing build regenerates command artifacts; review the manifest/reference and typed operations after registry changes.
374
+
316
375
  ## Release verification
317
376
 
318
377
  The `gosh-cli-v*` release workflow runs
@@ -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
  ]
@@ -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
@@ -35,16 +35,16 @@ var __toESM = (mod, isNodeMode, target) => {
35
35
  var __commonJS = (cb, mod) => () => (mod || cb((mod = { exports: {} }).exports, mod), mod.exports);
36
36
  var __require = import.meta.require;
37
37
 
38
- // ../../../node_modules/.bun/dotenv@18.0.5/node_modules/dotenv/dist/index.cjs
38
+ // ../../../node_modules/.bun/dotenv@18.0.6/node_modules/dotenv/dist/index.cjs
39
39
  var require_dist = __commonJS(function(exports, module) {
40
- var I = (e, o) => () => {
40
+ var S = (e, o) => () => {
41
41
  try {
42
42
  return o || e((o = { exports: {} }).exports, o), o.exports;
43
43
  } catch (t) {
44
44
  throw o = 0, t;
45
45
  }
46
46
  };
47
- var S = I((xe, U) => {
47
+ var j = S((xe, U) => {
48
48
  function G(e) {
49
49
  return typeof e == "string" ? !["false", "0", "no", "off", ""].includes(e.toLowerCase()) : !!e;
50
50
  }
@@ -58,8 +58,8 @@ var require_dist = __commonJS(function(exports, module) {
58
58
  }
59
59
  U.exports = { parseBoolean: G, optionsFromEnv: X };
60
60
  });
61
- var N = I((ye, x) => {
62
- var Y = __require("fs"), j = __require("path"), z = __require("os"), { URL: Z, fileURLToPath: ee } = __require("url"), { parseBoolean: k, optionsFromEnv: B } = S(), te = /(?:^|^)\s*(?:export\s+)?([\w.-]+)(?:\s*=\s*?|:\s+?)(\s*'(?:\\'|[^'])*'|\s*"(?:\\"|[^"])*"|\s*`(?:\\`|[^`])*`|[^#\r\n]+)?\s*(?:#.*)?(?:$|$)/mg, b = new Uint8Array(256);
61
+ var N = S((ye, x) => {
62
+ var Y = __require("fs"), k = __require("path"), z = __require("os"), { URL: Z, fileURLToPath: ee } = __require("url"), { parseBoolean: $, optionsFromEnv: B } = j(), te = /(?:^|^)\s*(?:export\s+)?([\w.-]+)(?:\s*=\s*?|:\s+?)(\s*'(?:\\'|[^'])*'|\s*"(?:\\"|[^"])*"|\s*`(?:\\`|[^`])*`|[^#\r\n]+)?\s*(?:#.*)?(?:$|$)/mg, b = new Uint8Array(256);
63
63
  for (let e = 48;e <= 57; e++)
64
64
  b[e] = 1;
65
65
  for (let e = 65;e <= 90; e++)
@@ -185,7 +185,7 @@ var require_dist = __commonJS(function(exports, module) {
185
185
  return o;
186
186
  }
187
187
  function ne(e, o) {
188
- return o && k(o.fast) ? oe(e) : re(e);
188
+ return o && $(o.fast) ? oe(e) : re(e);
189
189
  }
190
190
  function T(e) {
191
191
  console.log(`\u2506 ${e}`);
@@ -194,16 +194,16 @@ var require_dist = __commonJS(function(exports, module) {
194
194
  console.error(`\u25C7 ${e}`);
195
195
  }
196
196
  function V(e) {
197
- return e[0] === "~" ? j.join(z.homedir(), e.slice(1)) : e;
197
+ return e[0] === "~" ? k.join(z.homedir(), e.slice(1)) : e;
198
198
  }
199
199
  function ie(e = {}) {
200
200
  return { ...B(), ...e };
201
201
  }
202
202
  function ce(e) {
203
203
  e = ie(e);
204
- let o = j.resolve(process.cwd(), ".env"), t = "utf8", n = process.env;
204
+ let o = k.resolve(process.cwd(), ".env"), t = "utf8", n = process.env;
205
205
  e && e.processEnv != null && (n = e.processEnv);
206
- let r = k(e && e.debug);
206
+ let r = $(e && e.debug);
207
207
  e && e.encoding ? t = e.encoding : r && T("no encoding is specified (UTF-8 is used by default)");
208
208
  let s = [o];
209
209
  if (e && e.path)
@@ -222,12 +222,12 @@ var require_dist = __commonJS(function(exports, module) {
222
222
  } catch (a) {
223
223
  r && T(`failed to load ${c} ${a.message}`), i = a;
224
224
  }
225
- let p = A.populate(n, l, e), f = k(Object.prototype.hasOwnProperty.call(e, "quiet") ? e.quiet : B(n).quiet);
225
+ let p = A.populate(n, l, e), f = $(Object.prototype.hasOwnProperty.call(e, "quiet") ? e.quiet : B(n).quiet);
226
226
  if (r || !f) {
227
227
  let c = Object.keys(p).length, a = [];
228
228
  for (let g of s)
229
229
  try {
230
- let h = j.relative(process.cwd(), g instanceof Z ? ee(g) : g);
230
+ let h = k.relative(process.cwd(), g instanceof Z ? ee(g) : g);
231
231
  a.push(h);
232
232
  } catch (h) {
233
233
  r && T(`failed to load ${g} ${h.message}`), i = h;
@@ -240,7 +240,7 @@ var require_dist = __commonJS(function(exports, module) {
240
240
  return A.configDotenv(e);
241
241
  }
242
242
  function le(e, o, t = {}) {
243
- let n = !!(t && t.debug), r = !!(t && t.override), s = {};
243
+ let n = $(t && t.debug), r = $(t && t.override), s = {};
244
244
  if (e === null || typeof e != "object" || o === null || typeof o != "object") {
245
245
  let i = new Error("OBJECT_REQUIRED: Please check the processEnv argument being passed to populate");
246
246
  throw i.code = "OBJECT_REQUIRED", i;
@@ -256,7 +256,7 @@ var require_dist = __commonJS(function(exports, module) {
256
256
  x.exports.populate = A.populate;
257
257
  x.exports = A;
258
258
  });
259
- var M = I((Te, W) => {
259
+ var M = S((Te, W) => {
260
260
  var _ = __require("child_process"), fe = __require("fs"), L = __require("path");
261
261
  function ue(e) {
262
262
  let o = ['"'], t = 0;
@@ -309,9 +309,9 @@ var require_dist = __commonJS(function(exports, module) {
309
309
  }
310
310
  W.exports = pe;
311
311
  });
312
- var K = I(($e, F) => {
313
- var he = __require("fs"), ge = __require("os"), Q = __require("path"), me = __require("child_process"), ve = M(), R = N(), { optionsFromEnv: Ce } = S();
314
- function $() {
312
+ var K = S(($e, F) => {
313
+ var he = __require("fs"), ge = __require("os"), Q = __require("path"), me = __require("child_process"), ve = M(), R = N(), { optionsFromEnv: Ce } = j();
314
+ function D() {
315
315
  console.log(["Usage: dotenv run [--help] [-q|--quiet] [--debug] [--override] [--fast] [-f|--file <paths>] [--] <command> [args...]", "", "Run a command with environment variables from a .env file.", "Place dotenv options before the command; all following arguments go to the command.", "", "Options:", " -f, --file <paths> .env paths, comma-separated or repeated (default: .env)", " -q, --quiet suppress the injected env message", " --debug enable debug logging", " --override override existing environment variables", " --fast use the faster character-scanner parser", "", "Environment variables (DOTENV_CONFIG_* names remain as fallbacks):", " DOTENV_PATH, DOTENV_ENCODING, DOTENV_QUIET,", " DOTENV_DEBUG, DOTENV_OVERRIDE,", " DOTENV_FAST"].join(`
316
316
  `));
317
317
  }
@@ -383,24 +383,24 @@ var require_dist = __commonJS(function(exports, module) {
383
383
  function J(e) {
384
384
  let o = e[0];
385
385
  if (o === "--help" || o === "-h") {
386
- $();
386
+ D();
387
387
  return;
388
388
  }
389
389
  if (o !== "run") {
390
- $(), process.exitCode = 1;
390
+ D(), process.exitCode = 1;
391
391
  return;
392
392
  }
393
393
  let t = we(e.slice(1));
394
394
  if (t.help) {
395
- $();
395
+ D();
396
396
  return;
397
397
  }
398
398
  if (t.error) {
399
- console.error(`dotenv: ${t.error}`), $(), process.exitCode = 1;
399
+ console.error(`dotenv: ${t.error}`), D(), process.exitCode = 1;
400
400
  return;
401
401
  }
402
402
  if (t.command.length === 0) {
403
- $(), process.exitCode = 1;
403
+ D(), process.exitCode = 1;
404
404
  return;
405
405
  }
406
406
  let n = Ee(t);
@@ -456,13 +456,13 @@ var require_dist = __commonJS(function(exports, module) {
456
456
  F.exports = J;
457
457
  F === __require.main && J(process.argv.slice(2));
458
458
  });
459
- var D = N();
459
+ var I = N();
460
460
  var Oe = K();
461
- module.exports = D;
462
- module.exports.config = D.config;
463
- module.exports.configDotenv = D.configDotenv;
464
- module.exports.parse = D.parse;
465
- module.exports.populate = D.populate;
461
+ module.exports = I;
462
+ module.exports.config = I.config;
463
+ module.exports.configDotenv = I.configDotenv;
464
+ module.exports.parse = I.parse;
465
+ module.exports.populate = I.populate;
466
466
  });
467
467
 
468
468
  // src/load-env.ts
@@ -6716,126 +6716,64 @@ var logoutCliSession = async ({
6716
6716
  return { remoteRevoked };
6717
6717
  });
6718
6718
  };
6719
- // src/api-client.ts
6720
- import {
6721
- createTRPCProxyClient,
6722
- httpBatchLink,
6723
- httpSubscriptionLink,
6724
- loggerLink,
6725
- splitLink
6726
- } from "@trpc/client";
6727
- import SuperJSON from "superjson";
6728
-
6729
- // src/features/authenticated-event-source.ts
6730
- class AuthenticatedEventSource {
6731
- url;
6732
- init;
6733
- CLOSED = 2;
6734
- CONNECTING = 0;
6735
- OPEN = 1;
6736
- readyState = this.CONNECTING;
6737
- abortController = null;
6738
- closed = false;
6739
- lastEventId = "";
6740
- listeners = new Map;
6741
- retryMilliseconds = 1000;
6742
- constructor(url, init = {}) {
6743
- this.url = url;
6744
- this.init = init;
6745
- this.connect();
6746
- }
6747
- addEventListener(type, listener) {
6748
- const listeners = this.listeners.get(type) ?? new Set;
6749
- listeners.add(listener);
6750
- this.listeners.set(type, listeners);
6751
- }
6752
- removeEventListener(type, listener) {
6753
- this.listeners.get(type)?.delete(listener);
6754
- }
6755
- close = () => {
6756
- this.closed = true;
6757
- this.readyState = this.CLOSED;
6758
- this.abortController?.abort();
6759
- };
6760
- dispatch(type, event) {
6761
- for (const listener of this.listeners.get(type) ?? [])
6762
- listener(event);
6763
- }
6764
- async connect() {
6765
- if (this.closed)
6766
- return;
6767
- this.readyState = this.CONNECTING;
6768
- this.abortController = new AbortController;
6769
- try {
6770
- const response = await fetch(this.url, {
6771
- headers: {
6772
- Accept: "text/event-stream",
6773
- ...this.init.headers,
6774
- ...this.lastEventId ? { "Last-Event-ID": this.lastEventId } : {}
6775
- },
6776
- signal: this.abortController.signal
6777
- });
6778
- if (!response.ok || !response.body) {
6779
- throw new Error(`SSE connection failed with HTTP ${response.status}.`);
6780
- }
6781
- this.readyState = this.OPEN;
6782
- this.dispatch("open", new Event("open"));
6783
- await this.consume(response.body);
6784
- } catch (error) {
6785
- if (!this.closed) {
6786
- this.dispatch("error", new ErrorEvent("error", {
6787
- error: error instanceof Error ? error : new Error(String(error))
6788
- }));
6789
- }
6790
- }
6791
- if (!this.closed) {
6792
- setTimeout(() => void this.connect(), this.retryMilliseconds);
6793
- }
6794
- }
6795
- async consume(body) {
6796
- const reader = body.getReader();
6797
- const decoder = new TextDecoder;
6798
- let pending = "";
6799
- while (!this.closed) {
6800
- const { done, value } = await reader.read();
6801
- pending += decoder.decode(value, { stream: !done });
6802
- const blocks = pending.split(/\r?\n\r?\n/);
6803
- pending = blocks.pop() ?? "";
6804
- for (const block of blocks)
6805
- this.processBlock(block);
6806
- if (done)
6807
- break;
6808
- }
6809
- }
6810
- processBlock(block) {
6811
- let data = "";
6812
- let eventType = "message";
6813
- for (const line of block.split(/\r?\n/)) {
6814
- if (!line || line.startsWith(":"))
6815
- continue;
6816
- const separator = line.indexOf(":");
6817
- const field = separator === -1 ? line : line.slice(0, separator);
6818
- const rawValue = separator === -1 ? "" : line.slice(separator + 1);
6819
- const value = rawValue.startsWith(" ") ? rawValue.slice(1) : rawValue;
6820
- if (field === "data")
6821
- data += `${value}
6822
- `;
6823
- if (field === "event")
6824
- eventType = value;
6825
- if (field === "id")
6826
- this.lastEventId = value;
6827
- if (field === "retry" && /^\d+$/.test(value)) {
6828
- this.retryMilliseconds = Number(value);
6829
- }
6830
- }
6831
- if (!data)
6832
- return;
6833
- this.dispatch(eventType, new MessageEvent(eventType, {
6834
- data: data.slice(0, -1),
6835
- lastEventId: this.lastEventId
6836
- }));
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."
6837
6765
  }
6838
- }
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
+ });
6839
6777
 
6840
6778
  // src/runtime/index.ts
6841
6779
  import { createInterface } from "readline/promises";
@@ -7075,6 +7013,168 @@ var createProcessRuntime = () => ({
7075
7013
  }
7076
7014
  });
7077
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
+
7078
7178
  // src/config.ts
7079
7179
  var DEFAULT_GOSH_API_BASE_URL = "https://app.gosh.sg";
7080
7180
  var normalizeApiBaseUrl = (value) => {
@@ -8228,10 +8328,14 @@ var allowedFlags = new Set([
8228
8328
  "input",
8229
8329
  "no-browser",
8230
8330
  "quiet",
8231
- "target"
8331
+ "target",
8332
+ "type",
8333
+ "format"
8232
8334
  ]);
8233
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.";
8234
8337
  var commandHelp = (command) => ({
8338
+ beforeUse: updateBeforeUse,
8235
8339
  effects: command.effects,
8236
8340
  input: command.input === "json" ? "Pass the exact tRPC input object with --input '<json>'." : "This command does not accept --input.",
8237
8341
  path: command.path,
@@ -8241,9 +8345,14 @@ var commandHelp = (command) => ({
8241
8345
  target: "Pass --target with the exact resource being changed."
8242
8346
  },
8243
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
+ } : {},
8244
8352
  usage: `gosh ${commandName(command)}${command.input === "json" ? " --input '<json>'" : ""}`
8245
8353
  });
8246
8354
  var rootHelp = {
8355
+ beforeUse: updateBeforeUse,
8247
8356
  authentication: {
8248
8357
  instructions: "Run 'gosh auth login'. Approve the device in your browser; credentials are stored through Bun.secrets."
8249
8358
  },
@@ -8251,6 +8360,7 @@ var rootHelp = {
8251
8360
  "gosh help [command path]",
8252
8361
  "gosh commands",
8253
8362
  "gosh version",
8363
+ "gosh assets requirements [--type station] [--format text]",
8254
8364
  "gosh <command path> [--input '<json>']"
8255
8365
  ],
8256
8366
  globalOptions: {
@@ -8286,6 +8396,16 @@ var emitHelp = (runtime, apiBaseUrl, positional) => {
8286
8396
  command: "help"
8287
8397
  }, rootHelp);
8288
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
+ }
8289
8409
  const resolved = findCommand(positional);
8290
8410
  if (!resolved || resolved.consumed !== positional.length) {
8291
8411
  throw new CliError("USAGE", `Unknown GOSH command: ${positional.join(" ")}`);
@@ -8302,8 +8422,14 @@ var runGoshCli = async (argv, dependencies) => {
8302
8422
  let apiBaseUrl = DEFAULT_GOSH_API_BASE_URL;
8303
8423
  try {
8304
8424
  validateFlags(flags);
8305
- apiBaseUrl = resolveApiBaseUrl(dependencies.runtime, flags);
8306
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
+ }
8307
8433
  if (first === undefined || first === "help" || flags.help === true) {
8308
8434
  return emitHelp(dependencies.runtime, apiBaseUrl, first === "help" ? rest : positional);
8309
8435
  }
@@ -8325,7 +8451,32 @@ var runGoshCli = async (argv, dependencies) => {
8325
8451
  apiBaseUrl,
8326
8452
  cliVersion: GOSH_CLI_VERSION,
8327
8453
  command: "commands"
8328
- }, 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);
8329
8480
  }
8330
8481
  const credentialStore = dependencies.credentialStore ?? createBunCredentialStore({
8331
8482
  account: resolveCliCredentialAccount(dependencies.runtime.env),
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@go-labs-sg/gosh",
3
- "version": "0.11.0",
3
+ "version": "0.13.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",
@@ -19,7 +19,7 @@
19
19
  ],
20
20
  "dependencies": {
21
21
  "@trpc/client": "^11.19.0",
22
- "dotenv": "^18.0.1",
22
+ "dotenv": "^18.0.6",
23
23
  "superjson": "^2.2.6"
24
24
  },
25
25
  "engines": {