@codeyam-editor/codeyam-editor 0.1.1 → 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 +22 -3
- package/npm/editor-dev.js +286 -36
- package/npm/editor.js +98 -35
- 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 +298 -14
- package/npm/scenario-handlers.js +16 -5
- package/npm/scenario-interactivity.js +199 -0
- package/npm/scenario-metrics.js +16 -2
- package/npm/scenario-mocks.js +62 -25
- package/npm/scenario-playwright.js +169 -6
- package/npm/utils.js +507 -21
- package/npm/vm-urls.js +117 -0
- package/package.json +8 -7
- 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 -132
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}`);
|
package/npm/editor-dev.js
CHANGED
|
@@ -14,17 +14,48 @@
|
|
|
14
14
|
* Rust binary, then forwards args to it.
|
|
15
15
|
* codeyam-editor-dev editor step 1
|
|
16
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`.
|
|
17
27
|
*/
|
|
18
28
|
|
|
19
|
-
const { execSync, spawn } = require("child_process");
|
|
29
|
+
const { execSync, spawn, spawnSync } = require("child_process");
|
|
20
30
|
const path = require("path");
|
|
21
|
-
const
|
|
22
|
-
|
|
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");
|
|
23
44
|
const { open } = require("./open");
|
|
45
|
+
const { classifyProject } = require("./preflight-guidance");
|
|
46
|
+
const { isEmptyProjectDir } = require("./empty-project-dir");
|
|
24
47
|
|
|
25
|
-
//
|
|
26
|
-
//
|
|
27
|
-
|
|
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
|
+
}
|
|
28
59
|
|
|
29
60
|
const args = process.argv.slice(2);
|
|
30
61
|
const KNOWN_SUBCOMMANDS = ["start", "init", "editor"];
|
|
@@ -42,78 +73,297 @@ function hasSubcommand(argv = args) {
|
|
|
42
73
|
return false;
|
|
43
74
|
}
|
|
44
75
|
|
|
45
|
-
/**
|
|
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
|
+
*/
|
|
46
81
|
function buildBinary() {
|
|
47
82
|
console.error("Building Rust binary...");
|
|
48
83
|
try {
|
|
49
84
|
execSync("cargo build", { stdio: "inherit", cwd: rootDir });
|
|
85
|
+
return true;
|
|
50
86
|
} catch {
|
|
51
|
-
|
|
87
|
+
return false;
|
|
52
88
|
}
|
|
53
89
|
}
|
|
54
90
|
|
|
55
|
-
|
|
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
|
+
};
|
|
56
210
|
|
|
57
211
|
if (require.main === module) {
|
|
58
212
|
// --- Passthrough mode: build binary, then forward to it ---
|
|
59
213
|
if (hasSubcommand()) {
|
|
60
|
-
|
|
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
|
+
}
|
|
61
229
|
const binary = ensureBinary();
|
|
62
230
|
const child = spawn(binary, args, {
|
|
63
231
|
stdio: "inherit",
|
|
64
232
|
cwd: process.cwd(),
|
|
65
|
-
env:
|
|
233
|
+
env: process.env,
|
|
66
234
|
});
|
|
67
235
|
child.on("exit", (code) => {
|
|
68
236
|
process.exit(code ?? 1);
|
|
69
237
|
});
|
|
70
238
|
} else {
|
|
71
239
|
// --- Launcher mode: build everything, start server, open browser ---
|
|
72
|
-
const SERVER_PORT = 14199;
|
|
73
240
|
const shouldRestart = args.includes("--restart");
|
|
74
241
|
|
|
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
242
|
const launcherMain = async () => {
|
|
86
243
|
const projectDir = process.cwd();
|
|
87
244
|
console.error(`Editor repo: ${rootDir}`);
|
|
88
245
|
console.error(`Target project: ${projectDir}`);
|
|
89
246
|
|
|
90
|
-
|
|
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);
|
|
91
302
|
|
|
92
|
-
//
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
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
|
+
}
|
|
98
328
|
}
|
|
99
329
|
|
|
100
330
|
const serverChild = shouldRestart
|
|
101
331
|
? await restartServer(SERVER_PORT)
|
|
102
332
|
: await ensureServer(SERVER_PORT);
|
|
103
333
|
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
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
|
+
}
|
|
110
351
|
}
|
|
111
352
|
|
|
112
353
|
const url = `http://localhost:${SERVER_PORT}`;
|
|
113
354
|
console.error(`Editor UI: ${url}`);
|
|
114
|
-
|
|
355
|
+
if (hasCommand) {
|
|
356
|
+
console.error(`Live Preview: http://localhost:${appPort}`);
|
|
357
|
+
}
|
|
115
358
|
console.error(`Project: ${projectDir}`);
|
|
116
359
|
open(url);
|
|
360
|
+
const banner = postLaunchInitBanner({
|
|
361
|
+
preflightAction: preflight.action,
|
|
362
|
+
wasEmpty,
|
|
363
|
+
});
|
|
364
|
+
if (banner) {
|
|
365
|
+
console.error(banner);
|
|
366
|
+
}
|
|
117
367
|
const keepAlive = setInterval(() => {}, 60_000);
|
|
118
368
|
|
|
119
369
|
process.on("SIGINT", () => {
|
package/npm/editor.js
CHANGED
|
@@ -16,14 +16,23 @@ const { execSync } = require("child_process");
|
|
|
16
16
|
const fs = require("fs");
|
|
17
17
|
const os = require("os");
|
|
18
18
|
const path = require("path");
|
|
19
|
-
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");
|
|
20
34
|
const { open } = require("./open");
|
|
21
35
|
|
|
22
|
-
const SERVER_PORT = parseInt(process.env.CODEYAM_EDITOR_PORT || "14199", 10);
|
|
23
|
-
const VITE_PORT = parseInt(process.env.PORT || "5173", 10);
|
|
24
|
-
// The reverse proxy sits on VITE_PORT; the actual Vite dev server runs on
|
|
25
|
-
// VITE_PORT + 1. We need to wait for the real Vite server, not the proxy.
|
|
26
|
-
const VITE_INTERNAL_PORT = VITE_PORT + 1;
|
|
27
36
|
const shouldRestart = process.argv.includes("--restart");
|
|
28
37
|
|
|
29
38
|
function buildBinary() {
|
|
@@ -32,7 +41,6 @@ function buildBinary() {
|
|
|
32
41
|
execSync("cargo build", { stdio: "inherit", cwd: rootDir });
|
|
33
42
|
return true;
|
|
34
43
|
} catch {
|
|
35
|
-
console.error("Failed to build Rust binary. Continuing with existing build (if any)...");
|
|
36
44
|
return false;
|
|
37
45
|
}
|
|
38
46
|
}
|
|
@@ -47,6 +55,33 @@ function cargoBinDir() {
|
|
|
47
55
|
return path.join(os.homedir(), ".cargo", "bin");
|
|
48
56
|
}
|
|
49
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
|
+
|
|
50
85
|
// Install target/debug/codeyam-editor to <cargo_bin>/codeyam-editor
|
|
51
86
|
// atomically (write to tmp in same dir, then rename), and refresh the
|
|
52
87
|
// codeyam-editor-pty-broker hardlink so it points at the new inode.
|
|
@@ -71,9 +106,7 @@ function installBinary() {
|
|
|
71
106
|
console.error(`Installing fresh binary to ${dstPath}...`);
|
|
72
107
|
try {
|
|
73
108
|
fs.mkdirSync(binDir, { recursive: true });
|
|
74
|
-
|
|
75
|
-
fs.chmodSync(tmpPath, 0o755);
|
|
76
|
-
fs.renameSync(tmpPath, dstPath);
|
|
109
|
+
linkOrCopyBinary(srcPath, dstPath, tmpPath);
|
|
77
110
|
try {
|
|
78
111
|
fs.unlinkSync(linkPath);
|
|
79
112
|
} catch (e) {
|
|
@@ -98,32 +131,44 @@ function sweepStaleArtifacts() {
|
|
|
98
131
|
}
|
|
99
132
|
}
|
|
100
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
|
+
*/
|
|
101
139
|
async function main() {
|
|
102
|
-
//
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
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;
|
|
122
163
|
|
|
123
164
|
// Rebuild the Rust binary so code changes are picked up on restart.
|
|
124
165
|
// This script is only invoked from a source checkout (via `npm run editor`),
|
|
125
166
|
// so Cargo.toml and the crates tree are always present.
|
|
126
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" });
|
|
127
172
|
// Refresh <cargo_bin>/codeyam-editor (and the pty-broker hardlink) so
|
|
128
173
|
// the broker auto-spawn execs a binary that knows the `pty-broker
|
|
129
174
|
// daemon` subcommand. Skip if the build failed — installing a stale
|
|
@@ -145,11 +190,27 @@ async function main() {
|
|
|
145
190
|
|
|
146
191
|
// Start the Rust backend — it serves ui/dist/ and launches the Vite dev
|
|
147
192
|
// server (configured via startCommand in editor.json) for Live Preview HMR.
|
|
148
|
-
// On --restart: stop the existing server
|
|
149
|
-
// target/ is idle
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
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.
|
|
153
214
|
}
|
|
154
215
|
if (shouldRestart) {
|
|
155
216
|
sweepStaleArtifacts();
|
|
@@ -180,8 +241,10 @@ async function main() {
|
|
|
180
241
|
|
|
181
242
|
module.exports = {
|
|
182
243
|
buildBinary,
|
|
244
|
+
staleBinaryBanner,
|
|
183
245
|
cargoBinDir,
|
|
184
246
|
installBinary,
|
|
247
|
+
linkOrCopyBinary,
|
|
185
248
|
main,
|
|
186
249
|
sweepStaleArtifacts,
|
|
187
250
|
};
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Launcher-side mirror of `is_empty_project_dir` in
|
|
5
|
+
* `crates/codeyam-editor/src/commands/init.rs`. The Rust side decides
|
|
6
|
+
* which `/codeyam-…` skill the init binary points at; this JS mirror
|
|
7
|
+
* decides the same for the launcher's post-launch tail banner. Keep
|
|
8
|
+
* both in sync — the test `empty-project-dir.test.js` and Rust's
|
|
9
|
+
* `is_empty_project_dir_ignores_hidden_entries` /
|
|
10
|
+
* `is_empty_project_dir_detects_visible_entry` pin the contract.
|
|
11
|
+
*
|
|
12
|
+
* A "fresh" project directory has no non-hidden entries — only things
|
|
13
|
+
* like `.git/`, `.gitignore`, or other dotfiles. Hidden entries don't
|
|
14
|
+
* count as project content, so a dir holding only `.git/` and
|
|
15
|
+
* `.gitignore` (the "I just ran `git init`" case) still reads as empty.
|
|
16
|
+
* A read error returns false, matching the Rust `Err(_) => false` branch:
|
|
17
|
+
* a missing / unreadable dir defaults to the existing-project path rather
|
|
18
|
+
* than falsely scaffolding over nothing.
|
|
19
|
+
*/
|
|
20
|
+
|
|
21
|
+
const fs = require("node:fs");
|
|
22
|
+
|
|
23
|
+
function isEmptyProjectDir(projectDir) {
|
|
24
|
+
let entries;
|
|
25
|
+
try {
|
|
26
|
+
entries = fs.readdirSync(projectDir);
|
|
27
|
+
} catch {
|
|
28
|
+
return false;
|
|
29
|
+
}
|
|
30
|
+
return entries.every((name) => name.startsWith("."));
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
module.exports = { isEmptyProjectDir };
|