summer-engine 4.3.0 → 4.6.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (231) hide show
  1. package/.claude-plugin/marketplace.json +1 -1
  2. package/.claude-plugin/plugin.json +3 -1
  3. package/.codex-plugin/plugin.json +5 -3
  4. package/.cursor-plugin/plugin.json +4 -2
  5. package/.factory-plugin/plugin.json +4 -2
  6. package/AGENTS.md +2 -2
  7. package/CHANGELOG.md +100 -0
  8. package/README.md +67 -27
  9. package/commands/start.md +19 -15
  10. package/commands/summer.md +1 -0
  11. package/dist/cli/command-usage.d.ts +3 -1
  12. package/dist/cli/command-usage.js +10 -4
  13. package/dist/cli/commands/create.d.ts +1 -25
  14. package/dist/cli/commands/create.js +10 -138
  15. package/dist/cli/commands/dev.d.ts +66 -0
  16. package/dist/cli/commands/dev.js +234 -0
  17. package/dist/cli/commands/install.d.ts +15 -58
  18. package/dist/cli/commands/install.js +31 -481
  19. package/dist/cli/commands/login.js +1 -1
  20. package/dist/cli/commands/ops.d.ts +17 -1
  21. package/dist/cli/commands/ops.js +97 -9
  22. package/dist/cli/commands/preview.d.ts +58 -0
  23. package/dist/cli/commands/preview.js +181 -0
  24. package/dist/cli/commands/run.d.ts +1 -17
  25. package/dist/cli/commands/run.js +17 -206
  26. package/dist/cli/commands/setup.js +175 -32
  27. package/dist/cli/index.js +5 -0
  28. package/dist/core/agent-host.d.ts +3 -0
  29. package/dist/core/agent-host.js +1 -1
  30. package/dist/core/api-client.d.ts +10 -2
  31. package/dist/core/api-client.js +13 -8
  32. package/dist/core/capabilities/agent-playbook.js +1 -0
  33. package/dist/core/capabilities/apps.d.ts +9 -9
  34. package/dist/core/capabilities/asset-import.d.ts +4 -1
  35. package/dist/core/capabilities/asset-import.js +15 -6
  36. package/dist/core/capabilities/capture-gameplay.js +1 -1
  37. package/dist/core/capabilities/connect-signal.d.ts +2 -2
  38. package/dist/core/capabilities/content-make/index.d.ts +81 -0
  39. package/dist/core/capabilities/content-make/index.js +706 -0
  40. package/dist/core/capabilities/content-make/media.d.ts +57 -0
  41. package/dist/core/capabilities/content-make/media.js +172 -0
  42. package/dist/core/capabilities/content-make/plan.d.ts +130 -0
  43. package/dist/core/capabilities/content-make/plan.js +433 -0
  44. package/dist/core/capabilities/content-make/render.d.ts +64 -0
  45. package/dist/core/capabilities/content-make/render.js +370 -0
  46. package/dist/core/capabilities/content-make/study.d.ts +13 -0
  47. package/dist/core/capabilities/content-make/study.js +165 -0
  48. package/dist/core/capabilities/content-make/types.d.ts +173 -0
  49. package/dist/core/capabilities/content-make/types.js +6 -0
  50. package/dist/core/capabilities/content-make/voice.d.ts +7 -0
  51. package/dist/core/capabilities/content-make/voice.js +27 -0
  52. package/dist/core/capabilities/events.d.ts +2 -2
  53. package/dist/core/capabilities/export-game.d.ts +4 -1
  54. package/dist/core/capabilities/export-game.js +4 -3
  55. package/dist/core/capabilities/export-presets.d.ts +2 -1
  56. package/dist/core/capabilities/export-presets.js +2 -1
  57. package/dist/core/capabilities/go-live.d.ts +14 -0
  58. package/dist/core/capabilities/go-live.js +408 -0
  59. package/dist/core/capabilities/image-generation.d.ts +6 -6
  60. package/dist/core/capabilities/item-manifest.d.ts +92 -0
  61. package/dist/core/capabilities/item-manifest.js +247 -0
  62. package/dist/core/capabilities/placement.d.ts +7 -7
  63. package/dist/core/capabilities/projects-local.d.ts +68 -0
  64. package/dist/core/capabilities/projects-local.js +172 -0
  65. package/dist/core/capabilities/publish-build.d.ts +13 -2
  66. package/dist/core/capabilities/publish-build.js +87 -21
  67. package/dist/core/capabilities/publish-download.js +1 -1
  68. package/dist/core/capabilities/runtime-control.d.ts +7 -2
  69. package/dist/core/capabilities/runtime-control.js +27 -1
  70. package/dist/core/capabilities/seeing/args.d.ts +2 -2
  71. package/dist/core/capabilities/store-checks.d.ts +45 -0
  72. package/dist/core/capabilities/store-checks.js +85 -0
  73. package/dist/core/capabilities/store-details.d.ts +37 -0
  74. package/dist/core/capabilities/store-details.js +94 -0
  75. package/dist/core/capabilities/summer-bundle.d.ts +21 -2
  76. package/dist/core/capabilities/summer-bundle.js +54 -7
  77. package/dist/core/capabilities/sync-items.d.ts +89 -0
  78. package/dist/core/capabilities/sync-items.js +352 -0
  79. package/dist/core/capabilities/tool-dispatch.js +66 -6
  80. package/dist/core/capabilities/video-ogv.d.ts +49 -0
  81. package/dist/core/capabilities/video-ogv.js +260 -0
  82. package/dist/core/capability-skew.js +2 -2
  83. package/dist/core/cli-usage-beacon.js +26 -2
  84. package/dist/core/cli-usage.d.ts +5 -2
  85. package/dist/core/cli-usage.js +8 -2
  86. package/dist/core/engine-install.d.ts +53 -0
  87. package/dist/core/engine-install.js +93 -0
  88. package/dist/core/engine-installer.d.ts +134 -0
  89. package/dist/core/engine-installer.js +760 -0
  90. package/dist/core/engine-launch.d.ts +126 -0
  91. package/dist/core/engine-launch.js +398 -0
  92. package/dist/core/engine-self-start.d.ts +63 -0
  93. package/dist/core/engine-self-start.js +282 -0
  94. package/dist/core/engine.d.ts +7 -1
  95. package/dist/core/engine.js +9 -2
  96. package/dist/core/error-codes.d.ts +4 -1
  97. package/dist/core/error-codes.js +7 -3
  98. package/dist/core/library-read.d.ts +2 -2
  99. package/dist/core/library-search.d.ts +2 -2
  100. package/dist/core/local-project.d.ts +50 -0
  101. package/dist/core/local-project.js +234 -0
  102. package/dist/core/ops/areas.js +9 -3
  103. package/dist/core/ops/catalog.d.ts +9 -0
  104. package/dist/core/ops/catalog.js +56 -4
  105. package/dist/core/ops/engine-answer.d.ts +4 -1
  106. package/dist/core/ops/engine-answer.js +16 -3
  107. package/dist/core/ops/engine.js +4 -4
  108. package/dist/core/ops/save-files.d.ts +6 -1
  109. package/dist/core/ops/save-files.js +67 -50
  110. package/dist/core/ops/search.js +9 -2
  111. package/dist/core/preview/input.d.ts +35 -0
  112. package/dist/core/preview/input.js +105 -0
  113. package/dist/core/preview/launch-config.d.ts +52 -0
  114. package/dist/core/preview/launch-config.js +137 -0
  115. package/dist/core/preview/page.d.ts +22 -0
  116. package/dist/core/preview/page.js +232 -0
  117. package/dist/core/preview/preview-source.d.ts +179 -0
  118. package/dist/core/preview/preview-source.js +350 -0
  119. package/dist/core/preview/server.d.ts +26 -0
  120. package/dist/core/preview/server.js +218 -0
  121. package/dist/core/telemetry.d.ts +20 -16
  122. package/dist/core/telemetry.js +92 -16
  123. package/dist/core/templates.d.ts +20 -0
  124. package/dist/core/templates.js +300 -3
  125. package/dist/core/tool-profiles.d.ts +2 -2
  126. package/dist/core/tool-profiles.js +1 -0
  127. package/dist/core/usage-fields.d.ts +41 -0
  128. package/dist/core/usage-fields.js +150 -0
  129. package/dist/core/util/sleep.d.ts +3 -0
  130. package/dist/core/util/sleep.js +12 -0
  131. package/dist/installer/agent-config.d.ts +29 -0
  132. package/dist/installer/agent-config.js +75 -11
  133. package/dist/installer/agent-detect.d.ts +44 -0
  134. package/dist/installer/agent-detect.js +166 -0
  135. package/dist/installer/agent-table.d.ts +19 -0
  136. package/dist/installer/agent-table.js +130 -0
  137. package/dist/installer/setup-error.d.ts +19 -0
  138. package/dist/installer/setup-error.js +34 -0
  139. package/dist/installer/version-check.d.ts +5 -0
  140. package/dist/installer/version-check.js +21 -0
  141. package/dist/mcp/ops-surface.d.ts +7 -0
  142. package/dist/mcp/ops-surface.js +40 -3
  143. package/dist/mcp/server.d.ts +14 -4
  144. package/dist/mcp/server.js +42 -8
  145. package/dist/mcp/tool-surface.d.ts +8 -0
  146. package/dist/mcp/tool-surface.js +19 -2
  147. package/dist/mcp/tools/creator-tools.js +92 -14
  148. package/dist/mcp/tools/debug-tools.js +2 -0
  149. package/dist/mcp/tools/local-project-tools.d.ts +6 -0
  150. package/dist/mcp/tools/local-project-tools.js +33 -0
  151. package/dist/mcp/tools/navigation-tools.js +2 -1
  152. package/dist/mcp/tools/project-tools.js +5 -8
  153. package/dist/mcp/tools/seeing-tools.js +1 -1
  154. package/dist/mcp/tools/visual-tools.js +1 -1
  155. package/dist/mcp/tools/with-engine.js +1 -1
  156. package/dist/project-memory/agent-instructions.js +11 -3
  157. package/gemini-extension.json +3 -1
  158. package/library/references/mcp-tools-reference/mcp-tools-reference.md +18 -5
  159. package/library/skills/README.md +12 -8
  160. package/library/skills/animated-loop/SKILL.md +10 -16
  161. package/library/skills/cinematic-cutscene/SKILL.md +7 -18
  162. package/library/skills/headless-scripting/SKILL.md +7 -4
  163. package/library/skills/in-game-purchases/SKILL.md +25 -8
  164. package/library/skills/in-game-purchases/resource.yaml +2 -1
  165. package/library/skills/launch-ready/SKILL.md +351 -0
  166. package/library/skills/launch-ready/resource.yaml +45 -0
  167. package/library/skills/make-content/SKILL.md +202 -0
  168. package/library/skills/make-content/resource.yaml +32 -0
  169. package/library/skills/make-game/SKILL.md +6 -4
  170. package/library/skills/make-game/resource.yaml +2 -1
  171. package/library/skills/navigate-summer/SKILL.md +4 -2
  172. package/library/skills/new-project/SKILL.md +15 -22
  173. package/library/skills/new-project/resource.yaml +2 -1
  174. package/library/skills/painted-25d-world/SKILL.md +366 -0
  175. package/library/skills/painted-25d-world/_building-blocks/blender-route.md +33 -0
  176. package/library/skills/painted-25d-world/_building-blocks/blockout_builder.gd +149 -0
  177. package/library/skills/painted-25d-world/_building-blocks/capture_paint_view.gd +32 -0
  178. package/library/skills/painted-25d-world/_building-blocks/paint_projection.gdshader +23 -0
  179. package/library/skills/painted-25d-world/_building-blocks/paintcam_check.gd +25 -0
  180. package/library/skills/painted-25d-world/_building-blocks/painted_door.gd +17 -0
  181. package/library/skills/painted-25d-world/_building-blocks/painted_location.gd +56 -0
  182. package/library/skills/painted-25d-world/_building-blocks/painted_player.gd +125 -0
  183. package/library/skills/painted-25d-world/_building-blocks/painted_world.gd +111 -0
  184. package/library/skills/painted-25d-world/_building-blocks/tools/cam.py +57 -0
  185. package/library/skills/painted-25d-world/_building-blocks/tools/detail4k.py +81 -0
  186. package/library/skills/painted-25d-world/_building-blocks/tools/overlay.py +17 -0
  187. package/library/skills/painted-25d-world/_building-blocks/tools/preview.py +60 -0
  188. package/library/skills/painted-25d-world/resource.yaml +39 -0
  189. package/library/skills/publish-your-game/SKILL.md +13 -14
  190. package/library/skills/publish-your-game/resource.yaml +4 -3
  191. package/library/skills/release-checklist/SKILL.md +188 -0
  192. package/library/skills/release-checklist/resource.yaml +40 -0
  193. package/library/skills/release-readiness/SKILL.md +1 -1
  194. package/library/skills/release-readiness/resource.yaml +1 -1
  195. package/library/skills/store-art/SKILL.md +1 -1
  196. package/library/skills/store-art/resource.yaml +1 -1
  197. package/library/skills/store-listing/SKILL.md +1 -1
  198. package/library/skills/store-listing/resource.yaml +1 -1
  199. package/library/skills/summer-cli/SKILL.md +2 -0
  200. package/library/skills/summer-store/SKILL.md +295 -39
  201. package/library/skills/summer-store/resource.yaml +9 -3
  202. package/library/skills/trailer-shot/SKILL.md +3 -3
  203. package/library/templates/README.md +5 -0
  204. package/library/tools/content-make/resource.yaml +132 -0
  205. package/library/tools/frame-shot/GUIDE.md +1 -1
  206. package/library/tools/go-live/resource.yaml +67 -0
  207. package/library/tools/import-from-url/GUIDE.md +1 -0
  208. package/library/tools/import-from-url-batch/GUIDE.md +1 -1
  209. package/library/tools/play/GUIDE.md +2 -0
  210. package/library/tools/projects-create-local/resource.yaml +55 -0
  211. package/library/tools/projects-open-local/resource.yaml +51 -0
  212. package/library/tools/publish-build/resource.yaml +3 -3
  213. package/library/tools/screenshot/GUIDE.md +1 -1
  214. package/library/tools/store-set-details/resource.yaml +114 -0
  215. package/library/tools/sync-items/resource.yaml +72 -0
  216. package/package.json +1 -1
  217. package/registry/generated/aliases.json +6 -0
  218. package/registry/generated/counts.json +3 -3
  219. package/registry/generated/gemini-extension.json +3 -1
  220. package/registry/generated/index.json +463 -37
  221. package/registry/generated/marketplace.claude.json +1 -1
  222. package/registry/generated/plugin.claude-directory.json +4 -2
  223. package/registry/generated/plugin.claude.json +3 -1
  224. package/registry/generated/plugin.codex.json +5 -3
  225. package/registry/generated/plugin.cursor.json +4 -2
  226. package/registry/generated/plugin.factory.json +4 -2
  227. package/registry/generated/server.json +2 -2
  228. package/registry/generated/skills-index.md +12 -8
  229. package/registry/generated/skills-registry.json +62 -5
  230. package/registry/operations/legacy-names.json +40 -0
  231. package/registry/operations/public-catalog.json +10144 -387
