@akanjs/cli 3.0.0-alpha.9 → 3.0.0-alpha.91

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 (135) hide show
  1. package/.build-stamp +1 -1
  2. package/README.ko.md +1 -1
  3. package/README.md +1 -1
  4. package/{abstractCompactor-e5yq60qz.js → abstractCompactor-cefxzggj.js} +3 -5
  5. package/{agent.command-f0gqkfgz.js → agent.command-r93wmq7m.js} +11 -13
  6. package/{application.command-x903m2qq.js → application.command-5nqfqgdn.js} +62 -38
  7. package/{applicationBuildRunner-ha3y8ew8.js → applicationBuildRunner-b21m07fs.js} +23 -20
  8. package/{applicationReleasePackager-gg8ccbeh.js → applicationReleasePackager-3wh58n2m.js} +8 -9
  9. package/buildBatch.proc.js +12 -14
  10. package/capacitorApp-gzcx6txb.js +12 -0
  11. package/{cloud.command-zs6kyf83.js → cloud.command-g0pk0k60.js} +15 -18
  12. package/commandManifest.json +1 -1
  13. package/{context.command-fmepgjzg.js → context.command-qd8kqrnh.js} +19 -23
  14. package/dependencyScanner-shs82ynp.js +8 -0
  15. package/devTui-2xf0meaq.js +552 -0
  16. package/devTuiView-7jy8f30g.js +9 -0
  17. package/getDirname-dr3jz1xg.js +7 -0
  18. package/{guideline.command-n6sw0a0c.js → guideline.command-f38x8jhw.js} +16 -18
  19. package/guidelines/agentRule/agentRule.generate.json +31 -0
  20. package/guidelines/agentRule/agentRule.instruction.md +533 -0
  21. package/guidelines/conventions/conventions.instruction.md +406 -197
  22. package/guidelines/cssRule/cssRule.instruction.md +7 -0
  23. package/guidelines/framework/framework.generate.json +3 -2
  24. package/guidelines/framework/framework.instruction.md +1 -9
  25. package/guidelines/mcpRule/mcpRule.generate.json +25 -0
  26. package/guidelines/mcpRule/mcpRule.instruction.md +217 -0
  27. package/guidelines/modelStore/modelStore.instruction.md +12 -0
  28. package/guidelines/modelTemplate/modelTemplate.instruction.md +5 -0
  29. package/guidelines/queryRule/queryRule.generate.json +31 -0
  30. package/guidelines/queryRule/queryRule.instruction.md +143 -0
  31. package/guidelines/runtimeRule/runtimeRule.generate.json +25 -0
  32. package/guidelines/runtimeRule/runtimeRule.instruction.md +213 -0
  33. package/guidelines/ssrRule/ssrRule.instruction.md +49 -5
  34. package/guidelines/transportRule/transportRule.generate.json +31 -0
  35. package/guidelines/transportRule/transportRule.instruction.md +60 -0
  36. package/guidelines/workspaceOnboarding/workspaceOnboarding.instruction.md +51 -760
  37. package/guidelines/workspaceRecipes/workspaceRecipes.generate.json +33 -0
  38. package/guidelines/workspaceRecipes/workspaceRecipes.instruction.md +402 -0
  39. package/incrementalBuilder.proc.js +26 -25
  40. package/{index-774gcx29.js → index-0emb9eat.js} +41 -34
  41. package/index-3d9y066c.js +76 -0
  42. package/{index-3vta1tqa.js → index-3vvbv974.js} +32 -32
  43. package/{index-76rn3g2c.js → index-47pj3aee.js} +4 -4
  44. package/{index-y4vkyhvv.js → index-5xdqxbn9.js} +14 -14
  45. package/{index-1577bej2.js → index-6wwzs1bc.js} +2 -2
  46. package/{index-wnp7hwq7.js → index-92zztqy1.js} +7 -7
  47. package/{index-jhpa13zj.js → index-9bwhxrqf.js} +57 -57
  48. package/{index-bjpxzr6s.js → index-9wtbw98m.js} +6 -3
  49. package/{index-fgc8r6dj.js → index-a5vmm6x4.js} +3 -1
  50. package/{index-xys926f2.js → index-by5gpp8x.js} +4 -4
  51. package/{index-kdh76j26.js → index-cezy7yb3.js} +100 -32
  52. package/{index-62955kn5.js → index-d61rcfa2.js} +758 -127
  53. package/{index-82jrh4hj.js → index-deekbtkj.js} +13 -13
  54. package/{index-rxkpk7n4.js → index-e82dyeqy.js} +4 -4
  55. package/{index-qzdjd4ct.js → index-h759vjfa.js} +18 -18
  56. package/{index-j37qq1f2.js → index-h7yw2z9n.js} +7 -7
  57. package/{index-qb61h0k7.js → index-jydtyzva.js} +1098 -1360
  58. package/{index-46tjzh6s.js → index-kcxmv48c.js} +29 -29
  59. package/{index-4m0bphac.js → index-kfnr6c19.js} +31 -30
  60. package/{index-xmmzf150.js → index-mpmyqm1y.js} +1 -1
  61. package/{index-xjd3rqzj.js → index-n68jx79w.js} +6 -15
  62. package/{index-0cj2zxbm.js → index-n85fer99.js} +12 -11
  63. package/{index-779ske1y.js → index-pgsswppj.js} +27 -27
  64. package/index-pvb4vdwr.js +159 -0
  65. package/{index-67546d0j.js → index-q0y2jq9j.js} +14 -2
  66. package/{index-z8ptfkzv.js → index-q8sn827n.js} +969 -240
  67. package/{index-5sn5w2mt.js → index-r50qen0z.js} +94 -61
  68. package/{index-eyyf2149.js → index-rntcdss1.js} +36 -37
  69. package/{index-gcv8ncfh.js → index-sjak5v5b.js} +16 -12
  70. package/{index-j9cxndc0.js → index-w8kmx1b6.js} +20 -11
  71. package/{index-jf2ya7r2.js → index-y74k5anj.js} +8 -8
  72. package/{index-d43t8af2.js → index-zkvpaggp.js} +58 -25
  73. package/index.js +22 -25
  74. package/{library.command-cv9gdbmg.js → library.command-em103wtn.js} +11 -10
  75. package/{localRegistry.command-d8fg36zc.js → localRegistry.command-gz8apn4y.js} +20 -23
  76. package/{module.command-qyr1s9cn.js → module.command-451pztkc.js} +13 -16
  77. package/{package.command-wsjx0k47.js → package.command-rfhyf38t.js} +8 -10
  78. package/package.json +5 -7
  79. package/{page.command-rdzg65rm.js → page.command-kt19da5g.js} +7 -9
  80. package/{primitive.command-89g38b53.js → primitive.command-pgy69ere.js} +13 -16
  81. package/{quality.command-0cs2z92d.js → quality.command-gpe2vqb5.js} +226 -105
  82. package/recipeScanner-fpagtgc1.js +11 -0
  83. package/{repair.command-6fphxfwj.js → repair.command-2sbrz187.js} +12 -14
  84. package/{routeSourceValidator-e4jmbfcx.js → routeSourceValidator-ze6x894c.js} +0 -2
  85. package/{scalar.command-xfer69s6.js → scalar.command-jcf4e5sv.js} +11 -14
  86. package/subspace.command-kqzqtk0p.js +755 -0
  87. package/templates/app/env/env.client.debug.ts.template +1 -5
  88. package/templates/app/env/env.client.develop.ts.template +1 -5
  89. package/templates/app/env/env.client.local.ts.template +1 -5
  90. package/templates/app/env/env.client.main.ts.template +1 -5
  91. package/templates/app/env/env.client.testing.ts.template +1 -5
  92. package/templates/app/env/env.client.type.ts +2 -6
  93. package/templates/app/env/env.server.debug.ts.template +1 -5
  94. package/templates/app/env/env.server.develop.ts.template +1 -5
  95. package/templates/app/env/env.server.local.ts.template +1 -5
  96. package/templates/app/env/env.server.main.ts.template +1 -5
  97. package/templates/app/main.ts +3 -1
  98. package/templates/appSample/lib/task/task.signal.ts +11 -6
  99. package/templates/appSample/srvkit/AuthGuard.ts +9 -5
  100. package/templates/appSample/srvkit/SessionInternalArg.ts +4 -3
  101. package/templates/crudPages/[__model__Id]/edit/page.tsx +5 -5
  102. package/templates/crudPages/[__model__Id]/page.tsx +10 -13
  103. package/templates/crudPages/new/page.tsx +5 -5
  104. package/templates/crudPages/page.tsx +9 -9
  105. package/templates/crudSinglePage/page.tsx +11 -7
  106. package/templates/env/_env.server.type.ts +7 -5
  107. package/templates/lib/db.ts +1 -1
  108. package/templates/lib/dict.ts +1 -1
  109. package/templates/lib/sig.ts +1 -1
  110. package/templates/lib/srv.ts +6 -2
  111. package/templates/module/__model__.dictionary.ts +4 -1
  112. package/templates/module/__model__.signal.ts +2 -3
  113. package/templates/module/__model__.store.ts +1 -2
  114. package/templates/workspaceRoot/.gitignore.template +0 -1
  115. package/templates/workspaceRoot/.husky/pre-commit.template +16 -0
  116. package/templates/workspaceRoot/.vscode/settings.json.template +1 -1
  117. package/templates/workspaceRoot/AGENTS.md.template +4 -0
  118. package/templates/workspaceRoot/CLAUDE.md.template +19 -0
  119. package/templates/workspaceRoot/biome.json.template +2 -1
  120. package/templates/workspaceRoot/docs/AI-DEVELOPMENT.md.template +10 -14
  121. package/templates/workspaceRoot/docs/GENERATED.md.template +12 -10
  122. package/templates/workspaceRoot/package.json.template +2 -4
  123. package/typeChecker-3xgrzc2v.js +7 -0
  124. package/typecheck.proc.js +4 -5
  125. package/{workflow.command-2ygfqt4f.js → workflow.command-m4z25a2p.js} +22 -25
  126. package/{workspace.command-88m0zs64.js → workspace.command-dqgj9zbk.js} +96 -81
  127. package/capacitorApp-9xswcgx7.js +0 -61
  128. package/dependencyScanner-f6grrep3.js +0 -9
  129. package/getDirname-m9s0cmhq.js +0 -8
  130. package/index-mxvakhsm.js +0 -66
  131. package/index-r24hmh0q.js +0 -4
  132. package/recipeScanner-tghkzrrt.js +0 -12
  133. package/typeChecker-kravn7ns.js +0 -8
  134. /package/{index-0wae5ebk.js → index-meh558jy.js} +0 -0
  135. /package/{index-ss469dec.js → index-nz8prx9g.js} +0 -0
