@akanjs/cli 3.0.0-beta.2 → 3.0.0-beta.20

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 (187) hide show
  1. package/.build-stamp +1 -1
  2. package/CodeAgent-awv258wb.js +9 -0
  3. package/CodeAgentRpcHost-p7h6n2hp.js +7 -0
  4. package/CodeAgentRpcListener-7p7z5dd7.js +97 -0
  5. package/CodeTui-1nqh35ez.js +1457 -0
  6. package/README.md +3 -0
  7. package/TunnelShare-scf4e4pd.js +9 -0
  8. package/{agent.command-d2gfxfck.js → agent.command-1s9ag7sb.js} +8 -9
  9. package/akanCode.js +102 -0
  10. package/{application.command-pj2m7584.js → application.command-49hwy6va.js} +51 -78
  11. package/{applicationBuildRunner-kxwbssgn.js → applicationBuildRunner-ybyp97pv.js} +63 -64
  12. package/{applicationReleasePackager-4yasxwv9.js → applicationReleasePackager-esnrphw8.js} +40 -60
  13. package/buildBatch.proc.js +19 -24
  14. package/{capacitorApp-36rbr858.js → capacitorApp-1synz4en.js} +3 -4
  15. package/cloud.command-d7rqtzt9.js +94 -0
  16. package/cloud.script-bkbpcr96.js +19 -0
  17. package/code.command-nn48xzej.js +154 -0
  18. package/commandManifest.json +1 -1
  19. package/consoleToStderr-h5bryj94.js +7 -0
  20. package/context.command-s3mcmvpa.js +48 -0
  21. package/context.runner-k99nb5pk.js +21 -0
  22. package/{dependencyScanner-shs82ynp.js → dependencyScanner-ye2e6pdz.js} +2 -2
  23. package/{devTui-40ggmd99.js → devTui-q7k7bakd.js} +122 -40
  24. package/fileSys-5ngz93ga.js +9 -0
  25. package/guideline.command-sct4qnt5.js +42 -0
  26. package/guidelines/agentRule/agentRule.instruction.md +206 -25
  27. package/guidelines/conventions/conventions.instruction.md +79 -24
  28. package/guidelines/fieldRule/fieldRule.instruction.md +1 -0
  29. package/guidelines/mcpRule/mcpRule.instruction.md +7 -5
  30. package/guidelines/queryRule/queryRule.instruction.md +17 -7
  31. package/guidelines/runtimeRule/runtimeRule.instruction.md +74 -0
  32. package/guidelines/ssrRule/ssrRule.instruction.md +4 -2
  33. package/guidelines/transportRule/transportRule.instruction.md +51 -0
  34. package/guidelines/workspaceRecipes/workspaceRecipes.instruction.md +66 -3
  35. package/incrementalBuilder.proc.js +31 -48
  36. package/{index-h7yw2z9n.js → index-0bq14vr1.js} +8 -14
  37. package/index-13ffe9wp.js +69 -0
  38. package/index-22d57v4c.js +266 -0
  39. package/{index-fz5q3gd7.js → index-31yr441d.js} +10 -17
  40. package/index-3hx1z96c.js +87 -0
  41. package/index-3sq321ak.js +53 -0
  42. package/{index-kcxmv48c.js → index-3t7g9e4r.js} +45 -79
  43. package/{index-q0y2jq9j.js → index-5b6tavv3.js} +6 -3
  44. package/{index-47pj3aee.js → index-5e0n4hqx.js} +11 -12
  45. package/{index-259dsnz3.js → index-5pc1qrbv.js} +5 -8
  46. package/index-7md2g9vw.js +6 -0
  47. package/index-8mna0947.js +187 -0
  48. package/{index-zry5nz81.js → index-8sf7wmdr.js} +51 -30
  49. package/{index-2tv3fdx4.js → index-9d9bgcx4.js} +342 -765
  50. package/index-9dp031by.js +12 -0
  51. package/{index-ne2qy0nk.js → index-ahemdr5n.js} +12 -12
  52. package/{index-ws5e0j51.js → index-as7xmyz8.js} +40 -24
  53. package/index-axzjaqve.js +1118 -0
  54. package/{index-3d9y066c.js → index-bjwxs904.js} +9 -8
  55. package/index-c6eggbee.js +123 -0
  56. package/index-d2242ksy.js +99 -0
  57. package/{index-a64esmtb.js → index-ddxvdd1p.js} +169 -305
  58. package/{index-92zztqy1.js → index-dy0jx9ay.js} +2 -17
  59. package/{index-pnqbkf5p.js → index-dzf3r49a.js} +6 -2
  60. package/{index-w8kmx1b6.js → index-fgq75haa.js} +198 -290
  61. package/{index-by5gpp8x.js → index-hj6w4ka1.js} +2 -1
  62. package/{index-hwkygn9s.js → index-khpfpcsd.js} +302 -33
  63. package/{index-wt7qrtd4.js → index-mxav6j3c.js} +93 -232
  64. package/{index-gyd220p2.js → index-myhc3c1j.js} +554 -577
  65. package/{index-600zhnve.js → index-nv5976x3.js} +56 -197
  66. package/index-p6pbz9n5.js +25 -0
  67. package/{index-4edzf1gr.js → index-przybvy5.js} +77 -150
  68. package/index-smme4r47.js +49 -0
  69. package/{index-mak3cd28.js → index-w2zdcw3k.js} +137 -255
  70. package/index-wa5jbxj1.js +760 -0
  71. package/{index-v0qh106m.js → index-ww4z397j.js} +527 -723
  72. package/{index-j9yhg404.js → index-xqjwd384.js} +4 -7
  73. package/{index-7737gxcp.js → index-xqpzbab5.js} +107 -241
  74. package/index-xsjh6nzh.js +2729 -0
  75. package/{index-ktb5njgy.js → index-ygn77a5w.js} +47 -99
  76. package/index-ztp3e73t.js +700 -0
  77. package/index.js +23 -22
  78. package/{library.command-nbsky672.js → library.command-1gx04jgn.js} +4 -5
  79. package/{localRegistry.command-yeq57wjr.js → localRegistry.command-zndxj1q6.js} +22 -42
  80. package/{module.command-29vqzrh8.js → module.command-3waddk95.js} +8 -11
  81. package/{package.command-axv0abmn.js → package.command-zrc7br1y.js} +6 -7
  82. package/package.json +14 -5
  83. package/{page.command-vs9s3zrh.js → page.command-vpjgardh.js} +4 -5
  84. package/{primitive.command-cbxmqbtc.js → primitive.command-sz84tk4t.js} +9 -22
  85. package/prompter-ptaa3nk0.js +8 -0
  86. package/{quality.command-gmbs7nws.js → quality.command-g869hafs.js} +203 -376
  87. package/{recipeScanner-fpagtgc1.js → recipeScanner-4b69ybe0.js} +1 -1
  88. package/{repair.command-bqsjptsj.js → repair.command-chsnjvvv.js} +11 -35
  89. package/{routeSourceValidator-apyqw7wk.js → routeSourceValidator-7ycnfz0e.js} +0 -6
  90. package/{scalar.command-6k1vhe9s.js → scalar.command-2v21newx.js} +7 -10
  91. package/skills/akan-code/SKILL.md +130 -0
  92. package/skills/akan-debug/SKILL.md +42 -0
  93. package/skills/akan-deps/SKILL.md +50 -0
  94. package/skills/akan-err/SKILL.md +39 -0
  95. package/skills/akan-module/SKILL.md +45 -0
  96. package/skills/akan-scaffold/SKILL.md +76 -0
  97. package/skills/akan-scalar/SKILL.md +52 -0
  98. package/skills/akan-service/SKILL.md +34 -0
  99. package/skills/akan-store/SKILL.md +67 -0
  100. package/skills/akan-ui/SKILL.md +63 -0
  101. package/skills/akan-validate/SKILL.md +44 -0
  102. package/{subspace.command-fwjfcts7.js → subspace.command-ckcbwfp1.js} +138 -67
  103. package/templates/__scalar/__model__/__model__.abstract.md.template +8 -0
  104. package/templates/appSample/lib/__scalar/workHistory/workHistory.abstract.ts +8 -39
  105. package/templates/appSample/lib/_noti/noti.abstract.ts +6 -29
  106. package/templates/appSample/lib/task/task.abstract.ts +12 -32
  107. package/templates/appSample/lib/task/task.signal.spec.ts +15 -18
  108. package/templates/appSample/lib/task/task.signal.test.ts +15 -1
  109. package/templates/appSample/lib/task/task.signal.ts +1 -1
  110. package/templates/appSample/srvkit/{AuthGuard.ts → guards.ts} +7 -4
  111. package/templates/appSample/srvkit/{SessionInternalArg.ts → internalArgs.ts} +6 -4
  112. package/templates/client.ts +7 -30
  113. package/templates/crudPages/[__model__Id]/edit/page.tsx +2 -1
  114. package/templates/crudPages/[__model__Id]/page.tsx +2 -1
  115. package/templates/crudPages/new/page.tsx +2 -1
  116. package/templates/crudPages/page.tsx +2 -1
  117. package/templates/crudSinglePage/page.tsx +2 -1
  118. package/templates/lib/__lib/lib.dictionary.ts +0 -2
  119. package/templates/lib/__lib/lib.document.ts +0 -2
  120. package/templates/lib/sig.ts +8 -30
  121. package/templates/localDev/docker-compose.yaml.template +3 -14
  122. package/templates/module/__model__.signal.ts +12 -4
  123. package/templates/pkgRoot/tsconfig.json.template +2 -7
  124. package/templates/server.ts +0 -2
  125. package/templates/service/__model__.abstract.md.template +8 -0
  126. package/tunnel.command-kd2yr4fx.js +129 -0
  127. package/{typeChecker-3xgrzc2v.js → typeChecker-px49mezc.js} +1 -1
  128. package/typecheck.proc.js +1 -1
  129. package/workflow.command-44252cqv.js +79 -0
  130. package/{workspace.command-8s050m0h.js → workspace.command-fqcztt5p.js} +77 -109
  131. package/abstractCompactor-mjh5ppsq.js +0 -101
  132. package/cloud.command-jshwdq22.js +0 -93
  133. package/context.command-ez8a0zyz.js +0 -80
  134. package/devTuiView-c08ccbat.js +0 -9
  135. package/getDirname-dr3jz1xg.js +0 -7
  136. package/guideline.command-xmqwx8zm.js +0 -359
  137. package/guidelines/agentRule/agentRule.generate.json +0 -31
  138. package/guidelines/componentRule/componentRule.generate.json +0 -31
  139. package/guidelines/conventions/conventions.generate.json +0 -25
  140. package/guidelines/cssRule/cssRule.generate.json +0 -31
  141. package/guidelines/docPageRule/docPageRule.generate.json +0 -31
  142. package/guidelines/docPageRule/docPageRule.instruction.md +0 -25
  143. package/guidelines/docSyncRule/docSyncRule.generate.json +0 -31
  144. package/guidelines/docSyncRule/docSyncRule.instruction.md +0 -25
  145. package/guidelines/enumConstant/enumConstant.generate.json +0 -31
  146. package/guidelines/enumConstant/enumConstant.instruction.md +0 -28
  147. package/guidelines/fieldRule/fieldRule.generate.json +0 -31
  148. package/guidelines/framework/framework.generate.json +0 -32
  149. package/guidelines/mcpRule/mcpRule.generate.json +0 -35
  150. package/guidelines/modelConstant/modelConstant.generate.json +0 -32
  151. package/guidelines/modelDictionary/modelDictionary.generate.json +0 -32
  152. package/guidelines/modelDocument/modelDocument.generate.json +0 -31
  153. package/guidelines/modelService/modelService.generate.json +0 -31
  154. package/guidelines/modelSignal/modelSignal.generate.json +0 -31
  155. package/guidelines/modelStore/modelStore.generate.json +0 -31
  156. package/guidelines/modelTemplate/modelTemplate.generate.json +0 -31
  157. package/guidelines/modelUnit/modelUnit.generate.json +0 -31
  158. package/guidelines/modelUtil/modelUtil.generate.json +0 -31
  159. package/guidelines/modelView/modelView.generate.json +0 -31
  160. package/guidelines/modelZone/modelZone.generate.json +0 -31
  161. package/guidelines/moduleCodegen/moduleCodegen.generate.json +0 -31
  162. package/guidelines/moduleCodegen/moduleCodegen.instruction.md +0 -28
  163. package/guidelines/moduleOverview/moduleOverview.generate.json +0 -31
  164. package/guidelines/queryRule/queryRule.generate.json +0 -31
  165. package/guidelines/recipeRule/recipeRule.generate.json +0 -31
  166. package/guidelines/runtimeRule/runtimeRule.generate.json +0 -25
  167. package/guidelines/scalarConstant/scalarConstant.generate.json +0 -32
  168. package/guidelines/scalarDictionary/scalarDictionary.generate.json +0 -32
  169. package/guidelines/scalarModule/scalarModule.generate.json +0 -31
  170. package/guidelines/sharedUiUsage/sharedUiUsage.generate.json +0 -31
  171. package/guidelines/sharedUiUsage/sharedUiUsage.instruction.md +0 -25
  172. package/guidelines/ssrRule/ssrRule.generate.json +0 -31
  173. package/guidelines/transportRule/transportRule.generate.json +0 -31
  174. package/guidelines/utilUiUsage/utilUiUsage.generate.json +0 -31
  175. package/guidelines/utilUiUsage/utilUiUsage.instruction.md +0 -25
  176. package/guidelines/workspaceOnboarding/workspaceOnboarding.generate.json +0 -24
  177. package/guidelines/workspaceRecipes/workspaceRecipes.generate.json +0 -33
  178. package/index-2hq52jq3.js +0 -465
  179. package/index-6wwzs1bc.js +0 -7
  180. package/index-8pm30m73.js +0 -841
  181. package/index-kn1h7p1x.js +0 -560
  182. package/index-meh558jy.js +0 -65
  183. package/index-rwg2aa4f.js +0 -229
  184. package/templates/__scalar/__model__/__model__.abstract.md +0 -28
  185. package/templates/module/__model__.abstract.md +0 -28
  186. package/templates/service/__model__.abstract.md +0 -28
  187. package/workflow.command-nwpcdc8p.js +0 -107
