@puzzmo/cli 1.0.40 → 1.0.42

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.
@@ -1,13 +1,43 @@
1
+ export declare const defaultSource = "https://api.puzzmo.com";
2
+ export type TokenEntry = {
3
+ /** Server identifier (e.g. "api.puzzmo.com" or "localhost:8911") */
4
+ source: string;
5
+ /** The pzt- prefixed JWT issued by that server */
6
+ token: string;
7
+ };
1
8
  type Config = {
2
- token?: string;
3
- apiURL?: string;
9
+ tokens: TokenEntry[];
4
10
  };
5
- /** Reads the CLI config from ~/.puzzmo/config.json */
11
+ /** Reads ~/.puzzmo/config.json. Returns an empty token list if the file doesn't exist. */
6
12
  export declare const readConfig: () => Config;
7
- /** Writes the CLI config to ~/.puzzmo/config.json */
8
- export declare const writeConfig: (config: Config) => void;
9
- /** Returns the auth token from config or PUZZMO_TOKEN env var */
10
- export declare const getToken: () => string | undefined;
11
- /** Returns the API base URL */
12
- export declare const getAPIURL: () => string;
13
+ /** Saves a token for `source`, replacing any existing entry for that same source */
14
+ export declare const addToken: (source: string, token: string) => void;
15
+ /** Removes any saved token for `source`. Returns true if something was removed. */
16
+ export declare const removeToken: (source: string) => boolean;
17
+ /** Returns saved tokens. PUZZMO_TOKEN env var (with optional PUZZMO_API_URL) overrides everything. */
18
+ export declare const getTokens: () => TokenEntry[];
19
+ /** Best-guess token for clients that don't care which server it points at (e.g. .mcp.json scaffolding) */
20
+ export declare const getDefaultToken: () => string | undefined;
21
+ type TokenPayload = {
22
+ teamID?: string;
23
+ createdByID?: string;
24
+ iat?: number;
25
+ };
26
+ /** Decodes a `pzt-<jwt>` token (without verifying) and returns its payload, or null on failure */
27
+ export declare const decodeTokenPayload: (token: string) => TokenPayload | null;
28
+ /** Strips protocol and trailing slashes so two spellings of the same server compare equal */
29
+ export declare const normalizeSource: (source: string) => string;
30
+ /** Re-attaches a protocol to a normalized source. Localhost defaults to http, everything else to https. */
31
+ export declare const sourceToURL: (source: string) => string;
32
+ /** Sorts saved tokens by upload preference: localhost > *.puzzmo.com > anything else */
33
+ export declare const sortByServerPriority: (tokens: TokenEntry[]) => TokenEntry[];
34
+ /** Returns saved tokens whose JWT teamID matches the given teamID */
35
+ export declare const findTokensForTeam: (teamID: string) => TokenEntry[];
36
+ /** True if a quick fetch to `${source}/graphql` returns any HTTP response within `timeoutMs` */
37
+ export declare const isServerReachable: (source: string, timeoutMs?: number) => Promise<boolean>;
38
+ /**
39
+ * Picks the (source, token) pair to use for `teamID`. Prefers localhost when reachable,
40
+ * then *.puzzmo.com, then any other matching source. Returns null if no token matches.
41
+ */
42
+ export declare const resolveServerForTeam: (teamID: string) => Promise<TokenEntry | null>;
13
43
  export {};
@@ -3,27 +3,121 @@ import os from "node:os";
3
3
  import path from "node:path";
4
4
  const configDir = path.join(os.homedir(), ".puzzmo");
5
5
  const configPath = path.join(configDir, "config.json");
6
- /** Reads the CLI config from ~/.puzzmo/config.json */
6
+ export const defaultSource = "https://api.puzzmo.com";
7
+ /** Reads ~/.puzzmo/config.json. Returns an empty token list if the file doesn't exist. */
7
8
  export const readConfig = () => {
8
9
  try {
9
- const raw = fs.readFileSync(configPath, "utf-8");
10
- return JSON.parse(raw);
10
+ const parsed = JSON.parse(fs.readFileSync(configPath, "utf-8"));
11
+ return { tokens: parsed.tokens ?? [] };
11
12
  }
12
13
  catch {
13
- return {};
14
+ return { tokens: [] };
14
15
  }
15
16
  };
