@codeyam-editor/codeyam-editor 0.1.0-staging.01efa1a
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 +19 -0
- package/npm/cloud-branch-switch.js +442 -0
- package/npm/cloud.js +2184 -0
- package/npm/container.js +54 -0
- package/npm/editor-dev.js +459 -0
- package/npm/editor.js +272 -0
- package/npm/empty-project-dir.js +33 -0
- package/npm/fleet-auth.js +111 -0
- package/npm/fleet-branch.js +49 -0
- package/npm/fleet-broker-death.js +44 -0
- package/npm/fleet-broker-debounce.js +63 -0
- package/npm/fleet-broker-status.js +45 -0
- package/npm/fleet-build-data.js +81 -0
- package/npm/fleet-cleanup.js +297 -0
- package/npm/fleet-effect-status.js +179 -0
- package/npm/fleet-env.js +125 -0
- package/npm/fleet-grouping.js +56 -0
- package/npm/fleet-job-badge.js +83 -0
- package/npm/fleet-memory.js +157 -0
- package/npm/fleet-plans.js +96 -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 +93 -0
- package/npm/fleet-reset.js +222 -0
- package/npm/fleet-roster.js +81 -0
- package/npm/launch-config.js +717 -0
- package/npm/open.js +15 -0
- package/npm/preflight-guidance.js +77 -0
- package/npm/scenario-check.js +604 -0
- package/npm/scenario-handlers.js +82 -0
- package/npm/scenario-interactivity.js +245 -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 +534 -0
- package/npm/utils.js +823 -0
- package/npm/vm-urls.js +117 -0
- package/package.json +32 -0
- package/ui/dist/assets/ContainerInspector-C8EkH4PP.js +2 -0
- package/ui/dist/assets/EditorShell-By-2rl7D.js +187 -0
- package/ui/dist/assets/Markdown-BHBqz-Mj.js +1 -0
- package/ui/dist/assets/ScenarioDataPanel-Cg8zOFNZ.js +5 -0
- package/ui/dist/assets/ScenarioDataPanel-DYP7pi_n.css +32 -0
- package/ui/dist/assets/ScenarioDataPanelFullPage-WOUYbba7.js +1 -0
- package/ui/dist/assets/ScenarioRenderer-CBTlIVPs.js +1 -0
- package/ui/dist/assets/StepsSimulator-CRtmlyNZ.js +1 -0
- package/ui/dist/assets/index-DzoF9RKw.js +27 -0
- package/ui/dist/assets/index-ckz94rKI.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/container.js
ADDED
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
|
|
3
|
+
const path = require("path");
|
|
4
|
+
/**
|
|
5
|
+
* Start the container monitoring UI.
|
|
6
|
+
*
|
|
7
|
+
* Starts the Rust backend (if not already running) and opens the
|
|
8
|
+
* inline monitoring UI at http://localhost:14199.
|
|
9
|
+
*/
|
|
10
|
+
|
|
11
|
+
const {
|
|
12
|
+
ensureServer,
|
|
13
|
+
rootDir,
|
|
14
|
+
killChildProcess,
|
|
15
|
+
readMergedEditorConfig,
|
|
16
|
+
resolveLauncherPort,
|
|
17
|
+
} = require("./utils");
|
|
18
|
+
const { open } = require("./open");
|
|
19
|
+
|
|
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
|
+
*/
|
|
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
|
+
|
|
36
|
+
const serverChild = await ensureServer(PORT);
|
|
37
|
+
const url = `http://localhost:${PORT}`;
|
|
38
|
+
console.error(`Container UI: ${url}`);
|
|
39
|
+
console.error(`Logs: ${path.join(rootDir, ".codeyam", "logs")}`);
|
|
40
|
+
open(url);
|
|
41
|
+
const keepAlive = setInterval(() => {}, 60_000);
|
|
42
|
+
|
|
43
|
+
process.on("SIGINT", () => {
|
|
44
|
+
clearInterval(keepAlive);
|
|
45
|
+
killChildProcess(serverChild);
|
|
46
|
+
process.exit(0);
|
|
47
|
+
});
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
module.exports = { main };
|
|
51
|
+
|
|
52
|
+
if (require.main === module) {
|
|
53
|
+
main();
|
|
54
|
+
}
|
|
@@ -0,0 +1,459 @@
|
|
|
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
|
+
rootDir,
|
|
37
|
+
killChildProcess,
|
|
38
|
+
readMergedEditorConfig,
|
|
39
|
+
resolveLauncherPort,
|
|
40
|
+
staleBinaryBanner,
|
|
41
|
+
enforceFreshBinary,
|
|
42
|
+
} = require("./utils");
|
|
43
|
+
const { open } = require("./open");
|
|
44
|
+
const { classifyProject } = require("./preflight-guidance");
|
|
45
|
+
const { isEmptyProjectDir } = require("./empty-project-dir");
|
|
46
|
+
|
|
47
|
+
// When invoked through the `codeyam-editor-dev` symlink, tell the Rust
|
|
48
|
+
// binary to emit "codeyam-editor-dev" in all scaffolded commands,
|
|
49
|
+
// permissions, and installed files. When invoked through the
|
|
50
|
+
// `codeyam-editor` symlink, leave CODEYAM_CLI unset so the binary
|
|
51
|
+
// emits the canonical "codeyam-editor" — matching the cloud install
|
|
52
|
+
// path and the new hook/skill convention. The runtime cost of the
|
|
53
|
+
// rebuild-on-passthrough behavior is identical for both names.
|
|
54
|
+
const invokedAs = path.basename(process.argv[1] || "");
|
|
55
|
+
if (invokedAs === "codeyam-editor-dev") {
|
|
56
|
+
process.env.CODEYAM_CLI = "codeyam-editor-dev";
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
const args = process.argv.slice(2);
|
|
60
|
+
const KNOWN_SUBCOMMANDS = ["start", "init", "editor"];
|
|
61
|
+
|
|
62
|
+
/**
|
|
63
|
+
* Check if the first non-flag argument is a known subcommand.
|
|
64
|
+
* Accepts an explicit argv array so the helper is testable without mutating
|
|
65
|
+
* process.argv (the module-level `args` const is the production default).
|
|
66
|
+
*/
|
|
67
|
+
function hasSubcommand(argv = args) {
|
|
68
|
+
for (const arg of argv) {
|
|
69
|
+
if (arg.startsWith("-")) continue;
|
|
70
|
+
return KNOWN_SUBCOMMANDS.includes(arg);
|
|
71
|
+
}
|
|
72
|
+
return false;
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
/**
|
|
76
|
+
* Build the Rust binary from the editor repo. Returns true on success,
|
|
77
|
+
* false if `cargo build` failed. This function only reports the outcome —
|
|
78
|
+
* the caller decides whether a failed build is fatal (see staleBinaryBanner).
|
|
79
|
+
*/
|
|
80
|
+
function buildBinary() {
|
|
81
|
+
console.error("Building Rust binary...");
|
|
82
|
+
try {
|
|
83
|
+
execSync("cargo build", { stdio: "inherit", cwd: rootDir });
|
|
84
|
+
return true;
|
|
85
|
+
} catch {
|
|
86
|
+
return false;
|
|
87
|
+
}
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
/**
|
|
91
|
+
* Decide whether the dev wrapper should rebuild the Rust binary (and the UI,
|
|
92
|
+
* for the launcher path) before forwarding to it. The answer is always yes:
|
|
93
|
+
* `codeyam-editor-dev` ALWAYS rebuilds before running. Running fresh source is
|
|
94
|
+
* the entire purpose of the dev wrapper — if a developer wants a frozen tool
|
|
95
|
+
* that never rebuilds, the cargo-installed `codeyam-editor` binary is exactly
|
|
96
|
+
* that.
|
|
97
|
+
*
|
|
98
|
+
* `cargo build` is idempotent: a sub-second no-op that does not relink when
|
|
99
|
+
* nothing changed. So always-building costs nothing for a developer who only
|
|
100
|
+
* uses the editor as a tool against their own app (the binary never changes →
|
|
101
|
+
* the server-side stale-binary watcher never fires → zero mid-session
|
|
102
|
+
* restarts), and produces a rebuild+restart only when editor source actually
|
|
103
|
+
* changed — which is exactly when the developer wants the new code live.
|
|
104
|
+
*
|
|
105
|
+
* The injected cwd / repoRoot / env are retained so the contract — "rebuild
|
|
106
|
+
* regardless of where you run it or what env is set" — is demonstrable in a
|
|
107
|
+
* unit test across the dimensions that used to gate the decision (self-host
|
|
108
|
+
* vs. external project, `CODEYAM_DEV_REBUILD` set or not). They no longer
|
|
109
|
+
* influence the result.
|
|
110
|
+
*/
|
|
111
|
+
function shouldRebuildOnPassthrough() {
|
|
112
|
+
return true;
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
/**
|
|
116
|
+
* Returns true when the merged project config has a non-empty start
|
|
117
|
+
* command — either at `apps[0].startCommand` (per-app, preferred) or at
|
|
118
|
+
* the top-level `startCommand` (legacy default). Mirrors the Rust
|
|
119
|
+
* `resolve_start_command` precedence in
|
|
120
|
+
* `crates/codeyam-editor/src/commands/start.rs`.
|
|
121
|
+
*
|
|
122
|
+
* When neither slot has a non-empty string, the editor server runs in
|
|
123
|
+
* "editor UI only" mode and the launcher must skip the dev-server port
|
|
124
|
+
* wait — otherwise the user stares at "Waiting for dev server on port
|
|
125
|
+
* N..." for the full 15s timeout.
|
|
126
|
+
*/
|
|
127
|
+
function hasStartCommand(mergedConfig) {
|
|
128
|
+
if (mergedConfig == null || typeof mergedConfig !== "object") return false;
|
|
129
|
+
const apps = Array.isArray(mergedConfig.apps) ? mergedConfig.apps : null;
|
|
130
|
+
if (apps && apps.length > 0) {
|
|
131
|
+
const cmd = apps[0] && apps[0].startCommand;
|
|
132
|
+
if (typeof cmd === "string" && cmd.trim() !== "") return true;
|
|
133
|
+
}
|
|
134
|
+
const topLevel = mergedConfig.startCommand;
|
|
135
|
+
return typeof topLevel === "string" && topLevel.trim() !== "";
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
/**
|
|
139
|
+
* Decide what the launcher should do given a `classifyProject` result.
|
|
140
|
+
* Pure — no fs, no spawn, no process.exit — so `launcherMain`'s branching
|
|
141
|
+
* is unit-testable without building a binary or spawning a server (the same
|
|
142
|
+
* policy/classification split as `classifyPortHolder` in utils.js).
|
|
143
|
+
*
|
|
144
|
+
* modern → { action: "proceed" } launch as-is
|
|
145
|
+
* none → { action: "init" } greenfield: init, then launch
|
|
146
|
+
* legacy → { action: "bail", guidance, code: 2 } print guidance and exit
|
|
147
|
+
*
|
|
148
|
+
* Greenfield auto-inits because the dev launcher is a one-shot "build
|
|
149
|
+
* everything, run the editor against this project" tool — running it in a
|
|
150
|
+
* fresh dir should just work. Legacy bails instead: `cmd_init`'s legacy
|
|
151
|
+
* branch deliberately skips `editor.json` creation so `/codeyam-onboard`
|
|
152
|
+
* can migrate from the preserved classification, so auto-running init would
|
|
153
|
+
* strand them. The Rust-side `start_preflight` stays the safety net for
|
|
154
|
+
* direct `codeyam-editor start` calls; this launcher composes init + start.
|
|
155
|
+
*
|
|
156
|
+
* Intentional divergence (improve39): this launcher auto-inits ALL `none`
|
|
157
|
+
* projects (empty or existing-source); the Rust `start_preflight` now
|
|
158
|
+
* auto-inits only the EMPTY case and still bails (exit 2) on existing-source.
|
|
159
|
+
* That gap is pre-existing and deliberate — the dev launcher is a local
|
|
160
|
+
* convenience, not the direct-`start` contract.
|
|
161
|
+
*/
|
|
162
|
+
function planPreflightAction(classification) {
|
|
163
|
+
if (classification.kind === "legacy") {
|
|
164
|
+
return { action: "bail", guidance: classification.guidance, code: 2 };
|
|
165
|
+
}
|
|
166
|
+
if (classification.kind === "none") {
|
|
167
|
+
return { action: "init" };
|
|
168
|
+
}
|
|
169
|
+
return { action: "proceed" };
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
/**
|
|
173
|
+
* Pick the launcher's post-launch tail banner — printed right next to the
|
|
174
|
+
* `Editor UI: http://…` line where the user is actually looking, after the
|
|
175
|
+
* editor server is up. Pure (no fs, no spawn) so the three-arm decision is
|
|
176
|
+
* unit-testable without spawning a binary, mirroring `planPreflightAction`.
|
|
177
|
+
*
|
|
178
|
+
* Skill assumption: /codeyam-editor scaffolds a project from scratch;
|
|
179
|
+
* /codeyam-onboard migrates an existing source tree. Empty greenfield
|
|
180
|
+
* (no non-hidden entries before init) must route to the scaffold path;
|
|
181
|
+
* existing greenfield routes to the migration path. Mirrors the same
|
|
182
|
+
* split in `cmd_init` (init.rs:10-24) — keep both in sync. Do NOT
|
|
183
|
+
* "simplify" by collapsing the branches; the skill each path names is the
|
|
184
|
+
* whole point.
|
|
185
|
+
*
|
|
186
|
+
* action !== "init" → null (modern: proceed, no banner;
|
|
187
|
+
* legacy already bailed earlier)
|
|
188
|
+
* action "init", wasEmpty → greenfield message. /codeyam-editor is driven
|
|
189
|
+
* from inside the browser UI, never hand-invoked,
|
|
190
|
+
* so the wording follows `browserOpened`: if the
|
|
191
|
+
* launcher actually opened a tab, promise it; if
|
|
192
|
+
* not (e.g. --restart attach), point at the URL
|
|
193
|
+
* so the user still has a path forward. The
|
|
194
|
+
* banner must never promise a tab that didn't
|
|
195
|
+
* appear.
|
|
196
|
+
* action "init", !wasEmpty → /codeyam-onboard migrate-existing
|
|
197
|
+
*/
|
|
198
|
+
function postLaunchInitBanner({ preflightAction, wasEmpty, browserOpened, url }) {
|
|
199
|
+
if (preflightAction !== "init") return null;
|
|
200
|
+
if (wasEmpty) {
|
|
201
|
+
return browserOpened
|
|
202
|
+
? "\nA browser window is opening where you can begin your project."
|
|
203
|
+
: `\nOpen ${url} in your browser to begin your project.`;
|
|
204
|
+
}
|
|
205
|
+
return (
|
|
206
|
+
"\nInitialized .codeyam/ in this directory. To populate scenarios, " +
|
|
207
|
+
"open Claude Code here and run `/codeyam-onboard`."
|
|
208
|
+
);
|
|
209
|
+
}
|
|
210
|
+
|
|
211
|
+
/**
|
|
212
|
+
* Decide whether the launcher should open a browser tab to the editor UI.
|
|
213
|
+
* Pure (no fs, no spawn) so the gating is unit-testable without a binary,
|
|
214
|
+
* mirroring `planPreflightAction` / `postLaunchInitBanner`.
|
|
215
|
+
*
|
|
216
|
+
* The browser open belongs ONLY on a genuine cold start — bringing the
|
|
217
|
+
* editor server up fresh against a not-yet-running port. Two cases must
|
|
218
|
+
* NOT re-pop a tab, because one is presumably already open:
|
|
219
|
+
*
|
|
220
|
+
* shouldRestart → false `--restart` (rebuild-self, restart-server routed
|
|
221
|
+
* through this wrapper, a manual --restart) tears
|
|
222
|
+
* down and respawns a server the user is already
|
|
223
|
+
* looking at. Re-opening spams tabs — the reported
|
|
224
|
+
* symptom. Mirrors the Rust `--no-open` restart
|
|
225
|
+
* spawn in restart_server.rs.
|
|
226
|
+
* wasRunning → false `ensureServer` attached to an already-running
|
|
227
|
+
* same-project server (returned null) — no fresh
|
|
228
|
+
* process, so no new tab.
|
|
229
|
+
*
|
|
230
|
+
* Only a fresh spawn on a cold start (neither restart nor attach) opens the
|
|
231
|
+
* UI, matching the Rust `no_open`-gated `open_url` in start.rs.
|
|
232
|
+
*
|
|
233
|
+
* freshInit → true A brand-new empty project that was just init'd
|
|
234
|
+
* this run. The entire greenfield onboarding flow
|
|
235
|
+
* is driven from inside the editor UI, and a
|
|
236
|
+
* project that didn't exist a moment ago has no
|
|
237
|
+
* pre-existing tab for the --restart tab-spam guard
|
|
238
|
+
* to protect. So a fresh init opens regardless of
|
|
239
|
+
* `shouldRestart` — UNLESS we merely attached to an
|
|
240
|
+
* already-running server (`wasRunning`), which
|
|
241
|
+
* implies a tab is already up.
|
|
242
|
+
*/
|
|
243
|
+
function shouldOpenBrowser({ shouldRestart, wasRunning, freshInit }) {
|
|
244
|
+
if (freshInit) return !wasRunning;
|
|
245
|
+
return !shouldRestart && !wasRunning;
|
|
246
|
+
}
|
|
247
|
+
|
|
248
|
+
module.exports = {
|
|
249
|
+
hasSubcommand,
|
|
250
|
+
buildBinary,
|
|
251
|
+
shouldRebuildOnPassthrough,
|
|
252
|
+
staleBinaryBanner,
|
|
253
|
+
hasStartCommand,
|
|
254
|
+
planPreflightAction,
|
|
255
|
+
postLaunchInitBanner,
|
|
256
|
+
shouldOpenBrowser,
|
|
257
|
+
};
|
|
258
|
+
|
|
259
|
+
if (require.main === module) {
|
|
260
|
+
// --- Passthrough mode: build binary, then forward to it ---
|
|
261
|
+
if (hasSubcommand()) {
|
|
262
|
+
// Always rebuild before forwarding — running fresh source is the whole
|
|
263
|
+
// point of the dev wrapper. `cargo build` is a sub-second no-op when
|
|
264
|
+
// nothing changed (no relink → the server-side stale-binary watcher never
|
|
265
|
+
// fires), and relinks only when editor source actually changed, which is
|
|
266
|
+
// exactly when the developer wants the new code live. `ensureBinary()`
|
|
267
|
+
// still builds once if no binary exists at all.
|
|
268
|
+
if (shouldRebuildOnPassthrough()) {
|
|
269
|
+
enforceFreshBinary(buildBinary(), { action: "run editor commands" });
|
|
270
|
+
}
|
|
271
|
+
const binary = ensureBinary();
|
|
272
|
+
const child = spawn(binary, args, {
|
|
273
|
+
stdio: "inherit",
|
|
274
|
+
cwd: process.cwd(),
|
|
275
|
+
env: process.env,
|
|
276
|
+
});
|
|
277
|
+
child.on("exit", (code) => {
|
|
278
|
+
process.exit(code ?? 1);
|
|
279
|
+
});
|
|
280
|
+
} else {
|
|
281
|
+
// --- Launcher mode: build everything, start server, open browser ---
|
|
282
|
+
const shouldRestart = args.includes("--restart");
|
|
283
|
+
|
|
284
|
+
const launcherMain = async () => {
|
|
285
|
+
const projectDir = process.cwd();
|
|
286
|
+
console.error(`Editor repo: ${rootDir}`);
|
|
287
|
+
console.error(`Target project: ${projectDir}`);
|
|
288
|
+
|
|
289
|
+
// Greenfield projects are auto-initialized here so the dev launcher
|
|
290
|
+
// stays a one-shot "build everything, run the editor against this
|
|
291
|
+
// project" tool. Legacy projects bail with guidance; modern projects
|
|
292
|
+
// skip init. See `planPreflightAction` for the rationale on each
|
|
293
|
+
// branch. Subcommand pass-through (handled above) is unaffected — the
|
|
294
|
+
// binary classifies its own subcommands.
|
|
295
|
+
const preflight = planPreflightAction(classifyProject(projectDir));
|
|
296
|
+
// Capture emptiness BEFORE init runs — the spawn below creates
|
|
297
|
+
// .codeyam/ (and install_editor_files writes .claude/ etc.), which
|
|
298
|
+
// would make every project look non-empty by the time the
|
|
299
|
+
// post-launch banner prints. Same constraint as the Rust comment in
|
|
300
|
+
// `init_greenfield_branch` (init.rs:82-85). Computed as a sibling
|
|
301
|
+
// local (not folded into `preflight`) because emptiness is
|
|
302
|
+
// orthogonal to the preflight kind.
|
|
303
|
+
const wasEmpty = isEmptyProjectDir(projectDir);
|
|
304
|
+
if (preflight.action === "bail") {
|
|
305
|
+
console.error(preflight.guidance);
|
|
306
|
+
process.exit(preflight.code);
|
|
307
|
+
}
|
|
308
|
+
if (preflight.action === "init") {
|
|
309
|
+
// Run init from projectDir before the config reads below so the
|
|
310
|
+
// freshly-written .codeyam/editor.json feeds port resolution.
|
|
311
|
+
// ensureBinary builds the binary first if this is a fresh checkout.
|
|
312
|
+
console.error(`Initializing codeyam-editor in ${projectDir}...`);
|
|
313
|
+
const initBinary = ensureBinary();
|
|
314
|
+
const initResult = spawnSync(initBinary, ["init"], {
|
|
315
|
+
cwd: projectDir,
|
|
316
|
+
stdio: "inherit",
|
|
317
|
+
env: process.env,
|
|
318
|
+
});
|
|
319
|
+
if (initResult.status !== 0) {
|
|
320
|
+
console.error(
|
|
321
|
+
`codeyam-editor init exited ${initResult.status ?? "by signal"}; aborting launch.`,
|
|
322
|
+
);
|
|
323
|
+
process.exit(1);
|
|
324
|
+
}
|
|
325
|
+
}
|
|
326
|
+
|
|
327
|
+
// Editor server's control port is owned by the editor repo's
|
|
328
|
+
// merged config (rootDir), not the target project's. The target
|
|
329
|
+
// project's own `editor.local.json` only contributes to the
|
|
330
|
+
// app/dev-server port below.
|
|
331
|
+
const editorMerged = readMergedEditorConfig(rootDir);
|
|
332
|
+
const SERVER_PORT = resolveLauncherPort(
|
|
333
|
+
process.env.CODEYAM_EDITOR_PORT,
|
|
334
|
+
editorMerged.proxy && editorMerged.proxy.controlPort,
|
|
335
|
+
14199,
|
|
336
|
+
);
|
|
337
|
+
|
|
338
|
+
// Target project's dev-server port comes from its own
|
|
339
|
+
// editor.json + editor.local.json (developers iterating on the
|
|
340
|
+
// editor against a real client project rely on the override
|
|
341
|
+
// file here too).
|
|
342
|
+
const projectMerged = readMergedEditorConfig(projectDir);
|
|
343
|
+
const appPort = resolveLauncherPort(undefined, projectMerged.port, 3000);
|
|
344
|
+
|
|
345
|
+
// Always rebuild the Rust binary + UI before launching — same contract
|
|
346
|
+
// as the passthrough path. The build runs BEFORE the server (re)starts
|
|
347
|
+
// below, so the new server records its start time after the relink and
|
|
348
|
+
// the stale-binary watcher reads it as fresh (no immediate SIGUSR1). A
|
|
349
|
+
// no-op `cargo build` (nothing changed) doesn't relink, so an external
|
|
350
|
+
// client-project launch is never disrupted. `ensureBinary()` still
|
|
351
|
+
// builds once if no binary exists.
|
|
352
|
+
if (shouldRebuildOnPassthrough()) {
|
|
353
|
+
enforceFreshBinary(buildBinary(), {
|
|
354
|
+
action: "run editor commands",
|
|
355
|
+
continueVerb: "launching with",
|
|
356
|
+
});
|
|
357
|
+
|
|
358
|
+
// Build the UI from the editor repo
|
|
359
|
+
console.error("Building UI...");
|
|
360
|
+
try {
|
|
361
|
+
execSync("npx vite build", { stdio: "inherit", cwd: path.join(rootDir, "ui") });
|
|
362
|
+
} catch {
|
|
363
|
+
console.error("Failed to build UI. Continuing with existing build (if any)...");
|
|
364
|
+
}
|
|
365
|
+
}
|
|
366
|
+
|
|
367
|
+
const serverChild = shouldRestart
|
|
368
|
+
? await restartServer(SERVER_PORT)
|
|
369
|
+
: await ensureServer(SERVER_PORT);
|
|
370
|
+
|
|
371
|
+
// Re-read the (possibly just-initialized) project config to decide
|
|
372
|
+
// whether to wait on a dev server at all. On a project where init
|
|
373
|
+
// couldn't detect a framework (or a user-configured "editor UI
|
|
374
|
+
// only" setup), `startCommand` is empty — the editor server itself
|
|
375
|
+
// prints "No startCommand configured. Running editor UI only." and
|
|
376
|
+
// never spawns a dev server, so waiting 15s for `appPort + 1` is a
|
|
377
|
+
// pure UX hang. The launcher silently skips the wait; the server's
|
|
378
|
+
// own log line is the canonical signal to the user.
|
|
379
|
+
const postInitMerged = readMergedEditorConfig(projectDir);
|
|
380
|
+
const hasCommand = hasStartCommand(postInitMerged);
|
|
381
|
+
if (hasCommand) {
|
|
382
|
+
const viteInternalPort = appPort + 1;
|
|
383
|
+
console.error(`Waiting for dev server on port ${viteInternalPort}...`);
|
|
384
|
+
const viteReady = await waitForPort(viteInternalPort, 15000, 300);
|
|
385
|
+
if (!viteReady) {
|
|
386
|
+
console.error(`Warning: Dev server not ready on port ${viteInternalPort}. Live Preview may not work initially.`);
|
|
387
|
+
}
|
|
388
|
+
}
|
|
389
|
+
|
|
390
|
+
const url = `http://localhost:${SERVER_PORT}`;
|
|
391
|
+
console.error(`Editor UI: ${url}`);
|
|
392
|
+
if (hasCommand) {
|
|
393
|
+
console.error(`Live Preview: http://localhost:${appPort}`);
|
|
394
|
+
}
|
|
395
|
+
console.error(`Project: ${projectDir}`);
|
|
396
|
+
// Open the browser only on a genuine cold start. On the --restart path
|
|
397
|
+
// (rebuild-self, restart-server, manual --restart) and when we attached
|
|
398
|
+
// to an already-running server, a tab is presumably already open — the
|
|
399
|
+
// `Editor UI: <url>` line above is the signal. `ensureServer` returns
|
|
400
|
+
// null when it attached rather than spawning, so that is `wasRunning`.
|
|
401
|
+
// Exception: a fresh greenfield init (`freshInit`) opens regardless of
|
|
402
|
+
// --restart, because a project that didn't exist a moment ago has no
|
|
403
|
+
// pre-existing tab to protect and the onboarding flow lives in the UI.
|
|
404
|
+
// Compute the open decision once and feed the SAME boolean into the
|
|
405
|
+
// banner so it can never promise a tab that didn't appear.
|
|
406
|
+
const freshInit = preflight.action === "init" && wasEmpty;
|
|
407
|
+
const openedBrowser = shouldOpenBrowser({
|
|
408
|
+
shouldRestart,
|
|
409
|
+
wasRunning: serverChild === null,
|
|
410
|
+
freshInit,
|
|
411
|
+
});
|
|
412
|
+
if (openedBrowser) {
|
|
413
|
+
open(url);
|
|
414
|
+
}
|
|
415
|
+
const banner = postLaunchInitBanner({
|
|
416
|
+
preflightAction: preflight.action,
|
|
417
|
+
wasEmpty,
|
|
418
|
+
browserOpened: openedBrowser,
|
|
419
|
+
url,
|
|
420
|
+
});
|
|
421
|
+
if (banner) {
|
|
422
|
+
console.error(banner);
|
|
423
|
+
}
|
|
424
|
+
const keepAlive = setInterval(() => {}, 60_000);
|
|
425
|
+
|
|
426
|
+
// Track launcher-initiated teardown so the unexpected-exit handler below
|
|
427
|
+
// can tell "we killed it on SIGINT" from "the server died on its own".
|
|
428
|
+
let tearingDown = false;
|
|
429
|
+
|
|
430
|
+
process.on("SIGINT", () => {
|
|
431
|
+
tearingDown = true;
|
|
432
|
+
clearInterval(keepAlive);
|
|
433
|
+
killChildProcess(serverChild);
|
|
434
|
+
process.exit(0);
|
|
435
|
+
});
|
|
436
|
+
|
|
437
|
+
// Defense-in-depth against orphaned launchers: if the spawned server
|
|
438
|
+
// child exits and we did NOT initiate it (SIGINT), clear keepAlive and
|
|
439
|
+
// exit rather than lingering as a node process holding a dead child
|
|
440
|
+
// handle. We deliberately do NOT respawn — an exited server should let
|
|
441
|
+
// the launcher exit cleanly so stale supervisors can't accumulate across
|
|
442
|
+
// repeated launches. The in-place SIGUSR1 re-exec keeps the SAME pid, so
|
|
443
|
+
// it does NOT emit an "exit" here and does not trip this path; only a
|
|
444
|
+
// genuinely dead server does. `serverChild` is null when we attached to
|
|
445
|
+
// an already-running server (nothing of ours to watch).
|
|
446
|
+
if (serverChild) {
|
|
447
|
+
serverChild.on("exit", (code) => {
|
|
448
|
+
if (tearingDown) {
|
|
449
|
+
return;
|
|
450
|
+
}
|
|
451
|
+
clearInterval(keepAlive);
|
|
452
|
+
process.exit(code ?? 1);
|
|
453
|
+
});
|
|
454
|
+
}
|
|
455
|
+
};
|
|
456
|
+
|
|
457
|
+
launcherMain();
|
|
458
|
+
}
|
|
459
|
+
}
|