@drawcall/create 0.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +249 -0
- package/dist/cli.d.ts +2 -0
- package/dist/cli.js +18 -0
- package/dist/command.d.ts +16 -0
- package/dist/command.js +123 -0
- package/dist/constants.d.ts +26 -0
- package/dist/constants.js +94 -0
- package/dist/create.d.ts +35 -0
- package/dist/create.js +348 -0
- package/dist/harness.d.ts +13 -0
- package/dist/harness.js +81 -0
- package/dist/index.d.ts +8 -0
- package/dist/index.js +8 -0
- package/dist/progress-log.d.ts +31 -0
- package/dist/progress-log.js +95 -0
- package/dist/prompts.d.ts +6 -0
- package/dist/prompts.js +138 -0
- package/dist/scaffold.d.ts +15 -0
- package/dist/scaffold.js +220 -0
- package/dist/subprocess.d.ts +21 -0
- package/dist/subprocess.js +117 -0
- package/package.json +47 -0
package/dist/prompts.js
ADDED
|
@@ -0,0 +1,138 @@
|
|
|
1
|
+
import { ASSET_SURVEY_FILE, GOAL_FILE, PLAN_FILE, PROOF_DIR, README_FILE, TECH_SURVEY_FILE } from "./constants.js";
|
|
2
|
+
// Shared quality philosophy, stated in the project's language (see LANGUAGE.md): a turn
|
|
3
|
+
// either changes the product (build) or writes a record (think); a step is real only when
|
|
4
|
+
// the product really does the thing when run; the goal must never be shrunk. Stated as a few
|
|
5
|
+
// principles the harness applies by reasoning, not a checklist of concrete examples —
|
|
6
|
+
// enumerated lists make the model anchor on the listed cases and go shallow on everything else.
|
|
7
|
+
function buildPrinciples() {
|
|
8
|
+
return `Principles — apply them by reasoning about this goal, not by matching a checklist:
|
|
9
|
+
1. Real, not nominal: a step is done only when the product actually does the thing when run. The right category being present is not done, and the goal must never be silently shrunk into a prototype.
|
|
10
|
+
2. Fit the tools: the installed Drawcall skills and runtime packages are the default toolkit. Discover what is installed rather than assuming — read the installed skills' \`SKILL.md\` docs for the real API and examples before using one. For each need the work has, fit the right skill/package/asset and use its real API/exports/examples for its role; if nothing fits, name the fit-gap and its user-visible tradeoff rather than shipping a silent cheap substitute.
|
|
11
|
+
3. Right feedback, right place: present state where the user perceives it and in the medium that suits it, and keep debug/instrumentation out of the default product view.
|
|
12
|
+
4. See and judge your own work: the test that counts is observing the real running product — drive it, and watch and listen to clips and screenshots from the player's seat — and judging what you see and hear against the look and feel the goal commits to, then iterating until it actually reads right. Not internal counters, not source-file validity, and not mere presence: a part that runs without error but looks wrong, reads flat, sounds wrong, or feels off is not done.
|
|
13
|
+
5. Things that move or act are embodied: whatever the user sees move, act, or react carries its motion/animation, its moment-to-moment behavior, and its on-entity feedback as parts of the feature, not as later polish. A controller or path or number is not the behavior; a model is not the animation.`;
|
|
14
|
+
}
|
|
15
|
+
// The anti-blind-spot move for planning/building: derive the concerns for each slice from the
|
|
16
|
+
// goal instead of recognising them from a fixed list.
|
|
17
|
+
function buildSliceMethod() {
|
|
18
|
+
return `Method — derive the concerns per slice instead of recognising them from a list: for every user-visible thing a slice introduces, of whatever kind, reason from the goal about what would make that specific thing feel real and complete, budget those concerns in the slice, then explicitly name whatever you are leaving unaddressed. Treat broad labels (combat, enemies, building, navigation, UI, audio, and so on) as prompts to decompose into the parts that thing actually needs — then group the parts that complete one capability into this turn's step and build them together as one provable whole, rather than fragmenting each into its own deferred step. Decomposing is to see the scope and budget it, not to multiply steps.`;
|
|
19
|
+
}
|
|
20
|
+
export function buildTemplatePrompt(userPrompt) {
|
|
21
|
+
return `Stay in the current directory.
|
|
22
|
+
|
|
23
|
+
Goal: ${userPrompt}
|
|
24
|
+
|
|
25
|
+
${buildPrinciples()}
|
|
26
|
+
|
|
27
|
+
This directory already holds a scaffolded project (package.json, installed skills/packages).
|
|
28
|
+
Use npx @drawcall/market to look for a reasonable starter that covers the requested experience.
|
|
29
|
+
Read the Market workflow with \`npx @drawcall/market skill\` if needed, and search starters with commands shaped like \`npx @drawcall/market search "<query>" --type template --limit 3\`.
|
|
30
|
+
Prefer a fitting starter or asset-backed starter over a from-scratch setup when it covers the requested experience.
|
|
31
|
+
If one fits, apply it into this directory, merging it with what is already here and preserving the installed skills/packages, without creating a nested project.
|
|
32
|
+
Do not choose a template that omits major requirements from the goal.
|
|
33
|
+
If none fits, make no filesystem changes and exit successfully.
|
|
34
|
+
Do not implement the app.`;
|
|
35
|
+
}
|
|
36
|
+
export function buildSurveyAssetsPrompt(userPrompt) {
|
|
37
|
+
return `Stay in the current project directory.
|
|
38
|
+
|
|
39
|
+
Goal: ${userPrompt}
|
|
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.
|
|
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 preview — metadata, screenshots, files — not the name.
|
|
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.
|
|
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.
|
|
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.`;
|
|
52
|
+
}
|
|
53
|
+
export function buildSurveyTechnologyPrompt(userPrompt) {
|
|
54
|
+
return `Stay in the current project directory.
|
|
55
|
+
|
|
56
|
+
Goal: ${userPrompt}
|
|
57
|
+
|
|
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.
|
|
59
|
+
|
|
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.
|
|
61
|
+
|
|
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.
|
|
63
|
+
|
|
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.
|
|
66
|
+
- <need>: no installed technology fits — fit-gap, the build implements it.`;
|
|
67
|
+
}
|
|
68
|
+
export function buildGoalPrompt(userPrompt) {
|
|
69
|
+
return `Stay in the current project directory.
|
|
70
|
+
|
|
71
|
+
Goal: ${userPrompt}
|
|
72
|
+
|
|
73
|
+
${buildPrinciples()}
|
|
74
|
+
|
|
75
|
+
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.
|
|
77
|
+
|
|
78
|
+
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
|
+
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.
|
|
80
|
+
- Experience and fantasy (top — what the game is for): the core feeling and kind of fun it trades in (sensation, make-believe, drama, challenge and mastery, fellowship, discovery, expression, or pastime — name the one or two that are primary), who the player is and the fantasy they get to live, the premise and the hook that makes the player care, the handful of design pillars every lower choice must serve, and — beyond the first session — what would bring a player back. A game with none of this reads as a tech demo.
|
|
81
|
+
- Loops and progression (how the experience is produced over time): the core loop stated as the one-sentence "what you do", the macro arc of a session and what makes one sitting feel complete, and how challenge, variety, and stakes build across it — the shape of the curve, not just "it gets harder". Commit to where the game's lasting richness comes from — mastery, systemic interaction, story, exploration, replay variety, atmosphere — and be honest about which are thin.
|
|
82
|
+
- Systems and rules (the authored substrate): the player's verbs, the objectives, the resources and their economy, the boundaries, and the win and loss conditions where they exist — the rules that, set in motion, produce the loops above.
|
|
83
|
+
- Entities and world (the concrete cast): the specific things that fill the game — the player, allies, enemies, obstacles, pickups — each as a presence with a role and a behavior rather than a prop, and the world or levels that hold them at a real scale, in numbers. Where the game lives on its fiction, this is also the level of the story and the people in it: the overall plot, each significant character's identity, motivation, and backstory, and the world's lore and the immersion it builds — written to the depth that game needs, not reduced to a one-line premise. Some games are essentially non-narrative or single-screen; say so plainly.
|
|
84
|
+
- Feel and feedback (bottom — moment to moment, the micro loop): what the player sees, hears, and feels in the instant they act — embodiment, animation, game feel, audio, VFX — presented where the player perceives it. Feedback is part of the verb, not a later polish pass; a controller or a number is not the feel. For each thing that carries the game, say what it actually looks like and how it reads and behaves second-to-second, concrete enough to hold a screenshot against. But the still frame is not the whole of feel: some of what carries a game is continuous and not visual at all — above all the audio bed and the mood it sustains (music, ambience, the soundscape under the action), which no screenshot holds. Give it the same concreteness — what it sounds like and how it shifts with the game's state — and remember it when you set the real-done bar below, where it is judged by ear over a clip rather than by a frame.
|
|
85
|
+
- Controls, camera, and onboarding (across the stack): the input scheme and perspective, how the player learns the verbs without a wall of tutorial, and how they can always tell what their current goal is.
|
|
86
|
+
- Presentation and the ending: the player-facing states and the moments between them — the first-load screen the player meets while assets stream in (a loading screen carrying the game's identity and progress, never a blank canvas), title, pause, win and loss screens, replay — and the camera moves, cutscenes, and transitions that frame beats and carry the player between states; and how a session resolves, in success and in failure where the game can be lost.
|
|
87
|
+
|
|
88
|
+
The document must contain a playthrough that grounds the whole design: a concrete, timed traversal from the player's seat ("on load …; a few seconds in …; a minute in …") that walks a real session to its terminal as a script, not a summary. Trace the arcs that actually matter for this game — a full successful run for certain, and the loss or failure path too where the game can be lost (a game that can be lost but cannot show its own loss is not finished); do not manufacture a failure arc for a game that has none. For each beat make three things concrete: what the player perceives and how they come to know what to do next (the signpost, prompt, marker, or feedback that motivates the action — never assume the player simply knows); what they do; and how the game responds, including the transitions — camera moves, cutscenes, screen and state changes — that carry them between moments. Track the player's state and growing sense of progress so it reads as a lived session, not a feature list. This playthrough is part of the goal document, not the proof step below.
|
|
89
|
+
|
|
90
|
+
Plus, for this pipeline, Real-done — the bar later turns hold themselves to: the handful of things that make this game genuinely good, not merely present, each one paired with how a build-turn confirms it in the running product, so the bar and its check live together rather than as two separate lists. Write each as an aspect plus its validation. The aspect is in look-and-feel terms concrete enough to judge by eye or ear — "the map reads as <its intended place> from the player's seat", not "a map exists"; "the enemy reads as <its intended threat> as it moves and takes a hit", not "the enemy is implemented". The validation is the observation that settles that aspect, made by watching and listening to the running product from the player's seat and not assembled as evidence for someone else: a runtime fact that is plainly true or false (a hit drops health; death shows the loss screen), or a qualitative read of a screenshot or clip — by eye ("the night forest looks stunning, a place you'd want to linger") or by ear ("the score swells into the fight and drops to a hush when it ends") — or whatever mix the aspect needs; judge continuous and audio aspects over a clip, not a still frame. Cover the game this way, and treat the playthrough(s) above — reproduced end-to-end in the running product — as the overarching check, not only isolated slices.
|
|
91
|
+
|
|
92
|
+
Do not create ${PLAN_FILE} and do not implement the app.`;
|
|
93
|
+
}
|
|
94
|
+
export function buildPlanPrompt(userPrompt) {
|
|
95
|
+
return `Stay in the current project directory.
|
|
96
|
+
|
|
97
|
+
Goal: ${userPrompt}
|
|
98
|
+
|
|
99
|
+
${buildPrinciples()}
|
|
100
|
+
|
|
101
|
+
Ground the plan in what is real: ${GOAL_FILE} is the fixed goal, ${README_FILE} the claimed current state, and the scratch surveys ${ASSET_SURVEY_FILE} and ${TECH_SURVEY_FILE} the already-gathered map of what fits. Verify the actual project state before writing — the surveys can be stale.
|
|
102
|
+
|
|
103
|
+
Write ${PLAN_FILE}: the ordered steps from the current state to the goal. A step is one coherent vertical slice of the real product that a single build-turn — one harness execution — can build and verify end to end, and that leaves the game more playable than before. The first step is already such a slice, not setup alone.
|
|
104
|
+
|
|
105
|
+
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
|
+
|
|
107
|
+
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
|
+
|
|
109
|
+
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.
|
|
110
|
+
|
|
111
|
+
Decide; do not hedge. A build-turn ignores "optional", "if available", "maybe", and "if it proves too heavy", so make the call now — pick the approach, or, where something genuinely may not exist, name it as a fit-gap with the tradeoff it forces. Every line should be something a build-turn must act on.
|
|
112
|
+
|
|
113
|
+
For each step name:
|
|
114
|
+
- Outcome — what the player can newly do, see, and feel in the running product.
|
|
115
|
+
- Fit — the skills, packages, and assets that serve it, with the real APIs/exports/asset ids that matter, and any fit-gap with its tradeoff. Recommend the technology and content; leave the file layout to the build-turn.
|
|
116
|
+
- Gate — how the build-turn confirms the slice is genuinely done: not evidence written for someone else but the check it runs on itself, across every quality the slice needs — that it behaves correctly when run, that it is built soundly enough to carry the later slices, and that it reads right on screen when judged by eye against the goal's real-done bar, iterated until it does. Name the runtime checks, screenshots, or clips that would settle each.
|
|
117
|
+
|
|
118
|
+
Update ${README_FILE} to reflect the current state, the planned scope, and the gaps that remain.
|
|
119
|
+
Do not implement the plan.`;
|
|
120
|
+
}
|
|
121
|
+
export function buildBuildPrompt(userPrompt, turn) {
|
|
122
|
+
return `Stay in the current project directory.
|
|
123
|
+
|
|
124
|
+
Goal: ${userPrompt}
|
|
125
|
+
|
|
126
|
+
Build turn ${turn}.
|
|
127
|
+
|
|
128
|
+
${buildPrinciples()}
|
|
129
|
+
|
|
130
|
+
${buildSliceMethod()}
|
|
131
|
+
|
|
132
|
+
Use ${README_FILE} as the claimed current state, ${GOAL_FILE} as the fixed goal, and ${PLAN_FILE} as the plan.
|
|
133
|
+
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
|
+
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.
|
|
136
|
+
If real findings change the project understanding, update ${GOAL_FILE} lightly and honestly.
|
|
137
|
+
Finally, update ${README_FILE} so it truthfully describes the new proven state, what changed, what remains in ${PLAN_FILE}, and any remaining gaps.`;
|
|
138
|
+
}
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
import { type HarnessName } from "./constants.js";
|
|
2
|
+
import { type CommandRunner } from "./subprocess.js";
|
|
3
|
+
export declare function generateProjectName(): string;
|
|
4
|
+
export declare function initGitRepo(cwd: string, runner: CommandRunner): Promise<void>;
|
|
5
|
+
export declare function commitAll(cwd: string, runner: CommandRunner, message: string): Promise<void>;
|
|
6
|
+
export declare function initNpmProject(cwd: string, _runner: CommandRunner): Promise<void>;
|
|
7
|
+
export declare function ensureProjectGitignore(cwd: string): Promise<void>;
|
|
8
|
+
export declare function ensureNpmrc(cwd: string): Promise<void>;
|
|
9
|
+
export declare function ensureBaseProjectDirectories(cwd: string): Promise<void>;
|
|
10
|
+
export declare function installSkills(cwd: string, harness: HarnessName, runner: CommandRunner): Promise<void>;
|
|
11
|
+
export declare function installDependencies(cwd: string, runner: CommandRunner): Promise<void>;
|
|
12
|
+
export declare function ensureStateReadme(cwd: string): Promise<void>;
|
|
13
|
+
export declare function ensureLocalCliShims(cwd: string): Promise<void>;
|
|
14
|
+
export declare function ensureMarketCliShim(cwd: string): Promise<void>;
|
|
15
|
+
export declare function ensureActaCliShim(cwd: string): Promise<void>;
|
package/dist/scaffold.js
ADDED
|
@@ -0,0 +1,220 @@
|
|
|
1
|
+
import { randomBytes } from "node:crypto";
|
|
2
|
+
import { existsSync } from "node:fs";
|
|
3
|
+
import { chmod, mkdir, readFile, rm, writeFile } from "node:fs/promises";
|
|
4
|
+
import { basename, dirname, join } from "node:path";
|
|
5
|
+
import { GITIGNORE_ENTRIES, PACKAGES, SKILLS } from "./constants.js";
|
|
6
|
+
import { assertExitCode } from "./subprocess.js";
|
|
7
|
+
export function generateProjectName() {
|
|
8
|
+
return `dc-${randomBytes(3).toString("hex")}`;
|
|
9
|
+
}
|
|
10
|
+
export async function initGitRepo(cwd, runner) {
|
|
11
|
+
await assertExitCode(runner({ command: "git", args: ["init"], cwd }), "failed to initialise git repository");
|
|
12
|
+
}
|
|
13
|
+
export async function commitAll(cwd, runner, message) {
|
|
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");
|
|
16
|
+
}
|
|
17
|
+
export async function initNpmProject(cwd, _runner) {
|
|
18
|
+
const packageJsonPath = join(cwd, "package.json");
|
|
19
|
+
if (existsSync(packageJsonPath))
|
|
20
|
+
return;
|
|
21
|
+
await writeFile(packageJsonPath, `${JSON.stringify({
|
|
22
|
+
name: toPackageName(basename(cwd)),
|
|
23
|
+
version: "1.0.0",
|
|
24
|
+
private: true,
|
|
25
|
+
// Seed runnable scripts so every project is launchable from turn 1 (Vite is the pinned
|
|
26
|
+
// bundler). Build turns serve via `npm run dev` and bundle via `npm run build`; without
|
|
27
|
+
// these, weaker harnesses ship a project with no documented way to run the game.
|
|
28
|
+
type: "module",
|
|
29
|
+
scripts: {
|
|
30
|
+
dev: "vite",
|
|
31
|
+
build: "vite build",
|
|
32
|
+
preview: "vite preview"
|
|
33
|
+
}
|
|
34
|
+
}, null, 2)}\n`);
|
|
35
|
+
}
|
|
36
|
+
function toPackageName(name) {
|
|
37
|
+
const normalized = name
|
|
38
|
+
.toLowerCase()
|
|
39
|
+
.replace(/[^a-z0-9._~-]+/g, "-")
|
|
40
|
+
.replace(/^-+|-+$/g, "");
|
|
41
|
+
return normalized || "drawcall-project";
|
|
42
|
+
}
|
|
43
|
+
export async function ensureProjectGitignore(cwd) {
|
|
44
|
+
const gitignorePath = join(cwd, ".gitignore");
|
|
45
|
+
const existing = existsSync(gitignorePath) ? await readFile(gitignorePath, "utf8") : "";
|
|
46
|
+
const existingEntries = new Set(existing
|
|
47
|
+
.split(/\r?\n/)
|
|
48
|
+
.map((line) => line.trim())
|
|
49
|
+
.filter((line) => line && !line.startsWith("#")));
|
|
50
|
+
const missingEntries = GITIGNORE_ENTRIES.filter((entry) => !existingEntries.has(entry));
|
|
51
|
+
if (missingEntries.length === 0)
|
|
52
|
+
return;
|
|
53
|
+
const prefix = existing.trimEnd();
|
|
54
|
+
const nextContent = [prefix, ...missingEntries].filter(Boolean).join("\n") + "\n";
|
|
55
|
+
await writeFile(gitignorePath, nextContent);
|
|
56
|
+
}
|
|
57
|
+
// The curated package set pins a recent `three`, but some Drawcall packages declare peer ranges
|
|
58
|
+
// that lag a minor or two (e.g. @drawcall/flipbook peers an older three). The scaffold's own
|
|
59
|
+
// install passes `--legacy-peer-deps`, but later installs — a harness running `npm install`, or
|
|
60
|
+
// `market install` applying a template — run plain and would abort on the first peer mismatch.
|
|
61
|
+
// A persisted `.npmrc` makes the whole project tolerant so those installs don't veto a pinned
|
|
62
|
+
// version over a peer-range lag.
|
|
63
|
+
export async function ensureNpmrc(cwd) {
|
|
64
|
+
const npmrcPath = join(cwd, ".npmrc");
|
|
65
|
+
const existing = existsSync(npmrcPath) ? await readFile(npmrcPath, "utf8") : "";
|
|
66
|
+
if (/^\s*legacy-peer-deps\s*=/m.test(existing))
|
|
67
|
+
return;
|
|
68
|
+
const prefix = existing.trimEnd();
|
|
69
|
+
const nextContent = [prefix, "legacy-peer-deps=true"].filter(Boolean).join("\n") + "\n";
|
|
70
|
+
await writeFile(npmrcPath, nextContent);
|
|
71
|
+
}
|
|
72
|
+
export async function ensureBaseProjectDirectories(cwd) {
|
|
73
|
+
for (const directoryName of ["src", "public"]) {
|
|
74
|
+
const directory = join(cwd, directoryName);
|
|
75
|
+
await mkdir(directory, { recursive: true });
|
|
76
|
+
const placeholder = join(directory, ".gitkeep");
|
|
77
|
+
if (!existsSync(placeholder))
|
|
78
|
+
await writeFile(placeholder, "");
|
|
79
|
+
}
|
|
80
|
+
}
|
|
81
|
+
// Our harness names don't all match the `skills` CLI's agent ids.
|
|
82
|
+
const SKILLS_AGENT = {
|
|
83
|
+
opencode: "opencode",
|
|
84
|
+
codex: "codex",
|
|
85
|
+
claude: "claude-code",
|
|
86
|
+
pi: "pi",
|
|
87
|
+
gemini: "gemini-cli",
|
|
88
|
+
// The skills CLI has no dedicated grok agent; "universal" installs to .agents/skills, which
|
|
89
|
+
// grok discovers. Verify skill discovery in grok's e2e run and adjust if it misses them.
|
|
90
|
+
grok: "universal",
|
|
91
|
+
// Likewise no dedicated forge agent; "universal" (.agents/skills) is the portable target.
|
|
92
|
+
// Verify forge actually discovers the installed skills in its e2e run and adjust if it misses.
|
|
93
|
+
forge: "universal"
|
|
94
|
+
};
|
|
95
|
+
export async function installSkills(cwd, harness, runner) {
|
|
96
|
+
for (const skill of SKILLS) {
|
|
97
|
+
await assertExitCode(runner({
|
|
98
|
+
command: "npx",
|
|
99
|
+
args: ["--yes", "skills", "add", skill, "-y", "--agent", SKILLS_AGENT[harness]],
|
|
100
|
+
cwd
|
|
101
|
+
}), `failed to install skill ${skill}`);
|
|
102
|
+
}
|
|
103
|
+
}
|
|
104
|
+
export async function installDependencies(cwd, runner) {
|
|
105
|
+
// This is a fixed, curated set of packages we install together; their peer ranges drift
|
|
106
|
+
// independently (e.g. a runtime that pins an older `three` minor), and modern npm aborts the
|
|
107
|
+
// whole install on the first peer mismatch. We don't want a generated project to fail to
|
|
108
|
+
// scaffold over a peer-range lag in one package, so we install the set as-specified and let
|
|
109
|
+
// the pinned versions stand rather than letting peer resolution veto them.
|
|
110
|
+
await assertExitCode(runner({ command: "npm", args: ["install", "--legacy-peer-deps", ...PACKAGES], cwd }), "failed to install dependencies");
|
|
111
|
+
await ensureLocalCliShims(cwd);
|
|
112
|
+
}
|
|
113
|
+
// Called after the template stage: an applied starter may already ship a README, so this only
|
|
114
|
+
// creates a minimal state-record when none exists. The fixed goal lives in GOAL.md; the plan
|
|
115
|
+
// lives in PLAN.md.
|
|
116
|
+
export async function ensureStateReadme(cwd) {
|
|
117
|
+
const readmePath = join(cwd, "README.md");
|
|
118
|
+
if (existsSync(readmePath))
|
|
119
|
+
return;
|
|
120
|
+
await writeFile(readmePath, `# Project State
|
|
121
|
+
|
|
122
|
+
This is the state-record: what the product actually is right now. The fixed goal
|
|
123
|
+
lives in GOAL.md; the plan toward it lives in PLAN.md once planning has run.
|
|
124
|
+
|
|
125
|
+
## Verified State
|
|
126
|
+
|
|
127
|
+
- Nothing verified yet — the project was just scaffolded.
|
|
128
|
+
|
|
129
|
+
## Next Step
|
|
130
|
+
|
|
131
|
+
- Survey the fitting installed skills, packages, and Market assets, then plan and build the first real slice toward the full goal without shrinking it into a prototype.
|
|
132
|
+
|
|
133
|
+
## Proof Log
|
|
134
|
+
|
|
135
|
+
- No proof-run has been recorded yet.
|
|
136
|
+
`);
|
|
137
|
+
}
|
|
138
|
+
// --- Installed-package shims -------------------------------------------------
|
|
139
|
+
// The @drawcall/acta and @drawcall/market packages ship CLIs/install logic that
|
|
140
|
+
// assume a different layout than a freshly-generated project. These helpers
|
|
141
|
+
// patch the installed copies in node_modules so their bins resolve locally.
|
|
142
|
+
export async function ensureLocalCliShims(cwd) {
|
|
143
|
+
await ensureActaCliShim(cwd);
|
|
144
|
+
await ensureMarketCliShim(cwd);
|
|
145
|
+
}
|
|
146
|
+
export async function ensureMarketCliShim(cwd) {
|
|
147
|
+
const packageRoot = join(cwd, "node_modules", "@drawcall", "market");
|
|
148
|
+
await rewriteMarketInstallRoot(join(packageRoot, "dist", "install.js"));
|
|
149
|
+
await rewriteMarketInstallRoot(join(packageRoot, "src", "install.ts"));
|
|
150
|
+
}
|
|
151
|
+
export async function ensureActaCliShim(cwd) {
|
|
152
|
+
const packageRoot = join(cwd, "node_modules", "@drawcall", "acta");
|
|
153
|
+
const devShim = join(packageRoot, "src", "cli", "dev.ts");
|
|
154
|
+
const sourceCli = join(packageRoot, "src", "cli", "index.js");
|
|
155
|
+
const distCli = join(packageRoot, "dist", "cli", "index.js");
|
|
156
|
+
const packageJsonPath = join(packageRoot, "package.json");
|
|
157
|
+
const binShim = join(cwd, "node_modules", ".bin", "acta");
|
|
158
|
+
if (!existsSync(distCli))
|
|
159
|
+
return;
|
|
160
|
+
await mkdir(dirname(sourceCli), { recursive: true });
|
|
161
|
+
await writeFile(sourceCli, "import '../../dist/cli/index.js';\n");
|
|
162
|
+
if (existsSync(devShim)) {
|
|
163
|
+
await writeFile(devShim, "#!/usr/bin/env node\nimport '../../dist/cli/index.js';\n");
|
|
164
|
+
await chmod(devShim, 0o755);
|
|
165
|
+
}
|
|
166
|
+
await rewriteActaPackageBin(packageJsonPath);
|
|
167
|
+
await rewriteActaPackageEntrypoint(packageJsonPath);
|
|
168
|
+
await mkdir(dirname(binShim), { recursive: true });
|
|
169
|
+
await rm(binShim, { force: true });
|
|
170
|
+
await writeFile(binShim, "#!/usr/bin/env node\nimport '../@drawcall/acta/dist/cli/index.js';\n");
|
|
171
|
+
await chmod(binShim, 0o755);
|
|
172
|
+
}
|
|
173
|
+
async function rewriteActaPackageBin(packageJsonPath) {
|
|
174
|
+
if (!existsSync(packageJsonPath))
|
|
175
|
+
return;
|
|
176
|
+
const packageJson = JSON.parse(await readFile(packageJsonPath, "utf8"));
|
|
177
|
+
const existingBin = packageJson.bin && typeof packageJson.bin === "object" ? packageJson.bin : {};
|
|
178
|
+
packageJson.bin = { ...existingBin, acta: "dist/cli/index.js" };
|
|
179
|
+
await writeFile(packageJsonPath, `${JSON.stringify(packageJson, null, 2)}\n`);
|
|
180
|
+
}
|
|
181
|
+
async function rewriteActaPackageEntrypoint(packageJsonPath) {
|
|
182
|
+
if (!existsSync(packageJsonPath))
|
|
183
|
+
return;
|
|
184
|
+
const packageJson = JSON.parse(await readFile(packageJsonPath, "utf8"));
|
|
185
|
+
packageJson.main = "dist/index.js";
|
|
186
|
+
packageJson.types = "dist/index.d.ts";
|
|
187
|
+
packageJson.exports = {
|
|
188
|
+
...(packageJson.exports ?? {}),
|
|
189
|
+
".": {
|
|
190
|
+
types: "./dist/index.d.ts",
|
|
191
|
+
import: "./dist/index.js"
|
|
192
|
+
}
|
|
193
|
+
};
|
|
194
|
+
await writeFile(packageJsonPath, `${JSON.stringify(packageJson, null, 2)}\n`);
|
|
195
|
+
}
|
|
196
|
+
async function rewriteMarketInstallRoot(filePath) {
|
|
197
|
+
if (!existsSync(filePath))
|
|
198
|
+
return;
|
|
199
|
+
const source = await readFile(filePath, "utf8");
|
|
200
|
+
const replacement = `export async function findInstallRoot(cwd = process.cwd()) {
|
|
201
|
+
const start = path.resolve(cwd);
|
|
202
|
+
let dir = start;
|
|
203
|
+
while (true) {
|
|
204
|
+
if (await isFile(path.join(dir, 'package.json'))) {
|
|
205
|
+
return dir;
|
|
206
|
+
}
|
|
207
|
+
const parent = path.dirname(dir);
|
|
208
|
+
if (parent === dir)
|
|
209
|
+
break;
|
|
210
|
+
dir = parent;
|
|
211
|
+
}
|
|
212
|
+
return start;
|
|
213
|
+
}
|
|
214
|
+
|
|
215
|
+
async function downloadAssets`;
|
|
216
|
+
const patched = source.replace(/export async function findInstallRoot[\s\S]*?\n}\n\n?async function downloadAssets/, replacement);
|
|
217
|
+
if (patched !== source) {
|
|
218
|
+
await writeFile(filePath, patched);
|
|
219
|
+
}
|
|
220
|
+
}
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
import { type SpawnOptions } from "node:child_process";
|
|
2
|
+
export type CommandInvocation = {
|
|
3
|
+
command: string;
|
|
4
|
+
args: string[];
|
|
5
|
+
cwd: string;
|
|
6
|
+
timeoutMs?: number;
|
|
7
|
+
};
|
|
8
|
+
export type CommandResult = {
|
|
9
|
+
exitCode: number;
|
|
10
|
+
timedOut?: boolean;
|
|
11
|
+
};
|
|
12
|
+
export type CommandRunner = (invocation: CommandInvocation) => Promise<CommandResult>;
|
|
13
|
+
export type CommandExists = (command: string, env: NodeJS.ProcessEnv) => Promise<boolean>;
|
|
14
|
+
export type SubprocessRunnerOptions = Pick<SpawnOptions, "stdio" | "env"> & {
|
|
15
|
+
killGraceMs?: number;
|
|
16
|
+
logFile?: string;
|
|
17
|
+
};
|
|
18
|
+
export declare function createSubprocessRunner(options?: SubprocessRunnerOptions): CommandRunner;
|
|
19
|
+
export declare function buildSubprocessEnv(cwd: string, baseEnv?: NodeJS.ProcessEnv): NodeJS.ProcessEnv;
|
|
20
|
+
export declare function isCommandAvailable(command: string, env?: NodeJS.ProcessEnv): Promise<boolean>;
|
|
21
|
+
export declare function assertExitCode(run: Promise<CommandResult>, message: string): Promise<void>;
|
|
@@ -0,0 +1,117 @@
|
|
|
1
|
+
import { spawn } from "node:child_process";
|
|
2
|
+
import { createWriteStream } from "node:fs";
|
|
3
|
+
import { delimiter, dirname, join, resolve } from "node:path";
|
|
4
|
+
import which from "which";
|
|
5
|
+
import { CliError, TIMEOUT_EXIT_CODE, TIMEOUT_KILL_GRACE_MS } from "./constants.js";
|
|
6
|
+
export function createSubprocessRunner(options = { stdio: "inherit" }) {
|
|
7
|
+
return (invocation) => runSubprocess(invocation, options);
|
|
8
|
+
}
|
|
9
|
+
function runSubprocess({ command, args, cwd, timeoutMs }, options) {
|
|
10
|
+
return new Promise((resolveResult, reject) => {
|
|
11
|
+
// A separate process group lets a timeout kill the whole child tree at once.
|
|
12
|
+
const detached = process.platform !== "win32";
|
|
13
|
+
// When a log file is set, capture all child output into it and keep the
|
|
14
|
+
// terminal clean for the progress note; otherwise stream straight through.
|
|
15
|
+
const logStream = openSessionLog(options.logFile, [command, ...args]);
|
|
16
|
+
const child = spawn(command, args, {
|
|
17
|
+
cwd,
|
|
18
|
+
detached,
|
|
19
|
+
env: buildSubprocessEnv(cwd, options.env ?? process.env),
|
|
20
|
+
stdio: logStream ? ["ignore", "pipe", "pipe"] : (options.stdio ?? "inherit")
|
|
21
|
+
});
|
|
22
|
+
if (logStream) {
|
|
23
|
+
child.stdout?.pipe(logStream, { end: false });
|
|
24
|
+
child.stderr?.pipe(logStream, { end: false });
|
|
25
|
+
}
|
|
26
|
+
let timedOut = false;
|
|
27
|
+
let killTimer;
|
|
28
|
+
let timeout;
|
|
29
|
+
if (timeoutMs !== undefined) {
|
|
30
|
+
timeout = setTimeout(() => {
|
|
31
|
+
timedOut = true;
|
|
32
|
+
const notice = `[drawcall-create] ${command} timed out after ${Math.ceil(timeoutMs / 1000)}s`;
|
|
33
|
+
if (logStream)
|
|
34
|
+
logStream.write(`${notice}\n`);
|
|
35
|
+
else
|
|
36
|
+
console.error(notice);
|
|
37
|
+
killChildProcess(child.pid, detached, "SIGTERM");
|
|
38
|
+
const grace = options.killGraceMs ?? TIMEOUT_KILL_GRACE_MS;
|
|
39
|
+
killTimer = setTimeout(() => killChildProcess(child.pid, detached, "SIGKILL"), grace);
|
|
40
|
+
}, timeoutMs);
|
|
41
|
+
}
|
|
42
|
+
// The child sits in its own process group, so terminal signals never reach it directly.
|
|
43
|
+
const forwardSignal = (signal) => {
|
|
44
|
+
killChildProcess(child.pid, detached, signal);
|
|
45
|
+
process.exit(signal === "SIGINT" ? 130 : 143);
|
|
46
|
+
};
|
|
47
|
+
process.on("SIGINT", forwardSignal);
|
|
48
|
+
process.on("SIGTERM", forwardSignal);
|
|
49
|
+
const cleanup = () => {
|
|
50
|
+
if (timeout)
|
|
51
|
+
clearTimeout(timeout);
|
|
52
|
+
if (killTimer)
|
|
53
|
+
clearTimeout(killTimer);
|
|
54
|
+
process.off("SIGINT", forwardSignal);
|
|
55
|
+
process.off("SIGTERM", forwardSignal);
|
|
56
|
+
logStream?.end();
|
|
57
|
+
};
|
|
58
|
+
child.once("error", (error) => {
|
|
59
|
+
cleanup();
|
|
60
|
+
reject(error);
|
|
61
|
+
});
|
|
62
|
+
child.once("exit", (code, signal) => {
|
|
63
|
+
cleanup();
|
|
64
|
+
if (timedOut)
|
|
65
|
+
return resolveResult({ exitCode: TIMEOUT_EXIT_CODE, timedOut: true });
|
|
66
|
+
resolveResult({ exitCode: signal ? 1 : (code ?? 1), timedOut: false });
|
|
67
|
+
});
|
|
68
|
+
});
|
|
69
|
+
}
|
|
70
|
+
export function buildSubprocessEnv(cwd, baseEnv = process.env) {
|
|
71
|
+
const projectParent = dirname(resolve(cwd));
|
|
72
|
+
const localBin = join(resolve(cwd), "node_modules", ".bin");
|
|
73
|
+
const existingCeiling = baseEnv.GIT_CEILING_DIRECTORIES;
|
|
74
|
+
const existingPath = baseEnv.PATH;
|
|
75
|
+
return {
|
|
76
|
+
...baseEnv,
|
|
77
|
+
PATH: existingPath ? `${localBin}${delimiter}${existingPath}` : localBin,
|
|
78
|
+
GIT_CEILING_DIRECTORIES: existingCeiling
|
|
79
|
+
? `${projectParent}${delimiter}${existingCeiling}`
|
|
80
|
+
: projectParent
|
|
81
|
+
};
|
|
82
|
+
}
|
|
83
|
+
export async function isCommandAvailable(command, env = process.env) {
|
|
84
|
+
const resolved = await which(command, {
|
|
85
|
+
nothrow: true,
|
|
86
|
+
path: env.PATH,
|
|
87
|
+
pathExt: env.PATHEXT
|
|
88
|
+
});
|
|
89
|
+
return resolved !== null;
|
|
90
|
+
}
|
|
91
|
+
export async function assertExitCode(run, message) {
|
|
92
|
+
const { exitCode } = await run;
|
|
93
|
+
if (exitCode !== 0)
|
|
94
|
+
throw new CliError(`${message} (exit code ${exitCode})`);
|
|
95
|
+
}
|
|
96
|
+
function openSessionLog(logFile, command) {
|
|
97
|
+
if (logFile === undefined)
|
|
98
|
+
return undefined;
|
|
99
|
+
const stream = createWriteStream(logFile, { flags: "a" });
|
|
100
|
+
stream.write(`\n$ ${command.join(" ")}\n`);
|
|
101
|
+
return stream;
|
|
102
|
+
}
|
|
103
|
+
function killChildProcess(pid, detached, signal) {
|
|
104
|
+
if (!pid)
|
|
105
|
+
return;
|
|
106
|
+
if (process.platform === "win32") {
|
|
107
|
+
// Signals only reach the direct child (often a cmd shim); taskkill tears down the tree.
|
|
108
|
+
spawn("taskkill", ["/pid", String(pid), "/T", "/F"], { stdio: "ignore" }).unref();
|
|
109
|
+
return;
|
|
110
|
+
}
|
|
111
|
+
try {
|
|
112
|
+
process.kill(detached ? -pid : pid, signal);
|
|
113
|
+
}
|
|
114
|
+
catch {
|
|
115
|
+
// The process may have exited between the timeout and kill attempt.
|
|
116
|
+
}
|
|
117
|
+
}
|
package/package.json
ADDED
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@drawcall/create",
|
|
3
|
+
"version": "0.0.0",
|
|
4
|
+
"type": "module",
|
|
5
|
+
"description": "Create projects with an installed local harness.",
|
|
6
|
+
"license": "MIT",
|
|
7
|
+
"repository": {
|
|
8
|
+
"type": "git",
|
|
9
|
+
"url": "git+https://github.com/drawcall-ai/create.git"
|
|
10
|
+
},
|
|
11
|
+
"engines": {
|
|
12
|
+
"node": ">=20"
|
|
13
|
+
},
|
|
14
|
+
"bin": {
|
|
15
|
+
"drawcall-create": "./dist/cli.js"
|
|
16
|
+
},
|
|
17
|
+
"exports": {
|
|
18
|
+
".": {
|
|
19
|
+
"types": "./dist/index.d.ts",
|
|
20
|
+
"import": "./dist/index.js"
|
|
21
|
+
}
|
|
22
|
+
},
|
|
23
|
+
"types": "./dist/index.d.ts",
|
|
24
|
+
"files": [
|
|
25
|
+
"dist"
|
|
26
|
+
],
|
|
27
|
+
"dependencies": {
|
|
28
|
+
"commander": "^14.0.3",
|
|
29
|
+
"which": "^6.0.1"
|
|
30
|
+
},
|
|
31
|
+
"devDependencies": {
|
|
32
|
+
"@types/node": "^22.19.21",
|
|
33
|
+
"@types/which": "^3.0.4",
|
|
34
|
+
"prettier": "3.8.4",
|
|
35
|
+
"tsx": "^4.19.2",
|
|
36
|
+
"typescript": "^5.7.2",
|
|
37
|
+
"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
|
+
}
|