nuspace 0.2.1__tar.gz → 0.3.0__tar.gz

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 (210) hide show
  1. {nuspace-0.2.1 → nuspace-0.3.0}/.github/workflows/publish-nuspace-ui.yml +5 -5
  2. {nuspace-0.2.1 → nuspace-0.3.0}/.github/workflows/publish-nuspace.yml +2 -2
  3. {nuspace-0.2.1 → nuspace-0.3.0}/CHANGELOG.md +15 -2
  4. {nuspace-0.2.1 → nuspace-0.3.0}/Makefile +1 -1
  5. {nuspace-0.2.1 → nuspace-0.3.0}/PKG-INFO +4 -4
  6. nuspace-0.3.0/examples/headless.py +277 -0
  7. nuspace-0.3.0/examples/web.py +141 -0
  8. {nuspace-0.2.1 → nuspace-0.3.0}/pyproject.toml +10 -13
  9. nuspace-0.3.0/src/nuspace/__init__.py +82 -0
  10. nuspace-0.3.0/src/nuspace/cli/__init__.py +11 -0
  11. nuspace-0.3.0/src/nuspace/cli/__main__.py +11 -0
  12. nuspace-0.3.0/src/nuspace/cli/cell.py +175 -0
  13. nuspace-0.3.0/src/nuspace/cli/main.py +71 -0
  14. nuspace-0.3.0/src/nuspace/cli/plane.py +175 -0
  15. nuspace-0.3.0/src/nuspace/cli/space.py +212 -0
  16. nuspace-0.3.0/src/nuspace/cli/utils.py +97 -0
  17. nuspace-0.3.0/src/nuspace/drivers/__init__.py +25 -0
  18. nuspace-0.3.0/src/nuspace/drivers/headless.py +81 -0
  19. nuspace-0.3.0/src/nuspace/drivers/web.py +234 -0
  20. nuspace-0.3.0/src/nuspace/exec/__init__.py +51 -0
  21. nuspace-0.3.0/src/nuspace/exec/cell.py +300 -0
  22. nuspace-0.3.0/src/nuspace/exec/plane.py +240 -0
  23. nuspace-0.3.0/src/nuspace/exec/space.py +75 -0
  24. nuspace-0.3.0/src/nuspace/exec/utils.py +78 -0
  25. nuspace-0.3.0/src/nuspace/ops/__init__.py +158 -0
  26. nuspace-0.3.0/src/nuspace/ops/cell.py +494 -0
  27. nuspace-0.3.0/src/nuspace/ops/chat.py +251 -0
  28. nuspace-0.3.0/src/nuspace/ops/groups.py +868 -0
  29. nuspace-0.3.0/src/nuspace/ops/plane.py +349 -0
  30. nuspace-0.3.0/src/nuspace/ops/space.py +102 -0
  31. nuspace-0.3.0/src/nuspace/ops/templates.py +162 -0
  32. nuspace-0.3.0/src/nuspace/ops/utils.py +103 -0
  33. nuspace-0.3.0/src/nuspace/presets.py +134 -0
  34. nuspace-0.3.0/src/nuspace/shapes.py +256 -0
  35. nuspace-0.3.0/src/nuspace/space.py +354 -0
  36. nuspace-0.3.0/src/nuspace/web/__init__.py +61 -0
  37. nuspace-0.3.0/src/nuspace/web/route.py +112 -0
  38. nuspace-0.3.0/src/nuspace/web/session.py +153 -0
  39. nuspace-0.3.0/src/nuspace/web/shell.py +121 -0
  40. nuspace-0.3.0/src/nuspace/web/sidebar/__init__.py +44 -0
  41. nuspace-0.3.0/src/nuspace/web/sidebar/driver.py +232 -0
  42. nuspace-0.3.0/src/nuspace/web/sidebar/interactions.py +123 -0
  43. nuspace-0.3.0/src/nuspace/web/sidebar/ref.py +60 -0
  44. nuspace-0.3.0/src/nuspace/web/utils.py +243 -0
  45. nuspace-0.3.0/src/nuspace/web/viewer/__init__.py +57 -0
  46. nuspace-0.3.0/src/nuspace/web/viewer/driver.py +322 -0
  47. nuspace-0.3.0/src/nuspace/web/viewer/interactions.py +138 -0
  48. nuspace-0.3.0/src/nuspace/web/viewer/ref.py +179 -0
  49. nuspace-0.3.0/tests/test_chat.py +252 -0
  50. nuspace-0.3.0/tests/test_groups.py +153 -0
  51. {nuspace-0.2.1/src/nuspace/web/ui → nuspace-0.3.0/ts}/package-lock.json +7 -6
  52. {nuspace-0.2.1/src/nuspace/web/ui → nuspace-0.3.0/ts}/package.json +2 -2
  53. {nuspace-0.2.1/src/nuspace/web/ui → nuspace-0.3.0/ts}/pyproject.toml +1 -1
  54. nuspace-0.3.0/ts/src/App.tsx +52 -0
  55. {nuspace-0.2.1/src/nuspace/web/ui → nuspace-0.3.0/ts}/src/app/connect.ts +38 -8
  56. nuspace-0.3.0/ts/src/app/ids.ts +26 -0
  57. nuspace-0.3.0/ts/src/app/router.ts +81 -0
  58. {nuspace-0.2.1/src/nuspace/web/ui → nuspace-0.3.0/ts}/src/app/surfaces.ts +12 -13
  59. nuspace-0.3.0/ts/src/components/index.ts +17 -0
  60. {nuspace-0.2.1/src/nuspace/web/ui → nuspace-0.3.0/ts}/src/design/document.ts +5 -5
  61. {nuspace-0.2.1/src/nuspace/web/ui → nuspace-0.3.0/ts}/src/design/index.ts +9 -18
  62. {nuspace-0.2.1/src/nuspace/web/ui → nuspace-0.3.0/ts}/src/design/rail.ts +25 -10
  63. nuspace-0.3.0/ts/src/design/shell.ts +47 -0
  64. {nuspace-0.2.1/src/nuspace/web/ui → nuspace-0.3.0/ts}/src/index.css +0 -1
  65. {nuspace-0.2.1/src/nuspace/web/ui → nuspace-0.3.0/ts}/src/refs/register.ts +14 -16
  66. nuspace-0.3.0/ts/src/refs/route.ts +48 -0
  67. {nuspace-0.2.1/src/nuspace/web/ui/src/refs/pages → nuspace-0.3.0/ts/src/refs/sidebar}/Rail.tsx +170 -133
  68. nuspace-0.3.0/ts/src/refs/sidebar/footer.tsx +61 -0
  69. nuspace-0.3.0/ts/src/refs/sidebar/header.tsx +15 -0
  70. nuspace-0.3.0/ts/src/refs/sidebar/ops.ts +40 -0
  71. {nuspace-0.2.1/src/nuspace/web/ui/src/components/rail → nuspace-0.3.0/ts/src/refs/sidebar}/row-input.tsx +1 -1
  72. {nuspace-0.2.1/src/nuspace/web/ui/src/components/rail → nuspace-0.3.0/ts/src/refs/sidebar}/row.tsx +16 -3
  73. nuspace-0.3.0/ts/src/refs/sidebar/sidebar.tsx +60 -0
  74. nuspace-0.3.0/ts/src/refs/sidebar/state.ts +67 -0
  75. nuspace-0.3.0/ts/src/refs/sidebar/types.ts +127 -0
  76. {nuspace-0.2.1/src/nuspace/web/ui/src/refs/pages → nuspace-0.3.0/ts/src/refs/viewer}/Canvas.tsx +73 -55
  77. {nuspace-0.2.1/src/nuspace/web/ui/src/refs/pages → nuspace-0.3.0/ts/src/refs/viewer}/Program.tsx +2 -2
  78. {nuspace-0.2.1/src/nuspace/web/ui/src/refs/pages → nuspace-0.3.0/ts/src/refs/viewer}/blocks.ts +10 -16
  79. {nuspace-0.2.1/src/nuspace/web/ui/src/refs/pages → nuspace-0.3.0/ts/src/refs/viewer}/ops.ts +17 -21
  80. {nuspace-0.2.1/src/nuspace/web/ui/src/refs/pages → nuspace-0.3.0/ts/src/refs/viewer}/state.ts +19 -35
  81. nuspace-0.3.0/ts/src/refs/viewer/types.ts +100 -0
  82. nuspace-0.3.0/ts/src/refs/viewer/viewer.tsx +91 -0
  83. {nuspace-0.2.1/src/nuspace/web/ui → nuspace-0.3.0/ts}/vitest.config.ts +1 -1
  84. nuspace-0.2.1/examples/apps.py +0 -113
  85. nuspace-0.2.1/examples/pages.py +0 -189
  86. nuspace-0.2.1/examples/web.py +0 -69
  87. nuspace-0.2.1/src/nuspace/__init__.py +0 -40
  88. nuspace-0.2.1/src/nuspace/_root.py +0 -34
  89. nuspace-0.2.1/src/nuspace/agent/__init__.py +0 -47
  90. nuspace-0.2.1/src/nuspace/agent/model.py +0 -134
  91. nuspace-0.2.1/src/nuspace/agent/ops.py +0 -223
  92. nuspace-0.2.1/src/nuspace/agent/prompt.py +0 -248
  93. nuspace-0.2.1/src/nuspace/agent/runner.py +0 -219
  94. nuspace-0.2.1/src/nuspace/agent/shapes.py +0 -84
  95. nuspace-0.2.1/src/nuspace/apps/__init__.py +0 -75
  96. nuspace-0.2.1/src/nuspace/apps/ops.py +0 -277
  97. nuspace-0.2.1/src/nuspace/apps/runner.py +0 -257
  98. nuspace-0.2.1/src/nuspace/apps/shapes.py +0 -51
  99. nuspace-0.2.1/src/nuspace/chat/__init__.py +0 -21
  100. nuspace-0.2.1/src/nuspace/chat/ops.py +0 -145
  101. nuspace-0.2.1/src/nuspace/chat/shapes.py +0 -56
  102. nuspace-0.2.1/src/nuspace/cli/__init__.py +0 -6
  103. nuspace-0.2.1/src/nuspace/cli/__main__.py +0 -7
  104. nuspace-0.2.1/src/nuspace/cli/_meta.py +0 -12
  105. nuspace-0.2.1/src/nuspace/cli/main.py +0 -35
  106. nuspace-0.2.1/src/nuspace/core/__init__.py +0 -18
  107. nuspace-0.2.1/src/nuspace/core/host.py +0 -255
  108. nuspace-0.2.1/src/nuspace/core/ids.py +0 -30
  109. nuspace-0.2.1/src/nuspace/core/session.py +0 -127
  110. nuspace-0.2.1/src/nuspace/core/shapes.py +0 -94
  111. nuspace-0.2.1/src/nuspace/core/tpl.py +0 -242
  112. nuspace-0.2.1/src/nuspace/core/ui.py +0 -114
  113. nuspace-0.2.1/src/nuspace/pages/__init__.py +0 -101
  114. nuspace-0.2.1/src/nuspace/pages/ops.py +0 -510
  115. nuspace-0.2.1/src/nuspace/pages/runner.py +0 -474
  116. nuspace-0.2.1/src/nuspace/pages/shapes.py +0 -112
  117. nuspace-0.2.1/src/nuspace/web/__init__.py +0 -80
  118. nuspace-0.2.1/src/nuspace/web/apps/__init__.py +0 -18
  119. nuspace-0.2.1/src/nuspace/web/apps/driver.py +0 -176
  120. nuspace-0.2.1/src/nuspace/web/apps/ref.py +0 -114
  121. nuspace-0.2.1/src/nuspace/web/arms.py +0 -103
  122. nuspace-0.2.1/src/nuspace/web/chat/__init__.py +0 -19
  123. nuspace-0.2.1/src/nuspace/web/chat/driver.py +0 -116
  124. nuspace-0.2.1/src/nuspace/web/chat/ref.py +0 -101
  125. nuspace-0.2.1/src/nuspace/web/lens/__init__.py +0 -22
  126. nuspace-0.2.1/src/nuspace/web/lens/driver.py +0 -98
  127. nuspace-0.2.1/src/nuspace/web/lens/ref.py +0 -74
  128. nuspace-0.2.1/src/nuspace/web/lens/reflect.py +0 -371
  129. nuspace-0.2.1/src/nuspace/web/nav.py +0 -75
  130. nuspace-0.2.1/src/nuspace/web/pages/__init__.py +0 -21
  131. nuspace-0.2.1/src/nuspace/web/pages/driver.py +0 -286
  132. nuspace-0.2.1/src/nuspace/web/pages/ref.py +0 -171
  133. nuspace-0.2.1/src/nuspace/web/pages/session.py +0 -171
  134. nuspace-0.2.1/src/nuspace/web/serve/__init__.py +0 -33
  135. nuspace-0.2.1/src/nuspace/web/serve/app.py +0 -148
  136. nuspace-0.2.1/src/nuspace/web/serve/fabric.py +0 -186
  137. nuspace-0.2.1/src/nuspace/web/serve/session.py +0 -184
  138. nuspace-0.2.1/src/nuspace/web/serve/shell.py +0 -119
  139. nuspace-0.2.1/src/nuspace/web/space.py +0 -234
  140. nuspace-0.2.1/src/nuspace/web/ui/src/App.tsx +0 -151
  141. nuspace-0.2.1/src/nuspace/web/ui/src/app/router.ts +0 -123
  142. nuspace-0.2.1/src/nuspace/web/ui/src/components/index.ts +0 -24
  143. nuspace-0.2.1/src/nuspace/web/ui/src/components/page-header.tsx +0 -180
  144. nuspace-0.2.1/src/nuspace/web/ui/src/components/rail/header.tsx +0 -43
  145. nuspace-0.2.1/src/nuspace/web/ui/src/components/rail/index.ts +0 -24
  146. nuspace-0.2.1/src/nuspace/web/ui/src/design/apps.ts +0 -89
  147. nuspace-0.2.1/src/nuspace/web/ui/src/design/chat.ts +0 -120
  148. nuspace-0.2.1/src/nuspace/web/ui/src/design/lens.css +0 -107
  149. nuspace-0.2.1/src/nuspace/web/ui/src/design/lens.ts +0 -259
  150. nuspace-0.2.1/src/nuspace/web/ui/src/design/shell.ts +0 -57
  151. nuspace-0.2.1/src/nuspace/web/ui/src/refs/apps/Rail.tsx +0 -316
  152. nuspace-0.2.1/src/nuspace/web/ui/src/refs/apps/Source.tsx +0 -142
  153. nuspace-0.2.1/src/nuspace/web/ui/src/refs/apps/apps.tsx +0 -226
  154. nuspace-0.2.1/src/nuspace/web/ui/src/refs/apps/ops.ts +0 -34
  155. nuspace-0.2.1/src/nuspace/web/ui/src/refs/apps/state.ts +0 -122
  156. nuspace-0.2.1/src/nuspace/web/ui/src/refs/apps/types.ts +0 -69
  157. nuspace-0.2.1/src/nuspace/web/ui/src/refs/chat/chat.tsx +0 -299
  158. nuspace-0.2.1/src/nuspace/web/ui/src/refs/chat/ops.ts +0 -25
  159. nuspace-0.2.1/src/nuspace/web/ui/src/refs/chat/types.ts +0 -105
  160. nuspace-0.2.1/src/nuspace/web/ui/src/refs/lens/lens.tsx +0 -623
  161. nuspace-0.2.1/src/nuspace/web/ui/src/refs/lens/types.ts +0 -90
  162. nuspace-0.2.1/src/nuspace/web/ui/src/refs/nav/nav.ts +0 -75
  163. nuspace-0.2.1/src/nuspace/web/ui/src/refs/pages/pages.tsx +0 -159
  164. nuspace-0.2.1/src/nuspace/web/ui/src/refs/pages/types.ts +0 -211
  165. nuspace-0.2.1/src/nuspace/web/wire.py +0 -51
  166. nuspace-0.2.1/tests/nuspace/apps/__init__.py +0 -0
  167. nuspace-0.2.1/tests/nuspace/apps/conftest.py +0 -147
  168. nuspace-0.2.1/tests/nuspace/apps/test_changed_key.py +0 -85
  169. nuspace-0.2.1/tests/nuspace/apps/test_lifecycle.py +0 -210
  170. nuspace-0.2.1/tests/nuspace/apps/test_ops.py +0 -184
  171. nuspace-0.2.1/tests/nuspace/pages/__init__.py +0 -0
  172. nuspace-0.2.1/tests/nuspace/pages/conftest.py +0 -200
  173. nuspace-0.2.1/tests/nuspace/pages/test_invariant.py +0 -97
  174. nuspace-0.2.1/tests/nuspace/pages/test_lifecycle.py +0 -228
  175. nuspace-0.2.1/tests/nuspace/pages/test_ops.py +0 -435
  176. nuspace-0.2.1/tests/nuspace/test_imports.py +0 -94
  177. nuspace-0.2.1/tests/nuspace/web/__init__.py +0 -0
  178. nuspace-0.2.1/tests/nuspace/web/test_apps_driver.py +0 -321
  179. nuspace-0.2.1/tests/nuspace/web/test_lens_driver.py +0 -336
  180. nuspace-0.2.1/tests/nuspace/web/test_lens_reflect.py +0 -164
  181. nuspace-0.2.1/tests/nuspace/web/test_pages_driver.py +0 -341
  182. nuspace-0.2.1/tests/nuspace/web/test_serve.py +0 -114
  183. nuspace-0.2.1/tests/nuspace/web/test_supervised.py +0 -424
  184. {nuspace-0.2.1 → nuspace-0.3.0}/.gitignore +0 -0
  185. {nuspace-0.2.1 → nuspace-0.3.0}/.pre-commit-config.yaml +0 -0
  186. {nuspace-0.2.1 → nuspace-0.3.0}/LICENSE.md +0 -0
  187. {nuspace-0.2.1 → nuspace-0.3.0}/README.md +0 -0
  188. {nuspace-0.2.1/src/nuspace/web/ui → nuspace-0.3.0/ts}/.gitignore +0 -0
  189. {nuspace-0.2.1/src/nuspace/web/ui → nuspace-0.3.0/ts}/biome.json +0 -0
  190. {nuspace-0.2.1/src/nuspace/web/ui → nuspace-0.3.0/ts}/index.html +0 -0
  191. {nuspace-0.2.1/src/nuspace/web/ui → nuspace-0.3.0/ts}/src/app/local.ts +0 -0
  192. {nuspace-0.2.1/src/nuspace/web/ui → nuspace-0.3.0/ts}/src/app/theme.ts +0 -0
  193. {nuspace-0.2.1/src/nuspace/web/ui → nuspace-0.3.0/ts}/src/app/wire.ts +0 -0
  194. {nuspace-0.2.1/src/nuspace/web/ui → nuspace-0.3.0/ts}/src/components/section-status-dot.tsx +0 -0
  195. {nuspace-0.2.1/src/nuspace/web/ui → nuspace-0.3.0/ts}/src/design/section-status.ts +0 -0
  196. {nuspace-0.2.1/src/nuspace/web/ui → nuspace-0.3.0/ts}/src/design/tokens.css +0 -0
  197. {nuspace-0.2.1/src/nuspace/web/ui → nuspace-0.3.0/ts}/src/main.tsx +0 -0
  198. {nuspace-0.2.1/src/nuspace/web/ui/src/components/rail → nuspace-0.3.0/ts/src/refs/sidebar}/roving-focus.ts +0 -0
  199. {nuspace-0.2.1/src/nuspace/web/ui/src/refs/pages → nuspace-0.3.0/ts/src/refs/viewer}/Code.tsx +0 -0
  200. {nuspace-0.2.1/src/nuspace/web/ui/src/refs/pages → nuspace-0.3.0/ts/src/refs/viewer}/ProseRef.test.tsx +0 -0
  201. {nuspace-0.2.1/src/nuspace/web/ui/src/refs/pages → nuspace-0.3.0/ts/src/refs/viewer}/ProseRef.tsx +0 -0
  202. {nuspace-0.2.1/src/nuspace/web/ui/src/refs/pages → nuspace-0.3.0/ts/src/refs/viewer}/Slash.tsx +0 -0
  203. {nuspace-0.2.1/src/nuspace/web/ui/src/refs/pages → nuspace-0.3.0/ts/src/refs/viewer}/monaco-impl.ts +0 -0
  204. {nuspace-0.2.1/src/nuspace/web/ui/src/refs/pages → nuspace-0.3.0/ts/src/refs/viewer}/monaco.ts +0 -0
  205. {nuspace-0.2.1/src/nuspace/web/ui/src/refs/pages → nuspace-0.3.0/ts/src/refs/viewer}/prose.ts +0 -0
  206. {nuspace-0.2.1/src/nuspace/web/ui → nuspace-0.3.0/ts}/src/vite-env.d.ts +0 -0
  207. {nuspace-0.2.1/src/nuspace/web/ui → nuspace-0.3.0/ts}/tsconfig.app.json +0 -0
  208. {nuspace-0.2.1/src/nuspace/web/ui → nuspace-0.3.0/ts}/tsconfig.json +0 -0
  209. {nuspace-0.2.1/src/nuspace/web/ui → nuspace-0.3.0/ts}/tsconfig.node.json +0 -0
  210. {nuspace-0.2.1/src/nuspace/web/ui → nuspace-0.3.0/ts}/vite.config.ts +0 -0
