@puzzmo/cli 1.0.50 → 1.0.52
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 +12 -2
- package/docs/games-changed.md +223 -0
- package/lib/commands/changed.d.ts +12 -0
- package/lib/commands/changed.js +178 -0
- package/lib/commands/changed.js.map +1 -0
- package/lib/commands/game/create.js +1 -10
- package/lib/commands/game/create.js.map +1 -1
- package/lib/commands/upload.d.ts +2 -0
- package/lib/commands/upload.js +37 -13
- package/lib/commands/upload.js.map +1 -1
- package/lib/commands/validate.d.ts +1 -1
- package/lib/commands/validate.js +1 -1
- package/lib/commands/validate.js.map +1 -1
- package/lib/index.js +57 -3
- package/lib/index.js.map +1 -1
- package/lib/queries/__generated__/cliGameRuntimesQuery.graphql.d.ts +37 -0
- package/lib/queries/__generated__/cliGameRuntimesQuery.graphql.js +256 -0
- package/lib/queries/__generated__/cliGameRuntimesQuery.graphql.js.map +1 -0
- package/lib/queries/gameRuntimes.d.ts +14 -0
- package/lib/queries/gameRuntimes.js +52 -0
- package/lib/queries/gameRuntimes.js.map +1 -0
- package/lib/util/api.d.ts +3 -0
- package/lib/util/api.js.map +1 -1
- package/lib/util/belay.d.ts +34 -0
- package/lib/util/belay.js +22 -0
- package/lib/util/belay.js.map +1 -0
- package/lib/util/createGame.d.ts +3 -0
- package/lib/util/createGame.js +6 -1
- package/lib/util/createGame.js.map +1 -1
- package/lib/util/discoverGames.d.ts +10 -2
- package/lib/util/discoverGames.js +14 -11
- package/lib/util/discoverGames.js.map +1 -1
- package/lib/util/slugify.d.ts +8 -0
- package/lib/util/slugify.js +17 -0
- package/lib/util/slugify.js.map +1 -0
- package/package.json +1 -1
- package/schemas/puzzmo-file-schema.json +14 -2
- package/src/commands/changed.ts +239 -0
- package/src/commands/game/create.ts +1 -12
- package/src/commands/upload.ts +41 -15
- package/src/commands/validate.ts +1 -1
- package/src/index.ts +63 -3
- package/src/queries/__generated__/cliGameRuntimesQuery.graphql.ts +301 -0
- package/src/queries/gameRuntimes.ts +64 -0
- package/src/util/api.ts +3 -0
- package/src/util/belay.ts +56 -0
- package/src/util/createGame.ts +9 -1
- package/src/util/discoverGames.ts +23 -11
- package/src/util/slugify.ts +17 -0
- package/templates/minesweeper/README.md +1 -1
package/src/index.ts
CHANGED
|
@@ -3,6 +3,7 @@
|
|
|
3
3
|
import { defineCommand, runMain } from "citty"
|
|
4
4
|
|
|
5
5
|
import { agentTest } from "./commands/agent-test.js"
|
|
6
|
+
import { type ChangedAgainst, changed } from "./commands/changed.js"
|
|
6
7
|
import { gameCreate } from "./commands/game/create.js"
|
|
7
8
|
import { login } from "./commands/login.js"
|
|
8
9
|
import { migrate } from "./commands/migrate.js"
|
|
@@ -30,8 +31,12 @@ const uploadCommand = defineCommand({
|
|
|
30
31
|
args: {
|
|
31
32
|
dir: { type: "positional", description: "Directory to scan", required: false, default: "." },
|
|
32
33
|
verbose: { type: "boolean", description: "Print request URLs and full error bodies", alias: "v" },
|
|
34
|
+
"create-missing": {
|
|
35
|
+
type: "boolean",
|
|
36
|
+
description: "Create games that don't exist yet without an interactive prompt (for CI).",
|
|
37
|
+
},
|
|
33
38
|
},
|
|
34
|
-
run: ({ args }) => upload(args.dir, { verbose: args.verbose }),
|
|
39
|
+
run: ({ args }) => upload(args.dir, { verbose: args.verbose, createMissing: args["create-missing"] }),
|
|
35
40
|
})
|
|
36
41
|
|
|
37
42
|
const validateCommand = defineCommand({
|
|
@@ -42,6 +47,38 @@ const validateCommand = defineCommand({
|
|
|
42
47
|
run: ({ args }) => validate(args.dir),
|
|
43
48
|
})
|
|
44
49
|
|
|
50
|
+
const changedCommand = defineCommand({
|
|
51
|
+
meta: { name: "changed", description: "Report which games changed since their last deployed build (for CI)." },
|
|
52
|
+
args: {
|
|
53
|
+
dir: { type: "positional", description: "Directory to scan", required: false, default: "." },
|
|
54
|
+
json: { type: "boolean", description: "Output a JSON report for every discovered game" },
|
|
55
|
+
list: { type: "boolean", description: "Output only the changed game folders, one per line" },
|
|
56
|
+
matrix: { type: "boolean", description: "Output a GitHub Actions matrix of the changed games" },
|
|
57
|
+
ref: { type: "string", description: "Git ref to compare against (default HEAD)" },
|
|
58
|
+
against: {
|
|
59
|
+
type: "enum",
|
|
60
|
+
options: ["latest", "next", "previous"],
|
|
61
|
+
description: "Which deployed build to use as the baseline (default latest)",
|
|
62
|
+
},
|
|
63
|
+
"include-uncommitted": { type: "boolean", description: "Also count uncommitted working-tree changes" },
|
|
64
|
+
},
|
|
65
|
+
run: ({ args }) =>
|
|
66
|
+
changed(args.dir, {
|
|
67
|
+
json: args.json,
|
|
68
|
+
list: args.list,
|
|
69
|
+
matrix: args.matrix,
|
|
70
|
+
ref: args.ref,
|
|
71
|
+
against: args.against as ChangedAgainst | undefined,
|
|
72
|
+
includeUncommitted: args["include-uncommitted"],
|
|
73
|
+
}),
|
|
74
|
+
})
|
|
75
|
+
|
|
76
|
+
// Commands that operate across every puzzmo.json in a repo live under `puzzmo games`.
|
|
77
|
+
const gamesCommand = defineCommand({
|
|
78
|
+
meta: { name: "games", description: "Commands that run across all the games in your repo" },
|
|
79
|
+
subCommands: { changed: changedCommand, upload: uploadCommand, validate: validateCommand },
|
|
80
|
+
})
|
|
81
|
+
|
|
45
82
|
const migrateCommand = defineCommand({
|
|
46
83
|
meta: { name: "migrate", description: "List and select migration skills from dev.puzzmo.com" },
|
|
47
84
|
run: () => migrate(),
|
|
@@ -79,14 +116,37 @@ const gameCommand = defineCommand({
|
|
|
79
116
|
subCommands: { create: gameCreateCommand },
|
|
80
117
|
})
|
|
81
118
|
|
|
119
|
+
/** Warn that a top-level command moved under `puzzmo games`, then run it anyway. */
|
|
120
|
+
const movedToGames = (name: string) => console.warn(`"puzzmo ${name}" is now "puzzmo games ${name}". The old form still works for now.`)
|
|
121
|
+
|
|
122
|
+
// Deprecated top-level aliases, kept hidden so existing scripts (e.g. `puzzmo upload`) keep working.
|
|
123
|
+
const uploadAlias = defineCommand({
|
|
124
|
+
meta: { ...uploadCommand.meta, hidden: true },
|
|
125
|
+
args: uploadCommand.args,
|
|
126
|
+
run: ({ args }) => {
|
|
127
|
+
movedToGames("upload")
|
|
128
|
+
return upload(args.dir, { verbose: args.verbose })
|
|
129
|
+
},
|
|
130
|
+
})
|
|
131
|
+
|
|
132
|
+
const validateAlias = defineCommand({
|
|
133
|
+
meta: { ...validateCommand.meta, hidden: true },
|
|
134
|
+
args: validateCommand.args,
|
|
135
|
+
run: ({ args }) => {
|
|
136
|
+
movedToGames("validate")
|
|
137
|
+
return validate(args.dir)
|
|
138
|
+
},
|
|
139
|
+
})
|
|
140
|
+
|
|
82
141
|
const main = defineCommand({
|
|
83
142
|
meta: { name: "puzzmo", description: "Puzzmo CLI" },
|
|
84
143
|
subCommands: {
|
|
85
144
|
login: loginCommand,
|
|
86
|
-
|
|
87
|
-
validate: validateCommand,
|
|
145
|
+
games: gamesCommand,
|
|
88
146
|
migrate: migrateCommand,
|
|
89
147
|
game: gameCommand,
|
|
148
|
+
upload: uploadAlias,
|
|
149
|
+
validate: validateAlias,
|
|
90
150
|
"agent-test": defineCommand({
|
|
91
151
|
meta: { name: "agent-test", description: "Run the agent test harness", hidden: true },
|
|
92
152
|
run: () => agentTest(),
|
|
@@ -0,0 +1,301 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @generated SignedSource<<5f5ceed697fe8720947305fca5196bee>>
|
|
3
|
+
* @lightSyntaxTransform
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
/* tslint:disable */
|
|
7
|
+
/* eslint-disable */
|
|
8
|
+
// @ts-nocheck
|
|
9
|
+
|
|
10
|
+
import { ConcreteRequest } from 'relay-runtime';
|
|
11
|
+
export type cliGameRuntimesQuery$variables = {
|
|
12
|
+
token: string;
|
|
13
|
+
};
|
|
14
|
+
export type cliGameRuntimesQuery$data = {
|
|
15
|
+
readonly teamForToken: {
|
|
16
|
+
readonly games: {
|
|
17
|
+
readonly edges: ReadonlyArray<{
|
|
18
|
+
readonly node: {
|
|
19
|
+
readonly runtime: {
|
|
20
|
+
readonly currentVersion: {
|
|
21
|
+
readonly assetsSha: string;
|
|
22
|
+
} | null | undefined;
|
|
23
|
+
readonly nextVersion: {
|
|
24
|
+
readonly assetsSha: string;
|
|
25
|
+
} | null | undefined;
|
|
26
|
+
readonly previousVersion: {
|
|
27
|
+
readonly assetsSha: string;
|
|
28
|
+
} | null | undefined;
|
|
29
|
+
} | null | undefined;
|
|
30
|
+
readonly slug: string;
|
|
31
|
+
};
|
|
32
|
+
} | null | undefined> | null | undefined;
|
|
33
|
+
};
|
|
34
|
+
readonly slug: string;
|
|
35
|
+
} | null | undefined;
|
|
36
|
+
};
|
|
37
|
+
export type cliGameRuntimesQuery = {
|
|
38
|
+
response: cliGameRuntimesQuery$data;
|
|
39
|
+
variables: cliGameRuntimesQuery$variables;
|
|
40
|
+
};
|
|
41
|
+
|
|
42
|
+
const node: ConcreteRequest = (function(){
|
|
43
|
+
var v0 = [
|
|
44
|
+
{
|
|
45
|
+
"defaultValue": null,
|
|
46
|
+
"kind": "LocalArgument",
|
|
47
|
+
"name": "token"
|
|
48
|
+
}
|
|
49
|
+
],
|
|
50
|
+
v1 = [
|
|
51
|
+
{
|
|
52
|
+
"kind": "Variable",
|
|
53
|
+
"name": "token",
|
|
54
|
+
"variableName": "token"
|
|
55
|
+
}
|
|
56
|
+
],
|
|
57
|
+
v2 = {
|
|
58
|
+
"alias": null,
|
|
59
|
+
"args": null,
|
|
60
|
+
"kind": "ScalarField",
|
|
61
|
+
"name": "slug",
|
|
62
|
+
"storageKey": null
|
|
63
|
+
},
|
|
64
|
+
v3 = [
|
|
65
|
+
{
|
|
66
|
+
"kind": "Literal",
|
|
67
|
+
"name": "first",
|
|
68
|
+
"value": 500
|
|
69
|
+
}
|
|
70
|
+
],
|
|
71
|
+
v4 = {
|
|
72
|
+
"alias": null,
|
|
73
|
+
"args": null,
|
|
74
|
+
"kind": "ScalarField",
|
|
75
|
+
"name": "assetsSha",
|
|
76
|
+
"storageKey": null
|
|
77
|
+
},
|
|
78
|
+
v5 = [
|
|
79
|
+
(v4/*:: as any*/)
|
|
80
|
+
],
|
|
81
|
+
v6 = {
|
|
82
|
+
"alias": null,
|
|
83
|
+
"args": null,
|
|
84
|
+
"kind": "ScalarField",
|
|
85
|
+
"name": "id",
|
|
86
|
+
"storageKey": null
|
|
87
|
+
},
|
|
88
|
+
v7 = [
|
|
89
|
+
(v4/*:: as any*/),
|
|
90
|
+
(v6/*:: as any*/)
|
|
91
|
+
];
|
|
92
|
+
return {
|
|
93
|
+
"fragment": {
|
|
94
|
+
"argumentDefinitions": (v0/*:: as any*/),
|
|
95
|
+
"kind": "Fragment",
|
|
96
|
+
"metadata": null,
|
|
97
|
+
"name": "cliGameRuntimesQuery",
|
|
98
|
+
"selections": [
|
|
99
|
+
{
|
|
100
|
+
"alias": null,
|
|
101
|
+
"args": (v1/*:: as any*/),
|
|
102
|
+
"concreteType": "Team",
|
|
103
|
+
"kind": "LinkedField",
|
|
104
|
+
"name": "teamForToken",
|
|
105
|
+
"plural": false,
|
|
106
|
+
"selections": [
|
|
107
|
+
(v2/*:: as any*/),
|
|
108
|
+
{
|
|
109
|
+
"alias": null,
|
|
110
|
+
"args": (v3/*:: as any*/),
|
|
111
|
+
"concreteType": "GameConnection",
|
|
112
|
+
"kind": "LinkedField",
|
|
113
|
+
"name": "games",
|
|
114
|
+
"plural": false,
|
|
115
|
+
"selections": [
|
|
116
|
+
{
|
|
117
|
+
"alias": null,
|
|
118
|
+
"args": null,
|
|
119
|
+
"concreteType": "GameEdge",
|
|
120
|
+
"kind": "LinkedField",
|
|
121
|
+
"name": "edges",
|
|
122
|
+
"plural": true,
|
|
123
|
+
"selections": [
|
|
124
|
+
{
|
|
125
|
+
"alias": null,
|
|
126
|
+
"args": null,
|
|
127
|
+
"concreteType": "Game",
|
|
128
|
+
"kind": "LinkedField",
|
|
129
|
+
"name": "node",
|
|
130
|
+
"plural": false,
|
|
131
|
+
"selections": [
|
|
132
|
+
(v2/*:: as any*/),
|
|
133
|
+
{
|
|
134
|
+
"alias": null,
|
|
135
|
+
"args": null,
|
|
136
|
+
"concreteType": "GameRuntime",
|
|
137
|
+
"kind": "LinkedField",
|
|
138
|
+
"name": "runtime",
|
|
139
|
+
"plural": false,
|
|
140
|
+
"selections": [
|
|
141
|
+
{
|
|
142
|
+
"alias": null,
|
|
143
|
+
"args": null,
|
|
144
|
+
"concreteType": "GameRuntimeVersion",
|
|
145
|
+
"kind": "LinkedField",
|
|
146
|
+
"name": "currentVersion",
|
|
147
|
+
"plural": false,
|
|
148
|
+
"selections": (v5/*:: as any*/),
|
|
149
|
+
"storageKey": null
|
|
150
|
+
},
|
|
151
|
+
{
|
|
152
|
+
"alias": null,
|
|
153
|
+
"args": null,
|
|
154
|
+
"concreteType": "GameRuntimeVersion",
|
|
155
|
+
"kind": "LinkedField",
|
|
156
|
+
"name": "nextVersion",
|
|
157
|
+
"plural": false,
|
|
158
|
+
"selections": (v5/*:: as any*/),
|
|
159
|
+
"storageKey": null
|
|
160
|
+
},
|
|
161
|
+
{
|
|
162
|
+
"alias": null,
|
|
163
|
+
"args": null,
|
|
164
|
+
"concreteType": "GameRuntimeVersion",
|
|
165
|
+
"kind": "LinkedField",
|
|
166
|
+
"name": "previousVersion",
|
|
167
|
+
"plural": false,
|
|
168
|
+
"selections": (v5/*:: as any*/),
|
|
169
|
+
"storageKey": null
|
|
170
|
+
}
|
|
171
|
+
],
|
|
172
|
+
"storageKey": null
|
|
173
|
+
}
|
|
174
|
+
],
|
|
175
|
+
"storageKey": null
|
|
176
|
+
}
|
|
177
|
+
],
|
|
178
|
+
"storageKey": null
|
|
179
|
+
}
|
|
180
|
+
],
|
|
181
|
+
"storageKey": "games(first:500)"
|
|
182
|
+
}
|
|
183
|
+
],
|
|
184
|
+
"storageKey": null
|
|
185
|
+
}
|
|
186
|
+
],
|
|
187
|
+
"type": "Query",
|
|
188
|
+
"abstractKey": null
|
|
189
|
+
},
|
|
190
|
+
"kind": "Request",
|
|
191
|
+
"operation": {
|
|
192
|
+
"argumentDefinitions": (v0/*:: as any*/),
|
|
193
|
+
"kind": "Operation",
|
|
194
|
+
"name": "cliGameRuntimesQuery",
|
|
195
|
+
"selections": [
|
|
196
|
+
{
|
|
197
|
+
"alias": null,
|
|
198
|
+
"args": (v1/*:: as any*/),
|
|
199
|
+
"concreteType": "Team",
|
|
200
|
+
"kind": "LinkedField",
|
|
201
|
+
"name": "teamForToken",
|
|
202
|
+
"plural": false,
|
|
203
|
+
"selections": [
|
|
204
|
+
(v2/*:: as any*/),
|
|
205
|
+
{
|
|
206
|
+
"alias": null,
|
|
207
|
+
"args": (v3/*:: as any*/),
|
|
208
|
+
"concreteType": "GameConnection",
|
|
209
|
+
"kind": "LinkedField",
|
|
210
|
+
"name": "games",
|
|
211
|
+
"plural": false,
|
|
212
|
+
"selections": [
|
|
213
|
+
{
|
|
214
|
+
"alias": null,
|
|
215
|
+
"args": null,
|
|
216
|
+
"concreteType": "GameEdge",
|
|
217
|
+
"kind": "LinkedField",
|
|
218
|
+
"name": "edges",
|
|
219
|
+
"plural": true,
|
|
220
|
+
"selections": [
|
|
221
|
+
{
|
|
222
|
+
"alias": null,
|
|
223
|
+
"args": null,
|
|
224
|
+
"concreteType": "Game",
|
|
225
|
+
"kind": "LinkedField",
|
|
226
|
+
"name": "node",
|
|
227
|
+
"plural": false,
|
|
228
|
+
"selections": [
|
|
229
|
+
(v2/*:: as any*/),
|
|
230
|
+
{
|
|
231
|
+
"alias": null,
|
|
232
|
+
"args": null,
|
|
233
|
+
"concreteType": "GameRuntime",
|
|
234
|
+
"kind": "LinkedField",
|
|
235
|
+
"name": "runtime",
|
|
236
|
+
"plural": false,
|
|
237
|
+
"selections": [
|
|
238
|
+
{
|
|
239
|
+
"alias": null,
|
|
240
|
+
"args": null,
|
|
241
|
+
"concreteType": "GameRuntimeVersion",
|
|
242
|
+
"kind": "LinkedField",
|
|
243
|
+
"name": "currentVersion",
|
|
244
|
+
"plural": false,
|
|
245
|
+
"selections": (v7/*:: as any*/),
|
|
246
|
+
"storageKey": null
|
|
247
|
+
},
|
|
248
|
+
{
|
|
249
|
+
"alias": null,
|
|
250
|
+
"args": null,
|
|
251
|
+
"concreteType": "GameRuntimeVersion",
|
|
252
|
+
"kind": "LinkedField",
|
|
253
|
+
"name": "nextVersion",
|
|
254
|
+
"plural": false,
|
|
255
|
+
"selections": (v7/*:: as any*/),
|
|
256
|
+
"storageKey": null
|
|
257
|
+
},
|
|
258
|
+
{
|
|
259
|
+
"alias": null,
|
|
260
|
+
"args": null,
|
|
261
|
+
"concreteType": "GameRuntimeVersion",
|
|
262
|
+
"kind": "LinkedField",
|
|
263
|
+
"name": "previousVersion",
|
|
264
|
+
"plural": false,
|
|
265
|
+
"selections": (v7/*:: as any*/),
|
|
266
|
+
"storageKey": null
|
|
267
|
+
},
|
|
268
|
+
(v6/*:: as any*/)
|
|
269
|
+
],
|
|
270
|
+
"storageKey": null
|
|
271
|
+
},
|
|
272
|
+
(v6/*:: as any*/)
|
|
273
|
+
],
|
|
274
|
+
"storageKey": null
|
|
275
|
+
}
|
|
276
|
+
],
|
|
277
|
+
"storageKey": null
|
|
278
|
+
}
|
|
279
|
+
],
|
|
280
|
+
"storageKey": "games(first:500)"
|
|
281
|
+
},
|
|
282
|
+
(v6/*:: as any*/)
|
|
283
|
+
],
|
|
284
|
+
"storageKey": null
|
|
285
|
+
}
|
|
286
|
+
]
|
|
287
|
+
},
|
|
288
|
+
"params": {
|
|
289
|
+
"cacheID": "169b76dbd259ac615d2b712641a54087",
|
|
290
|
+
"id": null,
|
|
291
|
+
"metadata": {},
|
|
292
|
+
"name": "cliGameRuntimesQuery",
|
|
293
|
+
"operationKind": "query",
|
|
294
|
+
"text": "query cliGameRuntimesQuery(\n $token: String!\n) {\n teamForToken(token: $token) {\n slug\n games(first: 500) {\n edges {\n node {\n slug\n runtime {\n currentVersion {\n assetsSha\n id\n }\n nextVersion {\n assetsSha\n id\n }\n previousVersion {\n assetsSha\n id\n }\n id\n }\n id\n }\n }\n }\n id\n }\n}\n"
|
|
295
|
+
}
|
|
296
|
+
};
|
|
297
|
+
})();
|
|
298
|
+
|
|
299
|
+
(node as any).hash = "6467f74d92adb125c762ed24be7d4ac8";
|
|
300
|
+
|
|
301
|
+
export default node;
|
|
@@ -0,0 +1,64 @@
|
|
|
1
|
+
import { graphql, query } from "../util/belay.js"
|
|
2
|
+
import type { cliGameRuntimesQuery } from "./__generated__/cliGameRuntimesQuery.graphql.js"
|
|
3
|
+
|
|
4
|
+
const gameRuntimesQuery = graphql`
|
|
5
|
+
query cliGameRuntimesQuery($token: String!) {
|
|
6
|
+
teamForToken(token: $token) {
|
|
7
|
+
slug
|
|
8
|
+
games(first: 500) {
|
|
9
|
+
edges {
|
|
10
|
+
node {
|
|
11
|
+
slug
|
|
12
|
+
runtime {
|
|
13
|
+
currentVersion {
|
|
14
|
+
assetsSha
|
|
15
|
+
}
|
|
16
|
+
nextVersion {
|
|
17
|
+
assetsSha
|
|
18
|
+
}
|
|
19
|
+
previousVersion {
|
|
20
|
+
assetsSha
|
|
21
|
+
}
|
|
22
|
+
}
|
|
23
|
+
}
|
|
24
|
+
}
|
|
25
|
+
}
|
|
26
|
+
}
|
|
27
|
+
}
|
|
28
|
+
`
|
|
29
|
+
|
|
30
|
+
/** The deployed runtime build SHAs for a game, read from its runtime's current/next/previous version slots. */
|
|
31
|
+
export type GameVersions = {
|
|
32
|
+
/** SHA of the live/current runtime build */
|
|
33
|
+
current: string | null
|
|
34
|
+
/** SHA of the staged "next" runtime build, if one exists */
|
|
35
|
+
next: string | null
|
|
36
|
+
/** SHA of the previous runtime build, if one exists */
|
|
37
|
+
previous: string | null
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
/**
|
|
41
|
+
* Fetches the runtime build SHAs for every game in the token's team, keyed by game slug. The token is sent both as
|
|
42
|
+
* the `teamForToken` argument and as the Bearer header — the latter is what unlocks the team-gated `runtime` field.
|
|
43
|
+
*/
|
|
44
|
+
export const fetchTeamGameVersions = async (apiURL: string, token: string): Promise<Map<string, GameVersions>> => {
|
|
45
|
+
const { data, errors } = await query<cliGameRuntimesQuery>(gameRuntimesQuery, {
|
|
46
|
+
variables: { token },
|
|
47
|
+
url: `${apiURL}/graphql`,
|
|
48
|
+
headers: { Authorization: `Bearer ${token}` },
|
|
49
|
+
})
|
|
50
|
+
|
|
51
|
+
if (errors?.length) throw new Error(`teamForToken failed: ${errors.map((e) => e.message).join("; ")}`)
|
|
52
|
+
|
|
53
|
+
const versions = new Map<string, GameVersions>()
|
|
54
|
+
for (const edge of data?.teamForToken?.games?.edges ?? []) {
|
|
55
|
+
const game = edge?.node
|
|
56
|
+
if (!game) continue
|
|
57
|
+
versions.set(game.slug, {
|
|
58
|
+
current: game.runtime?.currentVersion?.assetsSha || null,
|
|
59
|
+
next: game.runtime?.nextVersion?.assetsSha || null,
|
|
60
|
+
previous: game.runtime?.previousVersion?.assetsSha || null,
|
|
61
|
+
})
|
|
62
|
+
}
|
|
63
|
+
return versions
|
|
64
|
+
}
|
package/src/util/api.ts
CHANGED
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
// "belay lite" — a local copy of the parts of @puzzmo-com/belay the CLI needs.
|
|
2
|
+
// The CLI is published to npm and ships unbundled, so it can't depend on the private,
|
|
3
|
+
// source-only belay package; this vendors the tiny query/graphql helpers instead.
|
|
4
|
+
// The `graphql` tag lets the Relay compiler validate these queries and generate types
|
|
5
|
+
// (see the "cli" project in relay.config.json). Keep this in sync with packages/belay.
|
|
6
|
+
|
|
7
|
+
/** Standard GraphQL error shape */
|
|
8
|
+
export type GraphQLError = {
|
|
9
|
+
message: string
|
|
10
|
+
locations?: Array<{ line: number; column: number }>
|
|
11
|
+
path?: Array<string | number>
|
|
12
|
+
extensions?: Record<string, unknown>
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
/** GraphQL response shape */
|
|
16
|
+
export type GraphQLResponse<TData> = {
|
|
17
|
+
data?: TData
|
|
18
|
+
errors?: GraphQLError[]
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
/** Operation type with variables and response — matches Relay's generated operation types */
|
|
22
|
+
export type OperationType = {
|
|
23
|
+
variables: Record<string, unknown>
|
|
24
|
+
response: unknown
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
/** Request options for the query function */
|
|
28
|
+
export type RequestOptions<TVariables> = {
|
|
29
|
+
variables: TVariables
|
|
30
|
+
url: string
|
|
31
|
+
headers?: HeadersInit
|
|
32
|
+
signal?: AbortSignal
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
/**
|
|
36
|
+
* A graphql tagged template literal that returns the query string. This lets the Relay
|
|
37
|
+
* compiler pick up and validate the operation (and generate its types) without bundling relay-runtime.
|
|
38
|
+
*/
|
|
39
|
+
export const graphql = (strings: TemplateStringsArray): string => strings[0]!
|
|
40
|
+
|
|
41
|
+
/** Execute a GraphQL query via fetch, typed with a Relay-generated operation type. */
|
|
42
|
+
export const query = async <T extends OperationType>(
|
|
43
|
+
queryString: string,
|
|
44
|
+
options: RequestOptions<T["variables"]>,
|
|
45
|
+
): Promise<GraphQLResponse<T["response"]>> => {
|
|
46
|
+
const { variables, url, headers, signal } = options
|
|
47
|
+
|
|
48
|
+
const response = await fetch(url, {
|
|
49
|
+
method: "POST",
|
|
50
|
+
headers: { "Content-Type": "application/json", ...headers },
|
|
51
|
+
signal,
|
|
52
|
+
body: JSON.stringify({ query: queryString, variables }),
|
|
53
|
+
})
|
|
54
|
+
|
|
55
|
+
return response.json() as Promise<GraphQLResponse<T["response"]>>
|
|
56
|
+
}
|
package/src/util/createGame.ts
CHANGED
|
@@ -25,6 +25,9 @@ export type CreateUserGameOptions = {
|
|
|
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
|
|
28
|
+
oneliner?: string
|
|
29
|
+
description?: string
|
|
30
|
+
highlightColor?: string
|
|
28
31
|
}
|
|
29
32
|
|
|
30
33
|
export type CreatedGame = { id: string; slug: string; displayName: string; newAccessToken: string | null }
|
|
@@ -40,7 +43,12 @@ export const createUserGame = async (options: CreateUserGameOptions): Promise<Cr
|
|
|
40
43
|
query: createUserGameMutation,
|
|
41
44
|
variables: {
|
|
42
45
|
teamAccessToken: options.teamAccessToken,
|
|
43
|
-
input: {
|
|
46
|
+
input: {
|
|
47
|
+
displayName: options.displayName,
|
|
48
|
+
oneliner: options.oneliner,
|
|
49
|
+
description: options.description,
|
|
50
|
+
highlightColor: options.highlightColor,
|
|
51
|
+
},
|
|
44
52
|
},
|
|
45
53
|
}),
|
|
46
54
|
})
|
|
@@ -20,7 +20,10 @@ export type DiscoveredGame = {
|
|
|
20
20
|
puzzmoJsonDir: string
|
|
21
21
|
/** Validated puzzmo.json contents */
|
|
22
22
|
puzzmoFile: PuzzmoFile
|
|
23
|
-
/**
|
|
23
|
+
/**
|
|
24
|
+
* Absolute path to the dist directory containing the build artifacts (empty string when discovered with requireDist: false and none was
|
|
25
|
+
* found)
|
|
26
|
+
*/
|
|
24
27
|
distDir: string
|
|
25
28
|
}
|
|
26
29
|
|
|
@@ -37,8 +40,15 @@ export type DiscoveryResult = {
|
|
|
37
40
|
errors: DiscoveryError[]
|
|
38
41
|
}
|
|
39
42
|
|
|
43
|
+
/** Options for {@link discoverGames} */
|
|
44
|
+
export type DiscoverOptions = {
|
|
45
|
+
/** Require each game to have a non-empty dist folder (default true). Set false for commands that run before a build. */
|
|
46
|
+
requireDist?: boolean
|
|
47
|
+
}
|
|
48
|
+
|
|
40
49
|
/** Walks `rootDir` looking for puzzmo.json files, validates each, and resolves their dist directory */
|
|
41
|
-
export const discoverGames = async (rootDir: string): Promise<DiscoveryResult> => {
|
|
50
|
+
export const discoverGames = async (rootDir: string, options: DiscoverOptions = {}): Promise<DiscoveryResult> => {
|
|
51
|
+
const { requireDist = true } = options
|
|
42
52
|
const root = path.resolve(rootDir)
|
|
43
53
|
const puzzmoJsonPaths = findPuzzmoJsonFiles(root)
|
|
44
54
|
|
|
@@ -66,18 +76,20 @@ export const discoverGames = async (rootDir: string): Promise<DiscoveryResult> =
|
|
|
66
76
|
const puzzmoJsonDir = path.dirname(puzzmoJsonPath)
|
|
67
77
|
const distDir = resolveDistDir(puzzmoFile, puzzmoJsonDir, root)
|
|
68
78
|
|
|
69
|
-
if (
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
79
|
+
if (requireDist) {
|
|
80
|
+
if (!distDir) {
|
|
81
|
+
fileErrors.push(`Could not find a dist/build folder for ${puzzmoFile.game.slug}. Set "output.dir" in puzzmo.json.`)
|
|
82
|
+
} else if (!hasFiles(distDir)) {
|
|
83
|
+
fileErrors.push(`Dist folder is empty: ${distDir}`)
|
|
84
|
+
}
|
|
74
85
|
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
86
|
+
if (fileErrors.length) {
|
|
87
|
+
errors.push({ puzzmoJsonPath, slug: puzzmoFile.game.slug, errors: fileErrors })
|
|
88
|
+
continue
|
|
89
|
+
}
|
|
78
90
|
}
|
|
79
91
|
|
|
80
|
-
games.push({ puzzmoJsonPath, puzzmoJsonDir, puzzmoFile, distDir: distDir
|
|
92
|
+
games.push({ puzzmoJsonPath, puzzmoJsonDir, puzzmoFile, distDir: distDir ?? "" })
|
|
81
93
|
}
|
|
82
94
|
|
|
83
95
|
return { games, errors }
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Converts a string to a URL-friendly slug. Mirrors packages/shared/slugify.ts —
|
|
3
|
+
* the CLI is standalone and does not depend on @puzzmo-com/shared, so the host's
|
|
4
|
+
* slug derivation is replicated here. Keep in sync with the shared version: the
|
|
5
|
+
* server derives a new game's slug from its displayName the same way, and
|
|
6
|
+
* `upload` uses this to refuse creating a game whose puzzmo.json slug wouldn't match.
|
|
7
|
+
*/
|
|
8
|
+
export const slugify = (text: string): string =>
|
|
9
|
+
text
|
|
10
|
+
.toString()
|
|
11
|
+
.normalize("NFD")
|
|
12
|
+
.replace(/[\u0300-\u036f]/g, "")
|
|
13
|
+
.toLowerCase()
|
|
14
|
+
.trim()
|
|
15
|
+
.replace(/\s+/g, "-")
|
|
16
|
+
.replace(/[^\w-]+/g, "")
|
|
17
|
+
.replace(/--+/g, "-")
|