@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.
Files changed (69) hide show
  1. package/agent.command-h4afc69n.js +26 -0
  2. package/application.command-4ctkfdan.js +165 -0
  3. package/applicationBuildRunner-esa1kj7v.js +284 -0
  4. package/applicationReleasePackager-brvth6rs.js +245 -0
  5. package/capacitorApp-y0h6cgft.js +58 -0
  6. package/cloud.command-rpztn4fh.js +94 -0
  7. package/commandManifest.json +1 -0
  8. package/context.command-nqbtak4f.js +82 -0
  9. package/dependencyScanner-m4x5maek.js +9 -0
  10. package/guideline.command-ya0dh44f.js +356 -0
  11. package/incrementalBuilder.proc.js +89 -17394
  12. package/index-1xdrsbry.js +1447 -0
  13. package/index-45aj5ry0.js +990 -0
  14. package/index-5vvwc0cz.js +559 -0
  15. package/index-61keag0s.js +40 -0
  16. package/index-6pz1j0zj.js +62 -0
  17. package/index-73pr2cmy.js +534 -0
  18. package/index-76rn3g2c.js +76 -0
  19. package/index-85msc0wg.js +161 -0
  20. package/index-8pkbzj26.js +840 -0
  21. package/index-8rc0bm04.js +514 -0
  22. package/index-9sp6fsc5.js +1884 -0
  23. package/index-a6sbyy0b.js +2769 -0
  24. package/index-b0brjbp3.js +83 -0
  25. package/index-fgc8r6dj.js +33 -0
  26. package/index-h6ca6qg0.js +2777 -0
  27. package/index-hdqztm58.js +758 -0
  28. package/index-hwzpw9c1.js +202 -0
  29. package/index-pmm9e2jf.js +120 -0
  30. package/index-qaq13qk3.js +80 -0
  31. package/index-qhtr07v8.js +1072 -0
  32. package/index-r24hmh0q.js +4 -0
  33. package/index-ss469dec.js +11 -0
  34. package/index-swf4bmbg.js +25 -0
  35. package/index-w7fyqjrw.js +462 -0
  36. package/index-wnp7hwq7.js +193 -0
  37. package/index-wq8jwx8z.js +224 -0
  38. package/index-x53a5nya.js +301 -0
  39. package/index-xmc2w32q.js +4359 -0
  40. package/index-y3hdhy4p.js +229 -0
  41. package/index.js +54 -23340
  42. package/library.command-r15zdqvp.js +33 -0
  43. package/localRegistry.command-5tcahs3f.js +178 -0
  44. package/module.command-qrj3kmyz.js +54 -0
  45. package/package.command-r8sq5kzp.js +43 -0
  46. package/package.json +2 -2
  47. package/page.command-c6xdx0xm.js +24 -0
  48. package/primitive.command-pv9ssmtf.js +62 -0
  49. package/quality.command-es67wvdp.js +809 -0
  50. package/repair.command-677675vw.js +67 -0
  51. package/routeSourceValidator-wbhmbwpj.js +132 -0
  52. package/scalar.command-kabkd6wd.js +35 -0
  53. package/templates/app/page/_layout.tsx +19 -1
  54. package/templates/app/public/fonts/Pretendard-Black.woff2 +0 -0
  55. package/templates/app/public/fonts/Pretendard-Bold.woff2 +0 -0
  56. package/templates/app/public/fonts/Pretendard-ExtraBold.woff2 +0 -0
  57. package/templates/app/public/fonts/Pretendard-ExtraLight.woff2 +0 -0
  58. package/templates/app/public/fonts/Pretendard-Light.woff2 +0 -0
  59. package/templates/app/public/fonts/Pretendard-Medium.woff2 +0 -0
  60. package/templates/app/public/fonts/Pretendard-Regular.woff2 +0 -0
  61. package/templates/app/public/fonts/Pretendard-SemiBold.woff2 +0 -0
  62. package/templates/app/public/fonts/Pretendard-Thin.woff2 +0 -0
  63. package/templates/facetIndex/index.ts +1 -1
  64. package/typeChecker-kravn7ns.js +8 -0
  65. package/typecheck.proc.js +4 -194
  66. package/workflow.command-64r6cw0w.js +108 -0
  67. package/workspace.command-vrws0rgx.js +435 -0
  68. package/README.ko.md +0 -72
  69. package/README.md +0 -85
