@codeyam-editor/codeyam-editor 0.1.0-staging.c9412f0 → 0.1.1
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/npm/container.js +7 -3
- package/npm/editor-dev.js +77 -69
- package/npm/editor.js +120 -10
- package/npm/postinstall.js +48 -11
- package/npm/scenario-check.js +15 -2
- package/npm/scenario-playwright.js +1 -0
- package/npm/utils.js +44 -4
- package/package.json +3 -2
package/npm/container.js
CHANGED
|
@@ -8,7 +8,7 @@ const path = require("path");
|
|
|
8
8
|
* inline monitoring UI at http://localhost:14199.
|
|
9
9
|
*/
|
|
10
10
|
|
|
11
|
-
const { ensureServer, rootDir } = require("./utils");
|
|
11
|
+
const { ensureServer, rootDir, killChildProcess } = require("./utils");
|
|
12
12
|
const { open } = require("./open");
|
|
13
13
|
|
|
14
14
|
const PORT = 14199;
|
|
@@ -23,9 +23,13 @@ async function main() {
|
|
|
23
23
|
|
|
24
24
|
process.on("SIGINT", () => {
|
|
25
25
|
clearInterval(keepAlive);
|
|
26
|
-
|
|
26
|
+
killChildProcess(serverChild);
|
|
27
27
|
process.exit(0);
|
|
28
28
|
});
|
|
29
29
|
}
|
|
30
30
|
|
|
31
|
-
main
|
|
31
|
+
module.exports = { main };
|
|
32
|
+
|
|
33
|
+
if (require.main === module) {
|
|
34
|
+
main();
|
|
35
|
+
}
|
package/npm/editor-dev.js
CHANGED
|
@@ -19,7 +19,7 @@
|
|
|
19
19
|
const { execSync, spawn } = require("child_process");
|
|
20
20
|
const path = require("path");
|
|
21
21
|
const fs = require("fs");
|
|
22
|
-
const { ensureServer, restartServer, waitForPort, ensureBinary, uiDistDir, rootDir } = require("./utils");
|
|
22
|
+
const { ensureServer, restartServer, waitForPort, ensureBinary, uiDistDir, rootDir, killChildProcess } = require("./utils");
|
|
23
23
|
const { open } = require("./open");
|
|
24
24
|
|
|
25
25
|
// Tell the Rust binary to emit "codeyam-editor-dev" in all instructions,
|
|
@@ -29,9 +29,13 @@ process.env.CODEYAM_CLI = "codeyam-editor-dev";
|
|
|
29
29
|
const args = process.argv.slice(2);
|
|
30
30
|
const KNOWN_SUBCOMMANDS = ["start", "init", "editor"];
|
|
31
31
|
|
|
32
|
-
/**
|
|
33
|
-
|
|
34
|
-
|
|
32
|
+
/**
|
|
33
|
+
* Check if the first non-flag argument is a known subcommand.
|
|
34
|
+
* Accepts an explicit argv array so the helper is testable without mutating
|
|
35
|
+
* process.argv (the module-level `args` const is the production default).
|
|
36
|
+
*/
|
|
37
|
+
function hasSubcommand(argv = args) {
|
|
38
|
+
for (const arg of argv) {
|
|
35
39
|
if (arg.startsWith("-")) continue;
|
|
36
40
|
return KNOWN_SUBCOMMANDS.includes(arg);
|
|
37
41
|
}
|
|
@@ -48,73 +52,77 @@ function buildBinary() {
|
|
|
48
52
|
}
|
|
49
53
|
}
|
|
50
54
|
|
|
51
|
-
|
|
52
|
-
if (hasSubcommand()) {
|
|
53
|
-
buildBinary();
|
|
54
|
-
const binary = ensureBinary();
|
|
55
|
-
const child = spawn(binary, args, {
|
|
56
|
-
stdio: "inherit",
|
|
57
|
-
cwd: process.cwd(),
|
|
58
|
-
env: { ...process.env, CODEYAM_EDITOR_UI_DIR: uiDistDir() },
|
|
59
|
-
});
|
|
60
|
-
child.on("exit", (code) => {
|
|
61
|
-
process.exit(code ?? 1);
|
|
62
|
-
});
|
|
63
|
-
} else {
|
|
64
|
-
// --- Launcher mode: build everything, start server, open browser ---
|
|
65
|
-
const SERVER_PORT = 14199;
|
|
66
|
-
const shouldRestart = args.includes("--restart");
|
|
67
|
-
|
|
68
|
-
function readProjectPort() {
|
|
69
|
-
try {
|
|
70
|
-
const configPath = path.join(process.cwd(), ".codeyam", "editor.json");
|
|
71
|
-
const config = JSON.parse(fs.readFileSync(configPath, "utf8"));
|
|
72
|
-
return config.port || 3000;
|
|
73
|
-
} catch {
|
|
74
|
-
return 3000;
|
|
75
|
-
}
|
|
76
|
-
}
|
|
77
|
-
|
|
78
|
-
async function main() {
|
|
79
|
-
const projectDir = process.cwd();
|
|
80
|
-
console.error(`Editor repo: ${rootDir}`);
|
|
81
|
-
console.error(`Target project: ${projectDir}`);
|
|
55
|
+
module.exports = { hasSubcommand, buildBinary };
|
|
82
56
|
|
|
57
|
+
if (require.main === module) {
|
|
58
|
+
// --- Passthrough mode: build binary, then forward to it ---
|
|
59
|
+
if (hasSubcommand()) {
|
|
83
60
|
buildBinary();
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
}
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
const serverChild = shouldRestart
|
|
94
|
-
? await restartServer(SERVER_PORT)
|
|
95
|
-
: await ensureServer(SERVER_PORT);
|
|
96
|
-
|
|
97
|
-
const appPort = readProjectPort();
|
|
98
|
-
const viteInternalPort = appPort + 1;
|
|
99
|
-
console.error(`Waiting for dev server on port ${viteInternalPort}...`);
|
|
100
|
-
const viteReady = await waitForPort(viteInternalPort, 15000, 300);
|
|
101
|
-
if (!viteReady) {
|
|
102
|
-
console.error(`Warning: Dev server not ready on port ${viteInternalPort}. Live Preview may not work initially.`);
|
|
103
|
-
}
|
|
104
|
-
|
|
105
|
-
const url = `http://localhost:${SERVER_PORT}`;
|
|
106
|
-
console.error(`Editor UI: ${url}`);
|
|
107
|
-
console.error(`Live Preview: http://localhost:${appPort}`);
|
|
108
|
-
console.error(`Project: ${projectDir}`);
|
|
109
|
-
open(url);
|
|
110
|
-
const keepAlive = setInterval(() => {}, 60_000);
|
|
111
|
-
|
|
112
|
-
process.on("SIGINT", () => {
|
|
113
|
-
clearInterval(keepAlive);
|
|
114
|
-
if (serverChild) serverChild.kill("SIGINT");
|
|
115
|
-
process.exit(0);
|
|
61
|
+
const binary = ensureBinary();
|
|
62
|
+
const child = spawn(binary, args, {
|
|
63
|
+
stdio: "inherit",
|
|
64
|
+
cwd: process.cwd(),
|
|
65
|
+
env: { ...process.env, CODEYAM_EDITOR_UI_DIR: uiDistDir() },
|
|
66
|
+
});
|
|
67
|
+
child.on("exit", (code) => {
|
|
68
|
+
process.exit(code ?? 1);
|
|
116
69
|
});
|
|
70
|
+
} else {
|
|
71
|
+
// --- Launcher mode: build everything, start server, open browser ---
|
|
72
|
+
const SERVER_PORT = 14199;
|
|
73
|
+
const shouldRestart = args.includes("--restart");
|
|
74
|
+
|
|
75
|
+
const readProjectPort = () => {
|
|
76
|
+
try {
|
|
77
|
+
const configPath = path.join(process.cwd(), ".codeyam", "editor.json");
|
|
78
|
+
const config = JSON.parse(fs.readFileSync(configPath, "utf8"));
|
|
79
|
+
return config.port || 3000;
|
|
80
|
+
} catch {
|
|
81
|
+
return 3000;
|
|
82
|
+
}
|
|
83
|
+
};
|
|
84
|
+
|
|
85
|
+
const launcherMain = async () => {
|
|
86
|
+
const projectDir = process.cwd();
|
|
87
|
+
console.error(`Editor repo: ${rootDir}`);
|
|
88
|
+
console.error(`Target project: ${projectDir}`);
|
|
89
|
+
|
|
90
|
+
buildBinary();
|
|
91
|
+
|
|
92
|
+
// Build the UI from the editor repo
|
|
93
|
+
console.error("Building UI...");
|
|
94
|
+
try {
|
|
95
|
+
execSync("npx vite build", { stdio: "inherit", cwd: path.join(rootDir, "ui") });
|
|
96
|
+
} catch {
|
|
97
|
+
console.error("Failed to build UI. Continuing with existing build (if any)...");
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
const serverChild = shouldRestart
|
|
101
|
+
? await restartServer(SERVER_PORT)
|
|
102
|
+
: await ensureServer(SERVER_PORT);
|
|
103
|
+
|
|
104
|
+
const appPort = readProjectPort();
|
|
105
|
+
const viteInternalPort = appPort + 1;
|
|
106
|
+
console.error(`Waiting for dev server on port ${viteInternalPort}...`);
|
|
107
|
+
const viteReady = await waitForPort(viteInternalPort, 15000, 300);
|
|
108
|
+
if (!viteReady) {
|
|
109
|
+
console.error(`Warning: Dev server not ready on port ${viteInternalPort}. Live Preview may not work initially.`);
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
const url = `http://localhost:${SERVER_PORT}`;
|
|
113
|
+
console.error(`Editor UI: ${url}`);
|
|
114
|
+
console.error(`Live Preview: http://localhost:${appPort}`);
|
|
115
|
+
console.error(`Project: ${projectDir}`);
|
|
116
|
+
open(url);
|
|
117
|
+
const keepAlive = setInterval(() => {}, 60_000);
|
|
118
|
+
|
|
119
|
+
process.on("SIGINT", () => {
|
|
120
|
+
clearInterval(keepAlive);
|
|
121
|
+
killChildProcess(serverChild);
|
|
122
|
+
process.exit(0);
|
|
123
|
+
});
|
|
124
|
+
};
|
|
125
|
+
|
|
126
|
+
launcherMain();
|
|
117
127
|
}
|
|
118
|
-
|
|
119
|
-
main();
|
|
120
128
|
}
|
package/npm/editor.js
CHANGED
|
@@ -13,12 +13,14 @@
|
|
|
13
13
|
*/
|
|
14
14
|
|
|
15
15
|
const { execSync } = require("child_process");
|
|
16
|
+
const fs = require("fs");
|
|
17
|
+
const os = require("os");
|
|
16
18
|
const path = require("path");
|
|
17
|
-
const { ensureServer,
|
|
19
|
+
const { ensureServer, stopServer, isPortInUse, waitForPort, rootDir, killChildProcess } = require("./utils");
|
|
18
20
|
const { open } = require("./open");
|
|
19
21
|
|
|
20
|
-
const SERVER_PORT = 14199;
|
|
21
|
-
const VITE_PORT = 5173;
|
|
22
|
+
const SERVER_PORT = parseInt(process.env.CODEYAM_EDITOR_PORT || "14199", 10);
|
|
23
|
+
const VITE_PORT = parseInt(process.env.PORT || "5173", 10);
|
|
22
24
|
// The reverse proxy sits on VITE_PORT; the actual Vite dev server runs on
|
|
23
25
|
// VITE_PORT + 1. We need to wait for the real Vite server, not the proxy.
|
|
24
26
|
const VITE_INTERNAL_PORT = VITE_PORT + 1;
|
|
@@ -28,16 +30,107 @@ function buildBinary() {
|
|
|
28
30
|
console.error("Building Rust binary...");
|
|
29
31
|
try {
|
|
30
32
|
execSync("cargo build", { stdio: "inherit", cwd: rootDir });
|
|
33
|
+
return true;
|
|
31
34
|
} catch {
|
|
32
35
|
console.error("Failed to build Rust binary. Continuing with existing build (if any)...");
|
|
36
|
+
return false;
|
|
37
|
+
}
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
// Resolve <CARGO_HOME>/bin (falling back to $HOME/.cargo/bin). Mirrors
|
|
41
|
+
// `installed_binary_path()` in crates/control-api/src/pty_broker_startup.rs
|
|
42
|
+
// — the Rust broker auto-spawn execs the binary at this path, so we MUST
|
|
43
|
+
// keep it in sync with the freshly-built debug binary or `npm run editor`
|
|
44
|
+
// will start a server whose PTY-broker auto-spawn execs a stale install.
|
|
45
|
+
function cargoBinDir() {
|
|
46
|
+
if (process.env.CARGO_HOME) return path.join(process.env.CARGO_HOME, "bin");
|
|
47
|
+
return path.join(os.homedir(), ".cargo", "bin");
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
// Install target/debug/codeyam-editor to <cargo_bin>/codeyam-editor
|
|
51
|
+
// atomically (write to tmp in same dir, then rename), and refresh the
|
|
52
|
+
// codeyam-editor-pty-broker hardlink so it points at the new inode.
|
|
53
|
+
//
|
|
54
|
+
// The PTY broker auto-spawn (control-api/src/pty_broker_startup.rs) always
|
|
55
|
+
// resolves the *installed* path, NOT current_exe() — that was a deliberate
|
|
56
|
+
// fix to prevent a fork-bomb where test binaries spawned themselves. So a
|
|
57
|
+
// fresh `target/debug/codeyam-editor` is not enough; the install path must
|
|
58
|
+
// be refreshed too. Without this, build tabs hang on "Reconnecting..."
|
|
59
|
+
// because the broker daemon errors out (`unrecognized subcommand`) before
|
|
60
|
+
// the UDS socket opens.
|
|
61
|
+
function installBinary() {
|
|
62
|
+
const srcPath = path.join(rootDir, "target", "debug", "codeyam-editor");
|
|
63
|
+
if (!fs.existsSync(srcPath)) {
|
|
64
|
+
console.error(`Skipping install: ${srcPath} not found.`);
|
|
65
|
+
return;
|
|
66
|
+
}
|
|
67
|
+
const binDir = cargoBinDir();
|
|
68
|
+
const dstPath = path.join(binDir, "codeyam-editor");
|
|
69
|
+
const linkPath = path.join(binDir, "codeyam-editor-pty-broker");
|
|
70
|
+
const tmpPath = path.join(binDir, `.codeyam-editor.tmp.${process.pid}`);
|
|
71
|
+
console.error(`Installing fresh binary to ${dstPath}...`);
|
|
72
|
+
try {
|
|
73
|
+
fs.mkdirSync(binDir, { recursive: true });
|
|
74
|
+
fs.copyFileSync(srcPath, tmpPath);
|
|
75
|
+
fs.chmodSync(tmpPath, 0o755);
|
|
76
|
+
fs.renameSync(tmpPath, dstPath);
|
|
77
|
+
try {
|
|
78
|
+
fs.unlinkSync(linkPath);
|
|
79
|
+
} catch (e) {
|
|
80
|
+
if (e.code !== "ENOENT") throw e;
|
|
81
|
+
}
|
|
82
|
+
fs.linkSync(dstPath, linkPath);
|
|
83
|
+
} catch (err) {
|
|
84
|
+
console.error(`Failed to install binary: ${err.message}`);
|
|
85
|
+
try { fs.unlinkSync(tmpPath); } catch {}
|
|
86
|
+
}
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
// Evict Rust build artifacts not touched in the last day so target/ doesn't
|
|
90
|
+
// grow unboundedly. Must run after the old server is stopped (it holds files
|
|
91
|
+
// in target/debug/ open) and before the new server starts. Non-fatal.
|
|
92
|
+
function sweepStaleArtifacts() {
|
|
93
|
+
console.error("Sweeping stale Rust build artifacts (idle >1 day)...");
|
|
94
|
+
try {
|
|
95
|
+
execSync("cargo sweep --time 1", { stdio: "inherit", cwd: rootDir });
|
|
96
|
+
} catch {
|
|
97
|
+
console.error("cargo-sweep failed (install with `cargo install cargo-sweep`). Continuing.");
|
|
33
98
|
}
|
|
34
99
|
}
|
|
35
100
|
|
|
36
101
|
async function main() {
|
|
102
|
+
// Sync ports to editor.json so the Rust backend uses them
|
|
103
|
+
const editorJsonPath = path.join(rootDir, ".codeyam", "editor.json");
|
|
104
|
+
if (fs.existsSync(editorJsonPath)) {
|
|
105
|
+
const editorJson = JSON.parse(fs.readFileSync(editorJsonPath, "utf8"));
|
|
106
|
+
editorJson.port = VITE_PORT;
|
|
107
|
+
editorJson.startCommand = `cd ui && npx vite --port ${VITE_PORT}`;
|
|
108
|
+
|
|
109
|
+
const uiApp = editorJson.apps?.find(a => a.dir === "ui");
|
|
110
|
+
if (uiApp) {
|
|
111
|
+
uiApp.port = VITE_PORT;
|
|
112
|
+
uiApp.startCommand = `npx vite --port ${VITE_PORT}`;
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
if (editorJson.proxy) {
|
|
116
|
+
editorJson.proxy.controlPort = SERVER_PORT;
|
|
117
|
+
editorJson.proxy.httpPort = SERVER_PORT - 99;
|
|
118
|
+
editorJson.proxy.dbPort = SERVER_PORT - 98;
|
|
119
|
+
}
|
|
120
|
+
fs.writeFileSync(editorJsonPath, JSON.stringify(editorJson, null, 2));
|
|
121
|
+
}
|
|
122
|
+
|
|
37
123
|
// Rebuild the Rust binary so code changes are picked up on restart.
|
|
38
124
|
// This script is only invoked from a source checkout (via `npm run editor`),
|
|
39
125
|
// so Cargo.toml and the crates tree are always present.
|
|
40
|
-
buildBinary();
|
|
126
|
+
const buildOk = buildBinary();
|
|
127
|
+
// Refresh <cargo_bin>/codeyam-editor (and the pty-broker hardlink) so
|
|
128
|
+
// the broker auto-spawn execs a binary that knows the `pty-broker
|
|
129
|
+
// daemon` subcommand. Skip if the build failed — installing a stale
|
|
130
|
+
// debug binary over a working install is worse than leaving it.
|
|
131
|
+
if (buildOk) {
|
|
132
|
+
installBinary();
|
|
133
|
+
}
|
|
41
134
|
|
|
42
135
|
// Build the UI so the backend can serve it as static files
|
|
43
136
|
console.error("Building UI...");
|
|
@@ -51,10 +144,17 @@ async function main() {
|
|
|
51
144
|
}
|
|
52
145
|
|
|
53
146
|
// Start the Rust backend — it serves ui/dist/ and launches the Vite dev
|
|
54
|
-
// server (configured via startCommand in editor.json) for Live Preview HMR
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
147
|
+
// server (configured via startCommand in editor.json) for Live Preview HMR.
|
|
148
|
+
// On --restart: stop the existing server first, sweep stale artifacts while
|
|
149
|
+
// target/ is idle (no process holding files open), then start fresh.
|
|
150
|
+
if (shouldRestart && (await isPortInUse(SERVER_PORT))) {
|
|
151
|
+
console.error(`Stopping existing server on port ${SERVER_PORT}...`);
|
|
152
|
+
await stopServer(SERVER_PORT);
|
|
153
|
+
}
|
|
154
|
+
if (shouldRestart) {
|
|
155
|
+
sweepStaleArtifacts();
|
|
156
|
+
}
|
|
157
|
+
const serverChild = await ensureServer(SERVER_PORT);
|
|
58
158
|
|
|
59
159
|
// Wait for the actual Vite dev server on the internal port (not the reverse
|
|
60
160
|
// proxy on VITE_PORT, which starts immediately with the Rust binary).
|
|
@@ -73,9 +173,19 @@ async function main() {
|
|
|
73
173
|
|
|
74
174
|
process.on("SIGINT", () => {
|
|
75
175
|
clearInterval(keepAlive);
|
|
76
|
-
|
|
176
|
+
killChildProcess(serverChild);
|
|
77
177
|
process.exit(0);
|
|
78
178
|
});
|
|
79
179
|
}
|
|
80
180
|
|
|
81
|
-
|
|
181
|
+
module.exports = {
|
|
182
|
+
buildBinary,
|
|
183
|
+
cargoBinDir,
|
|
184
|
+
installBinary,
|
|
185
|
+
main,
|
|
186
|
+
sweepStaleArtifacts,
|
|
187
|
+
};
|
|
188
|
+
|
|
189
|
+
if (require.main === module) {
|
|
190
|
+
main();
|
|
191
|
+
}
|
package/npm/postinstall.js
CHANGED
|
@@ -5,11 +5,17 @@
|
|
|
5
5
|
*
|
|
6
6
|
* Downloads the pre-built platform binary and UI assets from GitHub Releases.
|
|
7
7
|
* Skipped when running in local development (cargo builds from source instead).
|
|
8
|
+
*
|
|
9
|
+
* Cross-platform: uses Node-native `fetch` + the `tar` npm package to download
|
|
10
|
+
* and extract. Avoids depending on `curl` / `tar` system binaries — those exist
|
|
11
|
+
* on macOS and Linux but not on a default Windows install, and the previous
|
|
12
|
+
* `curl | tar xz` shell pipe was the original Windows-install blocker.
|
|
8
13
|
*/
|
|
9
14
|
|
|
10
15
|
const fs = require("fs");
|
|
11
16
|
const path = require("path");
|
|
12
|
-
const {
|
|
17
|
+
const { Readable } = require("stream");
|
|
18
|
+
const tar = require("tar");
|
|
13
19
|
|
|
14
20
|
const rootDir = path.resolve(__dirname, "..");
|
|
15
21
|
|
|
@@ -17,8 +23,15 @@ const PLATFORM_MAP = {
|
|
|
17
23
|
"darwin-arm64": "codeyam-editor-darwin-arm64",
|
|
18
24
|
"darwin-x64": "codeyam-editor-darwin-x64",
|
|
19
25
|
"linux-x64": "codeyam-editor-linux-x64",
|
|
26
|
+
"win32-x64": "codeyam-editor-win32-x64",
|
|
20
27
|
};
|
|
21
28
|
|
|
29
|
+
function binaryName() {
|
|
30
|
+
return process.platform === "win32"
|
|
31
|
+
? "codeyam-editor.exe"
|
|
32
|
+
: "codeyam-editor";
|
|
33
|
+
}
|
|
34
|
+
|
|
22
35
|
function getArtifactName() {
|
|
23
36
|
const key = `${process.platform}-${process.arch}`;
|
|
24
37
|
const name = PLATFORM_MAP[key];
|
|
@@ -26,7 +39,10 @@ function getArtifactName() {
|
|
|
26
39
|
console.error(
|
|
27
40
|
`codeyam-editor: unsupported platform ${process.platform} ${process.arch}`
|
|
28
41
|
);
|
|
29
|
-
console.error("Supported: macOS (arm64, x64), Linux (x64)");
|
|
42
|
+
console.error("Supported: macOS (arm64, x64), Linux (x64), Windows (x64)");
|
|
43
|
+
console.error(
|
|
44
|
+
"You can build from source instead: cargo build --release"
|
|
45
|
+
);
|
|
30
46
|
process.exit(0); // Don't fail the install — user may be building from source
|
|
31
47
|
}
|
|
32
48
|
return name;
|
|
@@ -47,9 +63,22 @@ function getReleaseTag() {
|
|
|
47
63
|
return "latest";
|
|
48
64
|
}
|
|
49
65
|
|
|
50
|
-
function
|
|
66
|
+
async function downloadAndExtract(url, dest) {
|
|
67
|
+
const res = await fetch(url);
|
|
68
|
+
if (!res.ok) {
|
|
69
|
+
throw new Error(`HTTP ${res.status} ${res.statusText}`);
|
|
70
|
+
}
|
|
71
|
+
await new Promise((resolve, reject) => {
|
|
72
|
+
Readable.fromWeb(res.body)
|
|
73
|
+
.pipe(tar.x({ cwd: dest }))
|
|
74
|
+
.on("finish", resolve)
|
|
75
|
+
.on("error", reject);
|
|
76
|
+
});
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
async function download() {
|
|
51
80
|
// Skip if a binary already exists (e.g. local dev with cargo build)
|
|
52
|
-
const binPath = path.join(rootDir, "bin",
|
|
81
|
+
const binPath = path.join(rootDir, "bin", binaryName());
|
|
53
82
|
if (fs.existsSync(binPath)) {
|
|
54
83
|
return;
|
|
55
84
|
}
|
|
@@ -71,13 +100,9 @@ function download() {
|
|
|
71
100
|
fs.mkdirSync(uiDir, { recursive: true });
|
|
72
101
|
|
|
73
102
|
try {
|
|
74
|
-
|
|
75
|
-
execSync(
|
|
76
|
-
`curl -fsSL "${url}" | tar xz -C "${rootDir}"`,
|
|
77
|
-
{ stdio: ["ignore", "inherit", "inherit"], timeout: 120000 }
|
|
78
|
-
);
|
|
103
|
+
await downloadAndExtract(url, rootDir);
|
|
79
104
|
|
|
80
|
-
// Ensure binary is executable
|
|
105
|
+
// Ensure binary is executable. No-op on Windows but harmless.
|
|
81
106
|
if (fs.existsSync(binPath)) {
|
|
82
107
|
fs.chmodSync(binPath, 0o755);
|
|
83
108
|
}
|
|
@@ -85,6 +110,7 @@ function download() {
|
|
|
85
110
|
console.error("codeyam-editor installed successfully!");
|
|
86
111
|
} catch (err) {
|
|
87
112
|
console.error(`Failed to download codeyam-editor binary from ${url}`);
|
|
113
|
+
console.error(` Reason: ${err.message}`);
|
|
88
114
|
console.error(
|
|
89
115
|
"You can build from source instead: cargo build --release"
|
|
90
116
|
);
|
|
@@ -92,4 +118,15 @@ function download() {
|
|
|
92
118
|
}
|
|
93
119
|
}
|
|
94
120
|
|
|
95
|
-
|
|
121
|
+
module.exports = {
|
|
122
|
+
PLATFORM_MAP,
|
|
123
|
+
binaryName,
|
|
124
|
+
getArtifactName,
|
|
125
|
+
getReleaseTag,
|
|
126
|
+
downloadAndExtract,
|
|
127
|
+
download,
|
|
128
|
+
};
|
|
129
|
+
|
|
130
|
+
if (require.main === module) {
|
|
131
|
+
download();
|
|
132
|
+
}
|
package/npm/scenario-check.js
CHANGED
|
@@ -43,6 +43,8 @@ const {
|
|
|
43
43
|
handleRequestFailed,
|
|
44
44
|
} = require("./scenario-handlers");
|
|
45
45
|
|
|
46
|
+
const BLANK_RETRY_DELAY_MS = 500;
|
|
47
|
+
|
|
46
48
|
async function runScenarioCheck(config) {
|
|
47
49
|
const { url, outputPath, width, height, httpMocks = {} } = config;
|
|
48
50
|
const issues = [];
|
|
@@ -113,8 +115,18 @@ async function runScenarioCheck(config) {
|
|
|
113
115
|
);
|
|
114
116
|
}
|
|
115
117
|
|
|
116
|
-
|
|
117
|
-
|
|
118
|
+
// Cold-start retry: a single re-collect after a short pause covers
|
|
119
|
+
// the React.lazy / Suspense-fallback race that flagged 4-of-59
|
|
120
|
+
// scenarios as blank against a cold Vite dev server on 2026-04-30.
|
|
121
|
+
// One retry is enough — the Suspense window is sub-second in
|
|
122
|
+
// practice, longer waits just slow down genuine blank-render bugs.
|
|
123
|
+
let contentState = await collectContentState(frame);
|
|
124
|
+
let hasContent = hasRenderableContent(contentState);
|
|
125
|
+
if (!hasContent) {
|
|
126
|
+
await new Promise((r) => setTimeout(r, BLANK_RETRY_DELAY_MS));
|
|
127
|
+
contentState = await collectContentState(frame);
|
|
128
|
+
hasContent = hasRenderableContent(contentState);
|
|
129
|
+
}
|
|
118
130
|
|
|
119
131
|
if (!hasContent) {
|
|
120
132
|
pushIssue(
|
|
@@ -192,6 +204,7 @@ async function main() {
|
|
|
192
204
|
|
|
193
205
|
module.exports = {
|
|
194
206
|
runScenarioCheck,
|
|
207
|
+
main,
|
|
195
208
|
};
|
|
196
209
|
|
|
197
210
|
if (require.main === module) {
|
package/npm/utils.js
CHANGED
|
@@ -6,12 +6,22 @@ const fs = require("fs");
|
|
|
6
6
|
const rootDir = path.resolve(__dirname, "..");
|
|
7
7
|
const serverStatePath = path.join(rootDir, ".codeyam", "server-state.json");
|
|
8
8
|
|
|
9
|
+
/**
|
|
10
|
+
* Platform-correct executable filename for the codeyam-editor binary.
|
|
11
|
+
* Windows requires the `.exe` extension; on Unix the binary has no extension.
|
|
12
|
+
* Accepts an explicit platform so the helper is testable across platforms.
|
|
13
|
+
*/
|
|
14
|
+
function binaryName(platform = process.platform) {
|
|
15
|
+
return platform === "win32" ? "codeyam-editor.exe" : "codeyam-editor";
|
|
16
|
+
}
|
|
17
|
+
|
|
9
18
|
/** Find the compiled binary — check npm-installed location first, then dev builds. */
|
|
10
19
|
function findBinary() {
|
|
20
|
+
const name = binaryName();
|
|
11
21
|
const candidates = [
|
|
12
|
-
path.join(rootDir, "bin",
|
|
13
|
-
path.join(rootDir, "target", "debug",
|
|
14
|
-
path.join(rootDir, "target", "release",
|
|
22
|
+
path.join(rootDir, "bin", name), // npm install (postinstall)
|
|
23
|
+
path.join(rootDir, "target", "debug", name), // local dev (debug)
|
|
24
|
+
path.join(rootDir, "target", "release", name), // local dev (release)
|
|
15
25
|
];
|
|
16
26
|
|
|
17
27
|
for (const candidate of candidates) {
|
|
@@ -136,14 +146,38 @@ async function requestServerShutdown(port = 14199, timeoutMs = 1500) {
|
|
|
136
146
|
|
|
137
147
|
function tryKillPid(pid) {
|
|
138
148
|
try {
|
|
139
|
-
process.kill(pid, "SIGINT")
|
|
149
|
+
// On Windows, `process.kill(pid, "SIGINT")` either errors or no-ops
|
|
150
|
+
// depending on Node version. Drop the signal so Node maps to a
|
|
151
|
+
// platform TerminateProcess. Less graceful, but the child does exit.
|
|
152
|
+
if (process.platform === "win32") {
|
|
153
|
+
process.kill(pid);
|
|
154
|
+
} else {
|
|
155
|
+
process.kill(pid, "SIGINT");
|
|
156
|
+
}
|
|
140
157
|
return true;
|
|
141
158
|
} catch {
|
|
142
159
|
return false;
|
|
143
160
|
}
|
|
144
161
|
}
|
|
145
162
|
|
|
163
|
+
/**
|
|
164
|
+
* Send the platform-correct termination signal to a spawned child process.
|
|
165
|
+
* Used by editor.js / editor-dev.js / container.js when the user hits Ctrl+C —
|
|
166
|
+
* `child.kill("SIGINT")` is unreliable on Windows.
|
|
167
|
+
*/
|
|
168
|
+
function killChildProcess(child) {
|
|
169
|
+
if (!child) return;
|
|
170
|
+
if (process.platform === "win32") {
|
|
171
|
+
child.kill();
|
|
172
|
+
} else {
|
|
173
|
+
child.kill("SIGINT");
|
|
174
|
+
}
|
|
175
|
+
}
|
|
176
|
+
|
|
146
177
|
function findListeningPids(port) {
|
|
178
|
+
// Process discovery on Windows is best-effort — `lsof` is not installed and
|
|
179
|
+
// a `netstat -ano` parser is a follow-up. The API-shutdown path and the
|
|
180
|
+
// PID-file path together stop the server cleanly in normal operation.
|
|
147
181
|
if (process.platform === "win32") return [];
|
|
148
182
|
|
|
149
183
|
const result = spawnSync("lsof", [`-tiTCP:${port}`, "-sTCP:LISTEN"], {
|
|
@@ -229,6 +263,7 @@ async function ensureServer(port = 14199) {
|
|
|
229
263
|
module.exports = {
|
|
230
264
|
rootDir,
|
|
231
265
|
serverStatePath,
|
|
266
|
+
binaryName,
|
|
232
267
|
findBinary,
|
|
233
268
|
ensureBinary,
|
|
234
269
|
uiDistDir,
|
|
@@ -237,7 +272,12 @@ module.exports = {
|
|
|
237
272
|
waitForPort,
|
|
238
273
|
waitForPortToClose,
|
|
239
274
|
readServerState,
|
|
275
|
+
clearServerState,
|
|
276
|
+
requestServerShutdown,
|
|
277
|
+
tryKillPid,
|
|
278
|
+
findListeningPids,
|
|
240
279
|
stopServer,
|
|
241
280
|
restartServer,
|
|
242
281
|
ensureServer,
|
|
282
|
+
killChildProcess,
|
|
243
283
|
};
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@codeyam-editor/codeyam-editor",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.1",
|
|
4
4
|
"description": "Language-agnostic managed execution sandbox for scenario-driven development",
|
|
5
5
|
"bin": {
|
|
6
6
|
"codeyam-editor": "./npm/cli.js"
|
|
@@ -9,7 +9,8 @@
|
|
|
9
9
|
"postinstall": "node ./npm/postinstall.js"
|
|
10
10
|
},
|
|
11
11
|
"dependencies": {
|
|
12
|
-
"playwright": "^1.58.2"
|
|
12
|
+
"playwright": "^1.58.2",
|
|
13
|
+
"tar": "^7.5.13"
|
|
13
14
|
},
|
|
14
15
|
"keywords": [
|
|
15
16
|
"codeyam",
|