@codeyam-editor/codeyam-editor 0.1.0 → 0.1.2
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/cli.js +5 -2
- package/npm/cloud-branch-switch.js +442 -0
- package/npm/cloud.js +1521 -0
- package/npm/container.js +28 -5
- package/npm/editor-dev.js +378 -0
- package/npm/editor.js +199 -12
- package/npm/empty-project-dir.js +33 -0
- package/npm/fleet-auth.js +111 -0
- package/npm/fleet-cleanup.js +124 -0
- package/npm/fleet-env.js +125 -0
- package/npm/fleet-memory.js +127 -0
- package/npm/fleet-pool-policy.js +78 -0
- package/npm/fleet-pool.js +401 -0
- package/npm/fleet-queue.js +40 -0
- package/npm/fleet-reconcile.js +36 -0
- package/npm/fleet-reset.js +144 -0
- package/npm/fleet-roster.js +81 -0
- package/npm/launch-config.js +544 -0
- package/npm/preflight-guidance.js +68 -0
- package/npm/scenario-check.js +355 -290
- package/npm/scenario-handlers.js +82 -0
- package/npm/scenario-interactivity.js +199 -0
- package/npm/scenario-issues.js +35 -0
- package/npm/scenario-metrics.js +108 -0
- package/npm/scenario-mocks.js +107 -0
- package/npm/scenario-playwright.js +355 -0
- package/npm/utils.js +548 -22
- package/npm/vm-urls.js +117 -0
- package/package.json +13 -6
- package/ui/dist/assets/ContainerInspector-C8EkH4PP.js +2 -0
- package/ui/dist/assets/EditorShell-DWaeBxgj.js +186 -0
- package/ui/dist/assets/Markdown-BHBqz-Mj.js +1 -0
- package/ui/dist/assets/ScenarioDataPanel-DYP7pi_n.css +32 -0
- package/ui/dist/assets/ScenarioDataPanel-zJSXtXL4.js +5 -0
- package/ui/dist/assets/ScenarioDataPanelFullPage-C0YHQB-T.js +1 -0
- package/ui/dist/assets/ScenarioRenderer-CgDxuigm.js +1 -0
- package/ui/dist/assets/StepsSimulator-CRtmlyNZ.js +1 -0
- package/ui/dist/assets/index-BUYNFqKK.js +27 -0
- package/ui/dist/assets/index-DVr5rY2d.css +1 -0
- package/ui/dist/assets/markdown-v0F0UEt3.js +37 -0
- package/ui/dist/assets/react-CSS0HapR.js +28 -0
- package/ui/dist/assets/useEvents-DggWrtHe.js +1 -0
- package/ui/dist/assets/xterm--24IGk-x.js +9 -0
- package/ui/dist/favicon.ico +0 -0
- package/ui/dist/favicon.png +0 -0
- package/ui/dist/index.html +16 -0
- package/npm/postinstall.js +0 -95
package/npm/container.js
CHANGED
|
@@ -8,12 +8,31 @@ const path = require("path");
|
|
|
8
8
|
* inline monitoring UI at http://localhost:14199.
|
|
9
9
|
*/
|
|
10
10
|
|
|
11
|
-
const {
|
|
11
|
+
const {
|
|
12
|
+
ensureServer,
|
|
13
|
+
rootDir,
|
|
14
|
+
killChildProcess,
|
|
15
|
+
readMergedEditorConfig,
|
|
16
|
+
resolveLauncherPort,
|
|
17
|
+
} = require("./utils");
|
|
12
18
|
const { open } = require("./open");
|
|
13
19
|
|
|
14
|
-
|
|
15
|
-
|
|
20
|
+
/**
|
|
21
|
+
* npm wrapper entry point for the container binary: ensures the Rust
|
|
22
|
+
* editor server is running and opens the container monitoring UI in
|
|
23
|
+
* a browser.
|
|
24
|
+
*/
|
|
16
25
|
async function main() {
|
|
26
|
+
// Same resolution order as the editor launcher: env var
|
|
27
|
+
// (CODEYAM_EDITOR_PORT) → merged editor.json + editor.local.json
|
|
28
|
+
// (proxy.controlPort) → hard fallback (14199).
|
|
29
|
+
const merged = readMergedEditorConfig(rootDir);
|
|
30
|
+
const PORT = resolveLauncherPort(
|
|
31
|
+
process.env.CODEYAM_EDITOR_PORT,
|
|
32
|
+
merged.proxy && merged.proxy.controlPort,
|
|
33
|
+
14199,
|
|
34
|
+
);
|
|
35
|
+
|
|
17
36
|
const serverChild = await ensureServer(PORT);
|
|
18
37
|
const url = `http://localhost:${PORT}`;
|
|
19
38
|
console.error(`Container UI: ${url}`);
|
|
@@ -23,9 +42,13 @@ async function main() {
|
|
|
23
42
|
|
|
24
43
|
process.on("SIGINT", () => {
|
|
25
44
|
clearInterval(keepAlive);
|
|
26
|
-
|
|
45
|
+
killChildProcess(serverChild);
|
|
27
46
|
process.exit(0);
|
|
28
47
|
});
|
|
29
48
|
}
|
|
30
49
|
|
|
31
|
-
main
|
|
50
|
+
module.exports = { main };
|
|
51
|
+
|
|
52
|
+
if (require.main === module) {
|
|
53
|
+
main();
|
|
54
|
+
}
|
|
@@ -0,0 +1,378 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Development CLI for running the codeyam-editor against any project.
|
|
5
|
+
*
|
|
6
|
+
* Two modes:
|
|
7
|
+
*
|
|
8
|
+
* Launcher — no subcommand (or --restart): builds the Rust binary and UI
|
|
9
|
+
* from the editor repo, starts the server, waits for readiness, opens browser.
|
|
10
|
+
* codeyam-editor-dev
|
|
11
|
+
* codeyam-editor-dev --restart
|
|
12
|
+
*
|
|
13
|
+
* Passthrough — any subcommand (start, init, editor, ...): rebuilds the
|
|
14
|
+
* Rust binary, then forwards args to it.
|
|
15
|
+
* codeyam-editor-dev editor step 1
|
|
16
|
+
* codeyam-editor-dev editor advance
|
|
17
|
+
*
|
|
18
|
+
* The wrapper is symlinked twice by scripts/bootstrap.sh:
|
|
19
|
+
* codeyam-editor-dev → this file (preserves the -dev branding;
|
|
20
|
+
* scaffolded content emits "codeyam-editor-dev").
|
|
21
|
+
* codeyam-editor → this file (shadows the canonical name on PATH
|
|
22
|
+
* so hooks and skills that emit `codeyam-editor` still get the
|
|
23
|
+
* auto-rebuild behavior on local dev; scaffolded content emits the
|
|
24
|
+
* canonical "codeyam-editor").
|
|
25
|
+
* The invocation name is read from `process.argv[1]` to decide which
|
|
26
|
+
* branding the Rust binary should use via `CODEYAM_CLI`.
|
|
27
|
+
*/
|
|
28
|
+
|
|
29
|
+
const { execSync, spawn, spawnSync } = require("child_process");
|
|
30
|
+
const path = require("path");
|
|
31
|
+
const {
|
|
32
|
+
ensureServer,
|
|
33
|
+
restartServer,
|
|
34
|
+
waitForPort,
|
|
35
|
+
ensureBinary,
|
|
36
|
+
isSelfHostTarget,
|
|
37
|
+
rootDir,
|
|
38
|
+
killChildProcess,
|
|
39
|
+
readMergedEditorConfig,
|
|
40
|
+
resolveLauncherPort,
|
|
41
|
+
staleBinaryBanner,
|
|
42
|
+
enforceFreshBinary,
|
|
43
|
+
} = require("./utils");
|
|
44
|
+
const { open } = require("./open");
|
|
45
|
+
const { classifyProject } = require("./preflight-guidance");
|
|
46
|
+
const { isEmptyProjectDir } = require("./empty-project-dir");
|
|
47
|
+
|
|
48
|
+
// When invoked through the `codeyam-editor-dev` symlink, tell the Rust
|
|
49
|
+
// binary to emit "codeyam-editor-dev" in all scaffolded commands,
|
|
50
|
+
// permissions, and installed files. When invoked through the
|
|
51
|
+
// `codeyam-editor` symlink, leave CODEYAM_CLI unset so the binary
|
|
52
|
+
// emits the canonical "codeyam-editor" — matching the cloud install
|
|
53
|
+
// path and the new hook/skill convention. The runtime cost of the
|
|
54
|
+
// rebuild-on-passthrough behavior is identical for both names.
|
|
55
|
+
const invokedAs = path.basename(process.argv[1] || "");
|
|
56
|
+
if (invokedAs === "codeyam-editor-dev") {
|
|
57
|
+
process.env.CODEYAM_CLI = "codeyam-editor-dev";
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
const args = process.argv.slice(2);
|
|
61
|
+
const KNOWN_SUBCOMMANDS = ["start", "init", "editor"];
|
|
62
|
+
|
|
63
|
+
/**
|
|
64
|
+
* Check if the first non-flag argument is a known subcommand.
|
|
65
|
+
* Accepts an explicit argv array so the helper is testable without mutating
|
|
66
|
+
* process.argv (the module-level `args` const is the production default).
|
|
67
|
+
*/
|
|
68
|
+
function hasSubcommand(argv = args) {
|
|
69
|
+
for (const arg of argv) {
|
|
70
|
+
if (arg.startsWith("-")) continue;
|
|
71
|
+
return KNOWN_SUBCOMMANDS.includes(arg);
|
|
72
|
+
}
|
|
73
|
+
return false;
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
/**
|
|
77
|
+
* Build the Rust binary from the editor repo. Returns true on success,
|
|
78
|
+
* false if `cargo build` failed. This function only reports the outcome —
|
|
79
|
+
* the caller decides whether a failed build is fatal (see staleBinaryBanner).
|
|
80
|
+
*/
|
|
81
|
+
function buildBinary() {
|
|
82
|
+
console.error("Building Rust binary...");
|
|
83
|
+
try {
|
|
84
|
+
execSync("cargo build", { stdio: "inherit", cwd: rootDir });
|
|
85
|
+
return true;
|
|
86
|
+
} catch {
|
|
87
|
+
return false;
|
|
88
|
+
}
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
/**
|
|
92
|
+
* Decide whether the dev wrapper should rebuild the Rust binary (and the UI,
|
|
93
|
+
* for the launcher path) before forwarding to it. Pure: takes injected cwd /
|
|
94
|
+
* repoRoot / env so the four-way decision is testable without spawning cargo
|
|
95
|
+
* or reading process.env.
|
|
96
|
+
*
|
|
97
|
+
* The wrapper rebuilds in two cases:
|
|
98
|
+
* - The target IS the editor repo itself (developing the editor on its
|
|
99
|
+
* own checkout). This is the canonical self-host case.
|
|
100
|
+
* - `CODEYAM_DEV_REBUILD=1` is explicitly set (the documented opt-in for
|
|
101
|
+
* "editing the editor source while running it against an external client
|
|
102
|
+
* project", named in editor-dev.js's earlier history).
|
|
103
|
+
*
|
|
104
|
+
* Otherwise — the common case where a developer uses the editor as a tool
|
|
105
|
+
* against their own app — the wrapper skips the rebuild entirely. The
|
|
106
|
+
* existing binary runs unchanged, and the server-side stale-binary watcher
|
|
107
|
+
* (also gated on self-host) never SIGUSR1's. The combined effect is what the
|
|
108
|
+
* plan calls out: an external-project session is never disrupted by a bare
|
|
109
|
+
* commit, a fleet rebase, or any other event that would have relinked the
|
|
110
|
+
* binary without changing what runs.
|
|
111
|
+
*/
|
|
112
|
+
function shouldRebuildOnPassthrough({ cwd, repoRoot, env }) {
|
|
113
|
+
if (env.CODEYAM_DEV_REBUILD === "1") return true;
|
|
114
|
+
return isSelfHostTarget(cwd, repoRoot);
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
/**
|
|
118
|
+
* Returns true when the merged project config has a non-empty start
|
|
119
|
+
* command — either at `apps[0].startCommand` (per-app, preferred) or at
|
|
120
|
+
* the top-level `startCommand` (legacy default). Mirrors the Rust
|
|
121
|
+
* `resolve_start_command` precedence in
|
|
122
|
+
* `crates/codeyam-editor/src/commands/start.rs`.
|
|
123
|
+
*
|
|
124
|
+
* When neither slot has a non-empty string, the editor server runs in
|
|
125
|
+
* "editor UI only" mode and the launcher must skip the dev-server port
|
|
126
|
+
* wait — otherwise the user stares at "Waiting for dev server on port
|
|
127
|
+
* N..." for the full 15s timeout.
|
|
128
|
+
*/
|
|
129
|
+
function hasStartCommand(mergedConfig) {
|
|
130
|
+
if (mergedConfig == null || typeof mergedConfig !== "object") return false;
|
|
131
|
+
const apps = Array.isArray(mergedConfig.apps) ? mergedConfig.apps : null;
|
|
132
|
+
if (apps && apps.length > 0) {
|
|
133
|
+
const cmd = apps[0] && apps[0].startCommand;
|
|
134
|
+
if (typeof cmd === "string" && cmd.trim() !== "") return true;
|
|
135
|
+
}
|
|
136
|
+
const topLevel = mergedConfig.startCommand;
|
|
137
|
+
return typeof topLevel === "string" && topLevel.trim() !== "";
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
/**
|
|
141
|
+
* Decide what the launcher should do given a `classifyProject` result.
|
|
142
|
+
* Pure — no fs, no spawn, no process.exit — so `launcherMain`'s branching
|
|
143
|
+
* is unit-testable without building a binary or spawning a server (the same
|
|
144
|
+
* policy/classification split as `classifyPortHolder` in utils.js).
|
|
145
|
+
*
|
|
146
|
+
* modern → { action: "proceed" } launch as-is
|
|
147
|
+
* none → { action: "init" } greenfield: init, then launch
|
|
148
|
+
* legacy → { action: "bail", guidance, code: 2 } print guidance and exit
|
|
149
|
+
*
|
|
150
|
+
* Greenfield auto-inits because the dev launcher is a one-shot "build
|
|
151
|
+
* everything, run the editor against this project" tool — running it in a
|
|
152
|
+
* fresh dir should just work. Legacy bails instead: `cmd_init`'s legacy
|
|
153
|
+
* branch deliberately skips `editor.json` creation so `/codeyam-onboard`
|
|
154
|
+
* can migrate from the preserved classification, so auto-running init would
|
|
155
|
+
* strand them. The Rust-side `start_preflight` stays the safety net for
|
|
156
|
+
* direct `codeyam-editor start` calls; this launcher composes init + start.
|
|
157
|
+
*/
|
|
158
|
+
function planPreflightAction(classification) {
|
|
159
|
+
if (classification.kind === "legacy") {
|
|
160
|
+
return { action: "bail", guidance: classification.guidance, code: 2 };
|
|
161
|
+
}
|
|
162
|
+
if (classification.kind === "none") {
|
|
163
|
+
return { action: "init" };
|
|
164
|
+
}
|
|
165
|
+
return { action: "proceed" };
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
/**
|
|
169
|
+
* Pick the launcher's post-launch tail banner — printed right next to the
|
|
170
|
+
* `Editor UI: http://…` line where the user is actually looking, after the
|
|
171
|
+
* editor server is up. Pure (no fs, no spawn) so the three-arm decision is
|
|
172
|
+
* unit-testable without spawning a binary, mirroring `planPreflightAction`.
|
|
173
|
+
*
|
|
174
|
+
* Skill assumption: /codeyam-editor scaffolds a project from scratch;
|
|
175
|
+
* /codeyam-onboard migrates an existing source tree. Empty greenfield
|
|
176
|
+
* (no non-hidden entries before init) must route to the scaffold path;
|
|
177
|
+
* existing greenfield routes to the migration path. Mirrors the same
|
|
178
|
+
* split in `cmd_init` (init.rs:10-24) — keep both in sync. Do NOT
|
|
179
|
+
* "simplify" by collapsing the branches; the skill each path names is the
|
|
180
|
+
* whole point.
|
|
181
|
+
*
|
|
182
|
+
* action !== "init" → null (modern: proceed, no banner;
|
|
183
|
+
* legacy already bailed earlier)
|
|
184
|
+
* action "init", wasEmpty → /codeyam-editor scaffold-from-scratch
|
|
185
|
+
* action "init", !wasEmpty → /codeyam-onboard migrate-existing
|
|
186
|
+
*/
|
|
187
|
+
function postLaunchInitBanner({ preflightAction, wasEmpty }) {
|
|
188
|
+
if (preflightAction !== "init") return null;
|
|
189
|
+
if (wasEmpty) {
|
|
190
|
+
return (
|
|
191
|
+
"\nInitialized .codeyam/ in this directory. Open Claude Code here and " +
|
|
192
|
+
"run `/codeyam-editor` to scaffold a new project from scratch."
|
|
193
|
+
);
|
|
194
|
+
}
|
|
195
|
+
return (
|
|
196
|
+
"\nInitialized .codeyam/ in this directory. To populate scenarios, " +
|
|
197
|
+
"open Claude Code here and run `/codeyam-onboard`."
|
|
198
|
+
);
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
module.exports = {
|
|
202
|
+
hasSubcommand,
|
|
203
|
+
buildBinary,
|
|
204
|
+
shouldRebuildOnPassthrough,
|
|
205
|
+
staleBinaryBanner,
|
|
206
|
+
hasStartCommand,
|
|
207
|
+
planPreflightAction,
|
|
208
|
+
postLaunchInitBanner,
|
|
209
|
+
};
|
|
210
|
+
|
|
211
|
+
if (require.main === module) {
|
|
212
|
+
// --- Passthrough mode: build binary, then forward to it ---
|
|
213
|
+
if (hasSubcommand()) {
|
|
214
|
+
// Only rebuild when the target IS the editor repo itself, or when the
|
|
215
|
+
// developer has explicitly opted into the "edit editor against external
|
|
216
|
+
// project" workflow via CODEYAM_DEV_REBUILD=1. On any other project the
|
|
217
|
+
// editor binary is a fixed tool and a `cargo build` here ripples into a
|
|
218
|
+
// stale-binary SIGUSR1 re-exec that resets the user's session for no
|
|
219
|
+
// reason. `ensureBinary()` still builds once if no binary exists at all.
|
|
220
|
+
if (
|
|
221
|
+
shouldRebuildOnPassthrough({
|
|
222
|
+
cwd: process.cwd(),
|
|
223
|
+
repoRoot: rootDir,
|
|
224
|
+
env: process.env,
|
|
225
|
+
})
|
|
226
|
+
) {
|
|
227
|
+
enforceFreshBinary(buildBinary(), { action: "run editor commands" });
|
|
228
|
+
}
|
|
229
|
+
const binary = ensureBinary();
|
|
230
|
+
const child = spawn(binary, args, {
|
|
231
|
+
stdio: "inherit",
|
|
232
|
+
cwd: process.cwd(),
|
|
233
|
+
env: process.env,
|
|
234
|
+
});
|
|
235
|
+
child.on("exit", (code) => {
|
|
236
|
+
process.exit(code ?? 1);
|
|
237
|
+
});
|
|
238
|
+
} else {
|
|
239
|
+
// --- Launcher mode: build everything, start server, open browser ---
|
|
240
|
+
const shouldRestart = args.includes("--restart");
|
|
241
|
+
|
|
242
|
+
const launcherMain = async () => {
|
|
243
|
+
const projectDir = process.cwd();
|
|
244
|
+
console.error(`Editor repo: ${rootDir}`);
|
|
245
|
+
console.error(`Target project: ${projectDir}`);
|
|
246
|
+
|
|
247
|
+
// Greenfield projects are auto-initialized here so the dev launcher
|
|
248
|
+
// stays a one-shot "build everything, run the editor against this
|
|
249
|
+
// project" tool. Legacy projects bail with guidance; modern projects
|
|
250
|
+
// skip init. See `planPreflightAction` for the rationale on each
|
|
251
|
+
// branch. Subcommand pass-through (handled above) is unaffected — the
|
|
252
|
+
// binary classifies its own subcommands.
|
|
253
|
+
const preflight = planPreflightAction(classifyProject(projectDir));
|
|
254
|
+
// Capture emptiness BEFORE init runs — the spawn below creates
|
|
255
|
+
// .codeyam/ (and install_editor_files writes .claude/ etc.), which
|
|
256
|
+
// would make every project look non-empty by the time the
|
|
257
|
+
// post-launch banner prints. Same constraint as the Rust comment in
|
|
258
|
+
// `init_greenfield_branch` (init.rs:82-85). Computed as a sibling
|
|
259
|
+
// local (not folded into `preflight`) because emptiness is
|
|
260
|
+
// orthogonal to the preflight kind.
|
|
261
|
+
const wasEmpty = isEmptyProjectDir(projectDir);
|
|
262
|
+
if (preflight.action === "bail") {
|
|
263
|
+
console.error(preflight.guidance);
|
|
264
|
+
process.exit(preflight.code);
|
|
265
|
+
}
|
|
266
|
+
if (preflight.action === "init") {
|
|
267
|
+
// Run init from projectDir before the config reads below so the
|
|
268
|
+
// freshly-written .codeyam/editor.json feeds port resolution.
|
|
269
|
+
// ensureBinary builds the binary first if this is a fresh checkout.
|
|
270
|
+
console.error(`Initializing codeyam-editor in ${projectDir}...`);
|
|
271
|
+
const initBinary = ensureBinary();
|
|
272
|
+
const initResult = spawnSync(initBinary, ["init"], {
|
|
273
|
+
cwd: projectDir,
|
|
274
|
+
stdio: "inherit",
|
|
275
|
+
env: process.env,
|
|
276
|
+
});
|
|
277
|
+
if (initResult.status !== 0) {
|
|
278
|
+
console.error(
|
|
279
|
+
`codeyam-editor init exited ${initResult.status ?? "by signal"}; aborting launch.`,
|
|
280
|
+
);
|
|
281
|
+
process.exit(1);
|
|
282
|
+
}
|
|
283
|
+
}
|
|
284
|
+
|
|
285
|
+
// Editor server's control port is owned by the editor repo's
|
|
286
|
+
// merged config (rootDir), not the target project's. The target
|
|
287
|
+
// project's own `editor.local.json` only contributes to the
|
|
288
|
+
// app/dev-server port below.
|
|
289
|
+
const editorMerged = readMergedEditorConfig(rootDir);
|
|
290
|
+
const SERVER_PORT = resolveLauncherPort(
|
|
291
|
+
process.env.CODEYAM_EDITOR_PORT,
|
|
292
|
+
editorMerged.proxy && editorMerged.proxy.controlPort,
|
|
293
|
+
14199,
|
|
294
|
+
);
|
|
295
|
+
|
|
296
|
+
// Target project's dev-server port comes from its own
|
|
297
|
+
// editor.json + editor.local.json (developers iterating on the
|
|
298
|
+
// editor against a real client project rely on the override
|
|
299
|
+
// file here too).
|
|
300
|
+
const projectMerged = readMergedEditorConfig(projectDir);
|
|
301
|
+
const appPort = resolveLauncherPort(undefined, projectMerged.port, 3000);
|
|
302
|
+
|
|
303
|
+
// Same gate as the passthrough path: only rebuild the Rust binary +
|
|
304
|
+
// UI when the launcher target IS the editor repo itself, or when
|
|
305
|
+
// CODEYAM_DEV_REBUILD=1 is explicitly set. Launching the editor
|
|
306
|
+
// against an external project must not relink the binary — the
|
|
307
|
+
// server-side stale-binary watcher would otherwise SIGUSR1-restart
|
|
308
|
+
// the freshly-launched server immediately. `ensureBinary()` still
|
|
309
|
+
// builds once if no binary exists.
|
|
310
|
+
const shouldRebuildLaunch = shouldRebuildOnPassthrough({
|
|
311
|
+
cwd: projectDir,
|
|
312
|
+
repoRoot: rootDir,
|
|
313
|
+
env: process.env,
|
|
314
|
+
});
|
|
315
|
+
if (shouldRebuildLaunch) {
|
|
316
|
+
enforceFreshBinary(buildBinary(), {
|
|
317
|
+
action: "run editor commands",
|
|
318
|
+
continueVerb: "launching with",
|
|
319
|
+
});
|
|
320
|
+
|
|
321
|
+
// Build the UI from the editor repo
|
|
322
|
+
console.error("Building UI...");
|
|
323
|
+
try {
|
|
324
|
+
execSync("npx vite build", { stdio: "inherit", cwd: path.join(rootDir, "ui") });
|
|
325
|
+
} catch {
|
|
326
|
+
console.error("Failed to build UI. Continuing with existing build (if any)...");
|
|
327
|
+
}
|
|
328
|
+
}
|
|
329
|
+
|
|
330
|
+
const serverChild = shouldRestart
|
|
331
|
+
? await restartServer(SERVER_PORT)
|
|
332
|
+
: await ensureServer(SERVER_PORT);
|
|
333
|
+
|
|
334
|
+
// Re-read the (possibly just-initialized) project config to decide
|
|
335
|
+
// whether to wait on a dev server at all. On a project where init
|
|
336
|
+
// couldn't detect a framework (or a user-configured "editor UI
|
|
337
|
+
// only" setup), `startCommand` is empty — the editor server itself
|
|
338
|
+
// prints "No startCommand configured. Running editor UI only." and
|
|
339
|
+
// never spawns a dev server, so waiting 15s for `appPort + 1` is a
|
|
340
|
+
// pure UX hang. The launcher silently skips the wait; the server's
|
|
341
|
+
// own log line is the canonical signal to the user.
|
|
342
|
+
const postInitMerged = readMergedEditorConfig(projectDir);
|
|
343
|
+
const hasCommand = hasStartCommand(postInitMerged);
|
|
344
|
+
if (hasCommand) {
|
|
345
|
+
const viteInternalPort = appPort + 1;
|
|
346
|
+
console.error(`Waiting for dev server on port ${viteInternalPort}...`);
|
|
347
|
+
const viteReady = await waitForPort(viteInternalPort, 15000, 300);
|
|
348
|
+
if (!viteReady) {
|
|
349
|
+
console.error(`Warning: Dev server not ready on port ${viteInternalPort}. Live Preview may not work initially.`);
|
|
350
|
+
}
|
|
351
|
+
}
|
|
352
|
+
|
|
353
|
+
const url = `http://localhost:${SERVER_PORT}`;
|
|
354
|
+
console.error(`Editor UI: ${url}`);
|
|
355
|
+
if (hasCommand) {
|
|
356
|
+
console.error(`Live Preview: http://localhost:${appPort}`);
|
|
357
|
+
}
|
|
358
|
+
console.error(`Project: ${projectDir}`);
|
|
359
|
+
open(url);
|
|
360
|
+
const banner = postLaunchInitBanner({
|
|
361
|
+
preflightAction: preflight.action,
|
|
362
|
+
wasEmpty,
|
|
363
|
+
});
|
|
364
|
+
if (banner) {
|
|
365
|
+
console.error(banner);
|
|
366
|
+
}
|
|
367
|
+
const keepAlive = setInterval(() => {}, 60_000);
|
|
368
|
+
|
|
369
|
+
process.on("SIGINT", () => {
|
|
370
|
+
clearInterval(keepAlive);
|
|
371
|
+
killChildProcess(serverChild);
|
|
372
|
+
process.exit(0);
|
|
373
|
+
});
|
|
374
|
+
};
|
|
375
|
+
|
|
376
|
+
launcherMain();
|
|
377
|
+
}
|
|
378
|
+
}
|
package/npm/editor.js
CHANGED
|
@@ -13,18 +13,170 @@
|
|
|
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 {
|
|
19
|
+
const {
|
|
20
|
+
ensureServer,
|
|
21
|
+
stopServer,
|
|
22
|
+
waitForPort,
|
|
23
|
+
rootDir,
|
|
24
|
+
killChildProcess,
|
|
25
|
+
readMergedEditorConfig,
|
|
26
|
+
resolveLauncherPort,
|
|
27
|
+
resolveWrappedAppPort,
|
|
28
|
+
classifyPortHolder,
|
|
29
|
+
formatForeignListenerError,
|
|
30
|
+
formatCrossProjectError,
|
|
31
|
+
staleBinaryBanner,
|
|
32
|
+
enforceFreshBinary,
|
|
33
|
+
} = require("./utils");
|
|
18
34
|
const { open } = require("./open");
|
|
19
35
|
|
|
20
|
-
const SERVER_PORT = 14199;
|
|
21
|
-
const VITE_PORT = 5173;
|
|
22
|
-
// The reverse proxy sits on VITE_PORT; the actual Vite dev server runs on
|
|
23
|
-
// VITE_PORT + 1. We need to wait for the real Vite server, not the proxy.
|
|
24
|
-
const VITE_INTERNAL_PORT = VITE_PORT + 1;
|
|
25
36
|
const shouldRestart = process.argv.includes("--restart");
|
|
26
37
|
|
|
38
|
+
function buildBinary() {
|
|
39
|
+
console.error("Building Rust binary...");
|
|
40
|
+
try {
|
|
41
|
+
execSync("cargo build", { stdio: "inherit", cwd: rootDir });
|
|
42
|
+
return true;
|
|
43
|
+
} catch {
|
|
44
|
+
return false;
|
|
45
|
+
}
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
// Resolve <CARGO_HOME>/bin (falling back to $HOME/.cargo/bin). Mirrors
|
|
49
|
+
// `installed_binary_path()` in crates/control-api/src/pty_broker_startup.rs
|
|
50
|
+
// — the Rust broker auto-spawn execs the binary at this path, so we MUST
|
|
51
|
+
// keep it in sync with the freshly-built debug binary or `npm run editor`
|
|
52
|
+
// will start a server whose PTY-broker auto-spawn execs a stale install.
|
|
53
|
+
function cargoBinDir() {
|
|
54
|
+
if (process.env.CARGO_HOME) return path.join(process.env.CARGO_HOME, "bin");
|
|
55
|
+
return path.join(os.homedir(), ".cargo", "bin");
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
// Place a hardlink at `dstPath` pointing at the same inode as `srcPath`,
|
|
59
|
+
// replacing any existing file at `dstPath`. On EXDEV (cross-filesystem,
|
|
60
|
+
// rare; target/ on a separate mount from the cargo bin dir) falls back
|
|
61
|
+
// to an atomic copy-and-rename via `tmpPath` so we still succeed — at
|
|
62
|
+
// the cost of distinct inodes, which means the dev binary's
|
|
63
|
+
// `stale_cargo_bin_message` check will fire on every boot.
|
|
64
|
+
//
|
|
65
|
+
// Hardlink-by-default is what keeps the stale-binary advisory silent on
|
|
66
|
+
// `npm run editor`: the freshly-installed binary at <cargo_bin> shares
|
|
67
|
+
// an inode with target/debug/codeyam-editor, so the inode-identity
|
|
68
|
+
// check returns "same underlying file" and nothing is printed.
|
|
69
|
+
function linkOrCopyBinary(srcPath, dstPath, tmpPath) {
|
|
70
|
+
try {
|
|
71
|
+
fs.unlinkSync(dstPath);
|
|
72
|
+
} catch (e) {
|
|
73
|
+
if (e.code !== "ENOENT") throw e;
|
|
74
|
+
}
|
|
75
|
+
try {
|
|
76
|
+
fs.linkSync(srcPath, dstPath);
|
|
77
|
+
} catch (err) {
|
|
78
|
+
if (err.code !== "EXDEV") throw err;
|
|
79
|
+
fs.copyFileSync(srcPath, tmpPath);
|
|
80
|
+
fs.chmodSync(tmpPath, 0o755);
|
|
81
|
+
fs.renameSync(tmpPath, dstPath);
|
|
82
|
+
}
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
// Install target/debug/codeyam-editor to <cargo_bin>/codeyam-editor
|
|
86
|
+
// atomically (write to tmp in same dir, then rename), and refresh the
|
|
87
|
+
// codeyam-editor-pty-broker hardlink so it points at the new inode.
|
|
88
|
+
//
|
|
89
|
+
// The PTY broker auto-spawn (control-api/src/pty_broker_startup.rs) always
|
|
90
|
+
// resolves the *installed* path, NOT current_exe() — that was a deliberate
|
|
91
|
+
// fix to prevent a fork-bomb where test binaries spawned themselves. So a
|
|
92
|
+
// fresh `target/debug/codeyam-editor` is not enough; the install path must
|
|
93
|
+
// be refreshed too. Without this, build tabs hang on "Reconnecting..."
|
|
94
|
+
// because the broker daemon errors out (`unrecognized subcommand`) before
|
|
95
|
+
// the UDS socket opens.
|
|
96
|
+
function installBinary() {
|
|
97
|
+
const srcPath = path.join(rootDir, "target", "debug", "codeyam-editor");
|
|
98
|
+
if (!fs.existsSync(srcPath)) {
|
|
99
|
+
console.error(`Skipping install: ${srcPath} not found.`);
|
|
100
|
+
return;
|
|
101
|
+
}
|
|
102
|
+
const binDir = cargoBinDir();
|
|
103
|
+
const dstPath = path.join(binDir, "codeyam-editor");
|
|
104
|
+
const linkPath = path.join(binDir, "codeyam-editor-pty-broker");
|
|
105
|
+
const tmpPath = path.join(binDir, `.codeyam-editor.tmp.${process.pid}`);
|
|
106
|
+
console.error(`Installing fresh binary to ${dstPath}...`);
|
|
107
|
+
try {
|
|
108
|
+
fs.mkdirSync(binDir, { recursive: true });
|
|
109
|
+
linkOrCopyBinary(srcPath, dstPath, tmpPath);
|
|
110
|
+
try {
|
|
111
|
+
fs.unlinkSync(linkPath);
|
|
112
|
+
} catch (e) {
|
|
113
|
+
if (e.code !== "ENOENT") throw e;
|
|
114
|
+
}
|
|
115
|
+
fs.linkSync(dstPath, linkPath);
|
|
116
|
+
} catch (err) {
|
|
117
|
+
console.error(`Failed to install binary: ${err.message}`);
|
|
118
|
+
try { fs.unlinkSync(tmpPath); } catch {}
|
|
119
|
+
}
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
// Evict Rust build artifacts not touched in the last day so target/ doesn't
|
|
123
|
+
// grow unboundedly. Must run after the old server is stopped (it holds files
|
|
124
|
+
// in target/debug/ open) and before the new server starts. Non-fatal.
|
|
125
|
+
function sweepStaleArtifacts() {
|
|
126
|
+
console.error("Sweeping stale Rust build artifacts (idle >1 day)...");
|
|
127
|
+
try {
|
|
128
|
+
execSync("cargo sweep --time 1", { stdio: "inherit", cwd: rootDir });
|
|
129
|
+
} catch {
|
|
130
|
+
console.error("cargo-sweep failed (install with `cargo install cargo-sweep`). Continuing.");
|
|
131
|
+
}
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
/**
|
|
135
|
+
* npm wrapper entry point for the editor binary: resolves ports from
|
|
136
|
+
* the merged editor config, ensures the Rust backend is running, and
|
|
137
|
+
* opens the editor UI.
|
|
138
|
+
*/
|
|
27
139
|
async function main() {
|
|
140
|
+
// Read ports from the merged editor config (editor.json + per-developer
|
|
141
|
+
// editor.local.json). The Rust backend reads the same merged config on
|
|
142
|
+
// its own — we resolve here so the launcher's log lines, `waitForPort`
|
|
143
|
+
// calls, and `--port` arg agree with what the binary will actually
|
|
144
|
+
// bind. Env vars still win for scripted/CI invocations
|
|
145
|
+
// (CODEYAM_EDITOR_PORT, PORT), then the merged config, then a hard
|
|
146
|
+
// fallback for fresh checkouts with no override file.
|
|
147
|
+
const merged = readMergedEditorConfig(rootDir);
|
|
148
|
+
const SERVER_PORT = resolveLauncherPort(
|
|
149
|
+
process.env.CODEYAM_EDITOR_PORT,
|
|
150
|
+
merged.proxy && merged.proxy.controlPort,
|
|
151
|
+
14199,
|
|
152
|
+
);
|
|
153
|
+
// `resolveWrappedAppPort` mirrors `resolve_app_port` in
|
|
154
|
+
// `crates/codeyam-editor/src/commands/start.rs`: `env > apps[0].port >
|
|
155
|
+
// config.port`. Without this the launcher's log line and
|
|
156
|
+
// `waitForPort` probe targeted one port while the backend bound
|
|
157
|
+
// another, producing the misleading "Vite dev server not ready on
|
|
158
|
+
// port N" warning whenever apps[0] declared its own port.
|
|
159
|
+
const VITE_PORT = resolveWrappedAppPort(merged, process.env.PORT, 5173);
|
|
160
|
+
// The reverse proxy sits on VITE_PORT; the actual Vite dev server runs on
|
|
161
|
+
// VITE_PORT + 1. We need to wait for the real Vite server, not the proxy.
|
|
162
|
+
const VITE_INTERNAL_PORT = VITE_PORT + 1;
|
|
163
|
+
|
|
164
|
+
// Rebuild the Rust binary so code changes are picked up on restart.
|
|
165
|
+
// This script is only invoked from a source checkout (via `npm run editor`),
|
|
166
|
+
// so Cargo.toml and the crates tree are always present.
|
|
167
|
+
const buildOk = buildBinary();
|
|
168
|
+
// A failed rebuild means the server would boot on a stale binary that no
|
|
169
|
+
// longer matches the source. Refuse by default; CODEYAM_ALLOW_STALE=1
|
|
170
|
+
// forces the old behavior (continue with the last-good install).
|
|
171
|
+
enforceFreshBinary(buildOk, { action: "start the editor server" });
|
|
172
|
+
// Refresh <cargo_bin>/codeyam-editor (and the pty-broker hardlink) so
|
|
173
|
+
// the broker auto-spawn execs a binary that knows the `pty-broker
|
|
174
|
+
// daemon` subcommand. Skip if the build failed — installing a stale
|
|
175
|
+
// debug binary over a working install is worse than leaving it.
|
|
176
|
+
if (buildOk) {
|
|
177
|
+
installBinary();
|
|
178
|
+
}
|
|
179
|
+
|
|
28
180
|
// Build the UI so the backend can serve it as static files
|
|
29
181
|
console.error("Building UI...");
|
|
30
182
|
try {
|
|
@@ -37,10 +189,33 @@ async function main() {
|
|
|
37
189
|
}
|
|
38
190
|
|
|
39
191
|
// Start the Rust backend — it serves ui/dist/ and launches the Vite dev
|
|
40
|
-
// server (configured via startCommand in editor.json) for Live Preview HMR
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
192
|
+
// server (configured via startCommand in editor.json) for Live Preview HMR.
|
|
193
|
+
// On --restart: identity-check first, then stop the existing server,
|
|
194
|
+
// sweep stale artifacts while target/ is idle, then start fresh.
|
|
195
|
+
//
|
|
196
|
+
// Stopping someone else's editor server on `--restart` is the worst
|
|
197
|
+
// possible outcome of a port collision — only stop the listener if
|
|
198
|
+
// the identity probe confirms it's this project's server.
|
|
199
|
+
if (shouldRestart) {
|
|
200
|
+
const holder = await classifyPortHolder(SERVER_PORT);
|
|
201
|
+
if (holder.kind === "foreign") {
|
|
202
|
+
console.error(formatForeignListenerError(SERVER_PORT));
|
|
203
|
+
process.exit(1);
|
|
204
|
+
}
|
|
205
|
+
if (holder.kind === "cross-project") {
|
|
206
|
+
console.error(formatCrossProjectError(SERVER_PORT, holder, "restart"));
|
|
207
|
+
process.exit(1);
|
|
208
|
+
}
|
|
209
|
+
if (holder.kind === "same-project") {
|
|
210
|
+
console.error(`Stopping existing server on port ${SERVER_PORT}...`);
|
|
211
|
+
await stopServer(SERVER_PORT);
|
|
212
|
+
}
|
|
213
|
+
// holder.kind === "free": nothing to stop, fall through to ensureServer.
|
|
214
|
+
}
|
|
215
|
+
if (shouldRestart) {
|
|
216
|
+
sweepStaleArtifacts();
|
|
217
|
+
}
|
|
218
|
+
const serverChild = await ensureServer(SERVER_PORT);
|
|
44
219
|
|
|
45
220
|
// Wait for the actual Vite dev server on the internal port (not the reverse
|
|
46
221
|
// proxy on VITE_PORT, which starts immediately with the Rust binary).
|
|
@@ -59,9 +234,21 @@ async function main() {
|
|
|
59
234
|
|
|
60
235
|
process.on("SIGINT", () => {
|
|
61
236
|
clearInterval(keepAlive);
|
|
62
|
-
|
|
237
|
+
killChildProcess(serverChild);
|
|
63
238
|
process.exit(0);
|
|
64
239
|
});
|
|
65
240
|
}
|
|
66
241
|
|
|
67
|
-
|
|
242
|
+
module.exports = {
|
|
243
|
+
buildBinary,
|
|
244
|
+
staleBinaryBanner,
|
|
245
|
+
cargoBinDir,
|
|
246
|
+
installBinary,
|
|
247
|
+
linkOrCopyBinary,
|
|
248
|
+
main,
|
|
249
|
+
sweepStaleArtifacts,
|
|
250
|
+
};
|
|
251
|
+
|
|
252
|
+
if (require.main === module) {
|
|
253
|
+
main();
|
|
254
|
+
}
|