@akanjs/cli 3.1.0-rc.1 → 3.1.0-rc.3

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 (74) hide show
  1. package/.build-stamp +1 -1
  2. package/{CodeAgent-kyprmy06.js → CodeAgent-ctk4fh2b.js} +2 -2
  3. package/{CodeTui-21mtze3j.js → CodeTui-mrxtrdzw.js} +1 -1
  4. package/TunnelShare-rj74jd6x.js +13 -0
  5. package/{agent.command-f3gsc1gd.js → agent.command-ncre6kwz.js} +5 -5
  6. package/akanCode.js +2 -2
  7. package/{application.command-5s780vnz.js → application.command-54mawreg.js} +52 -41
  8. package/{applicationBuildRunner-1yaqsrdq.js → applicationBuildRunner-8573zr2w.js} +16 -30
  9. package/buildBatch.proc.js +22 -13
  10. package/{cloud.command-yj6xb2a7.js → cloud.command-nyemv0m3.js} +57 -65
  11. package/cloud.script-ht6yb203.js +23 -0
  12. package/{code.command-3wkn4ja5.js → code.command-spsrzg63.js} +6 -6
  13. package/{context.command-9jjn3gcw.js → context.command-768zv7z4.js} +16 -13
  14. package/context.runner-tf1b53qq.js +24 -0
  15. package/{guideline.command-dtczrqpf.js → guideline.command-4ycwc51q.js} +2 -2
  16. package/guidelines/conventions/conventions.instruction.md +28 -15
  17. package/guidelines/cssRule/cssRule.instruction.md +12 -6
  18. package/guidelines/mcpRule/mcpRule.instruction.md +27 -11
  19. package/guidelines/runtimeRule/runtimeRule.instruction.md +24 -15
  20. package/guidelines/transportRule/transportRule.instruction.md +28 -5
  21. package/incrementalBuilder.proc.js +9 -6
  22. package/{index-2etcv0sc.js → index-00seamy5.js} +1 -1
  23. package/{index-jx4bzvwg.js → index-1a7nhq56.js} +92 -52
  24. package/{index-e0cvze1s.js → index-1nyxvz8j.js} +4 -4
  25. package/index-2z3kykvd.js +248 -0
  26. package/{index-rtct7d43.js → index-3675mvjw.js} +1 -1
  27. package/{index-56d2g8ms.js → index-3ddt3qjg.js} +1 -1
  28. package/{index-6jjtnhkw.js → index-3x7yeecy.js} +51 -9
  29. package/{index-ww9bhp67.js → index-4pf8g7ff.js} +1 -1
  30. package/index-8935h3n1.js +33 -0
  31. package/{index-v29746vb.js → index-8eqkxwsx.js} +3 -3
  32. package/{index-c2yrsxdh.js → index-91rt5bye.js} +96 -320
  33. package/{index-55mynnqs.js → index-bv210r6t.js} +459 -210
  34. package/{index-c76k723e.js → index-c4h5b5nq.js} +2 -2
  35. package/{index-qt22xstj.js → index-c9zjb3tw.js} +5 -3
  36. package/{index-c1x0be5j.js → index-dmsvd2rv.js} +2 -2
  37. package/index-ec080x99.js +1124 -0
  38. package/{index-ctanya41.js → index-fjqvd5mx.js} +4 -4
  39. package/{index-aprt7pkz.js → index-fvn6f5wx.js} +229 -1327
  40. package/{index-rc5rwyps.js → index-jj7yapwq.js} +71 -60
  41. package/index-kmamh5bv.js +241 -0
  42. package/{index-fm46xrf1.js → index-q5abh0jq.js} +3 -3
  43. package/{index-1w2qaee2.js → index-qf8g88j9.js} +361 -291
  44. package/{index-gnyf1fqy.js → index-qvghfv6v.js} +2 -2
  45. package/{index-ae5pwhgs.js → index-r5d6rq19.js} +10 -8
  46. package/{index-2mx692qq.js → index-vhfjzda3.js} +2 -2
  47. package/{index-8r4d7cmt.js → index-w9b71b6y.js} +4 -4
  48. package/{index-8ggztp8k.js → index-wanhh011.js} +42 -177
  49. package/{index-05g3jayg.js → index-wdze51wz.js} +1 -1
  50. package/index-wjqbc7sf.js +668 -0
  51. package/{index-rwq3ekqc.js → index-xf40a295.js} +7 -7
  52. package/{index-krq7btbt.js → index-xm5tmpv0.js} +3 -3
  53. package/index.js +22 -22
  54. package/{library.command-gaga5bmq.js → library.command-88ahgych.js} +3 -3
  55. package/{localRegistry.command-vsreyy42.js → localRegistry.command-ykea1m7b.js} +12 -9
  56. package/{module.command-7vpf6db4.js → module.command-zky4b7m9.js} +5 -5
  57. package/{package.command-twmvvf69.js → package.command-vh8pcedh.js} +3 -3
  58. package/package.json +2 -2
  59. package/{page.command-5asq7fdf.js → page.command-fyqeqxc3.js} +3 -3
  60. package/{pipeline.command-d0v38572.js → pipeline.command-r5t30py6.js} +220 -219
  61. package/{primitive.command-dyrs2fm8.js → primitive.command-a0516zfk.js} +6 -6
  62. package/{projectTypecheck-cvtkrzp2.js → projectTypecheck-7djd4z50.js} +1 -1
  63. package/{quality.command-6emsqpbg.js → quality.command-k7jcnfn0.js} +2 -2
  64. package/{repair.command-8t6aegv5.js → repair.command-85zg2an3.js} +5 -5
  65. package/{scalar.command-bnggy1x7.js → scalar.command-g5vmqwca.js} +4 -4
  66. package/{subspace.command-md2teygq.js → subspace.command-mp4evgsj.js} +421 -362
  67. package/{tunnel.command-jx3tdm2d.js → tunnel.command-kbnr99sj.js} +7 -4
  68. package/{workflow.command-jx0wag5r.js → workflow.command-9y9p1n8y.js} +10 -10
  69. package/{workspace.command-yxd7rr7g.js → workspace.command-jmec2kjg.js} +33 -30
  70. package/TunnelShare-q4jq08qg.js +0 -9
  71. package/cloud.script-h6pvcpa2.js +0 -20
  72. package/context.runner-atcrzz5b.js +0 -21
  73. package/index-6w0rbr9d.js +0 -68
  74. package/index-pgj2rt78.js +0 -404
@@ -2,20 +2,208 @@
2
2
  import {
3
3
  binPlatforms,
4
4
  ServerEnvProfile,
5
- Executor
6
- } from "./index-55mynnqs.js";
5
+ Executor,
6
+ WorkspaceExecutor
7
+ } from "./index-bv210r6t.js";
7
8
  import {
8
9
  resolveAppId,
9
10
  targetHtmlFilename
10
11
  } from "./index-q958tqqg.js";