@@ -3,9 +3,21 @@
3
3
  * summer-bundle.json, client.pck, and for a game with a host server.pck plus
4
4
  * config/. This reads what the build upload declares; the Platform verifies
5
5
  * the uploaded bytes again. Same rules as Studio's browser check.
6
+ *
7
+ * Two layouts, chosen by the manifest's schema (summer-platform
8
+ * docs/build-publications.md, "summer.games bundle layout"):
9
+ * - summer.bundle.v2 (Summer Engine 0.7.4 and newer) is fixed: exactly the
10
+ * paths in BUNDLE_V2_PATHS and nothing else, whatever the game declares.
11
+ * - summer.bundle.v1 (older engines): a hosted game's configuration may be
12
+ * any number of config/ files.
6
13
  */
7
- export declare const BUNDLE_SCHEMA = "summer.bundle.v1";
14
+ export declare const BUNDLE_SCHEMA_V1 = "summer.bundle.v1";
15
+ export declare const BUNDLE_SCHEMA_V2 = "summer.bundle.v2";
16
+ export declare const BUNDLE_SCHEMAS: readonly ["summer.bundle.v1", "summer.bundle.v2"];
17
+ export type BundleSchema = (typeof BUNDLE_SCHEMAS)[number];
8
18
  export declare const BUNDLE_MANIFEST = "summer-bundle.json";
