@akanjs/cli 3.0.0-alpha.87 → 3.0.0-alpha.89
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/.build-stamp +1 -1
- package/{abstractCompactor-pwc6d6yk.js → abstractCompactor-cefxzggj.js} +2 -2
- package/{agent.command-180hh0ye.js → agent.command-r93wmq7m.js} +6 -6
- package/{application.command-v8dywdqh.js → application.command-5nqfqgdn.js} +9 -8
- package/{applicationBuildRunner-27wyznpv.js → applicationBuildRunner-b21m07fs.js} +4 -3
- package/{applicationReleasePackager-ktjt0erc.js → applicationReleasePackager-3wh58n2m.js} +2 -2
- package/buildBatch.proc.js +4 -4
- package/{capacitorApp-kwa67kzs.js → capacitorApp-gzcx6txb.js} +3 -3
- package/{cloud.command-12k4wy4x.js → cloud.command-g0pk0k60.js} +11 -11
- package/{context.command-dk2m2sjx.js → context.command-qd8kqrnh.js} +14 -14
- package/{dependencyScanner-c64m6fxp.js → dependencyScanner-shs82ynp.js} +2 -2
- package/devTui-2xf0meaq.js +552 -0
- package/devTuiView-7jy8f30g.js +9 -0
- package/{guideline.command-w6ddv13r.js → guideline.command-f38x8jhw.js} +4 -4
- package/guidelines/agentRule/agentRule.instruction.md +9 -1
- package/guidelines/conventions/conventions.instruction.md +30 -0
- package/guidelines/modelStore/modelStore.instruction.md +12 -0
- package/guidelines/modelTemplate/modelTemplate.instruction.md +5 -0
- package/guidelines/runtimeRule/runtimeRule.instruction.md +37 -0
- package/incrementalBuilder.proc.js +8 -5
- package/{index-5y7qba1c.js → index-0emb9eat.js} +3 -3
- package/{index-cd5x18qh.js → index-3vvbv974.js} +4 -4
- package/{index-v0s24t8j.js → index-5xdqxbn9.js} +3 -3
- package/{index-k86zc21w.js → index-9bwhxrqf.js} +6 -6
- package/{index-ft1sfjz0.js → index-cezy7yb3.js} +66 -7
- package/{index-a6c5pc4p.js → index-d61rcfa2.js} +614 -62
- package/{index-gz0c482j.js → index-deekbtkj.js} +6 -6
- package/{index-saheq2bs.js → index-e82dyeqy.js} +1 -1
- package/{index-j9vnq8tr.js → index-h759vjfa.js} +3 -3
- package/{index-z3c9mmw4.js → index-jydtyzva.js} +2 -2
- package/{index-mfm35dhh.js → index-kcxmv48c.js} +1 -1
- package/{index-6c3ex9qw.js → index-kfnr6c19.js} +4 -4
- package/{index-1kzsn94d.js → index-mpmyqm1y.js} +1 -1
- package/{index-wet2ytnr.js → index-n85fer99.js} +2 -2
- package/{index-b2nt0w0k.js → index-pgsswppj.js} +1 -1
- package/{index-m9c7k0gm.js → index-pvb4vdwr.js} +1 -1
- package/{index-ayy5ecax.js → index-q0y2jq9j.js} +13 -1
- package/{index-k1n0h7dk.js → index-q8sn827n.js} +254 -126
- package/{index-dq6wq1tv.js → index-r50qen0z.js} +10 -10
- package/{index-5n0hf8r9.js → index-rntcdss1.js} +3 -3
- package/{index-a8h184wr.js → index-sjak5v5b.js} +6 -6
- package/{index-x23688ar.js → index-w8kmx1b6.js} +8 -4
- package/{index-qcracvc9.js → index-y74k5anj.js} +3 -3
- package/{index-xxe90kqy.js → index-zkvpaggp.js} +2 -2
- package/index.js +20 -20
- package/{library.command-wpedct6c.js → library.command-em103wtn.js} +4 -4
- package/{localRegistry.command-3cew8f75.js → localRegistry.command-gz8apn4y.js} +10 -10
- package/{module.command-8n4kct3e.js → module.command-451pztkc.js} +7 -7
- package/{package.command-fcbd0b1d.js → package.command-rfhyf38t.js} +5 -5
- package/package.json +2 -2
- package/{page.command-x4379grq.js → page.command-kt19da5g.js} +4 -4
- package/{primitive.command-8etmnqhq.js → primitive.command-pgy69ere.js} +8 -8
- package/{quality.command-hfx7z8yr.js → quality.command-gpe2vqb5.js} +3 -3
- package/{repair.command-j0fs5qkz.js → repair.command-2sbrz187.js} +6 -6
- package/{scalar.command-2emk89eh.js → scalar.command-jcf4e5sv.js} +6 -6
- package/{subspace.command-nr32nm35.js → subspace.command-kqzqtk0p.js} +6 -6
- package/{workflow.command-cvpmzwjg.js → workflow.command-m4z25a2p.js} +11 -11
- package/{workspace.command-rqnms515.js → workspace.command-dqgj9zbk.js} +22 -22
|
@@ -2,16 +2,16 @@
|
|
|
2
2
|
import {
|
|
3
3
|
runner,
|
|
4
4
|
script
|
|
5
|
-
} from "./index-
|
|
5
|
+
} from "./index-cezy7yb3.js";
|
|
6
6
|
import {
|
|
7
7
|
WorkspaceExecutor,
|
|
8
8
|
AppExecutor,
|
|
9
9
|
LibExecutor,
|
|
10
10
|
PkgExecutor
|
|
11
|
-
} from "./index-
|
|
11
|
+
} from "./index-q8sn827n.js";
|
|
12
12
|
import {
|
|
13
13
|
LibraryScript
|
|
14
|
-
} from "./index-
|
|
14
|
+
} from "./index-zkvpaggp.js";
|
|
15
15
|
import {
|
|
16
16
|
HmrWatcher,
|
|
17
17
|
WatchRootResolver
|
|
@@ -19,11 +19,21 @@ import {
|
|
|
19
19
|
import {
|
|
20
20
|
SlicePlanner,
|
|
21
21
|
formatSlicePlan
|
|
22
|
-
} from "./index-
|
|
22
|
+
} from "./index-pvb4vdwr.js";
|
|
23
23
|
import {
|
|
24
24
|
resolveMobileTargets
|
|
25
25
|
} from "./index-47pj3aee.js";
|
|
26
26
|
|
|
27
|
+
// pkgs/@akanjs/cli/openBrowser.ts
|
|
28
|
+
import { spawn } from "child_process";
|
|
29
|
+
function openBrowser(url) {
|
|
30
|
+
const command = process.platform === "darwin" ? ["open", url] : process.platform === "win32" ? ["cmd", "/c", "start", "", url] : ["xdg-open", url];
|
|
31
|
+
const child = spawn(command[0], command.slice(1), { detached: true, stdio: "ignore" });
|
|
32
|
+
child.on("error", () => {});
|
|
33
|
+
child.unref();
|
|
34
|
+
return Promise.resolve();
|
|
35
|
+
}
|
|
36
|
+
|
|
27
37
|
// pkgs/@akanjs/devkit/applicationBuildReporter.ts
|
|
28
38
|
import { Logger } from "akanjs/common";
|
|
29
39
|
|
|
@@ -71,7 +81,7 @@ Caused by: ${ApplicationBuildReporter.formatError(error.cause)}` : "";
|
|
|
71
81
|
|
|
72
82
|
// pkgs/@akanjs/cli/application/application.script.ts
|
|
73
83
|
import { confirm } from "@inquirer/prompts";
|
|
74
|
-
import { Logger as
|
|
84
|
+
import { Logger as Logger6 } from "akanjs/common";
|
|
75
85
|
|
|
76
86
|
// pkgs/@akanjs/cli/application/application.runner.ts
|
|
77
87
|
import path5 from "path";
|
|
@@ -239,6 +249,8 @@ class IncrementalBuilderHost {
|
|
|
239
249
|
app;
|
|
240
250
|
ready = false;
|
|
241
251
|
#onMessage;
|
|
252
|
+
#stdio;
|
|
253
|
+
#onOutput;
|
|
242
254
|
#proc = null;
|
|
243
255
|
#status = "stopped";
|
|
244
256
|
#restartAttempts = 0;
|
|
@@ -249,11 +261,13 @@ class IncrementalBuilderHost {
|
|
|
249
261
|
#manualStop = false;
|
|
250
262
|
#inFlight = new Map;
|
|
251
263
|
#startOptions = {};
|
|
252
|
-
constructor({ app, entry, env, onMessage }) {
|
|
264
|
+
constructor({ app, entry, env, stdio = "inherit", onMessage, onOutput }) {
|
|
253
265
|
this.app = app;
|
|
254
266
|
this.entry = entry;
|
|
255
267
|
this.env = env;
|
|
268
|
+
this.#stdio = stdio;
|
|
256
269
|
this.#onMessage = onMessage;
|
|
270
|
+
this.#onOutput = onOutput ?? null;
|
|
257
271
|
}
|
|
258
272
|
get status() {
|
|
259
273
|
return this.#status;
|
|
@@ -279,7 +293,7 @@ class IncrementalBuilderHost {
|
|
|
279
293
|
proc = Bun.spawn(["bun", this.entry], {
|
|
280
294
|
cwd: this.app.cwdPath,
|
|
281
295
|
env: { ...this.env, AKAN_WATCH: "1", ...afterRecycle ? { AKAN_BUILDER_ANNOUNCE_BOOT: "1" } : {} },
|
|
282
|
-
stdio: ["ignore",
|
|
296
|
+
stdio: ["ignore", this.#stdio, this.#stdio],
|
|
283
297
|
ipc: (msg) => {
|
|
284
298
|
if (this.#proc !== proc)
|
|
285
299
|
return;
|
|
@@ -327,8 +341,24 @@ class IncrementalBuilderHost {
|
|
|
327
341
|
}
|
|
328
342
|
});
|
|
329
343
|
this.#proc = proc;
|
|
344
|
+
if (this.#stdio === "pipe" && this.#onOutput) {
|
|
345
|
+
this.#drain(proc.stdout, "stdout");
|
|
346
|
+
this.#drain(proc.stderr, "stderr");
|
|
347
|
+
}
|
|
330
348
|
this.logger.verbose(`builder spawned pid=${proc.pid} entry=${this.entry}${isRestart ? " restart=1" : ""}`);
|
|
331
349
|
}
|
|
350
|
+
async#drain(stream, kind) {
|
|
351
|
+
if (!stream)
|
|
352
|
+
return;
|
|
353
|
+
const decoder = new TextDecoder;
|
|
354
|
+
try {
|
|
355
|
+
for await (const chunk of stream) {
|
|
356
|
+
const text = decoder.decode(chunk, { stream: true });
|
|
357
|
+
if (text)
|
|
358
|
+
this.#onOutput?.(kind, text);
|
|
359
|
+
}
|
|
360
|
+
} catch {}
|
|
361
|
+
}
|
|
332
362
|
#scheduleRestart() {
|
|
333
363
|
if (this.#manualStop || this.#restartTimer)
|
|
334
364
|
return;
|
|
@@ -424,7 +454,10 @@ class IncrementalBuilderHost {
|
|
|
424
454
|
this.ready = false;
|
|
425
455
|
this.#status = "stopped";
|
|
426
456
|
}
|
|
427
|
-
static async create(app, env, onMessage
|
|
457
|
+
static async create(app, env, onMessage, {
|
|
458
|
+
stdio = "inherit",
|
|
459
|
+
onOutput
|
|
460
|
+
} = {}) {
|
|
428
461
|
const candidates = [
|
|
429
462
|
path.join(app.workspace.workspaceRoot, "pkgs/@akanjs/devkit/incrementalBuilder/incrementalBuilder.proc.ts"),
|
|
430
463
|
path.join(app.workspace.workspaceRoot, "node_modules/@akanjs/devkit/incrementalBuilder/incrementalBuilder.proc.ts"),
|
|
@@ -433,7 +466,7 @@ class IncrementalBuilderHost {
|
|
|
433
466
|
];
|
|
434
467
|
for (const c of candidates)
|
|
435
468
|
if (await Bun.file(c).exists())
|
|
436
|
-
return new IncrementalBuilderHost({ app, entry: c, env, onMessage });
|
|
469
|
+
return new IncrementalBuilderHost({ app, entry: c, env, stdio, onMessage, onOutput });
|
|
437
470
|
throw new Error(`[cli] frontend builder entry not found; looked in: ${candidates.join(", ")}`);
|
|
438
471
|
}
|
|
439
472
|
}
|
|
@@ -502,6 +535,15 @@ var shouldRestartBuilderByDevPlan = (message) => message.devPlan?.actions.includ
|
|
|
502
535
|
var shouldAbandonBackendRecovery = (attempts, maxAttempts = BACKEND_RECOVERY_MAX_ATTEMPTS) => attempts >= maxAttempts;
|
|
503
536
|
var normalizeBackendReportedGeneration = (generation) => generation >= 0 ? generation : undefined;
|
|
504
537
|
var shouldRestartDevHostByDevPlan = (message) => message.devPlan?.actions.includes("restart-dev-host") ?? message.kinds.includes("config");
|
|
538
|
+
var devHostStateByBackendState = {
|
|
539
|
+
starting: "starting",
|
|
540
|
+
ready: "ready",
|
|
541
|
+
"restart-pending": "restarting",
|
|
542
|
+
recovering: "recovering",
|
|
543
|
+
stopping: "stopped",
|
|
544
|
+
stopped: "stopped"
|
|
545
|
+
};
|
|
546
|
+
var devHostStateOf = (state, gaveUp) => state === "stopped" && gaveUp ? "failed" : devHostStateByBackendState[state];
|
|
505
547
|
var RESTART_ROLE_ORDER = ["server", "shared", "barrel", "config"];
|
|
506
548
|
var generationValue = (generation) => generation ?? -1;
|
|
507
549
|
var isLegacyBackendFallbackFile = (file, workspaceRoot) => {
|
|
@@ -826,8 +868,10 @@ var BUILDER_RECOVERY_MAX_DELAY_MS = 60000;
|
|
|
826
868
|
class AkanAppHost {
|
|
827
869
|
app;
|
|
828
870
|
logger = new Logger3("AkanAppHost");
|
|
829
|
-
|
|
871
|
+
stdio;
|
|
830
872
|
env;
|
|
873
|
+
#onDevEvent;
|
|
874
|
+
#lastDevState = null;
|
|
831
875
|
#backend = null;
|
|
832
876
|
#builder = null;
|
|
833
877
|
#backendReady = false;
|
|
@@ -866,12 +910,23 @@ class AkanAppHost {
|
|
|
866
910
|
#builderGapStamp = null;
|
|
867
911
|
#pendingBuilderMessages = [];
|
|
868
912
|
#builderRequests = new BuilderRequestRouter;
|
|
869
|
-
constructor(app, {
|
|
913
|
+
constructor(app, {
|
|
914
|
+
env,
|
|
915
|
+
stdio = "inherit",
|
|
916
|
+
onDevEvent
|
|
917
|
+
}) {
|
|
870
918
|
this.app = app;
|
|
871
919
|
this.env = env;
|
|
872
|
-
this.
|
|
920
|
+
this.stdio = stdio;
|
|
921
|
+
this.#onDevEvent = onDevEvent ?? null;
|
|
873
922
|
this.#backendGraph = new BackendImportGraph(app, this.logger);
|
|
874
923
|
}
|
|
924
|
+
#emitDevEvent(state, detail) {
|
|
925
|
+
if (!this.#onDevEvent || state === this.#lastDevState)
|
|
926
|
+
return;
|
|
927
|
+
this.#lastDevState = state;
|
|
928
|
+
this.#onDevEvent({ app: this.app.name, state, ...detail ? { detail } : {} });
|
|
929
|
+
}
|
|
875
930
|
async start() {
|
|
876
931
|
if (this.#backend)
|
|
877
932
|
await this.#stopBackend();
|
|
@@ -926,7 +981,7 @@ class AkanAppHost {
|
|
|
926
981
|
this.#backendStderrTail = [];
|
|
927
982
|
const backend = Bun.spawn(["bun", `apps/${this.app.name}/main.ts`], {
|
|
928
983
|
cwd: this.app.workspace.workspaceRoot,
|
|
929
|
-
stdio: this.
|
|
984
|
+
stdio: this.stdio === "pipe" ? ["ignore", "pipe", "pipe"] : ["inherit", "inherit", "inherit"],
|
|
930
985
|
env: this.env,
|
|
931
986
|
ipc: (msg) => {
|
|
932
987
|
if (!msg || typeof msg !== "object")
|
|
@@ -967,7 +1022,7 @@ class AkanAppHost {
|
|
|
967
1022
|
});
|
|
968
1023
|
this.#backend = backend;
|
|
969
1024
|
this.logger.verbose(`backend spawned pid=${backend.pid}`);
|
|
970
|
-
if (this.
|
|
1025
|
+
if (this.stdio === "pipe") {
|
|
971
1026
|
this.#forwardBackendStream(backend.stderr, "stderr");
|
|
972
1027
|
this.#forwardBackendStream(backend.stdout, "stdout");
|
|
973
1028
|
}
|
|
@@ -988,14 +1043,13 @@ class AkanAppHost {
|
|
|
988
1043
|
try {
|
|
989
1044
|
for await (const chunk of stream) {
|
|
990
1045
|
const text = decoder.decode(chunk, { stream: true });
|
|
991
|
-
if (!text
|
|
1046
|
+
if (!text)
|
|
992
1047
|
continue;
|
|
993
1048
|
if (kind === "stderr") {
|
|
994
1049
|
this.#recordBackendStderr(text);
|
|
995
|
-
|
|
996
|
-
} else
|
|
997
|
-
|
|
998
|
-
}
|
|
1050
|
+
process.stderr.write(text);
|
|
1051
|
+
} else
|
|
1052
|
+
process.stdout.write(text);
|
|
999
1053
|
}
|
|
1000
1054
|
} catch {}
|
|
1001
1055
|
}
|
|
@@ -1042,6 +1096,7 @@ class AkanAppHost {
|
|
|
1042
1096
|
const prev = this.#backendLifecycleState;
|
|
1043
1097
|
this.#backendLifecycleState = next;
|
|
1044
1098
|
this.logger.verbose(`[backend-lifecycle] ${prev} -> ${next}${detail ? ` ${detail}` : ""}`);
|
|
1099
|
+
this.#emitDevEvent(devHostStateOf(next, this.#backendGaveUp), detail);
|
|
1045
1100
|
}
|
|
1046
1101
|
#sendToBackend(message) {
|
|
1047
1102
|
if (!this.#backend || !this.#backendReady) {
|
|
@@ -1371,6 +1426,7 @@ ${this.#backendStderrTail.join(`
|
|
|
1371
1426
|
return;
|
|
1372
1427
|
}
|
|
1373
1428
|
this.#suspended = true;
|
|
1429
|
+
this.#emitDevEvent("suspended", `idle ${Math.round(idleMs / 1000)}s`);
|
|
1374
1430
|
this.#stopBuilder();
|
|
1375
1431
|
this.#openBuilderGap("idle suspend");
|
|
1376
1432
|
this.logger.info(`[idle-suspend] no build activity for ${Math.round(idleMs / 1000)}s; released the builder \u2014 the next edit or route request brings it back`);
|
|
@@ -1435,6 +1491,7 @@ ${this.#backendStderrTail.join(`
|
|
|
1435
1491
|
this.#wokeAtMono = performance.now();
|
|
1436
1492
|
this.#flushPendingBuilderMessages();
|
|
1437
1493
|
this.#armIdleSuspend();
|
|
1494
|
+
this.#emitDevEvent(devHostStateOf(this.#backendLifecycleState, this.#backendGaveUp));
|
|
1438
1495
|
}
|
|
1439
1496
|
}
|
|
1440
1497
|
async#applyIdleWake(batch) {
|
|
@@ -1771,6 +1828,9 @@ ${this.#backendStderrTail.join(`
|
|
|
1771
1828
|
for (let attempt = 1;attempt <= BUILDER_START_MAX_ATTEMPTS; attempt++) {
|
|
1772
1829
|
this.#builder = await IncrementalBuilderHost.create(this.app, this.env, (msg) => {
|
|
1773
1830
|
this.#enqueueBuilderMessage(msg);
|
|
1831
|
+
}, {
|
|
1832
|
+
stdio: this.stdio,
|
|
1833
|
+
onOutput: (kind, text) => void (kind === "stderr" ? process.stderr : process.stdout).write(text)
|
|
1774
1834
|
});
|
|
1775
1835
|
try {
|
|
1776
1836
|
await this.#waitForBuilderReady(attempt, { announceBootState });
|
|
@@ -1904,22 +1964,10 @@ ${uniqueCandidates.map((candidate) => ` - ${candidate}`).join(`
|
|
|
1904
1964
|
// pkgs/@akanjs/cli/application/application.runner.ts
|
|
1905
1965
|
import { Logger as Logger4 } from "akanjs/common";
|
|
1906
1966
|
import ora from "ora";
|
|
1907
|
-
|
|
1908
|
-
|
|
1909
|
-
|
|
1910
|
-
|
|
1911
|
-
const command = process.platform === "darwin" ? ["open", url] : process.platform === "win32" ? ["cmd", "/c", "start", "", url] : ["xdg-open", url];
|
|
1912
|
-
const child = spawn(command[0], command.slice(1), { detached: true, stdio: "ignore" });
|
|
1913
|
-
child.on("error", () => {});
|
|
1914
|
-
child.unref();
|
|
1915
|
-
return Promise.resolve();
|
|
1916
|
-
}
|
|
1917
|
-
|
|
1918
|
-
// pkgs/@akanjs/cli/application/application.runner.ts
|
|
1919
|
-
var loadBuildRunner = async () => (await import("./applicationBuildRunner-27wyznpv.js")).ApplicationBuildRunner;
|
|
1920
|
-
var loadReleasePackager = async () => (await import("./applicationReleasePackager-ktjt0erc.js")).ApplicationReleasePackager;
|
|
1921
|
-
var loadCapacitorApp = async () => (await import("./capacitorApp-kwa67kzs.js")).CapacitorApp;
|
|
1922
|
-
var loadAbstractCompactor = async () => (await import("./abstractCompactor-pwc6d6yk.js")).AbstractCompactor;
|
|
1967
|
+
var loadBuildRunner = async () => (await import("./applicationBuildRunner-b21m07fs.js")).ApplicationBuildRunner;
|
|
1968
|
+
var loadReleasePackager = async () => (await import("./applicationReleasePackager-3wh58n2m.js")).ApplicationReleasePackager;
|
|
1969
|
+
var loadCapacitorApp = async () => (await import("./capacitorApp-gzcx6txb.js")).CapacitorApp;
|
|
1970
|
+
var loadAbstractCompactor = async () => (await import("./abstractCompactor-cefxzggj.js")).AbstractCompactor;
|
|
1923
1971
|
var loadPrompts = async () => await import("@inquirer/prompts");
|
|
1924
1972
|
|
|
1925
1973
|
class ApplicationRunner extends runner("application") {
|
|
@@ -2086,9 +2134,14 @@ try {
|
|
|
2086
2134
|
} = {}) {
|
|
2087
2135
|
return new (await loadBuildRunner())(app, { fast, reporter }).build({ spinner });
|
|
2088
2136
|
}
|
|
2089
|
-
async start(app, {
|
|
2137
|
+
async start(app, {
|
|
2138
|
+
open = false,
|
|
2139
|
+
onStart,
|
|
2140
|
+
onDevEvent,
|
|
2141
|
+
stdio = "inherit"
|
|
2142
|
+
} = {}) {
|
|
2090
2143
|
const { env } = await app.prepareCommand("start");
|
|
2091
|
-
const appHost = await new AkanAppHost(app, { env,
|
|
2144
|
+
const appHost = await new AkanAppHost(app, { env, stdio, onDevEvent }).start();
|
|
2092
2145
|
onStart?.();
|
|
2093
2146
|
if (open)
|
|
2094
2147
|
setTimeout(() => openBrowser(`http://localhost:${env.AKAN_PUBLIC_CLIENT_PORT ?? env.PORT ?? "8282"}`), 3000);
|
|
@@ -2320,6 +2373,444 @@ ${detail}`
|
|
|
2320
2373
|
}
|
|
2321
2374
|
}
|
|
2322
2375
|
|
|
2376
|
+
// pkgs/@akanjs/cli/application/devPortReclaimer.ts
|
|
2377
|
+
class DevPortReclaimer {
|
|
2378
|
+
static lsofTimeoutMs = 3000;
|
|
2379
|
+
static releaseTimeoutMs = 8000;
|
|
2380
|
+
static maxAncestorDepth = 8;
|
|
2381
|
+
static #releasePollMs = 100;
|
|
2382
|
+
static wsPortOffset = 1e4;
|
|
2383
|
+
static portsFor(ports) {
|
|
2384
|
+
return [...new Set(ports.flatMap((port) => [port, port + DevPortReclaimer.wsPortOffset]))].sort((left, right) => left - right);
|
|
2385
|
+
}
|
|
2386
|
+
static isAkanCommand(command) {
|
|
2387
|
+
return /(^|\/)akan(\s|$)/.test(command) || command.includes("@akanjs/cli") || command.includes("akanjs/server") || /\bapps\/[^/\s]+\/main\.ts\b/.test(command) || command.includes("incrementalBuilder.proc") || command.includes("rscWorker");
|
|
2388
|
+
}
|
|
2389
|
+
async reclaim(ports) {
|
|
2390
|
+
const killed = [];
|
|
2391
|
+
const foreign = [];
|
|
2392
|
+
const seen = new Set;
|
|
2393
|
+
for (const port of DevPortReclaimer.portsFor(ports)) {
|
|
2394
|
+
for (const holder of await this.holdersOf(port)) {
|
|
2395
|
+
if (seen.has(holder.pid))
|
|
2396
|
+
continue;
|
|
2397
|
+
seen.add(holder.pid);
|
|
2398
|
+
if (!holder.akan) {
|
|
2399
|
+
foreign.push(holder);
|
|
2400
|
+
continue;
|
|
2401
|
+
}
|
|
2402
|
+
if (await this.#terminate(holder.pid))
|
|
2403
|
+
killed.push(holder);
|
|
2404
|
+
else
|
|
2405
|
+
foreign.push(holder);
|
|
2406
|
+
}
|
|
2407
|
+
}
|
|
2408
|
+
return { killed, foreign };
|
|
2409
|
+
}
|
|
2410
|
+
async holdersOf(port) {
|
|
2411
|
+
const holders = [];
|
|
2412
|
+
for (const listenerPid of await this.#pidsOn(port)) {
|
|
2413
|
+
if (listenerPid === process.pid || listenerPid === process.ppid)
|
|
2414
|
+
continue;
|
|
2415
|
+
const command = await this.#commandOf(listenerPid);
|
|
2416
|
+
if (!command)
|
|
2417
|
+
continue;
|
|
2418
|
+
if (!DevPortReclaimer.isAkanCommand(command)) {
|
|
2419
|
+
holders.push({ pid: listenerPid, command, port, listenerPid, akan: false });
|
|
2420
|
+
continue;
|
|
2421
|
+
}
|
|
2422
|
+
const root = await this.#akanRootOf(listenerPid, command);
|
|
2423
|
+
holders.push({ pid: root.pid, command: root.command, port, listenerPid, akan: true });
|
|
2424
|
+
}
|
|
2425
|
+
return holders;
|
|
2426
|
+
}
|
|
2427
|
+
async#akanRootOf(pid, command) {
|
|
2428
|
+
let root = { pid, command };
|
|
2429
|
+
for (let depth = 0;depth < DevPortReclaimer.maxAncestorDepth; depth += 1) {
|
|
2430
|
+
const parentPid = await this.#ppidOf(root.pid);
|
|
2431
|
+
if (parentPid === null || parentPid <= 1 || parentPid === process.pid || parentPid === process.ppid)
|
|
2432
|
+
break;
|
|
2433
|
+
const parentCommand = await this.#commandOf(parentPid);
|
|
2434
|
+
if (!parentCommand || !DevPortReclaimer.isAkanCommand(parentCommand))
|
|
2435
|
+
break;
|
|
2436
|
+
root = { pid: parentPid, command: parentCommand };
|
|
2437
|
+
}
|
|
2438
|
+
return root;
|
|
2439
|
+
}
|
|
2440
|
+
async#ppidOf(pid) {
|
|
2441
|
+
const output = await this.#run(["ps", "-p", String(pid), "-o", "ppid="]);
|
|
2442
|
+
const parsed = Number(output.trim());
|
|
2443
|
+
return Number.isInteger(parsed) && parsed > 0 ? parsed : null;
|
|
2444
|
+
}
|
|
2445
|
+
async#pidsOn(port) {
|
|
2446
|
+
const output = await this.#run(["lsof", "-ti", `tcp:${port}`, "-sTCP:LISTEN"]);
|
|
2447
|
+
return [
|
|
2448
|
+
...new Set(output.split(`
|
|
2449
|
+
`).map((line) => Number(line.trim())).filter((pid) => Number.isInteger(pid) && pid > 0))
|
|
2450
|
+
];
|
|
2451
|
+
}
|
|
2452
|
+
async#commandOf(pid) {
|
|
2453
|
+
const output = await this.#run(["ps", "-p", String(pid), "-o", "command="]);
|
|
2454
|
+
return output.trim() || null;
|
|
2455
|
+
}
|
|
2456
|
+
async#terminate(pid) {
|
|
2457
|
+
if (!this.#signal(pid, "SIGTERM"))
|
|
2458
|
+
return !this.#alive(pid);
|
|
2459
|
+
const deadline = Date.now() + DevPortReclaimer.releaseTimeoutMs;
|
|
2460
|
+
while (Date.now() < deadline) {
|
|
2461
|
+
if (!this.#alive(pid))
|
|
2462
|
+
return true;
|
|
2463
|
+
await Bun.sleep(DevPortReclaimer.#releasePollMs);
|
|
2464
|
+
}
|
|
2465
|
+
this.#signal(pid, "SIGKILL");
|
|
2466
|
+
await Bun.sleep(DevPortReclaimer.#releasePollMs);
|
|
2467
|
+
return !this.#alive(pid);
|
|
2468
|
+
}
|
|
2469
|
+
#signal(pid, signal) {
|
|
2470
|
+
try {
|
|
2471
|
+
process.kill(pid, signal);
|
|
2472
|
+
return true;
|
|
2473
|
+
} catch {
|
|
2474
|
+
return false;
|
|
2475
|
+
}
|
|
2476
|
+
}
|
|
2477
|
+
#alive(pid) {
|
|
2478
|
+
try {
|
|
2479
|
+
process.kill(pid, 0);
|
|
2480
|
+
return true;
|
|
2481
|
+
} catch (error) {
|
|
2482
|
+
return error.code === "EPERM";
|
|
2483
|
+
}
|
|
2484
|
+
}
|
|
2485
|
+
async#run(command) {
|
|
2486
|
+
const proc = Bun.spawn(command, { stdio: ["ignore", "pipe", "ignore"] });
|
|
2487
|
+
const timer = setTimeout(() => proc.kill("SIGKILL"), DevPortReclaimer.lsofTimeoutMs);
|
|
2488
|
+
try {
|
|
2489
|
+
return await new Response(proc.stdout).text();
|
|
2490
|
+
} catch {
|
|
2491
|
+
return "";
|
|
2492
|
+
} finally {
|
|
2493
|
+
clearTimeout(timer);
|
|
2494
|
+
}
|
|
2495
|
+
}
|
|
2496
|
+
}
|
|
2497
|
+
|
|
2498
|
+
// pkgs/@akanjs/cli/application/devStreamView.ts
|
|
2499
|
+
import { Logger as Logger5 } from "akanjs/common";
|
|
2500
|
+
import chalk from "chalk";
|
|
2501
|
+
var appColors = ["cyan", "magenta", "yellow", "green", "blue", "red"];
|
|
2502
|
+
|
|
2503
|
+
class DevStreamView {
|
|
2504
|
+
#logger = new Logger5("akan start");
|
|
2505
|
+
#partial = new Map;
|
|
2506
|
+
#lastState = new Map;
|
|
2507
|
+
#color = new Map;
|
|
2508
|
+
#width = 0;
|
|
2509
|
+
constructor(appNames) {
|
|
2510
|
+
this.#width = appNames.reduce((max, name) => Math.max(max, name.length), 0);
|
|
2511
|
+
appNames.forEach((name, idx) => {
|
|
2512
|
+
this.#color.set(name, appColors[idx % appColors.length] ?? "cyan");
|
|
2513
|
+
});
|
|
2514
|
+
}
|
|
2515
|
+
onOutput = (app, kind, text) => {
|
|
2516
|
+
const key = `${app}:${kind}`;
|
|
2517
|
+
const pending = `${this.#partial.get(key) ?? ""}${text}`;
|
|
2518
|
+
const lines = pending.split(`
|
|
2519
|
+
`);
|
|
2520
|
+
this.#partial.set(key, lines.pop() ?? "");
|
|
2521
|
+
if (lines.length === 0)
|
|
2522
|
+
return;
|
|
2523
|
+
const prefixed = lines.map((line) => `${this.#prefix(app)}${line}`).join(`
|
|
2524
|
+
`);
|
|
2525
|
+
(kind === "stderr" ? process.stderr : process.stdout).write(`${prefixed}
|
|
2526
|
+
`);
|
|
2527
|
+
};
|
|
2528
|
+
onStatus = (statuses) => {
|
|
2529
|
+
for (const status of statuses) {
|
|
2530
|
+
if (this.#lastState.get(status.name) === status.state)
|
|
2531
|
+
continue;
|
|
2532
|
+
this.#lastState.set(status.name, status.state);
|
|
2533
|
+
const detail = status.detail ? ` (${status.detail})` : "";
|
|
2534
|
+
const where = status.state === "ready" ? ` \u2014 ${status.url}` : "";
|
|
2535
|
+
const line = `${status.name} ${status.state}${detail}${where}`;
|
|
2536
|
+
if (status.state === "failed")
|
|
2537
|
+
this.#logger.error(line);
|
|
2538
|
+
else if (status.state === "stopped")
|
|
2539
|
+
this.#logger.warn(line);
|
|
2540
|
+
else
|
|
2541
|
+
this.#logger.info(line);
|
|
2542
|
+
}
|
|
2543
|
+
};
|
|
2544
|
+
onNote = (text, level) => {
|
|
2545
|
+
if (level === "warn")
|
|
2546
|
+
this.#logger.warn(text);
|
|
2547
|
+
else
|
|
2548
|
+
this.#logger.info(text);
|
|
2549
|
+
};
|
|
2550
|
+
waitForExit = () => new Promise(() => {
|
|
2551
|
+
return;
|
|
2552
|
+
});
|
|
2553
|
+
close = () => {
|
|
2554
|
+
for (const [key, remainder] of this.#partial) {
|
|
2555
|
+
if (!remainder)
|
|
2556
|
+
continue;
|
|
2557
|
+
const app = key.slice(0, key.lastIndexOf(":"));
|
|
2558
|
+
process.stdout.write(`${this.#prefix(app)}${remainder}
|
|
2559
|
+
`);
|
|
2560
|
+
}
|
|
2561
|
+
this.#partial.clear();
|
|
2562
|
+
};
|
|
2563
|
+
#prefix(app) {
|
|
2564
|
+
const color = this.#color.get(app) ?? "cyan";
|
|
2565
|
+
return `${chalk[color](app.padEnd(this.#width))} ${chalk.dim("\u2502")} `;
|
|
2566
|
+
}
|
|
2567
|
+
}
|
|
2568
|
+
|
|
2569
|
+
// pkgs/@akanjs/cli/application/devSupervisor.ts
|
|
2570
|
+
class DevSupervisor {
|
|
2571
|
+
static supervisedEnvKey = "AKAN_DEV_SUPERVISED";
|
|
2572
|
+
static childHooks() {
|
|
2573
|
+
if (process.env[DevSupervisor.supervisedEnvKey] !== "1" || !process.send)
|
|
2574
|
+
return {};
|
|
2575
|
+
const send = process.send.bind(process);
|
|
2576
|
+
return {
|
|
2577
|
+
stdio: "pipe",
|
|
2578
|
+
onDevEvent: (event) => {
|
|
2579
|
+
try {
|
|
2580
|
+
send(event);
|
|
2581
|
+
} catch {}
|
|
2582
|
+
}
|
|
2583
|
+
};
|
|
2584
|
+
}
|
|
2585
|
+
static readyTimeoutMs = 180000;
|
|
2586
|
+
static shutdownGraceMs = 12000;
|
|
2587
|
+
#options;
|
|
2588
|
+
#children = new Map;
|
|
2589
|
+
#view = null;
|
|
2590
|
+
#stopping = false;
|
|
2591
|
+
constructor(options) {
|
|
2592
|
+
this.#options = options;
|
|
2593
|
+
}
|
|
2594
|
+
get statuses() {
|
|
2595
|
+
return [...this.#children.values()].map((child) => child.status);
|
|
2596
|
+
}
|
|
2597
|
+
async run(view) {
|
|
2598
|
+
this.#view = view;
|
|
2599
|
+
const ports = await this.#assignPorts();
|
|
2600
|
+
for (const app of this.#options.apps)
|
|
2601
|
+
this.#children.set(app.name, this.#makeChild(app, ports.get(app.name) ?? 0));
|
|
2602
|
+
this.#publishStatus();
|
|
2603
|
+
const stopped = this.#installSignalHandlers();
|
|
2604
|
+
this.#bootInOrder();
|
|
2605
|
+
await Promise.race([view.waitForExit(), stopped]);
|
|
2606
|
+
await this.stop();
|
|
2607
|
+
view.close?.();
|
|
2608
|
+
}
|
|
2609
|
+
async#assignPorts() {
|
|
2610
|
+
const taken = new Set;
|
|
2611
|
+
const ports = new Map;
|
|
2612
|
+
for (const app of this.#options.apps) {
|
|
2613
|
+
let port = await app.getDevPort();
|
|
2614
|
+
while (taken.has(port))
|
|
2615
|
+
port += 1;
|
|
2616
|
+
taken.add(port);
|
|
2617
|
+
ports.set(app.name, port);
|
|
2618
|
+
}
|
|
2619
|
+
return ports;
|
|
2620
|
+
}
|
|
2621
|
+
#makeChild(app, port) {
|
|
2622
|
+
let markReady;
|
|
2623
|
+
const ready = new Promise((resolve) => {
|
|
2624
|
+
markReady = resolve;
|
|
2625
|
+
});
|
|
2626
|
+
return {
|
|
2627
|
+
status: {
|
|
2628
|
+
app,
|
|
2629
|
+
name: app.name,
|
|
2630
|
+
port,
|
|
2631
|
+
url: `http://localhost:${port}`,
|
|
2632
|
+
state: "starting",
|
|
2633
|
+
detail: "queued",
|
|
2634
|
+
exitCode: null
|
|
2635
|
+
},
|
|
2636
|
+
proc: null,
|
|
2637
|
+
ready,
|
|
2638
|
+
markReady,
|
|
2639
|
+
opened: false
|
|
2640
|
+
};
|
|
2641
|
+
}
|
|
2642
|
+
async#bootInOrder() {
|
|
2643
|
+
const concurrency = Math.max(1, Math.trunc(this.#options.concurrency ?? 1));
|
|
2644
|
+
const queue = [...this.#children.values()];
|
|
2645
|
+
const waves = [];
|
|
2646
|
+
for (let idx = 0;idx < queue.length; idx += concurrency)
|
|
2647
|
+
waves.push(queue.slice(idx, idx + concurrency));
|
|
2648
|
+
for (const wave of waves) {
|
|
2649
|
+
if (this.#stopping)
|
|
2650
|
+
return;
|
|
2651
|
+
for (const child of wave)
|
|
2652
|
+
this.#spawnChild(child);
|
|
2653
|
+
this.#publishStatus();
|
|
2654
|
+
await Promise.all(wave.map(async (child) => await this.#waitForReady(child)));
|
|
2655
|
+
}
|
|
2656
|
+
}
|
|
2657
|
+
async#waitForReady(child) {
|
|
2658
|
+
let timer = null;
|
|
2659
|
+
const expired = new Promise((resolve) => {
|
|
2660
|
+
timer = setTimeout(() => resolve("expired"), DevSupervisor.readyTimeoutMs);
|
|
2661
|
+
});
|
|
2662
|
+
try {
|
|
2663
|
+
if (await Promise.race([child.ready.then(() => "ready"), expired]) === "expired")
|
|
2664
|
+
this.#note(`${child.status.name} has not reported ready after ${Math.round(DevSupervisor.readyTimeoutMs / 1000)}s; starting the next app anyway`, "warn");
|
|
2665
|
+
} finally {
|
|
2666
|
+
if (timer)
|
|
2667
|
+
clearTimeout(timer);
|
|
2668
|
+
}
|
|
2669
|
+
}
|
|
2670
|
+
static childArgs(name, { write = true } = {}) {
|
|
2671
|
+
return [
|
|
2672
|
+
"start",
|
|
2673
|
+
name,
|
|
2674
|
+
"--plain",
|
|
2675
|
+
"--dbup",
|
|
2676
|
+
"false",
|
|
2677
|
+
"--write",
|
|
2678
|
+
String(write)
|
|
2679
|
+
];
|
|
2680
|
+
}
|
|
2681
|
+
#spawnChild(child) {
|
|
2682
|
+
const { app, name, port } = child.status;
|
|
2683
|
+
const args = DevSupervisor.childArgs(name, { write: this.#options.write ?? true });
|
|
2684
|
+
const proc = Bun.spawn([process.execPath, Bun.main, ...args], {
|
|
2685
|
+
cwd: app.workspace.workspaceRoot,
|
|
2686
|
+
env: {
|
|
2687
|
+
...process.env,
|
|
2688
|
+
[DevSupervisor.supervisedEnvKey]: "1",
|
|
2689
|
+
AKAN_PUBLIC_APP_NAME: name,
|
|
2690
|
+
AKAN_DEV_PORT: String(port)
|
|
2691
|
+
},
|
|
2692
|
+
stdio: ["ignore", "pipe", "pipe"],
|
|
2693
|
+
ipc: (message) => {
|
|
2694
|
+
if (!message || typeof message !== "object" || typeof message.state !== "string")
|
|
2695
|
+
return;
|
|
2696
|
+
this.#applyChildEvent(child, message);
|
|
2697
|
+
},
|
|
2698
|
+
serialization: "advanced",
|
|
2699
|
+
onExit: (_proc, exitCode, signalCode) => {
|
|
2700
|
+
child.proc = null;
|
|
2701
|
+
child.status.exitCode = exitCode;
|
|
2702
|
+
if (this.#stopping)
|
|
2703
|
+
return;
|
|
2704
|
+
child.status.state = "stopped";
|
|
2705
|
+
child.status.detail = signalCode ? `killed by ${signalCode}` : `exited with ${exitCode}`;
|
|
2706
|
+
child.markReady();
|
|
2707
|
+
this.#publishStatus();
|
|
2708
|
+
}
|
|
2709
|
+
});
|
|
2710
|
+
child.proc = proc;
|
|
2711
|
+
child.status.state = "starting";
|
|
2712
|
+
child.status.detail = `pid ${proc.pid}`;
|
|
2713
|
+
this.#drain(proc.stdout, name, "stdout");
|
|
2714
|
+
this.#drain(proc.stderr, name, "stderr");
|
|
2715
|
+
}
|
|
2716
|
+
#applyChildEvent(child, event) {
|
|
2717
|
+
child.status.state = event.state;
|
|
2718
|
+
child.status.detail = event.detail ?? "";
|
|
2719
|
+
if (event.state === "ready") {
|
|
2720
|
+
child.markReady();
|
|
2721
|
+
if (this.#options.open && !child.opened) {
|
|
2722
|
+
child.opened = true;
|
|
2723
|
+
openBrowser(child.status.url);
|
|
2724
|
+
}
|
|
2725
|
+
}
|
|
2726
|
+
if (event.state === "failed")
|
|
2727
|
+
child.markReady();
|
|
2728
|
+
this.#publishStatus();
|
|
2729
|
+
}
|
|
2730
|
+
async#drain(stream, name, kind) {
|
|
2731
|
+
const decoder = new TextDecoder;
|
|
2732
|
+
try {
|
|
2733
|
+
for await (const chunk of stream) {
|
|
2734
|
+
const text = decoder.decode(chunk, { stream: true });
|
|
2735
|
+
if (text)
|
|
2736
|
+
this.#view?.onOutput(name, kind, text);
|
|
2737
|
+
}
|
|
2738
|
+
} catch {}
|
|
2739
|
+
}
|
|
2740
|
+
#publishStatus() {
|
|
2741
|
+
this.#view?.onStatus(this.statuses);
|
|
2742
|
+
}
|
|
2743
|
+
#note(text, level) {
|
|
2744
|
+
this.#view?.onNote(text, level);
|
|
2745
|
+
}
|
|
2746
|
+
#installSignalHandlers() {
|
|
2747
|
+
return new Promise((resolve) => {
|
|
2748
|
+
let asked = false;
|
|
2749
|
+
const onSignal = () => {
|
|
2750
|
+
if (asked) {
|
|
2751
|
+
this.#note("abandoning the wait; children may be left running", "warn");
|
|
2752
|
+
process.exit(130);
|
|
2753
|
+
}
|
|
2754
|
+
asked = true;
|
|
2755
|
+
resolve();
|
|
2756
|
+
};
|
|
2757
|
+
process.on("SIGINT", onSignal);
|
|
2758
|
+
process.on("SIGTERM", onSignal);
|
|
2759
|
+
});
|
|
2760
|
+
}
|
|
2761
|
+
async restart(name) {
|
|
2762
|
+
const child = this.#children.get(name);
|
|
2763
|
+
if (!child || this.#stopping)
|
|
2764
|
+
return;
|
|
2765
|
+
const running = child.proc;
|
|
2766
|
+
if (running) {
|
|
2767
|
+
running.kill("SIGTERM");
|
|
2768
|
+
await running.exited;
|
|
2769
|
+
}
|
|
2770
|
+
child.status.state = "starting";
|
|
2771
|
+
child.status.detail = "restarting";
|
|
2772
|
+
child.status.exitCode = null;
|
|
2773
|
+
this.#publishStatus();
|
|
2774
|
+
this.#spawnChild(child);
|
|
2775
|
+
this.#publishStatus();
|
|
2776
|
+
}
|
|
2777
|
+
async stop() {
|
|
2778
|
+
if (this.#stopping)
|
|
2779
|
+
return;
|
|
2780
|
+
this.#stopping = true;
|
|
2781
|
+
const running = [...this.#children.values()].filter((child) => Boolean(child.proc));
|
|
2782
|
+
for (const child of running)
|
|
2783
|
+
child.proc.kill("SIGTERM");
|
|
2784
|
+
const exited = Promise.all(running.map(async (child) => await child.proc.exited));
|
|
2785
|
+
let timer = null;
|
|
2786
|
+
const expired = new Promise((resolve) => {
|
|
2787
|
+
timer = setTimeout(() => resolve("expired"), DevSupervisor.shutdownGraceMs);
|
|
2788
|
+
});
|
|
2789
|
+
try {
|
|
2790
|
+
if (await Promise.race([exited.then(() => "exited"), expired]) === "expired") {
|
|
2791
|
+
for (const child of running) {
|
|
2792
|
+
if (!child.proc.killed) {
|
|
2793
|
+
this.#note(`${child.status.name} did not exit in time; killing it`, "warn");
|
|
2794
|
+
child.proc.kill("SIGKILL");
|
|
2795
|
+
}
|
|
2796
|
+
}
|
|
2797
|
+
}
|
|
2798
|
+
} finally {
|
|
2799
|
+
if (timer)
|
|
2800
|
+
clearTimeout(timer);
|
|
2801
|
+
}
|
|
2802
|
+
}
|
|
2803
|
+
}
|
|
2804
|
+
|
|
2805
|
+
// pkgs/@akanjs/cli/application/devUiMode.ts
|
|
2806
|
+
var resolveDevUi = (plain, { isTty = !!process.stdout.isTTY, columns = process.stdout.columns ?? 0 } = {}) => {
|
|
2807
|
+
if (plain)
|
|
2808
|
+
return { mode: "stream", downgraded: false };
|
|
2809
|
+
if (!isTty || columns <= 0)
|
|
2810
|
+
return { mode: "stream", downgraded: true };
|
|
2811
|
+
return { mode: "tui", downgraded: false };
|
|
2812
|
+
};
|
|
2813
|
+
|
|
2323
2814
|
// pkgs/@akanjs/cli/application/application.script.ts
|
|
2324
2815
|
class ApplicationScript extends script("application", [ApplicationRunner, LibraryScript]) {
|
|
2325
2816
|
static dbShutdownTimeoutMs = 20000;
|
|
@@ -2403,7 +2894,7 @@ class ApplicationScript extends script("application", [ApplicationRunner, Librar
|
|
|
2403
2894
|
spinner.succeed(`Application created in apps/${app.name}`);
|
|
2404
2895
|
await app.scanSync();
|
|
2405
2896
|
if (start)
|
|
2406
|
-
await this.
|
|
2897
|
+
await this.startOne(app, { open: true });
|
|
2407
2898
|
}
|
|
2408
2899
|
async removeApplication(app) {
|
|
2409
2900
|
const spinner = app.spinning("Removing application...");
|
|
@@ -2414,7 +2905,7 @@ class ApplicationScript extends script("application", [ApplicationRunner, Librar
|
|
|
2414
2905
|
const spinner = app.spinning("Planning distributable slice...");
|
|
2415
2906
|
const plan = await this.applicationRunner.planSlice(app);
|
|
2416
2907
|
spinner.succeed(`Slice planned (${plan.libs.length} libs, ${plan.warnings.length} warnings)`);
|
|
2417
|
-
|
|
2908
|
+
Logger6.rawLog(format === "json" ? JSON.stringify(plan, null, 2) : formatSlicePlan(plan));
|
|
2418
2909
|
}
|
|
2419
2910
|
async sync(sys) {
|
|
2420
2911
|
if (sys.type === "app")
|
|
@@ -2425,13 +2916,13 @@ class ApplicationScript extends script("application", [ApplicationRunner, Librar
|
|
|
2425
2916
|
async compact(sys, { module = null, minLines, interactive = false } = {}) {
|
|
2426
2917
|
const { scanned, reports } = await this.applicationRunner.compact(sys, { module, minLines, interactive });
|
|
2427
2918
|
if (!reports.length) {
|
|
2428
|
-
|
|
2919
|
+
Logger6.rawLog(`No abstract file long enough to compact in ${sys.name} (${scanned} scanned)`);
|
|
2429
2920
|
return;
|
|
2430
2921
|
}
|
|
2431
2922
|
for (const report of reports)
|
|
2432
|
-
|
|
2923
|
+
Logger6.rawLog(`${report.status}: ${report.path} (${report.beforeLines} -> ${report.afterLines} lines)`);
|
|
2433
2924
|
const compactedNum = reports.filter((report) => report.status === "compacted").length;
|
|
2434
|
-
|
|
2925
|
+
Logger6.rawLog(`Compacted ${compactedNum}/${reports.length} abstract files in ${sys.name}`);
|
|
2435
2926
|
}
|
|
2436
2927
|
async script(app, filename) {
|
|
2437
2928
|
const scriptFilename = filename ?? await this.applicationRunner.getScriptFilename(app);
|
|
@@ -2448,18 +2939,18 @@ class ApplicationScript extends script("application", [ApplicationRunner, Librar
|
|
|
2448
2939
|
async build(app, { write = true, fast = false, quiet = false } = {}) {
|
|
2449
2940
|
await app.scanSync({ write });
|
|
2450
2941
|
if (!quiet)
|
|
2451
|
-
|
|
2942
|
+
Logger6.rawLog(`Creating an optimized production build for ${app.name}...`);
|
|
2452
2943
|
try {
|
|
2453
2944
|
const result = await this.applicationRunner.build(app, {
|
|
2454
2945
|
fast,
|
|
2455
2946
|
spinner: !quiet
|
|
2456
2947
|
});
|
|
2457
|
-
|
|
2948
|
+
Logger6.rawLog(`${app.name} built in dist/apps/${app.name}`);
|
|
2458
2949
|
if (!quiet)
|
|
2459
2950
|
ApplicationBuildReporter.printSummary(result);
|
|
2460
2951
|
} catch (error) {
|
|
2461
|
-
|
|
2462
|
-
|
|
2952
|
+
Logger6.rawLog(`${app.name} build failed in dist/apps/${app.name}`, undefined, "error");
|
|
2953
|
+
Logger6.rawLog(ApplicationBuildReporter.formatError(error), undefined, "error");
|
|
2463
2954
|
throw error;
|
|
2464
2955
|
}
|
|
2465
2956
|
}
|
|
@@ -2471,7 +2962,7 @@ class ApplicationScript extends script("application", [ApplicationRunner, Librar
|
|
|
2471
2962
|
spinner.succeed(`${app.name} typechecked`);
|
|
2472
2963
|
} catch (error) {
|
|
2473
2964
|
spinner.fail(`${app.name} typecheck failed`);
|
|
2474
|
-
|
|
2965
|
+
Logger6.rawLog(ApplicationBuildReporter.formatError(error), undefined, "error");
|
|
2475
2966
|
throw error;
|
|
2476
2967
|
}
|
|
2477
2968
|
}
|
|
@@ -2496,12 +2987,20 @@ class ApplicationScript extends script("application", [ApplicationRunner, Librar
|
|
|
2496
2987
|
}
|
|
2497
2988
|
await this.applicationRunner.test(exec);
|
|
2498
2989
|
}
|
|
2499
|
-
async start(
|
|
2500
|
-
|
|
2501
|
-
|
|
2502
|
-
|
|
2503
|
-
|
|
2504
|
-
|
|
2990
|
+
async start(apps, { open = false, dbup = true, write = true, plain = false, kill = false, concurrency = 1 } = {}) {
|
|
2991
|
+
const first = apps[0];
|
|
2992
|
+
if (!first)
|
|
2993
|
+
throw new Error("No app selected to start");
|
|
2994
|
+
const { mode, downgraded } = resolveDevUi(plain);
|
|
2995
|
+
if (downgraded)
|
|
2996
|
+
first.workspace.log("no sized terminal to draw on; printing prefixed lines instead");
|
|
2997
|
+
if (kill)
|
|
2998
|
+
await this.reclaimDevPorts(apps);
|
|
2999
|
+
if (mode === "stream" && apps.length === 1)
|
|
3000
|
+
return await this.startOne(first, { open, dbup, write, ...DevSupervisor.childHooks() });
|
|
3001
|
+
await this.#startMany(apps, { open, dbup, write, mode, concurrency });
|
|
3002
|
+
}
|
|
3003
|
+
async startOne(app, { open = false, dbup = true, stdio = "inherit", write = true, onDevEvent } = {}) {
|
|
2505
3004
|
await app.scanSync({ write });
|
|
2506
3005
|
const akanConfig = await app.getConfig();
|
|
2507
3006
|
const databaseMode = process.env.AKAN_DATABASE_MODE ?? akanConfig.defaultDatabaseMode ?? "single";
|
|
@@ -2517,10 +3016,55 @@ class ApplicationScript extends script("application", [ApplicationRunner, Librar
|
|
|
2517
3016
|
onStart: () => {
|
|
2518
3017
|
spinner.succeed(`${app.name} prepared, ready to start`);
|
|
2519
3018
|
},
|
|
2520
|
-
|
|
3019
|
+
onDevEvent,
|
|
3020
|
+
stdio
|
|
2521
3021
|
});
|
|
2522
3022
|
return akanAppHost;
|
|
2523
3023
|
}
|
|
3024
|
+
async reclaimDevPorts(apps) {
|
|
3025
|
+
const workspace = apps[0]?.workspace;
|
|
3026
|
+
if (!workspace)
|
|
3027
|
+
return;
|
|
3028
|
+
const ports = await Promise.all(apps.map(async (app) => await app.getDevPort()));
|
|
3029
|
+
const { killed, foreign } = await new DevPortReclaimer().reclaim(ports);
|
|
3030
|
+
for (const holder of killed)
|
|
3031
|
+
workspace.log(`freed port ${holder.port} \u2014 killed pid ${holder.pid} (${holder.command})`);
|
|
3032
|
+
for (const holder of foreign)
|
|
3033
|
+
Logger6.rawLog(`port ${holder.port} is held by pid ${holder.pid} and was left alone: ${holder.command}`, undefined, "error");
|
|
3034
|
+
if (killed.length === 0 && foreign.length === 0)
|
|
3035
|
+
workspace.log("dev ports are free; nothing to kill");
|
|
3036
|
+
}
|
|
3037
|
+
async#startMany(apps, { open, dbup, write, mode, concurrency }) {
|
|
3038
|
+
const workspace = apps[0]?.workspace;
|
|
3039
|
+
if (!workspace)
|
|
3040
|
+
throw new Error("No app selected to start");
|
|
3041
|
+
const startedDatabase = dbup ? await this.#prepareSharedDatabase(apps) : false;
|
|
3042
|
+
const supervisor = new DevSupervisor({ apps, mode, concurrency, open, write });
|
|
3043
|
+
const view = mode === "tui" ? await (await import("./devTuiView-7jy8f30g.js")).createDevTuiView(supervisor) : new DevStreamView(apps.map((app) => app.name));
|
|
3044
|
+
await supervisor.run(view);
|
|
3045
|
+
if (startedDatabase)
|
|
3046
|
+
await this.#stopDatabase(workspace);
|
|
3047
|
+
}
|
|
3048
|
+
async#prepareSharedDatabase(apps) {
|
|
3049
|
+
const workspace = apps[0]?.workspace;
|
|
3050
|
+
if (!workspace)
|
|
3051
|
+
return false;
|
|
3052
|
+
const modes = new Set;
|
|
3053
|
+
for (const app of apps) {
|
|
3054
|
+
if (app.getEnv() !== "local")
|
|
3055
|
+
continue;
|
|
3056
|
+
const akanConfig = await app.getConfig();
|
|
3057
|
+
const mode = process.env.AKAN_DATABASE_MODE ?? akanConfig.defaultDatabaseMode ?? "single";
|
|
3058
|
+
await this.syncDatabaseModeDependencies(app, akanConfig, mode);
|
|
3059
|
+
if (mode !== "single")
|
|
3060
|
+
modes.add(mode);
|
|
3061
|
+
}
|
|
3062
|
+
let started = false;
|
|
3063
|
+
for (const mode of modes)
|
|
3064
|
+
if (!await this.dbup(workspace, mode))
|
|
3065
|
+
started = true;
|
|
3066
|
+
return started;
|
|
3067
|
+
}
|
|
2524
3068
|
async buildIos(app, { write = true, target, env = "debug", regenerate = false } = {}) {
|
|
2525
3069
|
await app.scanSync({ write });
|
|
2526
3070
|
await this.applicationRunner.buildIos(app, { target, env, regenerate });
|
|
@@ -2618,7 +3162,7 @@ class ApplicationScript extends script("application", [ApplicationRunner, Librar
|
|
|
2618
3162
|
let stopping = false;
|
|
2619
3163
|
process.on("SIGINT", () => {
|
|
2620
3164
|
if (stopping) {
|
|
2621
|
-
|
|
3165
|
+
Logger6.rawLog("Abandoning the local database teardown; containers are left running.", undefined, "error");
|
|
2622
3166
|
process.exit(130);
|
|
2623
3167
|
}
|
|
2624
3168
|
stopping = true;
|
|
@@ -2626,13 +3170,21 @@ class ApplicationScript extends script("application", [ApplicationRunner, Librar
|
|
|
2626
3170
|
});
|
|
2627
3171
|
}
|
|
2628
3172
|
async#stopDatabase(workspace) {
|
|
2629
|
-
|
|
2630
|
-
const
|
|
2631
|
-
|
|
2632
|
-
})
|
|
2633
|
-
|
|
2634
|
-
|
|
2635
|
-
|
|
3173
|
+
let timer = null;
|
|
3174
|
+
const timeout = new Promise((resolve) => {
|
|
3175
|
+
timer = setTimeout(() => resolve("timeout"), ApplicationScript.dbShutdownTimeoutMs);
|
|
3176
|
+
});
|
|
3177
|
+
try {
|
|
3178
|
+
const result = await Promise.race([this.dbdown(workspace).catch(() => {
|
|
3179
|
+
return;
|
|
3180
|
+
}), timeout]);
|
|
3181
|
+
if (result !== "timeout")
|
|
3182
|
+
return;
|
|
3183
|
+
Logger6.rawLog(`Local database did not stop within ${ApplicationScript.dbShutdownTimeoutMs}ms; run \`akan dbdown\` once Docker responds.`, undefined, "error");
|
|
3184
|
+
} finally {
|
|
3185
|
+
if (timer)
|
|
3186
|
+
clearTimeout(timer);
|
|
3187
|
+
}
|
|
2636
3188
|
}
|
|
2637
3189
|
}
|
|
2638
3190
|
|