@bitmagic/cli 0.1.22 → 0.1.25
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 +58 -19
- package/dist/cli.d.ts +10 -6
- package/dist/cli.js +2 -2
- package/dist/cli.js.map +1 -1
- package/dist/commands/dev.d.ts +5 -1
- package/dist/commands/dev.js +82 -13
- package/dist/commands/dev.js.map +1 -1
- package/dist/commands/generate.js +3 -3
- package/dist/commands/generate.js.map +1 -1
- package/dist/commands/reload.d.ts +10 -0
- package/dist/commands/reload.js +82 -0
- package/dist/commands/reload.js.map +1 -0
- package/dist/commands/upgrade.js +6 -0
- package/dist/commands/upgrade.js.map +1 -1
- package/dist/editor/journal.js +2 -2
- package/dist/editor/reload-bus.d.ts +38 -0
- package/dist/editor/reload-bus.js +66 -0
- package/dist/editor/reload-bus.js.map +1 -0
- package/dist/editor/server.d.ts +7 -0
- package/dist/editor/server.js +51 -9
- package/dist/editor/server.js.map +1 -1
- package/dist/editor/shell-page.d.ts +24 -5
- package/dist/editor/shell-page.js +389 -36
- package/dist/editor/shell-page.js.map +1 -1
- package/dist/editor/watch.d.ts +101 -0
- package/dist/editor/watch.js +228 -0
- package/dist/editor/watch.js.map +1 -0
- package/dist/local-port.d.ts +10 -0
- package/dist/local-port.js +23 -0
- package/dist/local-port.js.map +1 -1
- package/dist/project/dev-handle.d.ts +14 -0
- package/dist/project/dev-handle.js +53 -0
- package/dist/project/dev-handle.js.map +1 -0
- package/dist/scaffold/claude-settings.d.ts +41 -0
- package/dist/scaffold/claude-settings.js +98 -0
- package/dist/scaffold/claude-settings.js.map +1 -0
- package/dist/scaffold/project-files.js +23 -8
- package/dist/scaffold/project-files.js.map +1 -1
- package/dist/scaffold/project.js +5 -0
- package/dist/scaffold/project.js.map +1 -1
- package/dist/scaffold/upgrade-project.d.ts +6 -0
- package/dist/scaffold/upgrade-project.js +19 -1
- package/dist/scaffold/upgrade-project.js.map +1 -1
- package/dist/verify/browser.js +3 -1
- package/dist/verify/browser.js.map +1 -1
- package/package.json +1 -1
- package/dist/commands/edit.d.ts +0 -10
- package/dist/commands/edit.js +0 -148
- package/dist/commands/edit.js.map +0 -1
|
@@ -0,0 +1,101 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Deciding *when* the browser should reload.
|
|
3
|
+
*
|
|
4
|
+
* The hard part is not noticing a change — `fs.watch` does that — it is not reloading at the wrong
|
|
5
|
+
* moment. Three wrong moments, each of which this file exists to avoid:
|
|
6
|
+
*
|
|
7
|
+
* 1. **Mid-burst.** An agent editing five files produces five reloads unless the writes are let
|
|
8
|
+
* go quiet first. Hence the debounce.
|
|
9
|
+
* 2. **Mid-compile.** A `.ts` edit changes `src/` long before `tsc --watch` finishes emitting
|
|
10
|
+
* `dist/`, and every module the project loads resolves into `dist/`. Reloading on the source
|
|
11
|
+
* write shows the creator the *old* build and reads as "my change did nothing". Hence the
|
|
12
|
+
* compile gate: reloads are held while a compile is open and released when it lands clean.
|
|
13
|
+
* 3. **After a failed compile.** `dist/` still holds the last good emit, so a reload would show
|
|
14
|
+
* working code for broken source — the most confusing outcome of the three. The pending reload
|
|
15
|
+
* is dropped and the shell is told the build failed instead.
|
|
16
|
+
*
|
|
17
|
+
* `ReloadCoordinator` holds all of that and touches no I/O, so it is testable with fake timers and
|
|
18
|
+
* no filesystem. `watchProject` and `pipeTscWatch` are the thin I/O shells that feed it.
|
|
19
|
+
*/
|
|
20
|
+
import { type ChildProcess } from 'child_process';
|
|
21
|
+
import type { BuildStatus } from './reload-bus.js';
|
|
22
|
+
/**
|
|
23
|
+
* How long the writes must stay quiet before a reload fires.
|
|
24
|
+
*
|
|
25
|
+
* Long enough that `tsc --watch` (which has its own ~250 ms debounce before it reports
|
|
26
|
+
* `File change detected`) has almost always opened its compile by the time this elapses, so the
|
|
27
|
+
* gate below actually gets to hold the reload. Too short and a `.ts` edit produces two reloads —
|
|
28
|
+
* one stale, one correct — instead of one.
|
|
29
|
+
*/
|
|
30
|
+
export declare const RELOAD_QUIET_MS = 700;
|
|
31
|
+
/**
|
|
32
|
+
* How long after one of our own writes to ignore file events.
|
|
33
|
+
*
|
|
34
|
+
* `/api/scene/save` and the HQ generation both write `world.json`, and a reload fired at the
|
|
35
|
+
* creator's own gizmo drag would throw away the drag that caused it. `fs.watch` delivers
|
|
36
|
+
* asynchronously but promptly, so this only has to outlast the delivery, not the write.
|
|
37
|
+
*/
|
|
38
|
+
export declare const OWN_WRITE_GRACE_MS = 400;
|
|
39
|
+
export interface ReloadCoordinatorOptions {
|
|
40
|
+
onReload: (reason: string) => void;
|
|
41
|
+
onBuild: (status: BuildStatus, message?: string) => void;
|
|
42
|
+
quietMs?: number;
|
|
43
|
+
}
|
|
44
|
+
export declare class ReloadCoordinator {
|
|
45
|
+
private readonly options;
|
|
46
|
+
private timer;
|
|
47
|
+
private pendingReason;
|
|
48
|
+
private compiling;
|
|
49
|
+
private suppressUntil;
|
|
50
|
+
private stopped;
|
|
51
|
+
constructor(options: ReloadCoordinatorOptions);
|
|
52
|
+
/** A file under a watched directory changed. Arms — or re-arms — the quiet window. */
|
|
53
|
+
noteFileChange(reason: string): void;
|
|
54
|
+
/**
|
|
55
|
+
* Reload as soon as it is safe to — now, or the moment the compile in flight lands clean.
|
|
56
|
+
*
|
|
57
|
+
* The wait is the whole point for `bitmagic reload`: an agent's stop hook fires immediately after
|
|
58
|
+
* its last edit, which is precisely when `tsc` is still compiling it. Returns whether the reload
|
|
59
|
+
* was queued behind a compile rather than sent, so the command can say which happened.
|
|
60
|
+
*/
|
|
61
|
+
requestNow(reason: string): {
|
|
62
|
+
queued: boolean;
|
|
63
|
+
};
|
|
64
|
+
/** The sidecar wrote `world.json` itself; the resulting file events are not news. */
|
|
65
|
+
noteOwnWrite(): void;
|
|
66
|
+
noteCompileStart(): void;
|
|
67
|
+
noteCompileEnd(errorCount: number): void;
|
|
68
|
+
stop(): void;
|
|
69
|
+
/** Send the pending reload, unless a compile is still open — then it waits for `noteCompileEnd`. */
|
|
70
|
+
private release;
|
|
71
|
+
}
|
|
72
|
+
export interface ProjectWatcher {
|
|
73
|
+
close(): void;
|
|
74
|
+
}
|
|
75
|
+
/**
|
|
76
|
+
* Watch the two directories that decide what the browser shows: `src/` (what the agent and the
|
|
77
|
+
* creator edit — including `work/world.json`) and `dist/` (what the page actually loads).
|
|
78
|
+
*
|
|
79
|
+
* `dist/` is watched as well as `src/` even though the compile gate already covers the normal
|
|
80
|
+
* `tsc --watch` path, because it is what a build run outside this process — a manual `tsc`, an
|
|
81
|
+
* editor's own compiler — lands in, and that should reach the browser too.
|
|
82
|
+
*
|
|
83
|
+
* A directory that cannot be watched is skipped with a note rather than failing the command: a
|
|
84
|
+
* missing `dist/` means "not built yet", and losing auto-reload is a smaller loss than losing
|
|
85
|
+
* `bitmagic dev`.
|
|
86
|
+
*/
|
|
87
|
+
export declare function watchProject(root: string, coordinator: ReloadCoordinator, log: (message: string) => void): ProjectWatcher;
|
|
88
|
+
/**
|
|
89
|
+
* Feed one line of `tsc --watch` output to the coordinator. Exported for its test — the markers are
|
|
90
|
+
* tsc's own wording, and a version that changes them would otherwise break auto-reload silently.
|
|
91
|
+
*/
|
|
92
|
+
export declare function noteTscLine(line: string, coordinator: ReloadCoordinator): void;
|
|
93
|
+
/**
|
|
94
|
+
* Spawn `tsc --watch` with its output piped through us instead of inherited.
|
|
95
|
+
*
|
|
96
|
+
* Piping is what makes the compile gate possible — the only signal that a build finished, and
|
|
97
|
+
* whether it finished clean, is tsc's own `Found N errors` line. Every line is re-echoed verbatim,
|
|
98
|
+
* so the creator sees exactly what an inherited stdio would have shown, and `--pretty` is forced
|
|
99
|
+
* back on when our own stdout is a terminal (tsc drops colour the moment it is piped).
|
|
100
|
+
*/
|
|
101
|
+
export declare function pipeTscWatch(tsc: string, cwd: string, coordinator: ReloadCoordinator): ChildProcess;
|
|
@@ -0,0 +1,228 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Deciding *when* the browser should reload.
|
|
3
|
+
*
|
|
4
|
+
* The hard part is not noticing a change — `fs.watch` does that — it is not reloading at the wrong
|
|
5
|
+
* moment. Three wrong moments, each of which this file exists to avoid:
|
|
6
|
+
*
|
|
7
|
+
* 1. **Mid-burst.** An agent editing five files produces five reloads unless the writes are let
|
|
8
|
+
* go quiet first. Hence the debounce.
|
|
9
|
+
* 2. **Mid-compile.** A `.ts` edit changes `src/` long before `tsc --watch` finishes emitting
|
|
10
|
+
* `dist/`, and every module the project loads resolves into `dist/`. Reloading on the source
|
|
11
|
+
* write shows the creator the *old* build and reads as "my change did nothing". Hence the
|
|
12
|
+
* compile gate: reloads are held while a compile is open and released when it lands clean.
|
|
13
|
+
* 3. **After a failed compile.** `dist/` still holds the last good emit, so a reload would show
|
|
14
|
+
* working code for broken source — the most confusing outcome of the three. The pending reload
|
|
15
|
+
* is dropped and the shell is told the build failed instead.
|
|
16
|
+
*
|
|
17
|
+
* `ReloadCoordinator` holds all of that and touches no I/O, so it is testable with fake timers and
|
|
18
|
+
* no filesystem. `watchProject` and `pipeTscWatch` are the thin I/O shells that feed it.
|
|
19
|
+
*/
|
|
20
|
+
import { spawn } from 'child_process';
|
|
21
|
+
import * as fs from 'fs';
|
|
22
|
+
import * as path from 'path';
|
|
23
|
+
/**
|
|
24
|
+
* How long the writes must stay quiet before a reload fires.
|
|
25
|
+
*
|
|
26
|
+
* Long enough that `tsc --watch` (which has its own ~250 ms debounce before it reports
|
|
27
|
+
* `File change detected`) has almost always opened its compile by the time this elapses, so the
|
|
28
|
+
* gate below actually gets to hold the reload. Too short and a `.ts` edit produces two reloads —
|
|
29
|
+
* one stale, one correct — instead of one.
|
|
30
|
+
*/
|
|
31
|
+
export const RELOAD_QUIET_MS = 700;
|
|
32
|
+
/**
|
|
33
|
+
* How long after one of our own writes to ignore file events.
|
|
34
|
+
*
|
|
35
|
+
* `/api/scene/save` and the HQ generation both write `world.json`, and a reload fired at the
|
|
36
|
+
* creator's own gizmo drag would throw away the drag that caused it. `fs.watch` delivers
|
|
37
|
+
* asynchronously but promptly, so this only has to outlast the delivery, not the write.
|
|
38
|
+
*/
|
|
39
|
+
export const OWN_WRITE_GRACE_MS = 400;
|
|
40
|
+
export class ReloadCoordinator {
|
|
41
|
+
options;
|
|
42
|
+
timer = null;
|
|
43
|
+
pendingReason = null;
|
|
44
|
+
compiling = false;
|
|
45
|
+
suppressUntil = 0;
|
|
46
|
+
stopped = false;
|
|
47
|
+
constructor(options) {
|
|
48
|
+
this.options = { quietMs: RELOAD_QUIET_MS, ...options };
|
|
49
|
+
}
|
|
50
|
+
/** A file under a watched directory changed. Arms — or re-arms — the quiet window. */
|
|
51
|
+
noteFileChange(reason) {
|
|
52
|
+
if (this.stopped)
|
|
53
|
+
return;
|
|
54
|
+
if (Date.now() < this.suppressUntil)
|
|
55
|
+
return;
|
|
56
|
+
this.pendingReason = reason;
|
|
57
|
+
if (this.timer)
|
|
58
|
+
clearTimeout(this.timer);
|
|
59
|
+
this.timer = setTimeout(() => {
|
|
60
|
+
this.timer = null;
|
|
61
|
+
this.release();
|
|
62
|
+
}, this.options.quietMs);
|
|
63
|
+
}
|
|
64
|
+
/**
|
|
65
|
+
* Reload as soon as it is safe to — now, or the moment the compile in flight lands clean.
|
|
66
|
+
*
|
|
67
|
+
* The wait is the whole point for `bitmagic reload`: an agent's stop hook fires immediately after
|
|
68
|
+
* its last edit, which is precisely when `tsc` is still compiling it. Returns whether the reload
|
|
69
|
+
* was queued behind a compile rather than sent, so the command can say which happened.
|
|
70
|
+
*/
|
|
71
|
+
requestNow(reason) {
|
|
72
|
+
if (this.stopped)
|
|
73
|
+
return { queued: false };
|
|
74
|
+
if (this.timer) {
|
|
75
|
+
clearTimeout(this.timer);
|
|
76
|
+
this.timer = null;
|
|
77
|
+
}
|
|
78
|
+
this.pendingReason = reason;
|
|
79
|
+
this.release();
|
|
80
|
+
return { queued: this.pendingReason !== null };
|
|
81
|
+
}
|
|
82
|
+
/** The sidecar wrote `world.json` itself; the resulting file events are not news. */
|
|
83
|
+
noteOwnWrite() {
|
|
84
|
+
this.suppressUntil = Date.now() + OWN_WRITE_GRACE_MS;
|
|
85
|
+
}
|
|
86
|
+
noteCompileStart() {
|
|
87
|
+
if (this.stopped || this.compiling)
|
|
88
|
+
return;
|
|
89
|
+
this.compiling = true;
|
|
90
|
+
this.options.onBuild('building');
|
|
91
|
+
}
|
|
92
|
+
noteCompileEnd(errorCount) {
|
|
93
|
+
if (this.stopped)
|
|
94
|
+
return;
|
|
95
|
+
this.compiling = false;
|
|
96
|
+
if (errorCount > 0) {
|
|
97
|
+
// Dropped, not deferred: `dist/` holds the last good emit, so reloading would show working
|
|
98
|
+
// code for broken source. The shell says so instead, and the next clean compile reloads.
|
|
99
|
+
this.pendingReason = null;
|
|
100
|
+
this.options.onBuild('failed', `${errorCount} TypeScript error${errorCount === 1 ? '' : 's'} — the game was not rebuilt.`);
|
|
101
|
+
return;
|
|
102
|
+
}
|
|
103
|
+
this.options.onBuild('ok');
|
|
104
|
+
this.release();
|
|
105
|
+
}
|
|
106
|
+
stop() {
|
|
107
|
+
this.stopped = true;
|
|
108
|
+
if (this.timer)
|
|
109
|
+
clearTimeout(this.timer);
|
|
110
|
+
this.timer = null;
|
|
111
|
+
this.pendingReason = null;
|
|
112
|
+
}
|
|
113
|
+
/** Send the pending reload, unless a compile is still open — then it waits for `noteCompileEnd`. */
|
|
114
|
+
release() {
|
|
115
|
+
if (this.pendingReason === null || this.compiling)
|
|
116
|
+
return;
|
|
117
|
+
const reason = this.pendingReason;
|
|
118
|
+
this.pendingReason = null;
|
|
119
|
+
this.options.onReload(reason);
|
|
120
|
+
}
|
|
121
|
+
}
|
|
122
|
+
/** Path segments that never mean "the project changed". */
|
|
123
|
+
const IGNORED_SEGMENTS = new Set(['node_modules', '.bitmagic', '.vite-cache', '.git']);
|
|
124
|
+
/**
|
|
125
|
+
* Editors write through temporary siblings — vim's `4913`/`~`, JetBrains' `___jb_tmp___`, an
|
|
126
|
+
* atomic-rename `.tmp`. Reloading on those means reloading on a file that no longer exists.
|
|
127
|
+
*/
|
|
128
|
+
function isIgnoredPath(relative) {
|
|
129
|
+
const segments = relative.split(path.sep);
|
|
130
|
+
if (segments.some((segment) => IGNORED_SEGMENTS.has(segment)))
|
|
131
|
+
return true;
|
|
132
|
+
const name = segments[segments.length - 1] ?? '';
|
|
133
|
+
return name.startsWith('.') || name.endsWith('~') || name.includes('___jb_') || name.endsWith('.tmp');
|
|
134
|
+
}
|
|
135
|
+
/**
|
|
136
|
+
* Watch the two directories that decide what the browser shows: `src/` (what the agent and the
|
|
137
|
+
* creator edit — including `work/world.json`) and `dist/` (what the page actually loads).
|
|
138
|
+
*
|
|
139
|
+
* `dist/` is watched as well as `src/` even though the compile gate already covers the normal
|
|
140
|
+
* `tsc --watch` path, because it is what a build run outside this process — a manual `tsc`, an
|
|
141
|
+
* editor's own compiler — lands in, and that should reach the browser too.
|
|
142
|
+
*
|
|
143
|
+
* A directory that cannot be watched is skipped with a note rather than failing the command: a
|
|
144
|
+
* missing `dist/` means "not built yet", and losing auto-reload is a smaller loss than losing
|
|
145
|
+
* `bitmagic dev`.
|
|
146
|
+
*/
|
|
147
|
+
export function watchProject(root, coordinator, log) {
|
|
148
|
+
const watchers = [];
|
|
149
|
+
for (const dir of ['src', 'dist']) {
|
|
150
|
+
const absolute = path.join(root, dir);
|
|
151
|
+
if (!fs.existsSync(absolute))
|
|
152
|
+
continue;
|
|
153
|
+
try {
|
|
154
|
+
const watcher = fs.watch(absolute, { recursive: true }, (_event, filename) => {
|
|
155
|
+
const relative = filename === null ? '' : String(filename);
|
|
156
|
+
if (relative !== '' && isIgnoredPath(relative))
|
|
157
|
+
return;
|
|
158
|
+
coordinator.noteFileChange(path.posix.join(dir, relative.split(path.sep).join('/')));
|
|
159
|
+
});
|
|
160
|
+
watcher.on('error', (error) => {
|
|
161
|
+
log(`[dev] stopped watching ${dir}/: ${error.message}`);
|
|
162
|
+
});
|
|
163
|
+
watchers.push(watcher);
|
|
164
|
+
}
|
|
165
|
+
catch (error) {
|
|
166
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
167
|
+
log(`[dev] could not watch ${dir}/ (${message}) — reload on change is off for it.`);
|
|
168
|
+
}
|
|
169
|
+
}
|
|
170
|
+
return {
|
|
171
|
+
close: () => {
|
|
172
|
+
for (const watcher of watchers)
|
|
173
|
+
watcher.close();
|
|
174
|
+
},
|
|
175
|
+
};
|
|
176
|
+
}
|
|
177
|
+
/** tsc colourises when it thinks it is on a terminal; the markers below must match either way. */
|
|
178
|
+
// eslint-disable-next-line no-control-regex
|
|
179
|
+
const ANSI = /\u001B\[[0-9;]*m/g;
|
|
180
|
+
const COMPILE_STARTED = /(File change detected|Starting compilation in watch mode)/;
|
|
181
|
+
const COMPILE_FINISHED = /Found (\d+) error/;
|
|
182
|
+
/**
|
|
183
|
+
* Feed one line of `tsc --watch` output to the coordinator. Exported for its test — the markers are
|
|
184
|
+
* tsc's own wording, and a version that changes them would otherwise break auto-reload silently.
|
|
185
|
+
*/
|
|
186
|
+
export function noteTscLine(line, coordinator) {
|
|
187
|
+
const plain = line.replace(ANSI, '');
|
|
188
|
+
const finished = COMPILE_FINISHED.exec(plain);
|
|
189
|
+
if (finished) {
|
|
190
|
+
coordinator.noteCompileEnd(Number(finished[1]));
|
|
191
|
+
return;
|
|
192
|
+
}
|
|
193
|
+
if (COMPILE_STARTED.test(plain))
|
|
194
|
+
coordinator.noteCompileStart();
|
|
195
|
+
}
|
|
196
|
+
/**
|
|
197
|
+
* Spawn `tsc --watch` with its output piped through us instead of inherited.
|
|
198
|
+
*
|
|
199
|
+
* Piping is what makes the compile gate possible — the only signal that a build finished, and
|
|
200
|
+
* whether it finished clean, is tsc's own `Found N errors` line. Every line is re-echoed verbatim,
|
|
201
|
+
* so the creator sees exactly what an inherited stdio would have shown, and `--pretty` is forced
|
|
202
|
+
* back on when our own stdout is a terminal (tsc drops colour the moment it is piped).
|
|
203
|
+
*/
|
|
204
|
+
export function pipeTscWatch(tsc, cwd, coordinator) {
|
|
205
|
+
const args = ['--watch', '--preserveWatchOutput'];
|
|
206
|
+
if (process.stdout.isTTY)
|
|
207
|
+
args.push('--pretty');
|
|
208
|
+
const child = spawn(tsc, args, { cwd, stdio: ['inherit', 'pipe', 'pipe'] });
|
|
209
|
+
const consume = (stream, echo) => {
|
|
210
|
+
if (!stream)
|
|
211
|
+
return;
|
|
212
|
+
let buffered = '';
|
|
213
|
+
stream.setEncoding('utf-8');
|
|
214
|
+
stream.on('data', (chunk) => {
|
|
215
|
+
echo.write(chunk);
|
|
216
|
+
buffered += chunk;
|
|
217
|
+
const lines = buffered.split('\n');
|
|
218
|
+
// The last element is whatever came after the final newline — an incomplete line, held over.
|
|
219
|
+
buffered = lines.pop() ?? '';
|
|
220
|
+
for (const line of lines)
|
|
221
|
+
noteTscLine(line, coordinator);
|
|
222
|
+
});
|
|
223
|
+
};
|
|
224
|
+
consume(child.stdout, process.stdout);
|
|
225
|
+
consume(child.stderr, process.stderr);
|
|
226
|
+
return child;
|
|
227
|
+
}
|
|
228
|
+
//# sourceMappingURL=watch.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"watch.js","sourceRoot":"","sources":["../../src/editor/watch.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;GAkBG;AACH,OAAO,EAAE,KAAK,EAAqB,MAAM,eAAe,CAAC;AACzD,OAAO,KAAK,EAAE,MAAM,IAAI,CAAC;AACzB,OAAO,KAAK,IAAI,MAAM,MAAM,CAAC;AAI7B;;;;;;;GAOG;AACH,MAAM,CAAC,MAAM,eAAe,GAAG,GAAG,CAAC;AAEnC;;;;;;GAMG;AACH,MAAM,CAAC,MAAM,kBAAkB,GAAG,GAAG,CAAC;AAQtC,MAAM,OAAO,iBAAiB;IACX,OAAO,CAAqC;IACrD,KAAK,GAAyC,IAAI,CAAC;IACnD,aAAa,GAAkB,IAAI,CAAC;IACpC,SAAS,GAAG,KAAK,CAAC;IAClB,aAAa,GAAG,CAAC,CAAC;IAClB,OAAO,GAAG,KAAK,CAAC;IAExB,YAAY,OAAiC;QAC3C,IAAI,CAAC,OAAO,GAAG,EAAE,OAAO,EAAE,eAAe,EAAE,GAAG,OAAO,EAAE,CAAC;IAC1D,CAAC;IAED,sFAAsF;IACtF,cAAc,CAAC,MAAc;QAC3B,IAAI,IAAI,CAAC,OAAO;YAAE,OAAO;QACzB,IAAI,IAAI,CAAC,GAAG,EAAE,GAAG,IAAI,CAAC,aAAa;YAAE,OAAO;QAC5C,IAAI,CAAC,aAAa,GAAG,MAAM,CAAC;QAC5B,IAAI,IAAI,CAAC,KAAK;YAAE,YAAY,CAAC,IAAI,CAAC,KAAK,CAAC,CAAC;QACzC,IAAI,CAAC,KAAK,GAAG,UAAU,CAAC,GAAG,EAAE;YAC3B,IAAI,CAAC,KAAK,GAAG,IAAI,CAAC;YAClB,IAAI,CAAC,OAAO,EAAE,CAAC;QACjB,CAAC,EAAE,IAAI,CAAC,OAAO,CAAC,OAAO,CAAC,CAAC;IAC3B,CAAC;IAED;;;;;;OAMG;IACH,UAAU,CAAC,MAAc;QACvB,IAAI,IAAI,CAAC,OAAO;YAAE,OAAO,EAAE,MAAM,EAAE,KAAK,EAAE,CAAC;QAC3C,IAAI,IAAI,CAAC,KAAK,EAAE,CAAC;YACf,YAAY,CAAC,IAAI,CAAC,KAAK,CAAC,CAAC;YACzB,IAAI,CAAC,KAAK,GAAG,IAAI,CAAC;QACpB,CAAC;QACD,IAAI,CAAC,aAAa,GAAG,MAAM,CAAC;QAC5B,IAAI,CAAC,OAAO,EAAE,CAAC;QACf,OAAO,EAAE,MAAM,EAAE,IAAI,CAAC,aAAa,KAAK,IAAI,EAAE,CAAC;IACjD,CAAC;IAED,qFAAqF;IACrF,YAAY;QACV,IAAI,CAAC,aAAa,GAAG,IAAI,CAAC,GAAG,EAAE,GAAG,kBAAkB,CAAC;IACvD,CAAC;IAED,gBAAgB;QACd,IAAI,IAAI,CAAC,OAAO,IAAI,IAAI,CAAC,SAAS;YAAE,OAAO;QAC3C,IAAI,CAAC,SAAS,GAAG,IAAI,CAAC;QACtB,IAAI,CAAC,OAAO,CAAC,OAAO,CAAC,UAAU,CAAC,CAAC;IACnC,CAAC;IAED,cAAc,CAAC,UAAkB;QAC/B,IAAI,IAAI,CAAC,OAAO;YAAE,OAAO;QACzB,IAAI,CAAC,SAAS,GAAG,KAAK,CAAC;QACvB,IAAI,UAAU,GAAG,CAAC,EAAE,CAAC;YACnB,2FAA2F;YAC3F,yFAAyF;YACzF,IAAI,CAAC,aAAa,GAAG,IAAI,CAAC;YAC1B,IAAI,CAAC,OAAO,CAAC,OAAO,CAClB,QAAQ,EACR,GAAG,UAAU,oBAAoB,UAAU,KAAK,CAAC,CAAC,CAAC,CAAC,EAAE,CAAC,CAAC,CAAC,GAAG,8BAA8B,CAC3F,CAAC;YACF,OAAO;QACT,CAAC;QACD,IAAI,CAAC,OAAO,CAAC,OAAO,CAAC,IAAI,CAAC,CAAC;QAC3B,IAAI,CAAC,OAAO,EAAE,CAAC;IACjB,CAAC;IAED,IAAI;QACF,IAAI,CAAC,OAAO,GAAG,IAAI,CAAC;QACpB,IAAI,IAAI,CAAC,KAAK;YAAE,YAAY,CAAC,IAAI,CAAC,KAAK,CAAC,CAAC;QACzC,IAAI,CAAC,KAAK,GAAG,IAAI,CAAC;QAClB,IAAI,CAAC,aAAa,GAAG,IAAI,CAAC;IAC5B,CAAC;IAED,oGAAoG;IAC5F,OAAO;QACb,IAAI,IAAI,CAAC,aAAa,KAAK,IAAI,IAAI,IAAI,CAAC,SAAS;YAAE,OAAO;QAC1D,MAAM,MAAM,GAAG,IAAI,CAAC,aAAa,CAAC;QAClC,IAAI,CAAC,aAAa,GAAG,IAAI,CAAC;QAC1B,IAAI,CAAC,OAAO,CAAC,QAAQ,CAAC,MAAM,CAAC,CAAC;IAChC,CAAC;CACF;AAED,2DAA2D;AAC3D,MAAM,gBAAgB,GAAG,IAAI,GAAG,CAAC,CAAC,cAAc,EAAE,WAAW,EAAE,aAAa,EAAE,MAAM,CAAC,CAAC,CAAC;AAEvF;;;GAGG;AACH,SAAS,aAAa,CAAC,QAAgB;IACrC,MAAM,QAAQ,GAAG,QAAQ,CAAC,KAAK,CAAC,IAAI,CAAC,GAAG,CAAC,CAAC;IAC1C,IAAI,QAAQ,CAAC,IAAI,CAAC,CAAC,OAAO,EAAE,EAAE,CAAC,gBAAgB,CAAC,GAAG,CAAC,OAAO,CAAC,CAAC;QAAE,OAAO,IAAI,CAAC;IAC3E,MAAM,IAAI,GAAG,QAAQ,CAAC,QAAQ,CAAC,MAAM,GAAG,CAAC,CAAC,IAAI,EAAE,CAAC;IACjD,OAAO,IAAI,CAAC,UAAU,CAAC,GAAG,CAAC,IAAI,IAAI,CAAC,QAAQ,CAAC,GAAG,CAAC,IAAI,IAAI,CAAC,QAAQ,CAAC,QAAQ,CAAC,IAAI,IAAI,CAAC,QAAQ,CAAC,MAAM,CAAC,CAAC;AACxG,CAAC;AAMD;;;;;;;;;;;GAWG;AACH,MAAM,UAAU,YAAY,CAC1B,IAAY,EACZ,WAA8B,EAC9B,GAA8B;IAE9B,MAAM,QAAQ,GAAmB,EAAE,CAAC;IACpC,KAAK,MAAM,GAAG,IAAI,CAAC,KAAK,EAAE,MAAM,CAAC,EAAE,CAAC;QAClC,MAAM,QAAQ,GAAG,IAAI,CAAC,IAAI,CAAC,IAAI,EAAE,GAAG,CAAC,CAAC;QACtC,IAAI,CAAC,EAAE,CAAC,UAAU,CAAC,QAAQ,CAAC;YAAE,SAAS;QACvC,IAAI,CAAC;YACH,MAAM,OAAO,GAAG,EAAE,CAAC,KAAK,CAAC,QAAQ,EAAE,EAAE,SAAS,EAAE,IAAI,EAAE,EAAE,CAAC,MAAM,EAAE,QAAQ,EAAE,EAAE;gBAC3E,MAAM,QAAQ,GAAG,QAAQ,KAAK,IAAI,CAAC,CAAC,CAAC,EAAE,CAAC,CAAC,CAAC,MAAM,CAAC,QAAQ,CAAC,CAAC;gBAC3D,IAAI,QAAQ,KAAK,EAAE,IAAI,aAAa,CAAC,QAAQ,CAAC;oBAAE,OAAO;gBACvD,WAAW,CAAC,cAAc,CAAC,IAAI,CAAC,KAAK,CAAC,IAAI,CAAC,GAAG,EAAE,QAAQ,CAAC,KAAK,CAAC,IAAI,CAAC,GAAG,CAAC,CAAC,IAAI,CAAC,GAAG,CAAC,CAAC,CAAC,CAAC;YACvF,CAAC,CAAC,CAAC;YACH,OAAO,CAAC,EAAE,CAAC,OAAO,EAAE,CAAC,KAAY,EAAE,EAAE;gBACnC,GAAG,CAAC,0BAA0B,GAAG,MAAM,KAAK,CAAC,OAAO,EAAE,CAAC,CAAC;YAC1D,CAAC,CAAC,CAAC;YACH,QAAQ,CAAC,IAAI,CAAC,OAAO,CAAC,CAAC;QACzB,CAAC;QAAC,OAAO,KAAK,EAAE,CAAC;YACf,MAAM,OAAO,GAAG,KAAK,YAAY,KAAK,CAAC,CAAC,CAAC,KAAK,CAAC,OAAO,CAAC,CAAC,CAAC,MAAM,CAAC,KAAK,CAAC,CAAC;YACvE,GAAG,CAAC,yBAAyB,GAAG,MAAM,OAAO,qCAAqC,CAAC,CAAC;QACtF,CAAC;IACH,CAAC;IACD,OAAO;QACL,KAAK,EAAE,GAAG,EAAE;YACV,KAAK,MAAM,OAAO,IAAI,QAAQ;gBAAE,OAAO,CAAC,KAAK,EAAE,CAAC;QAClD,CAAC;KACF,CAAC;AACJ,CAAC;AAED,kGAAkG;AAClG,4CAA4C;AAC5C,MAAM,IAAI,GAAG,mBAAmB,CAAC;AAEjC,MAAM,eAAe,GAAG,2DAA2D,CAAC;AACpF,MAAM,gBAAgB,GAAG,mBAAmB,CAAC;AAE7C;;;GAGG;AACH,MAAM,UAAU,WAAW,CAAC,IAAY,EAAE,WAA8B;IACtE,MAAM,KAAK,GAAG,IAAI,CAAC,OAAO,CAAC,IAAI,EAAE,EAAE,CAAC,CAAC;IACrC,MAAM,QAAQ,GAAG,gBAAgB,CAAC,IAAI,CAAC,KAAK,CAAC,CAAC;IAC9C,IAAI,QAAQ,EAAE,CAAC;QACb,WAAW,CAAC,cAAc,CAAC,MAAM,CAAC,QAAQ,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC;QAChD,OAAO;IACT,CAAC;IACD,IAAI,eAAe,CAAC,IAAI,CAAC,KAAK,CAAC;QAAE,WAAW,CAAC,gBAAgB,EAAE,CAAC;AAClE,CAAC;AAED;;;;;;;GAOG;AACH,MAAM,UAAU,YAAY,CAC1B,GAAW,EACX,GAAW,EACX,WAA8B;IAE9B,MAAM,IAAI,GAAG,CAAC,SAAS,EAAE,uBAAuB,CAAC,CAAC;IAClD,IAAI,OAAO,CAAC,MAAM,CAAC,KAAK;QAAE,IAAI,CAAC,IAAI,CAAC,UAAU,CAAC,CAAC;IAChD,MAAM,KAAK,GAAG,KAAK,CAAC,GAAG,EAAE,IAAI,EAAE,EAAE,GAAG,EAAE,KAAK,EAAE,CAAC,SAAS,EAAE,MAAM,EAAE,MAAM,CAAC,EAAE,CAAC,CAAC;IAE5E,MAAM,OAAO,GAAG,CAAC,MAAuB,EAAE,IAAc,EAAQ,EAAE;QAChE,IAAI,CAAC,MAAM;YAAE,OAAO;QACpB,IAAI,QAAQ,GAAG,EAAE,CAAC;QAClB,MAAM,CAAC,WAAW,CAAC,OAAO,CAAC,CAAC;QAC5B,MAAM,CAAC,EAAE,CAAC,MAAM,EAAE,CAAC,KAAa,EAAE,EAAE;YAClC,IAAI,CAAC,KAAK,CAAC,KAAK,CAAC,CAAC;YAClB,QAAQ,IAAI,KAAK,CAAC;YAClB,MAAM,KAAK,GAAG,QAAQ,CAAC,KAAK,CAAC,IAAI,CAAC,CAAC;YACnC,6FAA6F;YAC7F,QAAQ,GAAG,KAAK,CAAC,GAAG,EAAE,IAAI,EAAE,CAAC;YAC7B,KAAK,MAAM,IAAI,IAAI,KAAK;gBAAE,WAAW,CAAC,IAAI,EAAE,WAAW,CAAC,CAAC;QAC3D,CAAC,CAAC,CAAC;IACL,CAAC,CAAC;IACF,OAAO,CAAC,KAAK,CAAC,MAAM,EAAE,OAAO,CAAC,MAAM,CAAC,CAAC;IACtC,OAAO,CAAC,KAAK,CAAC,MAAM,EAAE,OAAO,CAAC,MAAM,CAAC,CAAC;IACtC,OAAO,KAAK,CAAC;AACf,CAAC"}
|
package/dist/local-port.d.ts
CHANGED
|
@@ -16,6 +16,16 @@
|
|
|
16
16
|
*/
|
|
17
17
|
export declare const CORS_ALLOWED_PORT_MIN = 3000;
|
|
18
18
|
export declare const CORS_ALLOWED_PORT_MAX = 3199;
|
|
19
|
+
/**
|
|
20
|
+
* A `--port`-style flag parsed and checked against the window above.
|
|
21
|
+
*
|
|
22
|
+
* Every port `bitmagic dev` binds goes through here, the game port for the reason documented above
|
|
23
|
+
* and the shell port because the two are adjacent by default: a creator who moves one will move the
|
|
24
|
+
* other, and being told the rule once, at the point of the mistake, beats discovering it as a
|
|
25
|
+
* terrain-less world. (The shell itself fetches nothing from the CDN, so its own check is purely
|
|
26
|
+
* that kindness.)
|
|
27
|
+
*/
|
|
28
|
+
export declare function parseLocalPort(value: string | undefined, fallback: number, flag: string): number;
|
|
19
29
|
/**
|
|
20
30
|
* A free port the CDN will serve assets to, or `null` when the whole window is taken.
|
|
21
31
|
*
|
package/dist/local-port.js
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import * as net from 'net';
|
|
2
|
+
import { CliError } from './errors.js';
|
|
2
3
|
/**
|
|
3
4
|
* The port window the platform's bucket CORS policy allowlists as an origin.
|
|
4
5
|
*
|
|
@@ -17,6 +18,28 @@ import * as net from 'net';
|
|
|
17
18
|
*/
|
|
18
19
|
export const CORS_ALLOWED_PORT_MIN = 3000;
|
|
19
20
|
export const CORS_ALLOWED_PORT_MAX = 3199;
|
|
21
|
+
/**
|
|
22
|
+
* A `--port`-style flag parsed and checked against the window above.
|
|
23
|
+
*
|
|
24
|
+
* Every port `bitmagic dev` binds goes through here, the game port for the reason documented above
|
|
25
|
+
* and the shell port because the two are adjacent by default: a creator who moves one will move the
|
|
26
|
+
* other, and being told the rule once, at the point of the mistake, beats discovering it as a
|
|
27
|
+
* terrain-less world. (The shell itself fetches nothing from the CDN, so its own check is purely
|
|
28
|
+
* that kindness.)
|
|
29
|
+
*/
|
|
30
|
+
export function parseLocalPort(value, fallback, flag) {
|
|
31
|
+
if (value === undefined)
|
|
32
|
+
return fallback;
|
|
33
|
+
const port = Number(value);
|
|
34
|
+
if (!Number.isInteger(port)) {
|
|
35
|
+
throw new CliError(`\`${flag}\` must be a whole number, got "${value}".`);
|
|
36
|
+
}
|
|
37
|
+
if (port < CORS_ALLOWED_PORT_MIN || port > CORS_ALLOWED_PORT_MAX) {
|
|
38
|
+
throw new CliError(`\`${flag}\` must be between ${CORS_ALLOWED_PORT_MIN} and ${CORS_ALLOWED_PORT_MAX}. `
|
|
39
|
+
+ 'The asset CDN only allows those origins; outside that window the game loads with no terrain.');
|
|
40
|
+
}
|
|
41
|
+
return port;
|
|
42
|
+
}
|
|
20
43
|
function tryListen(port) {
|
|
21
44
|
return new Promise((resolve) => {
|
|
22
45
|
const server = net.createServer();
|
package/dist/local-port.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"local-port.js","sourceRoot":"","sources":["../src/local-port.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,GAAG,MAAM,KAAK,CAAC;
|
|
1
|
+
{"version":3,"file":"local-port.js","sourceRoot":"","sources":["../src/local-port.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,GAAG,MAAM,KAAK,CAAC;AAC3B,OAAO,EAAE,QAAQ,EAAE,MAAM,aAAa,CAAC;AAEvC;;;;;;;;;;;;;;;GAeG;AACH,MAAM,CAAC,MAAM,qBAAqB,GAAG,IAAI,CAAC;AAC1C,MAAM,CAAC,MAAM,qBAAqB,GAAG,IAAI,CAAC;AAE1C;;;;;;;;GAQG;AACH,MAAM,UAAU,cAAc,CAAC,KAAyB,EAAE,QAAgB,EAAE,IAAY;IACtF,IAAI,KAAK,KAAK,SAAS;QAAE,OAAO,QAAQ,CAAC;IACzC,MAAM,IAAI,GAAG,MAAM,CAAC,KAAK,CAAC,CAAC;IAC3B,IAAI,CAAC,MAAM,CAAC,SAAS,CAAC,IAAI,CAAC,EAAE,CAAC;QAC5B,MAAM,IAAI,QAAQ,CAAC,KAAK,IAAI,mCAAmC,KAAK,IAAI,CAAC,CAAC;IAC5E,CAAC;IACD,IAAI,IAAI,GAAG,qBAAqB,IAAI,IAAI,GAAG,qBAAqB,EAAE,CAAC;QACjE,MAAM,IAAI,QAAQ,CAChB,KAAK,IAAI,sBAAsB,qBAAqB,QAAQ,qBAAqB,IAAI;cACnF,8FAA8F,CACjG,CAAC;IACJ,CAAC;IACD,OAAO,IAAI,CAAC;AACd,CAAC;AAED,SAAS,SAAS,CAAC,IAAY;IAC7B,OAAO,IAAI,OAAO,CAAC,CAAC,OAAO,EAAE,EAAE;QAC7B,MAAM,MAAM,GAAG,GAAG,CAAC,YAAY,EAAE,CAAC;QAClC,MAAM,CAAC,IAAI,CAAC,OAAO,EAAE,GAAG,EAAE,CAAC,OAAO,CAAC,IAAI,CAAC,CAAC,CAAC;QAC1C,MAAM,CAAC,MAAM,CAAC,IAAI,EAAE,WAAW,EAAE,GAAG,EAAE;YACpC,MAAM,OAAO,GAAG,MAAM,CAAC,OAAO,EAAE,CAAC;YACjC,MAAM,KAAK,GAAG,OAAO,KAAK,IAAI,IAAI,OAAO,OAAO,KAAK,QAAQ,CAAC,CAAC,CAAC,OAAO,CAAC,IAAI,CAAC,CAAC,CAAC,IAAI,CAAC;YACpF,MAAM,CAAC,KAAK,CAAC,GAAG,EAAE,CAAC,OAAO,CAAC,KAAK,CAAC,CAAC,CAAC;QACrC,CAAC,CAAC,CAAC;IACL,CAAC,CAAC,CAAC;AACL,CAAC;AAED;;;;;;;;;;GAUG;AACH,MAAM,CAAC,KAAK,UAAU,sBAAsB;IAC1C,MAAM,IAAI,GAAG,qBAAqB,GAAG,qBAAqB,GAAG,CAAC,CAAC;IAC/D,MAAM,KAAK,GAAG,IAAI,CAAC,KAAK,CAAC,IAAI,CAAC,MAAM,EAAE,GAAG,IAAI,CAAC,CAAC;IAC/C,KAAK,IAAI,CAAC,GAAG,CAAC,EAAE,CAAC,GAAG,IAAI,EAAE,CAAC,IAAI,CAAC,EAAE,CAAC;QACjC,MAAM,IAAI,GAAG,qBAAqB,GAAG,CAAC,CAAC,KAAK,GAAG,CAAC,CAAC,GAAG,IAAI,CAAC,CAAC;QAC1D,MAAM,KAAK,GAAG,MAAM,SAAS,CAAC,IAAI,CAAC,CAAC;QACpC,IAAI,KAAK,KAAK,IAAI;YAAE,OAAO,KAAK,CAAC;IACnC,CAAC;IACD,OAAO,IAAI,CAAC;AACd,CAAC"}
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
export interface DevHandle {
|
|
2
|
+
/** The `--editor-port` value: where the tabbed Game/Editor shell is served — the URL to open. */
|
|
3
|
+
editorPort: number;
|
|
4
|
+
/** The `--port` value: where vite serves the game itself. */
|
|
5
|
+
gamePort: number;
|
|
6
|
+
/** The `bitmagic dev` process, for a human reading the file. Never used as a liveness check. */
|
|
7
|
+
pid: number;
|
|
8
|
+
}
|
|
9
|
+
export declare function devHandlePath(root: string): string;
|
|
10
|
+
export declare function writeDevHandle(root: string, handle: DevHandle): void;
|
|
11
|
+
/** Best-effort: a handle we cannot remove is a stale file, never a failed shutdown. */
|
|
12
|
+
export declare function removeDevHandle(root: string): void;
|
|
13
|
+
/** The recorded handle, or null when there is no readable one. */
|
|
14
|
+
export declare function readDevHandle(root: string): DevHandle | null;
|
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* `.bitmagic/dev.json` — how a second process finds the `bitmagic dev` already running here.
|
|
3
|
+
*
|
|
4
|
+
* `bitmagic reload` needs the shell's port, and hard-coding 3011 would break the moment a creator
|
|
5
|
+
* passes `--editor-port`. So `dev` writes what it bound the instant it is listening, and removes the
|
|
6
|
+
* file on shutdown. `.bitmagic/` is already gitignored (scaffold/project-files.ts renderGitignore),
|
|
7
|
+
* so this never reaches a commit.
|
|
8
|
+
*
|
|
9
|
+
* A stale file is expected, not exceptional: a `dev` killed with SIGKILL leaves one behind. Nothing
|
|
10
|
+
* here tries to detect that — `reload` simply fails to connect to the recorded port and reports that
|
|
11
|
+
* no dev server is running, which is the same outcome and the same message as no file at all. The
|
|
12
|
+
* pid is recorded for a human reading the file, not for liveness checks.
|
|
13
|
+
*/
|
|
14
|
+
import * as fs from 'fs';
|
|
15
|
+
import * as path from 'path';
|
|
16
|
+
export function devHandlePath(root) {
|
|
17
|
+
return path.join(root, '.bitmagic', 'dev.json');
|
|
18
|
+
}
|
|
19
|
+
export function writeDevHandle(root, handle) {
|
|
20
|
+
const file = devHandlePath(root);
|
|
21
|
+
fs.mkdirSync(path.dirname(file), { recursive: true });
|
|
22
|
+
fs.writeFileSync(file, `${JSON.stringify(handle, null, 2)}\n`);
|
|
23
|
+
}
|
|
24
|
+
/** Best-effort: a handle we cannot remove is a stale file, never a failed shutdown. */
|
|
25
|
+
export function removeDevHandle(root) {
|
|
26
|
+
try {
|
|
27
|
+
fs.rmSync(devHandlePath(root), { force: true });
|
|
28
|
+
}
|
|
29
|
+
catch {
|
|
30
|
+
// See above.
|
|
31
|
+
}
|
|
32
|
+
}
|
|
33
|
+
/** The recorded handle, or null when there is no readable one. */
|
|
34
|
+
export function readDevHandle(root) {
|
|
35
|
+
let parsed;
|
|
36
|
+
try {
|
|
37
|
+
parsed = JSON.parse(fs.readFileSync(devHandlePath(root), 'utf-8'));
|
|
38
|
+
}
|
|
39
|
+
catch {
|
|
40
|
+
return null;
|
|
41
|
+
}
|
|
42
|
+
if (typeof parsed !== 'object' || parsed === null || Array.isArray(parsed))
|
|
43
|
+
return null;
|
|
44
|
+
const record = parsed;
|
|
45
|
+
if (typeof record.editorPort !== 'number' || typeof record.gamePort !== 'number')
|
|
46
|
+
return null;
|
|
47
|
+
return {
|
|
48
|
+
editorPort: record.editorPort,
|
|
49
|
+
gamePort: record.gamePort,
|
|
50
|
+
pid: typeof record.pid === 'number' ? record.pid : 0,
|
|
51
|
+
};
|
|
52
|
+
}
|
|
53
|
+
//# sourceMappingURL=dev-handle.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"dev-handle.js","sourceRoot":"","sources":["../../src/project/dev-handle.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;GAYG;AACH,OAAO,KAAK,EAAE,MAAM,IAAI,CAAC;AACzB,OAAO,KAAK,IAAI,MAAM,MAAM,CAAC;AAW7B,MAAM,UAAU,aAAa,CAAC,IAAY;IACxC,OAAO,IAAI,CAAC,IAAI,CAAC,IAAI,EAAE,WAAW,EAAE,UAAU,CAAC,CAAC;AAClD,CAAC;AAED,MAAM,UAAU,cAAc,CAAC,IAAY,EAAE,MAAiB;IAC5D,MAAM,IAAI,GAAG,aAAa,CAAC,IAAI,CAAC,CAAC;IACjC,EAAE,CAAC,SAAS,CAAC,IAAI,CAAC,OAAO,CAAC,IAAI,CAAC,EAAE,EAAE,SAAS,EAAE,IAAI,EAAE,CAAC,CAAC;IACtD,EAAE,CAAC,aAAa,CAAC,IAAI,EAAE,GAAG,IAAI,CAAC,SAAS,CAAC,MAAM,EAAE,IAAI,EAAE,CAAC,CAAC,IAAI,CAAC,CAAC;AACjE,CAAC;AAED,uFAAuF;AACvF,MAAM,UAAU,eAAe,CAAC,IAAY;IAC1C,IAAI,CAAC;QACH,EAAE,CAAC,MAAM,CAAC,aAAa,CAAC,IAAI,CAAC,EAAE,EAAE,KAAK,EAAE,IAAI,EAAE,CAAC,CAAC;IAClD,CAAC;IAAC,MAAM,CAAC;QACP,aAAa;IACf,CAAC;AACH,CAAC;AAED,kEAAkE;AAClE,MAAM,UAAU,aAAa,CAAC,IAAY;IACxC,IAAI,MAAe,CAAC;IACpB,IAAI,CAAC;QACH,MAAM,GAAG,IAAI,CAAC,KAAK,CAAC,EAAE,CAAC,YAAY,CAAC,aAAa,CAAC,IAAI,CAAC,EAAE,OAAO,CAAC,CAAC,CAAC;IACrE,CAAC;IAAC,MAAM,CAAC;QACP,OAAO,IAAI,CAAC;IACd,CAAC;IACD,IAAI,OAAO,MAAM,KAAK,QAAQ,IAAI,MAAM,KAAK,IAAI,IAAI,KAAK,CAAC,OAAO,CAAC,MAAM,CAAC;QAAE,OAAO,IAAI,CAAC;IACxF,MAAM,MAAM,GAAG,MAAiC,CAAC;IACjD,IAAI,OAAO,MAAM,CAAC,UAAU,KAAK,QAAQ,IAAI,OAAO,MAAM,CAAC,QAAQ,KAAK,QAAQ;QAAE,OAAO,IAAI,CAAC;IAC9F,OAAO;QACL,UAAU,EAAE,MAAM,CAAC,UAAU;QAC7B,QAAQ,EAAE,MAAM,CAAC,QAAQ;QACzB,GAAG,EAAE,OAAO,MAAM,CAAC,GAAG,KAAK,QAAQ,CAAC,CAAC,CAAC,MAAM,CAAC,GAAG,CAAC,CAAC,CAAC,CAAC;KACrD,CAAC;AACJ,CAAC"}
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* `.claude/settings.json` — the one line that makes the browser reload when the agent stops.
|
|
3
|
+
*
|
|
4
|
+
* `bitmagic dev` reloads on its own once writes go quiet, which is right for a human saving a file
|
|
5
|
+
* and a guess for an agent, because an agent pauses to think and a long enough pause reads as
|
|
6
|
+
* "finished". Claude Code's `Stop` hook removes the guess: it fires exactly once, when the turn
|
|
7
|
+
* ends, and running `bitmagic reload` there means the creator sees the finished work rather than a
|
|
8
|
+
* frame from the middle of it.
|
|
9
|
+
*
|
|
10
|
+
* ── Why this file is merged and not rendered ─────────────────────────────────────────────────
|
|
11
|
+
*
|
|
12
|
+
* Every other file the CLI ships into `.claude/` is in PLATFORM_GENERATED_FILES, which `upgrade`
|
|
13
|
+
* overwrites wholesale — correct for a skill that documents the CLI, since a stale copy is worse
|
|
14
|
+
* than a lost edit. `settings.json` is the opposite: it is where a creator puts their permissions,
|
|
15
|
+
* their env, their own hooks. Overwriting it would delete work that was never ours, so `upgrade`
|
|
16
|
+
* adds our hook to whatever is there and touches nothing else, in the same read-modify-write shape
|
|
17
|
+
* `bitmagic.json` already uses.
|
|
18
|
+
*
|
|
19
|
+
* Anything that cannot be merged confidently is left alone entirely. A creator with a hand-written
|
|
20
|
+
* settings file that we cannot parse keeps their file; they lose auto-reload-on-stop, which the
|
|
21
|
+
* watcher in `editor/watch.ts` mostly covers anyway.
|
|
22
|
+
*/
|
|
23
|
+
export declare const CLAUDE_SETTINGS_FILE = ".claude/settings.json";
|
|
24
|
+
/** What the hook runs. One definition, so the scaffold and the merge cannot disagree. */
|
|
25
|
+
export declare const RELOAD_HOOK_COMMAND = "bitmagic reload";
|
|
26
|
+
export interface ReloadHookMerge {
|
|
27
|
+
/** The text to write, or null to leave the file exactly as it is. */
|
|
28
|
+
contents: string | null;
|
|
29
|
+
/** Present only when `contents` is null *because* the file could not be merged. */
|
|
30
|
+
skipped?: string;
|
|
31
|
+
}
|
|
32
|
+
/** The settings a project with no `.claude/settings.json` at all gets. */
|
|
33
|
+
export declare function renderClaudeSettings(): string;
|
|
34
|
+
/**
|
|
35
|
+
* Add the Stop hook to an existing settings file, preserving everything else in it.
|
|
36
|
+
*
|
|
37
|
+
* `existing` is the file's text, or undefined when there is no file. Returns `contents: null` when
|
|
38
|
+
* there is nothing to do — the hook is already there, or the file is not something we can safely
|
|
39
|
+
* rewrite.
|
|
40
|
+
*/
|
|
41
|
+
export declare function mergeReloadHook(existing: string | undefined): ReloadHookMerge;
|
|
@@ -0,0 +1,98 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* `.claude/settings.json` — the one line that makes the browser reload when the agent stops.
|
|
3
|
+
*
|
|
4
|
+
* `bitmagic dev` reloads on its own once writes go quiet, which is right for a human saving a file
|
|
5
|
+
* and a guess for an agent, because an agent pauses to think and a long enough pause reads as
|
|
6
|
+
* "finished". Claude Code's `Stop` hook removes the guess: it fires exactly once, when the turn
|
|
7
|
+
* ends, and running `bitmagic reload` there means the creator sees the finished work rather than a
|
|
8
|
+
* frame from the middle of it.
|
|
9
|
+
*
|
|
10
|
+
* ── Why this file is merged and not rendered ─────────────────────────────────────────────────
|
|
11
|
+
*
|
|
12
|
+
* Every other file the CLI ships into `.claude/` is in PLATFORM_GENERATED_FILES, which `upgrade`
|
|
13
|
+
* overwrites wholesale — correct for a skill that documents the CLI, since a stale copy is worse
|
|
14
|
+
* than a lost edit. `settings.json` is the opposite: it is where a creator puts their permissions,
|
|
15
|
+
* their env, their own hooks. Overwriting it would delete work that was never ours, so `upgrade`
|
|
16
|
+
* adds our hook to whatever is there and touches nothing else, in the same read-modify-write shape
|
|
17
|
+
* `bitmagic.json` already uses.
|
|
18
|
+
*
|
|
19
|
+
* Anything that cannot be merged confidently is left alone entirely. A creator with a hand-written
|
|
20
|
+
* settings file that we cannot parse keeps their file; they lose auto-reload-on-stop, which the
|
|
21
|
+
* watcher in `editor/watch.ts` mostly covers anyway.
|
|
22
|
+
*/
|
|
23
|
+
export const CLAUDE_SETTINGS_FILE = '.claude/settings.json';
|
|
24
|
+
/** What the hook runs. One definition, so the scaffold and the merge cannot disagree. */
|
|
25
|
+
export const RELOAD_HOOK_COMMAND = 'bitmagic reload';
|
|
26
|
+
function isRecord(value) {
|
|
27
|
+
return typeof value === 'object' && value !== null && !Array.isArray(value);
|
|
28
|
+
}
|
|
29
|
+
function stringify(settings) {
|
|
30
|
+
return `${JSON.stringify(settings, null, 2)}\n`;
|
|
31
|
+
}
|
|
32
|
+
/** The `Stop` matcher group this CLI owns. */
|
|
33
|
+
function reloadMatcher() {
|
|
34
|
+
return { hooks: [{ type: 'command', command: RELOAD_HOOK_COMMAND }] };
|
|
35
|
+
}
|
|
36
|
+
/** The settings a project with no `.claude/settings.json` at all gets. */
|
|
37
|
+
export function renderClaudeSettings() {
|
|
38
|
+
return stringify({ hooks: { Stop: [reloadMatcher()] } });
|
|
39
|
+
}
|
|
40
|
+
/**
|
|
41
|
+
* Does this `Stop` group already run our command?
|
|
42
|
+
*
|
|
43
|
+
* Matched by substring rather than equality on purpose: a creator who wrapped it — `cd app &&
|
|
44
|
+
* bitmagic reload`, `bitmagic reload --port 3021` — has the hook, and adding a second one would
|
|
45
|
+
* reload twice per turn.
|
|
46
|
+
*/
|
|
47
|
+
function runsReload(matcher) {
|
|
48
|
+
if (!isRecord(matcher))
|
|
49
|
+
return false;
|
|
50
|
+
const hooks = matcher.hooks;
|
|
51
|
+
if (!Array.isArray(hooks))
|
|
52
|
+
return false;
|
|
53
|
+
return hooks.some((hook) => {
|
|
54
|
+
if (!isRecord(hook))
|
|
55
|
+
return false;
|
|
56
|
+
const command = hook.command;
|
|
57
|
+
return typeof command === 'string' && command.includes(RELOAD_HOOK_COMMAND);
|
|
58
|
+
});
|
|
59
|
+
}
|
|
60
|
+
/**
|
|
61
|
+
* Add the Stop hook to an existing settings file, preserving everything else in it.
|
|
62
|
+
*
|
|
63
|
+
* `existing` is the file's text, or undefined when there is no file. Returns `contents: null` when
|
|
64
|
+
* there is nothing to do — the hook is already there, or the file is not something we can safely
|
|
65
|
+
* rewrite.
|
|
66
|
+
*/
|
|
67
|
+
export function mergeReloadHook(existing) {
|
|
68
|
+
if (existing === undefined || existing.trim() === '') {
|
|
69
|
+
return { contents: renderClaudeSettings() };
|
|
70
|
+
}
|
|
71
|
+
let parsed;
|
|
72
|
+
try {
|
|
73
|
+
parsed = JSON.parse(existing);
|
|
74
|
+
}
|
|
75
|
+
catch {
|
|
76
|
+
return { contents: null, skipped: `${CLAUDE_SETTINGS_FILE} is not valid JSON` };
|
|
77
|
+
}
|
|
78
|
+
if (!isRecord(parsed)) {
|
|
79
|
+
return { contents: null, skipped: `${CLAUDE_SETTINGS_FILE} does not contain a settings object` };
|
|
80
|
+
}
|
|
81
|
+
const hooks = parsed.hooks === undefined ? {} : parsed.hooks;
|
|
82
|
+
if (!isRecord(hooks)) {
|
|
83
|
+
return { contents: null, skipped: `${CLAUDE_SETTINGS_FILE} has a "hooks" value we cannot merge into` };
|
|
84
|
+
}
|
|
85
|
+
const stop = hooks.Stop === undefined ? [] : hooks.Stop;
|
|
86
|
+
if (!Array.isArray(stop)) {
|
|
87
|
+
return { contents: null, skipped: `${CLAUDE_SETTINGS_FILE} has a "hooks.Stop" value we cannot merge into` };
|
|
88
|
+
}
|
|
89
|
+
if (stop.some(runsReload))
|
|
90
|
+
return { contents: null };
|
|
91
|
+
return {
|
|
92
|
+
contents: stringify({
|
|
93
|
+
...parsed,
|
|
94
|
+
hooks: { ...hooks, Stop: [...stop, reloadMatcher()] },
|
|
95
|
+
}),
|
|
96
|
+
};
|
|
97
|
+
}
|
|
98
|
+
//# sourceMappingURL=claude-settings.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"claude-settings.js","sourceRoot":"","sources":["../../src/scaffold/claude-settings.ts"],"names":[],"mappings":"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"}
|