@@ -0,0 +1,67 @@
1
+ ---
2
+ name: akan-store
3
+ description: "Fill in a module store, or call module data and actions from UI — what store() already generates (CRUD, form setters, slice actions) and what belongs in your subclass."
4
+ ---
5
+
6
+ # Akan store: what is already generated
7
+
8
+ `class CommentStore extends store(sig.comment, () => ({ /* extra state */ })) { /* extra actions */ }`
9
+ gives you the whole CRUD and form surface for free. **Redeclaring any of it is a typecheck error
10
+ ("already exists"), and the fix is to delete your version and call the generated one** — never rename around
11
+ it, never cast, never reimplement.
12
+
13
+ For a model `comment` (`<Slice>` is a signal slice suffix, e.g. `InPublic`):
14
+
15
+ ## Generated actions — `st.do.*`
16
+
17
+ - write: `createComment(input)`, `updateComment(id, input)`, `removeComment(id)`
18
+ - form: `newComment()`, `editComment(model)`, `writeOnComment(path, value)`, `set<Field>OnComment(value)`,
19
+ `add<Field>OnComment(value)` / `sub<Field>OnComment(idx)` for arrays, `upload<Field>OnComment(fileList)`
20
+ for file fields, `checkCommentSubmitable()`, `submitComment()`, `createCommentInForm()`,
21
+ `updateCommentInForm()`
22
+ - single: `viewComment(model)`, `mergeComment(model, data)`, `setComment(...models)`, `resetComment()`
23
+ - per slice: `initComment<Slice>(...args)`, `refreshComment<Slice>()`, `selectComment<Slice>(model)`,
24
+ `setPageOfComment<Slice>(page)`, `loadMoreOfComment<Slice>()`, `setLimitOfComment<Slice>(limit)`,
25
+ `setQueryArgsOfComment<Slice>(...)`, `setSortOfComment<Slice>(sort)`
26
+
27
+ ## Generated state — `st.use.*`
28
+
29
+ `comment`, `commentLoading`, `commentForm`, `commentFormLoading`, `commentSubmit`, `commentModal`,
30
+ `commentViewAt`, `commentOperation`; per slice `commentList<Slice>`, `commentListLoading<Slice>`,
31
+ `commentInitList<Slice>`, `commentSelection<Slice>`, `commentInsight<Slice>`, `pageOfComment<Slice>`,
32
+ `lastPageOfComment<Slice>`, `hasMoreOfComment<Slice>`, `limitOfComment<Slice>`, `sortOfComment<Slice>`.
33
+
34
+ ## What your subclass is for
35
+
36
+ Only what the generated set does not cover: extra client state, a multi-step or cross-model workflow, and
37
+ calls to the non-CRUD endpoints you added in the signal. Each is an `async` method using `this.set(...)` /
38
+ `this.get()` / `this.pick(...)`.
39
+
40
+ To reach another store, cast `this`: `import type { RootStore } from "../st"`, then
41
+ `(this as unknown as RootStore).logout()` to call its action or `(this as unknown as RootStore).set({ … })` /
42
+ `.get()` to write or read its state. Every store is mixed into one root at runtime, so the cast only tells the
43
+ type what `this` already is. Keep it `import type` — `st.ts` imports every store, so a value import is a cycle.
44
+
45
+ A store that needs none of this stays empty. `store(sig.comment, () => ({}))` with an empty body is the
46
+ correct, complete result — an empty store is not an unfinished one.
47
+
48
+ ## The rule the linter enforces
49
+
50
+ - **An action returns nothing.** Every method dispatches through `st.do.<action>()` and is typed `void`, so a
51
+ returned value is unreachable. Write it into state with `this.set({ … })`.
52
+
53
+ ## From UI
54
+
55
+ Read with `st.use.commentListInPublic()`, act with `st.do.createComment(input)` / `st.do.submitComment()`.
56
+ Do not wrap a generated action in a store method just to rename it.
57
+
58
+ **Pass a form setter by reference**: `onChange={st.do.setTypeOnComment}`, never
59
+ `onChange={(v) => st.do.setTypeOnComment(v)}`. The two run identically, but the arrow is an anonymous
60
+ closure, so the control emits no `data-akan-action` and publishes no agent tool for the field —
61
+ `no-unpublished-form-setter.grit` fails the build on it.
62
+
63
+ ## Signal side
64
+
65
+ The scaffolded `slice(…, { guards: { root, get, cru } })` already exposes get / list / create / update /
66
+ remove. Add a query or mutation only for behaviour beyond CRUD, and give it its own `guards` array — a
67
+ custom endpoint never inherits the slice's.
@@ -0,0 +1,63 @@
1
+ ---
2
+ name: akan-ui
3
+ description: "Build Akan pages and components: the server-by-default rule, the Unit/View vs Template/Zone/Util split, forms, and what never to hand-roll."
4
+ ---
5
+
6
+ # Akan UI
7
+
8
+ A new View / Unit / Template / Zone / Util for an existing module comes from the `create-ui` workflow (the
9
+ `akan-scaffold` skill). Edit the scaffolded component instead of authoring one from scratch.
10
+
11
+ ## The boundary is mechanical, not a judgement call
12
+
13
+ | role | side | directive |
14
+ | --- | --- | --- |
15
+ | `<Model>.Unit.tsx`, `<Model>.View.tsx` | server | never `"use client"` |
16
+ | `<Model>.Template.tsx`, `<Model>.Zone.tsx`, `<Model>.Util.tsx` | client | `"use client"` on line 1 |
17
+ | `page/**` | server | never |
18
+
19
+ **Server is the default and `"use client"` is a cost you justify per component.** A component earns the
20
+ directive only by using a hook, a JSX event handler, the store, a browser global, or a client-only package.
21
+ Rendering markup, reading a param, calling `l()`, and mapping over data are all server work. Measure with
22
+ `akan quality ssr`; treat 50% server share as the floor and a falling share as a regression.
23
+
24
+ Four moves that keep markup on the server: wrap the interaction and render `children` untouched; split a
25
+ compound component so only the provider holds state; fetch in the route and pass the result into a `Zone` as
26
+ an `init` prop; push the boundary down to the leaf that needs it.
27
+
28
+ ## Never hand-roll these
29
+
30
+ - loading / empty / list states → `Load.Units` / `Load.View` / `Load.Edit` with `renderItem`, `renderList`,
31
+ `renderView`, `renderEmpty`; `<Empty />` for a bare placeholder; `Model.New` / `Model.Edit` /
32
+ `Model.SureToRemove` for CRUD modals.
33
+ - a mount-time fetch → the route calls `fetch.init<Model><Slice>(…)` before the first byte. A
34
+ `useEffect(…, [])` that loads data is flagged by `akan quality ssr`, and `fetch.init*` from a client file
35
+ fails lint outright.
36
+ - a model field input → `Field.*`, never a bare `<input>`.
37
+ - a mode switch → `Tab`, not a `useState`.
38
+
39
+ ## Component shape
40
+
41
+ `export const X = ({ … }: XProps) => { return (…); };` — arrow const, block body. Never `React.FC`, never
42
+ `defaultProps`, never `PropsWithChildren`; children are `children: ReactNode`. Declare
43
+ `interface XProps` directly above the component with `className?: string` first.
44
+
45
+ Conditional render is `cond ? <X/> : null`. **Never `{cond && <X/>}`** — in a `className` context it renders
46
+ the literal string `"false"`.
47
+
48
+ ## Classes
49
+
50
+ Never hand-order Tailwind classes; the formatter sorts them and its output is correct. Stay inside the
51
+ semantic token vocabulary — `bg-primary`, `text-foreground/70` — because a raw palette class (`bg-blue-500`),
52
+ an arbitrary colour (`bg-[#3b82f6]`), a daisyUI legacy class, and a colour literal in `style={{…}}` all
53
+ render no CSS and fail lint. Reach for `cn` only for a conditional or to merge an incoming `className`, and
54
+ merge the caller last.
55
+
56
+ ## Forms
57
+
58
+ Store-driven end to end: `value={xForm.field}` with `onChange={st.do.setFieldOnX}`, **the setter passed by
59
+ reference**. An inline arrow silently drops the control's agent annotation and fails
60
+ `no-unpublished-form-setter.grit`. Nested rows use `st.do.writeOnX("payments.3.name", v)`.
61
+
62
+ Full detail: `get_guideline` with `ssrRule`, `componentRule`, `cssRule`, `recipeRule`, or the per-role
63
+ `modelUnit` / `modelView` / `modelZone` / `modelTemplate` / `modelUtil`.
@@ -0,0 +1,44 @@
1
+ ---
2
+ name: akan-validate
3
+ description: "Run the akan check loop after editing and fix each failure class the right way — no silencing, no lint-disable, no gaming the checker."
4
+ ---
5
+
6
+ # Akan validation loop
7
+
8
+ ## One call does the whole chain
9
+
10
+ `akan_verify` runs sync → lint → typecheck → `quality ssr` over everything the working tree has changed, in
11
+ the right order, and reports only what failed. **Call it after editing, before you say you are done.** It
12
+ also runs itself at the end of a turn that changed source, so a failure comes back to you either way.
13
+
14
+ The same chain by hand, for the app you are editing:
15
+
16
+ - `akan sync <app>` — after adding, renaming, or deleting any file. Regenerates the barrels.
17
+ - `akan lint <app>` — fast; run after every edit. Prints up to 200 diagnostics.
18
+ - `akan typecheck <app>` — the client/server boundary violations land here, not in lint.
19
+ - `akan quality ssr` — only when you wrote or changed a `.tsx`. Did the server share hold?
20
+ - `akan build <app>` — the full production build; slower, and only when the build itself is in question.
21
+
22
+ Raw `tsc` misses the generated barrels and reports false errors. Do not reach for it.
23
+
24
+ ## Fixing rules
25
+
26
+ **Apply the real fix the rule intends. Do not silence the checker.** No blanket lint-disable, no throwaway
27
+ re-export, no export-in-place trick, no `as any`, no non-null assertion. Gaming a check is a defect, not a
28
+ fix. When a suppression is genuinely right it carries a reason:
29
+ `// biome-ignore lint/<rule>: <why>` — and a grit plugin diagnostic is suppressed as `lint/plugin`, not the
30
+ bare `plugin` category Biome's own message suggests.
31
+
32
+ Common classes and their real fix:
33
+
34
+ | symptom | fix |
35
+ | --- | --- |
36
+ | a member "already exists" or conflicts with the base class | you redeclared something Akan generates, usually store CRUD. **Delete your version** and call the generated one — do not rename, cast, or suppress. See `akan-store`. |
37
+ | `throw new Error` | a typed `Err` with a dictionary key. See `akan-err`. |
38
+ | an edit to a generated file | regenerate instead: `akan sync <app>` or `repair_generated`. |
39
+ | a file in the wrong place | relocate to `common/` / `webkit/` / `srvkit/` / `ui/`. Never a sibling helper `.ts` inside `lib/<model>/`. |
40
+ | a missing dependency | declare it in the owning app or lib `package.json`. See `akan-deps`. |
41
+ | a Tailwind class order or colour complaint | let the formatter sort; use semantic tokens. See `akan-ui`. |
42
+
43
+ Re-run until clean. If a failure is genuinely outside the scope of what you changed, say so in your reply
44
+ instead of forcing it.
@@ -1,25 +1,28 @@
1
1
  // @bun
