@akanjs/cli 3.0.0-alpha.11 → 3.0.0-alpha.111
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/README.ko.md +1 -1
- package/README.md +1 -1
- package/{abstractCompactor-e5yq60qz.js → abstractCompactor-mjh5ppsq.js} +3 -5
- package/{agent.command-a3n15c6t.js → agent.command-d2gfxfck.js} +11 -13
- package/{application.command-rdm3et0g.js → application.command-pj2m7584.js} +65 -38
- package/{applicationBuildRunner-6m8n71w4.js → applicationBuildRunner-kxwbssgn.js} +27 -23
- package/{applicationReleasePackager-gg8ccbeh.js → applicationReleasePackager-4yasxwv9.js} +8 -9
- package/buildBatch.proc.js +12 -14
- package/capacitorApp-36rbr858.js +12 -0
- package/{cloud.command-56aeekqn.js → cloud.command-gxqp8xdv.js} +15 -18
- package/commandManifest.json +1 -1
- package/{context.command-ysxawe7h.js → context.command-ez8a0zyz.js} +19 -23
- package/dependencyScanner-shs82ynp.js +8 -0
- package/devTui-40ggmd99.js +553 -0
- package/devTuiView-c08ccbat.js +9 -0
- package/getDirname-dr3jz1xg.js +7 -0
- package/{guideline.command-yqf9a372.js → guideline.command-xmqwx8zm.js} +24 -22
- package/guidelines/agentRule/agentRule.generate.json +31 -0
- package/guidelines/agentRule/agentRule.instruction.md +531 -0
- package/guidelines/conventions/conventions.instruction.md +493 -369
- package/guidelines/framework/framework.generate.json +3 -2
- package/guidelines/framework/framework.instruction.md +0 -9
- package/guidelines/mcpRule/mcpRule.generate.json +35 -0
- package/guidelines/mcpRule/mcpRule.instruction.md +460 -0
- package/guidelines/modelStore/modelStore.instruction.md +12 -0
- package/guidelines/modelTemplate/modelTemplate.instruction.md +5 -0
- package/guidelines/queryRule/queryRule.generate.json +31 -0
- package/guidelines/queryRule/queryRule.instruction.md +178 -0
- package/guidelines/runtimeRule/runtimeRule.generate.json +25 -0
- package/guidelines/runtimeRule/runtimeRule.instruction.md +220 -0
- package/guidelines/ssrRule/ssrRule.instruction.md +51 -5
- package/guidelines/transportRule/transportRule.generate.json +31 -0
- package/guidelines/transportRule/transportRule.instruction.md +60 -0
- package/guidelines/workspaceOnboarding/workspaceOnboarding.instruction.md +54 -764
- package/guidelines/workspaceRecipes/workspaceRecipes.generate.json +33 -0
- package/guidelines/workspaceRecipes/workspaceRecipes.instruction.md +467 -0
- package/incrementalBuilder.proc.js +26 -25
- package/{index-8pd02prk.js → index-259dsnz3.js} +58 -25
- package/{index-yzsxc30b.js → index-2tv3fdx4.js} +975 -234
- package/index-3d9y066c.js +76 -0
- package/{index-76rn3g2c.js → index-47pj3aee.js} +4 -4
- package/{index-m90fn8xe.js → index-4edzf1gr.js} +32 -32
- package/{index-3789fdpg.js → index-600zhnve.js} +94 -61
- package/{index-1577bej2.js → index-6wwzs1bc.js} +2 -2
- package/{index-cv9cxs5k.js → index-7737gxcp.js} +18 -18
- package/{index-a5cs7d8r.js → index-8few9nyh.js} +13 -13
- package/{index-mewacxye.js → index-8pm30m73.js} +57 -57
- package/{index-wnp7hwq7.js → index-92zztqy1.js} +7 -7
- package/{index-fgc8r6dj.js → index-a5vmm6x4.js} +3 -1
- package/{index-cav1azma.js → index-a64esmtb.js} +1106 -1369
- package/{index-nwqqssnb.js → index-aj7tyh5w.js} +26 -23
- package/{index-xys926f2.js → index-by5gpp8x.js} +4 -4
- package/{index-yzmqvg23.js → index-fz5q3gd7.js} +8 -8
- package/{index-kcsa1zff.js → index-gyd220p2.js} +4 -4
- package/{index-j37qq1f2.js → index-h7yw2z9n.js} +7 -7
- package/{index-bjpxzr6s.js → index-hwkygn9s.js} +51 -3
- package/index-j9yhg404.js +159 -0
- package/{index-46tjzh6s.js → index-kcxmv48c.js} +29 -29
- package/{index-0cj2zxbm.js → index-kn1h7p1x.js} +12 -11
- package/{index-e4b2ae6b.js → index-ktb5njgy.js} +41 -34
- package/{index-etwh7sd3.js → index-mak3cd28.js} +100 -32
- package/{index-xjd3rqzj.js → index-n68jx79w.js} +6 -15
- package/{index-z1s5f4e0.js → index-ne2qy0nk.js} +36 -37
- package/{index-65aeh789.js → index-pnqbkf5p.js} +1 -1
- package/{index-67546d0j.js → index-q0y2jq9j.js} +14 -2
- package/{index-4mgkgba3.js → index-rwg2aa4f.js} +14 -14
- package/{index-bzznxgcx.js → index-v0qh106m.js} +1002 -128
- package/{index-j9cxndc0.js → index-w8kmx1b6.js} +20 -11
- package/{index-h5whaj26.js → index-ws5e0j51.js} +16 -12
- package/{index-9va8zayb.js → index-wt7qrtd4.js} +27 -27
- package/index.js +22 -25
- package/{library.command-x6t29a30.js → library.command-nbsky672.js} +11 -10
- package/{localRegistry.command-649g1n7b.js → localRegistry.command-ts6t5e5a.js} +20 -23
- package/{module.command-8w8ys5t3.js → module.command-29vqzrh8.js} +13 -16
- package/{package.command-zgy7f3fz.js → package.command-axv0abmn.js} +8 -10
- package/package.json +5 -7
- package/{page.command-yjmnkyk2.js → page.command-vs9s3zrh.js} +7 -9
- package/{primitive.command-gfqaagsg.js → primitive.command-cbxmqbtc.js} +13 -16
- package/{quality.command-ec8r1dn3.js → quality.command-gmbs7nws.js} +184 -334
- package/recipeScanner-fpagtgc1.js +11 -0
- package/{repair.command-p9ey6mkm.js → repair.command-bqsjptsj.js} +12 -14
- package/{routeSourceValidator-e4jmbfcx.js → routeSourceValidator-apyqw7wk.js} +90 -19
- package/{scalar.command-9dhmqhtm.js → scalar.command-6k1vhe9s.js} +11 -14
- package/subspace.command-35a3t6vv.js +755 -0
- package/templates/app/env/env.client.debug.ts.template +1 -5
- package/templates/app/env/env.client.develop.ts.template +1 -5
- package/templates/app/env/env.client.local.ts.template +1 -5
- package/templates/app/env/env.client.main.ts.template +1 -5
- package/templates/app/env/env.client.testing.ts.template +1 -5
- package/templates/app/env/env.client.type.ts +2 -6
- package/templates/app/env/env.server.debug.ts.template +1 -5
- package/templates/app/env/env.server.develop.ts.template +1 -5
- package/templates/app/env/env.server.local.ts.template +1 -5
- package/templates/app/env/env.server.main.ts.template +1 -5
- package/templates/app/main.ts +3 -1
- package/templates/app/page/_index.tsx +3 -3
- package/templates/app/page/_layout.tsx +31 -32
- package/templates/appSample/lib/task/task.signal.ts +11 -6
- package/templates/appSample/page/task/[taskId]/_index.tsx +22 -27
- package/templates/appSample/page/task/[taskId]/edit.tsx +25 -30
- package/templates/appSample/page/task/_index.tsx +3 -7
- package/templates/appSample/page/task/_layout.tsx +4 -15
- package/templates/appSample/page/task/new.tsx +4 -10
- package/templates/appSample/srvkit/AuthGuard.ts +2 -2
- package/templates/crudPages/[__model__Id]/edit/page.tsx +27 -29
- package/templates/crudPages/[__model__Id]/page.tsx +24 -32
- package/templates/crudPages/new/page.tsx +26 -25
- package/templates/crudPages/page.tsx +27 -26
- package/templates/crudSinglePage/page.tsx +26 -21
- package/templates/env/_env.server.type.ts +7 -5
- package/templates/lib/db.ts +1 -1
- package/templates/lib/dict.ts +1 -1
- package/templates/lib/sig.ts +5 -1
- package/templates/lib/srv.ts +6 -2
- package/templates/module/__model__.dictionary.ts +4 -1
- package/templates/module/__model__.signal.ts +2 -3
- package/templates/module/__model__.store.ts +1 -2
- package/templates/workspaceRoot/.gitignore.template +0 -1
- package/templates/workspaceRoot/.husky/pre-commit.template +16 -0
- package/templates/workspaceRoot/.vscode/settings.json.template +1 -1
- package/templates/workspaceRoot/AGENTS.md.template +4 -0
- package/templates/workspaceRoot/CLAUDE.md.template +19 -0
- package/templates/workspaceRoot/biome.json.template +1 -1
- package/templates/workspaceRoot/docs/AI-DEVELOPMENT.md.template +10 -14
- package/templates/workspaceRoot/docs/GENERATED.md.template +12 -10
- package/templates/workspaceRoot/package.json.template +2 -4
- package/typeChecker-3xgrzc2v.js +7 -0
- package/typecheck.proc.js +4 -5
- package/{workflow.command-ycwc6zr6.js → workflow.command-nwpcdc8p.js} +22 -25
- package/{workspace.command-xhyhajxa.js → workspace.command-aczjxzbf.js} +64 -66
- package/capacitorApp-mr81qdxq.js +0 -61
- package/dependencyScanner-f6grrep3.js +0 -9
- package/getDirname-m9s0cmhq.js +0 -8
- package/index-mxvakhsm.js +0 -66
- package/index-r24hmh0q.js +0 -4
- package/recipeScanner-tghkzrrt.js +0 -12
- package/typeChecker-kravn7ns.js +0 -8
- /package/{index-0wae5ebk.js → index-meh558jy.js} +0 -0
- /package/{index-ss469dec.js → index-nz8prx9g.js} +0 -0
|
@@ -1,27 +1,123 @@
|
|
|
1
1
|
// @bun
|
|
2
|
-
import {
|
|
3
|
-
LibraryScript
|
|
4
|
-
} from "./index-8pd02prk.js";
|
|
5
|
-
import {
|
|
6
|
-
resolveMobileTargets
|
|
7
|
-
} from "./index-76rn3g2c.js";
|
|
8
2
|
import {
|
|
9
3
|
runner,
|
|
10
4
|
script
|
|
11
|
-
} from "./index-
|
|
5
|
+
} from "./index-mak3cd28.js";
|
|
6
|
+
import {
|
|
7
|
+
WorkspaceExecutor,
|
|
8
|
+
AppExecutor,
|
|
9
|
+
LibExecutor,
|
|
10
|
+
PkgExecutor
|
|
11
|
+
} from "./index-2tv3fdx4.js";
|
|
12
|
+
import {
|
|
13
|
+
LibraryScript
|
|
14
|
+
} from "./index-259dsnz3.js";
|
|
12
15
|
import {
|
|
13
16
|
HmrWatcher,
|
|
14
17
|
WatchRootResolver
|
|
15
|
-
} from "./index-
|
|
18
|
+
} from "./index-hwkygn9s.js";
|
|
16
19
|
import {
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
WorkspaceExecutor
|
|
21
|
-
} from "./index-yzsxc30b.js";
|
|
20
|
+
SlicePlanner,
|
|
21
|
+
formatSlicePlan
|
|
22
|
+
} from "./index-j9yhg404.js";
|
|
22
23
|
import {
|
|
23
|
-
|
|
24
|
-
} from "./index-
|
|
24
|
+
resolveMobileTargets
|
|
25
|
+
} from "./index-47pj3aee.js";
|
|
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
|
+
|
|
37
|
+
// pkgs/@akanjs/cli/application/devLogBuffer.ts
|
|
38
|
+
var ansi = new RegExp(`${String.fromCharCode(27)}(?:[@-Z\\\\-_]|\\[[\\s\\S]*?[@-~])`, "g");
|
|
39
|
+
var stripAnsi = (text) => text.replace(ansi, "");
|
|
40
|
+
var plainTextOf = (lines, { withApp = false } = {}) => {
|
|
41
|
+
const width = withApp ? lines.reduce((max, line) => Math.max(max, line.app.length), 0) : 0;
|
|
42
|
+
return lines.map((line) => `${withApp ? `${line.app.padEnd(width)} \u2502 ` : ""}${stripAnsi(line.text)}`).join(`
|
|
43
|
+
`);
|
|
44
|
+
};
|
|
45
|
+
var HOST_SOURCE = "host";
|
|
46
|
+
var childPrefix = /^\[child:(\d+) ([a-z-]+)\] \[(?:stdout|stderr)\] /;
|
|
47
|
+
var sourceOf = (text) => {
|
|
48
|
+
const match = childPrefix.exec(text);
|
|
49
|
+
return match ? `#${match[1]} ${match[2]}` : HOST_SOURCE;
|
|
50
|
+
};
|
|
51
|
+
|
|
52
|
+
class DevLogBuffer {
|
|
53
|
+
static defaultLimit = 5000;
|
|
54
|
+
#limit;
|
|
55
|
+
#lines = [];
|
|
56
|
+
#partial = new Map;
|
|
57
|
+
#sources = new Map;
|
|
58
|
+
#seq = 0;
|
|
59
|
+
constructor({ limit = DevLogBuffer.defaultLimit } = {}) {
|
|
60
|
+
this.#limit = Math.max(1, limit);
|
|
61
|
+
}
|
|
62
|
+
get size() {
|
|
63
|
+
return this.#lines.length;
|
|
64
|
+
}
|
|
65
|
+
sourcesOf(app) {
|
|
66
|
+
return [...this.#sources.get(app) ?? []];
|
|
67
|
+
}
|
|
68
|
+
push(app, kind, chunk) {
|
|
69
|
+
const key = `${app}:${kind}`;
|
|
70
|
+
const parts = `${this.#partial.get(key) ?? ""}${chunk}`.split(`
|
|
71
|
+
`);
|
|
72
|
+
this.#partial.set(key, parts.pop() ?? "");
|
|
73
|
+
return this.#append(app, kind, parts);
|
|
74
|
+
}
|
|
75
|
+
flushPartials() {
|
|
76
|
+
const flushed = [];
|
|
77
|
+
for (const [key, remainder] of [...this.#partial]) {
|
|
78
|
+
this.#partial.set(key, "");
|
|
79
|
+
if (!remainder)
|
|
80
|
+
continue;
|
|
81
|
+
const split = key.lastIndexOf(":");
|
|
82
|
+
const app = key.slice(0, split);
|
|
83
|
+
const kind = key.slice(split + 1) === "stderr" ? "stderr" : "stdout";
|
|
84
|
+
flushed.push(...this.#append(app, kind, [remainder]));
|
|
85
|
+
}
|
|
86
|
+
return flushed;
|
|
87
|
+
}
|
|
88
|
+
select({ app = null, source = null, grep, errorsOnly = false } = {}) {
|
|
89
|
+
const needle = grep?.trim().toLowerCase();
|
|
90
|
+
return this.#lines.filter((line) => {
|
|
91
|
+
if (app !== null && line.app !== app)
|
|
92
|
+
return false;
|
|
93
|
+
if (source !== null && line.source !== source)
|
|
94
|
+
return false;
|
|
95
|
+
if (errorsOnly && line.kind !== "stderr")
|
|
96
|
+
return false;
|
|
97
|
+
if (needle && !stripAnsi(line.text).toLowerCase().includes(needle))
|
|
98
|
+
return false;
|
|
99
|
+
return true;
|
|
100
|
+
});
|
|
101
|
+
}
|
|
102
|
+
clear(app = null) {
|
|
103
|
+
const kept = app === null ? [] : this.#lines.filter((line) => line.app !== app);
|
|
104
|
+
this.#lines.length = 0;
|
|
105
|
+
this.#lines.push(...kept);
|
|
106
|
+
}
|
|
107
|
+
#append(app, kind, texts) {
|
|
108
|
+
const known = this.#sources.get(app) ?? new Set;
|
|
109
|
+
this.#sources.set(app, known);
|
|
110
|
+
const added = texts.map((text) => {
|
|
111
|
+
const source = sourceOf(text);
|
|
112
|
+
known.add(source);
|
|
113
|
+
return { seq: ++this.#seq, app, source, kind, text };
|
|
114
|
+
});
|
|
115
|
+
this.#lines.push(...added);
|
|
116
|
+
if (this.#lines.length > this.#limit)
|
|
117
|
+
this.#lines.splice(0, this.#lines.length - this.#limit);
|
|
118
|
+
return added;
|
|
119
|
+
}
|
|
120
|
+
}
|
|
25
121
|
|
|
26
122
|
// pkgs/@akanjs/devkit/applicationBuildReporter.ts
|
|
27
123
|
import { Logger } from "akanjs/common";
|
|
@@ -70,11 +166,12 @@ Caused by: ${ApplicationBuildReporter.formatError(error.cause)}` : "";
|
|
|
70
166
|
|
|
71
167
|
// pkgs/@akanjs/cli/application/application.script.ts
|
|
72
168
|
import { confirm } from "@inquirer/prompts";
|
|
73
|
-
import { Logger as
|
|
169
|
+
import { Logger as Logger6 } from "akanjs/common";
|
|
170
|
+
|
|
171
|
+
// pkgs/@akanjs/cli/application/application.runner.ts
|
|
172
|
+
import path5 from "path";
|
|
74
173
|
|
|
75
174
|
// pkgs/@akanjs/devkit/akanApp/akanApp.host.ts
|
|
76
|
-
import { stat } from "fs/promises";
|
|
77
|
-
import path2 from "path";
|
|
78
175
|
import { Logger as Logger3 } from "akanjs/common";
|
|
79
176
|
|
|
80
177
|
// pkgs/@akanjs/devkit/sshTunnel.ts
|
|
@@ -237,6 +334,8 @@ class IncrementalBuilderHost {
|
|
|
237
334
|
app;
|
|
238
335
|
ready = false;
|
|
239
336
|
#onMessage;
|
|
337
|
+
#stdio;
|
|
338
|
+
#onOutput;
|
|
240
339
|
#proc = null;
|
|
241
340
|
#status = "stopped";
|
|
242
341
|
#restartAttempts = 0;
|
|
@@ -247,11 +346,13 @@ class IncrementalBuilderHost {
|
|
|
247
346
|
#manualStop = false;
|
|
248
347
|
#inFlight = new Map;
|
|
249
348
|
#startOptions = {};
|
|
250
|
-
constructor({ app, entry, env, onMessage }) {
|
|
349
|
+
constructor({ app, entry, env, stdio = "inherit", onMessage, onOutput }) {
|
|
251
350
|
this.app = app;
|
|
252
351
|
this.entry = entry;
|
|
253
352
|
this.env = env;
|
|
353
|
+
this.#stdio = stdio;
|
|
254
354
|
this.#onMessage = onMessage;
|
|
355
|
+
this.#onOutput = onOutput ?? null;
|
|
255
356
|
}
|
|
256
357
|
get status() {
|
|
257
358
|
return this.#status;
|
|
@@ -277,7 +378,7 @@ class IncrementalBuilderHost {
|
|
|
277
378
|
proc = Bun.spawn(["bun", this.entry], {
|
|
278
379
|
cwd: this.app.cwdPath,
|
|
279
380
|
env: { ...this.env, AKAN_WATCH: "1", ...afterRecycle ? { AKAN_BUILDER_ANNOUNCE_BOOT: "1" } : {} },
|
|
280
|
-
stdio: ["ignore",
|
|
381
|
+
stdio: ["ignore", this.#stdio, this.#stdio],
|
|
281
382
|
ipc: (msg) => {
|
|
282
383
|
if (this.#proc !== proc)
|
|
283
384
|
return;
|
|
@@ -325,8 +426,24 @@ class IncrementalBuilderHost {
|
|
|
325
426
|
}
|
|
326
427
|
});
|
|
327
428
|
this.#proc = proc;
|
|
429
|
+
if (this.#stdio === "pipe" && this.#onOutput) {
|
|
430
|
+
this.#drain(proc.stdout, "stdout");
|
|
431
|
+
this.#drain(proc.stderr, "stderr");
|
|
432
|
+
}
|
|
328
433
|
this.logger.verbose(`builder spawned pid=${proc.pid} entry=${this.entry}${isRestart ? " restart=1" : ""}`);
|
|
329
434
|
}
|
|
435
|
+
async#drain(stream, kind) {
|
|
436
|
+
if (!stream)
|
|
437
|
+
return;
|
|
438
|
+
const decoder = new TextDecoder;
|
|
439
|
+
try {
|
|
440
|
+
for await (const chunk of stream) {
|
|
441
|
+
const text = decoder.decode(chunk, { stream: true });
|
|
442
|
+
if (text)
|
|
443
|
+
this.#onOutput?.(kind, text);
|
|
444
|
+
}
|
|
445
|
+
} catch {}
|
|
446
|
+
}
|
|
330
447
|
#scheduleRestart() {
|
|
331
448
|
if (this.#manualStop || this.#restartTimer)
|
|
332
449
|
return;
|
|
@@ -350,7 +467,7 @@ class IncrementalBuilderHost {
|
|
|
350
467
|
return false;
|
|
351
468
|
this.#recycleRequested = true;
|
|
352
469
|
this.#status = "recycling";
|
|
353
|
-
this.logger.
|
|
470
|
+
this.logger.debug(`recycling builder pid=${proc.pid} (${reason})`);
|
|
354
471
|
this.#recycleTimer = setTimeout(() => {
|
|
355
472
|
this.#recycleTimer = null;
|
|
356
473
|
if (this.#proc !== proc)
|
|
@@ -422,7 +539,10 @@ class IncrementalBuilderHost {
|
|
|
422
539
|
this.ready = false;
|
|
423
540
|
this.#status = "stopped";
|
|
424
541
|
}
|
|
425
|
-
static async create(app, env, onMessage
|
|
542
|
+
static async create(app, env, onMessage, {
|
|
543
|
+
stdio = "inherit",
|
|
544
|
+
onOutput
|
|
545
|
+
} = {}) {
|
|
426
546
|
const candidates = [
|
|
427
547
|
path.join(app.workspace.workspaceRoot, "pkgs/@akanjs/devkit/incrementalBuilder/incrementalBuilder.proc.ts"),
|
|
428
548
|
path.join(app.workspace.workspaceRoot, "node_modules/@akanjs/devkit/incrementalBuilder/incrementalBuilder.proc.ts"),
|
|
@@ -431,7 +551,7 @@ class IncrementalBuilderHost {
|
|
|
431
551
|
];
|
|
432
552
|
for (const c of candidates)
|
|
433
553
|
if (await Bun.file(c).exists())
|
|
434
|
-
return new IncrementalBuilderHost({ app, entry: c, env, onMessage });
|
|
554
|
+
return new IncrementalBuilderHost({ app, entry: c, env, stdio, onMessage, onOutput });
|
|
435
555
|
throw new Error(`[cli] frontend builder entry not found; looked in: ${candidates.join(", ")}`);
|
|
436
556
|
}
|
|
437
557
|
}
|
|
@@ -470,38 +590,23 @@ class BuilderRequestRouter {
|
|
|
470
590
|
}
|
|
471
591
|
}
|
|
472
592
|
|
|
473
|
-
// pkgs/@akanjs/devkit/akanApp/
|
|
474
|
-
|
|
475
|
-
|
|
476
|
-
|
|
477
|
-
|
|
478
|
-
|
|
593
|
+
// pkgs/@akanjs/devkit/akanApp/BackendImportGraph.ts
|
|
594
|
+
import { stat } from "fs/promises";
|
|
595
|
+
import path3 from "path";
|
|
596
|
+
|
|
597
|
+
// pkgs/@akanjs/devkit/akanApp/devHostPolicy.ts
|
|
598
|
+
import path2 from "path";
|
|
479
599
|
var BACKEND_RECOVERY_MAX_ATTEMPTS = 5;
|
|
480
|
-
var BACKEND_STDERR_TAIL_LIMIT = 40;
|
|
481
|
-
var BUILDER_READY_TIMEOUT_MS = 150000;
|
|
482
|
-
var BUILDER_START_MAX_ATTEMPTS = 3;
|
|
483
600
|
var HELD_BUILDER_REQUEST_LIMIT = 64;
|
|
484
|
-
var BUILDER_RSS_RECYCLE_QUIET_MS = 750;
|
|
485
601
|
var BUILDER_MIN_RSS_RECYCLE_INTERVAL_MS = 30000;
|
|
486
602
|
var BUILDER_TIGHT_RSS_REPORT_LIMIT = 2;
|
|
487
|
-
var BUILDER_RSS_SETTLE_MS = 20000;
|
|
488
|
-
var PS_RSS_TIMEOUT_MS = 2000;
|
|
489
603
|
var BUILDER_RSS_HARD_MULTIPLE = 1.5;
|
|
490
604
|
var DEV_IDLE_SUSPEND_MS = 300000;
|
|
491
605
|
var DEV_IDLE_MIN_UPTIME_MS = 30000;
|
|
492
|
-
var BUILDER_RECOVERY_BASE_DELAY_MS = 2000;
|
|
493
|
-
var BUILDER_RECOVERY_MAX_DELAY_MS = 60000;
|
|
494
606
|
var SOURCE_EXTS = new Set([".ts", ".tsx", ".js", ".jsx", ".mjs", ".cjs"]);
|
|
495
|
-
var NON_SOURCE_EXT_RE = /\.(css|scss|sass|less|json|svg|png|jpe?g|webp|gif|avif|ico|woff2?|ttf|otf|mp3|mp4|wav|html)$/i;
|
|
496
607
|
var SERVER_SUFFIXES = [".service.ts", ".document.ts"];
|
|
497
608
|
var SHARED_SUFFIXES = [".constant.ts", ".dictionary.ts", ".signal.ts"];
|
|
498
609
|
var RUNTIME_METADATA_BASENAMES = new Set(["dict.ts", "sig.ts", "useClient.ts", "useServer.ts"]);
|
|
499
|
-
var GRAPH_IMPORT_KINDS = new Set([
|
|
500
|
-
"import-statement",
|
|
501
|
-
"require-call",
|
|
502
|
-
"require-resolve",
|
|
503
|
-
"dynamic-import"
|
|
504
|
-
]);
|
|
505
610
|
var shouldRestartBackendByDevPlan = (message) => {
|
|
506
611
|
if (!message.devPlan)
|
|
507
612
|
return null;
|
|
@@ -515,6 +620,15 @@ var shouldRestartBuilderByDevPlan = (message) => message.devPlan?.actions.includ
|
|
|
515
620
|
var shouldAbandonBackendRecovery = (attempts, maxAttempts = BACKEND_RECOVERY_MAX_ATTEMPTS) => attempts >= maxAttempts;
|
|
516
621
|
var normalizeBackendReportedGeneration = (generation) => generation >= 0 ? generation : undefined;
|
|
517
622
|
var shouldRestartDevHostByDevPlan = (message) => message.devPlan?.actions.includes("restart-dev-host") ?? message.kinds.includes("config");
|
|
623
|
+
var devHostStateByBackendState = {
|
|
624
|
+
starting: "starting",
|
|
625
|
+
ready: "ready",
|
|
626
|
+
"restart-pending": "restarting",
|
|
627
|
+
recovering: "recovering",
|
|
628
|
+
stopping: "stopped",
|
|
629
|
+
stopped: "stopped"
|
|
630
|
+
};
|
|
631
|
+
var devHostStateOf = (state, gaveUp) => state === "stopped" && gaveUp ? "failed" : devHostStateByBackendState[state];
|
|
518
632
|
var RESTART_ROLE_ORDER = ["server", "shared", "barrel", "config"];
|
|
519
633
|
var generationValue = (generation) => generation ?? -1;
|
|
520
634
|
var isLegacyBackendFallbackFile = (file, workspaceRoot) => {
|
|
@@ -682,6 +796,15 @@ var mergeInvalidateMessages = (current, next) => {
|
|
|
682
796
|
var buildStatusReplaySequence = (pendingReplay, latestByPhase) => [...pendingReplay, ...latestByPhase.values()];
|
|
683
797
|
var filesChangedSince = (before, after) => [...before].filter(([file, stamp]) => after.get(file) !== stamp).map(([file]) => file);
|
|
684
798
|
|
|
799
|
+
// pkgs/@akanjs/devkit/akanApp/BackendImportGraph.ts
|
|
800
|
+
var NON_SOURCE_EXT_RE = /\.(css|scss|sass|less|json|svg|png|jpe?g|webp|gif|avif|ico|woff2?|ttf|otf|mp3|mp4|wav|html)$/i;
|
|
801
|
+
var GRAPH_IMPORT_KINDS = new Set([
|
|
802
|
+
"import-statement",
|
|
803
|
+
"require-call",
|
|
804
|
+
"require-resolve",
|
|
805
|
+
"dynamic-import"
|
|
806
|
+
]);
|
|
807
|
+
|
|
685
808
|
class BackendImportGraph {
|
|
686
809
|
#app;
|
|
687
810
|
#logger;
|
|
@@ -704,7 +827,7 @@ class BackendImportGraph {
|
|
|
704
827
|
return this.#lastRefreshSucceeded;
|
|
705
828
|
}
|
|
706
829
|
has(file) {
|
|
707
|
-
return this.#files.has(
|
|
830
|
+
return this.#files.has(path3.resolve(file));
|
|
708
831
|
}
|
|
709
832
|
async fingerprint() {
|
|
710
833
|
const stamps = await Promise.all([...this.#files].map(async (file) => {
|
|
@@ -732,9 +855,9 @@ class BackendImportGraph {
|
|
|
732
855
|
const roots = await this.#entrypoints();
|
|
733
856
|
const files = new Set;
|
|
734
857
|
const queue = [...roots];
|
|
735
|
-
const workspaceRoot =
|
|
858
|
+
const workspaceRoot = path3.resolve(this.#app.workspace.workspaceRoot);
|
|
736
859
|
while (queue.length > 0) {
|
|
737
|
-
const current =
|
|
860
|
+
const current = path3.resolve(queue.pop());
|
|
738
861
|
if (files.has(current))
|
|
739
862
|
continue;
|
|
740
863
|
if (!this.#isWorkspaceSource(current, workspaceRoot))
|
|
@@ -743,7 +866,7 @@ class BackendImportGraph {
|
|
|
743
866
|
if (!imports)
|
|
744
867
|
continue;
|
|
745
868
|
files.add(current);
|
|
746
|
-
const importerDir =
|
|
869
|
+
const importerDir = path3.dirname(current);
|
|
747
870
|
for (const imp of imports) {
|
|
748
871
|
if (!GRAPH_IMPORT_KINDS.has(imp.kind) || !imp.path || NON_SOURCE_EXT_RE.test(imp.path))
|
|
749
872
|
continue;
|
|
@@ -774,7 +897,7 @@ class BackendImportGraph {
|
|
|
774
897
|
const roots = [`${this.#app.cwdPath}/main.ts`, `${this.#app.cwdPath}/server.ts`];
|
|
775
898
|
const existing = [];
|
|
776
899
|
for (const root of roots) {
|
|
777
|
-
const abs =
|
|
900
|
+
const abs = path3.resolve(root);
|
|
778
901
|
if (await Bun.file(abs).exists())
|
|
779
902
|
existing.push(abs);
|
|
780
903
|
}
|
|
@@ -783,25 +906,25 @@ class BackendImportGraph {
|
|
|
783
906
|
#resolve(specifier, importerDir) {
|
|
784
907
|
try {
|
|
785
908
|
const resolved = Bun.resolveSync(specifier, importerDir);
|
|
786
|
-
if (!
|
|
909
|
+
if (!path3.isAbsolute(resolved))
|
|
787
910
|
return null;
|
|
788
|
-
if (!SOURCE_EXTS.has(
|
|
911
|
+
if (!SOURCE_EXTS.has(path3.extname(resolved).toLowerCase()))
|
|
789
912
|
return null;
|
|
790
|
-
return
|
|
913
|
+
return path3.resolve(resolved);
|
|
791
914
|
} catch {
|
|
792
915
|
return null;
|
|
793
916
|
}
|
|
794
917
|
}
|
|
795
918
|
#isWorkspaceSource(file, workspaceRoot) {
|
|
796
|
-
const rel =
|
|
797
|
-
if (rel.startsWith("..") ||
|
|
919
|
+
const rel = path3.relative(workspaceRoot, file);
|
|
920
|
+
if (rel.startsWith("..") || path3.isAbsolute(rel))
|
|
798
921
|
return false;
|
|
799
|
-
if (rel.includes(`${
|
|
922
|
+
if (rel.includes(`${path3.sep}node_modules${path3.sep}`) || rel.includes(`${path3.sep}.akan${path3.sep}`))
|
|
800
923
|
return false;
|
|
801
|
-
return SOURCE_EXTS.has(
|
|
924
|
+
return SOURCE_EXTS.has(path3.extname(file).toLowerCase());
|
|
802
925
|
}
|
|
803
926
|
#scanImports(file, source) {
|
|
804
|
-
const ext =
|
|
927
|
+
const ext = path3.extname(file).toLowerCase();
|
|
805
928
|
if (ext === ".tsx")
|
|
806
929
|
return this.#tsxTranspiler.scanImports(source);
|
|
807
930
|
if (ext === ".jsx")
|
|
@@ -812,11 +935,28 @@ class BackendImportGraph {
|
|
|
812
935
|
}
|
|
813
936
|
}
|
|
814
937
|
|
|
938
|
+
// pkgs/@akanjs/devkit/akanApp/akanApp.host.ts
|
|
939
|
+
var backendMsgTypeSet = new Set(["build-route", "build-csr"]);
|
|
940
|
+
var BACKEND_RESTART_DEBOUNCE_MS = 120;
|
|
941
|
+
var BACKEND_GRACEFUL_TIMEOUT_MS = 8000;
|
|
942
|
+
var BACKEND_RECOVERY_BASE_DELAY_MS = 1000;
|
|
943
|
+
var BACKEND_RECOVERY_MAX_DELAY_MS = 30000;
|
|
944
|
+
var BACKEND_STDERR_TAIL_LIMIT = 40;
|
|
945
|
+
var BUILDER_READY_TIMEOUT_MS = 150000;
|
|
946
|
+
var BUILDER_START_MAX_ATTEMPTS = 3;
|
|
947
|
+
var BUILDER_RSS_RECYCLE_QUIET_MS = 750;
|
|
948
|
+
var BUILDER_RSS_SETTLE_MS = 20000;
|
|
949
|
+
var PS_RSS_TIMEOUT_MS = 2000;
|
|
950
|
+
var BUILDER_RECOVERY_BASE_DELAY_MS = 2000;
|
|
951
|
+
var BUILDER_RECOVERY_MAX_DELAY_MS = 60000;
|
|
952
|
+
|
|
815
953
|
class AkanAppHost {
|
|
816
954
|
app;
|
|
817
955
|
logger = new Logger3("AkanAppHost");
|
|
818
|
-
|
|
956
|
+
stdio;
|
|
819
957
|
env;
|
|
958
|
+
#onDevEvent;
|
|
959
|
+
#lastDevState = null;
|
|
820
960
|
#backend = null;
|
|
821
961
|
#builder = null;
|
|
822
962
|
#backendReady = false;
|
|
@@ -855,12 +995,23 @@ class AkanAppHost {
|
|
|
855
995
|
#builderGapStamp = null;
|
|
856
996
|
#pendingBuilderMessages = [];
|
|
857
997
|
#builderRequests = new BuilderRequestRouter;
|
|
858
|
-
constructor(app, {
|
|
998
|
+
constructor(app, {
|
|
999
|
+
env,
|
|
1000
|
+
stdio = "inherit",
|
|
1001
|
+
onDevEvent
|
|
1002
|
+
}) {
|
|
859
1003
|
this.app = app;
|
|
860
1004
|
this.env = env;
|
|
861
|
-
this.
|
|
1005
|
+
this.stdio = stdio;
|
|
1006
|
+
this.#onDevEvent = onDevEvent ?? null;
|
|
862
1007
|
this.#backendGraph = new BackendImportGraph(app, this.logger);
|
|
863
1008
|
}
|
|
1009
|
+
#emitDevEvent(state, detail) {
|
|
1010
|
+
if (!this.#onDevEvent || state === this.#lastDevState)
|
|
1011
|
+
return;
|
|
1012
|
+
this.#lastDevState = state;
|
|
1013
|
+
this.#onDevEvent({ app: this.app.name, state, ...detail ? { detail } : {} });
|
|
1014
|
+
}
|
|
864
1015
|
async start() {
|
|
865
1016
|
if (this.#backend)
|
|
866
1017
|
await this.#stopBackend();
|
|
@@ -907,6 +1058,7 @@ class AkanAppHost {
|
|
|
907
1058
|
}
|
|
908
1059
|
#startBackend(startStatus = null) {
|
|
909
1060
|
this.#builderRequests.startGeneration();
|
|
1061
|
+
this.#discardPendingBuilderMessages("the backend restarted while the builder was away");
|
|
910
1062
|
this.#backendStartStatus = startStatus;
|
|
911
1063
|
this.#backendGaveUp = false;
|
|
912
1064
|
this.#setBackendLifecycleState("starting");
|
|
@@ -914,7 +1066,7 @@ class AkanAppHost {
|
|
|
914
1066
|
this.#backendStderrTail = [];
|
|
915
1067
|
const backend = Bun.spawn(["bun", `apps/${this.app.name}/main.ts`], {
|
|
916
1068
|
cwd: this.app.workspace.workspaceRoot,
|
|
917
|
-
stdio: this.
|
|
1069
|
+
stdio: this.stdio === "pipe" ? ["ignore", "pipe", "pipe"] : ["inherit", "inherit", "inherit"],
|
|
918
1070
|
env: this.env,
|
|
919
1071
|
ipc: (msg) => {
|
|
920
1072
|
if (!msg || typeof msg !== "object")
|
|
@@ -955,7 +1107,7 @@ class AkanAppHost {
|
|
|
955
1107
|
});
|
|
956
1108
|
this.#backend = backend;
|
|
957
1109
|
this.logger.verbose(`backend spawned pid=${backend.pid}`);
|
|
958
|
-
if (this.
|
|
1110
|
+
if (this.stdio === "pipe") {
|
|
959
1111
|
this.#forwardBackendStream(backend.stderr, "stderr");
|
|
960
1112
|
this.#forwardBackendStream(backend.stdout, "stdout");
|
|
961
1113
|
}
|
|
@@ -976,14 +1128,13 @@ class AkanAppHost {
|
|
|
976
1128
|
try {
|
|
977
1129
|
for await (const chunk of stream) {
|
|
978
1130
|
const text = decoder.decode(chunk, { stream: true });
|
|
979
|
-
if (!text
|
|
1131
|
+
if (!text)
|
|
980
1132
|
continue;
|
|
981
1133
|
if (kind === "stderr") {
|
|
982
1134
|
this.#recordBackendStderr(text);
|
|
983
|
-
|
|
984
|
-
} else
|
|
985
|
-
|
|
986
|
-
}
|
|
1135
|
+
process.stderr.write(text);
|
|
1136
|
+
} else
|
|
1137
|
+
process.stdout.write(text);
|
|
987
1138
|
}
|
|
988
1139
|
} catch {}
|
|
989
1140
|
}
|
|
@@ -1030,6 +1181,7 @@ class AkanAppHost {
|
|
|
1030
1181
|
const prev = this.#backendLifecycleState;
|
|
1031
1182
|
this.#backendLifecycleState = next;
|
|
1032
1183
|
this.logger.verbose(`[backend-lifecycle] ${prev} -> ${next}${detail ? ` ${detail}` : ""}`);
|
|
1184
|
+
this.#emitDevEvent(devHostStateOf(next, this.#backendGaveUp), detail);
|
|
1033
1185
|
}
|
|
1034
1186
|
#sendToBackend(message) {
|
|
1035
1187
|
if (!this.#backend || !this.#backendReady) {
|
|
@@ -1359,6 +1511,7 @@ ${this.#backendStderrTail.join(`
|
|
|
1359
1511
|
return;
|
|
1360
1512
|
}
|
|
1361
1513
|
this.#suspended = true;
|
|
1514
|
+
this.#emitDevEvent("suspended", `idle ${Math.round(idleMs / 1000)}s`);
|
|
1362
1515
|
this.#stopBuilder();
|
|
1363
1516
|
this.#openBuilderGap("idle suspend");
|
|
1364
1517
|
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`);
|
|
@@ -1423,6 +1576,7 @@ ${this.#backendStderrTail.join(`
|
|
|
1423
1576
|
this.#wokeAtMono = performance.now();
|
|
1424
1577
|
this.#flushPendingBuilderMessages();
|
|
1425
1578
|
this.#armIdleSuspend();
|
|
1579
|
+
this.#emitDevEvent(devHostStateOf(this.#backendLifecycleState, this.#backendGaveUp));
|
|
1426
1580
|
}
|
|
1427
1581
|
}
|
|
1428
1582
|
async#applyIdleWake(batch) {
|
|
@@ -1498,6 +1652,12 @@ ${this.#backendStderrTail.join(`
|
|
|
1498
1652
|
this.#pendingBuilderMessages.push(message);
|
|
1499
1653
|
this.logger.verbose(`[builder] holding ${message.type} until the builder is ready (${this.#pendingBuilderMessages.length} waiting)`);
|
|
1500
1654
|
}
|
|
1655
|
+
#discardPendingBuilderMessages(reason) {
|
|
1656
|
+
const held = this.#pendingBuilderMessages.splice(0);
|
|
1657
|
+
if (held.length === 0)
|
|
1658
|
+
return;
|
|
1659
|
+
this.logger.verbose(`[builder] dropped ${held.length} held builder request(s): ${reason}`);
|
|
1660
|
+
}
|
|
1501
1661
|
#failPendingBuilderMessages(reason) {
|
|
1502
1662
|
const held = this.#pendingBuilderMessages.splice(0);
|
|
1503
1663
|
if (held.length === 0)
|
|
@@ -1753,6 +1913,9 @@ ${this.#backendStderrTail.join(`
|
|
|
1753
1913
|
for (let attempt = 1;attempt <= BUILDER_START_MAX_ATTEMPTS; attempt++) {
|
|
1754
1914
|
this.#builder = await IncrementalBuilderHost.create(this.app, this.env, (msg) => {
|
|
1755
1915
|
this.#enqueueBuilderMessage(msg);
|
|
1916
|
+
}, {
|
|
1917
|
+
stdio: this.stdio,
|
|
1918
|
+
onOutput: (kind, text) => void (kind === "stderr" ? process.stderr : process.stdout).write(text)
|
|
1756
1919
|
});
|
|
1757
1920
|
try {
|
|
1758
1921
|
await this.#waitForBuilderReady(attempt, { announceBootState });
|
|
@@ -1858,20 +2021,20 @@ ${this.#backendStderrTail.join(`
|
|
|
1858
2021
|
}
|
|
1859
2022
|
}
|
|
1860
2023
|
// pkgs/@akanjs/devkit/applicationTestPreload.ts
|
|
1861
|
-
import
|
|
2024
|
+
import path4 from "path";
|
|
1862
2025
|
var SIGNAL_TEST_PRELOAD_PATH = "test/signalTest.preload.ts";
|
|
1863
2026
|
async function resolveSignalTestPreloadPath(target) {
|
|
1864
2027
|
const candidates = [];
|
|
1865
2028
|
const addResolvedPackageCandidate = (basePath) => {
|
|
1866
2029
|
try {
|
|
1867
|
-
candidates.push(
|
|
2030
|
+
candidates.push(path4.join(path4.dirname(Bun.resolveSync("akanjs/package.json", basePath)), SIGNAL_TEST_PRELOAD_PATH));
|
|
1868
2031
|
} catch {}
|
|
1869
2032
|
};
|
|
1870
2033
|
addResolvedPackageCandidate(target.cwdPath);
|
|
1871
2034
|
addResolvedPackageCandidate(process.cwd());
|
|
1872
|
-
addResolvedPackageCandidate(
|
|
2035
|
+
addResolvedPackageCandidate(path4.dirname(Bun.main));
|
|
1873
2036
|
addResolvedPackageCandidate(import.meta.dir);
|
|
1874
|
-
candidates.push(
|
|
2037
|
+
candidates.push(path4.join(target.cwdPath, "../../node_modules/akanjs", SIGNAL_TEST_PRELOAD_PATH), path4.join(target.cwdPath, "../../pkgs/akanjs", SIGNAL_TEST_PRELOAD_PATH), path4.join(process.cwd(), "node_modules/akanjs", SIGNAL_TEST_PRELOAD_PATH), path4.join(process.cwd(), "pkgs/akanjs", SIGNAL_TEST_PRELOAD_PATH), path4.join(path4.dirname(Bun.main), "../../akanjs", SIGNAL_TEST_PRELOAD_PATH), path4.resolve(import.meta.dir, "../../akanjs", SIGNAL_TEST_PRELOAD_PATH));
|
|
1875
2038
|
const uniqueCandidates = [...new Set(candidates)];
|
|
1876
2039
|
for (const candidate of uniqueCandidates) {
|
|
1877
2040
|
if (await Bun.file(candidate).exists())
|
|
@@ -1886,22 +2049,10 @@ ${uniqueCandidates.map((candidate) => ` - ${candidate}`).join(`
|
|
|
1886
2049
|
// pkgs/@akanjs/cli/application/application.runner.ts
|
|
1887
2050
|
import { Logger as Logger4 } from "akanjs/common";
|
|
1888
2051
|
import ora from "ora";
|
|
1889
|
-
|
|
1890
|
-
|
|
1891
|
-
|
|
1892
|
-
|
|
1893
|
-
const command = process.platform === "darwin" ? ["open", url] : process.platform === "win32" ? ["cmd", "/c", "start", "", url] : ["xdg-open", url];
|
|
1894
|
-
const child = spawn(command[0], command.slice(1), { detached: true, stdio: "ignore" });
|
|
1895
|
-
child.on("error", () => {});
|
|
1896
|
-
child.unref();
|
|
1897
|
-
return Promise.resolve();
|
|
1898
|
-
}
|
|
1899
|
-
|
|
1900
|
-
// pkgs/@akanjs/cli/application/application.runner.ts
|
|
1901
|
-
var loadBuildRunner = async () => (await import("./applicationBuildRunner-6m8n71w4.js")).ApplicationBuildRunner;
|
|
1902
|
-
var loadReleasePackager = async () => (await import("./applicationReleasePackager-gg8ccbeh.js")).ApplicationReleasePackager;
|
|
1903
|
-
var loadCapacitorApp = async () => (await import("./capacitorApp-mr81qdxq.js")).CapacitorApp;
|
|
1904
|
-
var loadAbstractCompactor = async () => (await import("./abstractCompactor-e5yq60qz.js")).AbstractCompactor;
|
|
2052
|
+
var loadBuildRunner = async () => (await import("./applicationBuildRunner-kxwbssgn.js")).ApplicationBuildRunner;
|
|
2053
|
+
var loadReleasePackager = async () => (await import("./applicationReleasePackager-4yasxwv9.js")).ApplicationReleasePackager;
|
|
2054
|
+
var loadCapacitorApp = async () => (await import("./capacitorApp-36rbr858.js")).CapacitorApp;
|
|
2055
|
+
var loadAbstractCompactor = async () => (await import("./abstractCompactor-mjh5ppsq.js")).AbstractCompactor;
|
|
1905
2056
|
var loadPrompts = async () => await import("@inquirer/prompts");
|
|
1906
2057
|
|
|
1907
2058
|
class ApplicationRunner extends runner("application") {
|
|
@@ -1914,8 +2065,11 @@ class ApplicationRunner extends runner("application") {
|
|
|
1914
2065
|
});
|
|
1915
2066
|
return AppExecutor.from(workspace, appName);
|
|
1916
2067
|
}
|
|
2068
|
+
async planSlice(app) {
|
|
2069
|
+
return await new SlicePlanner(app).plan();
|
|
2070
|
+
}
|
|
1917
2071
|
async removeApplication(app) {
|
|
1918
|
-
await app.workspace.
|
|
2072
|
+
await app.workspace.removeDir(`apps/${app.name}`);
|
|
1919
2073
|
}
|
|
1920
2074
|
async getConfig(app) {
|
|
1921
2075
|
return await app.getConfig();
|
|
@@ -1945,10 +2099,69 @@ class ApplicationRunner extends runner("application") {
|
|
|
1945
2099
|
stdio: "inherit"
|
|
1946
2100
|
});
|
|
1947
2101
|
}
|
|
2102
|
+
#runtimeDirOf(app, override) {
|
|
2103
|
+
return path5.resolve(override ?? process.env.AKAN_RUNTIME_DIR ?? path5.join(app.workspace.workspaceRoot, "local", "apps", app.name, "runtime"));
|
|
2104
|
+
}
|
|
2105
|
+
async runLogs(app, options) {
|
|
2106
|
+
const { LogControlUnavailableError, LogTailClient } = await import("akanjs/server/logging/logTailClient");
|
|
2107
|
+
const { LogQueryMatcher } = await import("akanjs/server/logging/logQuery");
|
|
2108
|
+
const runtimeDir = this.#runtimeDirOf(app, options.runtimeDir);
|
|
2109
|
+
const socketPath = LogTailClient.socketPath(runtimeDir);
|
|
2110
|
+
const { level, grep, endpoint, trace, child, role, origin, since } = options;
|
|
2111
|
+
const query = Object.fromEntries(Object.entries({ level, grep, endpoint, trace, child, role, origin, since }).filter(([, value]) => value !== undefined && value !== null && value !== ""));
|
|
2112
|
+
const print = (record) => process.stdout.write(options.json ? `${JSON.stringify(record)}
|
|
2113
|
+
` : Logger4.render(record));
|
|
2114
|
+
const note = (text) => process.stderr.write(`[akan logs] ${text}
|
|
2115
|
+
`);
|
|
2116
|
+
let finish = null;
|
|
2117
|
+
const closed = new Promise((resolve) => {
|
|
2118
|
+
finish = resolve;
|
|
2119
|
+
});
|
|
2120
|
+
let client;
|
|
2121
|
+
try {
|
|
2122
|
+
client = await LogTailClient.connect(socketPath, {
|
|
2123
|
+
onRecord: (entry) => print(entry.record),
|
|
2124
|
+
onEvent: (response) => {
|
|
2125
|
+
if (response.type === "dropped")
|
|
2126
|
+
note(`${response.count} records dropped (reader too slow)`);
|
|
2127
|
+
},
|
|
2128
|
+
onClose: () => finish?.()
|
|
2129
|
+
});
|
|
2130
|
+
} catch (error) {
|
|
2131
|
+
if (error instanceof LogControlUnavailableError)
|
|
2132
|
+
throw new Error(`${app.name} is not running (no ${path5.basename(socketPath)} in ${runtimeDir}). Start it with \`akan start ${app.name}\`, or pass --runtime-dir for a built app.`);
|
|
2133
|
+
throw error;
|
|
2134
|
+
}
|
|
2135
|
+
const sinceMs = since ? LogQueryMatcher.parseSince(since) : undefined;
|
|
2136
|
+
const noteCoverage = (coverage) => {
|
|
2137
|
+
if (sinceMs !== undefined && coverage.from !== null && sinceMs < coverage.from)
|
|
2138
|
+
note(`${LogTailClient.describeCoverage(coverage)}; older records are not retained`);
|
|
2139
|
+
};
|
|
2140
|
+
if (endpoint)
|
|
2141
|
+
note("requests served by the primitive query fast path carry no endpoint and are not shown");
|
|
2142
|
+
if (options.follow === false) {
|
|
2143
|
+
const { entries, coverage } = await client.history({
|
|
2144
|
+
...query,
|
|
2145
|
+
...options.replay ? { limit: options.replay } : {}
|
|
2146
|
+
});
|
|
2147
|
+
for (const entry of entries)
|
|
2148
|
+
print(entry.record);
|
|
2149
|
+
noteCoverage(coverage);
|
|
2150
|
+
client.close();
|
|
2151
|
+
return;
|
|
2152
|
+
}
|
|
2153
|
+
const subscribed = await client.subscribe(query, { replay: options.replay });
|
|
2154
|
+
noteCoverage(subscribed.coverage);
|
|
2155
|
+
const stop = () => client.close();
|
|
2156
|
+
process.once("SIGINT", stop);
|
|
2157
|
+
process.once("SIGTERM", stop);
|
|
2158
|
+
await closed;
|
|
2159
|
+
}
|
|
1948
2160
|
async runConsole(app) {
|
|
1949
2161
|
const serverPath = `${app.cwdPath}/server.ts`;
|
|
1950
2162
|
if (!await app.exists("server.ts"))
|
|
1951
2163
|
throw new Error(`Server file not found: apps/${app.name}/server.ts`);
|
|
2164
|
+
const runtimeDir = this.#runtimeDirOf(app);
|
|
1952
2165
|
const code = `
|
|
1953
2166
|
const serverModule = await import(${JSON.stringify(serverPath)});
|
|
1954
2167
|
const { assertAkanConsoleAllowed, startAkanConsole } = await import("akanjs/server");
|
|
@@ -1958,6 +2171,7 @@ assertAkanConsoleAllowed(server.env);
|
|
|
1958
2171
|
await server.start({ listen: false, web: false });
|
|
1959
2172
|
try {
|
|
1960
2173
|
await startAkanConsole(server, {
|
|
2174
|
+
runtimeDir: ${JSON.stringify(runtimeDir)},
|
|
1961
2175
|
globals: {
|
|
1962
2176
|
srv: serverModule.srv,
|
|
1963
2177
|
sig: serverModule.sig,
|
|
@@ -2005,9 +2219,14 @@ try {
|
|
|
2005
2219
|
} = {}) {
|
|
2006
2220
|
return new (await loadBuildRunner())(app, { fast, reporter }).build({ spinner });
|
|
2007
2221
|
}
|
|
2008
|
-
async start(app, {
|
|
2222
|
+
async start(app, {
|
|
2223
|
+
open = false,
|
|
2224
|
+
onStart,
|
|
2225
|
+
onDevEvent,
|
|
2226
|
+
stdio = "inherit"
|
|
2227
|
+
} = {}) {
|
|
2009
2228
|
const { env } = await app.prepareCommand("start");
|
|
2010
|
-
const appHost = await new AkanAppHost(app, { env,
|
|
2229
|
+
const appHost = await new AkanAppHost(app, { env, stdio, onDevEvent }).start();
|
|
2011
2230
|
onStart?.();
|
|
2012
2231
|
if (open)
|
|
2013
2232
|
setTimeout(() => openBrowser(`http://localhost:${env.AKAN_PUBLIC_CLIENT_PORT ?? env.PORT ?? "8282"}`), 3000);
|
|
@@ -2239,6 +2458,602 @@ ${detail}`
|
|
|
2239
2458
|
}
|
|
2240
2459
|
}
|
|
2241
2460
|
|
|
2461
|
+
// pkgs/@akanjs/cli/application/devPortReclaimer.ts
|
|
2462
|
+
class DevPortReclaimer {
|
|
2463
|
+
static lsofTimeoutMs = 3000;
|
|
2464
|
+
static releaseTimeoutMs = 8000;
|
|
2465
|
+
static maxAncestorDepth = 8;
|
|
2466
|
+
static #releasePollMs = 100;
|
|
2467
|
+
static wsPortOffset = 1e4;
|
|
2468
|
+
static portsFor(ports) {
|
|
2469
|
+
return [...new Set(ports.flatMap((port) => [port, port + DevPortReclaimer.wsPortOffset]))].sort((left, right) => left - right);
|
|
2470
|
+
}
|
|
2471
|
+
static isAkanCommand(command) {
|
|
2472
|
+
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");
|
|
2473
|
+
}
|
|
2474
|
+
async reclaim(ports) {
|
|
2475
|
+
const killed = [];
|
|
2476
|
+
const foreign = [];
|
|
2477
|
+
const seen = new Set;
|
|
2478
|
+
for (const port of DevPortReclaimer.portsFor(ports)) {
|
|
2479
|
+
for (const holder of await this.holdersOf(port)) {
|
|
2480
|
+
if (seen.has(holder.pid))
|
|
2481
|
+
continue;
|
|
2482
|
+
seen.add(holder.pid);
|
|
2483
|
+
if (!holder.akan) {
|
|
2484
|
+
foreign.push(holder);
|
|
2485
|
+
continue;
|
|
2486
|
+
}
|
|
2487
|
+
if (await this.#terminate(holder.pid))
|
|
2488
|
+
killed.push(holder);
|
|
2489
|
+
else
|
|
2490
|
+
foreign.push(holder);
|
|
2491
|
+
}
|
|
2492
|
+
}
|
|
2493
|
+
return { killed, foreign };
|
|
2494
|
+
}
|
|
2495
|
+
async holdersOf(port) {
|
|
2496
|
+
const holders = [];
|
|
2497
|
+
for (const listenerPid of await this.#pidsOn(port)) {
|
|
2498
|
+
if (listenerPid === process.pid || listenerPid === process.ppid)
|
|
2499
|
+
continue;
|
|
2500
|
+
const command = await this.#commandOf(listenerPid);
|
|
2501
|
+
if (!command)
|
|
2502
|
+
continue;
|
|
2503
|
+
if (!DevPortReclaimer.isAkanCommand(command)) {
|
|
2504
|
+
holders.push({ pid: listenerPid, command, port, listenerPid, akan: false });
|
|
2505
|
+
continue;
|
|
2506
|
+
}
|
|
2507
|
+
const root = await this.#akanRootOf(listenerPid, command);
|
|
2508
|
+
holders.push({ pid: root.pid, command: root.command, port, listenerPid, akan: true });
|
|
2509
|
+
}
|
|
2510
|
+
return holders;
|
|
2511
|
+
}
|
|
2512
|
+
async#akanRootOf(pid, command) {
|
|
2513
|
+
let root = { pid, command };
|
|
2514
|
+
for (let depth = 0;depth < DevPortReclaimer.maxAncestorDepth; depth += 1) {
|
|
2515
|
+
const parentPid = await this.#ppidOf(root.pid);
|
|
2516
|
+
if (parentPid === null || parentPid <= 1 || parentPid === process.pid || parentPid === process.ppid)
|
|
2517
|
+
break;
|
|
2518
|
+
const parentCommand = await this.#commandOf(parentPid);
|
|
2519
|
+
if (!parentCommand || !DevPortReclaimer.isAkanCommand(parentCommand))
|
|
2520
|
+
break;
|
|
2521
|
+
root = { pid: parentPid, command: parentCommand };
|
|
2522
|
+
}
|
|
2523
|
+
return root;
|
|
2524
|
+
}
|
|
2525
|
+
async#ppidOf(pid) {
|
|
2526
|
+
const output = await this.#run(["ps", "-p", String(pid), "-o", "ppid="]);
|
|
2527
|
+
const parsed = Number(output.trim());
|
|
2528
|
+
return Number.isInteger(parsed) && parsed > 0 ? parsed : null;
|
|
2529
|
+
}
|
|
2530
|
+
async#pidsOn(port) {
|
|
2531
|
+
const output = await this.#run(["lsof", "-ti", `tcp:${port}`, "-sTCP:LISTEN"]);
|
|
2532
|
+
return [
|
|
2533
|
+
...new Set(output.split(`
|
|
2534
|
+
`).map((line) => Number(line.trim())).filter((pid) => Number.isInteger(pid) && pid > 0))
|
|
2535
|
+
];
|
|
2536
|
+
}
|
|
2537
|
+
async#commandOf(pid) {
|
|
2538
|
+
const output = await this.#run(["ps", "-p", String(pid), "-o", "command="]);
|
|
2539
|
+
return output.trim() || null;
|
|
2540
|
+
}
|
|
2541
|
+
async#terminate(pid) {
|
|
2542
|
+
if (!this.#signal(pid, "SIGTERM"))
|
|
2543
|
+
return !this.#alive(pid);
|
|
2544
|
+
const deadline = Date.now() + DevPortReclaimer.releaseTimeoutMs;
|
|
2545
|
+
while (Date.now() < deadline) {
|
|
2546
|
+
if (!this.#alive(pid))
|
|
2547
|
+
return true;
|
|
2548
|
+
await Bun.sleep(DevPortReclaimer.#releasePollMs);
|
|
2549
|
+
}
|
|
2550
|
+
this.#signal(pid, "SIGKILL");
|
|
2551
|
+
await Bun.sleep(DevPortReclaimer.#releasePollMs);
|
|
2552
|
+
return !this.#alive(pid);
|
|
2553
|
+
}
|
|
2554
|
+
#signal(pid, signal) {
|
|
2555
|
+
try {
|
|
2556
|
+
process.kill(pid, signal);
|
|
2557
|
+
return true;
|
|
2558
|
+
} catch {
|
|
2559
|
+
return false;
|
|
2560
|
+
}
|
|
2561
|
+
}
|
|
2562
|
+
#alive(pid) {
|
|
2563
|
+
try {
|
|
2564
|
+
process.kill(pid, 0);
|
|
2565
|
+
return true;
|
|
2566
|
+
} catch (error) {
|
|
2567
|
+
return error.code === "EPERM";
|
|
2568
|
+
}
|
|
2569
|
+
}
|
|
2570
|
+
async#run(command) {
|
|
2571
|
+
const proc = Bun.spawn(command, { stdio: ["ignore", "pipe", "ignore"] });
|
|
2572
|
+
const timer = setTimeout(() => proc.kill("SIGKILL"), DevPortReclaimer.lsofTimeoutMs);
|
|
2573
|
+
try {
|
|
2574
|
+
return await new Response(proc.stdout).text();
|
|
2575
|
+
} catch {
|
|
2576
|
+
return "";
|
|
2577
|
+
} finally {
|
|
2578
|
+
clearTimeout(timer);
|
|
2579
|
+
}
|
|
2580
|
+
}
|
|
2581
|
+
}
|
|
2582
|
+
|
|
2583
|
+
// pkgs/@akanjs/cli/application/devStreamView.ts
|
|
2584
|
+
import { Logger as Logger5 } from "akanjs/common";
|
|
2585
|
+
import chalk from "chalk";
|
|
2586
|
+
var appColors = ["cyan", "magenta", "yellow", "green", "blue", "red"];
|
|
2587
|
+
|
|
2588
|
+
class DevStreamView {
|
|
2589
|
+
#logger = new Logger5("akan start");
|
|
2590
|
+
#partial = new Map;
|
|
2591
|
+
#lastState = new Map;
|
|
2592
|
+
#color = new Map;
|
|
2593
|
+
#width = 0;
|
|
2594
|
+
constructor(appNames) {
|
|
2595
|
+
this.#width = appNames.reduce((max, name) => Math.max(max, name.length), 0);
|
|
2596
|
+
appNames.forEach((name, idx) => {
|
|
2597
|
+
this.#color.set(name, appColors[idx % appColors.length] ?? "cyan");
|
|
2598
|
+
});
|
|
2599
|
+
}
|
|
2600
|
+
onOutput = (app, kind, text) => {
|
|
2601
|
+
const key = `${app}:${kind}`;
|
|
2602
|
+
const pending = `${this.#partial.get(key) ?? ""}${text}`;
|
|
2603
|
+
const lines = pending.split(`
|
|
2604
|
+
`);
|
|
2605
|
+
this.#partial.set(key, lines.pop() ?? "");
|
|
2606
|
+
if (lines.length === 0)
|
|
2607
|
+
return;
|
|
2608
|
+
const prefixed = lines.map((line) => `${this.#prefix(app)}${line}`).join(`
|
|
2609
|
+
`);
|
|
2610
|
+
(kind === "stderr" ? process.stderr : process.stdout).write(`${prefixed}
|
|
2611
|
+
`);
|
|
2612
|
+
};
|
|
2613
|
+
onStatus = (statuses) => {
|
|
2614
|
+
for (const status of statuses) {
|
|
2615
|
+
if (this.#lastState.get(status.name) === status.state)
|
|
2616
|
+
continue;
|
|
2617
|
+
this.#lastState.set(status.name, status.state);
|
|
2618
|
+
const detail = status.detail ? ` (${status.detail})` : "";
|
|
2619
|
+
const where = status.state === "ready" ? ` \u2014 ${status.url}` : "";
|
|
2620
|
+
const line = `${status.name} ${status.state}${detail}${where}`;
|
|
2621
|
+
if (status.state === "failed")
|
|
2622
|
+
this.#logger.error(line);
|
|
2623
|
+
else if (status.state === "stopped")
|
|
2624
|
+
this.#logger.warn(line);
|
|
2625
|
+
else
|
|
2626
|
+
this.#logger.info(line);
|
|
2627
|
+
}
|
|
2628
|
+
};
|
|
2629
|
+
onNote = (text, level) => {
|
|
2630
|
+
if (level === "warn")
|
|
2631
|
+
this.#logger.warn(text);
|
|
2632
|
+
else
|
|
2633
|
+
this.#logger.info(text);
|
|
2634
|
+
};
|
|
2635
|
+
waitForExit = () => new Promise(() => {
|
|
2636
|
+
return;
|
|
2637
|
+
});
|
|
2638
|
+
close = () => {
|
|
2639
|
+
for (const [key, remainder] of this.#partial) {
|
|
2640
|
+
if (!remainder)
|
|
2641
|
+
continue;
|
|
2642
|
+
const app = key.slice(0, key.lastIndexOf(":"));
|
|
2643
|
+
process.stdout.write(`${this.#prefix(app)}${remainder}
|
|
2644
|
+
`);
|
|
2645
|
+
}
|
|
2646
|
+
this.#partial.clear();
|
|
2647
|
+
};
|
|
2648
|
+
#prefix(app) {
|
|
2649
|
+
const color = this.#color.get(app) ?? "cyan";
|
|
2650
|
+
return `${chalk[color](app.padEnd(this.#width))} ${chalk.dim("\u2502")} `;
|
|
2651
|
+
}
|
|
2652
|
+
}
|
|
2653
|
+
|
|
2654
|
+
// pkgs/@akanjs/cli/application/devBootConcurrency.ts
|
|
2655
|
+
import os from "os";
|
|
2656
|
+
import { MemoryLimit as MemoryLimit2 } from "akanjs/server/memoryLimit";
|
|
2657
|
+
|
|
2658
|
+
class DevBootConcurrency {
|
|
2659
|
+
static perAppBytes = 900 * 1024 * 1024;
|
|
2660
|
+
static memoryShare = 0.5;
|
|
2661
|
+
static coresPerApp = 4;
|
|
2662
|
+
static budget() {
|
|
2663
|
+
const hostBytes = os.totalmem();
|
|
2664
|
+
const declaredBytes = MemoryLimit2.parseBytesEnv("AKAN_MEMORY_LIMIT") ?? MemoryLimit2.readCgroupBytes();
|
|
2665
|
+
return { memoryBytes: Math.min(hostBytes, declaredBytes ?? hostBytes), cores: os.availableParallelism() };
|
|
2666
|
+
}
|
|
2667
|
+
static resolve(appCount, requested, budget = DevBootConcurrency.budget()) {
|
|
2668
|
+
const apps = Math.max(1, Math.trunc(appCount));
|
|
2669
|
+
if (requested !== null && Number.isFinite(requested)) {
|
|
2670
|
+
const asked = Math.max(1, Math.trunc(requested));
|
|
2671
|
+
return { concurrency: Math.min(apps, asked), reason: `--concurrency ${asked}` };
|
|
2672
|
+
}
|
|
2673
|
+
const byMemory = Math.floor(budget.memoryBytes * DevBootConcurrency.memoryShare / DevBootConcurrency.perAppBytes);
|
|
2674
|
+
const byCores = Math.floor(budget.cores / DevBootConcurrency.coresPerApp);
|
|
2675
|
+
return {
|
|
2676
|
+
concurrency: Math.max(1, Math.min(apps, byMemory, byCores)),
|
|
2677
|
+
reason: `${budget.cores} cores, ${DevBootConcurrency.#formatBytes(budget.memoryBytes)} memory`
|
|
2678
|
+
};
|
|
2679
|
+
}
|
|
2680
|
+
static describe(appCount, plan) {
|
|
2681
|
+
if (plan.concurrency >= appCount)
|
|
2682
|
+
return `booting all ${appCount} apps at once (${plan.reason})`;
|
|
2683
|
+
return `booting ${plan.concurrency} of ${appCount} apps at a time (${plan.reason}) \u2014 --concurrency raises it`;
|
|
2684
|
+
}
|
|
2685
|
+
static #formatBytes(bytes) {
|
|
2686
|
+
const gigabytes = bytes / 1024 ** 3;
|
|
2687
|
+
return gigabytes >= 10 ? `${Math.round(gigabytes)}GB` : `${gigabytes.toFixed(1)}GB`;
|
|
2688
|
+
}
|
|
2689
|
+
}
|
|
2690
|
+
|
|
2691
|
+
// pkgs/@akanjs/cli/application/devSessionLog.ts
|
|
2692
|
+
import { mkdir, rename } from "fs/promises";
|
|
2693
|
+
import path6 from "path";
|
|
2694
|
+
import dayjs from "dayjs";
|
|
2695
|
+
class DevSessionLog {
|
|
2696
|
+
static fileName = "dev.log";
|
|
2697
|
+
static previousFileName = "dev.prev.log";
|
|
2698
|
+
static flushMs = 200;
|
|
2699
|
+
static bufferBytes = 64 * 1024;
|
|
2700
|
+
#workspaceRoot;
|
|
2701
|
+
#appNames;
|
|
2702
|
+
#now;
|
|
2703
|
+
#writers = new Map;
|
|
2704
|
+
#partial = new Map;
|
|
2705
|
+
#lastState = new Map;
|
|
2706
|
+
#flushTimer = null;
|
|
2707
|
+
constructor({ workspaceRoot, apps, now = () => new Date }) {
|
|
2708
|
+
this.#workspaceRoot = workspaceRoot;
|
|
2709
|
+
this.#appNames = [...apps];
|
|
2710
|
+
this.#now = now;
|
|
2711
|
+
}
|
|
2712
|
+
get appNames() {
|
|
2713
|
+
return [...this.#appNames];
|
|
2714
|
+
}
|
|
2715
|
+
pathOf(app) {
|
|
2716
|
+
return path6.join(this.#runtimeDirOf(app), DevSessionLog.fileName);
|
|
2717
|
+
}
|
|
2718
|
+
previousPathOf(app) {
|
|
2719
|
+
return path6.join(this.#runtimeDirOf(app), DevSessionLog.previousFileName);
|
|
2720
|
+
}
|
|
2721
|
+
relativePathOf(app) {
|
|
2722
|
+
return path6.relative(this.#workspaceRoot, this.pathOf(app));
|
|
2723
|
+
}
|
|
2724
|
+
async open() {
|
|
2725
|
+
const stamp = dayjs(this.#now()).format("YYYY-MM-DD HH:mm:ss");
|
|
2726
|
+
for (const app of this.#appNames) {
|
|
2727
|
+
const file = this.pathOf(app);
|
|
2728
|
+
await mkdir(path6.dirname(file), { recursive: true });
|
|
2729
|
+
await rename(file, this.previousPathOf(app)).catch(() => {
|
|
2730
|
+
return;
|
|
2731
|
+
});
|
|
2732
|
+
this.#writers.set(app, Bun.file(file).writer({ highWaterMark: DevSessionLog.bufferBytes }));
|
|
2733
|
+
this.#writeLine(app, `\u2500\u2500 akan start \xB7 ${app} \xB7 ${stamp} \u2500\u2500`);
|
|
2734
|
+
}
|
|
2735
|
+
}
|
|
2736
|
+
write(app, kind, chunk) {
|
|
2737
|
+
const key = `${app}:${kind}`;
|
|
2738
|
+
const parts = `${this.#partial.get(key) ?? ""}${chunk}`.split(`
|
|
2739
|
+
`);
|
|
2740
|
+
this.#partial.set(key, parts.pop() ?? "");
|
|
2741
|
+
for (const line of parts)
|
|
2742
|
+
this.#writeLine(app, stripAnsi(line));
|
|
2743
|
+
}
|
|
2744
|
+
note(text) {
|
|
2745
|
+
for (const app of this.#appNames)
|
|
2746
|
+
this.#writeLine(app, `[akan] ${text}`);
|
|
2747
|
+
}
|
|
2748
|
+
status(statuses) {
|
|
2749
|
+
for (const status of statuses) {
|
|
2750
|
+
if (this.#lastState.get(status.name) === status.state)
|
|
2751
|
+
continue;
|
|
2752
|
+
this.#lastState.set(status.name, status.state);
|
|
2753
|
+
const detail = status.detail ? ` (${status.detail})` : "";
|
|
2754
|
+
const where = status.state === "ready" ? ` \u2014 ${status.url}` : "";
|
|
2755
|
+
this.#writeLine(status.name, `[akan] ${status.name} ${status.state}${detail}${where}`);
|
|
2756
|
+
}
|
|
2757
|
+
}
|
|
2758
|
+
async close() {
|
|
2759
|
+
for (const [key, remainder] of this.#partial) {
|
|
2760
|
+
if (!remainder)
|
|
2761
|
+
continue;
|
|
2762
|
+
this.#writeLine(key.slice(0, key.lastIndexOf(":")), stripAnsi(remainder));
|
|
2763
|
+
}
|
|
2764
|
+
this.#partial.clear();
|
|
2765
|
+
if (this.#flushTimer)
|
|
2766
|
+
clearTimeout(this.#flushTimer);
|
|
2767
|
+
this.#flushTimer = null;
|
|
2768
|
+
await Promise.all([...this.#writers.values()].map(async (writer) => await writer.end()));
|
|
2769
|
+
this.#writers.clear();
|
|
2770
|
+
}
|
|
2771
|
+
#runtimeDirOf(app) {
|
|
2772
|
+
return path6.join(this.#workspaceRoot, "local", "apps", app, "runtime");
|
|
2773
|
+
}
|
|
2774
|
+
#writeLine(app, text) {
|
|
2775
|
+
const writer = this.#writers.get(app);
|
|
2776
|
+
if (!writer)
|
|
2777
|
+
return;
|
|
2778
|
+
writer.write(`${text}
|
|
2779
|
+
`);
|
|
2780
|
+
this.#scheduleFlush();
|
|
2781
|
+
}
|
|
2782
|
+
#scheduleFlush() {
|
|
2783
|
+
if (this.#flushTimer)
|
|
2784
|
+
return;
|
|
2785
|
+
this.#flushTimer = setTimeout(() => {
|
|
2786
|
+
this.#flushTimer = null;
|
|
2787
|
+
for (const writer of this.#writers.values())
|
|
2788
|
+
writer.flush();
|
|
2789
|
+
}, DevSessionLog.flushMs);
|
|
2790
|
+
}
|
|
2791
|
+
}
|
|
2792
|
+
|
|
2793
|
+
// pkgs/@akanjs/cli/application/devSupervisor.ts
|
|
2794
|
+
class DevSupervisor {
|
|
2795
|
+
static supervisedEnvKey = "AKAN_DEV_SUPERVISED";
|
|
2796
|
+
static childHooks() {
|
|
2797
|
+
if (process.env[DevSupervisor.supervisedEnvKey] !== "1" || !process.send)
|
|
2798
|
+
return {};
|
|
2799
|
+
const send = process.send.bind(process);
|
|
2800
|
+
return {
|
|
2801
|
+
stdio: "pipe",
|
|
2802
|
+
onDevEvent: (event) => {
|
|
2803
|
+
try {
|
|
2804
|
+
send(event);
|
|
2805
|
+
} catch {}
|
|
2806
|
+
}
|
|
2807
|
+
};
|
|
2808
|
+
}
|
|
2809
|
+
static readyTimeoutMs = 180000;
|
|
2810
|
+
static shutdownGraceMs = 12000;
|
|
2811
|
+
#options;
|
|
2812
|
+
#children = new Map;
|
|
2813
|
+
#sessionLog;
|
|
2814
|
+
#view = null;
|
|
2815
|
+
#stopping = false;
|
|
2816
|
+
constructor(options) {
|
|
2817
|
+
this.#options = options;
|
|
2818
|
+
this.#sessionLog = new DevSessionLog({
|
|
2819
|
+
workspaceRoot: options.apps[0]?.workspace.workspaceRoot ?? process.cwd(),
|
|
2820
|
+
apps: options.apps.map((app) => app.name)
|
|
2821
|
+
});
|
|
2822
|
+
}
|
|
2823
|
+
get statuses() {
|
|
2824
|
+
return [...this.#children.values()].map((child) => child.status);
|
|
2825
|
+
}
|
|
2826
|
+
get sessionLog() {
|
|
2827
|
+
return this.#sessionLog;
|
|
2828
|
+
}
|
|
2829
|
+
async run(view) {
|
|
2830
|
+
this.#view = view;
|
|
2831
|
+
await this.#sessionLog.open();
|
|
2832
|
+
const ports = await this.#assignPorts();
|
|
2833
|
+
for (const app of this.#options.apps)
|
|
2834
|
+
this.#children.set(app.name, this.#makeChild(app, ports.get(app.name) ?? 0));
|
|
2835
|
+
this.#publishStatus();
|
|
2836
|
+
const logPaths = this.#sessionLog.appNames.map((name) => this.#sessionLog.relativePathOf(name));
|
|
2837
|
+
this.#note(`session log: ${logPaths.join(" \xB7 ")}`, "info");
|
|
2838
|
+
const stopped = this.#installSignalHandlers();
|
|
2839
|
+
this.#bootInOrder();
|
|
2840
|
+
await Promise.race([view.waitForExit(), stopped]);
|
|
2841
|
+
await this.stop();
|
|
2842
|
+
view.close?.();
|
|
2843
|
+
await this.#sessionLog.close();
|
|
2844
|
+
}
|
|
2845
|
+
async#assignPorts() {
|
|
2846
|
+
const taken = new Set;
|
|
2847
|
+
const ports = new Map;
|
|
2848
|
+
for (const app of this.#options.apps) {
|
|
2849
|
+
let port = await app.getDevPort();
|
|
2850
|
+
while (taken.has(port))
|
|
2851
|
+
port += 1;
|
|
2852
|
+
taken.add(port);
|
|
2853
|
+
ports.set(app.name, port);
|
|
2854
|
+
}
|
|
2855
|
+
return ports;
|
|
2856
|
+
}
|
|
2857
|
+
#makeChild(app, port) {
|
|
2858
|
+
let markReady;
|
|
2859
|
+
const ready = new Promise((resolve) => {
|
|
2860
|
+
markReady = resolve;
|
|
2861
|
+
});
|
|
2862
|
+
return {
|
|
2863
|
+
status: {
|
|
2864
|
+
app,
|
|
2865
|
+
name: app.name,
|
|
2866
|
+
port,
|
|
2867
|
+
url: `http://localhost:${port}`,
|
|
2868
|
+
state: "starting",
|
|
2869
|
+
detail: "queued",
|
|
2870
|
+
exitCode: null
|
|
2871
|
+
},
|
|
2872
|
+
proc: null,
|
|
2873
|
+
ready,
|
|
2874
|
+
markReady,
|
|
2875
|
+
opened: false
|
|
2876
|
+
};
|
|
2877
|
+
}
|
|
2878
|
+
async#bootInOrder() {
|
|
2879
|
+
const queue = [...this.#children.values()];
|
|
2880
|
+
const plan = DevBootConcurrency.resolve(queue.length, this.#options.concurrency ?? null);
|
|
2881
|
+
const concurrency = plan.concurrency;
|
|
2882
|
+
if (queue.length > 1)
|
|
2883
|
+
this.#note(DevBootConcurrency.describe(queue.length, plan), "info");
|
|
2884
|
+
const waves = [];
|
|
2885
|
+
for (let idx = 0;idx < queue.length; idx += concurrency)
|
|
2886
|
+
waves.push(queue.slice(idx, idx + concurrency));
|
|
2887
|
+
for (const wave of waves) {
|
|
2888
|
+
if (this.#stopping)
|
|
2889
|
+
return;
|
|
2890
|
+
for (const child of wave)
|
|
2891
|
+
this.#spawnChild(child);
|
|
2892
|
+
this.#publishStatus();
|
|
2893
|
+
await Promise.all(wave.map(async (child) => await this.#waitForReady(child)));
|
|
2894
|
+
}
|
|
2895
|
+
}
|
|
2896
|
+
async#waitForReady(child) {
|
|
2897
|
+
let timer = null;
|
|
2898
|
+
const expired = new Promise((resolve) => {
|
|
2899
|
+
timer = setTimeout(() => resolve("expired"), DevSupervisor.readyTimeoutMs);
|
|
2900
|
+
});
|
|
2901
|
+
try {
|
|
2902
|
+
if (await Promise.race([child.ready.then(() => "ready"), expired]) === "expired")
|
|
2903
|
+
this.#note(`${child.status.name} has not reported ready after ${Math.round(DevSupervisor.readyTimeoutMs / 1000)}s; starting the next app anyway`, "warn");
|
|
2904
|
+
} finally {
|
|
2905
|
+
if (timer)
|
|
2906
|
+
clearTimeout(timer);
|
|
2907
|
+
}
|
|
2908
|
+
}
|
|
2909
|
+
static childArgs(name, { write = true } = {}) {
|
|
2910
|
+
return [
|
|
2911
|
+
"start",
|
|
2912
|
+
name,
|
|
2913
|
+
"--plain",
|
|
2914
|
+
"--dbup",
|
|
2915
|
+
"false",
|
|
2916
|
+
"--write",
|
|
2917
|
+
String(write)
|
|
2918
|
+
];
|
|
2919
|
+
}
|
|
2920
|
+
#spawnChild(child) {
|
|
2921
|
+
const { app, name, port } = child.status;
|
|
2922
|
+
const args = DevSupervisor.childArgs(name, { write: this.#options.write ?? true });
|
|
2923
|
+
const proc = Bun.spawn([process.execPath, Bun.main, ...args], {
|
|
2924
|
+
cwd: app.workspace.workspaceRoot,
|
|
2925
|
+
env: {
|
|
2926
|
+
...process.env,
|
|
2927
|
+
[DevSupervisor.supervisedEnvKey]: "1",
|
|
2928
|
+
AKAN_PUBLIC_APP_NAME: name,
|
|
2929
|
+
AKAN_DEV_PORT: String(port)
|
|
2930
|
+
},
|
|
2931
|
+
stdio: ["ignore", "pipe", "pipe"],
|
|
2932
|
+
ipc: (message) => {
|
|
2933
|
+
if (!message || typeof message !== "object" || typeof message.state !== "string")
|
|
2934
|
+
return;
|
|
2935
|
+
this.#applyChildEvent(child, message);
|
|
2936
|
+
},
|
|
2937
|
+
serialization: "advanced",
|
|
2938
|
+
onExit: (_proc, exitCode, signalCode) => {
|
|
2939
|
+
child.proc = null;
|
|
2940
|
+
child.status.exitCode = exitCode;
|
|
2941
|
+
if (this.#stopping)
|
|
2942
|
+
return;
|
|
2943
|
+
child.status.state = "stopped";
|
|
2944
|
+
child.status.detail = signalCode ? `killed by ${signalCode}` : `exited with ${exitCode}`;
|
|
2945
|
+
child.markReady();
|
|
2946
|
+
this.#publishStatus();
|
|
2947
|
+
}
|
|
2948
|
+
});
|
|
2949
|
+
child.proc = proc;
|
|
2950
|
+
child.status.state = "starting";
|
|
2951
|
+
child.status.detail = `pid ${proc.pid}`;
|
|
2952
|
+
this.#drain(proc.stdout, name, "stdout");
|
|
2953
|
+
this.#drain(proc.stderr, name, "stderr");
|
|
2954
|
+
}
|
|
2955
|
+
#applyChildEvent(child, event) {
|
|
2956
|
+
child.status.state = event.state;
|
|
2957
|
+
child.status.detail = event.detail ?? "";
|
|
2958
|
+
if (event.state === "ready") {
|
|
2959
|
+
child.markReady();
|
|
2960
|
+
if (this.#options.open && !child.opened) {
|
|
2961
|
+
child.opened = true;
|
|
2962
|
+
openBrowser(child.status.url);
|
|
2963
|
+
}
|
|
2964
|
+
}
|
|
2965
|
+
if (event.state === "failed")
|
|
2966
|
+
child.markReady();
|
|
2967
|
+
this.#publishStatus();
|
|
2968
|
+
}
|
|
2969
|
+
async#drain(stream, name, kind) {
|
|
2970
|
+
const decoder = new TextDecoder;
|
|
2971
|
+
try {
|
|
2972
|
+
for await (const chunk of stream) {
|
|
2973
|
+
const text = decoder.decode(chunk, { stream: true });
|
|
2974
|
+
if (!text)
|
|
2975
|
+
continue;
|
|
2976
|
+
this.#view?.onOutput(name, kind, text);
|
|
2977
|
+
this.#sessionLog.write(name, kind, text);
|
|
2978
|
+
}
|
|
2979
|
+
} catch {}
|
|
2980
|
+
}
|
|
2981
|
+
#publishStatus() {
|
|
2982
|
+
this.#view?.onStatus(this.statuses);
|
|
2983
|
+
this.#sessionLog.status(this.statuses);
|
|
2984
|
+
}
|
|
2985
|
+
#note(text, level) {
|
|
2986
|
+
this.#view?.onNote(text, level);
|
|
2987
|
+
this.#sessionLog.note(text);
|
|
2988
|
+
}
|
|
2989
|
+
#installSignalHandlers() {
|
|
2990
|
+
return new Promise((resolve) => {
|
|
2991
|
+
let asked = false;
|
|
2992
|
+
const onSignal = () => {
|
|
2993
|
+
if (asked) {
|
|
2994
|
+
this.#note("abandoning the wait; children may be left running", "warn");
|
|
2995
|
+
process.exit(130);
|
|
2996
|
+
}
|
|
2997
|
+
asked = true;
|
|
2998
|
+
resolve();
|
|
2999
|
+
};
|
|
3000
|
+
process.on("SIGINT", onSignal);
|
|
3001
|
+
process.on("SIGTERM", onSignal);
|
|
3002
|
+
});
|
|
3003
|
+
}
|
|
3004
|
+
async restart(name) {
|
|
3005
|
+
const child = this.#children.get(name);
|
|
3006
|
+
if (!child || this.#stopping)
|
|
3007
|
+
return;
|
|
3008
|
+
const running = child.proc;
|
|
3009
|
+
if (running) {
|
|
3010
|
+
running.kill("SIGTERM");
|
|
3011
|
+
await running.exited;
|
|
3012
|
+
}
|
|
3013
|
+
child.status.state = "starting";
|
|
3014
|
+
child.status.detail = "restarting";
|
|
3015
|
+
child.status.exitCode = null;
|
|
3016
|
+
this.#publishStatus();
|
|
3017
|
+
this.#spawnChild(child);
|
|
3018
|
+
this.#publishStatus();
|
|
3019
|
+
}
|
|
3020
|
+
async stop() {
|
|
3021
|
+
if (this.#stopping)
|
|
3022
|
+
return;
|
|
3023
|
+
this.#stopping = true;
|
|
3024
|
+
const running = [...this.#children.values()].filter((child) => Boolean(child.proc));
|
|
3025
|
+
for (const child of running)
|
|
3026
|
+
child.proc.kill("SIGTERM");
|
|
3027
|
+
const exited = Promise.all(running.map(async (child) => await child.proc.exited));
|
|
3028
|
+
let timer = null;
|
|
3029
|
+
const expired = new Promise((resolve) => {
|
|
3030
|
+
timer = setTimeout(() => resolve("expired"), DevSupervisor.shutdownGraceMs);
|
|
3031
|
+
});
|
|
3032
|
+
try {
|
|
3033
|
+
if (await Promise.race([exited.then(() => "exited"), expired]) === "expired") {
|
|
3034
|
+
for (const child of running) {
|
|
3035
|
+
if (!child.proc.killed) {
|
|
3036
|
+
this.#note(`${child.status.name} did not exit in time; killing it`, "warn");
|
|
3037
|
+
child.proc.kill("SIGKILL");
|
|
3038
|
+
}
|
|
3039
|
+
}
|
|
3040
|
+
}
|
|
3041
|
+
} finally {
|
|
3042
|
+
if (timer)
|
|
3043
|
+
clearTimeout(timer);
|
|
3044
|
+
}
|
|
3045
|
+
}
|
|
3046
|
+
}
|
|
3047
|
+
|
|
3048
|
+
// pkgs/@akanjs/cli/application/devUiMode.ts
|
|
3049
|
+
var resolveDevUi = (plain, { isTty = !!process.stdout.isTTY, columns = process.stdout.columns ?? 0 } = {}) => {
|
|
3050
|
+
if (plain)
|
|
3051
|
+
return { mode: "stream", downgraded: false };
|
|
3052
|
+
if (!isTty || columns <= 0)
|
|
3053
|
+
return { mode: "stream", downgraded: true };
|
|
3054
|
+
return { mode: "tui", downgraded: false };
|
|
3055
|
+
};
|
|
3056
|
+
|
|
2242
3057
|
// pkgs/@akanjs/cli/application/application.script.ts
|
|
2243
3058
|
class ApplicationScript extends script("application", [ApplicationRunner, LibraryScript]) {
|
|
2244
3059
|
static dbShutdownTimeoutMs = 20000;
|
|
@@ -2322,13 +3137,19 @@ class ApplicationScript extends script("application", [ApplicationRunner, Librar
|
|
|
2322
3137
|
spinner.succeed(`Application created in apps/${app.name}`);
|
|
2323
3138
|
await app.scanSync();
|
|
2324
3139
|
if (start)
|
|
2325
|
-
await this.
|
|
3140
|
+
await this.startOne(app, { open: true });
|
|
2326
3141
|
}
|
|
2327
3142
|
async removeApplication(app) {
|
|
2328
3143
|
const spinner = app.spinning("Removing application...");
|
|
2329
3144
|
await this.applicationRunner.removeApplication(app);
|
|
2330
3145
|
spinner.succeed(`Application ${app.name} (apps/${app.name}) removed`);
|
|
2331
3146
|
}
|
|
3147
|
+
async planSlice(app, format = "text") {
|
|
3148
|
+
const spinner = app.spinning("Planning distributable slice...");
|
|
3149
|
+
const plan = await this.applicationRunner.planSlice(app);
|
|
3150
|
+
spinner.succeed(`Slice planned (${plan.libs.length} libs, ${plan.warnings.length} warnings)`);
|
|
3151
|
+
Logger6.rawLog(format === "json" ? JSON.stringify(plan, null, 2) : formatSlicePlan(plan));
|
|
3152
|
+
}
|
|
2332
3153
|
async sync(sys) {
|
|
2333
3154
|
if (sys.type === "app")
|
|
2334
3155
|
await sys.scanSync();
|
|
@@ -2338,13 +3159,13 @@ class ApplicationScript extends script("application", [ApplicationRunner, Librar
|
|
|
2338
3159
|
async compact(sys, { module = null, minLines, interactive = false } = {}) {
|
|
2339
3160
|
const { scanned, reports } = await this.applicationRunner.compact(sys, { module, minLines, interactive });
|
|
2340
3161
|
if (!reports.length) {
|
|
2341
|
-
|
|
3162
|
+
Logger6.rawLog(`No abstract file long enough to compact in ${sys.name} (${scanned} scanned)`);
|
|
2342
3163
|
return;
|
|
2343
3164
|
}
|
|
2344
3165
|
for (const report of reports)
|
|
2345
|
-
|
|
3166
|
+
Logger6.rawLog(`${report.status}: ${report.path} (${report.beforeLines} -> ${report.afterLines} lines)`);
|
|
2346
3167
|
const compactedNum = reports.filter((report) => report.status === "compacted").length;
|
|
2347
|
-
|
|
3168
|
+
Logger6.rawLog(`Compacted ${compactedNum}/${reports.length} abstract files in ${sys.name}`);
|
|
2348
3169
|
}
|
|
2349
3170
|
async script(app, filename) {
|
|
2350
3171
|
const scriptFilename = filename ?? await this.applicationRunner.getScriptFilename(app);
|
|
@@ -2355,21 +3176,24 @@ class ApplicationScript extends script("application", [ApplicationRunner, Librar
|
|
|
2355
3176
|
await app.scanSync();
|
|
2356
3177
|
await this.applicationRunner.runConsole(app);
|
|
2357
3178
|
}
|
|
3179
|
+
async logs(app, options) {
|
|
3180
|
+
await this.applicationRunner.runLogs(app, options);
|
|
3181
|
+
}
|
|
2358
3182
|
async build(app, { write = true, fast = false, quiet = false } = {}) {
|
|
2359
3183
|
await app.scanSync({ write });
|
|
2360
3184
|
if (!quiet)
|
|
2361
|
-
|
|
3185
|
+
Logger6.rawLog(`Creating an optimized production build for ${app.name}...`);
|
|
2362
3186
|
try {
|
|
2363
3187
|
const result = await this.applicationRunner.build(app, {
|
|
2364
3188
|
fast,
|
|
2365
3189
|
spinner: !quiet
|
|
2366
3190
|
});
|
|
2367
|
-
|
|
3191
|
+
Logger6.rawLog(`${app.name} built in dist/apps/${app.name}`);
|
|
2368
3192
|
if (!quiet)
|
|
2369
3193
|
ApplicationBuildReporter.printSummary(result);
|
|
2370
3194
|
} catch (error) {
|
|
2371
|
-
|
|
2372
|
-
|
|
3195
|
+
Logger6.rawLog(`${app.name} build failed in dist/apps/${app.name}`, undefined, "error");
|
|
3196
|
+
Logger6.rawLog(ApplicationBuildReporter.formatError(error), undefined, "error");
|
|
2373
3197
|
throw error;
|
|
2374
3198
|
}
|
|
2375
3199
|
}
|
|
@@ -2381,15 +3205,15 @@ class ApplicationScript extends script("application", [ApplicationRunner, Librar
|
|
|
2381
3205
|
spinner.succeed(`${app.name} typechecked`);
|
|
2382
3206
|
} catch (error) {
|
|
2383
3207
|
spinner.fail(`${app.name} typecheck failed`);
|
|
2384
|
-
|
|
3208
|
+
Logger6.rawLog(ApplicationBuildReporter.formatError(error), undefined, "error");
|
|
2385
3209
|
throw error;
|
|
2386
3210
|
}
|
|
2387
3211
|
}
|
|
2388
3212
|
async test(exec, { write = true } = {}) {
|
|
2389
3213
|
if (exec instanceof LibExecutor) {
|
|
2390
3214
|
await this.libraryScript.syncLibrary(exec);
|
|
2391
|
-
const
|
|
2392
|
-
|
|
3215
|
+
const spinner = exec.spinning(`Preparing ${exec.name}...`);
|
|
3216
|
+
spinner.succeed(`${exec.name} prepared`);
|
|
2393
3217
|
await this.applicationRunner.test(exec);
|
|
2394
3218
|
return;
|
|
2395
3219
|
}
|
|
@@ -2406,12 +3230,20 @@ class ApplicationScript extends script("application", [ApplicationRunner, Librar
|
|
|
2406
3230
|
}
|
|
2407
3231
|
await this.applicationRunner.test(exec);
|
|
2408
3232
|
}
|
|
2409
|
-
async start(
|
|
2410
|
-
|
|
2411
|
-
|
|
2412
|
-
|
|
2413
|
-
|
|
2414
|
-
|
|
3233
|
+
async start(apps, { open = false, dbup = true, write = true, plain = false, kill = false, concurrency = null } = {}) {
|
|
3234
|
+
const first = apps[0];
|
|
3235
|
+
if (!first)
|
|
3236
|
+
throw new Error("No app selected to start");
|
|
3237
|
+
const { mode, downgraded } = resolveDevUi(plain);
|
|
3238
|
+
if (downgraded)
|
|
3239
|
+
first.workspace.log("no sized terminal to draw on; printing prefixed lines instead");
|
|
3240
|
+
if (kill)
|
|
3241
|
+
await this.reclaimDevPorts(apps);
|
|
3242
|
+
if (mode === "stream" && apps.length === 1)
|
|
3243
|
+
return await this.startOne(first, { open, dbup, write, ...DevSupervisor.childHooks() });
|
|
3244
|
+
await this.#startMany(apps, { open, dbup, write, mode, concurrency });
|
|
3245
|
+
}
|
|
3246
|
+
async startOne(app, { open = false, dbup = true, stdio = "inherit", write = true, onDevEvent } = {}) {
|
|
2415
3247
|
await app.scanSync({ write });
|
|
2416
3248
|
const akanConfig = await app.getConfig();
|
|
2417
3249
|
const databaseMode = process.env.AKAN_DATABASE_MODE ?? akanConfig.defaultDatabaseMode ?? "single";
|
|
@@ -2427,10 +3259,55 @@ class ApplicationScript extends script("application", [ApplicationRunner, Librar
|
|
|
2427
3259
|
onStart: () => {
|
|
2428
3260
|
spinner.succeed(`${app.name} prepared, ready to start`);
|
|
2429
3261
|
},
|
|
2430
|
-
|
|
3262
|
+
onDevEvent,
|
|
3263
|
+
stdio
|
|
2431
3264
|
});
|
|
2432
3265
|
return akanAppHost;
|
|
2433
3266
|
}
|
|
3267
|
+
async reclaimDevPorts(apps) {
|
|
3268
|
+
const workspace = apps[0]?.workspace;
|
|
3269
|
+
if (!workspace)
|
|
3270
|
+
return;
|
|
3271
|
+
const ports = await Promise.all(apps.map(async (app) => await app.getDevPort()));
|
|
3272
|
+
const { killed, foreign } = await new DevPortReclaimer().reclaim(ports);
|
|
3273
|
+
for (const holder of killed)
|
|
3274
|
+
workspace.log(`freed port ${holder.port} \u2014 killed pid ${holder.pid} (${holder.command})`);
|
|
3275
|
+
for (const holder of foreign)
|
|
3276
|
+
Logger6.rawLog(`port ${holder.port} is held by pid ${holder.pid} and was left alone: ${holder.command}`, undefined, "error");
|
|
3277
|
+
if (killed.length === 0 && foreign.length === 0)
|
|
3278
|
+
workspace.log("dev ports are free; nothing to kill");
|
|
3279
|
+
}
|
|
3280
|
+
async#startMany(apps, { open, dbup, write, mode, concurrency }) {
|
|
3281
|
+
const workspace = apps[0]?.workspace;
|
|
3282
|
+
if (!workspace)
|
|
3283
|
+
throw new Error("No app selected to start");
|
|
3284
|
+
const startedDatabase = dbup ? await this.#prepareSharedDatabase(apps) : false;
|
|
3285
|
+
const supervisor = new DevSupervisor({ apps, mode, concurrency, open, write });
|
|
3286
|
+
const view = mode === "tui" ? await (await import("./devTuiView-c08ccbat.js")).createDevTuiView(supervisor) : new DevStreamView(apps.map((app) => app.name));
|
|
3287
|
+
await supervisor.run(view);
|
|
3288
|
+
if (startedDatabase)
|
|
3289
|
+
await this.#stopDatabase(workspace);
|
|
3290
|
+
}
|
|
3291
|
+
async#prepareSharedDatabase(apps) {
|
|
3292
|
+
const workspace = apps[0]?.workspace;
|
|
3293
|
+
if (!workspace)
|
|
3294
|
+
return false;
|
|
3295
|
+
const modes = new Set;
|
|
3296
|
+
for (const app of apps) {
|
|
3297
|
+
if (app.getEnv() !== "local")
|
|
3298
|
+
continue;
|
|
3299
|
+
const akanConfig = await app.getConfig();
|
|
3300
|
+
const mode = process.env.AKAN_DATABASE_MODE ?? akanConfig.defaultDatabaseMode ?? "single";
|
|
3301
|
+
await this.syncDatabaseModeDependencies(app, akanConfig, mode);
|
|
3302
|
+
if (mode !== "single")
|
|
3303
|
+
modes.add(mode);
|
|
3304
|
+
}
|
|
3305
|
+
let started = false;
|
|
3306
|
+
for (const mode of modes)
|
|
3307
|
+
if (!await this.dbup(workspace, mode))
|
|
3308
|
+
started = true;
|
|
3309
|
+
return started;
|
|
3310
|
+
}
|
|
2434
3311
|
async buildIos(app, { write = true, target, env = "debug", regenerate = false } = {}) {
|
|
2435
3312
|
await app.scanSync({ write });
|
|
2436
3313
|
await this.applicationRunner.buildIos(app, { target, env, regenerate });
|
|
@@ -2528,7 +3405,7 @@ class ApplicationScript extends script("application", [ApplicationRunner, Librar
|
|
|
2528
3405
|
let stopping = false;
|
|
2529
3406
|
process.on("SIGINT", () => {
|
|
2530
3407
|
if (stopping) {
|
|
2531
|
-
|
|
3408
|
+
Logger6.rawLog("Abandoning the local database teardown; containers are left running.", undefined, "error");
|
|
2532
3409
|
process.exit(130);
|
|
2533
3410
|
}
|
|
2534
3411
|
stopping = true;
|
|
@@ -2536,25 +3413,22 @@ class ApplicationScript extends script("application", [ApplicationRunner, Librar
|
|
|
2536
3413
|
});
|
|
2537
3414
|
}
|
|
2538
3415
|
async#stopDatabase(workspace) {
|
|
2539
|
-
|
|
2540
|
-
const
|
|
2541
|
-
|
|
2542
|
-
})
|
|
2543
|
-
|
|
2544
|
-
|
|
2545
|
-
|
|
2546
|
-
|
|
2547
|
-
|
|
2548
|
-
|
|
2549
|
-
|
|
2550
|
-
|
|
2551
|
-
|
|
2552
|
-
|
|
2553
|
-
|
|
2554
|
-
const spinner = app.spinning("Testing application...");
|
|
2555
|
-
await this.applicationRunner.test(app);
|
|
2556
|
-
spinner.succeed(`Application ${app.name} (apps/${app.name}) test is successful`);
|
|
3416
|
+
let timer = null;
|
|
3417
|
+
const timeout = new Promise((resolve) => {
|
|
3418
|
+
timer = setTimeout(() => resolve("timeout"), ApplicationScript.dbShutdownTimeoutMs);
|
|
3419
|
+
});
|
|
3420
|
+
try {
|
|
3421
|
+
const result = await Promise.race([this.dbdown(workspace).catch(() => {
|
|
3422
|
+
return;
|
|
3423
|
+
}), timeout]);
|
|
3424
|
+
if (result !== "timeout")
|
|
3425
|
+
return;
|
|
3426
|
+
Logger6.rawLog(`Local database did not stop within ${ApplicationScript.dbShutdownTimeoutMs}ms; run \`akan dbdown\` once Docker responds.`, undefined, "error");
|
|
3427
|
+
} finally {
|
|
3428
|
+
if (timer)
|
|
3429
|
+
clearTimeout(timer);
|
|
3430
|
+
}
|
|
2557
3431
|
}
|
|
2558
3432
|
}
|
|
2559
3433
|
|
|
2560
|
-
export { openBrowser, ApplicationScript };
|
|
3434
|
+
export { openBrowser, plainTextOf, HOST_SOURCE, DevLogBuffer, ApplicationScript };
|