@codeyam-editor/codeyam-editor 0.1.4 → 0.1.6
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 -1
- package/npm/cloud-branch-switch.js +1 -1
- package/npm/cloud.js +3166 -150
- package/npm/codesign-plan.js +68 -0
- package/npm/container.js +3 -0
- package/npm/disk-pressure.js +78 -0
- package/npm/editor-dev.js +96 -15
- package/npm/editor.js +85 -37
- package/npm/empty-project-dir.js +92 -7
- package/npm/fleet-add-failure.js +252 -0
- package/npm/fleet-asset-version.js +37 -0
- package/npm/fleet-auth-health.js +213 -0
- package/npm/fleet-auth.js +6 -1
- package/npm/fleet-build-data.js +109 -5
- package/npm/fleet-cleanup.js +232 -16
- package/npm/fleet-destroy-safety.js +272 -0
- package/npm/fleet-early-roster.js +87 -0
- package/npm/fleet-effect-poll.js +110 -0
- package/npm/fleet-effect-status.js +33 -32
- package/npm/fleet-env.js +86 -0
- package/npm/fleet-ghcr-secret.js +129 -0
- package/npm/fleet-git-maintenance.js +142 -0
- package/npm/fleet-host-op-limiter.js +68 -0
- package/npm/fleet-host.js +74 -0
- package/npm/fleet-iap-token.js +165 -0
- package/npm/fleet-job-badge.js +1 -1
- package/npm/fleet-launchd.js +171 -0
- package/npm/fleet-live-status.js +155 -0
- package/npm/fleet-memory.js +245 -9
- package/npm/fleet-plans.js +233 -4
- package/npm/fleet-pool.js +75 -16
- package/npm/fleet-prewarm.js +241 -0
- package/npm/fleet-project-drift.js +58 -0
- package/npm/fleet-public-url.js +394 -0
- package/npm/fleet-queue.js +95 -1
- package/npm/fleet-reachability-debounce.js +69 -0
- package/npm/fleet-reachability.js +80 -0
- package/npm/fleet-reconcile.js +43 -1
- package/npm/fleet-reset.js +330 -13
- package/npm/fleet-roster.js +6 -8
- package/npm/fleet-running-plan.js +85 -0
- package/npm/fleet-self-update.js +142 -0
- package/npm/fleet-session-token-cache.js +65 -0
- package/npm/fleet-setup-steps.js +112 -0
- package/npm/fleet-sleep-status.js +33 -0
- package/npm/fleet-status-refs.js +96 -0
- package/npm/fleet-strand.js +181 -0
- package/npm/fleet-stuck-status.js +117 -0
- package/npm/fleet-vm-access.js +404 -0
- package/npm/fleet-vm-probe-target.js +282 -0
- package/npm/fleet-vm-readiness.js +237 -0
- package/npm/fleet-vm-slugs.js +116 -0
- package/npm/google-ingress.js +226 -0
- package/npm/ingress-invoke.js +770 -0
- package/npm/launch-config.js +386 -9
- package/npm/scenario-check.js +880 -37
- package/npm/scenario-handlers.js +154 -30
- package/npm/scenario-interactivity.js +145 -6
- package/npm/scenario-issues.js +13 -1
- package/npm/scenario-metrics.js +116 -1
- package/npm/scenario-mocks.js +144 -21
- package/npm/scenario-playwright.js +649 -38
- package/npm/script-drift.js +152 -0
- package/npm/utils.js +365 -52
- package/npm/vm-urls.js +69 -8
- package/package.json +5 -5
- package/ui/dist/assets/ContainerInspector-DgM0pTAx.js +2 -0
- package/ui/dist/assets/EditorShell-B7Q06uJ_.js +252 -0
- package/ui/dist/assets/EditorShell-DP5UY20G.css +1 -0
- package/ui/dist/assets/{Markdown-BHBqz-Mj.js → Markdown-Chgdf3PW.js} +1 -1
- package/ui/dist/assets/ProjectLauncherScreen-BRrEr8Ng.js +1 -0
- package/ui/dist/assets/ScenarioDataPanel-BeQfa28g.js +9 -0
- package/ui/dist/assets/{ScenarioDataPanel-DYP7pi_n.css → ScenarioDataPanel-C1LukYI6.css} +1 -1
- package/ui/dist/assets/ScenarioDataPanelFullPage-BLqRCxQX.js +1 -0
- package/ui/dist/assets/ScenarioRenderer-CIAxfHPh.js +21 -0
- package/ui/dist/assets/StepsSimulator-CymGgfbu.js +1 -0
- package/ui/dist/assets/index-BygCiJSb.css +1 -0
- package/ui/dist/assets/{index-BuK0lizt.js → index-DCHqRrwO.js} +9 -9
- package/ui/dist/assets/{markdown-v0F0UEt3.js → markdown-C9tbsVHX.js} +1 -1
- package/ui/dist/assets/react-nrLBr15I.js +28 -0
- package/ui/dist/assets/useEvents-S3Qk1Y_d.js +1 -0
- package/ui/dist/assets/useServerIdentity-B7rI0u3O.js +1 -0
- package/ui/dist/codeyam-mark.svg +4 -0
- package/ui/dist/codeyam-wordmark.svg +5 -0
- package/ui/dist/index.html +4 -4
- package/ui/dist/open-source/codeyam-counter.png +0 -0
- package/ui/dist/open-source/el-carot.png +0 -0
- package/ui/dist/open-source/tabcommand.png +0 -0
- package/ui/dist/assets/ContainerInspector-C8EkH4PP.js +0 -2
- package/ui/dist/assets/EditorShell-tMMdarZD.js +0 -187
- package/ui/dist/assets/ScenarioDataPanel-xWYGZOpo.js +0 -5
- package/ui/dist/assets/ScenarioDataPanelFullPage-5G6KuZgw.js +0 -1
- package/ui/dist/assets/ScenarioRenderer-CSdhX1Ak.js +0 -1
- package/ui/dist/assets/StepsSimulator-CRtmlyNZ.js +0 -1
- package/ui/dist/assets/index-BGqairFV.css +0 -1
- package/ui/dist/assets/react-CSS0HapR.js +0 -28
- package/ui/dist/assets/useEvents-DggWrtHe.js +0 -1
|
@@ -0,0 +1,68 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
// Pure helpers for scripts/package-mcpb.js's darwin code-signing step.
|
|
3
|
+
//
|
|
4
|
+
// Dependency-free and unit-tested (npm/codesign-plan.test.js) so the packaging
|
|
5
|
+
// script keeps the sign decision + argv rendering out of its execSync path:
|
|
6
|
+
// signDarwinBinary stays thin boundary glue while every branch of "do we sign,
|
|
7
|
+
// and with which command" is exercised here without invoking codesign.
|
|
8
|
+
|
|
9
|
+
/**
|
|
10
|
+
* The `codesign` argv for a Developer ID signature with a secure timestamp.
|
|
11
|
+
* Pure — takes the identity and binary path, returns the flag list. `--force`
|
|
12
|
+
* replaces cargo's ad-hoc signature (codesign refuses an already-signed binary
|
|
13
|
+
* otherwise); `--timestamp` embeds the secure timestamp a later notarization
|
|
14
|
+
* needs.
|
|
15
|
+
*/
|
|
16
|
+
function codesignArgs(identity, binaryPath) {
|
|
17
|
+
return ["--force", "--timestamp", "--sign", identity, binaryPath];
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
/**
|
|
21
|
+
* The shell-quoted `codesign …` command string for signing `binaryPath` with
|
|
22
|
+
* `identity`. Pure: quotes each argv element so a path or identity with spaces
|
|
23
|
+
* survives the shell.
|
|
24
|
+
*/
|
|
25
|
+
function codesignCommand(identity, binaryPath) {
|
|
26
|
+
const quoted = codesignArgs(identity, binaryPath)
|
|
27
|
+
.map((a) => `"${a}"`)
|
|
28
|
+
.join(" ");
|
|
29
|
+
return `codesign ${quoted}`;
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
/**
|
|
33
|
+
* Decide whether/how to sign the staged darwin binary, given the target
|
|
34
|
+
* `platform`, the configured Developer ID `identity`, the `hostPlatform`
|
|
35
|
+
* (process.platform), and a `serverLabel` for the warning text. Pure — no I/O,
|
|
36
|
+
* no env, no clock — so the decision and its warnings are unit-testable.
|
|
37
|
+
*
|
|
38
|
+
* Returns `{ action: "sign", identity }` when a real Developer ID signature is
|
|
39
|
+
* possible, otherwise `{ action: "skip", warning? }`: the non-darwin target is a
|
|
40
|
+
* silent skip (its binary is not Mach-O), while a missing identity or a
|
|
41
|
+
* non-macOS host skips WITH a warning so an unsigned local bundle stays testable
|
|
42
|
+
* on the machine that built it rather than failing the build.
|
|
43
|
+
*/
|
|
44
|
+
function signPlan({ platform, identity, hostPlatform, serverLabel }) {
|
|
45
|
+
if (platform !== "darwin") {
|
|
46
|
+
return { action: "skip" };
|
|
47
|
+
}
|
|
48
|
+
if (!identity) {
|
|
49
|
+
return {
|
|
50
|
+
action: "skip",
|
|
51
|
+
warning:
|
|
52
|
+
`⚠️ ${serverLabel} is ad-hoc signed — set MCPB_SIGN_IDENTITY (or ` +
|
|
53
|
+
`APPLE_SIGNING_IDENTITY) to a Developer ID identity to sign it. An unsigned ` +
|
|
54
|
+
`binary has no stable Keychain identity and re-prompts for access.`,
|
|
55
|
+
};
|
|
56
|
+
}
|
|
57
|
+
if (hostPlatform !== "darwin") {
|
|
58
|
+
return {
|
|
59
|
+
action: "skip",
|
|
60
|
+
warning:
|
|
61
|
+
`⚠️ cannot codesign ${serverLabel} on ${hostPlatform} — ` +
|
|
62
|
+
`sign the darwin binary on a macOS host.`,
|
|
63
|
+
};
|
|
64
|
+
}
|
|
65
|
+
return { action: "sign", identity };
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
module.exports = { codesignArgs, codesignCommand, signPlan };
|
package/npm/container.js
CHANGED
|
@@ -35,6 +35,9 @@ async function main() {
|
|
|
35
35
|
14199,
|
|
36
36
|
);
|
|
37
37
|
|
|
38
|
+
// A cloud container's stdin is not a TTY, so the cross-project takeover prompt
|
|
39
|
+
// in `ensureServer` resolves to "abort" (print-and-exit) here — never a silent
|
|
40
|
+
// kill of another project's editor in a headless context.
|
|
38
41
|
const serverChild = await ensureServer(PORT);
|
|
39
42
|
const url = `http://localhost:${PORT}`;
|
|
40
43
|
console.error(`Container UI: ${url}`);
|
package/npm/disk-pressure.js
CHANGED
|
@@ -45,6 +45,63 @@ const PRUNABLE_CACHE_DIRS = [
|
|
|
45
45
|
'.angular/cache',
|
|
46
46
|
];
|
|
47
47
|
|
|
48
|
+
// The in-container mount of the persistent cargo build-target volume
|
|
49
|
+
// (`cargo-target:/codeyam-build-target`, set as CARGO_TARGET_DIR in
|
|
50
|
+
// docker-compose.cloud.template.yml). This is SEPARATE from PRUNABLE_CACHE_DIRS
|
|
51
|
+
// on purpose: the frontend-cache allowlist deliberately excludes Rust target/
|
|
52
|
+
// (expensive to rebuild), so it can never reclaim this volume — yet on a
|
|
53
|
+
// self-host VM that builds codeyam-editor, this volume is exactly what fills the
|
|
54
|
+
// disk (VM-3 grew it to ~69GB and wedged). The reclamation below is narrowly
|
|
55
|
+
// keyed on this known mount path, NOT folded into the generic allowlist, so a
|
|
56
|
+
// client project's own Rust target/ is never touched.
|
|
57
|
+
//
|
|
58
|
+
// Stack assumption: only self-host VMs that run cargo write to this volume;
|
|
59
|
+
// guest/client VMs (Next.js, Vite, Tauri, Python, …) carry an empty or absent
|
|
60
|
+
// volume, so cargoTargetReclaimPlan returns an empty plan there (a guaranteed
|
|
61
|
+
// no-op — honoring the stack-agnosticism rule in CLAUDE.md).
|
|
62
|
+
const CARGO_TARGET_DIR = '/codeyam-build-target';
|
|
63
|
+
|
|
64
|
+
// Sub-dirs of CARGO_TARGET_DIR that are ALWAYS safe to delete: pure coverage
|
|
65
|
+
// instrumentation, regenerated on every `llvm-cov` run. Reclaiming this costs
|
|
66
|
+
// the warm incremental build nothing (~13GB on VM-3). Pruned at WARN already.
|
|
67
|
+
const CARGO_TARGET_ALWAYS_PRUNE = ['llvm-cov-target'];
|
|
68
|
+
|
|
69
|
+
// Sub-trees reclaimed ONLY at CRITICAL and ONLY when no build is in flight:
|
|
70
|
+
// `debug/` is the incremental compile cache that keeps `rebuild-self` fast, so
|
|
71
|
+
// deleting it forces a ~14-min cold recompile. We pay that cost only when the
|
|
72
|
+
// disk is genuinely critical AND nothing is mid-build (else we'd sabotage an
|
|
73
|
+
// in-flight compile to free space — the failure mode this tiering avoids).
|
|
74
|
+
const CARGO_TARGET_CRITICAL_CLEAN = ['debug'];
|
|
75
|
+
|
|
76
|
+
// Pure decision function for tiered cargo-target reclamation. Given the current
|
|
77
|
+
// pressure `level`, whether a build is in flight, and whether this VM actually
|
|
78
|
+
// has the cargo-target volume, return which sub-dirs to prune and whether the
|
|
79
|
+
// heavy `debug` clean was deferred (so the caller can log it — no silent cap).
|
|
80
|
+
//
|
|
81
|
+
// Tiers (mirrored by the bash guard + watchdog):
|
|
82
|
+
// - no cargo-target (guest/client VM) → prune nothing (total no-op)
|
|
83
|
+
// - warn → ALWAYS_PRUNE only (llvm-cov)
|
|
84
|
+
// - critical & idle → ALWAYS_PRUNE + CRITICAL_CLEAN
|
|
85
|
+
// - critical & build in flight → ALWAYS_PRUNE only, defer heavy
|
|
86
|
+
// - ok / unknown level → fail-safe: prune nothing
|
|
87
|
+
function cargoTargetReclaimPlan({ level, buildInFlight, hasCargoTarget } = {}) {
|
|
88
|
+
if (!hasCargoTarget) return { prune: [], deferredHeavyClean: false };
|
|
89
|
+
if (level === 'warn') {
|
|
90
|
+
return { prune: [...CARGO_TARGET_ALWAYS_PRUNE], deferredHeavyClean: false };
|
|
91
|
+
}
|
|
92
|
+
if (level === 'critical') {
|
|
93
|
+
if (buildInFlight) {
|
|
94
|
+
return { prune: [...CARGO_TARGET_ALWAYS_PRUNE], deferredHeavyClean: true };
|
|
95
|
+
}
|
|
96
|
+
return {
|
|
97
|
+
prune: [...CARGO_TARGET_ALWAYS_PRUNE, ...CARGO_TARGET_CRITICAL_CLEAN],
|
|
98
|
+
deferredHeavyClean: false,
|
|
99
|
+
};
|
|
100
|
+
}
|
|
101
|
+
// 'ok', undefined, or any unknown level → do nothing (fail safe).
|
|
102
|
+
return { prune: [], deferredHeavyClean: false };
|
|
103
|
+
}
|
|
104
|
+
|
|
48
105
|
// Classify an integer disk-usage percent into a pressure level. A non-numeric,
|
|
49
106
|
// non-finite, or out-of-range (<0 or >100) value is treated as a BAD READING
|
|
50
107
|
// and classified 'ok' with no action — a misread must never escalate to a
|
|
@@ -80,10 +137,31 @@ function parseDfUsedPct(dfOutput) {
|
|
|
80
137
|
return null;
|
|
81
138
|
}
|
|
82
139
|
|
|
140
|
+
// Classify a disk-guard sidecar age (updatedAt vs current time).
|
|
141
|
+
// If the sidecar is older than 600s (10 min), classify it as stale.
|
|
142
|
+
function classifySidecarAge(updatedAt, currentEpoch) {
|
|
143
|
+
if (typeof updatedAt !== 'number' || typeof currentEpoch !== 'number') return 'stale';
|
|
144
|
+
const age = currentEpoch - updatedAt;
|
|
145
|
+
if (age > 600) return 'stale';
|
|
146
|
+
return 'healthy';
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
// Map or classify a general disk state level including 'unknown-exec-failed'
|
|
150
|
+
function mapDiskStateLevel(usedPct, hasExecError) {
|
|
151
|
+
if (hasExecError) return 'unknown-exec-failed';
|
|
152
|
+
return classifyDiskPressure(usedPct);
|
|
153
|
+
}
|
|
154
|
+
|
|
83
155
|
module.exports = {
|
|
84
156
|
DISK_WARN_PCT,
|
|
85
157
|
DISK_CRITICAL_PCT,
|
|
86
158
|
PRUNABLE_CACHE_DIRS,
|
|
159
|
+
CARGO_TARGET_DIR,
|
|
160
|
+
CARGO_TARGET_ALWAYS_PRUNE,
|
|
161
|
+
CARGO_TARGET_CRITICAL_CLEAN,
|
|
162
|
+
cargoTargetReclaimPlan,
|
|
87
163
|
classifyDiskPressure,
|
|
88
164
|
parseDfUsedPct,
|
|
165
|
+
classifySidecarAge,
|
|
166
|
+
mapDiskStateLevel,
|
|
89
167
|
};
|
package/npm/editor-dev.js
CHANGED
|
@@ -28,6 +28,7 @@
|
|
|
28
28
|
|
|
29
29
|
const { execSync, spawn, spawnSync } = require("child_process");
|
|
30
30
|
const path = require("path");
|
|
31
|
+
const os = require("os");
|
|
31
32
|
const {
|
|
32
33
|
ensureServer,
|
|
33
34
|
restartServer,
|
|
@@ -89,6 +90,62 @@ function buildBinary() {
|
|
|
89
90
|
}
|
|
90
91
|
}
|
|
91
92
|
|
|
93
|
+
/**
|
|
94
|
+
* Build the React UI from the editor repo into ui/dist/. A failed build is
|
|
95
|
+
* non-fatal: log a warning and continue on the last-good bundle so a transient
|
|
96
|
+
* vite failure doesn't block launch. The cargo build embeds ui/dist/ at compile
|
|
97
|
+
* time via include_dir! (crates/control-api/src/embedded_ui.rs), so this MUST
|
|
98
|
+
* run before buildBinary() or the binary embeds the *previous* launch's bundle.
|
|
99
|
+
*/
|
|
100
|
+
function buildUI() {
|
|
101
|
+
console.error("Building UI...");
|
|
102
|
+
try {
|
|
103
|
+
execSync("npx vite build", { stdio: "inherit", cwd: path.join(rootDir, "ui") });
|
|
104
|
+
} catch {
|
|
105
|
+
console.error("Failed to build UI. Continuing with existing build (if any)...");
|
|
106
|
+
}
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
/**
|
|
110
|
+
* Build the launcher artifacts in embed-correct order: UI first (so the fresh
|
|
111
|
+
* ui/dist/ is on disk), then the Rust binary (which embeds that dist via
|
|
112
|
+
* include_dir!). build.rs declares `cargo:rerun-if-changed=ui/dist`, so a fresh
|
|
113
|
+
* bundle on disk before compilation forces cargo to recompile and embed it in
|
|
114
|
+
* the same launch — fixing the one-generation-stale UI bug. Only the launcher
|
|
115
|
+
* (no-subcommand) path calls this; the passthrough path rebuilds only the Rust
|
|
116
|
+
* binary and embeds no fresh UI, which is correct for non-launcher commands.
|
|
117
|
+
* Side-effecting steps are injectable so the ordering can be unit-tested.
|
|
118
|
+
*/
|
|
119
|
+
function buildLauncherArtifacts(deps = {}) {
|
|
120
|
+
const {
|
|
121
|
+
buildUI: doBuildUI = buildUI,
|
|
122
|
+
buildBinary: doBuildBinary = buildBinary,
|
|
123
|
+
enforceFreshBinary: doEnforceFreshBinary = enforceFreshBinary,
|
|
124
|
+
} = deps;
|
|
125
|
+
// UI build FIRST — the fresh bundle must be on disk before cargo embeds it.
|
|
126
|
+
doBuildUI();
|
|
127
|
+
doEnforceFreshBinary(doBuildBinary(), {
|
|
128
|
+
action: "run editor commands",
|
|
129
|
+
continueVerb: "launching with",
|
|
130
|
+
});
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
/**
|
|
134
|
+
* Rebuild artifacts for the passthrough path (a known subcommand like
|
|
135
|
+
* `editor step`/`advance`): the Rust binary ONLY, no UI build. Non-launcher
|
|
136
|
+
* commands never serve a fresh UI, so embedding a rebuilt bundle would be
|
|
137
|
+
* wasted work — and pulling vite into every CLI invocation would slow the
|
|
138
|
+
* common `editor advance` round-trip. Side-effecting steps are injectable so
|
|
139
|
+
* the "no UI build on passthrough" contract is unit-testable.
|
|
140
|
+
*/
|
|
141
|
+
function buildPassthroughArtifacts(deps = {}) {
|
|
142
|
+
const {
|
|
143
|
+
buildBinary: doBuildBinary = buildBinary,
|
|
144
|
+
enforceFreshBinary: doEnforceFreshBinary = enforceFreshBinary,
|
|
145
|
+
} = deps;
|
|
146
|
+
doEnforceFreshBinary(doBuildBinary(), { action: "run editor commands" });
|
|
147
|
+
}
|
|
148
|
+
|
|
92
149
|
/**
|
|
93
150
|
* Decide whether the dev wrapper should rebuild the Rust binary (and the UI,
|
|
94
151
|
* for the launcher path) before forwarding to it. The answer is always yes:
|
|
@@ -114,6 +171,32 @@ function shouldRebuildOnPassthrough() {
|
|
|
114
171
|
return true;
|
|
115
172
|
}
|
|
116
173
|
|
|
174
|
+
/**
|
|
175
|
+
* Translate a passthrough child's exit into the code the dev wrapper should
|
|
176
|
+
* forward. A subcommand passthrough must report the inner editor binary's OWN
|
|
177
|
+
* outcome faithfully so every caller (gates, scripts, the agent) can trust the
|
|
178
|
+
* exit code:
|
|
179
|
+
*
|
|
180
|
+
* - A real exit code (including 0) is forwarded verbatim — a successful
|
|
181
|
+
* `analyze-imports` that printed valid JSON and exited 0 stays exit 0,
|
|
182
|
+
* never a spurious 1.
|
|
183
|
+
* - A null/undefined code means the child was terminated by a signal (e.g.
|
|
184
|
+
* SIGPIPE after it already produced its output, or SIGTERM at teardown).
|
|
185
|
+
* Report it distinctly via the conventional 128+signum so a clean run that
|
|
186
|
+
* was merely signalled at the end is not collapsed into a generic `1` that
|
|
187
|
+
* reads as a command failure. The previous `code ?? 1` erased that
|
|
188
|
+
* distinction, surfacing a successful inner command as exit 1.
|
|
189
|
+
*
|
|
190
|
+
* Pure (no process.exit) so the mapping is unit-testable.
|
|
191
|
+
*/
|
|
192
|
+
function passthroughExitCode(code, signal) {
|
|
193
|
+
if (code !== null && code !== undefined) {
|
|
194
|
+
return code;
|
|
195
|
+
}
|
|
196
|
+
const signum = signal ? os.constants.signals[signal] : undefined;
|
|
197
|
+
return signum ? 128 + signum : 1;
|
|
198
|
+
}
|
|
199
|
+
|
|
117
200
|
/**
|
|
118
201
|
* Returns true when the merged project config has a non-empty start
|
|
119
202
|
* command — either at `apps[0].startCommand` (per-app, preferred) or at
|
|
@@ -250,7 +333,11 @@ function shouldOpenBrowser({ shouldRestart, wasRunning, freshInit }) {
|
|
|
250
333
|
module.exports = {
|
|
251
334
|
hasSubcommand,
|
|
252
335
|
buildBinary,
|
|
336
|
+
buildUI,
|
|
337
|
+
buildLauncherArtifacts,
|
|
338
|
+
buildPassthroughArtifacts,
|
|
253
339
|
shouldRebuildOnPassthrough,
|
|
340
|
+
passthroughExitCode,
|
|
254
341
|
staleBinaryBanner,
|
|
255
342
|
hasStartCommand,
|
|
256
343
|
planPreflightAction,
|
|
@@ -268,7 +355,7 @@ if (require.main === module) {
|
|
|
268
355
|
// exactly when the developer wants the new code live. `ensureBinary()`
|
|
269
356
|
// still builds once if no binary exists at all.
|
|
270
357
|
if (shouldRebuildOnPassthrough()) {
|
|
271
|
-
|
|
358
|
+
buildPassthroughArtifacts();
|
|
272
359
|
}
|
|
273
360
|
const binary = ensureBinary();
|
|
274
361
|
const child = spawn(binary, args, {
|
|
@@ -276,8 +363,11 @@ if (require.main === module) {
|
|
|
276
363
|
cwd: process.cwd(),
|
|
277
364
|
env: process.env,
|
|
278
365
|
});
|
|
279
|
-
child.on("exit", (code) => {
|
|
280
|
-
|
|
366
|
+
child.on("exit", (code, signal) => {
|
|
367
|
+
// Forward the inner editor binary's OWN outcome faithfully — exit 0
|
|
368
|
+
// stays 0, a signal-terminated child reports 128+signum, not a generic
|
|
369
|
+
// 1 that would mask a successful command (see passthroughExitCode).
|
|
370
|
+
process.exit(passthroughExitCode(code, signal));
|
|
281
371
|
});
|
|
282
372
|
} else {
|
|
283
373
|
// --- Launcher mode: build everything, start server, open browser ---
|
|
@@ -352,18 +442,9 @@ if (require.main === module) {
|
|
|
352
442
|
// client-project launch is never disrupted. `ensureBinary()` still
|
|
353
443
|
// builds once if no binary exists.
|
|
354
444
|
if (shouldRebuildOnPassthrough()) {
|
|
355
|
-
|
|
356
|
-
|
|
357
|
-
|
|
358
|
-
});
|
|
359
|
-
|
|
360
|
-
// Build the UI from the editor repo
|
|
361
|
-
console.error("Building UI...");
|
|
362
|
-
try {
|
|
363
|
-
execSync("npx vite build", { stdio: "inherit", cwd: path.join(rootDir, "ui") });
|
|
364
|
-
} catch {
|
|
365
|
-
console.error("Failed to build UI. Continuing with existing build (if any)...");
|
|
366
|
-
}
|
|
445
|
+
// Embed-correct order: UI build → cargo build (embeds the fresh
|
|
446
|
+
// ui/dist via include_dir!). See buildLauncherArtifacts.
|
|
447
|
+
buildLauncherArtifacts();
|
|
367
448
|
}
|
|
368
449
|
|
|
369
450
|
const serverChild = shouldRestart
|
package/npm/editor.js
CHANGED
|
@@ -28,8 +28,9 @@ const {
|
|
|
28
28
|
resolveLauncherPort,
|
|
29
29
|
resolveWrappedAppPort,
|
|
30
30
|
classifyPortHolder,
|
|
31
|
+
fetchServerIdentity,
|
|
31
32
|
formatForeignListenerError,
|
|
32
|
-
|
|
33
|
+
resolveCrossProjectCollision,
|
|
33
34
|
staleBinaryBanner,
|
|
34
35
|
enforceFreshBinary,
|
|
35
36
|
} = require("./utils");
|
|
@@ -47,6 +48,58 @@ function buildBinary() {
|
|
|
47
48
|
}
|
|
48
49
|
}
|
|
49
50
|
|
|
51
|
+
// Build the React UI into ui/dist/. A failed build is non-fatal: we log a
|
|
52
|
+
// warning and continue on the last-good bundle (a transient vite failure must
|
|
53
|
+
// not block launch). The cargo build embeds ui/dist/ at compile time via
|
|
54
|
+
// include_dir! (crates/control-api/src/embedded_ui.rs), so this MUST run
|
|
55
|
+
// before buildBinary() or the binary embeds the *previous* launch's bundle.
|
|
56
|
+
function buildUI() {
|
|
57
|
+
console.error("Building UI...");
|
|
58
|
+
try {
|
|
59
|
+
execSync("npx vite build", {
|
|
60
|
+
stdio: "inherit",
|
|
61
|
+
cwd: path.join(rootDir, "ui"),
|
|
62
|
+
});
|
|
63
|
+
} catch {
|
|
64
|
+
console.error("Failed to build UI. Continuing with existing build (if any)...");
|
|
65
|
+
}
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
// Build the launch artifacts in embed-correct order: UI first (so the fresh
|
|
69
|
+
// ui/dist/ is on disk), then the Rust binary (which embeds that dist via
|
|
70
|
+
// include_dir!), then install the freshly-compiled binary. build.rs declares
|
|
71
|
+
// `cargo:rerun-if-changed=ui/dist`, so a fresh bundle on disk before
|
|
72
|
+
// compilation forces cargo to recompile and embed it in the same launch —
|
|
73
|
+
// fixing the one-generation-stale UI bug. Side-effecting steps are injectable
|
|
74
|
+
// so the ordering can be unit-tested without shelling out to vite/cargo.
|
|
75
|
+
function buildArtifacts(deps = {}) {
|
|
76
|
+
const {
|
|
77
|
+
buildUI: doBuildUI = buildUI,
|
|
78
|
+
buildBinary: doBuildBinary = buildBinary,
|
|
79
|
+
enforceFreshBinary: doEnforceFreshBinary = enforceFreshBinary,
|
|
80
|
+
installBinary: doInstallBinary = installBinary,
|
|
81
|
+
} = deps;
|
|
82
|
+
|
|
83
|
+
// UI build FIRST — the fresh bundle must be on disk before cargo embeds it.
|
|
84
|
+
doBuildUI();
|
|
85
|
+
// Rebuild the Rust binary so code changes (and the just-built UI) are picked
|
|
86
|
+
// up on restart. This script is only invoked from a source checkout (via
|
|
87
|
+
// `npm run editor`), so Cargo.toml and the crates tree are always present.
|
|
88
|
+
const buildOk = doBuildBinary();
|
|
89
|
+
// A failed rebuild means the server would boot on a stale binary that no
|
|
90
|
+
// longer matches the source. Refuse by default; CODEYAM_ALLOW_STALE=1 forces
|
|
91
|
+
// the old behavior (continue with the last-good install).
|
|
92
|
+
doEnforceFreshBinary(buildOk, { action: "start the editor server" });
|
|
93
|
+
// Refresh <cargo_bin>/codeyam-editor (and the pty-broker hardlink) so the
|
|
94
|
+
// broker auto-spawn execs a binary that knows the `pty-broker daemon`
|
|
95
|
+
// subcommand. Skip if the build failed — installing a stale debug binary
|
|
96
|
+
// over a working install is worse than leaving it.
|
|
97
|
+
if (buildOk) {
|
|
98
|
+
doInstallBinary();
|
|
99
|
+
}
|
|
100
|
+
return buildOk;
|
|
101
|
+
}
|
|
102
|
+
|
|
50
103
|
// Resolve <CARGO_HOME>/bin (falling back to $HOME/.cargo/bin). Mirrors
|
|
51
104
|
// `installed_binary_path()` in crates/control-api/src/pty_broker_startup.rs
|
|
52
105
|
// — the Rust broker auto-spawn execs the binary at this path, so we MUST
|
|
@@ -181,32 +234,9 @@ async function main() {
|
|
|
181
234
|
// VITE_PORT + 1. We need to wait for the real Vite server, not the proxy.
|
|
182
235
|
const VITE_INTERNAL_PORT = VITE_PORT + 1;
|
|
183
236
|
|
|
184
|
-
//
|
|
185
|
-
//
|
|
186
|
-
|
|
187
|
-
const buildOk = buildBinary();
|
|
188
|
-
// A failed rebuild means the server would boot on a stale binary that no
|
|
189
|
-
// longer matches the source. Refuse by default; CODEYAM_ALLOW_STALE=1
|
|
190
|
-
// forces the old behavior (continue with the last-good install).
|
|
191
|
-
enforceFreshBinary(buildOk, { action: "start the editor server" });
|
|
192
|
-
// Refresh <cargo_bin>/codeyam-editor (and the pty-broker hardlink) so
|
|
193
|
-
// the broker auto-spawn execs a binary that knows the `pty-broker
|
|
194
|
-
// daemon` subcommand. Skip if the build failed — installing a stale
|
|
195
|
-
// debug binary over a working install is worse than leaving it.
|
|
196
|
-
if (buildOk) {
|
|
197
|
-
installBinary();
|
|
198
|
-
}
|
|
199
|
-
|
|
200
|
-
// Build the UI so the backend can serve it as static files
|
|
201
|
-
console.error("Building UI...");
|
|
202
|
-
try {
|
|
203
|
-
execSync("npx vite build", {
|
|
204
|
-
stdio: "inherit",
|
|
205
|
-
cwd: path.join(rootDir, "ui"),
|
|
206
|
-
});
|
|
207
|
-
} catch {
|
|
208
|
-
console.error("Failed to build UI. Continuing with existing build (if any)...");
|
|
209
|
-
}
|
|
237
|
+
// Build the launch artifacts in embed-correct order: UI build → cargo build
|
|
238
|
+
// (embeds the fresh ui/dist via include_dir!) → install. See buildArtifacts.
|
|
239
|
+
buildArtifacts();
|
|
210
240
|
|
|
211
241
|
// Start the Rust backend — it serves ui/dist/ and launches the Vite dev
|
|
212
242
|
// server (configured via startCommand in editor.json) for Live Preview HMR.
|
|
@@ -223,8 +253,12 @@ async function main() {
|
|
|
223
253
|
process.exit(1);
|
|
224
254
|
}
|
|
225
255
|
if (holder.kind === "cross-project") {
|
|
226
|
-
|
|
227
|
-
|
|
256
|
+
const outcome = await resolveCrossProjectCollision(SERVER_PORT, holder, "restart");
|
|
257
|
+
if (outcome.action !== "took-over") {
|
|
258
|
+
// "abort" (non-TTY) or "declined": resolver already printed the message.
|
|
259
|
+
process.exit(1);
|
|
260
|
+
}
|
|
261
|
+
// took-over: stopServer freed the port; fall through to the fresh-boot path.
|
|
228
262
|
}
|
|
229
263
|
if (holder.kind === "same-project") {
|
|
230
264
|
console.error(`Stopping existing server on port ${SERVER_PORT}...`);
|
|
@@ -237,17 +271,29 @@ async function main() {
|
|
|
237
271
|
}
|
|
238
272
|
const serverChild = await ensureServer(SERVER_PORT);
|
|
239
273
|
|
|
240
|
-
//
|
|
241
|
-
//
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
274
|
+
// With 2+ projects registered the launch diverts to the Project Launcher
|
|
275
|
+
// (selector) on SERVER_PORT — there is no wrapped app, so the Vite wait and
|
|
276
|
+
// the "Live Preview" line don't apply. Probe the identity to detect it;
|
|
277
|
+
// best-effort, a missed detection only yields a harmless "Vite not ready"
|
|
278
|
+
// warning, never a failure.
|
|
279
|
+
const identity = await fetchServerIdentity(SERVER_PORT);
|
|
280
|
+
const launcherMode = Boolean(identity && identity.role === "launcher");
|
|
281
|
+
|
|
282
|
+
if (!launcherMode) {
|
|
283
|
+
// Wait for the actual Vite dev server on the internal port (not the reverse
|
|
284
|
+
// proxy on VITE_PORT, which starts immediately with the Rust binary).
|
|
285
|
+
console.error("Waiting for Vite dev server...");
|
|
286
|
+
const viteReady = await waitForPort(VITE_INTERNAL_PORT, 15000, 300);
|
|
287
|
+
if (!viteReady) {
|
|
288
|
+
console.error(`Warning: Vite dev server not ready on port ${VITE_INTERNAL_PORT}. Live Preview may not work initially.`);
|
|
289
|
+
}
|
|
246
290
|
}
|
|
247
291
|
|
|
248
292
|
const url = `http://localhost:${SERVER_PORT}`;
|
|
249
|
-
console.error(`Editor UI: ${url}`);
|
|
250
|
-
|
|
293
|
+
console.error(launcherMode ? `Project selector: ${url}` : `Editor UI: ${url}`);
|
|
294
|
+
if (!launcherMode) {
|
|
295
|
+
console.error(`Live Preview: http://localhost:${VITE_PORT}`);
|
|
296
|
+
}
|
|
251
297
|
console.error(`Logs: ${path.join(rootDir, ".codeyam", "logs")}`);
|
|
252
298
|
open(url);
|
|
253
299
|
const keepAlive = setInterval(() => {}, 60_000);
|
|
@@ -271,6 +317,8 @@ async function main() {
|
|
|
271
317
|
|
|
272
318
|
module.exports = {
|
|
273
319
|
buildBinary,
|
|
320
|
+
buildUI,
|
|
321
|
+
buildArtifacts,
|
|
274
322
|
staleBinaryBanner,
|
|
275
323
|
cargoBinDir,
|
|
276
324
|
installBinary,
|
package/npm/empty-project-dir.js
CHANGED
|
@@ -1,13 +1,14 @@
|
|
|
1
1
|
"use strict";
|
|
2
2
|
|
|
3
3
|
/**
|
|
4
|
-
* Launcher-side
|
|
4
|
+
* Launcher-side mirrors of `is_empty_project_dir` and
|
|
5
|
+
* `looks_like_onboardable_project` in
|
|
5
6
|
* `crates/codeyam-editor/src/commands/init.rs`. The Rust side decides
|
|
6
|
-
* which `/codeyam-…` skill the init binary points at;
|
|
7
|
-
*
|
|
8
|
-
*
|
|
9
|
-
* `
|
|
10
|
-
*
|
|
7
|
+
* which `/codeyam-…` skill the init binary points at; these JS mirrors
|
|
8
|
+
* decide the same for the launcher's post-launch tail banner. Keep both
|
|
9
|
+
* predicates in sync with Rust — the test `empty-project-dir.test.js`
|
|
10
|
+
* and Rust's `is_empty_project_dir_*` / `looks_like_onboardable_project_*`
|
|
11
|
+
* tests pin the contracts.
|
|
11
12
|
*
|
|
12
13
|
* A "fresh" project directory has no non-hidden entries — only things
|
|
13
14
|
* like `.git/`, `.gitignore`, or other dotfiles. Hidden entries don't
|
|
@@ -19,6 +20,7 @@
|
|
|
19
20
|
*/
|
|
20
21
|
|
|
21
22
|
const fs = require("node:fs");
|
|
23
|
+
const path = require("node:path");
|
|
22
24
|
|
|
23
25
|
function isEmptyProjectDir(projectDir) {
|
|
24
26
|
let entries;
|
|
@@ -30,4 +32,87 @@ function isEmptyProjectDir(projectDir) {
|
|
|
30
32
|
return entries.every((name) => name.startsWith("."));
|
|
31
33
|
}
|
|
32
34
|
|
|
33
|
-
|
|
35
|
+
// Root files that signal a real project: dependency/build manifests,
|
|
36
|
+
// lockfiles, and the static-site `index.html`. Mirror of the Rust
|
|
37
|
+
// `MANIFEST_MARKERS` list.
|
|
38
|
+
const MANIFEST_MARKERS = new Set([
|
|
39
|
+
"package.json",
|
|
40
|
+
"Cargo.toml",
|
|
41
|
+
"pyproject.toml",
|
|
42
|
+
"requirements.txt",
|
|
43
|
+
"setup.py",
|
|
44
|
+
"go.mod",
|
|
45
|
+
"Gemfile",
|
|
46
|
+
"pom.xml",
|
|
47
|
+
"build.gradle",
|
|
48
|
+
"build.gradle.kts",
|
|
49
|
+
"composer.json",
|
|
50
|
+
"pubspec.yaml",
|
|
51
|
+
"Package.swift",
|
|
52
|
+
"mix.exs",
|
|
53
|
+
"deno.json",
|
|
54
|
+
"CMakeLists.txt",
|
|
55
|
+
"Makefile",
|
|
56
|
+
"tsconfig.json",
|
|
57
|
+
"index.html",
|
|
58
|
+
]);
|
|
59
|
+
|
|
60
|
+
// Subdirectories that signal an organized source tree. Mirror of the Rust
|
|
61
|
+
// `SOURCE_DIR_MARKERS` list.
|
|
62
|
+
const SOURCE_DIR_MARKERS = new Set([
|
|
63
|
+
"src",
|
|
64
|
+
"app",
|
|
65
|
+
"lib",
|
|
66
|
+
"pages",
|
|
67
|
+
"components",
|
|
68
|
+
"crates",
|
|
69
|
+
"packages",
|
|
70
|
+
"source",
|
|
71
|
+
"include",
|
|
72
|
+
]);
|
|
73
|
+
|
|
74
|
+
/**
|
|
75
|
+
* Does this directory contain a *project* worth onboarding, as opposed to
|
|
76
|
+
* being empty or holding only a stray file or two? Mirror of Rust
|
|
77
|
+
* `looks_like_onboardable_project`. A folder "contains a project" when it
|
|
78
|
+
* holds a recognized manifest / lockfile, a `.csproj` / `.xcodeproj`, a root
|
|
79
|
+
* `index.html`, OR an organized source subdirectory. Intentionally
|
|
80
|
+
* marker-based, not file-count based: a home dir full of loose files is still
|
|
81
|
+
* not a project. A read error returns false (not a project → the
|
|
82
|
+
* auto-init/open path), matching Rust's `Err(_) => false`.
|
|
83
|
+
*/
|
|
84
|
+
function looksLikeOnboardableProject(projectDir) {
|
|
85
|
+
let entries;
|
|
86
|
+
try {
|
|
87
|
+
entries = fs.readdirSync(projectDir, { withFileTypes: true });
|
|
88
|
+
} catch {
|
|
89
|
+
return false;
|
|
90
|
+
}
|
|
91
|
+
for (const entry of entries) {
|
|
92
|
+
const name = entry.name;
|
|
93
|
+
if (MANIFEST_MARKERS.has(name)) {
|
|
94
|
+
return true;
|
|
95
|
+
}
|
|
96
|
+
if (name.endsWith(".csproj") || name.endsWith(".xcodeproj")) {
|
|
97
|
+
return true;
|
|
98
|
+
}
|
|
99
|
+
// Source-tree markers only count as directories, not like-named files.
|
|
100
|
+
if (SOURCE_DIR_MARKERS.has(name)) {
|
|
101
|
+
let isDir = entry.isDirectory();
|
|
102
|
+
// A symlinked source dir surfaces as a symlink Dirent; resolve it.
|
|
103
|
+
if (!isDir && entry.isSymbolicLink()) {
|
|
104
|
+
try {
|
|
105
|
+
isDir = fs.statSync(path.join(projectDir, name)).isDirectory();
|
|
106
|
+
} catch {
|
|
107
|
+
isDir = false;
|
|
108
|
+
}
|
|
109
|
+
}
|
|
110
|
+
if (isDir) {
|
|
111
|
+
return true;
|
|
112
|
+
}
|
|
113
|
+
}
|
|
114
|
+
}
|
|
115
|
+
return false;
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
module.exports = { isEmptyProjectDir, looksLikeOnboardableProject };
|