@dreamboard-games/cli 0.1.30-alpha.2 → 0.1.30-alpha.21
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 +25 -107
- package/dist/agent-verifier/agent-workspace-verifier.mjs +1682 -0
- package/dist/agent-verifier/agent-workspace-verifier.mjs.map +1 -0
- package/dist/agent-verifier/chunk-22U6RMWO.mjs +722 -0
- package/dist/agent-verifier/chunk-22U6RMWO.mjs.map +1 -0
- package/dist/agent-verifier/chunk-4I2WWAPK.mjs +399 -0
- package/dist/agent-verifier/chunk-4I2WWAPK.mjs.map +1 -0
- package/dist/agent-verifier/chunk-BWBN2TDJ.mjs +2811 -0
- package/dist/agent-verifier/chunk-BWBN2TDJ.mjs.map +1 -0
- package/dist/agent-verifier/chunk-DQUYBIGQ.mjs +353 -0
- package/dist/agent-verifier/chunk-DQUYBIGQ.mjs.map +1 -0
- package/dist/agent-verifier/chunk-F2DIOJJZ.mjs +302 -0
- package/dist/agent-verifier/chunk-F2DIOJJZ.mjs.map +1 -0
- package/dist/agent-verifier/chunk-GWRZRWCF.mjs +107 -0
- package/dist/agent-verifier/chunk-GWRZRWCF.mjs.map +1 -0
- package/dist/agent-verifier/chunk-H6XDQJ3N.mjs +11 -0
- package/dist/agent-verifier/chunk-HUBV22JQ.mjs +89 -0
- package/dist/agent-verifier/chunk-HUBV22JQ.mjs.map +1 -0
- package/dist/agent-verifier/chunk-IWB4L2HV.mjs +273 -0
- package/dist/agent-verifier/chunk-IWB4L2HV.mjs.map +1 -0
- package/dist/agent-verifier/chunk-JZTH3EMV.mjs +14523 -0
- package/dist/agent-verifier/chunk-JZTH3EMV.mjs.map +1 -0
- package/dist/agent-verifier/chunk-KDAQ4CZY.mjs +192 -0
- package/dist/agent-verifier/chunk-KDAQ4CZY.mjs.map +1 -0
- package/dist/agent-verifier/chunk-M7UVBANQ.mjs +58 -0
- package/dist/agent-verifier/chunk-M7UVBANQ.mjs.map +1 -0
- package/dist/agent-verifier/chunk-MYMVXTZT.mjs +766 -0
- package/dist/agent-verifier/chunk-MYMVXTZT.mjs.map +1 -0
- package/dist/agent-verifier/chunk-OJFZVGEL.mjs +492 -0
- package/dist/agent-verifier/chunk-OJFZVGEL.mjs.map +1 -0
- package/dist/agent-verifier/chunk-QD4SQNUP.mjs +75 -0
- package/dist/agent-verifier/chunk-QD4SQNUP.mjs.map +1 -0
- package/dist/agent-verifier/chunk-RDYXWXXC.mjs +47 -0
- package/dist/agent-verifier/chunk-RDYXWXXC.mjs.map +1 -0
- package/dist/agent-verifier/chunk-TIDX3YLW.mjs +158 -0
- package/dist/agent-verifier/chunk-TIDX3YLW.mjs.map +1 -0
- package/dist/agent-verifier/chunk-TTB7AIHZ.mjs +214 -0
- package/dist/agent-verifier/chunk-TTB7AIHZ.mjs.map +1 -0
- package/dist/agent-verifier/chunk-UXGTT25Q.mjs +59 -0
- package/dist/agent-verifier/chunk-UXGTT25Q.mjs.map +1 -0
- package/dist/agent-verifier/chunk-YE7UAO3T.mjs +129 -0
- package/dist/agent-verifier/chunk-YE7UAO3T.mjs.map +1 -0
- package/dist/agent-verifier/chunk-ZEELHSY3.mjs +20 -0
- package/dist/agent-verifier/chunk-ZEELHSY3.mjs.map +1 -0
- package/dist/agent-verifier/global-config-IXZLY4BS.mjs +19 -0
- package/dist/agent-verifier/keychain-backend-UF3Z26JM.mjs +140 -0
- package/dist/agent-verifier/keychain-backend-UF3Z26JM.mjs.map +1 -0
- package/dist/agent-verifier/local-files-OF4QFISU.mjs +45 -0
- package/dist/agent-verifier/local-files-OF4QFISU.mjs.map +1 -0
- package/dist/agent-verifier/local-typecheck-DHVLM37Z.mjs +150 -0
- package/dist/agent-verifier/local-typecheck-DHVLM37Z.mjs.map +1 -0
- package/dist/agent-verifier/materialize-workspace-7DFE45ZH.mjs +90 -0
- package/dist/agent-verifier/materialize-workspace-7DFE45ZH.mjs.map +1 -0
- package/dist/agent-verifier/project-state-XKUSCFSV.mjs +33 -0
- package/dist/agent-verifier/project-state-XKUSCFSV.mjs.map +1 -0
- package/dist/agent-verifier/prompt-VKHMCQT6.mjs +756 -0
- package/dist/agent-verifier/prompt-VKHMCQT6.mjs.map +1 -0
- package/dist/agent-verifier/reducer-bundle-preflight-GLUJKTWU.mjs +312 -0
- package/dist/agent-verifier/reducer-bundle-preflight-GLUJKTWU.mjs.map +1 -0
- package/dist/agent-verifier/reducer-contract-preflight-WVQQPW5F.mjs +46 -0
- package/dist/agent-verifier/reducer-contract-preflight-WVQQPW5F.mjs.map +1 -0
- package/dist/agent-verifier/reducer-native-test-harness-H6G6RBRY.mjs +3436 -0
- package/dist/agent-verifier/reducer-native-test-harness-H6G6RBRY.mjs.map +1 -0
- package/dist/agent-verifier/static-scaffold-CC4KL6K7.mjs +24 -0
- package/dist/agent-verifier/static-scaffold-CC4KL6K7.mjs.map +1 -0
- package/dist/agent-verifier/workspace-codegen-SPPVHURX.mjs +10 -0
- package/dist/agent-verifier/workspace-codegen-SPPVHURX.mjs.map +1 -0
- package/dist/agent-verifier/workspace-dependencies-5HEEKZFP.mjs +17 -0
- package/dist/agent-verifier/workspace-dependencies-5HEEKZFP.mjs.map +1 -0
- package/dist/authoring-compatibility-internal.js +12 -0
- package/dist/authoring-compatibility-internal.js.map +1 -0
- package/dist/chunk-2H7UOFLK.js +11 -0
- package/dist/chunk-2H7UOFLK.js.map +1 -0
- package/dist/{chunk-TAQKH67O.js → chunk-2Z65YI7P.js} +2702 -7338
- package/dist/chunk-2Z65YI7P.js.map +1 -0
- package/dist/chunk-EQNBQVIW.js +204 -0
- package/dist/chunk-EQNBQVIW.js.map +1 -0
- package/dist/chunk-GQ3ZEAEG.js +4281 -0
- package/dist/chunk-GQ3ZEAEG.js.map +1 -0
- package/dist/chunk-UI7NWSYA.js +334 -0
- package/dist/chunk-UI7NWSYA.js.map +1 -0
- package/dist/{global-config-S4ZIPECE.js → global-config-GK2UC2X6.js} +4 -3
- package/dist/global-config-GK2UC2X6.js.map +1 -0
- package/dist/index.js +3421 -6435
- package/dist/index.js.map +1 -1
- package/dist/internal.js +15 -9
- package/dist/{keychain-backend-HDF4TZDL.js → keychain-backend-GO34KGTG.js} +12 -7
- package/dist/keychain-backend-GO34KGTG.js.map +1 -0
- package/dist/{prompt-NDV3AE5L.js → prompt-GMZABCJC.js} +2 -2
- package/package.json +11 -20
- package/release/authoring-release-set.json +38 -0
- package/skills/dreamboard/SKILL.md +30 -28
- package/skills/dreamboard/references/building-your-first-game.md +15 -15
- package/skills/dreamboard/references/cli.md +48 -47
- package/skills/dreamboard/references/manifest-authoring.md +11 -3
- package/skills/dreamboard/references/quickstart.md +16 -13
- package/skills/dreamboard/references/testing.md +10 -10
- package/dist/chunk-N7XPNNUI.js +0 -432
- package/dist/chunk-N7XPNNUI.js.map +0 -1
- package/dist/chunk-SEGVTWSK.js +0 -44
- package/dist/chunk-TAQKH67O.js.map +0 -1
- package/dist/dev-host/components/drawer.tsx +0 -132
- package/dist/dev-host/components/input.tsx +0 -21
- package/dist/dev-host/dev-api-proxy-plugin.ts +0 -328
- package/dist/dev-host/dev-author-dom-warnings.ts +0 -100
- package/dist/dev-host/dev-diagnostics.ts +0 -62
- package/dist/dev-host/dev-fallback-stylesheet.ts +0 -53
- package/dist/dev-host/dev-hmr-guard-plugin.ts +0 -47
- package/dist/dev-host/dev-host-controller.ts +0 -674
- package/dist/dev-host/dev-host-player-query.ts +0 -17
- package/dist/dev-host/dev-host-session-transport.ts +0 -52
- package/dist/dev-host/dev-host-storage.ts +0 -56
- package/dist/dev-host/dev-log-relay-plugin.ts +0 -510
- package/dist/dev-host/dev-runtime-config.ts +0 -14
- package/dist/dev-host/dev-runtime-platform.ts +0 -335
- package/dist/dev-host/dev-virtual-modules-plugin.ts +0 -64
- package/dist/dev-host/host-main.css +0 -224
- package/dist/dev-host/host-main.tsx +0 -948
- package/dist/dev-host/index.html +0 -56
- package/dist/dev-host/lib/utils.ts +0 -6
- package/dist/dev-host/plugin-main.ts +0 -61
- package/dist/dev-host/plugin.html +0 -24
- package/dist/dev-host/shared-styles.css +0 -144
- package/dist/dev-host/start-dev-server.ts +0 -140
- package/dist/dev-host/virtual-modules.d.ts +0 -27
- package/dist/keychain-backend-HDF4TZDL.js.map +0 -1
- package/skills/dreamboard/scripts/events-extract.mjs +0 -218
- /package/dist/{chunk-SEGVTWSK.js.map → agent-verifier/chunk-H6XDQJ3N.mjs.map} +0 -0
- /package/dist/{global-config-S4ZIPECE.js.map → agent-verifier/global-config-IXZLY4BS.mjs.map} +0 -0
- /package/dist/{prompt-NDV3AE5L.js.map → prompt-GMZABCJC.js.map} +0 -0
- /package/{dist/scaffold → scaffold}/assets/static/app/tsconfig.framework.json +0 -0
- /package/{dist/scaffold → scaffold}/assets/static/app/tsconfig.json +0 -0
- /package/{dist/scaffold → scaffold}/assets/static/ui/index.tsx +0 -0
- /package/{dist/scaffold → scaffold}/assets/static/ui/style.css +0 -0
- /package/{dist/scaffold → scaffold}/assets/static/ui/tsconfig.framework.json +0 -0
- /package/{dist/scaffold → scaffold}/assets/static/ui/tsconfig.json +0 -0
|
@@ -0,0 +1,302 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
|
|
3
|
+
// src/templates/testing-types-content.ts
|
|
4
|
+
var DEFAULT_REJECTION_CODES = [
|
|
5
|
+
"NOT_YOUR_TURN",
|
|
6
|
+
"action-unavailable",
|
|
7
|
+
"invalid-action-params",
|
|
8
|
+
"prompt-not-owned"
|
|
9
|
+
];
|
|
10
|
+
function renderLiteralUnion(values) {
|
|
11
|
+
if (values.length === 0) {
|
|
12
|
+
return "never";
|
|
13
|
+
}
|
|
14
|
+
return values.map((value) => JSON.stringify(value)).join(" | ");
|
|
15
|
+
}
|
|
16
|
+
var REDUCER_TESTING_TYPES_WRAPPER_CONTENT = `// Generated by dreamboard \u2014 do not edit by hand.
|
|
17
|
+
import game from "../app/game";
|
|
18
|
+
import {
|
|
19
|
+
contractFingerprint,
|
|
20
|
+
createReducerBundle,
|
|
21
|
+
} from "@dreamboard-games/sdk/reducer";
|
|
22
|
+
import { createTestRuntime as createDreamboardTestRuntime } from "@dreamboard-games/sdk/testing";
|
|
23
|
+
import type { CreateTestRuntimeOptions } from "@dreamboard-games/sdk/testing";
|
|
24
|
+
import { literals } from "../shared/manifest-contract";
|
|
25
|
+
import type { PhaseName } from "../shared/generated/ui-contract";
|
|
26
|
+
import {
|
|
27
|
+
BASE_STATES,
|
|
28
|
+
BASE_STATES_CONTRACT_FINGERPRINT,
|
|
29
|
+
} from "./generated/base-states.generated";
|
|
30
|
+
import type {
|
|
31
|
+
BaseDefinition,
|
|
32
|
+
ScenarioDefinition,
|
|
33
|
+
TestRunner,
|
|
34
|
+
} from "./generated/testing-contract";
|
|
35
|
+
|
|
36
|
+
export * from "./generated/testing-contract";
|
|
37
|
+
|
|
38
|
+
/**
|
|
39
|
+
* Workspace-narrowed \`defineBase\` wrapper. Accepts the generated
|
|
40
|
+
* \`BaseDefinition\` so \`setup({ seat, game })\` is typed against the
|
|
41
|
+
* workspace's player ids and interaction contract.
|
|
42
|
+
*/
|
|
43
|
+
export function defineBase<const Definition extends BaseDefinition>(
|
|
44
|
+
definition: Definition,
|
|
45
|
+
): Definition {
|
|
46
|
+
return definition;
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
/**
|
|
50
|
+
* Workspace-narrowed \`defineScenario\` wrapper. The generated
|
|
51
|
+
* \`ScenarioDefinition\` narrows \`ctx.view(playerId)\` in \`then\` based on
|
|
52
|
+
* the declared \`phase\`, keeps \`when\` union-typed, and constrains
|
|
53
|
+
* \`phase\` / \`stage\` to the manifest-derived literal types.
|
|
54
|
+
*/
|
|
55
|
+
export function defineScenario<
|
|
56
|
+
const Runners extends readonly TestRunner[] = readonly ["reducer"],
|
|
57
|
+
const Phase extends PhaseName | undefined = undefined,
|
|
58
|
+
>(
|
|
59
|
+
definition: ScenarioDefinition<Runners, Phase>,
|
|
60
|
+
): ScenarioDefinition<Runners, Phase> {
|
|
61
|
+
return definition;
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
export function createTestRuntime(options: {
|
|
65
|
+
baseId: keyof typeof BASE_STATES & string;
|
|
66
|
+
phase?: PhaseName;
|
|
67
|
+
controllingPlayerId?: (typeof literals.playerIds)[number];
|
|
68
|
+
userId?: string | null;
|
|
69
|
+
}) {
|
|
70
|
+
const reducerBundle =
|
|
71
|
+
createReducerBundle(game) satisfies CreateTestRuntimeOptions["bundle"];
|
|
72
|
+
const baseStates =
|
|
73
|
+
BASE_STATES satisfies CreateTestRuntimeOptions["baseStates"];
|
|
74
|
+
const runtime = createDreamboardTestRuntime({
|
|
75
|
+
baseId: options.baseId,
|
|
76
|
+
baseStates,
|
|
77
|
+
bundle: reducerBundle,
|
|
78
|
+
contractFingerprint: contractFingerprint(game).value,
|
|
79
|
+
expectedBaseStateFingerprint: BASE_STATES_CONTRACT_FINGERPRINT,
|
|
80
|
+
phase: options.phase,
|
|
81
|
+
userId: options.userId ?? "test-user",
|
|
82
|
+
playerIds: literals.playerIds.slice(
|
|
83
|
+
0,
|
|
84
|
+
BASE_STATES[options.baseId]?.fingerprint.players ?? literals.playerIds.length,
|
|
85
|
+
),
|
|
86
|
+
});
|
|
87
|
+
|
|
88
|
+
if (options.controllingPlayerId) {
|
|
89
|
+
runtime.setControllingPlayer(options.controllingPlayerId);
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
return runtime;
|
|
93
|
+
}
|
|
94
|
+
`;
|
|
95
|
+
function buildReducerTestingContractContent(options = {}) {
|
|
96
|
+
const rejectionCodes = Array.from(
|
|
97
|
+
/* @__PURE__ */ new Set([...options.rejectionCodes ?? [], ...DEFAULT_REJECTION_CODES])
|
|
98
|
+
).sort((left, right) => left.localeCompare(right));
|
|
99
|
+
return `// Generated by dreamboard \u2014 do not edit by hand.
|
|
100
|
+
import type game from "../../app/game";
|
|
101
|
+
import { literals, type SetupProfileId } from "../../shared/manifest-contract";
|
|
102
|
+
import {
|
|
103
|
+
type GameView,
|
|
104
|
+
type InteractionId,
|
|
105
|
+
type InteractionKey,
|
|
106
|
+
type InteractionParamsOf,
|
|
107
|
+
type PhaseName,
|
|
108
|
+
type StageName as WorkspaceStageName,
|
|
109
|
+
} from "../../shared/generated/ui-contract";
|
|
110
|
+
import type {
|
|
111
|
+
ExpectFn as SharedExpectFn,
|
|
112
|
+
TestRunner as SharedTestRunner,
|
|
113
|
+
} from "@dreamboard-games/sdk/testing";
|
|
114
|
+
import type { InteractionDescriptor } from "@dreamboard-games/sdk/runtime";
|
|
115
|
+
import { BASE_STATES } from "./base-states.generated";
|
|
116
|
+
|
|
117
|
+
export type GameDefinition = typeof game;
|
|
118
|
+
export type PlayerId = (typeof literals.playerIds)[number];
|
|
119
|
+
export type StateName = PhaseName;
|
|
120
|
+
export type BaseId = keyof typeof BASE_STATES & string;
|
|
121
|
+
export type InteractionDescriptorFor<Id extends string = string> =
|
|
122
|
+
InteractionDescriptor<Id>;
|
|
123
|
+
export type InteractionExplanation = {
|
|
124
|
+
interactionId: string;
|
|
125
|
+
phase: string;
|
|
126
|
+
step: string | null;
|
|
127
|
+
availability:
|
|
128
|
+
| "available"
|
|
129
|
+
| "notYourTurn"
|
|
130
|
+
| "wrongPhase"
|
|
131
|
+
| "wrongStep"
|
|
132
|
+
| "blocked";
|
|
133
|
+
rules: ReadonlyArray<{
|
|
134
|
+
ruleId: string;
|
|
135
|
+
outcome: "passed" | "failed" | "notEvaluated";
|
|
136
|
+
errorCode?: string;
|
|
137
|
+
message?: string;
|
|
138
|
+
}>;
|
|
139
|
+
actor: { required: readonly string[]; playerIsActor: boolean };
|
|
140
|
+
inputs: ReadonlyArray<{
|
|
141
|
+
key: string;
|
|
142
|
+
kind: string;
|
|
143
|
+
eligibleCount: number | "lazy";
|
|
144
|
+
}>;
|
|
145
|
+
};
|
|
146
|
+
export type TestRunner = SharedTestRunner;
|
|
147
|
+
export type ExpectFn = SharedExpectFn;
|
|
148
|
+
export type KnownRejectionCode = ${renderLiteralUnion(rejectionCodes)};
|
|
149
|
+
export type RejectionCode = [KnownRejectionCode] extends [never]
|
|
150
|
+
? string
|
|
151
|
+
: KnownRejectionCode;
|
|
152
|
+
|
|
153
|
+
type DefaultRunners = readonly ["reducer"];
|
|
154
|
+
type PhaseTaggedView<Phase extends PhaseName> = Extract<
|
|
155
|
+
GameView,
|
|
156
|
+
{ phase: Phase } | { currentPhase: Phase } | { state: Phase }
|
|
157
|
+
>;
|
|
158
|
+
type NarrowedView<Phase extends PhaseName> = [PhaseTaggedView<Phase>] extends [never]
|
|
159
|
+
? GameView
|
|
160
|
+
: PhaseTaggedView<Phase>;
|
|
161
|
+
|
|
162
|
+
export type ViewByPhase = {
|
|
163
|
+
[Phase in PhaseName]: NarrowedView<Phase>;
|
|
164
|
+
};
|
|
165
|
+
|
|
166
|
+
type InteractionKeyForId<Id extends InteractionId> = Extract<
|
|
167
|
+
InteractionKey,
|
|
168
|
+
\`\${string}.\${Id}\`
|
|
169
|
+
>;
|
|
170
|
+
type InteractionParamsForKey<Key extends InteractionKey> =
|
|
171
|
+
Key extends InteractionKey ? InteractionParamsOf<Key> : never;
|
|
172
|
+
type InteractionParamsOfId<Id extends InteractionId> =
|
|
173
|
+
InteractionParamsForKey<InteractionKeyForId<Id>>;
|
|
174
|
+
|
|
175
|
+
export interface BrowserRunnerSnapshot {
|
|
176
|
+
sessionId: string | null;
|
|
177
|
+
shortCode: string | null;
|
|
178
|
+
version: number;
|
|
179
|
+
currentPhase: string | null;
|
|
180
|
+
controllingPlayerId: string;
|
|
181
|
+
controllablePlayerIds: string[];
|
|
182
|
+
view: unknown;
|
|
183
|
+
availableInteractions?: string[];
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
export interface BrowserRunnerBridge {
|
|
187
|
+
snapshot(): Promise<BrowserRunnerSnapshot>;
|
|
188
|
+
submitInteraction(
|
|
189
|
+
playerId: PlayerId,
|
|
190
|
+
interactionId: string,
|
|
191
|
+
params: unknown,
|
|
192
|
+
): Promise<void>;
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
export interface BrowserRunnerDriver {
|
|
196
|
+
onReady?(bridge: BrowserRunnerBridge): Promise<void> | void;
|
|
197
|
+
interaction?(
|
|
198
|
+
bridge: BrowserRunnerBridge,
|
|
199
|
+
input: { playerId: PlayerId; interactionId: string; params: unknown },
|
|
200
|
+
): Promise<boolean | void> | boolean | void;
|
|
201
|
+
}
|
|
202
|
+
|
|
203
|
+
export interface ScenarioGameApi {
|
|
204
|
+
start(): Promise<void>;
|
|
205
|
+
/**
|
|
206
|
+
* Patch the reducer snapshot for deterministic setup-heavy scenarios.
|
|
207
|
+
* This is limited to reducer snapshot materialization and is rejected by
|
|
208
|
+
* live replay/browser runners so authored gameplay verification still
|
|
209
|
+
* submits real interactions.
|
|
210
|
+
*/
|
|
211
|
+
patchState(mutator: (state: Record<string, unknown>) => void): Promise<void>;
|
|
212
|
+
/**
|
|
213
|
+
* Submit a player interaction (action-kind or prompt-kind) to the game.
|
|
214
|
+
* The \`interactionId\` matches an \`InteractionId\` from the generated
|
|
215
|
+
* \`ui-contract\`; \`params\` is typed per interaction id.
|
|
216
|
+
*/
|
|
217
|
+
submit<Id extends InteractionId>(
|
|
218
|
+
playerId: PlayerId,
|
|
219
|
+
interactionId: Id,
|
|
220
|
+
params?: InteractionParamsOfId<Id>,
|
|
221
|
+
): Promise<void>;
|
|
222
|
+
}
|
|
223
|
+
|
|
224
|
+
export interface BaseContext {
|
|
225
|
+
game: ScenarioGameApi;
|
|
226
|
+
players(): readonly PlayerId[];
|
|
227
|
+
/**
|
|
228
|
+
* Resolve the seat at \`index\` in the base's players list.
|
|
229
|
+
* Throws if the index is out of range. Prefer \`seat(0)\`/\`seat(1)\` over
|
|
230
|
+
* literal player ids so bases stay portable across player counts and
|
|
231
|
+
* we never hard-code wire-shape assumptions like "player-1".
|
|
232
|
+
*/
|
|
233
|
+
seat(index: number): PlayerId;
|
|
234
|
+
}
|
|
235
|
+
|
|
236
|
+
export interface SharedScenarioContext {
|
|
237
|
+
game: ScenarioGameApi;
|
|
238
|
+
players(): readonly PlayerId[];
|
|
239
|
+
/**
|
|
240
|
+
* Resolve the seat at \`index\` in the current scenario's players list.
|
|
241
|
+
* Throws if the index is out of range. Prefer \`seat(0)\`/\`seat(1)\` over
|
|
242
|
+
* literal player ids so scenarios stay portable across player counts and
|
|
243
|
+
* we never hard-code wire-shape assumptions like "player-1".
|
|
244
|
+
*/
|
|
245
|
+
seat(index: number): PlayerId;
|
|
246
|
+
state(): StateName;
|
|
247
|
+
view(playerId: PlayerId): GameView;
|
|
248
|
+
interactions(playerId: PlayerId): readonly InteractionDescriptorFor[];
|
|
249
|
+
explain(playerId: PlayerId, interactionId: InteractionId): InteractionExplanation;
|
|
250
|
+
expect: ExpectFn;
|
|
251
|
+
}
|
|
252
|
+
|
|
253
|
+
export type ScenarioContext<
|
|
254
|
+
Phase extends PhaseName | undefined = undefined,
|
|
255
|
+
> = Omit<SharedScenarioContext, "state" | "view"> & {
|
|
256
|
+
state(): Phase extends PhaseName ? Phase : StateName;
|
|
257
|
+
view(playerId: PlayerId): Phase extends PhaseName ? ViewByPhase[Phase] : GameView;
|
|
258
|
+
};
|
|
259
|
+
|
|
260
|
+
export type ScenarioThenContext<
|
|
261
|
+
_Runners extends readonly TestRunner[] = DefaultRunners,
|
|
262
|
+
Phase extends PhaseName | undefined = undefined,
|
|
263
|
+
> = ScenarioContext<Phase>;
|
|
264
|
+
|
|
265
|
+
export interface BaseDefinition {
|
|
266
|
+
id: string;
|
|
267
|
+
seed?: number;
|
|
268
|
+
players?: number;
|
|
269
|
+
setupProfileId?: SetupProfileId;
|
|
270
|
+
extends?: BaseId | string;
|
|
271
|
+
setup: (ctx: BaseContext) => void | Promise<void>;
|
|
272
|
+
}
|
|
273
|
+
|
|
274
|
+
export interface ScenarioDefinition<
|
|
275
|
+
Runners extends readonly TestRunner[] = DefaultRunners,
|
|
276
|
+
Phase extends PhaseName | undefined = undefined,
|
|
277
|
+
> {
|
|
278
|
+
id: string;
|
|
279
|
+
description?: string;
|
|
280
|
+
from: BaseId | string;
|
|
281
|
+
runners?: Runners;
|
|
282
|
+
phase?: Phase;
|
|
283
|
+
stage?: Phase extends PhaseName ? WorkspaceStageName<Phase> : never;
|
|
284
|
+
when: (ctx: ScenarioContext<Phase>) => void | Promise<void>;
|
|
285
|
+
then: (ctx: ScenarioThenContext<Runners, Phase>) => void | Promise<void>;
|
|
286
|
+
}
|
|
287
|
+
|
|
288
|
+
export type {
|
|
289
|
+
GameView,
|
|
290
|
+
InteractionId,
|
|
291
|
+
InteractionParamsOf,
|
|
292
|
+
PhaseName,
|
|
293
|
+
WorkspaceStageName,
|
|
294
|
+
};
|
|
295
|
+
`;
|
|
296
|
+
}
|
|
297
|
+
|
|
298
|
+
export {
|
|
299
|
+
REDUCER_TESTING_TYPES_WRAPPER_CONTENT,
|
|
300
|
+
buildReducerTestingContractContent
|
|
301
|
+
};
|
|
302
|
+
//# sourceMappingURL=chunk-F2DIOJJZ.mjs.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../../src/templates/testing-types-content.ts"],"sourcesContent":["const DEFAULT_REJECTION_CODES = [\n \"NOT_YOUR_TURN\",\n \"action-unavailable\",\n \"invalid-action-params\",\n \"prompt-not-owned\",\n] as const;\n\nfunction renderLiteralUnion(values: readonly string[]): string {\n if (values.length === 0) {\n return \"never\";\n }\n return values.map((value) => JSON.stringify(value)).join(\" | \");\n}\n\nexport const REDUCER_TESTING_TYPES_WRAPPER_CONTENT = `\\\n// Generated by dreamboard — do not edit by hand.\nimport game from \"../app/game\";\nimport {\n contractFingerprint,\n createReducerBundle,\n} from \"@dreamboard-games/sdk/reducer\";\nimport { createTestRuntime as createDreamboardTestRuntime } from \"@dreamboard-games/sdk/testing\";\nimport type { CreateTestRuntimeOptions } from \"@dreamboard-games/sdk/testing\";\nimport { literals } from \"../shared/manifest-contract\";\nimport type { PhaseName } from \"../shared/generated/ui-contract\";\nimport {\n BASE_STATES,\n BASE_STATES_CONTRACT_FINGERPRINT,\n} from \"./generated/base-states.generated\";\nimport type {\n BaseDefinition,\n ScenarioDefinition,\n TestRunner,\n} from \"./generated/testing-contract\";\n\nexport * from \"./generated/testing-contract\";\n\n/**\n * Workspace-narrowed \\`defineBase\\` wrapper. Accepts the generated\n * \\`BaseDefinition\\` so \\`setup({ seat, game })\\` is typed against the\n * workspace's player ids and interaction contract.\n */\nexport function defineBase<const Definition extends BaseDefinition>(\n definition: Definition,\n): Definition {\n return definition;\n}\n\n/**\n * Workspace-narrowed \\`defineScenario\\` wrapper. The generated\n * \\`ScenarioDefinition\\` narrows \\`ctx.view(playerId)\\` in \\`then\\` based on\n * the declared \\`phase\\`, keeps \\`when\\` union-typed, and constrains\n * \\`phase\\` / \\`stage\\` to the manifest-derived literal types.\n */\nexport function defineScenario<\n const Runners extends readonly TestRunner[] = readonly [\"reducer\"],\n const Phase extends PhaseName | undefined = undefined,\n>(\n definition: ScenarioDefinition<Runners, Phase>,\n): ScenarioDefinition<Runners, Phase> {\n return definition;\n}\n\nexport function createTestRuntime(options: {\n baseId: keyof typeof BASE_STATES & string;\n phase?: PhaseName;\n controllingPlayerId?: (typeof literals.playerIds)[number];\n userId?: string | null;\n}) {\n const reducerBundle =\n createReducerBundle(game) satisfies CreateTestRuntimeOptions[\"bundle\"];\n const baseStates =\n BASE_STATES satisfies CreateTestRuntimeOptions[\"baseStates\"];\n const runtime = createDreamboardTestRuntime({\n baseId: options.baseId,\n baseStates,\n bundle: reducerBundle,\n contractFingerprint: contractFingerprint(game).value,\n expectedBaseStateFingerprint: BASE_STATES_CONTRACT_FINGERPRINT,\n phase: options.phase,\n userId: options.userId ?? \"test-user\",\n playerIds: literals.playerIds.slice(\n 0,\n BASE_STATES[options.baseId]?.fingerprint.players ?? literals.playerIds.length,\n ),\n });\n\n if (options.controllingPlayerId) {\n runtime.setControllingPlayer(options.controllingPlayerId);\n }\n\n return runtime;\n}\n`;\n\nexport function buildReducerTestingContractContent(\n options: {\n rejectionCodes?: readonly string[];\n } = {},\n): string {\n const rejectionCodes = Array.from(\n new Set([...(options.rejectionCodes ?? []), ...DEFAULT_REJECTION_CODES]),\n ).sort((left, right) => left.localeCompare(right));\n\n return `\\\n// Generated by dreamboard — do not edit by hand.\nimport type game from \"../../app/game\";\nimport { literals, type SetupProfileId } from \"../../shared/manifest-contract\";\nimport {\n type GameView,\n type InteractionId,\n type InteractionKey,\n type InteractionParamsOf,\n type PhaseName,\n type StageName as WorkspaceStageName,\n} from \"../../shared/generated/ui-contract\";\nimport type {\n ExpectFn as SharedExpectFn,\n TestRunner as SharedTestRunner,\n} from \"@dreamboard-games/sdk/testing\";\nimport type { InteractionDescriptor } from \"@dreamboard-games/sdk/runtime\";\nimport { BASE_STATES } from \"./base-states.generated\";\n\nexport type GameDefinition = typeof game;\nexport type PlayerId = (typeof literals.playerIds)[number];\nexport type StateName = PhaseName;\nexport type BaseId = keyof typeof BASE_STATES & string;\nexport type InteractionDescriptorFor<Id extends string = string> =\n InteractionDescriptor<Id>;\nexport type InteractionExplanation = {\n interactionId: string;\n phase: string;\n step: string | null;\n availability:\n | \"available\"\n | \"notYourTurn\"\n | \"wrongPhase\"\n | \"wrongStep\"\n | \"blocked\";\n rules: ReadonlyArray<{\n ruleId: string;\n outcome: \"passed\" | \"failed\" | \"notEvaluated\";\n errorCode?: string;\n message?: string;\n }>;\n actor: { required: readonly string[]; playerIsActor: boolean };\n inputs: ReadonlyArray<{\n key: string;\n kind: string;\n eligibleCount: number | \"lazy\";\n }>;\n};\nexport type TestRunner = SharedTestRunner;\nexport type ExpectFn = SharedExpectFn;\nexport type KnownRejectionCode = ${renderLiteralUnion(rejectionCodes)};\nexport type RejectionCode = [KnownRejectionCode] extends [never]\n ? string\n : KnownRejectionCode;\n\ntype DefaultRunners = readonly [\"reducer\"];\ntype PhaseTaggedView<Phase extends PhaseName> = Extract<\n GameView,\n { phase: Phase } | { currentPhase: Phase } | { state: Phase }\n>;\ntype NarrowedView<Phase extends PhaseName> = [PhaseTaggedView<Phase>] extends [never]\n ? GameView\n : PhaseTaggedView<Phase>;\n\nexport type ViewByPhase = {\n [Phase in PhaseName]: NarrowedView<Phase>;\n};\n\ntype InteractionKeyForId<Id extends InteractionId> = Extract<\n InteractionKey,\n \\`\\${string}.\\${Id}\\`\n>;\ntype InteractionParamsForKey<Key extends InteractionKey> =\n Key extends InteractionKey ? InteractionParamsOf<Key> : never;\ntype InteractionParamsOfId<Id extends InteractionId> =\n InteractionParamsForKey<InteractionKeyForId<Id>>;\n\nexport interface BrowserRunnerSnapshot {\n sessionId: string | null;\n shortCode: string | null;\n version: number;\n currentPhase: string | null;\n controllingPlayerId: string;\n controllablePlayerIds: string[];\n view: unknown;\n availableInteractions?: string[];\n}\n\nexport interface BrowserRunnerBridge {\n snapshot(): Promise<BrowserRunnerSnapshot>;\n submitInteraction(\n playerId: PlayerId,\n interactionId: string,\n params: unknown,\n ): Promise<void>;\n}\n\nexport interface BrowserRunnerDriver {\n onReady?(bridge: BrowserRunnerBridge): Promise<void> | void;\n interaction?(\n bridge: BrowserRunnerBridge,\n input: { playerId: PlayerId; interactionId: string; params: unknown },\n ): Promise<boolean | void> | boolean | void;\n}\n\nexport interface ScenarioGameApi {\n start(): Promise<void>;\n /**\n * Patch the reducer snapshot for deterministic setup-heavy scenarios.\n * This is limited to reducer snapshot materialization and is rejected by\n * live replay/browser runners so authored gameplay verification still\n * submits real interactions.\n */\n patchState(mutator: (state: Record<string, unknown>) => void): Promise<void>;\n /**\n * Submit a player interaction (action-kind or prompt-kind) to the game.\n * The \\`interactionId\\` matches an \\`InteractionId\\` from the generated\n * \\`ui-contract\\`; \\`params\\` is typed per interaction id.\n */\n submit<Id extends InteractionId>(\n playerId: PlayerId,\n interactionId: Id,\n params?: InteractionParamsOfId<Id>,\n ): Promise<void>;\n}\n\nexport interface BaseContext {\n game: ScenarioGameApi;\n players(): readonly PlayerId[];\n /**\n * Resolve the seat at \\`index\\` in the base's players list.\n * Throws if the index is out of range. Prefer \\`seat(0)\\`/\\`seat(1)\\` over\n * literal player ids so bases stay portable across player counts and\n * we never hard-code wire-shape assumptions like \"player-1\".\n */\n seat(index: number): PlayerId;\n}\n\nexport interface SharedScenarioContext {\n game: ScenarioGameApi;\n players(): readonly PlayerId[];\n /**\n * Resolve the seat at \\`index\\` in the current scenario's players list.\n * Throws if the index is out of range. Prefer \\`seat(0)\\`/\\`seat(1)\\` over\n * literal player ids so scenarios stay portable across player counts and\n * we never hard-code wire-shape assumptions like \"player-1\".\n */\n seat(index: number): PlayerId;\n state(): StateName;\n view(playerId: PlayerId): GameView;\n interactions(playerId: PlayerId): readonly InteractionDescriptorFor[];\n explain(playerId: PlayerId, interactionId: InteractionId): InteractionExplanation;\n expect: ExpectFn;\n}\n\nexport type ScenarioContext<\n Phase extends PhaseName | undefined = undefined,\n> = Omit<SharedScenarioContext, \"state\" | \"view\"> & {\n state(): Phase extends PhaseName ? Phase : StateName;\n view(playerId: PlayerId): Phase extends PhaseName ? ViewByPhase[Phase] : GameView;\n};\n\nexport type ScenarioThenContext<\n _Runners extends readonly TestRunner[] = DefaultRunners,\n Phase extends PhaseName | undefined = undefined,\n> = ScenarioContext<Phase>;\n\nexport interface BaseDefinition {\n id: string;\n seed?: number;\n players?: number;\n setupProfileId?: SetupProfileId;\n extends?: BaseId | string;\n setup: (ctx: BaseContext) => void | Promise<void>;\n}\n\nexport interface ScenarioDefinition<\n Runners extends readonly TestRunner[] = DefaultRunners,\n Phase extends PhaseName | undefined = undefined,\n> {\n id: string;\n description?: string;\n from: BaseId | string;\n runners?: Runners;\n phase?: Phase;\n stage?: Phase extends PhaseName ? WorkspaceStageName<Phase> : never;\n when: (ctx: ScenarioContext<Phase>) => void | Promise<void>;\n then: (ctx: ScenarioThenContext<Runners, Phase>) => void | Promise<void>;\n}\n\nexport type {\n GameView,\n InteractionId,\n InteractionParamsOf,\n PhaseName,\n WorkspaceStageName,\n};\n`;\n}\n"],"mappings":";;;AAAA,IAAM,0BAA0B;AAAA,EAC9B;AAAA,EACA;AAAA,EACA;AAAA,EACA;AACF;AAEA,SAAS,mBAAmB,QAAmC;AAC7D,MAAI,OAAO,WAAW,GAAG;AACvB,WAAO;AAAA,EACT;AACA,SAAO,OAAO,IAAI,CAAC,UAAU,KAAK,UAAU,KAAK,CAAC,EAAE,KAAK,KAAK;AAChE;AAEO,IAAM,wCAAwC;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAiF9C,SAAS,mCACd,UAEI,CAAC,GACG;AACR,QAAM,iBAAiB,MAAM;AAAA,IAC3B,oBAAI,IAAI,CAAC,GAAI,QAAQ,kBAAkB,CAAC,GAAI,GAAG,uBAAuB,CAAC;AAAA,EACzE,EAAE,KAAK,CAAC,MAAM,UAAU,KAAK,cAAc,KAAK,CAAC;AAEjD,SAAO;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA,mCAkD0B,mBAAmB,cAAc,CAAC;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAoJrE;","names":[]}
|
|
@@ -0,0 +1,107 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
|
|
3
|
+
// src/utils/atomic-file.ts
|
|
4
|
+
import { constants as fsConstants, promises as fs } from "fs";
|
|
5
|
+
import path from "path";
|
|
6
|
+
import crypto from "crypto";
|
|
7
|
+
async function atomicWriteFile(targetPath, contents, options = {}) {
|
|
8
|
+
if (contents.length === 0) {
|
|
9
|
+
throw new Error(
|
|
10
|
+
`Refusing to atomicWriteFile an empty payload to ${targetPath}`
|
|
11
|
+
);
|
|
12
|
+
}
|
|
13
|
+
const mode = options.mode ?? 384;
|
|
14
|
+
const shouldFsync = options.fsync ?? true;
|
|
15
|
+
const dir = path.dirname(targetPath);
|
|
16
|
+
await fs.mkdir(dir, { recursive: true });
|
|
17
|
+
const suffix = crypto.randomBytes(6).toString("hex");
|
|
18
|
+
const tmpPath = `${targetPath}.tmp-${process.pid}-${suffix}`;
|
|
19
|
+
const fh = await fs.open(
|
|
20
|
+
tmpPath,
|
|
21
|
+
fsConstants.O_WRONLY | fsConstants.O_CREAT | fsConstants.O_EXCL,
|
|
22
|
+
mode
|
|
23
|
+
);
|
|
24
|
+
try {
|
|
25
|
+
await fh.writeFile(contents, "utf8");
|
|
26
|
+
try {
|
|
27
|
+
await fh.chmod(mode);
|
|
28
|
+
} catch {
|
|
29
|
+
}
|
|
30
|
+
if (shouldFsync) {
|
|
31
|
+
try {
|
|
32
|
+
await fh.sync();
|
|
33
|
+
} catch {
|
|
34
|
+
}
|
|
35
|
+
}
|
|
36
|
+
} finally {
|
|
37
|
+
await fh.close();
|
|
38
|
+
}
|
|
39
|
+
try {
|
|
40
|
+
await fs.rename(tmpPath, targetPath);
|
|
41
|
+
} catch (err) {
|
|
42
|
+
await fs.unlink(tmpPath).catch(() => void 0);
|
|
43
|
+
throw err;
|
|
44
|
+
}
|
|
45
|
+
}
|
|
46
|
+
async function withFileLock(lockPath, fn, options = {}) {
|
|
47
|
+
const retries = options.retries ?? 100;
|
|
48
|
+
const minDelayMs = options.minDelayMs ?? 20;
|
|
49
|
+
const maxDelayMs = options.maxDelayMs ?? 200;
|
|
50
|
+
const staleMs = options.staleMs ?? 3e4;
|
|
51
|
+
await fs.mkdir(path.dirname(lockPath), { recursive: true });
|
|
52
|
+
let attempt = 0;
|
|
53
|
+
let acquired = false;
|
|
54
|
+
while (!acquired) {
|
|
55
|
+
try {
|
|
56
|
+
const fh = await fs.open(
|
|
57
|
+
lockPath,
|
|
58
|
+
fsConstants.O_WRONLY | fsConstants.O_CREAT | fsConstants.O_EXCL,
|
|
59
|
+
384
|
|
60
|
+
);
|
|
61
|
+
await fh.writeFile(`${process.pid}
|
|
62
|
+
`, "utf8");
|
|
63
|
+
await fh.close();
|
|
64
|
+
acquired = true;
|
|
65
|
+
break;
|
|
66
|
+
} catch (err) {
|
|
67
|
+
const code = err.code;
|
|
68
|
+
if (code !== "EEXIST") {
|
|
69
|
+
throw err;
|
|
70
|
+
}
|
|
71
|
+
}
|
|
72
|
+
let stat = null;
|
|
73
|
+
try {
|
|
74
|
+
stat = await fs.stat(lockPath);
|
|
75
|
+
} catch {
|
|
76
|
+
continue;
|
|
77
|
+
}
|
|
78
|
+
if (stat !== null) {
|
|
79
|
+
const ageMs = Date.now() - stat.mtimeMs;
|
|
80
|
+
if (ageMs > staleMs) {
|
|
81
|
+
await fs.unlink(lockPath).catch(() => void 0);
|
|
82
|
+
continue;
|
|
83
|
+
}
|
|
84
|
+
}
|
|
85
|
+
attempt += 1;
|
|
86
|
+
if (attempt >= retries) {
|
|
87
|
+
throw new Error(
|
|
88
|
+
`Timed out acquiring file lock at ${lockPath} after ${retries} attempts.`
|
|
89
|
+
);
|
|
90
|
+
}
|
|
91
|
+
const jitter = Math.floor(
|
|
92
|
+
Math.random() * Math.max(1, maxDelayMs - minDelayMs)
|
|
93
|
+
);
|
|
94
|
+
await new Promise((resolve) => setTimeout(resolve, minDelayMs + jitter));
|
|
95
|
+
}
|
|
96
|
+
try {
|
|
97
|
+
return await fn();
|
|
98
|
+
} finally {
|
|
99
|
+
await fs.unlink(lockPath).catch(() => void 0);
|
|
100
|
+
}
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
export {
|
|
104
|
+
atomicWriteFile,
|
|
105
|
+
withFileLock
|
|
106
|
+
};
|
|
107
|
+
//# sourceMappingURL=chunk-GWRZRWCF.mjs.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../../src/utils/atomic-file.ts"],"sourcesContent":["/**\n * Primitives for safely mutating local state files owned by the CLI.\n *\n * Two guarantees:\n * - Writes are atomic-ish: we stage the payload in a sibling temp file with\n * the target permissions, fsync the contents, then `rename` over the target.\n * On POSIX `rename` within the same directory is atomic; on Windows it is\n * atomic within the same volume which is always the case for files we write\n * inside `~/.dreamboard`.\n * - We refuse to clobber a file with an empty payload. The original bug that\n * wiped refresh tokens on a failing `sync`/`compile` hinged on `undefined`\n * JSON values being persisted and reloaded as `{}`. Forbidding empty\n * writes here removes that entire failure mode at the primitive level.\n *\n * Additionally, `withFileLock` provides a cross-process advisory lock built on\n * `O_CREAT | O_EXCL` so that parallel CLI invocations serialize around mutations\n * of the same credential state.\n */\n\nimport { constants as fsConstants, promises as fs, type Stats } from \"node:fs\";\nimport path from \"node:path\";\nimport crypto from \"node:crypto\";\n\nexport type AtomicWriteOptions = {\n /** File mode applied to the written file (default: 0o600). */\n mode?: number;\n /** Call `fsync` on the temp file before renaming. Default: true. */\n fsync?: boolean;\n};\n\nexport async function atomicWriteFile(\n targetPath: string,\n contents: string,\n options: AtomicWriteOptions = {},\n): Promise<void> {\n if (contents.length === 0) {\n throw new Error(\n `Refusing to atomicWriteFile an empty payload to ${targetPath}`,\n );\n }\n const mode = options.mode ?? 0o600;\n const shouldFsync = options.fsync ?? true;\n const dir = path.dirname(targetPath);\n await fs.mkdir(dir, { recursive: true });\n\n const suffix = crypto.randomBytes(6).toString(\"hex\");\n const tmpPath = `${targetPath}.tmp-${process.pid}-${suffix}`;\n\n const fh = await fs.open(\n tmpPath,\n fsConstants.O_WRONLY | fsConstants.O_CREAT | fsConstants.O_EXCL,\n mode,\n );\n try {\n await fh.writeFile(contents, \"utf8\");\n try {\n await fh.chmod(mode);\n } catch {\n // Some filesystems (e.g. network volumes, Windows) refuse chmod.\n // Ignoring here is safe: the `open` call above already created the\n // file with the requested mode on systems that honor it.\n }\n if (shouldFsync) {\n try {\n await fh.sync();\n } catch {\n // Best-effort. Not all backends (tmpfs on some platforms) support fsync.\n }\n }\n } finally {\n await fh.close();\n }\n\n try {\n await fs.rename(tmpPath, targetPath);\n } catch (err) {\n await fs.unlink(tmpPath).catch(() => undefined);\n throw err;\n }\n}\n\nexport type FileLockOptions = {\n /** Max number of acquisition attempts before giving up. Default: 100. */\n retries?: number;\n /** Minimum backoff between retries in ms. Default: 20. */\n minDelayMs?: number;\n /** Maximum backoff between retries in ms. Default: 200. */\n maxDelayMs?: number;\n /**\n * A lockfile older than this is considered stale and forcibly removed.\n * Guards against crashed processes leaving a permanent lock. Default: 30s.\n */\n staleMs?: number;\n};\n\nexport async function withFileLock<T>(\n lockPath: string,\n fn: () => Promise<T>,\n options: FileLockOptions = {},\n): Promise<T> {\n const retries = options.retries ?? 100;\n const minDelayMs = options.minDelayMs ?? 20;\n const maxDelayMs = options.maxDelayMs ?? 200;\n const staleMs = options.staleMs ?? 30_000;\n\n await fs.mkdir(path.dirname(lockPath), { recursive: true });\n\n let attempt = 0;\n let acquired = false;\n while (!acquired) {\n try {\n const fh = await fs.open(\n lockPath,\n fsConstants.O_WRONLY | fsConstants.O_CREAT | fsConstants.O_EXCL,\n 0o600,\n );\n await fh.writeFile(`${process.pid}\\n`, \"utf8\");\n await fh.close();\n acquired = true;\n break;\n } catch (err) {\n const code = (err as NodeJS.ErrnoException).code;\n if (code !== \"EEXIST\") {\n throw err;\n }\n }\n\n let stat: Stats | null = null;\n try {\n stat = await fs.stat(lockPath);\n } catch {\n continue;\n }\n if (stat !== null) {\n const ageMs = Date.now() - stat.mtimeMs;\n if (ageMs > staleMs) {\n await fs.unlink(lockPath).catch(() => undefined);\n continue;\n }\n }\n\n attempt += 1;\n if (attempt >= retries) {\n throw new Error(\n `Timed out acquiring file lock at ${lockPath} after ${retries} attempts.`,\n );\n }\n const jitter = Math.floor(\n Math.random() * Math.max(1, maxDelayMs - minDelayMs),\n );\n await new Promise((resolve) => setTimeout(resolve, minDelayMs + jitter));\n }\n\n try {\n return await fn();\n } finally {\n await fs.unlink(lockPath).catch(() => undefined);\n }\n}\n"],"mappings":";;;AAmBA,SAAS,aAAa,aAAa,YAAY,UAAsB;AACrE,OAAO,UAAU;AACjB,OAAO,YAAY;AASnB,eAAsB,gBACpB,YACA,UACA,UAA8B,CAAC,GAChB;AACf,MAAI,SAAS,WAAW,GAAG;AACzB,UAAM,IAAI;AAAA,MACR,mDAAmD,UAAU;AAAA,IAC/D;AAAA,EACF;AACA,QAAM,OAAO,QAAQ,QAAQ;AAC7B,QAAM,cAAc,QAAQ,SAAS;AACrC,QAAM,MAAM,KAAK,QAAQ,UAAU;AACnC,QAAM,GAAG,MAAM,KAAK,EAAE,WAAW,KAAK,CAAC;AAEvC,QAAM,SAAS,OAAO,YAAY,CAAC,EAAE,SAAS,KAAK;AACnD,QAAM,UAAU,GAAG,UAAU,QAAQ,QAAQ,GAAG,IAAI,MAAM;AAE1D,QAAM,KAAK,MAAM,GAAG;AAAA,IAClB;AAAA,IACA,YAAY,WAAW,YAAY,UAAU,YAAY;AAAA,IACzD;AAAA,EACF;AACA,MAAI;AACF,UAAM,GAAG,UAAU,UAAU,MAAM;AACnC,QAAI;AACF,YAAM,GAAG,MAAM,IAAI;AAAA,IACrB,QAAQ;AAAA,IAIR;AACA,QAAI,aAAa;AACf,UAAI;AACF,cAAM,GAAG,KAAK;AAAA,MAChB,QAAQ;AAAA,MAER;AAAA,IACF;AAAA,EACF,UAAE;AACA,UAAM,GAAG,MAAM;AAAA,EACjB;AAEA,MAAI;AACF,UAAM,GAAG,OAAO,SAAS,UAAU;AAAA,EACrC,SAAS,KAAK;AACZ,UAAM,GAAG,OAAO,OAAO,EAAE,MAAM,MAAM,MAAS;AAC9C,UAAM;AAAA,EACR;AACF;AAgBA,eAAsB,aACpB,UACA,IACA,UAA2B,CAAC,GAChB;AACZ,QAAM,UAAU,QAAQ,WAAW;AACnC,QAAM,aAAa,QAAQ,cAAc;AACzC,QAAM,aAAa,QAAQ,cAAc;AACzC,QAAM,UAAU,QAAQ,WAAW;AAEnC,QAAM,GAAG,MAAM,KAAK,QAAQ,QAAQ,GAAG,EAAE,WAAW,KAAK,CAAC;AAE1D,MAAI,UAAU;AACd,MAAI,WAAW;AACf,SAAO,CAAC,UAAU;AAChB,QAAI;AACF,YAAM,KAAK,MAAM,GAAG;AAAA,QAClB;AAAA,QACA,YAAY,WAAW,YAAY,UAAU,YAAY;AAAA,QACzD;AAAA,MACF;AACA,YAAM,GAAG,UAAU,GAAG,QAAQ,GAAG;AAAA,GAAM,MAAM;AAC7C,YAAM,GAAG,MAAM;AACf,iBAAW;AACX;AAAA,IACF,SAAS,KAAK;AACZ,YAAM,OAAQ,IAA8B;AAC5C,UAAI,SAAS,UAAU;AACrB,cAAM;AAAA,MACR;AAAA,IACF;AAEA,QAAI,OAAqB;AACzB,QAAI;AACF,aAAO,MAAM,GAAG,KAAK,QAAQ;AAAA,IAC/B,QAAQ;AACN;AAAA,IACF;AACA,QAAI,SAAS,MAAM;AACjB,YAAM,QAAQ,KAAK,IAAI,IAAI,KAAK;AAChC,UAAI,QAAQ,SAAS;AACnB,cAAM,GAAG,OAAO,QAAQ,EAAE,MAAM,MAAM,MAAS;AAC/C;AAAA,MACF;AAAA,IACF;AAEA,eAAW;AACX,QAAI,WAAW,SAAS;AACtB,YAAM,IAAI;AAAA,QACR,oCAAoC,QAAQ,UAAU,OAAO;AAAA,MAC/D;AAAA,IACF;AACA,UAAM,SAAS,KAAK;AAAA,MAClB,KAAK,OAAO,IAAI,KAAK,IAAI,GAAG,aAAa,UAAU;AAAA,IACrD;AACA,UAAM,IAAI,QAAQ,CAAC,YAAY,WAAW,SAAS,aAAa,MAAM,CAAC;AAAA,EACzE;AAEA,MAAI;AACF,WAAO,MAAM,GAAG;AAAA,EAClB,UAAE;AACA,UAAM,GAAG,OAAO,QAAQ,EAAE,MAAM,MAAM,MAAS;AAAA,EACjD;AACF;","names":[]}
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
var __defProp = Object.defineProperty;
|
|
3
|
+
var __export = (target, all) => {
|
|
4
|
+
for (var name in all)
|
|
5
|
+
__defProp(target, name, { get: all[name], enumerable: true });
|
|
6
|
+
};
|
|
7
|
+
|
|
8
|
+
export {
|
|
9
|
+
__export
|
|
10
|
+
};
|
|
11
|
+
//# sourceMappingURL=chunk-H6XDQJ3N.mjs.map
|
|
@@ -0,0 +1,89 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
import {
|
|
3
|
+
loadProjectAuthoringAdapter,
|
|
4
|
+
readWorkspaceTextFileIfExists,
|
|
5
|
+
validateGeneratedArtifacts,
|
|
6
|
+
writeWorkspaceTextFile
|
|
7
|
+
} from "./chunk-OJFZVGEL.mjs";
|
|
8
|
+
|
|
9
|
+
// src/services/project/workspace-codegen.ts
|
|
10
|
+
var STARTER_UI_SEED_FILES = /* @__PURE__ */ new Set([
|
|
11
|
+
"ui/interaction-routes.tsx",
|
|
12
|
+
"ui/setup-screen.tsx",
|
|
13
|
+
"ui/styles.ts",
|
|
14
|
+
"ui/ui-contract-typing-smoke.tsx"
|
|
15
|
+
]);
|
|
16
|
+
var SETUP_PROFILES_SEED_MARKER = "Dreamboard generated setup profile seeds.";
|
|
17
|
+
function isFrameworkOwnedSetupProfilesSeed(content) {
|
|
18
|
+
if (content === null || content === void 0) {
|
|
19
|
+
return false;
|
|
20
|
+
}
|
|
21
|
+
const trimmed = content.trim();
|
|
22
|
+
return trimmed.length === 0 || trimmed.includes(SETUP_PROFILES_SEED_MARKER);
|
|
23
|
+
}
|
|
24
|
+
async function applyWorkspaceCodegen(options) {
|
|
25
|
+
const { projectRoot, manifest } = options;
|
|
26
|
+
const { adapter } = await loadProjectAuthoringAdapter(projectRoot);
|
|
27
|
+
const artifacts = validateGeneratedArtifacts(adapter, [
|
|
28
|
+
...adapter.generateWorkspaceArtifacts(manifest),
|
|
29
|
+
...adapter.generateTestArtifacts({ manifest })
|
|
30
|
+
]);
|
|
31
|
+
const authoritativeFiles = new Map(
|
|
32
|
+
artifacts.filter((artifact) => artifact.ownership !== "seed").map((artifact) => [artifact.path, artifact.content])
|
|
33
|
+
);
|
|
34
|
+
const seedFiles = new Map(
|
|
35
|
+
artifacts.filter((artifact) => artifact.ownership === "seed").map((artifact) => [artifact.path, artifact.content])
|
|
36
|
+
);
|
|
37
|
+
const written = [];
|
|
38
|
+
const skipped = [];
|
|
39
|
+
const merged = [];
|
|
40
|
+
const existingUiAppBeforeSeeds = await readWorkspaceTextFileIfExists(
|
|
41
|
+
projectRoot,
|
|
42
|
+
"ui/App.tsx"
|
|
43
|
+
);
|
|
44
|
+
const shouldWriteStarterUiSeedFiles = existingUiAppBeforeSeeds === null || existingUiAppBeforeSeeds.trim().length === 0;
|
|
45
|
+
for (const [relativePath, content] of authoritativeFiles) {
|
|
46
|
+
const existingContent = await readWorkspaceTextFileIfExists(
|
|
47
|
+
projectRoot,
|
|
48
|
+
relativePath
|
|
49
|
+
);
|
|
50
|
+
await writeWorkspaceTextFile(projectRoot, relativePath, content);
|
|
51
|
+
if (existingContent !== content) {
|
|
52
|
+
written.push(relativePath);
|
|
53
|
+
}
|
|
54
|
+
}
|
|
55
|
+
for (const [relativePath, content] of seedFiles) {
|
|
56
|
+
const existingContent = await readWorkspaceTextFileIfExists(
|
|
57
|
+
projectRoot,
|
|
58
|
+
relativePath
|
|
59
|
+
);
|
|
60
|
+
if (STARTER_UI_SEED_FILES.has(relativePath) && !shouldWriteStarterUiSeedFiles && existingContent === null) {
|
|
61
|
+
skipped.push(relativePath);
|
|
62
|
+
continue;
|
|
63
|
+
}
|
|
64
|
+
const shouldRefreshFrameworkSeed = relativePath === "app/setup-profiles.ts" && isFrameworkOwnedSetupProfilesSeed(existingContent);
|
|
65
|
+
if (shouldRefreshFrameworkSeed) {
|
|
66
|
+
await writeWorkspaceTextFile(projectRoot, relativePath, content);
|
|
67
|
+
if (existingContent !== content) {
|
|
68
|
+
written.push(relativePath);
|
|
69
|
+
}
|
|
70
|
+
continue;
|
|
71
|
+
}
|
|
72
|
+
const hasExistingContent = existingContent !== null && existingContent.trim().length > 0;
|
|
73
|
+
if (hasExistingContent) {
|
|
74
|
+
skipped.push(relativePath);
|
|
75
|
+
continue;
|
|
76
|
+
}
|
|
77
|
+
await writeWorkspaceTextFile(projectRoot, relativePath, content);
|
|
78
|
+
written.push(relativePath);
|
|
79
|
+
}
|
|
80
|
+
written.sort();
|
|
81
|
+
skipped.sort();
|
|
82
|
+
merged.sort();
|
|
83
|
+
return { written, skipped, merged };
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
export {
|
|
87
|
+
applyWorkspaceCodegen
|
|
88
|
+
};
|
|
89
|
+
//# sourceMappingURL=chunk-HUBV22JQ.mjs.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../../src/services/project/workspace-codegen.ts"],"sourcesContent":["import type { GameTopologyManifest } from \"@dreamboard-games/sdk/types\";\nimport { loadProjectAuthoringAdapter } from \"../project-authoring/loader.js\";\nimport { validateGeneratedArtifacts } from \"../project-authoring/validation.js\";\nimport {\n readWorkspaceTextFileIfExists,\n writeWorkspaceTextFile,\n} from \"./workspace-path.js\";\n\nexport interface WorkspaceCodegenWriteResult {\n written: string[];\n skipped: string[];\n merged: string[];\n}\n\nconst STARTER_UI_SEED_FILES = new Set([\n \"ui/interaction-routes.tsx\",\n \"ui/setup-screen.tsx\",\n \"ui/styles.ts\",\n \"ui/ui-contract-typing-smoke.tsx\",\n]);\nconst SETUP_PROFILES_SEED_MARKER = \"Dreamboard generated setup profile seeds.\";\n\nfunction isFrameworkOwnedSetupProfilesSeed(\n content: string | null | undefined,\n): boolean {\n if (content === null || content === undefined) {\n return false;\n }\n const trimmed = content.trim();\n return trimmed.length === 0 || trimmed.includes(SETUP_PROFILES_SEED_MARKER);\n}\n\nexport async function applyWorkspaceCodegen(options: {\n projectRoot: string;\n manifest: GameTopologyManifest;\n}): Promise<WorkspaceCodegenWriteResult> {\n const { projectRoot, manifest } = options;\n const { adapter } = await loadProjectAuthoringAdapter(projectRoot);\n const artifacts = validateGeneratedArtifacts(adapter, [\n ...adapter.generateWorkspaceArtifacts(manifest),\n ...adapter.generateTestArtifacts({ manifest }),\n ]);\n const authoritativeFiles = new Map(\n artifacts\n .filter((artifact) => artifact.ownership !== \"seed\")\n .map((artifact) => [artifact.path, artifact.content]),\n );\n const seedFiles = new Map(\n artifacts\n .filter((artifact) => artifact.ownership === \"seed\")\n .map((artifact) => [artifact.path, artifact.content]),\n );\n\n const written: string[] = [];\n const skipped: string[] = [];\n const merged: string[] = [];\n const existingUiAppBeforeSeeds = await readWorkspaceTextFileIfExists(\n projectRoot,\n \"ui/App.tsx\",\n );\n const shouldWriteStarterUiSeedFiles =\n existingUiAppBeforeSeeds === null ||\n existingUiAppBeforeSeeds.trim().length === 0;\n\n for (const [relativePath, content] of authoritativeFiles) {\n const existingContent = await readWorkspaceTextFileIfExists(\n projectRoot,\n relativePath,\n );\n await writeWorkspaceTextFile(projectRoot, relativePath, content);\n if (existingContent !== content) {\n written.push(relativePath);\n }\n }\n\n for (const [relativePath, content] of seedFiles) {\n const existingContent = await readWorkspaceTextFileIfExists(\n projectRoot,\n relativePath,\n );\n if (\n STARTER_UI_SEED_FILES.has(relativePath) &&\n !shouldWriteStarterUiSeedFiles &&\n existingContent === null\n ) {\n skipped.push(relativePath);\n continue;\n }\n\n const shouldRefreshFrameworkSeed =\n relativePath === \"app/setup-profiles.ts\" &&\n isFrameworkOwnedSetupProfilesSeed(existingContent);\n\n if (shouldRefreshFrameworkSeed) {\n await writeWorkspaceTextFile(projectRoot, relativePath, content);\n if (existingContent !== content) {\n written.push(relativePath);\n }\n continue;\n }\n\n const hasExistingContent =\n existingContent !== null && existingContent.trim().length > 0;\n if (hasExistingContent) {\n skipped.push(relativePath);\n continue;\n }\n\n await writeWorkspaceTextFile(projectRoot, relativePath, content);\n written.push(relativePath);\n }\n\n written.sort();\n skipped.sort();\n merged.sort();\n return { written, skipped, merged };\n}\n"],"mappings":";;;;;;;;;AAcA,IAAM,wBAAwB,oBAAI,IAAI;AAAA,EACpC;AAAA,EACA;AAAA,EACA;AAAA,EACA;AACF,CAAC;AACD,IAAM,6BAA6B;AAEnC,SAAS,kCACP,SACS;AACT,MAAI,YAAY,QAAQ,YAAY,QAAW;AAC7C,WAAO;AAAA,EACT;AACA,QAAM,UAAU,QAAQ,KAAK;AAC7B,SAAO,QAAQ,WAAW,KAAK,QAAQ,SAAS,0BAA0B;AAC5E;AAEA,eAAsB,sBAAsB,SAGH;AACvC,QAAM,EAAE,aAAa,SAAS,IAAI;AAClC,QAAM,EAAE,QAAQ,IAAI,MAAM,4BAA4B,WAAW;AACjE,QAAM,YAAY,2BAA2B,SAAS;AAAA,IACpD,GAAG,QAAQ,2BAA2B,QAAQ;AAAA,IAC9C,GAAG,QAAQ,sBAAsB,EAAE,SAAS,CAAC;AAAA,EAC/C,CAAC;AACD,QAAM,qBAAqB,IAAI;AAAA,IAC7B,UACG,OAAO,CAAC,aAAa,SAAS,cAAc,MAAM,EAClD,IAAI,CAAC,aAAa,CAAC,SAAS,MAAM,SAAS,OAAO,CAAC;AAAA,EACxD;AACA,QAAM,YAAY,IAAI;AAAA,IACpB,UACG,OAAO,CAAC,aAAa,SAAS,cAAc,MAAM,EAClD,IAAI,CAAC,aAAa,CAAC,SAAS,MAAM,SAAS,OAAO,CAAC;AAAA,EACxD;AAEA,QAAM,UAAoB,CAAC;AAC3B,QAAM,UAAoB,CAAC;AAC3B,QAAM,SAAmB,CAAC;AAC1B,QAAM,2BAA2B,MAAM;AAAA,IACrC;AAAA,IACA;AAAA,EACF;AACA,QAAM,gCACJ,6BAA6B,QAC7B,yBAAyB,KAAK,EAAE,WAAW;AAE7C,aAAW,CAAC,cAAc,OAAO,KAAK,oBAAoB;AACxD,UAAM,kBAAkB,MAAM;AAAA,MAC5B;AAAA,MACA;AAAA,IACF;AACA,UAAM,uBAAuB,aAAa,cAAc,OAAO;AAC/D,QAAI,oBAAoB,SAAS;AAC/B,cAAQ,KAAK,YAAY;AAAA,IAC3B;AAAA,EACF;AAEA,aAAW,CAAC,cAAc,OAAO,KAAK,WAAW;AAC/C,UAAM,kBAAkB,MAAM;AAAA,MAC5B;AAAA,MACA;AAAA,IACF;AACA,QACE,sBAAsB,IAAI,YAAY,KACtC,CAAC,iCACD,oBAAoB,MACpB;AACA,cAAQ,KAAK,YAAY;AACzB;AAAA,IACF;AAEA,UAAM,6BACJ,iBAAiB,2BACjB,kCAAkC,eAAe;AAEnD,QAAI,4BAA4B;AAC9B,YAAM,uBAAuB,aAAa,cAAc,OAAO;AAC/D,UAAI,oBAAoB,SAAS;AAC/B,gBAAQ,KAAK,YAAY;AAAA,MAC3B;AACA;AAAA,IACF;AAEA,UAAM,qBACJ,oBAAoB,QAAQ,gBAAgB,KAAK,EAAE,SAAS;AAC9D,QAAI,oBAAoB;AACtB,cAAQ,KAAK,YAAY;AACzB;AAAA,IACF;AAEA,UAAM,uBAAuB,aAAa,cAAc,OAAO;AAC/D,YAAQ,KAAK,YAAY;AAAA,EAC3B;AAEA,UAAQ,KAAK;AACb,UAAQ,KAAK;AACb,SAAO,KAAK;AACZ,SAAO,EAAE,SAAS,SAAS,OAAO;AACpC;","names":[]}
|