@akanjs/cli 3.0.0-alpha.9 → 3.0.0-alpha.90
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-cefxzggj.js} +3 -5
- package/{agent.command-f0gqkfgz.js → agent.command-r93wmq7m.js} +11 -13
- package/{application.command-x903m2qq.js → application.command-5nqfqgdn.js} +62 -38
- package/{applicationBuildRunner-ha3y8ew8.js → applicationBuildRunner-b21m07fs.js} +23 -20
- package/{applicationReleasePackager-gg8ccbeh.js → applicationReleasePackager-3wh58n2m.js} +8 -9
- package/buildBatch.proc.js +12 -14
- package/capacitorApp-gzcx6txb.js +12 -0
- package/{cloud.command-zs6kyf83.js → cloud.command-g0pk0k60.js} +15 -18
- package/commandManifest.json +1 -1
- package/{context.command-fmepgjzg.js → context.command-qd8kqrnh.js} +19 -23
- package/dependencyScanner-shs82ynp.js +8 -0
- package/devTui-2xf0meaq.js +552 -0
- package/devTuiView-7jy8f30g.js +9 -0
- package/getDirname-dr3jz1xg.js +7 -0
- package/{guideline.command-n6sw0a0c.js → guideline.command-f38x8jhw.js} +16 -18
- package/guidelines/agentRule/agentRule.generate.json +31 -0
- package/guidelines/agentRule/agentRule.instruction.md +533 -0
- package/guidelines/conventions/conventions.instruction.md +406 -197
- package/guidelines/cssRule/cssRule.instruction.md +7 -0
- package/guidelines/framework/framework.generate.json +3 -2
- package/guidelines/framework/framework.instruction.md +1 -9
- package/guidelines/mcpRule/mcpRule.generate.json +25 -0
- package/guidelines/mcpRule/mcpRule.instruction.md +217 -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 +143 -0
- package/guidelines/runtimeRule/runtimeRule.generate.json +25 -0
- package/guidelines/runtimeRule/runtimeRule.instruction.md +213 -0
- package/guidelines/ssrRule/ssrRule.instruction.md +49 -5
- package/guidelines/transportRule/transportRule.generate.json +31 -0
- package/guidelines/transportRule/transportRule.instruction.md +60 -0
- package/guidelines/workspaceOnboarding/workspaceOnboarding.instruction.md +51 -760
- package/guidelines/workspaceRecipes/workspaceRecipes.generate.json +33 -0
- package/guidelines/workspaceRecipes/workspaceRecipes.instruction.md +402 -0
- package/incrementalBuilder.proc.js +26 -25
- package/{index-774gcx29.js → index-0emb9eat.js} +41 -34
- package/index-3d9y066c.js +76 -0
- package/{index-3vta1tqa.js → index-3vvbv974.js} +32 -32
- package/{index-76rn3g2c.js → index-47pj3aee.js} +4 -4
- package/{index-y4vkyhvv.js → index-5xdqxbn9.js} +14 -14
- package/{index-1577bej2.js → index-6wwzs1bc.js} +2 -2
- package/{index-wnp7hwq7.js → index-92zztqy1.js} +7 -7
- package/{index-jhpa13zj.js → index-9bwhxrqf.js} +57 -57
- package/{index-bjpxzr6s.js → index-9wtbw98m.js} +6 -3
- package/{index-fgc8r6dj.js → index-a5vmm6x4.js} +3 -1
- package/{index-xys926f2.js → index-by5gpp8x.js} +4 -4
- package/{index-kdh76j26.js → index-cezy7yb3.js} +100 -32
- package/{index-62955kn5.js → index-d61rcfa2.js} +758 -127
- package/{index-82jrh4hj.js → index-deekbtkj.js} +13 -13
- package/{index-rxkpk7n4.js → index-e82dyeqy.js} +4 -4
- package/{index-qzdjd4ct.js → index-h759vjfa.js} +18 -18
- package/{index-j37qq1f2.js → index-h7yw2z9n.js} +7 -7
- package/{index-qb61h0k7.js → index-jydtyzva.js} +1098 -1360
- package/{index-46tjzh6s.js → index-kcxmv48c.js} +29 -29
- package/{index-4m0bphac.js → index-kfnr6c19.js} +31 -30
- package/{index-xmmzf150.js → index-mpmyqm1y.js} +1 -1
- package/{index-xjd3rqzj.js → index-n68jx79w.js} +6 -15
- package/{index-0cj2zxbm.js → index-n85fer99.js} +12 -11
- package/{index-779ske1y.js → index-pgsswppj.js} +27 -27
- package/index-pvb4vdwr.js +159 -0
- package/{index-67546d0j.js → index-q0y2jq9j.js} +14 -2
- package/{index-z8ptfkzv.js → index-q8sn827n.js} +969 -240
- package/{index-5sn5w2mt.js → index-r50qen0z.js} +94 -61
- package/{index-eyyf2149.js → index-rntcdss1.js} +36 -37
- package/{index-gcv8ncfh.js → index-sjak5v5b.js} +16 -12
- package/{index-j9cxndc0.js → index-w8kmx1b6.js} +20 -11
- package/{index-jf2ya7r2.js → index-y74k5anj.js} +8 -8
- package/{index-d43t8af2.js → index-zkvpaggp.js} +58 -25
- package/index.js +22 -25
- package/{library.command-cv9gdbmg.js → library.command-em103wtn.js} +11 -10
- package/{localRegistry.command-d8fg36zc.js → localRegistry.command-gz8apn4y.js} +20 -23
- package/{module.command-qyr1s9cn.js → module.command-451pztkc.js} +13 -16
- package/{package.command-wsjx0k47.js → package.command-rfhyf38t.js} +8 -10
- package/package.json +5 -7
- package/{page.command-rdzg65rm.js → page.command-kt19da5g.js} +7 -9
- package/{primitive.command-89g38b53.js → primitive.command-pgy69ere.js} +13 -16
- package/{quality.command-0cs2z92d.js → quality.command-gpe2vqb5.js} +226 -105
- package/recipeScanner-fpagtgc1.js +11 -0
- package/{repair.command-6fphxfwj.js → repair.command-2sbrz187.js} +12 -14
- package/{routeSourceValidator-e4jmbfcx.js → routeSourceValidator-ze6x894c.js} +0 -2
- package/{scalar.command-xfer69s6.js → scalar.command-jcf4e5sv.js} +11 -14
- package/subspace.command-kqzqtk0p.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/appSample/lib/task/task.signal.ts +11 -6
- package/templates/appSample/srvkit/AuthGuard.ts +9 -5
- package/templates/appSample/srvkit/SessionInternalArg.ts +4 -3
- package/templates/crudPages/[__model__Id]/edit/page.tsx +5 -5
- package/templates/crudPages/[__model__Id]/page.tsx +10 -13
- package/templates/crudPages/new/page.tsx +5 -5
- package/templates/crudPages/page.tsx +9 -9
- package/templates/crudSinglePage/page.tsx +11 -7
- 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 +1 -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 +2 -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-2ygfqt4f.js → workflow.command-m4z25a2p.js} +22 -25
- package/{workspace.command-88m0zs64.js → workspace.command-dqgj9zbk.js} +96 -81
- package/capacitorApp-9xswcgx7.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,38 @@
|
|
|
1
1
|
// @bun
|
|
2
|
-
import {
|
|
3
|
-
LibraryScript
|
|
4
|
-
} from "./index-d43t8af2.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-cezy7yb3.js";
|
|
6
|
+
import {
|
|
7
|
+
WorkspaceExecutor,
|
|
8
|
+
AppExecutor,
|
|
9
|
+
LibExecutor,
|
|
10
|
+
PkgExecutor
|
|
11
|
+
} from "./index-q8sn827n.js";
|
|
12
|
+
import {
|
|
13
|
+
LibraryScript
|
|
14
|
+
} from "./index-zkvpaggp.js";
|
|
12
15
|
import {
|
|
13
16
|
HmrWatcher,
|
|
14
17
|
WatchRootResolver
|
|
15
|
-
} from "./index-
|
|
18
|
+
} from "./index-9wtbw98m.js";
|
|
16
19
|
import {
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
WorkspaceExecutor
|
|
21
|
-
} from "./index-z8ptfkzv.js";
|
|
20
|
+
SlicePlanner,
|
|
21
|
+
formatSlicePlan
|
|
22
|
+
} from "./index-pvb4vdwr.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
|
+
}
|
|
25
36
|
|
|
26
37
|
// pkgs/@akanjs/devkit/applicationBuildReporter.ts
|
|
27
38
|
import { Logger } from "akanjs/common";
|
|
@@ -70,11 +81,12 @@ Caused by: ${ApplicationBuildReporter.formatError(error.cause)}` : "";
|
|
|
70
81
|
|
|
71
82
|
// pkgs/@akanjs/cli/application/application.script.ts
|
|
72
83
|
import { confirm } from "@inquirer/prompts";
|
|
73
|
-
import { Logger as
|
|
84
|
+
import { Logger as Logger6 } from "akanjs/common";
|
|
85
|
+
|
|
86
|
+
// pkgs/@akanjs/cli/application/application.runner.ts
|
|
87
|
+
import path5 from "path";
|
|
74
88
|
|
|
75
89
|
// pkgs/@akanjs/devkit/akanApp/akanApp.host.ts
|
|
76
|
-
import { stat } from "fs/promises";
|
|
77
|
-
import path2 from "path";
|
|
78
90
|
import { Logger as Logger3 } from "akanjs/common";
|
|
79
91
|
|
|
80
92
|
// pkgs/@akanjs/devkit/sshTunnel.ts
|
|
@@ -237,6 +249,8 @@ class IncrementalBuilderHost {
|
|
|
237
249
|
app;
|
|
238
250
|
ready = false;
|
|
239
251
|
#onMessage;
|
|
252
|
+
#stdio;
|
|
253
|
+
#onOutput;
|
|
240
254
|
#proc = null;
|
|
241
255
|
#status = "stopped";
|
|
242
256
|
#restartAttempts = 0;
|
|
@@ -247,11 +261,13 @@ class IncrementalBuilderHost {
|
|
|
247
261
|
#manualStop = false;
|
|
248
262
|
#inFlight = new Map;
|
|
249
263
|
#startOptions = {};
|
|
250
|
-
constructor({ app, entry, env, onMessage }) {
|
|
264
|
+
constructor({ app, entry, env, stdio = "inherit", onMessage, onOutput }) {
|
|
251
265
|
this.app = app;
|
|
252
266
|
this.entry = entry;
|
|
253
267
|
this.env = env;
|
|
268
|
+
this.#stdio = stdio;
|
|
254
269
|
this.#onMessage = onMessage;
|
|
270
|
+
this.#onOutput = onOutput ?? null;
|
|
255
271
|
}
|
|
256
272
|
get status() {
|
|
257
273
|
return this.#status;
|
|
@@ -277,7 +293,7 @@ class IncrementalBuilderHost {
|
|
|
277
293
|
proc = Bun.spawn(["bun", this.entry], {
|
|
278
294
|
cwd: this.app.cwdPath,
|
|
279
295
|
env: { ...this.env, AKAN_WATCH: "1", ...afterRecycle ? { AKAN_BUILDER_ANNOUNCE_BOOT: "1" } : {} },
|
|
280
|
-
stdio: ["ignore",
|
|
296
|
+
stdio: ["ignore", this.#stdio, this.#stdio],
|
|
281
297
|
ipc: (msg) => {
|
|
282
298
|
if (this.#proc !== proc)
|
|
283
299
|
return;
|
|
@@ -325,8 +341,24 @@ class IncrementalBuilderHost {
|
|
|
325
341
|
}
|
|
326
342
|
});
|
|
327
343
|
this.#proc = proc;
|
|
344
|
+
if (this.#stdio === "pipe" && this.#onOutput) {
|
|
345
|
+
this.#drain(proc.stdout, "stdout");
|
|
346
|
+
this.#drain(proc.stderr, "stderr");
|
|
347
|
+
}
|
|
328
348
|
this.logger.verbose(`builder spawned pid=${proc.pid} entry=${this.entry}${isRestart ? " restart=1" : ""}`);
|
|
329
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
|
+
}
|
|
330
362
|
#scheduleRestart() {
|
|
331
363
|
if (this.#manualStop || this.#restartTimer)
|
|
332
364
|
return;
|
|
@@ -350,7 +382,7 @@ class IncrementalBuilderHost {
|
|
|
350
382
|
return false;
|
|
351
383
|
this.#recycleRequested = true;
|
|
352
384
|
this.#status = "recycling";
|
|
353
|
-
this.logger.
|
|
385
|
+
this.logger.debug(`recycling builder pid=${proc.pid} (${reason})`);
|
|
354
386
|
this.#recycleTimer = setTimeout(() => {
|
|
355
387
|
this.#recycleTimer = null;
|
|
356
388
|
if (this.#proc !== proc)
|
|
@@ -422,7 +454,10 @@ class IncrementalBuilderHost {
|
|
|
422
454
|
this.ready = false;
|
|
423
455
|
this.#status = "stopped";
|
|
424
456
|
}
|
|
425
|
-
static async create(app, env, onMessage
|
|
457
|
+
static async create(app, env, onMessage, {
|
|
458
|
+
stdio = "inherit",
|
|
459
|
+
onOutput
|
|
460
|
+
} = {}) {
|
|
426
461
|
const candidates = [
|
|
427
462
|
path.join(app.workspace.workspaceRoot, "pkgs/@akanjs/devkit/incrementalBuilder/incrementalBuilder.proc.ts"),
|
|
428
463
|
path.join(app.workspace.workspaceRoot, "node_modules/@akanjs/devkit/incrementalBuilder/incrementalBuilder.proc.ts"),
|
|
@@ -431,7 +466,7 @@ class IncrementalBuilderHost {
|
|
|
431
466
|
];
|
|
432
467
|
for (const c of candidates)
|
|
433
468
|
if (await Bun.file(c).exists())
|
|
434
|
-
return new IncrementalBuilderHost({ app, entry: c, env, onMessage });
|
|
469
|
+
return new IncrementalBuilderHost({ app, entry: c, env, stdio, onMessage, onOutput });
|
|
435
470
|
throw new Error(`[cli] frontend builder entry not found; looked in: ${candidates.join(", ")}`);
|
|
436
471
|
}
|
|
437
472
|
}
|
|
@@ -470,38 +505,23 @@ class BuilderRequestRouter {
|
|
|
470
505
|
}
|
|
471
506
|
}
|
|
472
507
|
|
|
473
|
-
// pkgs/@akanjs/devkit/akanApp/
|
|
474
|
-
|
|
475
|
-
|
|
476
|
-
|
|
477
|
-
|
|
478
|
-
|
|
508
|
+
// pkgs/@akanjs/devkit/akanApp/BackendImportGraph.ts
|
|
509
|
+
import { stat } from "fs/promises";
|
|
510
|
+
import path3 from "path";
|
|
511
|
+
|
|
512
|
+
// pkgs/@akanjs/devkit/akanApp/devHostPolicy.ts
|
|
513
|
+
import path2 from "path";
|
|
479
514
|
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
515
|
var HELD_BUILDER_REQUEST_LIMIT = 64;
|
|
484
|
-
var BUILDER_RSS_RECYCLE_QUIET_MS = 750;
|
|
485
516
|
var BUILDER_MIN_RSS_RECYCLE_INTERVAL_MS = 30000;
|
|
486
517
|
var BUILDER_TIGHT_RSS_REPORT_LIMIT = 2;
|
|
487
|
-
var BUILDER_RSS_SETTLE_MS = 20000;
|
|
488
|
-
var PS_RSS_TIMEOUT_MS = 2000;
|
|
489
518
|
var BUILDER_RSS_HARD_MULTIPLE = 1.5;
|
|
490
519
|
var DEV_IDLE_SUSPEND_MS = 300000;
|
|
491
520
|
var DEV_IDLE_MIN_UPTIME_MS = 30000;
|
|
492
|
-
var BUILDER_RECOVERY_BASE_DELAY_MS = 2000;
|
|
493
|
-
var BUILDER_RECOVERY_MAX_DELAY_MS = 60000;
|
|
494
521
|
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
522
|
var SERVER_SUFFIXES = [".service.ts", ".document.ts"];
|
|
497
523
|
var SHARED_SUFFIXES = [".constant.ts", ".dictionary.ts", ".signal.ts"];
|
|
498
524
|
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
525
|
var shouldRestartBackendByDevPlan = (message) => {
|
|
506
526
|
if (!message.devPlan)
|
|
507
527
|
return null;
|
|
@@ -515,6 +535,15 @@ var shouldRestartBuilderByDevPlan = (message) => message.devPlan?.actions.includ
|
|
|
515
535
|
var shouldAbandonBackendRecovery = (attempts, maxAttempts = BACKEND_RECOVERY_MAX_ATTEMPTS) => attempts >= maxAttempts;
|
|
516
536
|
var normalizeBackendReportedGeneration = (generation) => generation >= 0 ? generation : undefined;
|
|
517
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];
|
|
518
547
|
var RESTART_ROLE_ORDER = ["server", "shared", "barrel", "config"];
|
|
519
548
|
var generationValue = (generation) => generation ?? -1;
|
|
520
549
|
var isLegacyBackendFallbackFile = (file, workspaceRoot) => {
|
|
@@ -682,6 +711,15 @@ var mergeInvalidateMessages = (current, next) => {
|
|
|
682
711
|
var buildStatusReplaySequence = (pendingReplay, latestByPhase) => [...pendingReplay, ...latestByPhase.values()];
|
|
683
712
|
var filesChangedSince = (before, after) => [...before].filter(([file, stamp]) => after.get(file) !== stamp).map(([file]) => file);
|
|
684
713
|
|
|
714
|
+
// pkgs/@akanjs/devkit/akanApp/BackendImportGraph.ts
|
|
715
|
+
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;
|
|
716
|
+
var GRAPH_IMPORT_KINDS = new Set([
|
|
717
|
+
"import-statement",
|
|
718
|
+
"require-call",
|
|
719
|
+
"require-resolve",
|
|
720
|
+
"dynamic-import"
|
|
721
|
+
]);
|
|
722
|
+
|
|
685
723
|
class BackendImportGraph {
|
|
686
724
|
#app;
|
|
687
725
|
#logger;
|
|
@@ -704,7 +742,7 @@ class BackendImportGraph {
|
|
|
704
742
|
return this.#lastRefreshSucceeded;
|
|
705
743
|
}
|
|
706
744
|
has(file) {
|
|
707
|
-
return this.#files.has(
|
|
745
|
+
return this.#files.has(path3.resolve(file));
|
|
708
746
|
}
|
|
709
747
|
async fingerprint() {
|
|
710
748
|
const stamps = await Promise.all([...this.#files].map(async (file) => {
|
|
@@ -732,9 +770,9 @@ class BackendImportGraph {
|
|
|
732
770
|
const roots = await this.#entrypoints();
|
|
733
771
|
const files = new Set;
|
|
734
772
|
const queue = [...roots];
|
|
735
|
-
const workspaceRoot =
|
|
773
|
+
const workspaceRoot = path3.resolve(this.#app.workspace.workspaceRoot);
|
|
736
774
|
while (queue.length > 0) {
|
|
737
|
-
const current =
|
|
775
|
+
const current = path3.resolve(queue.pop());
|
|
738
776
|
if (files.has(current))
|
|
739
777
|
continue;
|
|
740
778
|
if (!this.#isWorkspaceSource(current, workspaceRoot))
|
|
@@ -743,7 +781,7 @@ class BackendImportGraph {
|
|
|
743
781
|
if (!imports)
|
|
744
782
|
continue;
|
|
745
783
|
files.add(current);
|
|
746
|
-
const importerDir =
|
|
784
|
+
const importerDir = path3.dirname(current);
|
|
747
785
|
for (const imp of imports) {
|
|
748
786
|
if (!GRAPH_IMPORT_KINDS.has(imp.kind) || !imp.path || NON_SOURCE_EXT_RE.test(imp.path))
|
|
749
787
|
continue;
|
|
@@ -774,7 +812,7 @@ class BackendImportGraph {
|
|
|
774
812
|
const roots = [`${this.#app.cwdPath}/main.ts`, `${this.#app.cwdPath}/server.ts`];
|
|
775
813
|
const existing = [];
|
|
776
814
|
for (const root of roots) {
|
|
777
|
-
const abs =
|
|
815
|
+
const abs = path3.resolve(root);
|
|
778
816
|
if (await Bun.file(abs).exists())
|
|
779
817
|
existing.push(abs);
|
|
780
818
|
}
|
|
@@ -783,25 +821,25 @@ class BackendImportGraph {
|
|
|
783
821
|
#resolve(specifier, importerDir) {
|
|
784
822
|
try {
|
|
785
823
|
const resolved = Bun.resolveSync(specifier, importerDir);
|
|
786
|
-
if (!
|
|
824
|
+
if (!path3.isAbsolute(resolved))
|
|
787
825
|
return null;
|
|
788
|
-
if (!SOURCE_EXTS.has(
|
|
826
|
+
if (!SOURCE_EXTS.has(path3.extname(resolved).toLowerCase()))
|
|
789
827
|
return null;
|
|
790
|
-
return
|
|
828
|
+
return path3.resolve(resolved);
|
|
791
829
|
} catch {
|
|
792
830
|
return null;
|
|
793
831
|
}
|
|
794
832
|
}
|
|
795
833
|
#isWorkspaceSource(file, workspaceRoot) {
|
|
796
|
-
const rel =
|
|
797
|
-
if (rel.startsWith("..") ||
|
|
834
|
+
const rel = path3.relative(workspaceRoot, file);
|
|
835
|
+
if (rel.startsWith("..") || path3.isAbsolute(rel))
|
|
798
836
|
return false;
|
|
799
|
-
if (rel.includes(`${
|
|
837
|
+
if (rel.includes(`${path3.sep}node_modules${path3.sep}`) || rel.includes(`${path3.sep}.akan${path3.sep}`))
|
|
800
838
|
return false;
|
|
801
|
-
return SOURCE_EXTS.has(
|
|
839
|
+
return SOURCE_EXTS.has(path3.extname(file).toLowerCase());
|
|
802
840
|
}
|
|
803
841
|
#scanImports(file, source) {
|
|
804
|
-
const ext =
|
|
842
|
+
const ext = path3.extname(file).toLowerCase();
|
|
805
843
|
if (ext === ".tsx")
|
|
806
844
|
return this.#tsxTranspiler.scanImports(source);
|
|
807
845
|
if (ext === ".jsx")
|
|
@@ -812,11 +850,28 @@ class BackendImportGraph {
|
|
|
812
850
|
}
|
|
813
851
|
}
|
|
814
852
|
|
|
853
|
+
// pkgs/@akanjs/devkit/akanApp/akanApp.host.ts
|
|
854
|
+
var backendMsgTypeSet = new Set(["build-route", "build-csr"]);
|
|
855
|
+
var BACKEND_RESTART_DEBOUNCE_MS = 120;
|
|
856
|
+
var BACKEND_GRACEFUL_TIMEOUT_MS = 8000;
|
|
857
|
+
var BACKEND_RECOVERY_BASE_DELAY_MS = 1000;
|
|
858
|
+
var BACKEND_RECOVERY_MAX_DELAY_MS = 30000;
|
|
859
|
+
var BACKEND_STDERR_TAIL_LIMIT = 40;
|
|
860
|
+
var BUILDER_READY_TIMEOUT_MS = 150000;
|
|
861
|
+
var BUILDER_START_MAX_ATTEMPTS = 3;
|
|
862
|
+
var BUILDER_RSS_RECYCLE_QUIET_MS = 750;
|
|
863
|
+
var BUILDER_RSS_SETTLE_MS = 20000;
|
|
864
|
+
var PS_RSS_TIMEOUT_MS = 2000;
|
|
865
|
+
var BUILDER_RECOVERY_BASE_DELAY_MS = 2000;
|
|
866
|
+
var BUILDER_RECOVERY_MAX_DELAY_MS = 60000;
|
|
867
|
+
|
|
815
868
|
class AkanAppHost {
|
|
816
869
|
app;
|
|
817
870
|
logger = new Logger3("AkanAppHost");
|
|
818
|
-
|
|
871
|
+
stdio;
|
|
819
872
|
env;
|
|
873
|
+
#onDevEvent;
|
|
874
|
+
#lastDevState = null;
|
|
820
875
|
#backend = null;
|
|
821
876
|
#builder = null;
|
|
822
877
|
#backendReady = false;
|
|
@@ -855,12 +910,23 @@ class AkanAppHost {
|
|
|
855
910
|
#builderGapStamp = null;
|
|
856
911
|
#pendingBuilderMessages = [];
|
|
857
912
|
#builderRequests = new BuilderRequestRouter;
|
|
858
|
-
constructor(app, {
|
|
913
|
+
constructor(app, {
|
|
914
|
+
env,
|
|
915
|
+
stdio = "inherit",
|
|
916
|
+
onDevEvent
|
|
917
|
+
}) {
|
|
859
918
|
this.app = app;
|
|
860
919
|
this.env = env;
|
|
861
|
-
this.
|
|
920
|
+
this.stdio = stdio;
|
|
921
|
+
this.#onDevEvent = onDevEvent ?? null;
|
|
862
922
|
this.#backendGraph = new BackendImportGraph(app, this.logger);
|
|
863
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
|
+
}
|
|
864
930
|
async start() {
|
|
865
931
|
if (this.#backend)
|
|
866
932
|
await this.#stopBackend();
|
|
@@ -907,6 +973,7 @@ class AkanAppHost {
|
|
|
907
973
|
}
|
|
908
974
|
#startBackend(startStatus = null) {
|
|
909
975
|
this.#builderRequests.startGeneration();
|
|
976
|
+
this.#discardPendingBuilderMessages("the backend restarted while the builder was away");
|
|
910
977
|
this.#backendStartStatus = startStatus;
|
|
911
978
|
this.#backendGaveUp = false;
|
|
912
979
|
this.#setBackendLifecycleState("starting");
|
|
@@ -914,7 +981,7 @@ class AkanAppHost {
|
|
|
914
981
|
this.#backendStderrTail = [];
|
|
915
982
|
const backend = Bun.spawn(["bun", `apps/${this.app.name}/main.ts`], {
|
|
916
983
|
cwd: this.app.workspace.workspaceRoot,
|
|
917
|
-
stdio: this.
|
|
984
|
+
stdio: this.stdio === "pipe" ? ["ignore", "pipe", "pipe"] : ["inherit", "inherit", "inherit"],
|
|
918
985
|
env: this.env,
|
|
919
986
|
ipc: (msg) => {
|
|
920
987
|
if (!msg || typeof msg !== "object")
|
|
@@ -955,7 +1022,7 @@ class AkanAppHost {
|
|
|
955
1022
|
});
|
|
956
1023
|
this.#backend = backend;
|
|
957
1024
|
this.logger.verbose(`backend spawned pid=${backend.pid}`);
|
|
958
|
-
if (this.
|
|
1025
|
+
if (this.stdio === "pipe") {
|
|
959
1026
|
this.#forwardBackendStream(backend.stderr, "stderr");
|
|
960
1027
|
this.#forwardBackendStream(backend.stdout, "stdout");
|
|
961
1028
|
}
|
|
@@ -976,14 +1043,13 @@ class AkanAppHost {
|
|
|
976
1043
|
try {
|
|
977
1044
|
for await (const chunk of stream) {
|
|
978
1045
|
const text = decoder.decode(chunk, { stream: true });
|
|
979
|
-
if (!text
|
|
1046
|
+
if (!text)
|
|
980
1047
|
continue;
|
|
981
1048
|
if (kind === "stderr") {
|
|
982
1049
|
this.#recordBackendStderr(text);
|
|
983
|
-
|
|
984
|
-
} else
|
|
985
|
-
|
|
986
|
-
}
|
|
1050
|
+
process.stderr.write(text);
|
|
1051
|
+
} else
|
|
1052
|
+
process.stdout.write(text);
|
|
987
1053
|
}
|
|
988
1054
|
} catch {}
|
|
989
1055
|
}
|
|
@@ -1030,6 +1096,7 @@ class AkanAppHost {
|
|
|
1030
1096
|
const prev = this.#backendLifecycleState;
|
|
1031
1097
|
this.#backendLifecycleState = next;
|
|
1032
1098
|
this.logger.verbose(`[backend-lifecycle] ${prev} -> ${next}${detail ? ` ${detail}` : ""}`);
|
|
1099
|
+
this.#emitDevEvent(devHostStateOf(next, this.#backendGaveUp), detail);
|
|
1033
1100
|
}
|
|
1034
1101
|
#sendToBackend(message) {
|
|
1035
1102
|
if (!this.#backend || !this.#backendReady) {
|
|
@@ -1359,6 +1426,7 @@ ${this.#backendStderrTail.join(`
|
|
|
1359
1426
|
return;
|
|
1360
1427
|
}
|
|
1361
1428
|
this.#suspended = true;
|
|
1429
|
+
this.#emitDevEvent("suspended", `idle ${Math.round(idleMs / 1000)}s`);
|
|
1362
1430
|
this.#stopBuilder();
|
|
1363
1431
|
this.#openBuilderGap("idle suspend");
|
|
1364
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`);
|
|
@@ -1423,6 +1491,7 @@ ${this.#backendStderrTail.join(`
|
|
|
1423
1491
|
this.#wokeAtMono = performance.now();
|
|
1424
1492
|
this.#flushPendingBuilderMessages();
|
|
1425
1493
|
this.#armIdleSuspend();
|
|
1494
|
+
this.#emitDevEvent(devHostStateOf(this.#backendLifecycleState, this.#backendGaveUp));
|
|
1426
1495
|
}
|
|
1427
1496
|
}
|
|
1428
1497
|
async#applyIdleWake(batch) {
|
|
@@ -1498,6 +1567,12 @@ ${this.#backendStderrTail.join(`
|
|
|
1498
1567
|
this.#pendingBuilderMessages.push(message);
|
|
1499
1568
|
this.logger.verbose(`[builder] holding ${message.type} until the builder is ready (${this.#pendingBuilderMessages.length} waiting)`);
|
|
1500
1569
|
}
|
|
1570
|
+
#discardPendingBuilderMessages(reason) {
|
|
1571
|
+
const held = this.#pendingBuilderMessages.splice(0);
|
|
1572
|
+
if (held.length === 0)
|
|
1573
|
+
return;
|
|
1574
|
+
this.logger.verbose(`[builder] dropped ${held.length} held builder request(s): ${reason}`);
|
|
1575
|
+
}
|
|
1501
1576
|
#failPendingBuilderMessages(reason) {
|
|
1502
1577
|
const held = this.#pendingBuilderMessages.splice(0);
|
|
1503
1578
|
if (held.length === 0)
|
|
@@ -1753,6 +1828,9 @@ ${this.#backendStderrTail.join(`
|
|
|
1753
1828
|
for (let attempt = 1;attempt <= BUILDER_START_MAX_ATTEMPTS; attempt++) {
|
|
1754
1829
|
this.#builder = await IncrementalBuilderHost.create(this.app, this.env, (msg) => {
|
|
1755
1830
|
this.#enqueueBuilderMessage(msg);
|
|
1831
|
+
}, {
|
|
1832
|
+
stdio: this.stdio,
|
|
1833
|
+
onOutput: (kind, text) => void (kind === "stderr" ? process.stderr : process.stdout).write(text)
|
|
1756
1834
|
});
|
|
1757
1835
|
try {
|
|
1758
1836
|
await this.#waitForBuilderReady(attempt, { announceBootState });
|
|
@@ -1858,20 +1936,20 @@ ${this.#backendStderrTail.join(`
|
|
|
1858
1936
|
}
|
|
1859
1937
|
}
|
|
1860
1938
|
// pkgs/@akanjs/devkit/applicationTestPreload.ts
|
|
1861
|
-
import
|
|
1939
|
+
import path4 from "path";
|
|
1862
1940
|
var SIGNAL_TEST_PRELOAD_PATH = "test/signalTest.preload.ts";
|
|
1863
1941
|
async function resolveSignalTestPreloadPath(target) {
|
|
1864
1942
|
const candidates = [];
|
|
1865
1943
|
const addResolvedPackageCandidate = (basePath) => {
|
|
1866
1944
|
try {
|
|
1867
|
-
candidates.push(
|
|
1945
|
+
candidates.push(path4.join(path4.dirname(Bun.resolveSync("akanjs/package.json", basePath)), SIGNAL_TEST_PRELOAD_PATH));
|
|
1868
1946
|
} catch {}
|
|
1869
1947
|
};
|
|
1870
1948
|
addResolvedPackageCandidate(target.cwdPath);
|
|
1871
1949
|
addResolvedPackageCandidate(process.cwd());
|
|
1872
|
-
addResolvedPackageCandidate(
|
|
1950
|
+
addResolvedPackageCandidate(path4.dirname(Bun.main));
|
|
1873
1951
|
addResolvedPackageCandidate(import.meta.dir);
|
|
1874
|
-
candidates.push(
|
|
1952
|
+
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
1953
|
const uniqueCandidates = [...new Set(candidates)];
|
|
1876
1954
|
for (const candidate of uniqueCandidates) {
|
|
1877
1955
|
if (await Bun.file(candidate).exists())
|
|
@@ -1886,22 +1964,10 @@ ${uniqueCandidates.map((candidate) => ` - ${candidate}`).join(`
|
|
|
1886
1964
|
// pkgs/@akanjs/cli/application/application.runner.ts
|
|
1887
1965
|
import { Logger as Logger4 } from "akanjs/common";
|
|
1888
1966
|
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-ha3y8ew8.js")).ApplicationBuildRunner;
|
|
1902
|
-
var loadReleasePackager = async () => (await import("./applicationReleasePackager-gg8ccbeh.js")).ApplicationReleasePackager;
|
|
1903
|
-
var loadCapacitorApp = async () => (await import("./capacitorApp-9xswcgx7.js")).CapacitorApp;
|
|
1904
|
-
var loadAbstractCompactor = async () => (await import("./abstractCompactor-e5yq60qz.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;
|
|
1905
1971
|
var loadPrompts = async () => await import("@inquirer/prompts");
|
|
1906
1972
|
|
|
1907
1973
|
class ApplicationRunner extends runner("application") {
|
|
@@ -1914,8 +1980,11 @@ class ApplicationRunner extends runner("application") {
|
|
|
1914
1980
|
});
|
|
1915
1981
|
return AppExecutor.from(workspace, appName);
|
|
1916
1982
|
}
|
|
1983
|
+
async planSlice(app) {
|
|
1984
|
+
return await new SlicePlanner(app).plan();
|
|
1985
|
+
}
|
|
1917
1986
|
async removeApplication(app) {
|
|
1918
|
-
await app.workspace.
|
|
1987
|
+
await app.workspace.removeDir(`apps/${app.name}`);
|
|
1919
1988
|
}
|
|
1920
1989
|
async getConfig(app) {
|
|
1921
1990
|
return await app.getConfig();
|
|
@@ -1945,10 +2014,69 @@ class ApplicationRunner extends runner("application") {
|
|
|
1945
2014
|
stdio: "inherit"
|
|
1946
2015
|
});
|
|
1947
2016
|
}
|
|
2017
|
+
#runtimeDirOf(app, override) {
|
|
2018
|
+
return path5.resolve(override ?? process.env.AKAN_RUNTIME_DIR ?? path5.join(app.workspace.workspaceRoot, "local", "apps", app.name, "runtime"));
|
|
2019
|
+
}
|
|
2020
|
+
async runLogs(app, options) {
|
|
2021
|
+
const { LogControlUnavailableError, LogTailClient } = await import("akanjs/server/logging/logTailClient");
|
|
2022
|
+
const { LogQueryMatcher } = await import("akanjs/server/logging/logQuery");
|
|
2023
|
+
const runtimeDir = this.#runtimeDirOf(app, options.runtimeDir);
|
|
2024
|
+
const socketPath = LogTailClient.socketPath(runtimeDir);
|
|
2025
|
+
const { level, grep, endpoint, trace, child, role, origin, since } = options;
|
|
2026
|
+
const query = Object.fromEntries(Object.entries({ level, grep, endpoint, trace, child, role, origin, since }).filter(([, value]) => value !== undefined && value !== null && value !== ""));
|
|
2027
|
+
const print = (record) => process.stdout.write(options.json ? `${JSON.stringify(record)}
|
|
2028
|
+
` : Logger4.render(record));
|
|
2029
|
+
const note = (text) => process.stderr.write(`[akan logs] ${text}
|
|
2030
|
+
`);
|
|
2031
|
+
let finish = null;
|
|
2032
|
+
const closed = new Promise((resolve) => {
|
|
2033
|
+
finish = resolve;
|
|
2034
|
+
});
|
|
2035
|
+
let client;
|
|
2036
|
+
try {
|
|
2037
|
+
client = await LogTailClient.connect(socketPath, {
|
|
2038
|
+
onRecord: (entry) => print(entry.record),
|
|
2039
|
+
onEvent: (response) => {
|
|
2040
|
+
if (response.type === "dropped")
|
|
2041
|
+
note(`${response.count} records dropped (reader too slow)`);
|
|
2042
|
+
},
|
|
2043
|
+
onClose: () => finish?.()
|
|
2044
|
+
});
|
|
2045
|
+
} catch (error) {
|
|
2046
|
+
if (error instanceof LogControlUnavailableError)
|
|
2047
|
+
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.`);
|
|
2048
|
+
throw error;
|
|
2049
|
+
}
|
|
2050
|
+
const sinceMs = since ? LogQueryMatcher.parseSince(since) : undefined;
|
|
2051
|
+
const noteCoverage = (coverage) => {
|
|
2052
|
+
if (sinceMs !== undefined && coverage.from !== null && sinceMs < coverage.from)
|
|
2053
|
+
note(`${LogTailClient.describeCoverage(coverage)}; older records are not retained`);
|
|
2054
|
+
};
|
|
2055
|
+
if (endpoint)
|
|
2056
|
+
note("requests served by the primitive query fast path carry no endpoint and are not shown");
|
|
2057
|
+
if (options.follow === false) {
|
|
2058
|
+
const { entries, coverage } = await client.history({
|
|
2059
|
+
...query,
|
|
2060
|
+
...options.replay ? { limit: options.replay } : {}
|
|
2061
|
+
});
|
|
2062
|
+
for (const entry of entries)
|
|
2063
|
+
print(entry.record);
|
|
2064
|
+
noteCoverage(coverage);
|
|
2065
|
+
client.close();
|
|
2066
|
+
return;
|
|
2067
|
+
}
|
|
2068
|
+
const subscribed = await client.subscribe(query, { replay: options.replay });
|
|
2069
|
+
noteCoverage(subscribed.coverage);
|
|
2070
|
+
const stop = () => client.close();
|
|
2071
|
+
process.once("SIGINT", stop);
|
|
2072
|
+
process.once("SIGTERM", stop);
|
|
2073
|
+
await closed;
|
|
2074
|
+
}
|
|
1948
2075
|
async runConsole(app) {
|
|
1949
2076
|
const serverPath = `${app.cwdPath}/server.ts`;
|
|
1950
2077
|
if (!await app.exists("server.ts"))
|
|
1951
2078
|
throw new Error(`Server file not found: apps/${app.name}/server.ts`);
|
|
2079
|
+
const runtimeDir = this.#runtimeDirOf(app);
|
|
1952
2080
|
const code = `
|
|
1953
2081
|
const serverModule = await import(${JSON.stringify(serverPath)});
|
|
1954
2082
|
const { assertAkanConsoleAllowed, startAkanConsole } = await import("akanjs/server");
|
|
@@ -1958,6 +2086,7 @@ assertAkanConsoleAllowed(server.env);
|
|
|
1958
2086
|
await server.start({ listen: false, web: false });
|
|
1959
2087
|
try {
|
|
1960
2088
|
await startAkanConsole(server, {
|
|
2089
|
+
runtimeDir: ${JSON.stringify(runtimeDir)},
|
|
1961
2090
|
globals: {
|
|
1962
2091
|
srv: serverModule.srv,
|
|
1963
2092
|
sig: serverModule.sig,
|
|
@@ -2005,9 +2134,14 @@ try {
|
|
|
2005
2134
|
} = {}) {
|
|
2006
2135
|
return new (await loadBuildRunner())(app, { fast, reporter }).build({ spinner });
|
|
2007
2136
|
}
|
|
2008
|
-
async start(app, {
|
|
2137
|
+
async start(app, {
|
|
2138
|
+
open = false,
|
|
2139
|
+
onStart,
|
|
2140
|
+
onDevEvent,
|
|
2141
|
+
stdio = "inherit"
|
|
2142
|
+
} = {}) {
|
|
2009
2143
|
const { env } = await app.prepareCommand("start");
|
|
2010
|
-
const appHost = await new AkanAppHost(app, { env,
|
|
2144
|
+
const appHost = await new AkanAppHost(app, { env, stdio, onDevEvent }).start();
|
|
2011
2145
|
onStart?.();
|
|
2012
2146
|
if (open)
|
|
2013
2147
|
setTimeout(() => openBrowser(`http://localhost:${env.AKAN_PUBLIC_CLIENT_PORT ?? env.PORT ?? "8282"}`), 3000);
|
|
@@ -2239,6 +2373,444 @@ ${detail}`
|
|
|
2239
2373
|
}
|
|
2240
2374
|
}
|
|
2241
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
|
+
|
|
2242
2814
|
// pkgs/@akanjs/cli/application/application.script.ts
|
|
2243
2815
|
class ApplicationScript extends script("application", [ApplicationRunner, LibraryScript]) {
|
|
2244
2816
|
static dbShutdownTimeoutMs = 20000;
|
|
@@ -2322,13 +2894,19 @@ class ApplicationScript extends script("application", [ApplicationRunner, Librar
|
|
|
2322
2894
|
spinner.succeed(`Application created in apps/${app.name}`);
|
|
2323
2895
|
await app.scanSync();
|
|
2324
2896
|
if (start)
|
|
2325
|
-
await this.
|
|
2897
|
+
await this.startOne(app, { open: true });
|
|
2326
2898
|
}
|
|
2327
2899
|
async removeApplication(app) {
|
|
2328
2900
|
const spinner = app.spinning("Removing application...");
|
|
2329
2901
|
await this.applicationRunner.removeApplication(app);
|
|
2330
2902
|
spinner.succeed(`Application ${app.name} (apps/${app.name}) removed`);
|
|
2331
2903
|
}
|
|
2904
|
+
async planSlice(app, format = "text") {
|
|
2905
|
+
const spinner = app.spinning("Planning distributable slice...");
|
|
2906
|
+
const plan = await this.applicationRunner.planSlice(app);
|
|
2907
|
+
spinner.succeed(`Slice planned (${plan.libs.length} libs, ${plan.warnings.length} warnings)`);
|
|
2908
|
+
Logger6.rawLog(format === "json" ? JSON.stringify(plan, null, 2) : formatSlicePlan(plan));
|
|
2909
|
+
}
|
|
2332
2910
|
async sync(sys) {
|
|
2333
2911
|
if (sys.type === "app")
|
|
2334
2912
|
await sys.scanSync();
|
|
@@ -2338,13 +2916,13 @@ class ApplicationScript extends script("application", [ApplicationRunner, Librar
|
|
|
2338
2916
|
async compact(sys, { module = null, minLines, interactive = false } = {}) {
|
|
2339
2917
|
const { scanned, reports } = await this.applicationRunner.compact(sys, { module, minLines, interactive });
|
|
2340
2918
|
if (!reports.length) {
|
|
2341
|
-
|
|
2919
|
+
Logger6.rawLog(`No abstract file long enough to compact in ${sys.name} (${scanned} scanned)`);
|
|
2342
2920
|
return;
|
|
2343
2921
|
}
|
|
2344
2922
|
for (const report of reports)
|
|
2345
|
-
|
|
2923
|
+
Logger6.rawLog(`${report.status}: ${report.path} (${report.beforeLines} -> ${report.afterLines} lines)`);
|
|
2346
2924
|
const compactedNum = reports.filter((report) => report.status === "compacted").length;
|
|
2347
|
-
|
|
2925
|
+
Logger6.rawLog(`Compacted ${compactedNum}/${reports.length} abstract files in ${sys.name}`);
|
|
2348
2926
|
}
|
|
2349
2927
|
async script(app, filename) {
|
|
2350
2928
|
const scriptFilename = filename ?? await this.applicationRunner.getScriptFilename(app);
|
|
@@ -2355,21 +2933,24 @@ class ApplicationScript extends script("application", [ApplicationRunner, Librar
|
|
|
2355
2933
|
await app.scanSync();
|
|
2356
2934
|
await this.applicationRunner.runConsole(app);
|
|
2357
2935
|
}
|
|
2936
|
+
async logs(app, options) {
|
|
2937
|
+
await this.applicationRunner.runLogs(app, options);
|
|
2938
|
+
}
|
|
2358
2939
|
async build(app, { write = true, fast = false, quiet = false } = {}) {
|
|
2359
2940
|
await app.scanSync({ write });
|
|
2360
2941
|
if (!quiet)
|
|
2361
|
-
|
|
2942
|
+
Logger6.rawLog(`Creating an optimized production build for ${app.name}...`);
|
|
2362
2943
|
try {
|
|
2363
2944
|
const result = await this.applicationRunner.build(app, {
|
|
2364
2945
|
fast,
|
|
2365
2946
|
spinner: !quiet
|
|
2366
2947
|
});
|
|
2367
|
-
|
|
2948
|
+
Logger6.rawLog(`${app.name} built in dist/apps/${app.name}`);
|
|
2368
2949
|
if (!quiet)
|
|
2369
2950
|
ApplicationBuildReporter.printSummary(result);
|
|
2370
2951
|
} catch (error) {
|
|
2371
|
-
|
|
2372
|
-
|
|
2952
|
+
Logger6.rawLog(`${app.name} build failed in dist/apps/${app.name}`, undefined, "error");
|
|
2953
|
+
Logger6.rawLog(ApplicationBuildReporter.formatError(error), undefined, "error");
|
|
2373
2954
|
throw error;
|
|
2374
2955
|
}
|
|
2375
2956
|
}
|
|
@@ -2381,15 +2962,15 @@ class ApplicationScript extends script("application", [ApplicationRunner, Librar
|
|
|
2381
2962
|
spinner.succeed(`${app.name} typechecked`);
|
|
2382
2963
|
} catch (error) {
|
|
2383
2964
|
spinner.fail(`${app.name} typecheck failed`);
|
|
2384
|
-
|
|
2965
|
+
Logger6.rawLog(ApplicationBuildReporter.formatError(error), undefined, "error");
|
|
2385
2966
|
throw error;
|
|
2386
2967
|
}
|
|
2387
2968
|
}
|
|
2388
2969
|
async test(exec, { write = true } = {}) {
|
|
2389
2970
|
if (exec instanceof LibExecutor) {
|
|
2390
2971
|
await this.libraryScript.syncLibrary(exec);
|
|
2391
|
-
const
|
|
2392
|
-
|
|
2972
|
+
const spinner = exec.spinning(`Preparing ${exec.name}...`);
|
|
2973
|
+
spinner.succeed(`${exec.name} prepared`);
|
|
2393
2974
|
await this.applicationRunner.test(exec);
|
|
2394
2975
|
return;
|
|
2395
2976
|
}
|
|
@@ -2406,12 +2987,20 @@ class ApplicationScript extends script("application", [ApplicationRunner, Librar
|
|
|
2406
2987
|
}
|
|
2407
2988
|
await this.applicationRunner.test(exec);
|
|
2408
2989
|
}
|
|
2409
|
-
async start(
|
|
2410
|
-
|
|
2411
|
-
|
|
2412
|
-
|
|
2413
|
-
|
|
2414
|
-
|
|
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 } = {}) {
|
|
2415
3004
|
await app.scanSync({ write });
|
|
2416
3005
|
const akanConfig = await app.getConfig();
|
|
2417
3006
|
const databaseMode = process.env.AKAN_DATABASE_MODE ?? akanConfig.defaultDatabaseMode ?? "single";
|
|
@@ -2427,10 +3016,55 @@ class ApplicationScript extends script("application", [ApplicationRunner, Librar
|
|
|
2427
3016
|
onStart: () => {
|
|
2428
3017
|
spinner.succeed(`${app.name} prepared, ready to start`);
|
|
2429
3018
|
},
|
|
2430
|
-
|
|
3019
|
+
onDevEvent,
|
|
3020
|
+
stdio
|
|
2431
3021
|
});
|
|
2432
3022
|
return akanAppHost;
|
|
2433
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
|
+
}
|
|
2434
3068
|
async buildIos(app, { write = true, target, env = "debug", regenerate = false } = {}) {
|
|
2435
3069
|
await app.scanSync({ write });
|
|
2436
3070
|
await this.applicationRunner.buildIos(app, { target, env, regenerate });
|
|
@@ -2528,7 +3162,7 @@ class ApplicationScript extends script("application", [ApplicationRunner, Librar
|
|
|
2528
3162
|
let stopping = false;
|
|
2529
3163
|
process.on("SIGINT", () => {
|
|
2530
3164
|
if (stopping) {
|
|
2531
|
-
|
|
3165
|
+
Logger6.rawLog("Abandoning the local database teardown; containers are left running.", undefined, "error");
|
|
2532
3166
|
process.exit(130);
|
|
2533
3167
|
}
|
|
2534
3168
|
stopping = true;
|
|
@@ -2536,24 +3170,21 @@ class ApplicationScript extends script("application", [ApplicationRunner, Librar
|
|
|
2536
3170
|
});
|
|
2537
3171
|
}
|
|
2538
3172
|
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`);
|
|
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
|
+
}
|
|
2557
3188
|
}
|
|
2558
3189
|
}
|
|
2559
3190
|
|