@joystack/cli 0.0.0-stage → 0.1.44

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (56) hide show
  1. package/CHANGELOG.md +749 -0
  2. package/LICENSE +202 -0
  3. package/README.md +21 -2
  4. package/bin/joystack.mjs +300 -0
  5. package/lib/agent-session.mjs +52 -0
  6. package/lib/agent-surface.mjs +333 -0
  7. package/lib/assets-json.mjs +205 -0
  8. package/lib/assets-page.mjs +184 -0
  9. package/lib/assets-sheet.mjs +365 -0
  10. package/lib/build-id.mjs +130 -0
  11. package/lib/bundle-primitives.mjs +123 -0
  12. package/lib/changelog.mjs +193 -0
  13. package/lib/config.mjs +56 -0
  14. package/lib/create.mjs +243 -0
  15. package/lib/dev.mjs +435 -0
  16. package/lib/engine.mjs +198 -0
  17. package/lib/feedback-sync.mjs +231 -0
  18. package/lib/feedback.mjs +209 -0
  19. package/lib/freeze.mjs +108 -0
  20. package/lib/game-files.mjs +99 -0
  21. package/lib/history.mjs +612 -0
  22. package/lib/inbox-cmd.mjs +57 -0
  23. package/lib/inject.mjs +250 -0
  24. package/lib/joystack-node-doors.mjs +230 -0
  25. package/lib/lane.mjs +141 -0
  26. package/lib/legacy-names.mjs +275 -0
  27. package/lib/lint.mjs +292 -0
  28. package/lib/login.mjs +134 -0
  29. package/lib/model-bake-cli.mjs +17 -0
  30. package/lib/model-bake.mjs +869 -0
  31. package/lib/npm.mjs +38 -0
  32. package/lib/ports.mjs +234 -0
  33. package/lib/primitives.mjs +393 -0
  34. package/lib/printable.mjs +12 -0
  35. package/lib/process-tree.mjs +30 -0
  36. package/lib/project.mjs +84 -0
  37. package/lib/publish.mjs +179 -0
  38. package/lib/replay-cmd.mjs +426 -0
  39. package/lib/replay-export.mjs +1379 -0
  40. package/lib/replay-list.mjs +308 -0
  41. package/lib/replay-profile.mjs +467 -0
  42. package/lib/replay-store.mjs +719 -0
  43. package/lib/room-reload.mjs +168 -0
  44. package/lib/room-server.mjs +539 -0
  45. package/lib/self-update.mjs +162 -0
  46. package/lib/shadow-resolve.mjs +85 -0
  47. package/lib/share.mjs +149 -0
  48. package/lib/smoke-gpu-preload.mjs +91 -0
  49. package/lib/source-path.mjs +33 -0
  50. package/lib/static-server.mjs +1238 -0
  51. package/lib/test-cmd.mjs +697 -0
  52. package/lib/test-policy.mjs +157 -0
  53. package/lib/test-reporter.mjs +177 -0
  54. package/lib/test-selection-cli.mjs +18 -0
  55. package/lib/update.mjs +316 -0
  56. package/package.json +27 -4