@@ -20,8 +20,8 @@
20
20
  # Environment name: (leave blank)
21
21
  #
22
22
  # Release flow:
23
- # 1. Bump `version` in both `src/nuspace/web/ui/pyproject.toml` and
24
- # `src/nuspace/web/ui/package.json` (keep them equal).
23
+ # 1. Bump `version` in both `ts/pyproject.toml` and
24
+ # `ts/package.json` (keep them equal).
25
25
  # 2. Commit and push to main.
26
26
  # 3. `git tag nuspace-ui@X.Y.Z && git push origin nuspace-ui@X.Y.Z`.
27
27
  name: Publish nuspace-ui to PyPI
@@ -40,7 +40,7 @@ concurrency:
40
40
  cancel-in-progress: false
41
41
 
42
42
  env:
43
- UI_DIR: src/nuspace/web/ui
43
+ UI_DIR: ts
44
44
 
45
45
  jobs:
46
46
  publish:
@@ -53,7 +53,7 @@ jobs:
53
53
  with:
54
54
  node-version: 24
55
55
  cache: npm
56
- cache-dependency-path: src/nuspace/web/ui/package-lock.json
56
+ cache-dependency-path: ts/package-lock.json
57
57
 
58
58
  - uses: actions/setup-python@v5
59
59
  with:
@@ -115,5 +115,5 @@ jobs:
115
115
  if: startsWith(github.ref, 'refs/tags/nuspace-ui@')
116
116
  uses: pypa/gh-action-pypi-publish@release/v1
117
117
  with:
118
- packages-dir: src/nuspace/web/ui/wheel-dist
118
+ packages-dir: ts/wheel-dist
119
119
  skip-existing: true
@@ -15,13 +15,13 @@
15
15
  #
16
16
  # Two distributions ship from this repo, on independent tags:
17
17
  # . -> nuspace (the runtime + the `nuspace` command)
18
- # src/nuspace/web/ui -> nuspace-ui (the compiled vite bundle)
18
+ # ts -> nuspace-ui (the compiled vite bundle)
19
19
  #
20
20
  # They version independently. `nuspace` hard-depends on `nuspace-ui>=X.Y.Z`, so
21
21
  # a python release can require a newer bundle, but a bundle-only fix ships on
22
22
  # its own tag without republishing this wheel.
23
23
  #
24
- # The wheel deliberately excludes `src/nuspace/web/ui/**` (see the hatch config
24
+ # The wheel deliberately excludes `ts/**` (see the hatch config
25
25
  # in pyproject.toml). The compiled assets live in the nuspace-ui wheel and are
26
26
  # found at runtime by `import nuspace_ui` -- see `_bundled_static()` in
