@akanjs/cli 2.4.0-rc.9 → 2.4.1-rc.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/agent.command-h4afc69n.js +26 -0
- package/application.command-4ctkfdan.js +165 -0
- package/applicationBuildRunner-esa1kj7v.js +284 -0
- package/applicationReleasePackager-brvth6rs.js +245 -0
- package/capacitorApp-y0h6cgft.js +58 -0
- package/cloud.command-rpztn4fh.js +94 -0
- package/commandManifest.json +1 -0
- package/context.command-nqbtak4f.js +82 -0
- package/dependencyScanner-m4x5maek.js +9 -0
- package/guideline.command-ya0dh44f.js +356 -0
- package/incrementalBuilder.proc.js +89 -17394
- package/index-1xdrsbry.js +1447 -0
- package/index-45aj5ry0.js +990 -0
- package/index-5vvwc0cz.js +559 -0
- package/index-61keag0s.js +40 -0
- package/index-6pz1j0zj.js +62 -0
- package/index-73pr2cmy.js +534 -0
- package/index-76rn3g2c.js +76 -0
- package/index-85msc0wg.js +161 -0
- package/index-8pkbzj26.js +840 -0
- package/index-8rc0bm04.js +514 -0
- package/index-9sp6fsc5.js +1884 -0
- package/index-a6sbyy0b.js +2769 -0
- package/index-b0brjbp3.js +83 -0
- package/index-fgc8r6dj.js +33 -0
- package/index-h6ca6qg0.js +2777 -0
- package/index-hdqztm58.js +758 -0
- package/index-hwzpw9c1.js +202 -0
- package/index-pmm9e2jf.js +120 -0
- package/index-qaq13qk3.js +80 -0
- package/index-qhtr07v8.js +1072 -0
- package/index-r24hmh0q.js +4 -0
- package/index-ss469dec.js +11 -0
- package/index-swf4bmbg.js +25 -0
- package/index-w7fyqjrw.js +462 -0
- package/index-wnp7hwq7.js +193 -0
- package/index-wq8jwx8z.js +224 -0
- package/index-x53a5nya.js +301 -0
- package/index-xmc2w32q.js +4359 -0
- package/index-y3hdhy4p.js +229 -0
- package/index.js +54 -23340
- package/library.command-r15zdqvp.js +33 -0
- package/localRegistry.command-5tcahs3f.js +178 -0
- package/module.command-qrj3kmyz.js +54 -0
- package/package.command-r8sq5kzp.js +43 -0
- package/package.json +2 -2
- package/page.command-c6xdx0xm.js +24 -0
- package/primitive.command-pv9ssmtf.js +62 -0
- package/quality.command-es67wvdp.js +809 -0
- package/repair.command-677675vw.js +67 -0
- package/routeSourceValidator-wbhmbwpj.js +132 -0
- package/scalar.command-kabkd6wd.js +35 -0
- package/templates/app/page/_layout.tsx +19 -1
- package/templates/app/public/fonts/Pretendard-Black.woff2 +0 -0
- package/templates/app/public/fonts/Pretendard-Bold.woff2 +0 -0
- package/templates/app/public/fonts/Pretendard-ExtraBold.woff2 +0 -0
- package/templates/app/public/fonts/Pretendard-ExtraLight.woff2 +0 -0
- package/templates/app/public/fonts/Pretendard-Light.woff2 +0 -0
- package/templates/app/public/fonts/Pretendard-Medium.woff2 +0 -0
- package/templates/app/public/fonts/Pretendard-Regular.woff2 +0 -0
- package/templates/app/public/fonts/Pretendard-SemiBold.woff2 +0 -0
- package/templates/app/public/fonts/Pretendard-Thin.woff2 +0 -0
- package/templates/facetIndex/index.ts +1 -1
- package/typeChecker-kravn7ns.js +8 -0
- package/typecheck.proc.js +4 -194
- package/workflow.command-64r6cw0w.js +108 -0
- package/workspace.command-vrws0rgx.js +435 -0
- package/README.ko.md +0 -72
- package/README.md +0 -85
|
@@ -0,0 +1,1884 @@
|
|
|
1
|
+
// @bun
|
|
2
|
+
import {
|
|
3
|
+
resolveMobileTargets
|
|
4
|
+
} from "./index-76rn3g2c.js";
|
|
5
|
+
import {
|
|
6
|
+
LibraryScript
|
|
7
|
+
} from "./index-pmm9e2jf.js";
|
|
8
|
+
import {
|
|
9
|
+
runner,
|
|
10
|
+
script
|
|
11
|
+
} from "./index-hdqztm58.js";
|
|
12
|
+
import {
|
|
13
|
+
AppExecutor,
|
|
14
|
+
LibExecutor,
|
|
15
|
+
PkgExecutor,
|
|
16
|
+
WorkspaceExecutor
|
|
17
|
+
} from "./index-a6sbyy0b.js";
|
|
18
|
+
import {
|
|
19
|
+
__require
|
|
20
|
+
} from "./index-r24hmh0q.js";
|
|
21
|
+
|
|
22
|
+
// pkgs/@akanjs/devkit/applicationBuildReporter.ts
|
|
23
|
+
import { Logger } from "akanjs/common";
|
|
24
|
+
|
|
25
|
+
class ApplicationBuildReporter {
|
|
26
|
+
static create() {
|
|
27
|
+
return {
|
|
28
|
+
phaseDone: (phase) => Logger.rawLog(ApplicationBuildReporter.formatPhaseLine(phase))
|
|
29
|
+
};
|
|
30
|
+
}
|
|
31
|
+
static printSummary(result) {
|
|
32
|
+
Logger.rawLog("");
|
|
33
|
+
Logger.rawLog(`Route artifacts: ${result.artifactDir}`);
|
|
34
|
+
Logger.rawLog(`Server output: ${result.outputDir}`);
|
|
35
|
+
Logger.rawLog(`Done in ${ApplicationBuildReporter.formatDuration(result.durationMs)}`);
|
|
36
|
+
}
|
|
37
|
+
static formatError(error) {
|
|
38
|
+
if (error instanceof AggregateError) {
|
|
39
|
+
const nestedMessages = error.errors.map((nestedError, index) => ApplicationBuildReporter.formatError(nestedError).trim() || `Unknown error ${index + 1}`).map((message) => message.replace(/^/gm, " ")).join(`
|
|
40
|
+
`);
|
|
41
|
+
return nestedMessages ? `${error.message}
|
|
42
|
+
${nestedMessages}` : error.message;
|
|
43
|
+
}
|
|
44
|
+
if (error instanceof Error) {
|
|
45
|
+
const causeMessage = error.cause ? `
|
|
46
|
+
Caused by: ${ApplicationBuildReporter.formatError(error.cause)}` : "";
|
|
47
|
+
return `${error.message}${causeMessage}`;
|
|
48
|
+
}
|
|
49
|
+
if (typeof error === "object" && error !== null && "message" in error)
|
|
50
|
+
return String(error.message);
|
|
51
|
+
return String(error);
|
|
52
|
+
}
|
|
53
|
+
static formatDuration(ms) {
|
|
54
|
+
if (ms < 1000)
|
|
55
|
+
return `${ms}ms`;
|
|
56
|
+
if (ms < 60000)
|
|
57
|
+
return `${Math.round(ms / 100) / 10}s`;
|
|
58
|
+
const seconds = Math.floor(ms / 1000);
|
|
59
|
+
return `${Math.floor(seconds / 60)}m ${seconds % 60}s`;
|
|
60
|
+
}
|
|
61
|
+
static formatPhaseLine(phase) {
|
|
62
|
+
const summary = phase.summary ? `: ${phase.summary}` : "";
|
|
63
|
+
return `\u2713 ${phase.label}${summary} (${ApplicationBuildReporter.formatDuration(phase.durationMs)})`;
|
|
64
|
+
}
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
// pkgs/@akanjs/cli/application/application.script.ts
|
|
68
|
+
import { confirm } from "@inquirer/prompts";
|
|
69
|
+
import { Logger as Logger5 } from "akanjs/common";
|
|
70
|
+
|
|
71
|
+
// pkgs/@akanjs/devkit/akanApp/akanApp.host.ts
|
|
72
|
+
import path2 from "path";
|
|
73
|
+
import { Logger as Logger3 } from "akanjs/common";
|
|
74
|
+
|
|
75
|
+
// pkgs/@akanjs/devkit/sshTunnel.ts
|
|
76
|
+
import { Client } from "ssh2";
|
|
77
|
+
var closeQuietly = (close) => {
|
|
78
|
+
try {
|
|
79
|
+
close();
|
|
80
|
+
} catch {}
|
|
81
|
+
};
|
|
82
|
+
var createSshClient = async (options) => new Promise((resolve, reject) => {
|
|
83
|
+
const client = new Client;
|
|
84
|
+
const cleanup = () => {
|
|
85
|
+
client.off("ready", onReady);
|
|
86
|
+
client.off("error", onError);
|
|
87
|
+
client.off("close", onClose);
|
|
88
|
+
};
|
|
89
|
+
const onReady = () => {
|
|
90
|
+
cleanup();
|
|
91
|
+
resolve(client);
|
|
92
|
+
};
|
|
93
|
+
const onError = (error) => {
|
|
94
|
+
cleanup();
|
|
95
|
+
reject(error);
|
|
96
|
+
};
|
|
97
|
+
const onClose = () => {
|
|
98
|
+
cleanup();
|
|
99
|
+
reject(new Error("SSH tunnel closed before it was ready"));
|
|
100
|
+
};
|
|
101
|
+
client.once("ready", onReady);
|
|
102
|
+
client.once("error", onError);
|
|
103
|
+
client.once("close", onClose);
|
|
104
|
+
client.connect({ readyTimeout: 1e4, ...options.sshOptions });
|
|
105
|
+
});
|
|
106
|
+
var createSshTunnel = async (options) => {
|
|
107
|
+
const client = await createSshClient(options);
|
|
108
|
+
const sockets = new Set;
|
|
109
|
+
let closing = false;
|
|
110
|
+
const closeSocket = (socket, error) => {
|
|
111
|
+
sockets.delete(socket);
|
|
112
|
+
closeBunSocket(socket, error);
|
|
113
|
+
};
|
|
114
|
+
try {
|
|
115
|
+
const server = Bun.listen({
|
|
116
|
+
hostname: options.localHost ?? "127.0.0.1",
|
|
117
|
+
port: options.localPort,
|
|
118
|
+
socket: {
|
|
119
|
+
open(socket) {
|
|
120
|
+
sockets.add(socket);
|
|
121
|
+
socket.data = { pending: [], closed: false };
|
|
122
|
+
client.forwardOut(options.srcHost ?? "127.0.0.1", options.srcPort ?? options.localPort, options.dstHost, options.dstPort, (error, stream) => {
|
|
123
|
+
if (error) {
|
|
124
|
+
closeSocket(socket, error);
|
|
125
|
+
return;
|
|
126
|
+
}
|
|
127
|
+
if (socket.data.closed) {
|
|
128
|
+
stream.destroy();
|
|
129
|
+
return;
|
|
130
|
+
}
|
|
131
|
+
socket.data.stream = stream;
|
|
132
|
+
stream.on("data", (chunk) => socket.write(chunk));
|
|
133
|
+
stream.once("close", () => closeSocket(socket));
|
|
134
|
+
stream.once("error", (streamError) => closeSocket(socket, streamError));
|
|
135
|
+
for (const chunk of socket.data.pending)
|
|
136
|
+
stream.write(chunk);
|
|
137
|
+
socket.data.pending = [];
|
|
138
|
+
});
|
|
139
|
+
},
|
|
140
|
+
data(socket, chunk) {
|
|
141
|
+
const buffer = Buffer.from(chunk);
|
|
142
|
+
if (socket.data.stream)
|
|
143
|
+
socket.data.stream.write(buffer);
|
|
144
|
+
else
|
|
145
|
+
socket.data.pending.push(buffer);
|
|
146
|
+
},
|
|
147
|
+
close(socket) {
|
|
148
|
+
closeSocket(socket);
|
|
149
|
+
},
|
|
150
|
+
error(socket, error) {
|
|
151
|
+
closeSocket(socket, error);
|
|
152
|
+
}
|
|
153
|
+
}
|
|
154
|
+
});
|
|
155
|
+
const closeTunnel = (error) => {
|
|
156
|
+
if (closing)
|
|
157
|
+
return;
|
|
158
|
+
closing = true;
|
|
159
|
+
for (const socket of sockets)
|
|
160
|
+
closeSocket(socket, error);
|
|
161
|
+
closeQuietly(() => client.end());
|
|
162
|
+
closeBunServer(server);
|
|
163
|
+
};
|
|
164
|
+
client.once("error", (error) => closeTunnel(error));
|
|
165
|
+
client.once("close", () => closeTunnel());
|
|
166
|
+
return { close: closeTunnel };
|
|
167
|
+
} catch (error) {
|
|
168
|
+
closeQuietly(() => client.end());
|
|
169
|
+
throw error;
|
|
170
|
+
}
|
|
171
|
+
};
|
|
172
|
+
var closeBunSocket = (socket, error) => {
|
|
173
|
+
if (socket.data.closed)
|
|
174
|
+
return;
|
|
175
|
+
socket.data.closed = true;
|
|
176
|
+
if (error)
|
|
177
|
+
socket.data.stream?.destroy();
|
|
178
|
+
else
|
|
179
|
+
socket.data.stream?.end();
|
|
180
|
+
closeQuietly(() => socket.end());
|
|
181
|
+
};
|
|
182
|
+
var closeBunServer = (server) => {
|
|
183
|
+
server.stop();
|
|
184
|
+
};
|
|
185
|
+
|
|
186
|
+
// pkgs/@akanjs/devkit/createTunnel.ts
|
|
187
|
+
var getSshTunnelOptions = (app, environment) => {
|
|
188
|
+
const { serveDomain, repoName } = WorkspaceExecutor.getBaseDevEnv();
|
|
189
|
+
return {
|
|
190
|
+
host: `${app.name}-${environment}.${serveDomain}`,
|
|
191
|
+
port: process.env.SSH_TUNNEL_PORT ? parseInt(process.env.SSH_TUNNEL_PORT) : 32767,
|
|
192
|
+
username: process.env.SSH_TUNNEL_USERNAME ?? "root",
|
|
193
|
+
password: process.env.SSH_TUNNEL_PASSWORD ?? repoName
|
|
194
|
+
};
|
|
195
|
+
};
|
|
196
|
+
var createTunnel = async (service, { app, environment, port = service === "postgres" ? 5432 : 6379 }) => {
|
|
197
|
+
const sshOptions = getSshTunnelOptions(app, environment);
|
|
198
|
+
await createSshTunnel({
|
|
199
|
+
localHost: "0.0.0.0",
|
|
200
|
+
localPort: port,
|
|
201
|
+
srcHost: "0.0.0.0",
|
|
202
|
+
srcPort: port,
|
|
203
|
+
dstHost: `${service}-0.${service}-svc.${app.name}-${environment}.svc.cluster.local`,
|
|
204
|
+
dstPort: service === "postgres" ? 5432 : 6379,
|
|
205
|
+
sshOptions
|
|
206
|
+
});
|
|
207
|
+
return `localhost:${port}`;
|
|
208
|
+
};
|
|
209
|
+
// pkgs/@akanjs/devkit/incrementalBuilder/incrementalBuilder.host.ts
|
|
210
|
+
import path from "path";
|
|
211
|
+
import { Logger as Logger2 } from "akanjs/common";
|
|
212
|
+
var builderMsgTypeSet = new Set([
|
|
213
|
+
"build-route-res",
|
|
214
|
+
"build-csr-res",
|
|
215
|
+
"builder-ready",
|
|
216
|
+
"invalidate",
|
|
217
|
+
"css-updated",
|
|
218
|
+
"pages-updated",
|
|
219
|
+
"build-status"
|
|
220
|
+
]);
|
|
221
|
+
|
|
222
|
+
class IncrementalBuilderHost {
|
|
223
|
+
static #restartBaseDelayMs = 1000;
|
|
224
|
+
static #restartMaxDelayMs = 30000;
|
|
225
|
+
logger = new Logger2("IncrementalBuilderHost");
|
|
226
|
+
entry;
|
|
227
|
+
env;
|
|
228
|
+
app;
|
|
229
|
+
ready = false;
|
|
230
|
+
#onMessage;
|
|
231
|
+
#proc = null;
|
|
232
|
+
#status = "stopped";
|
|
233
|
+
#restartAttempts = 0;
|
|
234
|
+
#restartTimer = null;
|
|
235
|
+
#manualStop = false;
|
|
236
|
+
#startOptions = {};
|
|
237
|
+
constructor({ app, entry, env, onMessage }) {
|
|
238
|
+
this.app = app;
|
|
239
|
+
this.entry = entry;
|
|
240
|
+
this.env = env;
|
|
241
|
+
this.#onMessage = onMessage;
|
|
242
|
+
}
|
|
243
|
+
get status() {
|
|
244
|
+
return this.#status;
|
|
245
|
+
}
|
|
246
|
+
start(options = {}) {
|
|
247
|
+
if (this.#proc)
|
|
248
|
+
this.stop();
|
|
249
|
+
this.#manualStop = false;
|
|
250
|
+
this.#startOptions = options;
|
|
251
|
+
this.#spawn(false);
|
|
252
|
+
return this;
|
|
253
|
+
}
|
|
254
|
+
#spawn(isRestart) {
|
|
255
|
+
this.#status = isRestart ? "restarting" : "starting";
|
|
256
|
+
this.ready = false;
|
|
257
|
+
let proc;
|
|
258
|
+
proc = Bun.spawn(["bun", this.entry], {
|
|
259
|
+
cwd: this.app.cwdPath,
|
|
260
|
+
env: { ...this.env, AKAN_WATCH: "1" },
|
|
261
|
+
stdio: ["ignore", "inherit", "inherit"],
|
|
262
|
+
ipc: (msg) => {
|
|
263
|
+
if (this.#proc !== proc)
|
|
264
|
+
return;
|
|
265
|
+
if (!msg || typeof msg !== "object")
|
|
266
|
+
return;
|
|
267
|
+
if (builderMsgTypeSet.has(msg.type))
|
|
268
|
+
this.#onMessage(msg);
|
|
269
|
+
if (msg.type === "builder-ready" && !this.ready) {
|
|
270
|
+
this.ready = true;
|
|
271
|
+
this.#status = "ready";
|
|
272
|
+
this.#restartAttempts = 0;
|
|
273
|
+
if (isRestart)
|
|
274
|
+
this.#startOptions.onRestartReady?.();
|
|
275
|
+
else
|
|
276
|
+
this.#startOptions.onReady?.();
|
|
277
|
+
}
|
|
278
|
+
},
|
|
279
|
+
serialization: "advanced",
|
|
280
|
+
onExit: () => {
|
|
281
|
+
if (this.#proc !== proc)
|
|
282
|
+
return;
|
|
283
|
+
this.#proc = null;
|
|
284
|
+
const wasReady = this.ready;
|
|
285
|
+
this.ready = false;
|
|
286
|
+
if (this.#manualStop || this.#status === "stopped")
|
|
287
|
+
return;
|
|
288
|
+
if (!wasReady) {
|
|
289
|
+
this.#status = "stopped";
|
|
290
|
+
this.#startOptions.onExit?.();
|
|
291
|
+
return;
|
|
292
|
+
}
|
|
293
|
+
this.#scheduleRestart();
|
|
294
|
+
}
|
|
295
|
+
});
|
|
296
|
+
this.#proc = proc;
|
|
297
|
+
this.logger.verbose(`builder spawned pid=${proc.pid} entry=${this.entry}${isRestart ? " restart=1" : ""}`);
|
|
298
|
+
}
|
|
299
|
+
#scheduleRestart() {
|
|
300
|
+
if (this.#manualStop || this.#restartTimer)
|
|
301
|
+
return;
|
|
302
|
+
this.#status = "restarting";
|
|
303
|
+
const attempt = this.#restartAttempts;
|
|
304
|
+
const delay = Math.min(IncrementalBuilderHost.#restartBaseDelayMs * 2 ** attempt, IncrementalBuilderHost.#restartMaxDelayMs);
|
|
305
|
+
this.#restartAttempts = attempt + 1;
|
|
306
|
+
this.logger.warn(`builder exited after ready; restarting in ${delay}ms (attempt ${this.#restartAttempts})`);
|
|
307
|
+
this.#restartTimer = setTimeout(() => {
|
|
308
|
+
this.#restartTimer = null;
|
|
309
|
+
if (this.#manualStop)
|
|
310
|
+
return;
|
|
311
|
+
this.#spawn(true);
|
|
312
|
+
}, delay);
|
|
313
|
+
}
|
|
314
|
+
send(message) {
|
|
315
|
+
if (!this.#proc || this.#status !== "ready") {
|
|
316
|
+
this.logger.warn(`incrementalBuilderHost is ${this.#status}; cannot send ${message.type}`);
|
|
317
|
+
return false;
|
|
318
|
+
}
|
|
319
|
+
try {
|
|
320
|
+
this.#proc.send(message);
|
|
321
|
+
return true;
|
|
322
|
+
} catch (error) {
|
|
323
|
+
this.logger.warn(`failed to send ${message.type} to builder: ${error instanceof Error ? error.message : String(error)}`);
|
|
324
|
+
return false;
|
|
325
|
+
}
|
|
326
|
+
}
|
|
327
|
+
stop() {
|
|
328
|
+
this.#manualStop = true;
|
|
329
|
+
if (this.#restartTimer) {
|
|
330
|
+
clearTimeout(this.#restartTimer);
|
|
331
|
+
this.#restartTimer = null;
|
|
332
|
+
}
|
|
333
|
+
if (this.#proc)
|
|
334
|
+
this.#proc.kill();
|
|
335
|
+
this.#proc = null;
|
|
336
|
+
this.ready = false;
|
|
337
|
+
this.#status = "stopped";
|
|
338
|
+
}
|
|
339
|
+
static async create(app, env, onMessage) {
|
|
340
|
+
const candidates = [
|
|
341
|
+
path.join(app.workspace.workspaceRoot, "pkgs/@akanjs/devkit/incrementalBuilder/incrementalBuilder.proc.ts"),
|
|
342
|
+
path.join(app.workspace.workspaceRoot, "node_modules/@akanjs/devkit/incrementalBuilder/incrementalBuilder.proc.ts"),
|
|
343
|
+
path.join(import.meta.dir, "incrementalBuilder.proc.js"),
|
|
344
|
+
path.join(import.meta.dir, "incrementalBuilder.proc.ts")
|
|
345
|
+
];
|
|
346
|
+
for (const c of candidates)
|
|
347
|
+
if (await Bun.file(c).exists())
|
|
348
|
+
return new IncrementalBuilderHost({ app, entry: c, env, onMessage });
|
|
349
|
+
throw new Error(`[cli] frontend builder entry not found; looked in: ${candidates.join(", ")}`);
|
|
350
|
+
}
|
|
351
|
+
}
|
|
352
|
+
// pkgs/@akanjs/devkit/akanApp/akanApp.host.ts
|
|
353
|
+
var backendMsgTypeSet = new Set(["build-route", "build-csr"]);
|
|
354
|
+
var BACKEND_RESTART_DEBOUNCE_MS = 120;
|
|
355
|
+
var BACKEND_GRACEFUL_TIMEOUT_MS = 8000;
|
|
356
|
+
var BACKEND_RECOVERY_BASE_DELAY_MS = 1000;
|
|
357
|
+
var BACKEND_RECOVERY_MAX_DELAY_MS = 30000;
|
|
358
|
+
var BACKEND_RECOVERY_MAX_ATTEMPTS = 5;
|
|
359
|
+
var BACKEND_STDERR_TAIL_LIMIT = 40;
|
|
360
|
+
var BUILDER_READY_TIMEOUT_MS = 150000;
|
|
361
|
+
var BUILDER_START_MAX_ATTEMPTS = 3;
|
|
362
|
+
var BUILDER_RECOVERY_BASE_DELAY_MS = 2000;
|
|
363
|
+
var BUILDER_RECOVERY_MAX_DELAY_MS = 60000;
|
|
364
|
+
var SOURCE_EXTS = new Set([".ts", ".tsx", ".js", ".jsx", ".mjs", ".cjs"]);
|
|
365
|
+
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;
|
|
366
|
+
var SERVER_SUFFIXES = [".service.ts", ".document.ts"];
|
|
367
|
+
var SHARED_SUFFIXES = [".constant.ts", ".dictionary.ts", ".signal.ts"];
|
|
368
|
+
var RUNTIME_METADATA_BASENAMES = new Set(["dict.ts", "sig.ts", "useClient.ts", "useServer.ts"]);
|
|
369
|
+
var GRAPH_IMPORT_KINDS = new Set([
|
|
370
|
+
"import-statement",
|
|
371
|
+
"require-call",
|
|
372
|
+
"require-resolve",
|
|
373
|
+
"dynamic-import"
|
|
374
|
+
]);
|
|
375
|
+
var shouldRestartBackendByDevPlan = (message) => {
|
|
376
|
+
if (!message.devPlan)
|
|
377
|
+
return null;
|
|
378
|
+
if (message.devPlan.actions.includes("report-error"))
|
|
379
|
+
return false;
|
|
380
|
+
if (message.devPlan.actions.includes("restart-builder"))
|
|
381
|
+
return false;
|
|
382
|
+
return message.devPlan.actions.includes("restart-backend");
|
|
383
|
+
};
|
|
384
|
+
var shouldRestartBuilderByDevPlan = (message) => message.devPlan?.actions.includes("restart-builder") ?? false;
|
|
385
|
+
var shouldAbandonBackendRecovery = (attempts, maxAttempts = BACKEND_RECOVERY_MAX_ATTEMPTS) => attempts >= maxAttempts;
|
|
386
|
+
var normalizeBackendReportedGeneration = (generation) => generation >= 0 ? generation : undefined;
|
|
387
|
+
var shouldRestartDevHostByDevPlan = (message) => message.devPlan?.actions.includes("restart-dev-host") ?? message.kinds.includes("config");
|
|
388
|
+
var RESTART_ROLE_ORDER = ["server", "shared", "barrel", "config"];
|
|
389
|
+
var generationValue = (generation) => generation ?? -1;
|
|
390
|
+
var isLegacyBackendFallbackFile = (file, workspaceRoot) => {
|
|
391
|
+
const abs = path2.resolve(file);
|
|
392
|
+
const ext = path2.extname(abs).toLowerCase();
|
|
393
|
+
if (!SOURCE_EXTS.has(ext))
|
|
394
|
+
return false;
|
|
395
|
+
const rel = path2.relative(path2.resolve(workspaceRoot), abs);
|
|
396
|
+
if (!rel || rel.startsWith("..") || path2.isAbsolute(rel))
|
|
397
|
+
return false;
|
|
398
|
+
const parts = rel.split(path2.sep).filter(Boolean);
|
|
399
|
+
const [scope] = parts;
|
|
400
|
+
if (scope !== "apps" && scope !== "libs" && scope !== "pkgs")
|
|
401
|
+
return false;
|
|
402
|
+
const base = path2.basename(abs);
|
|
403
|
+
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";
|
|
404
|
+
};
|
|
405
|
+
var shouldMarkBuildPhaseRecovered = (previousByPhase, status) => {
|
|
406
|
+
const previous = previousByPhase.get(status.phase);
|
|
407
|
+
return Boolean(previous && status.ok && !previous.ok && generationValue(status.generation) >= previous.generation);
|
|
408
|
+
};
|
|
409
|
+
var createBackendBuildStatus = ({
|
|
410
|
+
generation,
|
|
411
|
+
ok,
|
|
412
|
+
files = [],
|
|
413
|
+
message
|
|
414
|
+
}) => ({
|
|
415
|
+
generation,
|
|
416
|
+
phase: "backend",
|
|
417
|
+
ok,
|
|
418
|
+
files,
|
|
419
|
+
message
|
|
420
|
+
});
|
|
421
|
+
var backendRestartReasonFromMessage = (message) => {
|
|
422
|
+
const roleSet = new Set;
|
|
423
|
+
for (const role of message.devPlan?.roles ?? []) {
|
|
424
|
+
if (role === "server" || role === "shared" || role === "barrel" || role === "config")
|
|
425
|
+
roleSet.add(role);
|
|
426
|
+
}
|
|
427
|
+
return {
|
|
428
|
+
generation: message.devPlan?.generation ?? message.generation,
|
|
429
|
+
files: [...new Set(message.files)].sort(),
|
|
430
|
+
roles: RESTART_ROLE_ORDER.filter((role) => roleSet.has(role))
|
|
431
|
+
};
|
|
432
|
+
};
|
|
433
|
+
var mergeBackendRestartReasons = (current, next) => ({
|
|
434
|
+
generation: generationValue(next.generation) >= generationValue(current?.generation) ? next.generation : current?.generation,
|
|
435
|
+
files: [...new Set([...current?.files ?? [], ...next.files])].sort(),
|
|
436
|
+
roles: RESTART_ROLE_ORDER.filter((role) => current?.roles.includes(role) || next.roles.includes(role))
|
|
437
|
+
});
|
|
438
|
+
var shouldReplaceLastGoodMessage = (current, next) => !current || generationValue(next.data.generation) >= generationValue(current.data.generation);
|
|
439
|
+
var shouldQueueBuildStatusReplay = (backendReady, pendingReplayCount) => !backendReady || pendingReplayCount > 0;
|
|
440
|
+
var hasBuildFailureForGeneration = (statusByPhase, generation) => {
|
|
441
|
+
if (typeof generation !== "number")
|
|
442
|
+
return false;
|
|
443
|
+
for (const status of statusByPhase.values()) {
|
|
444
|
+
if (!status.ok && status.generation === generation)
|
|
445
|
+
return true;
|
|
446
|
+
}
|
|
447
|
+
return false;
|
|
448
|
+
};
|
|
449
|
+
var mergeDevPlans = (current, next) => {
|
|
450
|
+
if (!current)
|
|
451
|
+
return next;
|
|
452
|
+
if (!next)
|
|
453
|
+
return current;
|
|
454
|
+
const reasonByFile = { ...current.reasonByFile };
|
|
455
|
+
for (const [file, reasons] of Object.entries(next.reasonByFile)) {
|
|
456
|
+
reasonByFile[file] = [...new Set([...reasonByFile[file] ?? [], ...reasons])].sort();
|
|
457
|
+
}
|
|
458
|
+
return {
|
|
459
|
+
generation: Math.max(current.generation, next.generation),
|
|
460
|
+
files: [...new Set([...current.files, ...next.files])].sort(),
|
|
461
|
+
generatedFiles: [...new Set([...current.generatedFiles, ...next.generatedFiles])].sort(),
|
|
462
|
+
roles: [...new Set([...current.roles, ...next.roles])].sort(),
|
|
463
|
+
actions: [...new Set([...current.actions, ...next.actions])].sort(),
|
|
464
|
+
reasonByFile
|
|
465
|
+
};
|
|
466
|
+
};
|
|
467
|
+
var mergeInvalidateMessages = (current, next) => {
|
|
468
|
+
const generation = Math.max(generationValue(current.generation), generationValue(next.generation));
|
|
469
|
+
return {
|
|
470
|
+
type: "invalidate",
|
|
471
|
+
kinds: [...new Set([...current.kinds, ...next.kinds])].sort(),
|
|
472
|
+
files: [...new Set([...current.files, ...next.files])].sort(),
|
|
473
|
+
generation: generation >= 0 ? generation : undefined,
|
|
474
|
+
devPlan: mergeDevPlans(current.devPlan, next.devPlan)
|
|
475
|
+
};
|
|
476
|
+
};
|
|
477
|
+
var buildStatusReplaySequence = (pendingReplay, latestByPhase) => [...pendingReplay, ...latestByPhase.values()];
|
|
478
|
+
|
|
479
|
+
class BackendImportGraph {
|
|
480
|
+
#app;
|
|
481
|
+
#logger;
|
|
482
|
+
#tsTranspiler = new Bun.Transpiler({ loader: "ts" });
|
|
483
|
+
#tsxTranspiler = new Bun.Transpiler({ loader: "tsx" });
|
|
484
|
+
#jsTranspiler = new Bun.Transpiler({ loader: "js" });
|
|
485
|
+
#jsxTranspiler = new Bun.Transpiler({ loader: "jsx" });
|
|
486
|
+
#files = new Set;
|
|
487
|
+
#ready = false;
|
|
488
|
+
#lastRefreshSucceeded = false;
|
|
489
|
+
constructor(app, logger) {
|
|
490
|
+
this.#app = app;
|
|
491
|
+
this.#logger = logger;
|
|
492
|
+
}
|
|
493
|
+
get ready() {
|
|
494
|
+
return this.#ready;
|
|
495
|
+
}
|
|
496
|
+
get lastRefreshSucceeded() {
|
|
497
|
+
return this.#lastRefreshSucceeded;
|
|
498
|
+
}
|
|
499
|
+
has(file) {
|
|
500
|
+
return this.#files.has(path2.resolve(file));
|
|
501
|
+
}
|
|
502
|
+
async refresh() {
|
|
503
|
+
try {
|
|
504
|
+
const files = await this.#build();
|
|
505
|
+
this.#files = files;
|
|
506
|
+
this.#ready = true;
|
|
507
|
+
this.#lastRefreshSucceeded = true;
|
|
508
|
+
this.#logger.verbose(`[backend-graph] scanned ${files.size} files`);
|
|
509
|
+
return true;
|
|
510
|
+
} catch (err) {
|
|
511
|
+
this.#ready = this.#files.size > 0;
|
|
512
|
+
this.#lastRefreshSucceeded = false;
|
|
513
|
+
this.#logger.warn(`[backend-graph] scan failed; ${this.#ready ? "using previous graph" : "using fallback rules"}: ${err instanceof Error ? err.message : String(err)}`);
|
|
514
|
+
return this.#ready;
|
|
515
|
+
}
|
|
516
|
+
}
|
|
517
|
+
async#build() {
|
|
518
|
+
const roots = await this.#entrypoints();
|
|
519
|
+
const files = new Set;
|
|
520
|
+
const queue = [...roots];
|
|
521
|
+
const workspaceRoot = path2.resolve(this.#app.workspace.workspaceRoot);
|
|
522
|
+
while (queue.length > 0) {
|
|
523
|
+
const current = path2.resolve(queue.pop());
|
|
524
|
+
if (files.has(current))
|
|
525
|
+
continue;
|
|
526
|
+
if (!this.#isWorkspaceSource(current, workspaceRoot))
|
|
527
|
+
continue;
|
|
528
|
+
if (!await Bun.file(current).exists())
|
|
529
|
+
continue;
|
|
530
|
+
files.add(current);
|
|
531
|
+
const source = await Bun.file(current).text();
|
|
532
|
+
const imports = this.#scanImports(current, source);
|
|
533
|
+
const importerDir = path2.dirname(current);
|
|
534
|
+
for (const imp of imports) {
|
|
535
|
+
if (!GRAPH_IMPORT_KINDS.has(imp.kind) || !imp.path || NON_SOURCE_EXT_RE.test(imp.path))
|
|
536
|
+
continue;
|
|
537
|
+
const resolved = this.#resolve(imp.path, importerDir);
|
|
538
|
+
if (!resolved || files.has(resolved))
|
|
539
|
+
continue;
|
|
540
|
+
queue.push(resolved);
|
|
541
|
+
}
|
|
542
|
+
}
|
|
543
|
+
return files;
|
|
544
|
+
}
|
|
545
|
+
async#entrypoints() {
|
|
546
|
+
const roots = [`${this.#app.cwdPath}/main.ts`, `${this.#app.cwdPath}/server.ts`];
|
|
547
|
+
const existing = [];
|
|
548
|
+
for (const root of roots) {
|
|
549
|
+
const abs = path2.resolve(root);
|
|
550
|
+
if (await Bun.file(abs).exists())
|
|
551
|
+
existing.push(abs);
|
|
552
|
+
}
|
|
553
|
+
return existing;
|
|
554
|
+
}
|
|
555
|
+
#resolve(specifier, importerDir) {
|
|
556
|
+
try {
|
|
557
|
+
const resolved = Bun.resolveSync(specifier, importerDir);
|
|
558
|
+
if (!path2.isAbsolute(resolved))
|
|
559
|
+
return null;
|
|
560
|
+
if (!SOURCE_EXTS.has(path2.extname(resolved).toLowerCase()))
|
|
561
|
+
return null;
|
|
562
|
+
return path2.resolve(resolved);
|
|
563
|
+
} catch {
|
|
564
|
+
return null;
|
|
565
|
+
}
|
|
566
|
+
}
|
|
567
|
+
#isWorkspaceSource(file, workspaceRoot) {
|
|
568
|
+
const rel = path2.relative(workspaceRoot, file);
|
|
569
|
+
if (rel.startsWith("..") || path2.isAbsolute(rel))
|
|
570
|
+
return false;
|
|
571
|
+
if (rel.includes(`${path2.sep}node_modules${path2.sep}`) || rel.includes(`${path2.sep}.akan${path2.sep}`))
|
|
572
|
+
return false;
|
|
573
|
+
return SOURCE_EXTS.has(path2.extname(file).toLowerCase());
|
|
574
|
+
}
|
|
575
|
+
#scanImports(file, source) {
|
|
576
|
+
const ext = path2.extname(file).toLowerCase();
|
|
577
|
+
if (ext === ".tsx")
|
|
578
|
+
return this.#tsxTranspiler.scanImports(source);
|
|
579
|
+
if (ext === ".jsx")
|
|
580
|
+
return this.#jsxTranspiler.scanImports(source);
|
|
581
|
+
if (ext === ".js" || ext === ".mjs" || ext === ".cjs")
|
|
582
|
+
return this.#jsTranspiler.scanImports(source);
|
|
583
|
+
return this.#tsTranspiler.scanImports(source);
|
|
584
|
+
}
|
|
585
|
+
}
|
|
586
|
+
|
|
587
|
+
class AkanAppHost {
|
|
588
|
+
app;
|
|
589
|
+
logger = new Logger3("AkanAppHost");
|
|
590
|
+
withInk;
|
|
591
|
+
env;
|
|
592
|
+
#backend = null;
|
|
593
|
+
#builder = null;
|
|
594
|
+
#backendReady = false;
|
|
595
|
+
#plannedBackendStops = new WeakSet;
|
|
596
|
+
#restartTimer = null;
|
|
597
|
+
#backendRecoveryTimer = null;
|
|
598
|
+
#backendRecoveryAttempts = 0;
|
|
599
|
+
#backendGaveUp = false;
|
|
600
|
+
#backendLifecycleState = "stopped";
|
|
601
|
+
#pendingRestartReason = null;
|
|
602
|
+
#pendingRecycle = null;
|
|
603
|
+
#builderRecoveryTimer = null;
|
|
604
|
+
#builderRecoveryAttempts = 0;
|
|
605
|
+
#backendStartStatus = null;
|
|
606
|
+
#backendBuildStatusGeneration = 0;
|
|
607
|
+
#backendStderrTail = [];
|
|
608
|
+
#lastGoodFrontend = {};
|
|
609
|
+
#buildStatusByPhase = new Map;
|
|
610
|
+
#pendingBuildStatusReplay = [];
|
|
611
|
+
#builderMessageQueue = Promise.resolve();
|
|
612
|
+
#backendGraph;
|
|
613
|
+
constructor(app, { env, withInk = false }) {
|
|
614
|
+
this.app = app;
|
|
615
|
+
this.env = env;
|
|
616
|
+
this.withInk = withInk;
|
|
617
|
+
this.#backendGraph = new BackendImportGraph(app, this.logger);
|
|
618
|
+
}
|
|
619
|
+
async start() {
|
|
620
|
+
if (this.#backend)
|
|
621
|
+
await this.#stopBackend();
|
|
622
|
+
if (this.#builder)
|
|
623
|
+
this.#stopBuilder();
|
|
624
|
+
const [redisHost] = await Promise.all([
|
|
625
|
+
this.#prepareDatabase("redis"),
|
|
626
|
+
this.#backendGraph.refresh(),
|
|
627
|
+
this.#startBuilder()
|
|
628
|
+
]);
|
|
629
|
+
Object.assign(this.env, { REDIS_HOST: redisHost });
|
|
630
|
+
this.#startBackend();
|
|
631
|
+
return this;
|
|
632
|
+
}
|
|
633
|
+
async stop() {
|
|
634
|
+
if (this.#restartTimer) {
|
|
635
|
+
clearTimeout(this.#restartTimer);
|
|
636
|
+
this.#restartTimer = null;
|
|
637
|
+
}
|
|
638
|
+
if (this.#backendRecoveryTimer) {
|
|
639
|
+
clearTimeout(this.#backendRecoveryTimer);
|
|
640
|
+
this.#backendRecoveryTimer = null;
|
|
641
|
+
}
|
|
642
|
+
if (this.#builderRecoveryTimer) {
|
|
643
|
+
clearTimeout(this.#builderRecoveryTimer);
|
|
644
|
+
this.#builderRecoveryTimer = null;
|
|
645
|
+
}
|
|
646
|
+
await this.#stopBackend();
|
|
647
|
+
this.#stopBuilder();
|
|
648
|
+
return this;
|
|
649
|
+
}
|
|
650
|
+
kill() {
|
|
651
|
+
this.stop();
|
|
652
|
+
}
|
|
653
|
+
async#prepareDatabase(type) {
|
|
654
|
+
const environment = WorkspaceExecutor.getBaseDevEnv().env;
|
|
655
|
+
if (environment === "local")
|
|
656
|
+
return "localhost";
|
|
657
|
+
return await createTunnel(type, { app: this.app, environment });
|
|
658
|
+
}
|
|
659
|
+
#startBackend(startStatus = null) {
|
|
660
|
+
this.#backendStartStatus = startStatus;
|
|
661
|
+
this.#backendGaveUp = false;
|
|
662
|
+
this.#setBackendLifecycleState("starting");
|
|
663
|
+
this.#backendReady = false;
|
|
664
|
+
this.#backendStderrTail = [];
|
|
665
|
+
const backend = Bun.spawn(["bun", `apps/${this.app.name}/main.ts`], {
|
|
666
|
+
cwd: this.app.workspace.workspaceRoot,
|
|
667
|
+
stdio: this.withInk ? ["ignore", "pipe", "pipe"] : ["inherit", "inherit", "inherit"],
|
|
668
|
+
env: this.env,
|
|
669
|
+
ipc: (msg) => {
|
|
670
|
+
if (!msg || typeof msg !== "object")
|
|
671
|
+
return;
|
|
672
|
+
if (msg.type === "backend-ready") {
|
|
673
|
+
this.#backendReady = true;
|
|
674
|
+
this.#backendRecoveryAttempts = 0;
|
|
675
|
+
this.#setBackendLifecycleState("ready", `pid=${msg.pid}`);
|
|
676
|
+
this.#recordBackendReadyStatus();
|
|
677
|
+
this.logger.verbose(`backend ready pid=${msg.pid}`);
|
|
678
|
+
this.#replayBuilderState();
|
|
679
|
+
return;
|
|
680
|
+
}
|
|
681
|
+
if (msg.type === "build-status") {
|
|
682
|
+
const status = this.#recordBackendBuildStatus({
|
|
683
|
+
generation: normalizeBackendReportedGeneration(msg.data.generation),
|
|
684
|
+
ok: msg.data.ok,
|
|
685
|
+
files: msg.data.files,
|
|
686
|
+
message: msg.data.message
|
|
687
|
+
});
|
|
688
|
+
this.#sendOrQueueBuildStatus(status);
|
|
689
|
+
return;
|
|
690
|
+
}
|
|
691
|
+
if (backendMsgTypeSet.has(msg.type))
|
|
692
|
+
this.#sendToBuilder(msg);
|
|
693
|
+
},
|
|
694
|
+
serialization: "advanced",
|
|
695
|
+
onExit: () => {
|
|
696
|
+
this.#backendReady = false;
|
|
697
|
+
if (this.#backend === backend)
|
|
698
|
+
this.#backend = null;
|
|
699
|
+
if (this.#plannedBackendStops.has(backend)) {
|
|
700
|
+
this.#plannedBackendStops.delete(backend);
|
|
701
|
+
return;
|
|
702
|
+
}
|
|
703
|
+
this.#scheduleBackendRecovery("backend-exit");
|
|
704
|
+
}
|
|
705
|
+
});
|
|
706
|
+
this.#backend = backend;
|
|
707
|
+
this.logger.verbose(`backend spawned pid=${backend.pid}`);
|
|
708
|
+
if (this.withInk) {
|
|
709
|
+
this.#forwardBackendStream(backend.stderr, "stderr");
|
|
710
|
+
this.#forwardBackendStream(backend.stdout, "stdout");
|
|
711
|
+
}
|
|
712
|
+
}
|
|
713
|
+
#recordBackendStderr(chunk) {
|
|
714
|
+
const lines = chunk.split(/\r?\n/).filter((line) => line.length > 0);
|
|
715
|
+
if (lines.length === 0)
|
|
716
|
+
return;
|
|
717
|
+
this.#backendStderrTail.push(...lines);
|
|
718
|
+
if (this.#backendStderrTail.length > BACKEND_STDERR_TAIL_LIMIT) {
|
|
719
|
+
this.#backendStderrTail.splice(0, this.#backendStderrTail.length - BACKEND_STDERR_TAIL_LIMIT);
|
|
720
|
+
}
|
|
721
|
+
}
|
|
722
|
+
async#forwardBackendStream(stream, kind) {
|
|
723
|
+
if (!stream)
|
|
724
|
+
return;
|
|
725
|
+
const decoder = new TextDecoder;
|
|
726
|
+
try {
|
|
727
|
+
for await (const chunk of stream) {
|
|
728
|
+
const text = decoder.decode(chunk, { stream: true });
|
|
729
|
+
if (!text.trim())
|
|
730
|
+
continue;
|
|
731
|
+
if (kind === "stderr") {
|
|
732
|
+
this.#recordBackendStderr(text);
|
|
733
|
+
this.logger.warn(`[backend] ${text.trimEnd()}`);
|
|
734
|
+
} else {
|
|
735
|
+
this.logger.verbose(`[backend] ${text.trimEnd()}`);
|
|
736
|
+
}
|
|
737
|
+
}
|
|
738
|
+
} catch {}
|
|
739
|
+
}
|
|
740
|
+
#nextBackendBuildStatusGeneration(generation) {
|
|
741
|
+
if (typeof generation === "number") {
|
|
742
|
+
this.#backendBuildStatusGeneration = Math.max(this.#backendBuildStatusGeneration, generation);
|
|
743
|
+
return generation;
|
|
744
|
+
}
|
|
745
|
+
this.#backendBuildStatusGeneration += 1;
|
|
746
|
+
return this.#backendBuildStatusGeneration;
|
|
747
|
+
}
|
|
748
|
+
#recordBackendBuildStatus({
|
|
749
|
+
generation,
|
|
750
|
+
ok,
|
|
751
|
+
files,
|
|
752
|
+
message
|
|
753
|
+
}) {
|
|
754
|
+
const status = createBackendBuildStatus({
|
|
755
|
+
generation: this.#nextBackendBuildStatusGeneration(generation),
|
|
756
|
+
ok,
|
|
757
|
+
files,
|
|
758
|
+
message
|
|
759
|
+
});
|
|
760
|
+
this.#recordBuildStatus(status);
|
|
761
|
+
return status;
|
|
762
|
+
}
|
|
763
|
+
#recordBackendReadyStatus() {
|
|
764
|
+
const previous = this.#buildStatusByPhase.get("backend");
|
|
765
|
+
const startStatus = this.#backendStartStatus;
|
|
766
|
+
if (startStatus || previous?.ok === false) {
|
|
767
|
+
const status = this.#recordBackendBuildStatus({
|
|
768
|
+
generation: startStatus?.generation ?? previous?.generation,
|
|
769
|
+
ok: true,
|
|
770
|
+
files: startStatus?.files ?? previous?.files ?? [],
|
|
771
|
+
message: "Backend ready"
|
|
772
|
+
});
|
|
773
|
+
this.#sendOrQueueBuildStatus(status);
|
|
774
|
+
}
|
|
775
|
+
this.#backendStartStatus = null;
|
|
776
|
+
}
|
|
777
|
+
#setBackendLifecycleState(next, detail) {
|
|
778
|
+
if (this.#backendLifecycleState === next && !detail)
|
|
779
|
+
return;
|
|
780
|
+
const prev = this.#backendLifecycleState;
|
|
781
|
+
this.#backendLifecycleState = next;
|
|
782
|
+
this.logger.verbose(`[backend-lifecycle] ${prev} -> ${next}${detail ? ` ${detail}` : ""}`);
|
|
783
|
+
}
|
|
784
|
+
#sendToBackend(message) {
|
|
785
|
+
if (!this.#backend || !this.#backendReady) {
|
|
786
|
+
if (message.type === "css-updated" || message.type === "pages-updated" || message.type === "build-status") {
|
|
787
|
+
this.logger.verbose(`backend is not ready; will replay ${message.type}`);
|
|
788
|
+
return;
|
|
789
|
+
}
|
|
790
|
+
if (message.type !== "builder-ready")
|
|
791
|
+
this.logger.warn(`backend is not ready; dropping ${message.type}`);
|
|
792
|
+
return;
|
|
793
|
+
}
|
|
794
|
+
try {
|
|
795
|
+
this.#backend.send(message);
|
|
796
|
+
} catch (err) {
|
|
797
|
+
this.logger.warn(`failed to send ${message.type} to backend: ${err instanceof Error ? err.message : String(err)}`);
|
|
798
|
+
}
|
|
799
|
+
}
|
|
800
|
+
async#stopBackend() {
|
|
801
|
+
if (!this.#backend)
|
|
802
|
+
return;
|
|
803
|
+
const backend = this.#backend;
|
|
804
|
+
this.#plannedBackendStops.add(backend);
|
|
805
|
+
this.#backendReady = false;
|
|
806
|
+
this.#setBackendLifecycleState("stopping", `pid=${backend.pid}`);
|
|
807
|
+
this.logger.verbose(`stopping backend pid=${backend.pid}`);
|
|
808
|
+
try {
|
|
809
|
+
backend.kill("SIGTERM");
|
|
810
|
+
const timeout = new Promise((resolve) => setTimeout(() => resolve("timeout"), BACKEND_GRACEFUL_TIMEOUT_MS));
|
|
811
|
+
const result = await Promise.race([backend.exited, timeout]);
|
|
812
|
+
if (result === "timeout") {
|
|
813
|
+
this.logger.warn(`backend pid=${backend.pid} did not exit in ${BACKEND_GRACEFUL_TIMEOUT_MS}ms; force killing`);
|
|
814
|
+
backend.kill("SIGKILL");
|
|
815
|
+
await backend.exited.catch(() => {
|
|
816
|
+
return;
|
|
817
|
+
});
|
|
818
|
+
}
|
|
819
|
+
} finally {
|
|
820
|
+
if (this.#backend === backend)
|
|
821
|
+
this.#backend = null;
|
|
822
|
+
this.#setBackendLifecycleState("stopped", `pid=${backend.pid}`);
|
|
823
|
+
}
|
|
824
|
+
}
|
|
825
|
+
#scheduleBackendRestart(reason) {
|
|
826
|
+
this.#pendingRestartReason = mergeBackendRestartReasons(this.#pendingRestartReason, reason);
|
|
827
|
+
const pending = this.#pendingRestartReason;
|
|
828
|
+
this.#setBackendLifecycleState("restart-pending", `generation=${pending.generation ?? "(unknown)"} files=${pending.files.length} roles=${pending.roles.join(",") || "(none)"}`);
|
|
829
|
+
if (this.#backendRecoveryTimer) {
|
|
830
|
+
clearTimeout(this.#backendRecoveryTimer);
|
|
831
|
+
this.#backendRecoveryTimer = null;
|
|
832
|
+
}
|
|
833
|
+
if (this.#restartTimer)
|
|
834
|
+
clearTimeout(this.#restartTimer);
|
|
835
|
+
this.#restartTimer = setTimeout(() => {
|
|
836
|
+
this.#restartTimer = null;
|
|
837
|
+
const next = this.#pendingRestartReason;
|
|
838
|
+
this.#pendingRestartReason = null;
|
|
839
|
+
if (next)
|
|
840
|
+
this.#restartBackend(next);
|
|
841
|
+
}, BACKEND_RESTART_DEBOUNCE_MS);
|
|
842
|
+
}
|
|
843
|
+
async#restartBackend(reason) {
|
|
844
|
+
this.logger.verbose(`[backend-reload] restarting backend generation=${reason.generation ?? "(unknown)"} files=${reason.files.length} roles=${reason.roles.join(",") || "(none)"}`);
|
|
845
|
+
this.#backendRecoveryAttempts = 0;
|
|
846
|
+
await Promise.all([this.#stopBackend(), this.#backendGraph.refresh()]);
|
|
847
|
+
this.#startBackend({ generation: reason.generation, files: reason.files });
|
|
848
|
+
}
|
|
849
|
+
#scheduleBackendRecovery(reason) {
|
|
850
|
+
if (this.#backendRecoveryTimer || this.#backend)
|
|
851
|
+
return;
|
|
852
|
+
if (shouldAbandonBackendRecovery(this.#backendRecoveryAttempts)) {
|
|
853
|
+
const message = `Backend exited ${this.#backendRecoveryAttempts} times in a row (${reason}); waiting for an edit or a green build to retry.`;
|
|
854
|
+
this.#backendGaveUp = true;
|
|
855
|
+
this.#setBackendLifecycleState("stopped", `gave up after ${this.#backendRecoveryAttempts} recovery attempts`);
|
|
856
|
+
this.logger.error(`[backend-recovery] ${message}`);
|
|
857
|
+
if (this.#backendStderrTail.length > 0) {
|
|
858
|
+
this.logger.error(`[backend-recovery] recent backend stderr:
|
|
859
|
+
${this.#backendStderrTail.join(`
|
|
860
|
+
`)}`);
|
|
861
|
+
}
|
|
862
|
+
const abandonedStatus = this.#recordBackendBuildStatus({ ok: false, files: [], message });
|
|
863
|
+
this.#sendOrQueueBuildStatus(abandonedStatus);
|
|
864
|
+
return;
|
|
865
|
+
}
|
|
866
|
+
this.#setBackendLifecycleState("recovering", reason);
|
|
867
|
+
const attempt = this.#backendRecoveryAttempts;
|
|
868
|
+
const delay = Math.min(BACKEND_RECOVERY_BASE_DELAY_MS * 2 ** attempt, BACKEND_RECOVERY_MAX_DELAY_MS);
|
|
869
|
+
this.#backendRecoveryAttempts = attempt + 1;
|
|
870
|
+
const failureStatus = this.#recordBackendBuildStatus({
|
|
871
|
+
ok: false,
|
|
872
|
+
files: [],
|
|
873
|
+
message: `Backend exited unexpectedly (${reason}); restarting in ${delay}ms`
|
|
874
|
+
});
|
|
875
|
+
this.#sendOrQueueBuildStatus(failureStatus);
|
|
876
|
+
this.logger.warn(`[backend-recovery] backend exited unexpectedly (${reason}); restarting in ${delay}ms (attempt ${this.#backendRecoveryAttempts})`);
|
|
877
|
+
if (this.#backendStderrTail.length > 0) {
|
|
878
|
+
this.logger.warn(`[backend-recovery] recent backend stderr:
|
|
879
|
+
${this.#backendStderrTail.join(`
|
|
880
|
+
`)}`);
|
|
881
|
+
}
|
|
882
|
+
this.#backendRecoveryTimer = setTimeout(() => {
|
|
883
|
+
this.#backendRecoveryTimer = null;
|
|
884
|
+
if (this.#backend)
|
|
885
|
+
return;
|
|
886
|
+
this.#backendGraph.refresh().finally(() => {
|
|
887
|
+
if (!this.#backend)
|
|
888
|
+
this.#startBackend({ generation: failureStatus.generation, files: failureStatus.files });
|
|
889
|
+
});
|
|
890
|
+
}, delay);
|
|
891
|
+
}
|
|
892
|
+
#enqueueBuilderMessage(message) {
|
|
893
|
+
this.#builderMessageQueue = this.#builderMessageQueue.then(() => this.#handleBuilderMessage(message)).catch((err) => {
|
|
894
|
+
this.logger.warn(`failed to handle builder message: ${err instanceof Error ? err.message : String(err)}`);
|
|
895
|
+
});
|
|
896
|
+
}
|
|
897
|
+
async#handleBuilderMessage(message) {
|
|
898
|
+
if (message.type === "build-status") {
|
|
899
|
+
this.#recordBuildStatus(message.data);
|
|
900
|
+
this.#sendOrQueueBuildStatus(message.data);
|
|
901
|
+
this.#reviveBackendAfterGreenBuild(message.data);
|
|
902
|
+
return;
|
|
903
|
+
}
|
|
904
|
+
if (message.type === "pages-updated")
|
|
905
|
+
this.#recordLastGood(message);
|
|
906
|
+
if (message.type === "css-updated")
|
|
907
|
+
this.#recordLastGood(message);
|
|
908
|
+
if (message.type === "invalidate") {
|
|
909
|
+
await this.#handleInvalidate(message);
|
|
910
|
+
return;
|
|
911
|
+
}
|
|
912
|
+
this.#sendToBackend(message);
|
|
913
|
+
}
|
|
914
|
+
async#handleInvalidate(message) {
|
|
915
|
+
this.#logDevPlan(message);
|
|
916
|
+
const wantsDevHostRestart = shouldRestartDevHostByDevPlan(message);
|
|
917
|
+
const pending = this.#pendingRecycle;
|
|
918
|
+
if (wantsDevHostRestart || shouldRestartBuilderByDevPlan(message) || pending && message.kinds.includes("code")) {
|
|
919
|
+
const refreshConfig = wantsDevHostRestart || (pending?.refreshConfig ?? false);
|
|
920
|
+
const merged = pending ? mergeInvalidateMessages(pending.message, message) : message;
|
|
921
|
+
const generation = message.devPlan?.generation ?? message.generation;
|
|
922
|
+
if (hasBuildFailureForGeneration(this.#buildStatusByPhase, generation)) {
|
|
923
|
+
this.#deferRecycle(merged, { refreshConfig, generation });
|
|
924
|
+
return;
|
|
925
|
+
}
|
|
926
|
+
this.#pendingRecycle = null;
|
|
927
|
+
try {
|
|
928
|
+
if (refreshConfig)
|
|
929
|
+
await this.#restartDevHost(merged);
|
|
930
|
+
else
|
|
931
|
+
await this.#restartDevChildren(merged);
|
|
932
|
+
} catch (err) {
|
|
933
|
+
this.#recordDevHostRestartFailure(merged, err, refreshConfig ? "Config" : "Runtime metadata");
|
|
934
|
+
this.#resurrectDevChildren(merged);
|
|
935
|
+
}
|
|
936
|
+
return;
|
|
937
|
+
}
|
|
938
|
+
if (await this.#shouldRestartBackend(message)) {
|
|
939
|
+
this.#scheduleBackendRestart(backendRestartReasonFromMessage(message));
|
|
940
|
+
return;
|
|
941
|
+
}
|
|
942
|
+
this.#sendToBackend(message);
|
|
943
|
+
}
|
|
944
|
+
#deferRecycle(message, { refreshConfig, generation }) {
|
|
945
|
+
this.#pendingRecycle = { message, refreshConfig };
|
|
946
|
+
const kind = refreshConfig ? "Config" : "Runtime metadata";
|
|
947
|
+
this.logger.warn(`[dev-host] ${kind.toLowerCase()} restart deferred generation=${generation ?? "(unknown)"}; keeping the running dev server until the build error is fixed`);
|
|
948
|
+
const status = {
|
|
949
|
+
generation: generation ?? this.#nextBackendBuildStatusGeneration(),
|
|
950
|
+
phase: "scan",
|
|
951
|
+
ok: false,
|
|
952
|
+
files: message.files,
|
|
953
|
+
message: `${kind} change is on hold while the build is failing; it will apply automatically once the error is fixed.`
|
|
954
|
+
};
|
|
955
|
+
this.#recordBuildStatus(status);
|
|
956
|
+
this.#sendOrQueueBuildStatus(status);
|
|
957
|
+
}
|
|
958
|
+
#resurrectDevChildren(message) {
|
|
959
|
+
const generation = message.devPlan?.generation ?? message.generation;
|
|
960
|
+
if (!this.#backend)
|
|
961
|
+
this.#startBackend({ generation, files: message.files });
|
|
962
|
+
this.#scheduleBuilderRecovery({ generation, files: message.files });
|
|
963
|
+
}
|
|
964
|
+
#scheduleBuilderRecovery(reason) {
|
|
965
|
+
if (this.#builderRecoveryTimer || this.#builder)
|
|
966
|
+
return;
|
|
967
|
+
const attempt = this.#builderRecoveryAttempts;
|
|
968
|
+
const delay = Math.min(BUILDER_RECOVERY_BASE_DELAY_MS * 2 ** attempt, BUILDER_RECOVERY_MAX_DELAY_MS);
|
|
969
|
+
this.#builderRecoveryAttempts = attempt + 1;
|
|
970
|
+
this.logger.warn(`[builder-recovery] builder is down; retrying start in ${delay}ms (attempt ${this.#builderRecoveryAttempts})`);
|
|
971
|
+
this.#builderRecoveryTimer = setTimeout(() => {
|
|
972
|
+
this.#builderRecoveryTimer = null;
|
|
973
|
+
if (this.#builder)
|
|
974
|
+
return;
|
|
975
|
+
this.#recoverBuilder(reason);
|
|
976
|
+
}, delay);
|
|
977
|
+
}
|
|
978
|
+
async#recoverBuilder(reason) {
|
|
979
|
+
try {
|
|
980
|
+
await this.#startBuilder();
|
|
981
|
+
} catch (err) {
|
|
982
|
+
this.logger.warn(`[builder-recovery] builder start failed: ${err instanceof Error ? err.message : String(err)}`);
|
|
983
|
+
this.#scheduleBuilderRecovery(reason);
|
|
984
|
+
return;
|
|
985
|
+
}
|
|
986
|
+
this.#builderRecoveryAttempts = 0;
|
|
987
|
+
this.logger.info("[builder-recovery] builder recovered");
|
|
988
|
+
const status = {
|
|
989
|
+
generation: reason.generation ?? this.#nextBackendBuildStatusGeneration(),
|
|
990
|
+
phase: "scan",
|
|
991
|
+
ok: true,
|
|
992
|
+
files: reason.files,
|
|
993
|
+
message: "Builder recovered"
|
|
994
|
+
};
|
|
995
|
+
this.#recordBuildStatus(status);
|
|
996
|
+
this.#sendOrQueueBuildStatus(status);
|
|
997
|
+
if (!this.#backend && !this.#backendRecoveryTimer) {
|
|
998
|
+
this.#startBackend({ generation: reason.generation, files: reason.files });
|
|
999
|
+
}
|
|
1000
|
+
}
|
|
1001
|
+
#reviveBackendAfterGreenBuild(status) {
|
|
1002
|
+
if (!status.ok || !this.#backendGaveUp || this.#backend || this.#backendRecoveryTimer)
|
|
1003
|
+
return;
|
|
1004
|
+
this.logger.info(`[backend-recovery] build went green (generation=${status.generation}); retrying backend`);
|
|
1005
|
+
this.#backendRecoveryAttempts = 0;
|
|
1006
|
+
this.#startBackend({ generation: status.generation, files: status.files });
|
|
1007
|
+
}
|
|
1008
|
+
async#restartDevChildren(message) {
|
|
1009
|
+
const generation = message.devPlan?.generation ?? message.generation;
|
|
1010
|
+
this.logger.warn(`[dev-host] recycling builder/backend for runtime metadata generation=${generation ?? "(unknown)"} files=${message.files.length}`);
|
|
1011
|
+
await this.#recycleDevChildren(message);
|
|
1012
|
+
}
|
|
1013
|
+
async#restartDevHost(message) {
|
|
1014
|
+
const generation = message.devPlan?.generation ?? message.generation;
|
|
1015
|
+
this.logger.warn(`[dev-host] config change detected; restarting dev host generation=${generation ?? "(unknown)"} files=${message.files.length}`);
|
|
1016
|
+
await this.#recycleDevChildren(message, { refreshConfig: true });
|
|
1017
|
+
}
|
|
1018
|
+
async#recycleDevChildren(message, { refreshConfig = false } = {}) {
|
|
1019
|
+
const generation = message.devPlan?.generation ?? message.generation;
|
|
1020
|
+
if (this.#restartTimer) {
|
|
1021
|
+
clearTimeout(this.#restartTimer);
|
|
1022
|
+
this.#restartTimer = null;
|
|
1023
|
+
}
|
|
1024
|
+
if (this.#backendRecoveryTimer) {
|
|
1025
|
+
clearTimeout(this.#backendRecoveryTimer);
|
|
1026
|
+
this.#backendRecoveryTimer = null;
|
|
1027
|
+
}
|
|
1028
|
+
if (this.#builderRecoveryTimer) {
|
|
1029
|
+
clearTimeout(this.#builderRecoveryTimer);
|
|
1030
|
+
this.#builderRecoveryTimer = null;
|
|
1031
|
+
}
|
|
1032
|
+
this.#builderRecoveryAttempts = 0;
|
|
1033
|
+
this.#pendingRestartReason = null;
|
|
1034
|
+
this.#lastGoodFrontend = {};
|
|
1035
|
+
this.#buildStatusByPhase.clear();
|
|
1036
|
+
this.#pendingBuildStatusReplay = [];
|
|
1037
|
+
await this.#stopBackend();
|
|
1038
|
+
this.#stopBuilder();
|
|
1039
|
+
if (refreshConfig) {
|
|
1040
|
+
await this.app.getConfig({ refresh: true });
|
|
1041
|
+
const { env } = await this.app.prepareCommand("start");
|
|
1042
|
+
Object.assign(this.env, env);
|
|
1043
|
+
}
|
|
1044
|
+
await this.#backendGraph.refresh();
|
|
1045
|
+
await this.#startBuilder();
|
|
1046
|
+
this.#startBackend({ generation, files: message.files });
|
|
1047
|
+
}
|
|
1048
|
+
#recordLastGood(message) {
|
|
1049
|
+
if (message.type === "pages-updated") {
|
|
1050
|
+
if (!shouldReplaceLastGoodMessage(this.#lastGoodFrontend.pages, message))
|
|
1051
|
+
return;
|
|
1052
|
+
this.#lastGoodFrontend.pages = message;
|
|
1053
|
+
this.logger.verbose(`[last-good] pages generation=${message.data.generation ?? "(unknown)"} buildId=${message.data.buildId}`);
|
|
1054
|
+
return;
|
|
1055
|
+
}
|
|
1056
|
+
if (!shouldReplaceLastGoodMessage(this.#lastGoodFrontend.css, message))
|
|
1057
|
+
return;
|
|
1058
|
+
this.#lastGoodFrontend.css = message;
|
|
1059
|
+
this.logger.verbose(`[last-good] css generation=${message.data.generation ?? "(unknown)"} assets=${Object.keys(message.data.cssAssets).length}`);
|
|
1060
|
+
}
|
|
1061
|
+
#recordDevHostRestartFailure(message, err, kind) {
|
|
1062
|
+
const generation = message.devPlan?.generation ?? message.generation ?? this.#nextBackendBuildStatusGeneration();
|
|
1063
|
+
const detail = err instanceof Error ? err.message : String(err);
|
|
1064
|
+
this.logger.warn(`[dev-host] ${kind.toLowerCase()} restart failed generation=${generation}: ${detail}`);
|
|
1065
|
+
const status = {
|
|
1066
|
+
generation,
|
|
1067
|
+
phase: "scan",
|
|
1068
|
+
ok: false,
|
|
1069
|
+
files: message.files,
|
|
1070
|
+
message: `${kind} change failed to apply; recovering the dev server automatically: ${detail}`
|
|
1071
|
+
};
|
|
1072
|
+
this.#recordBuildStatus(status);
|
|
1073
|
+
this.#sendOrQueueBuildStatus(status);
|
|
1074
|
+
}
|
|
1075
|
+
#recordBuildStatus(status) {
|
|
1076
|
+
const recovered = shouldMarkBuildPhaseRecovered(this.#buildStatusByPhase, status);
|
|
1077
|
+
this.#buildStatusByPhase.set(status.phase, status);
|
|
1078
|
+
const label = `[build-status] generation=${status.generation} phase=${status.phase} ok=${status.ok} files=${status.files.length}`;
|
|
1079
|
+
if (status.ok)
|
|
1080
|
+
this.logger.verbose(`${label}${recovered ? " recovered=1" : ""}`);
|
|
1081
|
+
else
|
|
1082
|
+
this.logger.warn(`${label}${status.message ? ` message=${status.message}` : ""}`);
|
|
1083
|
+
}
|
|
1084
|
+
#sendOrQueueBuildStatus(status) {
|
|
1085
|
+
if (!this.#backend || shouldQueueBuildStatusReplay(this.#backendReady, this.#pendingBuildStatusReplay.length)) {
|
|
1086
|
+
this.#pendingBuildStatusReplay.push(status);
|
|
1087
|
+
this.logger.verbose(`backend is not ready; will replay build-status generation=${status.generation} phase=${status.phase}`);
|
|
1088
|
+
return;
|
|
1089
|
+
}
|
|
1090
|
+
this.#sendToBackend({ type: "build-status", data: status });
|
|
1091
|
+
}
|
|
1092
|
+
#replayBuilderState() {
|
|
1093
|
+
if (!this.#backendReady)
|
|
1094
|
+
return;
|
|
1095
|
+
if (this.#lastGoodFrontend.css)
|
|
1096
|
+
this.#sendToBackend(this.#lastGoodFrontend.css);
|
|
1097
|
+
if (this.#lastGoodFrontend.pages)
|
|
1098
|
+
this.#sendToBackend(this.#lastGoodFrontend.pages);
|
|
1099
|
+
const queuedStatuses = this.#pendingBuildStatusReplay.splice(0);
|
|
1100
|
+
for (const status of buildStatusReplaySequence(queuedStatuses, this.#buildStatusByPhase)) {
|
|
1101
|
+
this.#sendToBackend({ type: "build-status", data: status });
|
|
1102
|
+
}
|
|
1103
|
+
}
|
|
1104
|
+
#logDevPlan(message) {
|
|
1105
|
+
if (!message.devPlan)
|
|
1106
|
+
return;
|
|
1107
|
+
const { generation, roles, actions, reasonByFile } = message.devPlan;
|
|
1108
|
+
this.logger.verbose(`[dev-plan] generation=${generation} roles=${roles.join(",") || "(none)"} actions=${actions.join(",") || "(none)"} reasons=${Object.keys(reasonByFile).length}`);
|
|
1109
|
+
}
|
|
1110
|
+
async#shouldRestartBackend(message) {
|
|
1111
|
+
if (message.kinds.length === 1 && message.kinds[0] === "css")
|
|
1112
|
+
return false;
|
|
1113
|
+
if (message.devPlan) {
|
|
1114
|
+
const shouldRestart = shouldRestartBackendByDevPlan(message) ?? false;
|
|
1115
|
+
if (shouldRestart && message.kinds.includes("code"))
|
|
1116
|
+
await this.#backendGraph.refresh();
|
|
1117
|
+
return shouldRestart;
|
|
1118
|
+
}
|
|
1119
|
+
if (message.kinds.includes("code"))
|
|
1120
|
+
await this.#backendGraph.refresh();
|
|
1121
|
+
if (message.files.some((file) => this.#isBackendFile(file)))
|
|
1122
|
+
return true;
|
|
1123
|
+
if (!this.#backendGraph.lastRefreshSucceeded) {
|
|
1124
|
+
const fallbackFiles = message.files.filter((file) => isLegacyBackendFallbackFile(file, this.app.workspace.workspaceRoot));
|
|
1125
|
+
if (fallbackFiles.length > 0) {
|
|
1126
|
+
this.logger.warn(`[backend-graph] using path-role fallback for legacy invalidate; restart files=${fallbackFiles.length}`);
|
|
1127
|
+
return true;
|
|
1128
|
+
}
|
|
1129
|
+
}
|
|
1130
|
+
return false;
|
|
1131
|
+
}
|
|
1132
|
+
#isBackendFile(file) {
|
|
1133
|
+
return this.#backendGraph.has(file);
|
|
1134
|
+
}
|
|
1135
|
+
async#startBuilder() {
|
|
1136
|
+
const startTime = Date.now();
|
|
1137
|
+
this.app.verbose(`[cli] waiting for builder to complete initial base build\u2026`);
|
|
1138
|
+
let lastError;
|
|
1139
|
+
for (let attempt = 1;attempt <= BUILDER_START_MAX_ATTEMPTS; attempt++) {
|
|
1140
|
+
this.#builder = await IncrementalBuilderHost.create(this.app, this.env, (msg) => {
|
|
1141
|
+
this.#enqueueBuilderMessage(msg);
|
|
1142
|
+
});
|
|
1143
|
+
try {
|
|
1144
|
+
await this.#waitForBuilderReady(attempt);
|
|
1145
|
+
this.app.verbose(`[cli] base build ready in ${Date.now() - startTime}ms \u2014 starting backend`);
|
|
1146
|
+
return this.#builder;
|
|
1147
|
+
} catch (err) {
|
|
1148
|
+
lastError = err;
|
|
1149
|
+
this.#stopBuilder();
|
|
1150
|
+
if (attempt >= BUILDER_START_MAX_ATTEMPTS)
|
|
1151
|
+
break;
|
|
1152
|
+
this.app.verbose(`[cli] builder failed before ready; retrying (${attempt + 1}/${BUILDER_START_MAX_ATTEMPTS})`);
|
|
1153
|
+
}
|
|
1154
|
+
}
|
|
1155
|
+
throw lastError instanceof Error ? lastError : new Error(String(lastError));
|
|
1156
|
+
}
|
|
1157
|
+
#waitForBuilderReady(attempt) {
|
|
1158
|
+
return new Promise((resolve, reject) => {
|
|
1159
|
+
if (!this.#builder)
|
|
1160
|
+
throw new Error("Builder Not Found");
|
|
1161
|
+
let settled = false;
|
|
1162
|
+
const settle = (fn) => {
|
|
1163
|
+
if (settled)
|
|
1164
|
+
return;
|
|
1165
|
+
settled = true;
|
|
1166
|
+
clearTimeout(timeout);
|
|
1167
|
+
fn();
|
|
1168
|
+
};
|
|
1169
|
+
const timeout = setTimeout(() => {
|
|
1170
|
+
settle(() => reject(new Error("[cli] builder timed out before emitting builder-ready")));
|
|
1171
|
+
}, BUILDER_READY_TIMEOUT_MS);
|
|
1172
|
+
this.#builder.start({
|
|
1173
|
+
onExit: () => {
|
|
1174
|
+
settle(() => reject(new Error(`[cli] builder exited before emitting builder-ready (attempt ${attempt})`)));
|
|
1175
|
+
},
|
|
1176
|
+
onReady: () => {
|
|
1177
|
+
settle(resolve);
|
|
1178
|
+
},
|
|
1179
|
+
onRestartReady: () => {
|
|
1180
|
+
this.logger.verbose("[builder-recovery] builder ready after restart; replaying latest state");
|
|
1181
|
+
this.#replayBuilderState();
|
|
1182
|
+
}
|
|
1183
|
+
});
|
|
1184
|
+
});
|
|
1185
|
+
}
|
|
1186
|
+
#sendToBuilder(message) {
|
|
1187
|
+
if (message.type === "build-csr" && this.env.AKAN_DEV_CSR_REBUILD !== "1") {
|
|
1188
|
+
Object.assign(this.env, { AKAN_DEV_CSR_REBUILD: "1" });
|
|
1189
|
+
this.logger.verbose(`[csr] armed dev CSR rebuilds (${message.reason})`);
|
|
1190
|
+
}
|
|
1191
|
+
if (this.#builder?.send(message))
|
|
1192
|
+
return;
|
|
1193
|
+
const status = this.#builder?.status ?? "stopped";
|
|
1194
|
+
if (message.type === "build-route") {
|
|
1195
|
+
this.#sendToBackend({
|
|
1196
|
+
type: "build-route-res",
|
|
1197
|
+
id: message.id,
|
|
1198
|
+
ok: false,
|
|
1199
|
+
error: `builder is ${status}; reload after the builder is ready`
|
|
1200
|
+
});
|
|
1201
|
+
return;
|
|
1202
|
+
}
|
|
1203
|
+
if (message.type === "build-csr") {
|
|
1204
|
+
this.#sendToBackend({
|
|
1205
|
+
type: "build-csr-res",
|
|
1206
|
+
id: message.id,
|
|
1207
|
+
ok: false,
|
|
1208
|
+
error: `builder is ${status}; reload after the builder is ready`
|
|
1209
|
+
});
|
|
1210
|
+
return;
|
|
1211
|
+
}
|
|
1212
|
+
this.logger.warn("akanAppHost builder is not running");
|
|
1213
|
+
}
|
|
1214
|
+
#stopBuilder() {
|
|
1215
|
+
if (!this.#builder)
|
|
1216
|
+
return;
|
|
1217
|
+
this.#builder.stop();
|
|
1218
|
+
this.#builder = null;
|
|
1219
|
+
}
|
|
1220
|
+
}
|
|
1221
|
+
// pkgs/@akanjs/devkit/applicationTestPreload.ts
|
|
1222
|
+
import path3 from "path";
|
|
1223
|
+
var SIGNAL_TEST_PRELOAD_PATH = "test/signalTest.preload.ts";
|
|
1224
|
+
async function resolveSignalTestPreloadPath(target) {
|
|
1225
|
+
const candidates = [];
|
|
1226
|
+
const addResolvedPackageCandidate = (basePath) => {
|
|
1227
|
+
try {
|
|
1228
|
+
candidates.push(path3.join(path3.dirname(Bun.resolveSync("akanjs/package.json", basePath)), SIGNAL_TEST_PRELOAD_PATH));
|
|
1229
|
+
} catch {}
|
|
1230
|
+
};
|
|
1231
|
+
addResolvedPackageCandidate(target.cwdPath);
|
|
1232
|
+
addResolvedPackageCandidate(process.cwd());
|
|
1233
|
+
addResolvedPackageCandidate(path3.dirname(Bun.main));
|
|
1234
|
+
addResolvedPackageCandidate(import.meta.dir);
|
|
1235
|
+
candidates.push(path3.join(target.cwdPath, "../../node_modules/akanjs", SIGNAL_TEST_PRELOAD_PATH), path3.join(target.cwdPath, "../../pkgs/akanjs", SIGNAL_TEST_PRELOAD_PATH), path3.join(process.cwd(), "node_modules/akanjs", SIGNAL_TEST_PRELOAD_PATH), path3.join(process.cwd(), "pkgs/akanjs", SIGNAL_TEST_PRELOAD_PATH), path3.join(path3.dirname(Bun.main), "../../akanjs", SIGNAL_TEST_PRELOAD_PATH), path3.resolve(import.meta.dir, "../../akanjs", SIGNAL_TEST_PRELOAD_PATH));
|
|
1236
|
+
const uniqueCandidates = [...new Set(candidates)];
|
|
1237
|
+
for (const candidate of uniqueCandidates) {
|
|
1238
|
+
if (await Bun.file(candidate).exists())
|
|
1239
|
+
return candidate;
|
|
1240
|
+
}
|
|
1241
|
+
throw new Error(`Failed to locate ${SIGNAL_TEST_PRELOAD_PATH} from ${target.cwdPath}.
|
|
1242
|
+
Probed paths:
|
|
1243
|
+
${uniqueCandidates.map((candidate) => ` - ${candidate}`).join(`
|
|
1244
|
+
`)}`);
|
|
1245
|
+
}
|
|
1246
|
+
|
|
1247
|
+
// pkgs/@akanjs/cli/application/application.runner.ts
|
|
1248
|
+
import { Logger as Logger4 } from "akanjs/common";
|
|
1249
|
+
import ora from "ora";
|
|
1250
|
+
|
|
1251
|
+
// pkgs/@akanjs/cli/openBrowser.ts
|
|
1252
|
+
import { spawn } from "child_process";
|
|
1253
|
+
function openBrowser(url) {
|
|
1254
|
+
const command = process.platform === "darwin" ? ["open", url] : process.platform === "win32" ? ["cmd", "/c", "start", "", url] : ["xdg-open", url];
|
|
1255
|
+
const child = spawn(command[0], command.slice(1), { detached: true, stdio: "ignore" });
|
|
1256
|
+
child.on("error", () => {});
|
|
1257
|
+
child.unref();
|
|
1258
|
+
return Promise.resolve();
|
|
1259
|
+
}
|
|
1260
|
+
|
|
1261
|
+
// pkgs/@akanjs/cli/application/application.runner.ts
|
|
1262
|
+
var loadBuildRunner = async () => (await import("./applicationBuildRunner-esa1kj7v.js")).ApplicationBuildRunner;
|
|
1263
|
+
var loadReleasePackager = async () => (await import("./applicationReleasePackager-brvth6rs.js")).ApplicationReleasePackager;
|
|
1264
|
+
var loadCapacitorApp = async () => (await import("./capacitorApp-y0h6cgft.js")).CapacitorApp;
|
|
1265
|
+
var loadPrompts = async () => await import("@inquirer/prompts");
|
|
1266
|
+
|
|
1267
|
+
class ApplicationRunner extends runner("application") {
|
|
1268
|
+
async createApplication(appName, workspace, libs = []) {
|
|
1269
|
+
await workspace.applyTemplate({
|
|
1270
|
+
basePath: `apps/${appName}`,
|
|
1271
|
+
template: "app",
|
|
1272
|
+
dict: { appName },
|
|
1273
|
+
options: { libs }
|
|
1274
|
+
});
|
|
1275
|
+
return AppExecutor.from(workspace, appName);
|
|
1276
|
+
}
|
|
1277
|
+
async removeApplication(app) {
|
|
1278
|
+
await app.workspace.exec(`rm -rf apps/${app.name}`);
|
|
1279
|
+
}
|
|
1280
|
+
async getConfig(app) {
|
|
1281
|
+
return await app.getConfig();
|
|
1282
|
+
}
|
|
1283
|
+
async getScriptFilename(app) {
|
|
1284
|
+
if (!await app.exists("script")) {
|
|
1285
|
+
await app.mkdir("script");
|
|
1286
|
+
throw new Error(`No script files found. make a script file in apps/${app.name}/script folder`);
|
|
1287
|
+
}
|
|
1288
|
+
const scriptFiles = (await app.readdir("script")).filter((file) => file.endsWith(".ts"));
|
|
1289
|
+
const scriptFile = await (await loadPrompts()).select({
|
|
1290
|
+
message: "Select script to run",
|
|
1291
|
+
choices: scriptFiles.map((file) => ({ name: file, value: file.replace(".ts", "") }))
|
|
1292
|
+
});
|
|
1293
|
+
return scriptFile;
|
|
1294
|
+
}
|
|
1295
|
+
async runScript(app, filename) {
|
|
1296
|
+
const scriptName = filename.endsWith(".ts") ? filename.slice(0, -3) : filename;
|
|
1297
|
+
if (scriptName.includes("/") || scriptName.includes("\\") || scriptName.includes("..")) {
|
|
1298
|
+
throw new Error(`Invalid script filename: ${filename}`);
|
|
1299
|
+
}
|
|
1300
|
+
const scriptPath = `script/${scriptName}.ts`;
|
|
1301
|
+
if (!await app.exists(scriptPath))
|
|
1302
|
+
throw new Error(`Script file not found: apps/${app.name}/${scriptPath}`);
|
|
1303
|
+
await app.spawn("bun", [scriptPath], {
|
|
1304
|
+
env: app.getCommandEnv({ AKAN_COMMAND_TYPE: "script" }),
|
|
1305
|
+
stdio: "inherit"
|
|
1306
|
+
});
|
|
1307
|
+
}
|
|
1308
|
+
async runConsole(app) {
|
|
1309
|
+
const serverPath = `${app.cwdPath}/server.ts`;
|
|
1310
|
+
if (!await app.exists("server.ts"))
|
|
1311
|
+
throw new Error(`Server file not found: apps/${app.name}/server.ts`);
|
|
1312
|
+
const code = `
|
|
1313
|
+
const serverModule = await import(${JSON.stringify(serverPath)});
|
|
1314
|
+
const { assertAkanConsoleAllowed, startAkanConsole } = await import("akanjs/server");
|
|
1315
|
+
const server = serverModule.server;
|
|
1316
|
+
if (!server?.start) throw new Error("server.ts must export server with start()");
|
|
1317
|
+
assertAkanConsoleAllowed(server.env);
|
|
1318
|
+
await server.start({ listen: false, web: false });
|
|
1319
|
+
try {
|
|
1320
|
+
await startAkanConsole(server, {
|
|
1321
|
+
globals: {
|
|
1322
|
+
srv: serverModule.srv,
|
|
1323
|
+
sig: serverModule.sig,
|
|
1324
|
+
db: serverModule.db,
|
|
1325
|
+
cnst: serverModule.cnst,
|
|
1326
|
+
dict: serverModule.dict,
|
|
1327
|
+
option: serverModule.option,
|
|
1328
|
+
},
|
|
1329
|
+
});
|
|
1330
|
+
} finally {
|
|
1331
|
+
await server.stop();
|
|
1332
|
+
}
|
|
1333
|
+
`;
|
|
1334
|
+
await app.spawn("bun", ["-e", code], {
|
|
1335
|
+
env: app.getCommandEnv({ AKAN_COMMAND_TYPE: "console" }),
|
|
1336
|
+
stdio: "inherit"
|
|
1337
|
+
});
|
|
1338
|
+
}
|
|
1339
|
+
async typecheck(app, options = {}) {
|
|
1340
|
+
await new (await loadBuildRunner())(app).typecheck(options);
|
|
1341
|
+
}
|
|
1342
|
+
async test(exec) {
|
|
1343
|
+
const isSignalTarget = exec instanceof AppExecutor || exec instanceof LibExecutor;
|
|
1344
|
+
const preloadPath = isSignalTarget ? await resolveSignalTestPreloadPath(exec) : null;
|
|
1345
|
+
const env = isSignalTarget ? {
|
|
1346
|
+
AKAN_TEST_SIGNAL: "1",
|
|
1347
|
+
AKAN_TEST_TARGET_TYPE: exec.type,
|
|
1348
|
+
AKAN_TEST_TARGET_NAME: exec.name,
|
|
1349
|
+
AKAN_TEST_LIBS: exec.getScanInfo({ allowEmpty: true })?.getLibs().join(",") ?? ""
|
|
1350
|
+
} : {};
|
|
1351
|
+
const args = preloadPath ? ["test", "--isolate", "--preload", preloadPath] : ["test", "--isolate"];
|
|
1352
|
+
await exec.spawn("bun", args, {
|
|
1353
|
+
...isSignalTarget ? { env: { ...process.env, ...env } } : {},
|
|
1354
|
+
stdio: "inherit"
|
|
1355
|
+
});
|
|
1356
|
+
}
|
|
1357
|
+
async build(app, {
|
|
1358
|
+
fast = false,
|
|
1359
|
+
reporter,
|
|
1360
|
+
spinner = false
|
|
1361
|
+
} = {}) {
|
|
1362
|
+
return new (await loadBuildRunner())(app, { fast, reporter }).build({ spinner });
|
|
1363
|
+
}
|
|
1364
|
+
async start(app, { open = false, onStart, withInk = false } = {}) {
|
|
1365
|
+
const { env } = await app.prepareCommand("start");
|
|
1366
|
+
const appHost = await new AkanAppHost(app, { env, withInk }).start();
|
|
1367
|
+
onStart?.();
|
|
1368
|
+
if (open)
|
|
1369
|
+
setTimeout(() => openBrowser(`http://localhost:${env.AKAN_PUBLIC_CLIENT_PORT ?? env.PORT ?? "8282"}`), 3000);
|
|
1370
|
+
return appHost;
|
|
1371
|
+
}
|
|
1372
|
+
async buildIos(app, { target, env = "debug", regenerate = false } = {}) {
|
|
1373
|
+
const targets = await resolveMobileTargets(app, target);
|
|
1374
|
+
await this.#buildMobileCsr(app, env);
|
|
1375
|
+
await this.#runMobileTargets(targets, async (mobileTarget) => {
|
|
1376
|
+
await new (await loadCapacitorApp())(app, mobileTarget.config).buildIos({ env, regenerate });
|
|
1377
|
+
});
|
|
1378
|
+
}
|
|
1379
|
+
async startIos(app, {
|
|
1380
|
+
open = false,
|
|
1381
|
+
operation = "local",
|
|
1382
|
+
env = "local",
|
|
1383
|
+
target,
|
|
1384
|
+
device,
|
|
1385
|
+
regenerate = false,
|
|
1386
|
+
noAllowProvisioningUpdates = false
|
|
1387
|
+
} = {}) {
|
|
1388
|
+
const targets = await resolveMobileTargets(app, target);
|
|
1389
|
+
if (operation === "release")
|
|
1390
|
+
await this.#buildMobileCsr(app, env);
|
|
1391
|
+
await this.#runMobileTargets(targets, async (mobileTarget) => {
|
|
1392
|
+
const capacitorApp = new (await loadCapacitorApp())(app, mobileTarget.config);
|
|
1393
|
+
await capacitorApp.runIos({ operation, env, regenerate, noAllowProvisioningUpdates, iosDeviceId: device });
|
|
1394
|
+
if (open)
|
|
1395
|
+
await capacitorApp.openIos();
|
|
1396
|
+
});
|
|
1397
|
+
}
|
|
1398
|
+
async releaseIos(app, { target, env = "main", regenerate = false } = {}) {
|
|
1399
|
+
const targets = await resolveMobileTargets(app, target);
|
|
1400
|
+
await this.#buildMobileCsr(app, env);
|
|
1401
|
+
for (const mobileTarget of targets) {
|
|
1402
|
+
await new (await loadCapacitorApp())(app, mobileTarget.config).buildIos({ env, regenerate });
|
|
1403
|
+
}
|
|
1404
|
+
}
|
|
1405
|
+
async buildAndroid(app, { target, env = "debug", regenerate = false } = {}) {
|
|
1406
|
+
const targets = await resolveMobileTargets(app, target);
|
|
1407
|
+
await this.#buildMobileCsr(app, env);
|
|
1408
|
+
await this.#runMobileTargets(targets, async (mobileTarget) => {
|
|
1409
|
+
await new (await loadCapacitorApp())(app, mobileTarget.config).buildAndroid("apk", { env, regenerate });
|
|
1410
|
+
});
|
|
1411
|
+
}
|
|
1412
|
+
async startAndroid(app, {
|
|
1413
|
+
open = false,
|
|
1414
|
+
operation = "local",
|
|
1415
|
+
env = "local",
|
|
1416
|
+
target,
|
|
1417
|
+
regenerate = false
|
|
1418
|
+
} = {}) {
|
|
1419
|
+
const targets = await resolveMobileTargets(app, target);
|
|
1420
|
+
if (operation === "release")
|
|
1421
|
+
await this.#buildMobileCsr(app, env);
|
|
1422
|
+
await this.#runMobileTargets(targets, async (mobileTarget) => {
|
|
1423
|
+
const capacitorApp = new (await loadCapacitorApp())(app, mobileTarget.config);
|
|
1424
|
+
await capacitorApp.runAndroid({ operation, env, regenerate });
|
|
1425
|
+
if (open)
|
|
1426
|
+
await capacitorApp.openAndroid();
|
|
1427
|
+
});
|
|
1428
|
+
}
|
|
1429
|
+
async releaseAndroid(app, assembleType, { target, env = "main", regenerate = false } = {}) {
|
|
1430
|
+
const targets = await resolveMobileTargets(app, target);
|
|
1431
|
+
await this.#buildMobileCsr(app, env);
|
|
1432
|
+
for (const mobileTarget of targets) {
|
|
1433
|
+
await new (await loadCapacitorApp())(app, mobileTarget.config).buildAndroid(assembleType, { env, regenerate });
|
|
1434
|
+
app.log(`Release Android ${app.name}/${mobileTarget.name} ${assembleType} Completed.`);
|
|
1435
|
+
app.log(`Path : ${app.cwdPath}/android/app/build/outputs/${assembleType === "apk" ? "apk" : "bundle"}/release`);
|
|
1436
|
+
}
|
|
1437
|
+
}
|
|
1438
|
+
async#buildMobileCsr(app, env) {
|
|
1439
|
+
const prevEnv = {
|
|
1440
|
+
AKAN_PUBLIC_ENV: process.env.AKAN_PUBLIC_ENV,
|
|
1441
|
+
AKAN_PUBLIC_OPERATION_MODE: process.env.AKAN_PUBLIC_OPERATION_MODE,
|
|
1442
|
+
APP_OPERATION_MODE: process.env.APP_OPERATION_MODE
|
|
1443
|
+
};
|
|
1444
|
+
Object.assign(process.env, {
|
|
1445
|
+
AKAN_PUBLIC_ENV: env,
|
|
1446
|
+
AKAN_PUBLIC_OPERATION_MODE: env === "local" ? "local" : "cloud",
|
|
1447
|
+
APP_OPERATION_MODE: "release"
|
|
1448
|
+
});
|
|
1449
|
+
try {
|
|
1450
|
+
await new (await loadBuildRunner())(app).build({ spinner: true });
|
|
1451
|
+
} finally {
|
|
1452
|
+
for (const [key, value] of Object.entries(prevEnv)) {
|
|
1453
|
+
if (value === undefined)
|
|
1454
|
+
delete process.env[key];
|
|
1455
|
+
else
|
|
1456
|
+
process.env[key] = value;
|
|
1457
|
+
}
|
|
1458
|
+
}
|
|
1459
|
+
}
|
|
1460
|
+
async#runMobileTargets(targets, task) {
|
|
1461
|
+
const results = [];
|
|
1462
|
+
for (const target of targets) {
|
|
1463
|
+
try {
|
|
1464
|
+
await task(target);
|
|
1465
|
+
results.push({ target: target.name });
|
|
1466
|
+
} catch (error) {
|
|
1467
|
+
results.push({ target: target.name, error });
|
|
1468
|
+
}
|
|
1469
|
+
}
|
|
1470
|
+
const failures = results.filter((result) => result.error);
|
|
1471
|
+
if (failures.length === 0)
|
|
1472
|
+
return;
|
|
1473
|
+
for (const failure of failures) {
|
|
1474
|
+
const message = failure.error instanceof Error ? failure.error.message : String(failure.error);
|
|
1475
|
+
Logger4.rawLog(`Mobile target ${failure.target} failed: ${message}`, undefined, "error");
|
|
1476
|
+
}
|
|
1477
|
+
throw new Error(`${failures.length}/${results.length} mobile targets failed`);
|
|
1478
|
+
}
|
|
1479
|
+
async codepush(app, os) {
|
|
1480
|
+
const [target] = await resolveMobileTargets(app, undefined);
|
|
1481
|
+
if (!target)
|
|
1482
|
+
throw new Error(`No mobile target configured for ${app.name}`);
|
|
1483
|
+
const capacitorApp = new (await loadCapacitorApp())(app, target.config);
|
|
1484
|
+
await capacitorApp.init();
|
|
1485
|
+
}
|
|
1486
|
+
#getLocalDatabaseServices(mode) {
|
|
1487
|
+
if (mode === "single")
|
|
1488
|
+
return [];
|
|
1489
|
+
if (mode === "multiple")
|
|
1490
|
+
return ["redis", "libsql"];
|
|
1491
|
+
return ["redis", "postgres"];
|
|
1492
|
+
}
|
|
1493
|
+
async#isLocalDatabaseUp(workspace, mode) {
|
|
1494
|
+
const requiredServices = this.#getLocalDatabaseServices(mode);
|
|
1495
|
+
if (!requiredServices.length)
|
|
1496
|
+
return true;
|
|
1497
|
+
const output = await workspace.spawn("docker", ["compose", "ps", "--services", "--status", "running"], {
|
|
1498
|
+
cwd: `${workspace.workspaceRoot}/local`
|
|
1499
|
+
});
|
|
1500
|
+
const runningServices = new Set(output.split(/\s+/).filter(Boolean));
|
|
1501
|
+
return requiredServices.every((service) => runningServices.has(service));
|
|
1502
|
+
}
|
|
1503
|
+
async dbup(workspace, mode = "multiple") {
|
|
1504
|
+
if (mode === "single")
|
|
1505
|
+
return true;
|
|
1506
|
+
try {
|
|
1507
|
+
await workspace.applyTemplate({
|
|
1508
|
+
basePath: "local",
|
|
1509
|
+
template: "localDev",
|
|
1510
|
+
dict: { repoName: workspace.repoName },
|
|
1511
|
+
overwrite: false
|
|
1512
|
+
});
|
|
1513
|
+
const wasAlreadyUp = await this.#isLocalDatabaseUp(workspace, mode);
|
|
1514
|
+
if (!wasAlreadyUp)
|
|
1515
|
+
await workspace.spawn(`docker`, ["compose", "up", "-d", ...this.#getLocalDatabaseServices(mode)], {
|
|
1516
|
+
cwd: `${workspace.workspaceRoot}/local`
|
|
1517
|
+
});
|
|
1518
|
+
return wasAlreadyUp;
|
|
1519
|
+
} catch (error) {
|
|
1520
|
+
const detail = error instanceof Error ? error.message : typeof error === "string" ? error : JSON.stringify(error) || "Unknown error";
|
|
1521
|
+
throw new Error([
|
|
1522
|
+
"Docker daemon may not be running. Please install Docker or start the Docker daemon and try again.",
|
|
1523
|
+
`Original error:
|
|
1524
|
+
${detail}`
|
|
1525
|
+
].join(`
|
|
1526
|
+
|
|
1527
|
+
`));
|
|
1528
|
+
}
|
|
1529
|
+
}
|
|
1530
|
+
async dbdown(workspace) {
|
|
1531
|
+
await workspace.spawn(`docker`, ["compose", "down"], { cwd: `${workspace.workspaceRoot}/local` });
|
|
1532
|
+
}
|
|
1533
|
+
async configureApp(app) {
|
|
1534
|
+
const [target] = await resolveMobileTargets(app, undefined);
|
|
1535
|
+
if (!target)
|
|
1536
|
+
throw new Error(`No mobile target configured for ${app.name}`);
|
|
1537
|
+
const capacitorApp = new (await loadCapacitorApp())(app, target.config);
|
|
1538
|
+
await capacitorApp.init();
|
|
1539
|
+
if (await (await loadPrompts()).confirm({ message: "want to add camera permission?" }))
|
|
1540
|
+
await capacitorApp.addCamera();
|
|
1541
|
+
if (await (await loadPrompts()).confirm({ message: "want to add contact permission?" }))
|
|
1542
|
+
await capacitorApp.addContact();
|
|
1543
|
+
if (await (await loadPrompts()).confirm({ message: "want to add location permission?" }))
|
|
1544
|
+
await capacitorApp.addLocation();
|
|
1545
|
+
await capacitorApp.save();
|
|
1546
|
+
}
|
|
1547
|
+
async releaseSource(app, { rebuild, buildNum = 0, environment = "debug", local = true } = {}) {
|
|
1548
|
+
await new (await loadReleasePackager())(app, { build: () => this.build(app).then(() => {
|
|
1549
|
+
return;
|
|
1550
|
+
}) }).releaseSource({
|
|
1551
|
+
rebuild,
|
|
1552
|
+
buildNum,
|
|
1553
|
+
environment,
|
|
1554
|
+
local
|
|
1555
|
+
});
|
|
1556
|
+
return;
|
|
1557
|
+
}
|
|
1558
|
+
async createApplicationTemplate(workspace, appName) {
|
|
1559
|
+
await workspace.applyTemplate({ basePath: `apps/${appName}`, template: "appRoot", dict: { appName } });
|
|
1560
|
+
}
|
|
1561
|
+
async compressProjectFiles(app, { rebuild, buildNum = 0, environment = "debug", local = true } = {}) {
|
|
1562
|
+
await new (await loadReleasePackager())(app, {
|
|
1563
|
+
build: () => this.build(app).then(() => {
|
|
1564
|
+
return;
|
|
1565
|
+
})
|
|
1566
|
+
}).compressProjectFiles({
|
|
1567
|
+
rebuild,
|
|
1568
|
+
buildNum,
|
|
1569
|
+
environment,
|
|
1570
|
+
local
|
|
1571
|
+
});
|
|
1572
|
+
return;
|
|
1573
|
+
}
|
|
1574
|
+
async generateApplicationTemplate(app) {
|
|
1575
|
+
const openAIApiKey = process.env.OPENAI_API_KEY;
|
|
1576
|
+
if (!openAIApiKey)
|
|
1577
|
+
throw new Error("OPENAI_API_KEY is not set");
|
|
1578
|
+
const [{ StringOutputParser }, { PromptTemplate }, { RunnableSequence }, { ChatOpenAI }, prompts] = await Promise.all([
|
|
1579
|
+
import("@langchain/core/output_parsers"),
|
|
1580
|
+
import("@langchain/core/prompts"),
|
|
1581
|
+
import("@langchain/core/runnables"),
|
|
1582
|
+
import("@langchain/openai"),
|
|
1583
|
+
loadPrompts()
|
|
1584
|
+
]);
|
|
1585
|
+
const chatModel = new ChatOpenAI({ modelName: "gpt-4o", openAIApiKey });
|
|
1586
|
+
const projectName = await prompts.input({ message: "please enter project name." });
|
|
1587
|
+
const projectDesc = await prompts.input({
|
|
1588
|
+
message: "please enter project description. (40 ~ 60 characters)"
|
|
1589
|
+
});
|
|
1590
|
+
const spinner = ora("Gerating project files...");
|
|
1591
|
+
const mainPrompt = PromptTemplate.fromTemplate(`prompt.requestApplication()`);
|
|
1592
|
+
const chain = RunnableSequence.from([mainPrompt, chatModel, new StringOutputParser]);
|
|
1593
|
+
await chain.invoke({ projectName, projectDesc });
|
|
1594
|
+
spinner.succeed("Loading complete!");
|
|
1595
|
+
//! add path in tsconfig.json
|
|
1596
|
+
}
|
|
1597
|
+
async testApplication(app) {}
|
|
1598
|
+
}
|
|
1599
|
+
|
|
1600
|
+
// pkgs/@akanjs/cli/application/application.script.ts
|
|
1601
|
+
class ApplicationScript extends script("application", [ApplicationRunner, LibraryScript]) {
|
|
1602
|
+
async confirmDatabaseModeDependencyInstall(databaseMode, installSpecs) {
|
|
1603
|
+
return await confirm({
|
|
1604
|
+
message: [
|
|
1605
|
+
`Database mode '${databaseMode}' requires missing dependencies: ${installSpecs.join(", ")}.`,
|
|
1606
|
+
"Install them now?"
|
|
1607
|
+
].join(" "),
|
|
1608
|
+
default: true
|
|
1609
|
+
});
|
|
1610
|
+
}
|
|
1611
|
+
async syncDatabaseModeDependencies(app, akanConfig, databaseMode) {
|
|
1612
|
+
const installSpecs = akanConfig.getMissingDatabaseModeDependencySpecs(databaseMode);
|
|
1613
|
+
if (installSpecs.length === 0)
|
|
1614
|
+
return;
|
|
1615
|
+
const shouldInstall = await this.confirmDatabaseModeDependencyInstall(databaseMode, installSpecs);
|
|
1616
|
+
if (!shouldInstall)
|
|
1617
|
+
throw new Error(`Database mode '${databaseMode}' requires missing dependencies: ${installSpecs.join(", ")}.`);
|
|
1618
|
+
const spinner = app.workspace.spinning(`Installing database dependencies for ${databaseMode} mode...`);
|
|
1619
|
+
try {
|
|
1620
|
+
await app.workspace.spawn("bun", ["add", ...installSpecs], {
|
|
1621
|
+
stdio: "inherit"
|
|
1622
|
+
});
|
|
1623
|
+
await app.workspace.getPackageJson({ refresh: true });
|
|
1624
|
+
spinner.succeed(`Installed database dependencies for ${databaseMode} mode`);
|
|
1625
|
+
} catch (error) {
|
|
1626
|
+
spinner.fail(`Failed to install database dependencies for ${databaseMode} mode`);
|
|
1627
|
+
throw error;
|
|
1628
|
+
}
|
|
1629
|
+
}
|
|
1630
|
+
async confirmMobileDependencyInstall(installSpecs) {
|
|
1631
|
+
return await confirm({
|
|
1632
|
+
message: [`Mobile builds require missing dependencies: ${installSpecs.join(", ")}.`, "Install them now?"].join(" "),
|
|
1633
|
+
default: true
|
|
1634
|
+
});
|
|
1635
|
+
}
|
|
1636
|
+
async syncMobileDependencies(app, akanConfig) {
|
|
1637
|
+
const installSpecs = akanConfig.getMissingMobileDependencySpecs();
|
|
1638
|
+
if (installSpecs.length === 0)
|
|
1639
|
+
return;
|
|
1640
|
+
const shouldInstall = await this.confirmMobileDependencyInstall(installSpecs);
|
|
1641
|
+
if (!shouldInstall)
|
|
1642
|
+
throw new Error(`Mobile builds require missing dependencies: ${installSpecs.join(", ")}.`);
|
|
1643
|
+
const spinner = app.workspace.spinning("Installing mobile dependencies...");
|
|
1644
|
+
try {
|
|
1645
|
+
await app.workspace.spawn("bun", ["add", ...installSpecs], {
|
|
1646
|
+
stdio: "inherit"
|
|
1647
|
+
});
|
|
1648
|
+
await app.workspace.getPackageJson({ refresh: true });
|
|
1649
|
+
spinner.succeed("Installed mobile dependencies");
|
|
1650
|
+
} catch (error) {
|
|
1651
|
+
spinner.fail("Failed to install mobile dependencies");
|
|
1652
|
+
throw error;
|
|
1653
|
+
}
|
|
1654
|
+
}
|
|
1655
|
+
async syncMobileAppCapacitorPlugins(app, akanConfig) {
|
|
1656
|
+
const plugins = akanConfig.getMobileAppCapacitorPlugins();
|
|
1657
|
+
if (plugins.length === 0)
|
|
1658
|
+
return;
|
|
1659
|
+
const packageJson = await app.getPackageJson({ refresh: true });
|
|
1660
|
+
const dependencies = packageJson.dependencies ?? {};
|
|
1661
|
+
const missing = plugins.filter((plugin) => !dependencies[plugin]);
|
|
1662
|
+
if (missing.length === 0)
|
|
1663
|
+
return;
|
|
1664
|
+
const spinner = app.workspace.spinning(`Adding default Capacitor plugins to ${app.name}...`);
|
|
1665
|
+
try {
|
|
1666
|
+
packageJson.dependencies = { ...dependencies, ...Object.fromEntries(missing.map((plugin) => [plugin, "*"])) };
|
|
1667
|
+
await app.setPackageJson(packageJson);
|
|
1668
|
+
await app.workspace.spawn("bun", ["install"], { stdio: "inherit" });
|
|
1669
|
+
await app.getPackageJson({ refresh: true });
|
|
1670
|
+
spinner.succeed(`Added default Capacitor plugins to ${app.name}: ${missing.join(", ")}`);
|
|
1671
|
+
} catch (error) {
|
|
1672
|
+
spinner.fail(`Failed to add default Capacitor plugins to ${app.name}`);
|
|
1673
|
+
throw error;
|
|
1674
|
+
}
|
|
1675
|
+
}
|
|
1676
|
+
async createApplication(appName, workspace, { start = false, libs = [] } = {}) {
|
|
1677
|
+
const spinner = workspace.spinning("Creating application...");
|
|
1678
|
+
const app = await this.applicationRunner.createApplication(appName, workspace, libs);
|
|
1679
|
+
spinner.succeed(`Application created in apps/${app.name}`);
|
|
1680
|
+
await app.scanSync();
|
|
1681
|
+
if (start)
|
|
1682
|
+
await this.start(app, { open: true });
|
|
1683
|
+
}
|
|
1684
|
+
async removeApplication(app) {
|
|
1685
|
+
const spinner = app.spinning("Removing application...");
|
|
1686
|
+
await this.applicationRunner.removeApplication(app);
|
|
1687
|
+
spinner.succeed(`Application ${app.name} (apps/${app.name}) removed`);
|
|
1688
|
+
}
|
|
1689
|
+
async sync(sys) {
|
|
1690
|
+
if (sys.type === "app")
|
|
1691
|
+
await sys.scanSync();
|
|
1692
|
+
else
|
|
1693
|
+
await this.libraryScript.syncLibrary(sys);
|
|
1694
|
+
}
|
|
1695
|
+
async script(app, filename) {
|
|
1696
|
+
const scriptFilename = filename ?? await this.applicationRunner.getScriptFilename(app);
|
|
1697
|
+
await app.scanSync();
|
|
1698
|
+
await this.applicationRunner.runScript(app, scriptFilename);
|
|
1699
|
+
}
|
|
1700
|
+
async console(app) {
|
|
1701
|
+
await app.scanSync();
|
|
1702
|
+
await this.applicationRunner.runConsole(app);
|
|
1703
|
+
}
|
|
1704
|
+
async build(app, { write = true, fast = false, quiet = false } = {}) {
|
|
1705
|
+
await app.scanSync({ write });
|
|
1706
|
+
if (!quiet)
|
|
1707
|
+
Logger5.rawLog(`Creating an optimized production build for ${app.name}...`);
|
|
1708
|
+
try {
|
|
1709
|
+
const result = await this.applicationRunner.build(app, {
|
|
1710
|
+
fast,
|
|
1711
|
+
spinner: !quiet
|
|
1712
|
+
});
|
|
1713
|
+
Logger5.rawLog(`${app.name} built in dist/apps/${app.name}`);
|
|
1714
|
+
if (!quiet)
|
|
1715
|
+
ApplicationBuildReporter.printSummary(result);
|
|
1716
|
+
} catch (error) {
|
|
1717
|
+
Logger5.rawLog(`${app.name} build failed in dist/apps/${app.name}`, undefined, "error");
|
|
1718
|
+
Logger5.rawLog(ApplicationBuildReporter.formatError(error), undefined, "error");
|
|
1719
|
+
throw error;
|
|
1720
|
+
}
|
|
1721
|
+
}
|
|
1722
|
+
async typecheck(app, { write = true, clean = false, incremental = true } = {}) {
|
|
1723
|
+
await app.scanSync({ write });
|
|
1724
|
+
const spinner = app.spinning(`Typechecking ${app.name}...`);
|
|
1725
|
+
try {
|
|
1726
|
+
await this.applicationRunner.typecheck(app, { clean, incremental });
|
|
1727
|
+
spinner.succeed(`${app.name} typechecked`);
|
|
1728
|
+
} catch (error) {
|
|
1729
|
+
spinner.fail(`${app.name} typecheck failed`);
|
|
1730
|
+
Logger5.rawLog(ApplicationBuildReporter.formatError(error), undefined, "error");
|
|
1731
|
+
throw error;
|
|
1732
|
+
}
|
|
1733
|
+
}
|
|
1734
|
+
async test(exec, { write = true } = {}) {
|
|
1735
|
+
if (exec instanceof LibExecutor) {
|
|
1736
|
+
await this.libraryScript.syncLibrary(exec);
|
|
1737
|
+
const spinner2 = exec.spinning(`Preparing ${exec.name}...`);
|
|
1738
|
+
spinner2.succeed(`${exec.name} prepared`);
|
|
1739
|
+
await this.applicationRunner.test(exec);
|
|
1740
|
+
return;
|
|
1741
|
+
}
|
|
1742
|
+
const spinner = exec.spinning(`Preparing ${exec.name}...`);
|
|
1743
|
+
try {
|
|
1744
|
+
if (exec instanceof PkgExecutor)
|
|
1745
|
+
await exec.scan({ refresh: true });
|
|
1746
|
+
else
|
|
1747
|
+
await exec.scanSync({ write });
|
|
1748
|
+
spinner.succeed(`${exec.name} prepared`);
|
|
1749
|
+
} catch (error) {
|
|
1750
|
+
spinner.fail(`${exec.name} prepare failed`);
|
|
1751
|
+
throw error;
|
|
1752
|
+
}
|
|
1753
|
+
await this.applicationRunner.test(exec);
|
|
1754
|
+
}
|
|
1755
|
+
async start(app, {
|
|
1756
|
+
open = false,
|
|
1757
|
+
dbup = true,
|
|
1758
|
+
withInk = false,
|
|
1759
|
+
write = true
|
|
1760
|
+
} = {}) {
|
|
1761
|
+
await app.scanSync({ write });
|
|
1762
|
+
const akanConfig = await app.getConfig();
|
|
1763
|
+
const databaseMode = process.env.AKAN_DATABASE_MODE ?? akanConfig.defaultDatabaseMode ?? "single";
|
|
1764
|
+
await this.syncDatabaseModeDependencies(app, akanConfig, databaseMode);
|
|
1765
|
+
if (app.getEnv() === "local" && dbup && databaseMode !== "single") {
|
|
1766
|
+
const wasDbAlreadyUp = await this.dbup(app.workspace, databaseMode);
|
|
1767
|
+
if (!wasDbAlreadyUp)
|
|
1768
|
+
process.on("SIGINT", async () => {
|
|
1769
|
+
await this.dbdown(app.workspace);
|
|
1770
|
+
process.exit(0);
|
|
1771
|
+
});
|
|
1772
|
+
}
|
|
1773
|
+
const spinner = app.spinning("Preparing backend...");
|
|
1774
|
+
const akanAppHost = await this.applicationRunner.start(app, {
|
|
1775
|
+
open,
|
|
1776
|
+
onStart: () => {
|
|
1777
|
+
spinner.succeed(`${app.name} prepared, ready to start`);
|
|
1778
|
+
},
|
|
1779
|
+
withInk
|
|
1780
|
+
});
|
|
1781
|
+
return akanAppHost;
|
|
1782
|
+
}
|
|
1783
|
+
async buildIos(app, { write = true, target, env = "debug", regenerate = false } = {}) {
|
|
1784
|
+
await app.scanSync({ write });
|
|
1785
|
+
await this.applicationRunner.buildIos(app, { target, env, regenerate });
|
|
1786
|
+
}
|
|
1787
|
+
async startIos(app, {
|
|
1788
|
+
open = false,
|
|
1789
|
+
operation = "local",
|
|
1790
|
+
env = "local",
|
|
1791
|
+
write = true,
|
|
1792
|
+
target,
|
|
1793
|
+
device,
|
|
1794
|
+
regenerate = false,
|
|
1795
|
+
noAllowProvisioningUpdates = false
|
|
1796
|
+
} = {}) {
|
|
1797
|
+
await app.scanSync({ write });
|
|
1798
|
+
const akanConfig = await app.getConfig();
|
|
1799
|
+
await this.syncMobileDependencies(app, akanConfig);
|
|
1800
|
+
await this.syncMobileAppCapacitorPlugins(app, akanConfig);
|
|
1801
|
+
await this.applicationRunner.startIos(app, {
|
|
1802
|
+
open,
|
|
1803
|
+
operation,
|
|
1804
|
+
env,
|
|
1805
|
+
target,
|
|
1806
|
+
device,
|
|
1807
|
+
regenerate,
|
|
1808
|
+
noAllowProvisioningUpdates
|
|
1809
|
+
});
|
|
1810
|
+
}
|
|
1811
|
+
async releaseIos(app, { write = true, target, env = "main", regenerate = false, allowLocalRelease = false } = {}) {
|
|
1812
|
+
await app.scanSync({ write });
|
|
1813
|
+
if (env === "local" && !allowLocalRelease)
|
|
1814
|
+
throw new Error("releaseIos --env local is blocked. Pass allowLocalRelease only for explicit local release testing.");
|
|
1815
|
+
await this.applicationRunner.releaseIos(app, { target, env, regenerate });
|
|
1816
|
+
}
|
|
1817
|
+
async buildAndroid(app, { write = true, target, env = "debug", regenerate = false } = {}) {
|
|
1818
|
+
await app.scanSync({ write });
|
|
1819
|
+
await this.applicationRunner.buildAndroid(app, { target, env, regenerate });
|
|
1820
|
+
}
|
|
1821
|
+
async startAndroid(app, {
|
|
1822
|
+
open = false,
|
|
1823
|
+
operation = "local",
|
|
1824
|
+
env = "local",
|
|
1825
|
+
write = true,
|
|
1826
|
+
target,
|
|
1827
|
+
regenerate = false
|
|
1828
|
+
} = {}) {
|
|
1829
|
+
await app.scanSync({ write });
|
|
1830
|
+
const akanConfig = await app.getConfig();
|
|
1831
|
+
await this.syncMobileDependencies(app, akanConfig);
|
|
1832
|
+
await this.syncMobileAppCapacitorPlugins(app, akanConfig);
|
|
1833
|
+
await this.applicationRunner.startAndroid(app, {
|
|
1834
|
+
open,
|
|
1835
|
+
operation,
|
|
1836
|
+
env,
|
|
1837
|
+
target,
|
|
1838
|
+
regenerate
|
|
1839
|
+
});
|
|
1840
|
+
}
|
|
1841
|
+
async releaseAndroid(app, assembleType, { write = true, target, env = "main", regenerate = false, allowLocalRelease = false } = {}) {
|
|
1842
|
+
await app.scanSync({ write });
|
|
1843
|
+
if (env === "local" && !allowLocalRelease)
|
|
1844
|
+
throw new Error("releaseAndroid --env local is blocked. Pass allowLocalRelease only for explicit local release testing.");
|
|
1845
|
+
await this.applicationRunner.releaseAndroid(app, assembleType, {
|
|
1846
|
+
target,
|
|
1847
|
+
env,
|
|
1848
|
+
regenerate
|
|
1849
|
+
});
|
|
1850
|
+
}
|
|
1851
|
+
async configureApp(app) {
|
|
1852
|
+
await this.applicationRunner.configureApp(app);
|
|
1853
|
+
}
|
|
1854
|
+
async releaseSource(app, options) {
|
|
1855
|
+
await this.applicationRunner.releaseSource(app, options);
|
|
1856
|
+
}
|
|
1857
|
+
async codepush(app, os) {
|
|
1858
|
+
await this.applicationRunner.codepush(app, os);
|
|
1859
|
+
}
|
|
1860
|
+
async dbup(workspace, mode = "multiple") {
|
|
1861
|
+
const spinner = workspace.spinning(`Starting local database (${mode})...`);
|
|
1862
|
+
const wasAlreadyUp = await this.applicationRunner.dbup(workspace, mode);
|
|
1863
|
+
spinner.succeed(wasAlreadyUp ? `Local database (${mode}) was already up` : `Local database (${mode}) is up`);
|
|
1864
|
+
return wasAlreadyUp;
|
|
1865
|
+
}
|
|
1866
|
+
async dbdown(workspace) {
|
|
1867
|
+
const spinner = workspace.spinning("Stopping local database...");
|
|
1868
|
+
await this.applicationRunner.dbdown(workspace);
|
|
1869
|
+
spinner.succeed("Local database (/local/docker-compose.yaml) is down");
|
|
1870
|
+
}
|
|
1871
|
+
async testSys(sys) {
|
|
1872
|
+
if (sys.type === "app")
|
|
1873
|
+
await this.testApplication(sys);
|
|
1874
|
+
else
|
|
1875
|
+
await this.libraryScript.testLibrary(sys);
|
|
1876
|
+
}
|
|
1877
|
+
async testApplication(app) {
|
|
1878
|
+
const spinner = app.spinning("Testing application...");
|
|
1879
|
+
await this.applicationRunner.testApplication(app);
|
|
1880
|
+
spinner.succeed(`Application ${app.name} (apps/${app.name}) test is successful`);
|
|
1881
|
+
}
|
|
1882
|
+
}
|
|
1883
|
+
|
|
1884
|
+
export { openBrowser, ApplicationScript };
|