dreamboard 0.1.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.
- package/README.md +239 -0
- package/dist/chunk-2H7UOFLK.js +11 -0
- package/dist/chunk-2H7UOFLK.js.map +1 -0
- package/dist/chunk-FK6CWXQR.js +3479 -0
- package/dist/chunk-FK6CWXQR.js.map +1 -0
- package/dist/chunk-MP7IBNWW.js +13289 -0
- package/dist/chunk-MP7IBNWW.js.map +1 -0
- package/dist/dist-B3R64F6G.js +99 -0
- package/dist/dist-B3R64F6G.js.map +1 -0
- package/dist/embedded-harness-PF2LCIWC.js +345 -0
- package/dist/embedded-harness-PF2LCIWC.js.map +1 -0
- package/dist/index.js +25773 -0
- package/dist/index.js.map +1 -0
- package/dist/prompt-GMZABCJC.js +756 -0
- package/dist/prompt-GMZABCJC.js.map +1 -0
- package/package.json +40 -0
- package/skills/dreamboard/SKILL.md +119 -0
- package/skills/dreamboard/references/adversarial-testing.md +113 -0
- package/skills/dreamboard/references/all-players-tracking.md +75 -0
- package/skills/dreamboard/references/api-reference.md +193 -0
- package/skills/dreamboard/references/app-best-practices.md +86 -0
- package/skills/dreamboard/references/hands-vs-decks.md +86 -0
- package/skills/dreamboard/references/manifest-authoring.md +590 -0
- package/skills/dreamboard/references/phase-handlers.md +134 -0
- package/skills/dreamboard/references/rule-authoring.md +142 -0
- package/skills/dreamboard/references/scenario-format.md +99 -0
- package/skills/dreamboard/references/test-harness.md +225 -0
- package/skills/dreamboard/references/tts-migration-and-extractor.md +91 -0
- package/skills/dreamboard/references/ui-best-practices.md +158 -0
- package/skills/dreamboard/references/ui-genre-resource-management.md +187 -0
- package/skills/dreamboard/references/ui-genre-trick-taking.md +110 -0
- package/skills/dreamboard/references/ui-genre-worker-placement.md +143 -0
- package/skills/dreamboard/references/ui-style-guide.md +54 -0
- package/skills/dreamboard/scripts/events-extract.mjs +218 -0
- package/skills/dreamboard/scripts/extract_tts.py +1178 -0
|
@@ -0,0 +1,3479 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
|
|
3
|
+
// ../../node_modules/.pnpm/consola@3.4.2/node_modules/consola/dist/core.mjs
|
|
4
|
+
var LogLevels = {
|
|
5
|
+
silent: Number.NEGATIVE_INFINITY,
|
|
6
|
+
fatal: 0,
|
|
7
|
+
error: 0,
|
|
8
|
+
warn: 1,
|
|
9
|
+
log: 2,
|
|
10
|
+
info: 3,
|
|
11
|
+
success: 3,
|
|
12
|
+
fail: 3,
|
|
13
|
+
ready: 3,
|
|
14
|
+
start: 3,
|
|
15
|
+
box: 3,
|
|
16
|
+
debug: 4,
|
|
17
|
+
trace: 5,
|
|
18
|
+
verbose: Number.POSITIVE_INFINITY
|
|
19
|
+
};
|
|
20
|
+
var LogTypes = {
|
|
21
|
+
// Silent
|
|
22
|
+
silent: {
|
|
23
|
+
level: -1
|
|
24
|
+
},
|
|
25
|
+
// Level 0
|
|
26
|
+
fatal: {
|
|
27
|
+
level: LogLevels.fatal
|
|
28
|
+
},
|
|
29
|
+
error: {
|
|
30
|
+
level: LogLevels.error
|
|
31
|
+
},
|
|
32
|
+
// Level 1
|
|
33
|
+
warn: {
|
|
34
|
+
level: LogLevels.warn
|
|
35
|
+
},
|
|
36
|
+
// Level 2
|
|
37
|
+
log: {
|
|
38
|
+
level: LogLevels.log
|
|
39
|
+
},
|
|
40
|
+
// Level 3
|
|
41
|
+
info: {
|
|
42
|
+
level: LogLevels.info
|
|
43
|
+
},
|
|
44
|
+
success: {
|
|
45
|
+
level: LogLevels.success
|
|
46
|
+
},
|
|
47
|
+
fail: {
|
|
48
|
+
level: LogLevels.fail
|
|
49
|
+
},
|
|
50
|
+
ready: {
|
|
51
|
+
level: LogLevels.info
|
|
52
|
+
},
|
|
53
|
+
start: {
|
|
54
|
+
level: LogLevels.info
|
|
55
|
+
},
|
|
56
|
+
box: {
|
|
57
|
+
level: LogLevels.info
|
|
58
|
+
},
|
|
59
|
+
// Level 4
|
|
60
|
+
debug: {
|
|
61
|
+
level: LogLevels.debug
|
|
62
|
+
},
|
|
63
|
+
// Level 5
|
|
64
|
+
trace: {
|
|
65
|
+
level: LogLevels.trace
|
|
66
|
+
},
|
|
67
|
+
// Verbose
|
|
68
|
+
verbose: {
|
|
69
|
+
level: LogLevels.verbose
|
|
70
|
+
}
|
|
71
|
+
};
|
|
72
|
+
function isPlainObject$1(value) {
|
|
73
|
+
if (value === null || typeof value !== "object") {
|
|
74
|
+
return false;
|
|
75
|
+
}
|
|
76
|
+
const prototype = Object.getPrototypeOf(value);
|
|
77
|
+
if (prototype !== null && prototype !== Object.prototype && Object.getPrototypeOf(prototype) !== null) {
|
|
78
|
+
return false;
|
|
79
|
+
}
|
|
80
|
+
if (Symbol.iterator in value) {
|
|
81
|
+
return false;
|
|
82
|
+
}
|
|
83
|
+
if (Symbol.toStringTag in value) {
|
|
84
|
+
return Object.prototype.toString.call(value) === "[object Module]";
|
|
85
|
+
}
|
|
86
|
+
return true;
|
|
87
|
+
}
|
|
88
|
+
function _defu(baseObject, defaults, namespace = ".", merger) {
|
|
89
|
+
if (!isPlainObject$1(defaults)) {
|
|
90
|
+
return _defu(baseObject, {}, namespace, merger);
|
|
91
|
+
}
|
|
92
|
+
const object = Object.assign({}, defaults);
|
|
93
|
+
for (const key in baseObject) {
|
|
94
|
+
if (key === "__proto__" || key === "constructor") {
|
|
95
|
+
continue;
|
|
96
|
+
}
|
|
97
|
+
const value = baseObject[key];
|
|
98
|
+
if (value === null || value === void 0) {
|
|
99
|
+
continue;
|
|
100
|
+
}
|
|
101
|
+
if (merger && merger(object, key, value, namespace)) {
|
|
102
|
+
continue;
|
|
103
|
+
}
|
|
104
|
+
if (Array.isArray(value) && Array.isArray(object[key])) {
|
|
105
|
+
object[key] = [...value, ...object[key]];
|
|
106
|
+
} else if (isPlainObject$1(value) && isPlainObject$1(object[key])) {
|
|
107
|
+
object[key] = _defu(
|
|
108
|
+
value,
|
|
109
|
+
object[key],
|
|
110
|
+
(namespace ? `${namespace}.` : "") + key.toString(),
|
|
111
|
+
merger
|
|
112
|
+
);
|
|
113
|
+
} else {
|
|
114
|
+
object[key] = value;
|
|
115
|
+
}
|
|
116
|
+
}
|
|
117
|
+
return object;
|
|
118
|
+
}
|
|
119
|
+
function createDefu(merger) {
|
|
120
|
+
return (...arguments_) => (
|
|
121
|
+
// eslint-disable-next-line unicorn/no-array-reduce
|
|
122
|
+
arguments_.reduce((p, c2) => _defu(p, c2, "", merger), {})
|
|
123
|
+
);
|
|
124
|
+
}
|
|
125
|
+
var defu = createDefu();
|
|
126
|
+
function isPlainObject(obj) {
|
|
127
|
+
return Object.prototype.toString.call(obj) === "[object Object]";
|
|
128
|
+
}
|
|
129
|
+
function isLogObj(arg) {
|
|
130
|
+
if (!isPlainObject(arg)) {
|
|
131
|
+
return false;
|
|
132
|
+
}
|
|
133
|
+
if (!arg.message && !arg.args) {
|
|
134
|
+
return false;
|
|
135
|
+
}
|
|
136
|
+
if (arg.stack) {
|
|
137
|
+
return false;
|
|
138
|
+
}
|
|
139
|
+
return true;
|
|
140
|
+
}
|
|
141
|
+
var paused = false;
|
|
142
|
+
var queue = [];
|
|
143
|
+
var Consola = class _Consola {
|
|
144
|
+
options;
|
|
145
|
+
_lastLog;
|
|
146
|
+
_mockFn;
|
|
147
|
+
/**
|
|
148
|
+
* Creates an instance of Consola with specified options or defaults.
|
|
149
|
+
*
|
|
150
|
+
* @param {Partial<ConsolaOptions>} [options={}] - Configuration options for the Consola instance.
|
|
151
|
+
*/
|
|
152
|
+
constructor(options = {}) {
|
|
153
|
+
const types = options.types || LogTypes;
|
|
154
|
+
this.options = defu(
|
|
155
|
+
{
|
|
156
|
+
...options,
|
|
157
|
+
defaults: { ...options.defaults },
|
|
158
|
+
level: _normalizeLogLevel(options.level, types),
|
|
159
|
+
reporters: [...options.reporters || []]
|
|
160
|
+
},
|
|
161
|
+
{
|
|
162
|
+
types: LogTypes,
|
|
163
|
+
throttle: 1e3,
|
|
164
|
+
throttleMin: 5,
|
|
165
|
+
formatOptions: {
|
|
166
|
+
date: true,
|
|
167
|
+
colors: false,
|
|
168
|
+
compact: true
|
|
169
|
+
}
|
|
170
|
+
}
|
|
171
|
+
);
|
|
172
|
+
for (const type in types) {
|
|
173
|
+
const defaults = {
|
|
174
|
+
type,
|
|
175
|
+
...this.options.defaults,
|
|
176
|
+
...types[type]
|
|
177
|
+
};
|
|
178
|
+
this[type] = this._wrapLogFn(defaults);
|
|
179
|
+
this[type].raw = this._wrapLogFn(
|
|
180
|
+
defaults,
|
|
181
|
+
true
|
|
182
|
+
);
|
|
183
|
+
}
|
|
184
|
+
if (this.options.mockFn) {
|
|
185
|
+
this.mockTypes();
|
|
186
|
+
}
|
|
187
|
+
this._lastLog = {};
|
|
188
|
+
}
|
|
189
|
+
/**
|
|
190
|
+
* Gets the current log level of the Consola instance.
|
|
191
|
+
*
|
|
192
|
+
* @returns {number} The current log level.
|
|
193
|
+
*/
|
|
194
|
+
get level() {
|
|
195
|
+
return this.options.level;
|
|
196
|
+
}
|
|
197
|
+
/**
|
|
198
|
+
* Sets the minimum log level that will be output by the instance.
|
|
199
|
+
*
|
|
200
|
+
* @param {number} level - The new log level to set.
|
|
201
|
+
*/
|
|
202
|
+
set level(level) {
|
|
203
|
+
this.options.level = _normalizeLogLevel(
|
|
204
|
+
level,
|
|
205
|
+
this.options.types,
|
|
206
|
+
this.options.level
|
|
207
|
+
);
|
|
208
|
+
}
|
|
209
|
+
/**
|
|
210
|
+
* Displays a prompt to the user and returns the response.
|
|
211
|
+
* Throw an error if `prompt` is not supported by the current configuration.
|
|
212
|
+
*
|
|
213
|
+
* @template T
|
|
214
|
+
* @param {string} message - The message to display in the prompt.
|
|
215
|
+
* @param {T} [opts] - Optional options for the prompt. See {@link PromptOptions}.
|
|
216
|
+
* @returns {promise<T>} A promise that infer with the prompt options. See {@link PromptOptions}.
|
|
217
|
+
*/
|
|
218
|
+
prompt(message, opts) {
|
|
219
|
+
if (!this.options.prompt) {
|
|
220
|
+
throw new Error("prompt is not supported!");
|
|
221
|
+
}
|
|
222
|
+
return this.options.prompt(message, opts);
|
|
223
|
+
}
|
|
224
|
+
/**
|
|
225
|
+
* Creates a new instance of Consola, inheriting options from the current instance, with possible overrides.
|
|
226
|
+
*
|
|
227
|
+
* @param {Partial<ConsolaOptions>} options - Optional overrides for the new instance. See {@link ConsolaOptions}.
|
|
228
|
+
* @returns {ConsolaInstance} A new Consola instance. See {@link ConsolaInstance}.
|
|
229
|
+
*/
|
|
230
|
+
create(options) {
|
|
231
|
+
const instance = new _Consola({
|
|
232
|
+
...this.options,
|
|
233
|
+
...options
|
|
234
|
+
});
|
|
235
|
+
if (this._mockFn) {
|
|
236
|
+
instance.mockTypes(this._mockFn);
|
|
237
|
+
}
|
|
238
|
+
return instance;
|
|
239
|
+
}
|
|
240
|
+
/**
|
|
241
|
+
* Creates a new Consola instance with the specified default log object properties.
|
|
242
|
+
*
|
|
243
|
+
* @param {InputLogObject} defaults - Default properties to include in any log from the new instance. See {@link InputLogObject}.
|
|
244
|
+
* @returns {ConsolaInstance} A new Consola instance. See {@link ConsolaInstance}.
|
|
245
|
+
*/
|
|
246
|
+
withDefaults(defaults) {
|
|
247
|
+
return this.create({
|
|
248
|
+
...this.options,
|
|
249
|
+
defaults: {
|
|
250
|
+
...this.options.defaults,
|
|
251
|
+
...defaults
|
|
252
|
+
}
|
|
253
|
+
});
|
|
254
|
+
}
|
|
255
|
+
/**
|
|
256
|
+
* Creates a new Consola instance with a specified tag, which will be included in every log.
|
|
257
|
+
*
|
|
258
|
+
* @param {string} tag - The tag to include in each log of the new instance.
|
|
259
|
+
* @returns {ConsolaInstance} A new Consola instance. See {@link ConsolaInstance}.
|
|
260
|
+
*/
|
|
261
|
+
withTag(tag) {
|
|
262
|
+
return this.withDefaults({
|
|
263
|
+
tag: this.options.defaults.tag ? this.options.defaults.tag + ":" + tag : tag
|
|
264
|
+
});
|
|
265
|
+
}
|
|
266
|
+
/**
|
|
267
|
+
* Adds a custom reporter to the Consola instance.
|
|
268
|
+
* Reporters will be called for each log message, depending on their implementation and log level.
|
|
269
|
+
*
|
|
270
|
+
* @param {ConsolaReporter} reporter - The reporter to add. See {@link ConsolaReporter}.
|
|
271
|
+
* @returns {Consola} The current Consola instance.
|
|
272
|
+
*/
|
|
273
|
+
addReporter(reporter) {
|
|
274
|
+
this.options.reporters.push(reporter);
|
|
275
|
+
return this;
|
|
276
|
+
}
|
|
277
|
+
/**
|
|
278
|
+
* Removes a custom reporter from the Consola instance.
|
|
279
|
+
* If no reporter is specified, all reporters will be removed.
|
|
280
|
+
*
|
|
281
|
+
* @param {ConsolaReporter} reporter - The reporter to remove. See {@link ConsolaReporter}.
|
|
282
|
+
* @returns {Consola} The current Consola instance.
|
|
283
|
+
*/
|
|
284
|
+
removeReporter(reporter) {
|
|
285
|
+
if (reporter) {
|
|
286
|
+
const i2 = this.options.reporters.indexOf(reporter);
|
|
287
|
+
if (i2 !== -1) {
|
|
288
|
+
return this.options.reporters.splice(i2, 1);
|
|
289
|
+
}
|
|
290
|
+
} else {
|
|
291
|
+
this.options.reporters.splice(0);
|
|
292
|
+
}
|
|
293
|
+
return this;
|
|
294
|
+
}
|
|
295
|
+
/**
|
|
296
|
+
* Replaces all reporters of the Consola instance with the specified array of reporters.
|
|
297
|
+
*
|
|
298
|
+
* @param {ConsolaReporter[]} reporters - The new reporters to set. See {@link ConsolaReporter}.
|
|
299
|
+
* @returns {Consola} The current Consola instance.
|
|
300
|
+
*/
|
|
301
|
+
setReporters(reporters) {
|
|
302
|
+
this.options.reporters = Array.isArray(reporters) ? reporters : [reporters];
|
|
303
|
+
return this;
|
|
304
|
+
}
|
|
305
|
+
wrapAll() {
|
|
306
|
+
this.wrapConsole();
|
|
307
|
+
this.wrapStd();
|
|
308
|
+
}
|
|
309
|
+
restoreAll() {
|
|
310
|
+
this.restoreConsole();
|
|
311
|
+
this.restoreStd();
|
|
312
|
+
}
|
|
313
|
+
/**
|
|
314
|
+
* Overrides console methods with Consola logging methods for consistent logging.
|
|
315
|
+
*/
|
|
316
|
+
wrapConsole() {
|
|
317
|
+
for (const type in this.options.types) {
|
|
318
|
+
if (!console["__" + type]) {
|
|
319
|
+
console["__" + type] = console[type];
|
|
320
|
+
}
|
|
321
|
+
console[type] = this[type].raw;
|
|
322
|
+
}
|
|
323
|
+
}
|
|
324
|
+
/**
|
|
325
|
+
* Restores the original console methods, removing Consola overrides.
|
|
326
|
+
*/
|
|
327
|
+
restoreConsole() {
|
|
328
|
+
for (const type in this.options.types) {
|
|
329
|
+
if (console["__" + type]) {
|
|
330
|
+
console[type] = console["__" + type];
|
|
331
|
+
delete console["__" + type];
|
|
332
|
+
}
|
|
333
|
+
}
|
|
334
|
+
}
|
|
335
|
+
/**
|
|
336
|
+
* Overrides standard output and error streams to redirect them through Consola.
|
|
337
|
+
*/
|
|
338
|
+
wrapStd() {
|
|
339
|
+
this._wrapStream(this.options.stdout, "log");
|
|
340
|
+
this._wrapStream(this.options.stderr, "log");
|
|
341
|
+
}
|
|
342
|
+
_wrapStream(stream, type) {
|
|
343
|
+
if (!stream) {
|
|
344
|
+
return;
|
|
345
|
+
}
|
|
346
|
+
if (!stream.__write) {
|
|
347
|
+
stream.__write = stream.write;
|
|
348
|
+
}
|
|
349
|
+
stream.write = (data) => {
|
|
350
|
+
this[type].raw(String(data).trim());
|
|
351
|
+
};
|
|
352
|
+
}
|
|
353
|
+
/**
|
|
354
|
+
* Restores the original standard output and error streams, removing the Consola redirection.
|
|
355
|
+
*/
|
|
356
|
+
restoreStd() {
|
|
357
|
+
this._restoreStream(this.options.stdout);
|
|
358
|
+
this._restoreStream(this.options.stderr);
|
|
359
|
+
}
|
|
360
|
+
_restoreStream(stream) {
|
|
361
|
+
if (!stream) {
|
|
362
|
+
return;
|
|
363
|
+
}
|
|
364
|
+
if (stream.__write) {
|
|
365
|
+
stream.write = stream.__write;
|
|
366
|
+
delete stream.__write;
|
|
367
|
+
}
|
|
368
|
+
}
|
|
369
|
+
/**
|
|
370
|
+
* Pauses logging, queues incoming logs until resumed.
|
|
371
|
+
*/
|
|
372
|
+
pauseLogs() {
|
|
373
|
+
paused = true;
|
|
374
|
+
}
|
|
375
|
+
/**
|
|
376
|
+
* Resumes logging, processing any queued logs.
|
|
377
|
+
*/
|
|
378
|
+
resumeLogs() {
|
|
379
|
+
paused = false;
|
|
380
|
+
const _queue = queue.splice(0);
|
|
381
|
+
for (const item of _queue) {
|
|
382
|
+
item[0]._logFn(item[1], item[2]);
|
|
383
|
+
}
|
|
384
|
+
}
|
|
385
|
+
/**
|
|
386
|
+
* Replaces logging methods with mocks if a mock function is provided.
|
|
387
|
+
*
|
|
388
|
+
* @param {ConsolaOptions["mockFn"]} mockFn - The function to use for mocking logging methods. See {@link ConsolaOptions["mockFn"]}.
|
|
389
|
+
*/
|
|
390
|
+
mockTypes(mockFn) {
|
|
391
|
+
const _mockFn = mockFn || this.options.mockFn;
|
|
392
|
+
this._mockFn = _mockFn;
|
|
393
|
+
if (typeof _mockFn !== "function") {
|
|
394
|
+
return;
|
|
395
|
+
}
|
|
396
|
+
for (const type in this.options.types) {
|
|
397
|
+
this[type] = _mockFn(type, this.options.types[type]) || this[type];
|
|
398
|
+
this[type].raw = this[type];
|
|
399
|
+
}
|
|
400
|
+
}
|
|
401
|
+
_wrapLogFn(defaults, isRaw) {
|
|
402
|
+
return (...args) => {
|
|
403
|
+
if (paused) {
|
|
404
|
+
queue.push([this, defaults, args, isRaw]);
|
|
405
|
+
return;
|
|
406
|
+
}
|
|
407
|
+
return this._logFn(defaults, args, isRaw);
|
|
408
|
+
};
|
|
409
|
+
}
|
|
410
|
+
_logFn(defaults, args, isRaw) {
|
|
411
|
+
if ((defaults.level || 0) > this.level) {
|
|
412
|
+
return false;
|
|
413
|
+
}
|
|
414
|
+
const logObj = {
|
|
415
|
+
date: /* @__PURE__ */ new Date(),
|
|
416
|
+
args: [],
|
|
417
|
+
...defaults,
|
|
418
|
+
level: _normalizeLogLevel(defaults.level, this.options.types)
|
|
419
|
+
};
|
|
420
|
+
if (!isRaw && args.length === 1 && isLogObj(args[0])) {
|
|
421
|
+
Object.assign(logObj, args[0]);
|
|
422
|
+
} else {
|
|
423
|
+
logObj.args = [...args];
|
|
424
|
+
}
|
|
425
|
+
if (logObj.message) {
|
|
426
|
+
logObj.args.unshift(logObj.message);
|
|
427
|
+
delete logObj.message;
|
|
428
|
+
}
|
|
429
|
+
if (logObj.additional) {
|
|
430
|
+
if (!Array.isArray(logObj.additional)) {
|
|
431
|
+
logObj.additional = logObj.additional.split("\n");
|
|
432
|
+
}
|
|
433
|
+
logObj.args.push("\n" + logObj.additional.join("\n"));
|
|
434
|
+
delete logObj.additional;
|
|
435
|
+
}
|
|
436
|
+
logObj.type = typeof logObj.type === "string" ? logObj.type.toLowerCase() : "log";
|
|
437
|
+
logObj.tag = typeof logObj.tag === "string" ? logObj.tag : "";
|
|
438
|
+
const resolveLog = (newLog = false) => {
|
|
439
|
+
const repeated = (this._lastLog.count || 0) - this.options.throttleMin;
|
|
440
|
+
if (this._lastLog.object && repeated > 0) {
|
|
441
|
+
const args2 = [...this._lastLog.object.args];
|
|
442
|
+
if (repeated > 1) {
|
|
443
|
+
args2.push(`(repeated ${repeated} times)`);
|
|
444
|
+
}
|
|
445
|
+
this._log({ ...this._lastLog.object, args: args2 });
|
|
446
|
+
this._lastLog.count = 1;
|
|
447
|
+
}
|
|
448
|
+
if (newLog) {
|
|
449
|
+
this._lastLog.object = logObj;
|
|
450
|
+
this._log(logObj);
|
|
451
|
+
}
|
|
452
|
+
};
|
|
453
|
+
clearTimeout(this._lastLog.timeout);
|
|
454
|
+
const diffTime = this._lastLog.time && logObj.date ? logObj.date.getTime() - this._lastLog.time.getTime() : 0;
|
|
455
|
+
this._lastLog.time = logObj.date;
|
|
456
|
+
if (diffTime < this.options.throttle) {
|
|
457
|
+
try {
|
|
458
|
+
const serializedLog = JSON.stringify([
|
|
459
|
+
logObj.type,
|
|
460
|
+
logObj.tag,
|
|
461
|
+
logObj.args
|
|
462
|
+
]);
|
|
463
|
+
const isSameLog = this._lastLog.serialized === serializedLog;
|
|
464
|
+
this._lastLog.serialized = serializedLog;
|
|
465
|
+
if (isSameLog) {
|
|
466
|
+
this._lastLog.count = (this._lastLog.count || 0) + 1;
|
|
467
|
+
if (this._lastLog.count > this.options.throttleMin) {
|
|
468
|
+
this._lastLog.timeout = setTimeout(
|
|
469
|
+
resolveLog,
|
|
470
|
+
this.options.throttle
|
|
471
|
+
);
|
|
472
|
+
return;
|
|
473
|
+
}
|
|
474
|
+
}
|
|
475
|
+
} catch {
|
|
476
|
+
}
|
|
477
|
+
}
|
|
478
|
+
resolveLog(true);
|
|
479
|
+
}
|
|
480
|
+
_log(logObj) {
|
|
481
|
+
for (const reporter of this.options.reporters) {
|
|
482
|
+
reporter.log(logObj, {
|
|
483
|
+
options: this.options
|
|
484
|
+
});
|
|
485
|
+
}
|
|
486
|
+
}
|
|
487
|
+
};
|
|
488
|
+
function _normalizeLogLevel(input, types = {}, defaultLevel = 3) {
|
|
489
|
+
if (input === void 0) {
|
|
490
|
+
return defaultLevel;
|
|
491
|
+
}
|
|
492
|
+
if (typeof input === "number") {
|
|
493
|
+
return input;
|
|
494
|
+
}
|
|
495
|
+
if (types[input] && types[input].level !== void 0) {
|
|
496
|
+
return types[input].level;
|
|
497
|
+
}
|
|
498
|
+
return defaultLevel;
|
|
499
|
+
}
|
|
500
|
+
Consola.prototype.add = Consola.prototype.addReporter;
|
|
501
|
+
Consola.prototype.remove = Consola.prototype.removeReporter;
|
|
502
|
+
Consola.prototype.clear = Consola.prototype.removeReporter;
|
|
503
|
+
Consola.prototype.withScope = Consola.prototype.withTag;
|
|
504
|
+
Consola.prototype.mock = Consola.prototype.mockTypes;
|
|
505
|
+
Consola.prototype.pause = Consola.prototype.pauseLogs;
|
|
506
|
+
Consola.prototype.resume = Consola.prototype.resumeLogs;
|
|
507
|
+
function createConsola(options = {}) {
|
|
508
|
+
return new Consola(options);
|
|
509
|
+
}
|
|
510
|
+
|
|
511
|
+
// ../../node_modules/.pnpm/consola@3.4.2/node_modules/consola/dist/shared/consola.DRwqZj3T.mjs
|
|
512
|
+
import { formatWithOptions } from "util";
|
|
513
|
+
import { sep } from "path";
|
|
514
|
+
function parseStack(stack, message) {
|
|
515
|
+
const cwd = process.cwd() + sep;
|
|
516
|
+
const lines = stack.split("\n").splice(message.split("\n").length).map((l2) => l2.trim().replace("file://", "").replace(cwd, ""));
|
|
517
|
+
return lines;
|
|
518
|
+
}
|
|
519
|
+
function writeStream(data, stream) {
|
|
520
|
+
const write = stream.__write || stream.write;
|
|
521
|
+
return write.call(stream, data);
|
|
522
|
+
}
|
|
523
|
+
var bracket = (x) => x ? `[${x}]` : "";
|
|
524
|
+
var BasicReporter = class {
|
|
525
|
+
formatStack(stack, message, opts) {
|
|
526
|
+
const indent = " ".repeat((opts?.errorLevel || 0) + 1);
|
|
527
|
+
return indent + parseStack(stack, message).join(`
|
|
528
|
+
${indent}`);
|
|
529
|
+
}
|
|
530
|
+
formatError(err, opts) {
|
|
531
|
+
const message = err.message ?? formatWithOptions(opts, err);
|
|
532
|
+
const stack = err.stack ? this.formatStack(err.stack, message, opts) : "";
|
|
533
|
+
const level = opts?.errorLevel || 0;
|
|
534
|
+
const causedPrefix = level > 0 ? `${" ".repeat(level)}[cause]: ` : "";
|
|
535
|
+
const causedError = err.cause ? "\n\n" + this.formatError(err.cause, { ...opts, errorLevel: level + 1 }) : "";
|
|
536
|
+
return causedPrefix + message + "\n" + stack + causedError;
|
|
537
|
+
}
|
|
538
|
+
formatArgs(args, opts) {
|
|
539
|
+
const _args = args.map((arg) => {
|
|
540
|
+
if (arg && typeof arg.stack === "string") {
|
|
541
|
+
return this.formatError(arg, opts);
|
|
542
|
+
}
|
|
543
|
+
return arg;
|
|
544
|
+
});
|
|
545
|
+
return formatWithOptions(opts, ..._args);
|
|
546
|
+
}
|
|
547
|
+
formatDate(date, opts) {
|
|
548
|
+
return opts.date ? date.toLocaleTimeString() : "";
|
|
549
|
+
}
|
|
550
|
+
filterAndJoin(arr) {
|
|
551
|
+
return arr.filter(Boolean).join(" ");
|
|
552
|
+
}
|
|
553
|
+
formatLogObj(logObj, opts) {
|
|
554
|
+
const message = this.formatArgs(logObj.args, opts);
|
|
555
|
+
if (logObj.type === "box") {
|
|
556
|
+
return "\n" + [
|
|
557
|
+
bracket(logObj.tag),
|
|
558
|
+
logObj.title && logObj.title,
|
|
559
|
+
...message.split("\n")
|
|
560
|
+
].filter(Boolean).map((l2) => " > " + l2).join("\n") + "\n";
|
|
561
|
+
}
|
|
562
|
+
return this.filterAndJoin([
|
|
563
|
+
bracket(logObj.type),
|
|
564
|
+
bracket(logObj.tag),
|
|
565
|
+
message
|
|
566
|
+
]);
|
|
567
|
+
}
|
|
568
|
+
log(logObj, ctx) {
|
|
569
|
+
const line = this.formatLogObj(logObj, {
|
|
570
|
+
columns: ctx.options.stdout.columns || 0,
|
|
571
|
+
...ctx.options.formatOptions
|
|
572
|
+
});
|
|
573
|
+
return writeStream(
|
|
574
|
+
line + "\n",
|
|
575
|
+
logObj.level < 2 ? ctx.options.stderr || process.stderr : ctx.options.stdout || process.stdout
|
|
576
|
+
);
|
|
577
|
+
}
|
|
578
|
+
};
|
|
579
|
+
|
|
580
|
+
// ../../node_modules/.pnpm/consola@3.4.2/node_modules/consola/dist/index.mjs
|
|
581
|
+
import g$1 from "process";
|
|
582
|
+
|
|
583
|
+
// ../../node_modules/.pnpm/consola@3.4.2/node_modules/consola/dist/shared/consola.DXBYu-KD.mjs
|
|
584
|
+
import * as tty from "tty";
|
|
585
|
+
var {
|
|
586
|
+
env = {},
|
|
587
|
+
argv = [],
|
|
588
|
+
platform = ""
|
|
589
|
+
} = typeof process === "undefined" ? {} : process;
|
|
590
|
+
var isDisabled = "NO_COLOR" in env || argv.includes("--no-color");
|
|
591
|
+
var isForced = "FORCE_COLOR" in env || argv.includes("--color");
|
|
592
|
+
var isWindows = platform === "win32";
|
|
593
|
+
var isDumbTerminal = env.TERM === "dumb";
|
|
594
|
+
var isCompatibleTerminal = tty && tty.isatty && tty.isatty(1) && env.TERM && !isDumbTerminal;
|
|
595
|
+
var isCI = "CI" in env && ("GITHUB_ACTIONS" in env || "GITLAB_CI" in env || "CIRCLECI" in env);
|
|
596
|
+
var isColorSupported = !isDisabled && (isForced || isWindows && !isDumbTerminal || isCompatibleTerminal || isCI);
|
|
597
|
+
function replaceClose(index, string, close, replace, head = string.slice(0, Math.max(0, index)) + replace, tail = string.slice(Math.max(0, index + close.length)), next = tail.indexOf(close)) {
|
|
598
|
+
return head + (next < 0 ? tail : replaceClose(next, tail, close, replace));
|
|
599
|
+
}
|
|
600
|
+
function clearBleed(index, string, open, close, replace) {
|
|
601
|
+
return index < 0 ? open + string + close : open + replaceClose(index, string, close, replace) + close;
|
|
602
|
+
}
|
|
603
|
+
function filterEmpty(open, close, replace = open, at = open.length + 1) {
|
|
604
|
+
return (string) => string || !(string === "" || string === void 0) ? clearBleed(
|
|
605
|
+
("" + string).indexOf(close, at),
|
|
606
|
+
string,
|
|
607
|
+
open,
|
|
608
|
+
close,
|
|
609
|
+
replace
|
|
610
|
+
) : "";
|
|
611
|
+
}
|
|
612
|
+
function init(open, close, replace) {
|
|
613
|
+
return filterEmpty(`\x1B[${open}m`, `\x1B[${close}m`, replace);
|
|
614
|
+
}
|
|
615
|
+
var colorDefs = {
|
|
616
|
+
reset: init(0, 0),
|
|
617
|
+
bold: init(1, 22, "\x1B[22m\x1B[1m"),
|
|
618
|
+
dim: init(2, 22, "\x1B[22m\x1B[2m"),
|
|
619
|
+
italic: init(3, 23),
|
|
620
|
+
underline: init(4, 24),
|
|
621
|
+
inverse: init(7, 27),
|
|
622
|
+
hidden: init(8, 28),
|
|
623
|
+
strikethrough: init(9, 29),
|
|
624
|
+
black: init(30, 39),
|
|
625
|
+
red: init(31, 39),
|
|
626
|
+
green: init(32, 39),
|
|
627
|
+
yellow: init(33, 39),
|
|
628
|
+
blue: init(34, 39),
|
|
629
|
+
magenta: init(35, 39),
|
|
630
|
+
cyan: init(36, 39),
|
|
631
|
+
white: init(37, 39),
|
|
632
|
+
gray: init(90, 39),
|
|
633
|
+
bgBlack: init(40, 49),
|
|
634
|
+
bgRed: init(41, 49),
|
|
635
|
+
bgGreen: init(42, 49),
|
|
636
|
+
bgYellow: init(43, 49),
|
|
637
|
+
bgBlue: init(44, 49),
|
|
638
|
+
bgMagenta: init(45, 49),
|
|
639
|
+
bgCyan: init(46, 49),
|
|
640
|
+
bgWhite: init(47, 49),
|
|
641
|
+
blackBright: init(90, 39),
|
|
642
|
+
redBright: init(91, 39),
|
|
643
|
+
greenBright: init(92, 39),
|
|
644
|
+
yellowBright: init(93, 39),
|
|
645
|
+
blueBright: init(94, 39),
|
|
646
|
+
magentaBright: init(95, 39),
|
|
647
|
+
cyanBright: init(96, 39),
|
|
648
|
+
whiteBright: init(97, 39),
|
|
649
|
+
bgBlackBright: init(100, 49),
|
|
650
|
+
bgRedBright: init(101, 49),
|
|
651
|
+
bgGreenBright: init(102, 49),
|
|
652
|
+
bgYellowBright: init(103, 49),
|
|
653
|
+
bgBlueBright: init(104, 49),
|
|
654
|
+
bgMagentaBright: init(105, 49),
|
|
655
|
+
bgCyanBright: init(106, 49),
|
|
656
|
+
bgWhiteBright: init(107, 49)
|
|
657
|
+
};
|
|
658
|
+
function createColors(useColor = isColorSupported) {
|
|
659
|
+
return useColor ? colorDefs : Object.fromEntries(Object.keys(colorDefs).map((key) => [key, String]));
|
|
660
|
+
}
|
|
661
|
+
var colors = createColors();
|
|
662
|
+
function getColor(color, fallback = "reset") {
|
|
663
|
+
return colors[color] || colors[fallback];
|
|
664
|
+
}
|
|
665
|
+
var ansiRegex = [
|
|
666
|
+
String.raw`[\u001B\u009B][[\]()#;?]*(?:(?:(?:(?:;[-a-zA-Z\d\/#&.:=?%@~_]+)*|[a-zA-Z\d]+(?:;[-a-zA-Z\d\/#&.:=?%@~_]*)*)?\u0007)`,
|
|
667
|
+
String.raw`(?:(?:\d{1,4}(?:;\d{0,4})*)?[\dA-PR-TZcf-nq-uy=><~]))`
|
|
668
|
+
].join("|");
|
|
669
|
+
function stripAnsi(text) {
|
|
670
|
+
return text.replace(new RegExp(ansiRegex, "g"), "");
|
|
671
|
+
}
|
|
672
|
+
var boxStylePresets = {
|
|
673
|
+
solid: {
|
|
674
|
+
tl: "\u250C",
|
|
675
|
+
tr: "\u2510",
|
|
676
|
+
bl: "\u2514",
|
|
677
|
+
br: "\u2518",
|
|
678
|
+
h: "\u2500",
|
|
679
|
+
v: "\u2502"
|
|
680
|
+
},
|
|
681
|
+
double: {
|
|
682
|
+
tl: "\u2554",
|
|
683
|
+
tr: "\u2557",
|
|
684
|
+
bl: "\u255A",
|
|
685
|
+
br: "\u255D",
|
|
686
|
+
h: "\u2550",
|
|
687
|
+
v: "\u2551"
|
|
688
|
+
},
|
|
689
|
+
doubleSingle: {
|
|
690
|
+
tl: "\u2553",
|
|
691
|
+
tr: "\u2556",
|
|
692
|
+
bl: "\u2559",
|
|
693
|
+
br: "\u255C",
|
|
694
|
+
h: "\u2500",
|
|
695
|
+
v: "\u2551"
|
|
696
|
+
},
|
|
697
|
+
doubleSingleRounded: {
|
|
698
|
+
tl: "\u256D",
|
|
699
|
+
tr: "\u256E",
|
|
700
|
+
bl: "\u2570",
|
|
701
|
+
br: "\u256F",
|
|
702
|
+
h: "\u2500",
|
|
703
|
+
v: "\u2551"
|
|
704
|
+
},
|
|
705
|
+
singleThick: {
|
|
706
|
+
tl: "\u250F",
|
|
707
|
+
tr: "\u2513",
|
|
708
|
+
bl: "\u2517",
|
|
709
|
+
br: "\u251B",
|
|
710
|
+
h: "\u2501",
|
|
711
|
+
v: "\u2503"
|
|
712
|
+
},
|
|
713
|
+
singleDouble: {
|
|
714
|
+
tl: "\u2552",
|
|
715
|
+
tr: "\u2555",
|
|
716
|
+
bl: "\u2558",
|
|
717
|
+
br: "\u255B",
|
|
718
|
+
h: "\u2550",
|
|
719
|
+
v: "\u2502"
|
|
720
|
+
},
|
|
721
|
+
singleDoubleRounded: {
|
|
722
|
+
tl: "\u256D",
|
|
723
|
+
tr: "\u256E",
|
|
724
|
+
bl: "\u2570",
|
|
725
|
+
br: "\u256F",
|
|
726
|
+
h: "\u2550",
|
|
727
|
+
v: "\u2502"
|
|
728
|
+
},
|
|
729
|
+
rounded: {
|
|
730
|
+
tl: "\u256D",
|
|
731
|
+
tr: "\u256E",
|
|
732
|
+
bl: "\u2570",
|
|
733
|
+
br: "\u256F",
|
|
734
|
+
h: "\u2500",
|
|
735
|
+
v: "\u2502"
|
|
736
|
+
}
|
|
737
|
+
};
|
|
738
|
+
var defaultStyle = {
|
|
739
|
+
borderColor: "white",
|
|
740
|
+
borderStyle: "rounded",
|
|
741
|
+
valign: "center",
|
|
742
|
+
padding: 2,
|
|
743
|
+
marginLeft: 1,
|
|
744
|
+
marginTop: 1,
|
|
745
|
+
marginBottom: 1
|
|
746
|
+
};
|
|
747
|
+
function box(text, _opts = {}) {
|
|
748
|
+
const opts = {
|
|
749
|
+
..._opts,
|
|
750
|
+
style: {
|
|
751
|
+
...defaultStyle,
|
|
752
|
+
..._opts.style
|
|
753
|
+
}
|
|
754
|
+
};
|
|
755
|
+
const textLines = text.split("\n");
|
|
756
|
+
const boxLines = [];
|
|
757
|
+
const _color = getColor(opts.style.borderColor);
|
|
758
|
+
const borderStyle = {
|
|
759
|
+
...typeof opts.style.borderStyle === "string" ? boxStylePresets[opts.style.borderStyle] || boxStylePresets.solid : opts.style.borderStyle
|
|
760
|
+
};
|
|
761
|
+
if (_color) {
|
|
762
|
+
for (const key in borderStyle) {
|
|
763
|
+
borderStyle[key] = _color(
|
|
764
|
+
borderStyle[key]
|
|
765
|
+
);
|
|
766
|
+
}
|
|
767
|
+
}
|
|
768
|
+
const paddingOffset = opts.style.padding % 2 === 0 ? opts.style.padding : opts.style.padding + 1;
|
|
769
|
+
const height = textLines.length + paddingOffset;
|
|
770
|
+
const width = Math.max(
|
|
771
|
+
...textLines.map((line) => stripAnsi(line).length),
|
|
772
|
+
opts.title ? stripAnsi(opts.title).length : 0
|
|
773
|
+
) + paddingOffset;
|
|
774
|
+
const widthOffset = width + paddingOffset;
|
|
775
|
+
const leftSpace = opts.style.marginLeft > 0 ? " ".repeat(opts.style.marginLeft) : "";
|
|
776
|
+
if (opts.style.marginTop > 0) {
|
|
777
|
+
boxLines.push("".repeat(opts.style.marginTop));
|
|
778
|
+
}
|
|
779
|
+
if (opts.title) {
|
|
780
|
+
const title = _color ? _color(opts.title) : opts.title;
|
|
781
|
+
const left = borderStyle.h.repeat(
|
|
782
|
+
Math.floor((width - stripAnsi(opts.title).length) / 2)
|
|
783
|
+
);
|
|
784
|
+
const right = borderStyle.h.repeat(
|
|
785
|
+
width - stripAnsi(opts.title).length - stripAnsi(left).length + paddingOffset
|
|
786
|
+
);
|
|
787
|
+
boxLines.push(
|
|
788
|
+
`${leftSpace}${borderStyle.tl}${left}${title}${right}${borderStyle.tr}`
|
|
789
|
+
);
|
|
790
|
+
} else {
|
|
791
|
+
boxLines.push(
|
|
792
|
+
`${leftSpace}${borderStyle.tl}${borderStyle.h.repeat(widthOffset)}${borderStyle.tr}`
|
|
793
|
+
);
|
|
794
|
+
}
|
|
795
|
+
const valignOffset = opts.style.valign === "center" ? Math.floor((height - textLines.length) / 2) : opts.style.valign === "top" ? height - textLines.length - paddingOffset : height - textLines.length;
|
|
796
|
+
for (let i2 = 0; i2 < height; i2++) {
|
|
797
|
+
if (i2 < valignOffset || i2 >= valignOffset + textLines.length) {
|
|
798
|
+
boxLines.push(
|
|
799
|
+
`${leftSpace}${borderStyle.v}${" ".repeat(widthOffset)}${borderStyle.v}`
|
|
800
|
+
);
|
|
801
|
+
} else {
|
|
802
|
+
const line = textLines[i2 - valignOffset];
|
|
803
|
+
const left = " ".repeat(paddingOffset);
|
|
804
|
+
const right = " ".repeat(width - stripAnsi(line).length);
|
|
805
|
+
boxLines.push(
|
|
806
|
+
`${leftSpace}${borderStyle.v}${left}${line}${right}${borderStyle.v}`
|
|
807
|
+
);
|
|
808
|
+
}
|
|
809
|
+
}
|
|
810
|
+
boxLines.push(
|
|
811
|
+
`${leftSpace}${borderStyle.bl}${borderStyle.h.repeat(widthOffset)}${borderStyle.br}`
|
|
812
|
+
);
|
|
813
|
+
if (opts.style.marginBottom > 0) {
|
|
814
|
+
boxLines.push("".repeat(opts.style.marginBottom));
|
|
815
|
+
}
|
|
816
|
+
return boxLines.join("\n");
|
|
817
|
+
}
|
|
818
|
+
|
|
819
|
+
// ../../node_modules/.pnpm/consola@3.4.2/node_modules/consola/dist/index.mjs
|
|
820
|
+
import "util";
|
|
821
|
+
import "path";
|
|
822
|
+
import "tty";
|
|
823
|
+
var r = /* @__PURE__ */ Object.create(null);
|
|
824
|
+
var i = (e) => globalThis.process?.env || import.meta.env || globalThis.Deno?.env.toObject() || globalThis.__env__ || (e ? r : globalThis);
|
|
825
|
+
var o = new Proxy(r, { get(e, s2) {
|
|
826
|
+
return i()[s2] ?? r[s2];
|
|
827
|
+
}, has(e, s2) {
|
|
828
|
+
const E = i();
|
|
829
|
+
return s2 in E || s2 in r;
|
|
830
|
+
}, set(e, s2, E) {
|
|
831
|
+
const B = i(true);
|
|
832
|
+
return B[s2] = E, true;
|
|
833
|
+
}, deleteProperty(e, s2) {
|
|
834
|
+
if (!s2) return false;
|
|
835
|
+
const E = i(true);
|
|
836
|
+
return delete E[s2], true;
|
|
837
|
+
}, ownKeys() {
|
|
838
|
+
const e = i(true);
|
|
839
|
+
return Object.keys(e);
|
|
840
|
+
} });
|
|
841
|
+
var t = typeof process < "u" && process.env && process.env.NODE_ENV || "";
|
|
842
|
+
var f = [["APPVEYOR"], ["AWS_AMPLIFY", "AWS_APP_ID", { ci: true }], ["AZURE_PIPELINES", "SYSTEM_TEAMFOUNDATIONCOLLECTIONURI"], ["AZURE_STATIC", "INPUT_AZURE_STATIC_WEB_APPS_API_TOKEN"], ["APPCIRCLE", "AC_APPCIRCLE"], ["BAMBOO", "bamboo_planKey"], ["BITBUCKET", "BITBUCKET_COMMIT"], ["BITRISE", "BITRISE_IO"], ["BUDDY", "BUDDY_WORKSPACE_ID"], ["BUILDKITE"], ["CIRCLE", "CIRCLECI"], ["CIRRUS", "CIRRUS_CI"], ["CLOUDFLARE_PAGES", "CF_PAGES", { ci: true }], ["CODEBUILD", "CODEBUILD_BUILD_ARN"], ["CODEFRESH", "CF_BUILD_ID"], ["DRONE"], ["DRONE", "DRONE_BUILD_EVENT"], ["DSARI"], ["GITHUB_ACTIONS"], ["GITLAB", "GITLAB_CI"], ["GITLAB", "CI_MERGE_REQUEST_ID"], ["GOCD", "GO_PIPELINE_LABEL"], ["LAYERCI"], ["HUDSON", "HUDSON_URL"], ["JENKINS", "JENKINS_URL"], ["MAGNUM"], ["NETLIFY"], ["NETLIFY", "NETLIFY_LOCAL", { ci: false }], ["NEVERCODE"], ["RENDER"], ["SAIL", "SAILCI"], ["SEMAPHORE"], ["SCREWDRIVER"], ["SHIPPABLE"], ["SOLANO", "TDDIUM"], ["STRIDER"], ["TEAMCITY", "TEAMCITY_VERSION"], ["TRAVIS"], ["VERCEL", "NOW_BUILDER"], ["VERCEL", "VERCEL", { ci: false }], ["VERCEL", "VERCEL_ENV", { ci: false }], ["APPCENTER", "APPCENTER_BUILD_ID"], ["CODESANDBOX", "CODESANDBOX_SSE", { ci: false }], ["CODESANDBOX", "CODESANDBOX_HOST", { ci: false }], ["STACKBLITZ"], ["STORMKIT"], ["CLEAVR"], ["ZEABUR"], ["CODESPHERE", "CODESPHERE_APP_ID", { ci: true }], ["RAILWAY", "RAILWAY_PROJECT_ID"], ["RAILWAY", "RAILWAY_SERVICE_ID"], ["DENO-DEPLOY", "DENO_DEPLOYMENT_ID"], ["FIREBASE_APP_HOSTING", "FIREBASE_APP_HOSTING", { ci: true }]];
|
|
843
|
+
function b() {
|
|
844
|
+
if (globalThis.process?.env) for (const e of f) {
|
|
845
|
+
const s2 = e[1] || e[0];
|
|
846
|
+
if (globalThis.process?.env[s2]) return { name: e[0].toLowerCase(), ...e[2] };
|
|
847
|
+
}
|
|
848
|
+
return globalThis.process?.env?.SHELL === "/bin/jsh" && globalThis.process?.versions?.webcontainer ? { name: "stackblitz", ci: false } : { name: "", ci: false };
|
|
849
|
+
}
|
|
850
|
+
var l = b();
|
|
851
|
+
l.name;
|
|
852
|
+
function n(e) {
|
|
853
|
+
return e ? e !== "false" : false;
|
|
854
|
+
}
|
|
855
|
+
var I = globalThis.process?.platform || "";
|
|
856
|
+
var T = n(o.CI) || l.ci !== false;
|
|
857
|
+
var a = n(globalThis.process?.stdout && globalThis.process?.stdout.isTTY);
|
|
858
|
+
var g = n(o.DEBUG);
|
|
859
|
+
var R = t === "test" || n(o.TEST);
|
|
860
|
+
n(o.MINIMAL) || T || R || !a;
|
|
861
|
+
var A = /^win/i.test(I);
|
|
862
|
+
!n(o.NO_COLOR) && (n(o.FORCE_COLOR) || (a || A) && o.TERM !== "dumb" || T);
|
|
863
|
+
var C = (globalThis.process?.versions?.node || "").replace(/^v/, "") || null;
|
|
864
|
+
Number(C?.split(".")[0]) || null;
|
|
865
|
+
var y = globalThis.process || /* @__PURE__ */ Object.create(null);
|
|
866
|
+
var _ = { versions: {} };
|
|
867
|
+
new Proxy(y, { get(e, s2) {
|
|
868
|
+
if (s2 === "env") return o;
|
|
869
|
+
if (s2 in e) return e[s2];
|
|
870
|
+
if (s2 in _) return _[s2];
|
|
871
|
+
} });
|
|
872
|
+
var c = globalThis.process?.release?.name === "node";
|
|
873
|
+
var O = !!globalThis.Bun || !!globalThis.process?.versions?.bun;
|
|
874
|
+
var D = !!globalThis.Deno;
|
|
875
|
+
var L = !!globalThis.fastly;
|
|
876
|
+
var S = !!globalThis.Netlify;
|
|
877
|
+
var u = !!globalThis.EdgeRuntime;
|
|
878
|
+
var N = globalThis.navigator?.userAgent === "Cloudflare-Workers";
|
|
879
|
+
var F = [[S, "netlify"], [u, "edge-light"], [N, "workerd"], [L, "fastly"], [D, "deno"], [O, "bun"], [c, "node"]];
|
|
880
|
+
function G() {
|
|
881
|
+
const e = F.find((s2) => s2[0]);
|
|
882
|
+
if (e) return { name: e[1] };
|
|
883
|
+
}
|
|
884
|
+
var P = G();
|
|
885
|
+
P?.name || "";
|
|
886
|
+
function ansiRegex2({ onlyFirst = false } = {}) {
|
|
887
|
+
const ST = "(?:\\u0007|\\u001B\\u005C|\\u009C)";
|
|
888
|
+
const pattern = [
|
|
889
|
+
`[\\u001B\\u009B][[\\]()#;?]*(?:(?:(?:(?:;[-a-zA-Z\\d\\/#&.:=?%@~_]+)*|[a-zA-Z\\d]+(?:;[-a-zA-Z\\d\\/#&.:=?%@~_]*)*)?${ST})`,
|
|
890
|
+
"(?:(?:\\d{1,4}(?:;\\d{0,4})*)?[\\dA-PR-TZcf-nq-uy=><~]))"
|
|
891
|
+
].join("|");
|
|
892
|
+
return new RegExp(pattern, onlyFirst ? void 0 : "g");
|
|
893
|
+
}
|
|
894
|
+
var regex = ansiRegex2();
|
|
895
|
+
function stripAnsi2(string) {
|
|
896
|
+
if (typeof string !== "string") {
|
|
897
|
+
throw new TypeError(`Expected a \`string\`, got \`${typeof string}\``);
|
|
898
|
+
}
|
|
899
|
+
return string.replace(regex, "");
|
|
900
|
+
}
|
|
901
|
+
function isAmbiguous(x) {
|
|
902
|
+
return x === 161 || x === 164 || x === 167 || x === 168 || x === 170 || x === 173 || x === 174 || x >= 176 && x <= 180 || x >= 182 && x <= 186 || x >= 188 && x <= 191 || x === 198 || x === 208 || x === 215 || x === 216 || x >= 222 && x <= 225 || x === 230 || x >= 232 && x <= 234 || x === 236 || x === 237 || x === 240 || x === 242 || x === 243 || x >= 247 && x <= 250 || x === 252 || x === 254 || x === 257 || x === 273 || x === 275 || x === 283 || x === 294 || x === 295 || x === 299 || x >= 305 && x <= 307 || x === 312 || x >= 319 && x <= 322 || x === 324 || x >= 328 && x <= 331 || x === 333 || x === 338 || x === 339 || x === 358 || x === 359 || x === 363 || x === 462 || x === 464 || x === 466 || x === 468 || x === 470 || x === 472 || x === 474 || x === 476 || x === 593 || x === 609 || x === 708 || x === 711 || x >= 713 && x <= 715 || x === 717 || x === 720 || x >= 728 && x <= 731 || x === 733 || x === 735 || x >= 768 && x <= 879 || x >= 913 && x <= 929 || x >= 931 && x <= 937 || x >= 945 && x <= 961 || x >= 963 && x <= 969 || x === 1025 || x >= 1040 && x <= 1103 || x === 1105 || x === 8208 || x >= 8211 && x <= 8214 || x === 8216 || x === 8217 || x === 8220 || x === 8221 || x >= 8224 && x <= 8226 || x >= 8228 && x <= 8231 || x === 8240 || x === 8242 || x === 8243 || x === 8245 || x === 8251 || x === 8254 || x === 8308 || x === 8319 || x >= 8321 && x <= 8324 || x === 8364 || x === 8451 || x === 8453 || x === 8457 || x === 8467 || x === 8470 || x === 8481 || x === 8482 || x === 8486 || x === 8491 || x === 8531 || x === 8532 || x >= 8539 && x <= 8542 || x >= 8544 && x <= 8555 || x >= 8560 && x <= 8569 || x === 8585 || x >= 8592 && x <= 8601 || x === 8632 || x === 8633 || x === 8658 || x === 8660 || x === 8679 || x === 8704 || x === 8706 || x === 8707 || x === 8711 || x === 8712 || x === 8715 || x === 8719 || x === 8721 || x === 8725 || x === 8730 || x >= 8733 && x <= 8736 || x === 8739 || x === 8741 || x >= 8743 && x <= 8748 || x === 8750 || x >= 8756 && x <= 8759 || x === 8764 || x === 8765 || x === 8776 || x === 8780 || x === 8786 || x === 8800 || x === 8801 || x >= 8804 && x <= 8807 || x === 8810 || x === 8811 || x === 8814 || x === 8815 || x === 8834 || x === 8835 || x === 8838 || x === 8839 || x === 8853 || x === 8857 || x === 8869 || x === 8895 || x === 8978 || x >= 9312 && x <= 9449 || x >= 9451 && x <= 9547 || x >= 9552 && x <= 9587 || x >= 9600 && x <= 9615 || x >= 9618 && x <= 9621 || x === 9632 || x === 9633 || x >= 9635 && x <= 9641 || x === 9650 || x === 9651 || x === 9654 || x === 9655 || x === 9660 || x === 9661 || x === 9664 || x === 9665 || x >= 9670 && x <= 9672 || x === 9675 || x >= 9678 && x <= 9681 || x >= 9698 && x <= 9701 || x === 9711 || x === 9733 || x === 9734 || x === 9737 || x === 9742 || x === 9743 || x === 9756 || x === 9758 || x === 9792 || x === 9794 || x === 9824 || x === 9825 || x >= 9827 && x <= 9829 || x >= 9831 && x <= 9834 || x === 9836 || x === 9837 || x === 9839 || x === 9886 || x === 9887 || x === 9919 || x >= 9926 && x <= 9933 || x >= 9935 && x <= 9939 || x >= 9941 && x <= 9953 || x === 9955 || x === 9960 || x === 9961 || x >= 9963 && x <= 9969 || x === 9972 || x >= 9974 && x <= 9977 || x === 9979 || x === 9980 || x === 9982 || x === 9983 || x === 10045 || x >= 10102 && x <= 10111 || x >= 11094 && x <= 11097 || x >= 12872 && x <= 12879 || x >= 57344 && x <= 63743 || x >= 65024 && x <= 65039 || x === 65533 || x >= 127232 && x <= 127242 || x >= 127248 && x <= 127277 || x >= 127280 && x <= 127337 || x >= 127344 && x <= 127373 || x === 127375 || x === 127376 || x >= 127387 && x <= 127404 || x >= 917760 && x <= 917999 || x >= 983040 && x <= 1048573 || x >= 1048576 && x <= 1114109;
|
|
903
|
+
}
|
|
904
|
+
function isFullWidth(x) {
|
|
905
|
+
return x === 12288 || x >= 65281 && x <= 65376 || x >= 65504 && x <= 65510;
|
|
906
|
+
}
|
|
907
|
+
function isWide(x) {
|
|
908
|
+
return x >= 4352 && x <= 4447 || x === 8986 || x === 8987 || x === 9001 || x === 9002 || x >= 9193 && x <= 9196 || x === 9200 || x === 9203 || x === 9725 || x === 9726 || x === 9748 || x === 9749 || x >= 9776 && x <= 9783 || x >= 9800 && x <= 9811 || x === 9855 || x >= 9866 && x <= 9871 || x === 9875 || x === 9889 || x === 9898 || x === 9899 || x === 9917 || x === 9918 || x === 9924 || x === 9925 || x === 9934 || x === 9940 || x === 9962 || x === 9970 || x === 9971 || x === 9973 || x === 9978 || x === 9981 || x === 9989 || x === 9994 || x === 9995 || x === 10024 || x === 10060 || x === 10062 || x >= 10067 && x <= 10069 || x === 10071 || x >= 10133 && x <= 10135 || x === 10160 || x === 10175 || x === 11035 || x === 11036 || x === 11088 || x === 11093 || x >= 11904 && x <= 11929 || x >= 11931 && x <= 12019 || x >= 12032 && x <= 12245 || x >= 12272 && x <= 12287 || x >= 12289 && x <= 12350 || x >= 12353 && x <= 12438 || x >= 12441 && x <= 12543 || x >= 12549 && x <= 12591 || x >= 12593 && x <= 12686 || x >= 12688 && x <= 12773 || x >= 12783 && x <= 12830 || x >= 12832 && x <= 12871 || x >= 12880 && x <= 42124 || x >= 42128 && x <= 42182 || x >= 43360 && x <= 43388 || x >= 44032 && x <= 55203 || x >= 63744 && x <= 64255 || x >= 65040 && x <= 65049 || x >= 65072 && x <= 65106 || x >= 65108 && x <= 65126 || x >= 65128 && x <= 65131 || x >= 94176 && x <= 94180 || x === 94192 || x === 94193 || x >= 94208 && x <= 100343 || x >= 100352 && x <= 101589 || x >= 101631 && x <= 101640 || x >= 110576 && x <= 110579 || x >= 110581 && x <= 110587 || x === 110589 || x === 110590 || x >= 110592 && x <= 110882 || x === 110898 || x >= 110928 && x <= 110930 || x === 110933 || x >= 110948 && x <= 110951 || x >= 110960 && x <= 111355 || x >= 119552 && x <= 119638 || x >= 119648 && x <= 119670 || x === 126980 || x === 127183 || x === 127374 || x >= 127377 && x <= 127386 || x >= 127488 && x <= 127490 || x >= 127504 && x <= 127547 || x >= 127552 && x <= 127560 || x === 127568 || x === 127569 || x >= 127584 && x <= 127589 || x >= 127744 && x <= 127776 || x >= 127789 && x <= 127797 || x >= 127799 && x <= 127868 || x >= 127870 && x <= 127891 || x >= 127904 && x <= 127946 || x >= 127951 && x <= 127955 || x >= 127968 && x <= 127984 || x === 127988 || x >= 127992 && x <= 128062 || x === 128064 || x >= 128066 && x <= 128252 || x >= 128255 && x <= 128317 || x >= 128331 && x <= 128334 || x >= 128336 && x <= 128359 || x === 128378 || x === 128405 || x === 128406 || x === 128420 || x >= 128507 && x <= 128591 || x >= 128640 && x <= 128709 || x === 128716 || x >= 128720 && x <= 128722 || x >= 128725 && x <= 128727 || x >= 128732 && x <= 128735 || x === 128747 || x === 128748 || x >= 128756 && x <= 128764 || x >= 128992 && x <= 129003 || x === 129008 || x >= 129292 && x <= 129338 || x >= 129340 && x <= 129349 || x >= 129351 && x <= 129535 || x >= 129648 && x <= 129660 || x >= 129664 && x <= 129673 || x >= 129679 && x <= 129734 || x >= 129742 && x <= 129756 || x >= 129759 && x <= 129769 || x >= 129776 && x <= 129784 || x >= 131072 && x <= 196605 || x >= 196608 && x <= 262141;
|
|
909
|
+
}
|
|
910
|
+
function validate(codePoint) {
|
|
911
|
+
if (!Number.isSafeInteger(codePoint)) {
|
|
912
|
+
throw new TypeError(`Expected a code point, got \`${typeof codePoint}\`.`);
|
|
913
|
+
}
|
|
914
|
+
}
|
|
915
|
+
function eastAsianWidth(codePoint, { ambiguousAsWide = false } = {}) {
|
|
916
|
+
validate(codePoint);
|
|
917
|
+
if (isFullWidth(codePoint) || isWide(codePoint) || ambiguousAsWide && isAmbiguous(codePoint)) {
|
|
918
|
+
return 2;
|
|
919
|
+
}
|
|
920
|
+
return 1;
|
|
921
|
+
}
|
|
922
|
+
var emojiRegex = () => {
|
|
923
|
+
return /[#*0-9]\uFE0F?\u20E3|[\xA9\xAE\u203C\u2049\u2122\u2139\u2194-\u2199\u21A9\u21AA\u231A\u231B\u2328\u23CF\u23ED-\u23EF\u23F1\u23F2\u23F8-\u23FA\u24C2\u25AA\u25AB\u25B6\u25C0\u25FB\u25FC\u25FE\u2600-\u2604\u260E\u2611\u2614\u2615\u2618\u2620\u2622\u2623\u2626\u262A\u262E\u262F\u2638-\u263A\u2640\u2642\u2648-\u2653\u265F\u2660\u2663\u2665\u2666\u2668\u267B\u267E\u267F\u2692\u2694-\u2697\u2699\u269B\u269C\u26A0\u26A7\u26AA\u26B0\u26B1\u26BD\u26BE\u26C4\u26C8\u26CF\u26D1\u26E9\u26F0-\u26F5\u26F7\u26F8\u26FA\u2702\u2708\u2709\u270F\u2712\u2714\u2716\u271D\u2721\u2733\u2734\u2744\u2747\u2757\u2763\u27A1\u2934\u2935\u2B05-\u2B07\u2B1B\u2B1C\u2B55\u3030\u303D\u3297\u3299]\uFE0F?|[\u261D\u270C\u270D](?:\uD83C[\uDFFB-\uDFFF]|\uFE0F)?|[\u270A\u270B](?:\uD83C[\uDFFB-\uDFFF])?|[\u23E9-\u23EC\u23F0\u23F3\u25FD\u2693\u26A1\u26AB\u26C5\u26CE\u26D4\u26EA\u26FD\u2705\u2728\u274C\u274E\u2753-\u2755\u2795-\u2797\u27B0\u27BF\u2B50]|\u26D3\uFE0F?(?:\u200D\uD83D\uDCA5)?|\u26F9(?:\uD83C[\uDFFB-\uDFFF]|\uFE0F)?(?:\u200D[\u2640\u2642]\uFE0F?)?|\u2764\uFE0F?(?:\u200D(?:\uD83D\uDD25|\uD83E\uDE79))?|\uD83C(?:[\uDC04\uDD70\uDD71\uDD7E\uDD7F\uDE02\uDE37\uDF21\uDF24-\uDF2C\uDF36\uDF7D\uDF96\uDF97\uDF99-\uDF9B\uDF9E\uDF9F\uDFCD\uDFCE\uDFD4-\uDFDF\uDFF5\uDFF7]\uFE0F?|[\uDF85\uDFC2\uDFC7](?:\uD83C[\uDFFB-\uDFFF])?|[\uDFC4\uDFCA](?:\uD83C[\uDFFB-\uDFFF])?(?:\u200D[\u2640\u2642]\uFE0F?)?|[\uDFCB\uDFCC](?:\uD83C[\uDFFB-\uDFFF]|\uFE0F)?(?:\u200D[\u2640\u2642]\uFE0F?)?|[\uDCCF\uDD8E\uDD91-\uDD9A\uDE01\uDE1A\uDE2F\uDE32-\uDE36\uDE38-\uDE3A\uDE50\uDE51\uDF00-\uDF20\uDF2D-\uDF35\uDF37-\uDF43\uDF45-\uDF4A\uDF4C-\uDF7C\uDF7E-\uDF84\uDF86-\uDF93\uDFA0-\uDFC1\uDFC5\uDFC6\uDFC8\uDFC9\uDFCF-\uDFD3\uDFE0-\uDFF0\uDFF8-\uDFFF]|\uDDE6\uD83C[\uDDE8-\uDDEC\uDDEE\uDDF1\uDDF2\uDDF4\uDDF6-\uDDFA\uDDFC\uDDFD\uDDFF]|\uDDE7\uD83C[\uDDE6\uDDE7\uDDE9-\uDDEF\uDDF1-\uDDF4\uDDF6-\uDDF9\uDDFB\uDDFC\uDDFE\uDDFF]|\uDDE8\uD83C[\uDDE6\uDDE8\uDDE9\uDDEB-\uDDEE\uDDF0-\uDDF7\uDDFA-\uDDFF]|\uDDE9\uD83C[\uDDEA\uDDEC\uDDEF\uDDF0\uDDF2\uDDF4\uDDFF]|\uDDEA\uD83C[\uDDE6\uDDE8\uDDEA\uDDEC\uDDED\uDDF7-\uDDFA]|\uDDEB\uD83C[\uDDEE-\uDDF0\uDDF2\uDDF4\uDDF7]|\uDDEC\uD83C[\uDDE6\uDDE7\uDDE9-\uDDEE\uDDF1-\uDDF3\uDDF5-\uDDFA\uDDFC\uDDFE]|\uDDED\uD83C[\uDDF0\uDDF2\uDDF3\uDDF7\uDDF9\uDDFA]|\uDDEE\uD83C[\uDDE8-\uDDEA\uDDF1-\uDDF4\uDDF6-\uDDF9]|\uDDEF\uD83C[\uDDEA\uDDF2\uDDF4\uDDF5]|\uDDF0\uD83C[\uDDEA\uDDEC-\uDDEE\uDDF2\uDDF3\uDDF5\uDDF7\uDDFC\uDDFE\uDDFF]|\uDDF1\uD83C[\uDDE6-\uDDE8\uDDEE\uDDF0\uDDF7-\uDDFB\uDDFE]|\uDDF2\uD83C[\uDDE6\uDDE8-\uDDED\uDDF0-\uDDFF]|\uDDF3\uD83C[\uDDE6\uDDE8\uDDEA-\uDDEC\uDDEE\uDDF1\uDDF4\uDDF5\uDDF7\uDDFA\uDDFF]|\uDDF4\uD83C\uDDF2|\uDDF5\uD83C[\uDDE6\uDDEA-\uDDED\uDDF0-\uDDF3\uDDF7-\uDDF9\uDDFC\uDDFE]|\uDDF6\uD83C\uDDE6|\uDDF7\uD83C[\uDDEA\uDDF4\uDDF8\uDDFA\uDDFC]|\uDDF8\uD83C[\uDDE6-\uDDEA\uDDEC-\uDDF4\uDDF7-\uDDF9\uDDFB\uDDFD-\uDDFF]|\uDDF9\uD83C[\uDDE6\uDDE8\uDDE9\uDDEB-\uDDED\uDDEF-\uDDF4\uDDF7\uDDF9\uDDFB\uDDFC\uDDFF]|\uDDFA\uD83C[\uDDE6\uDDEC\uDDF2\uDDF3\uDDF8\uDDFE\uDDFF]|\uDDFB\uD83C[\uDDE6\uDDE8\uDDEA\uDDEC\uDDEE\uDDF3\uDDFA]|\uDDFC\uD83C[\uDDEB\uDDF8]|\uDDFD\uD83C\uDDF0|\uDDFE\uD83C[\uDDEA\uDDF9]|\uDDFF\uD83C[\uDDE6\uDDF2\uDDFC]|\uDF44(?:\u200D\uD83D\uDFEB)?|\uDF4B(?:\u200D\uD83D\uDFE9)?|\uDFC3(?:\uD83C[\uDFFB-\uDFFF])?(?:\u200D(?:[\u2640\u2642]\uFE0F?(?:\u200D\u27A1\uFE0F?)?|\u27A1\uFE0F?))?|\uDFF3\uFE0F?(?:\u200D(?:\u26A7\uFE0F?|\uD83C\uDF08))?|\uDFF4(?:\u200D\u2620\uFE0F?|\uDB40\uDC67\uDB40\uDC62\uDB40(?:\uDC65\uDB40\uDC6E\uDB40\uDC67|\uDC73\uDB40\uDC63\uDB40\uDC74|\uDC77\uDB40\uDC6C\uDB40\uDC73)\uDB40\uDC7F)?)|\uD83D(?:[\uDC3F\uDCFD\uDD49\uDD4A\uDD6F\uDD70\uDD73\uDD76-\uDD79\uDD87\uDD8A-\uDD8D\uDDA5\uDDA8\uDDB1\uDDB2\uDDBC\uDDC2-\uDDC4\uDDD1-\uDDD3\uDDDC-\uDDDE\uDDE1\uDDE3\uDDE8\uDDEF\uDDF3\uDDFA\uDECB\uDECD-\uDECF\uDEE0-\uDEE5\uDEE9\uDEF0\uDEF3]\uFE0F?|[\uDC42\uDC43\uDC46-\uDC50\uDC66\uDC67\uDC6B-\uDC6D\uDC72\uDC74-\uDC76\uDC78\uDC7C\uDC83\uDC85\uDC8F\uDC91\uDCAA\uDD7A\uDD95\uDD96\uDE4C\uDE4F\uDEC0\uDECC](?:\uD83C[\uDFFB-\uDFFF])?|[\uDC6E\uDC70\uDC71\uDC73\uDC77\uDC81\uDC82\uDC86\uDC87\uDE45-\uDE47\uDE4B\uDE4D\uDE4E\uDEA3\uDEB4\uDEB5](?:\uD83C[\uDFFB-\uDFFF])?(?:\u200D[\u2640\u2642]\uFE0F?)?|[\uDD74\uDD90](?:\uD83C[\uDFFB-\uDFFF]|\uFE0F)?|[\uDC00-\uDC07\uDC09-\uDC14\uDC16-\uDC25\uDC27-\uDC3A\uDC3C-\uDC3E\uDC40\uDC44\uDC45\uDC51-\uDC65\uDC6A\uDC79-\uDC7B\uDC7D-\uDC80\uDC84\uDC88-\uDC8E\uDC90\uDC92-\uDCA9\uDCAB-\uDCFC\uDCFF-\uDD3D\uDD4B-\uDD4E\uDD50-\uDD67\uDDA4\uDDFB-\uDE2D\uDE2F-\uDE34\uDE37-\uDE41\uDE43\uDE44\uDE48-\uDE4A\uDE80-\uDEA2\uDEA4-\uDEB3\uDEB7-\uDEBF\uDEC1-\uDEC5\uDED0-\uDED2\uDED5-\uDED7\uDEDC-\uDEDF\uDEEB\uDEEC\uDEF4-\uDEFC\uDFE0-\uDFEB\uDFF0]|\uDC08(?:\u200D\u2B1B)?|\uDC15(?:\u200D\uD83E\uDDBA)?|\uDC26(?:\u200D(?:\u2B1B|\uD83D\uDD25))?|\uDC3B(?:\u200D\u2744\uFE0F?)?|\uDC41\uFE0F?(?:\u200D\uD83D\uDDE8\uFE0F?)?|\uDC68(?:\u200D(?:[\u2695\u2696\u2708]\uFE0F?|\u2764\uFE0F?\u200D\uD83D(?:\uDC8B\u200D\uD83D)?\uDC68|\uD83C[\uDF3E\uDF73\uDF7C\uDF93\uDFA4\uDFA8\uDFEB\uDFED]|\uD83D(?:[\uDC68\uDC69]\u200D\uD83D(?:\uDC66(?:\u200D\uD83D\uDC66)?|\uDC67(?:\u200D\uD83D[\uDC66\uDC67])?)|[\uDCBB\uDCBC\uDD27\uDD2C\uDE80\uDE92]|\uDC66(?:\u200D\uD83D\uDC66)?|\uDC67(?:\u200D\uD83D[\uDC66\uDC67])?)|\uD83E(?:[\uDDAF\uDDBC\uDDBD](?:\u200D\u27A1\uFE0F?)?|[\uDDB0-\uDDB3]))|\uD83C(?:\uDFFB(?:\u200D(?:[\u2695\u2696\u2708]\uFE0F?|\u2764\uFE0F?\u200D\uD83D(?:\uDC8B\u200D\uD83D)?\uDC68\uD83C[\uDFFB-\uDFFF]|\uD83C[\uDF3E\uDF73\uDF7C\uDF93\uDFA4\uDFA8\uDFEB\uDFED]|\uD83D[\uDCBB\uDCBC\uDD27\uDD2C\uDE80\uDE92]|\uD83E(?:[\uDDAF\uDDBC\uDDBD](?:\u200D\u27A1\uFE0F?)?|[\uDDB0-\uDDB3]|\uDD1D\u200D\uD83D\uDC68\uD83C[\uDFFC-\uDFFF])))?|\uDFFC(?:\u200D(?:[\u2695\u2696\u2708]\uFE0F?|\u2764\uFE0F?\u200D\uD83D(?:\uDC8B\u200D\uD83D)?\uDC68\uD83C[\uDFFB-\uDFFF]|\uD83C[\uDF3E\uDF73\uDF7C\uDF93\uDFA4\uDFA8\uDFEB\uDFED]|\uD83D[\uDCBB\uDCBC\uDD27\uDD2C\uDE80\uDE92]|\uD83E(?:[\uDDAF\uDDBC\uDDBD](?:\u200D\u27A1\uFE0F?)?|[\uDDB0-\uDDB3]|\uDD1D\u200D\uD83D\uDC68\uD83C[\uDFFB\uDFFD-\uDFFF])))?|\uDFFD(?:\u200D(?:[\u2695\u2696\u2708]\uFE0F?|\u2764\uFE0F?\u200D\uD83D(?:\uDC8B\u200D\uD83D)?\uDC68\uD83C[\uDFFB-\uDFFF]|\uD83C[\uDF3E\uDF73\uDF7C\uDF93\uDFA4\uDFA8\uDFEB\uDFED]|\uD83D[\uDCBB\uDCBC\uDD27\uDD2C\uDE80\uDE92]|\uD83E(?:[\uDDAF\uDDBC\uDDBD](?:\u200D\u27A1\uFE0F?)?|[\uDDB0-\uDDB3]|\uDD1D\u200D\uD83D\uDC68\uD83C[\uDFFB\uDFFC\uDFFE\uDFFF])))?|\uDFFE(?:\u200D(?:[\u2695\u2696\u2708]\uFE0F?|\u2764\uFE0F?\u200D\uD83D(?:\uDC8B\u200D\uD83D)?\uDC68\uD83C[\uDFFB-\uDFFF]|\uD83C[\uDF3E\uDF73\uDF7C\uDF93\uDFA4\uDFA8\uDFEB\uDFED]|\uD83D[\uDCBB\uDCBC\uDD27\uDD2C\uDE80\uDE92]|\uD83E(?:[\uDDAF\uDDBC\uDDBD](?:\u200D\u27A1\uFE0F?)?|[\uDDB0-\uDDB3]|\uDD1D\u200D\uD83D\uDC68\uD83C[\uDFFB-\uDFFD\uDFFF])))?|\uDFFF(?:\u200D(?:[\u2695\u2696\u2708]\uFE0F?|\u2764\uFE0F?\u200D\uD83D(?:\uDC8B\u200D\uD83D)?\uDC68\uD83C[\uDFFB-\uDFFF]|\uD83C[\uDF3E\uDF73\uDF7C\uDF93\uDFA4\uDFA8\uDFEB\uDFED]|\uD83D[\uDCBB\uDCBC\uDD27\uDD2C\uDE80\uDE92]|\uD83E(?:[\uDDAF\uDDBC\uDDBD](?:\u200D\u27A1\uFE0F?)?|[\uDDB0-\uDDB3]|\uDD1D\u200D\uD83D\uDC68\uD83C[\uDFFB-\uDFFE])))?))?|\uDC69(?:\u200D(?:[\u2695\u2696\u2708]\uFE0F?|\u2764\uFE0F?\u200D\uD83D(?:\uDC8B\u200D\uD83D)?[\uDC68\uDC69]|\uD83C[\uDF3E\uDF73\uDF7C\uDF93\uDFA4\uDFA8\uDFEB\uDFED]|\uD83D(?:[\uDCBB\uDCBC\uDD27\uDD2C\uDE80\uDE92]|\uDC66(?:\u200D\uD83D\uDC66)?|\uDC67(?:\u200D\uD83D[\uDC66\uDC67])?|\uDC69\u200D\uD83D(?:\uDC66(?:\u200D\uD83D\uDC66)?|\uDC67(?:\u200D\uD83D[\uDC66\uDC67])?))|\uD83E(?:[\uDDAF\uDDBC\uDDBD](?:\u200D\u27A1\uFE0F?)?|[\uDDB0-\uDDB3]))|\uD83C(?:\uDFFB(?:\u200D(?:[\u2695\u2696\u2708]\uFE0F?|\u2764\uFE0F?\u200D\uD83D(?:[\uDC68\uDC69]|\uDC8B\u200D\uD83D[\uDC68\uDC69])\uD83C[\uDFFB-\uDFFF]|\uD83C[\uDF3E\uDF73\uDF7C\uDF93\uDFA4\uDFA8\uDFEB\uDFED]|\uD83D[\uDCBB\uDCBC\uDD27\uDD2C\uDE80\uDE92]|\uD83E(?:[\uDDAF\uDDBC\uDDBD](?:\u200D\u27A1\uFE0F?)?|[\uDDB0-\uDDB3]|\uDD1D\u200D\uD83D[\uDC68\uDC69]\uD83C[\uDFFC-\uDFFF])))?|\uDFFC(?:\u200D(?:[\u2695\u2696\u2708]\uFE0F?|\u2764\uFE0F?\u200D\uD83D(?:[\uDC68\uDC69]|\uDC8B\u200D\uD83D[\uDC68\uDC69])\uD83C[\uDFFB-\uDFFF]|\uD83C[\uDF3E\uDF73\uDF7C\uDF93\uDFA4\uDFA8\uDFEB\uDFED]|\uD83D[\uDCBB\uDCBC\uDD27\uDD2C\uDE80\uDE92]|\uD83E(?:[\uDDAF\uDDBC\uDDBD](?:\u200D\u27A1\uFE0F?)?|[\uDDB0-\uDDB3]|\uDD1D\u200D\uD83D[\uDC68\uDC69]\uD83C[\uDFFB\uDFFD-\uDFFF])))?|\uDFFD(?:\u200D(?:[\u2695\u2696\u2708]\uFE0F?|\u2764\uFE0F?\u200D\uD83D(?:[\uDC68\uDC69]|\uDC8B\u200D\uD83D[\uDC68\uDC69])\uD83C[\uDFFB-\uDFFF]|\uD83C[\uDF3E\uDF73\uDF7C\uDF93\uDFA4\uDFA8\uDFEB\uDFED]|\uD83D[\uDCBB\uDCBC\uDD27\uDD2C\uDE80\uDE92]|\uD83E(?:[\uDDAF\uDDBC\uDDBD](?:\u200D\u27A1\uFE0F?)?|[\uDDB0-\uDDB3]|\uDD1D\u200D\uD83D[\uDC68\uDC69]\uD83C[\uDFFB\uDFFC\uDFFE\uDFFF])))?|\uDFFE(?:\u200D(?:[\u2695\u2696\u2708]\uFE0F?|\u2764\uFE0F?\u200D\uD83D(?:[\uDC68\uDC69]|\uDC8B\u200D\uD83D[\uDC68\uDC69])\uD83C[\uDFFB-\uDFFF]|\uD83C[\uDF3E\uDF73\uDF7C\uDF93\uDFA4\uDFA8\uDFEB\uDFED]|\uD83D[\uDCBB\uDCBC\uDD27\uDD2C\uDE80\uDE92]|\uD83E(?:[\uDDAF\uDDBC\uDDBD](?:\u200D\u27A1\uFE0F?)?|[\uDDB0-\uDDB3]|\uDD1D\u200D\uD83D[\uDC68\uDC69]\uD83C[\uDFFB-\uDFFD\uDFFF])))?|\uDFFF(?:\u200D(?:[\u2695\u2696\u2708]\uFE0F?|\u2764\uFE0F?\u200D\uD83D(?:[\uDC68\uDC69]|\uDC8B\u200D\uD83D[\uDC68\uDC69])\uD83C[\uDFFB-\uDFFF]|\uD83C[\uDF3E\uDF73\uDF7C\uDF93\uDFA4\uDFA8\uDFEB\uDFED]|\uD83D[\uDCBB\uDCBC\uDD27\uDD2C\uDE80\uDE92]|\uD83E(?:[\uDDAF\uDDBC\uDDBD](?:\u200D\u27A1\uFE0F?)?|[\uDDB0-\uDDB3]|\uDD1D\u200D\uD83D[\uDC68\uDC69]\uD83C[\uDFFB-\uDFFE])))?))?|\uDC6F(?:\u200D[\u2640\u2642]\uFE0F?)?|\uDD75(?:\uD83C[\uDFFB-\uDFFF]|\uFE0F)?(?:\u200D[\u2640\u2642]\uFE0F?)?|\uDE2E(?:\u200D\uD83D\uDCA8)?|\uDE35(?:\u200D\uD83D\uDCAB)?|\uDE36(?:\u200D\uD83C\uDF2B\uFE0F?)?|\uDE42(?:\u200D[\u2194\u2195]\uFE0F?)?|\uDEB6(?:\uD83C[\uDFFB-\uDFFF])?(?:\u200D(?:[\u2640\u2642]\uFE0F?(?:\u200D\u27A1\uFE0F?)?|\u27A1\uFE0F?))?)|\uD83E(?:[\uDD0C\uDD0F\uDD18-\uDD1F\uDD30-\uDD34\uDD36\uDD77\uDDB5\uDDB6\uDDBB\uDDD2\uDDD3\uDDD5\uDEC3-\uDEC5\uDEF0\uDEF2-\uDEF8](?:\uD83C[\uDFFB-\uDFFF])?|[\uDD26\uDD35\uDD37-\uDD39\uDD3D\uDD3E\uDDB8\uDDB9\uDDCD\uDDCF\uDDD4\uDDD6-\uDDDD](?:\uD83C[\uDFFB-\uDFFF])?(?:\u200D[\u2640\u2642]\uFE0F?)?|[\uDDDE\uDDDF](?:\u200D[\u2640\u2642]\uFE0F?)?|[\uDD0D\uDD0E\uDD10-\uDD17\uDD20-\uDD25\uDD27-\uDD2F\uDD3A\uDD3F-\uDD45\uDD47-\uDD76\uDD78-\uDDB4\uDDB7\uDDBA\uDDBC-\uDDCC\uDDD0\uDDE0-\uDDFF\uDE70-\uDE7C\uDE80-\uDE89\uDE8F-\uDEC2\uDEC6\uDECE-\uDEDC\uDEDF-\uDEE9]|\uDD3C(?:\u200D[\u2640\u2642]\uFE0F?|\uD83C[\uDFFB-\uDFFF])?|\uDDCE(?:\uD83C[\uDFFB-\uDFFF])?(?:\u200D(?:[\u2640\u2642]\uFE0F?(?:\u200D\u27A1\uFE0F?)?|\u27A1\uFE0F?))?|\uDDD1(?:\u200D(?:[\u2695\u2696\u2708]\uFE0F?|\uD83C[\uDF3E\uDF73\uDF7C\uDF84\uDF93\uDFA4\uDFA8\uDFEB\uDFED]|\uD83D[\uDCBB\uDCBC\uDD27\uDD2C\uDE80\uDE92]|\uD83E(?:[\uDDAF\uDDBC\uDDBD](?:\u200D\u27A1\uFE0F?)?|[\uDDB0-\uDDB3]|\uDD1D\u200D\uD83E\uDDD1|\uDDD1\u200D\uD83E\uDDD2(?:\u200D\uD83E\uDDD2)?|\uDDD2(?:\u200D\uD83E\uDDD2)?))|\uD83C(?:\uDFFB(?:\u200D(?:[\u2695\u2696\u2708]\uFE0F?|\u2764\uFE0F?\u200D(?:\uD83D\uDC8B\u200D)?\uD83E\uDDD1\uD83C[\uDFFC-\uDFFF]|\uD83C[\uDF3E\uDF73\uDF7C\uDF84\uDF93\uDFA4\uDFA8\uDFEB\uDFED]|\uD83D[\uDCBB\uDCBC\uDD27\uDD2C\uDE80\uDE92]|\uD83E(?:[\uDDAF\uDDBC\uDDBD](?:\u200D\u27A1\uFE0F?)?|[\uDDB0-\uDDB3]|\uDD1D\u200D\uD83E\uDDD1\uD83C[\uDFFB-\uDFFF])))?|\uDFFC(?:\u200D(?:[\u2695\u2696\u2708]\uFE0F?|\u2764\uFE0F?\u200D(?:\uD83D\uDC8B\u200D)?\uD83E\uDDD1\uD83C[\uDFFB\uDFFD-\uDFFF]|\uD83C[\uDF3E\uDF73\uDF7C\uDF84\uDF93\uDFA4\uDFA8\uDFEB\uDFED]|\uD83D[\uDCBB\uDCBC\uDD27\uDD2C\uDE80\uDE92]|\uD83E(?:[\uDDAF\uDDBC\uDDBD](?:\u200D\u27A1\uFE0F?)?|[\uDDB0-\uDDB3]|\uDD1D\u200D\uD83E\uDDD1\uD83C[\uDFFB-\uDFFF])))?|\uDFFD(?:\u200D(?:[\u2695\u2696\u2708]\uFE0F?|\u2764\uFE0F?\u200D(?:\uD83D\uDC8B\u200D)?\uD83E\uDDD1\uD83C[\uDFFB\uDFFC\uDFFE\uDFFF]|\uD83C[\uDF3E\uDF73\uDF7C\uDF84\uDF93\uDFA4\uDFA8\uDFEB\uDFED]|\uD83D[\uDCBB\uDCBC\uDD27\uDD2C\uDE80\uDE92]|\uD83E(?:[\uDDAF\uDDBC\uDDBD](?:\u200D\u27A1\uFE0F?)?|[\uDDB0-\uDDB3]|\uDD1D\u200D\uD83E\uDDD1\uD83C[\uDFFB-\uDFFF])))?|\uDFFE(?:\u200D(?:[\u2695\u2696\u2708]\uFE0F?|\u2764\uFE0F?\u200D(?:\uD83D\uDC8B\u200D)?\uD83E\uDDD1\uD83C[\uDFFB-\uDFFD\uDFFF]|\uD83C[\uDF3E\uDF73\uDF7C\uDF84\uDF93\uDFA4\uDFA8\uDFEB\uDFED]|\uD83D[\uDCBB\uDCBC\uDD27\uDD2C\uDE80\uDE92]|\uD83E(?:[\uDDAF\uDDBC\uDDBD](?:\u200D\u27A1\uFE0F?)?|[\uDDB0-\uDDB3]|\uDD1D\u200D\uD83E\uDDD1\uD83C[\uDFFB-\uDFFF])))?|\uDFFF(?:\u200D(?:[\u2695\u2696\u2708]\uFE0F?|\u2764\uFE0F?\u200D(?:\uD83D\uDC8B\u200D)?\uD83E\uDDD1\uD83C[\uDFFB-\uDFFE]|\uD83C[\uDF3E\uDF73\uDF7C\uDF84\uDF93\uDFA4\uDFA8\uDFEB\uDFED]|\uD83D[\uDCBB\uDCBC\uDD27\uDD2C\uDE80\uDE92]|\uD83E(?:[\uDDAF\uDDBC\uDDBD](?:\u200D\u27A1\uFE0F?)?|[\uDDB0-\uDDB3]|\uDD1D\u200D\uD83E\uDDD1\uD83C[\uDFFB-\uDFFF])))?))?|\uDEF1(?:\uD83C(?:\uDFFB(?:\u200D\uD83E\uDEF2\uD83C[\uDFFC-\uDFFF])?|\uDFFC(?:\u200D\uD83E\uDEF2\uD83C[\uDFFB\uDFFD-\uDFFF])?|\uDFFD(?:\u200D\uD83E\uDEF2\uD83C[\uDFFB\uDFFC\uDFFE\uDFFF])?|\uDFFE(?:\u200D\uD83E\uDEF2\uD83C[\uDFFB-\uDFFD\uDFFF])?|\uDFFF(?:\u200D\uD83E\uDEF2\uD83C[\uDFFB-\uDFFE])?))?)/g;
|
|
924
|
+
};
|
|
925
|
+
var segmenter = globalThis.Intl?.Segmenter ? new Intl.Segmenter() : { segment: (str) => str.split("") };
|
|
926
|
+
var defaultIgnorableCodePointRegex = new RegExp("^\\p{Default_Ignorable_Code_Point}$", "u");
|
|
927
|
+
function stringWidth$1(string, options = {}) {
|
|
928
|
+
if (typeof string !== "string" || string.length === 0) {
|
|
929
|
+
return 0;
|
|
930
|
+
}
|
|
931
|
+
const {
|
|
932
|
+
ambiguousIsNarrow = true,
|
|
933
|
+
countAnsiEscapeCodes = false
|
|
934
|
+
} = options;
|
|
935
|
+
if (!countAnsiEscapeCodes) {
|
|
936
|
+
string = stripAnsi2(string);
|
|
937
|
+
}
|
|
938
|
+
if (string.length === 0) {
|
|
939
|
+
return 0;
|
|
940
|
+
}
|
|
941
|
+
let width = 0;
|
|
942
|
+
const eastAsianWidthOptions = { ambiguousAsWide: !ambiguousIsNarrow };
|
|
943
|
+
for (const { segment: character } of segmenter.segment(string)) {
|
|
944
|
+
const codePoint = character.codePointAt(0);
|
|
945
|
+
if (codePoint <= 31 || codePoint >= 127 && codePoint <= 159) {
|
|
946
|
+
continue;
|
|
947
|
+
}
|
|
948
|
+
if (codePoint >= 8203 && codePoint <= 8207 || codePoint === 65279) {
|
|
949
|
+
continue;
|
|
950
|
+
}
|
|
951
|
+
if (codePoint >= 768 && codePoint <= 879 || codePoint >= 6832 && codePoint <= 6911 || codePoint >= 7616 && codePoint <= 7679 || codePoint >= 8400 && codePoint <= 8447 || codePoint >= 65056 && codePoint <= 65071) {
|
|
952
|
+
continue;
|
|
953
|
+
}
|
|
954
|
+
if (codePoint >= 55296 && codePoint <= 57343) {
|
|
955
|
+
continue;
|
|
956
|
+
}
|
|
957
|
+
if (codePoint >= 65024 && codePoint <= 65039) {
|
|
958
|
+
continue;
|
|
959
|
+
}
|
|
960
|
+
if (defaultIgnorableCodePointRegex.test(character)) {
|
|
961
|
+
continue;
|
|
962
|
+
}
|
|
963
|
+
if (emojiRegex().test(character)) {
|
|
964
|
+
width += 2;
|
|
965
|
+
continue;
|
|
966
|
+
}
|
|
967
|
+
width += eastAsianWidth(codePoint, eastAsianWidthOptions);
|
|
968
|
+
}
|
|
969
|
+
return width;
|
|
970
|
+
}
|
|
971
|
+
function isUnicodeSupported() {
|
|
972
|
+
const { env: env2 } = g$1;
|
|
973
|
+
const { TERM, TERM_PROGRAM } = env2;
|
|
974
|
+
if (g$1.platform !== "win32") {
|
|
975
|
+
return TERM !== "linux";
|
|
976
|
+
}
|
|
977
|
+
return Boolean(env2.WT_SESSION) || Boolean(env2.TERMINUS_SUBLIME) || env2.ConEmuTask === "{cmd::Cmder}" || TERM_PROGRAM === "Terminus-Sublime" || TERM_PROGRAM === "vscode" || TERM === "xterm-256color" || TERM === "alacritty" || TERM === "rxvt-unicode" || TERM === "rxvt-unicode-256color" || env2.TERMINAL_EMULATOR === "JetBrains-JediTerm";
|
|
978
|
+
}
|
|
979
|
+
var TYPE_COLOR_MAP = {
|
|
980
|
+
info: "cyan",
|
|
981
|
+
fail: "red",
|
|
982
|
+
success: "green",
|
|
983
|
+
ready: "green",
|
|
984
|
+
start: "magenta"
|
|
985
|
+
};
|
|
986
|
+
var LEVEL_COLOR_MAP = {
|
|
987
|
+
0: "red",
|
|
988
|
+
1: "yellow"
|
|
989
|
+
};
|
|
990
|
+
var unicode = isUnicodeSupported();
|
|
991
|
+
var s = (c2, fallback) => unicode ? c2 : fallback;
|
|
992
|
+
var TYPE_ICONS = {
|
|
993
|
+
error: s("\u2716", "\xD7"),
|
|
994
|
+
fatal: s("\u2716", "\xD7"),
|
|
995
|
+
ready: s("\u2714", "\u221A"),
|
|
996
|
+
warn: s("\u26A0", "\u203C"),
|
|
997
|
+
info: s("\u2139", "i"),
|
|
998
|
+
success: s("\u2714", "\u221A"),
|
|
999
|
+
debug: s("\u2699", "D"),
|
|
1000
|
+
trace: s("\u2192", "\u2192"),
|
|
1001
|
+
fail: s("\u2716", "\xD7"),
|
|
1002
|
+
start: s("\u25D0", "o"),
|
|
1003
|
+
log: ""
|
|
1004
|
+
};
|
|
1005
|
+
function stringWidth(str) {
|
|
1006
|
+
const hasICU = typeof Intl === "object";
|
|
1007
|
+
if (!hasICU || !Intl.Segmenter) {
|
|
1008
|
+
return stripAnsi(str).length;
|
|
1009
|
+
}
|
|
1010
|
+
return stringWidth$1(str);
|
|
1011
|
+
}
|
|
1012
|
+
var FancyReporter = class extends BasicReporter {
|
|
1013
|
+
formatStack(stack, message, opts) {
|
|
1014
|
+
const indent = " ".repeat((opts?.errorLevel || 0) + 1);
|
|
1015
|
+
return `
|
|
1016
|
+
${indent}` + parseStack(stack, message).map(
|
|
1017
|
+
(line) => " " + line.replace(/^at +/, (m) => colors.gray(m)).replace(/\((.+)\)/, (_2, m) => `(${colors.cyan(m)})`)
|
|
1018
|
+
).join(`
|
|
1019
|
+
${indent}`);
|
|
1020
|
+
}
|
|
1021
|
+
formatType(logObj, isBadge, opts) {
|
|
1022
|
+
const typeColor = TYPE_COLOR_MAP[logObj.type] || LEVEL_COLOR_MAP[logObj.level] || "gray";
|
|
1023
|
+
if (isBadge) {
|
|
1024
|
+
return getBgColor(typeColor)(
|
|
1025
|
+
colors.black(` ${logObj.type.toUpperCase()} `)
|
|
1026
|
+
);
|
|
1027
|
+
}
|
|
1028
|
+
const _type = typeof TYPE_ICONS[logObj.type] === "string" ? TYPE_ICONS[logObj.type] : logObj.icon || logObj.type;
|
|
1029
|
+
return _type ? getColor2(typeColor)(_type) : "";
|
|
1030
|
+
}
|
|
1031
|
+
formatLogObj(logObj, opts) {
|
|
1032
|
+
const [message, ...additional] = this.formatArgs(logObj.args, opts).split(
|
|
1033
|
+
"\n"
|
|
1034
|
+
);
|
|
1035
|
+
if (logObj.type === "box") {
|
|
1036
|
+
return box(
|
|
1037
|
+
characterFormat(
|
|
1038
|
+
message + (additional.length > 0 ? "\n" + additional.join("\n") : "")
|
|
1039
|
+
),
|
|
1040
|
+
{
|
|
1041
|
+
title: logObj.title ? characterFormat(logObj.title) : void 0,
|
|
1042
|
+
style: logObj.style
|
|
1043
|
+
}
|
|
1044
|
+
);
|
|
1045
|
+
}
|
|
1046
|
+
const date = this.formatDate(logObj.date, opts);
|
|
1047
|
+
const coloredDate = date && colors.gray(date);
|
|
1048
|
+
const isBadge = logObj.badge ?? logObj.level < 2;
|
|
1049
|
+
const type = this.formatType(logObj, isBadge, opts);
|
|
1050
|
+
const tag = logObj.tag ? colors.gray(logObj.tag) : "";
|
|
1051
|
+
let line;
|
|
1052
|
+
const left = this.filterAndJoin([type, characterFormat(message)]);
|
|
1053
|
+
const right = this.filterAndJoin(opts.columns ? [tag, coloredDate] : [tag]);
|
|
1054
|
+
const space = (opts.columns || 0) - stringWidth(left) - stringWidth(right) - 2;
|
|
1055
|
+
line = space > 0 && (opts.columns || 0) >= 80 ? left + " ".repeat(space) + right : (right ? `${colors.gray(`[${right}]`)} ` : "") + left;
|
|
1056
|
+
line += characterFormat(
|
|
1057
|
+
additional.length > 0 ? "\n" + additional.join("\n") : ""
|
|
1058
|
+
);
|
|
1059
|
+
if (logObj.type === "trace") {
|
|
1060
|
+
const _err = new Error("Trace: " + logObj.message);
|
|
1061
|
+
line += this.formatStack(_err.stack || "", _err.message);
|
|
1062
|
+
}
|
|
1063
|
+
return isBadge ? "\n" + line + "\n" : line;
|
|
1064
|
+
}
|
|
1065
|
+
};
|
|
1066
|
+
function characterFormat(str) {
|
|
1067
|
+
return str.replace(/`([^`]+)`/gm, (_2, m) => colors.cyan(m)).replace(/\s+_([^_]+)_\s+/gm, (_2, m) => ` ${colors.underline(m)} `);
|
|
1068
|
+
}
|
|
1069
|
+
function getColor2(color = "white") {
|
|
1070
|
+
return colors[color] || colors.white;
|
|
1071
|
+
}
|
|
1072
|
+
function getBgColor(color = "bgWhite") {
|
|
1073
|
+
return colors[`bg${color[0].toUpperCase()}${color.slice(1)}`] || colors.bgWhite;
|
|
1074
|
+
}
|
|
1075
|
+
function createConsola2(options = {}) {
|
|
1076
|
+
let level = _getDefaultLogLevel();
|
|
1077
|
+
if (process.env.CONSOLA_LEVEL) {
|
|
1078
|
+
level = Number.parseInt(process.env.CONSOLA_LEVEL) ?? level;
|
|
1079
|
+
}
|
|
1080
|
+
const consola2 = createConsola({
|
|
1081
|
+
level,
|
|
1082
|
+
defaults: { level },
|
|
1083
|
+
stdout: process.stdout,
|
|
1084
|
+
stderr: process.stderr,
|
|
1085
|
+
prompt: (...args) => import("./prompt-GMZABCJC.js").then((m) => m.prompt(...args)),
|
|
1086
|
+
reporters: options.reporters || [
|
|
1087
|
+
options.fancy ?? !(T || R) ? new FancyReporter() : new BasicReporter()
|
|
1088
|
+
],
|
|
1089
|
+
...options
|
|
1090
|
+
});
|
|
1091
|
+
return consola2;
|
|
1092
|
+
}
|
|
1093
|
+
function _getDefaultLogLevel() {
|
|
1094
|
+
if (g) {
|
|
1095
|
+
return LogLevels.debug;
|
|
1096
|
+
}
|
|
1097
|
+
if (R) {
|
|
1098
|
+
return LogLevels.warn;
|
|
1099
|
+
}
|
|
1100
|
+
return LogLevels.info;
|
|
1101
|
+
}
|
|
1102
|
+
var consola = createConsola2();
|
|
1103
|
+
|
|
1104
|
+
// ../../packages/api-client/src/core/bodySerializer.gen.ts
|
|
1105
|
+
var jsonBodySerializer = {
|
|
1106
|
+
bodySerializer: (body) => JSON.stringify(
|
|
1107
|
+
body,
|
|
1108
|
+
(_key, value) => typeof value === "bigint" ? value.toString() : value
|
|
1109
|
+
)
|
|
1110
|
+
};
|
|
1111
|
+
|
|
1112
|
+
// ../../packages/api-client/src/core/params.gen.ts
|
|
1113
|
+
var extraPrefixesMap = {
|
|
1114
|
+
$body_: "body",
|
|
1115
|
+
$headers_: "headers",
|
|
1116
|
+
$path_: "path",
|
|
1117
|
+
$query_: "query"
|
|
1118
|
+
};
|
|
1119
|
+
var extraPrefixes = Object.entries(extraPrefixesMap);
|
|
1120
|
+
|
|
1121
|
+
// ../../packages/api-client/src/core/serverSentEvents.gen.ts
|
|
1122
|
+
var createSseClient = ({
|
|
1123
|
+
onRequest,
|
|
1124
|
+
onSseError,
|
|
1125
|
+
onSseEvent,
|
|
1126
|
+
responseTransformer,
|
|
1127
|
+
responseValidator,
|
|
1128
|
+
sseDefaultRetryDelay,
|
|
1129
|
+
sseMaxRetryAttempts,
|
|
1130
|
+
sseMaxRetryDelay,
|
|
1131
|
+
sseSleepFn,
|
|
1132
|
+
url,
|
|
1133
|
+
...options
|
|
1134
|
+
}) => {
|
|
1135
|
+
let lastEventId;
|
|
1136
|
+
const sleep = sseSleepFn ?? ((ms) => new Promise((resolve) => setTimeout(resolve, ms)));
|
|
1137
|
+
const createStream = async function* () {
|
|
1138
|
+
let retryDelay = sseDefaultRetryDelay ?? 3e3;
|
|
1139
|
+
let attempt = 0;
|
|
1140
|
+
const signal = options.signal ?? new AbortController().signal;
|
|
1141
|
+
while (true) {
|
|
1142
|
+
if (signal.aborted) break;
|
|
1143
|
+
attempt++;
|
|
1144
|
+
const headers = options.headers instanceof Headers ? options.headers : new Headers(options.headers);
|
|
1145
|
+
if (lastEventId !== void 0) {
|
|
1146
|
+
headers.set("Last-Event-ID", lastEventId);
|
|
1147
|
+
}
|
|
1148
|
+
try {
|
|
1149
|
+
const requestInit = {
|
|
1150
|
+
redirect: "follow",
|
|
1151
|
+
...options,
|
|
1152
|
+
body: options.serializedBody,
|
|
1153
|
+
headers,
|
|
1154
|
+
signal
|
|
1155
|
+
};
|
|
1156
|
+
let request = new Request(url, requestInit);
|
|
1157
|
+
if (onRequest) {
|
|
1158
|
+
request = await onRequest(url, requestInit);
|
|
1159
|
+
}
|
|
1160
|
+
const _fetch = options.fetch ?? globalThis.fetch;
|
|
1161
|
+
const response = await _fetch(request);
|
|
1162
|
+
if (!response.ok)
|
|
1163
|
+
throw new Error(
|
|
1164
|
+
`SSE failed: ${response.status} ${response.statusText}`
|
|
1165
|
+
);
|
|
1166
|
+
if (!response.body) throw new Error("No body in SSE response");
|
|
1167
|
+
const reader = response.body.pipeThrough(new TextDecoderStream()).getReader();
|
|
1168
|
+
let buffer = "";
|
|
1169
|
+
const abortHandler = () => {
|
|
1170
|
+
try {
|
|
1171
|
+
reader.cancel();
|
|
1172
|
+
} catch {
|
|
1173
|
+
}
|
|
1174
|
+
};
|
|
1175
|
+
signal.addEventListener("abort", abortHandler);
|
|
1176
|
+
try {
|
|
1177
|
+
while (true) {
|
|
1178
|
+
const { done, value } = await reader.read();
|
|
1179
|
+
if (done) break;
|
|
1180
|
+
buffer += value;
|
|
1181
|
+
const chunks = buffer.split("\r\n");
|
|
1182
|
+
buffer = chunks.pop() ?? "";
|
|
1183
|
+
for (const chunk of chunks) {
|
|
1184
|
+
const lines = chunk.split("\n");
|
|
1185
|
+
const dataLines = [];
|
|
1186
|
+
let eventName;
|
|
1187
|
+
for (const line of lines) {
|
|
1188
|
+
if (line.startsWith("data:")) {
|
|
1189
|
+
dataLines.push(line.replace(/^data:\s*/, ""));
|
|
1190
|
+
} else if (line.startsWith("event:")) {
|
|
1191
|
+
eventName = line.replace(/^event:\s*/, "");
|
|
1192
|
+
} else if (line.startsWith("id:")) {
|
|
1193
|
+
lastEventId = line.replace(/^id:\s*/, "");
|
|
1194
|
+
} else if (line.startsWith("retry:")) {
|
|
1195
|
+
const parsed = Number.parseInt(
|
|
1196
|
+
line.replace(/^retry:\s*/, ""),
|
|
1197
|
+
10
|
|
1198
|
+
);
|
|
1199
|
+
if (!Number.isNaN(parsed)) {
|
|
1200
|
+
retryDelay = parsed;
|
|
1201
|
+
}
|
|
1202
|
+
}
|
|
1203
|
+
}
|
|
1204
|
+
let data;
|
|
1205
|
+
let parsedJson = false;
|
|
1206
|
+
if (dataLines.length) {
|
|
1207
|
+
const rawData = dataLines.join("\n");
|
|
1208
|
+
try {
|
|
1209
|
+
data = JSON.parse(rawData);
|
|
1210
|
+
parsedJson = true;
|
|
1211
|
+
} catch {
|
|
1212
|
+
data = rawData;
|
|
1213
|
+
}
|
|
1214
|
+
}
|
|
1215
|
+
if (parsedJson) {
|
|
1216
|
+
if (responseValidator) {
|
|
1217
|
+
await responseValidator(data);
|
|
1218
|
+
}
|
|
1219
|
+
if (responseTransformer) {
|
|
1220
|
+
data = await responseTransformer(data);
|
|
1221
|
+
}
|
|
1222
|
+
}
|
|
1223
|
+
onSseEvent?.({
|
|
1224
|
+
data,
|
|
1225
|
+
event: eventName,
|
|
1226
|
+
id: lastEventId,
|
|
1227
|
+
retry: retryDelay
|
|
1228
|
+
});
|
|
1229
|
+
if (dataLines.length) {
|
|
1230
|
+
yield data;
|
|
1231
|
+
}
|
|
1232
|
+
}
|
|
1233
|
+
}
|
|
1234
|
+
} finally {
|
|
1235
|
+
signal.removeEventListener("abort", abortHandler);
|
|
1236
|
+
reader.releaseLock();
|
|
1237
|
+
}
|
|
1238
|
+
break;
|
|
1239
|
+
} catch (error) {
|
|
1240
|
+
onSseError?.(error);
|
|
1241
|
+
if (sseMaxRetryAttempts !== void 0 && attempt >= sseMaxRetryAttempts) {
|
|
1242
|
+
break;
|
|
1243
|
+
}
|
|
1244
|
+
const backoff = Math.min(
|
|
1245
|
+
retryDelay * 2 ** (attempt - 1),
|
|
1246
|
+
sseMaxRetryDelay ?? 3e4
|
|
1247
|
+
);
|
|
1248
|
+
await sleep(backoff);
|
|
1249
|
+
}
|
|
1250
|
+
}
|
|
1251
|
+
};
|
|
1252
|
+
const stream = createStream();
|
|
1253
|
+
return { stream };
|
|
1254
|
+
};
|
|
1255
|
+
|
|
1256
|
+
// ../../packages/api-client/src/core/pathSerializer.gen.ts
|
|
1257
|
+
var separatorArrayExplode = (style) => {
|
|
1258
|
+
switch (style) {
|
|
1259
|
+
case "label":
|
|
1260
|
+
return ".";
|
|
1261
|
+
case "matrix":
|
|
1262
|
+
return ";";
|
|
1263
|
+
case "simple":
|
|
1264
|
+
return ",";
|
|
1265
|
+
default:
|
|
1266
|
+
return "&";
|
|
1267
|
+
}
|
|
1268
|
+
};
|
|
1269
|
+
var separatorArrayNoExplode = (style) => {
|
|
1270
|
+
switch (style) {
|
|
1271
|
+
case "form":
|
|
1272
|
+
return ",";
|
|
1273
|
+
case "pipeDelimited":
|
|
1274
|
+
return "|";
|
|
1275
|
+
case "spaceDelimited":
|
|
1276
|
+
return "%20";
|
|
1277
|
+
default:
|
|
1278
|
+
return ",";
|
|
1279
|
+
}
|
|
1280
|
+
};
|
|
1281
|
+
var separatorObjectExplode = (style) => {
|
|
1282
|
+
switch (style) {
|
|
1283
|
+
case "label":
|
|
1284
|
+
return ".";
|
|
1285
|
+
case "matrix":
|
|
1286
|
+
return ";";
|
|
1287
|
+
case "simple":
|
|
1288
|
+
return ",";
|
|
1289
|
+
default:
|
|
1290
|
+
return "&";
|
|
1291
|
+
}
|
|
1292
|
+
};
|
|
1293
|
+
var serializeArrayParam = ({
|
|
1294
|
+
allowReserved,
|
|
1295
|
+
explode,
|
|
1296
|
+
name,
|
|
1297
|
+
style,
|
|
1298
|
+
value
|
|
1299
|
+
}) => {
|
|
1300
|
+
if (!explode) {
|
|
1301
|
+
const joinedValues2 = (allowReserved ? value : value.map((v) => encodeURIComponent(v))).join(separatorArrayNoExplode(style));
|
|
1302
|
+
switch (style) {
|
|
1303
|
+
case "label":
|
|
1304
|
+
return `.${joinedValues2}`;
|
|
1305
|
+
case "matrix":
|
|
1306
|
+
return `;${name}=${joinedValues2}`;
|
|
1307
|
+
case "simple":
|
|
1308
|
+
return joinedValues2;
|
|
1309
|
+
default:
|
|
1310
|
+
return `${name}=${joinedValues2}`;
|
|
1311
|
+
}
|
|
1312
|
+
}
|
|
1313
|
+
const separator = separatorArrayExplode(style);
|
|
1314
|
+
const joinedValues = value.map((v) => {
|
|
1315
|
+
if (style === "label" || style === "simple") {
|
|
1316
|
+
return allowReserved ? v : encodeURIComponent(v);
|
|
1317
|
+
}
|
|
1318
|
+
return serializePrimitiveParam({
|
|
1319
|
+
allowReserved,
|
|
1320
|
+
name,
|
|
1321
|
+
value: v
|
|
1322
|
+
});
|
|
1323
|
+
}).join(separator);
|
|
1324
|
+
return style === "label" || style === "matrix" ? separator + joinedValues : joinedValues;
|
|
1325
|
+
};
|
|
1326
|
+
var serializePrimitiveParam = ({
|
|
1327
|
+
allowReserved,
|
|
1328
|
+
name,
|
|
1329
|
+
value
|
|
1330
|
+
}) => {
|
|
1331
|
+
if (value === void 0 || value === null) {
|
|
1332
|
+
return "";
|
|
1333
|
+
}
|
|
1334
|
+
if (typeof value === "object") {
|
|
1335
|
+
throw new Error(
|
|
1336
|
+
"Deeply-nested arrays/objects aren\u2019t supported. Provide your own `querySerializer()` to handle these."
|
|
1337
|
+
);
|
|
1338
|
+
}
|
|
1339
|
+
return `${name}=${allowReserved ? value : encodeURIComponent(value)}`;
|
|
1340
|
+
};
|
|
1341
|
+
var serializeObjectParam = ({
|
|
1342
|
+
allowReserved,
|
|
1343
|
+
explode,
|
|
1344
|
+
name,
|
|
1345
|
+
style,
|
|
1346
|
+
value,
|
|
1347
|
+
valueOnly
|
|
1348
|
+
}) => {
|
|
1349
|
+
if (value instanceof Date) {
|
|
1350
|
+
return valueOnly ? value.toISOString() : `${name}=${value.toISOString()}`;
|
|
1351
|
+
}
|
|
1352
|
+
if (style !== "deepObject" && !explode) {
|
|
1353
|
+
let values = [];
|
|
1354
|
+
Object.entries(value).forEach(([key, v]) => {
|
|
1355
|
+
values = [
|
|
1356
|
+
...values,
|
|
1357
|
+
key,
|
|
1358
|
+
allowReserved ? v : encodeURIComponent(v)
|
|
1359
|
+
];
|
|
1360
|
+
});
|
|
1361
|
+
const joinedValues2 = values.join(",");
|
|
1362
|
+
switch (style) {
|
|
1363
|
+
case "form":
|
|
1364
|
+
return `${name}=${joinedValues2}`;
|
|
1365
|
+
case "label":
|
|
1366
|
+
return `.${joinedValues2}`;
|
|
1367
|
+
case "matrix":
|
|
1368
|
+
return `;${name}=${joinedValues2}`;
|
|
1369
|
+
default:
|
|
1370
|
+
return joinedValues2;
|
|
1371
|
+
}
|
|
1372
|
+
}
|
|
1373
|
+
const separator = separatorObjectExplode(style);
|
|
1374
|
+
const joinedValues = Object.entries(value).map(
|
|
1375
|
+
([key, v]) => serializePrimitiveParam({
|
|
1376
|
+
allowReserved,
|
|
1377
|
+
name: style === "deepObject" ? `${name}[${key}]` : key,
|
|
1378
|
+
value: v
|
|
1379
|
+
})
|
|
1380
|
+
).join(separator);
|
|
1381
|
+
return style === "label" || style === "matrix" ? separator + joinedValues : joinedValues;
|
|
1382
|
+
};
|
|
1383
|
+
|
|
1384
|
+
// ../../packages/api-client/src/core/utils.gen.ts
|
|
1385
|
+
var PATH_PARAM_RE = /\{[^{}]+\}/g;
|
|
1386
|
+
var defaultPathSerializer = ({ path: path3, url: _url }) => {
|
|
1387
|
+
let url = _url;
|
|
1388
|
+
const matches = _url.match(PATH_PARAM_RE);
|
|
1389
|
+
if (matches) {
|
|
1390
|
+
for (const match of matches) {
|
|
1391
|
+
let explode = false;
|
|
1392
|
+
let name = match.substring(1, match.length - 1);
|
|
1393
|
+
let style = "simple";
|
|
1394
|
+
if (name.endsWith("*")) {
|
|
1395
|
+
explode = true;
|
|
1396
|
+
name = name.substring(0, name.length - 1);
|
|
1397
|
+
}
|
|
1398
|
+
if (name.startsWith(".")) {
|
|
1399
|
+
name = name.substring(1);
|
|
1400
|
+
style = "label";
|
|
1401
|
+
} else if (name.startsWith(";")) {
|
|
1402
|
+
name = name.substring(1);
|
|
1403
|
+
style = "matrix";
|
|
1404
|
+
}
|
|
1405
|
+
const value = path3[name];
|
|
1406
|
+
if (value === void 0 || value === null) {
|
|
1407
|
+
continue;
|
|
1408
|
+
}
|
|
1409
|
+
if (Array.isArray(value)) {
|
|
1410
|
+
url = url.replace(
|
|
1411
|
+
match,
|
|
1412
|
+
serializeArrayParam({ explode, name, style, value })
|
|
1413
|
+
);
|
|
1414
|
+
continue;
|
|
1415
|
+
}
|
|
1416
|
+
if (typeof value === "object") {
|
|
1417
|
+
url = url.replace(
|
|
1418
|
+
match,
|
|
1419
|
+
serializeObjectParam({
|
|
1420
|
+
explode,
|
|
1421
|
+
name,
|
|
1422
|
+
style,
|
|
1423
|
+
value,
|
|
1424
|
+
valueOnly: true
|
|
1425
|
+
})
|
|
1426
|
+
);
|
|
1427
|
+
continue;
|
|
1428
|
+
}
|
|
1429
|
+
if (style === "matrix") {
|
|
1430
|
+
url = url.replace(
|
|
1431
|
+
match,
|
|
1432
|
+
`;${serializePrimitiveParam({
|
|
1433
|
+
name,
|
|
1434
|
+
value
|
|
1435
|
+
})}`
|
|
1436
|
+
);
|
|
1437
|
+
continue;
|
|
1438
|
+
}
|
|
1439
|
+
const replaceValue = encodeURIComponent(
|
|
1440
|
+
style === "label" ? `.${value}` : value
|
|
1441
|
+
);
|
|
1442
|
+
url = url.replace(match, replaceValue);
|
|
1443
|
+
}
|
|
1444
|
+
}
|
|
1445
|
+
return url;
|
|
1446
|
+
};
|
|
1447
|
+
var getUrl = ({
|
|
1448
|
+
baseUrl,
|
|
1449
|
+
path: path3,
|
|
1450
|
+
query,
|
|
1451
|
+
querySerializer,
|
|
1452
|
+
url: _url
|
|
1453
|
+
}) => {
|
|
1454
|
+
const pathUrl = _url.startsWith("/") ? _url : `/${_url}`;
|
|
1455
|
+
let url = (baseUrl ?? "") + pathUrl;
|
|
1456
|
+
if (path3) {
|
|
1457
|
+
url = defaultPathSerializer({ path: path3, url });
|
|
1458
|
+
}
|
|
1459
|
+
let search = query ? querySerializer(query) : "";
|
|
1460
|
+
if (search.startsWith("?")) {
|
|
1461
|
+
search = search.substring(1);
|
|
1462
|
+
}
|
|
1463
|
+
if (search) {
|
|
1464
|
+
url += `?${search}`;
|
|
1465
|
+
}
|
|
1466
|
+
return url;
|
|
1467
|
+
};
|
|
1468
|
+
function getValidRequestBody(options) {
|
|
1469
|
+
const hasBody = options.body !== void 0;
|
|
1470
|
+
const isSerializedBody = hasBody && options.bodySerializer;
|
|
1471
|
+
if (isSerializedBody) {
|
|
1472
|
+
if ("serializedBody" in options) {
|
|
1473
|
+
const hasSerializedBody = options.serializedBody !== void 0 && options.serializedBody !== "";
|
|
1474
|
+
return hasSerializedBody ? options.serializedBody : null;
|
|
1475
|
+
}
|
|
1476
|
+
return options.body !== "" ? options.body : null;
|
|
1477
|
+
}
|
|
1478
|
+
if (hasBody) {
|
|
1479
|
+
return options.body;
|
|
1480
|
+
}
|
|
1481
|
+
return void 0;
|
|
1482
|
+
}
|
|
1483
|
+
|
|
1484
|
+
// ../../packages/api-client/src/core/auth.gen.ts
|
|
1485
|
+
var getAuthToken = async (auth, callback) => {
|
|
1486
|
+
const token = typeof callback === "function" ? await callback(auth) : callback;
|
|
1487
|
+
if (!token) {
|
|
1488
|
+
return;
|
|
1489
|
+
}
|
|
1490
|
+
if (auth.scheme === "bearer") {
|
|
1491
|
+
return `Bearer ${token}`;
|
|
1492
|
+
}
|
|
1493
|
+
if (auth.scheme === "basic") {
|
|
1494
|
+
return `Basic ${btoa(token)}`;
|
|
1495
|
+
}
|
|
1496
|
+
return token;
|
|
1497
|
+
};
|
|
1498
|
+
|
|
1499
|
+
// ../../packages/api-client/src/client/utils.gen.ts
|
|
1500
|
+
var createQuerySerializer = ({
|
|
1501
|
+
parameters = {},
|
|
1502
|
+
...args
|
|
1503
|
+
} = {}) => {
|
|
1504
|
+
const querySerializer = (queryParams) => {
|
|
1505
|
+
const search = [];
|
|
1506
|
+
if (queryParams && typeof queryParams === "object") {
|
|
1507
|
+
for (const name in queryParams) {
|
|
1508
|
+
const value = queryParams[name];
|
|
1509
|
+
if (value === void 0 || value === null) {
|
|
1510
|
+
continue;
|
|
1511
|
+
}
|
|
1512
|
+
const options = parameters[name] || args;
|
|
1513
|
+
if (Array.isArray(value)) {
|
|
1514
|
+
const serializedArray = serializeArrayParam({
|
|
1515
|
+
allowReserved: options.allowReserved,
|
|
1516
|
+
explode: true,
|
|
1517
|
+
name,
|
|
1518
|
+
style: "form",
|
|
1519
|
+
value,
|
|
1520
|
+
...options.array
|
|
1521
|
+
});
|
|
1522
|
+
if (serializedArray) search.push(serializedArray);
|
|
1523
|
+
} else if (typeof value === "object") {
|
|
1524
|
+
const serializedObject = serializeObjectParam({
|
|
1525
|
+
allowReserved: options.allowReserved,
|
|
1526
|
+
explode: true,
|
|
1527
|
+
name,
|
|
1528
|
+
style: "deepObject",
|
|
1529
|
+
value,
|
|
1530
|
+
...options.object
|
|
1531
|
+
});
|
|
1532
|
+
if (serializedObject) search.push(serializedObject);
|
|
1533
|
+
} else {
|
|
1534
|
+
const serializedPrimitive = serializePrimitiveParam({
|
|
1535
|
+
allowReserved: options.allowReserved,
|
|
1536
|
+
name,
|
|
1537
|
+
value
|
|
1538
|
+
});
|
|
1539
|
+
if (serializedPrimitive) search.push(serializedPrimitive);
|
|
1540
|
+
}
|
|
1541
|
+
}
|
|
1542
|
+
}
|
|
1543
|
+
return search.join("&");
|
|
1544
|
+
};
|
|
1545
|
+
return querySerializer;
|
|
1546
|
+
};
|
|
1547
|
+
var getParseAs = (contentType) => {
|
|
1548
|
+
if (!contentType) {
|
|
1549
|
+
return "stream";
|
|
1550
|
+
}
|
|
1551
|
+
const cleanContent = contentType.split(";")[0]?.trim();
|
|
1552
|
+
if (!cleanContent) {
|
|
1553
|
+
return;
|
|
1554
|
+
}
|
|
1555
|
+
if (cleanContent.startsWith("application/json") || cleanContent.endsWith("+json")) {
|
|
1556
|
+
return "json";
|
|
1557
|
+
}
|
|
1558
|
+
if (cleanContent === "multipart/form-data") {
|
|
1559
|
+
return "formData";
|
|
1560
|
+
}
|
|
1561
|
+
if (["application/", "audio/", "image/", "video/"].some(
|
|
1562
|
+
(type) => cleanContent.startsWith(type)
|
|
1563
|
+
)) {
|
|
1564
|
+
return "blob";
|
|
1565
|
+
}
|
|
1566
|
+
if (cleanContent.startsWith("text/")) {
|
|
1567
|
+
return "text";
|
|
1568
|
+
}
|
|
1569
|
+
return;
|
|
1570
|
+
};
|
|
1571
|
+
var checkForExistence = (options, name) => {
|
|
1572
|
+
if (!name) {
|
|
1573
|
+
return false;
|
|
1574
|
+
}
|
|
1575
|
+
if (options.headers.has(name) || options.query?.[name] || options.headers.get("Cookie")?.includes(`${name}=`)) {
|
|
1576
|
+
return true;
|
|
1577
|
+
}
|
|
1578
|
+
return false;
|
|
1579
|
+
};
|
|
1580
|
+
var setAuthParams = async ({
|
|
1581
|
+
security,
|
|
1582
|
+
...options
|
|
1583
|
+
}) => {
|
|
1584
|
+
for (const auth of security) {
|
|
1585
|
+
if (checkForExistence(options, auth.name)) {
|
|
1586
|
+
continue;
|
|
1587
|
+
}
|
|
1588
|
+
const token = await getAuthToken(auth, options.auth);
|
|
1589
|
+
if (!token) {
|
|
1590
|
+
continue;
|
|
1591
|
+
}
|
|
1592
|
+
const name = auth.name ?? "Authorization";
|
|
1593
|
+
switch (auth.in) {
|
|
1594
|
+
case "query":
|
|
1595
|
+
if (!options.query) {
|
|
1596
|
+
options.query = {};
|
|
1597
|
+
}
|
|
1598
|
+
options.query[name] = token;
|
|
1599
|
+
break;
|
|
1600
|
+
case "cookie":
|
|
1601
|
+
options.headers.append("Cookie", `${name}=${token}`);
|
|
1602
|
+
break;
|
|
1603
|
+
case "header":
|
|
1604
|
+
default:
|
|
1605
|
+
options.headers.set(name, token);
|
|
1606
|
+
break;
|
|
1607
|
+
}
|
|
1608
|
+
}
|
|
1609
|
+
};
|
|
1610
|
+
var buildUrl = (options) => getUrl({
|
|
1611
|
+
baseUrl: options.baseUrl,
|
|
1612
|
+
path: options.path,
|
|
1613
|
+
query: options.query,
|
|
1614
|
+
querySerializer: typeof options.querySerializer === "function" ? options.querySerializer : createQuerySerializer(options.querySerializer),
|
|
1615
|
+
url: options.url
|
|
1616
|
+
});
|
|
1617
|
+
var mergeConfigs = (a2, b2) => {
|
|
1618
|
+
const config = { ...a2, ...b2 };
|
|
1619
|
+
if (config.baseUrl?.endsWith("/")) {
|
|
1620
|
+
config.baseUrl = config.baseUrl.substring(0, config.baseUrl.length - 1);
|
|
1621
|
+
}
|
|
1622
|
+
config.headers = mergeHeaders(a2.headers, b2.headers);
|
|
1623
|
+
return config;
|
|
1624
|
+
};
|
|
1625
|
+
var headersEntries = (headers) => {
|
|
1626
|
+
const entries = [];
|
|
1627
|
+
headers.forEach((value, key) => {
|
|
1628
|
+
entries.push([key, value]);
|
|
1629
|
+
});
|
|
1630
|
+
return entries;
|
|
1631
|
+
};
|
|
1632
|
+
var mergeHeaders = (...headers) => {
|
|
1633
|
+
const mergedHeaders = new Headers();
|
|
1634
|
+
for (const header of headers) {
|
|
1635
|
+
if (!header) {
|
|
1636
|
+
continue;
|
|
1637
|
+
}
|
|
1638
|
+
const iterator = header instanceof Headers ? headersEntries(header) : Object.entries(header);
|
|
1639
|
+
for (const [key, value] of iterator) {
|
|
1640
|
+
if (value === null) {
|
|
1641
|
+
mergedHeaders.delete(key);
|
|
1642
|
+
} else if (Array.isArray(value)) {
|
|
1643
|
+
for (const v of value) {
|
|
1644
|
+
mergedHeaders.append(key, v);
|
|
1645
|
+
}
|
|
1646
|
+
} else if (value !== void 0) {
|
|
1647
|
+
mergedHeaders.set(
|
|
1648
|
+
key,
|
|
1649
|
+
typeof value === "object" ? JSON.stringify(value) : value
|
|
1650
|
+
);
|
|
1651
|
+
}
|
|
1652
|
+
}
|
|
1653
|
+
}
|
|
1654
|
+
return mergedHeaders;
|
|
1655
|
+
};
|
|
1656
|
+
var Interceptors = class {
|
|
1657
|
+
fns = [];
|
|
1658
|
+
clear() {
|
|
1659
|
+
this.fns = [];
|
|
1660
|
+
}
|
|
1661
|
+
eject(id) {
|
|
1662
|
+
const index = this.getInterceptorIndex(id);
|
|
1663
|
+
if (this.fns[index]) {
|
|
1664
|
+
this.fns[index] = null;
|
|
1665
|
+
}
|
|
1666
|
+
}
|
|
1667
|
+
exists(id) {
|
|
1668
|
+
const index = this.getInterceptorIndex(id);
|
|
1669
|
+
return Boolean(this.fns[index]);
|
|
1670
|
+
}
|
|
1671
|
+
getInterceptorIndex(id) {
|
|
1672
|
+
if (typeof id === "number") {
|
|
1673
|
+
return this.fns[id] ? id : -1;
|
|
1674
|
+
}
|
|
1675
|
+
return this.fns.indexOf(id);
|
|
1676
|
+
}
|
|
1677
|
+
update(id, fn) {
|
|
1678
|
+
const index = this.getInterceptorIndex(id);
|
|
1679
|
+
if (this.fns[index]) {
|
|
1680
|
+
this.fns[index] = fn;
|
|
1681
|
+
return id;
|
|
1682
|
+
}
|
|
1683
|
+
return false;
|
|
1684
|
+
}
|
|
1685
|
+
use(fn) {
|
|
1686
|
+
this.fns.push(fn);
|
|
1687
|
+
return this.fns.length - 1;
|
|
1688
|
+
}
|
|
1689
|
+
};
|
|
1690
|
+
var createInterceptors = () => ({
|
|
1691
|
+
error: new Interceptors(),
|
|
1692
|
+
request: new Interceptors(),
|
|
1693
|
+
response: new Interceptors()
|
|
1694
|
+
});
|
|
1695
|
+
var defaultQuerySerializer = createQuerySerializer({
|
|
1696
|
+
allowReserved: false,
|
|
1697
|
+
array: {
|
|
1698
|
+
explode: true,
|
|
1699
|
+
style: "form"
|
|
1700
|
+
},
|
|
1701
|
+
object: {
|
|
1702
|
+
explode: true,
|
|
1703
|
+
style: "deepObject"
|
|
1704
|
+
}
|
|
1705
|
+
});
|
|
1706
|
+
var defaultHeaders = {
|
|
1707
|
+
"Content-Type": "application/json"
|
|
1708
|
+
};
|
|
1709
|
+
var createConfig = (override = {}) => ({
|
|
1710
|
+
...jsonBodySerializer,
|
|
1711
|
+
headers: defaultHeaders,
|
|
1712
|
+
parseAs: "auto",
|
|
1713
|
+
querySerializer: defaultQuerySerializer,
|
|
1714
|
+
...override
|
|
1715
|
+
});
|
|
1716
|
+
|
|
1717
|
+
// ../../packages/api-client/src/client/client.gen.ts
|
|
1718
|
+
var createClient = (config = {}) => {
|
|
1719
|
+
let _config = mergeConfigs(createConfig(), config);
|
|
1720
|
+
const getConfig = () => ({ ..._config });
|
|
1721
|
+
const setConfig = (config2) => {
|
|
1722
|
+
_config = mergeConfigs(_config, config2);
|
|
1723
|
+
return getConfig();
|
|
1724
|
+
};
|
|
1725
|
+
const interceptors = createInterceptors();
|
|
1726
|
+
const beforeRequest = async (options) => {
|
|
1727
|
+
const opts = {
|
|
1728
|
+
..._config,
|
|
1729
|
+
...options,
|
|
1730
|
+
fetch: options.fetch ?? _config.fetch ?? globalThis.fetch,
|
|
1731
|
+
headers: mergeHeaders(_config.headers, options.headers),
|
|
1732
|
+
serializedBody: void 0
|
|
1733
|
+
};
|
|
1734
|
+
if (opts.security) {
|
|
1735
|
+
await setAuthParams({
|
|
1736
|
+
...opts,
|
|
1737
|
+
security: opts.security
|
|
1738
|
+
});
|
|
1739
|
+
}
|
|
1740
|
+
if (opts.requestValidator) {
|
|
1741
|
+
await opts.requestValidator(opts);
|
|
1742
|
+
}
|
|
1743
|
+
if (opts.body !== void 0 && opts.bodySerializer) {
|
|
1744
|
+
opts.serializedBody = opts.bodySerializer(opts.body);
|
|
1745
|
+
}
|
|
1746
|
+
if (opts.body === void 0 || opts.serializedBody === "") {
|
|
1747
|
+
opts.headers.delete("Content-Type");
|
|
1748
|
+
}
|
|
1749
|
+
const url = buildUrl(opts);
|
|
1750
|
+
return { opts, url };
|
|
1751
|
+
};
|
|
1752
|
+
const request = async (options) => {
|
|
1753
|
+
const { opts, url } = await beforeRequest(options);
|
|
1754
|
+
const requestInit = {
|
|
1755
|
+
redirect: "follow",
|
|
1756
|
+
...opts,
|
|
1757
|
+
body: getValidRequestBody(opts)
|
|
1758
|
+
};
|
|
1759
|
+
let request2 = new Request(url, requestInit);
|
|
1760
|
+
for (const fn of interceptors.request.fns) {
|
|
1761
|
+
if (fn) {
|
|
1762
|
+
request2 = await fn(request2, opts);
|
|
1763
|
+
}
|
|
1764
|
+
}
|
|
1765
|
+
const _fetch = opts.fetch;
|
|
1766
|
+
let response;
|
|
1767
|
+
try {
|
|
1768
|
+
response = await _fetch(request2);
|
|
1769
|
+
} catch (error2) {
|
|
1770
|
+
let finalError2 = error2;
|
|
1771
|
+
for (const fn of interceptors.error.fns) {
|
|
1772
|
+
if (fn) {
|
|
1773
|
+
finalError2 = await fn(
|
|
1774
|
+
error2,
|
|
1775
|
+
void 0,
|
|
1776
|
+
request2,
|
|
1777
|
+
opts
|
|
1778
|
+
);
|
|
1779
|
+
}
|
|
1780
|
+
}
|
|
1781
|
+
finalError2 = finalError2 || {};
|
|
1782
|
+
if (opts.throwOnError) {
|
|
1783
|
+
throw finalError2;
|
|
1784
|
+
}
|
|
1785
|
+
return opts.responseStyle === "data" ? void 0 : {
|
|
1786
|
+
error: finalError2,
|
|
1787
|
+
request: request2,
|
|
1788
|
+
response: void 0
|
|
1789
|
+
};
|
|
1790
|
+
}
|
|
1791
|
+
for (const fn of interceptors.response.fns) {
|
|
1792
|
+
if (fn) {
|
|
1793
|
+
response = await fn(response, request2, opts);
|
|
1794
|
+
}
|
|
1795
|
+
}
|
|
1796
|
+
const result = {
|
|
1797
|
+
request: request2,
|
|
1798
|
+
response
|
|
1799
|
+
};
|
|
1800
|
+
if (response.ok) {
|
|
1801
|
+
const parseAs = (opts.parseAs === "auto" ? getParseAs(response.headers.get("Content-Type")) : opts.parseAs) ?? "json";
|
|
1802
|
+
if (response.status === 204 || response.headers.get("Content-Length") === "0") {
|
|
1803
|
+
let emptyData;
|
|
1804
|
+
switch (parseAs) {
|
|
1805
|
+
case "arrayBuffer":
|
|
1806
|
+
case "blob":
|
|
1807
|
+
case "text":
|
|
1808
|
+
emptyData = await response[parseAs]();
|
|
1809
|
+
break;
|
|
1810
|
+
case "formData":
|
|
1811
|
+
emptyData = new FormData();
|
|
1812
|
+
break;
|
|
1813
|
+
case "stream":
|
|
1814
|
+
emptyData = response.body;
|
|
1815
|
+
break;
|
|
1816
|
+
case "json":
|
|
1817
|
+
default:
|
|
1818
|
+
emptyData = {};
|
|
1819
|
+
break;
|
|
1820
|
+
}
|
|
1821
|
+
return opts.responseStyle === "data" ? emptyData : {
|
|
1822
|
+
data: emptyData,
|
|
1823
|
+
...result
|
|
1824
|
+
};
|
|
1825
|
+
}
|
|
1826
|
+
let data;
|
|
1827
|
+
switch (parseAs) {
|
|
1828
|
+
case "arrayBuffer":
|
|
1829
|
+
case "blob":
|
|
1830
|
+
case "formData":
|
|
1831
|
+
case "json":
|
|
1832
|
+
case "text":
|
|
1833
|
+
data = await response[parseAs]();
|
|
1834
|
+
break;
|
|
1835
|
+
case "stream":
|
|
1836
|
+
return opts.responseStyle === "data" ? response.body : {
|
|
1837
|
+
data: response.body,
|
|
1838
|
+
...result
|
|
1839
|
+
};
|
|
1840
|
+
}
|
|
1841
|
+
if (parseAs === "json") {
|
|
1842
|
+
if (opts.responseValidator) {
|
|
1843
|
+
await opts.responseValidator(data);
|
|
1844
|
+
}
|
|
1845
|
+
if (opts.responseTransformer) {
|
|
1846
|
+
data = await opts.responseTransformer(data);
|
|
1847
|
+
}
|
|
1848
|
+
}
|
|
1849
|
+
return opts.responseStyle === "data" ? data : {
|
|
1850
|
+
data,
|
|
1851
|
+
...result
|
|
1852
|
+
};
|
|
1853
|
+
}
|
|
1854
|
+
const textError = await response.text();
|
|
1855
|
+
let jsonError;
|
|
1856
|
+
try {
|
|
1857
|
+
jsonError = JSON.parse(textError);
|
|
1858
|
+
} catch {
|
|
1859
|
+
}
|
|
1860
|
+
const error = jsonError ?? textError;
|
|
1861
|
+
let finalError = error;
|
|
1862
|
+
for (const fn of interceptors.error.fns) {
|
|
1863
|
+
if (fn) {
|
|
1864
|
+
finalError = await fn(error, response, request2, opts);
|
|
1865
|
+
}
|
|
1866
|
+
}
|
|
1867
|
+
finalError = finalError || {};
|
|
1868
|
+
if (opts.throwOnError) {
|
|
1869
|
+
throw finalError;
|
|
1870
|
+
}
|
|
1871
|
+
return opts.responseStyle === "data" ? void 0 : {
|
|
1872
|
+
error: finalError,
|
|
1873
|
+
...result
|
|
1874
|
+
};
|
|
1875
|
+
};
|
|
1876
|
+
const makeMethodFn = (method) => (options) => request({ ...options, method });
|
|
1877
|
+
const makeSseFn = (method) => async (options) => {
|
|
1878
|
+
const { opts, url } = await beforeRequest(options);
|
|
1879
|
+
return createSseClient({
|
|
1880
|
+
...opts,
|
|
1881
|
+
body: opts.body,
|
|
1882
|
+
headers: opts.headers,
|
|
1883
|
+
method,
|
|
1884
|
+
onRequest: async (url2, init2) => {
|
|
1885
|
+
let request2 = new Request(url2, init2);
|
|
1886
|
+
for (const fn of interceptors.request.fns) {
|
|
1887
|
+
if (fn) {
|
|
1888
|
+
request2 = await fn(request2, opts);
|
|
1889
|
+
}
|
|
1890
|
+
}
|
|
1891
|
+
return request2;
|
|
1892
|
+
},
|
|
1893
|
+
serializedBody: getValidRequestBody(opts),
|
|
1894
|
+
url
|
|
1895
|
+
});
|
|
1896
|
+
};
|
|
1897
|
+
return {
|
|
1898
|
+
buildUrl,
|
|
1899
|
+
connect: makeMethodFn("CONNECT"),
|
|
1900
|
+
delete: makeMethodFn("DELETE"),
|
|
1901
|
+
get: makeMethodFn("GET"),
|
|
1902
|
+
getConfig,
|
|
1903
|
+
head: makeMethodFn("HEAD"),
|
|
1904
|
+
interceptors,
|
|
1905
|
+
options: makeMethodFn("OPTIONS"),
|
|
1906
|
+
patch: makeMethodFn("PATCH"),
|
|
1907
|
+
post: makeMethodFn("POST"),
|
|
1908
|
+
put: makeMethodFn("PUT"),
|
|
1909
|
+
request,
|
|
1910
|
+
setConfig,
|
|
1911
|
+
sse: {
|
|
1912
|
+
connect: makeSseFn("CONNECT"),
|
|
1913
|
+
delete: makeSseFn("DELETE"),
|
|
1914
|
+
get: makeSseFn("GET"),
|
|
1915
|
+
head: makeSseFn("HEAD"),
|
|
1916
|
+
options: makeSseFn("OPTIONS"),
|
|
1917
|
+
patch: makeSseFn("PATCH"),
|
|
1918
|
+
post: makeSseFn("POST"),
|
|
1919
|
+
put: makeSseFn("PUT"),
|
|
1920
|
+
trace: makeSseFn("TRACE")
|
|
1921
|
+
},
|
|
1922
|
+
trace: makeMethodFn("TRACE")
|
|
1923
|
+
};
|
|
1924
|
+
};
|
|
1925
|
+
|
|
1926
|
+
// ../../packages/api-client/src/client.gen.ts
|
|
1927
|
+
var client = createClient(createConfig({ baseUrl: "https://dreamboard.games" }));
|
|
1928
|
+
|
|
1929
|
+
// src/utils/fs.ts
|
|
1930
|
+
import { mkdir, readFile, stat, writeFile } from "fs/promises";
|
|
1931
|
+
import path from "path";
|
|
1932
|
+
|
|
1933
|
+
// src/generated/skill-content.generated.ts
|
|
1934
|
+
var SKILL_ASSET_FILES = {
|
|
1935
|
+
"references/adversarial-testing.md": "# Adversarial Testing\n\nAdversarial testing uses scenario files to intentionally stress game rules, turn gating, and edge conditions.\n\nUse this guide with `dreamboard run --scenario <file>`.\n\nThis file is about adversarial coverage strategy, not JSON schema.\n\n- For the JSON step format itself, see [scenario-format.md](scenario-format.md).\n- For TypeScript regression tests, see [test-harness.md](test-harness.md).\n\n## Goals\n\n1. Validate that illegal or out-of-sequence actions are rejected.\n2. Validate that legal actions still work after edge-state pressure.\n3. Catch flaky behavior by running with deterministic seeds.\n\n## Core Workflow\n\n1. Start from a known state:\n\n```bash\ndreamboard run --new-session --seed 1337 --players <playerCount>\n```\n\n2. Inspect `.dreamboard/run/latest-your-turn.json` to identify:\n\n - Current controllable player(s)\n - Available actions and parameter shapes\n - Turn/phase context\n\n3. Write a focused scenario file that targets one adversarial behavior.\n4. Execute the scenario:\n\n```bash\ndreamboard run --players <playerCount> --scenario scenario-for-<case>.json\n```\n\n5. Validate outcomes using:\n - CLI exit result (`scenario_completed` vs `scenario_rejected`)\n - `.dreamboard/run/events.ndjson`\n - `.dreamboard/run/last-run-summary.json`\n\nFor deeper inspection, extract specific fields from events:\n\n```bash\nnode .agents/skills/dreamboard/scripts/events-extract.mjs --type ACTION_REJECTED --field message.reason\nnode .agents/skills/dreamboard/scripts/events-extract.mjs --type YOUR_TURN --field message.availableActions --limit 1\n```\n\n## Progression Strategy\n\n1. Start simple first:\n - Begin with 1-step or 2-step scenarios.\n - Use a single player (`player-1`) and one expected behavior per file.\n - Confirm your action names and parameter shapes are correct before scaling up.\n2. Then move to complex simulations:\n - Create multi-step scenarios that chain several turns.\n - Include multiple players across steps (for example `player-1`, `player-2`, `player-3`).\n - Combine normal and adversarial actions in the same flow to test resilience after state transitions.\n3. Create scenario that fully tests a round (e.g. all players played their hand, or all players placed their workers)\n4. Ensure AUTO phase transitions are correct.\n5. Keep deterministic inputs (`--new-session --seed 1337`) while increasing complexity so failures stay reproducible.\n\nBuild scenario files using the contract in [scenario-format.md](scenario-format.md), then use the patterns below to decide what to test.\n\n## Adversarial Patterns\n\nCreate one scenario per pattern so failures are easy to triage.\n\n1. Invalid parameters: missing required input, invalid enum, or out-of-range values.\n2. Wrong player: action submitted by a non-active/non-controllable player.\n3. Order violation: execute actions in a sequence that should be illegal.\n4. Resource exhaustion: repeat actions until resources/cards/slots are depleted.\n5. Duplicate or replay behavior: attempt the same step repeatedly in one turn.\n6. Boundary transitions: move through end-of-round/end-of-game boundaries and attempt one extra action.\n\n## Practical Conventions\n\n1. Keep each scenario small and single-purpose.\n2. Name files by expected behavior, for example:\n - `scenario-reject-invalid-params.json`\n - `scenario-reject-wrong-player.json`\n - `scenario-endgame-boundary.json`\n3. Prefer `--new-session --seed 1337` while authoring/debugging to avoid random drift.\n4. If turn context is missing, `dreamboard run --scenario ...` will wait for the first `YOUR_TURN` automatically.\n5. API scenarios are strict: if a step gets `ACTION_REJECTED`, the run stops with `scenario_rejected`.\n6. Use `events-extract.ts` to quickly inspect rejection reasons, available actions, and player transitions before editing scenarios.\n\n### Error: `Card is not in your hand`\n\nWhen this rejection appears, verify the target player's hand state across turns before and after each scenario step.\n\n1. Extract rejection events first:\n\n```bash\nnode .agents/skills/dreamboard/scripts/events-extract.mjs --type ACTION_REJECTED --field message.reason\n```\n\n2. Inspect hand snapshots from turn events for the target player (example: `player-2`):\n\n```bash\nnode .agents/skills/dreamboard/scripts/events-extract.mjs --type YOUR_TURN --player player-2 --field message.gameState.hands\n```\n\n3. Compare entries by `index` in output to see how the player's hand changes between turns.\n4. Confirm the `cardId` in your scenario step exists in the target player's hand at the turn where the action is submitted.\n5. If the card disappears earlier than expected, inspect preceding steps for draws/discards/transfers that changed ownership.\n\n## Optional UI Driver\n\nUse `--scenario-driver ui` only for browser input-path coverage (click targets, interaction wiring).\nKeep most adversarial rule tests on the API driver for reliability and speed.\n",
|
|
1936
|
+
"references/all-players-tracking.md": '# ALL_PLAYERS Phase - Tracking Who Has Acted\n\nThe engine automatically tracks action progress in `ALL_PLAYERS` (`MultiplePlayer`) phases through `ctx.phase`. Do not implement manual KV tracking for this.\n\n## Built-in phase tracking API\n\nUse `ctx.phase` in `validateAction` and `checkCompletion`:\n\n- `ctx.phase.hasPlayerActed(playerId)` - has this player already submitted an action?\n- `ctx.phase.haveAllExpectedPlayersActed()` - are all currently expected players done?\n- `ctx.phase.getPlayersStillWaiting()` - which players are still pending?\n- `ctx.phase.getExpectedPlayers()` - who is expected to act this phase?\n- `ctx.phase.getPlayersWhoActed()` - who has already acted?\n\n## Recommended pattern\n\n```typescript\nimport type { MultiplePlayerPhaseDefinition } from "../sdk/phaseHandlers";\nimport { createMultiplePlayerPhase } from "../sdk/phaseHandlers";\nimport { validationSuccess, validationError } from "../sdk/validation.js";\n\ntype PhaseAction = "revealCard";\n\nconst phaseDefinition = {\n onEnter(ctx) {\n const { state, apis } = ctx;\n apis.gameApi.setActivePlayers(state.player.getOrder());\n // No manual tracking setup required.\n },\n\n validateAction(ctx, playerId, actionType, parameters) {\n if (ctx.phase.hasPlayerActed(playerId)) {\n return validationError("already-acted", "You already acted this phase");\n }\n return validationSuccess();\n },\n\n onPlayerAction(ctx, playerId, actionType, parameters) {\n const { apis } = ctx;\n // ... process action ...\n // Engine tracking updates automatically after accepted actions.\n },\n\n checkCompletion(ctx) {\n if (ctx.phase.haveAllExpectedPlayersActed()) {\n return "nextPhase";\n }\n return null;\n },\n\n onComplete(ctx) {\n // Optional cleanup only. No action-tracking cleanup required.\n },\n\n getUIArgs(ctx, playerId) {\n return {\n waitingOn: ctx.phase.getPlayersStillWaiting(),\n };\n },\n} satisfies MultiplePlayerPhaseDefinition<"revealCards", PhaseAction>;\n\nexport const phase = createMultiplePlayerPhase(phaseDefinition);\n```\n\n## If UI needs progress data\n\n`ctx.phase` is available in app logic. For UI, pass derived values via `getUIArgs()` (for example `waitingOn`), or write explicit UI-facing values via global/player state APIs.\n\n## Checklist for ALL_PLAYERS phases\n\n1. Call `apis.gameApi.setActivePlayers(state.player.getOrder())` in `onEnter`.\n2. Block duplicate actions with `ctx.phase.hasPlayerActed(playerId)` in `validateAction`.\n3. Process each action in `onPlayerAction` without manual tracking writes.\n4. Transition from `checkCompletion` when `ctx.phase.haveAllExpectedPlayersActed()` is true.\n5. Use `ctx.phase.getPlayersStillWaiting()` / `getPlayersWhoActed()` for logs or UI args as needed.\n',
|
|
1937
|
+
"references/api-reference.md": '# State & API Reference\n\n## Read APIs (`ctx.state.*`)\n\n### `state.player`\n\n| Method | Returns | Description |\n| -------------------------------------- | ------------- | ------------------------------------- |\n| `.getOrder()` | `PlayerId[]` | All players in turn order |\n| `.getCurrentIds()` | `PlayerId[]` | Currently active player IDs |\n| `.get(playerId)` | `Player` | Player data: `{ id, name, score }` |\n| `.getState(playerId)` | `PlayerState` | Player variables from manifest schema |\n| `.getHand(playerId, handId)` | `CardId[]` | Cards in a specific hand |\n| `.getAllHands(playerId)` | `CardId[]` | All cards across all hands |\n| `.isInHand(playerId, cardId, handId?)` | `boolean` | Check if card is in hand |\n| `.isActive(playerId)` | `boolean` | Is player currently active |\n\n### `state.deck`\n\n| Method | Returns | Description |\n| --------------------- | ---------------- | ---------------------- |\n| `.getCards(deckId)` | `CardId[]` | Cards in a shared deck |\n| `.getTopCard(deckId)` | `CardId \\| null` | Top card of deck |\n\n### `state.card`\n\n| Method | Returns | Description |\n| -------------------------------- | ------------------ | ---------------------------------------------------- |\n| `.get(cardId)` | `Card` | Full card data |\n| `.getProperties(cardId)` | Typed properties | Card-specific props (rank, suit, etc.) |\n| `.getLocation(cardId)` | `Location` | Where the card is (InHand, InDeck, InZone, Detached) |\n| `.getPlayedBy(cardId)` | `PlayerId \\| null` | Who played this card to a deck/zone |\n| `.getOwner(cardId)` | `PlayerId \\| null` | Card owner |\n| `.isVisibleTo(cardId, playerId)` | `boolean` | Visibility check |\n| `.getOwnedBy(playerId)` | `CardId[]` | All cards owned by player |\n\n### `state.game`\n\n| Method | Returns | Description |\n| -------------------- | ------------- | ------------------------------------- |\n| `.getGlobalState()` | `GlobalState` | Global variables from manifest schema |\n| `.getCurrentState()` | `StateName` | Current state machine state |\n\n## Mutation APIs (`ctx.apis.*`)\n\n### `apis.cardApi`\n\n| Method | Description |\n| --------------------------------------------------------------------------- | ------------------------------ |\n| `.moveCardsFromHandToDeck(playerId, handId, cardIds, deckId)` | Hand \u2192 Deck |\n| `.moveCardsFromHandToHand(fromPlayer, fromHand, toPlayer, toHand, cardIds)` | Hand \u2192 Hand |\n| `.moveCardsToPlayer(cardIds, toPlayerId, handId)` | Any \u2192 Hand |\n| `.flip(deckId, cardId)` | Flip card face up/down |\n| `.detachCard(cardId)` | Remove card from all locations |\n| `.transferOwnership(cardId, toPlayer)` | Change card owner |\n\n#### Card movement semantics\n\n- `moveCardsFromHandToHand(fromPlayer, fromHand, toPlayer, toHand, cardIds)` is **additive** at destination.\n- Destination cards are preserved; moved cards are added to the destination hand.\n- This API does **not** replace/overwrite the destination hand.\n- For pass/rotate mechanics where each player should receive exactly one other player\'s hand, avoid in-place cyclic moves. Snapshot source hands first, then move via a temporary location (or other two-phase transfer pattern).\n\n### `apis.deckApi`\n\n| Method | Description |\n| ------------------------------------------------------------- | ------------------------- |\n| `.moveCardsFromDeckToPlayer(deckId, playerId, handId, count)` | Deck \u2192 Hand (deal) |\n| `.moveCardsFromDeckToDeck(fromDeckId, toDeckId)` | Deck \u2192 Deck |\n| `.shuffle(deckId)` | Shuffle a deck |\n| `.addCards(deckId, cardIds)` | Add cards to a deck |\n| `.removeCard(deckId, cardId)` | Remove a card from a deck |\n\n### `apis.gameApi`\n\n| Method | Description |\n| ---------------------------------- | ---------------------------------------------- |\n| `.setActivePlayers(playerIds)` | Set who can act (use in ALL_PLAYERS `onEnter`) |\n| `.setNextPlayer(playerId)` | Set single active player |\n| `.advanceTurn()` | Move to next player in order |\n| `.declareWinner(playerId, reason)` | Declare game winner |\n| `.endGame()` | End the game |\n\n### `apis.globalStateApi`\n\n| Method | Description |\n| ------------------------------------- | ---------------------------- |\n| `.setGlobalState(newState)` | Replace all global variables |\n| `.setPlayerState(playerId, newState)` | Replace a player\'s variables |\n\n### `apis.kvApi`\n\nInternal-only key-value store. **UI cannot access these values.**\n\n| Method | Returns | Description |\n| ------------------ | ---------------- | ----------------------------------- |\n| `.set(key, value)` | `KvSetResult` | Store a JSON value |\n| `.get(key)` | `KvGetResult` | Read a value (`.success`, `.value`) |\n| `.delete(key)` | `KvDeleteResult` | Remove a key |\n| `.has(key)` | `boolean` | Check existence |\n| `.keys()` | `string[]` | List all keys |\n\n### `apis.resourceApi`\n\n| Method | Description |\n| ------------------------- | -------------------------- |\n| `.add(playerId, cost)` | Give resources to player |\n| `.deduct(playerId, cost)` | Take resources from player |\n\n### `apis.dieApi`\n\n| Method | Description |\n| -------------------------- | ------------------------- |\n| `.roll(dieId)` | Roll a die |\n| `.setValue(dieId, value?)` | Set die to specific value |\n\n## Typed KV Store\n\nUse `createTypedKv` from `sdk/stateApi.js` for type-safe KV access:\n\n```typescript\nimport { createTypedKv } from "../sdk/stateApi.js";\n\ninterface MyKv {\n playersActed: PlayerId[];\n roundData: { scores: number[] };\n}\n\nconst kv = createTypedKv<MyKv>(apis.kvApi);\nkv.set("playersActed", ["player-1"]); // Type-checked key and value\nconst acted = kv.get("playersActed"); // PlayerId[] | null\nkv.has("playersActed"); // boolean\nkv.delete("playersActed"); // boolean (existed)\n```\n\n## Card `playedBy` Tracking\n\nWhen cards move from a Hand to a Deck via `moveCardsFromHandToDeck()`, the engine automatically records `playedBy` on each card:\n\n```typescript\n// Move card from player\'s hand to a shared deck\napis.cardApi.moveCardsFromHandToDeck(\n playerId,\n "main-hand",\n [cardId],\n "play-area",\n);\n\n// Later, check who played a specific card\nconst whoPlayed = state.card.getPlayedBy(cardId); // PlayerId | null\n```\n\n## Location Type\n\nCards have a location discriminated union:\n\n```typescript\ntype Location =\n | { type: "Detached" }\n | {\n type: "InDeck";\n deckId: DeckId;\n playedBy: PlayerId | null;\n position: number | null;\n }\n | {\n type: "InHand";\n handId: HandId;\n playerId: PlayerId;\n position: number | null;\n }\n | {\n type: "InZone";\n zoneId: string;\n playedBy: PlayerId | null;\n position: number | null;\n };\n```\n\n## Validation Helpers\n\n```typescript\nimport { validationSuccess, validationError } from "../sdk/validation.js";\n\n// Valid action\nreturn validationSuccess();\n\n// Invalid action \u2014 errorCode should be kebab-case\nreturn validationError(\n "must-play-valid-combination",\n "You must play a valid card combination",\n);\n```\n',
|
|
1938
|
+
"references/app-best-practices.md": '# App Best Practices\n\nBest practices for working on `app/phases/*.ts` \u2014 the server-side game logic.\n\n## Before writing any code\n\n1. **Read the manifest** \u2014 understand all decks, hands, zones, resources, state machine states, and card types from `shared/manifest.d.ts`.\n2. **Read all existing phases** \u2014 read every file in `app/phases/` to understand the current flow.\n3. **Read the types** \u2014 review `app/sdk/types.d.ts`, `app/sdk/phaseHandlers.ts`, and `app/sdk/state.d.ts` for the full API surface.\n\n## Core principles\n\n1. **Authoritative script** \u2014 the app script is the source of truth for a multiplayer game. All mutations must go through APIs; never assume the UI tracks state.\n2. **Optimistic results** \u2014 use the data returned by mutation APIs within the same transaction. `state.*` reads may be stale until the next call.\n3. **Active vs next players** \u2014 use `setActivePlayers()` for simultaneous phases (ALL_PLAYERS); use `setNextPlayer()` for turn-based phases (SINGLE_PLAYER).\n4. **Card movement guardrail** \u2014 every card location change must use a move API. Never assume the UI or staging moved cards.\n5. **Manifest alignment** \u2014 use deck/hand/zone IDs exactly as defined in `shared/manifest.d.ts`. Do not invent new identifiers.\n6. **KV is internal** \u2014 `kvApi` is invisible to the UI. Pass data to UI via `getUIArgs()`, `globalStateApi`, or `playerStateApi`.\n7. **Preserve player-visible outcomes** \u2014 if an AUTO phase resolves a round or a reveal, keep the round-result information in state long enough for the next player-visible phase to render it.\n\n## Validation\n\n- Always implement `validateAction` for SINGLE_PLAYER and ALL_PLAYERS phases.\n- Return `validationSuccess()` for valid actions.\n- Return `validationError(code, message)` for invalid actions. Codes should be kebab-case (e.g., `\'must-play-valid-combination\'`).\n\n## UIArgs contract\n\n- Define UIArgs interfaces in `shared/ui-args.ts` to specify what data flows from app to UI.\n- Implement `getUIArgs(ctx, playerId)` in each player-facing phase to return the data the UI needs.\n- UIArgs should be minimal \u2014 only pass what the UI actually uses.\n- Avoid passing raw card IDs when you can pass computed values (e.g., `canPlayCard: boolean` instead of making the UI recompute).\n\n## Array and type safety\n\n- Check array length before indexing. Never assume arrays are non-empty.\n- Use guards from `app/generated/guards.ts` via `../generated/guards` for ID type assertions.\n- Never cast to `any` \u2014 always work with the generated types.\n\n## Logging\n\n- Log key actions with concise payloads (IDs and counts), not full arrays.\n- Use `ctx.logger` for structured logging.\n\n## Common patterns\n\n### Typed KV store\n\n```typescript\nimport { createTypedKv } from "../sdk/stateApi.js";\n\ninterface PhaseKv {\n playersActed: PlayerId[];\n roundNumber: number;\n}\n\nconst kv = createTypedKv<PhaseKv>(apis.kvApi);\nkv.set("playersActed", []);\nconst acted = kv.get("playersActed"); // PlayerId[] | null\n```\n\n### Card playedBy tracking\n\n```typescript\n// Cards moved via moveCardsFromHandToDeck automatically track playedBy\napis.cardApi.moveCardsFromHandToDeck(\n playerId,\n "main-hand",\n [cardId],\n "play-area",\n);\nconst whoPlayed = state.card.getPlayedBy(cardId); // PlayerId | null\n```\n\n### Turn advancement\n\n```typescript\n// In onAfterAction for SINGLE_PLAYER phases\nonAfterAction(ctx, playerId, actionType) {\n ctx.apis.gameApi.advanceTurn();\n},\n```\n\n### ALL_PLAYERS tracking\n\nSee [all-players-tracking.md](all-players-tracking.md) for the recommended `ctx.phase` tracking pattern.\n',
|
|
1939
|
+
"references/hands-vs-decks.md": '# Hands vs Decks\n\nThe framework distinguishes between **Hands** and **Decks**. Confusing them causes silent bugs.\n\n## Definitions\n\n| Concept | Manifest key | Type ID | Access API | Description |\n| -------- | ----------------------------- | -------- | ---------------------------------------- | ------------------------------------------------------------ |\n| **Hand** | `playerHandDefinitions` | `HandId` | `state.player.getHand(playerId, handId)` | Cards owned by a specific player. Per-player. |\n| **Deck** | `components` (type: `"deck"`) | `DeckId` | `state.deck.getCards(deckId)` | Shared card piles (draw pile, discard, battle zone). Global. |\n\n## Key rule\n\nA player\'s collection of cards is always a **Hand** (`HandId`), even if the real-world game calls it a "deck."\n\nFor example, in the War card game, each player\'s face-down pile is called a "deck" in real life, but in the framework it\'s defined as a **Hand** with id `"player-deck"` under `playerHandDefinitions`.\n\n## Manifest Example (War card game)\n\n```json\n{\n "components": [\n { "type": "deck", "id": "main-deck", "name": "Main Deck" },\n { "type": "deck", "id": "battle-zone", "name": "Battle Zone" },\n { "type": "deck", "id": "war-pile", "name": "War Pile" }\n ],\n "playerHandDefinitions": [\n {\n "id": "player-deck",\n "displayName": "Your Deck",\n "visibility": "ownerOnly"\n },\n { "id": "won-pile", "displayName": "Won Cards", "visibility": "ownerOnly" }\n ]\n}\n```\n\n- `"player-deck"` \u2192 **HandId** (per-player) \u2192 `state.player.getHand(playerId, "player-deck")`\n- `"battle-zone"` \u2192 **DeckId** (shared) \u2192 `state.deck.getCards("battle-zone")`\n- `"war-pile"` \u2192 **DeckId** (shared) \u2192 `state.deck.getCards("war-pile")`\n\n## Moving Cards\n\n```typescript\n// Hand \u2192 Deck (player plays card to shared area)\napis.cardApi.moveCardsFromHandToDeck(playerId, handId, cardIds, deckId);\n\n// Deck \u2192 Hand (deal from shared pile to player)\napis.deckApi.moveCardsFromDeckToPlayer(deckId, playerId, handId, count);\n\n// Hand \u2192 Hand (pass cards between players)\napis.cardApi.moveCardsFromHandToHand(\n fromPlayer,\n fromHand,\n toPlayer,\n toHand,\n cardIds,\n);\n\n// Deck \u2192 Deck (move all cards between shared piles)\napis.deckApi.moveCardsFromDeckToDeck(fromDeckId, toDeckId);\n\n// Any card \u2192 Hand (move specific cards to a player)\napis.cardApi.moveCardsToPlayer(cardIds, toPlayerId, handId);\n```\n\n## Hand Passing: Anti-Pattern vs Safe Pattern\n\n`moveCardsFromHandToHand` appends to destination. That makes in-place cyclic passing unsafe.\n\n### Anti-pattern (in-place cyclic pass)\n\n```typescript\nfor (let i = 0; i < order.length; i++) {\n const fromPlayer = order[i];\n const toPlayer = order[(i + 1) % order.length];\n const cardIds = state.player.getHand(fromPlayer, "hand");\n apis.cardApi.moveCardsFromHandToHand(\n fromPlayer,\n "hand",\n toPlayer,\n "hand",\n cardIds,\n );\n}\n```\n',
|
|
1940
|
+
"references/manifest-authoring.md": '# Manifest Authoring Guide\n\n`manifest.json` is the source of truth for your game\'s structure \u2014 components, actions, state machine, and variables. After editing, run `dreamboard update` to regenerate scaffolded files (`app/phases/`, `shared/manifest.d.ts`, etc.).\n\n## Top-Level Structure\n\n```json\n{\n "version": "1.0.0",\n "playerConfig": { ... },\n "deckDefinitions": [ ... ],\n "playerHandDefinitions": [ ... ],\n "components": [ ... ],\n "resources": [ ... ],\n "boardDefinitions": [ ... ],\n "availableActions": [ ... ],\n "stateMachine": { ... },\n "variableSchema": { ... }\n}\n```\n\n| Field | Required | Description |\n| ----------------------- | -------- | -------------------------------------------------- |\n| `version` | \u2705 | Manifest version - increment manually when updated |\n| `playerConfig` | \u2705 | Min/max/optimal player counts |\n| `deckDefinitions` | \u2705 | Card deck blueprints (preset or manual) |\n| `playerHandDefinitions` | \u2705 | Per-player card containers |\n| `components` | \u2705 | Shared game components (decks, dice) |\n| `resources` | \u274C | Typed resource economy (gold, wood) |\n| `boardDefinitions` | \u274C | Spatial boards (hex, network, square, track) |\n| `availableActions` | \u2705 | Player submission buttons |\n| `stateMachine` | \u2705 | Game phases and transitions |\n| `variableSchema` | \u2705 | Minimal state for game logic |\n\n---\n\n## Authoring Sequence\n\nWork through sections in this order. Skip any step that doesn\'t apply to your game.\n\n### 1. `playerConfig`\n\n```json\n{\n "playerConfig": {\n "minPlayers": 2,\n "maxPlayers": 4,\n "optimalPlayers": 4\n }\n}\n```\n\n| Field | Type | Range | Description |\n| ---------------- | ------- | ----- | ------------------------------------ |\n| `minPlayers` | integer | 1\u201310 | Minimum players required |\n| `maxPlayers` | integer | 1\u201310 | Maximum players supported |\n| `optimalPlayers` | integer | 1\u201310 | Best player count for the experience |\n\n### 2. `deckDefinitions`\n\nDeck definitions are blueprints for card types. There are two kinds:\n\n#### Preset decks\n\nUse `"standard_52_deck"` for standard playing cards. **Do NOT define 52 cards manually.**\n\n```json\n{\n "deckDefinitions": [\n {\n "type": "preset",\n "id": "standard_52_deck",\n "name": "Standard 52-Card Deck"\n }\n ]\n}\n```\n\n#### Manual (custom) decks\n\nDefine your own cards with a `cardSchema` and a `cards` list.\n\n```json\n{\n "type": "manual",\n "id": "resource-deck",\n "name": "Resource Cards",\n "cardSchema": {\n "properties": {\n "value": { "type": "integer", "description": "Point value of the card" },\n "category": { "type": "string", "description": "Resource category" }\n }\n },\n "cards": [\n {\n "type": "lumber",\n "name": "Lumber",\n "count": 4,\n "properties": { "value": "1", "category": "wood" }\n },\n {\n "type": "brick",\n "name": "Brick",\n "count": 3,\n "properties": { "value": "2", "category": "stone" }\n }\n ]\n}\n```\n\n**Card fields:**\n\n| Field | Required | Description |\n| ------------ | -------- | ---------------------------------------------------------------------------------------- |\n| `type` | \u2705 | Card type ID. Runtime IDs are generated as `{type}-1`, `{type}-2`, etc. when `count > 1` |\n| `name` | \u2705 | Display name |\n| `count` | \u2705 | Number of copies (\u2265 1) |\n| `properties` | \u2705 | Values matching `cardSchema` (all values are strings) |\n| `imageUrl` | \u274C | Card image URL |\n| `text` | \u274C | Text content on the card |\n| `cardType` | \u274C | Optional category within the deck |\n\n**Property schema types:** `string`, `integer`, `number`, `boolean`, `array`, `object`, `enum`, `deckId`, `cardId`, `playerId`\n\n### 3. `playerHandDefinitions`\n\nPer-player card containers. Each player gets their own instance automatically.\n\n```json\n{\n "playerHandDefinitions": [\n {\n "id": "main-hand",\n "displayName": "Hand",\n "visibility": "ownerOnly",\n "maxCards": 7,\n "deckDefinitionIds": ["standard_52_deck"]\n },\n {\n "id": "score-pile",\n "displayName": "Scored Cards",\n "visibility": "public"\n }\n ]\n}\n```\n\n| Field | Required | Default | Description |\n| ------------------- | -------- | ------------- | ----------------------------------------------------------- |\n| `id` | \u2705 | \u2014 | Unique hand ID (becomes `HandId` type) |\n| `displayName` | \u2705 | \u2014 | UI label |\n| `visibility` | \u274C | `"ownerOnly"` | `"ownerOnly"`, `"public"`, or `"hidden"` |\n| `maxCards` | \u274C | \u2014 | Maximum cards allowed |\n| `minCards` | \u274C | \u2014 | Minimum cards required |\n| `deckDefinitionIds` | \u274C | \u2014 | Restrict to cards from these deck definitions (empty = any) |\n| `description` | \u274C | \u2014 | Purpose description |\n\n> **DECK vs HAND:** `DeckComponent` (in `components`) is **shared** \u2014 one instance per game (draw piles, discard piles, trick zones). `PlayerHandDefinition` is **per-player** \u2014 private hands, tableaus, collected cards.\n\n### 4. `components` \u2014 Shared Decks\n\nDeck components are shared game zones that reference a deck definition.\n\n```json\n{\n "components": [\n {\n "type": "deck",\n "id": "draw-pile",\n "name": "Draw Pile",\n "deckDefinitionId": "standard_52_deck",\n "layout": "stack"\n },\n {\n "type": "deck",\n "id": "discard-pile",\n "name": "Discard Pile",\n "deckDefinitionId": "standard_52_deck",\n "layout": "spread"\n }\n ]\n}\n```\n\n| Field | Required | Default | Description |\n| ------------------ | -------- | --------- | --------------------------------------- |\n| `type` | \u2705 | \u2014 | `"deck"` |\n| `id` | \u2705 | \u2014 | Unique component ID |\n| `name` | \u2705 | \u2014 | Display name |\n| `deckDefinitionId` | \u2705 | \u2014 | Which deck definition this sources from |\n| `layout` | \u274C | `"stack"` | `"stack"`, `"spread"`, or `"fan"` |\n\n### 5. `components` \u2014 Dice\n\nAdd dice as components with `type: "die"`.\n\n```json\n{\n "type": "die",\n "id": "d6-die",\n "name": "Six-Sided Die",\n "sides": 6\n}\n```\n\n| Field | Required | Description |\n| ------- | -------- | --------------------------------------------- |\n| `type` | \u2705 | `"die"` |\n| `id` | \u2705 | Unique die ID (e.g., `"d6-die"`, `"d20-die"`) |\n| `name` | \u2705 | Display name |\n| `sides` | \u2705 | Number of sides (\u2265 2) |\n\n### 6. `resources` (Optional)\n\nTyped resource economy for games with currencies or materials. Resources have a dedicated API (`canAfford`, `deduct`, `add`, `transfer`).\n\n```json\n{\n "resources": [\n { "id": "gold", "name": "Gold" },\n { "id": "wood", "name": "Wood" },\n { "id": "victoryPoints", "name": "Victory Points" }\n ]\n}\n```\n\n| Field | Required | Description |\n| ------ | -------- | ------------------------------------------------------------------ |\n| `id` | \u2705 | Unique resource ID (alphanumeric + underscore, starts with letter) |\n| `name` | \u2705 | Display name |\n\n> **Don\'t duplicate resources in `playerVariableSchema`.** Use the resource system instead.\n\n### 7. `boardDefinitions` (Optional)\n\nFor games with spatial structure. Each board type has its own shape:\n\n| Board Type | Use Case | Key Concepts |\n| ---------- | ------------------------------------- | ------------------------------------------------------------------------------------------------- |\n| `hex` | Hexagonal grids (Catan) | Tiles (pre-defined with IDs/types), Edges `[TileId, TileId]`, Vertices `[TileId, TileId, TileId]` |\n| `network` | Graph maps (Ticket to Ride, Pandemic) | Nodes (locations using TileId), Edges `[TileId, TileId]` |\n| `square` | Grid boards (Chess, Checkers) | Cells derived from row/col (e.g., `"a1"` to `"h8"`), Pieces placed on cells |\n| `track` | Path boards (Monopoly, VP track) | Sequential spaces with IDs, pieces on spaces |\n\n### 8. `availableActions`\n\nActions are **submissions** (buttons), **NOT selections**. Card/tile selection happens via UI clicks \u2014 the action definition declares the parameters that carry the selected items in the POST request.\n\n```json\n{\n "availableActions": [\n {\n "actionType": "playCard",\n "displayName": "Play Card",\n "description": "Play the selected card from your hand",\n "parameters": [\n {\n "name": "cardId",\n "type": "cardId",\n "required": true,\n "array": false,\n "deckDefinitionId": "standard_52_deck"\n }\n ],\n "errorCodes": ["NOT_YOUR_TURN", "INVALID_CARD", "MUST_FOLLOW_SUIT"]\n },\n {\n "actionType": "pass",\n "displayName": "Pass",\n "parameters": [],\n "errorCodes": ["CANNOT_PASS"]\n },\n {\n "actionType": "discardCards",\n "displayName": "Discard",\n "parameters": [\n {\n "name": "cardIds",\n "type": "cardId",\n "required": true,\n "array": true,\n "minLength": 1,\n "maxLength": 3,\n "deckDefinitionId": "standard_52_deck"\n }\n ],\n "errorCodes": ["WRONG_COUNT", "CARD_NOT_IN_HAND"]\n }\n ]\n}\n```\n\n**ActionDefinition fields:**\n\n| Field | Required | Description |\n| ------------- | -------- | ------------------------------------------------------------------------- |\n| `actionType` | \u2705 | Unique ID in camelCase (e.g., `"playCard"`, `"rollDice"`) |\n| `displayName` | \u2705 | Button label |\n| `description` | \u274C | Help text |\n| `parameters` | \u2705 | List of parameters (can be empty for parameterless actions like `"pass"`) |\n| `errorCodes` | \u274C | Possible validation error codes |\n\n**ActionParameterDefinition fields:**\n\n| Field | Required | Default | Description |\n| ------------------ | -------- | ------- | ----------------------------------------------------- |\n| `name` | \u2705 | \u2014 | Parameter name |\n| `type` | \u2705 | \u2014 | See parameter types below |\n| `required` | \u274C | `true` | Whether required |\n| `array` | \u274C | `false` | Set `true` when multiple values can be sent |\n| `minLength` | \u274C | \u2014 | Min items (only when `array: true`) |\n| `maxLength` | \u274C | \u2014 | Max items (only when `array: true`) |\n| `deckDefinitionId` | \u274C | \u2014 | Links `"cardId"` params to a specific deck definition |\n| `description` | \u274C | \u2014 | Help text |\n\n**Parameter types:**\n\n| Type | Description |\n| -------------- | ----------------------------------------------------------- |\n| `"cardId"` | Runtime card instance ID (e.g., `"lumber-1"`, `"lumber-2"`) |\n| `"cardType"` | Manifest-level card type identifier (e.g., `"lumber"`) |\n| `"deckId"` | Deck component ID |\n| `"playerId"` | Player ID |\n| `"string"` | Free-form string |\n| `"number"` | Numeric value |\n| `"boolean"` | Boolean value |\n| `"tileId"` | Hex tile or network node ID |\n| `"edgeId"` | Edge between tiles/nodes |\n| `"vertexId"` | Vertex between tiles |\n| `"spaceId"` | Track board space ID |\n| `"pieceId"` | Board piece ID |\n| `"zoneId"` | Zone ID |\n| `"tokenId"` | Token ID |\n| `"resourceId"` | Resource ID |\n\n**Key rules:**\n\n- **NO `"selectCard"` actions.** UI clicks are not actions.\n- If an action involves cards, **always include a `cardId` parameter** with the correct `deckDefinitionId`. Use `array: true` when multiple cards can be sent.\n- Never use placeholder string params or empty parameter lists when the action consumes card data.\n- Use `"cardId"` for specific card instances, `"cardType"` for card categories.\n- Use camelCase for `actionType` names.\n- Always use the specific type (e.g. deckId, cardId, playerId, etc) to narrow the parameter type instead of string where appropriate.\n\n### 9. `stateMachine`\n\nDefine game phases and transitions.\n\n```json\n{\n "stateMachine": {\n "initialState": "dealCards",\n "states": [\n {\n "name": "dealCards",\n "type": "AUTO",\n "description": "Shuffle the deck and deal 7 cards to each player.",\n "transitions": [{ "targetState": "playCard" }],\n "autoAdvance": true\n },\n {\n "name": "playCard",\n "type": "SINGLE_PLAYER",\n "description": "Active player must play a valid card or draw from the pile.",\n "availableActions": ["playCard", "drawCard"],\n "transitions": [\n { "targetState": "playCard", "description": "Next player\'s turn" },\n {\n "targetState": "gameOver",\n "description": "Player has no cards left"\n }\n ],\n "autoAdvance": true\n },\n {\n "name": "gameOver",\n "type": "AUTO",\n "description": "Calculate final scores and determine the winner.",\n "transitions": []\n }\n ]\n }\n}\n```\n\n**State types:**\n\n| Type | Description | Example |\n| --------------- | ----------------------------------------------------------- | ------------------------------------------- |\n| `SINGLE_PLAYER` | Engine waits for **one** player to act, then auto-advances | Chess turns, Poker betting |\n| `ALL_PLAYERS` | Engine waits for **all** players to submit before advancing | Rock-Paper-Scissors, 7 Wonders card passing |\n| `AUTO` | No player interaction \u2014 executes immediately | Dealing, scoring, state checks |\n\n**StateDefinition fields:**\n\n| Field | Required | Default | Description |\n| ------------------ | -------- | ------- | ------------------------------------------------------------------------------- |\n| `name` | \u2705 | \u2014 | Unique state name in camelCase (use verbNoun format: `dealCards`, `playTurn`) |\n| `type` | \u2705 | \u2014 | `AUTO`, `SINGLE_PLAYER`, or `ALL_PLAYERS` |\n| `description` | \u2705 | \u2014 | Full logic description \u2014 what happens, what players can do |\n| `availableActions` | \u274C | \u2014 | Action types available in this state (only for `SINGLE_PLAYER` / `ALL_PLAYERS`) |\n| `transitions` | \u2705 | \u2014 | List of possible next states |\n| `autoAdvance` | \u274C | `true` | Whether to auto-advance when complete |\n\n**StateTransition fields:**\n\n| Field | Required | Description |\n| ------------- | -------- | ------------------------------- |\n| `targetState` | \u2705 | Name of the next state |\n| `description` | \u274C | When/why this transition occurs |\n\n### 10. `variableSchema`\n\nMinimal state for game logic. Split into global (shared) and per-player variables.\n\n```json\n{\n "variableSchema": {\n "globalVariableSchema": {\n "properties": {\n "currentRound": {\n "type": "integer",\n "description": "Current round number"\n },\n "trumpSuit": { "type": "string", "description": "Current trump suit" }\n }\n },\n "playerVariableSchema": {\n "properties": {\n "score": { "type": "integer", "description": "Player\'s current score" },\n "hasPassed": {\n "type": "boolean",\n "description": "Whether player has passed this round"\n }\n }\n }\n }\n}\n```\n\n**Rules for variables:**\n\n- **MINIMIZE state.** Only include what\'s needed for rules and logic.\n- \u2705 **Include:** scores, flags (`hasPassed`), logic blockers (`lastPlayedCards`), trump suit, round counters\n- \u274C **Exclude:** derivable data (hand sizes, deck sizes, current player \u2014 the engine tracks these)\n- \u274C **Don\'t duplicate resources** \u2014 use the `resources` section instead of player variables for economies\n- Use `globalVariableSchema` for shared/global state (turn counter, current round)\n- Use `playerVariableSchema` for per-player state (scores, flags)\n\n**Property types:** `string`, `integer`, `number`, `boolean`, `array` (with `items`), `object` (with `properties`), `enum` (with `enums` list), `deckId`, `cardId`, `playerId`\n\n---\n\n## ID Naming Conventions\n\n- Use **human-readable, kebab-case IDs** for components and hands: `"draw-pile"`, `"main-hand"`, `"d6-die"`\n- Use **camelCase** for state names and action types: `"dealCards"`, `"playCard"`, `"rollDice"`\n- Use **camelCase** for variable names: `"currentRound"`, `"hasPassed"`, `"trumpSuit"`\n- Resource IDs: alphanumeric + underscore, starting with a letter: `"gold"`, `"victoryPoints"`\n\n---\n\n## Minimal Example\n\nA simple draw-and-play card game for 2\u20134 players:\n\n```json\n{\n "version": "1.0.0",\n "playerConfig": {\n "minPlayers": 2,\n "maxPlayers": 4,\n "optimalPlayers": 3\n },\n "deckDefinitions": [\n {\n "type": "preset",\n "id": "standard_52_deck",\n "name": "Standard 52-Card Deck"\n }\n ],\n "playerHandDefinitions": [\n {\n "id": "main-hand",\n "displayName": "Hand",\n "visibility": "ownerOnly",\n "maxCards": 7,\n "deckDefinitionIds": ["standard_52_deck"]\n }\n ],\n "components": [\n {\n "type": "deck",\n "id": "draw-pile",\n "name": "Draw Pile",\n "deckDefinitionId": "standard_52_deck",\n "layout": "stack"\n },\n {\n "type": "deck",\n "id": "discard-pile",\n "name": "Discard Pile",\n "deckDefinitionId": "standard_52_deck",\n "layout": "spread"\n }\n ],\n "availableActions": [\n {\n "actionType": "playCard",\n "displayName": "Play Card",\n "description": "Play a card from your hand to the discard pile",\n "parameters": [\n {\n "name": "cardId",\n "type": "cardId",\n "required": true,\n "deckDefinitionId": "standard_52_deck"\n }\n ],\n "errorCodes": ["NOT_YOUR_TURN", "INVALID_PLAY"]\n },\n {\n "actionType": "drawCard",\n "displayName": "Draw Card",\n "description": "Draw a card from the draw pile",\n "parameters": [],\n "errorCodes": ["HAND_FULL", "DECK_EMPTY"]\n }\n ],\n "stateMachine": {\n "initialState": "dealCards",\n "states": [\n {\n "name": "dealCards",\n "type": "AUTO",\n "description": "Shuffle the deck and deal 5 cards to each player. Place remaining cards face-down as the draw pile. Flip the top card to start the discard pile.",\n "transitions": [{ "targetState": "playTurn" }]\n },\n {\n "name": "playTurn",\n "type": "SINGLE_PLAYER",\n "description": "Active player must play a matching card from their hand or draw from the draw pile. A card matches if it shares the same suit or rank as the top discard.",\n "availableActions": ["playCard", "drawCard"],\n "transitions": [\n { "targetState": "playTurn", "description": "Next player\'s turn" },\n {\n "targetState": "endRound",\n "description": "Player empties their hand"\n }\n ]\n },\n {\n "name": "endRound",\n "type": "AUTO",\n "description": "The player who emptied their hand wins. Calculate scores based on cards remaining in other players\' hands.",\n "transitions": []\n }\n ]\n },\n "variableSchema": {\n "globalVariableSchema": {\n "properties": {}\n },\n "playerVariableSchema": {\n "properties": {\n "score": {\n "type": "integer",\n "description": "Accumulated score across rounds"\n }\n }\n }\n }\n}\n```\n\n---\n\n## After Editing\n\nRun `dreamboard update` to push the manifest and regenerate scaffolded files:\n\n```bash\ndreamboard update\n```\n\nThis regenerates:\n\n- `app/phases/` \u2014 One handler file per state in the state machine\n- `shared/manifest.d.ts` \u2014 TypeScript type definitions derived from the manifest\n- Action handler stubs and variable type definitions\n',
|
|
1941
|
+
"references/phase-handlers.md": '# Phase Handlers\n\nPhase handlers are the core abstraction for implementing game logic. Each state in the manifest\'s `stateMachine` maps to a phase handler file in `app/phases/`.\n\n## Phase Types\n\n| Type | File Import | When to use |\n| --------------- | --------------------------- | ------------------------------------------------------------ |\n| `AUTO` | `createAutoPhase` | No player input needed (deal, resolve, score) |\n| `SINGLE_PLAYER` | `createSinglePlayerPhase` | One player acts at a time (play a card, roll dice) |\n| `ALL_PLAYERS` | `createMultiplePlayerPhase` | All players act simultaneously (reveal, pass cards, discard) |\n\n## Phase Lifecycle\n\n**AUTO**: `execute(ctx)` \u2192 returns next state name\n\n**SINGLE_PLAYER / ALL_PLAYERS**:\n\n1. `onEnter(ctx)` \u2014 initialize phase state, set active players\n2. `validateAction(ctx, playerId, actionType, parameters)` \u2014 validate before execution\n3. `onPlayerAction(ctx, playerId, actionType, parameters)` \u2014 process the action (mutate state)\n4. `onAfterAction(ctx, playerId, actionType)` \u2014 per-action side effects (e.g., advance turn)\n5. `checkCompletion(ctx)` \u2014 return `null` if not done, or the next state name if done\n6. `onComplete(ctx)` \u2014 cleanup after transition decision\n\n## PhaseContext (`ctx`)\n\n```typescript\ninterface PhaseContext {\n readonly state: StateApi; // Read game state\n readonly apis: GameApis; // Mutate game state\n readonly logger: Logger; // Log messages\n}\n```\n\n## AUTO Phase Example\n\n```typescript\nimport type { AutoPhaseDefinition } from "../sdk/phaseHandlers";\nimport { createAutoPhase } from "../sdk/phaseHandlers";\n\nconst phaseDefinition = {\n execute(ctx) {\n const { state, apis, logger } = ctx;\n apis.deckApi.shuffle("main-deck");\n for (const playerId of state.player.getOrder()) {\n apis.deckApi.moveCardsFromDeckToPlayer(\n "main-deck",\n playerId,\n "main-hand",\n 5,\n );\n }\n return "playPhase";\n },\n} satisfies AutoPhaseDefinition<"dealCards">;\n\nexport const phase = createAutoPhase(phaseDefinition);\n```\n\n## SINGLE_PLAYER Phase Example\n\n```typescript\nimport type { SinglePlayerPhaseDefinition } from "../sdk/phaseHandlers";\nimport { createSinglePlayerPhase } from "../sdk/phaseHandlers";\nimport { validationSuccess, validationError } from "../sdk/validation.js";\n\nconst phaseDefinition = {\n onEnter(ctx) {\n const { state, apis } = ctx;\n const firstPlayer = state.player.getOrder()[0];\n apis.gameApi.setNextPlayer(firstPlayer);\n },\n\n validateAction(ctx, playerId, actionType, parameters) {\n return validationSuccess();\n },\n\n onPlayerAction(ctx, playerId, actionType, parameters) {\n const { apis } = ctx;\n // Process action...\n },\n\n onAfterAction(ctx, playerId, actionType) {\n const { apis } = ctx;\n apis.gameApi.advanceTurn(); // Move to next player\n },\n\n checkCompletion(ctx) {\n // Return null to keep playing, or next state name to transition\n return null;\n },\n\n getUIArgs(ctx, playerId) {\n return {};\n },\n} satisfies SinglePlayerPhaseDefinition<"playPhase", "playCard">;\n\nexport const phase = createSinglePlayerPhase(phaseDefinition);\n```\n\n## ALL_PLAYERS Phase Example\n\nSee [all-players-tracking.md](all-players-tracking.md) for the recommended tracking pattern.\n\n## Terminal Phase (Game End)\n\nFor states with no transitions (game end), use `TerminalPhaseDefinition`:\n\n```typescript\nimport type { TerminalPhaseDefinition } from "../sdk/phaseHandlers";\nimport { createTerminalPhase } from "../sdk/phaseHandlers";\n\nconst phaseDefinition = {\n execute(ctx) {\n const { state, apis } = ctx;\n // Optionally declare winner\n apis.gameApi.declareWinner("player-1", "Most points");\n apis.gameApi.endGame();\n return "endGame";\n },\n} satisfies TerminalPhaseDefinition<"endGame">;\n\nexport const phase = createTerminalPhase(phaseDefinition);\n```\n\n## Type Safety\n\nPhase definitions use `satisfies` for type checking:\n\n- `AutoPhaseDefinition<\'stateName\'>` \u2014 state name from manifest\n- `SinglePlayerPhaseDefinition<\'stateName\', \'actionName\'>` \u2014 with action type union\n- `MultiplePlayerPhaseDefinition<\'stateName\', \'actionName\'>` \u2014 for ALL_PLAYERS\n- Union multiple actions: `\'playCard\' | \'drawCard\'`\n',
|
|
1942
|
+
"references/rule-authoring.md": '# Rule Authoring Guide\n\n`rule.md` is the source of truth for game intent. A strong rule document leads to better manifest generation, better phase scaffolding, and fewer logic bugs.\n\n## Recommended Document Structure\n\nUse this section order in `rule.md`.\n\n1. **Overview**\n\n - Game premise and objective.\n - Player count (min/max/optimal).\n - Match length target (for example 15-25 minutes).\n\n2. **Components**\n\n - Cards, decks, hands, tokens, dice, boards, resources.\n - Public vs hidden information.\n - Limits (deck sizes, hand limits, token caps).\n\n3. **Setup**\n\n - Initial board/deck/resource setup.\n - Starting player and initial turn order.\n - Initial cards/resources per player.\n\n4. **Gameplay**\n\n - Turn or phase loop in order.\n - Available actions per phase.\n - Validation constraints for actions.\n - Transition conditions between phases.\n\n5. **Scoring and Progression**\n\n - How points/resources/progress are gained or lost.\n - When scoring occurs (per action, end of round, end of game).\n - Tie-breakers.\n\n6. **Winning Conditions**\n\n - Exact end-game triggers.\n - Winner resolution rules, including ties.\n\n7. **Special Rules and Edge Cases**\n - Simultaneous actions.\n - Empty deck behavior.\n - Invalid/no-op action handling.\n - What happens when a player cannot act.\n\n## Authoring Rules That Map Cleanly to Engine Concepts\n\nWrite rules in a way that maps directly to Dreamboard systems:\n\n- **Phase model**\n\n - State whether a phase is automatic, single-player, or all-players.\n - State completion criteria for each phase.\n\n- **Action model**\n\n - Define each action by name, intent, and required inputs.\n - Include action constraints ("must", "cannot", min/max quantities).\n\n- **State model**\n\n - Separate persistent state from derived state.\n - Prefer explicit counters/flags over vague conditions.\n\n- **Visibility model**\n\n - Specify what each player can see at all times.\n - Explicitly call out hidden hands, private choices, and reveal timing.\n\n- **Determinism**\n - Resolve "if multiple options apply" with priority order.\n - Define random events precisely (shuffle, draw count, tie randomization).\n\n## Writing Style Requirements\n\n- Use explicit modal language:\n - Use "must" for mandatory behavior.\n - Use "may" only for optional player choices.\n- Define terms once and reuse them exactly.\n- Avoid UI instructions ("click", "drag") in rule logic.\n\n## High-Signal Template\n\nUse this template when creating or rewriting `rule.md`:\n\n```markdown\n# <Game Name>\n\n## Overview\n\n- Players: <min-max> (optimal: <n>)\n- Objective: <how to win>\n- Duration: <target minutes>\n\n## Components\n\n- Decks:\n- Player hands:\n- Shared zones:\n- Resources/tokens/dice:\n- Public vs hidden info:\n\n## Setup\n\n1. ...\n2. ...\n3. ...\n\n## Gameplay\n\n### Phase 1: <name> (<AUTO|SINGLE_PLAYER|ALL_PLAYERS>)\n\n- Entry:\n- Allowed actions:\n- Validation:\n- Completion -> Next phase:\n\n### Phase 2: <name> ...\n\n## Scoring and Progression\n\n- ...\n\n## Winning Conditions\n\n- End trigger:\n- Winner determination:\n- Tie-breaker:\n```\n\n## Iteration Workflow\n\n1. Edit `rule.md` first.\n2. Align `manifest.json` to the updated rules.\n3. Run `dreamboard update` to regenerate scaffolding.\n4. Implement/refine `app/phases/*.ts`.\n5. Validate flow with `dreamboard run`.\n',
|
|
1943
|
+
"references/scenario-format.md": '# Scenario Format\n\nScenarios are JSON files used with `dreamboard run --scenario <file>` to automate gameplay actions.\n\nUse this file for the JSON runtime-scenario contract.\n\n- For TypeScript regression tests with `dreamboard test generate` / `dreamboard test run`, see [test-harness.md](test-harness.md).\n- For what adversarial cases to write with these JSON scenarios, see [adversarial-testing.md](adversarial-testing.md).\n\n`dreamboard run` is now a single command flow:\n\n1. Without `--scenario`, it observes SSE until a stop condition (default `YOUR_TURN`).\n2. With `--scenario`, it executes scenario steps (default `--max-steps 1`).\n3. Scenario driver defaults to `api` (no Playwright required).\n4. Playwright is used only for `--scenario-driver ui` or `--screenshot`.\n\n## Default Structure (API Driver)\n\n```json\n{\n "steps": [\n {\n "playerId": "player-1",\n "actionType": "playCard",\n "parameters": { "cardId": "hearts-7" },\n "turns": 1\n },\n {\n "playerId": "player-1",\n "actionType": "endTurn",\n "parameters": {},\n "turns": 1\n }\n ]\n}\n```\n\n## API Step Fields (Default)\n\n| Field | Type | Description |\n| ------------ | ------------------------- | ----------------------------------------------------------------- |\n| `playerId` | `string` | Player ID this step applies to (required) |\n| `actionType` | `string` | Action type submitted to runtime API (required) |\n| `parameters` | `Record<string, unknown>` | Action parameters object (optional, default `{}`) |\n| `turns` | `number` | Number of turns to wait after this action (optional, default `1`) |\n\n## UI Step Fields (`--scenario-driver ui`)\n\n| Field | Type | Description |\n| ---------- | ---------- | --------------------------------------------------------------------------- |\n| `playerId` | `string` | Player ID this step applies to (required) |\n| `buttons` | `string[]` | Mouse/keyboard buttons to simulate (`"left_mouse_button"`, `"right"`, etc.) |\n| `turns` | `number` | Number of turns to apply this step |\n| `mouse_x` | `number` | X coordinate of the simulated click (optional) |\n| `mouse_y` | `number` | Y coordinate of the simulated click (optional) |\n\n## Conventions\n\n1. `playerId` is required on every step.\n2. Run one acting step per invocation for deterministic agent loops:\n - `dreamboard run` (observe turn context)\n - `dreamboard run --scenario ... --max-steps 1` (act once)\n3. All steps executed in one invocation must target the same `playerId`.\n4. The step `playerId` must be eligible for the latest `YOUR_TURN` context.\n\n## Observe Artifacts\n\nWhen running without `--scenario`, the CLI writes:\n\n1. `.dreamboard/run/session.json`\n2. `.dreamboard/run/events.ndjson`\n3. `.dreamboard/run/latest-your-turn.json`\n4. `.dreamboard/run/last-run-summary.json`\n\nBy default, `events.ndjson` stores only `YOUR_TURN` messages.\n\n## Usage\n\n```bash\n# Observe until actionable state (default: until YOUR_TURN)\ndreamboard run\n\n# Observe stream behavior\ndreamboard run --until GAME_ENDED\ndreamboard run --observe-events all\ndreamboard run --timeout-ms 15000\ndreamboard run --screenshot\n\n# Execute API scenario steps (default driver, no Playwright required)\ndreamboard run --scenario path/to/scenario.json\ndreamboard run --scenario path/to/scenario.json --max-steps 1\n\n# Execute UI scenario steps (Playwright driver)\ndreamboard run --scenario path/to/scenario.json --scenario-driver ui --max-steps 1\ndreamboard run --scenario path/to/scenario.json --scenario-driver ui --max-steps 1 --screenshot\n\n# Start a fresh session instead of resuming\ndreamboard run --new-session --players 4\n```\n',
|
|
1944
|
+
"references/test-harness.md": '# Test Harness\n\nTypeScript scenario tests run against the Dreamboard backend API using deterministic base snapshots.\n\nUse this file for the TypeScript regression harness:\n\n- `dreamboard test generate`\n- `dreamboard test run`\n\nThis is not the same system as `dreamboard run --scenario <file>.json`.\n\n- For JSON runtime scenarios, see [scenario-format.md](scenario-format.md).\n- For rejection-focused runtime coverage, see [adversarial-testing.md](adversarial-testing.md).\n\n## Workspace Layout\n\nThe harness expects this structure in the game project:\n\n- `test/base-scenarios.json`\n- `test/scenarios/*.scenario.ts`\n- `test/generated/*` (generated by `dreamboard test generate`)\n\nCurrent canonical example workspace (`examples/things-in-rings`) includes bases like `initial-turn` and `after-first-placement`, plus scenarios under `test/scenarios/`.\n\n## Commands\n\nGenerate deterministic bases:\n\n```bash\ndreamboard test generate\n```\n\nRun all scenarios:\n\n```bash\ndreamboard test run\n```\n\nRun one scenario file:\n\n```bash\ndreamboard test run --scenario test/scenarios/reject-card-not-in-hand.scenario.ts\n```\n\nSource-checkout builds may expose internal environment overrides for harness work, but those flags are not part of the published CLI contract.\n\nRegenerate whenever any fingerprint input changes: base definition (`seed`, `players`, `steps`), game manifest hash, compiled result ID, or game ID.\n\n## Base Definitions\n\n`test/base-scenarios.json` is required and must explicitly include `initial-turn`.\n\nEach base entry has:\n\n- `seed: number`\n- `players: number`\n- `steps: ApiScenarioStep[]`\n\n`steps` may be empty. Use an empty-step base plus an empty-step scenario to assert opening-state invariants without inventing fake actions.\n\nExample:\n\n```json\n{\n "initial-turn": {\n "seed": 1337,\n "players": 4,\n "steps": []\n },\n "after-first-placement": {\n "seed": 1337,\n "players": 4,\n "steps": [\n {\n "playerId": "player-2",\n "actionType": "placeThing",\n "parameters": { "cardId": "a-shadow", "ringId": "ring-1" }\n }\n ]\n }\n}\n```\n\nGeneration flow for each used base:\n\n1. Create session with deterministic seed/player count.\n2. Start game.\n3. Wait for first `YOUR_TURN`.\n4. Execute base setup steps.\n5. Save snapshot + fingerprint metadata.\n\nBase setup events are not counted in test assertions. `events` in `then.assert` only reflects actions from `when.steps`, so zero-step scenarios start with an empty event history.\n\n## Generated Artifacts\n\n`dreamboard test generate` writes:\n\n- `test/generated/base-states.generated.ts`\n- `test/generated/base-states.generated.d.ts`\n- `test/generated/scenario-manifest.generated.ts`\n- `test/generated/.generation-meta.json`\n\nThese files are generated and should not be edited manually.\n\n## Scenario Contract\n\nScenarios are loaded from `test/scenarios/**/*.scenario.ts` and validated strictly (unknown fields are rejected).\n\nIn current projects, scenario files typically import from `../testing-types`:\n\n```ts\nimport { AssertContext, defineScenario } from "../testing-types";\n\nexport default defineScenario({\n meta: {\n id: "reject-card-not-in-hand",\n description: "Player tries to place a card they don\'t hold",\n },\n given: {\n base: "initial-turn",\n },\n when: {\n steps: [\n {\n playerId: "player-2",\n actionType: "placeThing",\n parameters: { cardId: "a-dog", ringId: "ring-1" },\n },\n ],\n },\n then: {\n assert: ({ gameState, events, expect }: AssertContext) => {\n expect(events.count("ACTION_REJECTED")).toBe(1);\n expect(events.last("ACTION_REJECTED")?.errorCode).toBe(\n "CARD_NOT_IN_HAND",\n );\n expect(gameState.currentState).toBe("placeThing");\n },\n },\n});\n```\n\n`given.base` must exist in `test/base-scenarios.json`, or the run fails with an available-base list.\n\nFor invariant coverage, add at least one zero-step scenario:\n\n```ts\nimport {\n AssertContext,\n defineScenario,\n getNormalizedHands,\n} from "../testing-types";\n\nexport default defineScenario({\n meta: {\n id: "initial-state",\n description: "Opening hands are dealt to the correct players",\n },\n given: {\n base: "initial-turn",\n },\n when: {\n steps: [],\n },\n then: {\n assert: ({ gameState, expect }: AssertContext) => {\n const hands = getNormalizedHands(gameState);\n expect(hands["player-1"]?.hand).toEqual([\n "seat-1-flower-1",\n "seat-1-flower-2",\n "seat-1-flower-3",\n "seat-1-skull",\n ]);\n },\n },\n});\n```\n\n## Runtime Assertion Surface\n\n`then.assert` receives `{ gameState, events, expect }`.\n\n- `events.count(type)` returns number of matching events.\n- `events.last(type)` returns the latest matching event.\n- `events.all(type)` returns all matching events.\n- `expect(actual)` supports `toBe`, `toEqual`, `toBeDefined`, `toContain`, and `toBeGreaterThanOrEqual`.\n\nDuring scenario assertions, event history includes only scenario steps (base-step events are cleared before scenario execution).\n\n`gameState.globalVariables` and `gameState.playerVariables` are JSON-parsed for assertions.\n\n`testing-types` also exports:\n\n- `getNormalizedHands(gameState)` to read each player hand with sorted card IDs.\n- `getNormalizedDecks(gameState)` to read deck contents with sorted card IDs.\n\n## Failure And Staleness Signals\n\nCommon failures indicate stale generated bases:\n\n- `Missing test/generated/base-states.generated.ts. Run \'dreamboard test generate\' first.`\n- `Fingerprint mismatch: base setup changed (...). Run \'dreamboard test generate\' to refresh deterministic bases.`\n- `Fingerprint mismatch: compiled result changed (...). Run \'dreamboard test generate\' to refresh deterministic bases.`\n- `Fingerprint mismatch: base setup changed (...); compiled result changed (...). Run \'dreamboard test generate\' to refresh deterministic bases.`\n- Unknown `given.base` values.\n\n## Common edge cases\n\nWhen testing out-of-turn behavior in any turn-based game, include these checks:\n\n- Wrong actor attempts a valid action: choose a base where the active player is known, send a normal action from a different player, and assert a turn-violation error (for example `NOT_YOUR_TURN`).\n- Unauthorized role attempts an action: if your game has role-restricted actions, attempt the action from an actor without permission and assert the appropriate authorization error.\n- Correct actor, wrong phase/state: attempt an action that is valid in one phase/state while the game is in another, and assert a deterministic phase/state rejection.\n- Consecutive out-of-turn attempts: send multiple invalid actions from different non-active players and verify each is rejected independently.\n- No state mutation on rejection: assert no successful action event was emitted, the game phase/state is unchanged, and the active player pointer is unchanged.\n\nRecommended assertion template:\n\n```ts\nexpect(events.count("ACTION_REJECTED")).toBe(1);\nexpect(events.count("ACTION_EXECUTED")).toBe(0);\nexpect(events.last("ACTION_REJECTED")?.errorCode).toBe("NOT_YOUR_TURN");\nexpect(gameState.currentState).toBe(expectedState);\nexpect(gameState.currentPlayerIds[0]).toBe(expectedActivePlayerId);\n```\n',
|
|
1945
|
+
"references/tts-migration-and-extractor.md": "# Migrate From TTS\n\nGiven a workshop ID, full workshop URL, or location of a `.bin` file, use `extract_tts.py` to parse a TTS save into structured JSON and optionally download referenced assets.\n\n## Prereqs\n\n- Python 3\n- `steamcmd` (only needed when passing a Workshop ID instead of a local `.bin` file)\n\nVerify `steamcmd`:\n\n```sh\nsteamcmd +quit\n```\n\n### macOS\n\n```sh\nbrew install --cask steamcmd\n```\n\n### Windows\n\n1. Download `steamcmd.zip` from Valve: `https://steamcdn-a.akamaihd.net/client/installer/steamcmd.zip`\n2. Extract to a permanent folder, e.g. `C:\\steamcmd\\`\n3. Add that folder to your `PATH`\n4. Run once so it self-updates:\n\n```cmd\nsteamcmd +quit\n```\n\n## Basic Usage\n\n```sh\npython3 extract_tts.py <workshop_id> [output_dir]\npython3 extract_tts.py <workshop_url> [output_dir]\npython3 extract_tts.py <save.bin> [output_dir]\n```\n\nWorkshop ID is the numeric `id` from the Steam Workshop URL, e.g.\n`https://steamcommunity.com/sharedfiles/filedetails/?id=3385562324` -> `3385562324`.\n\n## Download Referenced Assets\n\nUse `--download` to fetch assets referenced in `asset_urls.json` and write `asset_files.json` with local relative paths.\n\n```sh\npython3 extract_tts.py <workshop_id|workshop_url|save.bin> [output_dir] --download\n```\n\nOptional tuning:\n\n```sh\npython3 extract_tts.py <workshop_id|workshop_url|save.bin> [output_dir] --download --download-workers 8 --download-rps 2.5\n```\n\n- `--download-workers`: max concurrent workers (default `6`, capped internally)\n- `--download-rps`: global request rate limit across workers (default `3.0` req/s)\n\nDownloaded files are stored under `assets/` with inferred subfolders such as `assets/pdf/`, `assets/deck/`, `assets/image/`, and `assets/mesh/`.\n\n## Recommended Flow Into Dreamboard\n\n```sh\npython3 extract_tts.py <workshop_id> [output_dir] --download\ndreamboard new <slug> ...\nmv [output_dir] <slug>/assets\n```\n\n## Output Files\n\n| File | Contents |\n| -------------------- | ----------------------------------------------------------------------------------------------- |\n| `metadata.json` | Save name, date, version, tags, table/sky URLs, play time, player counts |\n| `asset_urls.json` | Asset URLs grouped by TTS object nickname/type, with deck/card/PDF metadata |\n| `asset_urls.txt` | Flat list of every unique URL |\n| `asset_files.json` | Same shape as `asset_urls.json`, but with local relative file paths (when `--download` is used) |\n| `objects.json` | Per-object summary (GUID, name, custom deck/image/PDF info, contained objects) |\n| `scripts/` | Each unique Lua script and XML UI saved as individual files |\n| `scripts_index.json` | Maps each scripted object (GUID/path) to its script file |\n| `full_save.json` | Full parsed save tree with large script bodies replaced by placeholders |\n| `tab_states.json` | Player-colour tab contents (if present) |\n| `snap_points.json` | Table snap point positions (if present) |\n\n## How To Use The Extracted Data\n\n- Use `CustomPDF` / `PDFUrl` entries in `asset_urls.json` (or local files in `asset_files.json`) as source material for `rule.md`.\n- Use `TableURL` and `SkyURL` as references for UI tone and color direction.\n- Use `FaceURL` plus card grid metadata (`numWidth`, `numHeight`, `row`, `column`) to reconstruct decks/cards in `manifest.json`.\n- Use card metadata (`nickname`, `count`, `description`, `gmNotes`) to seed content and balancing decisions.\n",
|
|
1946
|
+
"references/ui-best-practices.md": '# UI Best Practices\n\nBest practices for working on `ui/App.tsx` and `ui/components/*` \u2014 the React frontend.\n\n## General principles\n\n1. **Use SDK components** \u2014 import from `./sdk/components/`. Prefer library components over rolling your own. Customize them via render props if needed.\n2. **Mobile-first** \u2014 mobile is the primary target, desktop is an enhancement. Start with mobile-optimized layouts, use large touch targets, and relative units (rem, em).\n3. **No state duplication** \u2014 avoid `React.useState()` for state that needs to sync across players. Use `useGameState`, `useUIArgs`, and action submitters instead.\n4. **Error handling on actions** \u2014 always wrap action submissions in try/catch and show feedback via `useToast`.\n5. **Do not use `any`** \u2014 always work with typed hooks and props.\n6. **Persistent feedback matters** \u2014 include a stable feedback region that survives auto-phase transitions so players can always see the current phase, latest status message, last round result, and game-over summary.\n\n## SDK hooks (from `./sdk/hooks/`)\n\n| Hook | Returns | When to use |\n| ---------------------- | ----------------------------------------------------------------- | ----------------------------------------------------------------------------------------------------------------------------------------------- |\n| `useGameState` | Full game state (currentPlayerIds, decks, currentState, isMyTurn) | Core game state. All fields are non-nullable \u2014 do not use optional chaining. |\n| `useCard(cardId)` | `CardItem \\| undefined` | Get a single card\'s data |\n| `useCards(cardIds)` | `CardItem[]` | Get multiple cards |\n| `useMyHand(handId)` | Hand data for current player | Player\'s cards |\n| `useMyResources()` | Current player\'s resources | Resource display |\n| `useMe` | `{ playerId, name, isHost }` | Current player identity |\n| `usePlayerInfo` | Map of all players | Player list/display |\n| `useAction` | Phase-specific action submitters | Submitting actions. Keyed by phase with "Actions" suffix (e.g., `playCardsActions`). Only current phase returns submitters; others return null. |\n| `useUIArgs` | Phase-specific UI args | Data from `getUIArgs()`. Keyed by phase name. Only current phase has data. |\n| `useGameNotifications` | Event listeners | `onYourTurn`, `onActionRejected`, `onGameEnded` |\n| `useLobby` | Lobby state | seats, canStart, hostUserId |\n| `useDice(dieIds)` | Dice values and roll function | Dice mechanics |\n\n## SDK components (from `./sdk/components/`)\n\n### Core display\n\n| Component | Purpose |\n| --------------- | -------------------------------------------------------------------------------------------- |\n| `Card` | Displays a game card with animations. Supports `renderContent` for custom faces. |\n| `ConnectedCard` | Auto-fetches card data from context. |\n| `Hand` | Container for player\'s hand. Uses render props: `renderCard`, `renderDrawer`, `renderEmpty`. |\n| `PlayArea` | Central board area for active cards. Supports grid/row layouts. |\n| `PlayerInfo` | Player avatar with status, score, and turn indicators. |\n\n### Action UI\n\n| Component | Purpose |\n| -------------- | ------------------------------------------------------------ |\n| `ActionButton` | Button with integrated cost display and affordability check. |\n| `ActionPanel` | Collapsible container for action groups. |\n| `ActionGroup` | Groups related actions with title and variant styling. |\n\n### Game state display\n\n| Component | Purpose |\n| ----------------- | ------------------------------------------------------------------- |\n| `PhaseIndicator` | Shows current phase and "Your Turn" indicator. |\n| `ResourceCounter` | Displays resource counts with icons and animations. |\n| `CostDisplay` | Shows resource costs with green/red affordability indication. |\n| `DiceRoller` | Display component for dice values with render prop. |\n| `GameEndDisplay` | End-of-game overlay with trophy, scoreboard, and return-to-lobby. |\n| `Drawer` | Mobile-friendly drawer for overflow content (e.g., too many cards). |\n\n### Board components\n\n| Component | Use case | Hook |\n| -------------- | --------------------------------------- | --------------------------------- |\n| `SquareGrid` | Chess, Checkers, Go, Tic-Tac-Toe | `useSquareBoard(boardId)` |\n| `HexGrid` | Catan, wargames, Hive | `useHexBoard(boardId)` |\n| `TrackBoard` | Monopoly, racing, Snakes & Ladders | `useTrackBoard(boardId)` |\n| `NetworkGraph` | Ticket to Ride, Pandemic, Power Grid | `useNetworkBoard(boardId)` |\n| `ZoneMap` | Risk, Small World, area control | `useZoneMap(zones, pieces)` |\n| `SlotSystem` | Agricola, Viticulture, worker placement | `useSlotSystem(slots, occupants)` |\n\n## Layout guidelines\n\n- Use Tailwind CSS for all styling.\n- Use `clsx` for conditional class composition.\n- Use `framer-motion` for animations (already included via SDK components).\n- Keep spacing tight on mobile, generous on desktop.\n- Limit simultaneous animations for performance.\n- Use `useIsMobile()` hook when you need device-specific behavior.\n\n## Persistent feedback region\n\nReserve a visible area near the top of the layout for game status. This should outlive individual action panels and phase-specific subviews.\n\n- Show the current phase and whether it is the local player\'s turn.\n- Show a human-readable status message for the latest meaningful change.\n- Keep the last round result visible until the next player-facing state can explain it.\n- Show a clear game-over summary when the game ends.\n\nIf your game uses AUTO phases, do not let those transitions wipe the only copy of round-resolution feedback before the UI can render it.\n\n## Common patterns\n\n### Phase-based rendering\n\n```tsx\nconst { currentState, isMyTurn } = useGameState();\nconst uiArgs = useUIArgs();\nconst actions = useAction();\n\n// Render based on current phase\nif (currentState === "playCards") {\n const phaseArgs = uiArgs.playCards;\n const phaseActions = actions.playCardsActions;\n return <PlayCardsView args={phaseArgs} actions={phaseActions} />;\n}\n```\n\n### Action submission with error handling\n\n```tsx\nconst { toast } = useToast();\n\nconst handlePlay = async () => {\n try {\n await actions.playCardsActions.playCard({ cardIds: selectedCards });\n } catch (err) {\n toast.error(err instanceof Error ? err.message : "Action failed");\n }\n};\n```\n\n### Game end detection\n\n```tsx\nconst { currentState } = useGameState();\n\nif (currentState === "endGame") {\n return <GameEndDisplay isGameOver scores={scores} />;\n}\n```\n\n## Public hands in UI\n\nFor public card zones (for example a scored area), prefer the built-in hook instead of custom `getUIArgs()` plumbing.\n\n```ts\nimport { useMyHand, usePublicHands } from "@dreamboard/ui-sdk";\n\nconst myHand = useMyHand("hand");\nconst scoredByPlayer = usePublicHands("scored-area");\nconst player2Scored = scoredByPlayer["player-2"] ?? [];\n```\n\nNotes:\n\n- `useMyHand(handId)` returns cards for the currently controlling player only.\n- `usePublicHands(handId)` returns all players\' cards for that hand, keyed by `playerId`.\n- `usePublicHands()` is only populated for hands marked `"visibility": "public"`.\n\n## Genre-specific guidance\n\nFor detailed UI patterns and recommended components per game genre, see:\n\n- [ui-genre-trick-taking.md](ui-genre-trick-taking.md) \u2014 Hearts, Spades, Bridge, Big Two\n- [ui-genre-worker-placement.md](ui-genre-worker-placement.md) \u2014 Agricola, Viticulture, Lords of Waterdeep\n- [ui-genre-resource-management.md](ui-genre-resource-management.md) \u2014 Catan, Splendor, engine builders\n',
|
|
1947
|
+
"references/ui-genre-resource-management.md": '# UI Guide: Resource Management Games\n\nPatterns for Catan, Splendor, engine builders, trading games, and similar resource-centric games.\n\n## Game characteristics\n\n- Players **collect, spend, and trade resources** to build structures, buy cards, or score points.\n- May involve a shared board (hex grid, network, market row) plus personal tableaux.\n- Key UI signals: resource pools, affordability, available purchases, trade offers, build options.\n\n## Recommended SDK components\n\n### Primary\n\n| Component | Usage |\n| ----------------------------- | -------------------------------------------------------------------------- |\n| `ResourceCounter` | Display each player\'s resource pool with icons and animated count changes. |\n| `CostDisplay` | Show build/purchase costs with green/red affordability feedback. |\n| `ActionButton` | "Build", "Buy", "Trade" buttons with integrated cost display. |\n| `ActionPanel` / `ActionGroup` | Organize actions by type (Build, Trade, End Turn). |\n| `Card` / `Hand` | Display purchasable cards or player tableau cards. |\n\n### Board components (pick based on game)\n\n| Component | Use case | Hook |\n| -------------- | ----------------------------------------- | -------------------------- |\n| `HexGrid` | Catan-style resource hex boards | `useHexBoard(boardId)` |\n| `NetworkGraph` | Trade route networks | `useNetworkBoard(boardId)` |\n| `SquareGrid` | Grid-based markets or tableaux | `useSquareBoard(boardId)` |\n| `PlayArea` | Market row / available cards for purchase | \u2014 |\n\n### Supporting\n\n| Component | Usage |\n| ---------------- | -------------------------------------------------------- |\n| `PlayerInfo` | Player name, score, and victory point tracker. |\n| `PhaseIndicator` | Current phase (roll, trade, build, etc.). |\n| `DiceRoller` | For dice-based resource generation (e.g., Catan). |\n| `GameEndDisplay` | Final scores with score breakdown. |\n| `Drawer` | Mobile drawer for trade interface or detailed card view. |\n\n### Hooks\n\n| Hook | Usage |\n| -------------------------- | ----------------------------------------------------- |\n| `useMyResources()` | Current player\'s resource counts. |\n| `useHexBoard(boardId)` | Hex board state for Catan-style games. |\n| `useNetworkBoard(boardId)` | Network/route state. |\n| `useSquareBoard(boardId)` | Grid board state. |\n| `useDice(dieIds)` | Dice rolling mechanics. |\n| `useGameState` | Turn order, current phase, active players. |\n| `useAction` | Submit build, trade, end-turn actions. |\n| `useUIArgs` | Phase-specific data (available builds, trade offers). |\n| `usePlayerInfo` | All player info for scoreboards. |\n\n## Key patterns\n\n### Resource display with affordability\n\n```tsx\nconst resources = useMyResources();\nconst resourceDefs: ResourceDefinition[] = [\n { type: "brick", label: "Brick", icon: Blocks, color: "text-red-600" },\n { type: "lumber", label: "Lumber", icon: TreePine, color: "text-green-700" },\n { type: "ore", label: "Ore", icon: Mountain, color: "text-slate-500" },\n { type: "grain", label: "Grain", icon: Wheat, color: "text-yellow-500" },\n { type: "wool", label: "Wool", icon: Cloud, color: "text-green-400" },\n];\n\n<ResourceCounter\n resources={resourceDefs.map((d) => ({\n ...d,\n iconColor: d.color,\n bgColor: "bg-slate-800",\n }))}\n counts={resources}\n layout="row"\n/>;\n```\n\n### Build actions with costs\n\n```tsx\nconst { buildPhase } = useUIArgs();\nconst resources = useMyResources();\n\n<ActionPanel title="Build" state="buildPhase">\n <ActionGroup title="Structures">\n <ActionButton\n label="Build Road"\n cost={{ brick: 1, lumber: 1 }}\n currentResources={resources}\n resourceDefs={resourceDefs}\n available={buildPhase?.canBuildRoad}\n onClick={() => actions.buildActions.build({ type: "road" })}\n />\n <ActionButton\n label="Build Settlement"\n cost={{ brick: 1, lumber: 1, grain: 1, wool: 1 }}\n currentResources={resources}\n resourceDefs={resourceDefs}\n available={buildPhase?.canBuildSettlement}\n onClick={() => actions.buildActions.build({ type: "settlement" })}\n />\n </ActionGroup>\n</ActionPanel>;\n```\n\n### Hex board for Catan-style games\n\n```tsx\nconst { tiles, edges, vertices } = useHexBoard("catan-board");\n\n<HexGrid\n tiles={tiles}\n edges={edges}\n vertices={vertices}\n hexSize={50}\n renderTile={(tile) => (\n <DefaultHexTile\n size={50}\n fill={terrainColors[tile.typeId]}\n label={tile.data?.diceNumber}\n />\n )}\n renderEdge={(edge, pos) => (\n <DefaultHexEdge position={pos} color={playerColors[edge.owner]} />\n )}\n renderVertex={(vertex, pos) => (\n <DefaultHexVertex position={pos} color={playerColors[vertex.owner]} />\n )}\n interactiveVertices={placementMode === "settlement"}\n interactiveEdges={placementMode === "road"}\n onInteractiveVertexClick={(v) => handlePlacement("settlement", v.id)}\n onInteractiveEdgeClick={(e) => handlePlacement("road", e.id)}\n/>;\n```\n\n### Market row (purchasable cards)\n\n```tsx\n<PlayArea\n cards={marketCards}\n layout="row"\n interactive\n onCardClick={(cardId) => handlePurchase(cardId)}\n renderCard={(card) => (\n <div className="relative">\n <DefaultCardContent card={card} />\n <CostDisplay\n cost={card.properties.cost}\n currentResources={resources}\n resourceDefs={resourceDefs}\n layout="inline"\n />\n </div>\n )}\n/>\n```\n\n## UIArgs recommendations\n\n```typescript\n// shared/ui-args.ts\nexport interface BuildPhaseUIArgs {\n canBuildRoad: boolean;\n canBuildSettlement: boolean;\n canBuildCity: boolean;\n canBuyCard: boolean;\n availableBuildLocations?: string[]; // Board positions where building is allowed\n}\n\nexport interface TradePhaseUIArgs {\n canTrade: boolean;\n tradeOffers?: {\n from: string;\n offering: Record<string, number>;\n requesting: Record<string, number>;\n }[];\n bankTradeRatios?: Record<string, number>; // e.g., { brick: 4, lumber: 3 } for ports\n}\n\nexport interface DicePhaseUIArgs {\n diceValues?: number[];\n resourcesGained?: Record<string, number>;\n}\n```\n',
|
|
1948
|
+
"references/ui-genre-trick-taking.md": '# UI Guide: Trick-Taking Games\n\nPatterns for Hearts, Spades, Bridge, Big Two, and similar card-based trick-taking games.\n\n## Game characteristics\n\n- Players hold a **hand of cards** and play them to a shared **trick area**.\n- One trick is resolved at a time; the winner collects cards.\n- Turn order matters \u2014 typically clockwise from the trick leader.\n- Key UI signals: whose turn it is, which cards are legal to play, current trick, scores.\n\n## Recommended SDK components\n\n### Primary\n\n| Component | Usage |\n| ------------ | --------------------------------------------------------------------------------------------------- |\n| `Hand` | Display the current player\'s hand. Use `renderCard` to highlight playable cards. |\n| `Card` | Render individual cards in the hand and trick area. Use `renderContent` for custom suit/rank faces. |\n| `PlayArea` | Display the current trick \u2014 cards played by each player. Use `layout="row"`. |\n| `PlayerInfo` | Show each player\'s name, score, and turn indicator around the table. |\n\n### Supporting\n\n| Component | Usage |\n| ---------------- | ----------------------------------------------------------- |\n| `PhaseIndicator` | Show current phase (dealing, passing, playing, scoring). |\n| `GameEndDisplay` | Final scoreboard at game end. |\n| `useToast` | Feedback for invalid plays (e.g., "Must follow suit"). |\n| `Drawer` | Overflow drawer when the hand has too many cards on mobile. |\n\n### Hooks\n\n| Hook | Usage |\n| -------------------- | -------------------------------------------------------------------------- |\n| `useMyHand(handId)` | Get the current player\'s cards. |\n| `useCard / useCards` | Resolve card data for display. |\n| `useGameState` | Get `currentPlayerIds`, `isMyTurn`, `currentState`. |\n| `useAction` | Submit play-card action. |\n| `useUIArgs` | Get phase-specific data (e.g., legal card IDs, trick cards, trick winner). |\n| `usePlayerInfo` | Map player IDs to names for trick display. |\n\n## Key patterns\n\n### Card selection for play\n\n```tsx\nconst [selectedCards, setSelectedCards] = useState<string[]>([]);\nconst { playCards } = useUIArgs();\nconst legalCardIds = new Set(playCards?.legalCardIds ?? []);\n\n<Hand\n cards={myCards}\n selectedIds={selectedCards}\n renderCard={({ card, isSelected, ...pos }) => (\n <Card\n card={card}\n selected={isSelected}\n disabled={!legalCardIds.has(card.id)}\n onCardClick={(id) => toggleSelection(id)}\n style={{\n position: "absolute",\n left: pos.x,\n transform: `translateY(${pos.y}px)`,\n zIndex: pos.zIndex,\n }}\n />\n )}\n renderDrawer={({ cards }) => <DrawerContent cards={cards} />}\n renderEmpty={() => <p>No cards</p>}\n/>;\n```\n\n### Trick display with player labels\n\n```tsx\nconst playerInfo = usePlayerInfo();\nconst { playCards } = useUIArgs();\nconst trickCards = playCards?.trickCards ?? []; // { cardId, playerId }[]\n\n<PlayArea\n cards={resolvedCards}\n layout="row"\n renderCard={(card) => (\n <div className="flex flex-col items-center gap-1">\n <span className="text-xs text-slate-400">\n {playerInfo[card.properties?.playedBy]?.name}\n </span>\n <SuitRankCard card={card} />\n </div>\n )}\n/>;\n```\n\n## UIArgs recommendations\n\n```typescript\n// shared/ui-args.ts\nexport interface PlayCardsUIArgs {\n legalCardIds: string[]; // Cards the player can legally play\n trickCards: { cardId: string; playerId: string }[]; // Cards in the current trick\n trickLeader: string | null; // Who led the trick\n trumpSuit?: string; // Trump suit if applicable\n}\n\nexport interface ScoringUIArgs {\n roundScores: Record<string, number>;\n totalScores: Record<string, number>;\n}\n```\n',
|
|
1949
|
+
"references/ui-genre-worker-placement.md": '# UI Guide: Worker Placement Games\n\nPatterns for Agricola, Viticulture, Lords of Waterdeep, Caverna, and similar euro-style worker placement games.\n\n## Game characteristics\n\n- Players place **worker tokens** on shared **action slots** to gain resources or take actions.\n- Slots may be exclusive (one player per round) or shared.\n- Phases typically include: placement, resolution/harvest, and cleanup.\n- Key UI signals: available slots, slot costs/rewards, number of workers remaining, resource counts.\n\n## Recommended SDK components\n\n### Primary\n\n| Component | Usage |\n| --------------------- | ----------------------------------------------------------------------------------------------------- |\n| `SlotSystem` | Core board \u2014 renders all action slots with occupants. Use `layout="grouped"` to organize by category. |\n| `DefaultSlotItem` | Pre-built slot renderer showing name, description, capacity, cost/reward labels. |\n| `DefaultSlotOccupant` | Pre-built worker token display with player color. |\n| `ResourceCounter` | Display each player\'s resource pool (wood, stone, food, etc.). |\n| `ActionButton` | "Place Worker" button with integrated cost display and affordability check. |\n\n### Supporting\n\n| Component | Usage |\n| ----------------------------- | ------------------------------------------------------------ |\n| `ActionPanel` / `ActionGroup` | Group available actions (e.g., "Place Worker", "End Turn"). |\n| `CostDisplay` | Show resource costs on slot details. |\n| `PlayerInfo` | Show each player\'s name, workers remaining, and turn status. |\n| `PhaseIndicator` | Distinguish placement phase from harvest/scoring phases. |\n| `GameEndDisplay` | Final scoring with detailed breakdown. |\n| `Drawer` | Mobile drawer for detailed slot inspection. |\n\n### Hooks\n\n| Hook | Usage |\n| --------------------------------- | ---------------------------------------------------------------------------------------------- |\n| `useSlotSystem(slots, occupants)` | Utility for slot lookups: `getSlot`, `isFull`, `getRemainingCapacity`, `getOccupantsByPlayer`. |\n| `useMyResources()` | Current player\'s resource pool. |\n| `useGameState` | Get `isMyTurn`, `currentState`, `currentPlayerIds`. |\n| `useAction` | Submit place-worker or end-turn actions. |\n| `useUIArgs` | Get phase-specific data (available slots, worker count, etc.). |\n| `usePlayerInfo` | Map player IDs to names and colors. |\n\n## Key patterns\n\n### Slot board with placement\n\n```tsx\nconst { placementPhase } = useUIArgs();\nconst slots = placementPhase?.slots ?? [];\nconst occupants = placementPhase?.occupants ?? [];\nconst availableSlotIds = new Set(placementPhase?.availableSlotIds ?? []);\nconst slotApi = useSlotSystem(slots, occupants);\n\n<SlotSystem\n slots={slots}\n occupants={occupants}\n layout="grouped"\n renderSlot={(slot, slotOccupants) => (\n <DefaultSlotItem\n name={slot.name}\n description={slot.description}\n capacity={slot.capacity}\n occupantCount={slotOccupants.length}\n isExclusive={slot.exclusive}\n isAvailable={availableSlotIds.has(slot.id)}\n costLabel={formatCost(slot.cost)}\n rewardLabel={formatReward(slot.reward)}\n onClick={() => handlePlaceWorker(slot.id)}\n renderOccupants={() => (\n <div className="flex gap-1">\n {slotOccupants.map((o) => (\n <DefaultSlotOccupant\n key={o.pieceId}\n color={playerColors[o.playerId]}\n label={playerInfo[o.playerId]?.name}\n />\n ))}\n </div>\n )}\n />\n )}\n/>;\n```\n\n### Resource display\n\n```tsx\nconst resources = useMyResources();\n\n<ResourceCounter\n resources={[\n {\n type: "wood",\n label: "Wood",\n icon: TreePine,\n iconColor: "text-amber-700",\n },\n {\n type: "stone",\n label: "Stone",\n icon: Mountain,\n iconColor: "text-slate-400",\n },\n { type: "food", label: "Food", icon: Apple, iconColor: "text-red-400" },\n { type: "gold", label: "Gold", icon: Coins, iconColor: "text-yellow-400" },\n ]}\n counts={resources}\n layout="row"\n/>;\n```\n\n### Action buttons with costs\n\n```tsx\n<ActionButton\n label="Build Farm"\n cost={{ wood: 2, stone: 1 }}\n currentResources={resources}\n resourceDefs={resourceDefinitions}\n icon={Home}\n onClick={() => actions.buildActions.build({ buildingType: "farm" })}\n/>\n```\n\n## UIArgs recommendations\n\n```typescript\n// shared/ui-args.ts\nexport interface PlacementPhaseUIArgs {\n slots: SlotDefinition[];\n occupants: SlotOccupant[];\n availableSlotIds: string[]; // Slots the current player can legally place on\n workersRemaining: number;\n}\n\nexport interface HarvestPhaseUIArgs {\n feedingRequired: number; // Food needed\n currentFood: number;\n}\n```\n',
|
|
1950
|
+
"references/ui-style-guide.md": '# UI Style Guide\n\nThis style guide details the design philosophy, visual tokens, and core components for the Dreamboard.games UI SDK. The framework uses a **Hand-Drawn** aesthetic that emphasizes human creativity over corporate polish.\n\n## Design Philosophy\n\nThe Hand-Drawn design style celebrates authentic imperfection and human touch in a digital world. It rejects clinical precision in favor of organic, playful irregularity that evokes sketches on paper, sticky notes on a wall, and napkin diagrams from a brainstorming session.\n\n### Core Principles\n\n- **No Straight Lines**: Every border, shape, and container uses irregular border-radius values to create wobbly, hand-drawn edges that reject geometric perfection.\n- **Authentic Texture**: The design layer paper grain, dot patterns, and subtle background textures to simulate physical media (notebook paper, post-its, sketch pads).\n- **Playful Rotation**: Elements are deliberately tilted using small rotation transforms (`-rotate-1`, `rotate-2`) to break rigid grid alignment and create casual energy.\n- **Hard Offset Shadows**: Reject soft blur shadows entirely. Use solid, offset box-shadows (like `4px 4px 0px`) to create a cut-paper, layered collage aesthetic.\n- **Handwritten Typography**: Use exclusively handwritten or marker-style fonts (like Kalam for headings and Patrick Hand for body text) that feel human and approachable.\n- **Limited Color Palette**: Stick to pencil blacks (`#2d2d2d`), paper whites (`#fdfbf7`), correction marker red (`#ff4d4d`), post-it yellow (`#fff9c4`), and ballpoint blue (`#2d5da1`).\n- **Intentional Messiness**: Embrace overlap, asymmetry, and visual "mistakes" that make the design feel spontaneous.\n\n## CSS Tokens & Utilities\n\nThe platform provides several global CSS utilities designed for this aesthetic:\n\n- **Borders**: `.wobbly-border`, `.wobbly-border-md`, `.wobbly-border-lg` apply the irregular `border-radius` shapes. Used with `border-2`, `border-[3px]`, or `border-[4px]`.\n- **Shadows**: `.hard-shadow`, `.hard-shadow-md`, `.hard-shadow-lg` apply the solid offset shadows. Use `.hard-shadow-sm` for active states (buttons pressing flat).\n\n## Implementing in Components\n\nWhen writing or modifying UI components in `ui/` or `ui-sdk/`:\n\n### Containers & Cards\n\n- Add `wobbly-border-md` or `wobbly-border-lg`.\n- Use `border-[3px] border-border` to give it a thick pencil-like stroke.\n- Apply `bg-[#fdfbf7]` (warm paper) or `bg-white`.\n- Add a solid shadow with `hard-shadow` or `hard-shadow-lg`.\n- Slightly rotate the element with `rotate-1` or `-rotate-1`.\n\n### Buttons\n\n- Use `wobbly-border`.\n- Normal state: `border-[3px] border-border bg-[#fdfbf7] text-foreground hard-shadow`.\n- Hover state: `hover:bg-primary hover:text-white hover:hard-shadow-sm hover:-translate-y-1`.\n- Active state: `active:shadow-none active:translate-y-1 active:translate-x-1`.\n- Secondary variants can use `bg-[#e5e0d8]` or `bg-[#fff9c4]`.\n\n### Typography\n\n- Add `font-sans` for standard handwritten text or `font-display` for bold marker headings.\n- Make things bold! `font-bold` works well with handwritten fonts to make them legible.\n\n### Decoration\n\n- For emphasis, wrap text in a post-it styling: `bg-[#fff9c4] px-2 py-1 border-2 border-border wobbly-border rotate-2 inline-block`.\n- Add "tape" to tops of containers: `<div className="absolute top-2 left-1/2 -translate-x-1/2 w-24 h-6 bg-[#e5e0d8] border border-border opacity-80 backdrop-blur-sm -rotate-2 z-10" />`.\n',
|
|
1951
|
+
"scripts/events-extract.mjs": '#!/usr/bin/env node\n\nimport { access, readFile } from "node:fs/promises";\n\nconst DEFAULT_EVENTS_PATH = ".dreamboard/run/events.ndjson";\n\nfunction printHelp() {\n console.log(`Extract fields from dreamboard run artifacts.\n\nUsage:\n node .agents/skills/dreamboard/scripts/events-extract.mjs [options]\n\nOptions:\n --file, -f <path> NDJSON file path (default: ${DEFAULT_EVENTS_PATH})\n --type <eventType> Filter by event type (repeatable)\n --field <path> Dot path to extract (e.g. message.reason)\n --player <playerId> Filter by player ID across common message fields\n --limit <number> Limit output records\n --no-index Omit line index in output\n --help, -h Show this help\n\nExamples:\n node .agents/skills/dreamboard/scripts/events-extract.mjs --type YOUR_TURN\n node .agents/skills/dreamboard/scripts/events-extract.mjs --type ACTION_REJECTED --field message.reason\n node .agents/skills/dreamboard/scripts/events-extract.mjs --player player-2 --field message.availableActions\n`);\n}\n\nfunction parseCliArgs(argv) {\n const options = {\n filePath: DEFAULT_EVENTS_PATH,\n types: new Set(),\n includeIndex: true,\n };\n\n for (let index = 0; index < argv.length; index += 1) {\n const token = argv[index];\n const nextToken = argv[index + 1];\n\n if (token === "--help" || token === "-h") {\n printHelp();\n process.exit(0);\n }\n if (token === "--no-index") {\n options.includeIndex = false;\n continue;\n }\n if (token === "--file" || token === "-f") {\n if (!nextToken) {\n throw new Error("--file requires a value");\n }\n options.filePath = nextToken;\n index += 1;\n continue;\n }\n if (token === "--type") {\n if (!nextToken) {\n throw new Error("--type requires a value");\n }\n options.types.add(nextToken);\n index += 1;\n continue;\n }\n if (token === "--field") {\n if (!nextToken) {\n throw new Error("--field requires a value");\n }\n options.fieldPath = nextToken;\n index += 1;\n continue;\n }\n if (token === "--player") {\n if (!nextToken) {\n throw new Error("--player requires a value");\n }\n options.playerId = nextToken;\n index += 1;\n continue;\n }\n if (token === "--limit") {\n if (!nextToken) {\n throw new Error("--limit requires a value");\n }\n const parsed = Number.parseInt(nextToken, 10);\n if (!Number.isFinite(parsed) || parsed <= 0) {\n throw new Error("--limit must be a positive integer");\n }\n options.limit = parsed;\n index += 1;\n continue;\n }\n\n throw new Error(`Unknown argument: ${token}`);\n }\n\n return options;\n}\n\nfunction getByPath(value, path) {\n const segments = path.split(".").filter(Boolean);\n let current = value;\n\n for (const segment of segments) {\n if (current === null || current === undefined) {\n return undefined;\n }\n if (typeof current !== "object") {\n return undefined;\n }\n current = current[segment];\n }\n\n return current;\n}\n\nfunction extractPlayerCandidates(record) {\n const message = record.message ?? {};\n const players = new Set();\n\n const singleCandidates = [\n message.playerId,\n message.targetPlayer,\n message.toUser,\n ];\n for (const candidate of singleCandidates) {\n if (typeof candidate === "string") {\n players.add(candidate);\n }\n }\n\n const listCandidates = [\n message.activePlayers,\n message.previousPlayers,\n message.currentPlayers,\n message.controllablePlayerIds,\n message.eligiblePlayerIds,\n ];\n for (const candidate of listCandidates) {\n if (!Array.isArray(candidate)) {\n continue;\n }\n for (const item of candidate) {\n if (typeof item === "string") {\n players.add(item);\n }\n }\n }\n\n return [...players];\n}\n\nfunction toOutputRecord(record, lineIndex, options) {\n const output = {};\n\n if (options.includeIndex) {\n output.index = lineIndex;\n }\n output.observedAt = record.observedAt ?? null;\n output.sessionId = record.sessionId ?? null;\n output.eventId = record.eventId ?? null;\n output.type = record.type ?? null;\n\n if (options.fieldPath) {\n output.field = options.fieldPath;\n output.value = getByPath(record, options.fieldPath);\n } else {\n output.message = record.message ?? null;\n }\n\n return output;\n}\n\nasync function main() {\n const options = parseCliArgs(process.argv.slice(2));\n await access(options.filePath);\n\n const text = await readFile(options.filePath, "utf8");\n const lines = text\n .split(/\\r?\\n/u)\n .map((line) => line.trim())\n .filter((line) => line.length > 0);\n\n let emitted = 0;\n for (let lineIndex = 0; lineIndex < lines.length; lineIndex += 1) {\n const line = lines[lineIndex];\n let record;\n try {\n record = JSON.parse(line);\n } catch {\n continue;\n }\n\n if (options.types.size > 0 && !options.types.has(record.type ?? "")) {\n continue;\n }\n\n if (options.playerId) {\n const players = extractPlayerCandidates(record);\n if (!players.includes(options.playerId)) {\n continue;\n }\n }\n\n const output = toOutputRecord(record, lineIndex + 1, options);\n console.log(JSON.stringify(output));\n emitted += 1;\n\n if (options.limit !== undefined && emitted >= options.limit) {\n break;\n }\n }\n}\n\nmain().catch((error) => {\n const message = error instanceof Error ? error.message : String(error);\n console.error(`events-extract failed: ${message}`);\n process.exit(1);\n});\n',
|
|
1952
|
+
"scripts/extract_tts.py": `#!/usr/bin/env python3
|
|
1953
|
+
"""
|
|
1954
|
+
TTS Binary Save Extractor
|
|
1955
|
+
=========================
|
|
1956
|
+
Parses Tabletop Simulator's BSON-like binary save format (.bin files) and
|
|
1957
|
+
extracts:
|
|
1958
|
+
\u2022 Save metadata (name, date, tags, table/sky URLs, \u2026)
|
|
1959
|
+
\u2022 All asset URLs grouped by TTS object nickname / type
|
|
1960
|
+
\u2022 Every Lua script and XML UI string to individual files
|
|
1961
|
+
\u2022 Per-object summaries (GUID, name, nickname, type, custom asset info, \u2026)
|
|
1962
|
+
\u2022 Full parsed JSON (with script bodies replaced by placeholders)
|
|
1963
|
+
\u2022 Optional download mode that saves assets + emits asset_files.json
|
|
1964
|
+
|
|
1965
|
+
Usage:
|
|
1966
|
+
# Extract directly from a .bin file:
|
|
1967
|
+
python3 extract_tts.py <save.bin> [output_dir]
|
|
1968
|
+
|
|
1969
|
+
# Download from Steam Workshop then extract:
|
|
1970
|
+
python3 extract_tts.py <workshop_id> [output_dir]
|
|
1971
|
+
|
|
1972
|
+
# Download referenced assets too:
|
|
1973
|
+
python3 extract_tts.py <save.bin> [output_dir] --download
|
|
1974
|
+
"""
|
|
1975
|
+
|
|
1976
|
+
import struct
|
|
1977
|
+
import argparse
|
|
1978
|
+
import json
|
|
1979
|
+
import re
|
|
1980
|
+
import subprocess
|
|
1981
|
+
import sys
|
|
1982
|
+
import hashlib
|
|
1983
|
+
import mimetypes
|
|
1984
|
+
import time
|
|
1985
|
+
import threading
|
|
1986
|
+
from concurrent.futures import ThreadPoolExecutor, as_completed
|
|
1987
|
+
from pathlib import Path
|
|
1988
|
+
from collections import defaultdict
|
|
1989
|
+
from urllib.parse import urlparse
|
|
1990
|
+
from urllib.request import Request, urlopen
|
|
1991
|
+
from urllib.error import HTTPError, URLError
|
|
1992
|
+
|
|
1993
|
+
# \u2500\u2500\u2500 Type codes \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500
|
|
1994
|
+
TYPE_DOUBLE = 0x01 # 8-byte IEEE 754 little-endian double
|
|
1995
|
+
TYPE_STRING = 0x02 # 4-byte LE int32 length + string bytes (null-terminated)
|
|
1996
|
+
TYPE_OBJECT = 0x03 # 4-byte LE int32 length + nested key/value records
|
|
1997
|
+
TYPE_ARRAY = 0x04 # 4-byte LE int32 length + integer-keyed nested records
|
|
1998
|
+
TYPE_BOOL = 0x08 # 1 byte (0x00 = false, anything else = true)
|
|
1999
|
+
TYPE_INT32 = 0x10 # 4-byte LE signed int32
|
|
2000
|
+
|
|
2001
|
+
HEADER_SIZE = 4 # magic / version bytes at the very start of the file
|
|
2002
|
+
|
|
2003
|
+
# \u2500\u2500\u2500 Low-level parser \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500
|
|
2004
|
+
|
|
2005
|
+
def read_cstring(data: bytes, pos: int):
|
|
2006
|
+
"""Return (string, next_pos) for a null-terminated UTF-8 key."""
|
|
2007
|
+
end = data.index(b'\\x00', pos)
|
|
2008
|
+
return data[pos:end].decode('utf-8', errors='replace'), end + 1
|
|
2009
|
+
|
|
2010
|
+
|
|
2011
|
+
def parse_records(data: bytes, pos: int, end: int) -> dict:
|
|
2012
|
+
"""
|
|
2013
|
+
Recursively parse all key/value records stored in data[pos:end].
|
|
2014
|
+
Returns a plain Python dict. Unknown type bytes abort the current frame.
|
|
2015
|
+
"""
|
|
2016
|
+
result = {}
|
|
2017
|
+
while pos < end:
|
|
2018
|
+
if pos >= len(data):
|
|
2019
|
+
break
|
|
2020
|
+
|
|
2021
|
+
type_byte = data[pos]
|
|
2022
|
+
pos += 1
|
|
2023
|
+
|
|
2024
|
+
# 0x00 = end-of-sequence sentinel; no key/value follows
|
|
2025
|
+
if type_byte == 0x00:
|
|
2026
|
+
break
|
|
2027
|
+
|
|
2028
|
+
key, pos = read_cstring(data, pos)
|
|
2029
|
+
|
|
2030
|
+
if type_byte == TYPE_DOUBLE:
|
|
2031
|
+
value = struct.unpack_from('<d', data, pos)[0]
|
|
2032
|
+
pos += 8
|
|
2033
|
+
|
|
2034
|
+
elif type_byte == TYPE_STRING:
|
|
2035
|
+
length = struct.unpack_from('<i', data, pos)[0]
|
|
2036
|
+
pos += 4
|
|
2037
|
+
raw = data[pos: pos + length]
|
|
2038
|
+
# Strip the trailing null terminator(s) the format always adds
|
|
2039
|
+
value = raw.rstrip(b'\\x00').decode('utf-8', errors='replace')
|
|
2040
|
+
pos += length
|
|
2041
|
+
|
|
2042
|
+
elif type_byte == TYPE_OBJECT:
|
|
2043
|
+
length = struct.unpack_from('<i', data, pos)[0]
|
|
2044
|
+
pos += 4
|
|
2045
|
+
# Length field is self-inclusive: actual content = length - 4 bytes
|
|
2046
|
+
content_size = length - 4
|
|
2047
|
+
value = parse_records(data, pos, pos + content_size)
|
|
2048
|
+
pos += content_size
|
|
2049
|
+
|
|
2050
|
+
elif type_byte == TYPE_ARRAY:
|
|
2051
|
+
length = struct.unpack_from('<i', data, pos)[0]
|
|
2052
|
+
pos += 4
|
|
2053
|
+
content_size = length - 4
|
|
2054
|
+
sub = parse_records(data, pos, pos + content_size)
|
|
2055
|
+
pos += content_size
|
|
2056
|
+
# Convert {"0": \u2026, "1": \u2026} to a plain list
|
|
2057
|
+
arr, i = [], 0
|
|
2058
|
+
while str(i) in sub:
|
|
2059
|
+
arr.append(sub[str(i)])
|
|
2060
|
+
i += 1
|
|
2061
|
+
value = arr
|
|
2062
|
+
|
|
2063
|
+
elif type_byte == TYPE_BOOL:
|
|
2064
|
+
value = bool(data[pos])
|
|
2065
|
+
pos += 1
|
|
2066
|
+
|
|
2067
|
+
elif type_byte == TYPE_INT32:
|
|
2068
|
+
value = struct.unpack_from('<i', data, pos)[0]
|
|
2069
|
+
pos += 4
|
|
2070
|
+
|
|
2071
|
+
else:
|
|
2072
|
+
print(f" [WARN] Unknown type 0x{type_byte:02X} at byte offset "
|
|
2073
|
+
f"{pos - 1} \u2014 stopping current frame.",
|
|
2074
|
+
file=sys.stderr)
|
|
2075
|
+
break
|
|
2076
|
+
|
|
2077
|
+
result[key] = value
|
|
2078
|
+
|
|
2079
|
+
return result
|
|
2080
|
+
|
|
2081
|
+
|
|
2082
|
+
# \u2500\u2500\u2500 URL extraction \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500
|
|
2083
|
+
|
|
2084
|
+
# Fields that are known to carry asset URLs in TTS saves
|
|
2085
|
+
URL_FIELD_NAMES = {
|
|
2086
|
+
'TableURL', 'SkyURL',
|
|
2087
|
+
'PDFUrl',
|
|
2088
|
+
'FaceURL', 'BackURL',
|
|
2089
|
+
'ImageURL', 'ImageSecondaryURL',
|
|
2090
|
+
'DiffuseURL', 'NormalURL',
|
|
2091
|
+
'ColliderURL', 'MeshURL',
|
|
2092
|
+
'AssetbundleURL', 'AssetbundleSecondaryURL',
|
|
2093
|
+
}
|
|
2094
|
+
|
|
2095
|
+
def extract_urls(obj, urls=None):
|
|
2096
|
+
"""Walk the parsed save tree and collect every URL by its field name."""
|
|
2097
|
+
if urls is None:
|
|
2098
|
+
urls = defaultdict(set)
|
|
2099
|
+
|
|
2100
|
+
if isinstance(obj, dict):
|
|
2101
|
+
for k, v in obj.items():
|
|
2102
|
+
if k in URL_FIELD_NAMES and isinstance(v, str) and v.strip():
|
|
2103
|
+
urls[k].add(v.strip())
|
|
2104
|
+
else:
|
|
2105
|
+
extract_urls(v, urls)
|
|
2106
|
+
elif isinstance(obj, list):
|
|
2107
|
+
for item in obj:
|
|
2108
|
+
extract_urls(item, urls)
|
|
2109
|
+
|
|
2110
|
+
return urls
|
|
2111
|
+
|
|
2112
|
+
def extract_urls_for_object(obj, urls=None):
|
|
2113
|
+
"""
|
|
2114
|
+
Collect URL fields for a single object without traversing child objects.
|
|
2115
|
+
This keeps nickname-grouped output scoped to each object itself.
|
|
2116
|
+
"""
|
|
2117
|
+
if urls is None:
|
|
2118
|
+
urls = defaultdict(set)
|
|
2119
|
+
|
|
2120
|
+
if isinstance(obj, dict):
|
|
2121
|
+
for k, v in obj.items():
|
|
2122
|
+
if k in {'ContainedObjects', 'States'}:
|
|
2123
|
+
continue
|
|
2124
|
+
if k in URL_FIELD_NAMES and isinstance(v, str) and v.strip():
|
|
2125
|
+
urls[k].add(v.strip())
|
|
2126
|
+
else:
|
|
2127
|
+
extract_urls_for_object(v, urls)
|
|
2128
|
+
elif isinstance(obj, list):
|
|
2129
|
+
for item in obj:
|
|
2130
|
+
extract_urls_for_object(item, urls)
|
|
2131
|
+
|
|
2132
|
+
return urls
|
|
2133
|
+
|
|
2134
|
+
def _simplify_url_fields(urls_by_field):
|
|
2135
|
+
"""Convert sets to stable JSON-friendly values (string or list)."""
|
|
2136
|
+
simplified = {}
|
|
2137
|
+
for field in sorted(urls_by_field.keys()):
|
|
2138
|
+
values = sorted(urls_by_field[field])
|
|
2139
|
+
if len(values) == 1:
|
|
2140
|
+
simplified[field] = values[0]
|
|
2141
|
+
else:
|
|
2142
|
+
simplified[field] = values
|
|
2143
|
+
return simplified
|
|
2144
|
+
|
|
2145
|
+
def _deck_key_sort_value(deck_key: str):
|
|
2146
|
+
try:
|
|
2147
|
+
return (0, int(deck_key))
|
|
2148
|
+
except (TypeError, ValueError):
|
|
2149
|
+
return (1, str(deck_key))
|
|
2150
|
+
|
|
2151
|
+
def _simplify_back_url_map(back_urls_by_id: dict):
|
|
2152
|
+
"""Return one BackURL string when all deck IDs share the same URL."""
|
|
2153
|
+
if not back_urls_by_id:
|
|
2154
|
+
return None
|
|
2155
|
+
|
|
2156
|
+
ordered = {
|
|
2157
|
+
deck_id: back_urls_by_id[deck_id]
|
|
2158
|
+
for deck_id in sorted(back_urls_by_id.keys(), key=_deck_key_sort_value)
|
|
2159
|
+
}
|
|
2160
|
+
unique_urls = sorted(set(ordered.values()))
|
|
2161
|
+
if len(unique_urls) == 1:
|
|
2162
|
+
return unique_urls[0]
|
|
2163
|
+
return ordered
|
|
2164
|
+
|
|
2165
|
+
def _extract_custom_pdf_info(obj: dict):
|
|
2166
|
+
"""Extract CustomPDF metadata from a single object when present."""
|
|
2167
|
+
if not isinstance(obj, dict):
|
|
2168
|
+
return None
|
|
2169
|
+
|
|
2170
|
+
custom_pdf = obj.get('CustomPDF')
|
|
2171
|
+
if not isinstance(custom_pdf, dict):
|
|
2172
|
+
return None
|
|
2173
|
+
|
|
2174
|
+
pdf_url = custom_pdf.get('PDFUrl')
|
|
2175
|
+
if not isinstance(pdf_url, str) or not pdf_url.strip():
|
|
2176
|
+
return None
|
|
2177
|
+
|
|
2178
|
+
page_count = None
|
|
2179
|
+
for key in ('PDFPageCount', 'NumPages', 'PageCount', 'Pages'):
|
|
2180
|
+
raw = custom_pdf.get(key)
|
|
2181
|
+
if isinstance(raw, int) and raw >= 0:
|
|
2182
|
+
page_count = raw
|
|
2183
|
+
break
|
|
2184
|
+
|
|
2185
|
+
current_page = custom_pdf.get('PDFPage')
|
|
2186
|
+
if not isinstance(current_page, int):
|
|
2187
|
+
current_page = None
|
|
2188
|
+
|
|
2189
|
+
page_offset = custom_pdf.get('PDFPageOffset')
|
|
2190
|
+
if not isinstance(page_offset, int):
|
|
2191
|
+
page_offset = None
|
|
2192
|
+
|
|
2193
|
+
return {
|
|
2194
|
+
'url': pdf_url.strip(),
|
|
2195
|
+
'pageCount': page_count,
|
|
2196
|
+
'currentPage': current_page,
|
|
2197
|
+
'pageOffset': page_offset,
|
|
2198
|
+
}
|
|
2199
|
+
|
|
2200
|
+
def _collect_card_metadata(objects, by_card_id=None):
|
|
2201
|
+
"""Collect explicit per-card metadata from card objects in the tree."""
|
|
2202
|
+
if by_card_id is None:
|
|
2203
|
+
by_card_id = defaultdict(list)
|
|
2204
|
+
|
|
2205
|
+
if not isinstance(objects, list):
|
|
2206
|
+
return by_card_id
|
|
2207
|
+
|
|
2208
|
+
for obj in objects:
|
|
2209
|
+
if not isinstance(obj, dict):
|
|
2210
|
+
continue
|
|
2211
|
+
|
|
2212
|
+
raw_card_id = obj.get('CardID')
|
|
2213
|
+
try:
|
|
2214
|
+
card_id = int(raw_card_id)
|
|
2215
|
+
except (TypeError, ValueError):
|
|
2216
|
+
card_id = None
|
|
2217
|
+
|
|
2218
|
+
if card_id is not None:
|
|
2219
|
+
by_card_id[card_id].append({
|
|
2220
|
+
'guid': str(obj.get('GUID', '')).strip(),
|
|
2221
|
+
'nickname': str(obj.get('Nickname', '')).strip(),
|
|
2222
|
+
'description': str(obj.get('Description', '')).strip(),
|
|
2223
|
+
'gmNotes': str(obj.get('GMNotes', '')).strip(),
|
|
2224
|
+
})
|
|
2225
|
+
|
|
2226
|
+
children = obj.get('ContainedObjects')
|
|
2227
|
+
if isinstance(children, list):
|
|
2228
|
+
_collect_card_metadata(children, by_card_id)
|
|
2229
|
+
|
|
2230
|
+
return by_card_id
|
|
2231
|
+
|
|
2232
|
+
def _consolidate_cards(cards: list) -> list:
|
|
2233
|
+
"""Collapse duplicate card definitions into one entry with count/guids."""
|
|
2234
|
+
grouped = {}
|
|
2235
|
+
for card in cards:
|
|
2236
|
+
key = (
|
|
2237
|
+
card.get('id'),
|
|
2238
|
+
card.get('nickname', ''),
|
|
2239
|
+
card.get('faceURLId'),
|
|
2240
|
+
card.get('row'),
|
|
2241
|
+
card.get('column'),
|
|
2242
|
+
card.get('description', ''),
|
|
2243
|
+
card.get('gmNotes', ''),
|
|
2244
|
+
)
|
|
2245
|
+
|
|
2246
|
+
if key not in grouped:
|
|
2247
|
+
grouped[key] = {
|
|
2248
|
+
'id': card.get('id'),
|
|
2249
|
+
'nickname': card.get('nickname', ''),
|
|
2250
|
+
'faceURLId': card.get('faceURLId'),
|
|
2251
|
+
'row': card.get('row'),
|
|
2252
|
+
'column': card.get('column'),
|
|
2253
|
+
'description': card.get('description', ''),
|
|
2254
|
+
'gmNotes': card.get('gmNotes', ''),
|
|
2255
|
+
'count': 0,
|
|
2256
|
+
'guids': [],
|
|
2257
|
+
}
|
|
2258
|
+
|
|
2259
|
+
grouped[key]['count'] += 1
|
|
2260
|
+
guid = str(card.get('guid', '')).strip()
|
|
2261
|
+
if guid and guid not in grouped[key]['guids']:
|
|
2262
|
+
grouped[key]['guids'].append(guid)
|
|
2263
|
+
|
|
2264
|
+
consolidated = list(grouped.values())
|
|
2265
|
+
consolidated.sort(key=lambda c: (c['faceURLId'], c['row'], c['column']))
|
|
2266
|
+
return consolidated
|
|
2267
|
+
|
|
2268
|
+
def extract_deck_cards(deck_obj: dict) -> tuple[dict, dict, list]:
|
|
2269
|
+
"""Build per-deck URL metadata and card positions for deck/card objects."""
|
|
2270
|
+
custom_deck = deck_obj.get('CustomDeck')
|
|
2271
|
+
if not isinstance(custom_deck, dict):
|
|
2272
|
+
return {}, {}, []
|
|
2273
|
+
|
|
2274
|
+
face_meta_by_id = {}
|
|
2275
|
+
back_urls_by_id = {}
|
|
2276
|
+
deck_width_by_id = {}
|
|
2277
|
+
|
|
2278
|
+
for deck_key, deck_def in custom_deck.items():
|
|
2279
|
+
if not isinstance(deck_def, dict):
|
|
2280
|
+
continue
|
|
2281
|
+
|
|
2282
|
+
num_width = deck_def.get('NumWidth')
|
|
2283
|
+
num_height = deck_def.get('NumHeight')
|
|
2284
|
+
if isinstance(num_width, int) and num_width > 0:
|
|
2285
|
+
deck_width_by_id[str(deck_key)] = num_width
|
|
2286
|
+
|
|
2287
|
+
face_url = deck_def.get('FaceURL')
|
|
2288
|
+
if (
|
|
2289
|
+
isinstance(face_url, str)
|
|
2290
|
+
and face_url.strip()
|
|
2291
|
+
and isinstance(num_width, int)
|
|
2292
|
+
and num_width > 0
|
|
2293
|
+
and isinstance(num_height, int)
|
|
2294
|
+
and num_height > 0
|
|
2295
|
+
):
|
|
2296
|
+
face_meta_by_id[str(deck_key)] = {
|
|
2297
|
+
'url': face_url.strip(),
|
|
2298
|
+
'numWidth': num_width,
|
|
2299
|
+
'numHeight': num_height,
|
|
2300
|
+
}
|
|
2301
|
+
|
|
2302
|
+
back_url = deck_def.get('BackURL')
|
|
2303
|
+
if isinstance(back_url, str) and back_url.strip():
|
|
2304
|
+
back_urls_by_id[str(deck_key)] = back_url.strip()
|
|
2305
|
+
|
|
2306
|
+
cards = []
|
|
2307
|
+
card_ids = []
|
|
2308
|
+
if isinstance(deck_obj.get('DeckIDs'), list):
|
|
2309
|
+
card_ids.extend(deck_obj['DeckIDs'])
|
|
2310
|
+
if deck_obj.get('CardID') not in (None, ''):
|
|
2311
|
+
card_ids.append(deck_obj['CardID'])
|
|
2312
|
+
|
|
2313
|
+
metadata_by_card_id = _collect_card_metadata(deck_obj.get('ContainedObjects', []))
|
|
2314
|
+
root_card_id = deck_obj.get('CardID')
|
|
2315
|
+
try:
|
|
2316
|
+
root_card_id_int = int(root_card_id)
|
|
2317
|
+
except (TypeError, ValueError):
|
|
2318
|
+
root_card_id_int = None
|
|
2319
|
+
if root_card_id_int is not None:
|
|
2320
|
+
metadata_by_card_id[root_card_id_int].insert(0, {
|
|
2321
|
+
'guid': str(deck_obj.get('GUID', '')).strip(),
|
|
2322
|
+
'nickname': str(deck_obj.get('Nickname', '')).strip(),
|
|
2323
|
+
'description': str(deck_obj.get('Description', '')).strip(),
|
|
2324
|
+
'gmNotes': str(deck_obj.get('GMNotes', '')).strip(),
|
|
2325
|
+
})
|
|
2326
|
+
|
|
2327
|
+
metadata_cursor_by_card_id = defaultdict(int)
|
|
2328
|
+
|
|
2329
|
+
for raw_id in card_ids:
|
|
2330
|
+
try:
|
|
2331
|
+
card_id = int(raw_id)
|
|
2332
|
+
except (TypeError, ValueError):
|
|
2333
|
+
continue
|
|
2334
|
+
|
|
2335
|
+
local_id = card_id % 100
|
|
2336
|
+
deck_key = str(card_id // 100)
|
|
2337
|
+
num_width = deck_width_by_id.get(deck_key)
|
|
2338
|
+
if not isinstance(num_width, int) or num_width <= 0:
|
|
2339
|
+
if len(deck_width_by_id) == 1:
|
|
2340
|
+
num_width = next(iter(deck_width_by_id.values()))
|
|
2341
|
+
else:
|
|
2342
|
+
continue
|
|
2343
|
+
|
|
2344
|
+
meta_items = metadata_by_card_id.get(card_id, [])
|
|
2345
|
+
meta_index = metadata_cursor_by_card_id[card_id]
|
|
2346
|
+
if meta_index < len(meta_items):
|
|
2347
|
+
card_meta = meta_items[meta_index]
|
|
2348
|
+
metadata_cursor_by_card_id[card_id] += 1
|
|
2349
|
+
else:
|
|
2350
|
+
card_meta = {
|
|
2351
|
+
'guid': '',
|
|
2352
|
+
'nickname': '',
|
|
2353
|
+
'description': '',
|
|
2354
|
+
'gmNotes': '',
|
|
2355
|
+
}
|
|
2356
|
+
|
|
2357
|
+
cards.append({
|
|
2358
|
+
'id': card_id,
|
|
2359
|
+
'nickname': card_meta['nickname'],
|
|
2360
|
+
'faceURLId': int(deck_key),
|
|
2361
|
+
'row': local_id // num_width,
|
|
2362
|
+
'column': local_id % num_width,
|
|
2363
|
+
'guid': card_meta['guid'],
|
|
2364
|
+
'description': card_meta['description'],
|
|
2365
|
+
'gmNotes': card_meta['gmNotes'],
|
|
2366
|
+
})
|
|
2367
|
+
|
|
2368
|
+
cards = _consolidate_cards(cards)
|
|
2369
|
+
|
|
2370
|
+
return face_meta_by_id, back_urls_by_id, cards
|
|
2371
|
+
|
|
2372
|
+
def _cards_signature(face_payload, back_payload):
|
|
2373
|
+
"""Stable signature for grouping cards that share deck assets."""
|
|
2374
|
+
return json.dumps(
|
|
2375
|
+
{
|
|
2376
|
+
'FaceURL': face_payload or {},
|
|
2377
|
+
'BackURL': back_payload or {},
|
|
2378
|
+
},
|
|
2379
|
+
ensure_ascii=False,
|
|
2380
|
+
sort_keys=True,
|
|
2381
|
+
)
|
|
2382
|
+
|
|
2383
|
+
def collect_nickname_assets(objects, grouped=None, cards_group_index=None, parent_label=''):
|
|
2384
|
+
"""Collect asset URLs grouped by nickname, merging cards by shared deck assets."""
|
|
2385
|
+
if grouped is None:
|
|
2386
|
+
grouped = {}
|
|
2387
|
+
if cards_group_index is None:
|
|
2388
|
+
cards_group_index = {}
|
|
2389
|
+
|
|
2390
|
+
if not isinstance(objects, list):
|
|
2391
|
+
return grouped
|
|
2392
|
+
|
|
2393
|
+
for obj in objects:
|
|
2394
|
+
if not isinstance(obj, dict):
|
|
2395
|
+
continue
|
|
2396
|
+
|
|
2397
|
+
nickname = str(obj.get('Nickname', '')).strip()
|
|
2398
|
+
name = str(obj.get('Name', '')).strip()
|
|
2399
|
+
guid = str(obj.get('GUID', '')).strip()
|
|
2400
|
+
label = nickname or parent_label or (name if name and name != 'Card' else '')
|
|
2401
|
+
|
|
2402
|
+
object_urls = extract_urls_for_object(obj)
|
|
2403
|
+
url_payload = _simplify_url_fields(object_urls)
|
|
2404
|
+
custom_pdf_info = _extract_custom_pdf_info(obj)
|
|
2405
|
+
face_meta_by_id, back_urls_by_id, cards = extract_deck_cards(obj)
|
|
2406
|
+
|
|
2407
|
+
if custom_pdf_info:
|
|
2408
|
+
url_payload['CustomPDF'] = custom_pdf_info
|
|
2409
|
+
|
|
2410
|
+
if face_meta_by_id:
|
|
2411
|
+
# Keep mapping stable by numeric deck ID order.
|
|
2412
|
+
url_payload['FaceURL'] = {
|
|
2413
|
+
deck_id: face_meta_by_id[deck_id]
|
|
2414
|
+
for deck_id in sorted(face_meta_by_id.keys(), key=_deck_key_sort_value)
|
|
2415
|
+
}
|
|
2416
|
+
|
|
2417
|
+
if back_urls_by_id:
|
|
2418
|
+
# Decks may have different card backs across deck IDs.
|
|
2419
|
+
url_payload['BackURL'] = _simplify_back_url_map(back_urls_by_id)
|
|
2420
|
+
|
|
2421
|
+
if cards:
|
|
2422
|
+
entry = dict(url_payload)
|
|
2423
|
+
entry['Cards'] = cards
|
|
2424
|
+
signature = _cards_signature(entry.get('FaceURL'), entry.get('BackURL'))
|
|
2425
|
+
existing_key = cards_group_index.get(signature)
|
|
2426
|
+
|
|
2427
|
+
if existing_key and isinstance(grouped.get(existing_key), dict):
|
|
2428
|
+
grouped[existing_key]['Cards'].extend(cards)
|
|
2429
|
+
grouped[existing_key]['Cards'] = _consolidate_cards(
|
|
2430
|
+
grouped[existing_key]['Cards']
|
|
2431
|
+
)
|
|
2432
|
+
else:
|
|
2433
|
+
key = label or (name if name and name != 'Card' else f"Deck {signature[:8]}")
|
|
2434
|
+
if key in grouped:
|
|
2435
|
+
key = f"{key} ({guid})" if guid else f"{key} ({len(grouped) + 1})"
|
|
2436
|
+
grouped[key] = entry
|
|
2437
|
+
cards_group_index[signature] = key
|
|
2438
|
+
elif label:
|
|
2439
|
+
if len(url_payload) == 1:
|
|
2440
|
+
entry = next(iter(url_payload.values()))
|
|
2441
|
+
elif url_payload:
|
|
2442
|
+
entry = url_payload
|
|
2443
|
+
else:
|
|
2444
|
+
entry = None
|
|
2445
|
+
|
|
2446
|
+
if entry is not None:
|
|
2447
|
+
key = label
|
|
2448
|
+
if key in grouped:
|
|
2449
|
+
key = f"{label} ({guid})" if guid else f"{label} ({len(grouped) + 1})"
|
|
2450
|
+
grouped[key] = entry
|
|
2451
|
+
|
|
2452
|
+
children = obj.get('ContainedObjects')
|
|
2453
|
+
has_deck_ids = isinstance(obj.get('DeckIDs'), list) and len(obj.get('DeckIDs')) > 0
|
|
2454
|
+
if isinstance(children, list) and not has_deck_ids:
|
|
2455
|
+
collect_nickname_assets(
|
|
2456
|
+
children,
|
|
2457
|
+
grouped,
|
|
2458
|
+
cards_group_index,
|
|
2459
|
+
label,
|
|
2460
|
+
)
|
|
2461
|
+
|
|
2462
|
+
states = obj.get('States')
|
|
2463
|
+
if isinstance(states, dict):
|
|
2464
|
+
collect_nickname_assets(
|
|
2465
|
+
list(states.values()),
|
|
2466
|
+
grouped,
|
|
2467
|
+
cards_group_index,
|
|
2468
|
+
label,
|
|
2469
|
+
)
|
|
2470
|
+
|
|
2471
|
+
return grouped
|
|
2472
|
+
|
|
2473
|
+
|
|
2474
|
+
# \u2500\u2500\u2500 Lua / XML script extraction \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500
|
|
2475
|
+
|
|
2476
|
+
SCRIPT_FIELD_NAMES = {'LuaScript', 'LuaScriptState', 'XmlUI'}
|
|
2477
|
+
|
|
2478
|
+
def extract_scripts(obj, scripts=None, path=''):
|
|
2479
|
+
"""
|
|
2480
|
+
Walk the tree and collect every non-empty script / XML string along with
|
|
2481
|
+
context (GUID, Name, Nickname).
|
|
2482
|
+
"""
|
|
2483
|
+
if scripts is None:
|
|
2484
|
+
scripts = []
|
|
2485
|
+
|
|
2486
|
+
if isinstance(obj, dict):
|
|
2487
|
+
guid = obj.get('GUID', '')
|
|
2488
|
+
name = obj.get('Name', '')
|
|
2489
|
+
nickname = obj.get('Nickname', '')
|
|
2490
|
+
|
|
2491
|
+
for field in SCRIPT_FIELD_NAMES:
|
|
2492
|
+
v = obj.get(field, '')
|
|
2493
|
+
if isinstance(v, str) and v.strip():
|
|
2494
|
+
scripts.append({
|
|
2495
|
+
'field': field,
|
|
2496
|
+
'guid': guid,
|
|
2497
|
+
'name': name,
|
|
2498
|
+
'nickname': nickname,
|
|
2499
|
+
'path': path,
|
|
2500
|
+
'content': v,
|
|
2501
|
+
})
|
|
2502
|
+
|
|
2503
|
+
for k, v in obj.items():
|
|
2504
|
+
if isinstance(v, (dict, list)):
|
|
2505
|
+
extract_scripts(v, scripts, f"{path}.{k}" if path else k)
|
|
2506
|
+
|
|
2507
|
+
elif isinstance(obj, list):
|
|
2508
|
+
for i, item in enumerate(obj):
|
|
2509
|
+
extract_scripts(item, scripts, f"{path}[{i}]")
|
|
2510
|
+
|
|
2511
|
+
return scripts
|
|
2512
|
+
|
|
2513
|
+
|
|
2514
|
+
# \u2500\u2500\u2500 Object summariser \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500
|
|
2515
|
+
|
|
2516
|
+
# Fields that are interesting but don't need deep inspection
|
|
2517
|
+
OBJECT_SCALAR_FIELDS = {
|
|
2518
|
+
'GUID', 'Name', 'Nickname', 'Description', 'GMNotes',
|
|
2519
|
+
'FogColor', 'Locked', 'Snap', 'Grid',
|
|
2520
|
+
'HideWhenFaceDown', 'Hands',
|
|
2521
|
+
'CardID', 'SidewaysCard', 'DeckIDs',
|
|
2522
|
+
'Value', 'LayoutGroupSortIndex',
|
|
2523
|
+
}
|
|
2524
|
+
|
|
2525
|
+
# Sub-objects we want to keep verbatim (they hold asset info)
|
|
2526
|
+
CUSTOM_SECTIONS = {
|
|
2527
|
+
'CustomImage', 'CustomMesh', 'CustomDeck',
|
|
2528
|
+
'CustomAssetbundle', 'CustomPDF',
|
|
2529
|
+
'Transform',
|
|
2530
|
+
}
|
|
2531
|
+
|
|
2532
|
+
def summarise_object(obj):
|
|
2533
|
+
"""
|
|
2534
|
+
Return a lightweight, human-readable summary of one TTS object.
|
|
2535
|
+
Large script bodies are replaced with a presence flag.
|
|
2536
|
+
"""
|
|
2537
|
+
if not isinstance(obj, dict):
|
|
2538
|
+
return obj
|
|
2539
|
+
|
|
2540
|
+
summary = {}
|
|
2541
|
+
|
|
2542
|
+
for k in OBJECT_SCALAR_FIELDS:
|
|
2543
|
+
v = obj.get(k)
|
|
2544
|
+
if v not in (None, '', 0, False, []):
|
|
2545
|
+
summary[k] = v
|
|
2546
|
+
|
|
2547
|
+
for k in CUSTOM_SECTIONS:
|
|
2548
|
+
if k in obj:
|
|
2549
|
+
summary[k] = obj[k]
|
|
2550
|
+
|
|
2551
|
+
for sk in SCRIPT_FIELD_NAMES:
|
|
2552
|
+
v = obj.get(sk, '')
|
|
2553
|
+
if isinstance(v, str) and v.strip():
|
|
2554
|
+
summary[f'{sk}_present'] = True
|
|
2555
|
+
|
|
2556
|
+
# Recurse into child-object containers
|
|
2557
|
+
for container in ('ContainedObjects', 'States'):
|
|
2558
|
+
v = obj.get(container)
|
|
2559
|
+
if isinstance(v, list):
|
|
2560
|
+
summary[container] = [summarise_object(o) for o in v]
|
|
2561
|
+
elif isinstance(v, dict):
|
|
2562
|
+
summary[container] = {k2: summarise_object(v2) for k2, v2 in v.items()}
|
|
2563
|
+
|
|
2564
|
+
return summary
|
|
2565
|
+
|
|
2566
|
+
|
|
2567
|
+
# \u2500\u2500\u2500 Script file writer \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500
|
|
2568
|
+
|
|
2569
|
+
def safe_filename(text: str, maxlen: int = 50) -> str:
|
|
2570
|
+
return re.sub(r'[^\\w\\-]', '_', text)[:maxlen]
|
|
2571
|
+
|
|
2572
|
+
|
|
2573
|
+
def write_scripts(scripts: list, out_dir: Path):
|
|
2574
|
+
"""
|
|
2575
|
+
Deduplicate and write each unique script body to a file.
|
|
2576
|
+
Returns the index list (content replaced by file path).
|
|
2577
|
+
"""
|
|
2578
|
+
lua_dir = out_dir / 'scripts'
|
|
2579
|
+
lua_dir.mkdir(exist_ok=True)
|
|
2580
|
+
|
|
2581
|
+
seen_content: dict[str, str] = {} # content \u2192 relative file path
|
|
2582
|
+
index = []
|
|
2583
|
+
|
|
2584
|
+
for s in scripts:
|
|
2585
|
+
content = s['content']
|
|
2586
|
+
field = s['field']
|
|
2587
|
+
|
|
2588
|
+
if content in seen_content:
|
|
2589
|
+
entry = {k: v for k, v in s.items() if k != 'content'}
|
|
2590
|
+
entry['file'] = seen_content[content]
|
|
2591
|
+
index.append(entry)
|
|
2592
|
+
continue
|
|
2593
|
+
|
|
2594
|
+
ext = '.xml' if field == 'XmlUI' else '.lua'
|
|
2595
|
+
label = s['nickname'] or s['name'] or s['guid'] or 'unknown'
|
|
2596
|
+
base = f"{s['guid']}_{safe_filename(label)}" if s['guid'] else safe_filename(label)
|
|
2597
|
+
fpath = lua_dir / f"{base}{ext}"
|
|
2598
|
+
|
|
2599
|
+
n = 1
|
|
2600
|
+
while fpath.exists():
|
|
2601
|
+
fpath = lua_dir / f"{base}_{n}{ext}"
|
|
2602
|
+
n += 1
|
|
2603
|
+
|
|
2604
|
+
fpath.write_text(content, encoding='utf-8')
|
|
2605
|
+
rel = str(fpath.relative_to(out_dir))
|
|
2606
|
+
seen_content[content] = rel
|
|
2607
|
+
|
|
2608
|
+
entry = {k: v for k, v in s.items() if k != 'content'}
|
|
2609
|
+
entry['file'] = rel
|
|
2610
|
+
index.append(entry)
|
|
2611
|
+
|
|
2612
|
+
return index, lua_dir
|
|
2613
|
+
|
|
2614
|
+
|
|
2615
|
+
# \u2500\u2500\u2500 Full-save JSON (with scripts stripped) \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500
|
|
2616
|
+
|
|
2617
|
+
def strip_script_bodies(obj, threshold: int = 80):
|
|
2618
|
+
"""Replace long script strings with a placeholder so the JSON stays sane."""
|
|
2619
|
+
if isinstance(obj, dict):
|
|
2620
|
+
return {
|
|
2621
|
+
k: (f'<{k}: {len(v)} chars>'
|
|
2622
|
+
if k in SCRIPT_FIELD_NAMES and isinstance(v, str) and len(v) > threshold
|
|
2623
|
+
else strip_script_bodies(v))
|
|
2624
|
+
for k, v in obj.items()
|
|
2625
|
+
}
|
|
2626
|
+
if isinstance(obj, list):
|
|
2627
|
+
return [strip_script_bodies(i) for i in obj]
|
|
2628
|
+
return obj
|
|
2629
|
+
|
|
2630
|
+
|
|
2631
|
+
# \u2500\u2500\u2500 steamcmd workshop downloader \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500
|
|
2632
|
+
|
|
2633
|
+
TTS_APP_ID = '286160'
|
|
2634
|
+
WORKSHOP_CONTENT_DIR = (
|
|
2635
|
+
Path.home() / 'Library' / 'Application Support' / 'Steam'
|
|
2636
|
+
/ 'steamapps' / 'workshop' / 'content' / TTS_APP_ID
|
|
2637
|
+
)
|
|
2638
|
+
|
|
2639
|
+
# Candidate locations for steamcmd, checked in order:
|
|
2640
|
+
# 1. Anywhere on PATH (e.g. \`brew install --cask steamcmd\`)
|
|
2641
|
+
# 2. Manual install next to this script (../steamcmd.sh)
|
|
2642
|
+
def _find_steamcmd() -> str | None:
|
|
2643
|
+
import shutil
|
|
2644
|
+
return shutil.which('steamcmd')
|
|
2645
|
+
|
|
2646
|
+
|
|
2647
|
+
def download_workshop_item(workshop_id: str) -> Path:
|
|
2648
|
+
"""
|
|
2649
|
+
Download a TTS Steam Workshop item via steamcmd and return the path to the
|
|
2650
|
+
.bin file inside the downloaded directory.
|
|
2651
|
+
|
|
2652
|
+
Raises SystemExit on failure.
|
|
2653
|
+
"""
|
|
2654
|
+
steamcmd = _find_steamcmd()
|
|
2655
|
+
if not steamcmd:
|
|
2656
|
+
print("Error: steamcmd not found.", file=sys.stderr)
|
|
2657
|
+
print(" Install it with: brew install --cask steamcmd", file=sys.stderr)
|
|
2658
|
+
print(" Or manually: https://developer.valvesoftware.com/wiki/SteamCMD",
|
|
2659
|
+
file=sys.stderr)
|
|
2660
|
+
sys.exit(1)
|
|
2661
|
+
|
|
2662
|
+
print(f"Downloading Workshop item {workshop_id} (app {TTS_APP_ID}) \u2026")
|
|
2663
|
+
print(f" Using steamcmd: {steamcmd}")
|
|
2664
|
+
|
|
2665
|
+
result = subprocess.run(
|
|
2666
|
+
[
|
|
2667
|
+
steamcmd,
|
|
2668
|
+
'+login', 'anonymous',
|
|
2669
|
+
'+workshop_download_item', TTS_APP_ID, workshop_id,
|
|
2670
|
+
'+quit',
|
|
2671
|
+
],
|
|
2672
|
+
text=True,
|
|
2673
|
+
capture_output=False, # let steamcmd print progress to the terminal
|
|
2674
|
+
)
|
|
2675
|
+
|
|
2676
|
+
if result.returncode != 0:
|
|
2677
|
+
print(f"Error: steamcmd exited with code {result.returncode}.", file=sys.stderr)
|
|
2678
|
+
sys.exit(result.returncode)
|
|
2679
|
+
|
|
2680
|
+
item_dir = WORKSHOP_CONTENT_DIR / workshop_id
|
|
2681
|
+
if not item_dir.exists():
|
|
2682
|
+
print(f"Error: expected download directory not found: {item_dir}", file=sys.stderr)
|
|
2683
|
+
sys.exit(1)
|
|
2684
|
+
|
|
2685
|
+
bins = sorted(item_dir.glob('*.bin'))
|
|
2686
|
+
if not bins:
|
|
2687
|
+
print(f"Error: no .bin file found in {item_dir}", file=sys.stderr)
|
|
2688
|
+
print(f" Directory contents: {list(item_dir.iterdir())}", file=sys.stderr)
|
|
2689
|
+
sys.exit(1)
|
|
2690
|
+
|
|
2691
|
+
if len(bins) > 1:
|
|
2692
|
+
print(f" Multiple .bin files found; using the largest: {bins}")
|
|
2693
|
+
bins.sort(key=lambda p: p.stat().st_size, reverse=True)
|
|
2694
|
+
|
|
2695
|
+
return bins[0]
|
|
2696
|
+
|
|
2697
|
+
|
|
2698
|
+
def parse_workshop_id(input_value: str) -> str | None:
|
|
2699
|
+
"""Return a Workshop ID from a raw ID or Steam workshop URL."""
|
|
2700
|
+
trimmed = input_value.strip()
|
|
2701
|
+
if trimmed.isdigit():
|
|
2702
|
+
return trimmed
|
|
2703
|
+
|
|
2704
|
+
parsed = urlparse(trimmed)
|
|
2705
|
+
if parsed.scheme in {'http', 'https'} and parsed.netloc.endswith('steamcommunity.com'):
|
|
2706
|
+
query_match = re.search(r'(?:^|[?&])id=(\\d+)(?:&|$)', parsed.query)
|
|
2707
|
+
if query_match:
|
|
2708
|
+
return query_match.group(1)
|
|
2709
|
+
|
|
2710
|
+
path_match = re.search(r'/sharedfiles/filedetails/(\\d+)', parsed.path)
|
|
2711
|
+
if path_match:
|
|
2712
|
+
return path_match.group(1)
|
|
2713
|
+
|
|
2714
|
+
return None
|
|
2715
|
+
|
|
2716
|
+
|
|
2717
|
+
# \u2500\u2500\u2500 Helpers \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500
|
|
2718
|
+
|
|
2719
|
+
def slugify(name: str) -> str:
|
|
2720
|
+
"""
|
|
2721
|
+
Convert a save name into a safe directory name:
|
|
2722
|
+
- spaces \u2192 hyphens
|
|
2723
|
+
- characters illegal in file/dir names stripped
|
|
2724
|
+
- collapse runs of hyphens
|
|
2725
|
+
- strip leading/trailing hyphens
|
|
2726
|
+
"""
|
|
2727
|
+
# Characters that are unsafe on macOS/Linux/Windows
|
|
2728
|
+
slug = re.sub(r'[/\\\\:*?"<>|]', '', name)
|
|
2729
|
+
slug = slug.replace(' ', '-')
|
|
2730
|
+
slug = re.sub(r'-{2,}', '-', slug)
|
|
2731
|
+
slug = slug.strip('-')
|
|
2732
|
+
return slug or 'untitled'
|
|
2733
|
+
|
|
2734
|
+
def is_http_url(value) -> bool:
|
|
2735
|
+
return isinstance(value, str) and value.startswith(('http://', 'https://'))
|
|
2736
|
+
|
|
2737
|
+
def collect_asset_urls(obj, path=(), collected=None):
|
|
2738
|
+
"""Collect unique asset URLs and their first-seen field path."""
|
|
2739
|
+
if collected is None:
|
|
2740
|
+
collected = {}
|
|
2741
|
+
|
|
2742
|
+
if is_http_url(obj):
|
|
2743
|
+
collected.setdefault(obj, path)
|
|
2744
|
+
return collected
|
|
2745
|
+
|
|
2746
|
+
if isinstance(obj, dict):
|
|
2747
|
+
for k, v in obj.items():
|
|
2748
|
+
collect_asset_urls(v, path + (str(k),), collected)
|
|
2749
|
+
elif isinstance(obj, list):
|
|
2750
|
+
for i, item in enumerate(obj):
|
|
2751
|
+
collect_asset_urls(item, path + (str(i),), collected)
|
|
2752
|
+
|
|
2753
|
+
return collected
|
|
2754
|
+
|
|
2755
|
+
def infer_asset_subdir(path: tuple[str, ...]) -> str:
|
|
2756
|
+
"""Infer an asset sub-folder from a URL's JSON field path."""
|
|
2757
|
+
fields = [p.lower() for p in path if not p.isdigit()]
|
|
2758
|
+
|
|
2759
|
+
if 'custompdf' in fields or 'pdfurl' in fields:
|
|
2760
|
+
return 'pdf'
|
|
2761
|
+
if 'faceurl' in fields or 'backurl' in fields:
|
|
2762
|
+
return 'deck'
|
|
2763
|
+
if 'meshurl' in fields or 'colliderurl' in fields:
|
|
2764
|
+
return 'mesh'
|
|
2765
|
+
if 'assetbundleurl' in fields or 'assetbundlesecondaryurl' in fields:
|
|
2766
|
+
return 'assetbundle'
|
|
2767
|
+
if (
|
|
2768
|
+
'imageurl' in fields
|
|
2769
|
+
or 'imagesecondaryurl' in fields
|
|
2770
|
+
or 'diffuseurl' in fields
|
|
2771
|
+
or 'normalurl' in fields
|
|
2772
|
+
):
|
|
2773
|
+
return 'image'
|
|
2774
|
+
if 'tableurl' in fields or 'skyurl' in fields:
|
|
2775
|
+
return 'environment'
|
|
2776
|
+
return 'misc'
|
|
2777
|
+
|
|
2778
|
+
def guess_extension(url: str, content_type: str, subdir: str) -> str:
|
|
2779
|
+
"""Guess a file extension from URL/content-type with sensible fallbacks."""
|
|
2780
|
+
parsed = urlparse(url)
|
|
2781
|
+
suffix = Path(parsed.path).suffix.lower()
|
|
2782
|
+
if 1 <= len(suffix) <= 8 and re.fullmatch(r'\\.[a-z0-9]+', suffix):
|
|
2783
|
+
return suffix
|
|
2784
|
+
|
|
2785
|
+
ctype = (content_type or '').split(';', 1)[0].strip().lower()
|
|
2786
|
+
mapped = {
|
|
2787
|
+
'application/pdf': '.pdf',
|
|
2788
|
+
'image/jpeg': '.jpg',
|
|
2789
|
+
'image/jpg': '.jpg',
|
|
2790
|
+
'image/png': '.png',
|
|
2791
|
+
'image/webp': '.webp',
|
|
2792
|
+
'image/gif': '.gif',
|
|
2793
|
+
}.get(ctype)
|
|
2794
|
+
if mapped:
|
|
2795
|
+
return mapped
|
|
2796
|
+
|
|
2797
|
+
guess = mimetypes.guess_extension(ctype) if ctype else None
|
|
2798
|
+
if guess:
|
|
2799
|
+
return '.jpg' if guess == '.jpe' else guess
|
|
2800
|
+
|
|
2801
|
+
if subdir == 'pdf':
|
|
2802
|
+
return '.pdf'
|
|
2803
|
+
return '.bin'
|
|
2804
|
+
|
|
2805
|
+
def detect_extension_from_content(data: bytes):
|
|
2806
|
+
"""Infer extension from file signature when headers/URL are ambiguous."""
|
|
2807
|
+
if data.startswith(b'%PDF-'):
|
|
2808
|
+
return '.pdf'
|
|
2809
|
+
return None
|
|
2810
|
+
|
|
2811
|
+
class DownloadRateLimiter:
|
|
2812
|
+
"""Simple process-local token bucket equivalent (fixed interval)."""
|
|
2813
|
+
def __init__(self, requests_per_second: float):
|
|
2814
|
+
self.interval = 0.0 if requests_per_second <= 0 else (1.0 / requests_per_second)
|
|
2815
|
+
self.lock = threading.Lock()
|
|
2816
|
+
self.next_allowed = 0.0
|
|
2817
|
+
|
|
2818
|
+
def wait(self):
|
|
2819
|
+
if self.interval <= 0:
|
|
2820
|
+
return
|
|
2821
|
+
with self.lock:
|
|
2822
|
+
now = time.monotonic()
|
|
2823
|
+
if now < self.next_allowed:
|
|
2824
|
+
time.sleep(self.next_allowed - now)
|
|
2825
|
+
now = time.monotonic()
|
|
2826
|
+
self.next_allowed = now + self.interval
|
|
2827
|
+
|
|
2828
|
+
def _download_single_asset(url: str, subdir: str, assets_root: Path, limiter: DownloadRateLimiter):
|
|
2829
|
+
"""Download one URL with retries, backoff, and atomic writes."""
|
|
2830
|
+
folder = assets_root / subdir
|
|
2831
|
+
folder.mkdir(parents=True, exist_ok=True)
|
|
2832
|
+
|
|
2833
|
+
digest = hashlib.sha1(url.encode('utf-8')).hexdigest()[:20]
|
|
2834
|
+
existing = sorted(folder.glob(f'{digest}.*'))
|
|
2835
|
+
if existing:
|
|
2836
|
+
chosen = existing[0]
|
|
2837
|
+
# Retro-fix older cached PDF downloads that were saved as .bin.
|
|
2838
|
+
if chosen.suffix == '.bin' and subdir == 'pdf':
|
|
2839
|
+
try:
|
|
2840
|
+
header = chosen.read_bytes()[:8]
|
|
2841
|
+
if detect_extension_from_content(header) == '.pdf':
|
|
2842
|
+
repaired = chosen.with_suffix('.pdf')
|
|
2843
|
+
chosen.replace(repaired)
|
|
2844
|
+
chosen = repaired
|
|
2845
|
+
except Exception:
|
|
2846
|
+
pass
|
|
2847
|
+
return str(chosen.relative_to(assets_root.parent)), 'cached'
|
|
2848
|
+
|
|
2849
|
+
max_attempts = 5
|
|
2850
|
+
for attempt in range(max_attempts):
|
|
2851
|
+
limiter.wait()
|
|
2852
|
+
try:
|
|
2853
|
+
req = Request(url, headers={'User-Agent': 'tts-extractor/1.0 (+respectful downloader)'})
|
|
2854
|
+
with urlopen(req, timeout=30) as resp:
|
|
2855
|
+
body = resp.read()
|
|
2856
|
+
content_type = resp.headers.get('Content-Type', '')
|
|
2857
|
+
ext = guess_extension(url, content_type, subdir)
|
|
2858
|
+
detected_ext = detect_extension_from_content(body[:16])
|
|
2859
|
+
if detected_ext:
|
|
2860
|
+
ext = detected_ext
|
|
2861
|
+
dest = folder / f'{digest}{ext}'
|
|
2862
|
+
tmp = folder / f'{digest}{ext}.part'
|
|
2863
|
+
tmp.write_bytes(body)
|
|
2864
|
+
tmp.replace(dest)
|
|
2865
|
+
return str(dest.relative_to(assets_root.parent)), 'downloaded'
|
|
2866
|
+
|
|
2867
|
+
except HTTPError as e:
|
|
2868
|
+
if e.code in (429, 500, 502, 503, 504) and attempt + 1 < max_attempts:
|
|
2869
|
+
retry_after = e.headers.get('Retry-After', '').strip()
|
|
2870
|
+
if retry_after.isdigit():
|
|
2871
|
+
time.sleep(min(int(retry_after), 30))
|
|
2872
|
+
else:
|
|
2873
|
+
time.sleep(min(2 ** attempt, 16))
|
|
2874
|
+
continue
|
|
2875
|
+
return None, f'HTTP {e.code}'
|
|
2876
|
+
|
|
2877
|
+
except URLError as e:
|
|
2878
|
+
if attempt + 1 < max_attempts:
|
|
2879
|
+
time.sleep(min(2 ** attempt, 16))
|
|
2880
|
+
continue
|
|
2881
|
+
return None, f'URL error: {e.reason}'
|
|
2882
|
+
|
|
2883
|
+
except Exception as e: # noqa: BLE001
|
|
2884
|
+
return None, f'Error: {e}'
|
|
2885
|
+
|
|
2886
|
+
return None, 'Retries exhausted'
|
|
2887
|
+
|
|
2888
|
+
def map_urls_to_files(obj, url_to_file: dict):
|
|
2889
|
+
"""Deep-copy an asset structure replacing URLs with local relative paths."""
|
|
2890
|
+
if is_http_url(obj):
|
|
2891
|
+
return url_to_file.get(obj, obj)
|
|
2892
|
+
if isinstance(obj, dict):
|
|
2893
|
+
return {k: map_urls_to_files(v, url_to_file) for k, v in obj.items()}
|
|
2894
|
+
if isinstance(obj, list):
|
|
2895
|
+
return [map_urls_to_files(i, url_to_file) for i in obj]
|
|
2896
|
+
return obj
|
|
2897
|
+
|
|
2898
|
+
def download_assets(asset_urls: dict, out_dir: Path, workers: int, requests_per_second: float):
|
|
2899
|
+
"""Download all URLs referenced by asset_urls and return URL\u2192file mapping."""
|
|
2900
|
+
assets_root = out_dir / 'assets'
|
|
2901
|
+
assets_root.mkdir(parents=True, exist_ok=True)
|
|
2902
|
+
|
|
2903
|
+
url_context = collect_asset_urls(asset_urls)
|
|
2904
|
+
if not url_context:
|
|
2905
|
+
return {}, {}
|
|
2906
|
+
|
|
2907
|
+
limiter = DownloadRateLimiter(requests_per_second)
|
|
2908
|
+
url_to_file: dict[str, str] = {}
|
|
2909
|
+
failures: dict[str, str] = {}
|
|
2910
|
+
|
|
2911
|
+
worker_count = max(1, min(workers, 16))
|
|
2912
|
+
with ThreadPoolExecutor(max_workers=worker_count) as executor:
|
|
2913
|
+
futures = {}
|
|
2914
|
+
for url, path in url_context.items():
|
|
2915
|
+
subdir = infer_asset_subdir(path)
|
|
2916
|
+
fut = executor.submit(_download_single_asset, url, subdir, assets_root, limiter)
|
|
2917
|
+
futures[fut] = url
|
|
2918
|
+
|
|
2919
|
+
for fut in as_completed(futures):
|
|
2920
|
+
url = futures[fut]
|
|
2921
|
+
rel_path, status = fut.result()
|
|
2922
|
+
if rel_path:
|
|
2923
|
+
url_to_file[url] = rel_path
|
|
2924
|
+
else:
|
|
2925
|
+
failures[url] = status
|
|
2926
|
+
|
|
2927
|
+
return url_to_file, failures
|
|
2928
|
+
|
|
2929
|
+
|
|
2930
|
+
# \u2500\u2500\u2500 Main \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500
|
|
2931
|
+
|
|
2932
|
+
def main():
|
|
2933
|
+
parser = argparse.ArgumentParser(
|
|
2934
|
+
description='Extract Tabletop Simulator binary saves and optionally download assets.'
|
|
2935
|
+
)
|
|
2936
|
+
parser.add_argument('input', help='Path to .bin save, Steam Workshop ID, or Workshop URL')
|
|
2937
|
+
parser.add_argument('output_dir', nargs='?', help='Output directory (default: save-name folder)')
|
|
2938
|
+
parser.add_argument(
|
|
2939
|
+
'--download',
|
|
2940
|
+
action='store_true',
|
|
2941
|
+
help='Download assets from asset_urls.json into assets/ and emit asset_files.json',
|
|
2942
|
+
)
|
|
2943
|
+
parser.add_argument(
|
|
2944
|
+
'--download-workers',
|
|
2945
|
+
type=int,
|
|
2946
|
+
default=6,
|
|
2947
|
+
help='Maximum concurrent download workers (default: 6, capped at 16)',
|
|
2948
|
+
)
|
|
2949
|
+
parser.add_argument(
|
|
2950
|
+
'--download-rps',
|
|
2951
|
+
type=float,
|
|
2952
|
+
default=3.0,
|
|
2953
|
+
help='Global request rate limit in requests/second across all workers (default: 3.0)',
|
|
2954
|
+
)
|
|
2955
|
+
args = parser.parse_args()
|
|
2956
|
+
|
|
2957
|
+
arg = args.input
|
|
2958
|
+
|
|
2959
|
+
# \u2500\u2500 Mode detection \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500
|
|
2960
|
+
# Numeric IDs and full Steam workshop URLs are treated as workshop inputs.
|
|
2961
|
+
# Everything else is treated as a path to a local .bin file.
|
|
2962
|
+
workshop_id = parse_workshop_id(arg)
|
|
2963
|
+
if workshop_id is not None:
|
|
2964
|
+
bin_path = download_workshop_item(workshop_id)
|
|
2965
|
+
print(f" Found: {bin_path}")
|
|
2966
|
+
base_dir = Path.cwd()
|
|
2967
|
+
else:
|
|
2968
|
+
bin_path = Path(arg)
|
|
2969
|
+
if not bin_path.exists():
|
|
2970
|
+
print(f"Error: '{bin_path}' not found.", file=sys.stderr)
|
|
2971
|
+
sys.exit(1)
|
|
2972
|
+
base_dir = bin_path.parent
|
|
2973
|
+
|
|
2974
|
+
# \u2500\u2500 Parse \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500
|
|
2975
|
+
print(f"Parsing {bin_path.name} ({bin_path.stat().st_size:,} bytes) \u2026")
|
|
2976
|
+
data = bin_path.read_bytes()
|
|
2977
|
+
|
|
2978
|
+
# Verify magic header (e1 66 04 00)
|
|
2979
|
+
magic = data[:4]
|
|
2980
|
+
if magic != b'\\xe1\\x66\\x04\\x00':
|
|
2981
|
+
print(f" [INFO] Unexpected header bytes: {magic.hex()} "
|
|
2982
|
+
"(continuing anyway)", file=sys.stderr)
|
|
2983
|
+
|
|
2984
|
+
root = parse_records(data, HEADER_SIZE, len(data))
|
|
2985
|
+
print(f" Parsed {len(root)} top-level keys.")
|
|
2986
|
+
|
|
2987
|
+
# \u2500\u2500 Resolve output directory \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500
|
|
2988
|
+
if args.output_dir:
|
|
2989
|
+
out_dir = Path(args.output_dir)
|
|
2990
|
+
else:
|
|
2991
|
+
save_name = root.get('SaveName', '').strip()
|
|
2992
|
+
dir_name = slugify(save_name) if save_name else slugify(bin_path.stem)
|
|
2993
|
+
out_dir = base_dir / dir_name
|
|
2994
|
+
out_dir.mkdir(parents=True, exist_ok=True)
|
|
2995
|
+
|
|
2996
|
+
# \u2500\u2500 1. Metadata \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500
|
|
2997
|
+
META_KEYS = [
|
|
2998
|
+
'SaveName', 'EpochTime', 'Date', 'VersionNumber',
|
|
2999
|
+
'GameMode', 'GameType', 'GameComplexity',
|
|
3000
|
+
'PlayingTime', 'PlayerCounts', 'Tags',
|
|
3001
|
+
'Table', 'TableURL', 'Sky', 'SkyURL',
|
|
3002
|
+
'Note', 'Gravity', 'PlayArea',
|
|
3003
|
+
]
|
|
3004
|
+
metadata = {k: root[k] for k in META_KEYS if k in root}
|
|
3005
|
+
|
|
3006
|
+
meta_path = out_dir / 'metadata.json'
|
|
3007
|
+
meta_path.write_text(json.dumps(metadata, indent=2, ensure_ascii=False),
|
|
3008
|
+
encoding='utf-8')
|
|
3009
|
+
print(f"\\n[1] Metadata \u2192 {meta_path.name}")
|
|
3010
|
+
print(f" Save: {metadata.get('SaveName', '?')} | "
|
|
3011
|
+
f"Date: {metadata.get('Date', '?')} | "
|
|
3012
|
+
f"Version: {metadata.get('VersionNumber', '?')}")
|
|
3013
|
+
|
|
3014
|
+
# \u2500\u2500 2. Asset URLs \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500
|
|
3015
|
+
urls_by_field = extract_urls(root)
|
|
3016
|
+
all_urls_sorted = sorted({u for vs in urls_by_field.values() for u in vs})
|
|
3017
|
+
nickname_assets = collect_nickname_assets(root.get('ObjectStates', []))
|
|
3018
|
+
|
|
3019
|
+
url_json_path = out_dir / 'asset_urls.json'
|
|
3020
|
+
url_json_path.write_text(
|
|
3021
|
+
json.dumps(nickname_assets, indent=2, ensure_ascii=False),
|
|
3022
|
+
encoding='utf-8'
|
|
3023
|
+
)
|
|
3024
|
+
|
|
3025
|
+
url_txt_path = out_dir / 'asset_urls.txt'
|
|
3026
|
+
url_txt_path.write_text('\\n'.join(all_urls_sorted), encoding='utf-8')
|
|
3027
|
+
|
|
3028
|
+
print(f"\\n[2] Asset URLs \u2192 {url_json_path.name} ({url_txt_path.name})")
|
|
3029
|
+
print(f" Nicknames captured: {len(nickname_assets)}")
|
|
3030
|
+
for field, vs in sorted(urls_by_field.items()):
|
|
3031
|
+
print(f" {field:30s}: {len(vs)} URL(s)")
|
|
3032
|
+
print(f" {'TOTAL unique':30s}: {len(all_urls_sorted)}")
|
|
3033
|
+
|
|
3034
|
+
if args.download:
|
|
3035
|
+
url_to_file, failures = download_assets(
|
|
3036
|
+
nickname_assets,
|
|
3037
|
+
out_dir,
|
|
3038
|
+
workers=args.download_workers,
|
|
3039
|
+
requests_per_second=max(args.download_rps, 0.1),
|
|
3040
|
+
)
|
|
3041
|
+
asset_files = map_urls_to_files(nickname_assets, url_to_file)
|
|
3042
|
+
asset_files_path = out_dir / 'asset_files.json'
|
|
3043
|
+
asset_files_path.write_text(
|
|
3044
|
+
json.dumps(asset_files, indent=2, ensure_ascii=False),
|
|
3045
|
+
encoding='utf-8',
|
|
3046
|
+
)
|
|
3047
|
+
|
|
3048
|
+
print(f"\\n[2b] Downloaded assets \u2192 assets/ ({asset_files_path.name})")
|
|
3049
|
+
print(f" Downloaded files: {len(url_to_file)} / {len(collect_asset_urls(nickname_assets))}")
|
|
3050
|
+
if failures:
|
|
3051
|
+
print(f" Failed: {len(failures)} URL(s)")
|
|
3052
|
+
for url, reason in sorted(failures.items())[:5]:
|
|
3053
|
+
print(f" - {reason}: {url}")
|
|
3054
|
+
|
|
3055
|
+
# \u2500\u2500 3. Scripts \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500
|
|
3056
|
+
# Global scripts (root-level)
|
|
3057
|
+
global_scripts = []
|
|
3058
|
+
for field in SCRIPT_FIELD_NAMES:
|
|
3059
|
+
v = root.get(field, '')
|
|
3060
|
+
if isinstance(v, str) and v.strip():
|
|
3061
|
+
global_scripts.append({'field': field, 'guid': '', 'name': 'GLOBAL',
|
|
3062
|
+
'nickname': '', 'path': '', 'content': v})
|
|
3063
|
+
|
|
3064
|
+
# All scripts from the object tree
|
|
3065
|
+
all_scripts = global_scripts + extract_scripts(root.get('ObjectStates', []),
|
|
3066
|
+
path='ObjectStates')
|
|
3067
|
+
|
|
3068
|
+
script_index, lua_dir = write_scripts(all_scripts, out_dir)
|
|
3069
|
+
|
|
3070
|
+
index_path = out_dir / 'scripts_index.json'
|
|
3071
|
+
index_path.write_text(json.dumps(script_index, indent=2, ensure_ascii=False),
|
|
3072
|
+
encoding='utf-8')
|
|
3073
|
+
|
|
3074
|
+
lua_count = sum(1 for s in script_index if s['field'] == 'LuaScript')
|
|
3075
|
+
xml_count = sum(1 for s in script_index if s['field'] == 'XmlUI')
|
|
3076
|
+
state_count = sum(1 for s in script_index if s['field'] == 'LuaScriptState')
|
|
3077
|
+
print(f"\\n[3] Scripts \u2192 {lua_dir.relative_to(out_dir)}/")
|
|
3078
|
+
print(f" LuaScript: {lua_count} | XmlUI: {xml_count} | "
|
|
3079
|
+
f"LuaScriptState: {state_count}")
|
|
3080
|
+
|
|
3081
|
+
# \u2500\u2500 4. Object summaries \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500
|
|
3082
|
+
objects = root.get('ObjectStates', [])
|
|
3083
|
+
summaries = [summarise_object(o) for o in objects]
|
|
3084
|
+
|
|
3085
|
+
obj_path = out_dir / 'objects.json'
|
|
3086
|
+
obj_path.write_text(json.dumps(summaries, indent=2, ensure_ascii=False),
|
|
3087
|
+
encoding='utf-8')
|
|
3088
|
+
|
|
3089
|
+
# Count by object type
|
|
3090
|
+
type_counts: dict[str, int] = defaultdict(int)
|
|
3091
|
+
def count_types(obj_list):
|
|
3092
|
+
for o in obj_list:
|
|
3093
|
+
if isinstance(o, dict):
|
|
3094
|
+
type_counts[o.get('Name', 'Unknown')] += 1
|
|
3095
|
+
count_types(o.get('ContainedObjects') or [])
|
|
3096
|
+
count_types(objects)
|
|
3097
|
+
|
|
3098
|
+
print(f"\\n[4] Objects \u2192 {obj_path.name} ({len(objects)} top-level)")
|
|
3099
|
+
for t, c in sorted(type_counts.items(), key=lambda x: -x[1]):
|
|
3100
|
+
print(f" {t:30s}: {c}")
|
|
3101
|
+
|
|
3102
|
+
# \u2500\u2500 5. Full parsed save (scripts stripped) \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500
|
|
3103
|
+
stripped = strip_script_bodies(root)
|
|
3104
|
+
full_path = out_dir / 'full_save.json'
|
|
3105
|
+
full_path.write_text(json.dumps(stripped, indent=2, ensure_ascii=False),
|
|
3106
|
+
encoding='utf-8')
|
|
3107
|
+
print(f"\\n[5] Full save JSON (scripts stripped) \u2192 {full_path.name}")
|
|
3108
|
+
|
|
3109
|
+
# \u2500\u2500 6. Tab states \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500
|
|
3110
|
+
tab_states = root.get('TabStates', {})
|
|
3111
|
+
if tab_states:
|
|
3112
|
+
tab_path = out_dir / 'tab_states.json'
|
|
3113
|
+
tab_path.write_text(json.dumps(tab_states, indent=2, ensure_ascii=False),
|
|
3114
|
+
encoding='utf-8')
|
|
3115
|
+
print(f"\\n[6] Tab states \u2192 {tab_path.name} ({len(tab_states)} tabs)")
|
|
3116
|
+
|
|
3117
|
+
# \u2500\u2500 7. Snap points \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500
|
|
3118
|
+
snap_points = root.get('SnapPoints', [])
|
|
3119
|
+
if snap_points:
|
|
3120
|
+
snap_path = out_dir / 'snap_points.json'
|
|
3121
|
+
snap_path.write_text(json.dumps(snap_points, indent=2, ensure_ascii=False),
|
|
3122
|
+
encoding='utf-8')
|
|
3123
|
+
print(f"\\n[7] Snap points \u2192 {snap_path.name} ({len(snap_points)} points)")
|
|
3124
|
+
|
|
3125
|
+
print(f"\\nAll output written to: {out_dir}/")
|
|
3126
|
+
|
|
3127
|
+
|
|
3128
|
+
if __name__ == '__main__':
|
|
3129
|
+
main()
|
|
3130
|
+
`,
|
|
3131
|
+
"SKILL.md": '---\nname: dreamboard\ndescription: Create multiplayer, rule-enforced, turn-based game on Dreamboard.games platform.\nmetadata:\n short-description: Dreamboard Game Development Workflow\n tags: [dreamboard, cli, game-dev, board-game, turn-based, multiplayer]\n---\n\n# Dreamboard\n\n## Goal\n\nCreate and iterate on a Dreamboard game locally, then sync rules, scaffold, UI, and tests back to the platform.\n\n## Why Dreamboard\n\nDreamboard exists to turn a board-game idea into a playable digital prototype quickly.\n\n- Describe the game instead of hand-building every primitive first.\n- Generate rules scaffolding, components, and UI structure faster.\n- Playtest with real players without repeated print-and-play friction.\n- Iterate live while Dreamboard handles turns, hands, and multiplayer plumbing.\n\n## Prereqs\n\n- Dreamboard CLI installed and available as `dreamboard`\n- Authenticated via `dreamboard login`\n\n## Core Workflow\n\n1. Create a game: `dreamboard new <slug> --description "<<short description>>"`\n2. Author `rule.md` and `manifest.json`\n3. Run `dreamboard update` after every manifest or rule change\n4. Implement rules in `app/phases/*.ts` and UI data in `shared/ui-args.ts`\n5. Implement UI in `ui/App.tsx` and `ui/components/*`\n6. Typecheck from the game root: `bun install && bun run typecheck`\n7. Push compiled changes: `dreamboard push`\n8. Generate deterministic bases: `dreamboard test generate`\n9. Run regression scenarios: `dreamboard test run`\n10. Validate the live UI with `dreamboard run --screenshot`\n\n## Choose The Right Reference\n\nRead only the reference that matches the task:\n\n- Rules and manifest:\n [references/rule-authoring.md](references/rule-authoring.md),\n [references/manifest-authoring.md](references/manifest-authoring.md)\n- App logic and engine concepts:\n [references/phase-handlers.md](references/phase-handlers.md),\n [references/api-reference.md](references/api-reference.md),\n [references/hands-vs-decks.md](references/hands-vs-decks.md),\n [references/all-players-tracking.md](references/all-players-tracking.md),\n [references/app-best-practices.md](references/app-best-practices.md)\n- UI and UX:\n [references/ui-best-practices.md](references/ui-best-practices.md),\n [references/ui-style-guide.md](references/ui-style-guide.md),\n genre references under `references/ui-genre-*.md`\n- TypeScript regression harness (`dreamboard test generate/run`):\n [references/test-harness.md](references/test-harness.md)\n- JSON runtime scenarios (`dreamboard run --scenario ...`):\n [references/scenario-format.md](references/scenario-format.md)\n- Rejection and edge-case pressure using JSON runtime scenarios:\n [references/adversarial-testing.md](references/adversarial-testing.md)\n- TTS migration:\n [references/tts-migration-and-extractor.md](references/tts-migration-and-extractor.md)\n\n## Testing Modes\n\nThere are two different scenario systems:\n\n- `dreamboard test generate` / `dreamboard test run`\n TypeScript scenario files in `test/scenarios/*.scenario.ts` against deterministic generated base snapshots. Use this for regression coverage.\n- `dreamboard run --scenario path/to/file.json`\n JSON action scripts that drive a live session. Use this for debugging flows, reproductions, and adversarial experiments.\n\nDo not mix the two formats.\n\n## UX Bar\n\nUX is part of the implementation, not polish for later.\n\n- Make turn ownership and available actions obvious without reading logs.\n- Keep round-resolution and game-over feedback visible long enough for a player to perceive it.\n- Use `dreamboard run --screenshot` to verify initial state, active player, round resolution, and game-over feedback on realistic viewport sizes.\n\n## Guardrails\n\n- `manifest.json` and `rule.md` are the source of truth for scaffolding.\n- Run `dreamboard update` after manifest changes to keep generated files in sync.\n- `dreamboard update` is local-first and fails when remote drift is detected.\n- Use `dreamboard update --pull` only when you explicitly need to reconcile unexpected remote changes into the workspace.\n- `dreamboard test generate` should be re-run after base changes, compiled-result changes, or game identity changes.\n- Add at least one zero-step `initial-state` scenario so setup invariants fail before action tests begin.\n- Keep `scripts/events-extract.mjs` as a debugging helper for `.dreamboard/run/events.ndjson`; assertions belong in test scenarios.\n\n## Editable Surface\n\nEdit:\n\n- `app/phases/*.ts`\n- `shared/ui-args.ts`\n- `ui/App.tsx`\n- `ui/components/*`\n- `ui/sdk/components/*`\n\nDo not edit framework-owned files such as:\n\n- `app/index.ts`\n- `app/sdk/*`\n- `app/generated/*`\n- `ui/index.tsx`\n- `ui/sdk/context/*`\n- `ui/sdk/hooks/*`\n- `ui/sdk/types/*`\n\n## Framework Feedback\n\nUse `feedback.md` in the game project root to record framework issues, missing features, or workflow friction. Include reproduction steps, expected behavior, and actual behavior when possible.\n'
|
|
3132
|
+
};
|
|
3133
|
+
var SKILL_MD_CONTENT = SKILL_ASSET_FILES["SKILL.md"];
|
|
3134
|
+
|
|
3135
|
+
// src/utils/fs.ts
|
|
3136
|
+
async function ensureDir(dirPath) {
|
|
3137
|
+
await mkdir(dirPath, { recursive: true });
|
|
3138
|
+
}
|
|
3139
|
+
async function installSkillFile(projectRoot) {
|
|
3140
|
+
const skillRoot = path.join(projectRoot, ".agents", "skills", "dreamboard");
|
|
3141
|
+
for (const [relativePath, content] of Object.entries(SKILL_ASSET_FILES)) {
|
|
3142
|
+
const targetPath = path.join(skillRoot, relativePath);
|
|
3143
|
+
await ensureDir(path.dirname(targetPath));
|
|
3144
|
+
await writeFile(targetPath, content, "utf8");
|
|
3145
|
+
}
|
|
3146
|
+
}
|
|
3147
|
+
async function exists(filePath) {
|
|
3148
|
+
try {
|
|
3149
|
+
await stat(filePath);
|
|
3150
|
+
return true;
|
|
3151
|
+
} catch {
|
|
3152
|
+
return false;
|
|
3153
|
+
}
|
|
3154
|
+
}
|
|
3155
|
+
async function readTextFile(filePath) {
|
|
3156
|
+
return readFile(filePath, "utf8");
|
|
3157
|
+
}
|
|
3158
|
+
async function readTextFileIfExists(filePath) {
|
|
3159
|
+
try {
|
|
3160
|
+
return await readFile(filePath, "utf8");
|
|
3161
|
+
} catch {
|
|
3162
|
+
return null;
|
|
3163
|
+
}
|
|
3164
|
+
}
|
|
3165
|
+
async function writeTextFile(filePath, content) {
|
|
3166
|
+
await ensureDir(path.dirname(filePath));
|
|
3167
|
+
await writeFile(filePath, content, "utf8");
|
|
3168
|
+
}
|
|
3169
|
+
async function readJsonFile(filePath) {
|
|
3170
|
+
const data = await readTextFile(filePath);
|
|
3171
|
+
return JSON.parse(data);
|
|
3172
|
+
}
|
|
3173
|
+
async function writeJsonFile(filePath, data) {
|
|
3174
|
+
await writeTextFile(filePath, `${JSON.stringify(data, null, 2)}
|
|
3175
|
+
`);
|
|
3176
|
+
}
|
|
3177
|
+
|
|
3178
|
+
// src/utils/crypto.ts
|
|
3179
|
+
import crypto from "crypto";
|
|
3180
|
+
function hashContent(content) {
|
|
3181
|
+
return crypto.createHash("sha256").update(content, "utf8").digest("hex");
|
|
3182
|
+
}
|
|
3183
|
+
function getUserIdFromToken(token) {
|
|
3184
|
+
const [, payload] = token.split(".");
|
|
3185
|
+
if (!payload) throw new Error("Invalid auth token.");
|
|
3186
|
+
const decoded = JSON.parse(
|
|
3187
|
+
Buffer.from(payload, "base64").toString("utf8")
|
|
3188
|
+
);
|
|
3189
|
+
if (!decoded.sub) throw new Error("Auth token missing user id.");
|
|
3190
|
+
return decoded.sub;
|
|
3191
|
+
}
|
|
3192
|
+
|
|
3193
|
+
// src/services/project/local-files.ts
|
|
3194
|
+
import { readdir, unlink } from "fs/promises";
|
|
3195
|
+
import path2 from "path";
|
|
3196
|
+
|
|
3197
|
+
// src/constants.ts
|
|
3198
|
+
var DEFAULT_API_BASE_URL = "https://api.dreamboard.games";
|
|
3199
|
+
var DEFAULT_WEB_BASE_URL = "https://dreamboard.games";
|
|
3200
|
+
var CODE_EDITS_BUCKET = "code-edits";
|
|
3201
|
+
var PROJECT_DIR_NAME = ".dreamboard";
|
|
3202
|
+
var ENVIRONMENT_CONFIGS = {
|
|
3203
|
+
local: {
|
|
3204
|
+
apiBaseUrl: "http://localhost:8080",
|
|
3205
|
+
webBaseUrl: "http://localhost:5173",
|
|
3206
|
+
supabaseUrl: "http://127.0.0.1:54321",
|
|
3207
|
+
supabaseAnonKey: "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpc3MiOiJzdXBhYmFzZS1kZW1vIiwicm9sZSI6ImFub24iLCJleHAiOjE5ODM4MTI5OTZ9.CRXP1A7WOeoJeXxjNni43kdQwgnWNReilDMblYTn_I0"
|
|
3208
|
+
},
|
|
3209
|
+
dev: {
|
|
3210
|
+
apiBaseUrl: "https://dev-dreamboard.fly.dev",
|
|
3211
|
+
webBaseUrl: "https://dev.dreamboard.games",
|
|
3212
|
+
supabaseUrl: "https://dev-supabase.dreamboard.games",
|
|
3213
|
+
supabaseAnonKey: process.env.SUPABASE_DEV_ANON_KEY || ""
|
|
3214
|
+
},
|
|
3215
|
+
prod: {
|
|
3216
|
+
apiBaseUrl: "https://api.dreamboard.games",
|
|
3217
|
+
webBaseUrl: "https://dreamboard.games",
|
|
3218
|
+
supabaseUrl: "https://supabase.dreamboard.games",
|
|
3219
|
+
supabaseAnonKey: process.env.SUPABASE_PROD_ANON_KEY || ""
|
|
3220
|
+
}
|
|
3221
|
+
};
|
|
3222
|
+
var PROJECT_CONFIG_FILE = "project.json";
|
|
3223
|
+
var SNAPSHOT_FILE = "snapshot.json";
|
|
3224
|
+
var MANIFEST_FILE = "manifest.json";
|
|
3225
|
+
var RULE_FILE = "rule.md";
|
|
3226
|
+
var DEFAULT_LOGIN_TIMEOUT_MS = 5 * 60 * 1e3;
|
|
3227
|
+
var DEFAULT_TURN_DELAY_MS = 250;
|
|
3228
|
+
var LOCAL_IGNORE_DIRS = /* @__PURE__ */ new Set([
|
|
3229
|
+
".dreamboard",
|
|
3230
|
+
".git",
|
|
3231
|
+
"node_modules",
|
|
3232
|
+
"dist"
|
|
3233
|
+
]);
|
|
3234
|
+
|
|
3235
|
+
// src/services/project/scaffold-ownership.generated.ts
|
|
3236
|
+
var SCAFFOLD_OWNERSHIP = {
|
|
3237
|
+
version: 2,
|
|
3238
|
+
allowedPaths: {
|
|
3239
|
+
rootFiles: ["package.json", "manifest.json", "rule.md"],
|
|
3240
|
+
directoryPrefixes: ["app/", "ui/", "shared/", "test/"]
|
|
3241
|
+
},
|
|
3242
|
+
dynamic: {
|
|
3243
|
+
generatedFiles: [
|
|
3244
|
+
"shared/manifest.ts",
|
|
3245
|
+
"app/generated/guards.ts",
|
|
3246
|
+
"app/index.ts"
|
|
3247
|
+
],
|
|
3248
|
+
seedFiles: ["shared/ui-args.ts", "ui/App.tsx"],
|
|
3249
|
+
seedFilePatterns: [
|
|
3250
|
+
{
|
|
3251
|
+
prefix: "app/phases/",
|
|
3252
|
+
suffix: ".ts"
|
|
3253
|
+
}
|
|
3254
|
+
]
|
|
3255
|
+
},
|
|
3256
|
+
cliStatic: {
|
|
3257
|
+
exactFiles: [
|
|
3258
|
+
"package.json",
|
|
3259
|
+
"shared/game-message.d.ts",
|
|
3260
|
+
"shared/index.ts",
|
|
3261
|
+
"app/tsconfig.json",
|
|
3262
|
+
"ui/index.tsx",
|
|
3263
|
+
"ui/tsconfig.json",
|
|
3264
|
+
"ui/package.json"
|
|
3265
|
+
],
|
|
3266
|
+
directoryPrefixes: ["app/sdk/", "ui/sdk/"]
|
|
3267
|
+
},
|
|
3268
|
+
preservedUserFiles: ["shared/ui-args.ts"]
|
|
3269
|
+
};
|
|
3270
|
+
|
|
3271
|
+
// src/services/project/scaffold-ownership.ts
|
|
3272
|
+
function normalizeProjectPath(filePath) {
|
|
3273
|
+
return filePath.replace(/^\.\//, "").replace(/^\/+/, "").replace(/\\/g, "/");
|
|
3274
|
+
}
|
|
3275
|
+
function isAllowedGamePath(filePath) {
|
|
3276
|
+
const path3 = normalizeProjectPath(filePath);
|
|
3277
|
+
if (SCAFFOLD_OWNERSHIP.allowedPaths.rootFiles.includes(path3)) return true;
|
|
3278
|
+
return SCAFFOLD_OWNERSHIP.allowedPaths.directoryPrefixes.some(
|
|
3279
|
+
(prefix) => path3.startsWith(prefix)
|
|
3280
|
+
);
|
|
3281
|
+
}
|
|
3282
|
+
function isDynamicGeneratedPath(filePath) {
|
|
3283
|
+
const path3 = normalizeProjectPath(filePath);
|
|
3284
|
+
return SCAFFOLD_OWNERSHIP.dynamic.generatedFiles.includes(path3);
|
|
3285
|
+
}
|
|
3286
|
+
function isDynamicSeedPath(filePath) {
|
|
3287
|
+
const path3 = normalizeProjectPath(filePath);
|
|
3288
|
+
if (SCAFFOLD_OWNERSHIP.dynamic.seedFiles.includes(path3)) return true;
|
|
3289
|
+
return SCAFFOLD_OWNERSHIP.dynamic.seedFilePatterns.some(
|
|
3290
|
+
(pattern) => path3.startsWith(pattern.prefix) && path3.endsWith(pattern.suffix)
|
|
3291
|
+
);
|
|
3292
|
+
}
|
|
3293
|
+
function isCliStaticPath(filePath) {
|
|
3294
|
+
const path3 = normalizeProjectPath(filePath);
|
|
3295
|
+
if (SCAFFOLD_OWNERSHIP.cliStatic.exactFiles.includes(path3)) return true;
|
|
3296
|
+
return SCAFFOLD_OWNERSHIP.cliStatic.directoryPrefixes.some(
|
|
3297
|
+
(prefix) => path3.startsWith(prefix)
|
|
3298
|
+
);
|
|
3299
|
+
}
|
|
3300
|
+
function isLibraryPath(filePath) {
|
|
3301
|
+
return isDynamicGeneratedPath(filePath) || isCliStaticPath(filePath);
|
|
3302
|
+
}
|
|
3303
|
+
var PRESERVED_USER_FILES = new Set(
|
|
3304
|
+
SCAFFOLD_OWNERSHIP.preservedUserFiles
|
|
3305
|
+
);
|
|
3306
|
+
|
|
3307
|
+
// src/services/project/local-files.ts
|
|
3308
|
+
function isAllowedGamePath2(filePath) {
|
|
3309
|
+
return isAllowedGamePath(filePath);
|
|
3310
|
+
}
|
|
3311
|
+
function isLibraryPath2(filePath) {
|
|
3312
|
+
return isLibraryPath(filePath);
|
|
3313
|
+
}
|
|
3314
|
+
function shouldWriteScaffoldFile(filePath, existingContent) {
|
|
3315
|
+
if (isLibraryPath2(filePath)) return "write";
|
|
3316
|
+
const hasContent = existingContent !== null && existingContent.trim().length > 0;
|
|
3317
|
+
return hasContent ? "skip" : "write";
|
|
3318
|
+
}
|
|
3319
|
+
async function writeSourceFiles(rootDir, files) {
|
|
3320
|
+
for (const [relativePath, content] of Object.entries(files)) {
|
|
3321
|
+
if (content === null || content === void 0) continue;
|
|
3322
|
+
const filePath = path2.join(rootDir, relativePath);
|
|
3323
|
+
await writeTextFile(filePath, content);
|
|
3324
|
+
}
|
|
3325
|
+
}
|
|
3326
|
+
async function writeScaffoldFiles(rootDir, files) {
|
|
3327
|
+
const written = [];
|
|
3328
|
+
const skipped = [];
|
|
3329
|
+
for (const [relativePath, content] of Object.entries(files)) {
|
|
3330
|
+
if (content === null || content === void 0) continue;
|
|
3331
|
+
const fullPath = path2.join(rootDir, relativePath);
|
|
3332
|
+
const existingContent = await readTextFileIfExists(fullPath);
|
|
3333
|
+
const decision = shouldWriteScaffoldFile(relativePath, existingContent);
|
|
3334
|
+
if (decision === "skip") {
|
|
3335
|
+
skipped.push(relativePath);
|
|
3336
|
+
continue;
|
|
3337
|
+
}
|
|
3338
|
+
await writeTextFile(fullPath, content);
|
|
3339
|
+
if (existingContent !== content) {
|
|
3340
|
+
written.push(relativePath);
|
|
3341
|
+
}
|
|
3342
|
+
}
|
|
3343
|
+
written.sort();
|
|
3344
|
+
skipped.sort();
|
|
3345
|
+
return { written, skipped };
|
|
3346
|
+
}
|
|
3347
|
+
async function removeExtraneousFiles(rootDir, keep) {
|
|
3348
|
+
const localFiles = await collectLocalFiles(rootDir);
|
|
3349
|
+
for (const filePath of Object.keys(localFiles)) {
|
|
3350
|
+
if (filePath === MANIFEST_FILE || filePath === RULE_FILE) continue;
|
|
3351
|
+
if (!keep.has(filePath)) {
|
|
3352
|
+
await unlink(path2.join(rootDir, filePath));
|
|
3353
|
+
}
|
|
3354
|
+
}
|
|
3355
|
+
}
|
|
3356
|
+
async function collectLocalFiles(rootDir) {
|
|
3357
|
+
const result = {};
|
|
3358
|
+
await walkDir(rootDir, rootDir, result);
|
|
3359
|
+
return result;
|
|
3360
|
+
}
|
|
3361
|
+
async function walkDir(rootDir, currentDir, result) {
|
|
3362
|
+
const entries = await readdir(currentDir, { withFileTypes: true });
|
|
3363
|
+
for (const entry of entries) {
|
|
3364
|
+
if (entry.isDirectory()) {
|
|
3365
|
+
if (LOCAL_IGNORE_DIRS.has(entry.name)) continue;
|
|
3366
|
+
await walkDir(rootDir, path2.join(currentDir, entry.name), result);
|
|
3367
|
+
} else if (entry.isFile()) {
|
|
3368
|
+
const filePath = path2.join(currentDir, entry.name);
|
|
3369
|
+
const relativePath = path2.relative(rootDir, filePath);
|
|
3370
|
+
result[relativePath] = await readTextFile(filePath);
|
|
3371
|
+
}
|
|
3372
|
+
}
|
|
3373
|
+
}
|
|
3374
|
+
async function writeManifest(rootDir, manifest) {
|
|
3375
|
+
const filePath = path2.join(rootDir, MANIFEST_FILE);
|
|
3376
|
+
await writeTextFile(filePath, `${JSON.stringify(manifest, null, 2)}
|
|
3377
|
+
`);
|
|
3378
|
+
}
|
|
3379
|
+
async function writeRule(rootDir, ruleText) {
|
|
3380
|
+
const filePath = path2.join(rootDir, RULE_FILE);
|
|
3381
|
+
await writeTextFile(filePath, ruleText);
|
|
3382
|
+
}
|
|
3383
|
+
async function loadManifest(rootDir) {
|
|
3384
|
+
const filePath = path2.join(rootDir, MANIFEST_FILE);
|
|
3385
|
+
return readJsonFile(filePath);
|
|
3386
|
+
}
|
|
3387
|
+
async function loadRule(rootDir) {
|
|
3388
|
+
const filePath = path2.join(rootDir, RULE_FILE);
|
|
3389
|
+
return readTextFile(filePath);
|
|
3390
|
+
}
|
|
3391
|
+
async function writeSnapshot(rootDir) {
|
|
3392
|
+
const files = await collectLocalFiles(rootDir);
|
|
3393
|
+
await writeSnapshotFromFiles(rootDir, files);
|
|
3394
|
+
}
|
|
3395
|
+
async function writeSnapshotFromFiles(rootDir, files) {
|
|
3396
|
+
const snapshot = {
|
|
3397
|
+
files: {}
|
|
3398
|
+
};
|
|
3399
|
+
for (const [filePath, content] of Object.entries(files)) {
|
|
3400
|
+
if (filePath.startsWith(`${PROJECT_DIR_NAME}/`)) continue;
|
|
3401
|
+
snapshot.files[filePath] = hashContent(content);
|
|
3402
|
+
}
|
|
3403
|
+
const snapshotPath = path2.join(rootDir, PROJECT_DIR_NAME, SNAPSHOT_FILE);
|
|
3404
|
+
await writeJsonFile(snapshotPath, snapshot);
|
|
3405
|
+
}
|
|
3406
|
+
async function getLocalDiff(rootDir) {
|
|
3407
|
+
const snapshotPath = path2.join(rootDir, PROJECT_DIR_NAME, SNAPSHOT_FILE);
|
|
3408
|
+
const snapshot = await readJsonFile(snapshotPath).catch(() => null);
|
|
3409
|
+
if (!snapshot) {
|
|
3410
|
+
return { modified: [], added: [], deleted: [] };
|
|
3411
|
+
}
|
|
3412
|
+
const files = await collectLocalFiles(rootDir);
|
|
3413
|
+
const currentHashes = {};
|
|
3414
|
+
for (const [filePath, content] of Object.entries(files)) {
|
|
3415
|
+
if (filePath.startsWith(`${PROJECT_DIR_NAME}/`)) continue;
|
|
3416
|
+
currentHashes[filePath] = hashContent(content);
|
|
3417
|
+
}
|
|
3418
|
+
const modified = [];
|
|
3419
|
+
const added = [];
|
|
3420
|
+
const deleted = [];
|
|
3421
|
+
for (const [filePath, hash] of Object.entries(currentHashes)) {
|
|
3422
|
+
const prevHash = snapshot.files[filePath];
|
|
3423
|
+
if (!prevHash) {
|
|
3424
|
+
added.push(filePath);
|
|
3425
|
+
} else if (prevHash !== hash) {
|
|
3426
|
+
modified.push(filePath);
|
|
3427
|
+
}
|
|
3428
|
+
}
|
|
3429
|
+
for (const filePath of Object.keys(snapshot.files)) {
|
|
3430
|
+
if (!currentHashes[filePath]) {
|
|
3431
|
+
deleted.push(filePath);
|
|
3432
|
+
}
|
|
3433
|
+
}
|
|
3434
|
+
return { modified, added, deleted };
|
|
3435
|
+
}
|
|
3436
|
+
|
|
3437
|
+
export {
|
|
3438
|
+
consola,
|
|
3439
|
+
client,
|
|
3440
|
+
DEFAULT_API_BASE_URL,
|
|
3441
|
+
DEFAULT_WEB_BASE_URL,
|
|
3442
|
+
CODE_EDITS_BUCKET,
|
|
3443
|
+
PROJECT_DIR_NAME,
|
|
3444
|
+
ENVIRONMENT_CONFIGS,
|
|
3445
|
+
PROJECT_CONFIG_FILE,
|
|
3446
|
+
MANIFEST_FILE,
|
|
3447
|
+
RULE_FILE,
|
|
3448
|
+
DEFAULT_LOGIN_TIMEOUT_MS,
|
|
3449
|
+
DEFAULT_TURN_DELAY_MS,
|
|
3450
|
+
ensureDir,
|
|
3451
|
+
installSkillFile,
|
|
3452
|
+
exists,
|
|
3453
|
+
readTextFile,
|
|
3454
|
+
readTextFileIfExists,
|
|
3455
|
+
writeTextFile,
|
|
3456
|
+
readJsonFile,
|
|
3457
|
+
writeJsonFile,
|
|
3458
|
+
hashContent,
|
|
3459
|
+
getUserIdFromToken,
|
|
3460
|
+
isAllowedGamePath,
|
|
3461
|
+
isDynamicGeneratedPath,
|
|
3462
|
+
isDynamicSeedPath,
|
|
3463
|
+
isLibraryPath,
|
|
3464
|
+
PRESERVED_USER_FILES,
|
|
3465
|
+
isAllowedGamePath2,
|
|
3466
|
+
isLibraryPath2,
|
|
3467
|
+
writeSourceFiles,
|
|
3468
|
+
writeScaffoldFiles,
|
|
3469
|
+
removeExtraneousFiles,
|
|
3470
|
+
collectLocalFiles,
|
|
3471
|
+
writeManifest,
|
|
3472
|
+
writeRule,
|
|
3473
|
+
loadManifest,
|
|
3474
|
+
loadRule,
|
|
3475
|
+
writeSnapshot,
|
|
3476
|
+
writeSnapshotFromFiles,
|
|
3477
|
+
getLocalDiff
|
|
3478
|
+
};
|
|
3479
|
+
//# sourceMappingURL=chunk-FK6CWXQR.js.map
|