2
+ import {
3
+ FileSys2
4
+ } from "./index-5b6tavv3.js";
5
+ import {
6
+ GlobalConfig
7
+ } from "./index-22d57v4c.js";
2
8
  import {
3
9
  Workspace,
10
+ CommandContainer,
4
11
  runner,
5
12
  script,
6
13
  command
7
- } from "./index-mak3cd28.js";
8
- import {
9
- FileSys
10
- } from "./index-q0y2jq9j.js";
14
+ } from "./index-w2zdcw3k.js";
11
15
  import {
12
16
  Executor,
13
17
  WorkspaceExecutor,
14
18
  AppExecutor
15
- } from "./index-2tv3fdx4.js";
16
- import"./index-6wwzs1bc.js";
19
+ } from "./index-9d9bgcx4.js";
17
20
  import {
18
21
  SlicePlanner
19
- } from "./index-j9yhg404.js";
22
+ } from "./index-xqjwd384.js";
20
23
  import {
21
24
  LibSource
22
- } from "./index-3d9y066c.js";
25
+ } from "./index-bjwxs904.js";
23
26
 
24
27
  // pkgs/@akanjs/devkit/subspace.ts
25
28
  import { mkdir, readdir, rename, rm } from "fs/promises";
@@ -45,6 +48,9 @@ class Subspace {
45
48
  get apps() {
46
49
  return this.#declaration.apps;
47
50
  }
51
+ get workspaceId() {
52
+ return this.#declaration.workspaceId;
53
+ }
48
54
  get #remote() {
49
55
  return `subspace-${this.#declaration.name}`;
50
56
  }