19
+ /** Every path a summer.bundle.v2 export may hold; the store refuses any other entry by name. */
20
+ export declare const BUNDLE_V2_PATHS: readonly ["summer-bundle.json", "client.pck", "server.pck", "config/summer.build.json", "config/world-definitions.json", "config/world-support.json"];
9
21
  /** The same platforms, in any order. */
10
22
  export declare function samePlatforms(a: readonly string[], b: readonly string[]): boolean;
11
23
  /**
@@ -23,13 +35,20 @@ export declare class BuildToolError extends Error {
23
35
  readonly detail?: Record<string, unknown> | undefined;
24
36
  constructor(code: string, message: string, recovery: string, status?: number | undefined, detail?: Record<string, unknown> | undefined);
25
37
  }
38
+ /**
39
+ * The structured failure every face prints for a BuildToolError: its code,
40
+ * the message without the recovery, the recovery, the HTTP status when the
41
+ * store sent one, and its detail (a manifest's problem list, the person's
42
+ * games).
43
+ */
44
+ export declare function buildToolFailure(error: BuildToolError): Record<string, unknown>;
26
45
  export interface BundleFile {
27
46
  path: string;
28
47
  sha256: string;
29
48
  size: number;
30
49
  }
31
50
  export interface SummerBundle {
32
- schema: typeof BUNDLE_SCHEMA;
51
+ schema: BundleSchema;
33
52
  summerVersion: string | null;
34
53
  engineSha: string | null;
35
54
  /** res:// scene the client pack opens. */
@@ -6,12 +6,30 @@ import { readZipEntries, readZipEntry, ZipReadError } from "../util/zip.js";
6
6
  * summer-bundle.json, client.pck, and for a game with a host server.pck plus
7
7
  * config/. This reads what the build upload declares; the Platform verifies
8
8
  * the uploaded bytes again. Same rules as Studio's browser check.
9
+ *
10
+ * Two layouts, chosen by the manifest's schema (summer-platform
11
+ * docs/build-publications.md, "summer.games bundle layout"):
12
+ * - summer.bundle.v2 (Summer Engine 0.7.4 and newer) is fixed: exactly the
13
+ * paths in BUNDLE_V2_PATHS and nothing else, whatever the game declares.
14
+ * - summer.bundle.v1 (older engines): a hosted game's configuration may be
15
+ * any number of config/ files.
9
16
  */
10
- export const BUNDLE_SCHEMA = "summer.bundle.v1";
17
+ export const BUNDLE_SCHEMA_V1 = "summer.bundle.v1";
18
+ export const BUNDLE_SCHEMA_V2 = "summer.bundle.v2";
19
+ export const BUNDLE_SCHEMAS = [BUNDLE_SCHEMA_V1, BUNDLE_SCHEMA_V2];
11
20
  export const BUNDLE_MANIFEST = "summer-bundle.json";
12
21
  const BUNDLE_CLIENT = "client.pck";
13
22
  const BUNDLE_SERVER = "server.pck";
14
23
  const BUNDLE_BUILD = "config/summer.build.json";
