@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
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
/** Standard GraphQL error shape */
|
|
2
|
+
export type GraphQLError = {
|
|
3
|
+
message: string;
|
|
4
|
+
locations?: Array<{
|
|
5
|
+
line: number;
|
|
6
|
+
column: number;
|
|
7
|
+
}>;
|
|
8
|
+
path?: Array<string | number>;
|
|
9
|
+
extensions?: Record<string, unknown>;
|
|
10
|
+
};
|
|
11
|
+
/** GraphQL response shape */
|
|
12
|
+
export type GraphQLResponse<TData> = {
|
|
13
|
+
data?: TData;
|
|
14
|
+
errors?: GraphQLError[];
|
|
15
|
+
};
|
|
16
|
+
/** Operation type with variables and response — matches Relay's generated operation types */
|
|
17
|
+
export type OperationType = {
|
|
18
|
+
variables: Record<string, unknown>;
|
|
19
|
+
response: unknown;
|
|
20
|
+
};
|
|
21
|
+
/** Request options for the query function */
|
|
22
|
+
export type RequestOptions<TVariables> = {
|
|
23
|
+
variables: TVariables;
|
|
24
|
+
url: string;
|
|
25
|
+
headers?: HeadersInit;
|
|
26
|
+
signal?: AbortSignal;
|
|
27
|
+
};
|
|
28
|
+
/**
|
|
29
|
+
* A graphql tagged template literal that returns the query string. This lets the Relay
|
|
30
|
+
* compiler pick up and validate the operation (and generate its types) without bundling relay-runtime.
|
|
31
|
+
*/
|
|
32
|
+
export declare const graphql: (strings: TemplateStringsArray) => string;
|
|
33
|
+
/** Execute a GraphQL query via fetch, typed with a Relay-generated operation type. */
|
|
34
|
+
export declare const query: <T extends OperationType>(queryString: string, options: RequestOptions<T["variables"]>) => Promise<GraphQLResponse<T["response"]>>;
|
|
@@ -0,0 +1,22 @@
|
|
|
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
|
+
* A graphql tagged template literal that returns the query string. This lets the Relay
|
|
8
|
+
* compiler pick up and validate the operation (and generate its types) without bundling relay-runtime.
|
|
9
|
+
*/
|
|
10
|
+
export const graphql = (strings) => strings[0];
|
|
11
|
+
/** Execute a GraphQL query via fetch, typed with a Relay-generated operation type. */
|
|
12
|
+
export const query = async (queryString, options) => {
|
|
13
|
+
const { variables, url, headers, signal } = options;
|
|
14
|
+
const response = await fetch(url, {
|
|
15
|
+
method: "POST",
|
|
16
|
+
headers: { "Content-Type": "application/json", ...headers },
|
|
17
|
+
signal,
|
|
18
|
+
body: JSON.stringify({ query: queryString, variables }),
|
|
19
|
+
});
|
|
20
|
+
return response.json();
|
|
21
|
+
};
|
|
22
|
+
//# sourceMappingURL=belay.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"belay.js","sourceRoot":"","sources":["../../src/util/belay.ts"],"names":[],"mappings":"AAAA,iFAA+E;AAC/E,sFAAsF;AACtF,kFAAkF;AAClF,sFAAsF;AACtF,uFAAuF;AA8BvF;;;GAGG;AACH,MAAM,CAAC,MAAM,OAAO,GAAG,CAAC,OAA6B,EAAU,EAAE,CAAC,OAAO,CAAC,CAAC,CAAE,CAAA;AAE7E,sFAAsF;AACtF,MAAM,CAAC,MAAM,KAAK,GAAG,KAAK,EACxB,WAAmB,EACnB,OAAuC,EACE,EAAE,CAAC;IAC5C,MAAM,EAAE,SAAS,EAAE,GAAG,EAAE,OAAO,EAAE,MAAM,EAAE,GAAG,OAAO,CAAA;IAEnD,MAAM,QAAQ,GAAG,MAAM,KAAK,CAAC,GAAG,EAAE;QAChC,MAAM,EAAE,MAAM;QACd,OAAO,EAAE,EAAE,cAAc,EAAE,kBAAkB,EAAE,GAAG,OAAO,EAAE;QAC3D,MAAM;QACN,IAAI,EAAE,IAAI,CAAC,SAAS,CAAC,EAAE,KAAK,EAAE,WAAW,EAAE,SAAS,EAAE,CAAC;KACxD,CAAC,CAAA;IAEF,OAAO,QAAQ,CAAC,IAAI,EAA6C,CAAA;AAAA,CAClE,CAAA"}
|
package/lib/util/createGame.d.ts
CHANGED
|
@@ -4,6 +4,9 @@ export type CreateUserGameOptions = {
|
|
|
4
4
|
/** A teamAccessToken (the same token saved by `puzzmo login`). The mutation uses it to identify the team. */
|
|
5
5
|
teamAccessToken: string;
|
|
6
6
|
displayName: string;
|
|
7
|
+
oneliner?: string;
|
|
8
|
+
description?: string;
|
|
9
|
+
highlightColor?: string;
|
|
7
10
|
};
|
|
8
11
|
export type CreatedGame = {
|
|
9
12
|
id: string;
|
package/lib/util/createGame.js
CHANGED
|
@@ -16,7 +16,12 @@ export const createUserGame = async (options) => {
|
|
|
16
16
|
query: createUserGameMutation,
|
|
17
17
|
variables: {
|
|
18
18
|
teamAccessToken: options.teamAccessToken,
|
|
19
|
-
input: {
|
|
19
|
+
input: {
|
|
20
|
+
displayName: options.displayName,
|
|
21
|
+
oneliner: options.oneliner,
|
|
22
|
+
description: options.description,
|
|
23
|
+
highlightColor: options.highlightColor,
|
|
24
|
+
},
|
|
20
25
|
},
|
|
21
26
|
}),
|
|
22
27
|
});
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"createGame.js","sourceRoot":"","sources":["../../src/util/createGame.ts"],"names":[],"mappings":"AAAA,MAAM,sBAAsB,GAAG;;;;;;;CAO9B,CAAA;
|
|
1
|
+
{"version":3,"file":"createGame.js","sourceRoot":"","sources":["../../src/util/createGame.ts"],"names":[],"mappings":"AAAA,MAAM,sBAAsB,GAAG;;;;;;;CAO9B,CAAA;AA2BD,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;oBACL,WAAW,EAAE,OAAO,CAAC,WAAW;oBAChC,QAAQ,EAAE,OAAO,CAAC,QAAQ;oBAC1B,WAAW,EAAE,OAAO,CAAC,WAAW;oBAChC,cAAc,EAAE,OAAO,CAAC,cAAc;iBACvC;aACF;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"}
|
|
@@ -7,7 +7,10 @@ export type DiscoveredGame = {
|
|
|
7
7
|
puzzmoJsonDir: string;
|
|
8
8
|
/** Validated puzzmo.json contents */
|
|
9
9
|
puzzmoFile: PuzzmoFile;
|
|
10
|
-
/**
|
|
10
|
+
/**
|
|
11
|
+
* Absolute path to the dist directory containing the build artifacts (empty string when discovered with requireDist: false and none was
|
|
12
|
+
* found)
|
|
13
|
+
*/
|
|
11
14
|
distDir: string;
|
|
12
15
|
};
|
|
13
16
|
/** A puzzmo.json that was found but could not be used (invalid JSON, schema errors, missing dist) */
|
|
@@ -21,5 +24,10 @@ export type DiscoveryResult = {
|
|
|
21
24
|
games: DiscoveredGame[];
|
|
22
25
|
errors: DiscoveryError[];
|
|
23
26
|
};
|
|
27
|
+
/** Options for {@link discoverGames} */
|
|
28
|
+
export type DiscoverOptions = {
|
|
29
|
+
/** Require each game to have a non-empty dist folder (default true). Set false for commands that run before a build. */
|
|
30
|
+
requireDist?: boolean;
|
|
31
|
+
};
|
|
24
32
|
/** Walks `rootDir` looking for puzzmo.json files, validates each, and resolves their dist directory */
|
|
25
|
-
export declare const discoverGames: (rootDir: string) => Promise<DiscoveryResult>;
|
|
33
|
+
export declare const discoverGames: (rootDir: string, options?: DiscoverOptions) => Promise<DiscoveryResult>;
|
|
@@ -7,7 +7,8 @@ const ignoredDirs = new Set(["node_modules", ".git", ".turbo", ".next", ".cache"
|
|
|
7
7
|
/** Folder names that are searched (in order) when resolving a game's dist directory */
|
|
8
8
|
const outputDirNames = ["dist", "build", "built", "output"];
|
|
9
9
|
/** Walks `rootDir` looking for puzzmo.json files, validates each, and resolves their dist directory */
|
|
10
|
-
export const discoverGames = async (rootDir) => {
|
|
10
|
+
export const discoverGames = async (rootDir, options = {}) => {
|
|
11
|
+
const { requireDist = true } = options;
|
|
11
12
|
const root = path.resolve(rootDir);
|
|
12
13
|
const puzzmoJsonPaths = findPuzzmoJsonFiles(root);
|
|
13
14
|
const games = [];
|
|
@@ -30,17 +31,19 @@ export const discoverGames = async (rootDir) => {
|
|
|
30
31
|
const puzzmoFile = validation.data;
|
|
31
32
|
const puzzmoJsonDir = path.dirname(puzzmoJsonPath);
|
|
32
33
|
const distDir = resolveDistDir(puzzmoFile, puzzmoJsonDir, root);
|
|
33
|
-
if (
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
34
|
+
if (requireDist) {
|
|
35
|
+
if (!distDir) {
|
|
36
|
+
fileErrors.push(`Could not find a dist/build folder for ${puzzmoFile.game.slug}. Set "output.dir" in puzzmo.json.`);
|
|
37
|
+
}
|
|
38
|
+
else if (!hasFiles(distDir)) {
|
|
39
|
+
fileErrors.push(`Dist folder is empty: ${distDir}`);
|
|
40
|
+
}
|
|
41
|
+
if (fileErrors.length) {
|
|
42
|
+
errors.push({ puzzmoJsonPath, slug: puzzmoFile.game.slug, errors: fileErrors });
|
|
43
|
+
continue;
|
|
44
|
+
}
|
|
42
45
|
}
|
|
43
|
-
games.push({ puzzmoJsonPath, puzzmoJsonDir, puzzmoFile, distDir: distDir });
|
|
46
|
+
games.push({ puzzmoJsonPath, puzzmoJsonDir, puzzmoFile, distDir: distDir ?? "" });
|
|
44
47
|
}
|
|
45
48
|
return { games, errors };
|
|
46
49
|
};
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"discoverGames.js","sourceRoot":"","sources":["../../src/util/discoverGames.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,MAAM,SAAS,CAAA;AACxB,OAAO,IAAI,MAAM,WAAW,CAAA;AAE5B,OAAO,EAAmB,KAAK,IAAI,UAAU,EAAE,mBAAmB,EAAE,MAAM,cAAc,CAAA;AAGxF,OAAO,EAAE,kBAAkB,EAAE,MAAM,yBAAyB,CAAA;AAE5D,iFAAiF;AACjF,MAAM,WAAW,GAAG,IAAI,GAAG,CAAC,CAAC,cAAc,EAAE,MAAM,EAAE,QAAQ,EAAE,OAAO,EAAE,QAAQ,EAAE,MAAM,EAAE,OAAO,EAAE,OAAO,EAAE,QAAQ,CAAC,CAAC,CAAA;AAEtH,uFAAuF;AACvF,MAAM,cAAc,GAAG,CAAC,MAAM,EAAE,OAAO,EAAE,OAAO,EAAE,QAAQ,CAAC,CAAA;
|
|
1
|
+
{"version":3,"file":"discoverGames.js","sourceRoot":"","sources":["../../src/util/discoverGames.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,8 @@
|
|
|
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 declare const slugify: (text: string) => string;
|
|
@@ -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) => text
|
|
9
|
+
.toString()
|
|
10
|
+
.normalize("NFD")
|
|
11
|
+
.replace(/[\u0300-\u036f]/g, "")
|
|
12
|
+
.toLowerCase()
|
|
13
|
+
.trim()
|
|
14
|
+
.replace(/\s+/g, "-")
|
|
15
|
+
.replace(/[^\w-]+/g, "")
|
|
16
|
+
.replace(/--+/g, "-");
|
|
17
|
+
//# sourceMappingURL=slugify.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"slugify.js","sourceRoot":"","sources":["../../src/util/slugify.ts"],"names":[],"mappings":"AAAA;;;;;;GAMG;AACH,MAAM,CAAC,MAAM,OAAO,GAAG,CAAC,IAAY,EAAU,EAAE,CAC9C,IAAI;KACD,QAAQ,EAAE;KACV,SAAS,CAAC,KAAK,CAAC;KAChB,OAAO,CAAC,kBAAkB,EAAE,EAAE,CAAC;KAC/B,WAAW,EAAE;KACb,IAAI,EAAE;KACN,OAAO,CAAC,MAAM,EAAE,GAAG,CAAC;KACpB,OAAO,CAAC,UAAU,EAAE,EAAE,CAAC;KACvB,OAAO,CAAC,MAAM,EAAE,GAAG,CAAC,CAAA"}
|
package/package.json
CHANGED
|
@@ -10,7 +10,7 @@
|
|
|
10
10
|
"properties": {
|
|
11
11
|
"displayName": {
|
|
12
12
|
"type": "string",
|
|
13
|
-
"description": "Display name shown in the UI, use | to indicate where a line break should go"
|
|
13
|
+
"description": "Display name shown in the UI, use | to indicate where a line break should go. Synced to the game on every upload; seeds a newly created game."
|
|
14
14
|
},
|
|
15
15
|
"slug": {
|
|
16
16
|
"type": "string",
|
|
@@ -19,6 +19,18 @@
|
|
|
19
19
|
"teamID": {
|
|
20
20
|
"type": "string",
|
|
21
21
|
"description": "The team ID that owns this game"
|
|
22
|
+
},
|
|
23
|
+
"oneliner": {
|
|
24
|
+
"type": "string",
|
|
25
|
+
"description": "Short tagline for the game. Synced to the game on every upload when present; seeds a newly created game."
|
|
26
|
+
},
|
|
27
|
+
"description": {
|
|
28
|
+
"type": "string",
|
|
29
|
+
"description": "Longer description of the game. Synced to the game on every upload when present; seeds a newly created game."
|
|
30
|
+
},
|
|
31
|
+
"highlightColor": {
|
|
32
|
+
"type": "string",
|
|
33
|
+
"description": "Hex highlight color (e.g. \"#FF8800\"). Synced to the game on every upload when present; seeds a newly created game."
|
|
22
34
|
}
|
|
23
35
|
},
|
|
24
36
|
"required": ["displayName", "slug", "teamID"],
|
|
@@ -39,7 +51,7 @@
|
|
|
39
51
|
},
|
|
40
52
|
"required": ["dir"],
|
|
41
53
|
"additionalProperties": false,
|
|
42
|
-
"description": "Where the built game artifacts live for this game. Used by `puzzmo upload` and `puzzmo validate` to find the dist folder when running from a repo root. If omitted, the CLI looks for a `dist`, `build`, or `built` folder — first next to puzzmo.json, then walking up to the repo root, optionally with `<slug>` as a sub-folder for shared output dirs."
|
|
54
|
+
"description": "Where the built game artifacts live for this game. Used by `puzzmo games upload` and `puzzmo games validate` to find the dist folder when running from a repo root. If omitted, the CLI looks for a `dist`, `build`, or `built` folder — first next to puzzmo.json, then walking up to the repo root, optionally with `<slug>` as a sub-folder for shared output dirs."
|
|
43
55
|
}
|
|
44
56
|
},
|
|
45
57
|
"required": ["game"],
|
|
@@ -0,0 +1,239 @@
|
|
|
1
|
+
import { execFileSync } from "node:child_process"
|
|
2
|
+
import fs from "node:fs"
|
|
3
|
+
import path from "node:path"
|
|
4
|
+
|
|
5
|
+
import { findTokensForTeam, getTokens, resolveServerForTeam, sourceToURL } from "../util/config.js"
|
|
6
|
+
import { type DiscoveredGame, discoverGames } from "../util/discoverGames.js"
|
|
7
|
+
import { fetchTeamGameVersions, type GameVersions } from "../queries/gameRuntimes.js"
|
|
8
|
+
|
|
9
|
+
/** Which deployed build slot to diff against: the team's latest build, the staged next, or the previous one. */
|
|
10
|
+
export type ChangedAgainst = "latest" | "next" | "previous"
|
|
11
|
+
|
|
12
|
+
export type ChangedOptions = {
|
|
13
|
+
json?: boolean
|
|
14
|
+
list?: boolean
|
|
15
|
+
matrix?: boolean
|
|
16
|
+
ref?: string
|
|
17
|
+
against?: ChangedAgainst
|
|
18
|
+
includeUncommitted?: boolean
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
type GameStatus = "changed" | "unchanged" | "new"
|
|
22
|
+
|
|
23
|
+
type ChangedEntry = {
|
|
24
|
+
slug: string
|
|
25
|
+
displayName: string
|
|
26
|
+
teamID: string
|
|
27
|
+
dir: string
|
|
28
|
+
baseSha: string | null
|
|
29
|
+
ref: string
|
|
30
|
+
status: GameStatus
|
|
31
|
+
changedFiles: number
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
type EvalResult = { entry: ChangedEntry } | { error: string }
|
|
35
|
+
|
|
36
|
+
/** Reports which discovered games changed since their last deployed build, scoped per game folder. */
|
|
37
|
+
export const changed = async (dir: string, options: ChangedOptions = {}) => {
|
|
38
|
+
const { json = false, list = false, matrix = false, against = "latest", includeUncommitted = false } = options
|
|
39
|
+
|
|
40
|
+
if (getTokens().length === 0) {
|
|
41
|
+
console.error("Not logged in. Run `puzzmo login <token>` or set PUZZMO_TOKEN.")
|
|
42
|
+
process.exit(1)
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
const rootDir = path.resolve(dir)
|
|
46
|
+
if (!fs.existsSync(rootDir)) {
|
|
47
|
+
console.error(`Directory not found: ${dir}`)
|
|
48
|
+
process.exit(1)
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
const repoRoot = getRepoRoot(rootDir)
|
|
52
|
+
if (!repoRoot) {
|
|
53
|
+
console.error(`Not a git repository: ${rootDir}`)
|
|
54
|
+
process.exit(1)
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
const ref = options.ref || "HEAD"
|
|
58
|
+
const refSha = revParseShort(ref, repoRoot)
|
|
59
|
+
if (!refSha) {
|
|
60
|
+
console.error(`Could not resolve git ref: ${ref}`)
|
|
61
|
+
process.exit(1)
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
const { games, errors: discoveryErrors } = await discoverGames(rootDir, { requireDist: false })
|
|
65
|
+
if (!games.length && !discoveryErrors.length) {
|
|
66
|
+
console.error(`No puzzmo.json files found under ${rootDir}`)
|
|
67
|
+
process.exit(1)
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
const hardErrors = discoveryErrors.map((err) => `${err.slug ?? path.relative(rootDir, err.puzzmoJsonPath)}: ${err.errors.join("; ")}`)
|
|
71
|
+
|
|
72
|
+
const teamVersions = new Map<string, Promise<Map<string, GameVersions>>>()
|
|
73
|
+
const results = await Promise.all(
|
|
74
|
+
games.map((game) => evaluateGame(game, { repoRoot, ref, refSha, against, includeUncommitted, teamVersions })),
|
|
75
|
+
)
|
|
76
|
+
const report: ChangedEntry[] = []
|
|
77
|
+
for (const result of results) {
|
|
78
|
+
if ("entry" in result) report.push(result.entry)
|
|
79
|
+
else hardErrors.push(result.error)
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
const buildable = report.filter((entry) => entry.status !== "unchanged")
|
|
83
|
+
|
|
84
|
+
if (json) console.log(JSON.stringify(report, null, 2))
|
|
85
|
+
else if (matrix) console.log(JSON.stringify({ include: buildable.map((entry) => ({ dir: entry.dir, slug: entry.slug })) }))
|
|
86
|
+
else if (list) for (const entry of buildable) console.log(entry.dir)
|
|
87
|
+
else if (report.length) printTable(report)
|
|
88
|
+
else if (!hardErrors.length) console.log(`No puzzmo.json files found under ${rootDir}`)
|
|
89
|
+
|
|
90
|
+
if (hardErrors.length) {
|
|
91
|
+
console.error(`\n${hardErrors.length} error${hardErrors.length === 1 ? "" : "s"}:`)
|
|
92
|
+
for (const message of hardErrors) console.error(` ${message}`)
|
|
93
|
+
process.exit(1)
|
|
94
|
+
}
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
type EvalContext = {
|
|
98
|
+
repoRoot: string
|
|
99
|
+
ref: string
|
|
100
|
+
refSha: string
|
|
101
|
+
against: ChangedAgainst
|
|
102
|
+
includeUncommitted: boolean
|
|
103
|
+
// Memoizes the per-team runtime fetch so games sharing a team only hit the API once.
|
|
104
|
+
teamVersions: Map<string, Promise<Map<string, GameVersions>>>
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
/** Resolves one game's deployed SHA and diffs it against the ref. Returns either a report entry or a hard error. */
|
|
108
|
+
const evaluateGame = async (game: DiscoveredGame, ctx: EvalContext): Promise<EvalResult> => {
|
|
109
|
+
const { slug, teamID } = game.puzzmoFile.game
|
|
110
|
+
const dir = toPosix(path.relative(ctx.repoRoot, game.puzzmoJsonDir)) || "."
|
|
111
|
+
|
|
112
|
+
const credential = await resolveServerForTeam(teamID)
|
|
113
|
+
if (!credential) {
|
|
114
|
+
const matches = findTokensForTeam(teamID)
|
|
115
|
+
const message =
|
|
116
|
+
matches.length === 0
|
|
117
|
+
? `No saved token for team ${teamID}. Run \`puzzmo login <token>\`.`
|
|
118
|
+
: `Token for team ${teamID} is registered against ${matches.map((m) => m.source).join(", ")} but none of those servers are reachable.`
|
|
119
|
+
return { error: `${slug}: ${message}` }
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
let versions: GameVersions | undefined
|
|
123
|
+
try {
|
|
124
|
+
versions = (await getTeamVersions(teamID, credential, ctx)).get(slug)
|
|
125
|
+
} catch (e) {
|
|
126
|
+
return { error: `${slug}: ${e instanceof Error ? e.message : String(e)}` }
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
const baseSha = pickBaseSha(versions ?? null, ctx.against)
|
|
130
|
+
|
|
131
|
+
// Never deployed (or the chosen slot is empty) — nothing to diff against, so it's new and buildable.
|
|
132
|
+
if (!baseSha) return { entry: makeEntry(game, dir, null, ctx.refSha, "new", 0) }
|
|
133
|
+
|
|
134
|
+
// The deployed commit has to exist locally or we can't diff — fail loudly rather than guessing.
|
|
135
|
+
if (!commitExists(baseSha, ctx.repoRoot))
|
|
136
|
+
return {
|
|
137
|
+
error: `${slug}: deployed commit ${baseSha} not found locally. Fetch full git history (e.g. actions/checkout fetch-depth: 0).`,
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
const changedFiles = countChanges(baseSha, ctx.ref, game.puzzmoJsonDir, ctx.includeUncommitted)
|
|
141
|
+
return { entry: makeEntry(game, dir, baseSha, ctx.refSha, changedFiles > 0 ? "changed" : "unchanged", changedFiles) }
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
const makeEntry = (
|
|
145
|
+
game: DiscoveredGame,
|
|
146
|
+
dir: string,
|
|
147
|
+
baseSha: string | null,
|
|
148
|
+
ref: string,
|
|
149
|
+
status: GameStatus,
|
|
150
|
+
changedFiles: number,
|
|
151
|
+
): ChangedEntry => ({
|
|
152
|
+
slug: game.puzzmoFile.game.slug,
|
|
153
|
+
displayName: game.puzzmoFile.game.displayName,
|
|
154
|
+
teamID: game.puzzmoFile.game.teamID,
|
|
155
|
+
dir,
|
|
156
|
+
baseSha,
|
|
157
|
+
ref,
|
|
158
|
+
status,
|
|
159
|
+
changedFiles,
|
|
160
|
+
})
|
|
161
|
+
|
|
162
|
+
/** Fetches a team's per-game runtime SHAs once, memoizing the in-flight promise so concurrent games share the call. */
|
|
163
|
+
const getTeamVersions = (
|
|
164
|
+
teamID: string,
|
|
165
|
+
credential: { source: string; token: string },
|
|
166
|
+
ctx: EvalContext,
|
|
167
|
+
): Promise<Map<string, GameVersions>> => {
|
|
168
|
+
let pending = ctx.teamVersions.get(teamID)
|
|
169
|
+
if (!pending) {
|
|
170
|
+
pending = fetchTeamGameVersions(sourceToURL(credential.source), credential.token)
|
|
171
|
+
ctx.teamVersions.set(teamID, pending)
|
|
172
|
+
}
|
|
173
|
+
return pending
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
/** Picks the baseline SHA for the chosen slot. `latest` is the most recent build the team has — the staged next, else the live current. */
|
|
177
|
+
const pickBaseSha = (versions: GameVersions | null, against: ChangedAgainst): string | null => {
|
|
178
|
+
if (!versions) return null
|
|
179
|
+
if (against === "next") return versions.next
|
|
180
|
+
if (against === "previous") return versions.previous
|
|
181
|
+
return versions.next ?? versions.current
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
/** Counts files differing under `dir` between `base` and `ref`, optionally folding in uncommitted working-tree changes. */
|
|
185
|
+
const countChanges = (base: string, ref: string, dir: string, includeUncommitted: boolean): number => {
|
|
186
|
+
const files = new Set(gitLines(["diff", "--name-only", `${base}..${ref}`, "--", "."], dir))
|
|
187
|
+
if (includeUncommitted) {
|
|
188
|
+
for (const file of gitLines(["diff", "--name-only", "--", "."], dir)) files.add(file) // unstaged
|
|
189
|
+
for (const file of gitLines(["diff", "--name-only", "--cached", "--", "."], dir)) files.add(file) // staged
|
|
190
|
+
for (const file of gitLines(["ls-files", "--others", "--exclude-standard", "--", "."], dir)) files.add(file) // untracked
|
|
191
|
+
}
|
|
192
|
+
return files.size
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
/** Prints the human-readable summary table. */
|
|
196
|
+
const printTable = (report: ChangedEntry[]) => {
|
|
197
|
+
const headers = ["GAME", "STATUS", "BASE", "FILES"]
|
|
198
|
+
const rows = report.map((entry) => [entry.slug, entry.status, entry.baseSha ?? "—", String(entry.changedFiles)])
|
|
199
|
+
const widths = headers.map((header, i) => Math.max(header.length, ...rows.map((row) => row[i].length)))
|
|
200
|
+
const format = (cols: string[]) => cols.map((col, i) => col.padEnd(widths[i])).join(" ")
|
|
201
|
+
|
|
202
|
+
console.log(format(headers))
|
|
203
|
+
for (const row of rows) console.log(format(row))
|
|
204
|
+
|
|
205
|
+
const changedCount = report.filter((entry) => entry.status !== "unchanged").length
|
|
206
|
+
console.log(`\n${changedCount} of ${report.length} game${report.length === 1 ? "" : "s"} changed.`)
|
|
207
|
+
}
|
|
208
|
+
|
|
209
|
+
const getRepoRoot = (cwd: string): string | null => tryGit(["rev-parse", "--show-toplevel"], cwd)
|
|
210
|
+
|
|
211
|
+
const revParseShort = (ref: string, cwd: string): string | null => tryGit(["rev-parse", "--short", ref], cwd)
|
|
212
|
+
|
|
213
|
+
/** True if `sha` resolves to a commit object in the repo at `cwd`. */
|
|
214
|
+
const commitExists = (sha: string, cwd: string): boolean => {
|
|
215
|
+
try {
|
|
216
|
+
execFileSync("git", ["rev-parse", "--verify", "--quiet", `${sha}^{commit}`], { cwd, stdio: "ignore" })
|
|
217
|
+
return true
|
|
218
|
+
} catch {
|
|
219
|
+
return false
|
|
220
|
+
}
|
|
221
|
+
}
|
|
222
|
+
|
|
223
|
+
/** Runs a git command and returns its trimmed stdout, or null if it failed (git's own stderr is suppressed). */
|
|
224
|
+
const tryGit = (args: string[], cwd: string): string | null => {
|
|
225
|
+
try {
|
|
226
|
+
return execFileSync("git", args, { encoding: "utf-8", cwd, stdio: ["ignore", "pipe", "ignore"] }).trim()
|
|
227
|
+
} catch {
|
|
228
|
+
return null
|
|
229
|
+
}
|
|
230
|
+
}
|
|
231
|
+
|
|
232
|
+
/** Runs a git command that emits one file path per line and returns the non-empty lines, or [] if it failed. */
|
|
233
|
+
const gitLines = (args: string[], cwd: string): string[] => {
|
|
234
|
+
const out = tryGit(args, cwd)
|
|
235
|
+
if (out === null) return []
|
|
236
|
+
return out.split("\n").filter(Boolean)
|
|
237
|
+
}
|
|
238
|
+
|
|
239
|
+
const toPosix = (p: string): string => p.split(path.sep).join("/")
|
|
@@ -11,6 +11,7 @@ import { runCommand, gitCommit } from "../../util/exec.js"
|
|
|
11
11
|
import { runSkillsPipelineTUI, runAgentWithBuildLoop } from "../../skills/runner.js"
|
|
12
12
|
import { login } from "../login.js"
|
|
13
13
|
import { getDefaultToken } from "../../util/config.js"
|
|
14
|
+
import { slugify } from "../../util/slugify.js"
|
|
14
15
|
import { detectRepoContext, type RepoType } from "./detectRepo.js"
|
|
15
16
|
|
|
16
17
|
export type Strategy = "import" | "blank" | "prompt"
|
|
@@ -27,18 +28,6 @@ export type CreateOptions = {
|
|
|
27
28
|
prompt?: string
|
|
28
29
|
}
|
|
29
30
|
|
|
30
|
-
/** Converts a string to a URL-friendly slug (matches packages/shared/slugify.ts) */
|
|
31
|
-
const slugify = (text: string) =>
|
|
32
|
-
text
|
|
33
|
-
.toString()
|
|
34
|
-
.normalize("NFD")
|
|
35
|
-
.replace(/[̀-ͯ]/g, "")
|
|
36
|
-
.toLowerCase()
|
|
37
|
-
.trim()
|
|
38
|
-
.replace(/\s+/g, "-")
|
|
39
|
-
.replace(/[^\w-]+/g, "")
|
|
40
|
-
.replace(/--+/g, "-")
|
|
41
|
-
|
|
42
31
|
/** Writes .mcp.json with dev server config */
|
|
43
32
|
const writeMcpConfig = (dir: string) => {
|
|
44
33
|
const token = getDefaultToken()
|
package/src/commands/upload.ts
CHANGED
|
@@ -16,9 +16,12 @@ import {
|
|
|
16
16
|
} from "../util/config.js"
|
|
17
17
|
import { createUserGame } from "../util/createGame.js"
|
|
18
18
|
import { discoverGames, type DiscoveredGame } from "../util/discoverGames.js"
|
|
19
|
+
import { slugify } from "../util/slugify.js"
|
|
19
20
|
|
|
20
21
|
type UploadOptions = {
|
|
21
22
|
verbose?: boolean
|
|
23
|
+
/** Create games that don't exist yet without prompting (for CI). */
|
|
24
|
+
createMissing?: boolean
|
|
22
25
|
}
|
|
23
26
|
|
|
24
27
|
type GameSuccess = {
|
|
@@ -45,6 +48,7 @@ type GameResult = GameSuccess | GameFailure
|
|
|
45
48
|
/** Uploads game build artifacts to Puzzmo by discovering puzzmo.json files in `dir` */
|
|
46
49
|
export const upload = async (dir: string, options: UploadOptions = {}) => {
|
|
47
50
|
const { verbose = false } = options
|
|
51
|
+
const autoCreate = options.createMissing ?? false
|
|
48
52
|
|
|
49
53
|
if (getTokens().length === 0) {
|
|
50
54
|
console.error("Not logged in. Run `puzzmo login <token>` or set PUZZMO_TOKEN.")
|
|
@@ -120,7 +124,7 @@ export const upload = async (dir: string, options: UploadOptions = {}) => {
|
|
|
120
124
|
result = await uploadOneGame(game, { credential, apiURL, sha, description, repoURL, verbose })
|
|
121
125
|
} catch (e) {
|
|
122
126
|
if (!(e instanceof GameNotFoundError)) throw e
|
|
123
|
-
const created = await maybeCreateMissingGame(e, game, { apiURL, teamAccessToken: credential.token })
|
|
127
|
+
const created = await maybeCreateMissingGame(e, game, { apiURL, teamAccessToken: credential.token, autoCreate })
|
|
124
128
|
if (!created) throw e
|
|
125
129
|
result = await uploadOneGame(game, { credential, apiURL, sha, description, repoURL, verbose })
|
|
126
130
|
}
|
|
@@ -313,31 +317,53 @@ const hashGames = (games: DiscoveredGame[]): string => {
|
|
|
313
317
|
|
|
314
318
|
/**
|
|
315
319
|
* When a game can't be uploaded because it doesn't exist on the user's account,
|
|
316
|
-
*
|
|
317
|
-
*
|
|
318
|
-
*
|
|
320
|
+
* create it. With `autoCreate` (CI: --create-missing) it
|
|
321
|
+
* creates without prompting; otherwise it asks for confirmation in an interactive
|
|
322
|
+
* shell. Returns true if the game was created and the caller can retry the upload.
|
|
323
|
+
* Returns false otherwise (non-interactive without autoCreate, or user declined).
|
|
319
324
|
*/
|
|
320
325
|
const maybeCreateMissingGame = async (
|
|
321
326
|
err: GameNotFoundError,
|
|
322
327
|
game: DiscoveredGame,
|
|
323
|
-
opts: { apiURL: string; teamAccessToken: string },
|
|
328
|
+
opts: { apiURL: string; teamAccessToken: string; autoCreate: boolean },
|
|
324
329
|
): Promise<boolean> => {
|
|
325
330
|
const slug = err.slug
|
|
326
|
-
const displayName = game.puzzmoFile.game
|
|
327
|
-
|
|
328
|
-
if
|
|
329
|
-
|
|
331
|
+
const { displayName, oneliner, description, highlightColor } = game.puzzmoFile.game
|
|
332
|
+
|
|
333
|
+
// Creation derives the slug from displayName server-side; if it wouldn't match the
|
|
334
|
+
// puzzmo.json slug, creating would mint an orphan game under the wrong slug and the
|
|
335
|
+
// retried upload would still 404. Refuse up front so nothing is created.
|
|
336
|
+
const expectedSlug = slugify(displayName)
|
|
337
|
+
if (expectedSlug !== slug) {
|
|
338
|
+
console.error(
|
|
339
|
+
` Game "${slug}" not found, and cannot be auto-created: displayName "${displayName}" slugifies to "${expectedSlug}", not "${slug}". ` +
|
|
340
|
+
`Set puzzmo.json game.slug to "${expectedSlug}" (or adjust game.displayName), or create the game manually.`,
|
|
341
|
+
)
|
|
330
342
|
return false
|
|
331
343
|
}
|
|
332
344
|
|
|
333
|
-
|
|
334
|
-
|
|
335
|
-
|
|
336
|
-
})
|
|
337
|
-
|
|
345
|
+
if (opts.autoCreate) {
|
|
346
|
+
console.log(` Game "${slug}" not found — creating it (displayName: "${displayName}")...`)
|
|
347
|
+
} else if (!process.stdin.isTTY) {
|
|
348
|
+
console.error(` Game "${slug}" not found on your account. Re-run interactively, or pass --create-missing to create it automatically.`)
|
|
349
|
+
return false
|
|
350
|
+
} else {
|
|
351
|
+
const consent = await p.confirm({
|
|
352
|
+
message: `Game "${slug}" doesn't exist on your account. Create it now (displayName: "${displayName}")?`,
|
|
353
|
+
initialValue: true,
|
|
354
|
+
})
|
|
355
|
+
if (p.isCancel(consent) || !consent) return false
|
|
356
|
+
}
|
|
338
357
|
|
|
339
358
|
try {
|
|
340
|
-
const created = await createUserGame({
|
|
359
|
+
const created = await createUserGame({
|
|
360
|
+
apiURL: opts.apiURL,
|
|
361
|
+
displayName,
|
|
362
|
+
oneliner,
|
|
363
|
+
description,
|
|
364
|
+
highlightColor,
|
|
365
|
+
teamAccessToken: opts.teamAccessToken,
|
|
366
|
+
})
|
|
341
367
|
console.log(` Created game ${created.slug}`)
|
|
342
368
|
if (created.slug !== slug) {
|
|
343
369
|
console.warn(` Server picked slug "${created.slug}" but your puzzmo.json uses "${slug}". Update puzzmo.json before re-uploading.`)
|
package/src/commands/validate.ts
CHANGED
|
@@ -3,7 +3,7 @@ import path from "node:path"
|
|
|
3
3
|
|
|
4
4
|
import { discoverGames } from "../util/discoverGames.js"
|
|
5
5
|
|
|
6
|
-
/** CLI command: puzzmo validate [dir] — discovers every puzzmo.json under dir and validates each */
|
|
6
|
+
/** CLI command: puzzmo games validate [dir] — discovers every puzzmo.json under dir and validates each */
|
|
7
7
|
export const validate = async (dir: string) => {
|
|
8
8
|
const rootDir = path.resolve(dir)
|
|
9
9
|
if (!fs.existsSync(rootDir)) {
|