@ejunz/ea-core 0.1.2-rc.5 → 0.1.3

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.
@@ -0,0 +1,20 @@
1
+ import * as ToolHost from "@ejunz/ea-core/tool-host";
2
+ import { createHostToolBridge, createHostToolClient } from "@ejunz/ea-core/tool-host";
3
+ //#region src/service/host-tools.ts
4
+ /**
5
+ * Publish the host-backed services and mount the host tool set.
6
+ *
7
+ * The session-persistence and storage providers read the same host through
8
+ * their own rows, so this installs only the tool plane and the per-session
9
+ * domain runtime the model plane resolves.
10
+ * @param ctx - the booting application root context.
11
+ * @param config - the host origin and the token it issued for this instance.
12
+ */
13
+ async function installHostToolBridge(ctx, config) {
14
+ const bridge = createHostToolBridge(config.origin, config.token);
15
+ ctx.provide("llmDomainRuntime", { resolve: (sessionId, signal) => bridge.runtime(sessionId, signal) });
16
+ ctx.provide("hostProvider", createHostToolClient(bridge));
17
+ await ctx.plugin(ToolHost);
18
+ }
19
+ //#endregion
20
+ export { installHostToolBridge as t };
@@ -0,0 +1,129 @@
1
+ import { t as INSTALL_ANCHOR } from "./profile-boot-BqakeoCE.js";
2
+ import { existsSync } from "node:fs";
3
+ import { DEFAULT_PROFILE_BUNDLES, PROFILE_TEMPLATES, initProfile, readProfileManifest, resolveBundleDir, resolveProfileDir, writeProfileManifest } from "@ejunz/ea-core/app-boot";
4
+ import { join, resolve } from "node:path";
5
+ import { spawnSync } from "node:child_process";
6
+ //#region src/commands/plugin.ts
7
+ /**
8
+ * `ea plugin --profile <name> <args...>` — profile plugin management as a
9
+ * thin yarn forwarder: initialize the profile on first use, run
10
+ * `yarn <args...>` in the profile directory, then reconcile the
11
+ * `ea.profile.bundles` layer list against the installed state (a dependency
12
+ * resolving to a package that declares `ea.bundle` joins the layer stack; a
13
+ * removed or bundle-less dependency leaves it). Reconciling by installed
14
+ * state, not by dependency diff, means `update` activates a package that
15
+ * gained its `ea.bundle` declaration in a newer version.
16
+ * @module @ejunz/ea-core/plugin
17
+ */
18
+ const NAME = "ea";
19
+ /**
20
+ * Whether a resolved dependency exports a profile patch, i.e. is a bundle.
21
+ * @param packageName - the dependency's package name.
22
+ * @param profileDir - the profile directory (resolution anchor).
23
+ * @returns true when the package manifest declares `ea.bundle`.
24
+ */
25
+ function exportsPatch(packageName, profileDir) {
26
+ let dir;
27
+ try {
28
+ dir = resolveBundleDir(NAME, packageName, INSTALL_ANCHOR, profileDir);
29
+ } catch {
30
+ return false;
31
+ }
32
+ return readProfileManifest(NAME, dir).ea?.bundle?.patch !== void 0;
33
+ }
34
+ /**
35
+ * Reconcile `ea.profile.bundles` against the installed state: yarn has
36
+ * already written the real installed names (so a git/path/tarball/alias spec
37
+ * on the command line reconciles by its true package name) and materialized
38
+ * the packages. A dependency that resolves to a `ea.bundle`-declaring
39
+ * package joins the layer stack (appended in dependency order); a
40
+ * dependency-listed name that no longer does — removed, or the installed
41
+ * version dropped the declaration — leaves it. In-box bundles from the
42
+ * profile template are not dependencies and are never touched. Warns once
43
+ * per newly-added bundle-less dependency (a plain library is fine; the
44
+ * warning is orientation).
45
+ */
46
+ function reconcilePlugins(before, profileDir) {
47
+ const after = readProfileManifest(NAME, profileDir);
48
+ const beforeDeps = new Set(Object.keys(before.dependencies ?? {}));
49
+ const dependencies = Object.keys(after.dependencies ?? {});
50
+ const plugins = after.ea?.profile?.bundles ?? [];
51
+ let changed = false;
52
+ for (const packageName of dependencies) {
53
+ const isBundle = exportsPatch(packageName, profileDir);
54
+ if (isBundle && !plugins.includes(packageName)) {
55
+ plugins.push(packageName);
56
+ changed = true;
57
+ } else if (!isBundle && !beforeDeps.has(packageName)) process.stderr.write(`${NAME}: warning: ${packageName} declares no ea.bundle — installed as a plain dependency, not a profile layer (a later update that gains one activates it automatically)
58
+ `);
59
+ }
60
+ const dependencySet = new Set(dependencies);
61
+ for (const packageName of [...plugins]) {
62
+ const wasDependency = beforeDeps.has(packageName) || dependencySet.has(packageName);
63
+ const stillBundle = dependencySet.has(packageName) && exportsPatch(packageName, profileDir);
64
+ if (wasDependency && !stillBundle) {
65
+ plugins.splice(plugins.indexOf(packageName), 1);
66
+ changed = true;
67
+ }
68
+ }
69
+ if (!changed) return;
70
+ after.ea = {
71
+ ...after.ea,
72
+ profile: {
73
+ ...after.ea?.profile,
74
+ bundles: plugins
75
+ }
76
+ };
77
+ writeProfileManifest(profileDir, after);
78
+ }
79
+ /**
80
+ * Rewrite relative filesystem specs against the user's invoking directory.
81
+ * yarn runs with cwd = the profile directory, so a bare `.` or `../plugin`
82
+ * (or their `file:`/`link:` forms) would silently resolve inside the profile
83
+ * — `add .` from a plugin checkout would self-link the profile. Absolute
84
+ * specs, registry names, and every other yarn argument pass through
85
+ * untouched.
86
+ * @param argument - one yarn argument, verbatim from argv.
87
+ * @param cwd - the directory `ea` was invoked from.
88
+ * @returns the argument with a relative path spec anchored to `cwd`.
89
+ */
90
+ function anchorPathSpec(argument, cwd) {
91
+ const match = /^(?<prefix>(?:file|link):)?(?<path>\.{1,2}(?:[/\\].*)?)$/.exec(argument);
92
+ if (match?.groups?.path === void 0) return argument;
93
+ return `${match.groups.prefix ?? ""}${resolve(cwd, match.groups.path)}`;
94
+ }
95
+ /**
96
+ * Run one `ea plugin` invocation: init if needed, forward to yarn, reconcile.
97
+ * @param profile - the profile name.
98
+ * @param args - yarn arguments with relative path specs anchored to the invoking directory.
99
+ * @returns the yarn exit code.
100
+ */
101
+ function runPlugin(profile, args) {
102
+ const dir = resolveProfileDir(profile);
103
+ if (!existsSync(join(dir, "package.json"))) {
104
+ initProfile(dir, PROFILE_TEMPLATES[profile] ?? DEFAULT_PROFILE_BUNDLES);
105
+ process.stderr.write(`${NAME}: initialized profile ${profile} at ${dir}\n`);
106
+ }
107
+ const before = readProfileManifest(NAME, dir);
108
+ const result = spawnSync("yarn", args.map((argument) => anchorPathSpec(argument, process.cwd())), {
109
+ cwd: dir,
110
+ stdio: "inherit",
111
+ shell: process.platform === "win32"
112
+ });
113
+ if (result.error !== void 0) {
114
+ if (result.error.code === "ENOENT") {
115
+ process.stderr.write(`${NAME}: yarn not found on PATH — install yarn to manage profile plugins\n`);
116
+ return 127;
117
+ }
118
+ throw result.error;
119
+ }
120
+ const exitCode = result.status ?? 1;
121
+ if (exitCode === 0) reconcilePlugins(before, dir);
122
+ else {
123
+ process.stderr.write(`${NAME}: yarn failed in profile directory ${dir}\n`);
124
+ if (args.some((argument) => /^git\+|^github:|\.git(?:#|$)/.test(argument))) process.stderr.write(`${NAME}: inspect Yarn's prepare/build diagnostics and re-run after resolving the reported dependency issue\n`);
125
+ }
126
+ return exitCode;
127
+ }
128
+ //#endregion
129
+ export { runPlugin };
@@ -0,0 +1,129 @@
1
+ import { t as INSTALL_ANCHOR } from "./profile-boot-DRHYAiGs.js";
2
+ import { existsSync } from "node:fs";
3
+ import { join, resolve } from "node:path";
4
+ import { DEFAULT_PROFILE_BUNDLES, PROFILE_TEMPLATES, initProfile, readProfileManifest, resolveBundleDir, resolveProfileDir, writeProfileManifest } from "@ejunz/ea-core/app-boot";
5
+ import { spawnSync } from "node:child_process";
6
+ //#region src/commands/plugin.ts
7
+ /**
8
+ * `ea plugin --profile <name> <args...>` — profile plugin management as a
9
+ * thin yarn forwarder: initialize the profile on first use, run
10
+ * `yarn <args...>` in the profile directory, then reconcile the
11
+ * `ea.profile.bundles` layer list against the installed state (a dependency
12
+ * resolving to a package that declares `ea.bundle` joins the layer stack; a
13
+ * removed or bundle-less dependency leaves it). Reconciling by installed
14
+ * state, not by dependency diff, means `update` activates a package that
15
+ * gained its `ea.bundle` declaration in a newer version.
16
+ * @module @ejunz/ea-core/plugin
17
+ */
18
+ const NAME = "ea";
19
+ /**
20
+ * Whether a resolved dependency exports a profile patch, i.e. is a bundle.
21
+ * @param packageName - the dependency's package name.
22
+ * @param profileDir - the profile directory (resolution anchor).
23
+ * @returns true when the package manifest declares `ea.bundle`.
24
+ */
25
+ function exportsPatch(packageName, profileDir) {
26
+ let dir;
27
+ try {
28
+ dir = resolveBundleDir(NAME, packageName, INSTALL_ANCHOR, profileDir);
29
+ } catch {
30
+ return false;
31
+ }
32
+ return readProfileManifest(NAME, dir).ea?.bundle?.patch !== void 0;
33
+ }
34
+ /**
35
+ * Reconcile `ea.profile.bundles` against the installed state: yarn has
36
+ * already written the real installed names (so a git/path/tarball/alias spec
37
+ * on the command line reconciles by its true package name) and materialized
38
+ * the packages. A dependency that resolves to a `ea.bundle`-declaring
39
+ * package joins the layer stack (appended in dependency order); a
40
+ * dependency-listed name that no longer does — removed, or the installed
41
+ * version dropped the declaration — leaves it. In-box bundles from the
42
+ * profile template are not dependencies and are never touched. Warns once
43
+ * per newly-added bundle-less dependency (a plain library is fine; the
44
+ * warning is orientation).
45
+ */
46
+ function reconcilePlugins(before, profileDir) {
47
+ const after = readProfileManifest(NAME, profileDir);
48
+ const beforeDeps = new Set(Object.keys(before.dependencies ?? {}));
49
+ const dependencies = Object.keys(after.dependencies ?? {});
50
+ const plugins = after.ea?.profile?.bundles ?? [];
51
+ let changed = false;
52
+ for (const packageName of dependencies) {
53
+ const isBundle = exportsPatch(packageName, profileDir);
54
+ if (isBundle && !plugins.includes(packageName)) {
55
+ plugins.push(packageName);
56
+ changed = true;
57
+ } else if (!isBundle && !beforeDeps.has(packageName)) process.stderr.write(`${NAME}: warning: ${packageName} declares no ea.bundle — installed as a plain dependency, not a profile layer (a later update that gains one activates it automatically)
58
+ `);
59
+ }
60
+ const dependencySet = new Set(dependencies);
61
+ for (const packageName of [...plugins]) {
62
+ const wasDependency = beforeDeps.has(packageName) || dependencySet.has(packageName);
63
+ const stillBundle = dependencySet.has(packageName) && exportsPatch(packageName, profileDir);
64
+ if (wasDependency && !stillBundle) {
65
+ plugins.splice(plugins.indexOf(packageName), 1);
66
+ changed = true;
67
+ }
68
+ }
69
+ if (!changed) return;
70
+ after.ea = {
71
+ ...after.ea,
72
+ profile: {
73
+ ...after.ea?.profile,
74
+ bundles: plugins
75
+ }
76
+ };
77
+ writeProfileManifest(profileDir, after);
78
+ }
79
+ /**
80
+ * Rewrite relative filesystem specs against the user's invoking directory.
81
+ * yarn runs with cwd = the profile directory, so a bare `.` or `../plugin`
82
+ * (or their `file:`/`link:` forms) would silently resolve inside the profile
83
+ * — `add .` from a plugin checkout would self-link the profile. Absolute
84
+ * specs, registry names, and every other yarn argument pass through
85
+ * untouched.
86
+ * @param argument - one yarn argument, verbatim from argv.
87
+ * @param cwd - the directory `ea` was invoked from.
88
+ * @returns the argument with a relative path spec anchored to `cwd`.
89
+ */
90
+ function anchorPathSpec(argument, cwd) {
91
+ const match = /^(?<prefix>(?:file|link):)?(?<path>\.{1,2}(?:[/\\].*)?)$/.exec(argument);
92
+ if (match?.groups?.path === void 0) return argument;
93
+ return `${match.groups.prefix ?? ""}${resolve(cwd, match.groups.path)}`;
94
+ }
95
+ /**
96
+ * Run one `ea plugin` invocation: init if needed, forward to yarn, reconcile.
97
+ * @param profile - the profile name.
98
+ * @param args - yarn arguments with relative path specs anchored to the invoking directory.
99
+ * @returns the yarn exit code.
100
+ */
101
+ function runPlugin(profile, args) {
102
+ const dir = resolveProfileDir(profile);
103
+ if (!existsSync(join(dir, "package.json"))) {
104
+ initProfile(dir, PROFILE_TEMPLATES[profile] ?? DEFAULT_PROFILE_BUNDLES);
105
+ process.stderr.write(`${NAME}: initialized profile ${profile} at ${dir}\n`);
106
+ }
107
+ const before = readProfileManifest(NAME, dir);
108
+ const result = spawnSync("yarn", args.map((argument) => anchorPathSpec(argument, process.cwd())), {
109
+ cwd: dir,
110
+ stdio: "inherit",
111
+ shell: process.platform === "win32"
112
+ });
113
+ if (result.error !== void 0) {
114
+ if (result.error.code === "ENOENT") {
115
+ process.stderr.write(`${NAME}: yarn not found on PATH — install yarn to manage profile plugins\n`);
116
+ return 127;
117
+ }
118
+ throw result.error;
119
+ }
120
+ const exitCode = result.status ?? 1;
121
+ if (exitCode === 0) reconcilePlugins(before, dir);
122
+ else {
123
+ process.stderr.write(`${NAME}: yarn failed in profile directory ${dir}\n`);
124
+ if (args.some((argument) => /^git\+|^github:|\.git(?:#|$)/.test(argument))) process.stderr.write(`${NAME}: inspect Yarn's prepare/build diagnostics and re-run after resolving the reported dependency issue\n`);
125
+ }
126
+ return exitCode;
127
+ }
128
+ //#endregion
129
+ export { runPlugin };
@@ -0,0 +1,317 @@
1
+ import { n as cliManifestPath, t as cliAssetPath } from "./cli-paths-Cpxmk9zd.js";
2
+ import { writeFileSync } from "node:fs";
3
+ import { PROFILE_PATCH_FILENAME, boot, composeEntries, healProfilesModuleFallback, installFailLoud, loadOptionalPatches, loadOverlayPatches, loadProfile, watchUserPatches } from "@ejunz/ea-core/app-boot";
4
+ import { join, resolve } from "node:path";
5
+ import { resolveEaHome } from "@ejunz/ea-core/home-paths";
6
+ import { EA_LAUNCH_ENVIRONMENT_KEY } from "@ejunz/ea-core/launch-environment";
7
+ import { provideCmdline } from "@ejunz/ea-core/cmdline";
8
+ //#region src/service/process-shutdown.ts
9
+ /** Bounded, escalating process shutdown for the long-lived CLI surfaces. */
10
+ /** Maximum grace allowed for the application tree to dispose before process exit. */
11
+ const PROCESS_SHUTDOWN_TIMEOUT_MS = 3e4;
12
+ /**
13
+ * Create one process-exit controller around an application disposer.
14
+ * @param dispose - Whole-application teardown that resolves at quiescence.
15
+ * @param forceExit - Function that exits the process immediately, replaceable by tests.
16
+ * @param complete - Function that records the natural completion code, replaceable by tests.
17
+ * @param timeoutMs - Grace before forced exit, replaceable by tests.
18
+ * @returns A controller whose normal calls coalesce and whose repeated signal call escalates.
19
+ */
20
+ function createProcessShutdown(dispose, forceExit = (code) => {
21
+ process.exit(code);
22
+ }, complete = (code) => {
23
+ process.exitCode = code;
24
+ }, timeoutMs = PROCESS_SHUTDOWN_TIMEOUT_MS) {
25
+ let pending;
26
+ let timeout;
27
+ let completed = false;
28
+ let forceExited = false;
29
+ const clearExitTimeout = () => {
30
+ /* v8 ignore else -- shutdown() arms the timer before any asynchronous exit path can run. */
31
+ if (timeout !== void 0) clearTimeout(timeout);
32
+ };
33
+ const forceExitOnce = (code) => {
34
+ if (forceExited) return;
35
+ forceExited = true;
36
+ clearExitTimeout();
37
+ forceExit(code);
38
+ };
39
+ const completeOnce = (code) => {
40
+ if (completed || forceExited) return;
41
+ completed = true;
42
+ clearExitTimeout();
43
+ complete(code);
44
+ };
45
+ const start = (code, forceAfterDispose) => {
46
+ if (pending !== void 0) return pending;
47
+ timeout = setTimeout(() => {
48
+ forceExitOnce(code);
49
+ }, timeoutMs);
50
+ pending = Promise.resolve().then(dispose).then(() => {
51
+ if (forceAfterDispose) forceExitOnce(code);
52
+ else completeOnce(code);
53
+ }, () => {
54
+ forceExitOnce(code);
55
+ });
56
+ return pending;
57
+ };
58
+ return {
59
+ shutdown(code) {
60
+ return start(code, false);
61
+ },
62
+ interrupt(code) {
63
+ if (pending !== void 0) {
64
+ forceExitOnce(code);
65
+ return;
66
+ }
67
+ start(code, true);
68
+ }
69
+ };
70
+ }
71
+ //#endregion
72
+ //#region src/service/profile-boot.ts
73
+ /**
74
+ * Shared profile boot for every `ea` surface: resolve the profile, stack its
75
+ * patch layers (bundle layers in `ea.profile.bundles` order, the profile's
76
+ * own `cordis.patch.yml`, `--patch` overlays, the telemetry switch), mount the
77
+ * tree over the profile's empty root config, keep the profile patch layer
78
+ * live, and wire fail-loud plus bounded shutdown.
79
+ *
80
+ * App flags are not the launcher's business: the invocation's inner arguments
81
+ * are provided to the tree through `ctx.cmdlineArgs`, where any injected app
82
+ * plugin may read the same immutable snapshot.
83
+ * @module @ejunz/ea-core/profile-boot
84
+ */
85
+ /** Shipped agent-preset root beside this app's own config. */
86
+ const SHIPPED_PRESET_ROOT = cliAssetPath("config/agent-presets/");
87
+ const FIBER_ACTIVE = 2;
88
+ const NAME = "ea";
89
+ /**
90
+ * The home-level user patch layer (`$EA_HOME/cordis.patch.yml`), applied
91
+ * over every profile's own layer. Resolved per call, not at module load:
92
+ * `$EA_HOME` may be set by the test or launcher after import.
93
+ * @returns the absolute patch-file path.
94
+ */
95
+ function homePatchPath() {
96
+ return join(resolveEaHome(), PROFILE_PATCH_FILENAME);
97
+ }
98
+ /** Absolute path of this ea installation's package.json. */
99
+ const INSTALL_ANCHOR = cliManifestPath();
100
+ /** The session-telemetry row id the EA_TELEMETRY_DISABLED switch targets. */
101
+ const TELEMETRY_ROW_ID = "session-telemetry-otel";
102
+ /** The empty root entry list every profile tree patches over. */
103
+ const PROFILE_ROOT_CONFIG = `# ea profile root — an empty entry list. The tree is composed as patches:
104
+ # each bundle in package.json's ea.profile.bundles, then cordis.patch.yml, then any
105
+ # --patch overlays. Edit cordis.patch.yml, not this file.
106
+ []
107
+ `;
108
+ /** Root config filename inside a profile directory. */
109
+ const PROFILE_ROOT_FILENAME = "cordis.yml";
110
+ /**
111
+ * Resolve the telemetry opt-out switch into its boot patch. ANY non-empty
112
+ * value (including `'0'`/`'false'`) disables: a privacy switch prefers
113
+ * off-by-mistake over on-by-mistake. A composition without the telemetry row
114
+ * exports nothing, so the switch is then trivially satisfied and no patch is
115
+ * generated — custom profiles need not mount telemetry to run with the
116
+ * switch set.
117
+ * @param disabledEnv - the raw `EA_TELEMETRY_DISABLED` value (`undefined` when unset).
118
+ * @param hasRow - whether the composition carries the telemetry row.
119
+ * @returns the disable patch, or `undefined` when no hard-disable patch is required.
120
+ */
121
+ function resolveTelemetryPatch(disabledEnv, hasRow) {
122
+ if ((disabledEnv ?? "") === "" || !hasRow) return void 0;
123
+ return {
124
+ id: TELEMETRY_ROW_ID,
125
+ disabled: true
126
+ };
127
+ }
128
+ /**
129
+ * Load a resolved profile for `name`: heal the shared module fallback, then
130
+ * (re)write the empty root config. The root is always rewritten: the whole
131
+ * composition is patch layers, and the vendored Loader's tree write-back (a
132
+ * plugin self-disposing persists the current tree) can bake composed rows
133
+ * into this file — which would duplicate every bundle insert on the next
134
+ * boot. The file exists on disk only because the Loader needs a real include
135
+ * root to anchor `baseUrl` at the profile directory (the config dump anchors
136
+ * on the same file, so both compose over the identical base).
137
+ * @param name - the profile name.
138
+ * @param userLayer - `false` skips parsing `cordis.patch.yml` (the default dump).
139
+ * @returns the loaded profile.
140
+ */
141
+ function prepareProfile(name, userLayer = true) {
142
+ healProfilesModuleFallback(INSTALL_ANCHOR);
143
+ const profile = loadProfile(NAME, name, INSTALL_ANCHOR, void 0, { userLayer });
144
+ writeFileSync(join(profile.dir, PROFILE_ROOT_FILENAME), PROFILE_ROOT_CONFIG);
145
+ return profile;
146
+ }
147
+ /** The full patch stack of one composed profile, in application order. */
148
+ function allPatches(composed) {
149
+ return [
150
+ ...composed.bundlePatches,
151
+ ...composed.profileConfigPatches,
152
+ ...composed.profile.patches,
153
+ ...composed.homePatches,
154
+ ...composed.overlays
155
+ ];
156
+ }
157
+ /**
158
+ * Load `name` and compose its effective patch stack: bundle layers in
159
+ * `ea.profile.bundles` order (the base bundle gates platform-specific rows
160
+ * on its own), the profile's user layer, the home-level user
161
+ * layer (`$EA_HOME/cordis.patch.yml` — machine-local preferences that apply
162
+ * to every profile, so it outranks the per-profile layer), `--patch` overlays,
163
+ * then the telemetry switch.
164
+ * @param name - the profile name.
165
+ * @param patchFiles - `--patch` overlay paths, in argv order.
166
+ * @returns the profile, its patch layers, and the composed row index.
167
+ */
168
+ function composeProfile(name, patchFiles, profileConfigPatches = []) {
169
+ const profile = prepareProfile(name);
170
+ const homePatches = loadOptionalPatches(NAME, homePatchPath()) ?? [];
171
+ const overlays = patchFiles.flatMap((file) => loadOverlayPatches(NAME, resolve(file)));
172
+ const bundlePatches = profile.layers.flatMap((layer) => layer.patches);
173
+ const configPatches = [...profileConfigPatches];
174
+ const rows = /* @__PURE__ */ new Map();
175
+ for (const row of composeEntries([
176
+ bundlePatches,
177
+ configPatches,
178
+ profile.patches,
179
+ homePatches,
180
+ overlays
181
+ ])) if (typeof row.id === "string") rows.set(row.id, row);
182
+ const composedOverlays = [...overlays];
183
+ if (rows.has("agent-presets")) composedOverlays.push({
184
+ id: "agent-presets",
185
+ config: {
186
+ ...rows.get("agent-presets")?.config ?? {},
187
+ roots: [{
188
+ path: SHIPPED_PRESET_ROOT,
189
+ trust: "system"
190
+ }]
191
+ }
192
+ });
193
+ const telemetryPatch = resolveTelemetryPatch(process.env.EA_TELEMETRY_DISABLED, rows.has(TELEMETRY_ROW_ID));
194
+ if (telemetryPatch !== void 0) composedOverlays.push(telemetryPatch);
195
+ return {
196
+ profile,
197
+ bundlePatches,
198
+ profileConfigPatches: configPatches,
199
+ homePatches,
200
+ overlays: composedOverlays,
201
+ rows
202
+ };
203
+ }
204
+ /**
205
+ * Boot one composed profile tree without owning process lifetime: no signal
206
+ * handlers, no fail-loud install, no live patch watchers, and no `process.exit`.
207
+ * The caller disposes the returned context.
208
+ * @param composed - the composition to mount, from {@link composeProfile}.
209
+ * @param options - environment snapshot, app arguments, and the tree's host hooks.
210
+ * @returns the settled root context.
211
+ */
212
+ async function bootProfileTree(composed, options) {
213
+ const rootConfig = join(composed.profile.dir, PROFILE_ROOT_FILENAME);
214
+ return await boot(NAME, rootConfig, structuredClone(allPatches(composed)), async (hostCtx) => {
215
+ options.onContext?.(hostCtx);
216
+ hostCtx.provide(EA_LAUNCH_ENVIRONMENT_KEY, options.environment);
217
+ provideCmdline(hostCtx, {
218
+ args: options.args,
219
+ exit: (code) => options.onExit?.(code)
220
+ });
221
+ await options.prepare?.(hostCtx, composed);
222
+ }, void 0, options.debug ?? false);
223
+ }
224
+ /**
225
+ * Re-throw a watcher-setup failure unless a shutdown already owns the tree:
226
+ * a signal aborted this invocation, or an app requested exit (`ctx.appExit`
227
+ * from a fast one-shot) and the root's disposal rejected the in-flight setup
228
+ * await. Either way the failure describes a tree that is exiting as asked,
229
+ * not a broken watch.
230
+ * @param ctx - the booted root context.
231
+ * @param signal - this invocation's signal-shutdown fact.
232
+ * @param error - the setup failure.
233
+ */
234
+ function suppressShutdownError(ctx, signal, error) {
235
+ if (signal.aborted) return;
236
+ if (ctx.fiber.state !== FIBER_ACTIVE || ctx.get("loader") === void 0) return;
237
+ throw error;
238
+ }
239
+ /**
240
+ * Boot one profile invocation end to end and leave process lifetime to the
241
+ * mounted plugins (or to a one-shot runner the composition mounts).
242
+ * @param options - environment snapshot, profile name, overlays, and the booted app's own arguments.
243
+ * @returns the settled root context and the shutdown controller.
244
+ */
245
+ async function runProfile(options) {
246
+ const composed = composeProfile(options.profile, options.patchFiles, options.profileConfigPatches);
247
+ const app = {};
248
+ const shutdown = createProcessShutdown(async () => {
249
+ await app.current?.fiber.dispose();
250
+ });
251
+ const signalShutdown = new AbortController();
252
+ const interrupt = (code) => {
253
+ signalShutdown.abort();
254
+ shutdown.interrupt(code);
255
+ };
256
+ process.on("SIGTERM", () => {
257
+ interrupt(0);
258
+ });
259
+ process.on("SIGINT", () => {
260
+ interrupt(130);
261
+ });
262
+ installFailLoud(NAME, process, async () => {
263
+ await app.current?.fiber.dispose();
264
+ });
265
+ const composeLive = () => structuredClone([
266
+ ...composed.bundlePatches,
267
+ ...composed.profileConfigPatches,
268
+ ...loadOptionalPatches(NAME, composed.profile.patchPath) ?? [],
269
+ ...loadOptionalPatches(NAME, homePatchPath()) ?? [],
270
+ ...composed.overlays
271
+ ]);
272
+ const ctx = await bootProfileTree(composed, {
273
+ environment: options.environment,
274
+ args: options.args,
275
+ ...options.debug === void 0 ? {} : { debug: options.debug },
276
+ onContext: (hostCtx) => {
277
+ app.current = hostCtx;
278
+ },
279
+ onExit: (code) => void shutdown.shutdown(code),
280
+ ...options.prepare === void 0 ? {} : { prepare: options.prepare }
281
+ });
282
+ app.current = ctx;
283
+ if (options.debug) {
284
+ const logger = ctx.logger("loader");
285
+ for (const row of composed.rows.values()) {
286
+ if (row.disabled === true || typeof row.name !== "string") continue;
287
+ logger.info("apply plugin %C", row.name);
288
+ }
289
+ }
290
+ if (!signalShutdown.signal.aborted && ctx.fiber.state === FIBER_ACTIVE && ctx.get("loader") !== void 0) try {
291
+ if (ctx.get("hmr") === void 0) {
292
+ if (ctx.get("timer") === void 0) await ctx.loader.create({ name: "@ejunz/cordis-plugin-timer" });
293
+ await ctx.loader.create({
294
+ name: "@ejunz/cordis-plugin-hmr",
295
+ config: { root: [] }
296
+ });
297
+ }
298
+ await watchUserPatches(ctx, {
299
+ binName: NAME,
300
+ filename: composed.profile.patchPath,
301
+ compose: composeLive
302
+ });
303
+ await watchUserPatches(ctx, {
304
+ binName: NAME,
305
+ filename: homePatchPath(),
306
+ compose: composeLive
307
+ });
308
+ } catch (error) {
309
+ suppressShutdownError(ctx, signalShutdown.signal, error);
310
+ }
311
+ return {
312
+ ctx,
313
+ shutdown
314
+ };
315
+ }
316
+ //#endregion
317
+ export { homePatchPath as a, runProfile as c, composeProfile as i, PROFILE_ROOT_FILENAME as n, prepareProfile as o, bootProfileTree as r, resolveTelemetryPatch as s, INSTALL_ANCHOR as t };
@@ -0,0 +1,2 @@
1
+ import { c as runProfile } from "./profile-boot-DRHYAiGs.js";
2
+ export { runProfile };