@akanjs/cli 3.0.0-beta.8 → 3.0.0
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-2vmp9qe0.js +9 -0
- package/CodeAgentRpcHost-p7h6n2hp.js +7 -0
- package/CodeAgentRpcListener-7p7z5dd7.js +97 -0
- package/CodeTui-5ed6bg2a.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-gn73rhe7.js} +8 -9
- package/akanCode.js +102 -0
- package/application.command-e968t0hw.js +246 -0
- package/{applicationBuildRunner-kxwbssgn.js → applicationBuildRunner-64taxseg.js} +121 -77
- package/buildBatch.proc.js +120 -32
- package/cloud.command-0jm301rr.js +95 -0
- package/cloud.script-nemnzjpv.js +20 -0
- package/code.command-18kcm7hq.js +154 -0
- package/commandManifest.json +1 -1
- package/consoleToStderr-h5bryj94.js +7 -0
- package/context.command-trd9bn18.js +48 -0
- package/context.runner-y5hwh673.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-bdmdn2de.js +42 -0
- package/guidelines/agentRule/agentRule.instruction.md +154 -26
- package/guidelines/conventions/conventions.instruction.md +134 -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-0wpybxtr.js +2729 -0
- package/{index-kcxmv48c.js → index-2ahj896a.js} +45 -79
- package/index-2wc41k5w.js +49 -0
- package/index-3hx1z96c.js +87 -0
- package/{index-2tv3fdx4.js → index-3w94e7n3.js} +1228 -1458
- package/{index-hwkygn9s.js → index-48k96p55.js} +325 -36
- package/index-4npz05nm.js +187 -0
- package/{index-q0y2jq9j.js → index-52w0fvh8.js} +21 -4
- package/index-6he6gvfg.js +4300 -0
- package/index-7md2g9vw.js +6 -0
- package/{index-04bk599g.js → index-93j60em0.js} +10 -17
- package/index-94s080qm.js +266 -0
- package/index-9dp031by.js +12 -0
- package/{index-ktb5njgy.js → index-9z771ptz.js} +49 -99
- package/{index-y6n8nmny.js → index-abxpehf8.js} +75 -203
- package/{index-3d9y066c.js → index-bjwxs904.js} +9 -8
- package/{index-9077wzd2.js → index-by9pxkr0.js} +43 -26
- package/index-c6eggbee.js +123 -0
- package/index-d2242ksy.js +99 -0
- package/index-drehwpdt.js +1119 -0
- package/{index-92zztqy1.js → index-dy0jx9ay.js} +2 -17
- package/{index-ffvk10zz.js → index-f3c78nd6.js} +107 -241
- package/index-gbggbqcp.js +1058 -0
- package/{index-by5gpp8x.js → index-hj6w4ka1.js} +2 -1
- package/index-hsmtysz4.js +53 -0
- package/{index-j9yhg404.js → index-jmrn54tt.js} +4 -7
- package/{index-a64esmtb.js → index-kcdj1z5v.js} +523 -1022
- package/{index-d43r08tf.js → index-kqabytj7.js} +77 -150
- package/{index-vyvre9en.js → index-mbxbhay5.js} +6 -2
- package/{index-vxtad5bd.js → index-mgxpx3f2.js} +5 -8
- package/index-p6pbz9n5.js +25 -0
- package/{index-sj5vxr8f.js → index-p993efkq.js} +981 -897
- package/{index-2xp0kfkt.js → index-pmhqttv3.js} +37 -25
- package/index-tqm7jjcn.js +700 -0
- package/{index-0g7r8xz3.js → index-vpj42xwt.js} +114 -232
- package/index-wa5jbxj1.js +760 -0
- package/{index-k2j1hc4r.js → index-wprnsjms.js} +12 -12
- package/{index-gyd220p2.js → index-y3mphr1m.js} +554 -577
- package/index-yg7jhz8x.js +69 -0
- package/{index-h7yw2z9n.js → index-zzvvc845.js} +8 -14
- package/index.js +23 -22
- package/{library.command-qzzbj6d8.js → library.command-9h749a1x.js} +4 -5
- package/{localRegistry.command-vyrgg4xw.js → localRegistry.command-2bqqhxyz.js} +23 -43
- package/{module.command-rq5hq3ax.js → module.command-nv8nzgft.js} +8 -12
- package/{package.command-d9hd6v9n.js → package.command-mq4bggp6.js} +6 -7
- package/package.json +15 -7
- package/{page.command-126d56te.js → page.command-j639n707.js} +4 -5
- package/{primitive.command-taxd1538.js → primitive.command-kz6849th.js} +9 -23
- package/prompter-76k56b37.js +8 -0
- package/{quality.command-6gsna0a3.js → quality.command-4hyabwkb.js} +205 -377
- package/{recipeScanner-fpagtgc1.js → recipeScanner-4b69ybe0.js} +1 -1
- package/{repair.command-tq2dgy72.js → repair.command-a9awpbqr.js} +11 -35
- package/{routeSourceValidator-apyqw7wk.js → routeSourceValidator-7ycnfz0e.js} +0 -6
- package/{scalar.command-3bcmrrkv.js → scalar.command-ma4fpm9m.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-3k2pmrs4.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-xwr46b66.js +129 -0
- package/{typeChecker-3xgrzc2v.js → typeChecker-px49mezc.js} +1 -1
- package/typecheck.proc.js +1 -1
- package/workflow.command-t7wqe4h7.js +79 -0
- package/{workspace.command-06wvstgr.js → workspace.command-4derhzrh.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-48k96p55.js";
|
|
2
37
|
import {
|
|
3
38
|
runner,
|
|
4
39
|
script
|
|
5
|
-
} from "./index-
|
|
40
|
+
} from "./index-vpj42xwt.js";
|
|
6
41
|
import {
|
|
7
42
|
WorkspaceExecutor,
|
|
8
43
|
AppExecutor,
|
|
9
44
|
LibExecutor,
|
|
10
45
|
PkgExecutor
|
|
11
|
-
} from "./index-
|
|
46
|
+
} from "./index-3w94e7n3.js";
|
|
47
|
+
import {
|
|
48
|
+
DesktopServerStage,
|
|
49
|
+
resolveMobileTargets,
|
|
50
|
+
resolveAppId,
|
|
51
|
+
NativeApp
|
|
52
|
+
} from "./index-gbggbqcp.js";
|
|
12
53
|
import {
|
|
13
54
|
LibraryScript
|
|
14
|
-
} from "./index-
|
|
55
|
+
} from "./index-mgxpx3f2.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-jmrn54tt.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-64taxseg.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;
|
|
@@ -2232,72 +2059,188 @@ try {
|
|
|
2232
2059
|
setTimeout(() => openBrowser(`http://localhost:${env.AKAN_PUBLIC_CLIENT_PORT ?? env.PORT ?? "8282"}`), 3000);
|
|
2233
2060
|
return appHost;
|
|
2234
2061
|
}
|
|
2235
|
-
async
|
|
2062
|
+
async buildMobile(app, platform, { target, env = "debug", profile = "release", installer = false, arch } = {}) {
|
|
2236
2063
|
const targets = await resolveMobileTargets(app, target);
|
|
2237
|
-
|
|
2064
|
+
NativeApp.assertInstaller(platform, installer);
|
|
2065
|
+
if (arch && platform !== "macos" && platform !== "windows" && platform !== "linux")
|
|
2066
|
+
throw new Error(`--arch picks a desktop app's CPU; ${platform} builds for its own.`);
|
|
2067
|
+
if (arch === "x64" && platform === "macos")
|
|
2068
|
+
throw new Error("A macOS app is built for Apple silicon (arm64) only.");
|
|
2069
|
+
const carried = await this.#stageMobile(app, platform, targets, env, arch);
|
|
2238
2070
|
await this.#runMobileTargets(targets, async (mobileTarget) => {
|
|
2239
|
-
await new (
|
|
2071
|
+
this.#reportBuild(app, mobileTarget, await new NativeApp(app, mobileTarget, env).build(platform, {
|
|
2072
|
+
profile,
|
|
2073
|
+
...carried && ApplicationRunner.carriesServer(mobileTarget, platform) ? { server: carried } : {},
|
|
2074
|
+
...installer ? { installer } : {},
|
|
2075
|
+
...arch ? { arch } : {}
|
|
2076
|
+
}));
|
|
2240
2077
|
});
|
|
2241
2078
|
}
|
|
2242
|
-
async
|
|
2243
|
-
|
|
2244
|
-
|
|
2245
|
-
|
|
2246
|
-
target
|
|
2247
|
-
|
|
2248
|
-
|
|
2249
|
-
|
|
2250
|
-
|
|
2251
|
-
|
|
2252
|
-
|
|
2253
|
-
|
|
2254
|
-
|
|
2255
|
-
|
|
2256
|
-
await
|
|
2257
|
-
|
|
2258
|
-
|
|
2259
|
-
}
|
|
2079
|
+
async buildDesktop(app, options = {}) {
|
|
2080
|
+
await this.buildMobile(app, NativeApp.desktopPlatform(), options);
|
|
2081
|
+
}
|
|
2082
|
+
async startMobile(app, platform, { target, env = "local", operation = "local", device, teamId, interrupt } = {}) {
|
|
2083
|
+
const mobileTarget = await ApplicationRunner.startTarget(app, platform, target);
|
|
2084
|
+
const nativeApp = new NativeApp(app, mobileTarget, env);
|
|
2085
|
+
const selection = { ...device ? { device } : {}, ...teamId ? { teamId } : {} };
|
|
2086
|
+
if (operation === "release") {
|
|
2087
|
+
const carried = await this.#stageMobile(app, platform, [mobileTarget], env);
|
|
2088
|
+
const running = await nativeApp.run(platform, {
|
|
2089
|
+
...selection,
|
|
2090
|
+
profile: "release",
|
|
2091
|
+
...carried ? { server: carried } : {}
|
|
2092
|
+
});
|
|
2093
|
+
await running.exited;
|
|
2094
|
+
return;
|
|
2095
|
+
}
|
|
2096
|
+
const upstream = `http://localhost:${await app.getDevPort()}`;
|
|
2097
|
+
if (!await ApplicationRunner.answers(upstream, app.name, app.workspace.workspaceRoot))
|
|
2098
|
+
throw new Error(`No dev server answers on ${upstream}; run \`akan start ${app.name}\` first.`);
|
|
2099
|
+
const { i18n } = await app.getConfig();
|
|
2100
|
+
const session = await nativeApp.dev(platform, { upstream, lang: i18n.defaultLocale, ...selection });
|
|
2101
|
+
if (interrupt)
|
|
2102
|
+
interrupt.add(async () => await session.stop(), "Abandoning the app's shutdown; its window may stay open.", 130);
|
|
2103
|
+
else
|
|
2104
|
+
process.once("SIGINT", () => {
|
|
2105
|
+
session.stop().finally(() => process.exit(130));
|
|
2106
|
+
});
|
|
2107
|
+
await session.exited;
|
|
2260
2108
|
}
|
|
2261
|
-
async
|
|
2109
|
+
async startDesktop(app, options = {}) {
|
|
2110
|
+
await this.startMobile(app, NativeApp.desktopPlatform(), options);
|
|
2111
|
+
}
|
|
2112
|
+
static async startTarget(app, platform = NativeApp.desktopPlatform(), target) {
|
|
2262
2113
|
const targets = await resolveMobileTargets(app, target);
|
|
2263
|
-
|
|
2264
|
-
|
|
2265
|
-
|
|
2114
|
+
const [mobileTarget] = targets;
|
|
2115
|
+
if (!mobileTarget || targets.length > 1)
|
|
2116
|
+
throw new Error(`start-${platform === "ios" || platform === "android" ? platform : "desktop"} runs one native target at a time; pass --target <name>.`);
|
|
2117
|
+
return mobileTarget;
|
|
2118
|
+
}
|
|
2119
|
+
static async answers(url, appName, workspaceRoot) {
|
|
2120
|
+
const signal = AbortSignal.timeout(3000);
|
|
2121
|
+
const health = await fetch(new URL("/_akan/app/health", url), { signal }).catch(() => null);
|
|
2122
|
+
if (!health)
|
|
2123
|
+
return false;
|
|
2124
|
+
const info = await fetch(new URL("/_akan/app/info", url), { signal }).then(async (res) => res.ok ? await res.json() : null).catch(() => null);
|
|
2125
|
+
const stopIt = `Stop it (\`akan start ${appName} --kill\` takes the port over) or give ${appName} another port with AKAN_DEV_PORT`;
|
|
2126
|
+
if (info?.appName === appName) {
|
|
2127
|
+
if (typeof info.workspaceRoot !== "string" || ApplicationRunner.#sameCheckout(info.workspaceRoot, workspaceRoot))
|
|
2128
|
+
return true;
|
|
2129
|
+
throw new Error(`${url} is the dev server of ${appName} in ${info.workspaceRoot}, not in ${workspaceRoot}. ${stopIt}.`);
|
|
2266
2130
|
}
|
|
2131
|
+
if (typeof info?.appName === "string")
|
|
2132
|
+
throw new Error(`${url} is the dev server of ${info.appName}, not ${appName}. ${stopIt}.`);
|
|
2133
|
+
const { pid } = await health.json().catch(() => null) ?? {};
|
|
2134
|
+
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
2135
|
}
|
|
2268
|
-
|
|
2136
|
+
static #sameCheckout(reported, own) {
|
|
2137
|
+
const canonical = (root) => {
|
|
2138
|
+
try {
|
|
2139
|
+
return realpathSync.native(root);
|
|
2140
|
+
} catch {
|
|
2141
|
+
return path5.resolve(root);
|
|
2142
|
+
}
|
|
2143
|
+
};
|
|
2144
|
+
return canonical(reported) === canonical(own);
|
|
2145
|
+
}
|
|
2146
|
+
async releaseIos(app, { target, env = "main", teamId, adHoc = false } = {}) {
|
|
2269
2147
|
const targets = await resolveMobileTargets(app, target);
|
|
2270
2148
|
await this.#buildMobileCsr(app, env);
|
|
2271
|
-
|
|
2272
|
-
await new (
|
|
2273
|
-
});
|
|
2149
|
+
for (const mobileTarget of targets)
|
|
2150
|
+
this.#reportBuild(app, mobileTarget, await new NativeApp(app, mobileTarget, env).releaseIos({ teamId, adHoc }));
|
|
2274
2151
|
}
|
|
2275
|
-
async
|
|
2276
|
-
open = false,
|
|
2277
|
-
operation = "local",
|
|
2278
|
-
env = "local",
|
|
2279
|
-
target,
|
|
2280
|
-
regenerate = false
|
|
2281
|
-
} = {}) {
|
|
2152
|
+
async releaseAndroid(app, format, { target, env = "main" } = {}) {
|
|
2282
2153
|
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
|
-
});
|
|
2154
|
+
NativeApp.androidSigning();
|
|
2155
|
+
await this.#buildMobileCsr(app, env);
|
|
2156
|
+
for (const mobileTarget of targets)
|
|
2157
|
+
this.#reportBuild(app, mobileTarget, await new NativeApp(app, mobileTarget, env).releaseAndroid({ formats: [format] }));
|
|
2291
2158
|
}
|
|
2292
|
-
async
|
|
2159
|
+
async packUpdate(app, platform, { target, env = "main", out, against }) {
|
|
2293
2160
|
const targets = await resolveMobileTargets(app, target);
|
|
2161
|
+
const [mobileTarget] = targets;
|
|
2162
|
+
if (!mobileTarget || targets.length > 1)
|
|
2163
|
+
throw new Error("pack-update packs one native target at a time; pass --target <name>.");
|
|
2294
2164
|
await this.#buildMobileCsr(app, env);
|
|
2295
|
-
|
|
2296
|
-
|
|
2297
|
-
|
|
2298
|
-
|
|
2165
|
+
const packed = await new NativeApp(app, mobileTarget, env).packUpdate(platform, { out });
|
|
2166
|
+
const size = packed.manifest.files.reduce((sum, file) => sum + file.size, 0);
|
|
2167
|
+
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}`);
|
|
2168
|
+
if (!against)
|
|
2169
|
+
return;
|
|
2170
|
+
const bundle = path5.join(packed.out, "bundle.json");
|
|
2171
|
+
const compat = await NativeApp.compareBundles(app.cwdPath, against, bundle);
|
|
2172
|
+
await Bun.write(path5.join(packed.out, "compat.json"), `${JSON.stringify({ against, ...compat }, null, 2)}
|
|
2173
|
+
`);
|
|
2174
|
+
if (!compat.compatible)
|
|
2175
|
+
throw new Error(`This bundle needs a new store build; the one ${against} came from provides another native API:
|
|
2176
|
+
- ${compat.problems.join(`
|
|
2177
|
+
- `)}`);
|
|
2178
|
+
app.logger.info(`runs in the store build of ${against}`);
|
|
2179
|
+
}
|
|
2180
|
+
async updateKeygen(app, platform, target) {
|
|
2181
|
+
const nativePlatform = platform === "desktop" ? NativeApp.desktopPlatform() : platform;
|
|
2182
|
+
const seen = new Set;
|
|
2183
|
+
for (const mobileTarget of await resolveMobileTargets(app, target ?? "all")) {
|
|
2184
|
+
const appId = resolveAppId(mobileTarget.config.appId, nativePlatform);
|
|
2185
|
+
if (seen.has(appId))
|
|
2186
|
+
continue;
|
|
2187
|
+
seen.add(appId);
|
|
2188
|
+
const { publicKey, keyPath, created } = await new NativeApp(app, mobileTarget).updateKeygen(nativePlatform);
|
|
2189
|
+
app.log(`${appId}: ${created ? "made" : "read"} ${keyPath}${created ? " (keep it private and backed up: without it, installed apps take no more updates)" : ""}`);
|
|
2190
|
+
app.log(` native: { updates: { url: "https://\u2026/${app.name}", publicKey: ${JSON.stringify(publicKey)} } }`);
|
|
2191
|
+
const declared = mobileTarget.config.updates?.publicKey;
|
|
2192
|
+
if (declared && declared !== publicKey)
|
|
2193
|
+
app.logger.warn(`native.targets.${mobileTarget.name}.updates.publicKey is another key's; releases would not verify.`);
|
|
2299
2194
|
}
|
|
2300
2195
|
}
|
|
2196
|
+
async publishUpdate(app, platform, { target, env = "main", channel } = {}) {
|
|
2197
|
+
const targets = await resolveMobileTargets(app, target);
|
|
2198
|
+
const nativePlatform = platform === "desktop" ? NativeApp.desktopPlatform() : platform;
|
|
2199
|
+
for (const mobileTarget of targets)
|
|
2200
|
+
await new NativeApp(app, mobileTarget, env).assertPublishable(nativePlatform, {
|
|
2201
|
+
server: ApplicationRunner.carriesServer(mobileTarget, nativePlatform),
|
|
2202
|
+
...channel ? { channel } : {}
|
|
2203
|
+
});
|
|
2204
|
+
const carried = await this.#stageMobile(app, nativePlatform, targets, env);
|
|
2205
|
+
for (const mobileTarget of targets) {
|
|
2206
|
+
const nativeApp = new NativeApp(app, mobileTarget, env);
|
|
2207
|
+
const published = await nativeApp.publishUpdate(nativePlatform, {
|
|
2208
|
+
...channel ? { channel } : {},
|
|
2209
|
+
...carried && ApplicationRunner.carriesServer(mobileTarget, nativePlatform) ? { server: carried } : {}
|
|
2210
|
+
});
|
|
2211
|
+
this.#reportBuild(app, mobileTarget, published.build);
|
|
2212
|
+
app.log(`${app.name}/${mobileTarget.name} ${published.bundle}: ${published.dir}/${published.channel}.json, ${published.files} files, ${Math.round(published.size / 1024)} KiB`);
|
|
2213
|
+
app.log(`Upload ${nativeApp.updatesDir} to ${mobileTarget.config.updates?.url}, the manifests last.`);
|
|
2214
|
+
}
|
|
2215
|
+
}
|
|
2216
|
+
#reportBuild(app, mobileTarget, { artifacts, warnings, signing }) {
|
|
2217
|
+
for (const warning of warnings)
|
|
2218
|
+
app.logger.warn(warning);
|
|
2219
|
+
if (signing)
|
|
2220
|
+
app.log(`Signed with ${signing.identity} (${signing.profile}, team ${signing.teamId})`);
|
|
2221
|
+
for (const artifact of artifacts)
|
|
2222
|
+
app.log(`${app.name}/${mobileTarget.name} ${artifact.kind}: ${artifact.path}`);
|
|
2223
|
+
}
|
|
2224
|
+
static carriesServer({ config }, platform = NativeApp.desktopPlatform()) {
|
|
2225
|
+
return platform !== "ios" && platform !== "android" && !!config.desktop?.server;
|
|
2226
|
+
}
|
|
2227
|
+
static serverOmit(targets, platform) {
|
|
2228
|
+
const carrying = targets.filter((target) => ApplicationRunner.carriesServer(target, platform));
|
|
2229
|
+
const omits = carrying.map(({ config }) => {
|
|
2230
|
+
const server = config.desktop?.server;
|
|
2231
|
+
return typeof server === "object" ? server.omit ?? [] : [];
|
|
2232
|
+
});
|
|
2233
|
+
const [first = [], ...rest] = omits;
|
|
2234
|
+
if (rest.some((omit) => omit.join("\x00") !== first.join("\x00")))
|
|
2235
|
+
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.`);
|
|
2236
|
+
return first;
|
|
2237
|
+
}
|
|
2238
|
+
async#stageMobile(app, platform, targets, env, arch) {
|
|
2239
|
+
const stage = targets.some((mobileTarget) => ApplicationRunner.carriesServer(mobileTarget, platform)) ? new DesktopServerStage(app) : null;
|
|
2240
|
+
await stage?.install(arch, ApplicationRunner.serverOmit(targets, platform));
|
|
2241
|
+
await this.#buildMobileCsr(app, env);
|
|
2242
|
+
return await stage?.prepare(env);
|
|
2243
|
+
}
|
|
2301
2244
|
async#buildMobileCsr(app, env) {
|
|
2302
2245
|
const prevEnv = {
|
|
2303
2246
|
AKAN_PUBLIC_ENV: process.env.AKAN_PUBLIC_ENV,
|
|
@@ -2310,7 +2253,7 @@ try {
|
|
|
2310
2253
|
APP_OPERATION_MODE: "release"
|
|
2311
2254
|
});
|
|
2312
2255
|
try {
|
|
2313
|
-
await new (await loadBuildRunner())(app).build({ spinner: true });
|
|
2256
|
+
await new (await loadBuildRunner())(app, { environment: env }).build({ spinner: true });
|
|
2314
2257
|
} finally {
|
|
2315
2258
|
for (const [key, value] of Object.entries(prevEnv)) {
|
|
2316
2259
|
if (value === undefined)
|
|
@@ -2335,24 +2278,23 @@ try {
|
|
|
2335
2278
|
return;
|
|
2336
2279
|
for (const failure of failures) {
|
|
2337
2280
|
const message = failure.error instanceof Error ? failure.error.message : String(failure.error);
|
|
2338
|
-
|
|
2281
|
+
Logger3.rawLog(`Native target ${failure.target} failed: ${message}`, undefined, "error");
|
|
2339
2282
|
}
|
|
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();
|
|
2283
|
+
throw new Error(`${failures.length}/${results.length} native targets failed`);
|
|
2348
2284
|
}
|
|
2349
2285
|
#getLocalDatabaseServices(mode) {
|
|
2350
2286
|
if (mode === "single")
|
|
2351
2287
|
return [];
|
|
2352
2288
|
if (mode === "multiple")
|
|
2353
|
-
return ["redis"
|
|
2289
|
+
return ["redis"];
|
|
2354
2290
|
return ["redis", "postgres"];
|
|
2355
2291
|
}
|
|
2292
|
+
async#assertComposeHas(workspace, services) {
|
|
2293
|
+
const compose = Bun.YAML.parse(await Bun.file(`${workspace.workspaceRoot}/local/docker-compose.yaml`).text());
|
|
2294
|
+
const missing = services.filter((service) => !compose?.services?.[service]);
|
|
2295
|
+
if (missing.length)
|
|
2296
|
+
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.`);
|
|
2297
|
+
}
|
|
2356
2298
|
async#isLocalDatabaseUp(workspace, mode) {
|
|
2357
2299
|
const requiredServices = this.#getLocalDatabaseServices(mode);
|
|
2358
2300
|
if (!requiredServices.length)
|
|
@@ -2366,16 +2308,18 @@ try {
|
|
|
2366
2308
|
async dbup(workspace, mode = "multiple") {
|
|
2367
2309
|
if (mode === "single")
|
|
2368
2310
|
return true;
|
|
2311
|
+
const services = this.#getLocalDatabaseServices(mode);
|
|
2312
|
+
await workspace.applyTemplate({
|
|
2313
|
+
basePath: "local",
|
|
2314
|
+
template: "localDev",
|
|
2315
|
+
dict: { repoName: workspace.repoName },
|
|
2316
|
+
overwrite: false
|
|
2317
|
+
});
|
|
2318
|
+
await this.#assertComposeHas(workspace, services);
|
|
2369
2319
|
try {
|
|
2370
|
-
await workspace.applyTemplate({
|
|
2371
|
-
basePath: "local",
|
|
2372
|
-
template: "localDev",
|
|
2373
|
-
dict: { repoName: workspace.repoName },
|
|
2374
|
-
overwrite: false
|
|
2375
|
-
});
|
|
2376
2320
|
const wasAlreadyUp = await this.#isLocalDatabaseUp(workspace, mode);
|
|
2377
2321
|
if (!wasAlreadyUp)
|
|
2378
|
-
await workspace.spawn(`docker`, ["compose", "up", "-d", ...
|
|
2322
|
+
await workspace.spawn(`docker`, ["compose", "up", "-d", ...services], {
|
|
2379
2323
|
cwd: `${workspace.workspaceRoot}/local`
|
|
2380
2324
|
});
|
|
2381
2325
|
return wasAlreadyUp;
|
|
@@ -2393,74 +2337,182 @@ ${detail}`
|
|
|
2393
2337
|
async dbdown(workspace) {
|
|
2394
2338
|
await workspace.spawn(`docker`, ["compose", "down"], { cwd: `${workspace.workspaceRoot}/local` });
|
|
2395
2339
|
}
|
|
2396
|
-
|
|
2397
|
-
|
|
2398
|
-
|
|
2399
|
-
|
|
2400
|
-
|
|
2401
|
-
|
|
2402
|
-
|
|
2403
|
-
|
|
2404
|
-
|
|
2405
|
-
|
|
2406
|
-
|
|
2407
|
-
|
|
2408
|
-
|
|
2409
|
-
|
|
2410
|
-
|
|
2411
|
-
|
|
2412
|
-
|
|
2413
|
-
|
|
2414
|
-
|
|
2415
|
-
|
|
2416
|
-
|
|
2417
|
-
|
|
2418
|
-
|
|
2419
|
-
|
|
2340
|
+
}
|
|
2341
|
+
|
|
2342
|
+
// pkgs/@akanjs/cli/application/hostProcessProbe.ts
|
|
2343
|
+
import { readdirSync, readFileSync, readlinkSync } from "fs";
|
|
2344
|
+
|
|
2345
|
+
class HostProcessProbe {
|
|
2346
|
+
static timeoutMs = 3000;
|
|
2347
|
+
static win32TimeoutMs = 20000;
|
|
2348
|
+
static #procTcpListen = "0A";
|
|
2349
|
+
static #win32ProcessQuery = "[Console]::OutputEncoding=[Text.Encoding]::UTF8; Get-CimInstance Win32_Process | Select-Object ProcessId,ParentProcessId,CommandLine,CreationDate | ConvertTo-Json -Compress";
|
|
2350
|
+
platform;
|
|
2351
|
+
#win32Table = null;
|
|
2352
|
+
constructor(platform = process.platform) {
|
|
2353
|
+
this.platform = platform;
|
|
2354
|
+
}
|
|
2355
|
+
async listenersOn(port) {
|
|
2356
|
+
if (this.platform === "win32")
|
|
2357
|
+
return HostProcessProbe.parseNetstat(await HostProcessProbe.run(["netstat", "-ano"]), port);
|
|
2358
|
+
if (this.platform === "linux")
|
|
2359
|
+
return HostProcessProbe.#procListenersOn(port);
|
|
2360
|
+
const output = await HostProcessProbe.run(["lsof", "-ti", `tcp:${port}`, "-sTCP:LISTEN"]);
|
|
2361
|
+
return [
|
|
2362
|
+
...new Set(output.split(`
|
|
2363
|
+
`).map((line) => Number(line.trim())).filter((pid) => Number.isInteger(pid) && pid > 0))
|
|
2364
|
+
];
|
|
2420
2365
|
}
|
|
2421
|
-
async
|
|
2422
|
-
|
|
2366
|
+
async parentOf(pid) {
|
|
2367
|
+
if (this.platform === "win32") {
|
|
2368
|
+
const child = await this.#win32Process(pid);
|
|
2369
|
+
const parent = child ? await this.#win32Process(child.ppid) : null;
|
|
2370
|
+
return child && parent ? HostProcessProbe.parentPidOf(child, parent) : null;
|
|
2371
|
+
}
|
|
2372
|
+
if (this.platform === "linux") {
|
|
2373
|
+
const stat = HostProcessProbe.#readProc(pid, "stat");
|
|
2374
|
+
return stat === null ? null : HostProcessProbe.parseProcStatPpid(stat);
|
|
2375
|
+
}
|
|
2376
|
+
const parsed = Number((await HostProcessProbe.run(["ps", "-p", String(pid), "-o", "ppid="])).trim());
|
|
2377
|
+
return Number.isInteger(parsed) && parsed > 0 ? parsed : null;
|
|
2423
2378
|
}
|
|
2424
|
-
async
|
|
2425
|
-
|
|
2426
|
-
|
|
2427
|
-
|
|
2428
|
-
|
|
2429
|
-
|
|
2430
|
-
|
|
2431
|
-
|
|
2432
|
-
environment,
|
|
2433
|
-
local
|
|
2434
|
-
});
|
|
2435
|
-
return;
|
|
2379
|
+
async commandOf(pid) {
|
|
2380
|
+
if (this.platform === "win32")
|
|
2381
|
+
return (await this.#win32Process(pid))?.command ?? null;
|
|
2382
|
+
if (this.platform === "linux") {
|
|
2383
|
+
const cmdline = HostProcessProbe.#readProc(pid, "cmdline");
|
|
2384
|
+
return cmdline?.split("\x00").filter(Boolean).join(" ") || null;
|
|
2385
|
+
}
|
|
2386
|
+
return (await HostProcessProbe.run(["ps", "-ww", "-p", String(pid), "-o", "command="])).trim() || null;
|
|
2436
2387
|
}
|
|
2437
|
-
|
|
2438
|
-
|
|
2439
|
-
|
|
2440
|
-
|
|
2441
|
-
|
|
2442
|
-
|
|
2443
|
-
|
|
2444
|
-
|
|
2445
|
-
|
|
2446
|
-
|
|
2447
|
-
|
|
2448
|
-
|
|
2449
|
-
|
|
2450
|
-
|
|
2451
|
-
|
|
2452
|
-
|
|
2453
|
-
|
|
2454
|
-
|
|
2455
|
-
|
|
2456
|
-
|
|
2457
|
-
|
|
2388
|
+
static parentPidOf(child, parent) {
|
|
2389
|
+
if (child.ppid !== parent.pid)
|
|
2390
|
+
return null;
|
|
2391
|
+
if (parent.startedAt !== null && child.startedAt !== null && parent.startedAt > child.startedAt)
|
|
2392
|
+
return null;
|
|
2393
|
+
return parent.pid;
|
|
2394
|
+
}
|
|
2395
|
+
static parseNetstat(output, port) {
|
|
2396
|
+
const pids = new Set;
|
|
2397
|
+
for (const line of output.split(/\r?\n/)) {
|
|
2398
|
+
const fields = line.trim().split(/\s+/);
|
|
2399
|
+
if (fields[0] !== "TCP" || fields.length < 5)
|
|
2400
|
+
continue;
|
|
2401
|
+
const [, local, remote] = fields;
|
|
2402
|
+
if (!local?.endsWith(`:${port}`) || !remote?.endsWith(":0"))
|
|
2403
|
+
continue;
|
|
2404
|
+
const pid = Number(fields.at(-1));
|
|
2405
|
+
if (Number.isInteger(pid) && pid > 0)
|
|
2406
|
+
pids.add(pid);
|
|
2407
|
+
}
|
|
2408
|
+
return [...pids];
|
|
2409
|
+
}
|
|
2410
|
+
static parseProcNetTcp(text, port) {
|
|
2411
|
+
const inodes = [];
|
|
2412
|
+
for (const line of text.split(`
|
|
2413
|
+
`).slice(1)) {
|
|
2414
|
+
const fields = line.trim().split(/\s+/);
|
|
2415
|
+
const [, local, , state, , , , , , inode] = fields;
|
|
2416
|
+
if (!local || !inode || state !== HostProcessProbe.#procTcpListen || inode === "0")
|
|
2417
|
+
continue;
|
|
2418
|
+
if (Number.parseInt(local.slice(local.lastIndexOf(":") + 1), 16) === port)
|
|
2419
|
+
inodes.push(inode);
|
|
2420
|
+
}
|
|
2421
|
+
return inodes;
|
|
2422
|
+
}
|
|
2423
|
+
static parseProcStatPpid(stat) {
|
|
2424
|
+
const ppid = Number(stat.slice(stat.lastIndexOf(")") + 2).split(" ")[1]);
|
|
2425
|
+
return Number.isInteger(ppid) && ppid > 0 ? ppid : null;
|
|
2426
|
+
}
|
|
2427
|
+
static parseWin32Processes(json) {
|
|
2428
|
+
const table = new Map;
|
|
2429
|
+
let parsed;
|
|
2430
|
+
try {
|
|
2431
|
+
parsed = JSON.parse(json.trim() || "[]");
|
|
2432
|
+
} catch {
|
|
2433
|
+
return table;
|
|
2434
|
+
}
|
|
2435
|
+
const rows = Array.isArray(parsed) ? parsed : [parsed];
|
|
2436
|
+
for (const row of rows) {
|
|
2437
|
+
if (typeof row?.ProcessId !== "number")
|
|
2438
|
+
continue;
|
|
2439
|
+
table.set(row.ProcessId, {
|
|
2440
|
+
pid: row.ProcessId,
|
|
2441
|
+
ppid: row.ParentProcessId ?? 0,
|
|
2442
|
+
command: row.CommandLine?.trim() || null,
|
|
2443
|
+
startedAt: HostProcessProbe.#parseCimDate(row.CreationDate)
|
|
2444
|
+
});
|
|
2445
|
+
}
|
|
2446
|
+
return table;
|
|
2447
|
+
}
|
|
2448
|
+
static async run(command, timeoutMs = HostProcessProbe.timeoutMs) {
|
|
2449
|
+
let proc;
|
|
2450
|
+
try {
|
|
2451
|
+
proc = Bun.spawn(command, { stdio: ["ignore", "pipe", "ignore"], windowsHide: true });
|
|
2452
|
+
} catch {
|
|
2453
|
+
return "";
|
|
2454
|
+
}
|
|
2455
|
+
const timer = setTimeout(() => proc.kill("SIGKILL"), timeoutMs);
|
|
2456
|
+
try {
|
|
2457
|
+
return await new Response(proc.stdout).text();
|
|
2458
|
+
} catch {
|
|
2459
|
+
return "";
|
|
2460
|
+
} finally {
|
|
2461
|
+
clearTimeout(timer);
|
|
2462
|
+
}
|
|
2463
|
+
}
|
|
2464
|
+
async#win32Process(pid) {
|
|
2465
|
+
this.#win32Table ??= await HostProcessProbe.#queryWin32Processes();
|
|
2466
|
+
if (!this.#win32Table.has(pid))
|
|
2467
|
+
this.#win32Table = await HostProcessProbe.#queryWin32Processes();
|
|
2468
|
+
return this.#win32Table.get(pid) ?? null;
|
|
2469
|
+
}
|
|
2470
|
+
static async#queryWin32Processes() {
|
|
2471
|
+
const output = await HostProcessProbe.run(["powershell", "-NoProfile", "-NonInteractive", "-Command", HostProcessProbe.#win32ProcessQuery], HostProcessProbe.win32TimeoutMs);
|
|
2472
|
+
return HostProcessProbe.parseWin32Processes(output);
|
|
2473
|
+
}
|
|
2474
|
+
static #parseCimDate(value) {
|
|
2475
|
+
if (!value)
|
|
2476
|
+
return null;
|
|
2477
|
+
const legacy = /\/Date\((\d+)/.exec(value)?.[1];
|
|
2478
|
+
const ms = legacy ? Number(legacy) : Date.parse(value);
|
|
2479
|
+
return Number.isFinite(ms) ? ms : null;
|
|
2480
|
+
}
|
|
2481
|
+
static #procListenersOn(port) {
|
|
2482
|
+
const inodes = new Set(["tcp", "tcp6"].flatMap((table) => HostProcessProbe.parseProcNetTcp(HostProcessProbe.#readProc("net", table) ?? "", port)));
|
|
2483
|
+
if (!inodes.size)
|
|
2484
|
+
return [];
|
|
2485
|
+
const pids = [];
|
|
2486
|
+
for (const entry of HostProcessProbe.#listDir("/proc")) {
|
|
2487
|
+
if (!/^\d+$/.test(entry))
|
|
2488
|
+
continue;
|
|
2489
|
+
const holds = HostProcessProbe.#listDir(`/proc/${entry}/fd`).some((fd) => {
|
|
2490
|
+
const link = HostProcessProbe.#orElse(() => readlinkSync(`/proc/${entry}/fd/${fd}`), "");
|
|
2491
|
+
const inode = /^socket:\[(\d+)\]$/.exec(link)?.[1];
|
|
2492
|
+
return !!inode && inodes.has(inode);
|
|
2493
|
+
});
|
|
2494
|
+
if (holds)
|
|
2495
|
+
pids.push(Number(entry));
|
|
2496
|
+
}
|
|
2497
|
+
return pids;
|
|
2498
|
+
}
|
|
2499
|
+
static #readProc(pid, file) {
|
|
2500
|
+
return HostProcessProbe.#orElse(() => readFileSync(`/proc/${pid}/${file}`, "utf8"), null);
|
|
2501
|
+
}
|
|
2502
|
+
static #listDir(dir) {
|
|
2503
|
+
return HostProcessProbe.#orElse(() => readdirSync(dir), []);
|
|
2504
|
+
}
|
|
2505
|
+
static #orElse(read, fallback) {
|
|
2506
|
+
try {
|
|
2507
|
+
return read();
|
|
2508
|
+
} catch {
|
|
2509
|
+
return fallback;
|
|
2510
|
+
}
|
|
2458
2511
|
}
|
|
2459
2512
|
}
|
|
2460
2513
|
|
|
2461
2514
|
// pkgs/@akanjs/cli/application/devPortReclaimer.ts
|
|
2462
2515
|
class DevPortReclaimer {
|
|
2463
|
-
static lsofTimeoutMs = 3000;
|
|
2464
2516
|
static releaseTimeoutMs = 8000;
|
|
2465
2517
|
static maxAncestorDepth = 8;
|
|
2466
2518
|
static #releasePollMs = 100;
|
|
@@ -2469,7 +2521,12 @@ class DevPortReclaimer {
|
|
|
2469
2521
|
return [...new Set(ports.flatMap((port) => [port, port + DevPortReclaimer.wsPortOffset]))].sort((left, right) => left - right);
|
|
2470
2522
|
}
|
|
2471
2523
|
static isAkanCommand(command) {
|
|
2472
|
-
|
|
2524
|
+
const normalized = command.replaceAll("\\", "/");
|
|
2525
|
+
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");
|
|
2526
|
+
}
|
|
2527
|
+
#probe;
|
|
2528
|
+
constructor(probe = new HostProcessProbe) {
|
|
2529
|
+
this.#probe = probe;
|
|
2473
2530
|
}
|
|
2474
2531
|
async reclaim(ports) {
|
|
2475
2532
|
const killed = [];
|
|
@@ -2494,10 +2551,10 @@ class DevPortReclaimer {
|
|
|
2494
2551
|
}
|
|
2495
2552
|
async holdersOf(port) {
|
|
2496
2553
|
const holders = [];
|
|
2497
|
-
for (const listenerPid of await this.#
|
|
2554
|
+
for (const listenerPid of await this.#probe.listenersOn(port)) {
|
|
2498
2555
|
if (listenerPid === process.pid || listenerPid === process.ppid)
|
|
2499
2556
|
continue;
|
|
2500
|
-
const command = await this.#commandOf(listenerPid);
|
|
2557
|
+
const command = await this.#probe.commandOf(listenerPid);
|
|
2501
2558
|
if (!command)
|
|
2502
2559
|
continue;
|
|
2503
2560
|
if (!DevPortReclaimer.isAkanCommand(command)) {
|
|
@@ -2512,44 +2569,37 @@ class DevPortReclaimer {
|
|
|
2512
2569
|
async#akanRootOf(pid, command) {
|
|
2513
2570
|
let root = { pid, command };
|
|
2514
2571
|
for (let depth = 0;depth < DevPortReclaimer.maxAncestorDepth; depth += 1) {
|
|
2515
|
-
const parentPid = await this.#
|
|
2572
|
+
const parentPid = await this.#probe.parentOf(root.pid);
|
|
2516
2573
|
if (parentPid === null || parentPid <= 1 || parentPid === process.pid || parentPid === process.ppid)
|
|
2517
2574
|
break;
|
|
2518
|
-
const parentCommand = await this.#commandOf(parentPid);
|
|
2575
|
+
const parentCommand = await this.#probe.commandOf(parentPid);
|
|
2519
2576
|
if (!parentCommand || !DevPortReclaimer.isAkanCommand(parentCommand))
|
|
2520
2577
|
break;
|
|
2521
2578
|
root = { pid: parentPid, command: parentCommand };
|
|
2522
2579
|
}
|
|
2523
2580
|
return root;
|
|
2524
2581
|
}
|
|
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
2582
|
async#terminate(pid) {
|
|
2583
|
+
if (this.#probe.platform === "win32") {
|
|
2584
|
+
await HostProcessProbe.run(["taskkill", "/PID", String(pid), "/T", "/F"]);
|
|
2585
|
+
return await this.#released(pid);
|
|
2586
|
+
}
|
|
2542
2587
|
if (!this.#signal(pid, "SIGTERM"))
|
|
2543
2588
|
return !this.#alive(pid);
|
|
2589
|
+
if (await this.#released(pid))
|
|
2590
|
+
return true;
|
|
2591
|
+
this.#signal(pid, "SIGKILL");
|
|
2592
|
+
await Bun.sleep(DevPortReclaimer.#releasePollMs);
|
|
2593
|
+
return !this.#alive(pid);
|
|
2594
|
+
}
|
|
2595
|
+
async#released(pid) {
|
|
2544
2596
|
const deadline = Date.now() + DevPortReclaimer.releaseTimeoutMs;
|
|
2545
2597
|
while (Date.now() < deadline) {
|
|
2546
2598
|
if (!this.#alive(pid))
|
|
2547
2599
|
return true;
|
|
2548
2600
|
await Bun.sleep(DevPortReclaimer.#releasePollMs);
|
|
2549
2601
|
}
|
|
2550
|
-
|
|
2551
|
-
await Bun.sleep(DevPortReclaimer.#releasePollMs);
|
|
2552
|
-
return !this.#alive(pid);
|
|
2602
|
+
return false;
|
|
2553
2603
|
}
|
|
2554
2604
|
#signal(pid, signal) {
|
|
2555
2605
|
try {
|
|
@@ -2567,26 +2617,15 @@ class DevPortReclaimer {
|
|
|
2567
2617
|
return error.code === "EPERM";
|
|
2568
2618
|
}
|
|
2569
2619
|
}
|
|
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
2620
|
}
|
|
2582
2621
|
|
|
2583
2622
|
// pkgs/@akanjs/cli/application/devStreamView.ts
|
|
2584
|
-
import { Logger as
|
|
2623
|
+
import { Logger as Logger4 } from "akanjs/common";
|
|
2585
2624
|
import chalk from "chalk";
|
|
2586
2625
|
var appColors = ["cyan", "magenta", "yellow", "green", "blue", "red"];
|
|
2587
2626
|
|
|
2588
2627
|
class DevStreamView {
|
|
2589
|
-
#logger = new
|
|
2628
|
+
#logger = new Logger4("akan start");
|
|
2590
2629
|
#partial = new Map;
|
|
2591
2630
|
#lastState = new Map;
|
|
2592
2631
|
#color = new Map;
|
|
@@ -2656,7 +2695,7 @@ import os from "os";
|
|
|
2656
2695
|
import { MemoryLimit as MemoryLimit2 } from "akanjs/server/memoryLimit";
|
|
2657
2696
|
|
|
2658
2697
|
class DevBootConcurrency {
|
|
2659
|
-
static perAppBytes =
|
|
2698
|
+
static perAppBytes = 1800 * 1024 * 1024;
|
|
2660
2699
|
static memoryShare = 0.5;
|
|
2661
2700
|
static coresPerApp = 4;
|
|
2662
2701
|
static budget() {
|
|
@@ -2692,6 +2731,7 @@ class DevBootConcurrency {
|
|
|
2692
2731
|
import { mkdir, rename } from "fs/promises";
|
|
2693
2732
|
import path6 from "path";
|
|
2694
2733
|
import dayjs from "dayjs";
|
|
2734
|
+
|
|
2695
2735
|
class DevSessionLog {
|
|
2696
2736
|
static fileName = "dev.log";
|
|
2697
2737
|
static previousFileName = "dev.prev.log";
|
|
@@ -2792,6 +2832,16 @@ class DevSessionLog {
|
|
|
2792
2832
|
|
|
2793
2833
|
// pkgs/@akanjs/cli/application/devSupervisor.ts
|
|
2794
2834
|
class DevSupervisor {
|
|
2835
|
+
static resolveDevUi(plain, {
|
|
2836
|
+
isTty = !!process.stdout.isTTY,
|
|
2837
|
+
columns = process.stdout.columns ?? 0
|
|
2838
|
+
} = {}) {
|
|
2839
|
+
if (plain)
|
|
2840
|
+
return { mode: "stream", downgraded: false };
|
|
2841
|
+
if (!isTty || columns <= 0)
|
|
2842
|
+
return { mode: "stream", downgraded: true };
|
|
2843
|
+
return { mode: "tui", downgraded: false };
|
|
2844
|
+
}
|
|
2795
2845
|
static supervisedEnvKey = "AKAN_DEV_SUPERVISED";
|
|
2796
2846
|
static childHooks() {
|
|
2797
2847
|
if (process.env[DevSupervisor.supervisedEnvKey] !== "1" || !process.send)
|
|
@@ -2806,7 +2856,7 @@ class DevSupervisor {
|
|
|
2806
2856
|
}
|
|
2807
2857
|
};
|
|
2808
2858
|
}
|
|
2809
|
-
static
|
|
2859
|
+
static bootTimeoutMs = 180000;
|
|
2810
2860
|
static shutdownGraceMs = 12000;
|
|
2811
2861
|
#options;
|
|
2812
2862
|
#children = new Map;
|
|
@@ -2855,23 +2905,21 @@ class DevSupervisor {
|
|
|
2855
2905
|
return ports;
|
|
2856
2906
|
}
|
|
2857
2907
|
#makeChild(app, port) {
|
|
2858
|
-
|
|
2859
|
-
const ready = new Promise((resolve) => {
|
|
2860
|
-
markReady = resolve;
|
|
2861
|
-
});
|
|
2908
|
+
const { promise: booted, resolve: markBooted } = Promise.withResolvers();
|
|
2862
2909
|
return {
|
|
2863
2910
|
status: {
|
|
2864
2911
|
app,
|
|
2865
2912
|
name: app.name,
|
|
2866
2913
|
port,
|
|
2867
2914
|
url: `http://localhost:${port}`,
|
|
2915
|
+
shareUrl: this.#options.shares?.get(app.name) ?? null,
|
|
2868
2916
|
state: "starting",
|
|
2869
2917
|
detail: "queued",
|
|
2870
2918
|
exitCode: null
|
|
2871
2919
|
},
|
|
2872
2920
|
proc: null,
|
|
2873
|
-
|
|
2874
|
-
|
|
2921
|
+
booted,
|
|
2922
|
+
markBooted,
|
|
2875
2923
|
opened: false
|
|
2876
2924
|
};
|
|
2877
2925
|
}
|
|
@@ -2890,20 +2938,22 @@ class DevSupervisor {
|
|
|
2890
2938
|
for (const child of wave)
|
|
2891
2939
|
this.#spawnChild(child);
|
|
2892
2940
|
this.#publishStatus();
|
|
2893
|
-
await Promise.all(wave.map(async (child) => await this.#
|
|
2941
|
+
await Promise.all(wave.map(async (child) => await this.#waitForBoot(child)));
|
|
2894
2942
|
}
|
|
2895
2943
|
}
|
|
2896
|
-
async#
|
|
2897
|
-
|
|
2944
|
+
async#waitForBoot(child) {
|
|
2945
|
+
if (await DevSupervisor.timesOut(child.booted, DevSupervisor.bootTimeoutMs))
|
|
2946
|
+
this.#note(`${child.status.name} has not finished booting after ${Math.round(DevSupervisor.bootTimeoutMs / 1000)}s; starting the next app anyway`, "warn");
|
|
2947
|
+
}
|
|
2948
|
+
static async timesOut(work, ms) {
|
|
2949
|
+
let timer;
|
|
2898
2950
|
const expired = new Promise((resolve) => {
|
|
2899
|
-
timer = setTimeout(() => resolve(
|
|
2951
|
+
timer = setTimeout(() => resolve(true), ms);
|
|
2900
2952
|
});
|
|
2901
2953
|
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");
|
|
2954
|
+
return await Promise.race([work.then(() => false), expired]);
|
|
2904
2955
|
} finally {
|
|
2905
|
-
|
|
2906
|
-
clearTimeout(timer);
|
|
2956
|
+
clearTimeout(timer);
|
|
2907
2957
|
}
|
|
2908
2958
|
}
|
|
2909
2959
|
static childArgs(name, { write = true } = {}) {
|
|
@@ -2930,9 +2980,12 @@ class DevSupervisor {
|
|
|
2930
2980
|
},
|
|
2931
2981
|
stdio: ["ignore", "pipe", "pipe"],
|
|
2932
2982
|
ipc: (message) => {
|
|
2933
|
-
if (!message || typeof message !== "object"
|
|
2983
|
+
if (!message || typeof message !== "object")
|
|
2934
2984
|
return;
|
|
2935
|
-
|
|
2985
|
+
if ("booted" in message)
|
|
2986
|
+
child.markBooted();
|
|
2987
|
+
else if (typeof message.state === "string")
|
|
2988
|
+
this.#applyChildEvent(child, message);
|
|
2936
2989
|
},
|
|
2937
2990
|
serialization: "advanced",
|
|
2938
2991
|
onExit: (_proc, exitCode, signalCode) => {
|
|
@@ -2942,7 +2995,7 @@ class DevSupervisor {
|
|
|
2942
2995
|
return;
|
|
2943
2996
|
child.status.state = "stopped";
|
|
2944
2997
|
child.status.detail = signalCode ? `killed by ${signalCode}` : `exited with ${exitCode}`;
|
|
2945
|
-
child.
|
|
2998
|
+
child.markBooted();
|
|
2946
2999
|
this.#publishStatus();
|
|
2947
3000
|
}
|
|
2948
3001
|
});
|
|
@@ -2956,14 +3009,13 @@ class DevSupervisor {
|
|
|
2956
3009
|
child.status.state = event.state;
|
|
2957
3010
|
child.status.detail = event.detail ?? "";
|
|
2958
3011
|
if (event.state === "ready") {
|
|
2959
|
-
child.markReady();
|
|
2960
3012
|
if (this.#options.open && !child.opened) {
|
|
2961
3013
|
child.opened = true;
|
|
2962
3014
|
openBrowser(child.status.url);
|
|
2963
3015
|
}
|
|
2964
3016
|
}
|
|
2965
3017
|
if (event.state === "failed")
|
|
2966
|
-
child.
|
|
3018
|
+
child.markBooted();
|
|
2967
3019
|
this.#publishStatus();
|
|
2968
3020
|
}
|
|
2969
3021
|
async#drain(stream, name, kind) {
|
|
@@ -3025,44 +3077,81 @@ class DevSupervisor {
|
|
|
3025
3077
|
for (const child of running)
|
|
3026
3078
|
child.proc.kill("SIGTERM");
|
|
3027
3079
|
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);
|
|
3080
|
+
if (!await DevSupervisor.timesOut(exited, DevSupervisor.shutdownGraceMs))
|
|
3081
|
+
return;
|
|
3082
|
+
for (const child of running) {
|
|
3083
|
+
if (child.proc.killed)
|
|
3084
|
+
continue;
|
|
3085
|
+
this.#note(`${child.status.name} did not exit in time; killing it`, "warn");
|
|
3086
|
+
child.proc.kill("SIGKILL");
|
|
3044
3087
|
}
|
|
3045
3088
|
}
|
|
3046
3089
|
}
|
|
3047
3090
|
|
|
3048
|
-
// pkgs/@akanjs/cli/application/
|
|
3049
|
-
|
|
3050
|
-
|
|
3051
|
-
|
|
3052
|
-
|
|
3053
|
-
|
|
3054
|
-
|
|
3055
|
-
|
|
3091
|
+
// pkgs/@akanjs/cli/application/interruptTeardown.ts
|
|
3092
|
+
import { Logger as Logger5 } from "akanjs/common";
|
|
3093
|
+
|
|
3094
|
+
class InterruptTeardown {
|
|
3095
|
+
#teardowns = [];
|
|
3096
|
+
#exit;
|
|
3097
|
+
#listen;
|
|
3098
|
+
#report;
|
|
3099
|
+
#interrupted = false;
|
|
3100
|
+
#chain = Promise.resolve();
|
|
3101
|
+
ownsExit = true;
|
|
3102
|
+
constructor({ exit, listen, report } = {}) {
|
|
3103
|
+
this.#exit = exit ?? ((code) => process.exit(code));
|
|
3104
|
+
this.#listen = listen ?? ((onSignal) => {
|
|
3105
|
+
process.on("SIGINT", onSignal);
|
|
3106
|
+
});
|
|
3107
|
+
this.#report = report ?? ((message) => Logger5.rawLog(message, undefined, "error"));
|
|
3108
|
+
}
|
|
3109
|
+
add(run, abandoned, exitCode = 0) {
|
|
3110
|
+
this.#teardowns.push({ run, abandoned, exitCode, state: "pending" });
|
|
3111
|
+
if (this.#teardowns.length === 1)
|
|
3112
|
+
this.#listen(() => this.#onSignal());
|
|
3113
|
+
}
|
|
3114
|
+
async runAll() {
|
|
3115
|
+
await this.#runTeardowns();
|
|
3116
|
+
}
|
|
3117
|
+
#onSignal() {
|
|
3118
|
+
if (this.#interrupted) {
|
|
3119
|
+
for (const one of this.#teardowns)
|
|
3120
|
+
if (one.state !== "done")
|
|
3121
|
+
this.#report(one.abandoned);
|
|
3122
|
+
this.#exit(130);
|
|
3123
|
+
return;
|
|
3124
|
+
}
|
|
3125
|
+
this.#interrupted = true;
|
|
3126
|
+
this.#runTeardowns().finally(() => {
|
|
3127
|
+
if (this.ownsExit)
|
|
3128
|
+
this.#exit(Math.max(0, ...this.#teardowns.map((one) => one.exitCode)));
|
|
3129
|
+
});
|
|
3130
|
+
}
|
|
3131
|
+
async#runTeardowns() {
|
|
3132
|
+
this.#chain = this.#chain.then(async () => {
|
|
3133
|
+
for (const one of this.#teardowns.toReversed()) {
|
|
3134
|
+
if (one.state !== "pending")
|
|
3135
|
+
continue;
|
|
3136
|
+
one.state = "running";
|
|
3137
|
+
await one.run().catch(() => {
|
|
3138
|
+
return;
|
|
3139
|
+
});
|
|
3140
|
+
one.state = "done";
|
|
3141
|
+
}
|
|
3142
|
+
});
|
|
3143
|
+
await this.#chain;
|
|
3144
|
+
}
|
|
3145
|
+
}
|
|
3056
3146
|
|
|
3057
3147
|
// pkgs/@akanjs/cli/application/application.script.ts
|
|
3058
3148
|
class ApplicationScript extends script("application", [ApplicationRunner, LibraryScript]) {
|
|
3059
3149
|
static dbShutdownTimeoutMs = 20000;
|
|
3150
|
+
static devServerReadyTimeoutMs = DevSupervisor.bootTimeoutMs;
|
|
3151
|
+
#interrupt = new InterruptTeardown;
|
|
3060
3152
|
async confirmDatabaseModeDependencyInstall(databaseMode, installSpecs) {
|
|
3061
3153
|
return await confirm({
|
|
3062
|
-
message:
|
|
3063
|
-
`Database mode '${databaseMode}' requires missing dependencies: ${installSpecs.join(", ")}.`,
|
|
3064
|
-
"Install them now?"
|
|
3065
|
-
].join(" "),
|
|
3154
|
+
message: `Database mode '${databaseMode}' requires missing dependencies: ${installSpecs.join(", ")}. Install them now?`,
|
|
3066
3155
|
default: true
|
|
3067
3156
|
});
|
|
3068
3157
|
}
|
|
@@ -3073,67 +3162,22 @@ class ApplicationScript extends script("application", [ApplicationRunner, Librar
|
|
|
3073
3162
|
const shouldInstall = await this.confirmDatabaseModeDependencyInstall(databaseMode, installSpecs);
|
|
3074
3163
|
if (!shouldInstall)
|
|
3075
3164
|
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
|
-
}
|
|
3165
|
+
await this.#addDependencies(app, installSpecs, `database dependencies for ${databaseMode} mode`);
|
|
3087
3166
|
}
|
|
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...");
|
|
3167
|
+
async#addDependencies(app, installSpecs, what) {
|
|
3168
|
+
const spinner = app.workspace.spinning(`Installing ${what}...`);
|
|
3102
3169
|
try {
|
|
3103
|
-
await app.workspace.spawn("bun", ["add", ...installSpecs], {
|
|
3104
|
-
stdio: "inherit"
|
|
3105
|
-
});
|
|
3170
|
+
await app.workspace.spawn("bun", ["add", ...installSpecs], { stdio: "inherit" });
|
|
3106
3171
|
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(", ")}`);
|
|
3172
|
+
spinner.succeed(`Installed ${what}`);
|
|
3129
3173
|
} catch (error) {
|
|
3130
|
-
spinner.fail(`Failed to
|
|
3174
|
+
spinner.fail(`Failed to install ${what}`);
|
|
3131
3175
|
throw error;
|
|
3132
3176
|
}
|
|
3133
3177
|
}
|
|
3134
|
-
async createApplication(appName, workspace, { start = false, libs = [] } = {}) {
|
|
3178
|
+
async createApplication(appName, workspace, { start = false, libs = [], sample = false } = {}) {
|
|
3135
3179
|
const spinner = workspace.spinning("Creating application...");
|
|
3136
|
-
const app = await this.applicationRunner.createApplication(appName, workspace, libs);
|
|
3180
|
+
const app = await this.applicationRunner.createApplication(appName, workspace, libs, { sample });
|
|
3137
3181
|
spinner.succeed(`Application created in apps/${app.name}`);
|
|
3138
3182
|
await app.scanSync();
|
|
3139
3183
|
if (start)
|
|
@@ -3156,17 +3200,6 @@ class ApplicationScript extends script("application", [ApplicationRunner, Librar
|
|
|
3156
3200
|
else
|
|
3157
3201
|
await this.libraryScript.syncLibrary(sys);
|
|
3158
3202
|
}
|
|
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
3203
|
async script(app, filename) {
|
|
3171
3204
|
const scriptFilename = filename ?? await this.applicationRunner.getScriptFilename(app);
|
|
3172
3205
|
await app.scanSync();
|
|
@@ -3184,10 +3217,7 @@ class ApplicationScript extends script("application", [ApplicationRunner, Librar
|
|
|
3184
3217
|
if (!quiet)
|
|
3185
3218
|
Logger6.rawLog(`Creating an optimized production build for ${app.name}...`);
|
|
3186
3219
|
try {
|
|
3187
|
-
const result = await this.applicationRunner.build(app, {
|
|
3188
|
-
fast,
|
|
3189
|
-
spinner: !quiet
|
|
3190
|
-
});
|
|
3220
|
+
const result = await this.applicationRunner.build(app, { fast, spinner: !quiet });
|
|
3191
3221
|
Logger6.rawLog(`${app.name} built in dist/apps/${app.name}`);
|
|
3192
3222
|
if (!quiet)
|
|
3193
3223
|
ApplicationBuildReporter.printSummary(result);
|
|
@@ -3212,8 +3242,7 @@ class ApplicationScript extends script("application", [ApplicationRunner, Librar
|
|
|
3212
3242
|
async test(exec, { write = true } = {}) {
|
|
3213
3243
|
if (exec instanceof LibExecutor) {
|
|
3214
3244
|
await this.libraryScript.syncLibrary(exec);
|
|
3215
|
-
|
|
3216
|
-
spinner.succeed(`${exec.name} prepared`);
|
|
3245
|
+
exec.spinning(`Preparing ${exec.name}...`).succeed(`${exec.name} prepared`);
|
|
3217
3246
|
await this.applicationRunner.test(exec);
|
|
3218
3247
|
return;
|
|
3219
3248
|
}
|
|
@@ -3230,23 +3259,33 @@ class ApplicationScript extends script("application", [ApplicationRunner, Librar
|
|
|
3230
3259
|
}
|
|
3231
3260
|
await this.applicationRunner.test(exec);
|
|
3232
3261
|
}
|
|
3233
|
-
async start(apps, {
|
|
3262
|
+
async start(apps, {
|
|
3263
|
+
open = false,
|
|
3264
|
+
dbup = true,
|
|
3265
|
+
write = true,
|
|
3266
|
+
plain = false,
|
|
3267
|
+
kill = false,
|
|
3268
|
+
share = false,
|
|
3269
|
+
concurrency = null
|
|
3270
|
+
} = {}) {
|
|
3234
3271
|
const first = apps[0];
|
|
3235
3272
|
if (!first)
|
|
3236
3273
|
throw new Error("No app selected to start");
|
|
3237
|
-
const { mode, downgraded } = resolveDevUi(plain);
|
|
3274
|
+
const { mode, downgraded } = DevSupervisor.resolveDevUi(plain);
|
|
3238
3275
|
if (downgraded)
|
|
3239
3276
|
first.workspace.log("no sized terminal to draw on; printing prefixed lines instead");
|
|
3240
3277
|
if (kill)
|
|
3241
3278
|
await this.reclaimDevPorts(apps);
|
|
3279
|
+
const shares = share ? await this.#shareOnInterrupt(apps) : null;
|
|
3242
3280
|
if (mode === "stream" && apps.length === 1)
|
|
3243
3281
|
return await this.startOne(first, { open, dbup, write, ...DevSupervisor.childHooks() });
|
|
3244
|
-
|
|
3282
|
+
this.#interrupt.ownsExit = false;
|
|
3283
|
+
await this.#startMany(apps, { open, dbup, write, mode, concurrency, shares });
|
|
3245
3284
|
}
|
|
3246
3285
|
async startOne(app, { open = false, dbup = true, stdio = "inherit", write = true, onDevEvent } = {}) {
|
|
3247
3286
|
await app.scanSync({ write });
|
|
3248
3287
|
const akanConfig = await app.getConfig();
|
|
3249
|
-
const databaseMode =
|
|
3288
|
+
const databaseMode = akanConfig.resolveDatabaseMode();
|
|
3250
3289
|
await this.syncDatabaseModeDependencies(app, akanConfig, databaseMode);
|
|
3251
3290
|
if (app.getEnv() === "local" && dbup && databaseMode !== "single") {
|
|
3252
3291
|
const wasDbAlreadyUp = await this.dbup(app.workspace, databaseMode);
|
|
@@ -3254,15 +3293,12 @@ class ApplicationScript extends script("application", [ApplicationRunner, Librar
|
|
|
3254
3293
|
this.#stopDatabaseOnInterrupt(app.workspace);
|
|
3255
3294
|
}
|
|
3256
3295
|
const spinner = app.spinning("Preparing backend...");
|
|
3257
|
-
|
|
3296
|
+
return await this.applicationRunner.start(app, {
|
|
3258
3297
|
open,
|
|
3259
|
-
onStart: () => {
|
|
3260
|
-
spinner.succeed(`${app.name} prepared, ready to start`);
|
|
3261
|
-
},
|
|
3298
|
+
onStart: () => spinner.succeed(`${app.name} prepared, ready to start`),
|
|
3262
3299
|
onDevEvent,
|
|
3263
3300
|
stdio
|
|
3264
3301
|
});
|
|
3265
|
-
return akanAppHost;
|
|
3266
3302
|
}
|
|
3267
3303
|
async reclaimDevPorts(apps) {
|
|
3268
3304
|
const workspace = apps[0]?.workspace;
|
|
@@ -3277,13 +3313,20 @@ class ApplicationScript extends script("application", [ApplicationRunner, Librar
|
|
|
3277
3313
|
if (killed.length === 0 && foreign.length === 0)
|
|
3278
3314
|
workspace.log("dev ports are free; nothing to kill");
|
|
3279
3315
|
}
|
|
3280
|
-
async#startMany(apps, {
|
|
3316
|
+
async#startMany(apps, {
|
|
3317
|
+
open,
|
|
3318
|
+
dbup,
|
|
3319
|
+
write,
|
|
3320
|
+
mode,
|
|
3321
|
+
concurrency,
|
|
3322
|
+
shares
|
|
3323
|
+
}) {
|
|
3281
3324
|
const workspace = apps[0]?.workspace;
|
|
3282
3325
|
if (!workspace)
|
|
3283
3326
|
throw new Error("No app selected to start");
|
|
3284
3327
|
const startedDatabase = dbup ? await this.#prepareSharedDatabase(apps) : false;
|
|
3285
|
-
const supervisor = new DevSupervisor({ apps, mode, concurrency, open, write });
|
|
3286
|
-
const view = mode === "tui" ?
|
|
3328
|
+
const supervisor = new DevSupervisor({ apps, mode, concurrency, open, write, shares });
|
|
3329
|
+
const view = mode === "tui" ? new (await import("./devTui-q7k7bakd.js")).DevTui(supervisor) : new DevStreamView(apps.map((app) => app.name));
|
|
3287
3330
|
await supervisor.run(view);
|
|
3288
3331
|
if (startedDatabase)
|
|
3289
3332
|
await this.#stopDatabase(workspace);
|
|
@@ -3297,7 +3340,7 @@ class ApplicationScript extends script("application", [ApplicationRunner, Librar
|
|
|
3297
3340
|
if (app.getEnv() !== "local")
|
|
3298
3341
|
continue;
|
|
3299
3342
|
const akanConfig = await app.getConfig();
|
|
3300
|
-
const mode =
|
|
3343
|
+
const mode = akanConfig.resolveDatabaseMode();
|
|
3301
3344
|
await this.syncDatabaseModeDependencies(app, akanConfig, mode);
|
|
3302
3345
|
if (mode !== "single")
|
|
3303
3346
|
modes.add(mode);
|
|
@@ -3308,82 +3351,121 @@ class ApplicationScript extends script("application", [ApplicationRunner, Librar
|
|
|
3308
3351
|
started = true;
|
|
3309
3352
|
return started;
|
|
3310
3353
|
}
|
|
3311
|
-
async buildIos(app, { write = true,
|
|
3354
|
+
async buildIos(app, { write = true, ...options } = {}) {
|
|
3312
3355
|
await app.scanSync({ write });
|
|
3313
|
-
await this.applicationRunner.
|
|
3356
|
+
await this.applicationRunner.buildMobile(app, "ios", options);
|
|
3314
3357
|
}
|
|
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
|
-
} = {}) {
|
|
3358
|
+
async buildAndroid(app, { write = true, ...options } = {}) {
|
|
3325
3359
|
await app.scanSync({ write });
|
|
3326
|
-
|
|
3327
|
-
|
|
3328
|
-
|
|
3329
|
-
await
|
|
3330
|
-
|
|
3331
|
-
|
|
3332
|
-
|
|
3333
|
-
|
|
3334
|
-
|
|
3335
|
-
|
|
3336
|
-
|
|
3360
|
+
await this.applicationRunner.buildMobile(app, "android", options);
|
|
3361
|
+
}
|
|
3362
|
+
async buildDesktop(app, { write = true, ...options } = {}) {
|
|
3363
|
+
await app.scanSync({ write });
|
|
3364
|
+
await this.applicationRunner.buildDesktop(app, options);
|
|
3365
|
+
}
|
|
3366
|
+
async startIos(app, { write = true, ...options } = {}) {
|
|
3367
|
+
await app.scanSync({ write });
|
|
3368
|
+
await this.applicationRunner.startMobile(app, "ios", options);
|
|
3369
|
+
}
|
|
3370
|
+
async startAndroid(app, { write = true, ...options } = {}) {
|
|
3371
|
+
await app.scanSync({ write });
|
|
3372
|
+
await this.applicationRunner.startMobile(app, "android", options);
|
|
3373
|
+
}
|
|
3374
|
+
async startDesktop(app, { write = true, ...options } = {}) {
|
|
3375
|
+
await app.scanSync({ write });
|
|
3376
|
+
if (options.operation === "release")
|
|
3377
|
+
return await this.applicationRunner.startDesktop(app, options);
|
|
3378
|
+
const mobileTarget = await ApplicationRunner.startTarget(app, undefined, options.target);
|
|
3379
|
+
if (!ApplicationRunner.carriesServer(mobileTarget))
|
|
3380
|
+
return await this.applicationRunner.startDesktop(app, options);
|
|
3381
|
+
DesktopServerStage.assertCarriable(await app.getConfig());
|
|
3382
|
+
const upstream = `http://localhost:${await app.getDevPort()}`;
|
|
3383
|
+
try {
|
|
3384
|
+
if (await ApplicationRunner.answers(upstream, app.name, app.workspace.workspaceRoot))
|
|
3385
|
+
app.log(`The desktop app follows the dev server on ${upstream}.`);
|
|
3386
|
+
else
|
|
3387
|
+
await this.#startDevServerFor(app, upstream);
|
|
3388
|
+
await this.applicationRunner.startDesktop(app, { ...options, interrupt: this.#interrupt });
|
|
3389
|
+
} finally {
|
|
3390
|
+
await this.#interrupt.runAll();
|
|
3391
|
+
}
|
|
3392
|
+
}
|
|
3393
|
+
async#startDevServerFor(app, upstream) {
|
|
3394
|
+
app.log(`No dev server answers on ${upstream}; starting \`akan start ${app.name}\` for the desktop app.`);
|
|
3395
|
+
let settle = () => {};
|
|
3396
|
+
const settled = new Promise((resolve) => settle = resolve);
|
|
3397
|
+
const appHost = await this.startOne(app, {
|
|
3398
|
+
write: false,
|
|
3399
|
+
onDevEvent: (event) => {
|
|
3400
|
+
if ("state" in event && (event.state === "ready" || event.state === "failed"))
|
|
3401
|
+
settle(event.state);
|
|
3402
|
+
}
|
|
3337
3403
|
});
|
|
3404
|
+
this.#interrupt.add(async () => {
|
|
3405
|
+
await appHost.stop();
|
|
3406
|
+
}, "Abandoning the dev server shutdown; its processes may still be running.", 130);
|
|
3407
|
+
let state;
|
|
3408
|
+
const timedOut = await DevSupervisor.timesOut(settled.then((settledState) => state = settledState), ApplicationScript.devServerReadyTimeoutMs);
|
|
3409
|
+
if (state === "ready")
|
|
3410
|
+
return;
|
|
3411
|
+
await this.#interrupt.runAll();
|
|
3412
|
+
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
3413
|
}
|
|
3339
|
-
async releaseIos(app, {
|
|
3414
|
+
async releaseIos(app, {
|
|
3415
|
+
write = true,
|
|
3416
|
+
allowLocalRelease = false,
|
|
3417
|
+
...options
|
|
3418
|
+
} = {}) {
|
|
3340
3419
|
await app.scanSync({ write });
|
|
3341
|
-
|
|
3342
|
-
|
|
3343
|
-
await this.applicationRunner.releaseIos(app, { target, env, regenerate });
|
|
3420
|
+
ApplicationScript.#assertReleaseEnv("releaseIos", options.env ?? "main", allowLocalRelease);
|
|
3421
|
+
await this.applicationRunner.releaseIos(app, options);
|
|
3344
3422
|
}
|
|
3345
|
-
async
|
|
3423
|
+
async releaseAndroid(app, format, {
|
|
3424
|
+
write = true,
|
|
3425
|
+
allowLocalRelease = false,
|
|
3426
|
+
...options
|
|
3427
|
+
} = {}) {
|
|
3346
3428
|
await app.scanSync({ write });
|
|
3347
|
-
|
|
3429
|
+
ApplicationScript.#assertReleaseEnv("releaseAndroid", options.env ?? "main", allowLocalRelease);
|
|
3430
|
+
await this.applicationRunner.releaseAndroid(app, format, options);
|
|
3348
3431
|
}
|
|
3349
|
-
async
|
|
3350
|
-
open = false,
|
|
3351
|
-
operation = "local",
|
|
3352
|
-
env = "local",
|
|
3432
|
+
async packUpdate(app, platform, {
|
|
3353
3433
|
write = true,
|
|
3354
|
-
|
|
3355
|
-
|
|
3434
|
+
allowLocalRelease = false,
|
|
3435
|
+
...options
|
|
3356
3436
|
} = {}) {
|
|
3357
3437
|
await app.scanSync({ write });
|
|
3358
|
-
|
|
3359
|
-
await this.
|
|
3360
|
-
|
|
3361
|
-
|
|
3362
|
-
|
|
3363
|
-
operation,
|
|
3364
|
-
env,
|
|
3365
|
-
target,
|
|
3366
|
-
regenerate
|
|
3367
|
-
});
|
|
3438
|
+
ApplicationScript.#assertReleaseEnv("packUpdate", options.env ?? "main", allowLocalRelease);
|
|
3439
|
+
await this.applicationRunner.packUpdate(app, platform, options);
|
|
3440
|
+
}
|
|
3441
|
+
async updateKeygen(app, platform, { target } = {}) {
|
|
3442
|
+
await this.applicationRunner.updateKeygen(app, platform, target);
|
|
3368
3443
|
}
|
|
3369
|
-
async
|
|
3444
|
+
async publishUpdate(app, platform, {
|
|
3445
|
+
write = true,
|
|
3446
|
+
allowLocalRelease = false,
|
|
3447
|
+
...options
|
|
3448
|
+
} = {}) {
|
|
3370
3449
|
await app.scanSync({ write });
|
|
3371
|
-
|
|
3372
|
-
|
|
3373
|
-
await this.applicationRunner.releaseAndroid(app, assembleType, {
|
|
3374
|
-
target,
|
|
3375
|
-
env,
|
|
3376
|
-
regenerate
|
|
3377
|
-
});
|
|
3450
|
+
ApplicationScript.#assertReleaseEnv("publishUpdate", options.env ?? "main", allowLocalRelease);
|
|
3451
|
+
await this.applicationRunner.publishUpdate(app, platform, options);
|
|
3378
3452
|
}
|
|
3379
|
-
|
|
3380
|
-
|
|
3453
|
+
static #assertReleaseEnv(command, env, allowLocalRelease) {
|
|
3454
|
+
if (env === "local" && !allowLocalRelease)
|
|
3455
|
+
throw new Error(`${command} --env local is blocked. Pass allowLocalRelease only for explicit local release testing.`);
|
|
3381
3456
|
}
|
|
3382
|
-
async
|
|
3383
|
-
await this.applicationRunner.
|
|
3457
|
+
async transferDatabase(app, direction, dir) {
|
|
3458
|
+
await this.applicationRunner.transferDatabase(app, direction, dir);
|
|
3384
3459
|
}
|
|
3385
|
-
async
|
|
3386
|
-
await
|
|
3460
|
+
async dbupDeclared(workspace, mode) {
|
|
3461
|
+
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())]);
|
|
3462
|
+
const needing = declared.filter((each) => each !== "single");
|
|
3463
|
+
if (!needing.length) {
|
|
3464
|
+
workspace.log("No app here declares a database mode that runs on local services; single needs none.");
|
|
3465
|
+
return;
|
|
3466
|
+
}
|
|
3467
|
+
for (const each of needing)
|
|
3468
|
+
await this.dbup(workspace, each);
|
|
3387
3469
|
}
|
|
3388
3470
|
async dbup(workspace, mode = "multiple") {
|
|
3389
3471
|
const spinner = workspace.spinning(`Starting local database (${mode})...`);
|
|
@@ -3401,34 +3483,36 @@ class ApplicationScript extends script("application", [ApplicationRunner, Librar
|
|
|
3401
3483
|
}
|
|
3402
3484
|
spinner.succeed("Local database (/local/docker-compose.yaml) is down");
|
|
3403
3485
|
}
|
|
3404
|
-
#
|
|
3405
|
-
|
|
3406
|
-
|
|
3407
|
-
|
|
3408
|
-
|
|
3409
|
-
|
|
3410
|
-
|
|
3411
|
-
|
|
3412
|
-
|
|
3486
|
+
async#shareOnInterrupt(apps) {
|
|
3487
|
+
const shares = [];
|
|
3488
|
+
const urls = new Map;
|
|
3489
|
+
this.#interrupt.add(async () => {
|
|
3490
|
+
await Promise.all(shares.map(async (share) => await share.close()));
|
|
3491
|
+
}, "Abandoning the tunnel release; the shares expire on their own.");
|
|
3492
|
+
await Promise.all(apps.map(async (app) => {
|
|
3493
|
+
const { TunnelShare } = await import("./TunnelShare-cwh9ezsm.js");
|
|
3494
|
+
const share = await TunnelShare.open(app, { workspace: app.workspace });
|
|
3495
|
+
shares.push(share);
|
|
3496
|
+
urls.set(app.name, share.url);
|
|
3497
|
+
Logger6.rawLog(`${app.name} is shared at ${share.url}`);
|
|
3498
|
+
})).catch((error) => {
|
|
3499
|
+
Logger6.rawLog(`Could not open the tunnel: ${error instanceof Error ? error.message : String(error)}`, undefined, "error");
|
|
3413
3500
|
});
|
|
3501
|
+
return urls;
|
|
3502
|
+
}
|
|
3503
|
+
#stopDatabaseOnInterrupt(workspace) {
|
|
3504
|
+
this.#interrupt.add(async () => {
|
|
3505
|
+
await this.#stopDatabase(workspace);
|
|
3506
|
+
}, "Abandoning the local database teardown; containers are left running.");
|
|
3414
3507
|
}
|
|
3415
3508
|
async#stopDatabase(workspace) {
|
|
3416
|
-
|
|
3417
|
-
|
|
3418
|
-
timer = setTimeout(() => resolve("timeout"), ApplicationScript.dbShutdownTimeoutMs);
|
|
3509
|
+
const stopped = this.dbdown(workspace).catch(() => {
|
|
3510
|
+
return;
|
|
3419
3511
|
});
|
|
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
|
-
}
|
|
3512
|
+
if (!await DevSupervisor.timesOut(stopped, ApplicationScript.dbShutdownTimeoutMs))
|
|
3513
|
+
return;
|
|
3514
|
+
Logger6.rawLog(`Local database did not stop within ${ApplicationScript.dbShutdownTimeoutMs}ms; run \`akan dbdown\` once Docker responds.`, undefined, "error");
|
|
3431
3515
|
}
|
|
3432
3516
|
}
|
|
3433
3517
|
|
|
3434
|
-
export {
|
|
3518
|
+
export { ApplicationScript };
|