12
+ import {
13
+ ExitCodeError
14
+ } from "./index-3x7yeecy.js";
15
+ import {
16
+ CsrArtifactBuilder
17
+ } from "./index-2z3kykvd.js";
18
+
19
+ // pkgs/@akanjs/devkit/mobile/nativePublicEnv.ts
20
+ import { createHash } from "crypto";
21
+ class NativePublicEnv {
22
+ static derivedKeys = [
23
+ "AKAN_PUBLIC_APP_NAME",
24
+ "AKAN_PUBLIC_BASE_PATHS",
25
+ "AKAN_PUBLIC_API_PREFIX",
26
+ "AKAN_PUBLIC_WS_PREFIX",
27
+ "AKAN_PUBLIC_RENDER_ENV",
28
+ "AKAN_PUBLIC_DEFAULT_LOCALE",
29
+ "AKAN_PUBLIC_LOCALES"
30
+ ];
31
+ static #keyPattern = /^AKAN_PUBLIC_[A-Z0-9_]{1,128}$/;
32
+ static #secretWords = ["SECRET", "TOKEN", "PASSWORD", "PRIVATE", "CREDENTIAL"];
33
+ static #maxKeys = 256;
34
+ static #maxBytes = 64 * 1024;
35
+ static of(env) {
36
+ return Object.fromEntries(Object.keys(env).filter((key) => key.startsWith("AKAN_PUBLIC_") && !NativePublicEnv.derivedKeys.includes(key)).sort().map((key) => [key, String(env[key])]));
37
+ }
38
+ static hash(publicEnv) {
39
+ return createHash("sha256").update(JSON.stringify(NativePublicEnv.of(publicEnv))).digest("hex");
40
+ }
41
+ static problems(publicEnv) {
42
+ const keys = Object.keys(publicEnv);
43
+ const problems = keys.flatMap((key) => {
44
+ const value = publicEnv[key];
45
+ if (!NativePublicEnv.#keyPattern.test(key))
46
+ return [`${key} is not an AKAN_PUBLIC_[A-Z0-9_] name`];
47
+ const word = NativePublicEnv.#secretWords.find((secret) => key.includes(secret));
48
+ if (word)
49
+ return [`${key} names a ${word.toLowerCase()}, which a page bundle must not carry`];
50
+ if (typeof value !== "string")
51
+ return [`${key} is not a string`];
52
+ if (/[\u0000-\u001f\u007f]/.test(value))
53
+ return [`${key} holds a control character`];
54
+ return [];
55
+ });
56
+ if (keys.length > NativePublicEnv.#maxKeys)
57
+ problems.push(`${keys.length} keys, more than ${NativePublicEnv.#maxKeys}`);
58
+ const bytes = Buffer.byteLength(JSON.stringify(publicEnv));
59
+ if (bytes > NativePublicEnv.#maxBytes)
60
+ problems.push(`${bytes} bytes, more than ${NativePublicEnv.#maxBytes}`);
61
+ return problems;
62
+ }
63
+ static diff(a, b) {
64
+ return [...new Set([...Object.keys(a), ...Object.keys(b)])].filter((key) => a[key] !== b[key]).sort((x, y) => x < y ? -1 : x > y ? 1 : 0);
65
+ }
66
+ static async built(app) {
67
+ const file = Bun.file(CsrArtifactBuilder.publicEnvPath(app));
68
+ if (!await file.exists())
69
+ return null;
70
+ return NativePublicEnv.of(await file.json());
71
+ }
72
+ static async recordedIn(bundleFile) {
73
+ const file = Bun.file(bundleFile);
74
+ if (!await file.exists())
75
+ return null;
76
+ const bundle = await file.json();
77
+ return bundle.web?.publicEnv ?? null;
78
+ }
79
+ static async load(file) {
80
+ const source = Bun.file(file);
81
+ if (!await source.exists())
82
+ throw new Error(`--public-env ${file} does not exist.`);
83
+ const json = await source.json().catch(() => null);
84
+ if (!json || typeof json !== "object" || Array.isArray(json))
85
+ throw new Error(`--public-env ${file} is not a JSON object of AKAN_PUBLIC_* values or a bundle.json.`);
86
+ const isBundle = json.schema === 1 && typeof json.web === "object";
87
+ const map = isBundle ? json.web.publicEnv : json;
88
+ if (!map)
89
+ throw new Error(`--public-env ${file} is a bundle.json from a build that recorded no public env.`);
90
+ const derived = Object.keys(map).filter((key) => NativePublicEnv.derivedKeys.includes(key));
91
+ if (derived.length)
92
+ throw new Error(`--public-env ${file} sets ${derived.join(", ")}, which akan.config.ts decides.`);
93
+ const problems = NativePublicEnv.problems(map);
94
+ if (problems.length)
95
+ throw new Error(`--public-env ${file} is refused:
96
+ - ${problems.join(`
97
+ - `)}`);
98
+ return NativePublicEnv.of(map);
99
+ }
100
+ static pin(publicEnv) {
101
+ for (const key of Object.keys(process.env))
102
+ if (key.startsWith("AKAN_PUBLIC_") && !NativePublicEnv.derivedKeys.includes(key))
103
+ delete process.env[key];
104
+ Object.assign(process.env, publicEnv);
105
+ WorkspaceExecutor.publicEnvPin = publicEnv;
106
+ }
107
+ static async problemAgainst(shipped, bundle) {
108
+ const [before, now] = await Promise.all([NativePublicEnv.recordedIn(shipped), NativePublicEnv.recordedIn(bundle)]);
109
+ if (!before)
110
+ return null;
111
+ const differs = NativePublicEnv.diff(before, now ?? {});
112
+ if (!differs.length)
113
+ return null;
114
+ return `public env ${differs.join(", ")} differs from the store build's (pass --public-env ${shipped} to repeat it)`;
115
+ }
116
+ }
117
+
118
+ // pkgs/@akanjs/devkit/mobile/updatePackFolder.ts
119
+ import { mkdtemp, rm } from "fs/promises";
120
+ import os from "os";
121
+ import path from "path";
122
+ class UpdatePackFolder {
123
+ static packFile = "pack.json";
124
+ static compatFile = "compat.json";
125
+ static bundleFile = "bundle.json";
126
+ static templateFile = "manifest.template.json";
127
+ static filesDir = "files";
128
+ static #records = [
129
+ UpdatePackFolder.templateFile,
130
+ UpdatePackFolder.bundleFile,
131
+ UpdatePackFolder.packFile,
132
+ UpdatePackFolder.compatFile
133
+ ];
134
+ static #sha256 = /^[0-9a-f]{64}$/;
135
+ static async clear(dir) {
136
+ await Promise.all([UpdatePackFolder.packFile, UpdatePackFolder.compatFile].map((name) => rm(path.join(dir, name), { force: true })));
137
+ }
138
+ static async read(dir) {
139
+ const file = Bun.file(path.join(dir, UpdatePackFolder.packFile));
140
+ if (!await file.exists())
141
+ throw new Error(`${dir} holds no ${UpdatePackFolder.packFile}: give the folder \`akan pack-update\` wrote.`);
142
+ const pack = await file.json();
143
+ if (pack.schema !== 1)
144
+ throw new Error(`${dir}/${UpdatePackFolder.packFile} is schema ${pack.schema}, and this akan reads schema 1.`);
145
+ return { ...pack, out: path.resolve(dir) };
146
+ }
147
+ static async write({ out, ...record }) {
148
+ await Bun.write(path.join(out, UpdatePackFolder.packFile), `${JSON.stringify(record, null, 2)}
149
+ `);
150
+ }
151
+ static async writeCompat(pack, compat) {
152
+ pack.compat = compat;
153
+ await Bun.write(path.join(pack.out, UpdatePackFolder.compatFile), `${JSON.stringify(compat, null, 2)}
154
+ `);
155
+ }
156
+ static async readBundle(file) {
157
+ return await Bun.file(file).json();
158
+ }
159
+ static storeBuildVerdict({ out, compat }, then) {
160
+ if (!compat || compat.compatible)
161
+ return null;
162
+ return new ExitCodeError(`The pack in ${out} needs a new store build; it differs from the one ${compat.against} came from:
163
+ - ${compat.problems.join(`
164
+ - `)}${then ? `
165
+ ${then}` : ""}`, 2);
166
+ }
167
+ static async withArchive(pack, use) {
168
+ const { templateFile, filesDir } = UpdatePackFolder;
169
+ const template = Bun.file(path.join(pack.out, templateFile));
170
+ if (!await template.exists())
171
+ throw new Error(`${pack.out} holds no ${templateFile}: give the folder \`akan pack-update\` wrote.`);
172
+ const { files = [] } = await template.json();
173
+ const hashes = [...new Set(files.map((file) => file.sha256))];
174
+ if (!hashes.every((hash) => typeof hash === "string" && UpdatePackFolder.#sha256.test(hash)))
175
+ throw new Error(`${pack.out}/${templateFile} lists a file whose sha256 is not 64 hex digits.`);
176
+ const present = await Promise.all(UpdatePackFolder.#records.map((name) => Bun.file(path.join(pack.out, name)).exists()));
177
+ const entries = [
178
+ ...UpdatePackFolder.#records.filter((_, idx) => present[idx]),
179
+ ...hashes.map((hash) => `${filesDir}/${hash}`)
180
+ ];
181
+ const dir = await mkdtemp(path.join(os.tmpdir(), "akan-pack-"));
182
+ try {
183
+ const tar = Bun.spawn(["tar", "-czf", "pack.tgz", "-C", pack.out, "-T", "-"], {
184
+ cwd: dir,
185
+ stdin: new Blob([entries.map((entry) => `./${entry}
186
+ `).join("")]),
187
+ stdout: "ignore",
188
+ stderr: "pipe"
189
+ });
190
+ const [code, stderr] = await Promise.all([tar.exited, new Response(tar.stderr).text()]);
191
+ if (code !== 0)
192
+ throw new Error(`tar of ${pack.out} failed: ${stderr.trim()}`);
193
+ return await use(path.join(dir, "pack.tgz"));
194
+ } finally {
195
+ await rm(dir, { recursive: true, force: true });
196
+ }
197
+ }
198
+ }
11
199
 
12
200
  // pkgs/@akanjs/devkit/mobile/nativeApp.ts
13
- import { rm as rm3 } from "fs/promises";
14
- import path7 from "path";
201
+ import path8 from "path";
202
+ import { toError } from "akanjs/common";
15
203
 
16
204
  // pkgs/@akanjs/devkit/mobile/desktopBin.ts
17
- import { chmod, mkdir, rename, rm, writeFile } from "fs/promises";
18
- import path from "path";
205
+ import { chmod, mkdir, rename, rm as rm2, writeFile } from "fs/promises";
206
+ import path2 from "path";
19
207
  class DesktopBin {
20
208
  app;
21
209
  config;
@@ -48,23 +236,23 @@ class DesktopBin {
48
236
  static fileName(name, executable, platform = process.platform) {
49
237
  if (platform !== "win32" || /\.(exe|cmd|bat|com)$/i.test(name))
50
238
  return name;
51
- return `${name}${path.extname(executable).toLowerCase() || ".exe"}`;
239
+ return `${name}${path2.extname(executable).toLowerCase() || ".exe"}`;
52
240
  }
53
241
  static downloadName(url) {
54
- const extension = /(?:\.tar)?\.[A-Za-z0-9]{1,8}$/.exec(path.posix.basename(new URL(url).pathname))?.[0] ?? "";
242
+ const extension = /(?:\.tar)?\.[A-Za-z0-9]{1,8}$/.exec(path2.posix.basename(new URL(url).pathname))?.[0] ?? "";
55
243
  return `download${extension.toLowerCase()}`;
56
244
  }
57
245
  static extractCommand(archive, dir, platform = process.platform) {
58
246
  if (platform === "linux" && /\.zip$/i.test(archive))
59
247
  return ["unzip", "-q", "-o", archive, "-d", dir];
60
- const tar = platform === "win32" ? path.join(process.env.SystemRoot ?? "C:\\Windows", "System32", "tar.exe") : "tar";
248
+ const tar = platform === "win32" ? path2.join(process.env.SystemRoot ?? "C:\\Windows", "System32", "tar.exe") : "tar";
61
249
  return [tar, "-xf", archive, "-C", dir];
62
250
  }
63
251
  static async sha256(file) {
64
252
  return new Bun.CryptoHasher("sha256").update(await Bun.file(file).arrayBuffer()).digest("hex");
65
253
  }
66
254
  get #cacheDir() {
67
- return path.join(this.app.cwdPath, ".akan", "cache", "bin");
255
+ return path2.join(this.app.cwdPath, ".akan", "cache", "bin");
68
256
  }
69
257
  async stage(dir, arch = process.arch) {
70
258
  const scanInfo = this.app.getScanInfo({ allowEmpty: true }) ?? await this.app.scan({ write: false });
@@ -74,14 +262,14 @@ class DesktopBin {
74
262
  const platform = DesktopBin.platform(arch);
75
263
  if (!platform)
76
264
  throw new Error(`bin: this desktop app is for ${process.platform}-${arch}, and a bin names only ${binPlatforms.join(", ")}.`);
77
- await rm(dir, { recursive: true, force: true });
265
+ await rm2(dir, { recursive: true, force: true });
78
266
  await mkdir(dir, { recursive: true });
79
267
  for (const [name, { owner, platforms }] of chosen) {
80
268
  const source = platforms[platform];
81
269
  if (!source)
82
270
  throw new Error(`${owner}: bin.${name} names no ${platform} file, the platform this desktop app is built for (it names ${Object.keys(platforms).join(", ") || "none"}).`);
83
271
  const executable = await this.#executable(`${owner}: bin.${name}.${platform}`, source);
84
- const target = path.join(dir, DesktopBin.fileName(name, executable));
272
+ const target = path2.join(dir, DesktopBin.fileName(name, executable));
85
273
  await Bun.write(target, Bun.file(executable));
86
274
  await chmod(target, 493);
87
275
  this.app.logger.info(`The desktop app carries ${name} from ${"url" in source ? source.url : source.path}`);
@@ -95,14 +283,14 @@ class DesktopBin {
95
283
  if (!source.file)
96
284
  return file;
97
285
  const key = "url" in source ? source.sha256 : await DesktopBin.sha256(file);
98
- const executable = path.join(await this.#extract(file, key), ...source.file.split("/"));
286
+ const executable = path2.join(await this.#extract(file, key), ...source.file.split("/"));
99
287
  if (!await Bun.file(executable).exists())
100
288
  throw new Error(`${at}: the archive holds no ${source.file}`);
101
289
  return executable;
102
290
  }
103
291
  async#download(at, { url, sha256 }) {
104
- const dir = path.join(this.#cacheDir, sha256);
105
- const file = path.join(dir, DesktopBin.downloadName(url));
292
+ const dir = path2.join(this.#cacheDir, sha256);
293
+ const file = path2.join(dir, DesktopBin.downloadName(url));
106
294
  if (await Bun.file(file).exists())
107
295
  return file;
108
296
  await mkdir(dir, { recursive: true });
@@ -119,10 +307,10 @@ class DesktopBin {
119
307
  return file;
120
308
  }
121
309
  async#extract(archive, key) {
122
- const dir = path.join(this.#cacheDir, key, "extracted");
310
+ const dir = path2.join(this.#cacheDir, key, "extracted");
123
311
  if (await Bun.file(`${dir}.done`).exists())
124
312
  return dir;
125
- await rm(dir, { recursive: true, force: true });
313
+ await rm2(dir, { recursive: true, force: true });
126
314
  await mkdir(dir, { recursive: true });
127
315
  const [command = "tar", ...args] = DesktopBin.extractCommand(archive, dir);
128
316
  if (command === "unzip" && !Bun.which("unzip"))
@@ -134,10 +322,11 @@ class DesktopBin {
134
322
  }
135
323
 
136
324
  // pkgs/@akanjs/devkit/mobile/nativeApi.ts
137
- import path2 from "path";
325
+ import path3 from "path";
138
326
 
139
327
  class NativeApi {
140
328
  static supportedMajor = 0;
329
+ static minimumVersion = "0.12.0";
141
330
  static entryOf(appDir) {
142
331
  const workspaceEntry = NativeApi.#resolve("@akanjs/native/api", appDir);
143
332
  if (workspaceEntry)
@@ -145,7 +334,7 @@ class NativeApi {
145
334
  const akanjsPackage = NativeApi.#resolve("akanjs/package.json", appDir);
146
335
  if (!akanjsPackage)
147
336
  throw new Error(`akanjs is not installed for ${appDir}; the native build ships inside it.`);
148
- return path2.join(path2.dirname(akanjsPackage), "vendor", "@akanjs", "native", "packages", "cli", "src", "api.ts");
337
+ return path3.join(path3.dirname(akanjsPackage), "vendor", "@akanjs", "native", "packages", "cli", "src", "api.ts");
149
338
  }
150
339
  static async load(appDir) {
151
340
  const entry = NativeApi.entryOf(appDir);
@@ -153,8 +342,8 @@ class NativeApi {
153
342
  throw new Error(`The installed akanjs has no native build at ${entry}; update akanjs.`);
154
343
  const api = await import(entry);
155
344
  const major = Number(api.API_VERSION.split(".")[0]);
156
- if (major !== NativeApi.supportedMajor)
157
- throw new Error(`The native build API is ${api.API_VERSION}, and this akan CLI speaks ${NativeApi.supportedMajor}.x; install matching versions of akanjs and @akanjs/cli.`);
345
+ if (major !== NativeApi.supportedMajor || Bun.semver.order(api.API_VERSION, NativeApi.minimumVersion) < 0)
346
+ throw new Error(`The native build API is ${api.API_VERSION}, and this akan CLI speaks ${NativeApi.minimumVersion} or a later ${NativeApi.supportedMajor}.x; install matching versions of akanjs and @akanjs/cli.`);
158
347
  return api;
159
348
  }
160
349
  static #resolve(specifier, from) {
@@ -223,7 +412,7 @@ class NativeAppLine {
223
412
 
224
413
  // pkgs/@akanjs/devkit/mobile/nativeConfig.ts
225
414
  import { readFileSync } from "fs";
226
- import path3 from "path";
415
+ import path4 from "path";
227
416
 
228
417
  // pkgs/@akanjs/devkit/mobile/usageDescriptions.ts
229
418
  import { capitalize } from "akanjs/common";
@@ -305,11 +494,11 @@ class NativeConfig {
305
494
  warnings.push(`Permission '${permission}' has no native plugin yet; the app ships without it.`);
306
495
  return used;
307
496
  });
308
- const abs = (relative) => path3.resolve(appPath, relative);
497
+ const abs = (relative) => path4.resolve(appPath, relative);
309
498
  const updates = NativeConfig.#updates(target, env);
310
- const folders = new Set(nativePlugins.map((plugin) => path3.resolve(plugin.dir)));
499
+ const folders = new Set(nativePlugins.map((plugin) => path4.resolve(plugin.dir)));
311
500
  const folderIds = new Set(nativePlugins.map((plugin) => plugin.id));
312
- const listed = (target.plugins ?? []).filter((spec) => NativeConfig.#isId(spec) ? !folderIds.has(spec) : !folders.has(path3.resolve(appPath, spec)));
501
+ const listed = (target.plugins ?? []).filter((spec) => NativeConfig.#isId(spec) ? !folderIds.has(spec) : !folders.has(path4.resolve(appPath, spec)));
313
502
  const plugins = [
314
503
  ...new Set([
315
504
  ...NativeConfig.basePlugins,
@@ -365,7 +554,7 @@ class NativeConfig {
365
554
  app: {
366
555
  id: resolveAppId(target.appId, platform),
367
556
  name: target.appName,
368
- fileName: target.fileName ?? NativeConfig.fileNameOf(path3.basename(appPath)),
557
+ fileName: target.fileName ?? NativeConfig.fileNameOf(path4.basename(appPath)),
369
558
  version: target.version,
370
559
  build: target.buildNum
371
560
  },
@@ -431,14 +620,14 @@ class NativeConfig {
431
620
  if (NativeConfig.#isId(spec))
432
621
  return spec;
433
622
  try {
434
- const { id } = JSON.parse(readFileSync(path3.join(path3.resolve(appPath, spec), "native-plugin.json"), "utf8"));
623
+ const { id } = JSON.parse(readFileSync(path4.join(path4.resolve(appPath, spec), "native-plugin.json"), "utf8"));
435
624
  return typeof id === "string" ? id : spec;
436
625
  } catch {
437
626
  return spec;
438
627
  }
439
628
  }
440
629
  static #isId(spec) {
441
- return !spec.startsWith(".") && !path3.isAbsolute(spec);
630
+ return !spec.startsWith(".") && !path4.isAbsolute(spec);
442
631
  }
443
632
  static #compact(value) {
444
633
  const kept = Object.entries(value).filter(([, entry]) => Array.isArray(entry) ? entry.length > 0 : entry && typeof entry === "object" ? Object.keys(entry).length > 0 : entry !== undefined);
@@ -459,23 +648,23 @@ class NativeConfig {
459
648
 
460
649
  // pkgs/@akanjs/devkit/mobile/nativePluginFolders.ts
461
650
  import { readdir } from "fs/promises";
462
- import path4 from "path";
651
+ import path5 from "path";
463
652
 
464
653
  class NativePluginFolders {
465
654
  static folder = "native";
466
655
  static async in(root, owner) {
467
- const base = path4.join(root, NativePluginFolders.folder);
656
+ const base = path5.join(root, NativePluginFolders.folder);
468
657
  const entries = await readdir(base, { withFileTypes: true }).catch(() => []);
469
658
  const folders = entries.filter((entry) => entry.isDirectory()).map((entry) => entry.name);
470
659
  return await Promise.all(folders.sort().map(async (name) => {
471
660
  const where = `${owner}/${NativePluginFolders.folder}/${name}`;
472
- const manifest = Bun.file(path4.join(base, name, "native-plugin.json"));
661
+ const manifest = Bun.file(path5.join(base, name, "native-plugin.json"));
473
662
  if (!await manifest.exists())
474
663
  throw new Error(`${where} has no native-plugin.json; a folder in native/ is one native plugin.`);
475
664
  const { id } = await manifest.json();
476
665
  if (id !== name)
477
666
  throw new Error(`${where}/native-plugin.json has id ${JSON.stringify(id)}; name the folder after its id.`);
478
- return { id, dir: path4.join(base, name), owner };
667
+ return { id, dir: path5.join(base, name), owner };
479
668
  }));
480
669
  }
481
670
  static select(own, fromLibs) {
@@ -494,15 +683,15 @@ class NativePluginFolders {
494
683
  const root = app.workspace.workspaceRoot;
495
684
  const [own, ...fromLibs] = await Promise.all([
496
685
  NativePluginFolders.in(app.cwdPath, `apps/${app.name}`),
497
- ...scanInfo.getLibs().map((lib) => NativePluginFolders.in(path4.join(root, "libs", lib), `libs/${lib}`))
686
+ ...scanInfo.getLibs().map((lib) => NativePluginFolders.in(path5.join(root, "libs", lib), `libs/${lib}`))
498
687
  ]);
499
688
  return NativePluginFolders.select(own ?? [], fromLibs.flat());
500
689
  }
501
690
  }
502
691
 
503
692
  // pkgs/@akanjs/devkit/mobile/nativeWebDir.ts
504
- import { cp, mkdir as mkdir2, rm as rm2, stat } from "fs/promises";
505
- import path5 from "path";
693
+ import { cp, mkdir as mkdir2, rm as rm3, stat } from "fs/promises";
694
+ import path6 from "path";
506
695
 
507
696
  class NativeWebDir {
508
697
  dir;
@@ -529,7 +718,7 @@ ${script}${html.slice(at)}`;
529
718
  }
530
719
  static #targetTag = "<script>window.__AKAN_MOBILE_TARGET__=";
531
720
  async clear() {
532
- await rm2(this.dir, { recursive: true, force: true });
721
+ await rm3(this.dir, { recursive: true, force: true });
533
722
  await mkdir2(this.dir, { recursive: true });
534
723
  }
535
724
  async assemble(target, { html, publicDir, fontsDir }) {
@@ -539,12 +728,12 @@ ${script}${html.slice(at)}`;
539
728
  if (publicDir && await NativeWebDir.#isDir(publicDir))
540
729
  await cp(publicDir, this.dir, { recursive: true });
541
730
  if (fontsDir && await NativeWebDir.#isDir(fontsDir))
542
- await cp(fontsDir, path5.join(this.dir, "_akan", "fonts"), { recursive: true });
543
- if (await NativeWebDir.#isDir(path5.join(this.dir, NativeWebDir.reservedEntry)))
731
+ await cp(fontsDir, path6.join(this.dir, "_akan", "fonts"), { recursive: true });
732
+ if (await NativeWebDir.#isDir(path6.join(this.dir, NativeWebDir.reservedEntry)))
544
733
  throw new Error(`public/${NativeWebDir.reservedEntry} is reserved for the native runtime.`);
545
- await Bun.write(path5.join(this.dir, "index.html"), NativeWebDir.injectTarget(await Bun.file(html).text(), target));
734
+ await Bun.write(path6.join(this.dir, "index.html"), NativeWebDir.injectTarget(await Bun.file(html).text(), target));
546
735
  const files = await Array.fromAsync(new Bun.Glob("**/*").scan({ cwd: this.dir, onlyFiles: true, dot: true }));
547
- return files.map((file) => file.split(path5.sep).join("/")).sort();
736
+ return files.map((file) => file.split(path6.sep).join("/")).sort();
548
737
  }
549
738
  static async#isDir(dir) {
550
739
  try {
@@ -557,20 +746,20 @@ ${script}${html.slice(at)}`;
557
746
 
558
747
  // pkgs/@akanjs/devkit/mobile/serverBundleSecrets.ts
559
748
  import { readdir as readdir2, readFile, stat as stat2 } from "fs/promises";
560
- import path6 from "path";
749
+ import path7 from "path";
561
750
  class ServerBundleSecrets {
562
751
  workspaceRoot;
563
752
  appDir;
564
753
  static #largestFile = 64 * 1024 * 1024;
754
+ static #printer = new Bun.Transpiler({ loader: "js", target: "bun" });
755
+ static #notSource = new Set(["node_modules", "env", "private", "public", "dist"]);
565
756
  constructor(workspaceRoot, appDir) {
566
757
  this.workspaceRoot = workspaceRoot;
567
758
  this.appDir = appDir;
568
759
  }
569
760
  async envFiles() {
570
- const libsDir = path6.join(this.workspaceRoot, "libs");
571
- const libs = await readdir2(libsDir).catch(() => []);
572
- const dirs = [path6.join(this.appDir, "env"), ...libs.map((lib) => path6.join(libsDir, lib, "env"))];
573
- const files = await Promise.all(dirs.map(async (dir) => (await readdir2(dir).catch(() => [])).filter((name) => /^env\.server\.[A-Za-z0-9.-]+\.ts$/.test(name) && !/\.(type|example)\.ts$/.test(name)).map((name) => path6.join(dir, name))));
761
+ const dirs = [path7.join(this.appDir, "env"), ...(await this.#libDirs()).map((lib) => path7.join(lib, "env"))];
762
+ const files = await Promise.all(dirs.map(async (dir) => (await readdir2(dir).catch(() => [])).filter((name) => /^env\.server\.[A-Za-z0-9.-]+\.ts$/.test(name) && !/\.(type|example)\.ts$/.test(name)).map((name) => path7.join(dir, name))));
574
763
  return files.flat();
575
764
  }
576
765
  async assertClean(bundleDir) {
@@ -578,38 +767,66 @@ class ServerBundleSecrets {
578
767
  const secrets = [];
579
768
  for (const file of await this.envFiles()) {
580
769
  try {
581
- const value = await ServerEnvProfile.evaluate(file, path6.dirname(path6.dirname(file)));
770
+ const value = await ServerEnvProfile.evaluate(file, path7.dirname(path7.dirname(file)));
582
771
  secrets.push(...ServerEnvProfile.secretValues(value).map((secret) => ({ file, ...secret })));
583
772
  } catch {
584
773
  unread.push(ServerBundleSecrets.#relative(this.workspaceRoot, file));
585
774
  }
586
775
  }
776
+ if (!secrets.length)
777
+ return unread;
778
+ const sought = secrets.map((secret) => ({ secret, needles: ServerBundleSecrets.#needles(secret.value) }));
587
779
  const found = [];
588
780
  for (const bundled of await ServerBundleSecrets.#files(bundleDir)) {
589
781
  const text = await readFile(bundled, "latin1");
590
- for (const secret of secrets)
591
- if (ServerBundleSecrets.#needles(secret.value).some((needle) => text.includes(needle)))
592
- found.push(`${ServerBundleSecrets.#relative(bundleDir, bundled)} holds ${ServerBundleSecrets.#relative(this.workspaceRoot, secret.file)} ${secret.path}`);
782
+ for (const { secret, needles } of sought)
783
+ if (needles.some((needle) => text.includes(needle)))
784
+ found.push({ bundled, secret });
593
785
  }
594
- if (found.length)
595
- throw new Error(`The server this app carries holds secret values from the workspace's env files, so it is not signed or sent anywhere:
596
- ${[...new Set(found)].join(`
786
+ const coded = found.length ? await this.#coded(found.map(({ secret }) => secret.value)) : new Set;
787
+ const leaks = found.filter(({ secret }) => !coded.has(secret.value)).map(({ bundled, secret }) => `${ServerBundleSecrets.#relative(bundleDir, bundled)} holds ${ServerBundleSecrets.#relative(this.workspaceRoot, secret.file)} ${secret.path}`);
788
+ if (leaks.length)
789
+ throw new Error(`The server this app carries holds secret values of the workspace's env files, so the release stops here:
790
+ ${[...new Set(leaks)].join(`
597
791
  `)}
598
- Build its server with a profile (native.desktop.server.profile), whose bundle carries no secret.`);
792
+ Build its server with a profile (native.desktop.server.profile), whose bundle leaves the env files' secrets out. A value a profile build still holds sits in code or a file the server carries: take it out there.`);
599
793
  return unread;
600
794
  }
795
+ async#coded(values) {
796
+ const plain = [...new Set(values)].filter((value) => !ServerEnvProfile.distinctive(value));
797
+ const coded = new Set;
798
+ if (!plain.length)
799
+ return coded;
800
+ for (const dir of [this.appDir, ...await this.#libDirs()])
801
+ for (const file of await ServerBundleSecrets.#sources(dir)) {
802
+ const text = await readFile(file, "utf8");
803
+ for (const value of plain)
804
+ if (text.includes(value))
805
+ coded.add(value);
806
+ }
807
+ return coded;
808
+ }
809
+ async#libDirs() {
810
+ const libsDir = path7.join(this.workspaceRoot, "libs");
811
+ return (await readdir2(libsDir).catch(() => [])).map((lib) => path7.join(libsDir, lib));
812
+ }
601
813
  static #needles(value) {
602
814
  const escaped = JSON.stringify(value).slice(1, -1);
603
815
  const line = value.split(/\r?\n/).find((part) => part.length >= 32);
604
- return [...new Set([value, escaped, ...line ? [line] : []])];
816
+ const forms = [value, escaped, ServerBundleSecrets.#printed(value), ...line ? [line] : []];
817
+ return [...new Set(forms.map((form) => Buffer.from(form, "utf8").toString("latin1")))];
818
+ }
819
+ static #printed(value) {
820
+ const code = ServerBundleSecrets.#printer.transformSync(`export default ${JSON.stringify(value)};`);
821
+ return /^export default (["'`])([\s\S]*)\1;?\s*$/.exec(code)?.[2] ?? value;
605
822
  }
606
823
  static #relative(from, to) {
607
- return path6.relative(from, to).split(path6.sep).join("/");
824
+ return path7.relative(from, to).split(path7.sep).join("/");
608
825
  }
609
826
  static async#files(dir) {
610
827
  const files = [];
611
828
  for (const entry of await readdir2(dir, { withFileTypes: true })) {
612
- const file = path6.join(dir, entry.name);
829
+ const file = path7.join(dir, entry.name);
613
830
  if (entry.isDirectory()) {
614
831
  if (entry.name !== "node_modules")
615
832
  files.push(...await ServerBundleSecrets.#files(file));
@@ -618,6 +835,20 @@ Build its server with a profile (native.desktop.server.profile), whose bundle ca
618
835
  }
619
836
  return files;
620
837
  }
838
+ static async#sources(dir) {
839
+ const files = [];
840
+ for (const entry of await readdir2(dir, { withFileTypes: true }).catch(() => [])) {
841
+ if (entry.name.startsWith("."))
842
+ continue;
843
+ const file = path7.join(dir, entry.name);
844
+ if (entry.isDirectory()) {
845
+ if (!ServerBundleSecrets.#notSource.has(entry.name))
846
+ files.push(...await ServerBundleSecrets.#sources(file));
847
+ } else if (/\.(?:[cm]?[jt]sx?|json)$/.test(entry.name) && !/\.(?:test|spec)\.[cm]?[jt]sx?$/.test(entry.name))
848
+ files.push(file);
849
+ }
850
+ return files;
851
+ }
621
852
  }
622
853
 
623
854
  // pkgs/@akanjs/devkit/mobile/nativeApp.ts
@@ -632,14 +863,14 @@ class NativeApp {
632
863
  this.app = app;
633
864
  this.target = target;
634
865
  this.env = env;
635
- this.targetRoot = path7.join(app.workspace.workspaceRoot, "dist", "native", app.name, target.name);
636
- this.web = new NativeWebDir(path7.join(this.targetRoot, "web"));
866
+ this.targetRoot = path8.join(app.workspace.workspaceRoot, "dist", "native", app.name, target.name);
867
+ this.web = new NativeWebDir(path8.join(this.targetRoot, "web"));
637
868
  }
638
869
  outDir(platform) {
639
- return path7.join(this.targetRoot, "build", platform);
870
+ return path8.join(this.targetRoot, "build", platform);
640
871
  }
641
872
  devOutDir(platform) {
642
- return path7.join(this.targetRoot, "dev", platform);
873
+ return path8.join(this.targetRoot, "dev", platform);
643
874
  }
644
875
  indexPath(platform) {
645
876
  const { config } = this.target;
@@ -649,13 +880,13 @@ class NativeApp {
649
880
  async assembleWeb(platform) {
650
881
  const dist = this.app.dist.cwdPath;
651
882
  return await this.web.assemble({ ...this.target.config, indexPath: this.indexPath(platform) }, {
652
- html: path7.join(dist, "csr", targetHtmlFilename(this.target.config)),
653
- publicDir: path7.join(dist, "public"),
654
- fontsDir: path7.join(dist, ".akan", "artifact", "fonts")
883
+ html: path8.join(dist, "csr", targetHtmlFilename(this.target.config)),
884
+ publicDir: path8.join(dist, "public"),
885
+ fontsDir: path8.join(dist, ".akan", "artifact", "fonts")
655
886
  });
656
887
  }
657
888
  get binDir() {
658
- return path7.join(this.targetRoot, "bin");
889
+ return path8.join(this.targetRoot, "bin");
659
890
  }
660
891
  async config({
661
892
  server,
@@ -739,16 +970,17 @@ class NativeApp {
739
970
  NativeApp.#assertServerPlatform(platform, server);
740
971
  NativeApp.assertInstaller(platform, installer);
741
972
  await this.assembleWeb(platform);
973
+ const publicEnv = await this.#publicEnv();
742
974
  const { api, config } = await this.prepare(platform, server, arch);
743
975
  const distribution = await this.#distribution(api, platform, profile, { sign, notarize }, server);
744
- return await api.build({
976
+ return await this.#recorded(this.outDir(platform), publicEnv, api.build({
745
977
  ...this.#task(platform, config, { env: await this.#runtimeEnv(server) }),
746
978
  profile,
747
979
  ...arch ? { arch } : {},
748
980
  ...platform === "windows" ? { windows: { ...installer ? { installer } : {}, ...distribution.windows } } : {},
749
981
  ...platform === "macos" ? { macos: { ...distribution.macos, ...installer ? { dmg: true } : {} } } : {},
750
982
  ...platform === "linux" && installer ? { linux: { appImage: true } } : {}
751
- });
983
+ }));
752
984
  }
753
985
  async run(platform, { device, teamId, profile = "debug", server } = {}) {
754
986
  NativeApp.#assertServerPlatform(platform, server);
@@ -804,22 +1036,25 @@ class NativeApp {
804
1036
  }
805
1037
  async releaseIos({ teamId, adHoc = false } = {}) {
806
1038
  await this.assembleWeb("ios");
1039
+ const publicEnv = await this.#publicEnv();
807
1040
  const { api, config } = await this.prepare("ios");
808
- return await api.release({
1041
+ return await this.#recorded(this.outDir("ios"), publicEnv, api.release({
809
1042
  ...this.#task("ios", config, { env: await this.#runtimeEnv() }),
810
1043
  platform: "ios",
811
1044
  signing: { ...teamId ? { teamId } : {}, ...adHoc ? { distribution: "ad-hoc" } : {} }
812
- });
1045
+ }));
813
1046
  }
814
1047
  get updatesDir() {
815
- return path7.join(this.targetRoot, "updates");
1048
+ return path8.join(this.targetRoot, "updates");
816
1049
  }
817
1050
  async publishUpdate(platform, {
818
1051
  channel,
819
1052
  server,
820
1053
  arch,
821
1054
  sign = false,
822
- notarize = false
1055
+ notarize = false,
1056
+ revoke,
1057
+ resetSequence
823
1058
  } = {}) {
824
1059
  NativeApp.#assertServerPlatform(platform, server);
825
1060
  this.#assertUpdates();
@@ -833,25 +1068,35 @@ class NativeApp {
833
1068
  ...channel ? { channel } : {},
834
1069
  ...arch ? { arch } : {},
835
1070
  ...distribution.macos ? { macos: distribution.macos } : {},
836
- ...distribution.windows ? { windows: distribution.windows } : {}
1071
+ ...distribution.windows ? { windows: distribution.windows } : {},
1072
+ ...revoke?.length ? { revoke } : {},
1073
+ ...resetSequence ? { resetSequence } : {}
837
1074
  });
838
1075
  }
839
1076
  async#distribution(api, platform, profile, request, server) {
840
1077
  const distribution = api.requestedDistribution(platform, profile, request);
1078
+ if (server && profile === "release")
1079
+ await this.#assertServerClean(server);
841
1080
  if (!distribution.notice) {
842
1081
  if (distribution.unused.length && (platform === "macos" || platform === "windows"))
843
1082
  this.app.logger.info(`${distribution.unused.join(", ")} set: this build is not signed without --sign.`);
844
1083
  return distribution;
845
1084
  }
846
- if (server) {
847
- const unread = await new ServerBundleSecrets(this.app.workspace.workspaceRoot, this.app.cwdPath).assertClean(server.dir);
848
- if (unread.length)
849
- this.app.logger.warn(`Not searched for secrets, as they could not be evaluated: ${unread.join(", ")}.`);
850
- }
851
1085
  this.app.logger.warn(distribution.notice);
852
1086
  return distribution;
853
1087
  }
854
- async assertPublishable(platform, { channel, server = false, arch } = {}) {
1088
+ async#assertServerClean(server) {
1089
+ const unread = await new ServerBundleSecrets(this.app.workspace.workspaceRoot, this.app.cwdPath).assertClean(server.dir);
1090
+ if (unread.length)
1091
+ throw new Error(`The server this app carries was not searched for the secret values of ${unread.join(", ")}, which could not be evaluated with no app env set, so the release stops here.`);
1092
+ }
1093
+ async assertPublishable(platform, {
1094
+ channel,
1095
+ server = false,
1096
+ arch,
1097
+ revoke,
1098
+ resetSequence
1099
+ } = {}) {
855
1100
  this.#assertUpdates();
856
1101
  const [api, { config }] = await Promise.all([
857
1102
  NativeApi.load(this.app.cwdPath),
@@ -866,10 +1111,12 @@ class NativeApp {
866
1111
  out: this.updatesDir,
867
1112
  server,
868
1113
  ...channel ? { channel } : {},
869
- ...arch ? { arch } : {}
1114
+ ...arch ? { arch } : {},
1115
+ ...revoke?.length ? { revoke } : {},
1116
+ ...resetSequence ? { resetSequence } : {}
870
1117
  });
871
1118
  } catch (error) {
872
- throw new Error(`Native target '${this.target.name}': ${error instanceof Error ? error.message : String(error)}`, {
1119
+ throw new Error(`Native target '${this.target.name}': ${toError(error).message}`, {
873
1120
  cause: error
874
1121
  });
875
1122
  }
@@ -888,24 +1135,26 @@ class NativeApp {
888
1135
  async releaseAndroid({ formats = ["aab"] } = {}) {
889
1136
  const signing = NativeApp.androidSigning();
890
1137
  await this.assembleWeb("android");
1138
+ const publicEnv = await this.#publicEnv();
891
1139
  const { api, config } = await this.prepare("android");
892
- return await api.release({
1140
+ return await this.#recorded(this.outDir("android"), publicEnv, api.release({
893
1141
  ...this.#task("android", config, { env: await this.#runtimeEnv() }),
894
1142
  platform: "android",
895
1143
  signing,
896
1144
  formats
897
- });
1145
+ }));
898
1146
  }
899
1147
  async packUpdate(platform, { out } = {}) {
900
1148
  await this.assembleWeb(platform);
1149
+ const publicEnv = await this.#publicEnv();
901
1150
  const { api, config } = await this.prepare(platform);
902
1151
  if (!config.updates)
903
1152
  throw new Error(`Native target '${this.target.name}' takes no updates: add native.updates: { url, publicKey } to akan.config.ts first.`);
904
- const dir = path7.resolve(out ?? path7.join(this.updatesDir, platform));
905
- await Promise.all(["pack.json", "compat.json"].map((name) => rm3(path7.join(dir, name), { force: true })));
906
- const packed = await api.packUpdate({ ...this.#task(platform, config), platform, out: dir });
1153
+ const dir = path8.resolve(out ?? path8.join(this.updatesDir, platform));
1154
+ await UpdatePackFolder.clear(dir);
1155
+ const packed = await this.#recorded(dir, publicEnv, api.packUpdate({ ...this.#task(platform, config), platform, out: dir }));
907
1156
  const channel = config.updates.channel ?? "production";
908
- const bundle = await Bun.file(path7.join(packed.out, "bundle.json")).json();
1157
+ const bundle = await UpdatePackFolder.readBundle(path8.join(packed.out, UpdatePackFolder.bundleFile));
909
1158
  const pack = {
910
1159
  schema: 1,
911
1160
  app: this.app.name,
@@ -917,24 +1166,39 @@ class NativeApp {
917
1166
  appVersion: packed.manifest.version,
918
1167
  nativeApi: packed.manifest.nativeApi ?? "",
919
1168
  webHash: bundle.web.hash,
1169
+ ...bundle.web.publicEnvHash ? { publicEnvHash: bundle.web.publicEnvHash } : {},
920
1170
  files: packed.manifest.files.length,
921
1171
  size: packed.manifest.files.reduce((sum, file) => sum + file.size, 0),
922
1172
  out: packed.out
923
1173
  };
924
1174
  return { ...packed, channel, pack };
925
1175
  }
926
- static async writePack(pack) {
927
- await Bun.write(path7.join(pack.out, "pack.json"), `${JSON.stringify(pack, null, 2)}
1176
+ async#publicEnv() {
1177
+ const publicEnv = await NativePublicEnv.built(this.app);
1178
+ if (!publicEnv)
1179
+ return null;
1180
+ const problems = NativePublicEnv.problems(publicEnv);
1181
+ if (!problems.length)
1182
+ return publicEnv;
1183
+ const message = `The web build inlines public env an update cannot carry:
1184
+ - ${problems.join(`
1185
+ - `)}`;
1186
+ if (this.target.config.updates)
1187
+ throw new Error(message);
1188
+ this.app.logger.warn(`${message}
1189
+ bundle.json records none, so no update can be checked against this build.`);
1190
+ return null;
1191
+ }
1192
+ async#recorded(dir, publicEnv, build) {
1193
+ const result = await build;
1194
+ const file = path8.join(dir, UpdatePackFolder.bundleFile);
1195
+ if (!publicEnv || !await Bun.file(file).exists())
1196
+ return result;
1197
+ const bundle = await UpdatePackFolder.readBundle(file);
1198
+ const web = { ...bundle.web, publicEnv, publicEnvHash: NativePublicEnv.hash(publicEnv) };
1199
+ await Bun.write(file, `${JSON.stringify({ ...bundle, web }, null, 2)}
928
1200
  `);
929
- }
930
- static async readPack(dir) {
931
- const file = Bun.file(path7.join(dir, "pack.json"));
932
- if (!await file.exists())
933
- throw new Error(`${dir} holds no pack.json: give the folder \`akan pack-update\` wrote.`);
934
- const pack = await file.json();
935
- if (pack.schema !== 1)
936
- throw new Error(`${dir}/pack.json is schema ${pack.schema}, and this akan reads schema 1.`);
937
- return { ...pack, out: path7.resolve(dir) };
1201
+ return result;
938
1202
  }
939
1203
  static async compareBundles(appDir, shipped, bundle) {
940
1204
  return (await NativeApi.load(appDir)).compareBundles(shipped, bundle);
@@ -990,198 +1254,4 @@ class NativeApp {
990
1254
  }
991
1255
  }
992
1256
 
993
- // pkgs/@akanjs/devkit/mobile/placeholderAppId.ts
994
- var PLACEHOLDER_APP_IDS = [
995
- "com.myapp.app",
996
- "com.myorg.myapp",
997
- "com.example.app",
998
- "com.example.myapp"
999
- ];
1000
- var placeholderAppIdSegment = /^(example|examples|myorg|myapp|mycompany|myorganization|changeme|todo|sample|test)$/;
1001
- var isPlaceholderAppId = (appId) => {
1002
- const normalized = appId?.trim().toLowerCase() ?? "";
1003
- if (!normalized)
1004
- return true;
1005
- if (PLACEHOLDER_APP_IDS.includes(normalized))
1006
- return true;
1007
- return normalized.split(".").some((segment) => placeholderAppIdSegment.test(segment));
1008
- };
1009
- // pkgs/@akanjs/devkit/mobile/desktopServerStage.ts
1010
- import { cp as cp2, mkdir as mkdir3, readdir as readdir3, rm as rm4, writeFile as writeFile2 } from "fs/promises";
1011
- import path8 from "path";
1012
- class DesktopServerStage {
1013
- app;
1014
- static uncarried = new Set([
1015
- "Dockerfile",
1016
- "package.json",
1017
- "rscWorker.js",
1018
- "console.js",
1019
- "console-runtime.js",
1020
- "csr",
1021
- "public",
1022
- ".akan"
1023
- ]);
1024
- static rscRenderer = "react-server-dom-webpack";
1025
- dir;
1026
- constructor(app) {
1027
- this.app = app;
1028
- this.dir = path8.join(app.cwdPath, ".akan", "desktop", "server");
1029
- }
1030
- static assertCarriable(config) {
1031
- if (!config.database.modes.includes("single"))
1032
- throw new Error(`desktop.server in native puts the server in the app, where only database mode single runs (no Redis or Postgres); apps/${config.app.name}/akan.config.ts declares ${config.database.modes.join(", ")}.`);
1033
- }
1034
- static env(config, environment, { modules = [], env = {} } = {}) {
1035
- const { PORT: _port, AKAN_LOG_TO_FILE: _fileLog, ...image } = config.getProductionEnv(environment);
1036
- return {
1037
- ...image,
1038
- ...env,
1039
- AKAN_PUBLIC_OPERATION_MODE: "edge",
1040
- AKAN_DATABASE_MODE: "single",
1041
- AKAN_DATABASE_MODES: "single",
1042
- AKAN_SSR: "false",
1043
- AKAN_CSR: "false",
1044
- AKAN_MCP: "false",
1045
- AKAN_SHUTDOWN_TIMEOUT_MS: "1000",
1046
- ...modules.length ? { AKAN_MODULES: modules.join(",") } : {}
1047
- };
1048
- }
1049
- static imageStepsNotice(config, carried) {
1050
- const steps = typeof config.docker === "string" ? null : config.docker.preRuns.length + config.docker.postRuns.length;
1051
- if (steps === 0)
1052
- return null;
1053
- const image = steps === null ? `apps/${config.app.name}/akan.config.ts writes its own Dockerfile` : `The image runs ${steps} docker step${steps === 1 ? "" : "s"} from the app and its libs`;
1054
- return carried.length ? {
1055
- level: "info",
1056
- message: `${image}; the desktop app's server runs none of them and carries only bin: ${carried.join(", ")}.`
1057
- } : {
1058
- level: "warn",
1059
- message: `${image}, and a desktop app's server runs none of them: declare in bin the executables its code spawns, or those calls fail on the user's computer.`
1060
- };
1061
- }
1062
- static packageJson(config, built, omit = []) {
1063
- const otherDrivers = new Set(config.database.modes.filter((mode) => mode !== "single").flatMap((mode) => config.getDatabaseModeRuntimePackages(mode)));
1064
- const dependencies = Object.entries(built.dependencies ?? {}).filter(([name]) => !omit.includes(name) && (config.externalLibs.includes(name) || name !== DesktopServerStage.rscRenderer && !otherDrivers.has(name)));
1065
- return { ...built, dependencies: Object.fromEntries(dependencies) };
1066
- }
1067
- static async dependentsOf(nodeModules, name) {
1068
- const dependents = new Set;
1069
- for await (const manifest of new Bun.Glob("**/package.json").scan({ cwd: nodeModules, onlyFiles: true })) {
1070
- const json = await Bun.file(path8.join(nodeModules, manifest)).json().catch(() => null);
1071
- if (!json?.name || json.name === name)
1072
- continue;
1073
- const declared = { ...json.dependencies, ...json.optionalDependencies, ...json.peerDependencies };
1074
- if (name in declared)
1075
- dependents.add(json.name);
1076
- }
1077
- return [...dependents].sort();
1078
- }
1079
- static installArgs(arch) {
1080
- return ["install", "--production", "--prefer-offline", ...arch && arch !== process.arch ? [`--cpu=${arch}`] : []];
1081
- }
1082
- async install(arch, omit = []) {
1083
- const config = await this.app.getConfig();
1084
- DesktopServerStage.assertCarriable(config);
1085
- await rm4(this.dir, { recursive: true, force: true });
1086
- await mkdir3(this.dir, { recursive: true });
1087
- const built = config.getProductionPackageJson();
1088
- for (const name of omit.filter((name) => !(name in (built.dependencies ?? {}))))
1089
- this.app.logger.warn(`desktop.server.omit names ${name}, which the server does not depend on directly.`);
1090
- await writeFile2(path8.join(this.dir, "package.json"), JSON.stringify(DesktopServerStage.packageJson(config, built, omit), null, 2));
1091
- try {
1092
- await this.app.spawn(process.execPath, DesktopServerStage.installArgs(arch), { cwd: this.dir });
1093
- } catch (error) {
1094
- throw new Error(`The desktop app's server could not install its packages in ${this.dir}: it needs the npm registry, or a Bun cache that already holds every one of them.
1095
- ${error instanceof Error ? error.message : String(error)}`, { cause: error });
1096
- }
1097
- await rm4(path8.join(this.dir, "node_modules", ".bin"), { recursive: true, force: true });
1098
- await this.#assertOmitted(omit);
1099
- }
1100
- async#assertOmitted(omit) {
1101
- const nodeModules = path8.join(this.dir, "node_modules");
1102
- const kept = [];
1103
- for (const name of omit) {
1104
- if (!await Bun.file(path8.join(nodeModules, name, "package.json")).exists())
1105
- continue;
1106
- const dependents = await DesktopServerStage.dependentsOf(nodeModules, name);
1107
- kept.push(`${name} (needed by ${dependents.join(", ") || "a package that names it"})`);
1108
- }
1109
- if (kept.length)
1110
- throw new Error(`desktop.server.omit leaves out packages another dependency still installs: ${kept.join("; ")}. Omit those too, or keep the package.`);
1111
- }
1112
- async prepare(environment, run = {}) {
1113
- const config = await this.app.getConfig();
1114
- const dist = this.app.dist.cwdPath;
1115
- await mkdir3(this.dir, { recursive: true });
1116
- for (const name of await readdir3(dist))
1117
- if (!DesktopServerStage.uncarried.has(name))
1118
- await cp2(path8.join(dist, name), path8.join(this.dir, name), { recursive: true });
1119
- const scanInfo = this.app.getScanInfo({ allowEmpty: true }) ?? await this.app.scan({ write: false });
1120
- const carried = [...DesktopBin.select(config, scanInfo.getLibs()).keys()];
1121
- const notice = DesktopServerStage.imageStepsNotice(config, carried);
1122
- if (notice)
1123
- this.app.logger[notice.level](notice.message);
1124
- return {
1125
- dir: this.dir,
1126
- entry: "main.js",
1127
- env: DesktopServerStage.env(config, environment, run),
1128
- ...run.restart !== undefined ? { restart: run.restart } : {},
1129
- ...run.watchdog ? { watchdog: run.watchdog } : {}
1130
- };
1131
- }
1132
- }
1133
- // pkgs/@akanjs/devkit/mobile/updateUploader.ts
1134
- import path9 from "path";
1135
-
1136
- class UpdateUploader {
1137
- bucket;
1138
- prefix;
1139
- static #manifest = /\.json(?:\.sig)?$/;
1140
- static parseTarget(target) {
1141
- const match = /^s3:\/\/([^/]+)\/?(.*)$/.exec(target.trim());
1142
- if (!match?.[1])
1143
- throw new Error(`--upload takes s3://<bucket>/<prefix>, not ${JSON.stringify(target)}.`);
1144
- return { bucket: match[1], prefix: (match[2] ?? "").replace(/\/+$/, "") };
1145
- }
1146
- static order(files) {
1147
- const manifests = files.filter((file) => UpdateUploader.#manifest.test(file));
1148
- return {
1149
- releases: files.filter((file) => !UpdateUploader.#manifest.test(file)).sort((a, b) => a.localeCompare(b)),
1150
- manifests: manifests.sort((a, b) => Number(a.endsWith(".json")) - Number(b.endsWith(".json")) || a.localeCompare(b))
1151
- };
1152
- }
1153
- constructor(bucket, prefix) {
1154
- this.bucket = bucket;
1155
- this.prefix = prefix;
1156
- }
1157
- static forTarget(target) {
1158
- const { bucket, prefix } = UpdateUploader.parseTarget(target);
1159
- return new UpdateUploader(new Bun.S3Client({ bucket }), prefix);
1160
- }
1161
- async upload(dir) {
1162
- const files = [];
1163
- for await (const file of new Bun.Glob("**/*").scan({ cwd: dir, onlyFiles: true })) {
1164
- if (!file.endsWith(".tmp"))
1165
- files.push(file.split(path9.sep).join("/"));
1166
- }
1167
- const { releases, manifests } = UpdateUploader.order(files);
1168
- const keyOf = (file) => this.prefix ? `${this.prefix}/${file}` : file;
1169
- let uploaded = 0;
1170
- let skipped = 0;
1171
- for (const file of releases) {
1172
- if (await this.bucket.exists(keyOf(file))) {
1173
- skipped++;
1174
- continue;
1175
- }
1176
- await this.bucket.write(keyOf(file), Bun.file(path9.join(dir, file)));
1177
- uploaded++;
1178
- }
1179
- for (const file of manifests) {
1180
- const type = file.endsWith(".json") ? "application/json" : "text/plain";
1181
- await this.bucket.write(keyOf(file), Bun.file(path9.join(dir, file)), { type });
1182
- uploaded++;
1183
- }
1184
- return { uploaded, skipped };
1185
- }
1186
- }
1187
- export { DesktopServerStage, NativeApp, isPlaceholderAppId, UpdateUploader };
1257
+ export { DesktopBin, NativePublicEnv, UpdatePackFolder, NativeApp };