24
+ /** Every path a summer.bundle.v2 export may hold; the store refuses any other entry by name. */
25
+ export const BUNDLE_V2_PATHS = [
26
+ BUNDLE_MANIFEST,
27
+ BUNDLE_CLIENT,
28
+ BUNDLE_SERVER,
29
+ BUNDLE_BUILD,
30
+ "config/world-definitions.json",
31
+ "config/world-support.json",
32
+ ];
15
33
  /** The same platforms, in any order. */
16
34
  export function samePlatforms(a, b) {
17
35
  return a.length === b.length && [...a].sort().join(",") === [...b].sort().join(",");
@@ -48,6 +66,22 @@ export class BuildToolError extends Error {
48
66
  this.name = "BuildToolError";
49
67
  }
50
68
  }
69
+ /**
70
+ * The structured failure every face prints for a BuildToolError: its code,
71
+ * the message without the recovery, the recovery, the HTTP status when the
72
+ * store sent one, and its detail (a manifest's problem list, the person's
73
+ * games).
74
+ */
75
+ export function buildToolFailure(error) {
76
+ return {
77
+ ok: false,
78
+ code: error.code,
79
+ message: error.message.replace(` ${error.recovery}`, ""),
80
+ recovery: error.recovery,
81
+ ...(error.status ? { status: error.status } : {}),
82
+ ...error.detail,
83
+ };
84
+ }
51
85
  function bundleError(message) {
52
86
  return new BuildToolError("bundle_invalid", message, AS_EXPORTED);
53
87
  }
@@ -84,13 +118,18 @@ export async function readSummerBundle(path) {
84
118
  }
85
119
  const client = manifest?.client;
86
120
  const mainScene = typeof client?.mainScene === "string" ? client.mainScene : null;
