@akanjs/cli 3.0.2 → 3.1.0-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 (84) hide show
  1. package/.build-stamp +1 -1
  2. package/{CodeAgent-szbjv4gv.js → CodeAgent-q7a4srt6.js} +2 -2
  3. package/{CodeTui-5nw86ab6.js → CodeTui-ak23dxc9.js} +1 -1
  4. package/{TunnelShare-cwh9ezsm.js → TunnelShare-21mm6dr5.js} +2 -2
  5. package/{agent.command-np1dffkb.js → agent.command-f3gsc1gd.js} +5 -5
  6. package/akanCode.js +2 -2
  7. package/{application.command-gk69v1xs.js → application.command-3v2d53gd.js} +37 -15
  8. package/{applicationBuildRunner-wx09n9g6.js → applicationBuildRunner-1yaqsrdq.js} +115 -103
  9. package/buildBatch.proc.js +16 -5
  10. package/cloud.command-kk2rwkvw.js +187 -0
  11. package/cloud.script-r7eyfey4.js +20 -0
  12. package/{code.command-hmtx14b2.js → code.command-30sca09d.js} +6 -6
  13. package/commandManifest.json +1 -1
  14. package/{context.command-6aqvdx43.js → context.command-k7eqacq6.js} +13 -13
  15. package/context.runner-hqd1ppqe.js +21 -0
  16. package/{guideline.command-axvf68gc.js → guideline.command-dtczrqpf.js} +2 -2
  17. package/guidelines/agentRule/agentRule.instruction.md +10 -2
  18. package/guidelines/conventions/conventions.instruction.md +68 -25
  19. package/guidelines/cssRule/cssRule.instruction.md +19 -0
  20. package/guidelines/mcpRule/mcpRule.instruction.md +67 -12
  21. package/guidelines/queryRule/queryRule.instruction.md +11 -1
  22. package/guidelines/runtimeRule/runtimeRule.instruction.md +97 -16
  23. package/guidelines/ssrRule/ssrRule.instruction.md +10 -7
  24. package/guidelines/transportRule/transportRule.instruction.md +19 -0
  25. package/guidelines/workspaceRecipes/workspaceRecipes.instruction.md +6 -6
  26. package/incrementalBuilder.proc.js +24 -3
  27. package/{index-h9ht6s60.js → index-05g3jayg.js} +1 -1
  28. package/{index-j38qkvkk.js → index-0bm04n5g.js} +12 -10
  29. package/{index-g38nhv87.js → index-2etcv0sc.js} +1 -1
  30. package/{index-zyw622y1.js → index-2mx692qq.js} +3 -3
  31. package/{index-jb0hmt3k.js → index-2t4tqd1c.js} +3 -3
  32. package/{index-50wb2a26.js → index-55mynnqs.js} +354 -129
  33. package/{index-q6ft9fkc.js → index-6jjtnhkw.js} +8 -3
  34. package/index-6w0rbr9d.js +68 -0
  35. package/{index-2wc41k5w.js → index-7bymavqk.js} +2 -2
  36. package/{index-gtjgnv36.js → index-aprt7pkz.js} +6 -9
  37. package/{index-1bjkfvm9.js → index-c1x0be5j.js} +2 -2
  38. package/{index-z750gk1j.js → index-c2yrsxdh.js} +13 -7
  39. package/{index-cb6yc5mb.js → index-c76k723e.js} +2 -2
  40. package/{index-6keyf4sg.js → index-ctanya41.js} +4 -4
  41. package/{index-f4e08ptp.js → index-e0cvze1s.js} +4 -4
  42. package/{index-hp3stk1m.js → index-fm46xrf1.js} +3 -3
  43. package/{index-3xmzr1p1.js → index-gnyf1fqy.js} +2 -2
  44. package/{index-zwd84m0w.js → index-jyv387qw.js} +4 -4
  45. package/{index-jk9xnck6.js → index-kjvrq3nc.js} +231 -133
  46. package/{index-xwprn9nr.js → index-nz15k0b6.js} +1 -1
  47. package/{index-nwd6qzvv.js → index-p9ystjrp.js} +138 -55
  48. package/index-q958tqqg.js +77 -0
  49. package/{index-50jjwg8y.js → index-qt22xstj.js} +1 -1
  50. package/{index-94s080qm.js → index-r2tpeb3n.js} +127 -1
  51. package/{index-bsv901fd.js → index-rwq3ekqc.js} +7 -7
  52. package/{index-t91a7339.js → index-tn4rpqp1.js} +96 -12
  53. package/{index-1gs9g12n.js → index-v29746vb.js} +4 -4
  54. package/index-ww9bhp67.js +62 -0
  55. package/{index-hxnnfy57.js → index-y8q31011.js} +180 -8
  56. package/index.js +22 -21
  57. package/{library.command-9m2rdwwz.js → library.command-gaga5bmq.js} +3 -3
  58. package/{localRegistry.command-kda81jyk.js → localRegistry.command-9e84e32q.js} +9 -9
  59. package/{module.command-q63dffya.js → module.command-7vpf6db4.js} +5 -5
  60. package/{package.command-y2jh0m99.js → package.command-twmvvf69.js} +3 -3
  61. package/package.json +2 -2
  62. package/{page.command-f3e4cwhh.js → page.command-5asq7fdf.js} +3 -3
  63. package/pipeline.command-d0v38572.js +1334 -0
  64. package/{primitive.command-r4kq75qj.js → primitive.command-dyrs2fm8.js} +6 -6
  65. package/projectTypecheck-cvtkrzp2.js +7 -0
  66. package/{quality.command-f7gzh26z.js → quality.command-6emsqpbg.js} +5 -4
  67. package/{repair.command-0qmyr6qa.js → repair.command-8t6aegv5.js} +5 -5
  68. package/{scalar.command-kddn7av4.js → scalar.command-bnggy1x7.js} +4 -4
  69. package/{subspace.command-ft65qvc4.js → subspace.command-f02j5tge.js} +158 -91
  70. package/templates/appSample/page/task/[taskId]/_index.tsx +2 -2
  71. package/templates/appSample/page/task/[taskId]/edit.tsx +2 -2
  72. package/templates/appSample/page/task/_index.tsx +2 -2
  73. package/templates/crudPages/[__model__Id]/edit/page.tsx +2 -2
  74. package/templates/crudPages/[__model__Id]/page.tsx +3 -3
  75. package/templates/crudPages/page.tsx +2 -2
  76. package/templates/crudSinglePage/page.tsx +3 -3
  77. package/templates/lib/cnst.ts +2 -2
  78. package/templates/lib/sig.ts +3 -3
  79. package/{tunnel.command-b3ymdk4f.js → tunnel.command-9x8nfn75.js} +16 -13
  80. package/{workflow.command-z1srcdfw.js → workflow.command-jx0wag5r.js} +10 -10
  81. package/{workspace.command-vp6vmx3z.js → workspace.command-cmj6mqyk.js} +57 -27
  82. package/cloud.command-tw06613r.js +0 -95
  83. package/cloud.script-64gzba8r.js +0 -20
  84. package/context.runner-hnkvv3z3.js +0 -21