@@ -0,0 +1,755 @@
1
+ // @bun
2
+ import {
3
+ Workspace,
4
+ runner,
5
+ script,
6
+ command
7
+ } from "./index-cezy7yb3.js";
8
+ import {
9
+ FileSys
10
+ } from "./index-q0y2jq9j.js";
11
+ import {
12
+ Executor,
13
+ WorkspaceExecutor,
14
+ AppExecutor
15
+ } from "./index-q8sn827n.js";
16
+ import"./index-6wwzs1bc.js";
17
+ import {
18
+ SlicePlanner
19
+ } from "./index-pvb4vdwr.js";
20
+ import {
21
+ LibSource
22
+ } from "./index-3d9y066c.js";
23
+
24
+ // pkgs/@akanjs/devkit/subspace.ts
25
+ import { mkdir, readdir, rename, rm } from "fs/promises";
26
+ import path from "path";
27
+ class Subspace {
28
+ static anchorFile = "akan.subspace.json";
29
+ static workspaceOnlyEntries = ["akan.subspace.ts"];
30
+ static memberDirs = ["apps", "libs"];
31
+ static subspaceOwnedDirs = ["env"];
32
+ static secretsMarkers = ["# akan:secrets (managed by akan.config.ts \u2014 do not edit)", "# akan:secrets:end"];
33
+ static holdDir = ".akan-subspace-hold";
34
+ #workspace;
35
+ #config;
36
+ #declaration;
37
+ constructor(workspace, config, declaration) {
38
+ this.#workspace = workspace;
39
+ this.#config = config;
40
+ this.#declaration = declaration;
41
+ }
42
+ get name() {
43
+ return this.#declaration.name;
44
+ }
45
+ get apps() {
46
+ return this.#declaration.apps;
47
+ }
48
+ get #remote() {
49
+ return `subspace-${this.#declaration.name}`;
50
+ }
51
+ get #clonePath() {
52
+ return path.join(this.#workspace.workspaceRoot, ".akan/subspace", this.#declaration.name);
53
+ }
54
+ async#git(args) {
55
+ return await this.#workspace.spawn("git", args);
56
+ }
57
+ async branch() {
58
+ return (await this.#git(["rev-parse", "--abbrev-ref", "HEAD"])).trim();
59
+ }
60
+ async headSha() {
61
+ return (await this.#git(["rev-parse", "--short", "HEAD"])).trim();
62
+ }
63
+ async fetch(branch) {
64
+ const remotes = (await this.#git(["remote"])).split(`
65
+ `).map((line) => line.trim());
66
+ if (remotes.includes(this.#remote))
67
+ await this.#git(["remote", "set-url", this.#remote, this.#declaration.repo]);
68
+ else
69
+ await this.#git(["remote", "add", this.#remote, this.#declaration.repo]);
70
+ try {
71
+ await this.#git(["fetch", "--quiet", this.#remote, branch]);
72
+ return true;
73
+ } catch {
74
+ return false;
75
+ }
76
+ }
77
+ async anchor(branch) {
78
+ const ref = `${this.#remote}/${branch}`;
79
+ const commit = (await this.#git(["log", "-1", "--format=%H", ref, "--", Subspace.anchorFile])).trim();
80
+ if (!commit)
81
+ return { commit: null, incoming: [] };
82
+ const log = await this.#git(["log", "--format=%H%x09%an%x09%s", `${commit}..${ref}`]);
83
+ const incoming = log.split(`
84
+ `).filter((line) => !!line.trim()).map((line) => {
85
+ const [sha = "", author = "", ...subject] = line.split("\t");
86
+ return { sha: sha.slice(0, 12), author, subject: subject.join("\t") };
87
+ });
88
+ return { commit, incoming };
89
+ }
90
+ async slice() {
91
+ const plans = await Promise.all(this.#declaration.apps.map(async (appName) => await new SlicePlanner(AppExecutor.from(this.#workspace, appName)).plan()));
92
+ const libs = [...new Set(plans.flatMap((plan) => plan.libs))].sort();
93
+ const paths = [...this.#declaration.apps.map((app) => `apps/${app}`), ...libs.map((lib) => `libs/${lib}`)];
94
+ return { plans, libs, paths };
95
+ }
96
+ #isSubspaceOwned(file) {
97
+ const segments = file.split("/");
98
+ if (!Subspace.memberDirs.includes(segments[0] ?? ""))
99
+ return false;
100
+ return Subspace.subspaceOwnedDirs.includes(segments[2] ?? "");
101
+ }
102
+ #isStamped(file) {
103
+ return /^libs\/[^/]+\/package\.json$/.test(file);
104
+ }
105
+ async#differsBeyondStamp(ref, file) {
106
+ const [theirs, ours] = await Promise.all([
107
+ this.#git(["show", `${ref}:${file}`]).catch(() => ""),
108
+ this.#git(["show", `HEAD:${file}`]).catch(() => "")
109
+ ]);
110
+ if (!theirs || !ours)
111
+ return true;
112
+ const normalize = (content) => {
113
+ try {
114
+ const manifest = JSON.parse(content);
115
+ delete manifest[LibSource.manifestKey];
116
+ return JSON.stringify(manifest);
117
+ } catch {
118
+ return content;
119
+ }
120
+ };
121
+ return normalize(theirs) !== normalize(ours);
122
+ }
123
+ async#meaningfulFiles(ref, files) {
124
+ const kept = await Promise.all(files.map(async (file) => this.#isStamped(file) ? await this.#differsBeyondStamp(ref, file) : true));
125
+ return files.filter((_, index) => kept[index]);
126
+ }
127
+ async status(branch) {
128
+ const hasBranch = await this.fetch(branch);
129
+ const [{ paths, libs }, anchor] = await Promise.all([
130
+ this.slice(),
131
+ hasBranch ? this.anchor(branch) : Promise.resolve({ commit: null, incoming: [] })
132
+ ]);
133
+ if (!hasBranch)
134
+ return { name: this.name, branch, hasBranch, anchor, behindPaths: [], driftedLibs: [] };
135
+ const ref = `${this.#remote}/${branch}`;
136
+ const diff = await this.#git(["diff", "--name-only", "HEAD", ref, "--", ...paths]);
137
+ const behindPaths = await this.#meaningfulFiles(ref, diff.split(`
138
+ `).filter((file) => !!file.trim()).filter((file) => !this.#isSubspaceOwned(file)));
139
+ const driftedLibs = libs.filter((lib) => behindPaths.some((file) => file.startsWith(`libs/${lib}/`)));
140
+ return { name: this.name, branch, hasBranch, anchor, behindPaths, driftedLibs };
141
+ }
142
+ async#ensureClone(branch) {
143
+ if (!await FileSys.dirExists(path.join(this.#clonePath, ".git"))) {
144
+ await rm(this.#clonePath, { recursive: true, force: true });
145
+ await mkdir(path.dirname(this.#clonePath), { recursive: true });
146
+ await this.#workspace.spawn("git", ["clone", "--quiet", this.#declaration.repo, this.#clonePath]);
147
+ }
148
+ const clone = new Executor(`subspace-${this.name}`, this.#clonePath);
149
+ await clone.spawn("git", ["fetch", "--quiet", "origin", branch]);
150
+ await clone.spawn("git", ["checkout", "--quiet", "-B", branch, `origin/${branch}`]);
151
+ await clone.spawn("git", ["clean", "-qfd"]);
152
+ return clone;
153
+ }
154
+ async#holdSubspaceOwned() {
155
+ const held = [];
156
+ const holdRoot = path.join(this.#clonePath, Subspace.holdDir);
157
+ await rm(holdRoot, { recursive: true, force: true });
158
+ for (const member of Subspace.memberDirs) {
159
+ const memberRoot = path.join(this.#clonePath, member);
160
+ if (!await FileSys.dirExists(memberRoot))
161
+ continue;
162
+ for (const name of await readdir(memberRoot)) {
163
+ for (const owned of Subspace.subspaceOwnedDirs) {
164
+ const from = path.join(memberRoot, name, owned);
165
+ if (!await FileSys.dirExists(from))
166
+ continue;
167
+ const to = path.join(holdRoot, member, name, owned);
168
+ await mkdir(path.dirname(to), { recursive: true });
169
+ await rename(from, to);
170
+ held.push({ from, to });
171
+ }
172
+ }
173
+ }
174
+ return held;
175
+ }
176
+ async#restoreSubspaceOwned(held) {
177
+ for (const { from, to } of held)
178
+ await Subspace.#copyMissing(to, from);
179
+ await rm(path.join(this.#clonePath, Subspace.holdDir), { recursive: true, force: true });
180
+ }
181
+ static async#copyMissing(from, to) {
182
+ await mkdir(to, { recursive: true });
183
+ for (const entry of await readdir(from, { withFileTypes: true })) {
184
+ const source = path.join(from, entry.name);
185
+ const target = path.join(to, entry.name);
186
+ if (entry.isDirectory())
187
+ await Subspace.#copyMissing(source, target);
188
+ else if (!await FileSys.entryExists(target))
189
+ await rename(source, target);
190
+ }
191
+ }
192
+ async#extract(paths, excludes = []) {
193
+ const pathspec = paths.map((entry) => `'${entry}'`).join(" ");
194
+ const excludeArgs = excludes.map((entry) => `--exclude='${entry}'`).join(" ");
195
+ await this.#workspace.exec(`git archive HEAD -- ${pathspec} | tar -x -C '${this.#clonePath}' ${excludeArgs}`.trim());
196
+ }
197
+ #localEnv() {
198
+ const { serveDomain } = WorkspaceExecutor.getBaseDevEnv();
199
+ return {
200
+ AKAN_WORKSPACE_ID: "local",
201
+ AKAN_PUBLIC_REPO_NAME: this.name,
202
+ AKAN_PUBLIC_SERVE_DOMAIN: serveDomain,
203
+ AKAN_PUBLIC_ENV: "local",
204
+ AKAN_PUBLIC_OPERATION_MODE: "local",
205
+ AKAN_PUBLIC_LOG_LEVEL: "warn"
206
+ };
207
+ }
208
+ async#writeLocalEnv() {
209
+ const excludePath = path.join(this.#clonePath, ".git/info/exclude");
210
+ const exclude = await FileSys.fileExists(excludePath) ? await FileSys.readText(excludePath) : "";
211
+ if (!exclude.split(`
212
+ `).includes("/.env")) {
213
+ await mkdir(path.dirname(excludePath), { recursive: true });
214
+ await FileSys.writeText(excludePath, exclude.trim() ? `${exclude.replace(/\n*$/, `
215
+ `)}/.env
216
+ ` : `/.env
217
+ `);
218
+ }
219
+ const envPath = path.join(this.#clonePath, ".env");
220
+ if (await FileSys.fileExists(envPath))
221
+ return;
222
+ const lines = Object.entries(this.#localEnv()).map(([key, value]) => `${key}=${value}`);
223
+ await FileSys.writeText(envPath, `${lines.join(`
224
+ `)}
225
+ `);
226
+ }
227
+ async#rewriteManifest(plans) {
228
+ const manifestPath = path.join(this.#clonePath, "package.json");
229
+ if (!await FileSys.fileExists(manifestPath))
230
+ return [];
231
+ const rootPackageJson = await FileSys.readJson(manifestPath);
232
+ const { packageJson, pruned, warnings } = await SlicePlanner.pruneDependencies(this.#workspace, rootPackageJson, plans.flatMap((plan) => plan.requiredDependencies));
233
+ for (const warning of warnings)
234
+ this.#workspace.logger.warn(warning);
235
+ await FileSys.writeJson(manifestPath, {
236
+ ...packageJson,
237
+ name: this.name,
238
+ description: `${this.name} workspace`
239
+ });
240
+ return pruned;
241
+ }
242
+ async#applySlice(clone, branch) {
243
+ const { libs, paths, plans } = await this.slice();
244
+ const held = await this.#holdSubspaceOwned();
245
+ for (const member of Subspace.memberDirs)
246
+ await rm(path.join(this.#clonePath, member), { recursive: true, force: true });
247
+ await this.#extract(["."], ["apps/*", "libs/*", "pkgs/*"]);
248
+ await this.#extract(paths);
249
+ await this.#restoreSubspaceOwned(held);
250
+ for (const entry of [...Subspace.workspaceOnlyEntries, ...this.#config.exclude])
251
+ await rm(path.join(this.#clonePath, entry), { recursive: true, force: true });
252
+ await this.#rewriteGitignore();
253
+ const pruned = await this.#rewriteManifest(plans);
254
+ await this.#rewriteWorkspaceSection(libs);
255
+ await this.#writeLocalEnv();
256
+ for (const lib of libs)
257
+ await this.#stampLib(clone, lib, branch);
258
+ return { libs, pruned };
259
+ }
260
+ async#rewriteGitignore() {
261
+ const gitignorePath = path.join(this.#clonePath, ".gitignore");
262
+ if (!await FileSys.fileExists(gitignorePath))
263
+ return;
264
+ const [begin = "", end = ""] = Subspace.secretsMarkers;
265
+ const lines = (await FileSys.readText(gitignorePath)).split(`
266
+ `);
267
+ const beginIdx = lines.indexOf(begin);
268
+ const endIdx = lines.indexOf(end);
269
+ const filtered = beginIdx >= 0 && endIdx > beginIdx ? [
270
+ ...lines.slice(0, beginIdx + 1),
271
+ ...lines.slice(beginIdx + 1, endIdx).filter((line) => this.#declaration.apps.some((app) => line.startsWith(`apps/${app}/`))),
272
+ ...lines.slice(endIdx)
273
+ ] : lines;
274
+ await FileSys.writeText(gitignorePath, filtered.filter((line) => line.trim() !== "**/bun.lock").join(`
275
+ `));
276
+ }
277
+ async#rewriteWorkspaceSection(libs) {
278
+ const replacements = [
279
+ [/^- Repo: .*$/m, `- Repo: ${this.name}`],
280
+ [/^- Apps: .*$/m, `- Apps: ${this.#declaration.apps.join(", ")}`],
281
+ [/^- Libraries: .*$/m, `- Libraries: ${libs.length ? libs.join(", ") : "(none)"}`]
282
+ ];
283
+ for (const file of ["AGENTS.md", "CLAUDE.md"]) {
284
+ const filePath = path.join(this.#clonePath, file);
285
+ if (!await FileSys.fileExists(filePath))
286
+ continue;
287
+ const content = await FileSys.readText(filePath);
288
+ const rewritten = replacements.reduce((text, [pattern, value]) => text.replace(pattern, value), content);
289
+ if (rewritten !== content)
290
+ await FileSys.writeText(filePath, rewritten);
291
+ }
292
+ }
293
+ async#stampLib(clone, lib, branch) {
294
+ const manifestPath = path.join(this.#clonePath, "libs", lib, "package.json");
295
+ if (!await FileSys.fileExists(manifestPath))
296
+ return;
297
+ const origin = `${this.#workspace.repoName}#${branch}`;
298
+ const sha = await this.headSha();
299
+ const [hash, previous] = await Promise.all([this.#hashLib(clone, lib), this.#committedStamp(clone, lib)]);
300
+ const manifest = await FileSys.readJson(manifestPath);
301
+ const akan = manifest[LibSource.manifestKey] ?? {};
302
+ const unchanged = previous?.origin === origin && previous.sha === sha && previous.hash === hash;
303
+ const syncedAt = unchanged ? previous.syncedAt : new Date().toISOString();
304
+ manifest[LibSource.manifestKey] = { ...akan, source: { origin, sha, hash, syncedAt } };
305
+ await FileSys.writeJson(manifestPath, manifest);
306
+ }
307
+ async#committedStamp(clone, lib) {
308
+ try {
309
+ const committed = await clone.spawn("git", ["show", `HEAD:libs/${lib}/package.json`]);
310
+ const manifest = JSON.parse(committed);
311
+ const akan = manifest[LibSource.manifestKey] ?? {};
312
+ return akan.source;
313
+ } catch {
314
+ return;
315
+ }
316
+ }
317
+ async#hashLib(clone, lib) {
318
+ const listed = await clone.spawn("git", [
319
+ "ls-files",
320
+ "-z",
321
+ "--cached",
322
+ "--others",
323
+ "--exclude-standard",
324
+ "--",
325
+ `libs/${lib}`
326
+ ]);
327
+ const files = listed.split("\x00").filter((file) => !!file && !this.#isSubspaceOwned(file)).sort();
328
+ const hasher = new Bun.CryptoHasher("sha256");
329
+ for (const file of files) {
330
+ const content = await FileSys.readText(path.join(this.#clonePath, file));
331
+ hasher.update(file);
332
+ hasher.update("\x00");
333
+ if (file === `libs/${lib}/package.json`) {
334
+ const manifest = JSON.parse(content);
335
+ delete manifest[LibSource.manifestKey];
336
+ hasher.update(JSON.stringify(manifest));
337
+ } else
338
+ hasher.update(content);
339
+ hasher.update("\x00");
340
+ }
341
+ return hasher.digest("hex").slice(0, 32);
342
+ }
343
+ async diff(branch, filter) {
344
+ if (!await this.fetch(branch))
345
+ throw new Error(`Subspace "${this.name}" has no branch "${branch}"`);
346
+ const { libs, paths } = await this.slice();
347
+ const libPaths = libs.map((lib) => `libs/${lib}`);
348
+ const appPaths = paths.filter((entry) => !libPaths.includes(entry));
349
+ const [app, libSection] = await Promise.all([
350
+ this.#diffSection(branch, appPaths, filter),
351
+ this.#diffSection(branch, libPaths, filter)
352
+ ]);
353
+ return { name: this.name, branch, app, libs: libSection };
354
+ }
355
+ async#diffSection(branch, paths, filter) {
356
+ const scoped = filter ? paths.filter((entry) => entry.startsWith(filter) || filter.startsWith(entry)) : paths;
357
+ if (!scoped.length)
358
+ return { files: [], patch: "" };
359
+ const from = `${this.#remote}/${branch}`;
360
+ const pathspec = filter ? [filter] : scoped;
361
+ const names = await this.#git(["diff", "--name-only", from, "HEAD", "--", ...pathspec]);
362
+ const files = await this.#meaningfulFiles(from, names.split(`
363
+ `).filter((file) => !!file.trim()).filter((file) => !this.#isSubspaceOwned(file)));
364
+ if (!files.length)
365
+ return { files: [], patch: "" };
366
+ return { files, patch: await this.#git(["diff", from, "HEAD", "--", ...files]) };
367
+ }
368
+ async#verify(clone) {
369
+ const env = { ...process.env, ...this.#localEnv() };
370
+ try {
371
+ await clone.spawn("bun", ["install"], { env });
372
+ for (const app of this.#declaration.apps)
373
+ await clone.spawn("bunx", ["akan", "sync", app], { env });
374
+ return null;
375
+ } catch (error) {
376
+ return error instanceof Error ? error.message : String(error);
377
+ }
378
+ }
379
+ async push(branch, { verify = true } = {}) {
380
+ this.#config.assertPushable(branch);
381
+ if (await this.#workspace.hasChanges())
382
+ return { name: this.name, outcome: "refused", reason: "workspace working tree is dirty", changedFiles: 0 };
383
+ if (!await this.fetch(branch))
384
+ return { name: this.name, outcome: "refused", reason: `subspace has no branch "${branch}"`, changedFiles: 0 };
385
+ const clone = await this.#ensureClone(branch);
386
+ const { pruned } = await this.#applySlice(clone, branch);
387
+ const dirty = (await clone.spawn("git", ["status", "--porcelain"])).trim();
388
+ if (!dirty)
389
+ return {
390
+ name: this.name,
391
+ outcome: "skipped",
392
+ reason: "already up to date",
393
+ changedFiles: 0,
394
+ prunedDependencies: pruned
395
+ };
396
+ const changedFiles = dirty.split(`
397
+ `).length;
398
+ if (verify) {
399
+ const failure = await this.#verify(clone);
400
+ if (failure)
401
+ return { name: this.name, outcome: "refused", reason: failure, changedFiles };
402
+ }
403
+ await this.#writeAnchorFile(branch);
404
+ await clone.spawn("git", ["add", "-A"]);
405
+ const message = `chore(subspace): sync from ${this.#workspace.repoName}@${await this.headSha()}`;
406
+ await clone.spawn("git", ["commit", "--quiet", "-m", message]);
407
+ await clone.spawn("git", ["push", "--quiet", "origin", branch]);
408
+ return {
409
+ name: this.name,
410
+ outcome: "pushed",
411
+ commit: (await clone.spawn("git", ["rev-parse", "--short", "HEAD"])).trim(),
412
+ changedFiles,
413
+ prunedDependencies: pruned
414
+ };
415
+ }
416
+ async#writeAnchorFile(branch) {
417
+ await FileSys.writeJson(path.join(this.#clonePath, Subspace.anchorFile), {
418
+ workspace: this.#workspace.repoName,
419
+ hubSha: await this.headSha(),
420
+ branch,
421
+ apps: this.#declaration.apps,
422
+ syncedAt: new Date().toISOString()
423
+ });
424
+ }
425
+ async pull(branch, { adoptLibs = false } = {}) {
426
+ if (!await this.fetch(branch))
427
+ throw new Error(`Subspace "${this.name}" has no branch "${branch}"`);
428
+ const anchor = await this.anchor(branch);
429
+ if (!anchor.commit)
430
+ throw new Error(`Subspace "${this.name}" carries no ${Subspace.anchorFile} \u2014 it has never received a push`);
431
+ if (!anchor.incoming.length)
432
+ return { name: this.name, applied: [], libPatch: null, ignored: [], incoming: [] };
433
+ const range = `${anchor.commit}..${this.#remote}/${branch}`;
434
+ const changed = (await this.#git(["diff", "--name-only", range])).split(`
435
+ `).filter((file) => !!file.trim());
436
+ const appPrefixes = this.#declaration.apps.map((app) => `apps/${app}/`);
437
+ const appFiles = changed.filter((file) => appPrefixes.some((prefix) => file.startsWith(prefix)) && !this.#isSubspaceOwned(file));
438
+ const libFiles = changed.filter((file) => file.startsWith("libs/") && !this.#isSubspaceOwned(file));
439
+ const ignored = changed.filter((file) => !appFiles.includes(file) && !libFiles.includes(file));
440
+ const applied = adoptLibs ? [...appFiles, ...libFiles] : appFiles;
441
+ if (applied.length)
442
+ await this.#applyPatch(range, applied, "incoming");
443
+ const saved = !adoptLibs && libFiles.length ? await this.#savePatch(range, libFiles, "libs") : null;
444
+ const libPatch = saved ? { path: saved.path, files: saved.files } : null;
445
+ return { name: this.name, applied, libPatch, ignored, incoming: anchor.incoming };
446
+ }
447
+ async#applyPatch(range, files, label) {
448
+ const { absolute } = await this.#savePatch(range, files, label);
449
+ await this.#git(["apply", "--3way", absolute]);
450
+ }
451
+ async#savePatch(range, files, label) {
452
+ const patchPath = path.join(this.#workspace.workspaceRoot, ".akan/subspace", `${this.name}-${label}.patch`);
453
+ await mkdir(path.dirname(patchPath), { recursive: true });
454
+ await FileSys.writeText(patchPath, await this.#git(["diff", range, "--", ...files]));
455
+ return { path: path.relative(this.#workspace.workspaceRoot, patchPath), absolute: patchPath, files };
456
+ }
457
+ }
458
+ function formatSubspaceStatuses(statuses) {
459
+ const sections = [
460
+ "Akan Subspace Status",
461
+ `branch: ${statuses[0]?.branch ?? "(none)"}`,
462
+ "",
463
+ ...statuses.flatMap((status) => {
464
+ if (!status.hasBranch)
465
+ return [` ${status.name}: no such branch in the subspace \u2014 push is refused`];
466
+ const behind = status.behindPaths.length ? `${status.behindPaths.length} file(s) behind` : "up to date";
467
+ const incoming = status.anchor.incoming.length ? `${status.anchor.incoming.length} customer commit(s) to pull` : "no customer commits";
468
+ const drift = status.driftedLibs.length ? ` DRIFTED LIBS: ${status.driftedLibs.join(", ")}` : null;
469
+ return [
470
+ ` ${status.name}: ${behind}, ${incoming}`,
471
+ ...status.anchor.incoming.map((commit) => ` ${commit.sha} ${commit.author} ${commit.subject}`),
472
+ ...drift ? [drift] : []
473
+ ];
474
+ })
475
+ ];
476
+ return sections.join(`
477
+ `);
478
+ }
479
+ function formatSubspacePushResults(results) {
480
+ const sections = [
481
+ "Akan Subspace Push",
482
+ "",
483
+ ...results.flatMap((result) => {
484
+ const detail = result.outcome === "pushed" ? `${result.commit} (${result.changedFiles} files)` : result.reason ?? "";
485
+ const [first = "", ...rest] = detail.split(`
486
+ `);
487
+ const pruned = result.prunedDependencies?.length ? [` ${result.prunedDependencies.length} unused root dependenc(ies) left out of package.json`] : [];
488
+ return [
489
+ ` ${result.outcome.padEnd(8)} ${result.name} ${first}`,
490
+ ...rest.map((line) => ` ${line}`),
491
+ ...pruned
492
+ ];
493
+ })
494
+ ];
495
+ return sections.join(`
496
+ `);
497
+ }
498
+ function formatSubspacePullResult(result) {
499
+ const sections = [
500
+ `Akan Subspace Pull \u2014 ${result.name}`,
501
+ "",
502
+ `Customer commits since the last push (${result.incoming.length}):`,
503
+ "",
504
+ ...result.incoming.length ? result.incoming.map((commit) => ` ${commit.sha} ${commit.author} ${commit.subject}`) : [" (none \u2014 nothing to pull)"],
505
+ "",
506
+ `Applied to the working tree, uncommitted (${result.applied.length}):`,
507
+ "",
508
+ ...result.applied.length ? result.applied.map((file) => ` ${file}`) : [" (none)"],
509
+ ...result.libPatch ? [
510
+ "",
511
+ `Library edits NOT applied (${result.libPatch.files.length}) \u2014 review before adopting:`,
512
+ "",
513
+ ...result.libPatch.files.map((file) => ` ${file}`),
514
+ "",
515
+ ` patch: ${result.libPatch.path}`,
516
+ " adopt with: akan subspace pull <name> --adopt-libs"
517
+ ] : [],
518
+ ...result.ignored.length ? ["", `Ignored (workspace-owned or subspace-owned): ${result.ignored.length} file(s)`] : []
519
+ ];
520
+ return sections.join(`
521
+ `);
522
+ }
523
+ function formatSubspaceDiff(result) {
524
+ const total = result.app.files.length + result.libs.files.length;
525
+ const sections = [
526
+ `Akan Subspace Diff \u2014 ${result.name} (${result.branch})`,
527
+ "what `akan subspace push` would change in the subspace",
528
+ "",
529
+ ...total ? [] : [" subspace is identical to the workspace for this slice."],
530
+ ...result.libs.files.length ? [
531
+ `LIBRARY changes (${result.libs.files.length}) \u2014 the subspace edited shared code:`,
532
+ "",
533
+ ...result.libs.files.map((file) => ` ${file}`),
534
+ "",
535
+ result.libs.patch
536
+ ] : [],
537
+ ...result.app.files.length ? [
538
+ `App changes (${result.app.files.length}):`,
539
+ "",
540
+ ...result.app.files.map((file) => ` ${file}`),
541
+ "",
542
+ result.app.patch
543
+ ] : []
544
+ ];
545
+ return sections.join(`
546
+ `);
547
+ }
548
+
549
+ // pkgs/@akanjs/cli/subspace/subspace.script.ts
550
+ import { Logger } from "akanjs/common";
551
+
552
+ // pkgs/@akanjs/devkit/subspaceConfig.ts
553
+ import path2 from "path";
554
+ class SubspaceConfig {
555
+ static fileName = "akan.subspace.ts";
556
+ static defaultPushableBranches = ["main", "develop", "debug"];
557
+ static async from(workspace) {
558
+ const configPath = path2.join(workspace.workspaceRoot, SubspaceConfig.fileName);
559
+ if (!await FileSys.fileExists(configPath))
560
+ return null;
561
+ const input = await import(configPath).then((mod) => mod.default);
562
+ return new SubspaceConfig(input);
563
+ }
564
+ pushableBranches;
565
+ exclude;
566
+ subspaces;
567
+ constructor(input) {
568
+ this.pushableBranches = input.pushableBranches ?? SubspaceConfig.defaultPushableBranches;
569
+ this.exclude = input.exclude ?? [];
570
+ this.subspaces = input.subspaces ?? [];
571
+ this.#assertValid();
572
+ }
573
+ #assertValid() {
574
+ const seenNames = new Set;
575
+ const appOwners = new Map;
576
+ for (const subspace of this.subspaces) {
577
+ if (!subspace.name || !subspace.repo)
578
+ throw new Error(`${SubspaceConfig.fileName}: every subspace needs a name and a repo`);
579
+ if (seenNames.has(subspace.name))
580
+ throw new Error(`${SubspaceConfig.fileName}: duplicate subspace "${subspace.name}"`);
581
+ seenNames.add(subspace.name);
582
+ if (!subspace.apps?.length)
583
+ throw new Error(`${SubspaceConfig.fileName}: subspace "${subspace.name}" declares no apps`);
584
+ for (const app of subspace.apps) {
585
+ const owner = appOwners.get(app);
586
+ if (owner)
587
+ throw new Error(`${SubspaceConfig.fileName}: app "${app}" is claimed by both "${owner}" and "${subspace.name}"`);
588
+ appOwners.set(app, subspace.name);
589
+ }
590
+ }
591
+ }
592
+ select(names) {
593
+ if (!names.length)
594
+ return this.subspaces;
595
+ return names.map((name) => {
596
+ const subspace = this.subspaces.find((candidate) => candidate.name === name);
597
+ if (!subspace)
598
+ throw new Error(`${SubspaceConfig.fileName}: unknown subspace "${name}"`);
599
+ return subspace;
600
+ });
601
+ }
602
+ assertPushable(branch) {
603
+ if (this.pushableBranches.includes(branch))
604
+ return;
605
+ throw new Error(`Branch "${branch}" is not pushable. ${SubspaceConfig.fileName} allows: ${this.pushableBranches.join(", ")}`);
606
+ }
607
+ }
608
+
609
+ // pkgs/@akanjs/cli/subspace/subspace.runner.ts
610
+ var loadPrompts = async () => await import("@inquirer/prompts");
611
+
612
+ class SubspaceRunner extends runner("subspace") {
613
+ static multiTargetActions = ["status", "push"];
614
+ async config(workspace) {
615
+ const config = await SubspaceConfig.from(workspace);
616
+ if (!config)
617
+ throw new Error(`No ${SubspaceConfig.fileName} at the workspace root. Declare the customer repos there first.`);
618
+ return config;
619
+ }
620
+ async subspaces(workspace, names) {
621
+ const config = await this.config(workspace);
622
+ const subspaces = config.select(names).map((declaration) => new Subspace(workspace, config, declaration));
623
+ const branch = (await workspace.spawn("git", ["rev-parse", "--abbrev-ref", "HEAD"])).trim();
624
+ return { config, subspaces, branch };
625
+ }
626
+ async selectNames(workspace, action) {
627
+ const config = await this.config(workspace);
628
+ const interactive = process.stdin.isTTY === true && process.stdout.isTTY === true;
629
+ return await SubspaceRunner.chooseNames(config, action, { interactive });
630
+ }
631
+ static async chooseNames(config, action, { interactive, prompts }) {
632
+ const multiple = SubspaceRunner.multiTargetActions.includes(action);
633
+ if (!config.subspaces.length)
634
+ throw new Error(`${SubspaceConfig.fileName} declares no subspaces.`);
635
+ if (!interactive) {
636
+ throw new Error(multiple ? `\`akan subspace ${action}\` needs a subspace name when it cannot prompt. Pass one, or run \`akan subspace ${action}-all\` for every subspace.` : `\`akan subspace ${action}\` needs a subspace name when it cannot prompt \u2014 it is reviewed one repo at a time.`);
637
+ }
638
+ const { checkbox, select } = prompts ?? await loadPrompts();
639
+ const choices = config.subspaces.map((subspace) => ({
640
+ name: subspace.name,
641
+ value: subspace.name,
642
+ description: `${subspace.repo} \xB7 ${subspace.apps.join(", ")}`
643
+ }));
644
+ if (multiple)
645
+ return await checkbox({ message: `Select subspaces to ${action}`, choices, required: true });
646
+ return [await select({ message: `Select a subspace to ${action}`, choices })];
647
+ }
648
+ async status(workspace, names) {
649
+ const { subspaces, branch } = await this.subspaces(workspace, names);
650
+ const statuses = [];
651
+ for (const subspace of subspaces)
652
+ statuses.push(await subspace.status(branch));
653
+ return statuses;
654
+ }
655
+ async push(workspace, names, { verify } = {}) {
656
+ const { config, subspaces, branch } = await this.subspaces(workspace, names);
657
+ config.assertPushable(branch);
658
+ const results = [];
659
+ for (const subspace of subspaces)
660
+ results.push(await subspace.push(branch, { verify }));
661
+ return results;
662
+ }
663
+ async diff(workspace, name, filter) {
664
+ const { subspaces, branch } = await this.subspaces(workspace, [name]);
665
+ const [subspace] = subspaces;
666
+ if (!subspace)
667
+ throw new Error(`Unknown subspace "${name}"`);
668
+ return await subspace.diff(branch, filter);
669
+ }
670
+ async pull(workspace, name, { adoptLibs } = {}) {
671
+ const { subspaces, branch } = await this.subspaces(workspace, [name]);
672
+ const [subspace] = subspaces;
673
+ if (!subspace)
674
+ throw new Error(`Unknown subspace "${name}"`);
675
+ return await subspace.pull(branch, { adoptLibs });
676
+ }
677
+ }
678
+
679
+ // pkgs/@akanjs/cli/subspace/subspace.script.ts
680
+ class SubspaceScript extends script("subspace", [SubspaceRunner]) {
681
+ async select(workspace, action) {
682
+ return await this.subspaceRunner.selectNames(workspace, action);
683
+ }
684
+ async status(workspace, names, format = "text") {
685
+ const spinner = workspace.spinning("Checking subspaces...");
686
+ const statuses = await this.subspaceRunner.status(workspace, names);
687
+ const drifted = statuses.filter((status) => status.driftedLibs.length).length;
688
+ spinner.succeed(`Checked ${statuses.length} subspace(s) (${drifted} with library drift)`);
689
+ Logger.rawLog(format === "json" ? JSON.stringify(statuses, null, 2) : formatSubspaceStatuses(statuses));
690
+ }
691
+ async push(workspace, names, { verify, format }) {
692
+ const spinner = workspace.spinning("Pushing to subspaces...");
693
+ const results = await this.subspaceRunner.push(workspace, names, { verify });
694
+ const pushed = results.filter((result) => result.outcome === "pushed").length;
695
+ spinner.succeed(`Pushed ${pushed} of ${results.length} subspace(s)`);
696
+ Logger.rawLog(format === "json" ? JSON.stringify(results, null, 2) : formatSubspacePushResults(results));
697
+ if (results.some((result) => result.outcome === "refused"))
698
+ process.exitCode = 1;
699
+ }
700
+ async diff(workspace, name, filter, format = "text") {
701
+ const spinner = workspace.spinning(`Diffing ${name}...`);
702
+ const result = await this.subspaceRunner.diff(workspace, name, filter);
703
+ const total = result.app.files.length + result.libs.files.length;
704
+ spinner.succeed(`${total} file(s) differ (${result.libs.files.length} in shared libraries)`);
705
+ Logger.rawLog(format === "json" ? JSON.stringify(result, null, 2) : formatSubspaceDiff(result));
706
+ }
707
+ async pull(workspace, name, { adoptLibs, format }) {
708
+ const spinner = workspace.spinning(`Pulling customer commits from ${name}...`);
709
+ const result = await this.subspaceRunner.pull(workspace, name, { adoptLibs });
710
+ spinner.succeed(`${result.incoming.length} customer commit(s), ${result.applied.length} file(s) applied`);
711
+ Logger.rawLog(format === "json" ? JSON.stringify(result, null, 2) : formatSubspacePullResult(result));
712
+ }
713
+ }
714
+
715
+ // pkgs/@akanjs/cli/subspace/subspace.command.ts
716
+ class SubspaceCommand extends command("subspace", [SubspaceScript], ({ public: target }) => ({
717
+ subspace: target({ desc: "Mirror apps and libraries between this workspace and the customer repos it serves" }).arg("action", String, {
718
+ desc: "subspace action; status-all and push-all take every declared subspace",
719
+ default: "status",
720
+ enum: ["status", "status-all", "diff", "push", "push-all", "pull"]
721
+ }).arg("subspace", String, {
722
+ desc: "subspace name; asks which one(s) when omitted, and is refused by status-all / push-all",
723
+ nullable: true
724
+ }).with(Workspace).option("format", String, { desc: "output format", default: "text", enum: ["text", "json"] }).option("verify", Boolean, {
725
+ flag: "y",
726
+ desc: "run bun install + akan sync in the subspace before committing",
727
+ default: true
728
+ }).option("adoptLibs", Boolean, { desc: "pull: also apply the subspace's library edits", default: false }).option("path", String, { desc: "diff: limit to one path", nullable: true }).exec(async function(action, subspace, workspace, format, verify, adoptLibs, filePath) {
729
+ const output = format;
730
+ const all = action === "status-all" || action === "push-all";
731
+ const base = all ? action.slice(0, -"-all".length) : action;
732
+ if (base !== "status" && base !== "diff" && base !== "push" && base !== "pull")
733
+ throw new Error(`Unknown subspace action: ${action}. Use status, status-all, diff, push, push-all or pull.`);
734
+ if (all && subspace)
735
+ throw new Error(`\`akan subspace ${action}\` takes no name. Run \`akan subspace ${base} ${subspace}\` instead.`);
736
+ const names = subspace ? [subspace] : all ? [] : await this.subspaceScript.select(workspace, base);
737
+ if (base === "status")
738
+ await this.subspaceScript.status(workspace, names, output);
739
+ else if (base === "push")
740
+ await this.subspaceScript.push(workspace, names, { verify, format: output });
741
+ else {
742
+ const [name] = names;
743
+ if (!name)
744
+ throw new Error(`\`akan subspace ${base}\` needs a name \u2014 it is reviewed one repo at a time.`);
745
+ if (base === "diff")
746
+ await this.subspaceScript.diff(workspace, name, filePath, output);
747
+ else
748
+ await this.subspaceScript.pull(workspace, name, { adoptLibs, format: output });
749
+ }
750
+ })
751
+ })) {
752
+ }
753
+ export {
754
+ SubspaceCommand
755
+ };