package/lib/create.mjs ADDED
@@ -0,0 +1,243 @@
1
+ // `joystack create <name>` — bootstrap a standalone Joystack game.
2
+ //
3
+ // The create-next-app gesture: one command → a complete folder that is its
4
+ // own repo, where `joystack dev` immediately serves a WORKING multiplayer game
5
+ // (the platform is not optional — the scaffold joins a room on line one, no
6
+ // "add multiplayer later" tier). The folder name is the slug.
7
+ //
8
+ // ── Where the creator's files come from ─────────────────────────────────────
9
+ //
10
+ // EVERYTHING a creator receives comes from the versioned engine artifact: the
11
+ // starter game (`scaffold/`) and the agent surface (`agent-surface/` — skills
12
+ // plus the Layer-0 managed block). This npm package ships CODE ONLY.
13
+ //
14
+ // Shipping a second copy of the surface (a `template/` folder) inside this
15
+ // package would be a bug with a long fuse. npm and the engine registry are two
16
+ // independent, immutable release lanes, so two copies drift the moment one ships
17
+ // without the other — and seeding from the CLI while stamping the ownership
18
+ // record with the ENGINE's version makes that record false from the start. The
19
+ // creator would pay for it on their first `joystack update`: skills they never
20
+ // touched would change under them, and a skill the CLI shipped but the engine
21
+ // didn't would be silently DELETED (agent-surface.mjs prunes files it owns that
22
+ // the artifact no longer lists), on a repo minutes old.
23
+ //
24
+ // One authored copy, one release lane. A skill or scaffold change ships with an
25
+ // engine release alone; the CLI is released only when the CLI's own code
26
+ // changes.
27
+ //
28
+ // The consequence, and it is deliberate: `create` needs the engine, and the
29
+ // engine download is login-gated — so CREATE SIGNS YOU IN. That gate existed
30
+ // anyway, one command later, at first `dev`. This moves it to where the creator
31
+ // is already waiting and leaves `dev` instant.
32
+ import { execFileSync } from 'node:child_process';
33
+ import { existsSync, mkdirSync, writeFileSync, readdirSync, readFileSync, rmSync, symlinkSync } from 'node:fs';
34
+ import { createRequire } from 'node:module';
35
+ import { join, relative, dirname } from 'node:path';
36
+ import { DEFAULT_API_BASE, getApiBase } from './config.mjs';
37
+ import { runNpm } from './npm.mjs';
38
+ import { ensureEngine } from './engine.mjs';
39
+ import { reconcileAgentSurface } from './agent-surface.mjs';
40
+ import { readEnv } from './legacy-names.mjs';
41
+
42
+ // The starter game, inside the installed engine artifact.
43
+ export const SCAFFOLD_DIR = 'scaffold';
44
+
45
+ const NAME_OK = /^[a-z0-9][a-z0-9-]{0,40}$/;
46
+
47
+ // npm is making install scripts opt-in: npm 11 warns that a package's
48
+ // postinstall is "not covered by allowScripts", npm 12 SKIPS it by default.
49
+ // esbuild and workerd (both under partykit) fetch their native binaries in a
50
+ // postinstall — skipped, `joystack dev` cannot start and nothing says why. The
51
+ // scaffold therefore ships its own approval, so a stranger's first install is
52
+ // clean on a strict npm without them ever running `npm approve-scripts`.
53
+ //
54
+ // Deliberately UNPINNED (name-only, not `esbuild@0.21.5`): partykit moves its
55
+ // esbuild/workerd pins between releases, and a version-pinned approval would
56
+ // quietly stop covering them on the next bump — the same silent breakage,
57
+ // later.
58
+ // fsevents is macOS-only (an optional dep of the file watchers) — it never
59
+ // installs on Windows, so the entry is simply unused there. Approving it
60
+ // keeps a Mac creator's first install warning-free, and under npm 12 keeps
61
+ // the watcher on native FSEvents instead of silently degrading to polling.
62
+ const ALLOW_SCRIPTS = { esbuild: true, workerd: true, fsevents: true };
63
+
64
+ // Smokes drive a real browser, so playwright ships in every scaffold's
65
+ // devDependencies and the Chromium binary is fetched at create time — a
66
+ // surprise "npm i -D playwright" + browser download mid-build is exactly
67
+ // what create-time provisioning exists to prevent.
68
+ const PLAYWRIGHT_SPEC = '^1.61.1';
69
+
70
+ // The Joystack rules ship as an ESLint config (the scaffold's eslint.config.js,
71
+ // importing the rules from the installed engine), so they squiggle in the
72
+ // creator's editor as the code is written — a far tighter loop than a failing
73
+ // gate, and the whole reason this is ESLint and not a bespoke scanner. That
74
+ // only works if eslint is actually installed, so it is a scaffold
75
+ // devDependency, exactly like playwright. `joystack test` also runs it.
76
+ const ESLINT_SPEC = '^10.7.0';
77
+
78
+ // Copy the starter game out of the artifact. One rename: `gitignore` →
79
+ // `.gitignore` (npm pack silently strips .gitignore files from a package, so
80
+ // the artifact stores it dot-less).
81
+ function copyScaffold(engineDir, dir, name, version) {
82
+ const root = join(engineDir, SCAFFOLD_DIR);
83
+ if (!existsSync(root)) {
84
+ // RELEASE ORDER: the engine must ship the scaffold BEFORE a CLI that expects
85
+ // it. If this CLI ever reaches a creator ahead of that engine, `create`
86
+ // resolves the latest engine, finds no starter game, and every create on
87
+ // earth fails — so say exactly what is wrong, and never blame the creator's
88
+ // setup for our release ordering.
89
+ throw new Error(
90
+ `Joystack engine ${version} ships no starter game — it predates engine-owned scaffolding. ` +
91
+ 'This is a platform-side issue, not something you did: please report it (`npx joystack feedback`) or retry shortly.',
92
+ );
93
+ }
94
+ for (const entry of readdirSync(root, { withFileTypes: true, recursive: true })) {
95
+ if (!entry.isFile()) continue;
96
+ const rel = relative(root, join(entry.parentPath, entry.name));
97
+ const dest = join(dir, rel === 'gitignore' ? '.gitignore' : rel);
98
+ mkdirSync(dirname(dest), { recursive: true });
99
+ writeFileSync(dest, readFileSync(join(root, rel), 'utf8').replaceAll('{{name}}', name));
100
+ }
101
+ }
102
+
103
+ async function scaffold({ dir, name, install, cliSpec, apiBase, log, ensure, reconcile }) {
104
+ // Written FIRST, and without the engine pin: npm needs a package.json to
105
+ // install into, and `ensureEngine` writes the pin itself once it knows which
106
+ // version it actually installed. ONE writer for the pin — the old code had
107
+ // `create` resolve it and `ensureEngine` resolve it again, which is precisely
108
+ // how they came to disagree.
109
+ writeFileSync(
110
+ join(dir, 'package.json'),
111
+ JSON.stringify(
112
+ {
113
+ name,
114
+ private: true,
115
+ // Every file a game is made of is an ES module.
116
+ type: 'module',
117
+ description: `A Joystack game. Play at https://${name}.joystack.games/`,
118
+ // The scaffold is a framework-v2 game (entities + traits + a world root,
119
+ // lowered to the engine runtime on this machine). This flag is what makes
120
+ // the room server build the v2 room and the serve path inject the v2
121
+ // import map + skeleton.
122
+ joystack: { framework: 'v2' },
123
+ scripts: { dev: 'joystack dev', publish: 'joystack publish', test: 'joystack test', lint: 'joystack lint' },
124
+ devDependencies: { '@joystack/cli': cliSpec, playwright: PLAYWRIGHT_SPEC, eslint: ESLINT_SPEC },
125
+ allowScripts: ALLOW_SCRIPTS,
126
+ },
127
+ null,
128
+ 2,
129
+ ) + '\n',
130
+ );
131
+
132
+ if (install) {
133
+ log(`Installing the CLI (npm install in ${name}/)…`);
134
+ runNpm(['install', '--no-audit', '--no-fund'], { cwd: dir, stdio: 'pipe' });
135
+ }
136
+
137
+ // The engine — and with it the starter game and the agent skills. Signs the
138
+ // creator in via the device flow if this machine has no token yet, and writes
139
+ // the `joystack.engine` pin.
140
+ const engine = await ensure(dir, { apiBase, log });
141
+
142
+ copyScaffold(engine.dir, dir, name, engine.version);
143
+
144
+ // The agent surface is PLATFORM-owned and reconciled on every `joystack update`,
145
+ // so it is placed here by the very code that will later update it — reading
146
+ // the artifact, recording the real hash of what landed. That identity is the
147
+ // whole point: a fresh repo's first `update` is now a genuine no-op, because
148
+ // the bytes on disk and the bytes in the ownership record came from the same
149
+ // artifact.
150
+ reconcile(dir, engine.dir, { log });
151
+
152
+ // Non-Claude agent CLIs (Codex, Gemini/Antigravity, Cursor, opencode)
153
+ // discover project skills at .agents/skills/ — alias it to the same files.
154
+ // After reconcile, so the link is never dangling.
155
+ try {
156
+ mkdirSync(join(dir, '.agents'), { recursive: true });
157
+ symlinkSync(join('..', '.claude', 'skills'), join(dir, '.agents', 'skills'), 'dir');
158
+ } catch {
159
+ // e.g. Windows without symlink rights — Claude Code (and any tool that
160
+ // reads .claude/skills/ directly) still works.
161
+ }
162
+
163
+ if (install) {
164
+ // Fetch the smoke-test browser NOW (cached machine-wide per version) so
165
+ // the first `joystack test` never stalls on a surprise download mid-build.
166
+ // Resolved + run via node directly — NEVER a nested `npx`: when create
167
+ // itself runs under `npm exec --package=<tarball>` (the stranger gesture),
168
+ // the inherited npm_config_* env makes an inner npx try to re-install the
169
+ // tarball and cancel.
170
+ try {
171
+ log('Fetching the test browser (Chromium — one-time download, cached for every game)…');
172
+ const req = createRequire(join(dir, 'package.json'));
173
+ const playwrightCli = join(dirname(req.resolve('playwright/package.json')), 'cli.js');
174
+ execFileSync(process.execPath, [playwrightCli, 'install', 'chromium'], { cwd: dir, stdio: 'inherit' });
175
+ } catch {
176
+ log('⚠️ Could not fetch Chromium (offline?) — `joystack test` will fetch it when first needed.');
177
+ }
178
+ }
179
+
180
+ try {
181
+ execFileSync('git', ['init', '-q'], { cwd: dir, stdio: 'pipe' });
182
+ // The initial commit is the first revert baseline (rejecting milestone 1
183
+ // resets to it) and what lets `git worktree add` work — on a never-committed
184
+ // repo it silently creates an empty orphan lane instead. It carries the
185
+ // engine pin AND the true agent surface, so a reset to baseline cannot
186
+ // silently change either.
187
+ execFileSync('git', ['add', '-A'], { cwd: dir, stdio: 'pipe' });
188
+ const msg = ['commit', '-q', '-m', 'initial scaffold (joystack create)'];
189
+ try {
190
+ execFileSync('git', msg, { cwd: dir, stdio: 'pipe' });
191
+ } catch {
192
+ // Non-dev machines often have no git identity configured — commit as
193
+ // the scaffold rather than leaving the repo without a baseline.
194
+ execFileSync('git', ['-c', 'user.name=Joystack', '-c', 'user.email=create@joystack.ai', ...msg], {
195
+ cwd: dir,
196
+ stdio: 'pipe',
197
+ });
198
+ }
199
+ } catch {
200
+ // git not installed — fine, the folder still works.
201
+ }
202
+
203
+ log('');
204
+ log(`✅ ${name} is ready (Joystack engine ${engine.version}).`);
205
+ log(` cd ${name} && npx joystack dev`);
206
+ return { dir, engineVersion: engine.version };
207
+ }
208
+
209
+ export async function create({
210
+ name,
211
+ cwd = process.cwd(),
212
+ install = true,
213
+ cliSpec = readEnv('JOYSTACK_CREATE_CLI_SPEC') || '^0.1.0',
214
+ apiBase = getApiBase(DEFAULT_API_BASE),
215
+ log = console.log,
216
+ // Seams, mirroring `joystack update` — the tests drive a real artifact on disk
217
+ // through the real reconciler, with no network and no account.
218
+ ensure = ensureEngine,
219
+ reconcile = reconcileAgentSurface,
220
+ } = {}) {
221
+ if (!name || !NAME_OK.test(name)) {
222
+ throw new Error(
223
+ `"${name ?? ''}" won't work as a game name — use lowercase letters, digits, and dashes (it becomes the URL: <name>.joystack.games).`,
224
+ );
225
+ }
226
+ const dir = join(cwd, name);
227
+ if (existsSync(dir)) throw new Error(`${dir} already exists — pick another name or remove it first.`);
228
+
229
+ mkdirSync(dir, { recursive: true });
230
+ try {
231
+ return await scaffold({ dir, name, install, cliSpec, apiBase, log, ensure, reconcile });
232
+ } catch (err) {
233
+ // ATOMIC. Everything a creator receives now arrives over the network behind
234
+ // a login, so a failure anywhere leaves a folder with no game, no skills and
235
+ // no initial commit — one that `joystack dev` cannot run, and that `create`
236
+ // would refuse to retry into ("already exists"). A folder that looks like a
237
+ // game and isn't one is worse than no folder at all: it fails later,
238
+ // somewhere more confusing. Take it back, and say so.
239
+ rmSync(dir, { recursive: true, force: true });
240
+ log(`\n⚠️ Could not finish creating ${name} — removed the incomplete folder so you can retry cleanly.`);
241
+ throw err;
242
+ }
243
+ }