@@ -2,17 +2,17 @@
2
2
  import {
3
3
  Workspace,
4
4
  command
5
- } from "./index-q6ft9fkc.js";
5
+ } from "./index-6jjtnhkw.js";
6
6
  import"./index-52w0fvh8.js";
7
- import"./index-50wb2a26.js";
7
+ import"./index-55mynnqs.js";
8
8
  import {
9
9
  renderPrimitiveReport
10
- } from "./index-h9ht6s60.js";
11
- import"./index-g38nhv87.js";
12
- import"./index-f4e08ptp.js";
10
+ } from "./index-05g3jayg.js";
11
+ import"./index-2etcv0sc.js";
12
+ import"./index-e0cvze1s.js";
13
13
  import {
14
14
  PrimitiveScript
15
- } from "./index-6keyf4sg.js";
15
+ } from "./index-ctanya41.js";
16
16
 
17
17
  // pkgs/@akanjs/cli/primitive/primitive.command.ts
18
18
  import { Logger } from "akanjs/common";
@@ -0,0 +1,7 @@
1
+ // @bun
2
+ import {
3
+ ProjectTypecheck2
4
+ } from "./index-6w0rbr9d.js";
5
+ export {
6
+ ProjectTypecheck2 as ProjectTypecheck
7
+ };
@@ -3,15 +3,16 @@ import {
3
3
  Workspace,
4
4
  script,
5
5
  command
6
- } from "./index-q6ft9fkc.js";
6
+ } from "./index-6jjtnhkw.js";
7
7
  import"./index-52w0fvh8.js";
8
8
  import {
9
9
  rootAllowedFiles,
10
10
  rootAllowedDirs,
11
11
  isAllowedLibFacetRootFile,
12
+ libFacetRootHintOf,
12
13
  facetIndexShadowOf,
13
14
  rootEntryHintOf
14
- } from "./index-50wb2a26.js";
15
+ } from "./index-55mynnqs.js";
15
16
 
16
17
  // pkgs/@akanjs/devkit/qualityScanner.ts
17
18
  import { createHash } from "crypto";
@@ -505,7 +506,7 @@ var RULE_FIXES = {
505
506
  "akan.layout.app-root-folder": "Move the folder's contents into a conventional app folder (common, env, lib, page, private, public, script, srvkit, ui, or webkit) and delete it.",
506
507
  "akan.layout.lib-root-file": "Move the file into a conventional lib folder (common, env, lib, page, private, public, srvkit, ui, or webkit).",
507
508
  "akan.layout.lib-root-folder": "Move the folder's contents into a conventional lib folder (common, env, lib, page, private, public, srvkit, ui, or webkit) and delete it.",
508
- "akan.layout.lib-facet-file": "Move the file into a domain module folder under lib/; keep the lib facet root limited to generated support facets.",
509
+ "akan.layout.lib-facet-file": "Move the file into a domain module folder under lib/ (a test beside the file it covers, or into srvkit/ when it drives several modules); keep the lib facet root limited to generated support facets.",
509
510
  "akan.layout.facet-index-shadow": "Delete the hand-written index file and run akan sync; move a namespace it builds into <facet>/<Folder>/index.tsx.",
510
511
  "akan.layout.module-ui-file": "Rename the file to an allowed module UI name, or move it to ui/ if it is not a module component.",
511
512
  "akan.ssr.unnecessary-use-client": 'Delete the "use client" directive so the file renders on the server. If it exists only to wrap one client child, drop the wrapper and use the child directly.',
@@ -708,7 +709,7 @@ class AkanQualityScanner {
708
709
  warnings.push(fileWarning("akan.layout.facet-index-shadow", "layout", sourceFile.file, `${path2.basename(sourceFile.file)} ${facetShadow}.`));
709
710
  const libFacetFile = getLibFacetRootFile(sourceFile.file);
710
711
  if (libFacetFile && !isAllowedLibFacetRootFile(libFacetFile))
711
- warnings.push(fileWarning("akan.layout.lib-facet-file", "layout", sourceFile.file, `Unexpected lib facet root file "${libFacetFile}". Keep direct lib/ root files limited to generated support facets.`));
712
+ warnings.push(fileWarning("akan.layout.lib-facet-file", "layout", sourceFile.file, `Unexpected lib facet root file "${libFacetFile}". ${libFacetRootHintOf(libFacetFile) ?? "Keep direct lib/ root files limited to generated support facets."}`));
712
713
  const moduleUiWarning = getModuleUiWarning(sourceFile.file);
713
714
  if (moduleUiWarning)
714
715
  warnings.push(moduleUiWarning);
@@ -3,15 +3,15 @@ import {
3
3
  Workspace,
4
4
  script,
5
5
  command
6
- } from "./index-q6ft9fkc.js";
6
+ } from "./index-6jjtnhkw.js";
7
7
  import"./index-52w0fvh8.js";
8
- import"./index-50wb2a26.js";
8
+ import"./index-55mynnqs.js";
9
9
  import"./index-hj6w4ka1.js";
10
- import"./index-h9ht6s60.js";
11
- import"./index-zyw622y1.js";
10
+ import"./index-05g3jayg.js";
11
+ import"./index-2mx692qq.js";
12
12
  import {
13
13
  RepairRunner
14
- } from "./index-hp3stk1m.js";
14
+ } from "./index-fm46xrf1.js";
15
15
 
