@joystack/cli 0.1.48 → 0.1.50
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/CHANGELOG.md +29 -0
- package/bin/joystack.mjs +23 -0
- package/lib/create.mjs +9 -7
- package/lib/dev.mjs +13 -1
- package/lib/land.mjs +63 -0
- package/lib/maker.mjs +63 -0
- package/lib/replay-export.mjs +16 -1
- package/lib/setup-guide.md +3 -2
- package/lib/static-server.mjs +8 -2
- package/package.json +1 -1
package/CHANGELOG.md
CHANGED
|
@@ -14,6 +14,35 @@ Versions: [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
|
|
|
14
14
|
|
|
15
15
|
## [Unreleased]
|
|
16
16
|
|
|
17
|
+
## [0.1.50] - 2026-10-11
|
|
18
|
+
|
|
19
|
+
### Changed
|
|
20
|
+
|
|
21
|
+
- `joystack setup` tells your agent that `create` makes an empty game, ready
|
|
22
|
+
to be built, rather than a working game to reshape.
|
|
23
|
+
|
|
24
|
+
## [0.1.49] - 2026-10-10
|
|
25
|
+
|
|
26
|
+
### Added
|
|
27
|
+
|
|
28
|
+
- `joystack land <name>` puts a lane's work into your game and removes the
|
|
29
|
+
lane, from the game folder or from inside the lane. It saves the lane's
|
|
30
|
+
unsaved work first. When the lane and the game changed the same lines, it
|
|
31
|
+
stops with the merge in progress and lists the files; once the merge is
|
|
32
|
+
settled and committed, run it again to remove the lane.
|
|
33
|
+
|
|
34
|
+
### Changed
|
|
35
|
+
|
|
36
|
+
- In the Joystack maker, `joystack lane`, `dev`, `publish` and `land` answer
|
|
37
|
+
for the maker: the folder is already the conversation's lane, its game is
|
|
38
|
+
already running on the person's page, and publishing does what the page's
|
|
39
|
+
Publish button does. On your own computer they work as before.
|
|
40
|
+
|
|
41
|
+
### Fixed
|
|
42
|
+
|
|
43
|
+
- On Windows, exporting a replay no longer fails now and then with `EPERM`
|
|
44
|
+
while another program still has the file it just wrote open.
|
|
45
|
+
|
|
17
46
|
## [0.1.48] - 2026-10-10
|
|
18
47
|
|
|
19
48
|
### Added
|
package/bin/joystack.mjs
CHANGED
|
@@ -6,6 +6,8 @@
|
|
|
6
6
|
import { dev } from '../lib/dev.mjs';
|
|
7
7
|
import { agentSessionFromEnv } from '../lib/agent-session.mjs';
|
|
8
8
|
import { lane } from '../lib/lane.mjs';
|
|
9
|
+
import { land } from '../lib/land.mjs';
|
|
10
|
+
import { MAKER_DEV, MAKER_LANE, makerDraft, makerPublish } from '../lib/maker.mjs';
|
|
9
11
|
import { login, logout, whoami } from '../lib/login.mjs';
|
|
10
12
|
import { publish } from '../lib/publish.mjs';
|
|
11
13
|
import { create } from '../lib/create.mjs';
|
|
@@ -38,6 +40,7 @@ Usage:
|
|
|
38
40
|
joystack assets <id> --sheet Draw a view sheet of a 3D asset to .joystack/sheets/ (--help: options)
|
|
39
41
|
joystack assets --json [<id>…] Print each asset's parameters, measured stats and flags as JSON
|
|
40
42
|
joystack lane <name> Open an isolated copy of the game to experiment in, safely
|
|
43
|
+
joystack land <name> Put a lane's work into the game, then remove the lane
|
|
41
44
|
joystack test [aspect] Run targeted checks (no aspect: conservative full run)
|
|
42
45
|
joystack test --since <base> Submission: ordinary + triggered extended checks
|
|
43
46
|
joystack test --all Explicit full audit; add --list to preview selection
|
|
@@ -72,6 +75,10 @@ its own.
|
|
|
72
75
|
try {
|
|
73
76
|
switch (cmd) {
|
|
74
77
|
case 'dev': {
|
|
78
|
+
if (makerDraft()) {
|
|
79
|
+
console.log(MAKER_DEV);
|
|
80
|
+
break;
|
|
81
|
+
}
|
|
75
82
|
const handle = await dev({ port: flag('port') ? Number(flag('port')) : undefined, share: rest.includes('--share'), reload: !rest.includes('--no-reload'), agentSession: agentSessionFromEnv() });
|
|
76
83
|
const shutdown = () => {
|
|
77
84
|
handle.stop();
|
|
@@ -188,12 +195,24 @@ try {
|
|
|
188
195
|
break;
|
|
189
196
|
}
|
|
190
197
|
case 'lane': {
|
|
198
|
+
if (makerDraft()) {
|
|
199
|
+
console.log(MAKER_LANE);
|
|
200
|
+
break;
|
|
201
|
+
}
|
|
191
202
|
const name = rest.find((a) => !a.startsWith('--'));
|
|
192
203
|
if (!name) throw new Error('Name the lane: joystack lane <name> (e.g. joystack lane judder)');
|
|
193
204
|
const { dir } = await lane(name);
|
|
194
205
|
console.log(`\nWork in it:\n cd ${dir} && npx joystack dev\n`);
|
|
195
206
|
break;
|
|
196
207
|
}
|
|
208
|
+
case 'land': {
|
|
209
|
+
if (makerDraft()) {
|
|
210
|
+
await makerPublish(makerDraft());
|
|
211
|
+
break;
|
|
212
|
+
}
|
|
213
|
+
await land(rest.find((a) => !a.startsWith('--')));
|
|
214
|
+
break;
|
|
215
|
+
}
|
|
197
216
|
case 'setup':
|
|
198
217
|
process.stdout.write(setupGuide());
|
|
199
218
|
break;
|
|
@@ -272,6 +291,10 @@ try {
|
|
|
272
291
|
break;
|
|
273
292
|
}
|
|
274
293
|
case 'publish':
|
|
294
|
+
if (makerDraft()) {
|
|
295
|
+
await makerPublish(makerDraft(), { slug: flag('slug') });
|
|
296
|
+
break;
|
|
297
|
+
}
|
|
275
298
|
await publish({ slug: flag('slug'), devTools: rest.includes('--dev-tools'), open: rest.includes('--open') });
|
|
276
299
|
break;
|
|
277
300
|
case 'feedback':
|
package/lib/create.mjs
CHANGED
|
@@ -1,14 +1,15 @@
|
|
|
1
1
|
// `joystack create <name>` — bootstrap a standalone Joystack game.
|
|
2
2
|
//
|
|
3
3
|
// The create-next-app gesture: one command → a complete folder that is its
|
|
4
|
-
// own repo, where `joystack dev` immediately serves
|
|
5
|
-
//
|
|
6
|
-
//
|
|
4
|
+
// own repo, where `joystack dev` immediately serves the game: an empty one, so
|
|
5
|
+
// the first game anyone sees in it is the one they asked for. It is
|
|
6
|
+
// multiplayer from line one (its world.js turns the room on, and `joystack
|
|
7
|
+
// dev` runs one from its first start). The folder name is the slug.
|
|
7
8
|
//
|
|
8
9
|
// ── Where the creator's files come from ─────────────────────────────────────
|
|
9
10
|
//
|
|
10
11
|
// EVERYTHING a creator receives comes from the versioned engine artifact: the
|
|
11
|
-
//
|
|
12
|
+
// skeleton every game starts as (`scaffold/`) and the agent surface (`agent-surface/` — skills
|
|
12
13
|
// plus the Layer-0 managed block). This npm package ships CODE ONLY.
|
|
13
14
|
//
|
|
14
15
|
// Shipping a second copy of the surface (a `template/` folder) inside this
|
|
@@ -39,7 +40,8 @@ import { ensureEngine } from './engine.mjs';
|
|
|
39
40
|
import { reconcileAgentSurface } from './agent-surface.mjs';
|
|
40
41
|
import { readEnv } from './legacy-names.mjs';
|
|
41
42
|
|
|
42
|
-
// The
|
|
43
|
+
// The skeleton every new game starts as, inside the installed engine artifact.
|
|
44
|
+
// The example games beside it (`examples/`) are read in place, never copied.
|
|
43
45
|
export const SCAFFOLD_DIR = 'scaffold';
|
|
44
46
|
|
|
45
47
|
const NAME_OK = /^[a-z0-9][a-z0-9-]{0,40}$/;
|
|
@@ -81,7 +83,7 @@ const PLAYWRIGHT_SPEC = '1.64.0';
|
|
|
81
83
|
// devDependency, exactly like playwright. `joystack test` also runs it.
|
|
82
84
|
const ESLINT_SPEC = '^10.7.0';
|
|
83
85
|
|
|
84
|
-
// Copy the
|
|
86
|
+
// Copy the skeleton out of the artifact. One rename: `gitignore` →
|
|
85
87
|
// `.gitignore` (npm pack silently strips .gitignore files from a package, so
|
|
86
88
|
// the artifact stores it dot-less).
|
|
87
89
|
function copyScaffold(engineDir, dir, name, version) {
|
|
@@ -140,7 +142,7 @@ async function scaffold({ dir, name, install, cliSpec, apiBase, log, ensure, rec
|
|
|
140
142
|
runNpm(['install', '--no-audit', '--no-fund'], { cwd: dir, stdio: 'pipe' });
|
|
141
143
|
}
|
|
142
144
|
|
|
143
|
-
// The engine — and with it the
|
|
145
|
+
// The engine — and with it the skeleton and the agent skills. Signs the
|
|
144
146
|
// creator in via the device flow if this machine has no token yet, and writes
|
|
145
147
|
// the `joystack.engine` pin.
|
|
146
148
|
const engine = await ensure(dir, { apiBase, log });
|
package/lib/dev.mjs
CHANGED
|
@@ -110,7 +110,17 @@ export async function dev(opts = {}) {
|
|
|
110
110
|
// (agent-session.mjs). Only the interactive `joystack dev` and `joystack assets`
|
|
111
111
|
// commands pass one; a stack started by `joystack test` or any other caller has
|
|
112
112
|
// none, so no test run can wake the agent that started it.
|
|
113
|
-
|
|
113
|
+
//
|
|
114
|
+
// `shareIsOwner` is for a host that serves the share link to the creator
|
|
115
|
+
// alone and keeps everyone else off the dev tools' routes (the Joystack
|
|
116
|
+
// maker): a report sent through the link then counts as one from this
|
|
117
|
+
// machine, so it can wake the agent and read its reply.
|
|
118
|
+
//
|
|
119
|
+
// `pollMs` is how often, in milliseconds, the game's files are compared for
|
|
120
|
+
// changes the operating system's events miss (live-versions.mjs); left unset
|
|
121
|
+
// it is that module's default of one second. Tests pass a shorter one so a
|
|
122
|
+
// save is acted on sooner.
|
|
123
|
+
async function startDev({ cwd = process.cwd(), port, noMp = process.env.NO_MP === '1', watch = true, reload = true, share = false, openShare = openTunnel, log = console.log, agentSession = null, shareIsOwner = false, devLogPath = null, pollMs } = {}, onFailure) {
|
|
114
124
|
const project = findProject(cwd);
|
|
115
125
|
// Q4 revision: the engine is not an npm dependency — install/refresh it from
|
|
116
126
|
// the platform's login-gated registry (no-op when the pinned version is in).
|
|
@@ -322,6 +332,7 @@ async function startDev({ cwd = process.cwd(), port, noMp = process.env.NO_MP ==
|
|
|
322
332
|
log, // agent-inbox report lines ride the dev logger
|
|
323
333
|
engineVersion: engine.version ?? null, // stamped on agent-inbox reports
|
|
324
334
|
agentSession, // woken by agent-inbox reports sent from this machine
|
|
335
|
+
shareIsOwner, // and by those sent through the share link, when it is the creator's alone
|
|
325
336
|
mounts: mountsFor(codeDir),
|
|
326
337
|
...(first ? { fixedBuild: first.build } : {}),
|
|
327
338
|
versions: live ? {
|
|
@@ -539,6 +550,7 @@ async function startDev({ cwd = process.cwd(), port, noMp = process.env.NO_MP ==
|
|
|
539
550
|
versions = createLiveVersions({
|
|
540
551
|
first,
|
|
541
552
|
auto: reload,
|
|
553
|
+
...(pollMs ? { intervalMs: pollMs } : {}),
|
|
542
554
|
log,
|
|
543
555
|
snapshot: sourceFiles,
|
|
544
556
|
make: (o) => makeVersion(o),
|
package/lib/land.mjs
ADDED
|
@@ -0,0 +1,63 @@
|
|
|
1
|
+
// `joystack land <name>`: put a lane's work into the game, then remove the
|
|
2
|
+
// lane (the /worktree skill, "Land the winner").
|
|
3
|
+
//
|
|
4
|
+
// Run from the game folder or from inside the lane. The lane's unsaved work
|
|
5
|
+
// is saved first, since landing is what the creator asked for. The game
|
|
6
|
+
// folder then merges the lane's branch. On a clean merge the lane's folder
|
|
7
|
+
// and branch go; on a clash the merge is left in progress in the game folder,
|
|
8
|
+
// with the rule for settling it, and running `land` again once it is settled
|
|
9
|
+
// and committed removes the lane.
|
|
10
|
+
import { execFileSync } from 'node:child_process';
|
|
11
|
+
import { existsSync } from 'node:fs';
|
|
12
|
+
import { dirname, resolve } from 'node:path';
|
|
13
|
+
import { isLane } from './lane.mjs';
|
|
14
|
+
import { findProject } from './project.mjs';
|
|
15
|
+
|
|
16
|
+
const git = (cwd, ...args) => execFileSync('git', args, { cwd, encoding: 'utf8', stdio: ['ignore', 'pipe', 'pipe'] }).trim();
|
|
17
|
+
|
|
18
|
+
export const CLASH_RULE =
|
|
19
|
+
'Keep both changes wherever both can be kept. Stop and ask the creator only where the two truly ' +
|
|
20
|
+
'contradict, in game terms ("both versions change how jumping feels"), never in git terms.';
|
|
21
|
+
|
|
22
|
+
export async function land(name, { cwd = process.cwd(), log = console.log } = {}) {
|
|
23
|
+
if (!name) throw new Error('Name the lane: joystack land <name> (the name it was opened with: joystack lane <name>)');
|
|
24
|
+
const here = findProject(cwd).dir;
|
|
25
|
+
const main = isLane(here) ? findProject(dirname(git(here, 'rev-parse', '--path-format=absolute', '--git-common-dir'))).dir : here;
|
|
26
|
+
const slug = findProject(main).slug;
|
|
27
|
+
const branch = `lane/${name}`;
|
|
28
|
+
const dir = resolve(main, '..', `${slug}-${name}`);
|
|
29
|
+
try {
|
|
30
|
+
git(main, 'rev-parse', '--verify', '--quiet', `refs/heads/${branch}`);
|
|
31
|
+
} catch {
|
|
32
|
+
throw new Error(`There is no lane called "${name}" (no branch ${branch}).`);
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
if (existsSync(dir) && git(dir, 'status', '--porcelain')) {
|
|
36
|
+
git(dir, 'add', '-A');
|
|
37
|
+
git(dir, 'commit', '-q', '-m', `save: lane ${name}`);
|
|
38
|
+
log(`→ saved the lane's unsaved work (${branch})`);
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
if (git(main, 'diff', '--name-only', '--diff-filter=U')) {
|
|
42
|
+
throw new Error(`The game folder ${main} is still in the middle of a merge. Settle it and commit, then run joystack land ${name} again.\n${CLASH_RULE}`);
|
|
43
|
+
}
|
|
44
|
+
try {
|
|
45
|
+
git(main, 'merge', '--no-edit', branch);
|
|
46
|
+
} catch (err) {
|
|
47
|
+
const clashed = git(main, 'diff', '--name-only', '--diff-filter=U');
|
|
48
|
+
if (!clashed) throw new Error(`Landing ${branch} failed: ${String(err.stderr || err.message).trim()}`);
|
|
49
|
+
throw new Error(
|
|
50
|
+
`Landing ${branch} clashed with the game in:\n${clashed.split('\n').map((f) => ` ${f}`).join('\n')}\n` +
|
|
51
|
+
`The merge is in progress in ${main}. ${CLASH_RULE}\n` +
|
|
52
|
+
`Then commit the merge, and run joystack land ${name} again to remove the lane.`,
|
|
53
|
+
);
|
|
54
|
+
}
|
|
55
|
+
log(`→ landed ${branch} into the game`);
|
|
56
|
+
|
|
57
|
+
if (existsSync(dir)) {
|
|
58
|
+
git(main, 'worktree', 'remove', dir);
|
|
59
|
+
log(`→ removed ${dir}`);
|
|
60
|
+
}
|
|
61
|
+
git(main, 'branch', '-d', branch);
|
|
62
|
+
return { dir, branch };
|
|
63
|
+
}
|
package/lib/maker.mjs
ADDED
|
@@ -0,0 +1,63 @@
|
|
|
1
|
+
// The commands that answer differently inside the Joystack maker.
|
|
2
|
+
//
|
|
3
|
+
// The maker is a web page around the same agent a creator runs in a game
|
|
4
|
+
// folder: it gives each conversation a lane of its own (a "draft"), runs that
|
|
5
|
+
// lane's dev server on the person's page, and has a Publish button that puts
|
|
6
|
+
// the draft in the game and the game online. Its agent reads the same
|
|
7
|
+
// AGENTS.md and skills as anyone, so it runs the same commands. These answer
|
|
8
|
+
// for the maker instead of doing what they do on a laptop, where they would
|
|
9
|
+
// open a lane the person never sees, start a server nobody looks at, or
|
|
10
|
+
// publish behind the page's back.
|
|
11
|
+
//
|
|
12
|
+
// The maker sets JOYSTACK_MAKER on the agent's process: the address of its
|
|
13
|
+
// own control API for this conversation's draft. Only the agent's commands
|
|
14
|
+
// carry it, never the dev servers the maker starts itself.
|
|
15
|
+
|
|
16
|
+
export function makerDraft(env = process.env) {
|
|
17
|
+
const at = String(env.JOYSTACK_MAKER ?? '').trim().replace(/\/+$/, '');
|
|
18
|
+
return at || null;
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
export const MAKER_LANE =
|
|
22
|
+
"You are in the Joystack maker: this folder is already this conversation's lane " +
|
|
23
|
+
'(the person sees it as a draft on their page). Build here. No new lane was made.';
|
|
24
|
+
|
|
25
|
+
export const MAKER_DEV =
|
|
26
|
+
"You are in the Joystack maker: this folder's dev server is already running, and the " +
|
|
27
|
+
'person plays the game on their page, beside the chat. Every file you save shows up ' +
|
|
28
|
+
'there. Nothing was started.';
|
|
29
|
+
|
|
30
|
+
// `publish` and `land` both do what the page's Publish button does: the draft
|
|
31
|
+
// goes into the game, and the game goes online under the name it has. The
|
|
32
|
+
// maker does it once the agent's turn ends, since it merges the files the
|
|
33
|
+
// agent is still working in. A game that has never been online has no name
|
|
34
|
+
// yet; the person picks one on the page.
|
|
35
|
+
export async function makerPublish(at, { slug = null, log = console.log, fetchImpl = fetch } = {}) {
|
|
36
|
+
if (slug) {
|
|
37
|
+
throw new Error(
|
|
38
|
+
'You are in the Joystack maker: a game here has one name, which the person picks on their page, ' +
|
|
39
|
+
'so --slug copies are not available. Run the command without --slug to publish under that name.',
|
|
40
|
+
);
|
|
41
|
+
}
|
|
42
|
+
let res;
|
|
43
|
+
try {
|
|
44
|
+
res = await fetchImpl(`${at}/publish`, { method: 'POST', signal: AbortSignal.timeout(15_000) });
|
|
45
|
+
} catch (err) {
|
|
46
|
+
throw new Error(`You are in the Joystack maker, and it did not answer (${err?.message ?? err}). Ask the person to press Publish on their page.`);
|
|
47
|
+
}
|
|
48
|
+
const body = await res.json().catch(() => ({}));
|
|
49
|
+
if (body.needsName) {
|
|
50
|
+
throw new Error(
|
|
51
|
+
'You are in the Joystack maker, and this game has never been published: its first publish needs ' +
|
|
52
|
+
'a name, which the person picks. Ask them to press Publish on their page.',
|
|
53
|
+
);
|
|
54
|
+
}
|
|
55
|
+
if (!res.ok || !body.ok) {
|
|
56
|
+
throw new Error(`You are in the Joystack maker, and it could not publish: ${body.error ?? `HTTP ${res.status}`}. Ask the person to press Publish on their page.`);
|
|
57
|
+
}
|
|
58
|
+
log(
|
|
59
|
+
'You are in the Joystack maker: publishing this draft. When this turn ends, it goes into the game ' +
|
|
60
|
+
`and the game goes online${body.url ? ` at ${body.url}` : ''}. The person sees it happen on their page.`,
|
|
61
|
+
);
|
|
62
|
+
return body;
|
|
63
|
+
}
|
package/lib/replay-export.mjs
CHANGED
|
@@ -1064,7 +1064,7 @@ function rowSink({ file, columns, rowsPerGroup }) {
|
|
|
1064
1064
|
finish() {
|
|
1065
1065
|
flush();
|
|
1066
1066
|
pq.finish();
|
|
1067
|
-
|
|
1067
|
+
renameInto(partial, file);
|
|
1068
1068
|
return rows;
|
|
1069
1069
|
},
|
|
1070
1070
|
// Called when the run did not get that far. Best-effort: a leftover
|
|
@@ -1075,6 +1075,21 @@ function rowSink({ file, columns, rowsPerGroup }) {
|
|
|
1075
1075
|
};
|
|
1076
1076
|
}
|
|
1077
1077
|
|
|
1078
|
+
// On Windows a file that was just written can be refused a rename for a moment
|
|
1079
|
+
// (EPERM, EBUSY or EACCES) while another process still has it open. The
|
|
1080
|
+
// rename is retried for up to two seconds before it fails.
|
|
1081
|
+
function renameInto(from, to) {
|
|
1082
|
+
for (let i = 0; ; i++) {
|
|
1083
|
+
try {
|
|
1084
|
+
renameSync(from, to);
|
|
1085
|
+
return;
|
|
1086
|
+
} catch (err) {
|
|
1087
|
+
if (i >= 40 || !['EPERM', 'EBUSY', 'EACCES'].includes(err.code)) throw err;
|
|
1088
|
+
Atomics.wait(new Int32Array(new SharedArrayBuffer(4)), 0, 0, 50);
|
|
1089
|
+
}
|
|
1090
|
+
}
|
|
1091
|
+
}
|
|
1092
|
+
|
|
1078
1093
|
/** A streaming Parquet writer for a table of `columns`, sized as the export sizes its own. */
|
|
1079
1094
|
export function rowSinkFor(file, columns, rowsPerGroup) {
|
|
1080
1095
|
return rowSink({ file, columns, rowsPerGroup: groupFor(columns, rowsPerGroup) });
|
package/lib/setup-guide.md
CHANGED
|
@@ -47,8 +47,9 @@ published URL slug, so it must be **lowercase letters, numbers and dashes**
|
|
|
47
47
|
npx -y joystack@latest create <name>
|
|
48
48
|
```
|
|
49
49
|
|
|
50
|
-
This scaffolds `<name>/` as its own git repo:
|
|
51
|
-
|
|
50
|
+
This scaffolds `<name>/` as its own git repo: an empty game that runs, ready
|
|
51
|
+
for the next session to build, plus all the agent instructions and the
|
|
52
|
+
`/build` workflow, inside the folder.
|
|
52
53
|
|
|
53
54
|
**This command will pause and ask the human to sign in.** That is expected — not
|
|
54
55
|
a hang, and nothing has gone wrong. Joystack's engine downloads with the game, and
|
package/lib/static-server.mjs
CHANGED
|
@@ -337,6 +337,9 @@ export function createStaticServer({
|
|
|
337
337
|
// in agent-session.mjs), or null. When set, an agent-inbox report from this
|
|
338
338
|
// machine also wakes that session.
|
|
339
339
|
agentSession = null,
|
|
340
|
+
// Whether a request through the share link is the creator's own (startDev's
|
|
341
|
+
// `shareIsOwner`): its agent-inbox reports then count as from this machine.
|
|
342
|
+
shareIsOwner = false,
|
|
340
343
|
}) {
|
|
341
344
|
// The tunnel hostname `joystack dev --share` opened to this server, once it is
|
|
342
345
|
// up (setShareHost). See trustedHost.
|
|
@@ -858,6 +861,9 @@ export function createStaticServer({
|
|
|
858
861
|
return turn;
|
|
859
862
|
};
|
|
860
863
|
const agentConnected = () => (sessionAlive(agentSession) ? agentSession.agent : null);
|
|
864
|
+
// The creator's own browser: on this machine, or through a share link that
|
|
865
|
+
// only the creator can reach the dev tools by.
|
|
866
|
+
const fromCreator = (req) => fromThisMachine(req) || (shareIsOwner && isShareHost(req.headers.host, shareHost));
|
|
861
867
|
|
|
862
868
|
// The reply credential: this server's port and a bearer token, readable only
|
|
863
869
|
// by the owner of the game folder, one file per server, so with two servers
|
|
@@ -890,7 +896,7 @@ export function createStaticServer({
|
|
|
890
896
|
res.writeHead(403, { 'Content-Type': 'text/plain' });
|
|
891
897
|
return res.end('cross-origin post rejected');
|
|
892
898
|
}
|
|
893
|
-
const local =
|
|
899
|
+
const local = fromCreator(req);
|
|
894
900
|
const maxBytes = local ? AGENT_INBOX_LOCAL_MAX_BYTES : AGENT_INBOX_MAX_BYTES;
|
|
895
901
|
const chunks = [];
|
|
896
902
|
let size = 0;
|
|
@@ -986,7 +992,7 @@ export function createStaticServer({
|
|
|
986
992
|
// so only this machine may read its replies. The custom header forces a CORS
|
|
987
993
|
// preflight, which this server never grants, so no foreign page can read it.
|
|
988
994
|
function handleInboxStatus(req, res) {
|
|
989
|
-
if (!
|
|
995
|
+
if (!fromCreator(req) || !sameOrigin(req, { shareHost }) || req.headers['x-joystack-agent-ui'] !== '1') {
|
|
990
996
|
res.writeHead(403, { 'Content-Type': 'text/plain' });
|
|
991
997
|
return res.end('dev tools only');
|
|
992
998
|
}
|
package/package.json
CHANGED