@akanjs/cli 3.1.0-rc.0 → 3.1.0-rc.2

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