16
- /** Writes the CLI config to ~/.puzzmo/config.json */
17
- export const writeConfig = (config) => {
17
+ const writeConfig = (config) => {
18
18
  fs.mkdirSync(configDir, { recursive: true });
19
19
  fs.writeFileSync(configPath, JSON.stringify(config, null, 2) + "\n");
20
20
  };
21
- /** Returns the auth token from config or PUZZMO_TOKEN env var */
22
- export const getToken = () => {
23
- return process.env.PUZZMO_TOKEN || readConfig().token;
21
+ /** Saves a token for `source`, replacing any existing entry for that same source */
22
+ export const addToken = (source, token) => {
23
+ const normalized = normalizeSource(source);
24
+ const { tokens } = readConfig();
25
+ const remaining = tokens.filter((t) => normalizeSource(t.source) !== normalized);
26
+ remaining.push({ source: normalized, token });
27
+ writeConfig({ tokens: remaining });
24
28
  };
25
- /** Returns the API base URL */
26
- export const getAPIURL = () => {
27
- return process.env.PUZZMO_API_URL || readConfig().apiURL || "https://api.puzzmo.com";
29
+ /** Removes any saved token for `source`. Returns true if something was removed. */
30
+ export const removeToken = (source) => {
31
+ const normalized = normalizeSource(source);
32
+ const { tokens } = readConfig();
33
+ const remaining = tokens.filter((t) => normalizeSource(t.source) !== normalized);
34
+ if (remaining.length === tokens.length)
35
+ return false;
36
+ writeConfig({ tokens: remaining });
37
+ return true;
38
+ };
39
+ /** Returns saved tokens. PUZZMO_TOKEN env var (with optional PUZZMO_API_URL) overrides everything. */
40
+ export const getTokens = () => {
41
+ const envToken = process.env.PUZZMO_TOKEN;
42
+ if (envToken) {
43
+ const source = normalizeSource(process.env.PUZZMO_API_URL ?? defaultSource);
44
+ return [{ source, token: envToken }];
45
+ }
46
+ return readConfig().tokens;
47
+ };
48
+ /** Best-guess token for clients that don't care which server it points at (e.g. .mcp.json scaffolding) */
49
+ export const getDefaultToken = () => getTokens()[0]?.token;
50
+ /** Decodes a `pzt-<jwt>` token (without verifying) and returns its payload, or null on failure */
51
+ export const decodeTokenPayload = (token) => {
52
+ try {
53
+ const stripped = token.startsWith("pzt-") ? token.slice(4) : token;
54
+ const parts = stripped.split(".");
55
+ if (parts.length < 2)
56
+ return null;
57
+ const payload = parts[1].replace(/-/g, "+").replace(/_/g, "/");
58
+ const padded = payload + "=".repeat((4 - (payload.length % 4)) % 4);
59
+ return JSON.parse(Buffer.from(padded, "base64").toString("utf-8"));
60
+ }
61
+ catch {
62
+ return null;
63
+ }
64
+ };
65
+ /** Strips protocol and trailing slashes so two spellings of the same server compare equal */
66
+ export const normalizeSource = (source) => source
67
+ .trim()
68
+ .replace(/^https?:\/\//, "")
69
+ .replace(/\/+$/, "");
70
+ /** Re-attaches a protocol to a normalized source. Localhost defaults to http, everything else to https. */
71
+ export const sourceToURL = (source) => {
72
+ const trimmed = source.trim().replace(/\/+$/, "");
73
+ if (/^https?:\/\//.test(trimmed))
74
+ return trimmed;
75
+ const normalized = normalizeSource(trimmed);
76
+ if (isLocalhost(normalized))
77
+ return `http://${normalized}`;
78
+ return `https://${normalized}`;
79
+ };
80
+ const isLocalhost = (source) => {
81
+ const n = normalizeSource(source);
82
+ return n.startsWith("localhost") || n.startsWith("127.0.0.1") || n.startsWith("0.0.0.0");
83
+ };
84
+ const isPuzzmoCom = (source) => /(^|\.)puzzmo\.com(:|\/|$)/.test(normalizeSource(source));
85
+ /** Sorts saved tokens by upload preference: localhost > *.puzzmo.com > anything else */
86
+ export const sortByServerPriority = (tokens) => {
87
+ const priority = (t) => (isLocalhost(t.source) ? 0 : isPuzzmoCom(t.source) ? 1 : 2);
88
+ return [...tokens].sort((a, b) => priority(a) - priority(b));
89
+ };
90
+ /** Returns saved tokens whose JWT teamID matches the given teamID */
91
+ export const findTokensForTeam = (teamID) => getTokens().filter((t) => decodeTokenPayload(t.token)?.teamID === teamID);
92
+ /** True if a quick fetch to `${source}/graphql` returns any HTTP response within `timeoutMs` */
93
+ export const isServerReachable = async (source, timeoutMs = 1500) => {
94
+ const url = `${sourceToURL(source)}/graphql`;
95
+ const controller = new AbortController();
96
+ const timer = setTimeout(() => controller.abort(), timeoutMs);
97
+ try {
98
+ await fetch(url, { method: "GET", signal: controller.signal });
99
+ return true;
100
+ }
101
+ catch {
102
+ return false;
103
+ }
104
+ finally {
105
+ clearTimeout(timer);
106
+ }
107
+ };
108
+ /**
109
+ * Picks the (source, token) pair to use for `teamID`. Prefers localhost when reachable,
110
+ * then *.puzzmo.com, then any other matching source. Returns null if no token matches.
111
+ */
112
+ export const resolveServerForTeam = async (teamID) => {
113
+ const matches = sortByServerPriority(findTokensForTeam(teamID));
114
+ if (matches.length === 0)
115
+ return null;
116
+ for (const candidate of matches) {
117
+ if (isLocalhost(candidate.source) && !(await isServerReachable(candidate.source)))
118
+ continue;
119
+ return candidate;
120
+ }
121
+ return null;
28
122
  };
29
123
  //# sourceMappingURL=config.js.map
@@ -1 +1 @@
1
- {"version":3,"file":"config.js","sourceRoot":"","sources":["../../src/util/config.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,MAAM,SAAS,CAAA;AACxB,OAAO,EAAE,MAAM,SAAS,CAAA;AACxB,OAAO,IAAI,MAAM,WAAW,CAAA;AAE5B,MAAM,SAAS,GAAG,IAAI,CAAC,IAAI,CAAC,EAAE,CAAC,OAAO,EAAE,EAAE,SAAS,CAAC,CAAA;AACpD,MAAM,UAAU,GAAG,IAAI,CAAC,IAAI,CAAC,SAAS,EAAE,aAAa,CAAC,CAAA;AAOtD,sDAAsD;AACtD,MAAM,CAAC,MAAM,UAAU,GAAG,GAAW,EAAE,CAAC;IACtC,IAAI,CAAC;QACH,MAAM,GAAG,GAAG,EAAE,CAAC,YAAY,CAAC,UAAU,EAAE,OAAO,CAAC,CAAA;QAChD,OAAO,IAAI,CAAC,KAAK,CAAC,GAAG,CAAC,CAAA;IACxB,CAAC;IAAC,MAAM,CAAC;QACP,OAAO,EAAE,CAAA;IACX,CAAC;AAAA,CACF,CAAA;AAED,qDAAqD;AACrD,MAAM,CAAC,MAAM,WAAW,GAAG,CAAC,MAAc,EAAE,EAAE,CAAC;IAC7C,EAAE,CAAC,SAAS,CAAC,SAAS,EAAE,EAAE,SAAS,EAAE,IAAI,EAAE,CAAC,CAAA;IAC5C,EAAE,CAAC,aAAa,CAAC,UAAU,EAAE,IAAI,CAAC,SAAS,CAAC,MAAM,EAAE,IAAI,EAAE,CAAC,CAAC,GAAG,IAAI,CAAC,CAAA;AAAA,CACrE,CAAA;AAED,iEAAiE;AACjE,MAAM,CAAC,MAAM,QAAQ,GAAG,GAAuB,EAAE,CAAC;IAChD,OAAO,OAAO,CAAC,GAAG,CAAC,YAAY,IAAI,UAAU,EAAE,CAAC,KAAK,CAAA;AAAA,CACtD,CAAA;AAED,+BAA+B;AAC/B,MAAM,CAAC,MAAM,SAAS,GAAG,GAAW,EAAE,CAAC;IACrC,OAAO,OAAO,CAAC,GAAG,CAAC,cAAc,IAAI,UAAU,EAAE,CAAC,MAAM,IAAI,wBAAwB,CAAA;AAAA,CACrF,CAAA"}
1
+ {"version":3,"file":"config.js","sourceRoot":"","sources":["../../src/util/config.ts"],"names":[],"mappings":"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"}
@@ -1,4 +1,6 @@
1
1
  export type CreateUserGameOptions = {
2
+ /** Base URL of the API server to call (e.g. "https://api.puzzmo.com") */
3
+ apiURL: string;
2
4
  /** A teamAccessToken (the same token saved by `puzzmo login`). The mutation uses it to identify the team. */
3
5
  teamAccessToken: string;
4
6
  displayName: string;
@@ -1,4 +1,3 @@
1
- import { getAPIURL } from "./config.js";
2
1
  const createUserGameMutation = `
3
2
  mutation cliCreateUserGameMutation($teamAccessToken: String!, $input: CreateUserGameInput!) {
4
3
  createUserGame(teamAccessToken: $teamAccessToken, input: $input) {
@@ -9,7 +8,7 @@ const createUserGameMutation = `
9
8
  `;
10
9
  /** Calls the createUserGame mutation using a teamAccessToken for auth. */
11
10
  export const createUserGame = async (options) => {
12
- const url = `${getAPIURL()}/graphql`;
11
+ const url = `${options.apiURL}/graphql`;
13
12
  const response = await fetch(url, {
14
13
  method: "POST",
15
14
  headers: { "Content-Type": "application/json" },
@@ -1 +1 @@
1
- {"version":3,"file":"createGame.js","sourceRoot":"","sources":["../../src/util/createGame.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,SAAS,EAAE,MAAM,aAAa,CAAA;AAEvC,MAAM,sBAAsB,GAAG;;;;;;;CAO9B,CAAA;AAsBD,0EAA0E;AAC1E,MAAM,CAAC,MAAM,cAAc,GAAG,KAAK,EAAE,OAA8B,EAAwB,EAAE,CAAC;IAC5F,MAAM,GAAG,GAAG,GAAG,SAAS,EAAE,UAAU,CAAA;IAEpC,MAAM,QAAQ,GAAG,MAAM,KAAK,CAAC,GAAG,EAAE;QAChC,MAAM,EAAE,MAAM;QACd,OAAO,EAAE,EAAE,cAAc,EAAE,kBAAkB,EAAE;QAC/C,IAAI,EAAE,IAAI,CAAC,SAAS,CAAC;YACnB,KAAK,EAAE,sBAAsB;YAC7B,SAAS,EAAE;gBACT,eAAe,EAAE,OAAO,CAAC,eAAe;gBACxC,KAAK,EAAE,EAAE,WAAW,EAAE,OAAO,CAAC,WAAW,EAAE;aAC5C;SACF,CAAC;KACH,CAAC,CAAA;IAEF,IAAI,CAAC,QAAQ,CAAC,EAAE;QAAE,MAAM,IAAI,KAAK,CAAC,kCAAkC,QAAQ,CAAC,MAAM,IAAI,QAAQ,CAAC,UAAU,EAAE,CAAC,CAAA;IAE7G,MAAM,MAAM,GAAG,CAAC,MAAM,QAAQ,CAAC,IAAI,EAAE,CAA2B,CAAA;IAEhE,IAAI,MAAM,CAAC,MAAM,EAAE,MAAM,EAAE,CAAC;QAC1B,MAAM,OAAO,GAAG,MAAM,CAAC,MAAM,CAAC,GAAG,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC,CAAC,CAAC,OAAO,CAAC,CAAC,IAAI,CAAC,IAAI,CAAC,CAAA;QAC9D,MAAM,IAAI,KAAK,CAAC,0BAA0B,OAAO,EAAE,CAAC,CAAA;IACtD,CAAC;IACD,IAAI,CAAC,MAAM,CAAC,IAAI,EAAE,cAAc;QAAE,MAAM,IAAI,KAAK,CAAC,iCAAiC,CAAC,CAAA;IAEpF,MAAM,EAAE,IAAI,EAAE,cAAc,EAAE,GAAG,MAAM,CAAC,IAAI,CAAC,cAAc,CAAA;IAC3D,OAAO,EAAE,EAAE,EAAE,IAAI,CAAC,EAAE,EAAE,IAAI,EAAE,IAAI,CAAC,IAAI,EAAE,WAAW,EAAE,IAAI,CAAC,WAAW,EAAE,cAAc,EAAE,CAAA;AAAA,CACvF,CAAA"}
1
+ {"version":3,"file":"createGame.js","sourceRoot":"","sources":["../../src/util/createGame.ts"],"names":[],"mappings":"AAAA,MAAM,sBAAsB,GAAG;;;;;;;CAO9B,CAAA;AAwBD,0EAA0E;AAC1E,MAAM,CAAC,MAAM,cAAc,GAAG,KAAK,EAAE,OAA8B,EAAwB,EAAE,CAAC;IAC5F,MAAM,GAAG,GAAG,GAAG,OAAO,CAAC,MAAM,UAAU,CAAA;IAEvC,MAAM,QAAQ,GAAG,MAAM,KAAK,CAAC,GAAG,EAAE;QAChC,MAAM,EAAE,MAAM;QACd,OAAO,EAAE,EAAE,cAAc,EAAE,kBAAkB,EAAE;QAC/C,IAAI,EAAE,IAAI,CAAC,SAAS,CAAC;YACnB,KAAK,EAAE,sBAAsB;YAC7B,SAAS,EAAE;gBACT,eAAe,EAAE,OAAO,CAAC,eAAe;gBACxC,KAAK,EAAE,EAAE,WAAW,EAAE,OAAO,CAAC,WAAW,EAAE;aAC5C;SACF,CAAC;KACH,CAAC,CAAA;IAEF,IAAI,CAAC,QAAQ,CAAC,EAAE;QAAE,MAAM,IAAI,KAAK,CAAC,kCAAkC,QAAQ,CAAC,MAAM,IAAI,QAAQ,CAAC,UAAU,EAAE,CAAC,CAAA;IAE7G,MAAM,MAAM,GAAG,CAAC,MAAM,QAAQ,CAAC,IAAI,EAAE,CAA2B,CAAA;IAEhE,IAAI,MAAM,CAAC,MAAM,EAAE,MAAM,EAAE,CAAC;QAC1B,MAAM,OAAO,GAAG,MAAM,CAAC,MAAM,CAAC,GAAG,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC,CAAC,CAAC,OAAO,CAAC,CAAC,IAAI,CAAC,IAAI,CAAC,CAAA;QAC9D,MAAM,IAAI,KAAK,CAAC,0BAA0B,OAAO,EAAE,CAAC,CAAA;IACtD,CAAC;IACD,IAAI,CAAC,MAAM,CAAC,IAAI,EAAE,cAAc;QAAE,MAAM,IAAI,KAAK,CAAC,iCAAiC,CAAC,CAAA;IAEpF,MAAM,EAAE,IAAI,EAAE,cAAc,EAAE,GAAG,MAAM,CAAC,IAAI,CAAC,cAAc,CAAA;IAC3D,OAAO,EAAE,EAAE,EAAE,IAAI,CAAC,EAAE,EAAE,IAAI,EAAE,IAAI,CAAC,IAAI,EAAE,WAAW,EAAE,IAAI,CAAC,WAAW,EAAE,cAAc,EAAE,CAAA;AAAA,CACvF,CAAA"}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@puzzmo/cli",
3
- "version": "1.0.40",
3
+ "version": "1.0.42",
4
4
  "type": "module",
5
5
  "bin": {
6
6
  "puzzmo": "./lib/index.js"
@@ -24,6 +24,7 @@
24
24
  "@cfworker/json-schema": "^4.1.1",
25
25
  "@clack/prompts": "^1.1.0",
26
26
  "@opencode-ai/sdk": "1.14.20",
27
- "@puzzmo/agent-cli-detect": "*"
27
+ "@puzzmo/agent-cli-detect": "*",
28
+ "citty": "^0.2.2"
28
29
  }
29
30
  }
@@ -10,12 +10,12 @@ import { downloadPage } from "../../download/page-downloader.js"
10
10
  import { runCommand, gitCommit } from "../../util/exec.js"
11
11
  import { runSkillsPipelineTUI, runAgentWithBuildLoop } from "../../skills/runner.js"
12
12
  import { login } from "../login.js"
13
- import { getToken } from "../../util/config.js"
13
+ import { getDefaultToken } from "../../util/config.js"
14
14
  import { detectRepoContext, type RepoType } from "./detectRepo.js"
15
15
 
16
- type Strategy = "import" | "blank" | "prompt"
16
+ export type Strategy = "import" | "blank" | "prompt"
17
17
 
18
- type CreateOptions = {
18
+ export type CreateOptions = {
19
19
  name?: string
20
20
  url?: string
21
21
  agent?: string
@@ -25,29 +25,6 @@ type CreateOptions = {
25
25
  prompt?: string
26
26
  }
27
27
 
28
- /** Parses CLI args into CreateOptions */
29
- const parseArgs = (args: string[]): CreateOptions => {
30
- const opts: CreateOptions = {}
31
- let i = 0
32
-
33
- while (i < args.length) {
34
- const arg = args[i]
35
- if (arg === "--name" && args[i + 1]) opts.name = args[++i]
36
- else if (arg === "--url" && args[i + 1]) opts.url = args[++i]
37
- else if (arg === "--agent" && args[i + 1]) opts.agent = args[++i]
38
- else if (arg === "--token" && args[i + 1]) opts.accessToken = args[++i]
39
- else if (arg === "--pm" && args[i + 1]) opts.pm = args[++i]
40
- else if (arg === "--prompt" && args[i + 1]) opts.prompt = args[++i]
41
- else if (arg === "--strategy" && args[i + 1]) {
42
- const v = args[++i]
43
- if (v === "import" || v === "blank" || v === "prompt") opts.strategy = v
44
- }
45
- i++
46
- }
47
-
48
- return opts
49
- }
50
-
51
28
  /** Converts a string to a URL-friendly slug (matches packages/shared/slugify.ts) */
52
29
  const slugify = (text: string) =>
53
30
  text
@@ -62,7 +39,7 @@ const slugify = (text: string) =>
62
39
 
63
40
  /** Writes .mcp.json with dev server config */
64
41
  const writeMcpConfig = (dir: string) => {
65
- const token = getToken()
42
+ const token = getDefaultToken()
66
43
  const mcpConfig = {
67
44
  mcpServers: {
68
45
  "dev.puzzmo.com": {
@@ -227,9 +204,7 @@ const buildPromptStrategyMessage = (userPrompt: string, displayName: string): st
227
204
  ].join("\n")
228
205
 
229
206
  /** Main game create wizard */
230
- export const gameCreate = async (args: string[]) => {
231
- const opts = parseArgs(args)
232
-
207
+ export const gameCreate = async (opts: CreateOptions) => {
233
208
  const require = createRequire(import.meta.url)
234
209
  const { version } = require("../../../package.json")
235
210
  p.intro(`Puzzmo Game Creator v${version}`)
@@ -1,17 +1,21 @@
1
1
  import * as p from "@clack/prompts"
2
2
 
3
- import { writeConfig, readConfig } from "../util/config.js"
3
+ import { defaultSource, addToken, decodeTokenPayload, normalizeSource } from "../util/config.js"
4
4
 
5
- /** Saves a CLI token to ~/.puzzmo/config.json */
6
- export const login = (token: string) => {
5
+ /** Saves a CLI token to ~/.puzzmo/config.json under the given source server */
6
+ export const login = (token: string, source: string = defaultSource) => {
7
7
  if (!token.startsWith("pzt-")) {
8
8
  p.log.error("Invalid CLI token. Generate one from dev.puzzmo.com.")
9
9
  process.exit(1)
10
10
  }
11
11
 
12
- const config = readConfig()
13
- config.token = token
14
- writeConfig(config)
12
+ const payload = decodeTokenPayload(token)
13
+ if (!payload?.teamID) {
14
+ p.log.error("Could not decode the team from this token. Make sure you're using a token from dev.puzzmo.com.")
15
+ process.exit(1)
16
+ }
15
17
 
16
- p.log.success("Logged in successfully. Token saved to ~/.puzzmo/config.json")
18
+ const normalized = normalizeSource(source)
19
+ addToken(normalized, token)
20
+ p.log.success(`Logged in to ${normalized} for team ${payload.teamID}. Token saved to ~/.puzzmo/config.json`)
17
21
  }
@@ -5,7 +5,7 @@ import path from "node:path"
5
5
  import * as p from "@clack/prompts"
6
6
 
7
7
  import { GameNotFoundError, uploadFiles } from "../util/api.js"
8
- import { getAPIURL, getToken } from "../util/config.js"
8
+ import { type TokenEntry, findTokensForTeam, getTokens, resolveServerForTeam, sourceToURL } from "../util/config.js"
9
9
  import { createUserGame } from "../util/createGame.js"
10
10
  import { discoverGames, type DiscoveredGame } from "../util/discoverGames.js"
11
11
 
@@ -16,6 +16,7 @@ type UploadOptions = {
16
16
  type GameSuccess = {
17
17
  ok: true
18
18
  slug: string
19
+ source: string
19
20
  fileCount: number
20
21
  totalBytes: number
21
22
  versionID: string
@@ -33,8 +34,8 @@ type GameResult = GameSuccess | GameFailure
33
34
  /** Uploads game build artifacts to Puzzmo by discovering puzzmo.json files in `dir` */
34
35
  export const upload = async (dir: string, options: UploadOptions = {}) => {
35
36
  const { verbose = false } = options
36
- const token = getToken()
37
- if (!token) {
37
+
38
+ if (getTokens().length === 0) {
38
39
  console.error("Not logged in. Run `puzzmo login <token>` or set PUZZMO_TOKEN.")
39
40
  process.exit(1)
40
41
  }
@@ -70,10 +71,6 @@ export const upload = async (dir: string, options: UploadOptions = {}) => {
70
71
  if (description) console.log(`\nMessage: ${description}`)
71
72
  if (repoURL) console.log(`Repo: ${repoURL}`)
72
73
 
73
- const apiURL = getAPIURL()
74
- const defaultURL = "https://api.puzzmo.com"
75
- if (apiURL !== defaultURL) console.log(`API: ${apiURL}`)
76
-
77
74
  const results: GameResult[] = []
78
75
  for (const err of discoveryErrors) {
79
76
  results.push({ ok: false, slug: err.slug ?? path.relative(rootDir, err.puzzmoJsonPath), error: err.errors.join("; ") })
@@ -81,22 +78,40 @@ export const upload = async (dir: string, options: UploadOptions = {}) => {
81
78
 
82
79
  for (let i = 0; i < games.length; i++) {
83
80
  const game = games[i]
84
- console.log(`\n[${i + 1}/${games.length}] Uploading ${game.puzzmoFile.game.slug}`)
81
+ const slug = game.puzzmoFile.game.slug
82
+ console.log(`\n[${i + 1}/${games.length}] Uploading ${slug}`)
83
+
84
+ const teamID = game.puzzmoFile.game.teamID
85
+ const credential = await resolveServerForTeam(teamID)
86
+ if (!credential) {
87
+ const matches = findTokensForTeam(teamID)
88
+ const message =
89
+ matches.length === 0
90
+ ? `No saved token for team ${teamID}. Run \`puzzmo login <token>\` (use \`--source\` if the token is for a non-default server).`
91
+ : `Token for team ${teamID} is registered against ${matches.map((m) => m.source).join(", ")} but none of those servers are reachable.`
92
+ console.error(` ${message}`)
93
+ results.push({ ok: false, slug, error: message })
94
+ continue
95
+ }
96
+
97
+ const apiURL = sourceToURL(credential.source)
98
+ console.log(` Server: ${credential.source}`)
99
+
85
100
  try {
86
101
  let result: GameSuccess
87
102
  try {
88
- result = await uploadOneGame(game, { token, sha, description, repoURL, verbose, rootDir })
103
+ result = await uploadOneGame(game, { credential, apiURL, sha, description, repoURL, verbose, rootDir })
89
104
  } catch (e) {
90
105
  if (!(e instanceof GameNotFoundError)) throw e
91
- const created = await maybeCreateMissingGame(e, game, { teamAccessToken: token })
106
+ const created = await maybeCreateMissingGame(e, game, { apiURL, teamAccessToken: credential.token })
92
107
  if (!created) throw e
93
- result = await uploadOneGame(game, { token, sha, description, repoURL, verbose, rootDir })
108
+ result = await uploadOneGame(game, { credential, apiURL, sha, description, repoURL, verbose, rootDir })
94
109
  }
95
110
  results.push(result)
96
111
  } catch (e) {
97
112
  const message = e instanceof Error ? e.message : String(e)
98
113
  console.error(` Failed: ${message}`)
99
- results.push({ ok: false, slug: game.puzzmoFile.game.slug, error: message })
114
+ results.push({ ok: false, slug, error: message })
100
115
  }
101
116
  }
102
117
 
@@ -107,7 +122,8 @@ export const upload = async (dir: string, options: UploadOptions = {}) => {
107
122
  }
108
123
 
109
124
  type UploadOneOptions = {
110
- token: string
125
+ credential: TokenEntry
126
+ apiURL: string
111
127
  sha: string
112
128
  description: string | null
113
129
  repoURL: string | null
@@ -117,7 +133,7 @@ type UploadOneOptions = {
117
133
 
118
134
  /** Uploads a single discovered game; throws on failure */
119
135
  const uploadOneGame = async (game: DiscoveredGame, opts: UploadOneOptions): Promise<GameSuccess> => {
120
- const { token, sha, description, repoURL, verbose, rootDir } = opts
136
+ const { credential, apiURL, sha, description, repoURL, verbose, rootDir } = opts
121
137
  const { puzzmoFile, distDir } = game
122
138
  const gameSlug = puzzmoFile.game.slug
123
139
 
@@ -130,7 +146,8 @@ const uploadOneGame = async (game: DiscoveredGame, opts: UploadOneOptions): Prom
130
146
  console.log(` ${files.length} file(s), ${formatBytes(totalBytes)} total (sha ${sha.slice(0, 8)})`)
131
147
 
132
148
  const result = await uploadFiles(
133
- token,
149
+ apiURL,
150
+ credential.token,
134
151
  gameSlug,
135
152
  sha,
136
153
  files,
@@ -146,6 +163,7 @@ const uploadOneGame = async (game: DiscoveredGame, opts: UploadOneOptions): Prom
146
163
  return {
147
164
  ok: true,
148
165
  slug: gameSlug,
166
+ source: credential.source,
149
167
  fileCount: files.length,
150
168
  totalBytes,
151
169
  versionID: result.versionID,
@@ -159,7 +177,7 @@ const printReport = (results: GameResult[]) => {
159
177
  const successes = results.filter((r): r is GameSuccess => r.ok)
160
178
  const failures = results.filter((r): r is GameFailure => !r.ok)
161
179
  for (const r of successes) {
162
- console.log(` OK ${r.slug.padEnd(24)} ${formatBytes(r.totalBytes).padStart(8)} ${r.versionID}`)
180
+ console.log(` OK ${r.slug.padEnd(24)} ${formatBytes(r.totalBytes).padStart(8)} ${r.source.padEnd(20)} ${r.versionID}`)
163
181
  }
164
182
  for (const r of failures) {
165
183
  console.log(` FAIL ${r.slug.padEnd(24)} ${r.error}`)
@@ -241,7 +259,7 @@ const hashGames = (games: DiscoveredGame[]): string => {
241
259
  const maybeCreateMissingGame = async (
242
260
  err: GameNotFoundError,
243
261
  game: DiscoveredGame,
244
- opts: { teamAccessToken: string },
262
+ opts: { apiURL: string; teamAccessToken: string },
245
263
  ): Promise<boolean> => {
246
264
  const slug = err.slug
247
265
  const displayName = game.puzzmoFile.game.displayName
@@ -258,7 +276,7 @@ const maybeCreateMissingGame = async (
258
276
  if (p.isCancel(consent) || !consent) return false
259
277
 
260
278
  try {
261
- const created = await createUserGame({ displayName, teamAccessToken: opts.teamAccessToken })
279
+ const created = await createUserGame({ apiURL: opts.apiURL, displayName, teamAccessToken: opts.teamAccessToken })
262
280
  console.log(` Created game ${created.slug} (${created.id})`)
263
281
  if (created.slug !== slug) {
264
282
  console.warn(` Server picked slug "${created.slug}" but your puzzmo.json uses "${slug}". Update puzzmo.json before re-uploading.`)