@@ -111,16 +117,16 @@ class Subspace {
111
117
  return true;
112
118
  const normalize = (content) => {
113
119
  try {
114
- const manifest = JSON.parse(content);
115
- delete manifest[LibSource.manifestKey];
116
- return JSON.stringify(manifest);
120
+ return LibSource.unstamped(content);
117
121
  } catch {
118
122
  return content;
119
123
  }
120
124
  };
121
125
  return normalize(theirs) !== normalize(ours);
122
126
  }
123
- async#meaningfulFiles(ref, files) {
127
+ async#changedSliceFiles(ref, diffArgs) {
128
+ const files = (await this.#git(["diff", "--name-only", ...diffArgs])).split(`
129
+ `).filter((file) => !!file.trim() && !this.#isSubspaceOwned(file));
124
130
  const kept = await Promise.all(files.map(async (file) => this.#isStamped(file) ? await this.#differsBeyondStamp(ref, file) : true));
125
131
  return files.filter((_, index) => kept[index]);
126
132
  }
@@ -133,14 +139,12 @@ class Subspace {
133
139
  if (!hasBranch)
134
140
  return { name: this.name, branch, hasBranch, anchor, behindPaths: [], driftedLibs: [] };
135
141
  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)));
142
+ const behindPaths = await this.#changedSliceFiles(ref, ["HEAD", ref, "--", ...paths]);
139
143
  const driftedLibs = libs.filter((lib) => behindPaths.some((file) => file.startsWith(`libs/${lib}/`)));
140
144
  return { name: this.name, branch, hasBranch, anchor, behindPaths, driftedLibs };
141
145
  }
142
146
  async#ensureClone(branch) {
143
- if (!await FileSys.dirExists(path.join(this.#clonePath, ".git"))) {
147
+ if (!await FileSys2.dirExists(path.join(this.#clonePath, ".git"))) {
144
148
  await rm(this.#clonePath, { recursive: true, force: true });
145
149
  await mkdir(path.dirname(this.#clonePath), { recursive: true });
146
150
  await this.#workspace.spawn("git", ["clone", "--quiet", this.#declaration.repo, this.#clonePath]);
@@ -157,12 +161,12 @@ class Subspace {
157
161
  await rm(holdRoot, { recursive: true, force: true });
158
162
  for (const member of Subspace.memberDirs) {
159
163
  const memberRoot = path.join(this.#clonePath, member);
160
- if (!await FileSys.dirExists(memberRoot))
164
+ if (!await FileSys2.dirExists(memberRoot))
161
165
  continue;
162
166
  for (const name of await readdir(memberRoot)) {
163
167
  for (const owned of Subspace.subspaceOwnedDirs) {
164
168
  const from = path.join(memberRoot, name, owned);
165
- if (!await FileSys.dirExists(from))
169
+ if (!await FileSys2.dirExists(from))
166
170
  continue;
167
171
  const to = path.join(holdRoot, member, name, owned);
168
172
  await mkdir(path.dirname(to), { recursive: true });
@@ -185,14 +189,25 @@ class Subspace {
185
189
  const target = path.join(to, entry.name);
186
190
  if (entry.isDirectory())
187
191
  await Subspace.#copyMissing(source, target);
188
- else if (!await FileSys.entryExists(target))
192
+ else if (!await FileSys2.entryExists(target))
189
193
  await rename(source, target);
190
194
  }
191
195
  }
192
196
  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());
197
+ const archivePath = path.relative(this.#workspace.workspaceRoot, `${this.#clonePath}.tar`);
198
+ await this.#workspace.spawn("git", ["archive", "-o", archivePath, "HEAD", "--", ...paths]);
199
+ try {
200
+ await this.#workspace.spawn("tar", [
201
+ "-x",
202
+ "-f",
203
+ archivePath,
204
+ "-C",
205
+ this.#clonePath,
206
+ ...excludes.map((entry) => `--exclude=${entry}`)
207
+ ]);
208
+ } finally {
209
+ await rm(path.join(this.#workspace.workspaceRoot, archivePath), { force: true });
210
+ }
196
211
  }
197
212
  #localEnv() {
198
213
  const { serveDomain } = WorkspaceExecutor.getBaseDevEnv();
@@ -207,32 +222,32 @@ class Subspace {
207
222
  }
208
223
  async#writeLocalEnv() {
209
224
  const excludePath = path.join(this.#clonePath, ".git/info/exclude");
210
- const exclude = await FileSys.fileExists(excludePath) ? await FileSys.readText(excludePath) : "";
225
+ const exclude = await FileSys2.fileExists(excludePath) ? await FileSys2.readText(excludePath) : "";
211
226
  if (!exclude.split(`
212
227
  `).includes("/.env")) {
213
228
  await mkdir(path.dirname(excludePath), { recursive: true });
214
- await FileSys.writeText(excludePath, exclude.trim() ? `${exclude.replace(/\n*$/, `
229
+ await FileSys2.writeText(excludePath, exclude.trim() ? `${exclude.replace(/\n*$/, `
215
230
  `)}/.env
216
231
  ` : `/.env
217
232
  `);
218
233
  }
219
234
  const envPath = path.join(this.#clonePath, ".env");
220
- if (await FileSys.fileExists(envPath))
235
+ if (await FileSys2.fileExists(envPath))
221
236
  return;
222
237
  const lines = Object.entries(this.#localEnv()).map(([key, value]) => `${key}=${value}`);
223
- await FileSys.writeText(envPath, `${lines.join(`
238
+ await FileSys2.writeText(envPath, `${lines.join(`
224
239
  `)}
225
240
  `);
226
241
  }
227
242
  async#rewriteManifest(plans) {
228
243
  const manifestPath = path.join(this.#clonePath, "package.json");
229
- if (!await FileSys.fileExists(manifestPath))
244
+ if (!await FileSys2.fileExists(manifestPath))
230
245
  return [];
231
- const rootPackageJson = await FileSys.readJson(manifestPath);
246
+ const rootPackageJson = await FileSys2.readJson(manifestPath);
232
247
  const { packageJson, pruned, warnings } = await SlicePlanner.pruneDependencies(this.#workspace, rootPackageJson, plans.flatMap((plan) => plan.requiredDependencies));
233
248
  for (const warning of warnings)
234
249
  this.#workspace.logger.warn(warning);
235
- await FileSys.writeJson(manifestPath, {
250
+ await FileSys2.writeJson(manifestPath, {
236
251
  ...packageJson,
237
252
  name: this.name,
238
253
  description: `${this.name} workspace`
@@ -259,11 +274,10 @@ class Subspace {
259
274
  }
260
275
  async#rewriteGitignore() {
261
276
  const gitignorePath = path.join(this.#clonePath, ".gitignore");
262
- if (!await FileSys.fileExists(gitignorePath))
277
+ if (!await FileSys2.fileExists(gitignorePath))
263
278
  return;
264
279
  const [begin = "", end = ""] = Subspace.secretsMarkers;
265
- const lines = (await FileSys.readText(gitignorePath)).split(`
266
- `);
280
+ const lines = (await FileSys2.readText(gitignorePath)).split(/\r?\n/);
267
281
  const beginIdx = lines.indexOf(begin);
268
282
  const endIdx = lines.indexOf(end);
269
283
  const filtered = beginIdx >= 0 && endIdx > beginIdx ? [
@@ -271,7 +285,7 @@ class Subspace {
271
285
  ...lines.slice(beginIdx + 1, endIdx).filter((line) => this.#declaration.apps.some((app) => line.startsWith(`apps/${app}/`))),
272
286
  ...lines.slice(endIdx)
273
287
  ] : lines;
274
- await FileSys.writeText(gitignorePath, filtered.filter((line) => line.trim() !== "**/bun.lock").join(`
288
+ await FileSys2.writeText(gitignorePath, filtered.filter((line) => line.trim() !== "**/bun.lock").join(`
275
289
  `));
276
290
  }
277
291
  async#rewriteWorkspaceSection(libs) {
@@ -282,27 +296,27 @@ class Subspace {
282
296
  ];
283
297
  for (const file of ["AGENTS.md", "CLAUDE.md"]) {
284
298
  const filePath = path.join(this.#clonePath, file);
285
- if (!await FileSys.fileExists(filePath))
299
+ if (!await FileSys2.fileExists(filePath))
286
300
  continue;
287
- const content = await FileSys.readText(filePath);
301
+ const content = await FileSys2.readText(filePath);
288
302
  const rewritten = replacements.reduce((text, [pattern, value]) => text.replace(pattern, value), content);
289
303
  if (rewritten !== content)
290
- await FileSys.writeText(filePath, rewritten);
304
+ await FileSys2.writeText(filePath, rewritten);
291
305
  }
292
306
  }
293
307
  async#stampLib(clone, lib, branch) {
294
308
  const manifestPath = path.join(this.#clonePath, "libs", lib, "package.json");
295
- if (!await FileSys.fileExists(manifestPath))
309
+ if (!await FileSys2.fileExists(manifestPath))
296
310
  return;
297
311
  const origin = `${this.#workspace.repoName}#${branch}`;
298
312
  const sha = await this.headSha();
299
313
  const [hash, previous] = await Promise.all([this.#hashLib(clone, lib), this.#committedStamp(clone, lib)]);
300
- const manifest = await FileSys.readJson(manifestPath);
314
+ const manifest = await FileSys2.readJson(manifestPath);
301
315
  const akan = manifest[LibSource.manifestKey] ?? {};
302
316
  const unchanged = previous?.origin === origin && previous.sha === sha && previous.hash === hash;
303
317
  const syncedAt = unchanged ? previous.syncedAt : new Date().toISOString();
304
318
  manifest[LibSource.manifestKey] = { ...akan, source: { origin, sha, hash, syncedAt } };
305
- await FileSys.writeJson(manifestPath, manifest);
319
+ await FileSys2.writeJson(manifestPath, manifest);
306
320
  }
307
321
  async#committedStamp(clone, lib) {
308
322
  try {
@@ -325,22 +339,9 @@ class Subspace {
325
339
  `libs/${lib}`
326
340
  ]);
327
341
  const candidates = listed.split("\x00").filter((file) => !!file && !this.#isSubspaceOwned(file)).sort();
328
- const present = await Promise.all(candidates.map(async (file) => await FileSys.entryExists(path.join(this.#clonePath, file))));
342
+ const present = await Promise.all(candidates.map(async (file) => await FileSys2.entryExists(path.join(this.#clonePath, file))));
329
343
  const files = candidates.filter((_, index) => present[index]);
330
- const hasher = new Bun.CryptoHasher("sha256");
331
- for (const file of files) {
332
- const content = await FileSys.readText(path.join(this.#clonePath, file));
333
- hasher.update(file);
334
- hasher.update("\x00");
335
- if (file === `libs/${lib}/package.json`) {
336
- const manifest = JSON.parse(content);
337
- delete manifest[LibSource.manifestKey];
338
- hasher.update(JSON.stringify(manifest));
339
- } else
340
- hasher.update(content);
341
- hasher.update("\x00");
342
- }
343
- return hasher.digest("hex").slice(0, 32);
344
+ return await LibSource.hashFiles(files, `libs/${lib}/package.json`, (file) => FileSys2.readText(path.join(this.#clonePath, file)));
344
345
  }
345
346
  async diff(branch, filter) {
346
347
  if (!await this.fetch(branch))
@@ -359,10 +360,7 @@ class Subspace {
359
360
  if (!scoped.length)
360
361
  return { files: [], patch: "" };
361
362
  const from = `${this.#remote}/${branch}`;
362
- const pathspec = filter ? [filter] : scoped;
363
- const names = await this.#git(["diff", "--name-only", from, "HEAD", "--", ...pathspec]);
364
- const files = await this.#meaningfulFiles(from, names.split(`
365
- `).filter((file) => !!file.trim()).filter((file) => !this.#isSubspaceOwned(file)));
363
+ const files = await this.#changedSliceFiles(from, [from, "HEAD", "--", ...filter ? [filter] : scoped]);
366
364
  if (!files.length)
367
365
  return { files: [], patch: "" };
368
366
  return { files, patch: await this.#git(["diff", from, "HEAD", "--", ...files]) };
@@ -416,7 +414,7 @@ class Subspace {
416
414
  };
417
415
  }
418
416
  async#writeAnchorFile(branch) {
419
- await FileSys.writeJson(path.join(this.#clonePath, Subspace.anchorFile), {
417
+ await FileSys2.writeJson(path.join(this.#clonePath, Subspace.anchorFile), {
420
418
  workspace: this.#workspace.repoName,
421
419
  hubSha: await this.headSha(),
422
420
  branch,
@@ -453,7 +451,7 @@ class Subspace {
453
451
  async#savePatch(range, files, label) {
454
452
  const patchPath = path.join(this.#workspace.workspaceRoot, ".akan/subspace", `${this.name}-${label}.patch`);
455
453
  await mkdir(path.dirname(patchPath), { recursive: true });
456
- await FileSys.writeText(patchPath, await this.#git(["diff", range, "--", ...files]));
454
+ await FileSys2.writeText(patchPath, await this.#git(["diff", range, "--", ...files]));
457
455
  return { path: path.relative(this.#workspace.workspaceRoot, patchPath), absolute: patchPath, files };
458
456
  }
459
457
  }
@@ -522,6 +520,23 @@ function formatSubspacePullResult(result) {
522
520
  return sections.join(`
523
521
  `);
524
522
  }
523
+ function formatSubspaceEnvUpload(result) {
524
+ const sections = [
525
+ `Akan Subspace Env Upload \u2014 ${result.name}`,
526
+ `cloud workspace: ${result.workspaceId} \xB7 host: ${result.host}`,
527
+ "",
528
+ ...result.outcome === "cancelled" ? [" cancelled \u2014 the subspace's env archive is untouched."] : [
529
+ `Uploaded (${result.files.length}), replacing that workspace's whole archive:`,
530
+ "",
531
+ ...result.files.map((file) => ` ${file}`),
532
+ "",
533
+ ` apps: ${result.apps.join(", ")}`,
534
+ ` libs: ${result.libs.join(", ") || "(none)"}`
535
+ ]
536
+ ];
537
+ return sections.join(`
538
+ `);
539
+ }
525
540
  function formatSubspaceDiff(result) {
526
541
  const total = result.app.files.length + result.libs.files.length;
527
542
  const sections = [
@@ -558,7 +573,7 @@ class SubspaceConfig {
558
573
  static defaultPushableBranches = ["main", "develop", "debug"];
559
574
  static async from(workspace) {
560
575
  const configPath = path2.join(workspace.workspaceRoot, SubspaceConfig.fileName);
561
- if (!await FileSys.fileExists(configPath))
576
+ if (!await FileSys2.fileExists(configPath))
562
577
  return null;
563
578
  const input = await import(configPath).then((mod) => mod.default);
564
579
  return new SubspaceConfig(input);
@@ -575,6 +590,7 @@ class SubspaceConfig {
575
590
  #assertValid() {
576
591
  const seenNames = new Set;
577
592
  const appOwners = new Map;
593
+ const idOwners = new Map;
578
594
  for (const subspace of this.subspaces) {
579
595
  if (!subspace.name || !subspace.repo)
580
596
  throw new Error(`${SubspaceConfig.fileName}: every subspace needs a name and a repo`);
@@ -589,6 +605,12 @@ class SubspaceConfig {
589
605
  throw new Error(`${SubspaceConfig.fileName}: app "${app}" is claimed by both "${owner}" and "${subspace.name}"`);
590
606
  appOwners.set(app, subspace.name);
591
607
  }
608
+ if (!subspace.workspaceId)
609
+ continue;
610
+ const idOwner = idOwners.get(subspace.workspaceId);
611
+ if (idOwner)
612
+ throw new Error(`${SubspaceConfig.fileName}: subspaces "${idOwner}" and "${subspace.name}" declare the same workspaceId`);
613
+ idOwners.set(subspace.workspaceId, subspace.name);
592
614
  }
593
615
  }
594
616
  select(names) {
@@ -625,10 +647,12 @@ class SubspaceRunner extends runner("subspace") {
625
647
  const branch = (await workspace.spawn("git", ["rev-parse", "--abbrev-ref", "HEAD"])).trim();
626
648
  return { config, subspaces, branch };
627
649
  }
650
+ static #isInteractive() {
651
+ return process.stdin.isTTY === true && process.stdout.isTTY === true;
652
+ }
628
653
  async selectNames(workspace, action) {
629
654
  const config = await this.config(workspace);
630
- const interactive = process.stdin.isTTY === true && process.stdout.isTTY === true;
631
- return await SubspaceRunner.chooseNames(config, action, { interactive });
655
+ return await SubspaceRunner.chooseNames(config, action, { interactive: SubspaceRunner.#isInteractive() });
632
656
  }
633
657
  static async chooseNames(config, action, { interactive, prompts }) {
634
658
  const multiple = SubspaceRunner.multiTargetActions.includes(action);
@@ -676,6 +700,43 @@ class SubspaceRunner extends runner("subspace") {
676
700
  throw new Error(`Unknown subspace "${name}"`);
677
701
  return await subspace.pull(branch, { adoptLibs });
678
702
  }
703
+ async uploadEnv(workspace, name, { host, force }) {
704
+ const config = await this.config(workspace);
705
+ const [declaration] = config.select([name]);
706
+ if (!declaration)
707
+ throw new Error(`Unknown subspace "${name}"`);
708
+ const subspace = new Subspace(workspace, config, declaration);
709
+ const { workspaceId } = subspace;
710
+ if (!workspaceId)
711
+ throw new Error(`Subspace "${subspace.name}" declares no workspaceId. Add its cloud workspace id to ${SubspaceConfig.fileName}.`);
712
+ if (workspaceId === workspace.getWorkspaceId({ allowEmpty: true }))
713
+ throw new Error(`Subspace "${subspace.name}" declares this workspace's own id. Uploading a slice there would replace the workspace's env archive with a subset of it.`);
714
+ const apps = subspace.apps;
715
+ const asked = await SubspaceRunner.confirmEnvUpload({ name: subspace.name, workspaceId, apps, force }, { interactive: SubspaceRunner.#isInteractive() });
716
+ if (!asked)
717
+ return { name: subspace.name, workspaceId, host, apps, libs: [], outcome: "cancelled", files: [] };
718
+ const { libs } = await subspace.slice();
719
+ const { CloudScript } = await import("./cloud.script-bkbpcr96.js");
720
+ const { files } = await CommandContainer.get(CloudScript).uploadEnv(workspace, {
721
+ host,
722
+ workspaceId,
723
+ scope: { apps, libs },
724
+ archivePath: `local/env.${subspace.name}.tar`
725
+ });
726
+ return { name: subspace.name, workspaceId, host, apps, libs, outcome: "uploaded", files };
727
+ }
728
+ static async confirmEnvUpload({ name, workspaceId, apps, force }, { interactive, prompts }) {
729
+ if (force)
730
+ return true;
731
+ const target = `subspace "${name}" (cloud workspace ${workspaceId})`;
732
+ if (!interactive)
733
+ throw new Error(`Refusing to replace the env archive of ${target} with no terminal to ask in. Pass --force.`);
734
+ const { confirm } = prompts ?? await loadPrompts();
735
+ return await confirm({
736
+ message: `Replace the env archive of ${target} with this workspace's env for ${apps.join(", ")} and the libraries they pull in?`,
737
+ default: false
738
+ });
739
+ }
679
740
  }
680
741
 
681
742
  // pkgs/@akanjs/cli/subspace/subspace.script.ts
@@ -706,6 +767,10 @@ class SubspaceScript extends script("subspace", [SubspaceRunner]) {
706
767
  spinner.succeed(`${total} file(s) differ (${result.libs.files.length} in shared libraries)`);
707
768
  Logger.rawLog(format === "json" ? JSON.stringify(result, null, 2) : formatSubspaceDiff(result));
708
769
  }
770
+ async uploadEnv(workspace, name, { host, force, format }) {
771
+ const result = await this.subspaceRunner.uploadEnv(workspace, name, { host, force });
772
+ Logger.rawLog(format === "json" ? JSON.stringify(result, null, 2) : formatSubspaceEnvUpload(result));
773
+ }
709
774
  async pull(workspace, name, { adoptLibs, format }) {
710
775
  const spinner = workspace.spinning(`Pulling customer commits from ${name}...`);
711
776
  const result = await this.subspaceRunner.pull(workspace, name, { adoptLibs });
@@ -719,7 +784,7 @@ class SubspaceCommand extends command("subspace", [SubspaceScript], ({ public: t
719
784
  subspace: target({ desc: "Mirror apps and libraries between this workspace and the customer repos it serves" }).arg("action", String, {
720
785
  desc: "subspace action; status-all and push-all take every declared subspace",
721
786
  default: "status",
722
- enum: ["status", "status-all", "diff", "push", "push-all", "pull"]
787
+ enum: ["status", "status-all", "diff", "push", "push-all", "pull", "upload-env"]
723
788
  }).arg("subspace", String, {
724
789
  desc: "subspace name; asks which one(s) when omitted, and is refused by status-all / push-all",
725
790
  nullable: true
@@ -727,12 +792,16 @@ class SubspaceCommand extends command("subspace", [SubspaceScript], ({ public: t
727
792
  flag: "y",
728
793
  desc: "run bun install + akan sync in the subspace before committing",
729
794
  default: true
730
- }).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) {
795
+ }).option("adoptLibs", Boolean, { desc: "pull: also apply the subspace's library edits", default: false }).option("path", String, { desc: "diff: limit to one path", nullable: true }).option("host", String, { desc: "upload-env: host of the cloud to target", default: GlobalConfig.akanCloudHost }).option("force", Boolean, {
796
+ flag: "F",
797
+ desc: "upload-env: replace the subspace's env archive without asking",
798
+ default: false
799
+ }).exec(async function(action, subspace, workspace, format, verify, adoptLibs, filePath, host, force) {
731
800
  const output = format;
732
801
  const all = action === "status-all" || action === "push-all";
733
802
  const base = all ? action.slice(0, -"-all".length) : action;
734
- if (base !== "status" && base !== "diff" && base !== "push" && base !== "pull")
735
- throw new Error(`Unknown subspace action: ${action}. Use status, status-all, diff, push, push-all or pull.`);
803
+ if (base !== "status" && base !== "diff" && base !== "push" && base !== "pull" && base !== "upload-env")
804
+ throw new Error(`Unknown subspace action: ${action}. Use status, status-all, diff, push, push-all, pull or upload-env.`);
736
805
  if (all && subspace)
737
806
  throw new Error(`\`akan subspace ${action}\` takes no name. Run \`akan subspace ${base} ${subspace}\` instead.`);
738
807
  const names = subspace ? [subspace] : all ? [] : await this.subspaceScript.select(workspace, base);
@@ -746,6 +815,8 @@ class SubspaceCommand extends command("subspace", [SubspaceScript], ({ public: t
746
815
  throw new Error(`\`akan subspace ${base}\` needs a name \u2014 it is reviewed one repo at a time.`);
747
816
  if (base === "diff")
748
817
  await this.subspaceScript.diff(workspace, name, filePath, output);
818
+ else if (base === "upload-env")
819
+ await this.subspaceScript.uploadEnv(workspace, name, { host, force, format: output });
749
820
  else
750
821
  await this.subspaceScript.pull(workspace, name, { adoptLibs, format: output });
751
822
  }
@@ -0,0 +1,8 @@
1
+ # <%= model %> Abstract
2
+
3
+ <One sentence: the value this scalar holds, and what embeds it.>
4
+
5
+ ## Rules
6
+
7
+ - <What a caller may assume about the value that the constant file cannot say: an order, a unit, a fixed value.>
8
+ - <Two to five of them. A single field's meaning goes in a trailing comment beside it in the constant file.>