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

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 (98) hide show
  1. package/.build-stamp +1 -1
  2. package/{CodeAgent-kyprmy06.js → CodeAgent-rkjyxgec.js} +2 -2
  3. package/{CodeTui-21mtze3j.js → CodeTui-h7pyj6xa.js} +1 -1
  4. package/TunnelShare-661cxz5e.js +14 -0
  5. package/{agent.command-f3gsc1gd.js → agent.command-3qxf4xcj.js} +5 -5
  6. package/akanCode.js +2 -2
  7. package/{application.command-5s780vnz.js → application.command-m1kcd7jf.js} +54 -42
  8. package/{applicationBuildRunner-1yaqsrdq.js → applicationBuildRunner-dj0xkrep.js} +26 -33
  9. package/buildBatch.proc.js +24 -15
  10. package/{cloud.command-yj6xb2a7.js → cloud.command-9cp1t0f5.js} +60 -67
  11. package/cloud.script-dgstqt1m.js +24 -0
  12. package/{code.command-3wkn4ja5.js → code.command-0ad6gw2a.js} +6 -6
  13. package/{context.command-9jjn3gcw.js → context.command-0qx447mb.js} +17 -13
  14. package/context.runner-1vsjztjg.js +25 -0
  15. package/{dependencyScanner-06z2dmrr.js → dependencyScanner-3afj2vt1.js} +1 -1
  16. package/{guideline.command-dtczrqpf.js → guideline.command-cv5bmrx2.js} +2 -2
  17. package/guidelines/agentRule/agentRule.instruction.md +3 -3
  18. package/guidelines/conventions/conventions.instruction.md +457 -1071
  19. package/guidelines/cssRule/cssRule.instruction.md +28 -7
  20. package/guidelines/fieldRule/fieldRule.instruction.md +62 -0
  21. package/guidelines/framework/framework.instruction.md +4 -7
  22. package/guidelines/mcpRule/mcpRule.instruction.md +43 -20
  23. package/guidelines/modelConstant/modelConstant.instruction.md +2 -1
  24. package/guidelines/modelStore/modelStore.instruction.md +9 -0
  25. package/guidelines/pageRule/pageRule.instruction.md +80 -0
  26. package/guidelines/queryRule/queryRule.instruction.md +44 -11
  27. package/guidelines/runtimeRule/runtimeRule.instruction.md +113 -17
  28. package/guidelines/ssrRule/ssrRule.instruction.md +13 -0
  29. package/guidelines/transportRule/transportRule.instruction.md +135 -13
  30. package/guidelines/workspaceOnboarding/workspaceOnboarding.instruction.md +4 -4
  31. package/guidelines/workspaceRecipes/workspaceRecipes.instruction.md +68 -7
  32. package/incrementalBuilder.proc.js +10 -7
  33. package/index-192ha4xh.js +1173 -0
  34. package/{index-c76k723e.js → index-1p98exm2.js} +2 -2
  35. package/{index-rwq3ekqc.js → index-1xj2t7cb.js} +7 -7
  36. package/{index-rtct7d43.js → index-20bxxyvn.js} +80 -80
  37. package/{index-qt22xstj.js → index-35hc95h5.js} +5 -3
  38. package/{index-8r4d7cmt.js → index-4mhy6qme.js} +4 -4
  39. package/{index-ww9bhp67.js → index-4pf8g7ff.js} +1 -1
  40. package/{index-2mx692qq.js → index-5edzv8fn.js} +5 -5
  41. package/{index-k9rrten6.js → index-5nd1wtkv.js} +92 -94
  42. package/index-62md4dwy.js +260 -0
  43. package/index-665zp9rs.js +271 -0
  44. package/{index-2etcv0sc.js → index-86jt2a0e.js} +1 -1
  45. package/{index-krq7btbt.js → index-8fp3b47h.js} +419 -418
  46. package/{index-c2yrsxdh.js → index-9z426msb.js} +109 -328
  47. package/{index-ctanya41.js → index-awbdfcf2.js} +4 -4
  48. package/{index-3hx1z96c.js → index-bqbf654f.js} +1 -1
  49. package/{index-rc5rwyps.js → index-bspm21y1.js} +71 -60
  50. package/index-cpb3nvma.js +667 -0
  51. package/{index-jx4bzvwg.js → index-d47ypebv.js} +97 -55
  52. package/{index-ae5pwhgs.js → index-d72hfmdm.js} +15 -13
  53. package/{index-e0cvze1s.js → index-fmzcs8v6.js} +4 -4
  54. package/{index-8ggztp8k.js → index-fyq1z6kt.js} +68 -201
  55. package/{index-1w2qaee2.js → index-g1mbppa9.js} +331 -259
  56. package/{index-gnyf1fqy.js → index-j237xtew.js} +6 -6
  57. package/{index-fm46xrf1.js → index-j4mkhz93.js} +3 -3
  58. package/{index-6jjtnhkw.js → index-jw1zm273.js} +74 -34
  59. package/{index-q958tqqg.js → index-m9jj544v.js} +1 -1
  60. package/index-mdjc93e1.js +33 -0
  61. package/{index-c1x0be5j.js → index-pydbnfez.js} +2 -2
  62. package/{index-56d2g8ms.js → index-qwa5k1j8.js} +1 -1
  63. package/{index-v29746vb.js → index-tamvyvj3.js} +3 -3
  64. package/{index-05g3jayg.js → index-trpasv9q.js} +260 -259
  65. package/{index-2ahj896a.js → index-tvehn2sp.js} +1 -1
  66. package/{index-55mynnqs.js → index-va06nr10.js} +1434 -1173
  67. package/index-vk0pa0mn.js +240 -0
  68. package/{index-aprt7pkz.js → index-w44cy4vs.js} +2271 -3369
  69. package/index.js +22 -22
  70. package/{library.command-gaga5bmq.js → library.command-erp2f56n.js} +3 -3
  71. package/{localRegistry.command-vsreyy42.js → localRegistry.command-y176p558.js} +15 -11
  72. package/{module.command-7vpf6db4.js → module.command-gs2r615e.js} +5 -5
  73. package/{package.command-twmvvf69.js → package.command-j10r76ax.js} +4 -4
  74. package/package.json +21 -21
  75. package/{page.command-5asq7fdf.js → page.command-4h4a8pnh.js} +3 -3
  76. package/{pipeline.command-d0v38572.js → pipeline.command-a2nrk26m.js} +221 -220
  77. package/{primitive.command-dyrs2fm8.js → primitive.command-w6t0asqz.js} +6 -6
  78. package/{projectTypecheck-cvtkrzp2.js → projectTypecheck-f534enqj.js} +1 -1
  79. package/{quality.command-6emsqpbg.js → quality.command-77x7vbbx.js} +4 -4
  80. package/{repair.command-8t6aegv5.js → repair.command-y6sstev7.js} +5 -5
  81. package/{scalar.command-bnggy1x7.js → scalar.command-20b8cy0k.js} +4 -4
  82. package/{subspace.command-md2teygq.js → subspace.command-5c7yasd7.js} +424 -364
  83. package/templates/appSample/lib/task/Task.Util.tsx +7 -7
  84. package/templates/appSample/lib/task/Task.Zone.tsx +1 -1
  85. package/templates/appSample/ui/QuantityControl.tsx +2 -2
  86. package/templates/crudSinglePage/page.tsx +1 -1
  87. package/templates/lib/cnst.ts +0 -1
  88. package/templates/workspaceRoot/.gitignore.template +23 -52
  89. package/templates/workspaceRoot/biome.json.template +1 -1
  90. package/{tunnel.command-jx3tdm2d.js → tunnel.command-2kw8vt9g.js} +8 -4
  91. package/{workflow.command-jx0wag5r.js → workflow.command-2djwmpqf.js} +10 -10
  92. package/{workspace.command-yxd7rr7g.js → workspace.command-6qfgq4b4.js} +38 -34
  93. package/TunnelShare-q4jq08qg.js +0 -9
  94. package/cloud.script-h6pvcpa2.js +0 -20
  95. package/context.runner-atcrzz5b.js +0 -21
  96. package/index-6w0rbr9d.js +0 -68
  97. package/index-n68jx79w.js +0 -120
  98. package/index-pgj2rt78.js +0 -404
