@akanjs/cli 3.0.0-beta.9 → 3.0.1
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/CodeAgent-zfab7pkn.js +9 -0
- package/CodeAgentRpcHost-p7h6n2hp.js +7 -0
- package/CodeAgentRpcListener-7p7z5dd7.js +97 -0
- package/CodeTui-0zhnr0pw.js +1457 -0
- package/README.ko.md +83 -28
- package/README.md +70 -27
- package/TunnelShare-cwh9ezsm.js +9 -0
- package/{agent.command-0q17pnrs.js → agent.command-aja6z9mn.js} +8 -9
- package/akanCode.js +102 -0
- package/application.command-049ehgx3.js +246 -0
- package/{applicationBuildRunner-kxwbssgn.js → applicationBuildRunner-75n5rpwp.js} +121 -77
- package/buildBatch.proc.js +120 -32
- package/cloud.command-gw4kqxt8.js +95 -0
- package/cloud.script-c723cpcb.js +20 -0
- package/code.command-x9t6z5nw.js +154 -0
- package/commandManifest.json +1 -1
- package/consoleToStderr-h5bryj94.js +7 -0
- package/context.command-yfbe1rtd.js +48 -0
- package/context.runner-erfe68j5.js +21 -0
- package/{dependencyScanner-shs82ynp.js → dependencyScanner-06z2dmrr.js} +2 -2
- package/{devTui-m1atgkc9.js → devTui-q7k7bakd.js} +122 -40
- package/fileSys-wzzrntkw.js +9 -0
- package/guideline.command-kq9gp690.js +42 -0
- package/guidelines/agentRule/agentRule.instruction.md +154 -26
- package/guidelines/conventions/conventions.instruction.md +138 -48
- package/guidelines/mcpRule/mcpRule.instruction.md +7 -5
- package/guidelines/queryRule/queryRule.instruction.md +21 -7
- package/guidelines/runtimeRule/runtimeRule.instruction.md +291 -6
- package/guidelines/ssrRule/ssrRule.instruction.md +4 -2
- package/guidelines/transportRule/transportRule.instruction.md +10 -8
- package/guidelines/workspaceOnboarding/workspaceOnboarding.instruction.md +20 -0
- package/guidelines/workspaceRecipes/workspaceRecipes.instruction.md +66 -3
- package/incrementalBuilder.proc.js +564 -109
- package/{index-2tv3fdx4.js → index-150s07n8.js} +1230 -1459
- package/{index-vyvre9en.js → index-25fvrxmy.js} +6 -2
- package/{index-kcxmv48c.js → index-2ahj896a.js} +45 -79
- package/index-2wc41k5w.js +49 -0
- package/index-3hx1z96c.js +87 -0
- package/{index-q0y2jq9j.js → index-52w0fvh8.js} +21 -4
- package/{index-04bk599g.js → index-5g4rkrvt.js} +10 -17
- package/index-5pvxf7z0.js +53 -0
- package/index-5q14pwzd.js +69 -0
- package/{index-vxtad5bd.js → index-66yspb67.js} +5 -8
- package/index-7md2g9vw.js +6 -0
- package/index-7p4c0yer.js +1058 -0
- package/{index-d43r08tf.js → index-8b639zes.js} +77 -150
- package/{index-ktb5njgy.js → index-8kvy8qb4.js} +49 -99
- package/index-94s080qm.js +266 -0
- package/index-9dp031by.js +12 -0
- package/{index-0g7r8xz3.js → index-a37by9hn.js} +114 -232
- package/{index-k2j1hc4r.js → index-b95yhqjr.js} +12 -12
- package/{index-3d9y066c.js → index-bjwxs904.js} +9 -8
- package/{index-2xp0kfkt.js → index-br8pkrk1.js} +37 -25
- package/index-c14nsvnv.js +700 -0
- package/index-c6eggbee.js +123 -0
- package/index-d2242ksy.js +99 -0
- package/{index-92zztqy1.js → index-dy0jx9ay.js} +2 -17
- package/{index-a64esmtb.js → index-fkvb79qe.js} +523 -1022
- package/index-h54yj9wr.js +1119 -0
- package/{index-by5gpp8x.js → index-hj6w4ka1.js} +2 -1
- package/{index-y6n8nmny.js → index-kn5d4jmg.js} +75 -203
- package/index-m3xsb1ms.js +4300 -0
- package/{index-gyd220p2.js → index-mb59b8x0.js} +554 -577
- package/{index-j9yhg404.js → index-mzjb6zp4.js} +4 -7
- package/index-p6pbz9n5.js +25 -0
- package/{index-sj5vxr8f.js → index-qe71g4ze.js} +983 -898
- package/index-rfcz04w9.js +187 -0
- package/{index-hwkygn9s.js → index-senscydg.js} +331 -36
- package/{index-ffvk10zz.js → index-vs8mefhy.js} +107 -241
- package/index-wa5jbxj1.js +760 -0
- package/index-y5ga5w9t.js +2729 -0
- package/{index-9077wzd2.js → index-y7hx1ygz.js} +43 -26
- package/{index-h7yw2z9n.js → index-zzvvc845.js} +8 -14
- package/index.js +23 -22
- package/{library.command-qzzbj6d8.js → library.command-keqjevpp.js} +4 -5
- package/{localRegistry.command-vyrgg4xw.js → localRegistry.command-pydeq5zf.js} +23 -43
- package/{module.command-rq5hq3ax.js → module.command-dkrnqx3v.js} +8 -12
- package/{package.command-d9hd6v9n.js → package.command-wmc0ys5y.js} +6 -7
- package/package.json +15 -7
- package/{page.command-126d56te.js → page.command-tf3erhrv.js} +4 -5
- package/{primitive.command-taxd1538.js → primitive.command-7hqpegj4.js} +9 -23
- package/prompter-76k56b37.js +8 -0
- package/{quality.command-6gsna0a3.js → quality.command-8122axq3.js} +205 -377
- package/{recipeScanner-fpagtgc1.js → recipeScanner-4b69ybe0.js} +1 -1
- package/{repair.command-tq2dgy72.js → repair.command-mcy12h0n.js} +11 -35
- package/{routeSourceValidator-apyqw7wk.js → routeSourceValidator-7ycnfz0e.js} +0 -6
- package/{scalar.command-3bcmrrkv.js → scalar.command-vvvbzttc.js} +7 -11
- package/skills/akan-code/SKILL.md +130 -0
- package/skills/akan-debug/SKILL.md +42 -0
- package/skills/akan-deps/SKILL.md +50 -0
- package/skills/akan-err/SKILL.md +39 -0
- package/skills/akan-module/SKILL.md +45 -0
- package/skills/akan-scaffold/SKILL.md +76 -0
- package/skills/akan-scalar/SKILL.md +52 -0
- package/skills/akan-service/SKILL.md +34 -0
- package/skills/akan-store/SKILL.md +67 -0
- package/skills/akan-ui/SKILL.md +63 -0
- package/skills/akan-validate/SKILL.md +44 -0
- package/{subspace.command-eddzqpx3.js → subspace.command-pzvqfy5h.js} +53 -62
- package/templates/__scalar/__model__/__model__.abstract.md.template +8 -0
- package/templates/app/page/_index.tsx +257 -258
- package/templates/app/page/_layout.tsx +5 -1
- package/templates/app/page/_overrides.tsx +12 -0
- package/templates/app/page/styles.css.template +316 -14
- package/templates/app/public/apple-touch-icon.png +0 -0
- package/templates/app/public/favicon.ico +0 -0
- package/templates/app/public/jelly/cloud.webp +0 -0
- package/templates/app/public/jelly/comet.webp +0 -0
- package/templates/app/public/jelly/moon.webp +0 -0
- package/templates/app/public/jelly/planet.webp +0 -0
- package/templates/app/public/jelly/rocket.webp +0 -0
- package/templates/app/public/jelly/star.webp +0 -0
- package/templates/app/public/jelly/sugar.webp +0 -0
- package/templates/app/public/logo.png +0 -0
- package/templates/app/ui/Recipe/index.ts +9 -0
- package/templates/app/ui/Recipe/jellyButton.ts +63 -0
- package/templates/appSample/lib/__scalar/workHistory/workHistory.abstract.ts +8 -39
- package/templates/appSample/lib/_noti/noti.abstract.ts +6 -29
- package/templates/appSample/lib/task/Task.Unit.tsx +1 -1
- package/templates/appSample/lib/task/Task.Util.tsx +10 -13
- package/templates/appSample/lib/task/Task.View.tsx +2 -2
- package/templates/appSample/lib/task/Task.Zone.tsx +5 -2
- package/templates/appSample/lib/task/task.abstract.ts +12 -32
- package/templates/appSample/lib/task/task.signal.spec.ts +1 -15
- package/templates/appSample/lib/task/task.signal.test.ts +15 -1
- package/templates/appSample/lib/task/task.signal.ts +10 -8
- package/templates/appSample/page/task/[taskId]/_index.tsx +6 -6
- package/templates/appSample/page/task/[taskId]/edit.tsx +5 -4
- package/templates/appSample/page/task/_index.tsx +4 -3
- package/templates/appSample/page/task/_layout.tsx +1 -1
- package/templates/appSample/page/task/new.tsx +5 -4
- package/templates/appSample/srvkit/{AuthGuard.ts → guards.ts} +7 -4
- package/templates/appSample/srvkit/{SessionInternalArg.ts → internalArgs.ts} +6 -4
- package/templates/appSample/ui/GlobalLoading.tsx +2 -1
- package/templates/appSample/ui/QuantityControl.tsx +3 -3
- package/templates/client.ts +7 -30
- package/templates/crudPages/[__model__Id]/edit/page.tsx +2 -1
- package/templates/crudPages/[__model__Id]/page.tsx +2 -1
- package/templates/crudPages/new/page.tsx +2 -1
- package/templates/crudPages/page.tsx +2 -1
- package/templates/crudSinglePage/page.tsx +2 -1
- package/templates/lib/__lib/lib.dictionary.ts +0 -2
- package/templates/lib/__lib/lib.document.ts +0 -2
- package/templates/lib/sig.ts +8 -30
- package/templates/localDev/docker-compose.yaml.template +3 -14
- package/templates/module/__model__.signal.ts +12 -4
- package/templates/pkgRoot/tsconfig.json.template +2 -7
- package/templates/server.ts +1 -3
- package/templates/service/__model__.abstract.md.template +8 -0
- package/templates/workspaceRoot/.gitignore.template +0 -1
- package/templates/workspaceRoot/AGENTS.md.template +6 -0
- package/templates/workspaceRoot/package.json.template +1 -3
- package/tunnel.command-82zz5xnq.js +129 -0
- package/{typeChecker-3xgrzc2v.js → typeChecker-px49mezc.js} +1 -1
- package/typecheck.proc.js +1 -1
- package/workflow.command-y8x3kgjv.js +79 -0
- package/{workspace.command-06wvstgr.js → workspace.command-jvsb7brc.js} +79 -109
- package/abstractCompactor-gmkc0z8j.js +0 -102
- package/application.command-18y0s485.js +0 -204
- package/applicationReleasePackager-4yasxwv9.js +0 -244
- package/capacitorApp-36rbr858.js +0 -12
- package/cloud.command-av97ndh6.js +0 -94
- package/cloud.script-cm11xjdz.js +0 -20
- package/context.command-5v37ef3n.js +0 -81
- package/devTuiView-0hc2emhe.js +0 -9
- package/getDirname-dr3jz1xg.js +0 -7
- package/guideline.command-4pz9g1rt.js +0 -360
- package/guidelines/agentRule/agentRule.generate.json +0 -31
- package/guidelines/componentRule/componentRule.generate.json +0 -31
- package/guidelines/conventions/conventions.generate.json +0 -25
- package/guidelines/cssRule/cssRule.generate.json +0 -31
- package/guidelines/docPageRule/docPageRule.generate.json +0 -31
- package/guidelines/docPageRule/docPageRule.instruction.md +0 -25
- package/guidelines/docSyncRule/docSyncRule.generate.json +0 -31
- package/guidelines/docSyncRule/docSyncRule.instruction.md +0 -25
- package/guidelines/enumConstant/enumConstant.generate.json +0 -31
- package/guidelines/enumConstant/enumConstant.instruction.md +0 -28
- package/guidelines/fieldRule/fieldRule.generate.json +0 -31
- package/guidelines/framework/framework.generate.json +0 -32
- package/guidelines/mcpRule/mcpRule.generate.json +0 -35
- package/guidelines/modelConstant/modelConstant.generate.json +0 -32
- package/guidelines/modelDictionary/modelDictionary.generate.json +0 -32
- package/guidelines/modelDocument/modelDocument.generate.json +0 -31
- package/guidelines/modelService/modelService.generate.json +0 -31
- package/guidelines/modelSignal/modelSignal.generate.json +0 -31
- package/guidelines/modelStore/modelStore.generate.json +0 -31
- package/guidelines/modelTemplate/modelTemplate.generate.json +0 -31
- package/guidelines/modelUnit/modelUnit.generate.json +0 -31
- package/guidelines/modelUtil/modelUtil.generate.json +0 -31
- package/guidelines/modelView/modelView.generate.json +0 -31
- package/guidelines/modelZone/modelZone.generate.json +0 -31
- package/guidelines/moduleCodegen/moduleCodegen.generate.json +0 -31
- package/guidelines/moduleCodegen/moduleCodegen.instruction.md +0 -28
- package/guidelines/moduleOverview/moduleOverview.generate.json +0 -31
- package/guidelines/queryRule/queryRule.generate.json +0 -31
- package/guidelines/recipeRule/recipeRule.generate.json +0 -31
- package/guidelines/runtimeRule/runtimeRule.generate.json +0 -25
- package/guidelines/scalarConstant/scalarConstant.generate.json +0 -32
- package/guidelines/scalarDictionary/scalarDictionary.generate.json +0 -32
- package/guidelines/scalarModule/scalarModule.generate.json +0 -31
- package/guidelines/sharedUiUsage/sharedUiUsage.generate.json +0 -31
- package/guidelines/sharedUiUsage/sharedUiUsage.instruction.md +0 -25
- package/guidelines/ssrRule/ssrRule.generate.json +0 -31
- package/guidelines/transportRule/transportRule.generate.json +0 -31
- package/guidelines/utilUiUsage/utilUiUsage.generate.json +0 -31
- package/guidelines/utilUiUsage/utilUiUsage.instruction.md +0 -25
- package/guidelines/workspaceOnboarding/workspaceOnboarding.generate.json +0 -24
- package/guidelines/workspaceRecipes/workspaceRecipes.generate.json +0 -33
- package/index-47pj3aee.js +0 -76
- package/index-5ystnsd5.js +0 -841
- package/index-6wwzs1bc.js +0 -7
- package/index-a17x49vg.js +0 -229
- package/index-m0xccmmm.js +0 -228
- package/index-meh558jy.js +0 -65
- package/index-rtxxan88.js +0 -471
- package/index-te54x9zm.js +0 -338
- package/index-w8kmx1b6.js +0 -2176
- package/index-wt7qrtd4.js +0 -1516
- package/templates/__scalar/__model__/__model__.abstract.md +0 -28
- package/templates/module/__model__.abstract.md +0 -28
- package/templates/service/__model__.abstract.md +0 -28
- package/templates/workspaceRoot/patches/react-dom@19.2.7.patch.template +0 -62
- package/workflow.command-gtdnzvbz.js +0 -108
|
@@ -1,178 +1,80 @@
|
|
|
1
1
|
// @bun
|
|
2
|
+
import {
|
|
3
|
+
ApplicationBuildReporter,
|
|
4
|
+
BUILDER_MIN_RSS_RECYCLE_INTERVAL_MS,
|
|
5
|
+
SOURCE_EXTS,
|
|
6
|
+
shouldRestartBackendByDevPlan,
|
|
7
|
+
shouldRestartBuilderByDevPlan,
|
|
8
|
+
shouldAbandonBackendRecovery,
|
|
9
|
+
normalizeBackendReportedGeneration,
|
|
10
|
+
shouldRestartDevHostByDevPlan,
|
|
11
|
+
devHostStateOf,
|
|
12
|
+
isLegacyBackendFallbackFile,
|
|
13
|
+
shouldMarkBuildPhaseRecovered,
|
|
14
|
+
shouldKeepBuildFailure,
|
|
15
|
+
createBackendBuildStatus,
|
|
16
|
+
backendRestartReasonFromMessage,
|
|
17
|
+
mergeBackendRestartReasons,
|
|
18
|
+
shouldReplaceLastGoodMessage,
|
|
19
|
+
shouldQueueBuildStatusReplay,
|
|
20
|
+
hasBuildFailureForGeneration,
|
|
21
|
+
decideBuilderRssRecycle,
|
|
22
|
+
decideBuilderRssSettle,
|
|
23
|
+
decideIdleSuspend,
|
|
24
|
+
resolveIdleSuspendMs,
|
|
25
|
+
hasAnyBuildFailure,
|
|
26
|
+
shouldRefreshConfigOnIdleWake,
|
|
27
|
+
shouldHoldForReturningBuilder,
|
|
28
|
+
shouldWarnBuilderRssCeilingTight,
|
|
29
|
+
isRssCeilingUnreachable,
|
|
30
|
+
shouldRelayRecycledFrontendState,
|
|
31
|
+
mergeInvalidateMessages,
|
|
32
|
+
buildStatusReplaySequence,
|
|
33
|
+
filesChangedSince,
|
|
34
|
+
HmrWatcher,
|
|
35
|
+
WatchRootResolver
|
|
36
|
+
} from "./index-senscydg.js";
|
|
2
37
|
import {
|
|
3
38
|
runner,
|
|
4
39
|
script
|
|
5
|
-
} from "./index-
|
|
40
|
+
} from "./index-a37by9hn.js";
|
|
6
41
|
import {
|
|
7
42
|
WorkspaceExecutor,
|
|
8
43
|
AppExecutor,
|
|
9
44
|
LibExecutor,
|
|
10
45
|
PkgExecutor
|
|
11
|
-
} from "./index-
|
|
46
|
+
} from "./index-150s07n8.js";
|
|
47
|
+
import {
|
|
48
|
+
DesktopServerStage,
|
|
49
|
+
resolveMobileTargets,
|
|
50
|
+
resolveAppId,
|
|
51
|
+
NativeApp
|
|
52
|
+
} from "./index-7p4c0yer.js";
|
|
12
53
|
import {
|
|
13
54
|
LibraryScript
|
|
14
|
-
} from "./index-
|
|
55
|
+
} from "./index-66yspb67.js";
|
|
15
56
|
import {
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
} from "./index-hwkygn9s.js";
|
|
57
|
+
stripAnsi
|
|
58
|
+
} from "./index-7md2g9vw.js";
|
|
19
59
|
import {
|
|
20
60
|
SlicePlanner,
|
|
21
61
|
formatSlicePlan
|
|
22
|
-
} from "./index-
|
|
62
|
+
} from "./index-mzjb6zp4.js";
|
|
23
63
|
import {
|
|
24
|
-
|
|
25
|
-
} from "./index-
|
|
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
|
-
}
|
|
121
|
-
|
|
122
|
-
// pkgs/@akanjs/devkit/applicationBuildReporter.ts
|
|
123
|
-
import { Logger } from "akanjs/common";
|
|
124
|
-
|
|
125
|
-
class ApplicationBuildReporter {
|
|
126
|
-
static create() {
|
|
127
|
-
return {
|
|
128
|
-
phaseDone: (phase) => Logger.rawLog(ApplicationBuildReporter.formatPhaseLine(phase))
|
|
129
|
-
};
|
|
130
|
-
}
|
|
131
|
-
static printSummary(result) {
|
|
132
|
-
Logger.rawLog("");
|
|
133
|
-
Logger.rawLog(`Route artifacts: ${result.artifactDir}`);
|
|
134
|
-
Logger.rawLog(`Server output: ${result.outputDir}`);
|
|
135
|
-
Logger.rawLog(`Done in ${ApplicationBuildReporter.formatDuration(result.durationMs)}`);
|
|
136
|
-
}
|
|
137
|
-
static formatError(error) {
|
|
138
|
-
if (error instanceof AggregateError) {
|
|
139
|
-
const nestedMessages = error.errors.map((nestedError, index) => ApplicationBuildReporter.formatError(nestedError).trim() || `Unknown error ${index + 1}`).map((message) => message.replace(/^/gm, " ")).join(`
|
|
140
|
-
`);
|
|
141
|
-
return nestedMessages ? `${error.message}
|
|
142
|
-
${nestedMessages}` : error.message;
|
|
143
|
-
}
|
|
144
|
-
if (error instanceof Error) {
|
|
145
|
-
const causeMessage = error.cause ? `
|
|
146
|
-
Caused by: ${ApplicationBuildReporter.formatError(error.cause)}` : "";
|
|
147
|
-
return `${error.message}${causeMessage}`;
|
|
148
|
-
}
|
|
149
|
-
if (typeof error === "object" && error !== null && "message" in error)
|
|
150
|
-
return String(error.message);
|
|
151
|
-
return String(error);
|
|
152
|
-
}
|
|
153
|
-
static formatDuration(ms) {
|
|
154
|
-
if (ms < 1000)
|
|
155
|
-
return `${ms}ms`;
|
|
156
|
-
if (ms < 60000)
|
|
157
|
-
return `${Math.round(ms / 100) / 10}s`;
|
|
158
|
-
const seconds = Math.floor(ms / 1000);
|
|
159
|
-
return `${Math.floor(seconds / 60)}m ${seconds % 60}s`;
|
|
160
|
-
}
|
|
161
|
-
static formatPhaseLine(phase) {
|
|
162
|
-
const summary = phase.summary ? `: ${phase.summary}` : "";
|
|
163
|
-
return `\u2713 ${phase.label}${summary} (${ApplicationBuildReporter.formatDuration(phase.durationMs)})`;
|
|
164
|
-
}
|
|
165
|
-
}
|
|
64
|
+
openBrowser
|
|
65
|
+
} from "./index-9dp031by.js";
|
|
166
66
|
|
|
167
67
|
// pkgs/@akanjs/cli/application/application.script.ts
|
|
168
68
|
import { confirm } from "@inquirer/prompts";
|
|
169
69
|
import { Logger as Logger6 } from "akanjs/common";
|
|
170
70
|
|
|
171
71
|
// pkgs/@akanjs/cli/application/application.runner.ts
|
|
72
|
+
import { realpathSync } from "fs";
|
|
172
73
|
import path5 from "path";
|
|
173
74
|
|
|
174
75
|
// pkgs/@akanjs/devkit/akanApp/akanApp.host.ts
|
|
175
|
-
import
|
|
76
|
+
import path3 from "path";
|
|
77
|
+
import { Logger as Logger2 } from "akanjs/common";
|
|
176
78
|
|
|
177
79
|
// pkgs/@akanjs/devkit/sshTunnel.ts
|
|
178
80
|
import { Client } from "ssh2";
|
|
@@ -261,7 +163,7 @@ var createSshTunnel = async (options) => {
|
|
|
261
163
|
for (const socket of sockets)
|
|
262
164
|
closeSocket(socket, error);
|
|
263
165
|
closeQuietly(() => client.end());
|
|
264
|
-
|
|
166
|
+
server.stop();
|
|
265
167
|
};
|
|
266
168
|
client.once("error", (error) => closeTunnel(error));
|
|
267
169
|
client.once("close", () => closeTunnel());
|
|
@@ -281,22 +183,10 @@ var closeBunSocket = (socket, error) => {
|
|
|
281
183
|
socket.data.stream?.end();
|
|
282
184
|
closeQuietly(() => socket.end());
|
|
283
185
|
};
|
|
284
|
-
var closeBunServer = (server) => {
|
|
285
|
-
server.stop();
|
|
286
|
-
};
|
|
287
186
|
|
|
288
187
|
// pkgs/@akanjs/devkit/createTunnel.ts
|
|
289
|
-
var getSshTunnelOptions = (app, environment) => {
|
|
290
|
-
const { serveDomain, repoName } = WorkspaceExecutor.getBaseDevEnv();
|
|
291
|
-
return {
|
|
292
|
-
host: `${app.name}-${environment}.${serveDomain}`,
|
|
293
|
-
port: process.env.SSH_TUNNEL_PORT ? parseInt(process.env.SSH_TUNNEL_PORT) : 32767,
|
|
294
|
-
username: process.env.SSH_TUNNEL_USERNAME ?? "root",
|
|
295
|
-
password: process.env.SSH_TUNNEL_PASSWORD ?? repoName
|
|
296
|
-
};
|
|
297
|
-
};
|
|
298
188
|
var createTunnel = async (service, { app, environment, port = service === "postgres" ? 5432 : 6379 }) => {
|
|
299
|
-
const
|
|
189
|
+
const { serveDomain, repoName } = WorkspaceExecutor.getBaseDevEnv();
|
|
300
190
|
await createSshTunnel({
|
|
301
191
|
localHost: "0.0.0.0",
|
|
302
192
|
localPort: port,
|
|
@@ -304,13 +194,20 @@ var createTunnel = async (service, { app, environment, port = service === "postg
|
|
|
304
194
|
srcPort: port,
|
|
305
195
|
dstHost: `${service}-0.${service}-svc.${app.name}-${environment}.svc.cluster.local`,
|
|
306
196
|
dstPort: service === "postgres" ? 5432 : 6379,
|
|
307
|
-
sshOptions
|
|
197
|
+
sshOptions: {
|
|
198
|
+
host: `${app.name}-${environment}.${serveDomain}`,
|
|
199
|
+
port: process.env.SSH_TUNNEL_PORT ? parseInt(process.env.SSH_TUNNEL_PORT) : 32767,
|
|
200
|
+
username: process.env.SSH_TUNNEL_USERNAME ?? "root",
|
|
201
|
+
password: process.env.SSH_TUNNEL_PASSWORD ?? repoName
|
|
202
|
+
}
|
|
308
203
|
});
|
|
309
204
|
return `localhost:${port}`;
|
|
310
205
|
};
|
|
311
206
|
// pkgs/@akanjs/devkit/incrementalBuilder/incrementalBuilder.host.ts
|
|
207
|
+
import fs from "fs";
|
|
312
208
|
import path from "path";
|
|
313
|
-
import { Logger
|
|
209
|
+
import { Logger } from "akanjs/common";
|
|
210
|
+
import { CSR_DEV_DIRNAME, CSR_DEV_PATCHING_MARKER, SSR_DEV_DIRNAME } from "akanjs/server/hmr/csrDevManifest";
|
|
314
211
|
import { MemoryLimit } from "akanjs/server/memoryLimit";
|
|
315
212
|
var builderMsgTypeSet = new Set([
|
|
316
213
|
"build-route-res",
|
|
@@ -319,16 +216,19 @@ var builderMsgTypeSet = new Set([
|
|
|
319
216
|
"invalidate",
|
|
320
217
|
"css-updated",
|
|
321
218
|
"pages-updated",
|
|
219
|
+
"csr-updated",
|
|
220
|
+
"ssr-updated",
|
|
322
221
|
"build-status",
|
|
323
|
-
"builder-metrics"
|
|
222
|
+
"builder-metrics",
|
|
223
|
+
"boot-armed"
|
|
324
224
|
]);
|
|
325
225
|
|
|
326
226
|
class IncrementalBuilderHost {
|
|
327
227
|
static #restartBaseDelayMs = 1000;
|
|
328
228
|
static #restartMaxDelayMs = 30000;
|
|
329
229
|
static #recycleDrainTimeoutMs = 30000;
|
|
330
|
-
static #devMaxRssBytes =
|
|
331
|
-
logger = new
|
|
230
|
+
static #devMaxRssBytes = 2048 * 1024 * 1024;
|
|
231
|
+
logger = new Logger("IncrementalBuilderHost");
|
|
332
232
|
entry;
|
|
333
233
|
env;
|
|
334
234
|
app;
|
|
@@ -343,20 +243,33 @@ class IncrementalBuilderHost {
|
|
|
343
243
|
#recycleTimer = null;
|
|
344
244
|
#recycleRequested = false;
|
|
345
245
|
#spawnAfterRecycle = false;
|
|
246
|
+
#patcherOff = false;
|
|
346
247
|
#manualStop = false;
|
|
347
248
|
#inFlight = new Map;
|
|
348
249
|
#startOptions = {};
|
|
349
|
-
constructor({
|
|
250
|
+
constructor({
|
|
251
|
+
app,
|
|
252
|
+
entry,
|
|
253
|
+
env,
|
|
254
|
+
stdio = "inherit",
|
|
255
|
+
onMessage,
|
|
256
|
+
onOutput,
|
|
257
|
+
patcherOff = false
|
|
258
|
+
}) {
|
|
350
259
|
this.app = app;
|
|
351
260
|
this.entry = entry;
|
|
352
261
|
this.env = env;
|
|
353
262
|
this.#stdio = stdio;
|
|
354
263
|
this.#onMessage = onMessage;
|
|
355
264
|
this.#onOutput = onOutput ?? null;
|
|
265
|
+
this.#patcherOff = patcherOff;
|
|
356
266
|
}
|
|
357
267
|
get status() {
|
|
358
268
|
return this.#status;
|
|
359
269
|
}
|
|
270
|
+
get patcherOff() {
|
|
271
|
+
return this.#patcherOff;
|
|
272
|
+
}
|
|
360
273
|
get pid() {
|
|
361
274
|
return this.#proc?.pid ?? null;
|
|
362
275
|
}
|
|
@@ -374,10 +287,16 @@ class IncrementalBuilderHost {
|
|
|
374
287
|
this.ready = false;
|
|
375
288
|
const afterRecycle = this.#spawnAfterRecycle;
|
|
376
289
|
this.#spawnAfterRecycle = false;
|
|
290
|
+
this.#checkPatchingMarker(isRestart && !afterRecycle);
|
|
377
291
|
let proc;
|
|
378
292
|
proc = Bun.spawn(["bun", this.entry], {
|
|
379
293
|
cwd: this.app.cwdPath,
|
|
380
|
-
env: {
|
|
294
|
+
env: {
|
|
295
|
+
...this.env,
|
|
296
|
+
AKAN_WATCH: "1",
|
|
297
|
+
...afterRecycle ? { AKAN_BUILDER_ANNOUNCE_BOOT: "1" } : {},
|
|
298
|
+
...this.#patcherOff ? { AKAN_DEV_CSR_PATCHER: "off" } : {}
|
|
299
|
+
},
|
|
381
300
|
stdio: ["ignore", this.#stdio, this.#stdio],
|
|
382
301
|
ipc: (msg) => {
|
|
383
302
|
if (this.#proc !== proc)
|
|
@@ -432,6 +351,17 @@ class IncrementalBuilderHost {
|
|
|
432
351
|
}
|
|
433
352
|
this.logger.verbose(`builder spawned pid=${proc.pid} entry=${this.entry}${isRestart ? " restart=1" : ""}`);
|
|
434
353
|
}
|
|
354
|
+
#checkPatchingMarker(afterCrash) {
|
|
355
|
+
const markers = [CSR_DEV_DIRNAME, SSR_DEV_DIRNAME].map((dirName) => path.join(this.app.cwdPath, ".akan/artifact", dirName, CSR_DEV_PATCHING_MARKER)).filter((marker) => fs.existsSync(marker));
|
|
356
|
+
if (markers.length === 0)
|
|
357
|
+
return;
|
|
358
|
+
if (afterCrash && !this.#patcherOff) {
|
|
359
|
+
this.#patcherOff = true;
|
|
360
|
+
this.logger.warn("the builder died while patching a dev module registry; registry saves go to a build worker until the next config or metadata restart (AKAN_DEV_CSR_PATCHER=off)");
|
|
361
|
+
}
|
|
362
|
+
for (const marker of markers)
|
|
363
|
+
fs.rmSync(marker, { force: true });
|
|
364
|
+
}
|
|
435
365
|
async#drain(stream, kind) {
|
|
436
366
|
if (!stream)
|
|
437
367
|
return;
|
|
@@ -518,12 +448,8 @@ class IncrementalBuilderHost {
|
|
|
518
448
|
const lost = [...this.#inFlight];
|
|
519
449
|
this.#inFlight.clear();
|
|
520
450
|
this.logger.warn(`failing ${lost.length} unanswered builder request(s): ${reason}`);
|
|
521
|
-
for (const [id, type] of lost)
|
|
522
|
-
|
|
523
|
-
this.#onMessage({ type: "build-route-res", id, ok: false, error: reason });
|
|
524
|
-
else
|
|
525
|
-
this.#onMessage({ type: "build-csr-res", id, ok: false, error: reason });
|
|
526
|
-
}
|
|
451
|
+
for (const [id, type] of lost)
|
|
452
|
+
this.#onMessage({ type: `${type}-res`, id, ok: false, error: reason });
|
|
527
453
|
}
|
|
528
454
|
stop() {
|
|
529
455
|
this.#manualStop = true;
|
|
@@ -541,7 +467,8 @@ class IncrementalBuilderHost {
|
|
|
541
467
|
}
|
|
542
468
|
static async create(app, env, onMessage, {
|
|
543
469
|
stdio = "inherit",
|
|
544
|
-
onOutput
|
|
470
|
+
onOutput,
|
|
471
|
+
patcherOff = false
|
|
545
472
|
} = {}) {
|
|
546
473
|
const candidates = [
|
|
547
474
|
path.join(app.workspace.workspaceRoot, "pkgs/@akanjs/devkit/incrementalBuilder/incrementalBuilder.proc.ts"),
|
|
@@ -551,7 +478,7 @@ class IncrementalBuilderHost {
|
|
|
551
478
|
];
|
|
552
479
|
for (const c of candidates)
|
|
553
480
|
if (await Bun.file(c).exists())
|
|
554
|
-
return new IncrementalBuilderHost({ app, entry: c, env, stdio, onMessage, onOutput });
|
|
481
|
+
return new IncrementalBuilderHost({ app, entry: c, env, stdio, onMessage, onOutput, patcherOff });
|
|
555
482
|
throw new Error(`[cli] frontend builder entry not found; looked in: ${candidates.join(", ")}`);
|
|
556
483
|
}
|
|
557
484
|
}
|
|
@@ -592,211 +519,7 @@ class BuilderRequestRouter {
|
|
|
592
519
|
|
|
593
520
|
// pkgs/@akanjs/devkit/akanApp/BackendImportGraph.ts
|
|
594
521
|
import { stat } from "fs/promises";
|
|
595
|
-
import path3 from "path";
|
|
596
|
-
|
|
597
|
-
// pkgs/@akanjs/devkit/akanApp/devHostPolicy.ts
|
|
598
522
|
import path2 from "path";
|
|
599
|
-
var BACKEND_RECOVERY_MAX_ATTEMPTS = 5;
|
|
600
|
-
var HELD_BUILDER_REQUEST_LIMIT = 64;
|
|
601
|
-
var BUILDER_MIN_RSS_RECYCLE_INTERVAL_MS = 30000;
|
|
602
|
-
var BUILDER_TIGHT_RSS_REPORT_LIMIT = 2;
|
|
603
|
-
var BUILDER_RSS_HARD_MULTIPLE = 1.5;
|
|
604
|
-
var DEV_IDLE_SUSPEND_MS = 300000;
|
|
605
|
-
var DEV_IDLE_MIN_UPTIME_MS = 30000;
|
|
606
|
-
var SOURCE_EXTS = new Set([".ts", ".tsx", ".js", ".jsx", ".mjs", ".cjs"]);
|
|
607
|
-
var SERVER_SUFFIXES = [".service.ts", ".document.ts"];
|
|
608
|
-
var SHARED_SUFFIXES = [".constant.ts", ".dictionary.ts", ".signal.ts"];
|
|
609
|
-
var RUNTIME_METADATA_BASENAMES = new Set(["dict.ts", "sig.ts", "useClient.ts", "useServer.ts"]);
|
|
610
|
-
var shouldRestartBackendByDevPlan = (message) => {
|
|
611
|
-
if (!message.devPlan)
|
|
612
|
-
return null;
|
|
613
|
-
if (message.devPlan.actions.includes("report-error"))
|
|
614
|
-
return false;
|
|
615
|
-
if (message.devPlan.actions.includes("restart-builder"))
|
|
616
|
-
return false;
|
|
617
|
-
return message.devPlan.actions.includes("restart-backend");
|
|
618
|
-
};
|
|
619
|
-
var shouldRestartBuilderByDevPlan = (message) => message.devPlan?.actions.includes("restart-builder") ?? false;
|
|
620
|
-
var shouldAbandonBackendRecovery = (attempts, maxAttempts = BACKEND_RECOVERY_MAX_ATTEMPTS) => attempts >= maxAttempts;
|
|
621
|
-
var normalizeBackendReportedGeneration = (generation) => generation >= 0 ? generation : undefined;
|
|
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];
|
|
632
|
-
var RESTART_ROLE_ORDER = ["server", "shared", "barrel", "config"];
|
|
633
|
-
var generationValue = (generation) => generation ?? -1;
|
|
634
|
-
var isLegacyBackendFallbackFile = (file, workspaceRoot) => {
|
|
635
|
-
const abs = path2.resolve(file);
|
|
636
|
-
const ext = path2.extname(abs).toLowerCase();
|
|
637
|
-
if (!SOURCE_EXTS.has(ext))
|
|
638
|
-
return false;
|
|
639
|
-
const rel = path2.relative(path2.resolve(workspaceRoot), abs);
|
|
640
|
-
if (!rel || rel.startsWith("..") || path2.isAbsolute(rel))
|
|
641
|
-
return false;
|
|
642
|
-
const parts = rel.split(path2.sep).filter(Boolean);
|
|
643
|
-
const [scope] = parts;
|
|
644
|
-
if (scope !== "apps" && scope !== "libs" && scope !== "pkgs")
|
|
645
|
-
return false;
|
|
646
|
-
const base = path2.basename(abs);
|
|
647
|
-
return parts.includes("srvkit") || parts.includes("common") || SERVER_SUFFIXES.some((suffix) => base.endsWith(suffix)) || SHARED_SUFFIXES.some((suffix) => base.endsWith(suffix)) || RUNTIME_METADATA_BASENAMES.has(base) || base === "main.ts" || base === "server.ts";
|
|
648
|
-
};
|
|
649
|
-
var shouldMarkBuildPhaseRecovered = (previousByPhase, status) => {
|
|
650
|
-
const previous = previousByPhase.get(status.phase);
|
|
651
|
-
return Boolean(previous && status.ok && !previous.ok && generationValue(status.generation) >= previous.generation);
|
|
652
|
-
};
|
|
653
|
-
var createBackendBuildStatus = ({
|
|
654
|
-
generation,
|
|
655
|
-
ok,
|
|
656
|
-
files = [],
|
|
657
|
-
message
|
|
658
|
-
}) => ({
|
|
659
|
-
generation,
|
|
660
|
-
phase: "backend",
|
|
661
|
-
ok,
|
|
662
|
-
files,
|
|
663
|
-
message
|
|
664
|
-
});
|
|
665
|
-
var backendRestartReasonFromMessage = (message) => {
|
|
666
|
-
const roleSet = new Set;
|
|
667
|
-
for (const role of message.devPlan?.roles ?? []) {
|
|
668
|
-
if (role === "server" || role === "shared" || role === "barrel" || role === "config")
|
|
669
|
-
roleSet.add(role);
|
|
670
|
-
}
|
|
671
|
-
return {
|
|
672
|
-
generation: message.devPlan?.generation ?? message.generation,
|
|
673
|
-
files: [...new Set(message.files)].sort(),
|
|
674
|
-
roles: RESTART_ROLE_ORDER.filter((role) => roleSet.has(role))
|
|
675
|
-
};
|
|
676
|
-
};
|
|
677
|
-
var mergeBackendRestartReasons = (current, next) => ({
|
|
678
|
-
generation: generationValue(next.generation) >= generationValue(current?.generation) ? next.generation : current?.generation,
|
|
679
|
-
files: [...new Set([...current?.files ?? [], ...next.files])].sort(),
|
|
680
|
-
roles: RESTART_ROLE_ORDER.filter((role) => current?.roles.includes(role) || next.roles.includes(role))
|
|
681
|
-
});
|
|
682
|
-
var shouldReplaceLastGoodMessage = (current, next) => !current || generationValue(next.data.generation) >= generationValue(current.data.generation);
|
|
683
|
-
var shouldQueueBuildStatusReplay = (backendReady, pendingReplayCount) => !backendReady || pendingReplayCount > 0;
|
|
684
|
-
var hasBuildFailureForGeneration = (statusByPhase, generation) => {
|
|
685
|
-
if (typeof generation !== "number")
|
|
686
|
-
return false;
|
|
687
|
-
for (const status of statusByPhase.values()) {
|
|
688
|
-
if (!status.ok && status.generation === generation)
|
|
689
|
-
return true;
|
|
690
|
-
}
|
|
691
|
-
return false;
|
|
692
|
-
};
|
|
693
|
-
var decideBuilderRssRecycle = ({
|
|
694
|
-
rssBytes,
|
|
695
|
-
ceilingBytes,
|
|
696
|
-
buildFailed,
|
|
697
|
-
msSinceLastRecycle,
|
|
698
|
-
minIntervalMs = BUILDER_MIN_RSS_RECYCLE_INTERVAL_MS
|
|
699
|
-
}) => {
|
|
700
|
-
if (!ceilingBytes)
|
|
701
|
-
return "unbounded";
|
|
702
|
-
if (rssBytes < ceilingBytes)
|
|
703
|
-
return "below-ceiling";
|
|
704
|
-
if (buildFailed)
|
|
705
|
-
return "build-failed";
|
|
706
|
-
if (msSinceLastRecycle !== null && msSinceLastRecycle < minIntervalMs)
|
|
707
|
-
return "too-soon";
|
|
708
|
-
return "recycle";
|
|
709
|
-
};
|
|
710
|
-
var decideBuilderRssSettle = ({
|
|
711
|
-
rssBytes,
|
|
712
|
-
ceilingBytes,
|
|
713
|
-
hardMultiple = BUILDER_RSS_HARD_MULTIPLE
|
|
714
|
-
}) => rssBytes >= ceilingBytes * hardMultiple ? "recycle-now" : "wait-and-recheck";
|
|
715
|
-
var decideIdleSuspend = ({
|
|
716
|
-
enabled,
|
|
717
|
-
suspended,
|
|
718
|
-
builderReady,
|
|
719
|
-
backendReady,
|
|
720
|
-
buildFailed,
|
|
721
|
-
restartPending,
|
|
722
|
-
msSinceWake,
|
|
723
|
-
minUptimeMs = DEV_IDLE_MIN_UPTIME_MS
|
|
724
|
-
}) => {
|
|
725
|
-
if (!enabled)
|
|
726
|
-
return "disabled";
|
|
727
|
-
if (suspended)
|
|
728
|
-
return "already-suspended";
|
|
729
|
-
if (!builderReady)
|
|
730
|
-
return "builder-not-ready";
|
|
731
|
-
if (!backendReady)
|
|
732
|
-
return "backend-not-ready";
|
|
733
|
-
if (buildFailed)
|
|
734
|
-
return "build-failed";
|
|
735
|
-
if (restartPending)
|
|
736
|
-
return "restart-pending";
|
|
737
|
-
if (msSinceWake !== null && msSinceWake < minUptimeMs)
|
|
738
|
-
return "too-soon";
|
|
739
|
-
return "suspend";
|
|
740
|
-
};
|
|
741
|
-
var resolveIdleSuspendMs = (raw) => {
|
|
742
|
-
if (raw === undefined || raw === "")
|
|
743
|
-
return DEV_IDLE_SUSPEND_MS;
|
|
744
|
-
const parsed = Number(raw);
|
|
745
|
-
if (!Number.isFinite(parsed) || parsed <= 0)
|
|
746
|
-
return null;
|
|
747
|
-
return Math.round(parsed);
|
|
748
|
-
};
|
|
749
|
-
var hasAnyBuildFailure = (statusByPhase) => [...statusByPhase.values()].some((status) => !status.ok);
|
|
750
|
-
var shouldRefreshConfigOnIdleWake = (batch) => !!batch && batch.kinds.has("config");
|
|
751
|
-
var RETURNING_BUILDER_STATUSES = new Set(["starting", "recycling", "restarting"]);
|
|
752
|
-
var shouldHoldForReturningBuilder = ({
|
|
753
|
-
status,
|
|
754
|
-
heldCount,
|
|
755
|
-
limit = HELD_BUILDER_REQUEST_LIMIT
|
|
756
|
-
}) => RETURNING_BUILDER_STATUSES.has(status) && heldCount < limit;
|
|
757
|
-
var shouldWarnBuilderRssCeilingTight = (reportsSinceRecycle, limit = BUILDER_TIGHT_RSS_REPORT_LIMIT) => reportsSinceRecycle >= limit;
|
|
758
|
-
var isRssCeilingUnreachable = (freshRssBytes, ceilingBytes) => freshRssBytes !== null && ceilingBytes !== null && freshRssBytes >= ceilingBytes;
|
|
759
|
-
var shouldRelayRecycledFrontendState = (current, next) => {
|
|
760
|
-
if (!current || current.type !== next.type)
|
|
761
|
-
return true;
|
|
762
|
-
if (current.type === "pages-updated" && next.type === "pages-updated")
|
|
763
|
-
return current.data.bundlePath !== next.data.bundlePath;
|
|
764
|
-
if (current.type === "css-updated" && next.type === "css-updated")
|
|
765
|
-
return JSON.stringify(current.data.cssAssets) !== JSON.stringify(next.data.cssAssets);
|
|
766
|
-
return true;
|
|
767
|
-
};
|
|
768
|
-
var mergeDevPlans = (current, next) => {
|
|
769
|
-
if (!current)
|
|
770
|
-
return next;
|
|
771
|
-
if (!next)
|
|
772
|
-
return current;
|
|
773
|
-
const reasonByFile = { ...current.reasonByFile };
|
|
774
|
-
for (const [file, reasons] of Object.entries(next.reasonByFile)) {
|
|
775
|
-
reasonByFile[file] = [...new Set([...reasonByFile[file] ?? [], ...reasons])].sort();
|
|
776
|
-
}
|
|
777
|
-
return {
|
|
778
|
-
generation: Math.max(current.generation, next.generation),
|
|
779
|
-
files: [...new Set([...current.files, ...next.files])].sort(),
|
|
780
|
-
generatedFiles: [...new Set([...current.generatedFiles, ...next.generatedFiles])].sort(),
|
|
781
|
-
roles: [...new Set([...current.roles, ...next.roles])].sort(),
|
|
782
|
-
actions: [...new Set([...current.actions, ...next.actions])].sort(),
|
|
783
|
-
reasonByFile
|
|
784
|
-
};
|
|
785
|
-
};
|
|
786
|
-
var mergeInvalidateMessages = (current, next) => {
|
|
787
|
-
const generation = Math.max(generationValue(current.generation), generationValue(next.generation));
|
|
788
|
-
return {
|
|
789
|
-
type: "invalidate",
|
|
790
|
-
kinds: [...new Set([...current.kinds, ...next.kinds])].sort(),
|
|
791
|
-
files: [...new Set([...current.files, ...next.files])].sort(),
|
|
792
|
-
generation: generation >= 0 ? generation : undefined,
|
|
793
|
-
devPlan: mergeDevPlans(current.devPlan, next.devPlan)
|
|
794
|
-
};
|
|
795
|
-
};
|
|
796
|
-
var buildStatusReplaySequence = (pendingReplay, latestByPhase) => [...pendingReplay, ...latestByPhase.values()];
|
|
797
|
-
var filesChangedSince = (before, after) => [...before].filter(([file, stamp]) => after.get(file) !== stamp).map(([file]) => file);
|
|
798
|
-
|
|
799
|
-
// pkgs/@akanjs/devkit/akanApp/BackendImportGraph.ts
|
|
800
523
|
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
524
|
var GRAPH_IMPORT_KINDS = new Set([
|
|
802
525
|
"import-statement",
|
|
@@ -827,7 +550,7 @@ class BackendImportGraph {
|
|
|
827
550
|
return this.#lastRefreshSucceeded;
|
|
828
551
|
}
|
|
829
552
|
has(file) {
|
|
830
|
-
return this.#files.has(
|
|
553
|
+
return this.#files.has(path2.resolve(file));
|
|
831
554
|
}
|
|
832
555
|
async fingerprint() {
|
|
833
556
|
const stamps = await Promise.all([...this.#files].map(async (file) => {
|
|
@@ -855,9 +578,9 @@ class BackendImportGraph {
|
|
|
855
578
|
const roots = await this.#entrypoints();
|
|
856
579
|
const files = new Set;
|
|
857
580
|
const queue = [...roots];
|
|
858
|
-
const workspaceRoot =
|
|
581
|
+
const workspaceRoot = path2.resolve(this.#app.workspace.workspaceRoot);
|
|
859
582
|
while (queue.length > 0) {
|
|
860
|
-
const current =
|
|
583
|
+
const current = path2.resolve(queue.pop());
|
|
861
584
|
if (files.has(current))
|
|
862
585
|
continue;
|
|
863
586
|
if (!this.#isWorkspaceSource(current, workspaceRoot))
|
|
@@ -866,7 +589,7 @@ class BackendImportGraph {
|
|
|
866
589
|
if (!imports)
|
|
867
590
|
continue;
|
|
868
591
|
files.add(current);
|
|
869
|
-
const importerDir =
|
|
592
|
+
const importerDir = path2.dirname(current);
|
|
870
593
|
for (const imp of imports) {
|
|
871
594
|
if (!GRAPH_IMPORT_KINDS.has(imp.kind) || !imp.path || NON_SOURCE_EXT_RE.test(imp.path))
|
|
872
595
|
continue;
|
|
@@ -897,7 +620,7 @@ class BackendImportGraph {
|
|
|
897
620
|
const roots = [`${this.#app.cwdPath}/main.ts`, `${this.#app.cwdPath}/server.ts`];
|
|
898
621
|
const existing = [];
|
|
899
622
|
for (const root of roots) {
|
|
900
|
-
const abs =
|
|
623
|
+
const abs = path2.resolve(root);
|
|
901
624
|
if (await Bun.file(abs).exists())
|
|
902
625
|
existing.push(abs);
|
|
903
626
|
}
|
|
@@ -906,25 +629,25 @@ class BackendImportGraph {
|
|
|
906
629
|
#resolve(specifier, importerDir) {
|
|
907
630
|
try {
|
|
908
631
|
const resolved = Bun.resolveSync(specifier, importerDir);
|
|
909
|
-
if (!
|
|
632
|
+
if (!path2.isAbsolute(resolved))
|
|
910
633
|
return null;
|
|
911
|
-
if (!SOURCE_EXTS.has(
|
|
634
|
+
if (!SOURCE_EXTS.has(path2.extname(resolved).toLowerCase()))
|
|
912
635
|
return null;
|
|
913
|
-
return
|
|
636
|
+
return path2.resolve(resolved);
|
|
914
637
|
} catch {
|
|
915
638
|
return null;
|
|
916
639
|
}
|
|
917
640
|
}
|
|
918
641
|
#isWorkspaceSource(file, workspaceRoot) {
|
|
919
|
-
const rel =
|
|
920
|
-
if (rel.startsWith("..") ||
|
|
642
|
+
const rel = path2.relative(workspaceRoot, file);
|
|
643
|
+
if (rel.startsWith("..") || path2.isAbsolute(rel))
|
|
921
644
|
return false;
|
|
922
|
-
if (rel.includes(`${
|
|
645
|
+
if (rel.includes(`${path2.sep}node_modules${path2.sep}`) || rel.includes(`${path2.sep}.akan${path2.sep}`))
|
|
923
646
|
return false;
|
|
924
|
-
return SOURCE_EXTS.has(
|
|
647
|
+
return SOURCE_EXTS.has(path2.extname(file).toLowerCase());
|
|
925
648
|
}
|
|
926
649
|
#scanImports(file, source) {
|
|
927
|
-
const ext =
|
|
650
|
+
const ext = path2.extname(file).toLowerCase();
|
|
928
651
|
if (ext === ".tsx")
|
|
929
652
|
return this.#tsxTranspiler.scanImports(source);
|
|
930
653
|
if (ext === ".jsx")
|
|
@@ -935,8 +658,44 @@ class BackendImportGraph {
|
|
|
935
658
|
}
|
|
936
659
|
}
|
|
937
660
|
|
|
661
|
+
// pkgs/@akanjs/devkit/akanApp/devBootLatch.ts
|
|
662
|
+
class DevBootLatch {
|
|
663
|
+
#onBooted;
|
|
664
|
+
#waitMs;
|
|
665
|
+
#ready = false;
|
|
666
|
+
#armed = false;
|
|
667
|
+
#booted = false;
|
|
668
|
+
#timer = null;
|
|
669
|
+
constructor(onBooted, { waitMs = 30000 } = {}) {
|
|
670
|
+
this.#onBooted = onBooted;
|
|
671
|
+
this.#waitMs = waitMs;
|
|
672
|
+
}
|
|
673
|
+
ready() {
|
|
674
|
+
this.#ready = true;
|
|
675
|
+
if (!this.#armed && !this.#timer) {
|
|
676
|
+
this.#timer = setTimeout(() => this.armed(), this.#waitMs);
|
|
677
|
+
this.#timer.unref?.();
|
|
678
|
+
}
|
|
679
|
+
this.#release();
|
|
680
|
+
}
|
|
681
|
+
armed() {
|
|
682
|
+
this.#armed = true;
|
|
683
|
+
this.#release();
|
|
684
|
+
}
|
|
685
|
+
#release() {
|
|
686
|
+
if (this.#booted || !this.#ready || !this.#armed)
|
|
687
|
+
return;
|
|
688
|
+
this.#booted = true;
|
|
689
|
+
if (this.#timer)
|
|
690
|
+
clearTimeout(this.#timer);
|
|
691
|
+
this.#timer = null;
|
|
692
|
+
this.#onBooted();
|
|
693
|
+
}
|
|
694
|
+
}
|
|
695
|
+
|
|
938
696
|
// pkgs/@akanjs/devkit/akanApp/akanApp.host.ts
|
|
939
697
|
var backendMsgTypeSet = new Set(["build-route", "build-csr"]);
|
|
698
|
+
var asMib = (bytes) => Math.round(bytes / 1024 / 1024);
|
|
940
699
|
var BACKEND_RESTART_DEBOUNCE_MS = 120;
|
|
941
700
|
var BACKEND_GRACEFUL_TIMEOUT_MS = 8000;
|
|
942
701
|
var BACKEND_RECOVERY_BASE_DELAY_MS = 1000;
|
|
@@ -952,13 +711,15 @@ var BUILDER_RECOVERY_MAX_DELAY_MS = 60000;
|
|
|
952
711
|
|
|
953
712
|
class AkanAppHost {
|
|
954
713
|
app;
|
|
955
|
-
logger = new
|
|
714
|
+
logger = new Logger2("AkanAppHost");
|
|
956
715
|
stdio;
|
|
957
716
|
env;
|
|
958
717
|
#onDevEvent;
|
|
959
718
|
#lastDevState = null;
|
|
719
|
+
#bootLatch = new DevBootLatch(() => this.#onDevEvent?.({ app: this.app.name, booted: true }));
|
|
960
720
|
#backend = null;
|
|
961
721
|
#builder = null;
|
|
722
|
+
#patcherOff = false;
|
|
962
723
|
#backendReady = false;
|
|
963
724
|
#plannedBackendStops = new WeakSet;
|
|
964
725
|
#restartTimer = null;
|
|
@@ -967,7 +728,10 @@ class AkanAppHost {
|
|
|
967
728
|
#backendGaveUp = false;
|
|
968
729
|
#backendLifecycleState = "stopped";
|
|
969
730
|
#pendingRestartReason = null;
|
|
731
|
+
#backendRestart = null;
|
|
970
732
|
#pendingRecycle = null;
|
|
733
|
+
#restartFailedAfterApply = null;
|
|
734
|
+
#stopping = false;
|
|
971
735
|
#builderRecoveryTimer = null;
|
|
972
736
|
#builderRecoveryAttempts = 0;
|
|
973
737
|
#backendStartStatus = null;
|
|
@@ -995,6 +759,7 @@ class AkanAppHost {
|
|
|
995
759
|
#builderGapStamp = null;
|
|
996
760
|
#pendingBuilderMessages = [];
|
|
997
761
|
#builderRequests = new BuilderRequestRouter;
|
|
762
|
+
#builderGeneration = 0;
|
|
998
763
|
constructor(app, {
|
|
999
764
|
env,
|
|
1000
765
|
stdio = "inherit",
|
|
@@ -1011,6 +776,8 @@ class AkanAppHost {
|
|
|
1011
776
|
return;
|
|
1012
777
|
this.#lastDevState = state;
|
|
1013
778
|
this.#onDevEvent({ app: this.app.name, state, ...detail ? { detail } : {} });
|
|
779
|
+
if (state === "ready")
|
|
780
|
+
this.#bootLatch.ready();
|
|
1014
781
|
}
|
|
1015
782
|
async start() {
|
|
1016
783
|
if (this.#backend)
|
|
@@ -1028,25 +795,24 @@ class AkanAppHost {
|
|
|
1028
795
|
return this;
|
|
1029
796
|
}
|
|
1030
797
|
async stop() {
|
|
798
|
+
this.#stopping = true;
|
|
799
|
+
this.#pendingRecycle = null;
|
|
1031
800
|
this.#cancelIdleSuspend();
|
|
1032
801
|
this.#stopIdleWatcher();
|
|
1033
|
-
|
|
1034
|
-
|
|
1035
|
-
this.#restartTimer = null;
|
|
1036
|
-
}
|
|
1037
|
-
if (this.#backendRecoveryTimer) {
|
|
1038
|
-
clearTimeout(this.#backendRecoveryTimer);
|
|
1039
|
-
this.#backendRecoveryTimer = null;
|
|
1040
|
-
}
|
|
1041
|
-
if (this.#builderRecoveryTimer) {
|
|
1042
|
-
clearTimeout(this.#builderRecoveryTimer);
|
|
1043
|
-
this.#builderRecoveryTimer = null;
|
|
1044
|
-
}
|
|
802
|
+
this.#clearRestartTimers();
|
|
803
|
+
this.#pendingRestartReason = null;
|
|
1045
804
|
this.#failPendingBuilderMessages("dev server is shutting down");
|
|
805
|
+
await this.#backendRestart;
|
|
1046
806
|
await this.#stopBackend();
|
|
1047
807
|
this.#stopBuilder();
|
|
1048
808
|
return this;
|
|
1049
809
|
}
|
|
810
|
+
#clearRestartTimers() {
|
|
811
|
+
for (const timer of [this.#restartTimer, this.#backendRecoveryTimer, this.#builderRecoveryTimer])
|
|
812
|
+
if (timer)
|
|
813
|
+
clearTimeout(timer);
|
|
814
|
+
this.#restartTimer = this.#backendRecoveryTimer = this.#builderRecoveryTimer = null;
|
|
815
|
+
}
|
|
1050
816
|
kill() {
|
|
1051
817
|
this.stop();
|
|
1052
818
|
}
|
|
@@ -1057,6 +823,12 @@ class AkanAppHost {
|
|
|
1057
823
|
return await createTunnel(type, { app: this.app, environment });
|
|
1058
824
|
}
|
|
1059
825
|
#startBackend(startStatus = null) {
|
|
826
|
+
if (this.#stopping)
|
|
827
|
+
return;
|
|
828
|
+
if (this.#backend) {
|
|
829
|
+
this.logger.warn(`backend pid=${this.#backend.pid} is still running; not starting a second one`);
|
|
830
|
+
return;
|
|
831
|
+
}
|
|
1060
832
|
this.#builderRequests.startGeneration();
|
|
1061
833
|
this.#discardPendingBuilderMessages("the backend restarted while the builder was away");
|
|
1062
834
|
this.#backendStartStatus = startStatus;
|
|
@@ -1077,6 +849,7 @@ class AkanAppHost {
|
|
|
1077
849
|
this.#setBackendLifecycleState("ready", `pid=${msg.pid}`);
|
|
1078
850
|
this.#recordBackendReadyStatus();
|
|
1079
851
|
this.logger.verbose(`backend ready pid=${msg.pid}`);
|
|
852
|
+
this.#forgetRouteBuildStatus();
|
|
1080
853
|
this.#replayBuilderState();
|
|
1081
854
|
return;
|
|
1082
855
|
}
|
|
@@ -1088,6 +861,8 @@ class AkanAppHost {
|
|
|
1088
861
|
message: msg.data.message
|
|
1089
862
|
});
|
|
1090
863
|
this.#sendOrQueueBuildStatus(status);
|
|
864
|
+
if (!status.ok)
|
|
865
|
+
this.#emitDevEvent("failed", status.message);
|
|
1091
866
|
return;
|
|
1092
867
|
}
|
|
1093
868
|
if (backendMsgTypeSet.has(msg.type))
|
|
@@ -1236,11 +1011,17 @@ class AkanAppHost {
|
|
|
1236
1011
|
clearTimeout(this.#restartTimer);
|
|
1237
1012
|
this.#restartTimer = setTimeout(() => {
|
|
1238
1013
|
this.#restartTimer = null;
|
|
1014
|
+
this.#backendRestart ??= this.#drainBackendRestarts().finally(() => {
|
|
1015
|
+
this.#backendRestart = null;
|
|
1016
|
+
});
|
|
1017
|
+
}, BACKEND_RESTART_DEBOUNCE_MS);
|
|
1018
|
+
}
|
|
1019
|
+
async#drainBackendRestarts() {
|
|
1020
|
+
while (this.#pendingRestartReason && !this.#restartTimer && !this.#stopping) {
|
|
1239
1021
|
const next = this.#pendingRestartReason;
|
|
1240
1022
|
this.#pendingRestartReason = null;
|
|
1241
|
-
|
|
1242
|
-
|
|
1243
|
-
}, BACKEND_RESTART_DEBOUNCE_MS);
|
|
1023
|
+
await this.#restartBackend(next);
|
|
1024
|
+
}
|
|
1244
1025
|
}
|
|
1245
1026
|
async#restartBackend(reason) {
|
|
1246
1027
|
this.logger.verbose(`[backend-reload] restarting backend generation=${reason.generation ?? "(unknown)"} files=${reason.files.length} roles=${reason.roles.join(",") || "(none)"}`);
|
|
@@ -1297,17 +1078,26 @@ ${this.#backendStderrTail.join(`
|
|
|
1297
1078
|
});
|
|
1298
1079
|
}
|
|
1299
1080
|
async#handleBuilderMessage(message) {
|
|
1081
|
+
if (this.#stopping)
|
|
1082
|
+
return;
|
|
1300
1083
|
this.#markDevActivity();
|
|
1084
|
+
this.#trackBuilderGeneration(message);
|
|
1301
1085
|
if (message.type === "build-status") {
|
|
1302
1086
|
this.#recordBuildStatus(message.data);
|
|
1303
1087
|
this.#sendOrQueueBuildStatus(message.data);
|
|
1304
1088
|
this.#reviveBackendAfterGreenBuild(message.data);
|
|
1089
|
+
await this.#resumeFailedRecycle(message.data);
|
|
1305
1090
|
return;
|
|
1306
1091
|
}
|
|
1307
1092
|
if (message.type === "builder-metrics") {
|
|
1308
1093
|
this.#handleBuilderMetrics(message.data);
|
|
1309
1094
|
return;
|
|
1310
1095
|
}
|
|
1096
|
+
if (message.type === "boot-armed") {
|
|
1097
|
+
this.logger.verbose("[builder] boot builds settled");
|
|
1098
|
+
this.#bootLatch.armed();
|
|
1099
|
+
return;
|
|
1100
|
+
}
|
|
1311
1101
|
if (message.type === "pages-updated" || message.type === "css-updated") {
|
|
1312
1102
|
const recycled = message.data.reason === "builder-recycle";
|
|
1313
1103
|
if (recycled && !this.#shouldRelayRecycledState(message))
|
|
@@ -1329,6 +1119,22 @@ ${this.#backendStderrTail.join(`
|
|
|
1329
1119
|
}
|
|
1330
1120
|
this.#sendToBackend(message);
|
|
1331
1121
|
}
|
|
1122
|
+
#trackBuilderGeneration(message) {
|
|
1123
|
+
const generation = AkanAppHost.#builderGenerationOf(message);
|
|
1124
|
+
if (generation === undefined || generation <= this.#builderGeneration)
|
|
1125
|
+
return;
|
|
1126
|
+
this.#builderGeneration = generation;
|
|
1127
|
+
Object.assign(this.env, { AKAN_BUILDER_INITIAL_GENERATION: String(generation) });
|
|
1128
|
+
}
|
|
1129
|
+
static #builderGenerationOf(message) {
|
|
1130
|
+
if (message.type === "invalidate")
|
|
1131
|
+
return message.generation;
|
|
1132
|
+
if (message.type === "build-status" || message.type === "builder-metrics")
|
|
1133
|
+
return message.data.generation;
|
|
1134
|
+
if (message.type === "pages-updated" || message.type === "css-updated")
|
|
1135
|
+
return message.data.generation;
|
|
1136
|
+
return;
|
|
1137
|
+
}
|
|
1332
1138
|
#shouldRelayRecycledState(message) {
|
|
1333
1139
|
const current = message.type === "pages-updated" ? this.#lastGoodFrontend.pages : this.#lastGoodFrontend.css;
|
|
1334
1140
|
if (shouldRelayRecycledFrontendState(current, message)) {
|
|
@@ -1342,7 +1148,6 @@ ${this.#backendStderrTail.join(`
|
|
|
1342
1148
|
if (this.#rssCeilingAbandoned)
|
|
1343
1149
|
return;
|
|
1344
1150
|
const ceilingBytes = IncrementalBuilderHost.maxRssBytes();
|
|
1345
|
-
const asMib = (bytes) => Math.round(bytes / 1024 / 1024);
|
|
1346
1151
|
const decision = decideBuilderRssRecycle({
|
|
1347
1152
|
rssBytes: metrics.rssBytes,
|
|
1348
1153
|
ceilingBytes,
|
|
@@ -1380,7 +1185,6 @@ ${this.#backendStderrTail.join(`
|
|
|
1380
1185
|
if (!isRssCeilingUnreachable(freshRssBytes, ceilingBytes))
|
|
1381
1186
|
return;
|
|
1382
1187
|
this.#rssCeilingAbandoned = true;
|
|
1383
|
-
const asMib = (bytes) => Math.round(bytes / 1024 / 1024);
|
|
1384
1188
|
this.logger.error(`[builder-recycle] a freshly recycled builder is already at ${asMib(freshRssBytes ?? 0)}MiB with nothing built on demand, so the ${asMib(ceilingBytes)}MiB ceiling cannot be met for this app; no longer enforcing it this session. Raise AKAN_BUILDER_MAX_RSS_MB, or set it to 0 to leave the builder unbounded.`);
|
|
1385
1189
|
}
|
|
1386
1190
|
#armRssRecycle(reason, over) {
|
|
@@ -1407,7 +1211,6 @@ ${this.#backendStderrTail.join(`
|
|
|
1407
1211
|
}, BUILDER_RSS_RECYCLE_QUIET_MS);
|
|
1408
1212
|
}
|
|
1409
1213
|
async#recycleBuilderForRssWhenStillOver(reason, { rssBytes, ceilingBytes }) {
|
|
1410
|
-
const asMib = (bytes) => Math.round(bytes / 1024 / 1024);
|
|
1411
1214
|
if (decideBuilderRssSettle({ rssBytes, ceilingBytes }) === "recycle-now") {
|
|
1412
1215
|
this.#recycleBuilderForRss(reason);
|
|
1413
1216
|
return;
|
|
@@ -1433,16 +1236,18 @@ ${this.#backendStderrTail.join(`
|
|
|
1433
1236
|
this.#recycleBuilderForRss(settledBytes === null ? reason : `${reason}; still ${asMib(settledBytes)}MiB after settling`);
|
|
1434
1237
|
}
|
|
1435
1238
|
static async readProcessRssBytes(pid) {
|
|
1239
|
+
if (process.platform === "win32")
|
|
1240
|
+
return await AkanAppHost.#readRssKbVia(["tasklist", "/FI", `PID eq ${pid}`, "/FO", "CSV", "/NH"], AkanAppHost.#tasklistRssKb);
|
|
1436
1241
|
const status = await Bun.file(`/proc/${pid}/status`).text().catch(() => null);
|
|
1437
1242
|
const vmRssKb = status === null ? null : /VmRSS:\s+(\d+) kB/.exec(status)?.[1];
|
|
1438
1243
|
if (vmRssKb)
|
|
1439
1244
|
return Number(vmRssKb) * 1024;
|
|
1440
|
-
return await AkanAppHost.#
|
|
1245
|
+
return await AkanAppHost.#readRssKbVia(["ps", "-o", "rss=", "-p", String(pid)], (output) => Number(output.trim()));
|
|
1441
1246
|
}
|
|
1442
|
-
static async#
|
|
1247
|
+
static async#readRssKbVia(command, parseKb, timeoutMs = PS_RSS_TIMEOUT_MS) {
|
|
1443
1248
|
let proc;
|
|
1444
1249
|
try {
|
|
1445
|
-
proc = Bun.spawn(
|
|
1250
|
+
proc = Bun.spawn(command, { stdio: ["ignore", "pipe", "ignore"] });
|
|
1446
1251
|
} catch {
|
|
1447
1252
|
return null;
|
|
1448
1253
|
}
|
|
@@ -1450,7 +1255,7 @@ ${this.#backendStderrTail.join(`
|
|
|
1450
1255
|
try {
|
|
1451
1256
|
const output = await new Response(proc.stdout).text();
|
|
1452
1257
|
await proc.exited;
|
|
1453
|
-
const rssKb =
|
|
1258
|
+
const rssKb = parseKb(output);
|
|
1454
1259
|
return Number.isFinite(rssKb) && rssKb > 0 ? rssKb * 1024 : null;
|
|
1455
1260
|
} catch {
|
|
1456
1261
|
return null;
|
|
@@ -1458,6 +1263,10 @@ ${this.#backendStderrTail.join(`
|
|
|
1458
1263
|
clearTimeout(killer);
|
|
1459
1264
|
}
|
|
1460
1265
|
}
|
|
1266
|
+
static #tasklistRssKb(output) {
|
|
1267
|
+
const memUsage = /"([^"]*)"\s*$/m.exec(output)?.[1];
|
|
1268
|
+
return memUsage ? Number(memUsage.replace(/\D/g, "")) : Number.NaN;
|
|
1269
|
+
}
|
|
1461
1270
|
#cancelRssRecycle() {
|
|
1462
1271
|
this.#rssRecycleReason = null;
|
|
1463
1272
|
this.#rssRecycleOver = null;
|
|
@@ -1517,7 +1326,7 @@ ${this.#backendStderrTail.join(`
|
|
|
1517
1326
|
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`);
|
|
1518
1327
|
}
|
|
1519
1328
|
get #restartPending() {
|
|
1520
|
-
return !!(this.#pendingRecycle || this.#restartTimer || this.#backendRecoveryTimer || this.#builderRecoveryTimer || this.#rssRecycleReason);
|
|
1329
|
+
return !!(this.#pendingRecycle || this.#restartTimer || this.#backendRestart || this.#backendRecoveryTimer || this.#builderRecoveryTimer || this.#rssRecycleReason);
|
|
1521
1330
|
}
|
|
1522
1331
|
async#startIdleWatcher() {
|
|
1523
1332
|
try {
|
|
@@ -1584,16 +1393,14 @@ ${this.#backendStderrTail.join(`
|
|
|
1584
1393
|
if (shouldRefreshConfigOnIdleWake(batch)) {
|
|
1585
1394
|
this.logger.verbose("[idle-suspend] config changed while suspended; restarting the dev host");
|
|
1586
1395
|
this.#discardBuilderGap("config change replaces the backend anyway");
|
|
1587
|
-
await this.#
|
|
1588
|
-
refreshConfig: true
|
|
1589
|
-
});
|
|
1396
|
+
await this.#applyRecycle({ type: "invalidate", kinds: [...batch?.kinds ?? []], files }, true);
|
|
1590
1397
|
return;
|
|
1591
1398
|
}
|
|
1592
1399
|
if (files.length > 0)
|
|
1593
1400
|
await this.#backendGraph.refresh();
|
|
1594
1401
|
await this.#startBuilder({ announceBootState: true });
|
|
1595
1402
|
const missed = await this.#takeBuilderGapChanges();
|
|
1596
|
-
const backendFiles = [...new Set([...files.filter((file) => this.#
|
|
1403
|
+
const backendFiles = [...new Set([...files.filter((file) => this.#backendGraph.has(file)), ...missed])];
|
|
1597
1404
|
if (backendFiles.length === 0)
|
|
1598
1405
|
return;
|
|
1599
1406
|
this.logger.verbose(`[idle-suspend] ${backendFiles.length} backend file(s) changed while suspended`);
|
|
@@ -1648,10 +1455,6 @@ ${this.#backendStderrTail.join(`
|
|
|
1648
1455
|
for (const message of pending)
|
|
1649
1456
|
this.#sendToBuilder(message);
|
|
1650
1457
|
}
|
|
1651
|
-
#holdUntilBuilderReady(message) {
|
|
1652
|
-
this.#pendingBuilderMessages.push(message);
|
|
1653
|
-
this.logger.verbose(`[builder] holding ${message.type} until the builder is ready (${this.#pendingBuilderMessages.length} waiting)`);
|
|
1654
|
-
}
|
|
1655
1458
|
#discardPendingBuilderMessages(reason) {
|
|
1656
1459
|
const held = this.#pendingBuilderMessages.splice(0);
|
|
1657
1460
|
if (held.length === 0)
|
|
@@ -1663,12 +1466,17 @@ ${this.#backendStderrTail.join(`
|
|
|
1663
1466
|
if (held.length === 0)
|
|
1664
1467
|
return;
|
|
1665
1468
|
this.logger.warn(`failing ${held.length} held builder request(s): ${reason}`);
|
|
1666
|
-
for (const message of held)
|
|
1667
|
-
|
|
1668
|
-
|
|
1669
|
-
|
|
1670
|
-
|
|
1671
|
-
|
|
1469
|
+
for (const message of held)
|
|
1470
|
+
this.#failBuilderRequest(message, reason);
|
|
1471
|
+
}
|
|
1472
|
+
#failBuilderRequest(message, error) {
|
|
1473
|
+
if (message.type === "build-route")
|
|
1474
|
+
this.#sendToBackend({ type: "build-route-res", id: message.id, ok: false, error });
|
|
1475
|
+
else if (message.type === "build-csr")
|
|
1476
|
+
this.#sendToBackend({ type: "build-csr-res", id: message.id, ok: false, error });
|
|
1477
|
+
else
|
|
1478
|
+
return false;
|
|
1479
|
+
return true;
|
|
1672
1480
|
}
|
|
1673
1481
|
#recycleBuilderForRss(reason) {
|
|
1674
1482
|
if (this.#pendingRecycle || this.#restartTimer) {
|
|
@@ -1686,7 +1494,8 @@ ${this.#backendStderrTail.join(`
|
|
|
1686
1494
|
this.#armRssRecycle(this.#rssRecycleReason);
|
|
1687
1495
|
const wantsDevHostRestart = shouldRestartDevHostByDevPlan(message);
|
|
1688
1496
|
const pending = this.#pendingRecycle;
|
|
1689
|
-
|
|
1497
|
+
const resumes = !!pending && message.kinds.includes("code") && this.#touchesFailedRecycle(message.files);
|
|
1498
|
+
if (wantsDevHostRestart || shouldRestartBuilderByDevPlan(message) || resumes) {
|
|
1690
1499
|
const refreshConfig = wantsDevHostRestart || (pending?.refreshConfig ?? false);
|
|
1691
1500
|
const merged = pending ? mergeInvalidateMessages(pending.message, message) : message;
|
|
1692
1501
|
const generation = message.devPlan?.generation ?? message.generation;
|
|
@@ -1694,16 +1503,7 @@ ${this.#backendStderrTail.join(`
|
|
|
1694
1503
|
this.#deferRecycle(merged, { refreshConfig, generation });
|
|
1695
1504
|
return;
|
|
1696
1505
|
}
|
|
1697
|
-
this.#
|
|
1698
|
-
try {
|
|
1699
|
-
if (refreshConfig)
|
|
1700
|
-
await this.#restartDevHost(merged);
|
|
1701
|
-
else
|
|
1702
|
-
await this.#restartDevChildren(merged);
|
|
1703
|
-
} catch (err) {
|
|
1704
|
-
this.#recordDevHostRestartFailure(merged, err, refreshConfig ? "Config" : "Runtime metadata");
|
|
1705
|
-
this.#resurrectDevChildren(merged);
|
|
1706
|
-
}
|
|
1506
|
+
await this.#applyRecycle(merged, refreshConfig);
|
|
1707
1507
|
return;
|
|
1708
1508
|
}
|
|
1709
1509
|
if (await this.#shouldRestartBackend(message)) {
|
|
@@ -1712,6 +1512,41 @@ ${this.#backendStderrTail.join(`
|
|
|
1712
1512
|
}
|
|
1713
1513
|
this.#sendToBackend(message);
|
|
1714
1514
|
}
|
|
1515
|
+
async#applyRecycle(message, refreshConfig) {
|
|
1516
|
+
if (this.#stopping)
|
|
1517
|
+
return;
|
|
1518
|
+
this.#pendingRecycle = null;
|
|
1519
|
+
const progress = { applied: !refreshConfig };
|
|
1520
|
+
try {
|
|
1521
|
+
if (refreshConfig)
|
|
1522
|
+
await this.#restartDevHost(message, progress);
|
|
1523
|
+
else
|
|
1524
|
+
await this.#restartDevChildren(message);
|
|
1525
|
+
} catch (err) {
|
|
1526
|
+
const kind = refreshConfig ? "Config" : "Runtime metadata";
|
|
1527
|
+
const generation = this.#recordDevHostRestartFailure(message, err, kind, { applied: progress.applied });
|
|
1528
|
+
if (progress.applied)
|
|
1529
|
+
this.#restartFailedAfterApply = generation;
|
|
1530
|
+
else
|
|
1531
|
+
this.#pendingRecycle = { message, refreshConfig, failed: true, failedGeneration: generation };
|
|
1532
|
+
this.#resurrectDevChildren(message);
|
|
1533
|
+
}
|
|
1534
|
+
}
|
|
1535
|
+
async#resumeFailedRecycle(status) {
|
|
1536
|
+
const pending = this.#pendingRecycle;
|
|
1537
|
+
if (!pending?.failed || !status.ok || status.phase !== "pages" || !this.#touchesFailedRecycle(status.files))
|
|
1538
|
+
return;
|
|
1539
|
+
if (status.generation <= (pending.failedGeneration ?? -1))
|
|
1540
|
+
return;
|
|
1541
|
+
await this.#applyRecycle(pending.message, pending.refreshConfig);
|
|
1542
|
+
}
|
|
1543
|
+
#touchesFailedRecycle(files) {
|
|
1544
|
+
const pending = this.#pendingRecycle;
|
|
1545
|
+
if (!pending?.failed)
|
|
1546
|
+
return true;
|
|
1547
|
+
const changed = new Set(pending.message.files);
|
|
1548
|
+
return files.some((file) => changed.has(file));
|
|
1549
|
+
}
|
|
1715
1550
|
#deferRecycle(message, { refreshConfig, generation }) {
|
|
1716
1551
|
this.#pendingRecycle = { message, refreshConfig };
|
|
1717
1552
|
const kind = refreshConfig ? "Config" : "Runtime metadata";
|
|
@@ -1757,8 +1592,10 @@ ${this.#backendStderrTail.join(`
|
|
|
1757
1592
|
this.#builderRecoveryAttempts = 0;
|
|
1758
1593
|
this.logger.info("[builder-recovery] builder recovered");
|
|
1759
1594
|
this.#restartBackendForGapChanges();
|
|
1595
|
+
const failedAfterApply = this.#restartFailedAfterApply;
|
|
1596
|
+
this.#restartFailedAfterApply = null;
|
|
1760
1597
|
const status = {
|
|
1761
|
-
generation: reason.generation ?? this.#nextBackendBuildStatusGeneration(),
|
|
1598
|
+
generation: failedAfterApply !== null ? this.#nextBackendBuildStatusGeneration(failedAfterApply + 1) : reason.generation ?? this.#nextBackendBuildStatusGeneration(),
|
|
1762
1599
|
phase: "scan",
|
|
1763
1600
|
ok: true,
|
|
1764
1601
|
files: reason.files,
|
|
@@ -1782,36 +1619,29 @@ ${this.#backendStderrTail.join(`
|
|
|
1782
1619
|
this.logger.warn(`[dev-host] recycling builder/backend for runtime metadata generation=${generation ?? "(unknown)"} files=${message.files.length}`);
|
|
1783
1620
|
await this.#recycleDevChildren(message);
|
|
1784
1621
|
}
|
|
1785
|
-
async#restartDevHost(message) {
|
|
1622
|
+
async#restartDevHost(message, progress) {
|
|
1786
1623
|
const generation = message.devPlan?.generation ?? message.generation;
|
|
1787
1624
|
this.logger.warn(`[dev-host] config change detected; restarting dev host generation=${generation ?? "(unknown)"} files=${message.files.length}`);
|
|
1788
|
-
await this.#recycleDevChildren(message, { refreshConfig: true });
|
|
1625
|
+
await this.#recycleDevChildren(message, { refreshConfig: true, progress });
|
|
1789
1626
|
}
|
|
1790
|
-
async#recycleDevChildren(message, { refreshConfig = false } = {}) {
|
|
1627
|
+
async#recycleDevChildren(message, { refreshConfig = false, progress } = {}) {
|
|
1791
1628
|
const generation = message.devPlan?.generation ?? message.generation;
|
|
1792
|
-
|
|
1793
|
-
clearTimeout(this.#restartTimer);
|
|
1794
|
-
this.#restartTimer = null;
|
|
1795
|
-
}
|
|
1796
|
-
if (this.#backendRecoveryTimer) {
|
|
1797
|
-
clearTimeout(this.#backendRecoveryTimer);
|
|
1798
|
-
this.#backendRecoveryTimer = null;
|
|
1799
|
-
}
|
|
1800
|
-
if (this.#builderRecoveryTimer) {
|
|
1801
|
-
clearTimeout(this.#builderRecoveryTimer);
|
|
1802
|
-
this.#builderRecoveryTimer = null;
|
|
1803
|
-
}
|
|
1629
|
+
this.#clearRestartTimers();
|
|
1804
1630
|
this.#builderRecoveryAttempts = 0;
|
|
1805
1631
|
this.#pendingRestartReason = null;
|
|
1632
|
+
await this.#backendRestart;
|
|
1806
1633
|
this.#lastGoodFrontend = {};
|
|
1807
1634
|
this.#buildStatusByPhase.clear();
|
|
1808
1635
|
this.#pendingBuildStatusReplay = [];
|
|
1809
1636
|
await this.#stopBackend();
|
|
1810
1637
|
this.#stopBuilder();
|
|
1638
|
+
this.#patcherOff = false;
|
|
1811
1639
|
if (refreshConfig) {
|
|
1812
1640
|
await this.app.getConfig({ refresh: true });
|
|
1813
1641
|
const { env } = await this.app.prepareCommand("start");
|
|
1814
1642
|
Object.assign(this.env, env);
|
|
1643
|
+
if (progress)
|
|
1644
|
+
progress.applied = true;
|
|
1815
1645
|
}
|
|
1816
1646
|
await this.#backendGraph.refresh();
|
|
1817
1647
|
await this.#startBuilder();
|
|
@@ -1830,22 +1660,27 @@ ${this.#backendStderrTail.join(`
|
|
|
1830
1660
|
this.#lastGoodFrontend.css = message;
|
|
1831
1661
|
this.logger.verbose(`[last-good] css generation=${message.data.generation ?? "(unknown)"} assets=${Object.keys(message.data.cssAssets).length}`);
|
|
1832
1662
|
}
|
|
1833
|
-
#recordDevHostRestartFailure(message, err, kind) {
|
|
1663
|
+
#recordDevHostRestartFailure(message, err, kind, { applied }) {
|
|
1834
1664
|
const generation = message.devPlan?.generation ?? message.generation ?? this.#nextBackendBuildStatusGeneration();
|
|
1835
1665
|
const detail = err instanceof Error ? err.message : String(err);
|
|
1836
1666
|
this.logger.warn(`[dev-host] ${kind.toLowerCase()} restart failed generation=${generation}: ${detail}`);
|
|
1667
|
+
const names = message.files.slice(0, 3).map((file) => path3.basename(file));
|
|
1668
|
+
const saved = names.length > 0 ? names.join(", ") : "the changed file";
|
|
1837
1669
|
const status = {
|
|
1838
1670
|
generation,
|
|
1839
1671
|
phase: "scan",
|
|
1840
1672
|
ok: false,
|
|
1841
1673
|
files: message.files,
|
|
1842
|
-
message: `${kind} change failed to
|
|
1674
|
+
message: applied ? `${kind} change applied, but the dev server failed to restart: ${detail}. It is recovering on its own.` : `${kind} change failed to apply: ${detail}. The dev server keeps running on the previous ${kind.toLowerCase()}; once it is fixed, save ${saved} again or restart the dev server to apply it.`
|
|
1843
1675
|
};
|
|
1844
1676
|
this.#recordBuildStatus(status);
|
|
1845
1677
|
this.#sendOrQueueBuildStatus(status);
|
|
1678
|
+
return generation;
|
|
1846
1679
|
}
|
|
1847
1680
|
#recordBuildStatus(status) {
|
|
1848
1681
|
const recovered = shouldMarkBuildPhaseRecovered(this.#buildStatusByPhase, status);
|
|
1682
|
+
if (shouldKeepBuildFailure(this.#buildStatusByPhase, status))
|
|
1683
|
+
return;
|
|
1849
1684
|
this.#buildStatusByPhase.set(status.phase, status);
|
|
1850
1685
|
const label = `[build-status] generation=${status.generation} phase=${status.phase} ok=${status.ok} files=${status.files.length}`;
|
|
1851
1686
|
if (status.ok)
|
|
@@ -1861,6 +1696,10 @@ ${this.#backendStderrTail.join(`
|
|
|
1861
1696
|
}
|
|
1862
1697
|
this.#sendToBackend({ type: "build-status", data: status });
|
|
1863
1698
|
}
|
|
1699
|
+
#forgetRouteBuildStatus() {
|
|
1700
|
+
this.#buildStatusByPhase.delete("route");
|
|
1701
|
+
this.#pendingBuildStatusReplay = this.#pendingBuildStatusReplay.filter((status) => status.phase !== "route");
|
|
1702
|
+
}
|
|
1864
1703
|
#replayBuilderState() {
|
|
1865
1704
|
if (!this.#backendReady)
|
|
1866
1705
|
return;
|
|
@@ -1890,7 +1729,7 @@ ${this.#backendStderrTail.join(`
|
|
|
1890
1729
|
}
|
|
1891
1730
|
if (message.kinds.includes("code"))
|
|
1892
1731
|
await this.#backendGraph.refresh();
|
|
1893
|
-
if (message.files.some((file) => this.#
|
|
1732
|
+
if (message.files.some((file) => this.#backendGraph.has(file)))
|
|
1894
1733
|
return true;
|
|
1895
1734
|
if (!this.#backendGraph.lastRefreshSucceeded) {
|
|
1896
1735
|
const fallbackFiles = message.files.filter((file) => isLegacyBackendFallbackFile(file, this.app.workspace.workspaceRoot));
|
|
@@ -1901,9 +1740,6 @@ ${this.#backendStderrTail.join(`
|
|
|
1901
1740
|
}
|
|
1902
1741
|
return false;
|
|
1903
1742
|
}
|
|
1904
|
-
#isBackendFile(file) {
|
|
1905
|
-
return this.#backendGraph.has(file);
|
|
1906
|
-
}
|
|
1907
1743
|
async#startBuilder({
|
|
1908
1744
|
announceBootState = false
|
|
1909
1745
|
} = {}) {
|
|
@@ -1915,7 +1751,8 @@ ${this.#backendStderrTail.join(`
|
|
|
1915
1751
|
this.#enqueueBuilderMessage(msg);
|
|
1916
1752
|
}, {
|
|
1917
1753
|
stdio: this.stdio,
|
|
1918
|
-
onOutput: (kind, text) => void (kind === "stderr" ? process.stderr : process.stdout).write(text)
|
|
1754
|
+
onOutput: (kind, text) => void (kind === "stderr" ? process.stderr : process.stdout).write(text),
|
|
1755
|
+
patcherOff: this.#patcherOff
|
|
1919
1756
|
});
|
|
1920
1757
|
try {
|
|
1921
1758
|
await this.#waitForBuilderReady(attempt, { announceBootState });
|
|
@@ -1989,33 +1826,20 @@ ${this.#backendStderrTail.join(`
|
|
|
1989
1826
|
return;
|
|
1990
1827
|
const status = this.#builder?.status ?? "stopped";
|
|
1991
1828
|
if (shouldHoldForReturningBuilder({ status, heldCount: this.#pendingBuilderMessages.length })) {
|
|
1992
|
-
this.#
|
|
1993
|
-
|
|
1994
|
-
}
|
|
1995
|
-
if (message.type === "build-route") {
|
|
1996
|
-
this.#sendToBackend({
|
|
1997
|
-
type: "build-route-res",
|
|
1998
|
-
id: message.id,
|
|
1999
|
-
ok: false,
|
|
2000
|
-
error: `builder is ${status}; reload after the builder is ready`
|
|
2001
|
-
});
|
|
1829
|
+
this.#pendingBuilderMessages.push(message);
|
|
1830
|
+
this.logger.verbose(`[builder] holding ${message.type} until the builder is ready (${this.#pendingBuilderMessages.length} waiting)`);
|
|
2002
1831
|
return;
|
|
2003
1832
|
}
|
|
2004
|
-
if (message
|
|
2005
|
-
this.#sendToBackend({
|
|
2006
|
-
type: "build-csr-res",
|
|
2007
|
-
id: message.id,
|
|
2008
|
-
ok: false,
|
|
2009
|
-
error: `builder is ${status}; reload after the builder is ready`
|
|
2010
|
-
});
|
|
1833
|
+
if (this.#failBuilderRequest(message, `builder is ${status}; reload after the builder is ready`))
|
|
2011
1834
|
return;
|
|
2012
|
-
}
|
|
2013
1835
|
this.logger.warn("akanAppHost builder is not running");
|
|
2014
1836
|
}
|
|
2015
1837
|
#stopBuilder() {
|
|
2016
1838
|
this.#cancelRssRecycle();
|
|
2017
1839
|
if (!this.#builder)
|
|
2018
1840
|
return;
|
|
1841
|
+
if (this.#builder.patcherOff)
|
|
1842
|
+
this.#patcherOff = true;
|
|
2019
1843
|
this.#builder.stop();
|
|
2020
1844
|
this.#builder = null;
|
|
2021
1845
|
}
|
|
@@ -2025,21 +1849,16 @@ import path4 from "path";
|
|
|
2025
1849
|
var SIGNAL_TEST_PRELOAD_PATH = "test/signalTest.preload.ts";
|
|
2026
1850
|
async function resolveSignalTestPreloadPath(target) {
|
|
2027
1851
|
const candidates = [];
|
|
2028
|
-
const
|
|
1852
|
+
for (const basePath of [target.cwdPath, process.cwd(), path4.dirname(Bun.main), import.meta.dir]) {
|
|
2029
1853
|
try {
|
|
2030
1854
|
candidates.push(path4.join(path4.dirname(Bun.resolveSync("akanjs/package.json", basePath)), SIGNAL_TEST_PRELOAD_PATH));
|
|
2031
1855
|
} catch {}
|
|
2032
|
-
}
|
|
2033
|
-
addResolvedPackageCandidate(target.cwdPath);
|
|
2034
|
-
addResolvedPackageCandidate(process.cwd());
|
|
2035
|
-
addResolvedPackageCandidate(path4.dirname(Bun.main));
|
|
2036
|
-
addResolvedPackageCandidate(import.meta.dir);
|
|
1856
|
+
}
|
|
2037
1857
|
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));
|
|
2038
1858
|
const uniqueCandidates = [...new Set(candidates)];
|
|
2039
|
-
for (const candidate of uniqueCandidates)
|
|
1859
|
+
for (const candidate of uniqueCandidates)
|
|
2040
1860
|
if (await Bun.file(candidate).exists())
|
|
2041
1861
|
return candidate;
|
|
2042
|
-
}
|
|
2043
1862
|
throw new Error(`Failed to locate ${SIGNAL_TEST_PRELOAD_PATH} from ${target.cwdPath}.
|
|
2044
1863
|
Probed paths:
|
|
2045
1864
|
${uniqueCandidates.map((candidate) => ` - ${candidate}`).join(`
|
|
@@ -2047,21 +1866,17 @@ ${uniqueCandidates.map((candidate) => ` - ${candidate}`).join(`
|
|
|
2047
1866
|
}
|
|
2048
1867
|
|
|
2049
1868
|
// pkgs/@akanjs/cli/application/application.runner.ts
|
|
2050
|
-
import { Logger as
|
|
2051
|
-
|
|
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-gmkc0z8j.js")).AbstractCompactor;
|
|
1869
|
+
import { Logger as Logger3 } from "akanjs/common";
|
|
1870
|
+
var loadBuildRunner = async () => (await import("./applicationBuildRunner-75n5rpwp.js")).ApplicationBuildRunner;
|
|
2056
1871
|
var loadPrompts = async () => await import("@inquirer/prompts");
|
|
2057
1872
|
|
|
2058
1873
|
class ApplicationRunner extends runner("application") {
|
|
2059
|
-
async createApplication(appName, workspace, libs = []) {
|
|
1874
|
+
async createApplication(appName, workspace, libs = [], { sample = false } = {}) {
|
|
2060
1875
|
await workspace.applyTemplate({
|
|
2061
1876
|
basePath: `apps/${appName}`,
|
|
2062
1877
|
template: "app",
|
|
2063
1878
|
dict: { appName },
|
|
2064
|
-
options: { libs }
|
|
1879
|
+
options: { libs, sample }
|
|
2065
1880
|
});
|
|
2066
1881
|
return AppExecutor.from(workspace, appName);
|
|
2067
1882
|
}
|
|
@@ -2071,9 +1886,6 @@ class ApplicationRunner extends runner("application") {
|
|
|
2071
1886
|
async removeApplication(app) {
|
|
2072
1887
|
await app.workspace.removeDir(`apps/${app.name}`);
|
|
2073
1888
|
}
|
|
2074
|
-
async getConfig(app) {
|
|
2075
|
-
return await app.getConfig();
|
|
2076
|
-
}
|
|
2077
1889
|
async getScriptFilename(app) {
|
|
2078
1890
|
if (!await app.exists("script")) {
|
|
2079
1891
|
await app.mkdir("script");
|
|
@@ -2095,7 +1907,7 @@ class ApplicationRunner extends runner("application") {
|
|
|
2095
1907
|
if (!await app.exists(scriptPath))
|
|
2096
1908
|
throw new Error(`Script file not found: apps/${app.name}/${scriptPath}`);
|
|
2097
1909
|
await app.spawn("bun", [scriptPath], {
|
|
2098
|
-
env: app.getCommandEnv({ AKAN_COMMAND_TYPE: "script" }),
|
|
1910
|
+
env: app.getCommandEnv({ AKAN_COMMAND_TYPE: "script", ...await app.getDatabaseModeEnv() }),
|
|
2099
1911
|
stdio: "inherit"
|
|
2100
1912
|
});
|
|
2101
1913
|
}
|
|
@@ -2110,13 +1922,10 @@ class ApplicationRunner extends runner("application") {
|
|
|
2110
1922
|
const { level, grep, endpoint, trace, child, role, origin, since } = options;
|
|
2111
1923
|
const query = Object.fromEntries(Object.entries({ level, grep, endpoint, trace, child, role, origin, since }).filter(([, value]) => value !== undefined && value !== null && value !== ""));
|
|
2112
1924
|
const print = (record) => process.stdout.write(options.json ? `${JSON.stringify(record)}
|
|
2113
|
-
` :
|
|
1925
|
+
` : Logger3.render(record));
|
|
2114
1926
|
const note = (text) => process.stderr.write(`[akan logs] ${text}
|
|
2115
1927
|
`);
|
|
2116
|
-
|
|
2117
|
-
const closed = new Promise((resolve) => {
|
|
2118
|
-
finish = resolve;
|
|
2119
|
-
});
|
|
1928
|
+
const { promise: closed, resolve: finish } = Promise.withResolvers();
|
|
2120
1929
|
let client;
|
|
2121
1930
|
try {
|
|
2122
1931
|
client = await LogTailClient.connect(socketPath, {
|
|
@@ -2125,7 +1934,7 @@ class ApplicationRunner extends runner("application") {
|
|
|
2125
1934
|
if (response.type === "dropped")
|
|
2126
1935
|
note(`${response.count} records dropped (reader too slow)`);
|
|
2127
1936
|
},
|
|
2128
|
-
onClose: () => finish
|
|
1937
|
+
onClose: () => finish()
|
|
2129
1938
|
});
|
|
2130
1939
|
} catch (error) {
|
|
2131
1940
|
if (error instanceof LogControlUnavailableError)
|
|
@@ -2167,7 +1976,7 @@ const serverModule = await import(${JSON.stringify(serverPath)});
|
|
|
2167
1976
|
const { assertAkanConsoleAllowed, startAkanConsole } = await import("akanjs/server");
|
|
2168
1977
|
const server = serverModule.server;
|
|
2169
1978
|
if (!server?.start) throw new Error("server.ts must export server with start()");
|
|
2170
|
-
assertAkanConsoleAllowed(
|
|
1979
|
+
assertAkanConsoleAllowed();
|
|
2171
1980
|
await server.start({ listen: false, web: false });
|
|
2172
1981
|
try {
|
|
2173
1982
|
await startAkanConsole(server, {
|
|
@@ -2186,17 +1995,35 @@ try {
|
|
|
2186
1995
|
}
|
|
2187
1996
|
`;
|
|
2188
1997
|
await app.spawn("bun", ["-e", code], {
|
|
2189
|
-
env: app.getCommandEnv({ AKAN_COMMAND_TYPE: "console" }),
|
|
1998
|
+
env: app.getCommandEnv({ AKAN_COMMAND_TYPE: "console", ...await app.getDatabaseModeEnv() }),
|
|
1999
|
+
stdio: "inherit"
|
|
2000
|
+
});
|
|
2001
|
+
}
|
|
2002
|
+
async transferDatabase(app, direction, dir) {
|
|
2003
|
+
const serverPath = `${app.cwdPath}/server.ts`;
|
|
2004
|
+
if (!await app.exists("server.ts"))
|
|
2005
|
+
throw new Error(`Server file not found: apps/${app.name}/server.ts`);
|
|
2006
|
+
const target = path5.resolve(app.workspace.workspaceRoot, dir);
|
|
2007
|
+
const code = `
|
|
2008
|
+
const { server } = await import(${JSON.stringify(serverPath)});
|
|
2009
|
+
const { DatabaseAdaptorRole, DocumentTransfer } = await import("akanjs/service");
|
|
2010
|
+
await server.start({ listen: false, web: false });
|
|
2011
|
+
try {
|
|
2012
|
+
const transfer = new DocumentTransfer(server.get(DatabaseAdaptorRole));
|
|
2013
|
+
const reports = await transfer.${direction === "export" ? "exportTo" : "importFrom"}(${JSON.stringify(target)});
|
|
2014
|
+
for (const { table, rows } of reports) console.info(\`${direction === "export" ? "exported" : "imported"} \${table}: \${rows} rows\`);
|
|
2015
|
+
} finally {
|
|
2016
|
+
await server.stop();
|
|
2017
|
+
}
|
|
2018
|
+
`;
|
|
2019
|
+
await app.spawn("bun", ["-e", code], {
|
|
2020
|
+
env: app.getCommandEnv({ AKAN_COMMAND_TYPE: "script", ...await app.getDatabaseModeEnv() }),
|
|
2190
2021
|
stdio: "inherit"
|
|
2191
2022
|
});
|
|
2192
2023
|
}
|
|
2193
2024
|
async typecheck(app, options = {}) {
|
|
2194
2025
|
await new (await loadBuildRunner())(app).typecheck(options);
|
|
2195
2026
|
}
|
|
2196
|
-
async compact(sys, { module, minLines, interactive } = {}) {
|
|
2197
|
-
const compactor = new (await loadAbstractCompactor())(sys, { minLines, interactive });
|
|
2198
|
-
return await compactor.compactAll({ module });
|
|
2199
|
-
}
|
|
2200
2027
|
async test(exec) {
|
|
2201
2028
|
const isSignalTarget = exec instanceof AppExecutor || exec instanceof LibExecutor;
|
|
2202
2029
|
const preloadPath = isSignalTarget ? await resolveSignalTestPreloadPath(exec) : null;
|
|
@@ -2204,7 +2031,8 @@ try {
|
|
|
2204
2031
|
AKAN_TEST_SIGNAL: "1",
|
|
2205
2032
|
AKAN_TEST_TARGET_TYPE: exec.type,
|
|
2206
2033
|
AKAN_TEST_TARGET_NAME: exec.name,
|
|
2207
|
-
AKAN_TEST_LIBS: exec.getScanInfo({ allowEmpty: true })?.getLibs().join(",") ?? ""
|
|
2034
|
+
AKAN_TEST_LIBS: exec.getScanInfo({ allowEmpty: true })?.getLibs().join(",") ?? "",
|
|
2035
|
+
AKAN_RUNTIME_DIR: path5.join(exec.workspace.workspaceRoot, "local", `${exec.type}s`, exec.name, "runtime")
|
|
2208
2036
|
} : {};
|
|
2209
2037
|
const args = preloadPath ? ["test", "--isolate", "--preload", preloadPath] : ["test", "--isolate"];
|
|
2210
2038
|
await exec.spawn("bun", args, {
|
|
@@ -2232,72 +2060,188 @@ try {
|
|
|
2232
2060
|
setTimeout(() => openBrowser(`http://localhost:${env.AKAN_PUBLIC_CLIENT_PORT ?? env.PORT ?? "8282"}`), 3000);
|
|
2233
2061
|
return appHost;
|
|
2234
2062
|
}
|
|
2235
|
-
async
|
|
2063
|
+
async buildMobile(app, platform, { target, env = "debug", profile = "release", installer = false, arch } = {}) {
|
|
2236
2064
|
const targets = await resolveMobileTargets(app, target);
|
|
2237
|
-
|
|
2065
|
+
NativeApp.assertInstaller(platform, installer);
|
|
2066
|
+
if (arch && platform !== "macos" && platform !== "windows" && platform !== "linux")
|
|
2067
|
+
throw new Error(`--arch picks a desktop app's CPU; ${platform} builds for its own.`);
|
|
2068
|
+
if (arch === "x64" && platform === "macos")
|
|
2069
|
+
throw new Error("A macOS app is built for Apple silicon (arm64) only.");
|
|
2070
|
+
const carried = await this.#stageMobile(app, platform, targets, env, arch);
|
|
2238
2071
|
await this.#runMobileTargets(targets, async (mobileTarget) => {
|
|
2239
|
-
await new (
|
|
2072
|
+
this.#reportBuild(app, mobileTarget, await new NativeApp(app, mobileTarget, env).build(platform, {
|
|
2073
|
+
profile,
|
|
2074
|
+
...carried && ApplicationRunner.carriesServer(mobileTarget, platform) ? { server: carried } : {},
|
|
2075
|
+
...installer ? { installer } : {},
|
|
2076
|
+
...arch ? { arch } : {}
|
|
2077
|
+
}));
|
|
2240
2078
|
});
|
|
2241
2079
|
}
|
|
2242
|
-
async
|
|
2243
|
-
|
|
2244
|
-
|
|
2245
|
-
|
|
2246
|
-
target
|
|
2247
|
-
|
|
2248
|
-
|
|
2249
|
-
|
|
2250
|
-
|
|
2251
|
-
|
|
2252
|
-
|
|
2253
|
-
|
|
2254
|
-
|
|
2255
|
-
|
|
2256
|
-
await
|
|
2257
|
-
|
|
2258
|
-
|
|
2259
|
-
}
|
|
2080
|
+
async buildDesktop(app, options = {}) {
|
|
2081
|
+
await this.buildMobile(app, NativeApp.desktopPlatform(), options);
|
|
2082
|
+
}
|
|
2083
|
+
async startMobile(app, platform, { target, env = "local", operation = "local", device, teamId, interrupt } = {}) {
|
|
2084
|
+
const mobileTarget = await ApplicationRunner.startTarget(app, platform, target);
|
|
2085
|
+
const nativeApp = new NativeApp(app, mobileTarget, env);
|
|
2086
|
+
const selection = { ...device ? { device } : {}, ...teamId ? { teamId } : {} };
|
|
2087
|
+
if (operation === "release") {
|
|
2088
|
+
const carried = await this.#stageMobile(app, platform, [mobileTarget], env);
|
|
2089
|
+
const running = await nativeApp.run(platform, {
|
|
2090
|
+
...selection,
|
|
2091
|
+
profile: "release",
|
|
2092
|
+
...carried ? { server: carried } : {}
|
|
2093
|
+
});
|
|
2094
|
+
await running.exited;
|
|
2095
|
+
return;
|
|
2096
|
+
}
|
|
2097
|
+
const upstream = `http://localhost:${await app.getDevPort()}`;
|
|
2098
|
+
if (!await ApplicationRunner.answers(upstream, app.name, app.workspace.workspaceRoot))
|
|
2099
|
+
throw new Error(`No dev server answers on ${upstream}; run \`akan start ${app.name}\` first.`);
|
|
2100
|
+
const { i18n } = await app.getConfig();
|
|
2101
|
+
const session = await nativeApp.dev(platform, { upstream, lang: i18n.defaultLocale, ...selection });
|
|
2102
|
+
if (interrupt)
|
|
2103
|
+
interrupt.add(async () => await session.stop(), "Abandoning the app's shutdown; its window may stay open.", 130);
|
|
2104
|
+
else
|
|
2105
|
+
process.once("SIGINT", () => {
|
|
2106
|
+
session.stop().finally(() => process.exit(130));
|
|
2107
|
+
});
|
|
2108
|
+
await session.exited;
|
|
2260
2109
|
}
|
|
2261
|
-
async
|
|
2110
|
+
async startDesktop(app, options = {}) {
|
|
2111
|
+
await this.startMobile(app, NativeApp.desktopPlatform(), options);
|
|
2112
|
+
}
|
|
2113
|
+
static async startTarget(app, platform = NativeApp.desktopPlatform(), target) {
|
|
2262
2114
|
const targets = await resolveMobileTargets(app, target);
|
|
2263
|
-
|
|
2264
|
-
|
|
2265
|
-
|
|
2115
|
+
const [mobileTarget] = targets;
|
|
2116
|
+
if (!mobileTarget || targets.length > 1)
|
|
2117
|
+
throw new Error(`start-${platform === "ios" || platform === "android" ? platform : "desktop"} runs one native target at a time; pass --target <name>.`);
|
|
2118
|
+
return mobileTarget;
|
|
2119
|
+
}
|
|
2120
|
+
static async answers(url, appName, workspaceRoot) {
|
|
2121
|
+
const signal = AbortSignal.timeout(3000);
|
|
2122
|
+
const health = await fetch(new URL("/_akan/app/health", url), { signal }).catch(() => null);
|
|
2123
|
+
if (!health)
|
|
2124
|
+
return false;
|
|
2125
|
+
const info = await fetch(new URL("/_akan/app/info", url), { signal }).then(async (res) => res.ok ? await res.json() : null).catch(() => null);
|
|
2126
|
+
const stopIt = `Stop it (\`akan start ${appName} --kill\` takes the port over) or give ${appName} another port with AKAN_DEV_PORT`;
|
|
2127
|
+
if (info?.appName === appName) {
|
|
2128
|
+
if (typeof info.workspaceRoot !== "string" || ApplicationRunner.#sameCheckout(info.workspaceRoot, workspaceRoot))
|
|
2129
|
+
return true;
|
|
2130
|
+
throw new Error(`${url} is the dev server of ${appName} in ${info.workspaceRoot}, not in ${workspaceRoot}. ${stopIt}.`);
|
|
2266
2131
|
}
|
|
2132
|
+
if (typeof info?.appName === "string")
|
|
2133
|
+
throw new Error(`${url} is the dev server of ${info.appName}, not ${appName}. ${stopIt}.`);
|
|
2134
|
+
const { pid } = await health.json().catch(() => null) ?? {};
|
|
2135
|
+
throw new Error(typeof pid === "number" ? `${url} is an akan dev server (pid ${pid}) too old to say which app it serves. ${stopIt}.` : `${url} answers, but not as an akan dev server. Stop what holds the port or give ${appName} another port with AKAN_DEV_PORT.`);
|
|
2267
2136
|
}
|
|
2268
|
-
|
|
2137
|
+
static #sameCheckout(reported, own) {
|
|
2138
|
+
const canonical = (root) => {
|
|
2139
|
+
try {
|
|
2140
|
+
return realpathSync.native(root);
|
|
2141
|
+
} catch {
|
|
2142
|
+
return path5.resolve(root);
|
|
2143
|
+
}
|
|
2144
|
+
};
|
|
2145
|
+
return canonical(reported) === canonical(own);
|
|
2146
|
+
}
|
|
2147
|
+
async releaseIos(app, { target, env = "main", teamId, adHoc = false } = {}) {
|
|
2269
2148
|
const targets = await resolveMobileTargets(app, target);
|
|
2270
2149
|
await this.#buildMobileCsr(app, env);
|
|
2271
|
-
|
|
2272
|
-
await new (
|
|
2273
|
-
});
|
|
2150
|
+
for (const mobileTarget of targets)
|
|
2151
|
+
this.#reportBuild(app, mobileTarget, await new NativeApp(app, mobileTarget, env).releaseIos({ teamId, adHoc }));
|
|
2274
2152
|
}
|
|
2275
|
-
async
|
|
2276
|
-
open = false,
|
|
2277
|
-
operation = "local",
|
|
2278
|
-
env = "local",
|
|
2279
|
-
target,
|
|
2280
|
-
regenerate = false
|
|
2281
|
-
} = {}) {
|
|
2153
|
+
async releaseAndroid(app, format, { target, env = "main" } = {}) {
|
|
2282
2154
|
const targets = await resolveMobileTargets(app, target);
|
|
2283
|
-
|
|
2284
|
-
|
|
2285
|
-
|
|
2286
|
-
|
|
2287
|
-
await capacitorApp.runAndroid({ operation, env, regenerate });
|
|
2288
|
-
if (open)
|
|
2289
|
-
await capacitorApp.openAndroid();
|
|
2290
|
-
});
|
|
2155
|
+
NativeApp.androidSigning();
|
|
2156
|
+
await this.#buildMobileCsr(app, env);
|
|
2157
|
+
for (const mobileTarget of targets)
|
|
2158
|
+
this.#reportBuild(app, mobileTarget, await new NativeApp(app, mobileTarget, env).releaseAndroid({ formats: [format] }));
|
|
2291
2159
|
}
|
|
2292
|
-
async
|
|
2160
|
+
async packUpdate(app, platform, { target, env = "main", out, against }) {
|
|
2293
2161
|
const targets = await resolveMobileTargets(app, target);
|
|
2162
|
+
const [mobileTarget] = targets;
|
|
2163
|
+
if (!mobileTarget || targets.length > 1)
|
|
2164
|
+
throw new Error("pack-update packs one native target at a time; pass --target <name>.");
|
|
2294
2165
|
await this.#buildMobileCsr(app, env);
|
|
2295
|
-
|
|
2296
|
-
|
|
2297
|
-
|
|
2298
|
-
|
|
2166
|
+
const packed = await new NativeApp(app, mobileTarget, env).packUpdate(platform, { out });
|
|
2167
|
+
const size = packed.manifest.files.reduce((sum, file) => sum + file.size, 0);
|
|
2168
|
+
app.logger.info(`packed ${mobileTarget.name} ${platform} for channel '${packed.channel}' to ${packed.out}: ${packed.manifest.files.length} files, ${(size / 1024).toFixed(0)} KiB, native API ${packed.manifest.nativeApi}`);
|
|
2169
|
+
if (!against)
|
|
2170
|
+
return;
|
|
2171
|
+
const bundle = path5.join(packed.out, "bundle.json");
|
|
2172
|
+
const compat = await NativeApp.compareBundles(app.cwdPath, against, bundle);
|
|
2173
|
+
await Bun.write(path5.join(packed.out, "compat.json"), `${JSON.stringify({ against, ...compat }, null, 2)}
|
|
2174
|
+
`);
|
|
2175
|
+
if (!compat.compatible)
|
|
2176
|
+
throw new Error(`This bundle needs a new store build; the one ${against} came from provides another native API:
|
|
2177
|
+
- ${compat.problems.join(`
|
|
2178
|
+
- `)}`);
|
|
2179
|
+
app.logger.info(`runs in the store build of ${against}`);
|
|
2180
|
+
}
|
|
2181
|
+
async updateKeygen(app, platform, target) {
|
|
2182
|
+
const nativePlatform = platform === "desktop" ? NativeApp.desktopPlatform() : platform;
|
|
2183
|
+
const seen = new Set;
|
|
2184
|
+
for (const mobileTarget of await resolveMobileTargets(app, target ?? "all")) {
|
|
2185
|
+
const appId = resolveAppId(mobileTarget.config.appId, nativePlatform);
|
|
2186
|
+
if (seen.has(appId))
|
|
2187
|
+
continue;
|
|
2188
|
+
seen.add(appId);
|
|
2189
|
+
const { publicKey, keyPath, created } = await new NativeApp(app, mobileTarget).updateKeygen(nativePlatform);
|
|
2190
|
+
app.log(`${appId}: ${created ? "made" : "read"} ${keyPath}${created ? " (keep it private and backed up: without it, installed apps take no more updates)" : ""}`);
|
|
2191
|
+
app.log(` native: { updates: { url: "https://\u2026/${app.name}", publicKey: ${JSON.stringify(publicKey)} } }`);
|
|
2192
|
+
const declared = mobileTarget.config.updates?.publicKey;
|
|
2193
|
+
if (declared && declared !== publicKey)
|
|
2194
|
+
app.logger.warn(`native.targets.${mobileTarget.name}.updates.publicKey is another key's; releases would not verify.`);
|
|
2299
2195
|
}
|
|
2300
2196
|
}
|
|
2197
|
+
async publishUpdate(app, platform, { target, env = "main", channel } = {}) {
|
|
2198
|
+
const targets = await resolveMobileTargets(app, target);
|
|
2199
|
+
const nativePlatform = platform === "desktop" ? NativeApp.desktopPlatform() : platform;
|
|
2200
|
+
for (const mobileTarget of targets)
|
|
2201
|
+
await new NativeApp(app, mobileTarget, env).assertPublishable(nativePlatform, {
|
|
2202
|
+
server: ApplicationRunner.carriesServer(mobileTarget, nativePlatform),
|
|
2203
|
+
...channel ? { channel } : {}
|
|
2204
|
+
});
|
|
2205
|
+
const carried = await this.#stageMobile(app, nativePlatform, targets, env);
|
|
2206
|
+
for (const mobileTarget of targets) {
|
|
2207
|
+
const nativeApp = new NativeApp(app, mobileTarget, env);
|
|
2208
|
+
const published = await nativeApp.publishUpdate(nativePlatform, {
|
|
2209
|
+
...channel ? { channel } : {},
|
|
2210
|
+
...carried && ApplicationRunner.carriesServer(mobileTarget, nativePlatform) ? { server: carried } : {}
|
|
2211
|
+
});
|
|
2212
|
+
this.#reportBuild(app, mobileTarget, published.build);
|
|
2213
|
+
app.log(`${app.name}/${mobileTarget.name} ${published.bundle}: ${published.dir}/${published.channel}.json, ${published.files} files, ${Math.round(published.size / 1024)} KiB`);
|
|
2214
|
+
app.log(`Upload ${nativeApp.updatesDir} to ${mobileTarget.config.updates?.url}, the manifests last.`);
|
|
2215
|
+
}
|
|
2216
|
+
}
|
|
2217
|
+
#reportBuild(app, mobileTarget, { artifacts, warnings, signing }) {
|
|
2218
|
+
for (const warning of warnings)
|
|
2219
|
+
app.logger.warn(warning);
|
|
2220
|
+
if (signing)
|
|
2221
|
+
app.log(`Signed with ${signing.identity} (${signing.profile}, team ${signing.teamId})`);
|
|
2222
|
+
for (const artifact of artifacts)
|
|
2223
|
+
app.log(`${app.name}/${mobileTarget.name} ${artifact.kind}: ${artifact.path}`);
|
|
2224
|
+
}
|
|
2225
|
+
static carriesServer({ config }, platform = NativeApp.desktopPlatform()) {
|
|
2226
|
+
return platform !== "ios" && platform !== "android" && !!config.desktop?.server;
|
|
2227
|
+
}
|
|
2228
|
+
static serverOmit(targets, platform) {
|
|
2229
|
+
const carrying = targets.filter((target) => ApplicationRunner.carriesServer(target, platform));
|
|
2230
|
+
const omits = carrying.map(({ config }) => {
|
|
2231
|
+
const server = config.desktop?.server;
|
|
2232
|
+
return typeof server === "object" ? server.omit ?? [] : [];
|
|
2233
|
+
});
|
|
2234
|
+
const [first = [], ...rest] = omits;
|
|
2235
|
+
if (rest.some((omit) => omit.join("\x00") !== first.join("\x00")))
|
|
2236
|
+
throw new Error(`The targets that carry the server omit different packages (${carrying.map(({ name }, idx) => `${name}: ${omits[idx]?.join(", ") || "none"}`).join("; ")}); build them one --target at a time.`);
|
|
2237
|
+
return first;
|
|
2238
|
+
}
|
|
2239
|
+
async#stageMobile(app, platform, targets, env, arch) {
|
|
2240
|
+
const stage = targets.some((mobileTarget) => ApplicationRunner.carriesServer(mobileTarget, platform)) ? new DesktopServerStage(app) : null;
|
|
2241
|
+
await stage?.install(arch, ApplicationRunner.serverOmit(targets, platform));
|
|
2242
|
+
await this.#buildMobileCsr(app, env);
|
|
2243
|
+
return await stage?.prepare(env);
|
|
2244
|
+
}
|
|
2301
2245
|
async#buildMobileCsr(app, env) {
|
|
2302
2246
|
const prevEnv = {
|
|
2303
2247
|
AKAN_PUBLIC_ENV: process.env.AKAN_PUBLIC_ENV,
|
|
@@ -2310,7 +2254,7 @@ try {
|
|
|
2310
2254
|
APP_OPERATION_MODE: "release"
|
|
2311
2255
|
});
|
|
2312
2256
|
try {
|
|
2313
|
-
await new (await loadBuildRunner())(app).build({ spinner: true });
|
|
2257
|
+
await new (await loadBuildRunner())(app, { environment: env }).build({ spinner: true });
|
|
2314
2258
|
} finally {
|
|
2315
2259
|
for (const [key, value] of Object.entries(prevEnv)) {
|
|
2316
2260
|
if (value === undefined)
|
|
@@ -2335,24 +2279,23 @@ try {
|
|
|
2335
2279
|
return;
|
|
2336
2280
|
for (const failure of failures) {
|
|
2337
2281
|
const message = failure.error instanceof Error ? failure.error.message : String(failure.error);
|
|
2338
|
-
|
|
2282
|
+
Logger3.rawLog(`Native target ${failure.target} failed: ${message}`, undefined, "error");
|
|
2339
2283
|
}
|
|
2340
|
-
throw new Error(`${failures.length}/${results.length}
|
|
2341
|
-
}
|
|
2342
|
-
async codepush(app, os) {
|
|
2343
|
-
const [target] = await resolveMobileTargets(app, undefined);
|
|
2344
|
-
if (!target)
|
|
2345
|
-
throw new Error(`No mobile target configured for ${app.name}`);
|
|
2346
|
-
const capacitorApp = new (await loadCapacitorApp())(app, target.config);
|
|
2347
|
-
await capacitorApp.init();
|
|
2284
|
+
throw new Error(`${failures.length}/${results.length} native targets failed`);
|
|
2348
2285
|
}
|
|
2349
2286
|
#getLocalDatabaseServices(mode) {
|
|
2350
2287
|
if (mode === "single")
|
|
2351
2288
|
return [];
|
|
2352
2289
|
if (mode === "multiple")
|
|
2353
|
-
return ["redis"
|
|
2290
|
+
return ["redis"];
|
|
2354
2291
|
return ["redis", "postgres"];
|
|
2355
2292
|
}
|
|
2293
|
+
async#assertComposeHas(workspace, services) {
|
|
2294
|
+
const compose = Bun.YAML.parse(await Bun.file(`${workspace.workspaceRoot}/local/docker-compose.yaml`).text());
|
|
2295
|
+
const missing = services.filter((service) => !compose?.services?.[service]);
|
|
2296
|
+
if (missing.length)
|
|
2297
|
+
throw new Error(`local/docker-compose.yaml declares no ${missing.join(" or ")} service. Add it, or move the file aside so the next akan dbup writes the current template.`);
|
|
2298
|
+
}
|
|
2356
2299
|
async#isLocalDatabaseUp(workspace, mode) {
|
|
2357
2300
|
const requiredServices = this.#getLocalDatabaseServices(mode);
|
|
2358
2301
|
if (!requiredServices.length)
|
|
@@ -2366,16 +2309,18 @@ try {
|
|
|
2366
2309
|
async dbup(workspace, mode = "multiple") {
|
|
2367
2310
|
if (mode === "single")
|
|
2368
2311
|
return true;
|
|
2312
|
+
const services = this.#getLocalDatabaseServices(mode);
|
|
2313
|
+
await workspace.applyTemplate({
|
|
2314
|
+
basePath: "local",
|
|
2315
|
+
template: "localDev",
|
|
2316
|
+
dict: { repoName: workspace.repoName },
|
|
2317
|
+
overwrite: false
|
|
2318
|
+
});
|
|
2319
|
+
await this.#assertComposeHas(workspace, services);
|
|
2369
2320
|
try {
|
|
2370
|
-
await workspace.applyTemplate({
|
|
2371
|
-
basePath: "local",
|
|
2372
|
-
template: "localDev",
|
|
2373
|
-
dict: { repoName: workspace.repoName },
|
|
2374
|
-
overwrite: false
|
|
2375
|
-
});
|
|
2376
2321
|
const wasAlreadyUp = await this.#isLocalDatabaseUp(workspace, mode);
|
|
2377
2322
|
if (!wasAlreadyUp)
|
|
2378
|
-
await workspace.spawn(`docker`, ["compose", "up", "-d", ...
|
|
2323
|
+
await workspace.spawn(`docker`, ["compose", "up", "-d", ...services], {
|
|
2379
2324
|
cwd: `${workspace.workspaceRoot}/local`
|
|
2380
2325
|
});
|
|
2381
2326
|
return wasAlreadyUp;
|
|
@@ -2393,74 +2338,182 @@ ${detail}`
|
|
|
2393
2338
|
async dbdown(workspace) {
|
|
2394
2339
|
await workspace.spawn(`docker`, ["compose", "down"], { cwd: `${workspace.workspaceRoot}/local` });
|
|
2395
2340
|
}
|
|
2396
|
-
|
|
2397
|
-
|
|
2398
|
-
|
|
2399
|
-
|
|
2400
|
-
|
|
2401
|
-
|
|
2402
|
-
|
|
2403
|
-
|
|
2404
|
-
|
|
2405
|
-
|
|
2406
|
-
|
|
2407
|
-
|
|
2408
|
-
|
|
2409
|
-
|
|
2410
|
-
|
|
2411
|
-
|
|
2412
|
-
|
|
2413
|
-
|
|
2414
|
-
|
|
2415
|
-
|
|
2416
|
-
|
|
2417
|
-
|
|
2418
|
-
|
|
2419
|
-
|
|
2341
|
+
}
|
|
2342
|
+
|
|
2343
|
+
// pkgs/@akanjs/cli/application/hostProcessProbe.ts
|
|
2344
|
+
import { readdirSync, readFileSync, readlinkSync } from "fs";
|
|
2345
|
+
|
|
2346
|
+
class HostProcessProbe {
|
|
2347
|
+
static timeoutMs = 3000;
|
|
2348
|
+
static win32TimeoutMs = 20000;
|
|
2349
|
+
static #procTcpListen = "0A";
|
|
2350
|
+
static #win32ProcessQuery = "[Console]::OutputEncoding=[Text.Encoding]::UTF8; Get-CimInstance Win32_Process | Select-Object ProcessId,ParentProcessId,CommandLine,CreationDate | ConvertTo-Json -Compress";
|
|
2351
|
+
platform;
|
|
2352
|
+
#win32Table = null;
|
|
2353
|
+
constructor(platform = process.platform) {
|
|
2354
|
+
this.platform = platform;
|
|
2355
|
+
}
|
|
2356
|
+
async listenersOn(port) {
|
|
2357
|
+
if (this.platform === "win32")
|
|
2358
|
+
return HostProcessProbe.parseNetstat(await HostProcessProbe.run(["netstat", "-ano"]), port);
|
|
2359
|
+
if (this.platform === "linux")
|
|
2360
|
+
return HostProcessProbe.#procListenersOn(port);
|
|
2361
|
+
const output = await HostProcessProbe.run(["lsof", "-ti", `tcp:${port}`, "-sTCP:LISTEN"]);
|
|
2362
|
+
return [
|
|
2363
|
+
...new Set(output.split(`
|
|
2364
|
+
`).map((line) => Number(line.trim())).filter((pid) => Number.isInteger(pid) && pid > 0))
|
|
2365
|
+
];
|
|
2420
2366
|
}
|
|
2421
|
-
async
|
|
2422
|
-
|
|
2367
|
+
async parentOf(pid) {
|
|
2368
|
+
if (this.platform === "win32") {
|
|
2369
|
+
const child = await this.#win32Process(pid);
|
|
2370
|
+
const parent = child ? await this.#win32Process(child.ppid) : null;
|
|
2371
|
+
return child && parent ? HostProcessProbe.parentPidOf(child, parent) : null;
|
|
2372
|
+
}
|
|
2373
|
+
if (this.platform === "linux") {
|
|
2374
|
+
const stat = HostProcessProbe.#readProc(pid, "stat");
|
|
2375
|
+
return stat === null ? null : HostProcessProbe.parseProcStatPpid(stat);
|
|
2376
|
+
}
|
|
2377
|
+
const parsed = Number((await HostProcessProbe.run(["ps", "-p", String(pid), "-o", "ppid="])).trim());
|
|
2378
|
+
return Number.isInteger(parsed) && parsed > 0 ? parsed : null;
|
|
2423
2379
|
}
|
|
2424
|
-
async
|
|
2425
|
-
|
|
2426
|
-
|
|
2427
|
-
|
|
2428
|
-
|
|
2429
|
-
|
|
2430
|
-
|
|
2431
|
-
|
|
2432
|
-
environment,
|
|
2433
|
-
local
|
|
2434
|
-
});
|
|
2435
|
-
return;
|
|
2380
|
+
async commandOf(pid) {
|
|
2381
|
+
if (this.platform === "win32")
|
|
2382
|
+
return (await this.#win32Process(pid))?.command ?? null;
|
|
2383
|
+
if (this.platform === "linux") {
|
|
2384
|
+
const cmdline = HostProcessProbe.#readProc(pid, "cmdline");
|
|
2385
|
+
return cmdline?.split("\x00").filter(Boolean).join(" ") || null;
|
|
2386
|
+
}
|
|
2387
|
+
return (await HostProcessProbe.run(["ps", "-ww", "-p", String(pid), "-o", "command="])).trim() || null;
|
|
2436
2388
|
}
|
|
2437
|
-
|
|
2438
|
-
|
|
2439
|
-
|
|
2440
|
-
|
|
2441
|
-
|
|
2442
|
-
|
|
2443
|
-
|
|
2444
|
-
|
|
2445
|
-
|
|
2446
|
-
|
|
2447
|
-
|
|
2448
|
-
|
|
2449
|
-
|
|
2450
|
-
|
|
2451
|
-
|
|
2452
|
-
|
|
2453
|
-
|
|
2454
|
-
|
|
2455
|
-
|
|
2456
|
-
|
|
2457
|
-
|
|
2389
|
+
static parentPidOf(child, parent) {
|
|
2390
|
+
if (child.ppid !== parent.pid)
|
|
2391
|
+
return null;
|
|
2392
|
+
if (parent.startedAt !== null && child.startedAt !== null && parent.startedAt > child.startedAt)
|
|
2393
|
+
return null;
|
|
2394
|
+
return parent.pid;
|
|
2395
|
+
}
|
|
2396
|
+
static parseNetstat(output, port) {
|
|
2397
|
+
const pids = new Set;
|
|
2398
|
+
for (const line of output.split(/\r?\n/)) {
|
|
2399
|
+
const fields = line.trim().split(/\s+/);
|
|
2400
|
+
if (fields[0] !== "TCP" || fields.length < 5)
|
|
2401
|
+
continue;
|
|
2402
|
+
const [, local, remote] = fields;
|
|
2403
|
+
if (!local?.endsWith(`:${port}`) || !remote?.endsWith(":0"))
|
|
2404
|
+
continue;
|
|
2405
|
+
const pid = Number(fields.at(-1));
|
|
2406
|
+
if (Number.isInteger(pid) && pid > 0)
|
|
2407
|
+
pids.add(pid);
|
|
2408
|
+
}
|
|
2409
|
+
return [...pids];
|
|
2410
|
+
}
|
|
2411
|
+
static parseProcNetTcp(text, port) {
|
|
2412
|
+
const inodes = [];
|
|
2413
|
+
for (const line of text.split(`
|
|
2414
|
+
`).slice(1)) {
|
|
2415
|
+
const fields = line.trim().split(/\s+/);
|
|
2416
|
+
const [, local, , state, , , , , , inode] = fields;
|
|
2417
|
+
if (!local || !inode || state !== HostProcessProbe.#procTcpListen || inode === "0")
|
|
2418
|
+
continue;
|
|
2419
|
+
if (Number.parseInt(local.slice(local.lastIndexOf(":") + 1), 16) === port)
|
|
2420
|
+
inodes.push(inode);
|
|
2421
|
+
}
|
|
2422
|
+
return inodes;
|
|
2423
|
+
}
|
|
2424
|
+
static parseProcStatPpid(stat) {
|
|
2425
|
+
const ppid = Number(stat.slice(stat.lastIndexOf(")") + 2).split(" ")[1]);
|
|
2426
|
+
return Number.isInteger(ppid) && ppid > 0 ? ppid : null;
|
|
2427
|
+
}
|
|
2428
|
+
static parseWin32Processes(json) {
|
|
2429
|
+
const table = new Map;
|
|
2430
|
+
let parsed;
|
|
2431
|
+
try {
|
|
2432
|
+
parsed = JSON.parse(json.trim() || "[]");
|
|
2433
|
+
} catch {
|
|
2434
|
+
return table;
|
|
2435
|
+
}
|
|
2436
|
+
const rows = Array.isArray(parsed) ? parsed : [parsed];
|
|
2437
|
+
for (const row of rows) {
|
|
2438
|
+
if (typeof row?.ProcessId !== "number")
|
|
2439
|
+
continue;
|
|
2440
|
+
table.set(row.ProcessId, {
|
|
2441
|
+
pid: row.ProcessId,
|
|
2442
|
+
ppid: row.ParentProcessId ?? 0,
|
|
2443
|
+
command: row.CommandLine?.trim() || null,
|
|
2444
|
+
startedAt: HostProcessProbe.#parseCimDate(row.CreationDate)
|
|
2445
|
+
});
|
|
2446
|
+
}
|
|
2447
|
+
return table;
|
|
2448
|
+
}
|
|
2449
|
+
static async run(command, timeoutMs = HostProcessProbe.timeoutMs) {
|
|
2450
|
+
let proc;
|
|
2451
|
+
try {
|
|
2452
|
+
proc = Bun.spawn(command, { stdio: ["ignore", "pipe", "ignore"], windowsHide: true });
|
|
2453
|
+
} catch {
|
|
2454
|
+
return "";
|
|
2455
|
+
}
|
|
2456
|
+
const timer = setTimeout(() => proc.kill("SIGKILL"), timeoutMs);
|
|
2457
|
+
try {
|
|
2458
|
+
return await new Response(proc.stdout).text();
|
|
2459
|
+
} catch {
|
|
2460
|
+
return "";
|
|
2461
|
+
} finally {
|
|
2462
|
+
clearTimeout(timer);
|
|
2463
|
+
}
|
|
2464
|
+
}
|
|
2465
|
+
async#win32Process(pid) {
|
|
2466
|
+
this.#win32Table ??= await HostProcessProbe.#queryWin32Processes();
|
|
2467
|
+
if (!this.#win32Table.has(pid))
|
|
2468
|
+
this.#win32Table = await HostProcessProbe.#queryWin32Processes();
|
|
2469
|
+
return this.#win32Table.get(pid) ?? null;
|
|
2470
|
+
}
|
|
2471
|
+
static async#queryWin32Processes() {
|
|
2472
|
+
const output = await HostProcessProbe.run(["powershell", "-NoProfile", "-NonInteractive", "-Command", HostProcessProbe.#win32ProcessQuery], HostProcessProbe.win32TimeoutMs);
|
|
2473
|
+
return HostProcessProbe.parseWin32Processes(output);
|
|
2474
|
+
}
|
|
2475
|
+
static #parseCimDate(value) {
|
|
2476
|
+
if (!value)
|
|
2477
|
+
return null;
|
|
2478
|
+
const legacy = /\/Date\((\d+)/.exec(value)?.[1];
|
|
2479
|
+
const ms = legacy ? Number(legacy) : Date.parse(value);
|
|
2480
|
+
return Number.isFinite(ms) ? ms : null;
|
|
2481
|
+
}
|
|
2482
|
+
static #procListenersOn(port) {
|
|
2483
|
+
const inodes = new Set(["tcp", "tcp6"].flatMap((table) => HostProcessProbe.parseProcNetTcp(HostProcessProbe.#readProc("net", table) ?? "", port)));
|
|
2484
|
+
if (!inodes.size)
|
|
2485
|
+
return [];
|
|
2486
|
+
const pids = [];
|
|
2487
|
+
for (const entry of HostProcessProbe.#listDir("/proc")) {
|
|
2488
|
+
if (!/^\d+$/.test(entry))
|
|
2489
|
+
continue;
|
|
2490
|
+
const holds = HostProcessProbe.#listDir(`/proc/${entry}/fd`).some((fd) => {
|
|
2491
|
+
const link = HostProcessProbe.#orElse(() => readlinkSync(`/proc/${entry}/fd/${fd}`), "");
|
|
2492
|
+
const inode = /^socket:\[(\d+)\]$/.exec(link)?.[1];
|
|
2493
|
+
return !!inode && inodes.has(inode);
|
|
2494
|
+
});
|
|
2495
|
+
if (holds)
|
|
2496
|
+
pids.push(Number(entry));
|
|
2497
|
+
}
|
|
2498
|
+
return pids;
|
|
2499
|
+
}
|
|
2500
|
+
static #readProc(pid, file) {
|
|
2501
|
+
return HostProcessProbe.#orElse(() => readFileSync(`/proc/${pid}/${file}`, "utf8"), null);
|
|
2502
|
+
}
|
|
2503
|
+
static #listDir(dir) {
|
|
2504
|
+
return HostProcessProbe.#orElse(() => readdirSync(dir), []);
|
|
2505
|
+
}
|
|
2506
|
+
static #orElse(read, fallback) {
|
|
2507
|
+
try {
|
|
2508
|
+
return read();
|
|
2509
|
+
} catch {
|
|
2510
|
+
return fallback;
|
|
2511
|
+
}
|
|
2458
2512
|
}
|
|
2459
2513
|
}
|
|
2460
2514
|
|
|
2461
2515
|
// pkgs/@akanjs/cli/application/devPortReclaimer.ts
|
|
2462
2516
|
class DevPortReclaimer {
|
|
2463
|
-
static lsofTimeoutMs = 3000;
|
|
2464
2517
|
static releaseTimeoutMs = 8000;
|
|
2465
2518
|
static maxAncestorDepth = 8;
|
|
2466
2519
|
static #releasePollMs = 100;
|
|
@@ -2469,7 +2522,12 @@ class DevPortReclaimer {
|
|
|
2469
2522
|
return [...new Set(ports.flatMap((port) => [port, port + DevPortReclaimer.wsPortOffset]))].sort((left, right) => left - right);
|
|
2470
2523
|
}
|
|
2471
2524
|
static isAkanCommand(command) {
|
|
2472
|
-
|
|
2525
|
+
const normalized = command.replaceAll("\\", "/");
|
|
2526
|
+
return /(^|\/)akan(\.exe)?"?(\s|$)/.test(normalized) || normalized.includes("@akanjs/cli") || normalized.includes("akanjs/server") || /\bapps\/[^/\s]+\/main\.ts\b/.test(normalized) || normalized.includes("incrementalBuilder.proc") || normalized.includes("rscWorker");
|
|
2527
|
+
}
|
|
2528
|
+
#probe;
|
|
2529
|
+
constructor(probe = new HostProcessProbe) {
|
|
2530
|
+
this.#probe = probe;
|
|
2473
2531
|
}
|
|
2474
2532
|
async reclaim(ports) {
|
|
2475
2533
|
const killed = [];
|
|
@@ -2494,10 +2552,10 @@ class DevPortReclaimer {
|
|
|
2494
2552
|
}
|
|
2495
2553
|
async holdersOf(port) {
|
|
2496
2554
|
const holders = [];
|
|
2497
|
-
for (const listenerPid of await this.#
|
|
2555
|
+
for (const listenerPid of await this.#probe.listenersOn(port)) {
|
|
2498
2556
|
if (listenerPid === process.pid || listenerPid === process.ppid)
|
|
2499
2557
|
continue;
|
|
2500
|
-
const command = await this.#commandOf(listenerPid);
|
|
2558
|
+
const command = await this.#probe.commandOf(listenerPid);
|
|
2501
2559
|
if (!command)
|
|
2502
2560
|
continue;
|
|
2503
2561
|
if (!DevPortReclaimer.isAkanCommand(command)) {
|
|
@@ -2512,44 +2570,37 @@ class DevPortReclaimer {
|
|
|
2512
2570
|
async#akanRootOf(pid, command) {
|
|
2513
2571
|
let root = { pid, command };
|
|
2514
2572
|
for (let depth = 0;depth < DevPortReclaimer.maxAncestorDepth; depth += 1) {
|
|
2515
|
-
const parentPid = await this.#
|
|
2573
|
+
const parentPid = await this.#probe.parentOf(root.pid);
|
|
2516
2574
|
if (parentPid === null || parentPid <= 1 || parentPid === process.pid || parentPid === process.ppid)
|
|
2517
2575
|
break;
|
|
2518
|
-
const parentCommand = await this.#commandOf(parentPid);
|
|
2576
|
+
const parentCommand = await this.#probe.commandOf(parentPid);
|
|
2519
2577
|
if (!parentCommand || !DevPortReclaimer.isAkanCommand(parentCommand))
|
|
2520
2578
|
break;
|
|
2521
2579
|
root = { pid: parentPid, command: parentCommand };
|
|
2522
2580
|
}
|
|
2523
2581
|
return root;
|
|
2524
2582
|
}
|
|
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
2583
|
async#terminate(pid) {
|
|
2584
|
+
if (this.#probe.platform === "win32") {
|
|
2585
|
+
await HostProcessProbe.run(["taskkill", "/PID", String(pid), "/T", "/F"]);
|
|
2586
|
+
return await this.#released(pid);
|
|
2587
|
+
}
|
|
2542
2588
|
if (!this.#signal(pid, "SIGTERM"))
|
|
2543
2589
|
return !this.#alive(pid);
|
|
2590
|
+
if (await this.#released(pid))
|
|
2591
|
+
return true;
|
|
2592
|
+
this.#signal(pid, "SIGKILL");
|
|
2593
|
+
await Bun.sleep(DevPortReclaimer.#releasePollMs);
|
|
2594
|
+
return !this.#alive(pid);
|
|
2595
|
+
}
|
|
2596
|
+
async#released(pid) {
|
|
2544
2597
|
const deadline = Date.now() + DevPortReclaimer.releaseTimeoutMs;
|
|
2545
2598
|
while (Date.now() < deadline) {
|
|
2546
2599
|
if (!this.#alive(pid))
|
|
2547
2600
|
return true;
|
|
2548
2601
|
await Bun.sleep(DevPortReclaimer.#releasePollMs);
|
|
2549
2602
|
}
|
|
2550
|
-
|
|
2551
|
-
await Bun.sleep(DevPortReclaimer.#releasePollMs);
|
|
2552
|
-
return !this.#alive(pid);
|
|
2603
|
+
return false;
|
|
2553
2604
|
}
|
|
2554
2605
|
#signal(pid, signal) {
|
|
2555
2606
|
try {
|
|
@@ -2567,26 +2618,15 @@ class DevPortReclaimer {
|
|
|
2567
2618
|
return error.code === "EPERM";
|
|
2568
2619
|
}
|
|
2569
2620
|
}
|
|
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
2621
|
}
|
|
2582
2622
|
|
|
2583
2623
|
// pkgs/@akanjs/cli/application/devStreamView.ts
|
|
2584
|
-
import { Logger as
|
|
2624
|
+
import { Logger as Logger4 } from "akanjs/common";
|
|
2585
2625
|
import chalk from "chalk";
|
|
2586
2626
|
var appColors = ["cyan", "magenta", "yellow", "green", "blue", "red"];
|
|
2587
2627
|
|
|
2588
2628
|
class DevStreamView {
|
|
2589
|
-
#logger = new
|
|
2629
|
+
#logger = new Logger4("akan start");
|
|
2590
2630
|
#partial = new Map;
|
|
2591
2631
|
#lastState = new Map;
|
|
2592
2632
|
#color = new Map;
|
|
@@ -2656,7 +2696,7 @@ import os from "os";
|
|
|
2656
2696
|
import { MemoryLimit as MemoryLimit2 } from "akanjs/server/memoryLimit";
|
|
2657
2697
|
|
|
2658
2698
|
class DevBootConcurrency {
|
|
2659
|
-
static perAppBytes =
|
|
2699
|
+
static perAppBytes = 1800 * 1024 * 1024;
|
|
2660
2700
|
static memoryShare = 0.5;
|
|
2661
2701
|
static coresPerApp = 4;
|
|
2662
2702
|
static budget() {
|
|
@@ -2692,6 +2732,7 @@ class DevBootConcurrency {
|
|
|
2692
2732
|
import { mkdir, rename } from "fs/promises";
|
|
2693
2733
|
import path6 from "path";
|
|
2694
2734
|
import dayjs from "dayjs";
|
|
2735
|
+
|
|
2695
2736
|
class DevSessionLog {
|
|
2696
2737
|
static fileName = "dev.log";
|
|
2697
2738
|
static previousFileName = "dev.prev.log";
|
|
@@ -2792,6 +2833,16 @@ class DevSessionLog {
|
|
|
2792
2833
|
|
|
2793
2834
|
// pkgs/@akanjs/cli/application/devSupervisor.ts
|
|
2794
2835
|
class DevSupervisor {
|
|
2836
|
+
static resolveDevUi(plain, {
|
|
2837
|
+
isTty = !!process.stdout.isTTY,
|
|
2838
|
+
columns = process.stdout.columns ?? 0
|
|
2839
|
+
} = {}) {
|
|
2840
|
+
if (plain)
|
|
2841
|
+
return { mode: "stream", downgraded: false };
|
|
2842
|
+
if (!isTty || columns <= 0)
|
|
2843
|
+
return { mode: "stream", downgraded: true };
|
|
2844
|
+
return { mode: "tui", downgraded: false };
|
|
2845
|
+
}
|
|
2795
2846
|
static supervisedEnvKey = "AKAN_DEV_SUPERVISED";
|
|
2796
2847
|
static childHooks() {
|
|
2797
2848
|
if (process.env[DevSupervisor.supervisedEnvKey] !== "1" || !process.send)
|
|
@@ -2806,7 +2857,7 @@ class DevSupervisor {
|
|
|
2806
2857
|
}
|
|
2807
2858
|
};
|
|
2808
2859
|
}
|
|
2809
|
-
static
|
|
2860
|
+
static bootTimeoutMs = 180000;
|
|
2810
2861
|
static shutdownGraceMs = 12000;
|
|
2811
2862
|
#options;
|
|
2812
2863
|
#children = new Map;
|
|
@@ -2855,23 +2906,21 @@ class DevSupervisor {
|
|
|
2855
2906
|
return ports;
|
|
2856
2907
|
}
|
|
2857
2908
|
#makeChild(app, port) {
|
|
2858
|
-
|
|
2859
|
-
const ready = new Promise((resolve) => {
|
|
2860
|
-
markReady = resolve;
|
|
2861
|
-
});
|
|
2909
|
+
const { promise: booted, resolve: markBooted } = Promise.withResolvers();
|
|
2862
2910
|
return {
|
|
2863
2911
|
status: {
|
|
2864
2912
|
app,
|
|
2865
2913
|
name: app.name,
|
|
2866
2914
|
port,
|
|
2867
2915
|
url: `http://localhost:${port}`,
|
|
2916
|
+
shareUrl: this.#options.shares?.get(app.name) ?? null,
|
|
2868
2917
|
state: "starting",
|
|
2869
2918
|
detail: "queued",
|
|
2870
2919
|
exitCode: null
|
|
2871
2920
|
},
|
|
2872
2921
|
proc: null,
|
|
2873
|
-
|
|
2874
|
-
|
|
2922
|
+
booted,
|
|
2923
|
+
markBooted,
|
|
2875
2924
|
opened: false
|
|
2876
2925
|
};
|
|
2877
2926
|
}
|
|
@@ -2890,20 +2939,22 @@ class DevSupervisor {
|
|
|
2890
2939
|
for (const child of wave)
|
|
2891
2940
|
this.#spawnChild(child);
|
|
2892
2941
|
this.#publishStatus();
|
|
2893
|
-
await Promise.all(wave.map(async (child) => await this.#
|
|
2942
|
+
await Promise.all(wave.map(async (child) => await this.#waitForBoot(child)));
|
|
2894
2943
|
}
|
|
2895
2944
|
}
|
|
2896
|
-
async#
|
|
2897
|
-
|
|
2945
|
+
async#waitForBoot(child) {
|
|
2946
|
+
if (await DevSupervisor.timesOut(child.booted, DevSupervisor.bootTimeoutMs))
|
|
2947
|
+
this.#note(`${child.status.name} has not finished booting after ${Math.round(DevSupervisor.bootTimeoutMs / 1000)}s; starting the next app anyway`, "warn");
|
|
2948
|
+
}
|
|
2949
|
+
static async timesOut(work, ms) {
|
|
2950
|
+
let timer;
|
|
2898
2951
|
const expired = new Promise((resolve) => {
|
|
2899
|
-
timer = setTimeout(() => resolve(
|
|
2952
|
+
timer = setTimeout(() => resolve(true), ms);
|
|
2900
2953
|
});
|
|
2901
2954
|
try {
|
|
2902
|
-
|
|
2903
|
-
this.#note(`${child.status.name} has not reported ready after ${Math.round(DevSupervisor.readyTimeoutMs / 1000)}s; starting the next app anyway`, "warn");
|
|
2955
|
+
return await Promise.race([work.then(() => false), expired]);
|
|
2904
2956
|
} finally {
|
|
2905
|
-
|
|
2906
|
-
clearTimeout(timer);
|
|
2957
|
+
clearTimeout(timer);
|
|
2907
2958
|
}
|
|
2908
2959
|
}
|
|
2909
2960
|
static childArgs(name, { write = true } = {}) {
|
|
@@ -2930,9 +2981,12 @@ class DevSupervisor {
|
|
|
2930
2981
|
},
|
|
2931
2982
|
stdio: ["ignore", "pipe", "pipe"],
|
|
2932
2983
|
ipc: (message) => {
|
|
2933
|
-
if (!message || typeof message !== "object"
|
|
2984
|
+
if (!message || typeof message !== "object")
|
|
2934
2985
|
return;
|
|
2935
|
-
|
|
2986
|
+
if ("booted" in message)
|
|
2987
|
+
child.markBooted();
|
|
2988
|
+
else if (typeof message.state === "string")
|
|
2989
|
+
this.#applyChildEvent(child, message);
|
|
2936
2990
|
},
|
|
2937
2991
|
serialization: "advanced",
|
|
2938
2992
|
onExit: (_proc, exitCode, signalCode) => {
|
|
@@ -2942,7 +2996,7 @@ class DevSupervisor {
|
|
|
2942
2996
|
return;
|
|
2943
2997
|
child.status.state = "stopped";
|
|
2944
2998
|
child.status.detail = signalCode ? `killed by ${signalCode}` : `exited with ${exitCode}`;
|
|
2945
|
-
child.
|
|
2999
|
+
child.markBooted();
|
|
2946
3000
|
this.#publishStatus();
|
|
2947
3001
|
}
|
|
2948
3002
|
});
|
|
@@ -2956,14 +3010,13 @@ class DevSupervisor {
|
|
|
2956
3010
|
child.status.state = event.state;
|
|
2957
3011
|
child.status.detail = event.detail ?? "";
|
|
2958
3012
|
if (event.state === "ready") {
|
|
2959
|
-
child.markReady();
|
|
2960
3013
|
if (this.#options.open && !child.opened) {
|
|
2961
3014
|
child.opened = true;
|
|
2962
3015
|
openBrowser(child.status.url);
|
|
2963
3016
|
}
|
|
2964
3017
|
}
|
|
2965
3018
|
if (event.state === "failed")
|
|
2966
|
-
child.
|
|
3019
|
+
child.markBooted();
|
|
2967
3020
|
this.#publishStatus();
|
|
2968
3021
|
}
|
|
2969
3022
|
async#drain(stream, name, kind) {
|
|
@@ -3025,44 +3078,81 @@ class DevSupervisor {
|
|
|
3025
3078
|
for (const child of running)
|
|
3026
3079
|
child.proc.kill("SIGTERM");
|
|
3027
3080
|
const exited = Promise.all(running.map(async (child) => await child.proc.exited));
|
|
3028
|
-
|
|
3029
|
-
|
|
3030
|
-
|
|
3031
|
-
|
|
3032
|
-
|
|
3033
|
-
|
|
3034
|
-
|
|
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);
|
|
3081
|
+
if (!await DevSupervisor.timesOut(exited, DevSupervisor.shutdownGraceMs))
|
|
3082
|
+
return;
|
|
3083
|
+
for (const child of running) {
|
|
3084
|
+
if (child.proc.killed)
|
|
3085
|
+
continue;
|
|
3086
|
+
this.#note(`${child.status.name} did not exit in time; killing it`, "warn");
|
|
3087
|
+
child.proc.kill("SIGKILL");
|
|
3044
3088
|
}
|
|
3045
3089
|
}
|
|
3046
3090
|
}
|
|
3047
3091
|
|
|
3048
|
-
// pkgs/@akanjs/cli/application/
|
|
3049
|
-
|
|
3050
|
-
|
|
3051
|
-
|
|
3052
|
-
|
|
3053
|
-
|
|
3054
|
-
|
|
3055
|
-
|
|
3092
|
+
// pkgs/@akanjs/cli/application/interruptTeardown.ts
|
|
3093
|
+
import { Logger as Logger5 } from "akanjs/common";
|
|
3094
|
+
|
|
3095
|
+
class InterruptTeardown {
|
|
3096
|
+
#teardowns = [];
|
|
3097
|
+
#exit;
|
|
3098
|
+
#listen;
|
|
3099
|
+
#report;
|
|
3100
|
+
#interrupted = false;
|
|
3101
|
+
#chain = Promise.resolve();
|
|
3102
|
+
ownsExit = true;
|
|
3103
|
+
constructor({ exit, listen, report } = {}) {
|
|
3104
|
+
this.#exit = exit ?? ((code) => process.exit(code));
|
|
3105
|
+
this.#listen = listen ?? ((onSignal) => {
|
|
3106
|
+
process.on("SIGINT", onSignal);
|
|
3107
|
+
});
|
|
3108
|
+
this.#report = report ?? ((message) => Logger5.rawLog(message, undefined, "error"));
|
|
3109
|
+
}
|
|
3110
|
+
add(run, abandoned, exitCode = 0) {
|
|
3111
|
+
this.#teardowns.push({ run, abandoned, exitCode, state: "pending" });
|
|
3112
|
+
if (this.#teardowns.length === 1)
|
|
3113
|
+
this.#listen(() => this.#onSignal());
|
|
3114
|
+
}
|
|
3115
|
+
async runAll() {
|
|
3116
|
+
await this.#runTeardowns();
|
|
3117
|
+
}
|
|
3118
|
+
#onSignal() {
|
|
3119
|
+
if (this.#interrupted) {
|
|
3120
|
+
for (const one of this.#teardowns)
|
|
3121
|
+
if (one.state !== "done")
|
|
3122
|
+
this.#report(one.abandoned);
|
|
3123
|
+
this.#exit(130);
|
|
3124
|
+
return;
|
|
3125
|
+
}
|
|
3126
|
+
this.#interrupted = true;
|
|
3127
|
+
this.#runTeardowns().finally(() => {
|
|
3128
|
+
if (this.ownsExit)
|
|
3129
|
+
this.#exit(Math.max(0, ...this.#teardowns.map((one) => one.exitCode)));
|
|
3130
|
+
});
|
|
3131
|
+
}
|
|
3132
|
+
async#runTeardowns() {
|
|
3133
|
+
this.#chain = this.#chain.then(async () => {
|
|
3134
|
+
for (const one of this.#teardowns.toReversed()) {
|
|
3135
|
+
if (one.state !== "pending")
|
|
3136
|
+
continue;
|
|
3137
|
+
one.state = "running";
|
|
3138
|
+
await one.run().catch(() => {
|
|
3139
|
+
return;
|
|
3140
|
+
});
|
|
3141
|
+
one.state = "done";
|
|
3142
|
+
}
|
|
3143
|
+
});
|
|
3144
|
+
await this.#chain;
|
|
3145
|
+
}
|
|
3146
|
+
}
|
|
3056
3147
|
|
|
3057
3148
|
// pkgs/@akanjs/cli/application/application.script.ts
|
|
3058
3149
|
class ApplicationScript extends script("application", [ApplicationRunner, LibraryScript]) {
|
|
3059
3150
|
static dbShutdownTimeoutMs = 20000;
|
|
3151
|
+
static devServerReadyTimeoutMs = DevSupervisor.bootTimeoutMs;
|
|
3152
|
+
#interrupt = new InterruptTeardown;
|
|
3060
3153
|
async confirmDatabaseModeDependencyInstall(databaseMode, installSpecs) {
|
|
3061
3154
|
return await confirm({
|
|
3062
|
-
message:
|
|
3063
|
-
`Database mode '${databaseMode}' requires missing dependencies: ${installSpecs.join(", ")}.`,
|
|
3064
|
-
"Install them now?"
|
|
3065
|
-
].join(" "),
|
|
3155
|
+
message: `Database mode '${databaseMode}' requires missing dependencies: ${installSpecs.join(", ")}. Install them now?`,
|
|
3066
3156
|
default: true
|
|
3067
3157
|
});
|
|
3068
3158
|
}
|
|
@@ -3073,67 +3163,22 @@ class ApplicationScript extends script("application", [ApplicationRunner, Librar
|
|
|
3073
3163
|
const shouldInstall = await this.confirmDatabaseModeDependencyInstall(databaseMode, installSpecs);
|
|
3074
3164
|
if (!shouldInstall)
|
|
3075
3165
|
throw new Error(`Database mode '${databaseMode}' requires missing dependencies: ${installSpecs.join(", ")}.`);
|
|
3076
|
-
|
|
3077
|
-
try {
|
|
3078
|
-
await app.workspace.spawn("bun", ["add", ...installSpecs], {
|
|
3079
|
-
stdio: "inherit"
|
|
3080
|
-
});
|
|
3081
|
-
await app.workspace.getPackageJson({ refresh: true });
|
|
3082
|
-
spinner.succeed(`Installed database dependencies for ${databaseMode} mode`);
|
|
3083
|
-
} catch (error) {
|
|
3084
|
-
spinner.fail(`Failed to install database dependencies for ${databaseMode} mode`);
|
|
3085
|
-
throw error;
|
|
3086
|
-
}
|
|
3166
|
+
await this.#addDependencies(app, installSpecs, `database dependencies for ${databaseMode} mode`);
|
|
3087
3167
|
}
|
|
3088
|
-
async
|
|
3089
|
-
|
|
3090
|
-
message: [`Mobile builds require missing dependencies: ${installSpecs.join(", ")}.`, "Install them now?"].join(" "),
|
|
3091
|
-
default: true
|
|
3092
|
-
});
|
|
3093
|
-
}
|
|
3094
|
-
async syncMobileDependencies(app, akanConfig) {
|
|
3095
|
-
const installSpecs = akanConfig.getMissingMobileDependencySpecs();
|
|
3096
|
-
if (installSpecs.length === 0)
|
|
3097
|
-
return;
|
|
3098
|
-
const shouldInstall = await this.confirmMobileDependencyInstall(installSpecs);
|
|
3099
|
-
if (!shouldInstall)
|
|
3100
|
-
throw new Error(`Mobile builds require missing dependencies: ${installSpecs.join(", ")}.`);
|
|
3101
|
-
const spinner = app.workspace.spinning("Installing mobile dependencies...");
|
|
3168
|
+
async#addDependencies(app, installSpecs, what) {
|
|
3169
|
+
const spinner = app.workspace.spinning(`Installing ${what}...`);
|
|
3102
3170
|
try {
|
|
3103
|
-
await app.workspace.spawn("bun", ["add", ...installSpecs], {
|
|
3104
|
-
stdio: "inherit"
|
|
3105
|
-
});
|
|
3171
|
+
await app.workspace.spawn("bun", ["add", ...installSpecs], { stdio: "inherit" });
|
|
3106
3172
|
await app.workspace.getPackageJson({ refresh: true });
|
|
3107
|
-
spinner.succeed(
|
|
3108
|
-
} catch (error) {
|
|
3109
|
-
spinner.fail("Failed to install mobile dependencies");
|
|
3110
|
-
throw error;
|
|
3111
|
-
}
|
|
3112
|
-
}
|
|
3113
|
-
async syncMobileAppCapacitorPlugins(app, akanConfig) {
|
|
3114
|
-
const plugins = akanConfig.getMobileAppCapacitorPlugins();
|
|
3115
|
-
if (plugins.length === 0)
|
|
3116
|
-
return;
|
|
3117
|
-
const packageJson = await app.getPackageJson({ refresh: true });
|
|
3118
|
-
const dependencies = packageJson.dependencies ?? {};
|
|
3119
|
-
const missing = plugins.filter((plugin) => !dependencies[plugin]);
|
|
3120
|
-
if (missing.length === 0)
|
|
3121
|
-
return;
|
|
3122
|
-
const spinner = app.workspace.spinning(`Adding default Capacitor plugins to ${app.name}...`);
|
|
3123
|
-
try {
|
|
3124
|
-
packageJson.dependencies = { ...dependencies, ...Object.fromEntries(missing.map((plugin) => [plugin, "*"])) };
|
|
3125
|
-
await app.setPackageJson(packageJson);
|
|
3126
|
-
await app.workspace.spawn("bun", ["install"], { stdio: "inherit" });
|
|
3127
|
-
await app.getPackageJson({ refresh: true });
|
|
3128
|
-
spinner.succeed(`Added default Capacitor plugins to ${app.name}: ${missing.join(", ")}`);
|
|
3173
|
+
spinner.succeed(`Installed ${what}`);
|
|
3129
3174
|
} catch (error) {
|
|
3130
|
-
spinner.fail(`Failed to
|
|
3175
|
+
spinner.fail(`Failed to install ${what}`);
|
|
3131
3176
|
throw error;
|
|
3132
3177
|
}
|
|
3133
3178
|
}
|
|
3134
|
-
async createApplication(appName, workspace, { start = false, libs = [] } = {}) {
|
|
3179
|
+
async createApplication(appName, workspace, { start = false, libs = [], sample = false } = {}) {
|
|
3135
3180
|
const spinner = workspace.spinning("Creating application...");
|
|
3136
|
-
const app = await this.applicationRunner.createApplication(appName, workspace, libs);
|
|
3181
|
+
const app = await this.applicationRunner.createApplication(appName, workspace, libs, { sample });
|
|
3137
3182
|
spinner.succeed(`Application created in apps/${app.name}`);
|
|
3138
3183
|
await app.scanSync();
|
|
3139
3184
|
if (start)
|
|
@@ -3156,17 +3201,6 @@ class ApplicationScript extends script("application", [ApplicationRunner, Librar
|
|
|
3156
3201
|
else
|
|
3157
3202
|
await this.libraryScript.syncLibrary(sys);
|
|
3158
3203
|
}
|
|
3159
|
-
async compact(sys, { module = null, minLines, interactive = false } = {}) {
|
|
3160
|
-
const { scanned, reports } = await this.applicationRunner.compact(sys, { module, minLines, interactive });
|
|
3161
|
-
if (!reports.length) {
|
|
3162
|
-
Logger6.rawLog(`No abstract file long enough to compact in ${sys.name} (${scanned} scanned)`);
|
|
3163
|
-
return;
|
|
3164
|
-
}
|
|
3165
|
-
for (const report of reports)
|
|
3166
|
-
Logger6.rawLog(`${report.status}: ${report.path} (${report.beforeLines} -> ${report.afterLines} lines)`);
|
|
3167
|
-
const compactedNum = reports.filter((report) => report.status === "compacted").length;
|
|
3168
|
-
Logger6.rawLog(`Compacted ${compactedNum}/${reports.length} abstract files in ${sys.name}`);
|
|
3169
|
-
}
|
|
3170
3204
|
async script(app, filename) {
|
|
3171
3205
|
const scriptFilename = filename ?? await this.applicationRunner.getScriptFilename(app);
|
|
3172
3206
|
await app.scanSync();
|
|
@@ -3184,10 +3218,7 @@ class ApplicationScript extends script("application", [ApplicationRunner, Librar
|
|
|
3184
3218
|
if (!quiet)
|
|
3185
3219
|
Logger6.rawLog(`Creating an optimized production build for ${app.name}...`);
|
|
3186
3220
|
try {
|
|
3187
|
-
const result = await this.applicationRunner.build(app, {
|
|
3188
|
-
fast,
|
|
3189
|
-
spinner: !quiet
|
|
3190
|
-
});
|
|
3221
|
+
const result = await this.applicationRunner.build(app, { fast, spinner: !quiet });
|
|
3191
3222
|
Logger6.rawLog(`${app.name} built in dist/apps/${app.name}`);
|
|
3192
3223
|
if (!quiet)
|
|
3193
3224
|
ApplicationBuildReporter.printSummary(result);
|
|
@@ -3212,8 +3243,7 @@ class ApplicationScript extends script("application", [ApplicationRunner, Librar
|
|
|
3212
3243
|
async test(exec, { write = true } = {}) {
|
|
3213
3244
|
if (exec instanceof LibExecutor) {
|
|
3214
3245
|
await this.libraryScript.syncLibrary(exec);
|
|
3215
|
-
|
|
3216
|
-
spinner.succeed(`${exec.name} prepared`);
|
|
3246
|
+
exec.spinning(`Preparing ${exec.name}...`).succeed(`${exec.name} prepared`);
|
|
3217
3247
|
await this.applicationRunner.test(exec);
|
|
3218
3248
|
return;
|
|
3219
3249
|
}
|
|
@@ -3230,23 +3260,33 @@ class ApplicationScript extends script("application", [ApplicationRunner, Librar
|
|
|
3230
3260
|
}
|
|
3231
3261
|
await this.applicationRunner.test(exec);
|
|
3232
3262
|
}
|
|
3233
|
-
async start(apps, {
|
|
3263
|
+
async start(apps, {
|
|
3264
|
+
open = false,
|
|
3265
|
+
dbup = true,
|
|
3266
|
+
write = true,
|
|
3267
|
+
plain = false,
|
|
3268
|
+
kill = false,
|
|
3269
|
+
share = false,
|
|
3270
|
+
concurrency = null
|
|
3271
|
+
} = {}) {
|
|
3234
3272
|
const first = apps[0];
|
|
3235
3273
|
if (!first)
|
|
3236
3274
|
throw new Error("No app selected to start");
|
|
3237
|
-
const { mode, downgraded } = resolveDevUi(plain);
|
|
3275
|
+
const { mode, downgraded } = DevSupervisor.resolveDevUi(plain);
|
|
3238
3276
|
if (downgraded)
|
|
3239
3277
|
first.workspace.log("no sized terminal to draw on; printing prefixed lines instead");
|
|
3240
3278
|
if (kill)
|
|
3241
3279
|
await this.reclaimDevPorts(apps);
|
|
3280
|
+
const shares = share ? await this.#shareOnInterrupt(apps) : null;
|
|
3242
3281
|
if (mode === "stream" && apps.length === 1)
|
|
3243
3282
|
return await this.startOne(first, { open, dbup, write, ...DevSupervisor.childHooks() });
|
|
3244
|
-
|
|
3283
|
+
this.#interrupt.ownsExit = false;
|
|
3284
|
+
await this.#startMany(apps, { open, dbup, write, mode, concurrency, shares });
|
|
3245
3285
|
}
|
|
3246
3286
|
async startOne(app, { open = false, dbup = true, stdio = "inherit", write = true, onDevEvent } = {}) {
|
|
3247
3287
|
await app.scanSync({ write });
|
|
3248
3288
|
const akanConfig = await app.getConfig();
|
|
3249
|
-
const databaseMode =
|
|
3289
|
+
const databaseMode = akanConfig.resolveDatabaseMode();
|
|
3250
3290
|
await this.syncDatabaseModeDependencies(app, akanConfig, databaseMode);
|
|
3251
3291
|
if (app.getEnv() === "local" && dbup && databaseMode !== "single") {
|
|
3252
3292
|
const wasDbAlreadyUp = await this.dbup(app.workspace, databaseMode);
|
|
@@ -3254,15 +3294,12 @@ class ApplicationScript extends script("application", [ApplicationRunner, Librar
|
|
|
3254
3294
|
this.#stopDatabaseOnInterrupt(app.workspace);
|
|
3255
3295
|
}
|
|
3256
3296
|
const spinner = app.spinning("Preparing backend...");
|
|
3257
|
-
|
|
3297
|
+
return await this.applicationRunner.start(app, {
|
|
3258
3298
|
open,
|
|
3259
|
-
onStart: () => {
|
|
3260
|
-
spinner.succeed(`${app.name} prepared, ready to start`);
|
|
3261
|
-
},
|
|
3299
|
+
onStart: () => spinner.succeed(`${app.name} prepared, ready to start`),
|
|
3262
3300
|
onDevEvent,
|
|
3263
3301
|
stdio
|
|
3264
3302
|
});
|
|
3265
|
-
return akanAppHost;
|
|
3266
3303
|
}
|
|
3267
3304
|
async reclaimDevPorts(apps) {
|
|
3268
3305
|
const workspace = apps[0]?.workspace;
|
|
@@ -3277,13 +3314,20 @@ class ApplicationScript extends script("application", [ApplicationRunner, Librar
|
|
|
3277
3314
|
if (killed.length === 0 && foreign.length === 0)
|
|
3278
3315
|
workspace.log("dev ports are free; nothing to kill");
|
|
3279
3316
|
}
|
|
3280
|
-
async#startMany(apps, {
|
|
3317
|
+
async#startMany(apps, {
|
|
3318
|
+
open,
|
|
3319
|
+
dbup,
|
|
3320
|
+
write,
|
|
3321
|
+
mode,
|
|
3322
|
+
concurrency,
|
|
3323
|
+
shares
|
|
3324
|
+
}) {
|
|
3281
3325
|
const workspace = apps[0]?.workspace;
|
|
3282
3326
|
if (!workspace)
|
|
3283
3327
|
throw new Error("No app selected to start");
|
|
3284
3328
|
const startedDatabase = dbup ? await this.#prepareSharedDatabase(apps) : false;
|
|
3285
|
-
const supervisor = new DevSupervisor({ apps, mode, concurrency, open, write });
|
|
3286
|
-
const view = mode === "tui" ?
|
|
3329
|
+
const supervisor = new DevSupervisor({ apps, mode, concurrency, open, write, shares });
|
|
3330
|
+
const view = mode === "tui" ? new (await import("./devTui-q7k7bakd.js")).DevTui(supervisor) : new DevStreamView(apps.map((app) => app.name));
|
|
3287
3331
|
await supervisor.run(view);
|
|
3288
3332
|
if (startedDatabase)
|
|
3289
3333
|
await this.#stopDatabase(workspace);
|
|
@@ -3297,7 +3341,7 @@ class ApplicationScript extends script("application", [ApplicationRunner, Librar
|
|
|
3297
3341
|
if (app.getEnv() !== "local")
|
|
3298
3342
|
continue;
|
|
3299
3343
|
const akanConfig = await app.getConfig();
|
|
3300
|
-
const mode =
|
|
3344
|
+
const mode = akanConfig.resolveDatabaseMode();
|
|
3301
3345
|
await this.syncDatabaseModeDependencies(app, akanConfig, mode);
|
|
3302
3346
|
if (mode !== "single")
|
|
3303
3347
|
modes.add(mode);
|
|
@@ -3308,82 +3352,121 @@ class ApplicationScript extends script("application", [ApplicationRunner, Librar
|
|
|
3308
3352
|
started = true;
|
|
3309
3353
|
return started;
|
|
3310
3354
|
}
|
|
3311
|
-
async buildIos(app, { write = true,
|
|
3355
|
+
async buildIos(app, { write = true, ...options } = {}) {
|
|
3312
3356
|
await app.scanSync({ write });
|
|
3313
|
-
await this.applicationRunner.
|
|
3357
|
+
await this.applicationRunner.buildMobile(app, "ios", options);
|
|
3314
3358
|
}
|
|
3315
|
-
async
|
|
3316
|
-
open = false,
|
|
3317
|
-
operation = "local",
|
|
3318
|
-
env = "local",
|
|
3319
|
-
write = true,
|
|
3320
|
-
target,
|
|
3321
|
-
device,
|
|
3322
|
-
regenerate = false,
|
|
3323
|
-
noAllowProvisioningUpdates = false
|
|
3324
|
-
} = {}) {
|
|
3359
|
+
async buildAndroid(app, { write = true, ...options } = {}) {
|
|
3325
3360
|
await app.scanSync({ write });
|
|
3326
|
-
|
|
3327
|
-
|
|
3328
|
-
|
|
3329
|
-
await
|
|
3330
|
-
|
|
3331
|
-
|
|
3332
|
-
|
|
3333
|
-
|
|
3334
|
-
|
|
3335
|
-
|
|
3336
|
-
|
|
3361
|
+
await this.applicationRunner.buildMobile(app, "android", options);
|
|
3362
|
+
}
|
|
3363
|
+
async buildDesktop(app, { write = true, ...options } = {}) {
|
|
3364
|
+
await app.scanSync({ write });
|
|
3365
|
+
await this.applicationRunner.buildDesktop(app, options);
|
|
3366
|
+
}
|
|
3367
|
+
async startIos(app, { write = true, ...options } = {}) {
|
|
3368
|
+
await app.scanSync({ write });
|
|
3369
|
+
await this.applicationRunner.startMobile(app, "ios", options);
|
|
3370
|
+
}
|
|
3371
|
+
async startAndroid(app, { write = true, ...options } = {}) {
|
|
3372
|
+
await app.scanSync({ write });
|
|
3373
|
+
await this.applicationRunner.startMobile(app, "android", options);
|
|
3374
|
+
}
|
|
3375
|
+
async startDesktop(app, { write = true, ...options } = {}) {
|
|
3376
|
+
await app.scanSync({ write });
|
|
3377
|
+
if (options.operation === "release")
|
|
3378
|
+
return await this.applicationRunner.startDesktop(app, options);
|
|
3379
|
+
const mobileTarget = await ApplicationRunner.startTarget(app, undefined, options.target);
|
|
3380
|
+
if (!ApplicationRunner.carriesServer(mobileTarget))
|
|
3381
|
+
return await this.applicationRunner.startDesktop(app, options);
|
|
3382
|
+
DesktopServerStage.assertCarriable(await app.getConfig());
|
|
3383
|
+
const upstream = `http://localhost:${await app.getDevPort()}`;
|
|
3384
|
+
try {
|
|
3385
|
+
if (await ApplicationRunner.answers(upstream, app.name, app.workspace.workspaceRoot))
|
|
3386
|
+
app.log(`The desktop app follows the dev server on ${upstream}.`);
|
|
3387
|
+
else
|
|
3388
|
+
await this.#startDevServerFor(app, upstream);
|
|
3389
|
+
await this.applicationRunner.startDesktop(app, { ...options, interrupt: this.#interrupt });
|
|
3390
|
+
} finally {
|
|
3391
|
+
await this.#interrupt.runAll();
|
|
3392
|
+
}
|
|
3393
|
+
}
|
|
3394
|
+
async#startDevServerFor(app, upstream) {
|
|
3395
|
+
app.log(`No dev server answers on ${upstream}; starting \`akan start ${app.name}\` for the desktop app.`);
|
|
3396
|
+
let settle = () => {};
|
|
3397
|
+
const settled = new Promise((resolve) => settle = resolve);
|
|
3398
|
+
const appHost = await this.startOne(app, {
|
|
3399
|
+
write: false,
|
|
3400
|
+
onDevEvent: (event) => {
|
|
3401
|
+
if ("state" in event && (event.state === "ready" || event.state === "failed"))
|
|
3402
|
+
settle(event.state);
|
|
3403
|
+
}
|
|
3337
3404
|
});
|
|
3405
|
+
this.#interrupt.add(async () => {
|
|
3406
|
+
await appHost.stop();
|
|
3407
|
+
}, "Abandoning the dev server shutdown; its processes may still be running.", 130);
|
|
3408
|
+
let state;
|
|
3409
|
+
const timedOut = await DevSupervisor.timesOut(settled.then((settledState) => state = settledState), ApplicationScript.devServerReadyTimeoutMs);
|
|
3410
|
+
if (state === "ready")
|
|
3411
|
+
return;
|
|
3412
|
+
await this.#interrupt.runAll();
|
|
3413
|
+
throw new Error(timedOut ? `akan start ${app.name} did not answer within ${ApplicationScript.devServerReadyTimeoutMs / 1000}s; see its log above.` : `akan start ${app.name} gave up restarting its server; see its log above.`);
|
|
3338
3414
|
}
|
|
3339
|
-
async releaseIos(app, {
|
|
3415
|
+
async releaseIos(app, {
|
|
3416
|
+
write = true,
|
|
3417
|
+
allowLocalRelease = false,
|
|
3418
|
+
...options
|
|
3419
|
+
} = {}) {
|
|
3340
3420
|
await app.scanSync({ write });
|
|
3341
|
-
|
|
3342
|
-
|
|
3343
|
-
await this.applicationRunner.releaseIos(app, { target, env, regenerate });
|
|
3421
|
+
ApplicationScript.#assertReleaseEnv("releaseIos", options.env ?? "main", allowLocalRelease);
|
|
3422
|
+
await this.applicationRunner.releaseIos(app, options);
|
|
3344
3423
|
}
|
|
3345
|
-
async
|
|
3424
|
+
async releaseAndroid(app, format, {
|
|
3425
|
+
write = true,
|
|
3426
|
+
allowLocalRelease = false,
|
|
3427
|
+
...options
|
|
3428
|
+
} = {}) {
|
|
3346
3429
|
await app.scanSync({ write });
|
|
3347
|
-
|
|
3430
|
+
ApplicationScript.#assertReleaseEnv("releaseAndroid", options.env ?? "main", allowLocalRelease);
|
|
3431
|
+
await this.applicationRunner.releaseAndroid(app, format, options);
|
|
3348
3432
|
}
|
|
3349
|
-
async
|
|
3350
|
-
open = false,
|
|
3351
|
-
operation = "local",
|
|
3352
|
-
env = "local",
|
|
3433
|
+
async packUpdate(app, platform, {
|
|
3353
3434
|
write = true,
|
|
3354
|
-
|
|
3355
|
-
|
|
3435
|
+
allowLocalRelease = false,
|
|
3436
|
+
...options
|
|
3356
3437
|
} = {}) {
|
|
3357
3438
|
await app.scanSync({ write });
|
|
3358
|
-
|
|
3359
|
-
await this.
|
|
3360
|
-
|
|
3361
|
-
|
|
3362
|
-
|
|
3363
|
-
operation,
|
|
3364
|
-
env,
|
|
3365
|
-
target,
|
|
3366
|
-
regenerate
|
|
3367
|
-
});
|
|
3439
|
+
ApplicationScript.#assertReleaseEnv("packUpdate", options.env ?? "main", allowLocalRelease);
|
|
3440
|
+
await this.applicationRunner.packUpdate(app, platform, options);
|
|
3441
|
+
}
|
|
3442
|
+
async updateKeygen(app, platform, { target } = {}) {
|
|
3443
|
+
await this.applicationRunner.updateKeygen(app, platform, target);
|
|
3368
3444
|
}
|
|
3369
|
-
async
|
|
3445
|
+
async publishUpdate(app, platform, {
|
|
3446
|
+
write = true,
|
|
3447
|
+
allowLocalRelease = false,
|
|
3448
|
+
...options
|
|
3449
|
+
} = {}) {
|
|
3370
3450
|
await app.scanSync({ write });
|
|
3371
|
-
|
|
3372
|
-
|
|
3373
|
-
await this.applicationRunner.releaseAndroid(app, assembleType, {
|
|
3374
|
-
target,
|
|
3375
|
-
env,
|
|
3376
|
-
regenerate
|
|
3377
|
-
});
|
|
3451
|
+
ApplicationScript.#assertReleaseEnv("publishUpdate", options.env ?? "main", allowLocalRelease);
|
|
3452
|
+
await this.applicationRunner.publishUpdate(app, platform, options);
|
|
3378
3453
|
}
|
|
3379
|
-
|
|
3380
|
-
|
|
3454
|
+
static #assertReleaseEnv(command, env, allowLocalRelease) {
|
|
3455
|
+
if (env === "local" && !allowLocalRelease)
|
|
3456
|
+
throw new Error(`${command} --env local is blocked. Pass allowLocalRelease only for explicit local release testing.`);
|
|
3381
3457
|
}
|
|
3382
|
-
async
|
|
3383
|
-
await this.applicationRunner.
|
|
3458
|
+
async transferDatabase(app, direction, dir) {
|
|
3459
|
+
await this.applicationRunner.transferDatabase(app, direction, dir);
|
|
3384
3460
|
}
|
|
3385
|
-
async
|
|
3386
|
-
await
|
|
3461
|
+
async dbupDeclared(workspace, mode) {
|
|
3462
|
+
const declared = mode ? [mode] : await Promise.all((await workspace.getApps()).map(async (appName) => (await AppExecutor.from(workspace, appName).getConfig()).database.modes)).then((modes) => [...new Set(modes.flat())]);
|
|
3463
|
+
const needing = declared.filter((each) => each !== "single");
|
|
3464
|
+
if (!needing.length) {
|
|
3465
|
+
workspace.log("No app here declares a database mode that runs on local services; single needs none.");
|
|
3466
|
+
return;
|
|
3467
|
+
}
|
|
3468
|
+
for (const each of needing)
|
|
3469
|
+
await this.dbup(workspace, each);
|
|
3387
3470
|
}
|
|
3388
3471
|
async dbup(workspace, mode = "multiple") {
|
|
3389
3472
|
const spinner = workspace.spinning(`Starting local database (${mode})...`);
|
|
@@ -3401,34 +3484,36 @@ class ApplicationScript extends script("application", [ApplicationRunner, Librar
|
|
|
3401
3484
|
}
|
|
3402
3485
|
spinner.succeed("Local database (/local/docker-compose.yaml) is down");
|
|
3403
3486
|
}
|
|
3404
|
-
#
|
|
3405
|
-
|
|
3406
|
-
|
|
3407
|
-
|
|
3408
|
-
|
|
3409
|
-
|
|
3410
|
-
|
|
3411
|
-
|
|
3412
|
-
|
|
3487
|
+
async#shareOnInterrupt(apps) {
|
|
3488
|
+
const shares = [];
|
|
3489
|
+
const urls = new Map;
|
|
3490
|
+
this.#interrupt.add(async () => {
|
|
3491
|
+
await Promise.all(shares.map(async (share) => await share.close()));
|
|
3492
|
+
}, "Abandoning the tunnel release; the shares expire on their own.");
|
|
3493
|
+
await Promise.all(apps.map(async (app) => {
|
|
3494
|
+
const { TunnelShare } = await import("./TunnelShare-cwh9ezsm.js");
|
|
3495
|
+
const share = await TunnelShare.open(app, { workspace: app.workspace });
|
|
3496
|
+
shares.push(share);
|
|
3497
|
+
urls.set(app.name, share.url);
|
|
3498
|
+
Logger6.rawLog(`${app.name} is shared at ${share.url}`);
|
|
3499
|
+
})).catch((error) => {
|
|
3500
|
+
Logger6.rawLog(`Could not open the tunnel: ${error instanceof Error ? error.message : String(error)}`, undefined, "error");
|
|
3413
3501
|
});
|
|
3502
|
+
return urls;
|
|
3503
|
+
}
|
|
3504
|
+
#stopDatabaseOnInterrupt(workspace) {
|
|
3505
|
+
this.#interrupt.add(async () => {
|
|
3506
|
+
await this.#stopDatabase(workspace);
|
|
3507
|
+
}, "Abandoning the local database teardown; containers are left running.");
|
|
3414
3508
|
}
|
|
3415
3509
|
async#stopDatabase(workspace) {
|
|
3416
|
-
|
|
3417
|
-
|
|
3418
|
-
timer = setTimeout(() => resolve("timeout"), ApplicationScript.dbShutdownTimeoutMs);
|
|
3510
|
+
const stopped = this.dbdown(workspace).catch(() => {
|
|
3511
|
+
return;
|
|
3419
3512
|
});
|
|
3420
|
-
|
|
3421
|
-
|
|
3422
|
-
|
|
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
|
-
}
|
|
3513
|
+
if (!await DevSupervisor.timesOut(stopped, ApplicationScript.dbShutdownTimeoutMs))
|
|
3514
|
+
return;
|
|
3515
|
+
Logger6.rawLog(`Local database did not stop within ${ApplicationScript.dbShutdownTimeoutMs}ms; run \`akan dbdown\` once Docker responds.`, undefined, "error");
|
|
3431
3516
|
}
|
|
3432
3517
|
}
|
|
3433
3518
|
|
|
3434
|
-
export {
|
|
3519
|
+
export { ApplicationScript };
|