@openparachute/app 0.2.0-rc.10
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/.parachute/config/schema +62 -0
- package/.parachute/info +14 -0
- package/.parachute/module.json +14 -0
- package/CHANGELOG.md +537 -0
- package/LICENSE +661 -0
- package/bin/parachute-app.ts +525 -0
- package/dist/admin/assets/index-BXlRNPxk.js +60 -0
- package/dist/admin/assets/index-DaGP1hmw.css +1 -0
- package/dist/admin/index.html +14 -0
- package/package.json +51 -0
- package/src/admin-routes.ts +884 -0
- package/src/auth.ts +212 -0
- package/src/bootstrap.ts +153 -0
- package/src/cache-headers.ts +106 -0
- package/src/config.ts +289 -0
- package/src/dcr.ts +334 -0
- package/src/dev-injection.ts +166 -0
- package/src/dev-mode.ts +205 -0
- package/src/dev-routes.ts +380 -0
- package/src/dev-watcher.ts +479 -0
- package/src/http-server.ts +682 -0
- package/src/index.ts +394 -0
- package/src/meta-schema.ts +715 -0
- package/src/npm-fetch.ts +320 -0
- package/src/operator-token.ts +95 -0
- package/src/provision-schema.ts +180 -0
- package/src/self-register.ts +184 -0
- package/src/services-manifest.ts +104 -0
- package/src/tenancy-injection.ts +149 -0
- package/src/ui-registry.ts +202 -0
package/src/index.ts
ADDED
|
@@ -0,0 +1,394 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @openparachute/app — library entry.
|
|
3
|
+
*
|
|
4
|
+
* Phase 1.1 wires the public surface: `serve` starts the long-running daemon
|
|
5
|
+
* that scans `$PARACHUTE_HOME/app/uis/`, mounts each declared UI at its
|
|
6
|
+
* declared path, serves the bundle with smart cache headers + SPA-routing
|
|
7
|
+
* fallback, and self-registers into `~/.parachute/services.json`. Admin
|
|
8
|
+
* verbs (`addUi`, `removeUi`, `listUis`, `reloadUi`) and dev mode
|
|
9
|
+
* (`setDevMode`) land in Phase 1.2 / 1.3.
|
|
10
|
+
*
|
|
11
|
+
* See the design doc:
|
|
12
|
+
* https://github.com/ParachuteComputer/parachute.computer/blob/main/design/2026-05-21-parachute-apps-design.md
|
|
13
|
+
*/
|
|
14
|
+
|
|
15
|
+
import pkg from "../package.json" with { type: "json" };
|
|
16
|
+
|
|
17
|
+
import { addUiInternal, buildUisExtraFieldForBoot } from "./admin-routes.ts";
|
|
18
|
+
import { maybeBootstrapDefaultApps } from "./bootstrap.ts";
|
|
19
|
+
import { type AppConfig, loadConfig, resolveConfigPath, resolveUisDir } from "./config.ts";
|
|
20
|
+
import { disableDevMode, enableDevMode } from "./dev-mode.ts";
|
|
21
|
+
import { stopAllWatchers } from "./dev-watcher.ts";
|
|
22
|
+
import { type AppState, startHttpServer } from "./http-server.ts";
|
|
23
|
+
import { resolveProjectRoot, selfRegister } from "./self-register.ts";
|
|
24
|
+
import { scanUis } from "./ui-registry.ts";
|
|
25
|
+
|
|
26
|
+
// Re-export everything so callers can drop down to a specific layer
|
|
27
|
+
// without an import-path puzzle.
|
|
28
|
+
export * from "./config.ts";
|
|
29
|
+
export * from "./meta-schema.ts";
|
|
30
|
+
export * from "./cache-headers.ts";
|
|
31
|
+
export * from "./ui-registry.ts";
|
|
32
|
+
export * from "./services-manifest.ts";
|
|
33
|
+
export * from "./auth.ts";
|
|
34
|
+
export * from "./operator-token.ts";
|
|
35
|
+
export * from "./dcr.ts";
|
|
36
|
+
export * from "./npm-fetch.ts";
|
|
37
|
+
export * from "./dev-mode.ts";
|
|
38
|
+
export * from "./dev-injection.ts";
|
|
39
|
+
export * from "./dev-watcher.ts";
|
|
40
|
+
export {
|
|
41
|
+
routeAdmin,
|
|
42
|
+
buildUisExtraFieldForBoot,
|
|
43
|
+
addUiInternal,
|
|
44
|
+
type AdminHandlerOpts,
|
|
45
|
+
type AdminMutableState,
|
|
46
|
+
type AddRequestBody,
|
|
47
|
+
type AddUiInternalResult,
|
|
48
|
+
type SerializedUi,
|
|
49
|
+
} from "./admin-routes.ts";
|
|
50
|
+
export {
|
|
51
|
+
maybeBootstrapDefaultApps,
|
|
52
|
+
type BootstrapOpts,
|
|
53
|
+
type BootstrapResult,
|
|
54
|
+
type BootstrapAddFn,
|
|
55
|
+
} from "./bootstrap.ts";
|
|
56
|
+
export {
|
|
57
|
+
provisionSchemaForUi,
|
|
58
|
+
type ProvisionSchemaOpts,
|
|
59
|
+
type ProvisionSchemaResult,
|
|
60
|
+
} from "./provision-schema.ts";
|
|
61
|
+
export { routeDev, type DevRoutesOpts } from "./dev-routes.ts";
|
|
62
|
+
export { resolveProjectRoot, selfRegister } from "./self-register.ts";
|
|
63
|
+
export type { SelfRegisterOpts, SelfRegisterResult } from "./self-register.ts";
|
|
64
|
+
export { startHttpServer } from "./http-server.ts";
|
|
65
|
+
export type { AppState, HttpServerOpts } from "./http-server.ts";
|
|
66
|
+
|
|
67
|
+
/** Package semver. */
|
|
68
|
+
export const VERSION: string = pkg.version;
|
|
69
|
+
|
|
70
|
+
/** Default healthz port (per design doc + canonical-ports pattern, app claims 1946). */
|
|
71
|
+
export const DEFAULT_PORT = 1946;
|
|
72
|
+
|
|
73
|
+
/** Default mount path for app under hub's reverse proxy. */
|
|
74
|
+
export const DEFAULT_MOUNT = "/app";
|
|
75
|
+
|
|
76
|
+
export type ServeOptions = {
|
|
77
|
+
/** Override the healthz port. Defaults to `DEFAULT_PORT` (1946). */
|
|
78
|
+
port?: number;
|
|
79
|
+
/** Override the config path (tests). Defaults to `resolveConfigPath()`. */
|
|
80
|
+
configPath?: string;
|
|
81
|
+
/** Override the uis-dir location (tests). Defaults to `resolveUisDir()`. */
|
|
82
|
+
uisDir?: string;
|
|
83
|
+
/** Override the bind hostname (tests). Defaults to `127.0.0.1`. */
|
|
84
|
+
hostname?: string;
|
|
85
|
+
/** Override the services.json path (tests). */
|
|
86
|
+
manifestPath?: string;
|
|
87
|
+
/** Skip self-registration (tests don't want to touch `~/.parachute/`). */
|
|
88
|
+
skipSelfRegister?: boolean;
|
|
89
|
+
/** Override `.parachute/` location (tests). */
|
|
90
|
+
parachuteDir?: string;
|
|
91
|
+
/** Logger override; default console. */
|
|
92
|
+
logger?: Pick<Console, "log" | "warn" | "error">;
|
|
93
|
+
/**
|
|
94
|
+
* Override `Bun.serve` (tests). Lets us assert on the dispatched config
|
|
95
|
+
* without binding a real port.
|
|
96
|
+
*/
|
|
97
|
+
serveFn?: typeof Bun.serve;
|
|
98
|
+
/**
|
|
99
|
+
* Override the absolute path to the built admin SPA bundle (tests). Defaults
|
|
100
|
+
* to `<package-root>/dist/admin/`.
|
|
101
|
+
*/
|
|
102
|
+
adminDir?: string;
|
|
103
|
+
/** Inject fetch for DCR calls (tests). */
|
|
104
|
+
fetchFn?: import("./dcr.ts").FetchFn;
|
|
105
|
+
/** Override the operator-token resolver (tests). */
|
|
106
|
+
operatorTokenOverride?: () => string | undefined;
|
|
107
|
+
/** Override the npm-fetch spawner (tests). */
|
|
108
|
+
npmSpawnFn?: import("./npm-fetch.ts").NpmSpawnFn;
|
|
109
|
+
/**
|
|
110
|
+
* Skip the first-boot default-app bootstrap (tests + CI). When omitted,
|
|
111
|
+
* bootstrap runs iff `state.registeredUis.length === 0` AND
|
|
112
|
+
* `config.bootstrap_default_apps.enabled === true`.
|
|
113
|
+
*/
|
|
114
|
+
skipBootstrap?: boolean;
|
|
115
|
+
/**
|
|
116
|
+
* Return a promise from `serve()` that callers can await to know when
|
|
117
|
+
* bootstrap is complete (tests). Production `serve()` callers don't
|
|
118
|
+
* need this; bootstrap is fire-and-forget for the daemon.
|
|
119
|
+
*/
|
|
120
|
+
awaitBootstrap?: boolean;
|
|
121
|
+
};
|
|
122
|
+
|
|
123
|
+
export type ServeHandle = {
|
|
124
|
+
/** The currently-resolved app config. */
|
|
125
|
+
config: AppConfig;
|
|
126
|
+
/** The running HTTP server — `server.stop()` for graceful shutdown. */
|
|
127
|
+
server: ReturnType<typeof Bun.serve>;
|
|
128
|
+
/** The mutable state object. */
|
|
129
|
+
state: AppState;
|
|
130
|
+
/** Stop the daemon. */
|
|
131
|
+
stop: () => Promise<void>;
|
|
132
|
+
/**
|
|
133
|
+
* Resolves once first-boot bootstrap completes. `undefined` when
|
|
134
|
+
* bootstrap was skipped (state non-empty or `skipBootstrap: true`).
|
|
135
|
+
* Tests `await handle.bootstrap` to assert post-bootstrap state.
|
|
136
|
+
*/
|
|
137
|
+
bootstrap?: Promise<import("./bootstrap.ts").BootstrapResult>;
|
|
138
|
+
};
|
|
139
|
+
|
|
140
|
+
/**
|
|
141
|
+
* Long-running daemon: scan `$PARACHUTE_HOME/app/uis/`, mount each UI at its
|
|
142
|
+
* declared path, serve the bundle with smart cache headers + SPA fallback.
|
|
143
|
+
*
|
|
144
|
+
* Phase 1.1: discovery is one-shot at startup. Phase 1.2 adds reload + watch.
|
|
145
|
+
*
|
|
146
|
+
* Returns a handle the CLI uses to wire SIGINT/SIGTERM into graceful
|
|
147
|
+
* shutdown.
|
|
148
|
+
*/
|
|
149
|
+
export function serve(opts: ServeOptions = {}): ServeHandle {
|
|
150
|
+
const logger = opts.logger ?? console;
|
|
151
|
+
const port = opts.port ?? DEFAULT_PORT;
|
|
152
|
+
const hostname = opts.hostname ?? "127.0.0.1";
|
|
153
|
+
|
|
154
|
+
const config = loadConfig({ configPath: opts.configPath, logger });
|
|
155
|
+
|
|
156
|
+
// Kill-switch: when `config.disabled` is true, skip the UI scan entirely
|
|
157
|
+
// so no bundles are mounted. The HTTP server still binds (healthz + the
|
|
158
|
+
// `.parachute/*` admin surface keep working) so an operator can flip the
|
|
159
|
+
// flag back via the admin SPA (Phase 1.2) without restarting the daemon.
|
|
160
|
+
// Per design doc + reviewer nit 3 — `disabled` was loaded but not honored.
|
|
161
|
+
const scan = config.disabled
|
|
162
|
+
? { registered: [], skipped: [] as Array<{ dirName: string; status: string; reason: string }> }
|
|
163
|
+
: scanUis({ uisDir: opts.uisDir, logger });
|
|
164
|
+
|
|
165
|
+
if (config.disabled) {
|
|
166
|
+
logger.log("[app] disabled (config.disabled=true) — no UIs mounted");
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
const state: AppState = {
|
|
170
|
+
config,
|
|
171
|
+
registeredUis: scan.registered,
|
|
172
|
+
skippedUis: scan.skipped.map((s) => ({
|
|
173
|
+
dirName: s.dirName,
|
|
174
|
+
status: s.status,
|
|
175
|
+
reason: s.reason,
|
|
176
|
+
})),
|
|
177
|
+
};
|
|
178
|
+
|
|
179
|
+
const startedAt = new Date();
|
|
180
|
+
const server = startHttpServer({
|
|
181
|
+
state,
|
|
182
|
+
port,
|
|
183
|
+
hostname,
|
|
184
|
+
startedAt,
|
|
185
|
+
logger,
|
|
186
|
+
parachuteDir: opts.parachuteDir,
|
|
187
|
+
serveFn: opts.serveFn,
|
|
188
|
+
adminDir: opts.adminDir,
|
|
189
|
+
adminOpts: {
|
|
190
|
+
uisDir: opts.uisDir,
|
|
191
|
+
manifestPath: opts.manifestPath,
|
|
192
|
+
fetchFn: opts.fetchFn,
|
|
193
|
+
operatorTokenOverride: opts.operatorTokenOverride,
|
|
194
|
+
npmSpawnFn: opts.npmSpawnFn,
|
|
195
|
+
logger,
|
|
196
|
+
skipSelfRegisterRefresh: opts.skipSelfRegister,
|
|
197
|
+
},
|
|
198
|
+
});
|
|
199
|
+
|
|
200
|
+
logger.log(
|
|
201
|
+
`[app] Listening on http://${hostname}:${server.port} — ${state.registeredUis.length} UI${
|
|
202
|
+
state.registeredUis.length === 1 ? "" : "s"
|
|
203
|
+
} hosted${state.skippedUis.length > 0 ? ` (${state.skippedUis.length} skipped)` : ""}`,
|
|
204
|
+
);
|
|
205
|
+
for (const ui of state.registeredUis) {
|
|
206
|
+
logger.log(`[app] ${ui.meta.path} → ${ui.meta.displayName} (${ui.meta.name})`);
|
|
207
|
+
}
|
|
208
|
+
|
|
209
|
+
// Phase 2.1 — first-boot default-app bootstrap. Runs only when no UIs
|
|
210
|
+
// are mounted (fresh install). Best-effort; failures log + continue so
|
|
211
|
+
// a network blip or unpublished package doesn't prevent daemon
|
|
212
|
+
// startup.
|
|
213
|
+
let bootstrapPromise: Promise<import("./bootstrap.ts").BootstrapResult> | undefined;
|
|
214
|
+
if (!opts.skipBootstrap && !config.disabled && state.registeredUis.length === 0) {
|
|
215
|
+
// Fire-and-forget — daemon doesn't block on bootstrap. The add path
|
|
216
|
+
// re-scans + swaps state in-place, so subsequent requests pick up
|
|
217
|
+
// the newly-mounted UIs without a restart. The promise is exposed
|
|
218
|
+
// on the handle so tests can `await handle.bootstrap`.
|
|
219
|
+
bootstrapPromise = runBootstrap({
|
|
220
|
+
config,
|
|
221
|
+
uisDir: opts.uisDir ?? resolveUisDir(),
|
|
222
|
+
adminOpts: {
|
|
223
|
+
state,
|
|
224
|
+
uisDir: opts.uisDir,
|
|
225
|
+
manifestPath: opts.manifestPath,
|
|
226
|
+
fetchFn: opts.fetchFn,
|
|
227
|
+
operatorTokenOverride: opts.operatorTokenOverride,
|
|
228
|
+
npmSpawnFn: opts.npmSpawnFn,
|
|
229
|
+
logger,
|
|
230
|
+
// Bootstrap's own callsite owns the post-bootstrap selfRegister;
|
|
231
|
+
// skip the per-add refresh to avoid stamping a stale partial
|
|
232
|
+
// services.json mid-iteration.
|
|
233
|
+
skipSelfRegisterRefresh: true,
|
|
234
|
+
},
|
|
235
|
+
manifestPath: opts.manifestPath,
|
|
236
|
+
skipSelfRegister: opts.skipSelfRegister,
|
|
237
|
+
logger,
|
|
238
|
+
}).catch((e) => {
|
|
239
|
+
logger.warn(`[app] bootstrap failed unexpectedly: ${(e as Error).message}`);
|
|
240
|
+
return { bootstrapped: [], skipped: [], failed: [], skipReason: "exception" };
|
|
241
|
+
});
|
|
242
|
+
}
|
|
243
|
+
|
|
244
|
+
if (!opts.skipSelfRegister) {
|
|
245
|
+
// `server.port` is `number | undefined` per Bun's types (it's undefined
|
|
246
|
+
// when the server uses unix sockets, which we don't here) — fall back to
|
|
247
|
+
// the operator's requested port. Both paths produce a `number`.
|
|
248
|
+
const portWritten = server.port ?? port;
|
|
249
|
+
selfRegister({
|
|
250
|
+
boundPort: portWritten,
|
|
251
|
+
installDir: resolveProjectRoot(),
|
|
252
|
+
manifestPath: opts.manifestPath,
|
|
253
|
+
extraFields: { uis: buildUisExtraFieldForBoot(state.registeredUis) },
|
|
254
|
+
logger,
|
|
255
|
+
});
|
|
256
|
+
}
|
|
257
|
+
|
|
258
|
+
const stop = async () => {
|
|
259
|
+
logger.log("[app] shutting down");
|
|
260
|
+
// Tear down any dev-mode file watchers so the process exits cleanly.
|
|
261
|
+
// The watcher slots own AbortControllers + FSWatchers; without this
|
|
262
|
+
// the daemon can hang on shutdown until the FSEvents stream closes.
|
|
263
|
+
stopAllWatchers();
|
|
264
|
+
server.stop();
|
|
265
|
+
logger.log("[app] stopped");
|
|
266
|
+
};
|
|
267
|
+
|
|
268
|
+
return {
|
|
269
|
+
config,
|
|
270
|
+
server,
|
|
271
|
+
state,
|
|
272
|
+
stop,
|
|
273
|
+
...(bootstrapPromise ? { bootstrap: bootstrapPromise } : {}),
|
|
274
|
+
};
|
|
275
|
+
}
|
|
276
|
+
|
|
277
|
+
/**
|
|
278
|
+
* One-shot: scan UIs + report status, exit. Non-daemon counterpart to
|
|
279
|
+
* `serve` — useful for `parachute-app list` (Phase 1.2) and config
|
|
280
|
+
* validation in CI.
|
|
281
|
+
*/
|
|
282
|
+
export function runOnce(opts: ServeOptions = {}): {
|
|
283
|
+
config: AppConfig;
|
|
284
|
+
state: AppState;
|
|
285
|
+
} {
|
|
286
|
+
const logger = opts.logger ?? console;
|
|
287
|
+
const config = loadConfig({ configPath: opts.configPath, logger });
|
|
288
|
+
const scan = scanUis({ uisDir: opts.uisDir, logger });
|
|
289
|
+
const state: AppState = {
|
|
290
|
+
config,
|
|
291
|
+
registeredUis: scan.registered,
|
|
292
|
+
skippedUis: scan.skipped.map((s) => ({
|
|
293
|
+
dirName: s.dirName,
|
|
294
|
+
status: s.status,
|
|
295
|
+
reason: s.reason,
|
|
296
|
+
})),
|
|
297
|
+
};
|
|
298
|
+
logger.log(
|
|
299
|
+
`[app] scan: ${state.registeredUis.length} active, ${state.skippedUis.length} skipped`,
|
|
300
|
+
);
|
|
301
|
+
for (const ui of state.registeredUis) {
|
|
302
|
+
logger.log(`[app] active ${ui.meta.path} (${ui.meta.name})`);
|
|
303
|
+
}
|
|
304
|
+
for (const s of state.skippedUis) {
|
|
305
|
+
logger.log(`[app] skip ${s.dirName} — ${s.status}: ${s.reason}`);
|
|
306
|
+
}
|
|
307
|
+
return { config, state };
|
|
308
|
+
}
|
|
309
|
+
|
|
310
|
+
/**
|
|
311
|
+
* Phase 1.3 surface — toggle dev mode for a UI with live reload.
|
|
312
|
+
*
|
|
313
|
+
* The dev-mode API now ships via `./dev-mode.ts`. Callers that want
|
|
314
|
+
* fine-grained control import `enableDevMode` / `disableDevMode` /
|
|
315
|
+
* `broadcastReload` directly. This wrapper stays as the canonical
|
|
316
|
+
* library-level façade for the simple "flip a UI into dev mode" case
|
|
317
|
+
* and keeps the surface stable for downstream consumers.
|
|
318
|
+
*/
|
|
319
|
+
export function setDevMode(
|
|
320
|
+
name: string,
|
|
321
|
+
enable: boolean,
|
|
322
|
+
): { name: string; enabled: boolean; enabledAt: number } {
|
|
323
|
+
const state = enable ? enableDevMode(name) : disableDevMode(name);
|
|
324
|
+
return { name, enabled: state.enabled, enabledAt: state.enabledAt };
|
|
325
|
+
}
|
|
326
|
+
|
|
327
|
+
/**
|
|
328
|
+
* Internal helper — invokes `maybeBootstrapDefaultApps` with a closure
|
|
329
|
+
* that delegates to `addUiInternal` for each declared default app.
|
|
330
|
+
* Exported for tests that want to exercise the wiring without going
|
|
331
|
+
* through the full `serve()` HTTP boot.
|
|
332
|
+
*
|
|
333
|
+
* After the bootstrap iteration completes, if any UIs were added, we
|
|
334
|
+
* call `selfRegister` once so services.json carries the new `uis` map
|
|
335
|
+
* in a single atomic write (vs the per-add stamps the admin path would
|
|
336
|
+
* normally do — `skipSelfRegisterRefresh: true` is set on the addOpts).
|
|
337
|
+
*/
|
|
338
|
+
export async function runBootstrap(args: {
|
|
339
|
+
config: AppConfig;
|
|
340
|
+
uisDir: string;
|
|
341
|
+
/** Pre-built admin opts — same shape `routeAdmin` consumes. */
|
|
342
|
+
adminOpts: import("./admin-routes.ts").AdminHandlerOpts;
|
|
343
|
+
/** Override the services.json manifest path (tests). */
|
|
344
|
+
manifestPath?: string;
|
|
345
|
+
logger?: Pick<Console, "log" | "warn" | "error">;
|
|
346
|
+
/** Skip post-bootstrap selfRegister (tests). */
|
|
347
|
+
skipSelfRegister?: boolean;
|
|
348
|
+
}): Promise<import("./bootstrap.ts").BootstrapResult> {
|
|
349
|
+
const logger = args.logger ?? console;
|
|
350
|
+
const result = await maybeBootstrapDefaultApps({
|
|
351
|
+
config: args.config,
|
|
352
|
+
uisDir: args.uisDir,
|
|
353
|
+
logger,
|
|
354
|
+
add: async (spec) => {
|
|
355
|
+
const outcome = await addUiInternal({ source: spec }, args.adminOpts);
|
|
356
|
+
if (!outcome.added) {
|
|
357
|
+
// Surface the underlying error message — best-effort body parse.
|
|
358
|
+
let detail = `HTTP ${outcome.response.status}`;
|
|
359
|
+
try {
|
|
360
|
+
const parsed = (await outcome.response.clone().json()) as {
|
|
361
|
+
error?: string;
|
|
362
|
+
message?: string;
|
|
363
|
+
};
|
|
364
|
+
detail = parsed.message ?? parsed.error ?? detail;
|
|
365
|
+
} catch {
|
|
366
|
+
// ignore
|
|
367
|
+
}
|
|
368
|
+
throw new Error(detail);
|
|
369
|
+
}
|
|
370
|
+
return { name: outcome.added.meta.name, path: outcome.added.meta.path };
|
|
371
|
+
},
|
|
372
|
+
});
|
|
373
|
+
|
|
374
|
+
// One-shot post-bootstrap services.json refresh: stamps the full uis
|
|
375
|
+
// map atomically so hub's per-request discovery sees the bootstrapped
|
|
376
|
+
// UIs on its next read.
|
|
377
|
+
if (!args.skipSelfRegister && result.bootstrapped.length > 0) {
|
|
378
|
+
try {
|
|
379
|
+
selfRegister({
|
|
380
|
+
boundPort: 0,
|
|
381
|
+
installDir: resolveProjectRoot(),
|
|
382
|
+
manifestPath: args.manifestPath,
|
|
383
|
+
extraFields: { uis: buildUisExtraFieldForBoot(args.adminOpts.state.registeredUis) },
|
|
384
|
+
logger,
|
|
385
|
+
});
|
|
386
|
+
} catch (e) {
|
|
387
|
+
logger.warn(`[app] bootstrap: services.json refresh failed: ${(e as Error).message}`);
|
|
388
|
+
}
|
|
389
|
+
}
|
|
390
|
+
return result;
|
|
391
|
+
}
|
|
392
|
+
|
|
393
|
+
/** Expose canonical resolvers for the bin. */
|
|
394
|
+
export { resolveConfigPath, resolveUisDir };
|