@@ -5,7 +5,7 @@ import {
5
5
  runner,
6
6
  script,
7
7
  command
8
- } from "./index-6jjtnhkw.js";
8
+ } from "./index-jw1zm273.js";
9
9
  import {
10
10
  FileSys2
11
11
  } from "./index-52w0fvh8.js";
@@ -14,21 +14,31 @@ import {
14
14
  Executor,
15
15
  WorkspaceExecutor,
16
16
  AppExecutor
17
- } from "./index-55mynnqs.js";
17
+ } from "./index-va06nr10.js";
18
18
  import {
19
19
  GlobalConfig
20
- } from "./index-pgj2rt78.js";
20
+ } from "./index-cpb3nvma.js";
21
+ import"./index-m9jj544v.js";
22
+ import"./index-g1mbppa9.js";
23
+ import"./index-192ha4xh.js";
21
24
  import {
22
25
  SubspaceConfig
23
- } from "./index-ww9bhp67.js";
26
+ } from "./index-4pf8g7ff.js";
24
27
  import {
25
28
  SlicePlanner
26
- } from "./index-qt22xstj.js";
29
+ } from "./index-35hc95h5.js";
30
+ import"./index-665zp9rs.js";
27
31
 
28
32
  // pkgs/@akanjs/devkit/subspace.ts
33
+ import { mkdir as mkdir2 } from "fs/promises";
34
+ import path2 from "path";
35
+ import { toError as toError2 } from "akanjs/common";
36
+
37
+ // pkgs/@akanjs/devkit/subspaceClone.ts
29
38
  import { mkdir, readdir, rename, rm } from "fs/promises";
30
39
  import path from "path";
