@akanjs/cli 3.0.0-beta.3 → 3.0.0-beta.30
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-xnfyyz8h.js +9 -0
- package/CodeAgentRpcHost-p7h6n2hp.js +7 -0
- package/CodeAgentRpcListener-7p7z5dd7.js +97 -0
- package/CodeTui-h66xb1fh.js +1457 -0
- package/README.md +3 -0
- package/TunnelShare-cwh9ezsm.js +9 -0
- package/{agent.command-d2gfxfck.js → agent.command-c4v7kjff.js} +8 -9
- package/akanCode.js +102 -0
- package/application.command-yzsswrz6.js +241 -0
- package/{applicationBuildRunner-kxwbssgn.js → applicationBuildRunner-qkt6spd5.js} +121 -77
- package/buildBatch.proc.js +120 -32
- package/cloud.command-v8h688jc.js +95 -0
- package/cloud.script-yct1sk4k.js +20 -0
- package/code.command-jff3tw7y.js +154 -0
- package/commandManifest.json +1 -1
- package/consoleToStderr-h5bryj94.js +7 -0
- package/context.command-y5sevk3c.js +48 -0
- package/context.runner-90hsmacr.js +21 -0
- package/{dependencyScanner-shs82ynp.js → dependencyScanner-06z2dmrr.js} +2 -2
- package/{devTui-40ggmd99.js → devTui-q7k7bakd.js} +122 -40
- package/fileSys-wzzrntkw.js +9 -0
- package/guideline.command-11cndccb.js +42 -0
- package/guidelines/agentRule/agentRule.instruction.md +209 -33
- package/guidelines/conventions/conventions.instruction.md +143 -41
- package/guidelines/fieldRule/fieldRule.instruction.md +1 -0
- package/guidelines/mcpRule/mcpRule.instruction.md +7 -5
- package/guidelines/queryRule/queryRule.instruction.md +21 -7
- package/guidelines/runtimeRule/runtimeRule.instruction.md +268 -6
- package/guidelines/ssrRule/ssrRule.instruction.md +4 -2
- package/guidelines/transportRule/transportRule.instruction.md +51 -0
- package/guidelines/workspaceOnboarding/workspaceOnboarding.instruction.md +20 -0
- package/guidelines/workspaceRecipes/workspaceRecipes.instruction.md +66 -3
- package/incrementalBuilder.proc.js +564 -109
- package/{index-fz5q3gd7.js → index-0ncc0mbk.js} +10 -17
- package/index-167c77aq.js +69 -0
- package/{index-kcxmv48c.js → index-2ahj896a.js} +45 -79
- package/index-2wc41k5w.js +49 -0
- package/{index-j9yhg404.js → index-35mmm9ca.js} +4 -7
- package/{index-a64esmtb.js → index-376gpdds.js} +523 -1022
- package/index-3hx1z96c.js +87 -0
- package/{index-v0qh106m.js → index-3rh7thcx.js} +961 -892
- package/{index-hwkygn9s.js → index-48k96p55.js} +325 -36
- package/{index-q0y2jq9j.js → index-52w0fvh8.js} +21 -4
- package/index-5rp0h6na.js +1119 -0
- package/index-6x9p8dpb.js +4300 -0
- package/index-7md2g9vw.js +6 -0
- package/index-8tv7356t.js +700 -0
- package/index-94s080qm.js +266 -0
- package/index-9dp031by.js +12 -0
- package/{index-ws5e0j51.js → index-a4s954kq.js} +43 -26
- package/{index-3d9y066c.js → index-bjwxs904.js} +9 -8
- package/{index-ne2qy0nk.js → index-bxmejydw.js} +12 -12
- package/index-c6eggbee.js +123 -0
- package/index-d2242ksy.js +99 -0
- package/{index-92zztqy1.js → index-dy0jx9ay.js} +2 -17
- package/{index-259dsnz3.js → index-gxamdf18.js} +5 -8
- package/{index-4edzf1gr.js → index-hf6f4xnb.js} +77 -150
- package/{index-by5gpp8x.js → index-hj6w4ka1.js} +2 -1
- package/{index-gyd220p2.js → index-hmdq5pqm.js} +554 -577
- package/{index-600zhnve.js → index-hqd0h1pb.js} +75 -203
- package/index-kb16nzyt.js +997 -0
- package/index-mk3b9rk1.js +2729 -0
- package/{index-2tv3fdx4.js → index-n3jk2501.js} +1205 -1458
- package/index-p6pbz9n5.js +25 -0
- package/{index-zry5nz81.js → index-qcejqgts.js} +51 -30
- package/{index-7737gxcp.js → index-rtwhw4aq.js} +107 -241
- package/{index-pnqbkf5p.js → index-se643zvp.js} +6 -2
- package/index-tsb0f8ya.js +53 -0
- package/{index-ktb5njgy.js → index-tzc1gtrg.js} +49 -99
- package/index-wa5jbxj1.js +760 -0
- package/index-wfwgcnyc.js +187 -0
- package/{index-mak3cd28.js → index-xb7qhxa8.js} +137 -255
- package/{index-h7yw2z9n.js → index-zzvvc845.js} +8 -14
- package/index.js +23 -22
- package/{library.command-nbsky672.js → library.command-n9kp7yms.js} +4 -5
- package/{localRegistry.command-yeq57wjr.js → localRegistry.command-jx6kxt2x.js} +23 -42
- package/{module.command-29vqzrh8.js → module.command-q2xjf6cx.js} +8 -11
- package/{package.command-axv0abmn.js → package.command-q5a3nsw5.js} +6 -7
- package/package.json +15 -7
- package/{page.command-vs9s3zrh.js → page.command-ppenrxjn.js} +4 -5
- package/{primitive.command-cbxmqbtc.js → primitive.command-dvgg8nem.js} +9 -22
- package/prompter-76k56b37.js +8 -0
- package/{quality.command-gmbs7nws.js → quality.command-pz7kg3pc.js} +205 -377
- package/{recipeScanner-fpagtgc1.js → recipeScanner-4b69ybe0.js} +1 -1
- package/{repair.command-bqsjptsj.js → repair.command-a6py9xvz.js} +11 -35
- package/{routeSourceValidator-apyqw7wk.js → routeSourceValidator-7ycnfz0e.js} +0 -6
- package/{scalar.command-6k1vhe9s.js → scalar.command-7f3xfpen.js} +7 -10
- 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-fwjfcts7.js → subspace.command-z7nqs508.js} +138 -67
- package/templates/__scalar/__model__/__model__.abstract.md.template +8 -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.abstract.ts +12 -32
- package/templates/appSample/lib/task/task.signal.spec.ts +15 -18
- package/templates/appSample/lib/task/task.signal.test.ts +15 -1
- package/templates/appSample/lib/task/task.signal.ts +1 -1
- package/templates/appSample/srvkit/{AuthGuard.ts → guards.ts} +7 -4
- package/templates/appSample/srvkit/{SessionInternalArg.ts → internalArgs.ts} +6 -4
- 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/package.json.template +1 -3
- package/tunnel.command-zy4wbcgt.js +129 -0
- package/{typeChecker-3xgrzc2v.js → typeChecker-px49mezc.js} +1 -1
- package/typecheck.proc.js +1 -1
- package/workflow.command-f142t17s.js +79 -0
- package/{workspace.command-8s050m0h.js → workspace.command-gpk1149x.js} +77 -109
- package/abstractCompactor-mjh5ppsq.js +0 -101
- package/application.command-pj2m7584.js +0 -204
- package/applicationReleasePackager-4yasxwv9.js +0 -244
- package/capacitorApp-36rbr858.js +0 -12
- package/cloud.command-jshwdq22.js +0 -93
- package/context.command-ez8a0zyz.js +0 -80
- package/devTuiView-c08ccbat.js +0 -9
- package/getDirname-dr3jz1xg.js +0 -7
- package/guideline.command-xmqwx8zm.js +0 -359
- 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-2hq52jq3.js +0 -465
- package/index-47pj3aee.js +0 -76
- package/index-6wwzs1bc.js +0 -7
- package/index-8pm30m73.js +0 -841
- package/index-kn1h7p1x.js +0 -560
- package/index-meh558jy.js +0 -65
- package/index-rwg2aa4f.js +0 -229
- 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-nwpcdc8p.js +0 -107
|
@@ -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-xb7qhxa8.js";
|
|
6
41
|
import {
|
|
7
42
|
WorkspaceExecutor,
|
|
8
43
|
AppExecutor,
|
|
9
44
|
LibExecutor,
|
|
10
45
|
PkgExecutor
|
|
11
|
-
} from "./index-
|
|
46
|
+
} from "./index-n3jk2501.js";
|
|
47
|
+
import {
|
|
48
|
+
DesktopServerStage,
|
|
49
|
+
resolveMobileTargets,
|
|
50
|
+
resolveAppId,
|
|
51
|
+
NativeApp
|
|
52
|
+
} from "./index-kb16nzyt.js";
|
|
12
53
|
import {
|
|
13
54
|
LibraryScript
|
|
14
|
-
} from "./index-
|
|
55
|
+
} from "./index-gxamdf18.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-35mmm9ca.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,12 +1866,8 @@ ${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-mjh5ppsq.js")).AbstractCompactor;
|
|
1869
|
+
import { Logger as Logger3 } from "akanjs/common";
|
|
1870
|
+
var loadBuildRunner = async () => (await import("./applicationBuildRunner-qkt6spd5.js")).ApplicationBuildRunner;
|
|
2056
1871
|
var loadPrompts = async () => await import("@inquirer/prompts");
|
|
2057
1872
|
|
|
2058
1873
|
class ApplicationRunner extends runner("application") {
|
|
@@ -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,173 @@ 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 } = {}) {
|
|
2236
2063
|
const targets = await resolveMobileTargets(app, target);
|
|
2237
|
-
|
|
2064
|
+
if (installer && platform !== "windows")
|
|
2065
|
+
throw new Error(`--installer builds a Windows setup program; this computer builds for ${platform}.`);
|
|
2066
|
+
const carried = await this.#stageMobile(app, platform, targets, env);
|
|
2238
2067
|
await this.#runMobileTargets(targets, async (mobileTarget) => {
|
|
2239
|
-
await new (
|
|
2068
|
+
this.#reportBuild(app, mobileTarget, await new NativeApp(app, mobileTarget, env).build(platform, {
|
|
2069
|
+
profile,
|
|
2070
|
+
...carried && ApplicationRunner.carriesServer(mobileTarget, platform) ? { server: carried } : {},
|
|
2071
|
+
...installer ? { installer } : {}
|
|
2072
|
+
}));
|
|
2240
2073
|
});
|
|
2241
2074
|
}
|
|
2242
|
-
async
|
|
2243
|
-
|
|
2244
|
-
|
|
2245
|
-
|
|
2246
|
-
target
|
|
2247
|
-
|
|
2248
|
-
|
|
2249
|
-
|
|
2250
|
-
|
|
2251
|
-
|
|
2252
|
-
|
|
2253
|
-
|
|
2254
|
-
|
|
2255
|
-
|
|
2256
|
-
await
|
|
2257
|
-
|
|
2258
|
-
|
|
2259
|
-
}
|
|
2075
|
+
async buildDesktop(app, options = {}) {
|
|
2076
|
+
await this.buildMobile(app, NativeApp.desktopPlatform(), options);
|
|
2077
|
+
}
|
|
2078
|
+
async startMobile(app, platform, { target, env = "local", operation = "local", device, teamId, interrupt } = {}) {
|
|
2079
|
+
const mobileTarget = await ApplicationRunner.startTarget(app, platform, target);
|
|
2080
|
+
const nativeApp = new NativeApp(app, mobileTarget, env);
|
|
2081
|
+
const selection = { ...device ? { device } : {}, ...teamId ? { teamId } : {} };
|
|
2082
|
+
if (operation === "release") {
|
|
2083
|
+
const carried = await this.#stageMobile(app, platform, [mobileTarget], env);
|
|
2084
|
+
const running = await nativeApp.run(platform, {
|
|
2085
|
+
...selection,
|
|
2086
|
+
profile: "release",
|
|
2087
|
+
...carried ? { server: carried } : {}
|
|
2088
|
+
});
|
|
2089
|
+
await running.exited;
|
|
2090
|
+
return;
|
|
2091
|
+
}
|
|
2092
|
+
const upstream = `http://localhost:${await app.getDevPort()}`;
|
|
2093
|
+
if (!await ApplicationRunner.answers(upstream, app.name, app.workspace.workspaceRoot))
|
|
2094
|
+
throw new Error(`No dev server answers on ${upstream}; run \`akan start ${app.name}\` first.`);
|
|
2095
|
+
const { i18n } = await app.getConfig();
|
|
2096
|
+
const session = await nativeApp.dev(platform, { upstream, lang: i18n.defaultLocale, ...selection });
|
|
2097
|
+
if (interrupt)
|
|
2098
|
+
interrupt.add(async () => await session.stop(), "Abandoning the app's shutdown; its window may stay open.", 130);
|
|
2099
|
+
else
|
|
2100
|
+
process.once("SIGINT", () => {
|
|
2101
|
+
session.stop().finally(() => process.exit(130));
|
|
2102
|
+
});
|
|
2103
|
+
await session.exited;
|
|
2104
|
+
}
|
|
2105
|
+
async startDesktop(app, options = {}) {
|
|
2106
|
+
await this.startMobile(app, NativeApp.desktopPlatform(), options);
|
|
2260
2107
|
}
|
|
2261
|
-
async
|
|
2108
|
+
static async startTarget(app, platform = NativeApp.desktopPlatform(), target) {
|
|
2262
2109
|
const targets = await resolveMobileTargets(app, target);
|
|
2263
|
-
|
|
2264
|
-
|
|
2265
|
-
|
|
2110
|
+
const [mobileTarget] = targets;
|
|
2111
|
+
if (!mobileTarget || targets.length > 1)
|
|
2112
|
+
throw new Error(`start-${platform === "ios" || platform === "android" ? platform : "desktop"} runs one native target at a time; pass --target <name>.`);
|
|
2113
|
+
return mobileTarget;
|
|
2114
|
+
}
|
|
2115
|
+
static async answers(url, appName, workspaceRoot) {
|
|
2116
|
+
const signal = AbortSignal.timeout(3000);
|
|
2117
|
+
const health = await fetch(new URL("/_akan/app/health", url), { signal }).catch(() => null);
|
|
2118
|
+
if (!health)
|
|
2119
|
+
return false;
|
|
2120
|
+
const info = await fetch(new URL("/_akan/app/info", url), { signal }).then(async (res) => res.ok ? await res.json() : null).catch(() => null);
|
|
2121
|
+
const stopIt = `Stop it (\`akan start ${appName} --kill\` takes the port over) or give ${appName} another port with AKAN_DEV_PORT`;
|
|
2122
|
+
if (info?.appName === appName) {
|
|
2123
|
+
if (typeof info.workspaceRoot !== "string" || ApplicationRunner.#sameCheckout(info.workspaceRoot, workspaceRoot))
|
|
2124
|
+
return true;
|
|
2125
|
+
throw new Error(`${url} is the dev server of ${appName} in ${info.workspaceRoot}, not in ${workspaceRoot}. ${stopIt}.`);
|
|
2266
2126
|
}
|
|
2127
|
+
if (typeof info?.appName === "string")
|
|
2128
|
+
throw new Error(`${url} is the dev server of ${info.appName}, not ${appName}. ${stopIt}.`);
|
|
2129
|
+
const { pid } = await health.json().catch(() => null) ?? {};
|
|
2130
|
+
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
2131
|
}
|
|
2268
|
-
|
|
2132
|
+
static #sameCheckout(reported, own) {
|
|
2133
|
+
const canonical = (root) => {
|
|
2134
|
+
try {
|
|
2135
|
+
return realpathSync.native(root);
|
|
2136
|
+
} catch {
|
|
2137
|
+
return path5.resolve(root);
|
|
2138
|
+
}
|
|
2139
|
+
};
|
|
2140
|
+
return canonical(reported) === canonical(own);
|
|
2141
|
+
}
|
|
2142
|
+
async releaseIos(app, { target, env = "main", teamId, adHoc = false } = {}) {
|
|
2269
2143
|
const targets = await resolveMobileTargets(app, target);
|
|
2270
2144
|
await this.#buildMobileCsr(app, env);
|
|
2271
|
-
|
|
2272
|
-
await new (
|
|
2273
|
-
});
|
|
2145
|
+
for (const mobileTarget of targets)
|
|
2146
|
+
this.#reportBuild(app, mobileTarget, await new NativeApp(app, mobileTarget, env).releaseIos({ teamId, adHoc }));
|
|
2274
2147
|
}
|
|
2275
|
-
async
|
|
2276
|
-
open = false,
|
|
2277
|
-
operation = "local",
|
|
2278
|
-
env = "local",
|
|
2279
|
-
target,
|
|
2280
|
-
regenerate = false
|
|
2281
|
-
} = {}) {
|
|
2148
|
+
async releaseAndroid(app, format, { target, env = "main" } = {}) {
|
|
2282
2149
|
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
|
-
});
|
|
2150
|
+
NativeApp.androidSigning();
|
|
2151
|
+
await this.#buildMobileCsr(app, env);
|
|
2152
|
+
for (const mobileTarget of targets)
|
|
2153
|
+
this.#reportBuild(app, mobileTarget, await new NativeApp(app, mobileTarget, env).releaseAndroid({ formats: [format] }));
|
|
2291
2154
|
}
|
|
2292
|
-
async
|
|
2155
|
+
async packUpdate(app, platform, { target, env = "main", out, against }) {
|
|
2293
2156
|
const targets = await resolveMobileTargets(app, target);
|
|
2157
|
+
const [mobileTarget] = targets;
|
|
2158
|
+
if (!mobileTarget || targets.length > 1)
|
|
2159
|
+
throw new Error("pack-update packs one native target at a time; pass --target <name>.");
|
|
2294
2160
|
await this.#buildMobileCsr(app, env);
|
|
2161
|
+
const packed = await new NativeApp(app, mobileTarget, env).packUpdate(platform, { out });
|
|
2162
|
+
const size = packed.manifest.files.reduce((sum, file) => sum + file.size, 0);
|
|
2163
|
+
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}`);
|
|
2164
|
+
if (!against)
|
|
2165
|
+
return;
|
|
2166
|
+
const bundle = path5.join(packed.out, "bundle.json");
|
|
2167
|
+
const compat = await NativeApp.compareBundles(app.cwdPath, against, bundle);
|
|
2168
|
+
await Bun.write(path5.join(packed.out, "compat.json"), `${JSON.stringify({ against, ...compat }, null, 2)}
|
|
2169
|
+
`);
|
|
2170
|
+
if (!compat.compatible)
|
|
2171
|
+
throw new Error(`This bundle needs a new store build; the one ${against} came from provides another native API:
|
|
2172
|
+
- ${compat.problems.join(`
|
|
2173
|
+
- `)}`);
|
|
2174
|
+
app.logger.info(`runs in the store build of ${against}`);
|
|
2175
|
+
}
|
|
2176
|
+
async updateKeygen(app, platform, target) {
|
|
2177
|
+
const nativePlatform = platform === "desktop" ? NativeApp.desktopPlatform() : platform;
|
|
2178
|
+
const seen = new Set;
|
|
2179
|
+
for (const mobileTarget of await resolveMobileTargets(app, target ?? "all")) {
|
|
2180
|
+
const appId = resolveAppId(mobileTarget.config.appId, nativePlatform);
|
|
2181
|
+
if (seen.has(appId))
|
|
2182
|
+
continue;
|
|
2183
|
+
seen.add(appId);
|
|
2184
|
+
const { publicKey, keyPath, created } = await new NativeApp(app, mobileTarget).updateKeygen(nativePlatform);
|
|
2185
|
+
app.log(`${appId}: ${created ? "made" : "read"} ${keyPath}${created ? " (keep it private and backed up: without it, installed apps take no more updates)" : ""}`);
|
|
2186
|
+
app.log(` native: { updates: { url: "https://\u2026/${app.name}", publicKey: ${JSON.stringify(publicKey)} } }`);
|
|
2187
|
+
const declared = mobileTarget.config.updates?.publicKey;
|
|
2188
|
+
if (declared && declared !== publicKey)
|
|
2189
|
+
app.logger.warn(`native.targets.${mobileTarget.name}.updates.publicKey is another key's; releases would not verify.`);
|
|
2190
|
+
}
|
|
2191
|
+
}
|
|
2192
|
+
async publishUpdate(app, platform, { target, env = "main", channel } = {}) {
|
|
2193
|
+
const targets = await resolveMobileTargets(app, target);
|
|
2194
|
+
const nativePlatform = platform === "desktop" ? NativeApp.desktopPlatform() : platform;
|
|
2195
|
+
for (const mobileTarget of targets)
|
|
2196
|
+
await new NativeApp(app, mobileTarget, env).assertPublishable(nativePlatform, {
|
|
2197
|
+
server: ApplicationRunner.carriesServer(mobileTarget, nativePlatform),
|
|
2198
|
+
...channel ? { channel } : {}
|
|
2199
|
+
});
|
|
2200
|
+
const carried = await this.#stageMobile(app, nativePlatform, targets, env);
|
|
2295
2201
|
for (const mobileTarget of targets) {
|
|
2296
|
-
|
|
2297
|
-
|
|
2298
|
-
|
|
2202
|
+
const nativeApp = new NativeApp(app, mobileTarget, env);
|
|
2203
|
+
const published = await nativeApp.publishUpdate(nativePlatform, {
|
|
2204
|
+
...channel ? { channel } : {},
|
|
2205
|
+
...carried && ApplicationRunner.carriesServer(mobileTarget, nativePlatform) ? { server: carried } : {}
|
|
2206
|
+
});
|
|
2207
|
+
this.#reportBuild(app, mobileTarget, published.build);
|
|
2208
|
+
app.log(`${app.name}/${mobileTarget.name} ${published.bundle}: ${published.dir}/${published.channel}.json, ${published.files} files, ${Math.round(published.size / 1024)} KiB`);
|
|
2209
|
+
app.log(`Upload ${nativeApp.updatesDir} to ${mobileTarget.config.updates?.url}, the manifests last.`);
|
|
2299
2210
|
}
|
|
2300
2211
|
}
|
|
2212
|
+
#reportBuild(app, mobileTarget, { artifacts, warnings, signing }) {
|
|
2213
|
+
for (const warning of warnings)
|
|
2214
|
+
app.logger.warn(warning);
|
|
2215
|
+
if (signing)
|
|
2216
|
+
app.log(`Signed with ${signing.identity} (${signing.profile}, team ${signing.teamId})`);
|
|
2217
|
+
for (const artifact of artifacts)
|
|
2218
|
+
app.log(`${app.name}/${mobileTarget.name} ${artifact.kind}: ${artifact.path}`);
|
|
2219
|
+
}
|
|
2220
|
+
static carriesServer({ config }, platform = NativeApp.desktopPlatform()) {
|
|
2221
|
+
return platform !== "ios" && platform !== "android" && config.desktop?.server === true;
|
|
2222
|
+
}
|
|
2223
|
+
async#stageMobile(app, platform, targets, env) {
|
|
2224
|
+
const stage = targets.some((mobileTarget) => ApplicationRunner.carriesServer(mobileTarget, platform)) ? new DesktopServerStage(app) : null;
|
|
2225
|
+
await stage?.install();
|
|
2226
|
+
await this.#buildMobileCsr(app, env);
|
|
2227
|
+
return await stage?.prepare(env);
|
|
2228
|
+
}
|
|
2301
2229
|
async#buildMobileCsr(app, env) {
|
|
2302
2230
|
const prevEnv = {
|
|
2303
2231
|
AKAN_PUBLIC_ENV: process.env.AKAN_PUBLIC_ENV,
|
|
@@ -2310,7 +2238,7 @@ try {
|
|
|
2310
2238
|
APP_OPERATION_MODE: "release"
|
|
2311
2239
|
});
|
|
2312
2240
|
try {
|
|
2313
|
-
await new (await loadBuildRunner())(app).build({ spinner: true });
|
|
2241
|
+
await new (await loadBuildRunner())(app, { environment: env }).build({ spinner: true });
|
|
2314
2242
|
} finally {
|
|
2315
2243
|
for (const [key, value] of Object.entries(prevEnv)) {
|
|
2316
2244
|
if (value === undefined)
|
|
@@ -2335,24 +2263,23 @@ try {
|
|
|
2335
2263
|
return;
|
|
2336
2264
|
for (const failure of failures) {
|
|
2337
2265
|
const message = failure.error instanceof Error ? failure.error.message : String(failure.error);
|
|
2338
|
-
|
|
2266
|
+
Logger3.rawLog(`Native target ${failure.target} failed: ${message}`, undefined, "error");
|
|
2339
2267
|
}
|
|
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();
|
|
2268
|
+
throw new Error(`${failures.length}/${results.length} native targets failed`);
|
|
2348
2269
|
}
|
|
2349
2270
|
#getLocalDatabaseServices(mode) {
|
|
2350
2271
|
if (mode === "single")
|
|
2351
2272
|
return [];
|
|
2352
2273
|
if (mode === "multiple")
|
|
2353
|
-
return ["redis"
|
|
2274
|
+
return ["redis"];
|
|
2354
2275
|
return ["redis", "postgres"];
|
|
2355
2276
|
}
|
|
2277
|
+
async#assertComposeHas(workspace, services) {
|
|
2278
|
+
const compose = Bun.YAML.parse(await Bun.file(`${workspace.workspaceRoot}/local/docker-compose.yaml`).text());
|
|
2279
|
+
const missing = services.filter((service) => !compose?.services?.[service]);
|
|
2280
|
+
if (missing.length)
|
|
2281
|
+
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.`);
|
|
2282
|
+
}
|
|
2356
2283
|
async#isLocalDatabaseUp(workspace, mode) {
|
|
2357
2284
|
const requiredServices = this.#getLocalDatabaseServices(mode);
|
|
2358
2285
|
if (!requiredServices.length)
|
|
@@ -2366,16 +2293,18 @@ try {
|
|
|
2366
2293
|
async dbup(workspace, mode = "multiple") {
|
|
2367
2294
|
if (mode === "single")
|
|
2368
2295
|
return true;
|
|
2296
|
+
const services = this.#getLocalDatabaseServices(mode);
|
|
2297
|
+
await workspace.applyTemplate({
|
|
2298
|
+
basePath: "local",
|
|
2299
|
+
template: "localDev",
|
|
2300
|
+
dict: { repoName: workspace.repoName },
|
|
2301
|
+
overwrite: false
|
|
2302
|
+
});
|
|
2303
|
+
await this.#assertComposeHas(workspace, services);
|
|
2369
2304
|
try {
|
|
2370
|
-
await workspace.applyTemplate({
|
|
2371
|
-
basePath: "local",
|
|
2372
|
-
template: "localDev",
|
|
2373
|
-
dict: { repoName: workspace.repoName },
|
|
2374
|
-
overwrite: false
|
|
2375
|
-
});
|
|
2376
2305
|
const wasAlreadyUp = await this.#isLocalDatabaseUp(workspace, mode);
|
|
2377
2306
|
if (!wasAlreadyUp)
|
|
2378
|
-
await workspace.spawn(`docker`, ["compose", "up", "-d", ...
|
|
2307
|
+
await workspace.spawn(`docker`, ["compose", "up", "-d", ...services], {
|
|
2379
2308
|
cwd: `${workspace.workspaceRoot}/local`
|
|
2380
2309
|
});
|
|
2381
2310
|
return wasAlreadyUp;
|
|
@@ -2393,74 +2322,182 @@ ${detail}`
|
|
|
2393
2322
|
async dbdown(workspace) {
|
|
2394
2323
|
await workspace.spawn(`docker`, ["compose", "down"], { cwd: `${workspace.workspaceRoot}/local` });
|
|
2395
2324
|
}
|
|
2396
|
-
|
|
2397
|
-
|
|
2398
|
-
|
|
2399
|
-
|
|
2400
|
-
|
|
2401
|
-
|
|
2402
|
-
|
|
2403
|
-
|
|
2404
|
-
|
|
2405
|
-
|
|
2406
|
-
|
|
2407
|
-
|
|
2408
|
-
|
|
2409
|
-
|
|
2410
|
-
|
|
2411
|
-
|
|
2412
|
-
|
|
2413
|
-
|
|
2414
|
-
|
|
2415
|
-
|
|
2416
|
-
|
|
2417
|
-
|
|
2418
|
-
|
|
2419
|
-
|
|
2325
|
+
}
|
|
2326
|
+
|
|
2327
|
+
// pkgs/@akanjs/cli/application/hostProcessProbe.ts
|
|
2328
|
+
import { readdirSync, readFileSync, readlinkSync } from "fs";
|
|
2329
|
+
|
|
2330
|
+
class HostProcessProbe {
|
|
2331
|
+
static timeoutMs = 3000;
|
|
2332
|
+
static win32TimeoutMs = 20000;
|
|
2333
|
+
static #procTcpListen = "0A";
|
|
2334
|
+
static #win32ProcessQuery = "[Console]::OutputEncoding=[Text.Encoding]::UTF8; Get-CimInstance Win32_Process | Select-Object ProcessId,ParentProcessId,CommandLine,CreationDate | ConvertTo-Json -Compress";
|
|
2335
|
+
platform;
|
|
2336
|
+
#win32Table = null;
|
|
2337
|
+
constructor(platform = process.platform) {
|
|
2338
|
+
this.platform = platform;
|
|
2339
|
+
}
|
|
2340
|
+
async listenersOn(port) {
|
|
2341
|
+
if (this.platform === "win32")
|
|
2342
|
+
return HostProcessProbe.parseNetstat(await HostProcessProbe.run(["netstat", "-ano"]), port);
|
|
2343
|
+
if (this.platform === "linux")
|
|
2344
|
+
return HostProcessProbe.#procListenersOn(port);
|
|
2345
|
+
const output = await HostProcessProbe.run(["lsof", "-ti", `tcp:${port}`, "-sTCP:LISTEN"]);
|
|
2346
|
+
return [
|
|
2347
|
+
...new Set(output.split(`
|
|
2348
|
+
`).map((line) => Number(line.trim())).filter((pid) => Number.isInteger(pid) && pid > 0))
|
|
2349
|
+
];
|
|
2420
2350
|
}
|
|
2421
|
-
async
|
|
2422
|
-
|
|
2351
|
+
async parentOf(pid) {
|
|
2352
|
+
if (this.platform === "win32") {
|
|
2353
|
+
const child = await this.#win32Process(pid);
|
|
2354
|
+
const parent = child ? await this.#win32Process(child.ppid) : null;
|
|
2355
|
+
return child && parent ? HostProcessProbe.parentPidOf(child, parent) : null;
|
|
2356
|
+
}
|
|
2357
|
+
if (this.platform === "linux") {
|
|
2358
|
+
const stat = HostProcessProbe.#readProc(pid, "stat");
|
|
2359
|
+
return stat === null ? null : HostProcessProbe.parseProcStatPpid(stat);
|
|
2360
|
+
}
|
|
2361
|
+
const parsed = Number((await HostProcessProbe.run(["ps", "-p", String(pid), "-o", "ppid="])).trim());
|
|
2362
|
+
return Number.isInteger(parsed) && parsed > 0 ? parsed : null;
|
|
2423
2363
|
}
|
|
2424
|
-
async
|
|
2425
|
-
|
|
2426
|
-
|
|
2427
|
-
|
|
2428
|
-
|
|
2429
|
-
|
|
2430
|
-
|
|
2431
|
-
|
|
2432
|
-
environment,
|
|
2433
|
-
local
|
|
2434
|
-
});
|
|
2435
|
-
return;
|
|
2364
|
+
async commandOf(pid) {
|
|
2365
|
+
if (this.platform === "win32")
|
|
2366
|
+
return (await this.#win32Process(pid))?.command ?? null;
|
|
2367
|
+
if (this.platform === "linux") {
|
|
2368
|
+
const cmdline = HostProcessProbe.#readProc(pid, "cmdline");
|
|
2369
|
+
return cmdline?.split("\x00").filter(Boolean).join(" ") || null;
|
|
2370
|
+
}
|
|
2371
|
+
return (await HostProcessProbe.run(["ps", "-ww", "-p", String(pid), "-o", "command="])).trim() || null;
|
|
2436
2372
|
}
|
|
2437
|
-
|
|
2438
|
-
|
|
2439
|
-
|
|
2440
|
-
|
|
2441
|
-
|
|
2442
|
-
|
|
2443
|
-
|
|
2444
|
-
|
|
2445
|
-
|
|
2446
|
-
|
|
2447
|
-
|
|
2448
|
-
|
|
2449
|
-
|
|
2450
|
-
|
|
2451
|
-
|
|
2452
|
-
|
|
2453
|
-
|
|
2454
|
-
|
|
2455
|
-
|
|
2456
|
-
|
|
2457
|
-
|
|
2373
|
+
static parentPidOf(child, parent) {
|
|
2374
|
+
if (child.ppid !== parent.pid)
|
|
2375
|
+
return null;
|
|
2376
|
+
if (parent.startedAt !== null && child.startedAt !== null && parent.startedAt > child.startedAt)
|
|
2377
|
+
return null;
|
|
2378
|
+
return parent.pid;
|
|
2379
|
+
}
|
|
2380
|
+
static parseNetstat(output, port) {
|
|
2381
|
+
const pids = new Set;
|
|
2382
|
+
for (const line of output.split(/\r?\n/)) {
|
|
2383
|
+
const fields = line.trim().split(/\s+/);
|
|
2384
|
+
if (fields[0] !== "TCP" || fields.length < 5)
|
|
2385
|
+
continue;
|
|
2386
|
+
const [, local, remote] = fields;
|
|
2387
|
+
if (!local?.endsWith(`:${port}`) || !remote?.endsWith(":0"))
|
|
2388
|
+
continue;
|
|
2389
|
+
const pid = Number(fields.at(-1));
|
|
2390
|
+
if (Number.isInteger(pid) && pid > 0)
|
|
2391
|
+
pids.add(pid);
|
|
2392
|
+
}
|
|
2393
|
+
return [...pids];
|
|
2394
|
+
}
|
|
2395
|
+
static parseProcNetTcp(text, port) {
|
|
2396
|
+
const inodes = [];
|
|
2397
|
+
for (const line of text.split(`
|
|
2398
|
+
`).slice(1)) {
|
|
2399
|
+
const fields = line.trim().split(/\s+/);
|
|
2400
|
+
const [, local, , state, , , , , , inode] = fields;
|
|
2401
|
+
if (!local || !inode || state !== HostProcessProbe.#procTcpListen || inode === "0")
|
|
2402
|
+
continue;
|
|
2403
|
+
if (Number.parseInt(local.slice(local.lastIndexOf(":") + 1), 16) === port)
|
|
2404
|
+
inodes.push(inode);
|
|
2405
|
+
}
|
|
2406
|
+
return inodes;
|
|
2407
|
+
}
|
|
2408
|
+
static parseProcStatPpid(stat) {
|
|
2409
|
+
const ppid = Number(stat.slice(stat.lastIndexOf(")") + 2).split(" ")[1]);
|
|
2410
|
+
return Number.isInteger(ppid) && ppid > 0 ? ppid : null;
|
|
2411
|
+
}
|
|
2412
|
+
static parseWin32Processes(json) {
|
|
2413
|
+
const table = new Map;
|
|
2414
|
+
let parsed;
|
|
2415
|
+
try {
|
|
2416
|
+
parsed = JSON.parse(json.trim() || "[]");
|
|
2417
|
+
} catch {
|
|
2418
|
+
return table;
|
|
2419
|
+
}
|
|
2420
|
+
const rows = Array.isArray(parsed) ? parsed : [parsed];
|
|
2421
|
+
for (const row of rows) {
|
|
2422
|
+
if (typeof row?.ProcessId !== "number")
|
|
2423
|
+
continue;
|
|
2424
|
+
table.set(row.ProcessId, {
|
|
2425
|
+
pid: row.ProcessId,
|
|
2426
|
+
ppid: row.ParentProcessId ?? 0,
|
|
2427
|
+
command: row.CommandLine?.trim() || null,
|
|
2428
|
+
startedAt: HostProcessProbe.#parseCimDate(row.CreationDate)
|
|
2429
|
+
});
|
|
2430
|
+
}
|
|
2431
|
+
return table;
|
|
2432
|
+
}
|
|
2433
|
+
static async run(command, timeoutMs = HostProcessProbe.timeoutMs) {
|
|
2434
|
+
let proc;
|
|
2435
|
+
try {
|
|
2436
|
+
proc = Bun.spawn(command, { stdio: ["ignore", "pipe", "ignore"], windowsHide: true });
|
|
2437
|
+
} catch {
|
|
2438
|
+
return "";
|
|
2439
|
+
}
|
|
2440
|
+
const timer = setTimeout(() => proc.kill("SIGKILL"), timeoutMs);
|
|
2441
|
+
try {
|
|
2442
|
+
return await new Response(proc.stdout).text();
|
|
2443
|
+
} catch {
|
|
2444
|
+
return "";
|
|
2445
|
+
} finally {
|
|
2446
|
+
clearTimeout(timer);
|
|
2447
|
+
}
|
|
2448
|
+
}
|
|
2449
|
+
async#win32Process(pid) {
|
|
2450
|
+
this.#win32Table ??= await HostProcessProbe.#queryWin32Processes();
|
|
2451
|
+
if (!this.#win32Table.has(pid))
|
|
2452
|
+
this.#win32Table = await HostProcessProbe.#queryWin32Processes();
|
|
2453
|
+
return this.#win32Table.get(pid) ?? null;
|
|
2454
|
+
}
|
|
2455
|
+
static async#queryWin32Processes() {
|
|
2456
|
+
const output = await HostProcessProbe.run(["powershell", "-NoProfile", "-NonInteractive", "-Command", HostProcessProbe.#win32ProcessQuery], HostProcessProbe.win32TimeoutMs);
|
|
2457
|
+
return HostProcessProbe.parseWin32Processes(output);
|
|
2458
|
+
}
|
|
2459
|
+
static #parseCimDate(value) {
|
|
2460
|
+
if (!value)
|
|
2461
|
+
return null;
|
|
2462
|
+
const legacy = /\/Date\((\d+)/.exec(value)?.[1];
|
|
2463
|
+
const ms = legacy ? Number(legacy) : Date.parse(value);
|
|
2464
|
+
return Number.isFinite(ms) ? ms : null;
|
|
2465
|
+
}
|
|
2466
|
+
static #procListenersOn(port) {
|
|
2467
|
+
const inodes = new Set(["tcp", "tcp6"].flatMap((table) => HostProcessProbe.parseProcNetTcp(HostProcessProbe.#readProc("net", table) ?? "", port)));
|
|
2468
|
+
if (!inodes.size)
|
|
2469
|
+
return [];
|
|
2470
|
+
const pids = [];
|
|
2471
|
+
for (const entry of HostProcessProbe.#listDir("/proc")) {
|
|
2472
|
+
if (!/^\d+$/.test(entry))
|
|
2473
|
+
continue;
|
|
2474
|
+
const holds = HostProcessProbe.#listDir(`/proc/${entry}/fd`).some((fd) => {
|
|
2475
|
+
const link = HostProcessProbe.#orElse(() => readlinkSync(`/proc/${entry}/fd/${fd}`), "");
|
|
2476
|
+
const inode = /^socket:\[(\d+)\]$/.exec(link)?.[1];
|
|
2477
|
+
return !!inode && inodes.has(inode);
|
|
2478
|
+
});
|
|
2479
|
+
if (holds)
|
|
2480
|
+
pids.push(Number(entry));
|
|
2481
|
+
}
|
|
2482
|
+
return pids;
|
|
2483
|
+
}
|
|
2484
|
+
static #readProc(pid, file) {
|
|
2485
|
+
return HostProcessProbe.#orElse(() => readFileSync(`/proc/${pid}/${file}`, "utf8"), null);
|
|
2486
|
+
}
|
|
2487
|
+
static #listDir(dir) {
|
|
2488
|
+
return HostProcessProbe.#orElse(() => readdirSync(dir), []);
|
|
2489
|
+
}
|
|
2490
|
+
static #orElse(read, fallback) {
|
|
2491
|
+
try {
|
|
2492
|
+
return read();
|
|
2493
|
+
} catch {
|
|
2494
|
+
return fallback;
|
|
2495
|
+
}
|
|
2458
2496
|
}
|
|
2459
2497
|
}
|
|
2460
2498
|
|
|
2461
2499
|
// pkgs/@akanjs/cli/application/devPortReclaimer.ts
|
|
2462
2500
|
class DevPortReclaimer {
|
|
2463
|
-
static lsofTimeoutMs = 3000;
|
|
2464
2501
|
static releaseTimeoutMs = 8000;
|
|
2465
2502
|
static maxAncestorDepth = 8;
|
|
2466
2503
|
static #releasePollMs = 100;
|
|
@@ -2469,7 +2506,12 @@ class DevPortReclaimer {
|
|
|
2469
2506
|
return [...new Set(ports.flatMap((port) => [port, port + DevPortReclaimer.wsPortOffset]))].sort((left, right) => left - right);
|
|
2470
2507
|
}
|
|
2471
2508
|
static isAkanCommand(command) {
|
|
2472
|
-
|
|
2509
|
+
const normalized = command.replaceAll("\\", "/");
|
|
2510
|
+
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");
|
|
2511
|
+
}
|
|
2512
|
+
#probe;
|
|
2513
|
+
constructor(probe = new HostProcessProbe) {
|
|
2514
|
+
this.#probe = probe;
|
|
2473
2515
|
}
|
|
2474
2516
|
async reclaim(ports) {
|
|
2475
2517
|
const killed = [];
|
|
@@ -2494,10 +2536,10 @@ class DevPortReclaimer {
|
|
|
2494
2536
|
}
|
|
2495
2537
|
async holdersOf(port) {
|
|
2496
2538
|
const holders = [];
|
|
2497
|
-
for (const listenerPid of await this.#
|
|
2539
|
+
for (const listenerPid of await this.#probe.listenersOn(port)) {
|
|
2498
2540
|
if (listenerPid === process.pid || listenerPid === process.ppid)
|
|
2499
2541
|
continue;
|
|
2500
|
-
const command = await this.#commandOf(listenerPid);
|
|
2542
|
+
const command = await this.#probe.commandOf(listenerPid);
|
|
2501
2543
|
if (!command)
|
|
2502
2544
|
continue;
|
|
2503
2545
|
if (!DevPortReclaimer.isAkanCommand(command)) {
|
|
@@ -2512,44 +2554,37 @@ class DevPortReclaimer {
|
|
|
2512
2554
|
async#akanRootOf(pid, command) {
|
|
2513
2555
|
let root = { pid, command };
|
|
2514
2556
|
for (let depth = 0;depth < DevPortReclaimer.maxAncestorDepth; depth += 1) {
|
|
2515
|
-
const parentPid = await this.#
|
|
2557
|
+
const parentPid = await this.#probe.parentOf(root.pid);
|
|
2516
2558
|
if (parentPid === null || parentPid <= 1 || parentPid === process.pid || parentPid === process.ppid)
|
|
2517
2559
|
break;
|
|
2518
|
-
const parentCommand = await this.#commandOf(parentPid);
|
|
2560
|
+
const parentCommand = await this.#probe.commandOf(parentPid);
|
|
2519
2561
|
if (!parentCommand || !DevPortReclaimer.isAkanCommand(parentCommand))
|
|
2520
2562
|
break;
|
|
2521
2563
|
root = { pid: parentPid, command: parentCommand };
|
|
2522
2564
|
}
|
|
2523
2565
|
return root;
|
|
2524
2566
|
}
|
|
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
2567
|
async#terminate(pid) {
|
|
2568
|
+
if (this.#probe.platform === "win32") {
|
|
2569
|
+
await HostProcessProbe.run(["taskkill", "/PID", String(pid), "/T", "/F"]);
|
|
2570
|
+
return await this.#released(pid);
|
|
2571
|
+
}
|
|
2542
2572
|
if (!this.#signal(pid, "SIGTERM"))
|
|
2543
2573
|
return !this.#alive(pid);
|
|
2574
|
+
if (await this.#released(pid))
|
|
2575
|
+
return true;
|
|
2576
|
+
this.#signal(pid, "SIGKILL");
|
|
2577
|
+
await Bun.sleep(DevPortReclaimer.#releasePollMs);
|
|
2578
|
+
return !this.#alive(pid);
|
|
2579
|
+
}
|
|
2580
|
+
async#released(pid) {
|
|
2544
2581
|
const deadline = Date.now() + DevPortReclaimer.releaseTimeoutMs;
|
|
2545
2582
|
while (Date.now() < deadline) {
|
|
2546
2583
|
if (!this.#alive(pid))
|
|
2547
2584
|
return true;
|
|
2548
2585
|
await Bun.sleep(DevPortReclaimer.#releasePollMs);
|
|
2549
2586
|
}
|
|
2550
|
-
|
|
2551
|
-
await Bun.sleep(DevPortReclaimer.#releasePollMs);
|
|
2552
|
-
return !this.#alive(pid);
|
|
2587
|
+
return false;
|
|
2553
2588
|
}
|
|
2554
2589
|
#signal(pid, signal) {
|
|
2555
2590
|
try {
|
|
@@ -2567,26 +2602,15 @@ class DevPortReclaimer {
|
|
|
2567
2602
|
return error.code === "EPERM";
|
|
2568
2603
|
}
|
|
2569
2604
|
}
|
|
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
2605
|
}
|
|
2582
2606
|
|
|
2583
2607
|
// pkgs/@akanjs/cli/application/devStreamView.ts
|
|
2584
|
-
import { Logger as
|
|
2608
|
+
import { Logger as Logger4 } from "akanjs/common";
|
|
2585
2609
|
import chalk from "chalk";
|
|
2586
2610
|
var appColors = ["cyan", "magenta", "yellow", "green", "blue", "red"];
|
|
2587
2611
|
|
|
2588
2612
|
class DevStreamView {
|
|
2589
|
-
#logger = new
|
|
2613
|
+
#logger = new Logger4("akan start");
|
|
2590
2614
|
#partial = new Map;
|
|
2591
2615
|
#lastState = new Map;
|
|
2592
2616
|
#color = new Map;
|
|
@@ -2656,7 +2680,7 @@ import os from "os";
|
|
|
2656
2680
|
import { MemoryLimit as MemoryLimit2 } from "akanjs/server/memoryLimit";
|
|
2657
2681
|
|
|
2658
2682
|
class DevBootConcurrency {
|
|
2659
|
-
static perAppBytes =
|
|
2683
|
+
static perAppBytes = 1800 * 1024 * 1024;
|
|
2660
2684
|
static memoryShare = 0.5;
|
|
2661
2685
|
static coresPerApp = 4;
|
|
2662
2686
|
static budget() {
|
|
@@ -2692,6 +2716,7 @@ class DevBootConcurrency {
|
|
|
2692
2716
|
import { mkdir, rename } from "fs/promises";
|
|
2693
2717
|
import path6 from "path";
|
|
2694
2718
|
import dayjs from "dayjs";
|
|
2719
|
+
|
|
2695
2720
|
class DevSessionLog {
|
|
2696
2721
|
static fileName = "dev.log";
|
|
2697
2722
|
static previousFileName = "dev.prev.log";
|
|
@@ -2792,6 +2817,16 @@ class DevSessionLog {
|
|
|
2792
2817
|
|
|
2793
2818
|
// pkgs/@akanjs/cli/application/devSupervisor.ts
|
|
2794
2819
|
class DevSupervisor {
|
|
2820
|
+
static resolveDevUi(plain, {
|
|
2821
|
+
isTty = !!process.stdout.isTTY,
|
|
2822
|
+
columns = process.stdout.columns ?? 0
|
|
2823
|
+
} = {}) {
|
|
2824
|
+
if (plain)
|
|
2825
|
+
return { mode: "stream", downgraded: false };
|
|
2826
|
+
if (!isTty || columns <= 0)
|
|
2827
|
+
return { mode: "stream", downgraded: true };
|
|
2828
|
+
return { mode: "tui", downgraded: false };
|
|
2829
|
+
}
|
|
2795
2830
|
static supervisedEnvKey = "AKAN_DEV_SUPERVISED";
|
|
2796
2831
|
static childHooks() {
|
|
2797
2832
|
if (process.env[DevSupervisor.supervisedEnvKey] !== "1" || !process.send)
|
|
@@ -2806,7 +2841,7 @@ class DevSupervisor {
|
|
|
2806
2841
|
}
|
|
2807
2842
|
};
|
|
2808
2843
|
}
|
|
2809
|
-
static
|
|
2844
|
+
static bootTimeoutMs = 180000;
|
|
2810
2845
|
static shutdownGraceMs = 12000;
|
|
2811
2846
|
#options;
|
|
2812
2847
|
#children = new Map;
|
|
@@ -2855,23 +2890,21 @@ class DevSupervisor {
|
|
|
2855
2890
|
return ports;
|
|
2856
2891
|
}
|
|
2857
2892
|
#makeChild(app, port) {
|
|
2858
|
-
|
|
2859
|
-
const ready = new Promise((resolve) => {
|
|
2860
|
-
markReady = resolve;
|
|
2861
|
-
});
|
|
2893
|
+
const { promise: booted, resolve: markBooted } = Promise.withResolvers();
|
|
2862
2894
|
return {
|
|
2863
2895
|
status: {
|
|
2864
2896
|
app,
|
|
2865
2897
|
name: app.name,
|
|
2866
2898
|
port,
|
|
2867
2899
|
url: `http://localhost:${port}`,
|
|
2900
|
+
shareUrl: this.#options.shares?.get(app.name) ?? null,
|
|
2868
2901
|
state: "starting",
|
|
2869
2902
|
detail: "queued",
|
|
2870
2903
|
exitCode: null
|
|
2871
2904
|
},
|
|
2872
2905
|
proc: null,
|
|
2873
|
-
|
|
2874
|
-
|
|
2906
|
+
booted,
|
|
2907
|
+
markBooted,
|
|
2875
2908
|
opened: false
|
|
2876
2909
|
};
|
|
2877
2910
|
}
|
|
@@ -2890,20 +2923,22 @@ class DevSupervisor {
|
|
|
2890
2923
|
for (const child of wave)
|
|
2891
2924
|
this.#spawnChild(child);
|
|
2892
2925
|
this.#publishStatus();
|
|
2893
|
-
await Promise.all(wave.map(async (child) => await this.#
|
|
2926
|
+
await Promise.all(wave.map(async (child) => await this.#waitForBoot(child)));
|
|
2894
2927
|
}
|
|
2895
2928
|
}
|
|
2896
|
-
async#
|
|
2897
|
-
|
|
2929
|
+
async#waitForBoot(child) {
|
|
2930
|
+
if (await DevSupervisor.timesOut(child.booted, DevSupervisor.bootTimeoutMs))
|
|
2931
|
+
this.#note(`${child.status.name} has not finished booting after ${Math.round(DevSupervisor.bootTimeoutMs / 1000)}s; starting the next app anyway`, "warn");
|
|
2932
|
+
}
|
|
2933
|
+
static async timesOut(work, ms) {
|
|
2934
|
+
let timer;
|
|
2898
2935
|
const expired = new Promise((resolve) => {
|
|
2899
|
-
timer = setTimeout(() => resolve(
|
|
2936
|
+
timer = setTimeout(() => resolve(true), ms);
|
|
2900
2937
|
});
|
|
2901
2938
|
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");
|
|
2939
|
+
return await Promise.race([work.then(() => false), expired]);
|
|
2904
2940
|
} finally {
|
|
2905
|
-
|
|
2906
|
-
clearTimeout(timer);
|
|
2941
|
+
clearTimeout(timer);
|
|
2907
2942
|
}
|
|
2908
2943
|
}
|
|
2909
2944
|
static childArgs(name, { write = true } = {}) {
|
|
@@ -2930,9 +2965,12 @@ class DevSupervisor {
|
|
|
2930
2965
|
},
|
|
2931
2966
|
stdio: ["ignore", "pipe", "pipe"],
|
|
2932
2967
|
ipc: (message) => {
|
|
2933
|
-
if (!message || typeof message !== "object"
|
|
2968
|
+
if (!message || typeof message !== "object")
|
|
2934
2969
|
return;
|
|
2935
|
-
|
|
2970
|
+
if ("booted" in message)
|
|
2971
|
+
child.markBooted();
|
|
2972
|
+
else if (typeof message.state === "string")
|
|
2973
|
+
this.#applyChildEvent(child, message);
|
|
2936
2974
|
},
|
|
2937
2975
|
serialization: "advanced",
|
|
2938
2976
|
onExit: (_proc, exitCode, signalCode) => {
|
|
@@ -2942,7 +2980,7 @@ class DevSupervisor {
|
|
|
2942
2980
|
return;
|
|
2943
2981
|
child.status.state = "stopped";
|
|
2944
2982
|
child.status.detail = signalCode ? `killed by ${signalCode}` : `exited with ${exitCode}`;
|
|
2945
|
-
child.
|
|
2983
|
+
child.markBooted();
|
|
2946
2984
|
this.#publishStatus();
|
|
2947
2985
|
}
|
|
2948
2986
|
});
|
|
@@ -2956,14 +2994,13 @@ class DevSupervisor {
|
|
|
2956
2994
|
child.status.state = event.state;
|
|
2957
2995
|
child.status.detail = event.detail ?? "";
|
|
2958
2996
|
if (event.state === "ready") {
|
|
2959
|
-
child.markReady();
|
|
2960
2997
|
if (this.#options.open && !child.opened) {
|
|
2961
2998
|
child.opened = true;
|
|
2962
2999
|
openBrowser(child.status.url);
|
|
2963
3000
|
}
|
|
2964
3001
|
}
|
|
2965
3002
|
if (event.state === "failed")
|
|
2966
|
-
child.
|
|
3003
|
+
child.markBooted();
|
|
2967
3004
|
this.#publishStatus();
|
|
2968
3005
|
}
|
|
2969
3006
|
async#drain(stream, name, kind) {
|
|
@@ -3025,44 +3062,81 @@ class DevSupervisor {
|
|
|
3025
3062
|
for (const child of running)
|
|
3026
3063
|
child.proc.kill("SIGTERM");
|
|
3027
3064
|
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);
|
|
3065
|
+
if (!await DevSupervisor.timesOut(exited, DevSupervisor.shutdownGraceMs))
|
|
3066
|
+
return;
|
|
3067
|
+
for (const child of running) {
|
|
3068
|
+
if (child.proc.killed)
|
|
3069
|
+
continue;
|
|
3070
|
+
this.#note(`${child.status.name} did not exit in time; killing it`, "warn");
|
|
3071
|
+
child.proc.kill("SIGKILL");
|
|
3044
3072
|
}
|
|
3045
3073
|
}
|
|
3046
3074
|
}
|
|
3047
3075
|
|
|
3048
|
-
// pkgs/@akanjs/cli/application/
|
|
3049
|
-
|
|
3050
|
-
|
|
3051
|
-
|
|
3052
|
-
|
|
3053
|
-
|
|
3054
|
-
|
|
3055
|
-
|
|
3076
|
+
// pkgs/@akanjs/cli/application/interruptTeardown.ts
|
|
3077
|
+
import { Logger as Logger5 } from "akanjs/common";
|
|
3078
|
+
|
|
3079
|
+
class InterruptTeardown {
|
|
3080
|
+
#teardowns = [];
|
|
3081
|
+
#exit;
|
|
3082
|
+
#listen;
|
|
3083
|
+
#report;
|
|
3084
|
+
#interrupted = false;
|
|
3085
|
+
#chain = Promise.resolve();
|
|
3086
|
+
ownsExit = true;
|
|
3087
|
+
constructor({ exit, listen, report } = {}) {
|
|
3088
|
+
this.#exit = exit ?? ((code) => process.exit(code));
|
|
3089
|
+
this.#listen = listen ?? ((onSignal) => {
|
|
3090
|
+
process.on("SIGINT", onSignal);
|
|
3091
|
+
});
|
|
3092
|
+
this.#report = report ?? ((message) => Logger5.rawLog(message, undefined, "error"));
|
|
3093
|
+
}
|
|
3094
|
+
add(run, abandoned, exitCode = 0) {
|
|
3095
|
+
this.#teardowns.push({ run, abandoned, exitCode, state: "pending" });
|
|
3096
|
+
if (this.#teardowns.length === 1)
|
|
3097
|
+
this.#listen(() => this.#onSignal());
|
|
3098
|
+
}
|
|
3099
|
+
async runAll() {
|
|
3100
|
+
await this.#runTeardowns();
|
|
3101
|
+
}
|
|
3102
|
+
#onSignal() {
|
|
3103
|
+
if (this.#interrupted) {
|
|
3104
|
+
for (const one of this.#teardowns)
|
|
3105
|
+
if (one.state !== "done")
|
|
3106
|
+
this.#report(one.abandoned);
|
|
3107
|
+
this.#exit(130);
|
|
3108
|
+
return;
|
|
3109
|
+
}
|
|
3110
|
+
this.#interrupted = true;
|
|
3111
|
+
this.#runTeardowns().finally(() => {
|
|
3112
|
+
if (this.ownsExit)
|
|
3113
|
+
this.#exit(Math.max(0, ...this.#teardowns.map((one) => one.exitCode)));
|
|
3114
|
+
});
|
|
3115
|
+
}
|
|
3116
|
+
async#runTeardowns() {
|
|
3117
|
+
this.#chain = this.#chain.then(async () => {
|
|
3118
|
+
for (const one of this.#teardowns.toReversed()) {
|
|
3119
|
+
if (one.state !== "pending")
|
|
3120
|
+
continue;
|
|
3121
|
+
one.state = "running";
|
|
3122
|
+
await one.run().catch(() => {
|
|
3123
|
+
return;
|
|
3124
|
+
});
|
|
3125
|
+
one.state = "done";
|
|
3126
|
+
}
|
|
3127
|
+
});
|
|
3128
|
+
await this.#chain;
|
|
3129
|
+
}
|
|
3130
|
+
}
|
|
3056
3131
|
|
|
3057
3132
|
// pkgs/@akanjs/cli/application/application.script.ts
|
|
3058
3133
|
class ApplicationScript extends script("application", [ApplicationRunner, LibraryScript]) {
|
|
3059
3134
|
static dbShutdownTimeoutMs = 20000;
|
|
3135
|
+
static devServerReadyTimeoutMs = DevSupervisor.bootTimeoutMs;
|
|
3136
|
+
#interrupt = new InterruptTeardown;
|
|
3060
3137
|
async confirmDatabaseModeDependencyInstall(databaseMode, installSpecs) {
|
|
3061
3138
|
return await confirm({
|
|
3062
|
-
message:
|
|
3063
|
-
`Database mode '${databaseMode}' requires missing dependencies: ${installSpecs.join(", ")}.`,
|
|
3064
|
-
"Install them now?"
|
|
3065
|
-
].join(" "),
|
|
3139
|
+
message: `Database mode '${databaseMode}' requires missing dependencies: ${installSpecs.join(", ")}. Install them now?`,
|
|
3066
3140
|
default: true
|
|
3067
3141
|
});
|
|
3068
3142
|
}
|
|
@@ -3073,61 +3147,16 @@ class ApplicationScript extends script("application", [ApplicationRunner, Librar
|
|
|
3073
3147
|
const shouldInstall = await this.confirmDatabaseModeDependencyInstall(databaseMode, installSpecs);
|
|
3074
3148
|
if (!shouldInstall)
|
|
3075
3149
|
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
|
-
}
|
|
3150
|
+
await this.#addDependencies(app, installSpecs, `database dependencies for ${databaseMode} mode`);
|
|
3087
3151
|
}
|
|
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...");
|
|
3152
|
+
async#addDependencies(app, installSpecs, what) {
|
|
3153
|
+
const spinner = app.workspace.spinning(`Installing ${what}...`);
|
|
3102
3154
|
try {
|
|
3103
|
-
await app.workspace.spawn("bun", ["add", ...installSpecs], {
|
|
3104
|
-
stdio: "inherit"
|
|
3105
|
-
});
|
|
3155
|
+
await app.workspace.spawn("bun", ["add", ...installSpecs], { stdio: "inherit" });
|
|
3106
3156
|
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(", ")}`);
|
|
3157
|
+
spinner.succeed(`Installed ${what}`);
|
|
3129
3158
|
} catch (error) {
|
|
3130
|
-
spinner.fail(`Failed to
|
|
3159
|
+
spinner.fail(`Failed to install ${what}`);
|
|
3131
3160
|
throw error;
|
|
3132
3161
|
}
|
|
3133
3162
|
}
|
|
@@ -3156,17 +3185,6 @@ class ApplicationScript extends script("application", [ApplicationRunner, Librar
|
|
|
3156
3185
|
else
|
|
3157
3186
|
await this.libraryScript.syncLibrary(sys);
|
|
3158
3187
|
}
|
|
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
3188
|
async script(app, filename) {
|
|
3171
3189
|
const scriptFilename = filename ?? await this.applicationRunner.getScriptFilename(app);
|
|
3172
3190
|
await app.scanSync();
|
|
@@ -3184,10 +3202,7 @@ class ApplicationScript extends script("application", [ApplicationRunner, Librar
|
|
|
3184
3202
|
if (!quiet)
|
|
3185
3203
|
Logger6.rawLog(`Creating an optimized production build for ${app.name}...`);
|
|
3186
3204
|
try {
|
|
3187
|
-
const result = await this.applicationRunner.build(app, {
|
|
3188
|
-
fast,
|
|
3189
|
-
spinner: !quiet
|
|
3190
|
-
});
|
|
3205
|
+
const result = await this.applicationRunner.build(app, { fast, spinner: !quiet });
|
|
3191
3206
|
Logger6.rawLog(`${app.name} built in dist/apps/${app.name}`);
|
|
3192
3207
|
if (!quiet)
|
|
3193
3208
|
ApplicationBuildReporter.printSummary(result);
|
|
@@ -3212,8 +3227,7 @@ class ApplicationScript extends script("application", [ApplicationRunner, Librar
|
|
|
3212
3227
|
async test(exec, { write = true } = {}) {
|
|
3213
3228
|
if (exec instanceof LibExecutor) {
|
|
3214
3229
|
await this.libraryScript.syncLibrary(exec);
|
|
3215
|
-
|
|
3216
|
-
spinner.succeed(`${exec.name} prepared`);
|
|
3230
|
+
exec.spinning(`Preparing ${exec.name}...`).succeed(`${exec.name} prepared`);
|
|
3217
3231
|
await this.applicationRunner.test(exec);
|
|
3218
3232
|
return;
|
|
3219
3233
|
}
|
|
@@ -3230,23 +3244,33 @@ class ApplicationScript extends script("application", [ApplicationRunner, Librar
|
|
|
3230
3244
|
}
|
|
3231
3245
|
await this.applicationRunner.test(exec);
|
|
3232
3246
|
}
|
|
3233
|
-
async start(apps, {
|
|
3247
|
+
async start(apps, {
|
|
3248
|
+
open = false,
|
|
3249
|
+
dbup = true,
|
|
3250
|
+
write = true,
|
|
3251
|
+
plain = false,
|
|
3252
|
+
kill = false,
|
|
3253
|
+
share = false,
|
|
3254
|
+
concurrency = null
|
|
3255
|
+
} = {}) {
|
|
3234
3256
|
const first = apps[0];
|
|
3235
3257
|
if (!first)
|
|
3236
3258
|
throw new Error("No app selected to start");
|
|
3237
|
-
const { mode, downgraded } = resolveDevUi(plain);
|
|
3259
|
+
const { mode, downgraded } = DevSupervisor.resolveDevUi(plain);
|
|
3238
3260
|
if (downgraded)
|
|
3239
3261
|
first.workspace.log("no sized terminal to draw on; printing prefixed lines instead");
|
|
3240
3262
|
if (kill)
|
|
3241
3263
|
await this.reclaimDevPorts(apps);
|
|
3264
|
+
const shares = share ? await this.#shareOnInterrupt(apps) : null;
|
|
3242
3265
|
if (mode === "stream" && apps.length === 1)
|
|
3243
3266
|
return await this.startOne(first, { open, dbup, write, ...DevSupervisor.childHooks() });
|
|
3244
|
-
|
|
3267
|
+
this.#interrupt.ownsExit = false;
|
|
3268
|
+
await this.#startMany(apps, { open, dbup, write, mode, concurrency, shares });
|
|
3245
3269
|
}
|
|
3246
3270
|
async startOne(app, { open = false, dbup = true, stdio = "inherit", write = true, onDevEvent } = {}) {
|
|
3247
3271
|
await app.scanSync({ write });
|
|
3248
3272
|
const akanConfig = await app.getConfig();
|
|
3249
|
-
const databaseMode =
|
|
3273
|
+
const databaseMode = akanConfig.resolveDatabaseMode();
|
|
3250
3274
|
await this.syncDatabaseModeDependencies(app, akanConfig, databaseMode);
|
|
3251
3275
|
if (app.getEnv() === "local" && dbup && databaseMode !== "single") {
|
|
3252
3276
|
const wasDbAlreadyUp = await this.dbup(app.workspace, databaseMode);
|
|
@@ -3254,15 +3278,12 @@ class ApplicationScript extends script("application", [ApplicationRunner, Librar
|
|
|
3254
3278
|
this.#stopDatabaseOnInterrupt(app.workspace);
|
|
3255
3279
|
}
|
|
3256
3280
|
const spinner = app.spinning("Preparing backend...");
|
|
3257
|
-
|
|
3281
|
+
return await this.applicationRunner.start(app, {
|
|
3258
3282
|
open,
|
|
3259
|
-
onStart: () => {
|
|
3260
|
-
spinner.succeed(`${app.name} prepared, ready to start`);
|
|
3261
|
-
},
|
|
3283
|
+
onStart: () => spinner.succeed(`${app.name} prepared, ready to start`),
|
|
3262
3284
|
onDevEvent,
|
|
3263
3285
|
stdio
|
|
3264
3286
|
});
|
|
3265
|
-
return akanAppHost;
|
|
3266
3287
|
}
|
|
3267
3288
|
async reclaimDevPorts(apps) {
|
|
3268
3289
|
const workspace = apps[0]?.workspace;
|
|
@@ -3277,13 +3298,20 @@ class ApplicationScript extends script("application", [ApplicationRunner, Librar
|
|
|
3277
3298
|
if (killed.length === 0 && foreign.length === 0)
|
|
3278
3299
|
workspace.log("dev ports are free; nothing to kill");
|
|
3279
3300
|
}
|
|
3280
|
-
async#startMany(apps, {
|
|
3301
|
+
async#startMany(apps, {
|
|
3302
|
+
open,
|
|
3303
|
+
dbup,
|
|
3304
|
+
write,
|
|
3305
|
+
mode,
|
|
3306
|
+
concurrency,
|
|
3307
|
+
shares
|
|
3308
|
+
}) {
|
|
3281
3309
|
const workspace = apps[0]?.workspace;
|
|
3282
3310
|
if (!workspace)
|
|
3283
3311
|
throw new Error("No app selected to start");
|
|
3284
3312
|
const startedDatabase = dbup ? await this.#prepareSharedDatabase(apps) : false;
|
|
3285
|
-
const supervisor = new DevSupervisor({ apps, mode, concurrency, open, write });
|
|
3286
|
-
const view = mode === "tui" ?
|
|
3313
|
+
const supervisor = new DevSupervisor({ apps, mode, concurrency, open, write, shares });
|
|
3314
|
+
const view = mode === "tui" ? new (await import("./devTui-q7k7bakd.js")).DevTui(supervisor) : new DevStreamView(apps.map((app) => app.name));
|
|
3287
3315
|
await supervisor.run(view);
|
|
3288
3316
|
if (startedDatabase)
|
|
3289
3317
|
await this.#stopDatabase(workspace);
|
|
@@ -3297,7 +3325,7 @@ class ApplicationScript extends script("application", [ApplicationRunner, Librar
|
|
|
3297
3325
|
if (app.getEnv() !== "local")
|
|
3298
3326
|
continue;
|
|
3299
3327
|
const akanConfig = await app.getConfig();
|
|
3300
|
-
const mode =
|
|
3328
|
+
const mode = akanConfig.resolveDatabaseMode();
|
|
3301
3329
|
await this.syncDatabaseModeDependencies(app, akanConfig, mode);
|
|
3302
3330
|
if (mode !== "single")
|
|
3303
3331
|
modes.add(mode);
|
|
@@ -3308,82 +3336,121 @@ class ApplicationScript extends script("application", [ApplicationRunner, Librar
|
|
|
3308
3336
|
started = true;
|
|
3309
3337
|
return started;
|
|
3310
3338
|
}
|
|
3311
|
-
async buildIos(app, { write = true,
|
|
3339
|
+
async buildIos(app, { write = true, ...options } = {}) {
|
|
3312
3340
|
await app.scanSync({ write });
|
|
3313
|
-
await this.applicationRunner.
|
|
3341
|
+
await this.applicationRunner.buildMobile(app, "ios", options);
|
|
3314
3342
|
}
|
|
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
|
-
} = {}) {
|
|
3343
|
+
async buildAndroid(app, { write = true, ...options } = {}) {
|
|
3325
3344
|
await app.scanSync({ write });
|
|
3326
|
-
|
|
3327
|
-
|
|
3328
|
-
|
|
3329
|
-
await
|
|
3330
|
-
|
|
3331
|
-
|
|
3332
|
-
|
|
3333
|
-
|
|
3334
|
-
|
|
3335
|
-
|
|
3336
|
-
|
|
3345
|
+
await this.applicationRunner.buildMobile(app, "android", options);
|
|
3346
|
+
}
|
|
3347
|
+
async buildDesktop(app, { write = true, ...options } = {}) {
|
|
3348
|
+
await app.scanSync({ write });
|
|
3349
|
+
await this.applicationRunner.buildDesktop(app, options);
|
|
3350
|
+
}
|
|
3351
|
+
async startIos(app, { write = true, ...options } = {}) {
|
|
3352
|
+
await app.scanSync({ write });
|
|
3353
|
+
await this.applicationRunner.startMobile(app, "ios", options);
|
|
3354
|
+
}
|
|
3355
|
+
async startAndroid(app, { write = true, ...options } = {}) {
|
|
3356
|
+
await app.scanSync({ write });
|
|
3357
|
+
await this.applicationRunner.startMobile(app, "android", options);
|
|
3358
|
+
}
|
|
3359
|
+
async startDesktop(app, { write = true, ...options } = {}) {
|
|
3360
|
+
await app.scanSync({ write });
|
|
3361
|
+
if (options.operation === "release")
|
|
3362
|
+
return await this.applicationRunner.startDesktop(app, options);
|
|
3363
|
+
const mobileTarget = await ApplicationRunner.startTarget(app, undefined, options.target);
|
|
3364
|
+
if (!ApplicationRunner.carriesServer(mobileTarget))
|
|
3365
|
+
return await this.applicationRunner.startDesktop(app, options);
|
|
3366
|
+
DesktopServerStage.assertCarriable(await app.getConfig());
|
|
3367
|
+
const upstream = `http://localhost:${await app.getDevPort()}`;
|
|
3368
|
+
try {
|
|
3369
|
+
if (await ApplicationRunner.answers(upstream, app.name, app.workspace.workspaceRoot))
|
|
3370
|
+
app.log(`The desktop app follows the dev server on ${upstream}.`);
|
|
3371
|
+
else
|
|
3372
|
+
await this.#startDevServerFor(app, upstream);
|
|
3373
|
+
await this.applicationRunner.startDesktop(app, { ...options, interrupt: this.#interrupt });
|
|
3374
|
+
} finally {
|
|
3375
|
+
await this.#interrupt.runAll();
|
|
3376
|
+
}
|
|
3377
|
+
}
|
|
3378
|
+
async#startDevServerFor(app, upstream) {
|
|
3379
|
+
app.log(`No dev server answers on ${upstream}; starting \`akan start ${app.name}\` for the desktop app.`);
|
|
3380
|
+
let settle = () => {};
|
|
3381
|
+
const settled = new Promise((resolve) => settle = resolve);
|
|
3382
|
+
const appHost = await this.startOne(app, {
|
|
3383
|
+
write: false,
|
|
3384
|
+
onDevEvent: (event) => {
|
|
3385
|
+
if ("state" in event && (event.state === "ready" || event.state === "failed"))
|
|
3386
|
+
settle(event.state);
|
|
3387
|
+
}
|
|
3337
3388
|
});
|
|
3389
|
+
this.#interrupt.add(async () => {
|
|
3390
|
+
await appHost.stop();
|
|
3391
|
+
}, "Abandoning the dev server shutdown; its processes may still be running.", 130);
|
|
3392
|
+
let state;
|
|
3393
|
+
const timedOut = await DevSupervisor.timesOut(settled.then((settledState) => state = settledState), ApplicationScript.devServerReadyTimeoutMs);
|
|
3394
|
+
if (state === "ready")
|
|
3395
|
+
return;
|
|
3396
|
+
await this.#interrupt.runAll();
|
|
3397
|
+
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
3398
|
}
|
|
3339
|
-
async releaseIos(app, {
|
|
3399
|
+
async releaseIos(app, {
|
|
3400
|
+
write = true,
|
|
3401
|
+
allowLocalRelease = false,
|
|
3402
|
+
...options
|
|
3403
|
+
} = {}) {
|
|
3340
3404
|
await app.scanSync({ write });
|
|
3341
|
-
|
|
3342
|
-
|
|
3343
|
-
await this.applicationRunner.releaseIos(app, { target, env, regenerate });
|
|
3405
|
+
ApplicationScript.#assertReleaseEnv("releaseIos", options.env ?? "main", allowLocalRelease);
|
|
3406
|
+
await this.applicationRunner.releaseIos(app, options);
|
|
3344
3407
|
}
|
|
3345
|
-
async
|
|
3408
|
+
async releaseAndroid(app, format, {
|
|
3409
|
+
write = true,
|
|
3410
|
+
allowLocalRelease = false,
|
|
3411
|
+
...options
|
|
3412
|
+
} = {}) {
|
|
3346
3413
|
await app.scanSync({ write });
|
|
3347
|
-
|
|
3414
|
+
ApplicationScript.#assertReleaseEnv("releaseAndroid", options.env ?? "main", allowLocalRelease);
|
|
3415
|
+
await this.applicationRunner.releaseAndroid(app, format, options);
|
|
3348
3416
|
}
|
|
3349
|
-
async
|
|
3350
|
-
open = false,
|
|
3351
|
-
operation = "local",
|
|
3352
|
-
env = "local",
|
|
3417
|
+
async packUpdate(app, platform, {
|
|
3353
3418
|
write = true,
|
|
3354
|
-
|
|
3355
|
-
|
|
3419
|
+
allowLocalRelease = false,
|
|
3420
|
+
...options
|
|
3356
3421
|
} = {}) {
|
|
3357
3422
|
await app.scanSync({ write });
|
|
3358
|
-
|
|
3359
|
-
await this.
|
|
3360
|
-
|
|
3361
|
-
|
|
3362
|
-
|
|
3363
|
-
operation,
|
|
3364
|
-
env,
|
|
3365
|
-
target,
|
|
3366
|
-
regenerate
|
|
3367
|
-
});
|
|
3423
|
+
ApplicationScript.#assertReleaseEnv("packUpdate", options.env ?? "main", allowLocalRelease);
|
|
3424
|
+
await this.applicationRunner.packUpdate(app, platform, options);
|
|
3425
|
+
}
|
|
3426
|
+
async updateKeygen(app, platform, { target } = {}) {
|
|
3427
|
+
await this.applicationRunner.updateKeygen(app, platform, target);
|
|
3368
3428
|
}
|
|
3369
|
-
async
|
|
3429
|
+
async publishUpdate(app, platform, {
|
|
3430
|
+
write = true,
|
|
3431
|
+
allowLocalRelease = false,
|
|
3432
|
+
...options
|
|
3433
|
+
} = {}) {
|
|
3370
3434
|
await app.scanSync({ write });
|
|
3371
|
-
|
|
3372
|
-
|
|
3373
|
-
await this.applicationRunner.releaseAndroid(app, assembleType, {
|
|
3374
|
-
target,
|
|
3375
|
-
env,
|
|
3376
|
-
regenerate
|
|
3377
|
-
});
|
|
3435
|
+
ApplicationScript.#assertReleaseEnv("publishUpdate", options.env ?? "main", allowLocalRelease);
|
|
3436
|
+
await this.applicationRunner.publishUpdate(app, platform, options);
|
|
3378
3437
|
}
|
|
3379
|
-
|
|
3380
|
-
|
|
3438
|
+
static #assertReleaseEnv(command, env, allowLocalRelease) {
|
|
3439
|
+
if (env === "local" && !allowLocalRelease)
|
|
3440
|
+
throw new Error(`${command} --env local is blocked. Pass allowLocalRelease only for explicit local release testing.`);
|
|
3381
3441
|
}
|
|
3382
|
-
async
|
|
3383
|
-
await this.applicationRunner.
|
|
3442
|
+
async transferDatabase(app, direction, dir) {
|
|
3443
|
+
await this.applicationRunner.transferDatabase(app, direction, dir);
|
|
3384
3444
|
}
|
|
3385
|
-
async
|
|
3386
|
-
await
|
|
3445
|
+
async dbupDeclared(workspace, mode) {
|
|
3446
|
+
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())]);
|
|
3447
|
+
const needing = declared.filter((each) => each !== "single");
|
|
3448
|
+
if (!needing.length) {
|
|
3449
|
+
workspace.log("No app here declares a database mode that runs on local services; single needs none.");
|
|
3450
|
+
return;
|
|
3451
|
+
}
|
|
3452
|
+
for (const each of needing)
|
|
3453
|
+
await this.dbup(workspace, each);
|
|
3387
3454
|
}
|
|
3388
3455
|
async dbup(workspace, mode = "multiple") {
|
|
3389
3456
|
const spinner = workspace.spinning(`Starting local database (${mode})...`);
|
|
@@ -3401,34 +3468,36 @@ class ApplicationScript extends script("application", [ApplicationRunner, Librar
|
|
|
3401
3468
|
}
|
|
3402
3469
|
spinner.succeed("Local database (/local/docker-compose.yaml) is down");
|
|
3403
3470
|
}
|
|
3404
|
-
#
|
|
3405
|
-
|
|
3406
|
-
|
|
3407
|
-
|
|
3408
|
-
|
|
3409
|
-
|
|
3410
|
-
|
|
3411
|
-
|
|
3412
|
-
|
|
3471
|
+
async#shareOnInterrupt(apps) {
|
|
3472
|
+
const shares = [];
|
|
3473
|
+
const urls = new Map;
|
|
3474
|
+
this.#interrupt.add(async () => {
|
|
3475
|
+
await Promise.all(shares.map(async (share) => await share.close()));
|
|
3476
|
+
}, "Abandoning the tunnel release; the shares expire on their own.");
|
|
3477
|
+
await Promise.all(apps.map(async (app) => {
|
|
3478
|
+
const { TunnelShare } = await import("./TunnelShare-cwh9ezsm.js");
|
|
3479
|
+
const share = await TunnelShare.open(app, { workspace: app.workspace });
|
|
3480
|
+
shares.push(share);
|
|
3481
|
+
urls.set(app.name, share.url);
|
|
3482
|
+
Logger6.rawLog(`${app.name} is shared at ${share.url}`);
|
|
3483
|
+
})).catch((error) => {
|
|
3484
|
+
Logger6.rawLog(`Could not open the tunnel: ${error instanceof Error ? error.message : String(error)}`, undefined, "error");
|
|
3413
3485
|
});
|
|
3486
|
+
return urls;
|
|
3487
|
+
}
|
|
3488
|
+
#stopDatabaseOnInterrupt(workspace) {
|
|
3489
|
+
this.#interrupt.add(async () => {
|
|
3490
|
+
await this.#stopDatabase(workspace);
|
|
3491
|
+
}, "Abandoning the local database teardown; containers are left running.");
|
|
3414
3492
|
}
|
|
3415
3493
|
async#stopDatabase(workspace) {
|
|
3416
|
-
|
|
3417
|
-
|
|
3418
|
-
timer = setTimeout(() => resolve("timeout"), ApplicationScript.dbShutdownTimeoutMs);
|
|
3494
|
+
const stopped = this.dbdown(workspace).catch(() => {
|
|
3495
|
+
return;
|
|
3419
3496
|
});
|
|
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
|
-
}
|
|
3497
|
+
if (!await DevSupervisor.timesOut(stopped, ApplicationScript.dbShutdownTimeoutMs))
|
|
3498
|
+
return;
|
|
3499
|
+
Logger6.rawLog(`Local database did not stop within ${ApplicationScript.dbShutdownTimeoutMs}ms; run \`akan dbdown\` once Docker responds.`, undefined, "error");
|
|
3431
3500
|
}
|
|
3432
3501
|
}
|
|
3433
3502
|
|
|
3434
|
-
export {
|
|
3503
|
+
export { ApplicationScript };
|