27
27
  # src/nuspace/web/serve/app.py. So this build needs no node toolchain, even
@@ -2,13 +2,26 @@
2
2
 
3
3
  ## Packages shipped from this repo
4
4
 
5
- - **nuspace** (the runtime, the `nuspace` command) — 0.2.1
6
- - **nuspace-ui** (the compiled web bundle) — 0.2.1
5
+ - **nuspace** (the runtime, the `nuspace` command) — 0.3.0
6
+ - **nuspace-ui** (the compiled web bundle) — 0.3.0
7
7
 
8
8
  Below is the changelog for **nuspace** - the full commit stream. Newest first.
9
9
  The `nuspace-ui` wheel is the same source tree's vite output and is not tracked
10
10
  separately here.
11
11
 
12
+ ## 0.3.0 — 2026-09-21
13
+
14
+ - Make a chat, ask it for something, and watch it work the space
15
+ - See how a job is set to run, and edit what it does in a real editor
16
+ - Keep pages and jobs in separate sidebar sections, and make either in one click
17
+ - Collapse the whole interface to one sidebar and one viewer over any plane
18
+ - A plane now says whether it draws and whether you can edit it
19
+ - Open a space in the browser and write prose and programs into a plane's cells
20
+ - Boot a browser tab with the chrome it holds before anything is drawn
21
+ - Let a cell draw on a browser connection, and let a driver say which planes are up
22
+ - Run a space of planes and cells, each cell with its own restart policy
23
+ - Clear the v0.2 source out of the way for the v0.3 rewrite
24
+
12
25
  ## 0.2.1 — 2026-09-17