31
- class Subspace {
40
+ import { toError } from "akanjs/common";
41
+ class SubspaceClone {
32
42
  static anchorFile = "akan.subspace.json";
33
43
  static subspaceOwnedRootEntries = ["akan.config.ts", "Jenkinsfile"];
34
44
  static syncSubjectPrefix = "chore(subspace): sync from ";
@@ -36,175 +46,107 @@ class Subspace {
36
46
  static subspaceOwnedDirs = ["env"];
37
47
  static secretsMarkers = ["# akan:secrets (managed by akan.config.ts \u2014 do not edit)", "# akan:secrets:end"];
38
48
  static holdDir = ".akan-subspace-hold";
39
- static #loopbackRegistry = /https?:\/\/(?:127\.0\.0\.1|localhost|0\.0\.0\.0|\[::1\])(?::\d+)?/;
49
+ static isSubspaceOwned(file) {
50
+ const segments = file.split("/");
51
+ if (!SubspaceClone.memberDirs.includes(segments[0] ?? ""))
52
+ return false;
53
+ return SubspaceClone.subspaceOwnedDirs.includes(segments[2] ?? "");
54
+ }
55
+ static async open(workspace, declaration, branch) {
56
+ const clone = new SubspaceClone(workspace, declaration);
57
+ await clone.#ensure(branch);
58
+ return clone;
59
+ }
40
60
  #workspace;
41
- #config;
42
61
  #declaration;
43
- constructor(workspace, config, declaration) {
62
+ #clonePath;
63
+ #clone;
64
+ constructor(workspace, declaration) {
44
65
  this.#workspace = workspace;
45
- this.#config = config;
46
66
  this.#declaration = declaration;
47
- }
48
- get name() {
49
- return this.#declaration.name;
50
- }
51
- get apps() {
52
- return this.#declaration.apps;
53
- }
54
- get workspaceId() {
55
- return this.#declaration.workspaceId;
56
- }
57
- get #remote() {
58
- return `subspace-${this.#declaration.name}`;
59
- }
60
- get #clonePath() {
61
- return path.join(this.#workspace.workspaceRoot, ".akan/subspace", this.#declaration.name);
62
- }
63
- async#git(args) {
64
- return await this.#workspace.spawn("git", args);
67
+ this.#clonePath = path.join(workspace.workspaceRoot, ".akan/subspace", declaration.name);
68
+ this.#clone = new Executor(`subspace-${declaration.name}`, this.#clonePath);
65
69
  }
66
70
  async#cloneGit(args) {
67
71
  return await this.#workspace.spawn("git", ["-C", this.#clonePath, ...args]);
68
72
  }
69
- async branch() {
70
- return (await this.#git(["rev-parse", "--abbrev-ref", "HEAD"])).trim();
71
- }
72
- async headSha() {
73
- return (await this.#git(["rev-parse", "--short", "HEAD"])).trim();
74
- }
75
- async fetch(branch) {
76
- const remotes = (await this.#git(["remote"])).split(`
77
- `).map((line) => line.trim());
78
- if (remotes.includes(this.#remote))
79
- await this.#git(["remote", "set-url", this.#remote, this.#declaration.repo]);
80
- else
81
- await this.#git(["remote", "add", this.#remote, this.#declaration.repo]);
82
- try {
83
- await this.#git(["fetch", "--quiet", this.#remote, branch]);
84
- return true;
85
- } catch {
86
- return false;
73
+ async#ensure(branch) {
74
+ if (!await FileSys2.dirExists(path.join(this.#clonePath, ".git"))) {
75
+ await rm(this.#clonePath, { recursive: true, force: true });
76
+ await mkdir(path.dirname(this.#clonePath), { recursive: true });
77
+ await this.#workspace.spawn("git", ["clone", "--quiet", this.#declaration.repo, this.#clonePath]);
87
78
  }
79
+ await this.#clone.spawn("git", ["fetch", "--quiet", "origin", branch]);
80
+ await this.#clone.spawn("git", ["checkout", "--quiet", "-f", "-B", branch, `origin/${branch}`]);
81
+ await this.#clone.spawn("git", ["clean", "-qfd"]);
88
82
  }
89
- async anchor(branch) {
90
- const ref = `${this.#remote}/${branch}`;
91
- const commit = (await this.#git(["log", "-1", "--format=%H", ref, "--", Subspace.anchorFile])).trim();
92
- if (!commit)
93
- return { commit: null, incoming: [] };
94
- const log = await this.#git(["log", "--format=%H%x09%an%x09%s", `${commit}..${ref}`]);
95
- const incoming = log.split(`
96
- `).filter((line) => !!line.trim()).map((line) => {
97
- const [sha = "", author = "", ...subject] = line.split("\t");
98
- return { sha: sha.slice(0, 12), author, subject: subject.join("\t") };
99
- });
100
- return { commit, incoming };
83
+ async applySlice({ libs, paths, plans }, { branch, sha, remove }) {
84
+ const held = await this.#holdSubspaceOwned();
85
+ const heldRootEntries = await this.#holdRootEntries();
86
+ for (const member of SubspaceClone.memberDirs)
87
+ await rm(path.join(this.#clonePath, member), { recursive: true, force: true });
88
+ await this.#extract(["."], ["apps/*", "libs/*", "pkgs/*"]);
89
+ await this.#extract(paths);
90
+ await this.#restoreRootEntries(heldRootEntries);
91
+ await this.#restoreSubspaceOwned(held);
92
+ for (const entry of remove)
93
+ if (!SubspaceClone.subspaceOwnedRootEntries.includes(entry))
94
+ await rm(path.join(this.#clonePath, entry), { recursive: true, force: true });
95
+ await this.#rewriteGitignore();
96
+ const pruned = await this.#rewriteManifest(plans);
97
+ await this.#rewriteWorkspaceSection(libs);
98
+ await this.#writeLocalEnv();
99
+ for (const lib of libs)
100
+ await this.#stampLib(lib, branch, sha);
101
+ return { libs, pruned };
101
102
  }
102
- async slice() {
103
- const plans = await Promise.all(this.#declaration.apps.map(async (appName) => await new SlicePlanner(AppExecutor.from(this.#workspace, appName)).plan()));
104
- const libs = [...new Set(plans.flatMap((plan) => plan.libs))].sort();
105
- const paths = [...this.#declaration.apps.map((app) => `apps/${app}`), ...libs.map((lib) => `libs/${lib}`)];
106
- return { plans, libs, paths };
103
+ async changedFiles() {
104
+ const dirty = (await this.#clone.spawn("git", ["status", "--porcelain"])).trim();
105
+ return dirty ? dirty.split(`
106
+ `).length : 0;
107
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;
108
+ async verify() {
109
+ const env = { ...process.env, ...this.#localEnv() };
118
110
  try {
119
- this.#config.assertPushable(branch);
111
+ await this.#clone.spawn("bun", ["install"], { env });
112
+ for (const app of this.#declaration.apps)
113
+ await this.#clone.spawn("bunx", ["akan", "sync", app], { env });
114
+ return null;
120
115
  } catch (error) {
121
- branchRefusal = error instanceof Error ? error.message : String(error);
116
+ return toError(error).message;
122
117
  }
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
- }
142
- #isSubspaceOwned(file) {
143
- const segments = file.split("/");
144
- if (!Subspace.memberDirs.includes(segments[0] ?? ""))
145
- return false;
146
- return Subspace.subspaceOwnedDirs.includes(segments[2] ?? "");
147
118
  }
148
- #isStamped(file) {
149
- return /^libs\/[^/]+\/package\.json$/.test(file);
119
+ async lock() {
120
+ const lockPath = path.join(this.#clonePath, "bun.lock");
121
+ return await FileSys2.fileExists(lockPath) ? await FileSys2.readText(lockPath) : null;
150
122
  }
151
- async#differsBeyondStamp(ref, file) {
152
- const [theirs, ours] = await Promise.all([
153
- this.#git(["show", `${ref}:${file}`]).catch(() => ""),
154
- this.#git(["show", `HEAD:${file}`]).catch(() => "")
155
- ]);
156
- if (!theirs || !ours)
157
- return true;
158
- const normalize = (content) => {
159
- try {
160
- return LibSource.unstamped(content);
161
- } catch {
162
- return content;
163
- }
164
- };
165
- return normalize(theirs) !== normalize(ours);
123
+ async commit(branch, sha) {
124
+ await this.#writeAnchorFile(branch, sha);
125
+ await this.#clone.spawn("git", ["add", "-A"]);
126
+ const message = `${SubspaceClone.syncSubjectPrefix}${this.#workspace.repoName}@${sha}`;
127
+ await this.#clone.spawn("git", ["commit", "--quiet", "-m", message]);
128
+ await this.#clone.spawn("git", ["push", "--quiet", "origin", branch]);
129
+ return (await this.#clone.spawn("git", ["rev-parse", "--short", "HEAD"])).trim();
166
130
  }
167
- async#changedSliceFiles(ref, diffArgs) {
168
- const files = (await this.#git(["diff", "--name-only", ...diffArgs])).split(`
169
- `).filter((file) => !!file.trim() && !this.#isSubspaceOwned(file));
170
- const kept = await Promise.all(files.map(async (file) => this.#isStamped(file) ? await this.#differsBeyondStamp(ref, file) : true));
171
- return files.filter((_, index) => kept[index]);
172
- }
173
- async status(branch) {
174
- const hasBranch = await this.fetch(branch);
175
- const [{ paths, libs }, anchor] = await Promise.all([
176
- this.slice(),
177
- hasBranch ? this.anchor(branch) : Promise.resolve({ commit: null, incoming: [] })
178
- ]);
179
- if (!hasBranch)
180
- return { name: this.name, branch, hasBranch, anchor, behindPaths: [], driftedLibs: [] };
181
- const ref = `${this.#remote}/${branch}`;
182
- const behindPaths = await this.#changedSliceFiles(ref, ["HEAD", ref, "--", ...paths]);
183
- const driftedLibs = libs.filter((lib) => behindPaths.some((file) => file.startsWith(`libs/${lib}/`)));
184
- return { name: this.name, branch, hasBranch, anchor, behindPaths, driftedLibs };
185
- }
186
- async#ensureClone(branch) {
187
- if (!await FileSys2.dirExists(path.join(this.#clonePath, ".git"))) {
188
- await rm(this.#clonePath, { recursive: true, force: true });
189
- await mkdir(path.dirname(this.#clonePath), { recursive: true });
190
- await this.#workspace.spawn("git", ["clone", "--quiet", this.#declaration.repo, this.#clonePath]);
191
- }
192
- const clone = new Executor(`subspace-${this.name}`, this.#clonePath);
193
- await clone.spawn("git", ["fetch", "--quiet", "origin", branch]);
194
- await clone.spawn("git", ["checkout", "--quiet", "-f", "-B", branch, `origin/${branch}`]);
195
- await clone.spawn("git", ["clean", "-qfd"]);
196
- return clone;
131
+ async#writeAnchorFile(branch, sha) {
132
+ await FileSys2.writeJson(path.join(this.#clonePath, SubspaceClone.anchorFile), {
133
+ workspace: this.#workspace.repoName,
134
+ hubSha: sha,
135
+ branch,
136
+ apps: this.#declaration.apps,
137
+ syncedAt: new Date().toISOString()
138
+ });
197
139
  }
198
140
  async#holdSubspaceOwned() {
199
141
  const held = [];
200
- const holdRoot = path.join(this.#clonePath, Subspace.holdDir);
142
+ const holdRoot = path.join(this.#clonePath, SubspaceClone.holdDir);
201
143
  await rm(holdRoot, { recursive: true, force: true });
202
- for (const member of Subspace.memberDirs) {
144
+ for (const member of SubspaceClone.memberDirs) {
203
145
  const memberRoot = path.join(this.#clonePath, member);
204
146
  if (!await FileSys2.dirExists(memberRoot))
205
147
  continue;
206
148
  for (const name of await readdir(memberRoot)) {
207
- for (const owned of Subspace.subspaceOwnedDirs) {
149
+ for (const owned of SubspaceClone.subspaceOwnedDirs) {
208
150
  const from = path.join(memberRoot, name, owned);
209
151
  if (!await FileSys2.dirExists(from))
210
152
  continue;
@@ -219,13 +161,13 @@ class Subspace {
219
161
  }
220
162
  async#restoreSubspaceOwned(held) {
221
163
  for (const { from, to } of held)
222
- await Subspace.#copyMissing(to, from);
223
- await rm(path.join(this.#clonePath, Subspace.holdDir), { recursive: true, force: true });
164
+ await SubspaceClone.#copyMissing(to, from);
165
+ await rm(path.join(this.#clonePath, SubspaceClone.holdDir), { recursive: true, force: true });
224
166
  }
225
167
  async#holdRootEntries() {
226
- const holdRoot = path.join(this.#clonePath, Subspace.holdDir, "root");
168
+ const holdRoot = path.join(this.#clonePath, SubspaceClone.holdDir, "root");
227
169
  const held = [];
228
- for (const entry of Subspace.subspaceOwnedRootEntries) {
170
+ for (const entry of SubspaceClone.subspaceOwnedRootEntries) {
229
171
  const from = path.join(this.#clonePath, entry);
230
172
  if (!await FileSys2.entryExists(from))
231
173
  continue;
@@ -238,8 +180,8 @@ class Subspace {
238
180
  return held;
239
181
  }
240
182
  async#restoreRootEntries(held) {
241
- const holdRoot = path.join(this.#clonePath, Subspace.holdDir, "root");
242
- for (const entry of Subspace.subspaceOwnedRootEntries) {
183
+ const holdRoot = path.join(this.#clonePath, SubspaceClone.holdDir, "root");
184
+ for (const entry of SubspaceClone.subspaceOwnedRootEntries) {
243
185
  const target = path.join(this.#clonePath, entry);
244
186
  await rm(target, { recursive: true, force: true });
245
187
  if (held.includes(entry))
@@ -248,7 +190,7 @@ class Subspace {
248
190
  }
249
191
  async#isPushedCopy(entry) {
250
192
  const subject = await this.#cloneGit(["log", "-1", "--format=%s", "--", entry]).catch(() => "");
251
- return subject.trim().startsWith(Subspace.syncSubjectPrefix);
193
+ return subject.trim().startsWith(SubspaceClone.syncSubjectPrefix);
252
194
  }
253
195
  static async#copyMissing(from, to) {
254
196
  await mkdir(to, { recursive: true });
@@ -256,7 +198,7 @@ class Subspace {
256
198
  const source = path.join(from, entry.name);
257
199
  const target = path.join(to, entry.name);
258
200
  if (entry.isDirectory())
259
- await Subspace.#copyMissing(source, target);
201
+ await SubspaceClone.#copyMissing(source, target);
260
202
  else if (!await FileSys2.entryExists(target))
261
203
  await rename(source, target);
262
204
  }
@@ -278,11 +220,11 @@ class Subspace {
278
220
  }
279
221
  }
280
222
  #localEnv() {
281
- const { serveDomain } = WorkspaceExecutor.getBaseDevEnv();
223
+ const { serveDomain, defaulted = [] } = WorkspaceExecutor.getBaseDevEnv();
282
224
  return {
283
225
  AKAN_WORKSPACE_ID: "local",
284
- AKAN_PUBLIC_REPO_NAME: this.name,
285
- AKAN_PUBLIC_SERVE_DOMAIN: serveDomain,
226
+ AKAN_PUBLIC_REPO_NAME: this.#declaration.name,
227
+ ...defaulted.includes("AKAN_PUBLIC_SERVE_DOMAIN") ? {} : { AKAN_PUBLIC_SERVE_DOMAIN: serveDomain },
286
228
  AKAN_PUBLIC_ENV: "local",
287
229
  AKAN_PUBLIC_OPERATION_MODE: "local",
288
230
  AKAN_PUBLIC_LOG_LEVEL: "warn"
@@ -317,37 +259,16 @@ class Subspace {
317
259
  this.#workspace.logger.warn(warning);
318
260
  await FileSys2.writeJson(manifestPath, {
319
261
  ...packageJson,
320
- name: this.name,
321
- description: `${this.name} workspace`
262
+ name: this.#declaration.name,
263
+ description: `${this.#declaration.name} workspace`
322
264
  });
323
265
  return pruned;
324
266
  }
325
- async#applySlice(clone, branch) {
326
- const { libs, paths, plans } = await this.slice();
327
- const held = await this.#holdSubspaceOwned();
328
- const heldRootEntries = await this.#holdRootEntries();
329
- for (const member of Subspace.memberDirs)
330
- await rm(path.join(this.#clonePath, member), { recursive: true, force: true });
331
- await this.#extract(["."], ["apps/*", "libs/*", "pkgs/*"]);
332
- await this.#extract(paths);
333
- await this.#restoreRootEntries(heldRootEntries);
334
- await this.#restoreSubspaceOwned(held);
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 });
338
- await this.#rewriteGitignore();
339
- const pruned = await this.#rewriteManifest(plans);
340
- await this.#rewriteWorkspaceSection(libs);
341
- await this.#writeLocalEnv();
342
- for (const lib of libs)
343
- await this.#stampLib(clone, lib, branch);
344
- return { libs, pruned };
345
- }
346
267
  async#rewriteGitignore() {
347
268
  const gitignorePath = path.join(this.#clonePath, ".gitignore");
348
269
  if (!await FileSys2.fileExists(gitignorePath))
349
270
  return;
350
- const [begin = "", end = ""] = Subspace.secretsMarkers;
271
+ const [begin = "", end = ""] = SubspaceClone.secretsMarkers;
351
272
  const lines = (await FileSys2.readText(gitignorePath)).split(/\r?\n/);
352
273
  const beginIdx = lines.indexOf(begin);
353
274
  const endIdx = lines.indexOf(end);
@@ -361,7 +282,7 @@ class Subspace {
361
282
  }
362
283
  async#rewriteWorkspaceSection(libs) {
363
284
  const replacements = [
364
- [/^- Repo: .*$/m, `- Repo: ${this.name}`],
285
+ [/^- Repo: .*$/m, `- Repo: ${this.#declaration.name}`],
365
286
  [/^- Apps: .*$/m, `- Apps: ${this.#declaration.apps.join(", ")}`],
366
287
  [/^- Libraries: .*$/m, `- Libraries: ${libs.length ? libs.join(", ") : "(none)"}`]
367
288
  ];
@@ -375,13 +296,12 @@ class Subspace {
375
296
  await FileSys2.writeText(filePath, rewritten);
376
297
  }
377
298
  }
378
- async#stampLib(clone, lib, branch) {
299
+ async#stampLib(lib, branch, sha) {
379
300
  const manifestPath = path.join(this.#clonePath, "libs", lib, "package.json");
380
301
  if (!await FileSys2.fileExists(manifestPath))
381
302
  return;
382
303
  const origin = `${this.#workspace.repoName}#${branch}`;
383
- const sha = await this.headSha();
384
- const [hash, previous] = await Promise.all([this.#hashLib(clone, lib), this.#committedStamp(clone, lib)]);
304
+ const [hash, previous] = await Promise.all([this.#hashLib(lib), this.#committedStamp(lib)]);
385
305
  const manifest = await FileSys2.readJson(manifestPath);
386
306
  const akan = manifest[LibSource.manifestKey] ?? {};
387
307
  const unchanged = previous?.origin === origin && previous.hash === hash;
@@ -389,9 +309,9 @@ class Subspace {
389
309
  manifest[LibSource.manifestKey] = { ...akan, source };
390
310
  await FileSys2.writeJson(manifestPath, manifest);
391
311
  }
392
- async#committedStamp(clone, lib) {
312
+ async#committedStamp(lib) {
393
313
  try {
394
- const committed = await clone.spawn("git", ["show", `HEAD:libs/${lib}/package.json`]);
314
+ const committed = await this.#clone.spawn("git", ["show", `HEAD:libs/${lib}/package.json`]);
395
315
  const manifest = JSON.parse(committed);
396
316
  const akan = manifest[LibSource.manifestKey] ?? {};
397
317
  return akan.source;
@@ -399,8 +319,8 @@ class Subspace {
399
319
  return;
400
320
  }
401
321
  }
402
- async#hashLib(clone, lib) {
403
- const listed = await clone.spawn("git", [
322
+ async#hashLib(lib) {
323
+ const listed = await this.#clone.spawn("git", [
404
324
  "ls-files",
405
325
  "-z",
406
326
  "--cached",
@@ -409,11 +329,165 @@ class Subspace {
409
329
  "--",
410
330
  `libs/${lib}`
411
331
  ]);
412
- const candidates = listed.split("\x00").filter((file) => !!file && !this.#isSubspaceOwned(file)).sort();
332
+ const candidates = listed.split("\x00").filter((file) => !!file && !SubspaceClone.isSubspaceOwned(file)).sort();
413
333
  const present = await Promise.all(candidates.map(async (file) => await FileSys2.entryExists(path.join(this.#clonePath, file))));
414
334
  const files = candidates.filter((_, index) => present[index]);
415
335
  return await LibSource.hashFiles(files, `libs/${lib}/package.json`, (file) => FileSys2.readText(path.join(this.#clonePath, file)));
416
336
  }
337
+ }
338
+
339
+ // pkgs/@akanjs/devkit/subspace.ts
340
+ class Subspace {
341
+ static retiredConfigFile = "akan.subspace.ts";
342
+ static #retiredConfigRefusal = `${Subspace.retiredConfigFile} is no longer read: move it under \`subspace\` in akan.config.ts and delete it`;
343
+ static #loopbackRegistry = /https?:\/\/(?:127\.0\.0\.1|localhost|0\.0\.0\.0|\[::1\])(?::\d+)?/;
344
+ #workspace;
345
+ #config;
346
+ #declaration;
347
+ constructor(workspace, config, declaration) {
348
+ this.#workspace = workspace;
349
+ this.#config = config;
350
+ this.#declaration = declaration;
351
+ }
352
+ get name() {
353
+ return this.#declaration.name;
354
+ }
355
+ get apps() {
356
+ return this.#declaration.apps;
357
+ }
358
+ get workspaceId() {
359
+ return this.#declaration.workspaceId;
360
+ }
361
+ get #remote() {
362
+ return `subspace-${this.#declaration.name}`;
363
+ }
364
+ async#git(args) {
365
+ return await this.#workspace.spawn("git", args);
366
+ }
367
+ async branch() {
368
+ return (await this.#git(["rev-parse", "--abbrev-ref", "HEAD"])).trim();
369
+ }
370
+ async headSha() {
371
+ return (await this.#git(["rev-parse", "--short", "HEAD"])).trim();
372
+ }
373
+ async fetch(branch) {
374
+ const remotes = (await this.#git(["remote"])).split(`
375
+ `).map((line) => line.trim());
376
+ if (remotes.includes(this.#remote))
377
+ await this.#git(["remote", "set-url", this.#remote, this.#declaration.repo]);
378
+ else
379
+ await this.#git(["remote", "add", this.#remote, this.#declaration.repo]);
380
+ try {
381
+ await this.#git(["fetch", "--quiet", this.#remote, branch]);
382
+ return true;
383
+ } catch {
384
+ return false;
385
+ }
386
+ }
387
+ async anchor(branch) {
388
+ const ref = `${this.#remote}/${branch}`;
389
+ const commit = (await this.#git(["log", "-1", "--format=%H", ref, "--", SubspaceClone.anchorFile])).trim();
390
+ if (!commit)
391
+ return { commit: null, incoming: [] };
392
+ const log = await this.#git(["log", "--format=%H%x09%an%x09%s", `${commit}..${ref}`]);
393
+ const incoming = log.split(`
394
+ `).filter((line) => !!line.trim()).map((line) => {
395
+ const [sha = "", author = "", ...subject] = line.split("\t");
396
+ return { sha: sha.slice(0, 12), author, subject: subject.join("\t") };
397
+ });
398
+ return { commit, incoming };
399
+ }
400
+ async slice() {
401
+ const plans = await Promise.all(this.#declaration.apps.map(async (appName) => await new SlicePlanner(AppExecutor.from(this.#workspace, appName)).plan()));
402
+ const libs = [...new Set(plans.flatMap((plan) => plan.libs))].sort();
403
+ const paths = [...this.#declaration.apps.map((app) => `apps/${app}`), ...libs.map((lib) => `libs/${lib}`)];
404
+ return { plans, libs, paths };
405
+ }
406
+ async plan(branch) {
407
+ const workspaceRefusal = await this.#workspaceRefusal();
408
+ const { plans, libs } = await this.slice();
409
+ const rootFiles = plans[0]?.rootFiles ?? [];
410
+ const present = new Set(SlicePlanner.rootEntriesOf(rootFiles));
411
+ const owned = SubspaceClone.subspaceOwnedRootEntries;
412
+ const excluded = this.#config.exclude.filter((entry) => !owned.includes(entry));
413
+ const excludedTops = new Set(excluded.map((entry) => entry.replace(/\/+$/, "")));
414
+ const libFiles = new Map(plans.flatMap((plan) => Object.entries(plan.libFiles)));
415
+ const { pruned, warnings } = await SlicePlanner.pruneDependencies(this.#workspace, await this.#workspace.getPackageJson(), plans.flatMap((plan) => plan.requiredDependencies));
416
+ let branchRefusal = null;
417
+ try {
418
+ this.#config.assertPushable(branch);
419
+ } catch (error) {
420
+ branchRefusal = toError2(error).message;
421
+ }
422
+ return {
423
+ name: this.name,
424
+ repo: this.#declaration.repo,
425
+ branch,
426
+ branchRefusal,
427
+ workspaceRefusal,
428
+ apps: [...this.#declaration.apps],
429
+ libs: libs.map((lib) => ({
430
+ name: lib,
431
+ apps: plans.filter((plan) => plan.libs.includes(lib)).map((plan) => plan.app)
432
+ })),
433
+ files: plans.reduce((sum, plan) => sum + plan.appFiles.length, 0) + [...libFiles.values()].reduce((sum, files) => sum + files.length, 0),
434
+ rootEntries: [...present].filter((entry) => !excludedTops.has(entry) && !owned.includes(entry) && entry !== Subspace.retiredConfigFile).sort((a, b) => a.localeCompare(b)),
435
+ excludedRootEntries: excluded,
436
+ subspaceOwnedRootEntries: owned.filter((entry) => present.has(entry)),
437
+ prunedDependencies: pruned,
438
+ lockProblem: await this.#headLockProblem(),
439
+ warnings: [...new Set([...plans.flatMap((plan) => plan.warnings), ...warnings])]
440
+ };
441
+ }
442
+ static planBlockers(plan) {
443
+ return [plan.branchRefusal, plan.workspaceRefusal, plan.lockProblem].filter((reason) => !!reason);
444
+ }
445
+ async#retiredConfigLeft() {
446
+ return await FileSys2.fileExists(path2.join(this.#workspace.workspaceRoot, Subspace.retiredConfigFile));
447
+ }
448
+ async#workspaceRefusal() {
449
+ if (await this.#retiredConfigLeft())
450
+ return Subspace.#retiredConfigRefusal;
451
+ return await this.#workspace.hasChanges() ? "workspace working tree is dirty" : null;
452
+ }
453
+ #isStamped(file) {
454
+ return /^libs\/[^/]+\/package\.json$/.test(file);
455
+ }
456
+ async#differsBeyondStamp(ref, file) {
457
+ const [theirs, ours] = await Promise.all([
458
+ this.#git(["show", `${ref}:${file}`]).catch(() => ""),
459
+ this.#git(["show", `HEAD:${file}`]).catch(() => "")
460
+ ]);
461
+ if (!theirs || !ours)
462
+ return true;
463
+ const normalize = (content) => {
464
+ try {
465
+ return LibSource.unstamped(content);
466
+ } catch {
467
+ return content;
468
+ }
469
+ };
470
+ return normalize(theirs) !== normalize(ours);
471
+ }
472
+ async#changedSliceFiles(ref, diffArgs) {
473
+ const files = (await this.#git(["diff", "--name-only", ...diffArgs])).split(`
474
+ `).filter((file) => !!file.trim() && !SubspaceClone.isSubspaceOwned(file));
475
+ const kept = await Promise.all(files.map(async (file) => this.#isStamped(file) ? await this.#differsBeyondStamp(ref, file) : true));
476
+ return files.filter((_, index) => kept[index]);
477
+ }
478
+ async status(branch) {
479
+ const hasBranch = await this.fetch(branch);
480
+ const [{ paths, libs }, anchor] = await Promise.all([
481
+ this.slice(),
482
+ hasBranch ? this.anchor(branch) : Promise.resolve({ commit: null, incoming: [] })
483
+ ]);
484
+ if (!hasBranch)
485
+ return { name: this.name, branch, hasBranch, anchor, behindPaths: [], driftedLibs: [] };
486
+ const ref = `${this.#remote}/${branch}`;
487
+ const behindPaths = await this.#changedSliceFiles(ref, ["HEAD", ref, "--", ...paths]);
488
+ const driftedLibs = libs.filter((lib) => behindPaths.some((file) => file.startsWith(`libs/${lib}/`)));
489
+ return { name: this.name, branch, hasBranch, anchor, behindPaths, driftedLibs };
490
+ }
417
491
  async diff(branch, filter) {
418
492
  if (!await this.fetch(branch))
419
493
  throw new Error(`Subspace "${this.name}" has no branch "${branch}"`);
@@ -436,34 +510,28 @@ class Subspace {
436
510
  return { files: [], patch: "" };
437
511
  return { files, patch: await this.#git(["diff", from, "HEAD", "--", ...files]) };
438
512
  }
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];
513
+ static #loopbackRegistryProblem(lock) {
514
+ const registry = lock?.match(Subspace.#loopbackRegistry)?.[0];
444
515
  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
516
  }
446
- async#verify(clone) {
447
- const env = { ...process.env, ...this.#localEnv() };
448
- try {
449
- await clone.spawn("bun", ["install"], { env });
450
- for (const app of this.#declaration.apps)
451
- await clone.spawn("bunx", ["akan", "sync", app], { env });
452
- return null;
453
- } catch (error) {
454
- return error instanceof Error ? error.message : String(error);
455
- }
517
+ async#headLockProblem() {
518
+ return Subspace.#loopbackRegistryProblem(await this.#git(["show", "HEAD:bun.lock"]).catch(() => null));
456
519
  }
457
520
  async push(branch, { verify = true } = {}) {
458
521
  this.#config.assertPushable(branch);
459
- if (await this.#workspace.hasChanges())
460
- return { name: this.name, outcome: "refused", reason: "workspace working tree is dirty", changedFiles: 0 };
522
+ const refusal = await this.#workspaceRefusal() ?? await this.#headLockProblem();
523
+ if (refusal)
524
+ return { name: this.name, outcome: "refused", reason: refusal, changedFiles: 0 };
461
525
  if (!await this.fetch(branch))
462
526
  return { name: this.name, outcome: "refused", reason: `subspace has no branch "${branch}"`, changedFiles: 0 };
463
- const clone = await this.#ensureClone(branch);
464
- const { pruned } = await this.#applySlice(clone, branch);
465
- const dirty = (await clone.spawn("git", ["status", "--porcelain"])).trim();
466
- if (!dirty)
527
+ const clone = await SubspaceClone.open(this.#workspace, this.#declaration, branch);
528
+ const { pruned } = await clone.applySlice(await this.slice(), {
529
+ branch,
530
+ sha: await this.headSha(),
531
+ remove: [Subspace.retiredConfigFile, ...this.#config.exclude]
532
+ });
533
+ const changedFiles = await clone.changedFiles();
534
+ if (!changedFiles)
467
535
  return {
468
536
  name: this.name,
469
537
  outcome: "skipped",
@@ -471,52 +539,38 @@ class Subspace {
471
539
  changedFiles: 0,
472
540
  prunedDependencies: pruned
473
541
  };
474
- const changedFiles = dirty.split(`
475
- `).length;
476
542
  if (verify) {
477
- const failure = await this.#verify(clone);
543
+ const failure = await clone.verify();
478
544
  if (failure)
479
545
  return { name: this.name, outcome: "refused", reason: failure, changedFiles };
480
546
  }
481
- const lockProblem = await this.#loopbackRegistryLock();
547
+ const lockProblem = Subspace.#loopbackRegistryProblem(await clone.lock());
482
548
  if (lockProblem)
483
549
  return { name: this.name, outcome: "refused", reason: lockProblem, changedFiles };
484
- await this.#writeAnchorFile(branch);
485
- await clone.spawn("git", ["add", "-A"]);
486
- const message = `${Subspace.syncSubjectPrefix}${this.#workspace.repoName}@${await this.headSha()}`;
487
- await clone.spawn("git", ["commit", "--quiet", "-m", message]);
488
- await clone.spawn("git", ["push", "--quiet", "origin", branch]);
489
550
  return {
490
551
  name: this.name,
491
552
  outcome: "pushed",
492
- commit: (await clone.spawn("git", ["rev-parse", "--short", "HEAD"])).trim(),
553
+ commit: await clone.commit(branch, await this.headSha()),
493
554
  changedFiles,
494
555
  prunedDependencies: pruned
495
556
  };
496
557
  }
497
- async#writeAnchorFile(branch) {
498
- await FileSys2.writeJson(path.join(this.#clonePath, Subspace.anchorFile), {
499
- workspace: this.#workspace.repoName,
500
- hubSha: await this.headSha(),
501
- branch,
502
- apps: this.#declaration.apps,
503
- syncedAt: new Date().toISOString()
504
- });
505
- }
506
558
  async pull(branch, { adoptLibs = false } = {}) {
559
+ if (await this.#retiredConfigLeft())
560
+ throw new Error(Subspace.#retiredConfigRefusal);
507
561
  if (!await this.fetch(branch))
508
562
  throw new Error(`Subspace "${this.name}" has no branch "${branch}"`);
509
563
  const anchor = await this.anchor(branch);
510
564
  if (!anchor.commit)
511
- throw new Error(`Subspace "${this.name}" carries no ${Subspace.anchorFile} \u2014 it has never received a push`);
565
+ throw new Error(`Subspace "${this.name}" carries no ${SubspaceClone.anchorFile} \u2014 it has never received a push`);
512
566
  if (!anchor.incoming.length)
513
567
  return { name: this.name, applied: [], libPatch: null, ignored: [], incoming: [] };
514
568
  const range = `${anchor.commit}..${this.#remote}/${branch}`;
515
569
  const changed = (await this.#git(["diff", "--name-only", range])).split(`
516
570
  `).filter((file) => !!file.trim());
517
571
  const appPrefixes = this.#declaration.apps.map((app) => `apps/${app}/`);
518
- const appFiles = changed.filter((file) => appPrefixes.some((prefix) => file.startsWith(prefix)) && !this.#isSubspaceOwned(file));
519
- const libFiles = changed.filter((file) => file.startsWith("libs/") && !this.#isSubspaceOwned(file));
572
+ const appFiles = changed.filter((file) => appPrefixes.some((prefix) => file.startsWith(prefix)) && !SubspaceClone.isSubspaceOwned(file));
573
+ const libFiles = changed.filter((file) => file.startsWith("libs/") && !SubspaceClone.isSubspaceOwned(file));
520
574
  const ignored = changed.filter((file) => !appFiles.includes(file) && !libFiles.includes(file));
521
575
  const applied = adoptLibs ? [...appFiles, ...libFiles] : appFiles;
522
576
  if (applied.length)
@@ -530,145 +584,149 @@ class Subspace {
530
584
  await this.#git(["apply", "--3way", absolute]);
531
585
  }
532
586
  async#savePatch(range, files, label) {
533
- const patchPath = path.join(this.#workspace.workspaceRoot, ".akan/subspace", `${this.name}-${label}.patch`);
534
- await mkdir(path.dirname(patchPath), { recursive: true });
587
+ const patchPath = path2.join(this.#workspace.workspaceRoot, ".akan/subspace", `${this.name}-${label}.patch`);
588
+ await mkdir2(path2.dirname(patchPath), { recursive: true });
535
589
  await FileSys2.writeText(patchPath, await this.#git(["diff", range, "--", ...files]));
536
- return { path: path.relative(this.#workspace.workspaceRoot, patchPath), absolute: patchPath, files };
590
+ return { path: path2.relative(this.#workspace.workspaceRoot, patchPath), absolute: patchPath, files };
537
591
  }
538
592
  }
539
- function formatSubspaceStatuses(statuses) {
540
- const sections = [
541
- "Akan Subspace Status",
542
- `branch: ${statuses[0]?.branch ?? "(none)"}`,
543
- "",
544
- ...statuses.flatMap((status) => {
545
- if (!status.hasBranch)
546
- return [` ${status.name}: no such branch in the subspace \u2014 push is refused`];
547
- const behind = status.behindPaths.length ? `${status.behindPaths.length} file(s) behind` : "up to date";
548
- const incoming = status.anchor.incoming.length ? `${status.anchor.incoming.length} customer commit(s) to pull` : "no customer commits";
549
- const drift = status.driftedLibs.length ? ` DRIFTED LIBS: ${status.driftedLibs.join(", ")}` : null;
550
- return [
551
- ` ${status.name}: ${behind}, ${incoming}`,
552
- ...status.anchor.incoming.map((commit) => ` ${commit.sha} ${commit.author} ${commit.subject}`),
553
- ...drift ? [drift] : []
554
- ];
555
- })
556
- ];
557
- return sections.join(`
593
+
594
+ // pkgs/@akanjs/devkit/subspaceReport.ts
595
+ class SubspaceReport {
596
+ static statuses(statuses) {
597
+ const sections = [
598
+ "Akan Subspace Status",
599
+ `branch: ${statuses[0]?.branch ?? "(none)"}`,
600
+ "",
601
+ ...statuses.flatMap((status) => {
602
+ if (!status.hasBranch)
603
+ return [` ${status.name}: no such branch in the subspace \u2014 push is refused`];
604
+ const behind = status.behindPaths.length ? `${status.behindPaths.length} file(s) behind` : "up to date";
605
+ const incoming = status.anchor.incoming.length ? `${status.anchor.incoming.length} customer commit(s) to pull` : "no customer commits";
606
+ const drift = status.driftedLibs.length ? ` DRIFTED LIBS: ${status.driftedLibs.join(", ")}` : null;
607
+ return [
608
+ ` ${status.name}: ${behind}, ${incoming}`,
609
+ ...status.anchor.incoming.map((commit) => ` ${commit.sha} ${commit.author} ${commit.subject}`),
610
+ ...drift ? [drift] : []
611
+ ];
612
+ })
613
+ ];
614
+ return sections.join(`
558
615
  `);
559
- }
560
- function formatSubspacePushResults(results) {
561
- const sections = [
562
- "Akan Subspace Push",
563
- "",
564
- ...results.flatMap((result) => {
565
- const detail = result.outcome === "pushed" ? `${result.commit} (${result.changedFiles} files)` : result.reason ?? "";
566
- const [first = "", ...rest] = detail.split(`
616
+ }
617
+ static pushResults(results) {
618
+ const sections = [
619
+ "Akan Subspace Push",
620
+ "",
621
+ ...results.flatMap((result) => {
622
+ const detail = result.outcome === "pushed" ? `${result.commit} (${result.changedFiles} files)` : result.reason ?? "";
623
+ const [first = "", ...rest] = detail.split(`
567
624
  `);
568
- const pruned = result.prunedDependencies?.length ? [` ${result.prunedDependencies.length} unused root dependenc(ies) left out of package.json`] : [];
569
- return [
570
- ` ${result.outcome.padEnd(8)} ${result.name} ${first}`,
571
- ...rest.map((line) => ` ${line}`),
572
- ...pruned
573
- ];
574
- })
575
- ];
576
- return sections.join(`
625
+ const pruned = result.prunedDependencies?.length ? [` ${result.prunedDependencies.length} unused root dependenc(ies) left out of package.json`] : [];
626
+ return [
627
+ ` ${result.outcome.padEnd(8)} ${result.name} ${first}`,
628
+ ...rest.map((line) => ` ${line}`),
629
+ ...pruned
630
+ ];
631
+ })
632
+ ];
633
+ return sections.join(`
577
634
  `);
578
- }
579
- function formatSubspacePullResult(result) {
580
- const sections = [
581
- `Akan Subspace Pull \u2014 ${result.name}`,
582
- "",
583
- `Customer commits since the last push (${result.incoming.length}):`,
584
- "",
585
- ...result.incoming.length ? result.incoming.map((commit) => ` ${commit.sha} ${commit.author} ${commit.subject}`) : [" (none \u2014 nothing to pull)"],
586
- "",
587
- `Applied to the working tree, uncommitted (${result.applied.length}):`,
588
- "",
589
- ...result.applied.length ? result.applied.map((file) => ` ${file}`) : [" (none)"],
590
- ...result.libPatch ? [
635
+ }
636
+ static pullResult(result) {
637
+ const sections = [
638
+ `Akan Subspace Pull \u2014 ${result.name}`,
639
+ "",
640
+ `Customer commits since the last push (${result.incoming.length}):`,
591
641
  "",
592
- `Library edits NOT applied (${result.libPatch.files.length}) \u2014 review before adopting:`,
642
+ ...result.incoming.length ? result.incoming.map((commit) => ` ${commit.sha} ${commit.author} ${commit.subject}`) : [" (none \u2014 nothing to pull)"],
593
643
  "",
594
- ...result.libPatch.files.map((file) => ` ${file}`),
644
+ `Applied to the working tree, uncommitted (${result.applied.length}):`,
595
645
  "",
596
- ` patch: ${result.libPatch.path}`,
597
- " adopt with: akan subspace pull <name> --adopt-libs"
598
- ] : [],
599
- ...result.ignored.length ? ["", `Ignored (workspace-owned or subspace-owned): ${result.ignored.length} file(s)`] : []
600
- ];
601
- return sections.join(`
646
+ ...result.applied.length ? result.applied.map((file) => ` ${file}`) : [" (none)"],
647
+ ...result.libPatch ? [
648
+ "",
649
+ `Library edits NOT applied (${result.libPatch.files.length}) \u2014 review before adopting:`,
650
+ "",
651
+ ...result.libPatch.files.map((file) => ` ${file}`),
652
+ "",
653
+ ` patch: ${result.libPatch.path}`,
654
+ " adopt with: akan subspace pull <name> --adopt-libs"
655
+ ] : [],
656
+ ...result.ignored.length ? ["", `Ignored (workspace-owned or subspace-owned): ${result.ignored.length} file(s)`] : []
657
+ ];
658
+ return sections.join(`
602
659
  `);
603
- }
604
- function formatSubspaceEnvUpload(result) {
605
- const sections = [
606
- `Akan Subspace Env Upload \u2014 ${result.name}`,
607
- `cloud workspace: ${result.workspaceId} \xB7 host: ${result.host}`,
608
- "",
609
- ...result.outcome === "cancelled" ? [" cancelled \u2014 the subspace's env archive is untouched."] : [
610
- `Uploaded (${result.files.length}), replacing that workspace's whole archive:`,
660
+ }
661
+ static envUpload(result) {
662
+ const sections = [
663
+ `Akan Subspace Env Upload \u2014 ${result.name}`,
664
+ `cloud workspace: ${result.workspaceId} \xB7 host: ${result.host}`,
611
665
  "",
612
- ...result.files.map((file) => ` ${file}`),
666
+ ...result.outcome === "cancelled" ? [" cancelled \u2014 the subspace's env archive is untouched."] : [
667
+ `Uploaded (${result.files.length}), replacing that workspace's whole archive:`,
668
+ "",
669
+ ...result.files.map((file) => ` ${file}`),
670
+ "",
671
+ ` apps: ${result.apps.join(", ")}`,
672
+ ` libs: ${result.libs.join(", ") || "(none)"}`
673
+ ]
674
+ ];
675
+ return sections.join(`
676
+ `);
677
+ }
678
+ static diff(result) {
679
+ const total = result.app.files.length + result.libs.files.length;
680
+ const sections = [
681
+ `Akan Subspace Diff \u2014 ${result.name} (${result.branch})`,
682
+ "what `akan subspace push` would change in the subspace",
613
683
  "",
614
- ` apps: ${result.apps.join(", ")}`,
615
- ` libs: ${result.libs.join(", ") || "(none)"}`
616
- ]
617
- ];
618
- return sections.join(`
684
+ ...total ? [] : [" subspace is identical to the workspace for this slice."],
685
+ ...result.libs.files.length ? [
686
+ `LIBRARY changes (${result.libs.files.length}) \u2014 the subspace edited shared code:`,
687
+ "",
688
+ ...result.libs.files.map((file) => ` ${file}`),
689
+ "",
690
+ result.libs.patch
691
+ ] : [],
692
+ ...result.app.files.length ? [
693
+ `App changes (${result.app.files.length}):`,
694
+ "",
695
+ ...result.app.files.map((file) => ` ${file}`),
696
+ "",
697
+ result.app.patch
698
+ ] : []
699
+ ];
700
+ return sections.join(`
619
701
  `);
620
- }
621
- function formatSubspaceDiff(result) {
622
- const total = result.app.files.length + result.libs.files.length;
623
- const sections = [
624
- `Akan Subspace Diff \u2014 ${result.name} (${result.branch})`,
625
- "what `akan subspace push` would change in the subspace",
626
- "",
627
- ...total ? [] : [" subspace is identical to the workspace for this slice."],
628
- ...result.libs.files.length ? [
629
- `LIBRARY changes (${result.libs.files.length}) \u2014 the subspace edited shared code:`,
702
+ }
703
+ static plan(plan) {
704
+ const list = (entries) => entries.length ? entries.join(", ") : "(none)";
705
+ const blockers = Subspace.planBlockers(plan);
706
+ const sections = [
707
+ `Akan Subspace Plan \u2014 ${plan.name} (${plan.branch})`,
708
+ `repo: ${plan.repo}`,
709
+ "what `akan subspace push` would ship, worked out from this checkout alone",
630
710
  "",
631
- ...result.libs.files.map((file) => ` ${file}`),
711
+ `apps: ${list(plan.apps)}`,
712
+ `libs: ${plan.libs.length ? "" : "(none)"}`.trimEnd(),
713
+ ...plan.libs.map((lib) => ` - ${lib.name} (from ${lib.apps.join(", ")})`),
714
+ `files under apps/ and libs/: ${plan.files}`,
632
715
  "",
633
- result.libs.patch
634
- ] : [],
635
- ...result.app.files.length ? [
636
- `App changes (${result.app.files.length}):`,
716
+ `root entries pushed: ${list(plan.rootEntries)}`,
717
+ `root entries excluded: ${list(plan.excludedRootEntries)}`,
718
+ `root entries the subspace keeps its own: ${list(plan.subspaceOwnedRootEntries)}`,
637
719
  "",
638
- ...result.app.files.map((file) => ` ${file}`),
720
+ `Root dependencies left out of package.json (${plan.prunedDependencies.length}):`,
721
+ ...plan.prunedDependencies.length ? plan.prunedDependencies.map((dep) => ` - ${dep}`) : [" (none)"],
639
722
  "",
640
- result.app.patch
641
- ] : []
642
- ];
643
- return sections.join(`
644
- `);
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(`
723
+ blockers.length ? "A push would be refused:" : "Nothing here would make a push refuse.",
724
+ ...blockers.map((reason) => ` - ${reason}`),
725
+ ...plan.warnings.length ? ["", `Warnings (${plan.warnings.length}):`, ...plan.warnings.map((w) => ` - ${w}`)] : []
726
+ ];
727
+ return sections.join(`
671
728
  `);
729
+ }
672
730
  }
673
731
 
674
732
  // pkgs/@akanjs/cli/subspace/subspace.script.ts
@@ -767,7 +825,7 @@ class SubspaceRunner extends runner("subspace") {
767
825
  if (!asked)
768
826
  return { name: subspace.name, workspaceId, host, apps, libs: [], outcome: "cancelled", files: [] };
769
827
  const { libs } = await subspace.slice();
770
- const { CloudScript } = await import("./cloud.script-h6pvcpa2.js");
828
+ const { CloudScript } = await import("./cloud.script-dgstqt1m.js");
771
829
  const { files } = await CommandContainer.get(CloudScript).uploadEnv(workspace, {
772
830
  host,
773
831
  workspaceId,
@@ -800,14 +858,14 @@ class SubspaceScript extends script("subspace", [SubspaceRunner]) {
800
858
  const statuses = await this.subspaceRunner.status(workspace, names);
801
859
  const drifted = statuses.filter((status) => status.driftedLibs.length).length;
802
860
  spinner.succeed(`Checked ${statuses.length} subspace(s) (${drifted} with library drift)`);
803
- Logger.rawLog(format === "json" ? JSON.stringify(statuses, null, 2) : formatSubspaceStatuses(statuses));
861
+ Logger.rawLog(format === "json" ? JSON.stringify(statuses, null, 2) : SubspaceReport.statuses(statuses));
804
862
  }
805
863
  async push(workspace, names, { verify, format }) {
806
864
  const spinner = workspace.spinning("Pushing to subspaces...");
807
865
  const results = await this.subspaceRunner.push(workspace, names, { verify });
808
866
  const pushed = results.filter((result) => result.outcome === "pushed").length;
809
867
  spinner.succeed(`Pushed ${pushed} of ${results.length} subspace(s)`);
810
- Logger.rawLog(format === "json" ? JSON.stringify(results, null, 2) : formatSubspacePushResults(results));
868
+ Logger.rawLog(format === "json" ? JSON.stringify(results, null, 2) : SubspaceReport.pushResults(results));
811
869
  if (results.some((result) => result.outcome === "refused"))
812
870
  process.exitCode = 1;
813
871
  }
@@ -815,27 +873,29 @@ class SubspaceScript extends script("subspace", [SubspaceRunner]) {
815
873
  const spinner = workspace.spinning(`Planning ${name}...`);
816
874
  const plan = await this.subspaceRunner.plan(workspace, name);
817
875
  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));
876
+ Logger.rawLog(format === "json" ? JSON.stringify(plan, null, 2) : SubspaceReport.plan(plan));
877
+ if (Subspace.planBlockers(plan).length)
878
+ process.exitCode = 1;
819
879
  }
820
880
  async diff(workspace, name, filter, format = "text") {
821
881
  const spinner = workspace.spinning(`Diffing ${name}...`);
822
882
  const result = await this.subspaceRunner.diff(workspace, name, filter);
823
883
  const total = result.app.files.length + result.libs.files.length;
824
884
  spinner.succeed(`${total} file(s) differ (${result.libs.files.length} in shared libraries)`);
825
- Logger.rawLog(format === "json" ? JSON.stringify(result, null, 2) : formatSubspaceDiff(result));
885
+ Logger.rawLog(format === "json" ? JSON.stringify(result, null, 2) : SubspaceReport.diff(result));
826
886
  }
827
887
  async uploadEnv(workspace, name, { host, force, format }) {
828
888
  const result = await this.subspaceRunner.uploadEnv(workspace, name, {
829
889
  host: await GlobalConfig.hostFor(workspace, host),
830
890
  force
831
891
  });
832
- Logger.rawLog(format === "json" ? JSON.stringify(result, null, 2) : formatSubspaceEnvUpload(result));
892
+ Logger.rawLog(format === "json" ? JSON.stringify(result, null, 2) : SubspaceReport.envUpload(result));
833
893
  }
834
894
  async pull(workspace, name, { adoptLibs, format }) {
835
895
  const spinner = workspace.spinning(`Pulling customer commits from ${name}...`);
836
896
  const result = await this.subspaceRunner.pull(workspace, name, { adoptLibs });
837
897
  spinner.succeed(`${result.incoming.length} customer commit(s), ${result.applied.length} file(s) applied`);
838
- Logger.rawLog(format === "json" ? JSON.stringify(result, null, 2) : formatSubspacePullResult(result));
898
+ Logger.rawLog(format === "json" ? JSON.stringify(result, null, 2) : SubspaceReport.pullResult(result));
839
899
  }
840
900
  }
841
901