@@ -0,0 +1,4 @@
1
+ // @bun
2
+ var __require = import.meta.require;
3
+
4
+ export { __require };
@@ -0,0 +1,11 @@
1
+ // @bun
2
+ // pkgs/@akanjs/cli/pluralizeName.ts
3
+ function pluralizeName(name) {
4
+ if (/[^aeiou]y$/i.test(name))
5
+ return `${name.slice(0, -1)}ies`;
6
+ if (/(s|x|z|ch|sh)$/i.test(name))
7
+ return `${name}es`;
8
+ return `${name}s`;
9
+ }
10
+
11
+ export { pluralizeName };
@@ -0,0 +1,25 @@
1
+ // @bun
2
+ import {
3
+ runner,
4
+ script
5
+ } from "./index-hdqztm58.js";
6
+
7
+ // pkgs/@akanjs/cli/page/page.runner.ts
8
+ class PageRunner extends runner("page") {
9
+ async createCrudPage(module, { app, basePath, single = false }) {
10
+ await app.applyTemplate({
11
+ basePath: basePath ?? `page/(${app.name})/(public)/${module.name}`,
12
+ template: single ? "crudSinglePage" : "crudPages",
13
+ dict: { model: module.name, appName: module.sys.name }
14
+ });
15
+ }
16
+ }
17
+
18
+ // pkgs/@akanjs/cli/page/page.script.ts
19
+ class PageScript extends script("page", [PageRunner]) {
20
+ async createCrudPage(module, { app, basePath, single = false }) {
21
+ await this.pageRunner.createCrudPage(module, { app, basePath, single });
22
+ }
23
+ }
24
+
25
+ export { PageScript };
@@ -0,0 +1,462 @@
1
+ // @bun
2
+ import {
3
+ openBrowser
4
+ } from "./index-9sp6fsc5.js";
5
+ import {
6
+ AiSession,
7
+ CloudApi,
8
+ GlobalConfig,
9
+ getDefaultHostConfig
10
+ } from "./index-5vvwc0cz.js";
11
+ import {
12
+ runner
13
+ } from "./index-hdqztm58.js";
14
+ import {
15
+ AppExecutor,
16
+ WorkspaceExecutor
17
+ } from "./index-a6sbyy0b.js";
18
+
19
+ // pkgs/@akanjs/cli/cloud/cloud.runner.ts
20
+ import path from "path";
21
+ import { confirm, input, select } from "@inquirer/prompts";
22
+ import { Logger, sleep } from "akanjs/common";
23
+ import chalk from "chalk";
24
+ import * as QRcode from "qrcode";
25
+
26
+ // pkgs/@akanjs/cli/npmRegistry.ts
27
+ var defaultNpmRegistry = "https://registry.npmjs.org";
28
+ var getNpmRegistryUrl = (registryUrl = process.env.AKAN_NPM_REGISTRY ?? defaultNpmRegistry) => registryUrl.replace(/\/+$/, "");
29
+ async function getLatestPackageVersion(packageName, tag = "latest", registryUrl) {
30
+ const registry = getNpmRegistryUrl(registryUrl);
31
+ const url = `${registry}/${encodeURIComponent(packageName).replace(/^%40/, "@")}`;
32
+ const res = await fetch(url);
33
+ if (!res.ok)
34
+ throw new Error(`Failed to fetch ${packageName} metadata from ${registry}`);
35
+ const metadata = await res.json();
36
+ const version = metadata["dist-tags"]?.[tag];
37
+ if (!version)
38
+ throw new Error(`No npm dist-tag "${tag}" found for ${packageName}`);
39
+ return version;
40
+ }
41
+
42
+ // pkgs/@akanjs/cli/cloud/cloud.runner.ts
43
+ var addRemoteEnvServerValue = "__addRemoteEnvServer";
44
+ var removeRemoteEnvServerValue = "__removeRemoteEnvServer";
45
+
46
+ class CloudRunner extends runner("cloud") {
47
+ #akanFrameworkPackages = new Set(["akanjs", "@akanjs/devkit", "@akanjs/cli", "create-akan-workspace"]);
48
+ #getRegistryArgs(registryUrl) {
49
+ return registryUrl ? ["--registry", getNpmRegistryUrl(registryUrl)] : [];
50
+ }
51
+ #getLocalRegistryAuthArgs(registryUrl) {
52
+ if (!registryUrl)
53
+ return [];
54
+ const { host, pathname } = new URL(getNpmRegistryUrl(registryUrl));
55
+ const registryPath = pathname === "/" ? "/" : `${pathname.replace(/\/+$/, "")}/`;
56
+ return [`--//${host}${registryPath}:_authToken=akan-local-registry`];
57
+ }
58
+ #getRegistryEnv(registryUrl) {
59
+ return registryUrl ? {
60
+ ...process.env,
61
+ AKAN_NPM_REGISTRY: getNpmRegistryUrl(registryUrl),
62
+ NPM_CONFIG_REGISTRY: getNpmRegistryUrl(registryUrl)
63
+ } : process.env;
64
+ }
65
+ async#addRemoteEnvServer() {
66
+ const name = (await input({
67
+ message: "Remote server name: ",
68
+ validate: (value) => value.trim() ? true : "Remote server name is required"
69
+ })).trim();
70
+ const host = (await input({
71
+ message: "Remote server host: ",
72
+ validate: (value) => value.trim() ? true : "Remote server host is required"
73
+ })).trim();
74
+ const username = (await input({ message: "Remote server username (optional): " })).trim() || undefined;
75
+ const portInput = (await input({
76
+ message: "Remote server SSH port (optional): ",
77
+ validate: (value) => {
78
+ const trimmed = value.trim();
79
+ if (!trimmed)
80
+ return true;
81
+ const port = Number(trimmed);
82
+ return Number.isInteger(port) && port > 0 ? true : "SSH port must be a positive integer";
83
+ }
84
+ })).trim();
85
+ const config = {
86
+ host,
87
+ ...username ? { username } : {},
88
+ ...portInput ? { port: Number(portInput) } : {}
89
+ };
90
+ await GlobalConfig.setRemoteEnvServer(name, config);
91
+ return { name, config };
92
+ }
93
+ async#selectRemoteEnvServer() {
94
+ const servers = await GlobalConfig.getRemoteEnvServers();
95
+ const serverEntries = Object.entries(servers).sort(([nameA], [nameB]) => nameA.localeCompare(nameB));
96
+ if (serverEntries.length === 0) {
97
+ Logger.info("No remote env servers configured. Add the first remote server for SCP mode.");
98
+ return await this.#addRemoteEnvServer();
99
+ }
100
+ const selectedName = await select({
101
+ message: "Select the remote env server",
102
+ choices: [
103
+ ...serverEntries.map(([name, config2]) => ({
104
+ name: `${name} (${config2.username ? `${config2.username}@` : ""}${config2.host}${config2.port ? `:${config2.port}` : ""})`,
105
+ value: name
106
+ })),
107
+ { name: "Add new remote server", value: addRemoteEnvServerValue },
108
+ { name: "Remove remote server", value: removeRemoteEnvServerValue }
109
+ ]
110
+ });
111
+ if (selectedName === addRemoteEnvServerValue)
112
+ return await this.#addRemoteEnvServer();
113
+ if (selectedName === removeRemoteEnvServerValue) {
114
+ await this.#removeRemoteEnvServer(serverEntries);
115
+ return await this.#selectRemoteEnvServer();
116
+ }
117
+ const config = servers[selectedName];
118
+ if (!config)
119
+ throw new Error(`Remote env server is not found: ${selectedName}`);
120
+ return { name: selectedName, config };
121
+ }
122
+ async#removeRemoteEnvServer(serverEntries) {
123
+ const selectedName = await select({
124
+ message: "Select the remote env server to remove",
125
+ choices: serverEntries.map(([name, config]) => ({
126
+ name: `${name} (${config.username ? `${config.username}@` : ""}${config.host}${config.port ? `:${config.port}` : ""})`,
127
+ value: name
128
+ }))
129
+ });
130
+ const shouldRemove = await confirm({
131
+ message: `Remove remote env server "${selectedName}"?`,
132
+ default: false
133
+ });
134
+ if (!shouldRemove)
135
+ return;
136
+ await GlobalConfig.removeRemoteEnvServer(selectedName);
137
+ Logger.info(`Removed remote env server "${selectedName}"`);
138
+ }
139
+ async#getRemoteEnvServerWithUsername() {
140
+ const remoteServer = await this.#selectRemoteEnvServer();
141
+ if (remoteServer.config.username)
142
+ return remoteServer;
143
+ const username = (await input({
144
+ message: `SSH username for ${remoteServer.config.host} (optional): `
145
+ })).trim();
146
+ return {
147
+ ...remoteServer,
148
+ config: {
149
+ ...remoteServer.config,
150
+ ...username ? { username } : {}
151
+ }
152
+ };
153
+ }
154
+ #getRemoteEnvArchivePath() {
155
+ return `${this.#getRemoteEnvArchiveDir()}/env.tar`;
156
+ }
157
+ #getRemoteEnvArchiveDir() {
158
+ const { repoName } = WorkspaceExecutor.getBaseDevEnv();
159
+ return `~/secrets/${repoName}`;
160
+ }
161
+ #getScpTarget(config, remotePath) {
162
+ return `${config.username ? `${config.username}@` : ""}${config.host}:${remotePath}`;
163
+ }
164
+ #getSshTarget(config) {
165
+ return `${config.username ? `${config.username}@` : ""}${config.host}`;
166
+ }
167
+ #getScpArgs(config, source, target) {
168
+ return [...config.port ? ["-P", config.port.toString()] : [], source, target];
169
+ }
170
+ #getSshArgs(config, command) {
171
+ return [...config.port ? ["-p", config.port.toString()] : [], this.#getSshTarget(config), command];
172
+ }
173
+ async login(host, workspace) {
174
+ const config = await GlobalConfig.getHostConfig(host);
175
+ const cloudApi = new CloudApi(workspace, config);
176
+ const self = config.auth ? await cloudApi.getRemoteSelf() : null;
177
+ if (self) {
178
+ Logger.rawLog(chalk.green(`
179
+ \u2713 Already logged in akan cloud as ${self.nickname}
180
+ `));
181
+ return true;
182
+ }
183
+ const remoteId = crypto.randomUUID();
184
+ const signinUrl = `${cloudApi.host}/remoteAuth?remoteId=${encodeURIComponent(remoteId)}`;
185
+ Logger.rawLog(chalk.bold(`
186
+ ${chalk.green("\u27A4")} Authentication Required`));
187
+ Logger.rawLog(chalk.dim("Please visit or click the following URL:"));
188
+ Logger.rawLog(`${chalk.cyan.underline(signinUrl)}
189
+ `);
190
+ try {
191
+ const qrcode = await new Promise((resolve, reject) => {
192
+ QRcode.toString(signinUrl, { type: "terminal", small: true }, (err, data) => {
193
+ if (err)
194
+ reject(err);
195
+ resolve(data);
196
+ });
197
+ });
198
+ Logger.rawLog(qrcode);
199
+ await openBrowser(signinUrl);
200
+ Logger.rawLog(chalk.dim("Opening browser..."));
201
+ } catch {
202
+ Logger.rawLog(chalk.yellow("Could not open browser. Please visit the URL manually."));
203
+ }
204
+ Logger.rawLog(chalk.dim("Waiting for authentication..."));
205
+ const MAX_RETRY = 300;
206
+ for (let i = 0;i < MAX_RETRY; i++) {
207
+ const accessToken = await cloudApi.getRemoteAuthToken(remoteId);
208
+ const self2 = await cloudApi.getRemoteSelf();
209
+ if (accessToken && self2) {
210
+ await GlobalConfig.setHostConfig({ host: config.host, auth: { accessToken, self: self2 } });
211
+ Logger.rawLog(chalk.green(`\r\u2713 Authentication successful!`));
212
+ Logger.rawLog(chalk.green.bold(`
213
+ \u2728 Welcome aboard, ${self2.nickname ?? "anonymous"}!`));
214
+ Logger.rawLog(chalk.dim(`You're now ready to use Akan CLI!
215
+ `));
216
+ return true;
217
+ }
218
+ await sleep(2000);
219
+ }
220
+ throw new Error(chalk.red("\u2716 Authentication timed out after 10 minutes. Please try again."));
221
+ }
222
+ async logout(host) {
223
+ const config = await GlobalConfig.getHostConfig(host);
224
+ if (config.auth?.self) {
225
+ await GlobalConfig.setHostConfig(getDefaultHostConfig(config.host));
226
+ Logger.rawLog(chalk.magenta.bold(`
227
+ \uD83D\uDC4B Goodbye, ${config.auth.self.nickname ?? "anonymous"}!`));
228
+ Logger.rawLog(chalk.dim(`\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500
229
+ `));
230
+ Logger.rawLog(chalk.cyan("You have been successfully logged out."));
231
+ Logger.rawLog(chalk.dim(`Thank you for using Akan CLI. Come back soon! \uD83C\uDF1F
232
+ `));
233
+ } else {
234
+ Logger.rawLog(chalk.yellow.bold(`
235
+ \u26A0\uFE0F No active session found`));
236
+ Logger.rawLog(chalk.dim(`You were not logged in to begin with
237
+ `));
238
+ }
239
+ }
240
+ async setLlm() {
241
+ await AiSession.init({ useExisting: false });
242
+ }
243
+ resetLlm() {
244
+ AiSession.setLlmConfig(null);
245
+ Logger.rawLog(chalk.green("\u2611\uFE0F LLM model config is cleared. Please run `akan set-llm` to set a new LLM model."));
246
+ }
247
+ async getAkanPkgs(workspace) {
248
+ const pkgs = await workspace.getPkgs();
249
+ return pkgs.filter((pkg) => pkg === "akanjs" || pkg === "create-akan-workspace" || pkg.startsWith("@akanjs/"));
250
+ }
251
+ async deployAkan(workspace, akanPkgs, { registryUrl, confirmPublish = true, tag: distTag } = {}) {
252
+ const registry = registryUrl ? getNpmRegistryUrl(registryUrl) : undefined;
253
+ const akanPackageJson = await workspace.readJson("pkgs/akanjs/package.json");
254
+ const [majorVersion, minorVersion, patchVersion, devPatchVersion] = akanPackageJson.version.split(".");
255
+ const isOfficialRelease = !devPatchVersion;
256
+ const targetVersionPrefix = isOfficialRelease ? `${majorVersion}.${minorVersion}` : `${majorVersion}.${minorVersion}.${patchVersion}`;
257
+ const tag = distTag ?? (isOfficialRelease ? "latest" : patchVersion.split("-").at(1) ?? "dev");
258
+ const getNextVersion = async (prefix, tag2) => {
259
+ try {
260
+ const latestPublishedVersion2 = await getLatestPackageVersion("akanjs", tag2, registry);
261
+ const latestPatch = latestPublishedVersion2.startsWith(prefix) ? parseInt(latestPublishedVersion2.split(".").at(-1) ?? "-1") : -1;
262
+ const nextVersion2 = `${prefix}.${latestPatch + 1}`;
263
+ return { nextVersion: nextVersion2, latestPublishedVersion: latestPublishedVersion2 };
264
+ } catch {
265
+ return { nextVersion: `${prefix}.0`, latestPublishedVersion: null };
266
+ }
267
+ };
268
+ const { nextVersion, latestPublishedVersion } = await getNextVersion(targetVersionPrefix, tag);
269
+ Logger.info(`Latest published version of akanjs: ${latestPublishedVersion ?? "none"}`);
270
+ Logger.info(`Next version of akanjs: ${nextVersion}`);
271
+ for (const library of akanPkgs) {
272
+ const packageJson = await workspace.readJson(`pkgs/${library}/package.json`);
273
+ const newPackageJsonStr = JSON.stringify(this.#normalizeAkanPackageJson(packageJson, library, nextVersion), null, 2);
274
+ await workspace.writeFile(`pkgs/${library}/package.json`, newPackageJsonStr);
275
+ const distPackageJson = await workspace.readJson(`dist/pkgs/${library}/package.json`);
276
+ const newDistPackageJson = this.#normalizeAkanPackageJson(distPackageJson, library, nextVersion);
277
+ await workspace.writeJson(`dist/pkgs/${library}/package.json`, newDistPackageJson);
278
+ }
279
+ if (confirmPublish) {
280
+ const isDeployConfirmed = await confirm({
281
+ message: "Are you sure you want to deploy the libraries?"
282
+ });
283
+ if (!isDeployConfirmed) {
284
+ Logger.error("Deployment cancelled");
285
+ return;
286
+ }
287
+ }
288
+ await Promise.all(akanPkgs.map(async (library) => {
289
+ Logger.info(`Publishing ${library}@${nextVersion} to ${registry ?? "npm"}...`);
290
+ await workspace.spawn("npm", ["publish", "--tag", tag, ...this.#getRegistryArgs(registry), ...this.#getLocalRegistryAuthArgs(registry)], {
291
+ cwd: path.join(workspace.workspaceRoot, "dist/pkgs", library),
292
+ env: this.#getRegistryEnv(registry),
293
+ stdio: "inherit"
294
+ });
295
+ Logger.info(`${library}@${nextVersion} is published to ${registry ?? "npm"}`);
296
+ }));
297
+ Logger.info(`All libraries are published to ${registry ?? "npm"}`);
298
+ }
299
+ async update(workspace, tag = "latest", { registryUrl } = {}) {
300
+ const registry = registryUrl ? getNpmRegistryUrl(registryUrl) : undefined;
301
+ const registryArgs = this.#getRegistryArgs(registry);
302
+ const env = this.#getRegistryEnv(registry);
303
+ if (!await workspace.exists("package.json"))
304
+ await workspace.spawn("bun", ["update", "-g", "akanjs", "--latest", `--tag=${tag}`, ...registryArgs], { env });
305
+ else
306
+ await Promise.all([
307
+ workspace.spawn("bun", ["update", "-g", "akanjs", "--latest", `--tag=${tag}`, ...registryArgs], { env }),
308
+ this.#updateAkanPkgs(workspace, tag, registry)
309
+ ]);
310
+ }
311
+ async#updateAkanPkgs(workspace, tag = "latest", registryUrl) {
312
+ const latestPublishedVersion = await getLatestPackageVersion("akanjs", tag, registryUrl);
313
+ const rootPackageJson = await workspace.getPackageJson();
314
+ if (!rootPackageJson.dependencies)
315
+ throw new Error("No dependencies found in package.json");
316
+ if (rootPackageJson.dependencies.akanjs)
317
+ rootPackageJson.dependencies.akanjs = latestPublishedVersion;
318
+ if (rootPackageJson.devDependencies?.akanjs)
319
+ rootPackageJson.devDependencies.akanjs = latestPublishedVersion;
320
+ if (rootPackageJson.dependencies["@akanjs/devkit"])
321
+ rootPackageJson.dependencies["@akanjs/devkit"] = latestPublishedVersion;
322
+ if (rootPackageJson.devDependencies?.["@akanjs/devkit"])
323
+ rootPackageJson.devDependencies["@akanjs/devkit"] = latestPublishedVersion;
324
+ await workspace.setPackageJson(rootPackageJson);
325
+ await workspace.spawn("bun", ["install", ...this.#getRegistryArgs(registryUrl)], {
326
+ env: this.#getRegistryEnv(registryUrl)
327
+ });
328
+ }
329
+ #normalizeAkanPackageJson(packageJson, packageName, version) {
330
+ const normalized = { ...packageJson, version };
331
+ for (const field of ["dependencies", "devDependencies", "peerDependencies", "optionalDependencies"]) {
332
+ const dependencies = normalized[field];
333
+ if (!dependencies)
334
+ continue;
335
+ normalized[field] = Object.fromEntries(Object.entries(dependencies).map(([dep, depVersion]) => [
336
+ dep,
337
+ dep !== packageName && this.#akanFrameworkPackages.has(dep) ? version : depVersion
338
+ ]));
339
+ }
340
+ return normalized;
341
+ }
342
+ async downloadEnv(cloudApi, workspace, workspaceId) {
343
+ await workspace.mkdir("local");
344
+ const localPath = await cloudApi.downloadEnv(workspaceId);
345
+ const relativePath = path.relative(workspace.workspaceRoot, localPath).split(path.sep).join("/");
346
+ await workspace.spawn("tar", ["-xf", relativePath], { cwd: workspace.workspaceRoot });
347
+ await workspace.remove(localPath);
348
+ }
349
+ async uploadEnv(cloudApi, workspaceId, filePath) {
350
+ const file = new File([Bun.file(filePath)], path.basename(filePath));
351
+ await cloudApi.uploadEnv(workspaceId, file);
352
+ }
353
+ async downloadEnvByScp(workspace) {
354
+ const envArchivePath = "local/env.tar";
355
+ const remoteServer = await this.#getRemoteEnvServerWithUsername();
356
+ const remoteArchivePath = this.#getRemoteEnvArchivePath();
357
+ const remoteTarget = this.#getScpTarget(remoteServer.config, remoteArchivePath);
358
+ await workspace.mkdir("local");
359
+ await workspace.remove(envArchivePath);
360
+ try {
361
+ Logger.info(`Downloading env archive from remote server "${remoteServer.name}"...`);
362
+ await workspace.spawn("scp", this.#getScpArgs(remoteServer.config, remoteTarget, envArchivePath), {
363
+ cwd: workspace.workspaceRoot,
364
+ stdio: "inherit"
365
+ });
366
+ await workspace.spawn("tar", ["-xf", envArchivePath], {
367
+ cwd: workspace.workspaceRoot
368
+ });
369
+ await workspace.remove(envArchivePath);
370
+ } catch (error) {
371
+ throw new Error(`Failed to download env archive from remote server "${remoteServer.name}"`, { cause: error });
372
+ }
373
+ }
374
+ async uploadEnvByScp(workspace, filePath) {
375
+ const remoteServer = await this.#getRemoteEnvServerWithUsername();
376
+ const remoteArchiveDir = this.#getRemoteEnvArchiveDir();
377
+ const remoteArchivePath = this.#getRemoteEnvArchivePath();
378
+ const remoteTarget = this.#getScpTarget(remoteServer.config, remoteArchivePath);
379
+ try {
380
+ await workspace.spawn("ssh", this.#getSshArgs(remoteServer.config, `mkdir -p ${remoteArchiveDir}`), {
381
+ cwd: workspace.workspaceRoot,
382
+ stdio: "inherit"
383
+ });
384
+ Logger.info(`Uploading env archive to remote server "${remoteServer.name}"...`);
385
+ await workspace.spawn("scp", this.#getScpArgs(remoteServer.config, filePath, remoteTarget), {
386
+ cwd: workspace.workspaceRoot,
387
+ stdio: "inherit"
388
+ });
389
+ } catch (error) {
390
+ throw new Error(`Failed to upload env archive to remote server "${remoteServer.name}"`, { cause: error });
391
+ }
392
+ }
393
+ async gatherEnvFiles(workspace) {
394
+ const envFilePattern = /^env\.(client|server)\.(?!(type|example)\.ts$).+\.ts$/;
395
+ const [appNames, libNames] = await workspace.getExecs();
396
+ const envDirs = [
397
+ ...appNames.map((appName) => `apps/${appName}/env`),
398
+ ...libNames.map((libName) => `libs/${libName}/env`)
399
+ ];
400
+ const defaultEnvFilePaths = (await Promise.all(envDirs.map(async (envDir) => (await workspace.readdir(envDir)).filter((fileName) => envFilePattern.test(fileName)).map((fileName) => `${envDir}/${fileName}`)))).flat();
401
+ await this.#syncSecretGitignore(workspace, appNames);
402
+ const customSecretPaths = await this.#gatherCustomSecretFiles(workspace, appNames);
403
+ const envFilePaths = [...new Set([...defaultEnvFilePaths, ...customSecretPaths])].sort();
404
+ await workspace.mkdir("local");
405
+ await workspace.remove("local/env.tar");
406
+ if (envFilePaths.length === 0)
407
+ throw new Error("No environment files found to archive");
408
+ await workspace.spawn("tar", ["-cf", "local/env.tar", ...envFilePaths], {
409
+ cwd: workspace.workspaceRoot
410
+ });
411
+ Logger.info(`Archived ${envFilePaths.length} environment files to local/env.tar`);
412
+ return { files: envFilePaths, path: "local/env.tar" };
413
+ }
414
+ async#gatherCustomSecretFiles(workspace, appNames) {
415
+ const secretPaths = await Promise.all(appNames.map(async (appName) => {
416
+ const config = await AppExecutor.from(workspace, appName).getConfig();
417
+ const secretGlobs = config.secrets ?? [];
418
+ if (secretGlobs.length === 0)
419
+ return [];
420
+ const appDir = path.join(workspace.workspaceRoot, "apps", appName);
421
+ return secretGlobs.flatMap((pattern) => Array.from(new Bun.Glob(pattern).scanSync({ cwd: appDir, onlyFiles: true })).map((match) => `apps/${appName}/${match.split(path.sep).join("/")}`));
422
+ }));
423
+ return secretPaths.flat();
424
+ }
425
+ async#syncSecretGitignore(workspace, appNames) {
426
+ const patterns = (await Promise.all(appNames.map(async (appName) => {
427
+ const config = await AppExecutor.from(workspace, appName).getConfig();
428
+ return (config.secrets ?? []).map((pattern) => `apps/${appName}/${pattern.replace(/^\/+/, "")}`);
429
+ }))).flat();
430
+ const uniquePatterns = [...new Set(patterns)].sort();
431
+ const existing = await workspace.exists(".gitignore") ? await workspace.readFile(".gitignore") : "";
432
+ const nextContent = this.#applySecretGitignoreBlock(existing, uniquePatterns);
433
+ if (nextContent === existing)
434
+ return;
435
+ await workspace.writeFile(".gitignore", nextContent);
436
+ Logger.info(uniquePatterns.length ? `Synced ${uniquePatterns.length} secret pattern(s) from akan.config.ts to .gitignore` : "Removed managed secret patterns from .gitignore");
437
+ }
438
+ #applySecretGitignoreBlock(content, patterns) {
439
+ const beginMarker = "# akan:secrets (managed by akan.config.ts \u2014 do not edit)";
440
+ const endMarker = "# akan:secrets:end";
441
+ const lines = content.split(`
442
+ `);
443
+ const beginIdx = lines.indexOf(beginMarker);
444
+ const endIdx = lines.indexOf(endMarker);
445
+ const stripped = beginIdx !== -1 && endIdx !== -1 && endIdx >= beginIdx ? [...lines.slice(0, beginIdx), ...lines.slice(endIdx + 1)] : [...lines];
446
+ while (stripped.length && stripped[stripped.length - 1]?.trim() === "")
447
+ stripped.pop();
448
+ if (patterns.length === 0)
449
+ return stripped.length ? `${stripped.join(`
450
+ `)}
451
+ ` : "";
452
+ const body = stripped.length ? `${stripped.join(`
453
+ `)}
454
+
455
+ ` : "";
456
+ return `${body}${[beginMarker, ...patterns, endMarker].join(`
457
+ `)}
458
+ `;
459
+ }
460
+ }
461
+
462
+ export { getNpmRegistryUrl, getLatestPackageVersion, CloudRunner };