@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.
- package/lib/commands/game/create.d.ts +11 -1
- package/lib/commands/game/create.js +3 -31
- package/lib/commands/game/create.js.map +1 -1
- package/lib/commands/login.d.ts +2 -2
- package/lib/commands/login.js +11 -7
- package/lib/commands/login.js.map +1 -1
- package/lib/commands/upload.js +26 -16
- package/lib/commands/upload.js.map +1 -1
- package/lib/index.js +76 -64
- package/lib/index.js.map +1 -1
- package/lib/util/api.d.ts +1 -1
- package/lib/util/api.js +1 -3
- package/lib/util/api.js.map +1 -1
- package/lib/util/config.d.ts +39 -9
- package/lib/util/config.js +106 -12
- package/lib/util/config.js.map +1 -1
- package/lib/util/createGame.d.ts +2 -0
- package/lib/util/createGame.js +1 -2
- package/lib/util/createGame.js.map +1 -1
- package/package.json +3 -2
- package/src/commands/game/create.ts +5 -30
- package/src/commands/login.ts +11 -7
- package/src/commands/upload.ts +36 -18
- package/src/index.ts +81 -64
- package/src/util/api.ts +1 -3
- package/src/util/config.ts +119 -15
- package/src/util/createGame.ts +3 -3
- package/templates/minesweeper/vite.config.ts +1 -0
package/src/index.ts
CHANGED
|
@@ -1,76 +1,93 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
2
|
|
|
3
|
+
import { defineCommand, runMain } from "citty"
|
|
4
|
+
|
|
3
5
|
import { agentTest } from "./commands/agent-test.js"
|
|
6
|
+
import { gameCreate } from "./commands/game/create.js"
|
|
4
7
|
import { login } from "./commands/login.js"
|
|
8
|
+
import { migrate } from "./commands/migrate.js"
|
|
5
9
|
import { upload } from "./commands/upload.js"
|
|
6
10
|
import { validate } from "./commands/validate.js"
|
|
7
|
-
import {
|
|
8
|
-
|
|
11
|
+
import { defaultSource } from "./util/config.js"
|
|
12
|
+
|
|
13
|
+
const loginCommand = defineCommand({
|
|
14
|
+
meta: {
|
|
15
|
+
name: "login",
|
|
16
|
+
description: "Save a CLI auth token. Multiple tokens can be stored, one per --source server.",
|
|
17
|
+
},
|
|
18
|
+
args: {
|
|
19
|
+
token: { type: "positional", description: "The pzt- token from dev.puzzmo.com", required: true },
|
|
20
|
+
source: { type: "string", description: "Server this token belongs to", default: defaultSource },
|
|
21
|
+
},
|
|
22
|
+
run: ({ args }) => login(args.token, args.source),
|
|
23
|
+
})
|
|
9
24
|
|
|
10
|
-
const
|
|
25
|
+
const uploadCommand = defineCommand({
|
|
26
|
+
meta: {
|
|
27
|
+
name: "upload",
|
|
28
|
+
description: "Discover puzzmo.json files and upload each game's build and sync the puzzmo.json.",
|
|
29
|
+
},
|
|
30
|
+
args: {
|
|
31
|
+
dir: { type: "positional", description: "Directory to scan", required: false, default: "." },
|
|
32
|
+
verbose: { type: "boolean", description: "Print request URLs and full error bodies", alias: "v" },
|
|
33
|
+
},
|
|
34
|
+
run: ({ args }) => upload(args.dir, { verbose: args.verbose }),
|
|
35
|
+
})
|
|
11
36
|
|
|
12
|
-
const
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
37
|
+
const validateCommand = defineCommand({
|
|
38
|
+
meta: { name: "validate", description: "Validate every puzzmo.json under [dir]" },
|
|
39
|
+
args: {
|
|
40
|
+
dir: { type: "positional", description: "Directory to scan", required: false, default: "." },
|
|
41
|
+
},
|
|
42
|
+
run: ({ args }) => validate(args.dir),
|
|
43
|
+
})
|
|
16
44
|
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
}
|
|
45
|
+
const migrateCommand = defineCommand({
|
|
46
|
+
meta: { name: "migrate", description: "List and select migration skills from dev.puzzmo.com" },
|
|
47
|
+
run: () => migrate(),
|
|
48
|
+
})
|
|
21
49
|
|
|
22
|
-
const
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
console.error(
|
|
45
|
-
"Usage: puzzmo game create [--strategy <import|blank|prompt>] [--prompt <text>] [--name <name>] [--url <url>] [--agent <agent>] [--token <token>] [--pm <npm|yarn|pnpm>]",
|
|
46
|
-
)
|
|
47
|
-
process.exit(1)
|
|
48
|
-
}
|
|
49
|
-
break
|
|
50
|
-
}
|
|
51
|
-
case "validate": {
|
|
52
|
-
validate(args[0] || ".")
|
|
53
|
-
break
|
|
54
|
-
}
|
|
55
|
-
case "migrate": {
|
|
56
|
-
await migrate()
|
|
57
|
-
break
|
|
58
|
-
}
|
|
59
|
-
case "agent-test": {
|
|
60
|
-
await agentTest()
|
|
61
|
-
break
|
|
62
|
-
}
|
|
63
|
-
default:
|
|
64
|
-
printUsage()
|
|
65
|
-
break
|
|
66
|
-
}
|
|
67
|
-
}
|
|
50
|
+
const gameCreateCommand = defineCommand({
|
|
51
|
+
meta: { name: "create", description: "Scaffold a new Puzzmo game project" },
|
|
52
|
+
args: {
|
|
53
|
+
strategy: { type: "enum", options: ["import", "blank", "prompt"], description: "How to seed the new game" },
|
|
54
|
+
prompt: { type: "string", description: "Game description (used with --strategy prompt)" },
|
|
55
|
+
name: { type: "string", description: "Game display name" },
|
|
56
|
+
url: { type: "string", description: "Source URL (used with --strategy import)" },
|
|
57
|
+
agent: { type: "string", description: "LLM agent id to drive scaffolding" },
|
|
58
|
+
token: { type: "string", description: "Save this token before creating the game" },
|
|
59
|
+
pm: { type: "enum", options: ["npm", "yarn", "pnpm"], description: "Package manager to use" },
|
|
60
|
+
},
|
|
61
|
+
run: ({ args }) =>
|
|
62
|
+
gameCreate({
|
|
63
|
+
strategy: args.strategy,
|
|
64
|
+
prompt: args.prompt,
|
|
65
|
+
name: args.name,
|
|
66
|
+
url: args.url,
|
|
67
|
+
agent: args.agent,
|
|
68
|
+
accessToken: args.token,
|
|
69
|
+
pm: args.pm,
|
|
70
|
+
}),
|
|
71
|
+
})
|
|
68
72
|
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
const cause = err.cause
|
|
73
|
-
console.error(`Caused by: ${cause instanceof Error ? cause.message : cause}`)
|
|
74
|
-
}
|
|
75
|
-
process.exit(1)
|
|
73
|
+
const gameCommand = defineCommand({
|
|
74
|
+
meta: { name: "game", description: "Game project commands" },
|
|
75
|
+
subCommands: { create: gameCreateCommand },
|
|
76
76
|
})
|
|
77
|
+
|
|
78
|
+
const main = defineCommand({
|
|
79
|
+
meta: { name: "puzzmo", description: "Puzzmo CLI" },
|
|
80
|
+
subCommands: {
|
|
81
|
+
login: loginCommand,
|
|
82
|
+
upload: uploadCommand,
|
|
83
|
+
validate: validateCommand,
|
|
84
|
+
migrate: migrateCommand,
|
|
85
|
+
game: gameCommand,
|
|
86
|
+
"agent-test": defineCommand({
|
|
87
|
+
meta: { name: "agent-test", description: "Run the agent test harness", hidden: true },
|
|
88
|
+
run: () => agentTest(),
|
|
89
|
+
}),
|
|
90
|
+
},
|
|
91
|
+
})
|
|
92
|
+
|
|
93
|
+
runMain(main)
|
package/src/util/api.ts
CHANGED
|
@@ -1,8 +1,6 @@
|
|
|
1
1
|
import fs from "node:fs"
|
|
2
2
|
import path from "node:path"
|
|
3
3
|
|
|
4
|
-
import { getAPIURL } from "./config.js"
|
|
5
|
-
|
|
6
4
|
/** The schema for a puzzmo.json file - mirrors PuzzmoFile from @puzzmo-com/shared/hostAPI */
|
|
7
5
|
export type PuzzmoFile = {
|
|
8
6
|
game: {
|
|
@@ -132,6 +130,7 @@ const formatBytes = (bytes: number): string => {
|
|
|
132
130
|
|
|
133
131
|
/** Multi-step upload: init -> batched file uploads -> complete */
|
|
134
132
|
export const uploadFiles = async (
|
|
133
|
+
apiURL: string,
|
|
135
134
|
token: string,
|
|
136
135
|
gameSlug: string,
|
|
137
136
|
sha: string,
|
|
@@ -142,7 +141,6 @@ export const uploadFiles = async (
|
|
|
142
141
|
options: UploadFilesOptions = {},
|
|
143
142
|
): Promise<CompleteResponse> => {
|
|
144
143
|
const { verbose = false, description, repoURL } = options
|
|
145
|
-
const apiURL = getAPIURL()
|
|
146
144
|
if (verbose) console.log(` API URL: ${apiURL}`)
|
|
147
145
|
|
|
148
146
|
// Step 1: Init session (includes puzzmo.json metadata)
|
package/src/util/config.ts
CHANGED
|
@@ -5,33 +5,137 @@ import path from "node:path"
|
|
|
5
5
|
const configDir = path.join(os.homedir(), ".puzzmo")
|
|
6
6
|
const configPath = path.join(configDir, "config.json")
|
|
7
7
|
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
8
|
+
export const defaultSource = "https://api.puzzmo.com"
|
|
9
|
+
|
|
10
|
+
export type TokenEntry = {
|
|
11
|
+
/** Server identifier (e.g. "api.puzzmo.com" or "localhost:8911") */
|
|
12
|
+
source: string
|
|
13
|
+
/** The pzt- prefixed JWT issued by that server */
|
|
14
|
+
token: string
|
|
11
15
|
}
|
|
12
16
|
|
|
13
|
-
|
|
17
|
+
type Config = { tokens: TokenEntry[] }
|
|
18
|
+
|
|
19
|
+
/** Reads ~/.puzzmo/config.json. Returns an empty token list if the file doesn't exist. */
|
|
14
20
|
export const readConfig = (): Config => {
|
|
15
21
|
try {
|
|
16
|
-
const
|
|
17
|
-
return
|
|
22
|
+
const parsed = JSON.parse(fs.readFileSync(configPath, "utf-8")) as Partial<Config>
|
|
23
|
+
return { tokens: parsed.tokens ?? [] }
|
|
18
24
|
} catch {
|
|
19
|
-
return {}
|
|
25
|
+
return { tokens: [] }
|
|
20
26
|
}
|
|
21
27
|
}
|
|
22
28
|
|
|
23
|
-
|
|
24
|
-
export const writeConfig = (config: Config) => {
|
|
29
|
+
const writeConfig = (config: Config) => {
|
|
25
30
|
fs.mkdirSync(configDir, { recursive: true })
|
|
26
31
|
fs.writeFileSync(configPath, JSON.stringify(config, null, 2) + "\n")
|
|
27
32
|
}
|
|
28
33
|
|
|
29
|
-
/**
|
|
30
|
-
export const
|
|
31
|
-
|
|
34
|
+
/** Saves a token for `source`, replacing any existing entry for that same source */
|
|
35
|
+
export const addToken = (source: string, token: string) => {
|
|
36
|
+
const normalized = normalizeSource(source)
|
|
37
|
+
const { tokens } = readConfig()
|
|
38
|
+
const remaining = tokens.filter((t) => normalizeSource(t.source) !== normalized)
|
|
39
|
+
remaining.push({ source: normalized, token })
|
|
40
|
+
writeConfig({ tokens: remaining })
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
/** Removes any saved token for `source`. Returns true if something was removed. */
|
|
44
|
+
export const removeToken = (source: string): boolean => {
|
|
45
|
+
const normalized = normalizeSource(source)
|
|
46
|
+
const { tokens } = readConfig()
|
|
47
|
+
const remaining = tokens.filter((t) => normalizeSource(t.source) !== normalized)
|
|
48
|
+
if (remaining.length === tokens.length) return false
|
|
49
|
+
writeConfig({ tokens: remaining })
|
|
50
|
+
return true
|
|
32
51
|
}
|
|
33
52
|
|
|
34
|
-
/** Returns
|
|
35
|
-
export const
|
|
36
|
-
|
|
53
|
+
/** Returns saved tokens. PUZZMO_TOKEN env var (with optional PUZZMO_API_URL) overrides everything. */
|
|
54
|
+
export const getTokens = (): TokenEntry[] => {
|
|
55
|
+
const envToken = process.env.PUZZMO_TOKEN
|
|
56
|
+
if (envToken) {
|
|
57
|
+
const source = normalizeSource(process.env.PUZZMO_API_URL ?? defaultSource)
|
|
58
|
+
return [{ source, token: envToken }]
|
|
59
|
+
}
|
|
60
|
+
return readConfig().tokens
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
/** Best-guess token for clients that don't care which server it points at (e.g. .mcp.json scaffolding) */
|
|
64
|
+
export const getDefaultToken = (): string | undefined => getTokens()[0]?.token
|
|
65
|
+
|
|
66
|
+
type TokenPayload = { teamID?: string; createdByID?: string; iat?: number }
|
|
67
|
+
|
|
68
|
+
/** Decodes a `pzt-<jwt>` token (without verifying) and returns its payload, or null on failure */
|
|
69
|
+
export const decodeTokenPayload = (token: string): TokenPayload | null => {
|
|
70
|
+
try {
|
|
71
|
+
const stripped = token.startsWith("pzt-") ? token.slice(4) : token
|
|
72
|
+
const parts = stripped.split(".")
|
|
73
|
+
if (parts.length < 2) return null
|
|
74
|
+
const payload = parts[1].replace(/-/g, "+").replace(/_/g, "/")
|
|
75
|
+
const padded = payload + "=".repeat((4 - (payload.length % 4)) % 4)
|
|
76
|
+
return JSON.parse(Buffer.from(padded, "base64").toString("utf-8")) as TokenPayload
|
|
77
|
+
} catch {
|
|
78
|
+
return null
|
|
79
|
+
}
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
/** Strips protocol and trailing slashes so two spellings of the same server compare equal */
|
|
83
|
+
export const normalizeSource = (source: string): string =>
|
|
84
|
+
source
|
|
85
|
+
.trim()
|
|
86
|
+
.replace(/^https?:\/\//, "")
|
|
87
|
+
.replace(/\/+$/, "")
|
|
88
|
+
|
|
89
|
+
/** Re-attaches a protocol to a normalized source. Localhost defaults to http, everything else to https. */
|
|
90
|
+
export const sourceToURL = (source: string): string => {
|
|
91
|
+
const trimmed = source.trim().replace(/\/+$/, "")
|
|
92
|
+
if (/^https?:\/\//.test(trimmed)) return trimmed
|
|
93
|
+
const normalized = normalizeSource(trimmed)
|
|
94
|
+
if (isLocalhost(normalized)) return `http://${normalized}`
|
|
95
|
+
return `https://${normalized}`
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
const isLocalhost = (source: string): boolean => {
|
|
99
|
+
const n = normalizeSource(source)
|
|
100
|
+
return n.startsWith("localhost") || n.startsWith("127.0.0.1") || n.startsWith("0.0.0.0")
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
const isPuzzmoCom = (source: string): boolean => /(^|\.)puzzmo\.com(:|\/|$)/.test(normalizeSource(source))
|
|
104
|
+
|
|
105
|
+
/** Sorts saved tokens by upload preference: localhost > *.puzzmo.com > anything else */
|
|
106
|
+
export const sortByServerPriority = (tokens: TokenEntry[]): TokenEntry[] => {
|
|
107
|
+
const priority = (t: TokenEntry) => (isLocalhost(t.source) ? 0 : isPuzzmoCom(t.source) ? 1 : 2)
|
|
108
|
+
return [...tokens].sort((a, b) => priority(a) - priority(b))
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
/** Returns saved tokens whose JWT teamID matches the given teamID */
|
|
112
|
+
export const findTokensForTeam = (teamID: string): TokenEntry[] => getTokens().filter((t) => decodeTokenPayload(t.token)?.teamID === teamID)
|
|
113
|
+
|
|
114
|
+
/** True if a quick fetch to `${source}/graphql` returns any HTTP response within `timeoutMs` */
|
|
115
|
+
export const isServerReachable = async (source: string, timeoutMs = 1500): Promise<boolean> => {
|
|
116
|
+
const url = `${sourceToURL(source)}/graphql`
|
|
117
|
+
const controller = new AbortController()
|
|
118
|
+
const timer = setTimeout(() => controller.abort(), timeoutMs)
|
|
119
|
+
try {
|
|
120
|
+
await fetch(url, { method: "GET", signal: controller.signal })
|
|
121
|
+
return true
|
|
122
|
+
} catch {
|
|
123
|
+
return false
|
|
124
|
+
} finally {
|
|
125
|
+
clearTimeout(timer)
|
|
126
|
+
}
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
/**
|
|
130
|
+
* Picks the (source, token) pair to use for `teamID`. Prefers localhost when reachable,
|
|
131
|
+
* then *.puzzmo.com, then any other matching source. Returns null if no token matches.
|
|
132
|
+
*/
|
|
133
|
+
export const resolveServerForTeam = async (teamID: string): Promise<TokenEntry | null> => {
|
|
134
|
+
const matches = sortByServerPriority(findTokensForTeam(teamID))
|
|
135
|
+
if (matches.length === 0) return null
|
|
136
|
+
for (const candidate of matches) {
|
|
137
|
+
if (isLocalhost(candidate.source) && !(await isServerReachable(candidate.source))) continue
|
|
138
|
+
return candidate
|
|
139
|
+
}
|
|
140
|
+
return null
|
|
37
141
|
}
|
package/src/util/createGame.ts
CHANGED
|
@@ -1,5 +1,3 @@
|
|
|
1
|
-
import { getAPIURL } from "./config.js"
|
|
2
|
-
|
|
3
1
|
const createUserGameMutation = `
|
|
4
2
|
mutation cliCreateUserGameMutation($teamAccessToken: String!, $input: CreateUserGameInput!) {
|
|
5
3
|
createUserGame(teamAccessToken: $teamAccessToken, input: $input) {
|
|
@@ -22,6 +20,8 @@ type CreateUserGameResponse = {
|
|
|
22
20
|
}
|
|
23
21
|
|
|
24
22
|
export type CreateUserGameOptions = {
|
|
23
|
+
/** Base URL of the API server to call (e.g. "https://api.puzzmo.com") */
|
|
24
|
+
apiURL: string
|
|
25
25
|
/** A teamAccessToken (the same token saved by `puzzmo login`). The mutation uses it to identify the team. */
|
|
26
26
|
teamAccessToken: string
|
|
27
27
|
displayName: string
|
|
@@ -31,7 +31,7 @@ export type CreatedGame = { id: string; slug: string; displayName: string; newAc
|
|
|
31
31
|
|
|
32
32
|
/** Calls the createUserGame mutation using a teamAccessToken for auth. */
|
|
33
33
|
export const createUserGame = async (options: CreateUserGameOptions): Promise<CreatedGame> => {
|
|
34
|
-
const url = `${
|
|
34
|
+
const url = `${options.apiURL}/graphql`
|
|
35
35
|
|
|
36
36
|
const response = await fetch(url, {
|
|
37
37
|
method: "POST",
|
|
@@ -2,6 +2,7 @@ import { defineConfig } from "vite"
|
|
|
2
2
|
import { puzzmoSimulator, appBundlePlugin } from "@puzzmo/sdk/vite"
|
|
3
3
|
|
|
4
4
|
export default defineConfig({
|
|
5
|
+
base: "./",
|
|
5
6
|
plugins: [puzzmoSimulator({ fixturesGlob: "/fixtures/puzzles/**/*.json" }), appBundlePlugin()],
|
|
6
7
|
build: {
|
|
7
8
|
outDir: "dist",
|