16
16
  // pkgs/@akanjs/cli/repair/repair.script.ts
17
17
  import { Logger } from "akanjs/common";
@@ -2,15 +2,15 @@
2
2
  import {
3
3
  Sys,
4
4
  command
5
- } from "./index-q6ft9fkc.js";
5
+ } from "./index-6jjtnhkw.js";
6
6
  import"./index-52w0fvh8.js";
7
- import"./index-50wb2a26.js";
7
+ import"./index-55mynnqs.js";
8
8
  import {
9
9
  renderPrimitiveReport
10
- } from "./index-h9ht6s60.js";
10
+ } from "./index-05g3jayg.js";
11
11
  import {
12
12
  ScalarScript
13
- } from "./index-cb6yc5mb.js";
13
+ } from "./index-c76k723e.js";
14
14
 
15
15
  // pkgs/@akanjs/cli/scalar/scalar.command.ts
16
16
  import { Logger, lowerlize } from "akanjs/common";
@@ -1,37 +1,42 @@
1
1
  // @bun
2
- import {
3
- FileSys2
4
- } from "./index-52w0fvh8.js";
5
- import {
6
- GlobalConfig
7
- } from "./index-94s080qm.js";
8
2
  import {
9
3
  Workspace,
10
4
  CommandContainer,
11
5
  runner,
12
6
  script,
13
7
  command
14
- } from "./index-q6ft9fkc.js";
8
+ } from "./index-6jjtnhkw.js";
9
+ import {
10
+ FileSys2
11
+ } from "./index-52w0fvh8.js";
15
12
  import {
16
13
  LibSource,
17
14
  Executor,
18
15
  WorkspaceExecutor,
19
16
  AppExecutor
20
- } from "./index-50wb2a26.js";
17
+ } from "./index-55mynnqs.js";
18
+ import {
19
+ GlobalConfig
20
+ } from "./index-r2tpeb3n.js";
21
+ import {
22
+ SubspaceConfig
23
+ } from "./index-ww9bhp67.js";
21
24
  import {
22
25
  SlicePlanner
23
- } from "./index-50jjwg8y.js";
26
+ } from "./index-qt22xstj.js";
24
27
 
25
28
  // pkgs/@akanjs/devkit/subspace.ts
26
29
  import { mkdir, readdir, rename, rm } from "fs/promises";
27
30
  import path from "path";