13
26
 
14
27
  - Open a page on quiet air and its own name instead of decoration
@@ -5,7 +5,7 @@ GREEN := \033[0;32m
5
5
  YELLOW := \033[1;33m
6
6
  NC := \033[0m
7
7
 
8
- UI_APP := src/nuspace/web/ui
8
+ UI_APP := ts
9
9
 
10
10
  help:
11
11
  @echo "$(BLUE)nuspace$(NC)"
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.5
2
2
  Name: nuspace
3
- Version: 0.2.1
3
+ Version: 0.3.0
4
4
  Summary: Nu runtime built on Nu. A self-hosted programmable space that runs Nu apps in a tree of containers and applications.
5
5
  Project-URL: Repository, https://github.com/nustackdev/nuspace
6
6
  Author-email: Gor Arakelyan <gorarkln@gmail.com>
@@ -17,9 +17,9 @@ Classifier: Programming Language :: Python :: 3.13
17
17
  Requires-Python: >=3.10
18
18
  Requires-Dist: click>=8.0
19
19
  Requires-Dist: nuagent>=0.1.3
20
- Requires-Dist: nucore>=0.5.1
21
- Requires-Dist: nuspace-ui>=0.2.1
22
- Requires-Dist: nustd[all]>=0.5.1
20
+ Requires-Dist: nucore>=0.5.2
21
+ Requires-Dist: nuspace-ui>=0.3.0
22
+ Requires-Dist: nustd[all]>=0.5.2
23
23
  Requires-Dist: rich-click>=1.6
24
24
  Requires-Dist: rich>=10.7
25
25
  Requires-Dist: typing-extensions>=4.7.0
@@ -0,0 +1,277 @@
1
+ """A Space of two Planes and three Cells, run headless, start to finish in twelve seconds.
2
+
3
+ Everything phase 1 claims, shown rather than described:
4
+
5
+ - a Plane whose ``exec_mode`` is ``async`` holds two Cells in one worker;
6
+ - a Plane whose ``exec_mode`` is ``mp`` gives its one Cell a process to itself;
7
+ - nothing a person wrote runs in the main process, which the pids prove;
8
+ - ``restart: always`` brings a program back after it returns, ``restart: no``
9
+ leaves it stopped;
10
+ - ``reload`` restarts the Cell whose program changed, and only that one.
11
+
12
+ Every Cell runs the same tiny program: count this run, stamp the process it ran
13
+ in, say which version of itself it is. Three numbers, and between them they
14
+ answer every question above.
15
+
16
+ Run it::
17
+
18
+ python examples/headless.py
19
+
20
+ The Space is left on disk, so ``nuspace -s <path> ls`` reads what it did.
21
+ """
22
+
23
+ from __future__ import annotations
24
+
25
+ import asyncio
26
+ import logging
27
+ import os
28
+ import tempfile
29
+
30
+ import nu
31
+ import nustd.kv
32
+ from nuspace import (
33
+ EXEC_ASYNC,
34
+ EXEC_MP,
35
+ RESTART_ALWAYS,
36
+ RESTART_NO,
37
+ TRIGGER_BOOT,
38
+ Space,
39
+ open_space,
40
+ ops,
41
+ store,
42
+ )
43
+ from nuspace.drivers import run_space
44
+
45
+
46
+ # Closing the Space drops two sockets under workers that are still holding them,
47
+ # and the transport logs every one of those as an error. Expected, and the only
48
+ # thing this demo has to say is what it printed itself. At module scope so a
49
+ # spawned worker, which imports this file, is quiet too.
50
+ logging.getLogger("invisibles").setLevel(logging.CRITICAL)
51
+
52
+
53
+ LOOP, BEAT, HOLD = "loop", "beat", "hold"
54
+ SOLO, WORK = "solo", "work"
55
+
56
+ #: How long the Space runs before the first report. Long enough for a Cell that
57
+ #: keeps coming back to have come back a good few times.
58
+ BOOT_SECONDS = 5.0
59
+
60
+ #: How long an edit is given to land before the report after it.
61
+ EDIT_SECONDS = 3.0
62
+
63
+
64
+ def source(version: int) -> str:
65
+ """The program every Cell holds. Counts the run, stamps the process, ends.
66
+
67
+ Ending is the point: a program that returns is what ``restart`` is about,
68
+ so the same three lines demonstrate both settings of it.
69
+ """
70
+ return f'''import os
71
+
72
+ import nu
73
+ import nustd.kv
74
+ from nuspace import Space
75
+
76
+
77
+ def out(plane, cell):
78
+ """Count this run, stamp the process it ran in, and end."""
79
+ state = Space.planes[plane].cells[cell].state
80
+ runs = nu.Int(nu.ToInt(state.get_item("runs", nu.Int(0))))
81
+ return nustd.kv.auto_flow_atomic(
82
+ state.set_item("runs", runs + nu.Int(1))
83
+ >> state.set_item("pid", nu.Int(os.getpid()))
84
+ >> state.set_item("version", nu.Int({version})),
85
+ scope=Space,
86
+ )
87
+ '''
88
+
89
+
90
+ # --- what the Space is -------------------------------------------------------
91
+
92
+
93
+ def seed():
94
+ """Two Planes and three Cells, each Cell one commit."""
95
+ return (
96
+ ops.add_plane(plane_id=LOOP, exec_mode=EXEC_ASYNC, trigger=TRIGGER_BOOT)
97
+ >> ops.add_cell(LOOP, source(1), cell_id=BEAT, restart=RESTART_ALWAYS)
98
+ >> ops.add_cell(LOOP, source(1), cell_id=HOLD, restart=RESTART_NO)
99
+ >> ops.add_plane(plane_id=SOLO, exec_mode=EXEC_MP, trigger=TRIGGER_BOOT)
100
+ >> ops.add_cell(SOLO, source(1), cell_id=WORK, restart=RESTART_ALWAYS)
101
+ )
102
+
103
+
104
+ # --- what it says while it runs ----------------------------------------------
105
+
106
+
107
+ def say(text):
108
+ return nu.Print(nu.STDOUT, nu.Str(text), flush=True)
109
+
110
+
111
+ def kept(plane, cell, key):
112
+ """One number a Cell's program kept, zero where it has not written it yet."""
113
+ state = Space.planes[plane].cells[cell].state
114
+ return nu.Int(nu.ToInt(state.get_item(key, nu.Int(0))))
115
+
116
+
117
+ def show(plane, cell):
118
+ return nu.Print(
119
+ nu.STDOUT,
120
+ nu.Str(f" {plane}/{cell:<6}"),
121
+ nu.Str(" runs "),
122
+ nu.ToStr(kept(plane, cell, "runs")),
123
+ nu.Str(" version "),
124
+ nu.ToStr(kept(plane, cell, "version")),
125
+ nu.Str(" pid "),
126
+ nu.ToStr(kept(plane, cell, "pid")),
127
+ sep="",
128
+ flush=True,
129
+ )
130
+
131
+
132
+ def report(title):
133
+ return say(title) >> show(LOOP, BEAT) >> show(LOOP, HOLD) >> show(SOLO, WORK)
134
+
135
+
136
+ def timeline():
137
+ """The whole demo as one branch: watch, edit, watch, edit, watch.
138
+
139
+ It runs beside the runtime in the main process. When it ends, the Race
140
+ around it ends, which is what closes the Space.
141
+ """
142
+ return (
143
+ nu.DelayedDo(
144
+ nu.Float(BOOT_SECONDS),
145
+ report(f"\nact 1 {BOOT_SECONDS:.0f} seconds in, nobody has touched anything"),
146
+ )
147
+ >> say("\nact 2 editing loop/beat to version 2")
148
+ >> ops.set_prog(LOOP, BEAT, source(2))
149
+ >> nu.DelayedDo(
150
+ nu.Float(EDIT_SECONDS),
151
+ report(" beat is on the new program, hold has not moved"),
152
+ )
153
+ >> say("\nact 3 editing loop/hold, which has been stopped since its one run")
154
+ >> ops.set_prog(LOOP, HOLD, source(2))
155
+ >> nu.DelayedDo(
156
+ nu.Float(EDIT_SECONDS),
157
+ report(" the edit started it again, for exactly one more run"),
158
+ )
159
+ )
160
+
161
+
162
+ def body():
163
+ """Seed the Space, then run it against the timeline until the timeline ends."""
164
+ return nustd.kv.auto_flow_atomic(seed(), scope=Space) >> nu.Race(
165
+ run_space(),
166
+ nustd.kv.auto_flow_atomic(timeline(), scope=Space),
167
+ )
168
+
169
+
170
+ # --- what it says afterwards -------------------------------------------------
171
+
172
+
173
+ def read_back(path):
174
+ """What the three Cells kept, read out of the closed store."""
175
+ value, _ = nu.run_in_loop(
176
+ nu.With(
177
+ store(path),
178
+ body=nustd.kv.Snapshot(
179
+ nu.Dict.of(
180
+ beat=ops.cell_state(LOOP, BEAT),
181
+ hold=ops.cell_state(LOOP, HOLD),
182
+ work=ops.cell_state(SOLO, WORK),
183
+ ),
184
+ scope=Space,
185
+ ),
186
+ ),
187
+ nu.Context(),
188
+ )
189
+ return value
190
+
191
+
192
+ def verdict(path, main_pid):
193
+ """Every claim as a check against the final state, so it passes or it does not."""
194
+ state = read_back(path)
195
+ runs = {k: state[k].get("runs", 0) for k in state}
196
+ pids = {k: state[k].get("pid", 0) for k in state}
197
+ version = {k: state[k].get("version", 0) for k in state}
198
+ checks = [
199
+ (
200
+ "restart always",
201
+ runs["beat"] >= 5,
202
+ f"loop/beat returned and came back {runs['beat']} times",
203
+ ),
204
+ (
205
+ "restart no",
206
+ runs["hold"] == 2,
207
+ f"loop/hold ran {runs['hold']} times, never once on its own account",
208
+ ),
209
+ (
210
+ "reload",
211
+ version["beat"] == 2,
212
+ "loop/beat is running the program it was edited to",
213
+ ),
214
+ (
215
+ "reload restarts a stopped Cell",
216
+ version["hold"] == 2 and runs["hold"] == 2,
217
+ "loop/hold was parked, and the edit is what started it again",
218
+ ),
219
+ (
220
+ "siblings are left alone",
221
+ runs["hold"] == 2,
222
+ "editing loop/beat left loop/hold on its single boot run",
223
+ ),
224
+ (
225
+ "nothing runs in main",
226
+ main_pid not in pids.values(),
227
+ f"main is {main_pid}, no Cell is",
228
+ ),
229
+ (
230
+ "async is one process for the Plane",
231
+ pids["beat"] == pids["hold"],
232
+ f"loop/beat and loop/hold both ran in {pids['beat']}",
233
+ ),
234
+ (
235
+ "mp is one process for the Cell",
236
+ pids["work"] not in (main_pid, pids["beat"]),
237
+ f"solo/work ran in {pids['work']}, on its own",
238
+ ),
239
+ ]
240
+ print("\nverdict, read back from the closed store\n")
241
+ for name, passed, detail in checks:
242
+ print(f" {'PASS' if passed else 'FAIL'} {name:<36}{detail}")
243
+ failed = [name for name, passed, _ in checks if not passed]
244
+ print()
245
+ if failed:
246
+ print(f"{len(failed)} check(s) failed: {', '.join(failed)}")
247
+ else:
248
+ print("all good")
249
+ print(f"\nthe Space is still there: nuspace -s {path} ls")
250
+
251
+
252
+ # --- driving it --------------------------------------------------------------
253
+
254
+
255
+ async def live(path):
256
+ """The Space open, from boot to the end of the timeline."""
257
+ # max_parallel=1: nothing in the runtime computes, and a larger budget
258
+ # rations each Plane's Race against a semaphore those arms never give back.
259
+ await nu.arun(open_space(body(), path=path), nu.Context(), max_parallel=1)
260
+
261
+
262
+ def main():
263
+ path = tempfile.mkdtemp(prefix="nuspace-demo-")
264
+ print(f"\nnuspace headless demo {path}\n")
265
+ print(" loop async boot beat (restart always) hold (restart no)")
266
+ print(" solo mp boot work (restart always)")
267
+ print(f" main process is {os.getpid()}")
268
+ asyncio.run(live(path))
269
+ # The read back happens after the loop is gone, on a store nothing holds
270
+ # any more, which is the point: this is what a person would find there.
271
+ verdict(path, os.getpid())
272
+
273
+
274
+ if __name__ == "__main__":
275
+ # The guard is load bearing: the pool spawns, and a child re-imports
276
+ # whatever __main__ is. Without it, opening a Space forks forever.
277
+ main()
@@ -0,0 +1,141 @@
1
+ """A Space in a browser, where opening a Plane is what starts it.
2
+
3
+ Everything phase 2 claims, in a tab rather than in a print:
4
+
5
+ - the sidebar lists every Plane that draws, under one row for the Space;
6
+ - clicking one selects it, and selecting it is what brings its Cells up;
7
+ - a Cell draws under its own node on the page, from the worker it runs in;
8
+ - navigating away takes that worker down, and navigating back brings it up
9
+ again, which the counter says out loud by starting from where it left off;
10
+ - prose is a Cell like any other: it holds a program that draws an editor.
11
+
12
+ Two Planes are seeded the first time and nothing after that, so what you write
13
+ is still there next time.
14
+
15
+ Run it::
16
+
17
+ python examples/web.py
18
+
19
+ A browser opens on http://127.0.0.1:8080. Pick Notes and type. Pick
20
+ Counter and watch it climb, then pick Notes again and come back to it: the
21
+ seconds are not the ones that went by while you were away, because nothing was
22
+ running while nobody was looking.
23
+
24
+ Ctrl+C stops it. The Space is left on disk, so `nuspace -s <path> ls` reads
25
+ what it did.
26
+ """
27
+
28
+ from __future__ import annotations
29
+
30
+ import logging
31
+ import tempfile
32
+ from pathlib import Path
33
+
34
+ import nu
35
+ import nustd.kv
36
+ from nuspace import EXEC_ASYNC, TRIGGER_NAV, Space, ops, presets, store
37
+ from nuspace.web.viewer import prose_source
38
+
39
+
40
+ # Closing the Space drops the sockets under workers that are still holding
41
+ # them, and the transport logs every one of those. Expected, and the only thing
42
+ # this demo has to say is what it drew. At module scope so a spawned worker,
43
+ # which imports this file, is quiet too.
44
+ logging.getLogger("invisibles").setLevel(logging.CRITICAL)
45
+
46
+
47
+ #: Kept between runs, because the point of a page is that what you wrote is
48
+ #: still on it tomorrow.
49
+ STORE = Path(tempfile.gettempdir()) / "nuspace-web-demo"
50
+
51
+ PORT = 8080
52
+
53
+ NOTES, COUNTER = "p_notes", "p_counter"
54
+ TEXT, TICK = "s_text", "s_tick"
55
+
56
+
57
+ #: A Cell that draws. One stat tile and a loop, and it says nothing about where
58
+ #: the tile lives: the ref is written bare and the host roots it under this
59
+ #: Cell's own node on whatever surface the Plane is drawn on.
60
+ TICKER = '''import nu
61
+ import nustd.kv
62
+ import nustd.ui
63
+ from nuspace import Space
64
+
65
+
66
+ def out(plane, cell):
67
+ """Count seconds, in the browser and in this Cell's own state."""
68
+ tile = nustd.ui.StatRef("seconds")
69
+ state = Space.planes[plane].cells[cell].state
70
+ now = nu.Int(nu.ToInt(state.get_item("ticks", nu.Int(0))))
71
+ step = state.set_item("ticks", now + nu.Int(1)) >> tile.set_value(
72
+ nu.ToStr(now + nu.Int(1))
73
+ )
74
+ # A program owns its own atomicity. Nothing brackets it on the way in,
75
+ # because the host cannot see inside a program it evaluates.
76
+ return nustd.kv.auto_flow_atomic(
77
+ tile.set_label(nu.Str("seconds this Plane has been open"))
78
+ >> nu.ForeverDo(nu.DelayedDo(nu.Float(1.0), step)),
79
+ scope=Space,
80
+ )
81
+ '''
82
+
83
+
84
+ def plane(plane_id, name):
85
+ """One Plane a tab can navigate to, drawn as a page."""
86
+ return ops.add_plane(
87
+ plane_id=plane_id,
88
+ name=name,
89
+ exec_mode=EXEC_ASYNC,
90
+ # Up because somebody is looking at it, and down again when they stop.
91
+ trigger=TRIGGER_NAV,
92
+ ui=True,
93
+ editable=True,
94
+ )
95
+
96
+
97
+ def seed():
98
+ """Two Planes, once. A Space that already has one is left alone.
99
+
100
+ Guarded rather than made idempotent: adding a Cell rewrites the row, so
101
+ seeding on every run would put the empty page back over whatever the last
102
+ run wrote on it.
103
+ """
104
+ made = (
105
+ plane(NOTES, "Notes")
106
+ >> ops.add_cell(NOTES, prose_source(Space), cell_id=TEXT, name=TEXT)
107
+ >> plane(COUNTER, "Counter")
108
+ >> ops.add_cell(COUNTER, TICKER, cell_id=TICK, name=TICK)
109
+ )
110
+ return nu.IfDo(nu.Eq(nu.Len(ops.plane_ids()), nu.Int(0)), made)
111
+
112
+
113
+ def main():
114
+ STORE.mkdir(parents=True, exist_ok=True)
115
+ print(f"\nnuspace web demo {STORE}")
116
+ print(" Notes a page to write on")
117
+ print(" Counter a Cell that draws, and only while you are looking")
118
+ # Seeded against a store this process then closes, because the Space the
119
+ # server opens takes the write lock for as long as it is up.
120
+ nu.run_in_loop(
121
+ nu.With(store(str(STORE)), body=nustd.kv.auto_flow_atomic(seed(), scope=Space)),
122
+ nu.Context(),
123
+ )
124
+ print(f"\n http://127.0.0.1:{PORT}, Ctrl+C to stop\n")
125
+ try:
126
+ # max_parallel=1: nothing in the runtime computes, every branch is an
127
+ # await, and a larger budget rations each arm against a semaphore those
128
+ # arms never give back.
129
+ nu.run_in_loop(
130
+ presets.full(path=str(STORE), port=PORT),
131
+ nu.Context(),
132
+ max_parallel=1,
133
+ )
134
+ except KeyboardInterrupt:
135
+ print("stopped")
136
+
137
+
138
+ if __name__ == "__main__":
139
+ # The guard is load bearing: the pool spawns, and a child re-imports
140
+ # whatever __main__ is. Without it, opening a Space forks forever.
141
+ main()
@@ -4,7 +4,7 @@ build-backend = "hatchling.build"
4
4
 
5
5
  [project]
6
6
  name = "nuspace"
7
- version = "0.2.1"
7
+ version = "0.3.0"
8
8
  description = "Nu runtime built on Nu. A self-hosted programmable space that runs Nu apps in a tree of containers and applications."
9
9
  readme = "README.md"
10
10
  license = "AGPL-3.0-or-later"
@@ -16,12 +16,12 @@ dependencies = [
16
16
  # ui for the browser, proxy (invisibles) for the worker's session, mp_pool
17
17
  # for the workers themselves. Picking extras just means discovering the
18
18
  # missing one at import time.
19
- "nucore>=0.5.1",
20
- "nustd[all]>=0.5.1",
19
+ "nucore>=0.5.2",
20
+ "nustd[all]>=0.5.2",
21
21
  # The compiled web bundle. A separate distribution only because PyPI caps
22
22
  # project size and a SPA rebuilt on every UI tweak would eat the quota --
23
23
  # not optional. `pip install nuspace` gives you the browser.
24
- "nuspace-ui>=0.2.1",
24
+ "nuspace-ui>=0.3.0",
25
25
  "nuagent>=0.1.3",
26
26
  "typing-extensions>=4.7.0",
27
27
  "click>=8.0",
@@ -56,21 +56,18 @@ nuspace = "nuspace.cli:main"
56
56
 
57
57
  [tool.hatch.build.targets.wheel]
58
58
  packages = ["src/nuspace"]
59
- exclude = [
60
- "src/nuspace/web/ui/**",
61
- ]
62
59
 
63
60
  [tool.hatch.build.targets.sdist]
64
61
  exclude = [
65
- "src/nuspace/web/ui/node_modules",
66
- "src/nuspace/web/ui/dist",
62
+ "ts/node_modules",
63
+ "ts/dist",
67
64
  ]
68
65
 
69
66
  [tool.uv.sources]
70
- nuspace-ui = { path = "./src/nuspace/web/ui" }
71
- nucore = { path = "../nu/packages/nucore", editable = true }
72
- nustd = { path = "../nu/packages/nustd", editable = true }
73
- nucli = { path = "../nu/packages/nucli", editable = true }
67
+ nuspace-ui = { path = "./ts" }
68
+ nucore = { path = "../nu", editable = true }
69
+ nustd = { path = "../nu/pkgs/nustd", editable = true }
70
+ nucli = { path = "../nu/pkgs/nucli", editable = true }
74
71
  nuagent = { path = "../nuagent", editable = true }
75
72
 
76
73
  [tool.uv]
@@ -0,0 +1,82 @@
1
+ """nuspace: a Nu orchestrator that supports different mediums and behaviors.
2
+
3
+ Everything is inside a **Space**. A Space is a collection of **Planes**, and a
4
+ Plane says how and when its **Cells** run and how they are drawn. A Cell is a
5
+ Nu program, and it is the only thing here that executes.
6
+
7
+ Six modules, and the names are the model::
8
+
9
+ nuspace.shapes the store layout
10
+ nuspace.space opening a Space in a process
11
+ nuspace.ops what a person does to a Space
12
+ nuspace.exec how a Space runs
13
+ nuspace.drivers when and what runs
14
+ nuspace.presets a Space open with some set of drivers in it
15
+
16
+ The store layout and the two entry points are re-exported here, so a program
17
+ that lives in a Cell says ``from nuspace import Space`` and a process that
18
+ opens one says ``open_space(run_space())``. Everything else is reached through
19
+ the module it lives in, because ``ops``, ``exec``, ``drivers`` and ``presets``
20
+ are families rather than functions.
21
+ """
22
+
23
+ from nuspace import drivers, exec, ops, presets
24
+ from nuspace.drivers import run_space
25
+ from nuspace.shapes import (
26
+ DEFAULT_EDITABLE,
27
+ DEFAULT_EXEC_MODE,
28
+ DEFAULT_RELOAD,
29
+ DEFAULT_RESTART,
30
+ DEFAULT_TRIGGER,
31
+ DEFAULT_UI,
32
+ EXEC_ASYNC,
33
+ EXEC_MODES,
34
+ EXEC_MP,
35
+ RESTART_ALWAYS,
36
+ RESTART_NO,
37
+ RESTART_ON_FAILURE,
38
+ RESTARTS,
39
+ TRIGGER_BOOT,
40
+ TRIGGER_MANUAL,
41
+ TRIGGER_NAV,
42
+ TRIGGERS,
43
+ Cell,
44
+ CellProps,
45
+ Plane,
46
+ PlaneProps,
47
+ Space,
48
+ )
49
+ from nuspace.space import open_space, store
50
+
51
+
52
+ __all__ = [
53
+ "DEFAULT_EDITABLE",
54
+ "DEFAULT_EXEC_MODE",
55
+ "DEFAULT_RELOAD",
56
+ "DEFAULT_RESTART",
57
+ "DEFAULT_TRIGGER",
58
+ "DEFAULT_UI",
59
+ "EXEC_ASYNC",
60
+ "EXEC_MODES",
61
+ "EXEC_MP",
62
+ "RESTARTS",
63
+ "RESTART_ALWAYS",
64
+ "RESTART_NO",
65
+ "RESTART_ON_FAILURE",
66
+ "TRIGGERS",
67
+ "TRIGGER_BOOT",
68
+ "TRIGGER_MANUAL",
69
+ "TRIGGER_NAV",
70
+ "Cell",
71
+ "CellProps",
72
+ "Plane",
73
+ "PlaneProps",
74
+ "Space",
75
+ "drivers",
76
+ "exec",
77
+ "open_space",
78
+ "ops",
79
+ "presets",
80
+ "run_space",
81
+ "store",
82
+ ]
@@ -0,0 +1,11 @@
1
+ """The ``nuspace`` command.
2
+
3
+ Seed a Space with ``plane`` and ``cell``, run it headless with ``run``, look at
4
+ it with ``ls``. A Space is a directory one process locks at a time, so the
5
+ commands that write want it to themselves and ``run`` holds it while it is up.
6
+ """
7
+
8
+ from nuspace.cli.main import cli, main
9
+
10
+
11
+ __all__ = ["cli", "main"]
@@ -0,0 +1,11 @@
1
+ """Enables ``python -m nuspace.cli``.
2
+
3
+ The guard is load bearing, not decoration: the worker pool spawns, and a child
4
+ re-imports whatever ``__main__`` is. Without it, opening a Space forks forever.
5
+ """
6
+
7
+ from nuspace.cli.main import main
8
+
9
+
10
+ if __name__ == "__main__":
11
+ main()