@drawcall/create 0.0.0 → 0.1.2

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/dist/command.d.ts CHANGED
@@ -1,9 +1,11 @@
1
1
  import { Command } from "commander";
2
2
  import { type HarnessName, type Stage } from "./constants.js";
3
3
  import { createProject } from "./create.js";
4
+ import { superviseBuild } from "./supervisor.js";
4
5
  export declare function createCreateCommand(command?: Command, options?: {
5
6
  version?: string;
6
7
  createProject?: typeof createProject;
8
+ superviseBuild?: typeof superviseBuild;
7
9
  }): Command;
8
10
  export declare function splitHarnessArgs(args: string[]): {
9
11
  promptParts: string[];
package/dist/command.js CHANGED
@@ -1,6 +1,7 @@
1
1
  import { Command } from "commander";
2
2
  import { CliError, DEFAULT_HARNESS_TIMEOUT_MS, HARNESS_NAMES, MAX_BUILD_TURNS, STAGES } from "./constants.js";
3
3
  import { createProject } from "./create.js";
4
+ import { superviseBuild } from "./supervisor.js";
4
5
  import { formatDuration } from "./progress-log.js";
5
6
  const CLI_OPTION_NAMES = [
6
7
  "--stage",
@@ -8,24 +9,38 @@ const CLI_OPTION_NAMES = [
8
9
  "--harness-timeout-minutes",
9
10
  "--max-turns",
10
11
  "--name",
11
- "--skip-template"
12
+ "--skip-template",
13
+ "--supervise"
12
14
  ];
13
15
  export function createCreateCommand(command = new Command(), options = {}) {
14
16
  const create = options.createProject ?? createProject;
17
+ const supervise = options.superviseBuild ?? superviseBuild;
15
18
  command
16
19
  .name("drawcall-create")
17
20
  .description("Create a project with an installed local harness")
18
- .argument("<args...>", "what should be created; use -- to pass following args to the harness")
21
+ .argument("[args...]", "what should be created; use -- to pass following args to the harness (omit with --supervise)")
19
22
  .option("--stage <name>", `which stage to run (${STAGES.join(", ")})`)
20
23
  .option("--harness <name>", `harness to use (${HARNESS_NAMES.join(", ")})`)
21
24
  .option("--harness-timeout-minutes <count>", `timeout for each harness invocation in minutes (default: ${DEFAULT_HARNESS_TIMEOUT_MS / 60_000})`)
22
25
  .option("--max-turns <count>", `maximum build turns (default: ${MAX_BUILD_TURNS})`)
23
26
  .option("--name <name>", "project directory name (default: a generated dc-xxxxxx name)")
24
27
  .option("--skip-template", "during a full run, skip starter template search and build from scratch")
28
+ .option("--supervise", "run only the build loop, crash-safe: each turn is a separate child process, the tree is reset to the last good commit before every turn, and a killed turn resumes automatically")
25
29
  .passThroughOptions()
26
30
  .version(options.version ?? "0.0.0")
27
- .action(async (args, commandOptions) => {
31
+ .action(async (args = [], commandOptions) => {
28
32
  const { promptParts, harnessArgs } = splitHarnessArgs(args);
33
+ if (commandOptions.supervise === true) {
34
+ const result = await supervise({
35
+ harness: parseHarnessName(commandOptions.harness),
36
+ harnessArgs,
37
+ harnessTimeoutMinutes: parsePositiveInteger(commandOptions.harnessTimeoutMinutes, "--harness-timeout-minutes"),
38
+ maxTurns: parsePositiveInteger(commandOptions.maxTurns, "--max-turns")
39
+ });
40
+ console.log(formatSupervised(result));
41
+ process.exitCode = result.stop === "stuck" ? 1 : 0;
42
+ return;
43
+ }
29
44
  const prompt = parsePromptParts(promptParts);
30
45
  const result = await create(prompt, {
31
46
  stage: parseStage(commandOptions.stage),
@@ -91,6 +106,14 @@ export function parseHarnessTimeoutMs(value) {
91
106
  const minutes = parsePositiveInteger(value, "--harness-timeout-minutes");
92
107
  return minutes === undefined ? undefined : minutes * 60_000;
93
108
  }
109
+ function formatSupervised(result) {
110
+ const reason = {
111
+ "plan-consumed": "Plan consumed — build complete",
112
+ "budget-exhausted": "Reached the build-turn budget; PLAN.md still records remaining work",
113
+ stuck: "Stopped: no forward progress across consecutive turns"
114
+ };
115
+ return ["", reason[result.stop], `Turns ${result.turns}`, `Path ${result.projectDir}`].join("\n");
116
+ }
94
117
  function formatSuccess(result) {
95
118
  const lines = [
96
119
  "",
@@ -2,7 +2,7 @@ export declare const HARNESS_NAMES: readonly ["opencode", "codex", "claude", "pi
2
2
  export type HarnessName = (typeof HARNESS_NAMES)[number];
3
3
  export declare const STAGES: readonly ["scaffold", "template", "survey-assets", "survey-technology", "goal", "plan", "build", "full"];
4
4
  export type Stage = (typeof STAGES)[number];
5
- export declare const PARALLEL_STAGES: readonly ["template", "survey-assets", "survey-technology"];
5
+ export declare const PARALLEL_STAGES: readonly ["survey-assets", "survey-technology"];
6
6
  export declare const SKILLS: readonly ["drawcall-ai/vitexec", "drawcall-ai/uikitml", "drawcall-ai/acta", "drawcall-ai/market", "drawcall-ai/speech", "drawcall-ai/flipbook", "drawcall-ai/skills"];
7
7
  export declare const PACKAGES: readonly ["vitexec@latest", "@drawcall/uikitml@latest", "@drawcall/acta@latest", "@drawcall/market@latest", "@drawcall/flipbook@latest", "@pmndrs/uikit@latest", "@pmndrs/pointer-events@latest", "@pmndrs/viverse@latest", "navcat@^0.4.1", "three@^0.184.0", "vite@^8.0.16", "typescript@^6.0.3", "elics@^3.4.2", "postprocessing@^6.39.1"];
8
8
  export declare const PACKAGE_NAMES: readonly ["vitexec", "@drawcall/uikitml", "@drawcall/acta", "@drawcall/market", "@drawcall/flipbook", "@pmndrs/uikit", "@pmndrs/pointer-events", "@pmndrs/viverse", "navcat", "three", "vite", "typescript", "elics"];
package/dist/constants.js CHANGED
@@ -1,4 +1,12 @@
1
- export const HARNESS_NAMES = ["opencode", "codex", "claude", "pi", "gemini", "grok", "forge"];
1
+ export const HARNESS_NAMES = [
2
+ "opencode",
3
+ "codex",
4
+ "claude",
5
+ "pi",
6
+ "gemini",
7
+ "grok",
8
+ "forge"
9
+ ];
2
10
  export const STAGES = [
3
11
  "scaffold",
4
12
  "template",
@@ -9,10 +17,10 @@ export const STAGES = [
9
17
  "build",
10
18
  "full"
11
19
  ];
12
- // In a "full" run these three stages run concurrently after scaffolding: they write disjoint
13
- // outputs (template touches the product; the surveys write gitignored scratch files) and none
14
- // of them depends on the others, so they share a barrier before the goal stage.
15
- export const PARALLEL_STAGES = ["template", "survey-assets", "survey-technology"];
20
+ // In a "full" run the template is applied first; then these two surveys run concurrently over the
21
+ // applied state (they write disjoint gitignored scratch files and don't depend on each other) and
22
+ // share a barrier before the goal stage.
23
+ export const PARALLEL_STAGES = ["survey-assets", "survey-technology"];
16
24
  export const SKILLS = [
17
25
  "drawcall-ai/vitexec",
18
26
  "drawcall-ai/uikitml",
package/dist/create.js CHANGED
@@ -1,4 +1,4 @@
1
- import { existsSync } from "node:fs";
1
+ import { appendFileSync, existsSync } from "node:fs";
2
2
  import { mkdir } from "node:fs/promises";
3
3
  import { basename, join, resolve } from "node:path";
4
4
  import { CliError, DEFAULT_HARNESS_TIMEOUT_MS, GOAL_FILE, MAX_BUILD_TURNS, PLAN_FILE, SESSION_LOG_FILE } from "./constants.js";
@@ -20,11 +20,24 @@ const delay = (ms) => new Promise((resolve) => setTimeout(resolve, ms));
20
20
  async function runTurn(harnessRunner, prompt) {
21
21
  let result = await runHarnessTurn(harnessRunner, prompt);
22
22
  for (let attempt = 2; attempt <= HARNESS_TURN_ATTEMPTS && result.exitCode !== 0 && !result.timedOut; attempt += 1) {
23
- await delay(RETRY_BACKOFF_MS * (attempt - 1));
23
+ const waitMs = RETRY_BACKOFF_MS * (attempt - 1);
24
+ // A retry is otherwise invisible: the runner just logs a second identical "$ <harness> …"
25
+ // invocation. Mark it so the log reads as "turn failed → retrying" rather than as a mysterious
26
+ // duplicate run, and so a slow build turn can be told apart from one that silently re-ran.
27
+ logToSession(harnessRunner.cwd, `[drawcall-create] harness turn failed (exit ${result.exitCode}); retry ${attempt}/${HARNESS_TURN_ATTEMPTS} after ${Math.round(waitMs / 1000)}s`);
28
+ await delay(waitMs);
24
29
  result = await runHarnessTurn(harnessRunner, prompt);
25
30
  }
26
31
  return result;
27
32
  }
33
+ function logToSession(cwd, line) {
34
+ try {
35
+ appendFileSync(join(cwd, SESSION_LOG_FILE), `${line}\n`);
36
+ }
37
+ catch {
38
+ // No session log on disk (e.g. a test's caller-supplied runner) — the retry proceeds anyway.
39
+ }
40
+ }
28
41
  export async function createProject(prompt, options = {}) {
29
42
  const { env = process.env, commandExists = isCommandAvailable } = options;
30
43
  const stage = options.stage ?? "full";
@@ -94,9 +107,10 @@ async function runStages(stage, harnessRunner, progressLog, prompt, projectName,
94
107
  }
95
108
  if (stage === "scaffold")
96
109
  return stopped(0);
97
- // template, survey-assets, and survey-technology are independent (disjoint outputs, no git,
98
- // surveys don't depend on the template). A "full" run does all three concurrently behind one
99
- // barrier; a single-stage run does just the requested one.
110
+ // A "full" run applies the template first, then surveys the applied state (assets + technology)
111
+ // concurrently behind one barrier the surveys read the implementation the template installed,
112
+ // so they cannot run alongside it. A skip-template full run surveys a bare scaffold instead. A
113
+ // single-stage run does just the requested one (surveys then read whatever is already in the cwd).
100
114
  if (stage === "full") {
101
115
  const { exitCode } = options.skipTemplate
102
116
  ? await runSurveyGroup(harnessRunner, progressLog, prompt)
@@ -169,27 +183,43 @@ async function runGroupTurns(harnessRunner, prompts) {
169
183
  }
170
184
  return Promise.all(prompts.map((prompt) => runTurn(harnessRunner, prompt)));
171
185
  }
172
- /** Apply a fitting Market starter and survey assets/technology concurrently, then commit. */
186
+ /** Apply a fitting Market starter, then survey assets/technology over the applied state. */
173
187
  async function runTemplateGroup(harnessRunner, progressLog, prompt) {
174
- progressLog.start("template/surveys");
188
+ // The template runs first and commits, so the surveys that follow can read the applied
189
+ // implementation (its src/ and the assets it installed via Market), not race them — the
190
+ // whole point of the template path is to survey what is already here and modify it.
191
+ progressLog.start("template");
192
+ try {
193
+ const template = await runTurn(harnessRunner, buildTemplatePrompt(prompt));
194
+ if (template.exitCode !== 0) {
195
+ progressLog.fail("template");
196
+ return template;
197
+ }
198
+ await finishTemplate(harnessRunner);
199
+ progressLog.succeed("template");
200
+ }
201
+ catch (error) {
202
+ progressLog.fail("template");
203
+ throw error;
204
+ }
205
+ // The two surveys write disjoint scratch files and don't depend on each other, so they run
206
+ // concurrently (serially for opencode — see SERIAL_GROUP_HARNESSES).
207
+ progressLog.start("surveys");
175
208
  try {
176
- // Array order is the call order: each runner pushes synchronously before its first await.
177
209
  const results = await runGroupTurns(harnessRunner, [
178
- buildTemplatePrompt(prompt),
179
210
  buildSurveyAssetsPrompt(prompt),
180
211
  buildSurveyTechnologyPrompt(prompt)
181
212
  ]);
182
213
  const failed = results.find((result) => result.exitCode !== 0);
183
214
  if (failed) {
184
- progressLog.fail("template/surveys");
215
+ progressLog.fail("surveys");
185
216
  return failed;
186
217
  }
187
- await finishTemplate(harnessRunner);
188
- progressLog.succeed("template/surveys");
218
+ progressLog.succeed("surveys");
189
219
  return { exitCode: 0 };
190
220
  }
191
221
  catch (error) {
192
- progressLog.fail("template/surveys");
222
+ progressLog.fail("surveys");
193
223
  throw error;
194
224
  }
195
225
  }
package/dist/index.d.ts CHANGED
@@ -5,4 +5,5 @@ export * from "./progress-log.js";
5
5
  export * from "./harness.js";
6
6
  export * from "./scaffold.js";
7
7
  export * from "./create.js";
8
+ export * from "./supervisor.js";
8
9
  export * from "./command.js";
package/dist/index.js CHANGED
@@ -5,4 +5,5 @@ export * from "./progress-log.js";
5
5
  export * from "./harness.js";
6
6
  export * from "./scaffold.js";
7
7
  export * from "./create.js";
8
+ export * from "./supervisor.js";
8
9
  export * from "./command.js";
package/dist/prompts.js CHANGED
@@ -38,31 +38,37 @@ export function buildSurveyAssetsPrompt(userPrompt) {
38
38
 
39
39
  Goal: ${userPrompt}
40
40
 
41
- You are surveying, not planning or building: catalogue the Market assets that already exist and fit this goal, so the goal and plan stages can draw on real options.
41
+ You are surveying, not planning or building: map the content (assets) this goal needs against what the project already has and what the Market can still add, so the goal and plan stages can modify what exists efficiently rather than re-acquire content that is already here.
42
42
 
43
- Search and preview the Drawcall Market with \`npx @drawcall/market\` (the \`market\` skill doc has the commands and the asset types). Start from the user-visible experience, not from obvious nouns alone: include the things the player sees, hears, controls, collides with, collects, inhabits, and receives as moment-to-moment feedback. For each distinct need, find the assets that fit and judge fit from the real previewmetadata, screenshots, filesnot the name.
43
+ Start with what is already installed. A starter may already be applied to this project, and the assets it brought in are the substrate the build modifies — not gaps to fill. Enumerate them with the \`market\` skill's installed-asset listing (read the skill doc for the command; it reads the project's lock file and prints every installed asset's name, version, type, and file path), and skim the project's own files (\`public/\` and the implementation) to see which of those are already wired in and as what. If nothing is installed yet a bare scaffoldsay so plainly; the survey is then purely about what to add.
44
44
 
45
- Before writing, cross-check the goal against the Market asset types that could carry it templates, models, humanoid models and animations, textures, environments, sound effects, background music, and flipbooks. This is a coverage pass, not a checklist to pad the file: mention a type only when it serves this goal or when its absence creates a real gap. Pay special attention to asset needs that are easy to misclassify as "implementation": animation clips, surface materials, sky/HDR, continuous audio such as ambience or background music, one-shot sound effects, and visual feedback such as fire, impacts, magic, weather, UI/world pings, or explosions. When the goal needs both event feedback and ongoing mood, survey both; do not let sound effects stand in for music/ambience, or a visual effect stand in for its matching sound.
45
+ Then survey what the goal still needs that is not already here. Start from the user-visible experience, not from obvious nouns alone: include the things the player sees, hears, controls, collides with, collects, inhabits, and receives as moment-to-moment feedback. For each distinct need, first check whether an already-installed asset covers it; only where none does, search and preview the Drawcall Market for a composable addition (\`npx @drawcall/market\`; the \`market\` skill doc has the commands and the asset types), and judge fit from the real preview metadata, screenshots, files not the name. Survey composable building blocks, not another turnkey starter: a second whole-game starter would substitute for building rather than supply a piece, and one may already be applied.
46
46
 
47
- Before finishing, check that each asset-shaped word or sensory promise in the goal has an entry or an explicit gap. If the goal names music, a storm, muzzle flashes, impacts, explosions, collectibles, a place type, a character type, or any similar visible/audible thing, the survey should either name fitting assets for it or say that Market has no fitting asset.
47
+ Before writing, cross-check the goal against the Market asset types that could carry it models, humanoid models and animations, textures, environments, sound effects, background music, and flipbooks. This is a coverage pass, not a checklist to pad the file: mention a type only when it serves this goal or when its absence creates a real gap. Pay special attention to asset needs that are easy to misclassify as "implementation": animation clips, surface materials, sky/HDR, continuous audio such as ambience or background music, one-shot sound effects, and visual feedback such as fire, impacts, magic, weather, UI/world pings, or explosions. When the goal needs both event feedback and ongoing mood, survey both; do not let sound effects stand in for music/ambience, or a visual effect stand in for its matching sound.
48
48
 
49
- Write your findings to ${ASSET_SURVEY_FILE} (scratch, not committed): one entry per user-visible need with the fitting asset(s), what each actually is, and the gaps where nothing fits. Keep it concrete and skimmable, e.g.:
50
- - Collectible / pickup feedback: \`pickups-coin@1.0.2\` plus a fitting pickup sound — low-poly gold disc and a short reward cue. Strong fit.
51
- - Large outdoor place: no fitting island terrain model found gap; use terrain/material assets instead.`;
49
+ Before finishing, check that each asset-shaped word or sensory promise in the goal has an entry resolved as one of three: already installed, a fitting Market asset to add, or a gap where Market has no fitting asset. If the goal names music, a storm, muzzle flashes, impacts, explosions, collectibles, a place type, a character type, or any similar visible/audible thing, the survey should say which of the three it is.
50
+
51
+ Write your findings to ${ASSET_SURVEY_FILE} (scratch, not committed): one entry per user-visible need, marking whether it is already installed (with the asset name and how it is wired), needs a Market addition (with the fitting asset and what it actually is), or is a gap where nothing fits. Keep it concrete and skimmable, e.g.:
52
+ - Player character model: already installed — \`humanoid-tactical-commando@1.0.0\`, wired as the player. Reuse as-is.
53
+ - Enemy zombie model: not installed — \`humanoid-undead-zombie@1.0.0\` fits (a hunched, decayed humanoid); add it.
54
+ - Large outdoor terrain mesh: no fitting Market asset — gap; build from ground textures + scattered props.`;
52
55
  }
53
56
  export function buildSurveyTechnologyPrompt(userPrompt) {
54
57
  return `Stay in the current project directory.
55
58
 
56
59
  Goal: ${userPrompt}
57
60
 
58
- You are surveying, not planning or building: catalogue the installed technology that fits this goal, so the goal and plan stages can draw on what is real.
61
+ You are surveying, not planning or building: catalogue the capability (technology) situation so the goal and plan stages can modify efficiently what the project already builds and how, what installed technology serves the goal's remaining needs, and where nothing fits.
62
+
63
+ Read the existing implementation first. A starter may already be applied: read the source it ships (the entry point, \`src/\`, and config) and map what the project already does — the systems it already has (the game loop and architecture, the controller and camera, combat, world generation, UI, audio, and so on) and the technology each is built on. This is what the build will keep or modify, so name it concretely: the system, what it does, and the skill/package/API it uses. If \`src/\` is a bare scaffold with nothing built yet, say so plainly; the survey is then about the toolkit and the goal's needs built from scratch.
59
64
 
60
- This project ships with a set of installed agent skills — each a \`SKILL.md\` doc covering one capability — and a set of npm packages, and that set changes over time. Discover what is actually installed now rather than assuming or working from memory: list and read every installed skill's \`SKILL.md\`, and read package.json for the runtime packages. Skim them all first so you know the full toolkit before routing a single need — the skill you skip is the one a need belongs to. Each \`SKILL.md\` is the authority on what its technology does, when it fits, and its limits; for installed library packages, confirm the specific exports against the real types, which can lag the docs, and for technology a skill adds on demand (assets, optional packages) trust the \`SKILL.md\` and note it as an install-time addition. Follow each skill's own routing and limits — when a skill says to compose specific pieces or warns against a shortcut, do that rather than guessing a turnkey that conflicts.
65
+ This project also ships a set of installed agent skills — each a \`SKILL.md\` doc covering one capability — and a set of npm packages, and that set changes over time. Discover what is actually installed now rather than assuming or working from memory: list and read every installed skill's \`SKILL.md\`, and read package.json for the runtime packages. Skim them all first so you know the full toolkit before routing a single need — the skill you skip is the one a need belongs to. Each \`SKILL.md\` is the authority on what its technology does, when it fits, and its limits; for installed library packages, confirm the specific exports against the real types, which can lag the docs, and for technology a skill adds on demand (assets, optional packages) trust the \`SKILL.md\` and note it as an install-time addition. Follow each skill's own routing and limits — when a skill says to compose specific pieces or warns against a shortcut, do that rather than guessing a turnkey that conflicts.
61
66
 
62
- Decompose the goal into its concrete needs and map each to the technology that fits. Where no installed technology fits a need — often a game-system need like quests, inventory, AI, or a signature custom mechanic — name it as a fit-gap the build must implement itself, rather than forcing a poor fit.
67
+ Decompose the goal into its concrete needs and map each to where it stands: already built in the implementation (name the system and whether it is reused as-is or modified toward the goal), served by an installed skill/package (the real API/exports that fit), or a fit-gap with no installed technology — often a game-system need like quests, inventory, AI, or a signature custom mechanic — that the build must implement itself. A need the current implementation already satisfies is the cheapest kind; say so rather than re-deriving it, and rather than forcing a poor fit where nothing matches.
63
68
 
64
- Write your findings to ${TECH_SURVEY_FILE} (scratch, not committed): one entry per need, with the fitting skill/package and the real API/exports/asset that serves it, or the gap where nothing fits. Keep it concrete and skimmable, e.g. one bullet per need:
65
- - <need>: <fitting skill/package> — <the real API/exports that matter> — strong fit, following the skill's recommended composition.
69
+ Write your findings to ${TECH_SURVEY_FILE} (scratch, not committed): one entry per need, marking it already-built (the system and the tech it uses, reused or modified), installed-tech-fits (the skill/package and the real API/exports), or fit-gap (the build implements it). Keep it concrete and skimmable, e.g. one bullet per need:
70
+ - <need>: already built — <system / file role> on <tech> reuse as-is, or modify <how> toward the goal.
71
+ - <need>: <fitting skill/package> — <the real API/exports that matter> — following the skill's recommended composition.
66
72
  - <need>: no installed technology fits — fit-gap, the build implements it.`;
67
73
  }
68
74
  export function buildGoalPrompt(userPrompt) {
@@ -73,7 +79,7 @@ Goal: ${userPrompt}
73
79
  ${buildPrinciples()}
74
80
 
75
81
  Create ${GOAL_FILE}: a concrete, fixed picture of the finished game that every later turn builds toward. It is the target, not a plan or task list, and stays stable as the project grows.
76
- Ground it in what actually exists: read the surveys ${ASSET_SURVEY_FILE} and ${TECH_SURVEY_FILE} for the assets and technology that fit, and read ${README_FILE} for the current state — weighing how close that state already is to what the request wants. When ${README_FILE} shows a substantial, coherent game already in the same space as the request (an applied starter, not a bare scaffold), anchor the finished picture on that real implementation: take the scope and shape it has already settled as the substrate, and reach past it only for what the request genuinely needs and the current state does not yet deliver — rather than re-deriving an idealized version that re-opens settled scope or adds large systems the starter deliberately leaves out. The closer the current state already is to realizing the request, the more the goal is bound to it; a bare scaffold or a poor-fit starter binds it not at all, and the goal is then the full target the request deserves. Name the handful that define the look, feel, and mechanics (e.g. "low-poly style, terrain from asset A, props B/C, movement via skill D") — the defining choices, not a full asset manifest (the surveys already hold that). Where nothing fits a needed part, say so as a fit-gap rather than quietly dropping it.
82
+ Ground it in what actually exists: read the surveys ${ASSET_SURVEY_FILE} and ${TECH_SURVEY_FILE} for the assets and technology that fit, and read ${README_FILE} for the current state — weighing how close that state already is to what the request wants. When ${README_FILE} shows a substantial, coherent game already in the same space as the request (an applied starter, not a bare scaffold), anchor the finished picture on that real implementation: take the scope and shape it has already settled as the substrate, and reach past it only for what the request genuinely needs and the current state does not yet deliver — rather than re-deriving an idealized version that re-opens settled scope or adds large systems the starter deliberately leaves out. The closer the current state already is to realizing the request, the more the goal is bound to it; a bare scaffold or a poor-fit starter binds it not at all, and the goal is then the full target the request deserves. Binding to the substrate means reusing what works, not holding back on what is cheap to change and decisive to the player. A template becomes the request's own game only when it stops reading as the recognizable starter and reads as the requested game: the working systems and the settled scope stay, but the surface the player meets is re-shaped to the request — above all the game's own name and identity, and the signature screens and UI language that brand the original (a title card, an end-screen verdict, the framing copy a player would recognize). These changes are inexpensive and decisive, so the goal commits to a distinct identity for this game and treats re-shaping that recognizable surface as part of what the request needs, not optional polish. Reason from this specific request about which cheap, high-leverage changes carry the most of its feeling — do not work down a fixed list, and do not stop at swapping one system while the game still announces itself as the starter. Name the handful that define the look, feel, and mechanics (e.g. "low-poly style, terrain from asset A, props B/C, movement via skill D") — the defining choices, not a full asset manifest (the surveys already hold that). Where nothing fits a needed part, say so as a fit-gap rather than quietly dropping it.
77
83
 
78
84
  Write it the way a strong, short game design document reads — concrete, decisive, easy to picture — but as a layered design, not a flat list of headings. A game's design runs from the experience it is for at the top down to how each thing looks and feels in the player's hands at the bottom, and the levels are causal in both directions: the top decides why every lower thing exists, while the player meets the game from the bottom up — touching the feel of a single action first, and only through it sensing the experience you aimed at. So design top-down and keep each lower choice traceable to the level above (this enemy, this loop, this verb earns its place by serving the experience), and never stop at rules-on-paper — because the player lives at the bottom, the bottom must be drawn as concretely as the top.
79
85
  The levels below, with the questions that live at each, are context to reason from, not a template to fill or a checklist to tick. Reason from this specific game about which levels carry it and how deep each goes — a puzzler lives on its mechanics, an exploration game on its world and mood, an arcade game on feel and mastery, a story RPG on its characters and the plot and world they inhabit — and say plainly what you are deliberately keeping thin or absent, which is itself a design decision. Go as deep on the level a game lives on as that game needs: the depth a story game owes its characters and plot is the depth an action game owes its feel — do not let any one level default to thin because it is harder to write. Add a question a level needs that is not here.
@@ -104,6 +110,8 @@ Write ${PLAN_FILE}: the ordered steps from the current state to the goal. A step
104
110
 
105
111
  Size each step to the most a single build-turn can confidently build and stand behind in one go, and group into it the features that complete one testable capability together. Resolve the real tension between two failure modes: a pure one-mechanic-at-a-time slice proves out every turn but tends to build throwaway scaffolding (a stand-in you later discard, a thin version you rewrite) and pay for the same area twice; a pure feature-batch implements each feature once but is too big to build and prove in a turn. Aim for the middle — group the features that naturally belong to one capability so each is implemented once, against its real collaborators, while the step still fits and proves in one turn. A system that only proves out with another (shooting needs something to shoot, loot needs someone to drop it) is usually a cue to group the two into one step, not to split them behind a stand-in: prefer building a feature with its real collaborators when they fit the same turn. Reach for a deliberate stand-in only when the real collaborator genuinely cannot fit the same turn and the stand-in is cheap and minimal — never substantial scaffolding you will throw away. Build shared foundations right the first time: the ECS spine, the controller/camera rig, the audio system and other substrate should be established correctly when first needed and reused — not built thin and "consolidated" in a later refactor (a planned pure-refactor step is a sign a foundation was under-built, and a build-turn spent on rework is a step the budget can't afford). A step that cannot exist until an earlier one lands comes after it. Size each step to one build-turn's worth, and let the plan run exactly as long as the real distance from the current state to the goal demands — no longer. The build-turn budget is a ceiling, not a quota to fill: when the current state already realizes most of the goal the plan is correspondingly short — as few as a single step — and you neither pad it with generic polish or re-verification of what already works nor stretch a small delta to resemble a full build; when the distance is large, use as many right-sized steps as it takes, preferring a few whole steps to a long trail of fragments and never a step too large for one turn to finish and stand behind.
106
112
 
113
+ When the plan starts from an applied starter rather than a blank repo, most steps are modifications of what already runs, not new construction: reskinning the look, renaming and re-theming the cast, swapping a model or texture, re-grading the lighting, re-parameterizing the procedural generation, re-pointing a system's inputs or win condition. Favor the cheapest change that lands the goal's intended shift in how the game reads and feels — a deep change in the experience need not be a deep change in the code, and re-shaping what exists usually beats rebuilding it. Plan the cheap, high-impact re-shapings of the player-facing surface that the goal commits to — the game's name and identity, the signature screens, the framing copy that brands the original — as first-class step content, not afterthoughts: each is a small edit, but together they are the highest-leverage work in a reskin, the easiest to drop under pressure, and what stops the result reading as the recognizable starter; leaving them undone leaves the player facing the old game's identity. Size and gate such a step exactly like a built-from-scratch slice — outcome, fit, and a gate that confirms it reads right when run — and do not add a step for any part of the goal the current state already satisfies.
114
+
107
115
  A slice is the whole of the thing it introduces, not its mechanic alone. Reason from the goal about what makes each one real in the player's hands — its animation and feedback, how the player comes to know what to do and can see their current goal, the transitions and screens that frame it — and carry those into the same step, never deferring them to a later polish the embodiment principle forbids. Which of these a slice lives on depends on what it is; weigh them, do not tick them off a list.
108
116
 
109
117
  Some of the game is not a vertical slice at all — the continuous, cross-cutting layers no single feature owns: the audio bed (ambience and music), the atmospheric look (lighting mood and the postprocessing pass), the shared HUD frame, and the first-load/loading screen. Place these deliberately instead of letting them fall to a final step a stalled plan may never reach. Introduce a layer's foundation in the first step where the player would feel its absence — world ambience and music with the first explorable world, the look pass once there is a scene to grade, the loading screen in the very first step (any real build loads assets before it can show anything, so a blank canvas is felt from the start) — and extend it as the game grows. Foundational mood is part of the feel the goal commits to, not last-minute polish; a game that reaches its last planned step before it has any music or atmosphere was planned in the wrong order.
@@ -132,7 +140,8 @@ ${buildSliceMethod()}
132
140
  Use ${README_FILE} as the claimed current state, ${GOAL_FILE} as the fixed goal, and ${PLAN_FILE} as the plan.
133
141
  Take the first remaining ${PLAN_FILE} step as this turn's task and build the whole of it — the grouped features it names — with the fitting skills/packages/assets, allowing only the small prerequisites or repairs that make the step actually work. A right-sized step is one turn's work, so complete it rather than fragmenting it.
134
142
  Only if the step genuinely cannot fit one turn, split off the smallest coherent remainder as a single new ${PLAN_FILE} step (not a trail of fragments), and finish the rest now. Do not add pure-refactor, cleanup, or "consolidate the architecture" steps that don't advance the product — build the foundation correctly here, which means laying the code out as cohesive modules from the first turn (follow each skill's recommended file layout, such as the ecs skill's one-file-per-component and one-file-per-system split, rather than piling the game into one growing main file) instead of leaving rework for a future turn the budget can't afford. When a feature needs a collaborator that exists later in the plan, prefer pulling it forward into this step over building a throwaway stand-in you will discard.
135
- Prove the result with a proof-run that actually launches and drives the real running repo this turn — a written description is never a substitute for a run. The proof is a machine-produced artifact saved under \`${PROOF_DIR}/\` (gitignored scratch): a screenshot or clip captured from the running app, or — if you cannot view images — a recorded runtime-state dump that asserts the real-done runtime facts, each produced by the command you actually ran rather than authored by hand. Look at the screenshots/clips from the player's seat and judge them against the goal's real-done bar, iterating on the build until it reads right; a prose "verification" note with no run behind it does not satisfy the gate. Then close or rewrite the ${PLAN_FILE} step to reflect what is actually proven; if a turn only proved part of the step, keep the unproven parts as first-class remaining steps rather than caveats.
143
+ Prove the result with a proof-run that actually launches and drives the real running repo this turn — a written description is never a substitute for a run. The proof is a machine-produced artifact saved under \`${PROOF_DIR}/\` (gitignored scratch): a screenshot or clip captured from the running app, or — if you cannot view images — a recorded runtime-state dump that asserts the real-done runtime facts, each produced by the command you actually ran rather than authored by hand. Look at the screenshots/clips from the player's seat and judge them against the goal's real-done bar, iterating on the build until it reads right; a prose "verification" note with no run behind it does not satisfy the gate. When the step modifies an already-proven product, scope the proof to what this step actually changed — the new behavior, the new look, the new feedback — and rely on the carried proof for systems reused unchanged rather than re-driving and re-proving the whole game each turn; that re-verification is the budget the turn cannot afford. Spend the time it saves on completing the step's full named scope: the cheap, high-impact edits — the identity, the wordmark, the title and HUD copy, the palette — whose deep effect on how the product reads is easy to drop under time pressure are exactly the ones that make it read as its new self, so land them, never leave the player-facing name or framing describing the old product. Then update ${PLAN_FILE} to reflect what is actually proven: close the step you finished, and if a turn only proved part of it, keep the unproven parts as first-class remaining steps rather than caveats.
144
+ When the last step is proven and the goal is genuinely realized in the running product — its playthroughs reproduce end to end and every real-done aspect is met — delete ${PLAN_FILE} so the pipeline knows the build is complete and stops; this is the done signal, so do not spend a further turn re-verifying what is already proven. While any real work remains, keep ${PLAN_FILE} holding only the steps that are still open.
136
145
  If real findings change the project understanding, update ${GOAL_FILE} lightly and honestly.
137
146
  Finally, update ${README_FILE} so it truthfully describes the new proven state, what changed, what remains in ${PLAN_FILE}, and any remaining gaps.`;
138
147
  }
package/dist/scaffold.js CHANGED
@@ -12,7 +12,24 @@ export async function initGitRepo(cwd, runner) {
12
12
  }
13
13
  export async function commitAll(cwd, runner, message) {
14
14
  await assertExitCode(runner({ command: "git", args: ["add", "-A"], cwd }), "failed to stage changes");
15
- await assertExitCode(runner({ command: "git", args: ["commit", "--allow-empty", "-m", message], cwd }), "failed to commit changes");
15
+ // A build turn often commits its own work mid-turn (the harness runs git itself). When it does,
16
+ // the staged tree is already clean here and an --allow-empty commit would mint a second, empty
17
+ // "feat: build turn N" — so commit count drifts above turn count. Only commit when something is
18
+ // actually staged; otherwise the turn's own commit already records this turn's completion.
19
+ if (await isTreeClean(cwd, runner))
20
+ return;
21
+ await assertExitCode(runner({ command: "git", args: ["commit", "-m", message], cwd }), "failed to commit changes");
22
+ }
23
+ // True when `git add -A` left nothing staged (a clean index against HEAD). `git diff --cached
24
+ // --quiet` exits 0 when there are no staged changes and 1 when there are, which is exactly the
25
+ // signal we want — so we read its exit code rather than asserting it.
26
+ async function isTreeClean(cwd, runner) {
27
+ const { exitCode } = await runner({
28
+ command: "git",
29
+ args: ["diff", "--cached", "--quiet"],
30
+ cwd
31
+ });
32
+ return exitCode === 0;
16
33
  }
17
34
  export async function initNpmProject(cwd, _runner) {
18
35
  const packageJsonPath = join(cwd, "package.json");
@@ -1,13 +1,16 @@
1
1
  import { spawn } from "node:child_process";
2
2
  import { createWriteStream } from "node:fs";
3
3
  import { delimiter, dirname, join, resolve } from "node:path";
4
+ import { createInterface } from "node:readline";
4
5
  import which from "which";
5
6
  import { CliError, TIMEOUT_EXIT_CODE, TIMEOUT_KILL_GRACE_MS } from "./constants.js";
7
+ import { formatDuration } from "./progress-log.js";
6
8
  export function createSubprocessRunner(options = { stdio: "inherit" }) {
7
9
  return (invocation) => runSubprocess(invocation, options);
8
10
  }
9
11
  function runSubprocess({ command, args, cwd, timeoutMs }, options) {
10
12
  return new Promise((resolveResult, reject) => {
13
+ const startedAt = Date.now();
11
14
  // A separate process group lets a timeout kill the whole child tree at once.
12
15
  const detached = process.platform !== "win32";
13
16
  // When a log file is set, capture all child output into it and keep the
@@ -20,8 +23,14 @@ function runSubprocess({ command, args, cwd, timeoutMs }, options) {
20
23
  stdio: logStream ? ["ignore", "pipe", "pipe"] : (options.stdio ?? "inherit")
21
24
  });
22
25
  if (logStream) {
23
- child.stdout?.pipe(logStream, { end: false });
24
- child.stderr?.pipe(logStream, { end: false });
26
+ // Stamp every captured line with the wall-clock time we received it instead of piping raw
27
+ // bytes. Reading the harness's output at our own boundary and timing it with our own clock is
28
+ // the one timing signal robust across every harness and version — it depends on no private
29
+ // session-transcript format or structured-output schema (both drift, and for backgrounded
30
+ // tool calls misreport durations). The gaps between timestamps are then where the wall-clock
31
+ // went: a long stall with no output is the model thinking or one command grinding.
32
+ forwardWithTimestamps(child.stdout, logStream);
33
+ forwardWithTimestamps(child.stderr, logStream);
25
34
  }
26
35
  let timedOut = false;
27
36
  let killTimer;
@@ -60,10 +69,21 @@ function runSubprocess({ command, args, cwd, timeoutMs }, options) {
60
69
  reject(error);
61
70
  });
62
71
  child.once("exit", (code, signal) => {
72
+ const exitCode = timedOut ? TIMEOUT_EXIT_CODE : signal ? 1 : (code ?? 1);
73
+ // Always record how long the command ran, so the log shows where wall-clock goes (each
74
+ // scaffold step, and each harness turn's total) without digging into the harness's own
75
+ // session transcript. A non-zero exit is named on the same line so a failure is unmistakable
76
+ // and the boundary between a failed turn and a following retry stays legible. Timeouts
77
+ // already logged their own notice, so only the duration is added for them.
78
+ if (logStream) {
79
+ const elapsed = formatDuration(Date.now() - startedAt);
80
+ const failure = !timedOut && exitCode !== 0
81
+ ? ` — exit ${exitCode}${signal ? ` (signal ${signal})` : ""}`
82
+ : "";
83
+ logStream.write(`[drawcall-create] ${command} done in ${elapsed}${failure}\n`);
84
+ }
63
85
  cleanup();
64
- if (timedOut)
65
- return resolveResult({ exitCode: TIMEOUT_EXIT_CODE, timedOut: true });
66
- resolveResult({ exitCode: signal ? 1 : (code ?? 1), timedOut: false });
86
+ resolveResult({ exitCode, timedOut });
67
87
  });
68
88
  });
69
89
  }
@@ -100,6 +120,23 @@ function openSessionLog(logFile, command) {
100
120
  stream.write(`\n$ ${command.join(" ")}\n`);
101
121
  return stream;
102
122
  }
123
+ // Copy a child stream into the log a line at a time, stamping each line with the wall-clock time we
124
+ // received it. readline does the line splitting (including a final line with no trailing newline,
125
+ // and \r\n) so a timestamp only ever prefixes a whole line.
126
+ function forwardWithTimestamps(source, sink) {
127
+ if (!source)
128
+ return;
129
+ const lines = createInterface({ input: source });
130
+ lines.on("line", (line) => sink.write(`${logTimestamp()} ${line}\n`));
131
+ }
132
+ // Absolute wall-clock HH:MM:SS.mmm so any two lines anywhere in the log — within a turn or across
133
+ // stages — can be subtracted directly, and the gap from the last output line to the "done in"
134
+ // footer (the idle tail before the process exits) is visible too.
135
+ function logTimestamp() {
136
+ const now = new Date();
137
+ const pad = (value, length = 2) => String(value).padStart(length, "0");
138
+ return `[${pad(now.getHours())}:${pad(now.getMinutes())}:${pad(now.getSeconds())}.${pad(now.getMilliseconds(), 3)}]`;
139
+ }
103
140
  function killChildProcess(pid, detached, signal) {
104
141
  if (!pid)
105
142
  return;
@@ -0,0 +1,28 @@
1
+ import { type HarnessName } from "./constants.js";
2
+ import { type CommandResult } from "./subprocess.js";
3
+ export type RunBuildTurnChild = (cwd: string) => Promise<CommandResult>;
4
+ export type SuperviseBuildOptions = {
5
+ cwd?: string;
6
+ env?: NodeJS.ProcessEnv;
7
+ maxTurns?: number;
8
+ harness?: HarnessName;
9
+ harnessArgs?: string[];
10
+ harnessTimeoutMinutes?: number;
11
+ runBuildTurnChild?: RunBuildTurnChild;
12
+ };
13
+ export type SuperviseBuildResult = {
14
+ projectDir: string;
15
+ turns: number;
16
+ /** Why the loop stopped — surfaced to the caller and the session log. */
17
+ stop: "plan-consumed" | "budget-exhausted" | "stuck";
18
+ };
19
+ /**
20
+ * Run the build stage so it ALWAYS completes or cleanly resumes, even when a turn's child process
21
+ * is killed mid-turn by an uncatchable OOM/jetsam SIGKILL. Each turn runs as a separate child, so
22
+ * this supervisor holds almost no memory and is an unlikely OOM victim itself; if a turn-child dies
23
+ * without committing, the supervisor resets to the last known-good commit and retries.
24
+ *
25
+ * The repo must already be scaffolded, surveyed, and planned (a committed PLAN.md) — run the
26
+ * earlier stages with a normal `createProject` first. This is the build loop only.
27
+ */
28
+ export declare function superviseBuild(options?: SuperviseBuildOptions): Promise<SuperviseBuildResult>;
@@ -0,0 +1,110 @@
1
+ import { execFileSync } from "node:child_process";
2
+ import { appendFileSync, existsSync, readFileSync } from "node:fs";
3
+ import { join, resolve } from "node:path";
4
+ import { fileURLToPath } from "node:url";
5
+ import { CliError, MAX_BUILD_TURNS, PLAN_FILE, SESSION_LOG_FILE } from "./constants.js";
6
+ import { createSubprocessRunner } from "./subprocess.js";
7
+ // Two consecutive turns that neither advance HEAD with real work nor touch PLAN.md mean the build
8
+ // is wedged (a turn that keeps crashing the same way, or a no-op turn the harness can't get past).
9
+ // Stopping then is honest: looping further just burns hours re-running the identical dead step.
10
+ const STUCK_ATTEMPT_LIMIT = 2;
11
+ /**
12
+ * Run the build stage so it ALWAYS completes or cleanly resumes, even when a turn's child process
13
+ * is killed mid-turn by an uncatchable OOM/jetsam SIGKILL. Each turn runs as a separate child, so
14
+ * this supervisor holds almost no memory and is an unlikely OOM victim itself; if a turn-child dies
15
+ * without committing, the supervisor resets to the last known-good commit and retries.
16
+ *
17
+ * The repo must already be scaffolded, surveyed, and planned (a committed PLAN.md) — run the
18
+ * earlier stages with a normal `createProject` first. This is the build loop only.
19
+ */
20
+ export async function superviseBuild(options = {}) {
21
+ const cwd = resolve(options.cwd ?? process.cwd());
22
+ const env = options.env ?? process.env;
23
+ const maxTurns = options.maxTurns ?? MAX_BUILD_TURNS;
24
+ const runBuildTurnChild = options.runBuildTurnChild ?? defaultRunBuildTurnChild(env, options);
25
+ if (!existsSync(join(cwd, ".git"))) {
26
+ throw new CliError(`supervised build expects an existing git repo at ${cwd}`);
27
+ }
28
+ const planPath = join(cwd, PLAN_FILE);
29
+ let staleAttempts = 0;
30
+ for (let turn = 1; turn <= maxTurns; turn += 1) {
31
+ if (!existsSync(planPath))
32
+ return done(cwd, turn - 1, "plan-consumed");
33
+ // The soccer hazard: a prior turn killed mid-edit can leave the tree dirty with partial,
34
+ // partially-destructive changes (e.g. half-deleted source). Build turns re-read repo state and
35
+ // redo the step, so resume ONLY from a clean committed state. `clean -fd` (not -x) preserves
36
+ // node_modules and the gitignored surveys/proof scratch; never `git add -A` here.
37
+ resetToLastGoodCommit(cwd);
38
+ const before = captureProgressMarker(cwd, planPath);
39
+ await runBuildTurnChild(cwd);
40
+ const after = captureProgressMarker(cwd, planPath);
41
+ if (!existsSync(planPath))
42
+ return done(cwd, turn, "plan-consumed");
43
+ if (madeProgress(before, after)) {
44
+ staleAttempts = 0;
45
+ continue;
46
+ }
47
+ staleAttempts += 1;
48
+ if (staleAttempts >= STUCK_ATTEMPT_LIMIT) {
49
+ logToSession(cwd, `[drawcall-create] supervised build stuck: ${STUCK_ATTEMPT_LIMIT} turns with no new commit and no ${PLAN_FILE} change`);
50
+ return done(cwd, turn, "stuck");
51
+ }
52
+ }
53
+ return done(cwd, maxTurns, existsSync(planPath) ? "budget-exhausted" : "plan-consumed");
54
+ }
55
+ function madeProgress(before, after) {
56
+ return after.head !== before.head || after.planText !== before.planText;
57
+ }
58
+ function captureProgressMarker(cwd, planPath) {
59
+ return {
60
+ head: headCommit(cwd),
61
+ planText: existsSync(planPath) ? readFileSync(planPath, "utf8") : ""
62
+ };
63
+ }
64
+ // HEAD's commit hash, or "" before the first commit exists. `git rev-parse HEAD` throws on an empty
65
+ // repo, which we read as "no commit yet" rather than a failure.
66
+ function headCommit(cwd) {
67
+ try {
68
+ return execFileSync("git", ["rev-parse", "HEAD"], { cwd, encoding: "utf8" }).trim();
69
+ }
70
+ catch {
71
+ return "";
72
+ }
73
+ }
74
+ function resetToLastGoodCommit(cwd) {
75
+ execFileSync("git", ["reset", "--hard", "HEAD"], { cwd, stdio: "ignore" });
76
+ execFileSync("git", ["clean", "-fd"], { cwd, stdio: "ignore" });
77
+ }
78
+ function done(cwd, turns, stop) {
79
+ return { projectDir: cwd, turns, stop };
80
+ }
81
+ function logToSession(cwd, line) {
82
+ try {
83
+ appendFileSync(join(cwd, SESSION_LOG_FILE), `${line}\n`);
84
+ }
85
+ catch {
86
+ // No session log on disk (e.g. a test working dir) — the supervisor proceeds anyway.
87
+ }
88
+ }
89
+ // Re-invoke this CLI's own build stage as a fresh child: `--stage build` runs exactly one turn
90
+ // (create.ts turnBudget), so the heavy turn state lives and dies in the child, not in the
91
+ // supervisor. cli.js sits next to this compiled module.
92
+ function defaultRunBuildTurnChild(env, options) {
93
+ const cliPath = fileURLToPath(new URL("./cli.js", import.meta.url));
94
+ const runner = createSubprocessRunner({ env, stdio: "inherit" });
95
+ const args = [cliPath, "--stage", "build"];
96
+ if (options.harness)
97
+ args.push("--harness", options.harness);
98
+ if (options.harnessTimeoutMinutes !== undefined) {
99
+ args.push("--harness-timeout-minutes", String(options.harnessTimeoutMinutes));
100
+ }
101
+ args.push(BUILD_RESUME_PROMPT);
102
+ if (options.harnessArgs && options.harnessArgs.length > 0) {
103
+ args.push("--", ...options.harnessArgs);
104
+ }
105
+ return (cwd) => runner({ command: process.execPath, args, cwd });
106
+ }
107
+ // The build stage re-reads the committed records (GOAL.md/PLAN.md/README.md) and continues the
108
+ // plan, so the prompt is just a resume marker — the real instructions live in the build prompt the
109
+ // child assembles. The CLI requires a non-empty prompt.
110
+ const BUILD_RESUME_PROMPT = "resume the build from the committed plan";
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@drawcall/create",
3
- "version": "0.0.0",
3
+ "version": "0.1.2",
4
4
  "type": "module",
5
5
  "description": "Create projects with an installed local harness.",
6
6
  "license": "MIT",
@@ -12,7 +12,7 @@
12
12
  "node": ">=20"
13
13
  },
14
14
  "bin": {
15
- "drawcall-create": "./dist/cli.js"
15
+ "drawcall-create": "dist/cli.js"
16
16
  },
17
17
  "exports": {
18
18
  ".": {
@@ -24,6 +24,15 @@
24
24
  "files": [
25
25
  "dist"
26
26
  ],
27
+ "scripts": {
28
+ "dev": "tsx src/cli.ts",
29
+ "build": "node -e \"require('fs').rmSync('dist', { recursive: true, force: true })\" && tsc -p tsconfig.json && node -e \"require('fs').chmodSync('dist/cli.js', 0o755)\"",
30
+ "typecheck": "tsc -p tsconfig.json --noEmit && tsc -p tsconfig.test.json",
31
+ "test": "vitest run",
32
+ "format": "prettier --write .",
33
+ "format:check": "prettier --check .",
34
+ "prepublishOnly": "npm run typecheck && npm test && npm run build"
35
+ },
27
36
  "dependencies": {
28
37
  "commander": "^14.0.3",
29
38
  "which": "^6.0.1"
@@ -35,13 +44,5 @@
35
44
  "tsx": "^4.19.2",
36
45
  "typescript": "^5.7.2",
37
46
  "vitest": "^4.1.8"
38
- },
39
- "scripts": {
40
- "dev": "tsx src/cli.ts",
41
- "build": "node -e \"require('fs').rmSync('dist', { recursive: true, force: true })\" && tsc -p tsconfig.json && node -e \"require('fs').chmodSync('dist/cli.js', 0o755)\"",
42
- "typecheck": "tsc -p tsconfig.json --noEmit && tsc -p tsconfig.test.json",
43
- "test": "vitest run",
44
- "format": "prettier --write .",
45
- "format:check": "prettier --check ."
46
47
  }
47
- }
48
+ }