28
31
  class Subspace {
29
32
  static anchorFile = "akan.subspace.json";
30
- static workspaceOnlyEntries = ["akan.subspace.ts"];
33
+ static subspaceOwnedRootEntries = ["akan.config.ts", "Jenkinsfile"];
34
+ static syncSubjectPrefix = "chore(subspace): sync from ";
31
35
  static memberDirs = ["apps", "libs"];
32
36
  static subspaceOwnedDirs = ["env"];
33
37
  static secretsMarkers = ["# akan:secrets (managed by akan.config.ts \u2014 do not edit)", "# akan:secrets:end"];
34
38
  static holdDir = ".akan-subspace-hold";
39
+ static #loopbackRegistry = /https?:\/\/(?:127\.0\.0\.1|localhost|0\.0\.0\.0|\[::1\])(?::\d+)?/;
35
40
  #workspace;
36
41
  #config;
37
42
  #declaration;
@@ -58,6 +63,9 @@ class Subspace {
58
63
  async#git(args) {
59
64
  return await this.#workspace.spawn("git", args);
60
65
  }
66
+ async#cloneGit(args) {
67
+ return await this.#workspace.spawn("git", ["-C", this.#clonePath, ...args]);
68
+ }
61
69
  async branch() {
62
70
  return (await this.#git(["rev-parse", "--abbrev-ref", "HEAD"])).trim();
63
71
  }
@@ -97,6 +105,40 @@ class Subspace {
97
105
  const paths = [...this.#declaration.apps.map((app) => `apps/${app}`), ...libs.map((lib) => `libs/${lib}`)];
98
106
  return { plans, libs, paths };
99
107
  }
108
+ async plan(branch) {
109
+ const { plans, libs } = await this.slice();
110
+ const rootFiles = plans[0]?.rootFiles ?? [];
111
+ const present = new Set(rootFiles.map((file) => file.split("/")[0] ?? file));
112
+ const owned = Subspace.subspaceOwnedRootEntries;
113
+ const excluded = this.#config.exclude.filter((entry) => !owned.includes(entry));
114
+ const excludedTops = new Set(excluded.map((entry) => entry.replace(/\/+$/, "")));
115
+ const libFiles = new Map(plans.flatMap((plan) => Object.entries(plan.libFiles)));
116
+ const { pruned, warnings } = await SlicePlanner.pruneDependencies(this.#workspace, await this.#workspace.getPackageJson(), plans.flatMap((plan) => plan.requiredDependencies));
117
+ let branchRefusal = null;
118
+ try {
119
+ this.#config.assertPushable(branch);
120
+ } catch (error) {
121
+ branchRefusal = error instanceof Error ? error.message : String(error);
122
+ }
123
+ return {
124
+ name: this.name,
125
+ repo: this.#declaration.repo,
126
+ branch,
127
+ branchRefusal,
128
+ apps: [...this.#declaration.apps],
129
+ libs: libs.map((lib) => ({
130
+ name: lib,
131
+ apps: plans.filter((plan) => plan.libs.includes(lib)).map((plan) => plan.app)
132
+ })),
133
+ files: plans.reduce((sum, plan) => sum + plan.appFiles.length, 0) + [...libFiles.values()].reduce((sum, files) => sum + files.length, 0),
134
+ rootEntries: [...present].filter((entry) => !excludedTops.has(entry) && !owned.includes(entry)).sort((a, b) => a.localeCompare(b)),
135
+ excludedRootEntries: excluded,
136
+ subspaceOwnedRootEntries: owned.filter((entry) => present.has(entry)),
137
+ prunedDependencies: pruned,
138
+ lockProblem: rootFiles.includes("bun.lock") ? await this.#loopbackRegistryLock(this.#workspace.workspaceRoot) : null,
139
+ warnings: [...new Set([...plans.flatMap((plan) => plan.warnings), ...warnings])]
140
+ };
141
+ }
100
142
  #isSubspaceOwned(file) {
101
143
  const segments = file.split("/");
102
144
  if (!Subspace.memberDirs.includes(segments[0] ?? ""))
@@ -180,6 +222,34 @@ class Subspace {
180
222
  await Subspace.#copyMissing(to, from);
181
223
  await rm(path.join(this.#clonePath, Subspace.holdDir), { recursive: true, force: true });
182
224
  }
225
+ async#holdRootEntries() {
226
+ const holdRoot = path.join(this.#clonePath, Subspace.holdDir, "root");
227
+ const held = [];
228
+ for (const entry of Subspace.subspaceOwnedRootEntries) {
229
+ const from = path.join(this.#clonePath, entry);
230
+ if (!await FileSys2.entryExists(from))
231
+ continue;
232
+ if (await this.#isPushedCopy(entry))
233
+ continue;
234
+ await mkdir(holdRoot, { recursive: true });
235
+ await rename(from, path.join(holdRoot, entry));
236
+ held.push(entry);
237
+ }
238
+ return held;
239
+ }
240
+ async#restoreRootEntries(held) {
241
+ const holdRoot = path.join(this.#clonePath, Subspace.holdDir, "root");
242
+ for (const entry of Subspace.subspaceOwnedRootEntries) {
243
+ const target = path.join(this.#clonePath, entry);
244
+ await rm(target, { recursive: true, force: true });
245
+ if (held.includes(entry))
246
+ await rename(path.join(holdRoot, entry), target);
247
+ }
248
+ }
249
+ async#isPushedCopy(entry) {
250
+ const subject = await this.#cloneGit(["log", "-1", "--format=%s", "--", entry]).catch(() => "");
251
+ return subject.trim().startsWith(Subspace.syncSubjectPrefix);
252
+ }
183
253
  static async#copyMissing(from, to) {
184
254
  await mkdir(to, { recursive: true });
185
255
  for (const entry of await readdir(from, { withFileTypes: true })) {
@@ -255,13 +325,16 @@ class Subspace {
255
325
  async#applySlice(clone, branch) {
256
326
  const { libs, paths, plans } = await this.slice();
257
327
  const held = await this.#holdSubspaceOwned();
328
+ const heldRootEntries = await this.#holdRootEntries();
258
329
  for (const member of Subspace.memberDirs)
259
330
  await rm(path.join(this.#clonePath, member), { recursive: true, force: true });
260
331
  await this.#extract(["."], ["apps/*", "libs/*", "pkgs/*"]);
261
332
  await this.#extract(paths);
333
+ await this.#restoreRootEntries(heldRootEntries);
262
334
  await this.#restoreSubspaceOwned(held);
263
- for (const entry of [...Subspace.workspaceOnlyEntries, ...this.#config.exclude])
264
- await rm(path.join(this.#clonePath, entry), { recursive: true, force: true });
335
+ for (const entry of this.#config.exclude)
336
+ if (!Subspace.subspaceOwnedRootEntries.includes(entry))
337
+ await rm(path.join(this.#clonePath, entry), { recursive: true, force: true });
265
338
  await this.#rewriteGitignore();
266
339
  const pruned = await this.#rewriteManifest(plans);
267
340
  await this.#rewriteWorkspaceSection(libs);
@@ -311,9 +384,9 @@ class Subspace {
311
384
  const [hash, previous] = await Promise.all([this.#hashLib(clone, lib), this.#committedStamp(clone, lib)]);
312
385
  const manifest = await FileSys2.readJson(manifestPath);
313
386
  const akan = manifest[LibSource.manifestKey] ?? {};
314
- const unchanged = previous?.origin === origin && previous.sha === sha && previous.hash === hash;
315
- const syncedAt = unchanged ? previous.syncedAt : new Date().toISOString();
316
- manifest[LibSource.manifestKey] = { ...akan, source: { origin, sha, hash, syncedAt } };
387
+ const unchanged = previous?.origin === origin && previous.hash === hash;
388
+ const source = unchanged ? previous : { origin, sha, hash, syncedAt: new Date().toISOString() };
389
+ manifest[LibSource.manifestKey] = { ...akan, source };
317
390
  await FileSys2.writeJson(manifestPath, manifest);
318
391
  }
319
392
  async#committedStamp(clone, lib) {
@@ -363,6 +436,13 @@ class Subspace {
363
436
  return { files: [], patch: "" };
364
437
  return { files, patch: await this.#git(["diff", from, "HEAD", "--", ...files]) };
365
438
  }
439
+ async#loopbackRegistryLock(root = this.#clonePath) {
440
+ const lockPath = path.join(root, "bun.lock");
441
+ if (!await FileSys2.fileExists(lockPath))
442
+ return null;
443
+ const registry = (await FileSys2.readText(lockPath)).match(Subspace.#loopbackRegistry)?.[0];
444
+ return registry ? `bun.lock resolves packages from ${registry}, a registry only this machine reaches; reinstall from a public registry and commit that lock first` : null;
445
+ }
366
446
  async#verify(clone) {
367
447
  const env = { ...process.env, ...this.#localEnv() };
368
448
  try {
@@ -398,9 +478,12 @@ class Subspace {
398
478
  if (failure)
399
479
  return { name: this.name, outcome: "refused", reason: failure, changedFiles };
400
480
  }
481
+ const lockProblem = await this.#loopbackRegistryLock();
482
+ if (lockProblem)
483
+ return { name: this.name, outcome: "refused", reason: lockProblem, changedFiles };
401
484
  await this.#writeAnchorFile(branch);
402
485
  await clone.spawn("git", ["add", "-A"]);
403
- const message = `chore(subspace): sync from ${this.#workspace.repoName}@${await this.headSha()}`;
486
+ const message = `${Subspace.syncSubjectPrefix}${this.#workspace.repoName}@${await this.headSha()}`;
404
487
  await clone.spawn("git", ["commit", "--quiet", "-m", message]);
405
488
  await clone.spawn("git", ["push", "--quiet", "origin", branch]);
406
489
  return {
@@ -560,74 +643,37 @@ function formatSubspaceDiff(result) {
560
643
  return sections.join(`
561
644
  `);
562
645
  }
646
+ function formatSubspacePlan(plan) {
647
+ const list = (entries) => entries.length ? entries.join(", ") : "(none)";
648
+ const blockers = [plan.branchRefusal, plan.lockProblem].filter((reason) => !!reason);
649
+ const sections = [
650
+ `Akan Subspace Plan \u2014 ${plan.name} (${plan.branch})`,
651
+ `repo: ${plan.repo}`,
652
+ "what `akan subspace push` would ship, worked out from this checkout alone",
653
+ "",
654
+ `apps: ${list(plan.apps)}`,
655
+ `libs: ${plan.libs.length ? "" : "(none)"}`.trimEnd(),
656
+ ...plan.libs.map((lib) => ` - ${lib.name} (from ${lib.apps.join(", ")})`),
657
+ `files under apps/ and libs/: ${plan.files}`,
658
+ "",
659
+ `root entries pushed: ${list(plan.rootEntries)}`,
660
+ `root entries excluded: ${list(plan.excludedRootEntries)}`,
661
+ `root entries the subspace keeps its own: ${list(plan.subspaceOwnedRootEntries)}`,
662
+ "",
663
+ `Root dependencies left out of package.json (${plan.prunedDependencies.length}):`,
664
+ ...plan.prunedDependencies.length ? plan.prunedDependencies.map((dep) => ` - ${dep}`) : [" (none)"],
665
+ "",
666
+ blockers.length ? "A push would be refused:" : "Nothing here would make a push refuse.",
667
+ ...blockers.map((reason) => ` - ${reason}`),
668
+ ...plan.warnings.length ? ["", `Warnings (${plan.warnings.length}):`, ...plan.warnings.map((w) => ` - ${w}`)] : []
669
+ ];
670
+ return sections.join(`
671
+ `);
672
+ }
563
673
 
564
674
  // pkgs/@akanjs/cli/subspace/subspace.script.ts
565
675
  import { Logger } from "akanjs/common";
566
676
 
567
- // pkgs/@akanjs/devkit/subspaceConfig.ts
568
- import path2 from "path";
569
- class SubspaceConfig {
570
- static fileName = "akan.subspace.ts";
571
- static defaultPushableBranches = ["main", "develop", "debug"];
572
- static async from(workspace) {
573
- const configPath = path2.join(workspace.workspaceRoot, SubspaceConfig.fileName);
574
- if (!await FileSys2.fileExists(configPath))
575
- return null;
576
- const input = await import(configPath).then((mod) => mod.default);
577
- return new SubspaceConfig(input);
578
- }
579
- pushableBranches;
580
- exclude;
581
- subspaces;
582
- constructor(input) {
583
- this.pushableBranches = input.pushableBranches ?? SubspaceConfig.defaultPushableBranches;
584
- this.exclude = input.exclude ?? [];
585
- this.subspaces = input.subspaces ?? [];
586
- this.#assertValid();
587
- }
588
- #assertValid() {
589
- const seenNames = new Set;
590
- const appOwners = new Map;
591
- const idOwners = new Map;
592
- for (const subspace of this.subspaces) {
593
- if (!subspace.name || !subspace.repo)
594
- throw new Error(`${SubspaceConfig.fileName}: every subspace needs a name and a repo`);
595
- if (seenNames.has(subspace.name))
596
- throw new Error(`${SubspaceConfig.fileName}: duplicate subspace "${subspace.name}"`);
597
- seenNames.add(subspace.name);
598
- if (!subspace.apps?.length)
599
- throw new Error(`${SubspaceConfig.fileName}: subspace "${subspace.name}" declares no apps`);
600
- for (const app of subspace.apps) {
601
- const owner = appOwners.get(app);
602
- if (owner)
603
- throw new Error(`${SubspaceConfig.fileName}: app "${app}" is claimed by both "${owner}" and "${subspace.name}"`);
604
- appOwners.set(app, subspace.name);
605
- }
606
- if (!subspace.workspaceId)
607
- continue;
608
- const idOwner = idOwners.get(subspace.workspaceId);
609
- if (idOwner)
610
- throw new Error(`${SubspaceConfig.fileName}: subspaces "${idOwner}" and "${subspace.name}" declare the same workspaceId`);
611
- idOwners.set(subspace.workspaceId, subspace.name);
612
- }
613
- }
614
- select(names) {
615
- if (!names.length)
616
- return this.subspaces;
617
- return names.map((name) => {
618
- const subspace = this.subspaces.find((candidate) => candidate.name === name);
619
- if (!subspace)
620
- throw new Error(`${SubspaceConfig.fileName}: unknown subspace "${name}"`);
621
- return subspace;
622
- });
623
- }
624
- assertPushable(branch) {
625
- if (this.pushableBranches.includes(branch))
626
- return;
627
- throw new Error(`Branch "${branch}" is not pushable. ${SubspaceConfig.fileName} allows: ${this.pushableBranches.join(", ")}`);
628
- }
629
- }
630
-
631
677
  // pkgs/@akanjs/cli/subspace/subspace.runner.ts
632
678
  var loadPrompts = async () => await import("@inquirer/prompts");
633
679
 
@@ -636,7 +682,7 @@ class SubspaceRunner extends runner("subspace") {
636
682
  async config(workspace) {
637
683
  const config = await SubspaceConfig.from(workspace);
638
684
  if (!config)
639
- throw new Error(`No ${SubspaceConfig.fileName} at the workspace root. Declare the customer repos there first.`);
685
+ throw new Error("The workspace root akan.config.ts declares no `subspace`. Declare the customer repos there first.");
640
686
  return config;
641
687
  }
642
688
  async subspaces(workspace, names) {
@@ -655,7 +701,7 @@ class SubspaceRunner extends runner("subspace") {
655
701
  static async chooseNames(config, action, { interactive, prompts }) {
656
702
  const multiple = SubspaceRunner.multiTargetActions.includes(action);
657
703
  if (!config.subspaces.length)
658
- throw new Error(`${SubspaceConfig.fileName} declares no subspaces.`);
704
+ throw new Error(`${SubspaceConfig.source} declares no subspaces.`);
659
705
  if (!interactive) {
660
706
  throw new Error(multiple ? `\`akan subspace ${action}\` needs a subspace name when it cannot prompt. Pass one, or run \`akan subspace ${action}-all\` for every subspace.` : `\`akan subspace ${action}\` needs a subspace name when it cannot prompt \u2014 it is reviewed one repo at a time.`);
661
707
  }
@@ -684,6 +730,13 @@ class SubspaceRunner extends runner("subspace") {
684
730
  results.push(await subspace.push(branch, { verify }));
685
731
  return results;
686
732
  }
733
+ async plan(workspace, name) {
734
+ const { subspaces, branch } = await this.subspaces(workspace, [name]);
735
+ const [subspace] = subspaces;
736
+ if (!subspace)
737
+ throw new Error(`Unknown subspace "${name}"`);
738
+ return await subspace.plan(branch);
739
+ }
687
740
  async diff(workspace, name, filter) {
688
741
  const { subspaces, branch } = await this.subspaces(workspace, [name]);
689
742
  const [subspace] = subspaces;
@@ -706,7 +759,7 @@ class SubspaceRunner extends runner("subspace") {
706
759
  const subspace = new Subspace(workspace, config, declaration);
707
760
  const { workspaceId } = subspace;
708
761
  if (!workspaceId)
709
- throw new Error(`Subspace "${subspace.name}" declares no workspaceId. Add its cloud workspace id to ${SubspaceConfig.fileName}.`);
762
+ throw new Error(`Subspace "${subspace.name}" declares no workspaceId. Add its cloud workspace id to ${SubspaceConfig.source}.`);
710
763
  if (workspaceId === workspace.getWorkspaceId({ allowEmpty: true }))
711
764
  throw new Error(`Subspace "${subspace.name}" declares this workspace's own id. Uploading a slice there would replace the workspace's env archive with a subset of it.`);
712
765
  const apps = subspace.apps;
@@ -714,7 +767,7 @@ class SubspaceRunner extends runner("subspace") {
714
767
  if (!asked)
715
768
  return { name: subspace.name, workspaceId, host, apps, libs: [], outcome: "cancelled", files: [] };
716
769
  const { libs } = await subspace.slice();
717
- const { CloudScript } = await import("./cloud.script-64gzba8r.js");
770
+ const { CloudScript } = await import("./cloud.script-r7eyfey4.js");
718
771
  const { files } = await CommandContainer.get(CloudScript).uploadEnv(workspace, {
719
772
  host,
720
773
  workspaceId,
@@ -758,6 +811,12 @@ class SubspaceScript extends script("subspace", [SubspaceRunner]) {
758
811
  if (results.some((result) => result.outcome === "refused"))
759
812
  process.exitCode = 1;
760
813
  }
814
+ async plan(workspace, name, format = "text") {
815
+ const spinner = workspace.spinning(`Planning ${name}...`);
816
+ const plan = await this.subspaceRunner.plan(workspace, name);
817
+ spinner.succeed(`${plan.apps.length} app(s), ${plan.libs.length} librar(ies), ${plan.files} file(s)`);
818
+ Logger.rawLog(format === "json" ? JSON.stringify(plan, null, 2) : formatSubspacePlan(plan));
819
+ }
761
820
  async diff(workspace, name, filter, format = "text") {
762
821
  const spinner = workspace.spinning(`Diffing ${name}...`);
763
822
  const result = await this.subspaceRunner.diff(workspace, name, filter);
@@ -766,7 +825,10 @@ class SubspaceScript extends script("subspace", [SubspaceRunner]) {
766
825
  Logger.rawLog(format === "json" ? JSON.stringify(result, null, 2) : formatSubspaceDiff(result));
767
826
  }
768
827
  async uploadEnv(workspace, name, { host, force, format }) {
769
- const result = await this.subspaceRunner.uploadEnv(workspace, name, { host, force });
828
+ const result = await this.subspaceRunner.uploadEnv(workspace, name, {
829
+ host: await GlobalConfig.hostFor(workspace, host),
830
+ force
831
+ });
770
832
  Logger.rawLog(format === "json" ? JSON.stringify(result, null, 2) : formatSubspaceEnvUpload(result));
771
833
  }
772
834
  async pull(workspace, name, { adoptLibs, format }) {
@@ -780,9 +842,9 @@ class SubspaceScript extends script("subspace", [SubspaceRunner]) {
780
842
  // pkgs/@akanjs/cli/subspace/subspace.command.ts
781
843
  class SubspaceCommand extends command("subspace", [SubspaceScript], ({ public: target }) => ({
782
844
  subspace: target({ desc: "Mirror apps and libraries between this workspace and the customer repos it serves" }).arg("action", String, {
783
- desc: "subspace action; status-all and push-all take every declared subspace",
845
+ desc: "subspace action; plan reads this checkout alone, status-all and push-all take every declared subspace",
784
846
  default: "status",
785
- enum: ["status", "status-all", "diff", "push", "push-all", "pull", "upload-env"]
847
+ enum: ["status", "status-all", "plan", "diff", "push", "push-all", "pull", "upload-env"]
786
848
  }).arg("subspace", String, {
787
849
  desc: "subspace name; asks which one(s) when omitted, and is refused by status-all / push-all",
788
850
  nullable: true
@@ -790,7 +852,10 @@ class SubspaceCommand extends command("subspace", [SubspaceScript], ({ public: t
790
852
  flag: "y",
791
853
  desc: "run bun install + akan sync in the subspace before committing",
792
854
  default: true
793
- }).option("adoptLibs", Boolean, { desc: "pull: also apply the subspace's library edits", default: false }).option("path", String, { desc: "diff: limit to one path", nullable: true }).option("host", String, { desc: "upload-env: host of the cloud to target", default: GlobalConfig.akanCloudHost }).option("force", Boolean, {
855
+ }).option("adoptLibs", Boolean, { desc: "pull: also apply the subspace's library edits", default: false }).option("path", String, { desc: "diff: limit to one path", nullable: true }).option("host", String, {
856
+ desc: "upload-env: host of the cloud to target (default: cloud.host of the root akan.config.ts, else Akan Cloud)",
857
+ nullable: true
858
+ }).option("force", Boolean, {
794
859
  flag: "F",
795
860
  desc: "upload-env: replace the subspace's env archive without asking",
796
861
  default: false
@@ -798,8 +863,8 @@ class SubspaceCommand extends command("subspace", [SubspaceScript], ({ public: t
798
863
  const output = format;
799
864
  const all = action === "status-all" || action === "push-all";
800
865
  const base = all ? action.slice(0, -"-all".length) : action;
801
- if (base !== "status" && base !== "diff" && base !== "push" && base !== "pull" && base !== "upload-env")
802
- throw new Error(`Unknown subspace action: ${action}. Use status, status-all, diff, push, push-all, pull or upload-env.`);
866
+ if (base !== "status" && base !== "plan" && base !== "diff" && base !== "push" && base !== "pull" && base !== "upload-env")
867
+ throw new Error(`Unknown subspace action: ${action}. Use status, status-all, plan, diff, push, push-all, pull or upload-env.`);
803
868
  if (all && subspace)
804
869
  throw new Error(`\`akan subspace ${action}\` takes no name. Run \`akan subspace ${base} ${subspace}\` instead.`);
805
870
  const names = subspace ? [subspace] : all ? [] : await this.subspaceScript.select(workspace, base);
@@ -811,7 +876,9 @@ class SubspaceCommand extends command("subspace", [SubspaceScript], ({ public: t
811
876
  const [name] = names;
812
877
  if (!name)
813
878
  throw new Error(`\`akan subspace ${base}\` needs a name \u2014 it is reviewed one repo at a time.`);
814
- if (base === "diff")
879
+ if (base === "plan")
880
+ await this.subspaceScript.plan(workspace, name, output);
881
+ else if (base === "diff")
815
882
  await this.subspaceScript.diff(workspace, name, filePath, output);
816
883
  else if (base === "upload-env")
817
884
  await this.subspaceScript.uploadEnv(workspace, name, { host, force, format: output });
@@ -11,9 +11,9 @@ import { Link } from "akanjs/ui";
11
11
 
12
12
  export default page()
13
13
  .param("taskId", ID)
14
- .render(async ({ taskId }) => {
14
+ .render(({ taskId }) => {
15
15
  const { l } = usePage();
16
- const { taskView } = await fetch.viewTask(taskId);
16
+ const { taskView } = fetch.viewTask(taskId);
17
17
  return (
18
18
  <main className="mx-auto max-w-4xl px-6 py-8">
19
19
  <div className="mb-6 flex items-center justify-between">
@@ -11,8 +11,8 @@ import { Link, Load } from "akanjs/ui";
11
11
 
12
12
  export default page()
13
13
  .param("taskId", ID)
14
- .render(async ({ taskId }) => {
15
- const { taskEdit } = await fetch.editTask(taskId);
14
+ .render(({ taskId }) => {
15
+ const { taskEdit } = fetch.editTask(taskId);
16
16
 
17
17
  return (
18
18
  <main className="mx-auto max-w-2xl px-6 py-8">
@@ -8,9 +8,9 @@ import { jellyButtonRecipe } from "@apps/${dict.appName}/ui";
8
8
  import { page } from "akanjs/client";
9
9
  import { Link } from "akanjs/ui";
10
10
 
11
- export default page().render(async () => {
11
+ export default page().render(() => {
12
12
  const { l } = usePage();
13
- const { taskInitInPublic } = await fetch.initTaskInPublic();
13
+ const { taskInitInPublic } = fetch.initTaskInPublic();
14
14
  return (
15
15
  <main className="mx-auto max-w-4xl px-6 py-8">
16
16
  <div className="mb-6 flex items-center justify-between">
@@ -18,9 +18,9 @@ import { Load } from "akanjs/ui";
18
18
  export default page()
19
19
  .param("${dict.model}Id", ID)
20
20
  .config({ transition: "none" })
21
- .render(async ({ ${dict.model}Id }) => {
21
+ .render(({ ${dict.model}Id }) => {
22
22
  const { l } = usePage();
23
- const [{ ${dict.model}Edit }] = await Promise.all([fetch.edit${dict.Model}(${dict.model}Id)]);
23
+ const { ${dict.model}Edit } = fetch.edit${dict.Model}(${dict.model}Id);
24
24
  return (
25
25
  <div className="container">
26
26
  <div className="m-4 mt-8 flex justify-between">
@@ -26,14 +26,14 @@ export default page()
26
26
  <meta property="og:description" content={${dict.model}Id} />
27
27
  </>
28
28
  ))
29
- .render(async ({ ${dict.model}Id }) => {
29
+ .render(({ ${dict.model}Id }) => {
30
30
  const { l } = usePage();
31
- const [{ ${dict.model}, ${dict.model}View }] = await Promise.all([fetch.view${dict.Model}(${dict.model}Id)]);
31
+ const { ${dict.model}View } = fetch.view${dict.Model}(${dict.model}Id);
32
32
  return (
33
33
  <div className="container flex flex-col gap-4">
34
34
  <div className="flex items-center gap-4 font-bold text-lg">
35
35
  <${dict.Model}.Zone.View view={${dict.model}View} />
36
- <Link className={buttonRecipe()} href={\`/${dict.model}/\${${dict.model}.id}/edit\`}>
36
+ <Link className={buttonRecipe()} href={\`/${dict.model}/\${${dict.model}Id}/edit\`}>
37
37
  {l("base.updateModel", { model: l("${dict.model}.modelName") })}
38
38
  </Link>
39
39
  </div>
@@ -16,9 +16,9 @@ import { buttonRecipe, Link } from "akanjs/ui";
16
16
 
17
17
  export default page()
18
18
  .config({ transition: "none" })
19
- .render(async () => {
19
+ .render(() => {
20
20
  const { l } = usePage();
21
- const [{ ${dict.model}InitInPublic }] = await Promise.all([fetch.init${dict.Model}InPublic()]);
21
+ const { ${dict.model}InitInPublic } = fetch.init${dict.Model}InPublic();
22
22
  return (
23
23
  <div className="container flex flex-col gap-4">
24
24
  <div className="mt-5 h-full w-full px-5">
@@ -16,15 +16,15 @@ import { buttonRecipe, Model } from "akanjs/ui";
16
16
 
17
17
  export default page()
18
18
  .config({ transition: "none" })
19
- .render(async () => {
19
+ .render(() => {
20
20
  const { l } = usePage();
21
- const [{ ${dict.model}InitInPublic }] = await Promise.all([fetch.init${dict.Model}InPublic()]);
21
+ const { ${dict.model}InitInPublic } = fetch.init${dict.Model}InPublic();
22
22
  return (
23
23
  <>
24
24
  <div className="flex animate-fadeIn items-center gap-4 px-4 pt-4">
25
25
  <div className="font-bold text-lg md:text-4xl">{l("${dict.model}.modelName")}</div>
26
26
  <Model.New
27
- className={buttonRecipe({ size: "sm", variant: "ghost" })}
27
+ trigger={<button className={buttonRecipe({ size: "sm", variant: "ghost" })}>{l("base.new")}</button>}
28
28
  slice={fetch.slice.${dict.model}InPublic}
29
29
  renderTitle="id"
30
30
  >
@@ -21,9 +21,9 @@ ${databaseModules.map((module) => `export * from "./${module}/${module}.constant
21
21
  ${databaseModules
22
22
  .map((module) => {
23
23
  const names = { Module: capitalize(module) };
24
- return `export const ${module} = ConstantRegistry.buildModel("${module}" as const, ${module}Cnst.${names.Module}Input, ${module}Cnst.${names.Module}Object, ${module}Cnst.${names.Module}, ${module}Cnst.Light${names.Module}, ${module}Cnst.${names.Module}Insight, ${module}Cnst);`;
24
+ return `export const ${module} = ConstantRegistry.buildModel("${module}" as const, ${module}Cnst.${names.Module}Input, ${module}Cnst.${names.Module}Object, ${module}Cnst.${names.Module}, ${module}Cnst.Light${names.Module}, ${module}Cnst.${names.Module}Insight, ${module}Cnst, "${scanInfo.name}");`;
25
25
  })
26
26
  .join("\n")}
27
- ${scalarModules.map((module) => `export const ${module} = ConstantRegistry.buildScalar("${module}" as const, ${module}Cnst.${capitalize(module)}, ${module}Cnst);`).join("\n")}
27
+ ${scalarModules.map((module) => `export const ${module} = ConstantRegistry.buildScalar("${module}" as const, ${module}Cnst.${capitalize(module)}, ${module}Cnst, "${scanInfo.name}");`).join("\n")}
28
28
  `;
29
29
  }