87
- if (manifest?.schema !== BUNDLE_SCHEMA ||
121
+ const schema = manifest?.schema;
122
+ if (!BUNDLE_SCHEMAS.includes(schema) ||
88
123
  client?.path !== BUNDLE_CLIENT ||
89
124
  !mainScene?.startsWith("res://") ||
90
125
  !Array.isArray(client?.targetPlatforms) ||
91
126
  !client.targetPlatforms.every((value) => typeof value === "string") ||
92
127
  !Array.isArray(manifest.files)) {
93
- throw new BuildToolError("bundle_invalid", `${BUNDLE_MANIFEST} is not a ${BUNDLE_SCHEMA} manifest.`, "Recovery: update Summer Engine and export again.");
128
+ throw new BuildToolError("bundle_invalid", typeof manifest?.schema === "string" && /^summer\.bundle\.v\d+$/.test(manifest.schema) && !BUNDLE_SCHEMAS.includes(schema)
129
+ ? `${BUNDLE_MANIFEST} is a ${manifest.schema} manifest, newer than this summer-engine package reads (${BUNDLE_SCHEMAS.join(", ")}).`
130
+ : `${BUNDLE_MANIFEST} is not a ${BUNDLE_SCHEMAS.join(" or ")} manifest.`, typeof manifest?.schema === "string" && /^summer\.bundle\.v\d+$/.test(manifest.schema) && !BUNDLE_SCHEMAS.includes(schema)
131
+ ? "Recovery: update the summer-engine package (npx -y summer-engine@latest), then upload the same export."
132
+ : "Recovery: update Summer Engine and export again.");
94
133
  }
95
134
  const hosted = Boolean(manifest.server);
96
135
  const compositionPath = typeof manifest.compositionPath === "string" ? manifest.compositionPath : null;
@@ -107,10 +146,18 @@ export async function readSummerBundle(path) {
107
146
  const allowed = (name) => name === BUNDLE_MANIFEST ||
108
147
  name === BUNDLE_CLIENT ||
109
148
  name === BUNDLE_BUILD ||
110
- (hosted && (name === BUNDLE_SERVER || name.startsWith("config/")));
149
+ (hosted &&
150
+ (name === BUNDLE_SERVER ||
151
+ (schema === BUNDLE_SCHEMA_V2 ? BUNDLE_V2_PATHS.includes(name) : name.startsWith("config/"))));
111
152
  const extra = entries.find((entry) => !allowed(entry.name));
112
- if (extra)
113
- throw bundleError(`The export contains ${extra.name}, which a summer.games export never has.`);
153
+ if (extra) {
154
+ throw bundleError(schema === BUNDLE_SCHEMA_V2
155
+ ? `The export contains ${extra.name}, which is not part of the ${BUNDLE_SCHEMA_V2} layout (${BUNDLE_V2_PATHS.join(", ")}).`
156
+ : `The export contains ${extra.name}, which a summer.games export never has.`);
157
+ }
158
+ if (schema === BUNDLE_SCHEMA_V2 && !byName.has(BUNDLE_BUILD)) {
159
+ throw bundleError(`A ${BUNDLE_SCHEMA_V2} export always holds ${BUNDLE_BUILD}, and this one does not.`);
160
+ }
114
161
  const files = [];
115
162
  for (const item of manifest.files) {
116
163
  const entry = typeof item?.path === "string" ? byName.get(item.path) : undefined;
@@ -150,7 +197,7 @@ export async function readSummerBundle(path) {
150
197
  throw bundleError("The export must name at least one platform, each once.");
151
198
  }
152
199
  return {
153
- schema: BUNDLE_SCHEMA,
200
+ schema,
154
201
  summerVersion: typeof manifest.export?.summerVersion === "string" ? manifest.export.summerVersion : null,
155
202
  engineSha: typeof manifest.export?.engineSha === "string" ? manifest.export.engineSha : null,
156
203
  mainScene: mainScene,
@@ -0,0 +1,89 @@
1
+ import { type ManifestItem } from "./item-manifest.js";
2
+ import { type PublishBuildDependencies } from "./publish-build.js";
3
+ /** The most items one owner approval may carry (economy.propose_items). */
4
+ export declare const MAX_ITEMS_PER_PROPOSAL = 50;
5
+ export interface SyncItemsInput {
6
+ gameId?: string;
7
+ project?: string;
8
+ /** The manifest, relative to the project. Defaults to summer.items.json. */
9
+ file?: string;
10
+ /** Only these slugs; the rest of the manifest is left out of the plan. */
11
+ slugs?: string[];
12
+ confirm?: boolean;
13
+ face: "cli" | "mcp";
14
+ }
15
+ /** What the owner is asked to apply: the manifest entry plus the media it now names. */
16
+ export interface ProposedItem {
17
+ slug: string;
18
+ action: "create" | "update";
19
+ definitionId?: string;
20
+ /** The revision the plan was made against; the owner's apply refuses if it moved on. */
21
+ basedOnRevisionId?: string;
22
+ item: Omit<ManifestItem, "media"> & {
23
+ mediaAssetIds: string[];
24
+ };
25
+ }
26
+ export interface ProposeItemsRequest {
27
+ gameId: string;
28
+ items: ProposedItem[];
29
+ idempotencyKey: string;
30
+ }
31
+ export interface SyncItemsDependencies extends PublishBuildDependencies {
32
+ readFile: (path: string) => Promise<Buffer>;
33
+ /** Ask the owner to apply the plan (the hosted economy.propose_items). */
34
+ propose: (request: ProposeItemsRequest) => Promise<Record<string, unknown>>;
35
+ /** The hosted MCP this machine's store sign-in was made for, or null without one. */
36
+ signedInResource: () => Promise<string | null>;
37
+ }
38
+ export declare const syncItemsDefaults: SyncItemsDependencies;
39
+ type Json = Record<string, any>;
40
+ export interface LocalMedia {
41
+ path: string;
42
+ sha256: string;
43
+ sizeBytes: number;
44
+ kind: "IMAGE" | "MODEL_3D";
45
+ contentType: string;
46
+ }
47
+ export interface RemoteItem {
48
+ definitionId: string;
49
+ /** Who manages the item: CREATOR_CATALOG (the creator's store items) or PASSPORT_CONTROL (a Passport collectible). */
50
+ authority?: string | null;
51
+ state: string | null;
52
+ slug: string;
53
+ revision: Json;
54
+ priceSparks: number | null;
55
+ mediaSha256: string[];
56
+ }
57
+ export type ItemPlan = {
58
+ slug: string;
59
+ action: "create";
60
+ changes: string[];
61
+ } | {
62
+ slug: string;
63
+ action: "update";
64
+ definitionId: string;
65
+ revisionId: string;
66
+ revision: "edit_draft" | "new_draft";
67
+ changes: string[];
68
+ warnings?: string[];
69
+ } | {
70
+ slug: string;
71
+ action: "unchanged";
72
+ definitionId: string;
73
+ } | {
74
+ slug: string;
75
+ action: "blocked";
76
+ definitionId?: string;
77
+ reason: string;
78
+ };
79
+ /**
80
+ * The plan for one manifest item against what the Game has under its slug.
81
+ * Pure: the same inputs always give the same plan.
82
+ */
83
+ export declare function planItem(item: ManifestItem, media: LocalMedia[], remote: RemoteItem | null): ItemPlan;
84
+ export type SyncItemsResult = Record<string, unknown> & {
85
+ ok: true;
86
+ status: string;
87
+ };
88
+ export declare function syncItems(input: SyncItemsInput, overrides?: Partial<SyncItemsDependencies>): Promise<SyncItemsResult>;
89
+ export {};
@@ -0,0 +1,352 @@
1
+ import { createHash } from "node:crypto";
2
+ import { readFile } from "node:fs/promises";
3
+ import { basename, extname, join } from "node:path";
4
+ import { resolveGatewayUrl } from "../config.js";
5
+ import { readStoredOAuthToken, resolveMcpResourceUrl } from "../oauth.js";
6
+ import { appendStoreJsonLine } from "../store.js";
7
+ import { resolveExportProject } from "./export-game.js";
8
+ import { behaviorOf, MEDIA_TYPES, readItemsManifest } from "./item-manifest.js";
9
+ import { createStore, defaultDependencies as publishDefaults, listGamesOrThrow, storeToken } from "./publish-build.js";
10
+ import { BuildToolError } from "./summer-bundle.js";
11
+ /**
12
+ * summer_sync_items: bring a Game's items in line with summer.items.json,
13
+ * matched by creator slug, so the same file gives the staging Game and the
14
+ * production Game the same items (each Game keeps its own definition ids).
15
+ *
16
+ * Through the creator store proxy Studio uses (PSE /api/creator-store/* ->
17
+ * /v1/management/*), with the store sign-in from "summer login --store":
18
+ *
19
+ * GET games/{g}/item-definitions?slug= the item with this slug, or none
20
+ * GET games/{g}/item-definitions/{d} its latest revision (presentation, behavior, media)
21
+ * GET .../revisions/{r}/price-book/diff its Sparks price
22
+ * GET media-assets/{a} the sha256 of each image it shows
23
+ * POST media-assets:begin-upload, PUT, :complete only the images that changed (confirm=true)
24
+ *
25
+ * That sign-in is a delegated agent credential: the platform lets it read and
26
+ * upload media, and refuses every item write with owner_approval_required
27
+ * (agent_gate.go). So this tool never writes an item. With confirm=true it
28
+ * uploads the changed images and asks the owner to apply the plan (the hosted
29
+ * economy.propose_items operation): the owner approves in Studio, and Studio
30
+ * creates or revises each item by slug, sets its price and submits it for
31
+ * review with the owner's own session. Nothing here activates or publishes an
32
+ * item; players see it only after Summer's review and the owner's release.
33
+ *
34
+ * Runs against the gateway the CLI is set to (SUMMER_GATEWAY_URL or
35
+ * "summer config set gateway.url"), so staging and production differ only in
36
+ * that setting, the hosted MCP (SUMMER_MCP_RESOURCE_URL), the store sign-in
37
+ * and the gameId. The machine keeps one store sign-in: after switching
38
+ * between staging and production, "summer login --store --force" signs in to
39
+ * the other one. A sign-in for another hosted MCP stops the call up front.
40
+ */
41
+ const seg = encodeURIComponent;
42
+ const MAX_PAGES = 20;
43
+ /** The most items one owner approval may carry (economy.propose_items). */
44
+ export const MAX_ITEMS_PER_PROPOSAL = 50;
45
+ /** Revisions a change may still be written into; the others get a new draft first. */
46
+ const EDITABLE = new Set(["DRAFT", "CHANGES_REQUESTED"]);
47
+ /** Revisions Summer is reviewing: a change waits until the review is decided. */
48
+ const IN_REVIEW = new Set(["SUBMITTED", "VERIFYING"]);
49
+ /** Why economy.propose_items did not take the plan, in plain words, and what to do. */
50
+ const PROPOSAL_FAILURES = {
51
+ invalid_input: "Recovery: Summer refused a field of the plan (details name it). Fix summer.items.json and call again; the uploaded media is reused.",
52
+ idempotency_conflict: "Recovery: this plan's key was already used for a different plan. Call again with confirm=false to read the Game afresh, then confirm the new plan.",
53
+ unavailable: "Recovery: Summer could not take the plan right now. Call again in a few minutes with the same summer.items.json; the owner is asked once.",
54
+ not_found: "Recovery: Summer found no such game for this account on this gateway. Check gameId with games.list (staging and production games have different ids).",
55
+ };
56
+ async function proposeOnHosted(request) {
57
+ // Loaded on use: the operation catalog imports the capabilities this file sits next to.
58
+ const { answerOfHosted, callHostedV4 } = await import("../ops/cloud.js");
59
+ const { idempotencyKey, ...input } = request;
60
+ return answerOfHosted(await callHostedV4("summer_call", { op: "economy.propose_items", input: { ...input, idempotencyKey } }));
61
+ }
62
+ export const syncItemsDefaults = {
63
+ ...publishDefaults,
64
+ readFile: (path) => readFile(path),
65
+ propose: proposeOnHosted,
66
+ signedInResource: async () => (await readStoredOAuthToken())?.resource ?? null,
67
+ };
68
+ /**
69
+ * The one store sign-in is for one hosted MCP. When SUMMER_MCP_RESOURCE_URL
70
+ * now names another (staging after production, or back), the store refuses
71
+ * the sign-in or the plan would reach the other environment: stop before
72
+ * anything is read or sent.
73
+ */
74
+ async function requireSignInForThisEnvironment(deps) {
75
+ const signedIn = await deps.signedInResource();
76
+ const current = resolveMcpResourceUrl();
77
+ if (signedIn && signedIn !== current) {
78
+ throw new BuildToolError("store_login_other_environment", `This machine's store sign-in is for ${signedIn}, but SUMMER_MCP_RESOURCE_URL is ${current}. Nothing was read or sent.`, 'Recovery: switching between staging and production takes both SUMMER_GATEWAY_URL and SUMMER_MCP_RESOURCE_URL, then "summer login --store --force" to sign in to that environment; call again after it.', undefined, { signedInResource: signedIn, resource: current });
79
+ }
80
+ }
81
+ const sameList = (left, right) => left.length === right.length && left.every((value, index) => value === right[index]);
82
+ /**
83
+ * The plan for one manifest item against what the Game has under its slug.
84
+ * Pure: the same inputs always give the same plan.
85
+ */
86
+ export function planItem(item, media, remote) {
87
+ const wanted = behaviorOf(item);
88
+ if (!remote) {
89
+ // The platform refuses an item created without media.
90
+ if (!media.length)
91
+ return { slug: item.slug, action: "blocked", reason: "a new item needs at least one image" };
92
+ return { slug: item.slug, action: "create", changes: ["new item", ...(item.priceSparks !== undefined ? ["price"] : []), "media"] };
93
+ }
94
+ // A Passport collectible has its own lifecycle (Passport control, take-down), and the owner's apply only writes store items.
95
+ if (remote.authority === "PASSPORT_CONTROL") {
96
+ return {
97
+ slug: item.slug,
98
+ action: "blocked",
99
+ definitionId: remote.definitionId,
100
+ reason: `The Game's "${item.slug}" is a Passport collectible, which summer.items.json does not manage. Give this item another slug, or change the Passport item in Studio.`,
101
+ };
102
+ }
103
+ const revision = remote.revision;
104
+ if (typeof revision.revisionId !== "string" || !revision.revisionId) {
105
+ return { slug: item.slug, action: "blocked", definitionId: remote.definitionId, reason: `Summer did not say which revision "${item.slug}" is on. Call again in a minute.` };
106
+ }
107
+ const behavior = revision.behavior ?? {};
108
+ // Class and ownership mode belong to the definition: a revision cannot change them.
109
+ if (behavior.class !== wanted.class || behavior.ownershipMode !== wanted.ownershipMode) {
110
+ return {
111
+ slug: item.slug,
112
+ action: "blocked",
113
+ definitionId: remote.definitionId,
114
+ reason: `The Game's "${item.slug}" is ${behavior.class} ${behavior.ownershipMode}; summer.items.json says ${wanted.class} ${wanted.ownershipMode}. Class and ownership mode cannot change: keep them, or give the new item a new slug.`,
115
+ };
116
+ }
117
+ const presentation = revision.presentation ?? {};
118
+ const changes = [];
119
+ if (presentation.name !== item.name)
120
+ changes.push("name");
121
+ if (presentation.description !== item.description)
122
+ changes.push("description");
123
+ if (!sameList(presentation.tags ?? [], item.tags))
124
+ changes.push("tags");
125
+ if ((presentation.backgroundColor ?? undefined) !== item.backgroundColor)
126
+ changes.push("backgroundColor");
127
+ const rights = behavior.rights ?? {};
128
+ const ownershipChanged = (behavior.maxOwned ?? undefined) !== wanted.maxOwned ||
129
+ rights.policy !== wanted.rights.policy ||
130
+ rights.consumable !== wanted.rights.consumable ||
131
+ rights.directTransferAllowed !== wanted.rights.directTransferAllowed ||
132
+ rights.giftingAfterOwnershipAllowed !== wanted.rights.giftingAfterOwnershipAllowed ||
133
+ rights.publicResalePolicyEligible !== wanted.rights.publicResalePolicyEligible ||
134
+ JSON.stringify(behavior.cosmetic ?? null) !== JSON.stringify(wanted.cosmetic ?? null);
135
+ if (ownershipChanged)
136
+ changes.push("ownership rules");
137
+ if ((revision.supply ?? undefined) !== item.supply)
138
+ changes.push("supply");
139
+ if (!sameList(remote.mediaSha256, media.map((file) => file.sha256)))
140
+ changes.push("media");
141
+ // No priceSparks leaves the item's price as it is (a price book cannot be removed).
142
+ if (item.priceSparks !== undefined && remote.priceSparks !== item.priceSparks)
143
+ changes.push("price");
144
+ if (!changes.length)
145
+ return { slug: item.slug, action: "unchanged", definitionId: remote.definitionId };
146
+ const state = String(revision.state ?? "");
147
+ if (IN_REVIEW.has(state)) {
148
+ return { slug: item.slug, action: "blocked", definitionId: remote.definitionId, reason: `Summer is reviewing "${item.slug}" (${state}); its changes (${changes.join(", ")}) wait until the review is decided. Call again then.` };
149
+ }
150
+ const live = remote.state === "LIVE";
151
+ return {
152
+ slug: item.slug,
153
+ action: "update",
154
+ definitionId: remote.definitionId,
155
+ revisionId: String(revision.revisionId),
156
+ revision: EDITABLE.has(state) ? "edit_draft" : "new_draft",
157
+ changes,
158
+ ...(live && ownershipChanged ? { warnings: [`"${item.slug}" is live: new ownership rules apply to copies players get after the new revision is released.`] } : {}),
159
+ };
160
+ }
161
+ async function hashMedia(project, path, deps) {
162
+ const type = MEDIA_TYPES[extname(path).toLowerCase()];
163
+ let bytes;
164
+ try {
165
+ bytes = await deps.readFile(join(project, path));
166
+ }
167
+ catch {
168
+ throw new BuildToolError("item_media_missing", `${path} (named in summer.items.json) is not in the project.`, "Recovery: add the file or fix its path, then call again.");
169
+ }
170
+ if (bytes.length === 0 || bytes.length > type.maxBytes) {
171
+ throw new BuildToolError("item_media_too_large", `${path} is ${bytes.length} bytes; the store takes 1 to ${type.maxBytes} bytes for this kind.`, "Recovery: export a smaller file and call again.");
172
+ }
173
+ return { path, sha256: createHash("sha256").update(bytes).digest("hex"), sizeBytes: bytes.length, kind: type.kind, contentType: type.contentType };
174
+ }
175
+ /** The Game's item with this slug (0 or 1), its latest revision, price and media digests. */
176
+ async function readRemote(store, gameId, slug) {
177
+ const page = await store.call("GET", `games/${seg(gameId)}/item-definitions?slug=${seg(slug)}`);
178
+ const found = (page.items ?? []).find((definition) => definition.slug === slug);
179
+ if (!found)
180
+ return null;
181
+ const definitionId = String(found.definitionId);
182
+ // A Passport collectible is blocked by its authority alone (planItem): nothing else of it is read.
183
+ if (found.authority === "PASSPORT_CONTROL") {
184
+ return { definitionId, authority: "PASSPORT_CONTROL", state: typeof found.state === "string" ? found.state : null, slug, revision: {}, priceSparks: null, mediaSha256: [] };
185
+ }
186
+ const current = await store.call("GET", `games/${seg(gameId)}/item-definitions/${seg(definitionId)}`);
187
+ const revision = current.revision ?? {};
188
+ let priceSparks = null;
189
+ if (revision.priceBookId) {
190
+ const diff = await store.call("GET", `games/${seg(gameId)}/item-definitions/${seg(definitionId)}/revisions/${seg(String(revision.revisionId))}/price-book/diff`);
191
+ priceSparks = typeof diff.priceBook?.baseSparksAmount === "number" ? diff.priceBook.baseSparksAmount : null;
192
+ }
193
+ const mediaSha256 = [];
194
+ for (const assetId of (revision.mediaAssetIds ?? [])) {
195
+ const asset = await store.call("GET", `media-assets/${seg(assetId)}`);
196
+ mediaSha256.push(String(asset.sha256 ?? ""));
197
+ }
198
+ const authority = typeof current.definition?.authority === "string" ? current.definition.authority : typeof found.authority === "string" ? found.authority : null;
199
+ return { definitionId, authority, state: typeof current.definition?.state === "string" ? current.definition.state : null, slug, revision, priceSparks, mediaSha256 };
200
+ }
201
+ /** Upload one file as a Game media asset; the same file and name continue the same upload. */
202
+ async function uploadMedia(store, gameId, file, project, deps) {
203
+ const originalFilename = basename(file.path);
204
+ const key = (step) => createHash("sha256").update(`${gameId}:${file.sha256}:${originalFilename}:item-media:${step}`).digest("hex");
205
+ const contract = await store.call("POST", "media-assets:begin-upload", {
206
+ key: key("begin"),
207
+ body: { gameId, kind: file.kind, contentType: file.contentType, byteLength: file.sizeBytes, sha256: file.sha256, originalFilename },
208
+ });
209
+ const assetId = String(contract.assetId ?? "");
210
+ if (!assetId || !contract.uploadSessionId)
211
+ throw new BuildToolError("store_invalid_response", "The store did not return a media upload.", "Recovery: retry; the same upload continues.");
212
+ const asset = await store.call("GET", `media-assets/${seg(assetId)}`);
213
+ if (asset.state === "PENDING_UPLOAD") {
214
+ if (Array.isArray(contract.parts) && contract.parts.length) {
215
+ throw new BuildToolError("item_media_too_large", `${file.path} needs a multipart upload, which this tool does not do.`, "Recovery: use a smaller file, or upload it in Studio.");
216
+ }
217
+ const response = await deps.fetch(String(contract.url), {
218
+ method: String(contract.method ?? "PUT"),
219
+ headers: contract.headers ?? {},
220
+ body: new Uint8Array(await deps.readFile(join(project, file.path))),
221
+ redirect: "error",
222
+ signal: AbortSignal.timeout(300_000),
223
+ });
224
+ if (!response.ok && response.status !== 412) {
225
+ throw new BuildToolError("item_media_upload_failed", `${file.path} did not upload (${response.status}).`, "Recovery: call again; the same upload continues.");
226
+ }
227
+ }
228
+ if (asset.state === "PENDING_UPLOAD" || asset.state === "UPLOADED") {
229
+ await store.call("POST", `media-assets/${seg(assetId)}:complete`, { key: key("complete"), body: { uploadSessionId: contract.uploadSessionId } });
230
+ }
231
+ if (asset.state === "REJECTED" || asset.state === "EXPIRED") {
232
+ throw new BuildToolError("item_media_refused", `Summer did not keep ${file.path} (${asset.state}).`, "Recovery: check the image (not blank, a real PNG, JPEG or WebP) and call again.");
233
+ }
234
+ return assetId;
235
+ }
236
+ export async function syncItems(input, overrides = {}) {
237
+ const deps = { ...syncItemsDefaults, ...overrides };
238
+ const project = resolveExportProject(input.project);
239
+ const manifest = await readItemsManifest(project, input.file);
240
+ const only = input.slugs?.length ? new Set(input.slugs) : null;
241
+ const unknown = only ? [...only].filter((slug) => !manifest.items.some((item) => item.slug === slug)) : [];
242
+ if (unknown.length) {
243
+ throw new BuildToolError("item_slug_unknown", `summer.items.json has no item ${unknown.map((slug) => `"${slug}"`).join(", ")}.`, "Recovery: pass slugs from summer.items.json, or add the items there first.");
244
+ }
245
+ const items = manifest.items.filter((item) => !only || only.has(item.slug));
246
+ const gameId = input.gameId?.trim();
247
+ if (!gameId)
248
+ return listGamesOrThrow(deps);
249
+ await requireSignInForThisEnvironment(deps);
250
+ const gateway = await resolveGatewayUrl();
251
+ const store = createStore(gateway, await storeToken(deps), deps);
252
+ const media = new Map();
253
+ const remote = new Map();
254
+ const plans = [];
255
+ for (const item of items) {
256
+ const files = await Promise.all(item.media.map((path) => hashMedia(project, path, deps)));
257
+ media.set(item.slug, files);
258
+ const found = await readRemote(store, gameId, item.slug);
259
+ remote.set(item.slug, found);
260
+ plans.push(planItem(item, files, found));
261
+ }
262
+ const notInManifest = await listUnmanaged(store, gameId, new Set(manifest.items.map((item) => item.slug)));
263
+ // Sorted by slug: the same file always gives the same proposal and the same batches.
264
+ const pending = plans.filter((plan) => plan.action === "create" || plan.action === "update").sort((a, b) => (a.slug < b.slug ? -1 : a.slug > b.slug ? 1 : 0));
265
+ const blocked = plans.filter((plan) => plan.action === "blocked");
266
+ const batches = Array.from({ length: Math.ceil(pending.length / MAX_ITEMS_PER_PROPOSAL) }, (_, index) => pending.slice(index * MAX_ITEMS_PER_PROPOSAL, (index + 1) * MAX_ITEMS_PER_PROPOSAL).map((plan) => plan.slug));
267
+ const warnings = [
268
+ ...plans.flatMap((plan) => (plan.action === "update" && plan.warnings) || []),
269
+ ...(batches.length > 1 ? [`${pending.length} items change, and one owner approval takes at most ${MAX_ITEMS_PER_PROPOSAL}: confirm them in ${batches.length} calls, each with slugs from one of batches.`] : []),
270
+ ];
271
+ const summary = {
272
+ gateway,
273
+ gameId,
274
+ manifest: manifest.path,
275
+ plan: plans,
276
+ ...(notInManifest.length ? { notInManifest } : {}),
277
+ ...(warnings.length ? { warnings } : {}),
278
+ ...(batches.length > 1 ? { batches } : {}),
279
+ };
280
+ if (input.confirm !== true || !pending.length) {
281
+ return {
282
+ ok: true,
283
+ status: pending.length ? "confirmation_required" : "up_to_date",
284
+ ...summary,
285
+ next: pending.length
286
+ ? `Show the user this plan for ${gateway} (game ${gameId}): ${pending.length} item(s) to create or change${blocked.length ? `, ${blocked.length} blocked` : ""}. Only after they approve, call again with confirm=true: it uploads the changed media and sends the plan to the game's owner, who approves it in Studio. Nothing was uploaded or changed.`
287
+ : blocked.length
288
+ ? "Nothing can change now: read each blocked item's reason."
289
+ : "The Game's items already match summer.items.json.",
290
+ };
291
+ }
292
+ if (batches.length > 1) {
293
+ throw new BuildToolError("item_plan_too_large", `${pending.length} items would change, and one owner approval takes at most ${MAX_ITEMS_PER_PROPOSAL}. Nothing was uploaded or sent.`, `Recovery: call again with confirm=true and slugs set to one batch at a time (batches lists them, ${MAX_ITEMS_PER_PROPOSAL} per batch, by slug); each batch gets its own approval link.`, undefined, { batches });
294
+ }
295
+ const audit = { at: new Date(deps.now()).toISOString(), operation: "sync_items", face: input.face, gateway, gameId, slugs: pending.map((plan) => plan.slug) };
296
+ await appendStoreJsonLine("creator-audit.jsonl", { ...audit, outcome: "started" });
297
+ try {
298
+ const proposed = [];
299
+ for (const plan of pending) {
300
+ const item = items.find((candidate) => candidate.slug === plan.slug);
301
+ const mediaAssetIds = [];
302
+ for (const file of media.get(item.slug) ?? [])
303
+ mediaAssetIds.push(await uploadMedia(store, gameId, file, project, deps));
304
+ const { media: _paths, ...declared } = item;
305
+ proposed.push({
306
+ slug: item.slug,
307
+ action: plan.action,
308
+ ...(plan.action === "update" ? { definitionId: plan.definitionId, basedOnRevisionId: plan.revisionId } : {}),
309
+ item: { ...declared, mediaAssetIds },
310
+ });
311
+ }
312
+ // One request per exact plan: a retry asks the owner once, not twice.
313
+ const idempotencyKey = createHash("sha256").update(JSON.stringify({ gameId, proposed })).digest("hex").slice(0, 64);
314
+ const answer = await deps.propose({ gameId, items: proposed, idempotencyKey });
315
+ await appendStoreJsonLine("creator-audit.jsonl", { ...audit, outcome: String(answer.status ?? (answer.ok === false ? "failed" : "sent")) });
316
+ if (answer.ok === false) {
317
+ const error = (answer.error ?? {});
318
+ const code = String(error.code ?? "items_proposal_failed");
319
+ throw new BuildToolError(code, `${String(error.message ?? "Summer did not take the item plan.").replace(/\.?$/, ".")} Nothing changed.`, PROPOSAL_FAILURES[code] ?? "Recovery: the media is uploaded; call again with the same summer.items.json to send the same plan.", undefined, { ...(error.details ? { details: error.details } : {}), ...(error.requestId ? { requestId: error.requestId } : {}) });
320
+ }
321
+ return {
322
+ ok: true,
323
+ status: answer.status === "needs_approval" ? "awaiting_owner_approval" : String(answer.status ?? "sent"),
324
+ ...summary,
325
+ ...(answer.approval ? { approval: answer.approval } : {}),
326
+ next: "Give the owner the approval link. Nothing changes until they approve in Studio; then each item is submitted for Summer's review. Call again later to see the result.",
327
+ };
328
+ }
329
+ catch (error) {
330
+ await appendStoreJsonLine("creator-audit.jsonl", { ...audit, outcome: "failed", code: error instanceof BuildToolError ? error.code : "sync_items_failed" });
331
+ throw error;
332
+ }
333
+ }
334
+ /** Slugged items the Game has that summer.items.json does not name: listed, never changed. */
335
+ async function listUnmanaged(store, gameId, named) {
336
+ const out = [];
337
+ let cursor = "";
338
+ for (let page = 0; page < MAX_PAGES; page++) {
339
+ const body = await store.call("GET", `games/${seg(gameId)}/item-definitions?limit=100${cursor ? `&cursor=${seg(cursor)}` : ""}`);
340
+ for (const definition of body.items ?? []) {
341
+ if (definition.authority === "PASSPORT_CONTROL")
342
+ continue;
343
+ const slug = typeof definition.slug === "string" ? definition.slug : null;
344
+ if (!slug || !named.has(slug))
345
+ out.push({ slug, definitionId: String(definition.definitionId), name: typeof definition.name === "string" ? definition.name : null });
346
+ }
347
+ if (!body.nextCursor)
348
+ break;
349
+ cursor = body.nextCursor;
350
+ }
351
+ return out;
352
+ }