@notis_ai/cli 0.2.18 → 0.2.19

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 (395) hide show
  1. package/README.md +542 -146
  2. package/bin/check-runtime.js +15 -0
  3. package/bin/notis.js +2 -0
  4. package/config/notis_app_design_rules.json +320 -0
  5. package/config/portal-route-roots.json +45 -0
  6. package/dist/agent-hooks/notis-agent-hook.mjs +42325 -18273
  7. package/dist/base-skills/notis-cli/.notis-base-skill.json +1 -0
  8. package/dist/base-skills/notis-cli/SKILL.md +51 -280
  9. package/dist/base-skills/notis-cli/references/app-delivery.md +17 -0
  10. package/dist/base-skills/notis-cli/references/intelligence.md +94 -0
  11. package/dist/base-skills/notis-cli/references/native-databases.md +21 -0
  12. package/dist/base-skills/notis-cli/references/tool-examples.md +56 -0
  13. package/dist/base-skills/notis-cli/references/troubleshooting.md +67 -0
  14. package/dist/base-skills/notis-query/.notis-base-skill.json +1 -0
  15. package/dist/base-skills/notis-query/SKILL.md +35 -699
  16. package/dist/base-skills/notis-query/references/database-discovery.md +59 -0
  17. package/dist/base-skills/notis-query/references/documents.md +74 -0
  18. package/dist/base-skills/notis-query/references/query.md +76 -0
  19. package/dist/base-skills/notis-spaces/.notis-base-skill.json +1 -0
  20. package/dist/base-skills/notis-spaces/SKILL.md +218 -0
  21. package/dist/base-skills/notis-spaces/references/architecture.md +116 -0
  22. package/dist/base-skills/notis-spaces/references/context.md +81 -0
  23. package/dist/base-skills/notis-spaces/references/design.md +141 -0
  24. package/dist/base-skills/notis-spaces/references/fixtures.md +29 -0
  25. package/dist/base-skills/notis-spaces/references/memory.md +58 -0
  26. package/dist/base-skills/notis-spaces/references/reading.md +73 -0
  27. package/dist/base-skills/notis-spaces/references/release.md +120 -0
  28. package/dist/base-skills/notis-spaces/references/reports.md +77 -0
  29. package/dist/base-skills/notis-spaces/references/sdk.md +273 -0
  30. package/dist/base-skills/notis-spaces/references/store.md +118 -0
  31. package/dist/base-skills/notis-spaces/references/troubleshooting.md +28 -0
  32. package/dist/base-skills/notis-spaces/references/views.md +229 -0
  33. package/{template/packages → dist}/sdk/package.json +9 -10
  34. package/dist/sdk/src/agentContext.ts +36 -0
  35. package/dist/sdk/src/bundleModule.ts +175 -0
  36. package/dist/sdk/src/components/Dialog.tsx +103 -0
  37. package/dist/sdk/src/components/DocumentEditor.tsx +113 -0
  38. package/dist/sdk/src/components/DocumentPage.tsx +184 -0
  39. package/{template/packages → dist}/sdk/src/components/Markdown.tsx +9 -3
  40. package/{template/packages → dist}/sdk/src/components/MarkdownEditor.tsx +26 -5
  41. package/{template/packages → dist}/sdk/src/components/MultiSelectActionBar.tsx +95 -12
  42. package/{template/packages → dist}/sdk/src/components/MultiSelectCheckbox.tsx +4 -2
  43. package/dist/sdk/src/components/NotisCommentBoundary.tsx +172 -0
  44. package/{template/packages → dist}/sdk/src/components/NotisSelectionBoundary.tsx +9 -5
  45. package/dist/sdk/src/components/RecordComponents.tsx +111 -0
  46. package/dist/sdk/src/components/Skeleton.tsx +38 -0
  47. package/dist/sdk/src/config.ts +31 -0
  48. package/{template/packages → dist}/sdk/src/documents.ts +21 -6
  49. package/dist/sdk/src/hooks/useAction.ts +28 -0
  50. package/dist/sdk/src/hooks/useAgentContext.ts +23 -0
  51. package/{template/packages → dist}/sdk/src/hooks/useBackend.ts +3 -2
  52. package/dist/sdk/src/hooks/useCloudComputer.ts +94 -0
  53. package/dist/sdk/src/hooks/useCloudComputerShell.ts +114 -0
  54. package/{template/packages → dist}/sdk/src/hooks/useCollectionInteractions.ts +157 -147
  55. package/dist/sdk/src/hooks/useDatabaseSchema.ts +50 -0
  56. package/{template/packages → dist}/sdk/src/hooks/useDatabaseSubscription.ts +2 -2
  57. package/dist/sdk/src/hooks/useDocument.ts +51 -0
  58. package/{template/packages → dist}/sdk/src/hooks/useDocuments.ts +25 -59
  59. package/{template/packages → dist}/sdk/src/hooks/useHandover.ts +7 -7
  60. package/dist/sdk/src/hooks/useLongPressSelection.ts +79 -0
  61. package/dist/sdk/src/hooks/useMarqueeSelection.ts +194 -0
  62. package/{template/packages → dist}/sdk/src/hooks/useMultiSelect.ts +2 -8
  63. package/dist/sdk/src/hooks/useNativeDocumentBody.ts +50 -0
  64. package/dist/sdk/src/hooks/useNativeDocuments.ts +143 -0
  65. package/{template/packages → dist}/sdk/src/hooks/useNotis.ts +6 -1
  66. package/dist/sdk/src/hooks/useNotisNavigation.ts +70 -0
  67. package/dist/sdk/src/hooks/useQuery.ts +81 -0
  68. package/dist/sdk/src/hooks/useToolQuery.ts +12 -0
  69. package/dist/sdk/src/hooks/useTopBarSearch.ts +102 -0
  70. package/dist/sdk/src/hooks/useTopBarTitleLeaf.ts +75 -0
  71. package/{template/packages → dist}/sdk/src/hooks/useUpsertDocument.ts +1 -1
  72. package/dist/sdk/src/hooks/useViewParams.ts +195 -0
  73. package/dist/sdk/src/hooks/useViewerReads.ts +129 -0
  74. package/{template/packages → dist}/sdk/src/index.ts +64 -1
  75. package/{template/packages → dist}/sdk/src/interactions/actions.ts +14 -1
  76. package/{template/packages → dist}/sdk/src/interactions/shortcuts.tsx +129 -23
  77. package/dist/sdk/src/interactions/visibility.ts +13 -0
  78. package/{template/packages → dist}/sdk/src/interactions.ts +5 -1
  79. package/dist/sdk/src/nativeData.ts +39 -0
  80. package/{template/packages → dist}/sdk/src/provider.tsx +9 -4
  81. package/dist/sdk/src/queryCache.ts +292 -0
  82. package/dist/sdk/src/queryErrors.ts +51 -0
  83. package/dist/sdk/src/runtime.ts +675 -0
  84. package/dist/sdk/src/runtimeModules.ts +33 -0
  85. package/dist/sdk/src/space.ts +442 -0
  86. package/dist/sdk/src/standardViewParams.ts +36 -0
  87. package/{template/packages → dist}/sdk/src/styles.css +81 -16
  88. package/dist/sdk/src/tailwind.ts +66 -0
  89. package/dist/sdk/src/ui/charts/RenderChartData.tsx +25 -0
  90. package/dist/sdk/src/ui/engine/core.ts +392 -0
  91. package/dist/sdk/src/ui/engine/dom.ts +183 -0
  92. package/dist/sdk/src/ui/engine/hooks.ts +195 -0
  93. package/dist/sdk/src/ui/engine/instance.ts +63 -0
  94. package/dist/sdk/src/ui/engine/keycapContext.ts +2 -0
  95. package/dist/sdk/src/ui/engine/registry.ts +262 -0
  96. package/dist/sdk/src/ui/engine/types.ts +62 -0
  97. package/dist/sdk/src/ui/icons.tsx +91 -0
  98. package/dist/sdk/src/ui/util.ts +83 -0
  99. package/dist/sdk/src/viewPeriod.ts +26 -0
  100. package/dist/sdk/src/vite.ts +92 -0
  101. package/dist/sdk/src/words/actions.ts +295 -0
  102. package/dist/sdk/src/words/clashes.ts +158 -0
  103. package/dist/sdk/src/words/format.ts +136 -0
  104. package/dist/sdk/src/words/index.ts +13 -0
  105. package/dist/sdk/src/words/keymap.ts +72 -0
  106. package/dist/sdk/src/words/keys.ts +297 -0
  107. package/dist/sdk/src/words/shell.ts +46 -0
  108. package/dist/sdk/src/words/types.ts +65 -0
  109. package/dist/skill-sync/index.js +832 -151
  110. package/dist/skill-sync/index.js.map +4 -4
  111. package/dist/skill-sync-worker.mjs +3941 -0
  112. package/dist/space-harness/host.js +1460 -0
  113. package/dist/space-sdk/dist/chunk-ZSN57ATH.js +5 -0
  114. package/dist/space-sdk/dist/index.js +15 -0
  115. package/dist/space-sdk/dist/ui.js +18 -0
  116. package/dist/space-sdk/package.json +33 -0
  117. package/dist/space-sdk/src/agentContext.d.ts +36 -0
  118. package/dist/space-sdk/src/components/Dialog.d.ts +20 -0
  119. package/dist/space-sdk/src/components/DocumentEditor.d.ts +46 -0
  120. package/dist/space-sdk/src/components/DocumentPage.d.ts +53 -0
  121. package/dist/space-sdk/src/components/Markdown.d.ts +18 -0
  122. package/dist/space-sdk/src/components/MarkdownEditor.d.ts +11 -0
  123. package/dist/space-sdk/src/components/MultiSelectActionBar.d.ts +31 -0
  124. package/dist/space-sdk/src/components/MultiSelectCheckbox.d.ts +14 -0
  125. package/dist/space-sdk/src/components/MultiSelectDragOverlay.d.ts +8 -0
  126. package/dist/space-sdk/src/components/NotisCommentBoundary.d.ts +36 -0
  127. package/dist/space-sdk/src/components/NotisSelectionBoundary.d.ts +16 -0
  128. package/dist/space-sdk/src/components/RecordComponents.d.ts +64 -0
  129. package/dist/space-sdk/src/components/ShortcutHints.d.ts +12 -0
  130. package/dist/space-sdk/src/components/Skeleton.d.ts +23 -0
  131. package/dist/space-sdk/src/config.ts +31 -0
  132. package/dist/space-sdk/src/documents.d.ts +59 -0
  133. package/dist/space-sdk/src/hooks/useAction.d.ts +8 -0
  134. package/dist/space-sdk/src/hooks/useActiveResource.d.ts +3 -0
  135. package/dist/space-sdk/src/hooks/useAgentContext.d.ts +7 -0
  136. package/dist/space-sdk/src/hooks/useBackend.d.ts +23 -0
  137. package/dist/space-sdk/src/hooks/useCloudComputer.d.ts +45 -0
  138. package/dist/space-sdk/src/hooks/useCloudComputerShell.d.ts +49 -0
  139. package/dist/space-sdk/src/hooks/useCollectionInteractions.d.ts +118 -0
  140. package/dist/space-sdk/src/hooks/useDatabaseSchema.d.ts +14 -0
  141. package/dist/space-sdk/src/hooks/useDatabaseSubscription.d.ts +26 -0
  142. package/dist/space-sdk/src/hooks/useDocument.d.ts +22 -0
  143. package/dist/space-sdk/src/hooks/useDocuments.d.ts +31 -0
  144. package/dist/space-sdk/src/hooks/useHandover.d.ts +43 -0
  145. package/dist/space-sdk/src/hooks/useLongPressSelection.d.ts +9 -0
  146. package/dist/space-sdk/src/hooks/useMarqueeSelection.d.ts +57 -0
  147. package/dist/space-sdk/src/hooks/useMultiSelect.d.ts +51 -0
  148. package/dist/space-sdk/src/hooks/useNativeDocumentBody.d.ts +24 -0
  149. package/dist/space-sdk/src/hooks/useNativeDocuments.d.ts +55 -0
  150. package/dist/space-sdk/src/hooks/useNotis.d.ts +25 -0
  151. package/dist/space-sdk/src/hooks/useNotisNavigation.d.ts +33 -0
  152. package/dist/space-sdk/src/hooks/useQuery.d.ts +22 -0
  153. package/dist/space-sdk/src/hooks/useTool.d.ts +20 -0
  154. package/dist/space-sdk/src/hooks/useToolQuery.d.ts +3 -0
  155. package/dist/space-sdk/src/hooks/useTools.d.ts +15 -0
  156. package/dist/space-sdk/src/hooks/useTopBarSearch.d.ts +48 -0
  157. package/dist/space-sdk/src/hooks/useTopBarTitleLeaf.d.ts +24 -0
  158. package/dist/space-sdk/src/hooks/useUpsertDocument.d.ts +27 -0
  159. package/dist/space-sdk/src/hooks/useViewParams.d.ts +76 -0
  160. package/dist/space-sdk/src/hooks/useViewerReads.d.ts +38 -0
  161. package/dist/space-sdk/src/index.d.ts +98 -0
  162. package/dist/space-sdk/src/interactions/actions.d.ts +33 -0
  163. package/dist/space-sdk/src/interactions/shortcuts.d.ts +42 -0
  164. package/dist/space-sdk/src/interactions/visibility.d.ts +2 -0
  165. package/dist/space-sdk/src/nativeData.d.ts +66 -0
  166. package/dist/space-sdk/src/provider.d.ts +17 -0
  167. package/dist/space-sdk/src/queryCache.d.ts +45 -0
  168. package/dist/space-sdk/src/queryErrors.ts +51 -0
  169. package/dist/space-sdk/src/runtime.d.ts +615 -0
  170. package/dist/space-sdk/src/runtimeModules.ts +33 -0
  171. package/dist/space-sdk/src/space.ts +442 -0
  172. package/dist/space-sdk/src/standardViewParams.d.ts +15 -0
  173. package/dist/space-sdk/src/tailwind-candidates.d.ts +3418 -0
  174. package/dist/space-sdk/src/tailwind.ts +66 -0
  175. package/dist/space-sdk/src/ui/actions/ActionToast.d.ts +15 -0
  176. package/dist/space-sdk/src/ui/actions/actionWords.d.ts +16 -0
  177. package/dist/space-sdk/src/ui/actions/index.d.ts +7 -0
  178. package/dist/space-sdk/src/ui/actions/useActions.d.ts +69 -0
  179. package/dist/space-sdk/src/ui/actions/useToast.d.ts +41 -0
  180. package/dist/space-sdk/src/ui/blocks/Tabs.d.ts +139 -0
  181. package/dist/space-sdk/src/ui/blocks/blocks.d.ts +217 -0
  182. package/dist/space-sdk/src/ui/blocks/index.d.ts +4 -0
  183. package/dist/space-sdk/src/ui/board/BoardView.d.ts +36 -0
  184. package/dist/space-sdk/src/ui/board/boardModel.d.ts +46 -0
  185. package/dist/space-sdk/src/ui/board/boardWords.d.ts +9 -0
  186. package/dist/space-sdk/src/ui/board/index.d.ts +5 -0
  187. package/dist/space-sdk/src/ui/calendar/CalendarView.d.ts +49 -0
  188. package/dist/space-sdk/src/ui/calendar/DayPicker.d.ts +23 -0
  189. package/dist/space-sdk/src/ui/calendar/calendarMath.d.ts +81 -0
  190. package/dist/space-sdk/src/ui/charts/Chart.d.ts +203 -0
  191. package/dist/space-sdk/src/ui/charts/ChartTable.d.ts +37 -0
  192. package/dist/space-sdk/src/ui/charts/RenderChartData.d.ts +16 -0
  193. package/dist/space-sdk/src/ui/charts/StatRow.d.ts +59 -0
  194. package/dist/space-sdk/src/ui/charts/chartData.d.ts +84 -0
  195. package/dist/space-sdk/src/ui/comments/CommentPane.d.ts +58 -0
  196. package/dist/space-sdk/src/ui/comments/commentModel.d.ts +157 -0
  197. package/dist/space-sdk/src/ui/comments/commentWords.d.ts +60 -0
  198. package/dist/space-sdk/src/ui/comments/index.d.ts +4 -0
  199. package/dist/space-sdk/src/ui/comments/useComments.d.ts +146 -0
  200. package/dist/space-sdk/src/ui/engine/HelpSheet.d.ts +12 -0
  201. package/dist/space-sdk/src/ui/engine/KeyboardEngine.d.ts +27 -0
  202. package/dist/space-sdk/src/ui/engine/core.d.ts +73 -0
  203. package/dist/space-sdk/src/ui/engine/hooks.d.ts +64 -0
  204. package/dist/space-sdk/src/ui/engine/index.d.ts +14 -0
  205. package/dist/space-sdk/src/ui/engine/instance.d.ts +21 -0
  206. package/dist/space-sdk/src/ui/engine/keycapContext.d.ts +2 -0
  207. package/dist/space-sdk/src/ui/engine/registry.d.ts +77 -0
  208. package/dist/space-sdk/src/ui/engine/step.d.ts +11 -0
  209. package/dist/space-sdk/src/ui/engine/types.d.ts +54 -0
  210. package/dist/space-sdk/src/ui/fields/fieldWords.d.ts +56 -0
  211. package/dist/space-sdk/src/ui/fields/fields.d.ts +162 -0
  212. package/dist/space-sdk/src/ui/fields/index.d.ts +5 -0
  213. package/dist/space-sdk/src/ui/fields/useFieldDraft.d.ts +22 -0
  214. package/dist/space-sdk/src/ui/gallery/AttachmentGallery.d.ts +53 -0
  215. package/dist/space-sdk/src/ui/gallery/ImageViewer.d.ts +27 -0
  216. package/dist/space-sdk/src/ui/gallery/galleryWords.d.ts +95 -0
  217. package/dist/space-sdk/src/ui/gallery/index.d.ts +5 -0
  218. package/dist/space-sdk/src/ui/icons.d.ts +13 -0
  219. package/dist/space-sdk/src/ui/index.d.ts +54 -0
  220. package/dist/space-sdk/src/ui/keycap/Keycap.d.ts +22 -0
  221. package/dist/space-sdk/src/ui/keycap/index.d.ts +2 -0
  222. package/dist/space-sdk/src/ui/layout/Panels.d.ts +50 -0
  223. package/dist/space-sdk/src/ui/layout/chatPanelEdge.d.ts +77 -0
  224. package/dist/space-sdk/src/ui/layout/index.d.ts +5 -0
  225. package/dist/space-sdk/src/ui/layout/panelMath.d.ts +91 -0
  226. package/dist/space-sdk/src/ui/list/ListReader.d.ts +380 -0
  227. package/dist/space-sdk/src/ui/list/Row.d.ts +145 -0
  228. package/dist/space-sdk/src/ui/list/SelectionBar.d.ts +65 -0
  229. package/dist/space-sdk/src/ui/list/index.d.ts +14 -0
  230. package/dist/space-sdk/src/ui/list/rowState.d.ts +22 -0
  231. package/dist/space-sdk/src/ui/list/useListKeys.d.ts +89 -0
  232. package/dist/space-sdk/src/ui/list/useReorder.d.ts +86 -0
  233. package/dist/space-sdk/src/ui/list/useSelection.d.ts +90 -0
  234. package/dist/space-sdk/src/ui/open/OpenAsMenu.d.ts +15 -0
  235. package/dist/space-sdk/src/ui/open/index.d.ts +6 -0
  236. package/dist/space-sdk/src/ui/open/openModeStore.d.ts +39 -0
  237. package/dist/space-sdk/src/ui/open/useOpenMode.d.ts +17 -0
  238. package/dist/space-sdk/src/ui/overlay/Menu.d.ts +88 -0
  239. package/dist/space-sdk/src/ui/overlay/Popover.d.ts +28 -0
  240. package/dist/space-sdk/src/ui/overlay/Tooltip.d.ts +19 -0
  241. package/dist/space-sdk/src/ui/overlay/index.d.ts +9 -0
  242. package/dist/space-sdk/src/ui/overlay/layerRoot.d.ts +24 -0
  243. package/dist/space-sdk/src/ui/overlay/position.d.ts +31 -0
  244. package/dist/space-sdk/src/ui/period/PeriodPicker.d.ts +30 -0
  245. package/dist/space-sdk/src/ui/period/dateFormat.d.ts +50 -0
  246. package/dist/space-sdk/src/ui/period/dateWords.d.ts +88 -0
  247. package/dist/space-sdk/src/ui/period/periods.d.ts +70 -0
  248. package/dist/space-sdk/src/ui/period/usePeriod.d.ts +23 -0
  249. package/dist/space-sdk/src/ui/present/index.d.ts +3 -0
  250. package/dist/space-sdk/src/ui/present/presentWords.d.ts +27 -0
  251. package/dist/space-sdk/src/ui/present/screens.d.ts +36 -0
  252. package/dist/space-sdk/src/ui/present/useAudienceScreen.d.ts +38 -0
  253. package/dist/space-sdk/src/ui/styles.d.ts +21 -0
  254. package/dist/space-sdk/src/ui/timeline/TimelineView.d.ts +44 -0
  255. package/dist/space-sdk/src/ui/timeline/timelineMath.d.ts +123 -0
  256. package/dist/space-sdk/src/viewPeriod.d.ts +8 -0
  257. package/dist/space-sdk/src/vite.ts +92 -0
  258. package/dist/space-sdk/src/words/actions.ts +295 -0
  259. package/dist/space-sdk/src/words/clashes.ts +158 -0
  260. package/dist/space-sdk/src/words/format.ts +136 -0
  261. package/dist/space-sdk/src/words/index.ts +13 -0
  262. package/dist/space-sdk/src/words/keymap.ts +72 -0
  263. package/dist/space-sdk/src/words/keys.ts +297 -0
  264. package/dist/space-sdk/src/words/shell.ts +46 -0
  265. package/dist/space-sdk/src/words/types.ts +65 -0
  266. package/dist/view-renderer/dist/host.css +1 -0
  267. package/dist/view-renderer/dist/host.js +2374 -0
  268. package/dist/view-renderer/dist/theme.css +73 -0
  269. package/dist/view-renderer/src/dom.js +212 -0
  270. package/dist/view-renderer/src/errors.js +8 -0
  271. package/dist/view-renderer/src/host.tsx +250 -0
  272. package/dist/view-renderer/src/index.js +4 -0
  273. package/dist/view-renderer/src/png.js +213 -0
  274. package/dist/view-renderer/src/pool.js +110 -0
  275. package/dist/view-renderer/src/remote-broker.js +59 -0
  276. package/dist/view-renderer/src/renderer.js +271 -0
  277. package/package.json +21 -11
  278. package/skills/notis-cli/AGENT_INSTRUCTIONS.md +63 -12
  279. package/skills/notis-onboarding/BRIEF.md +14 -9
  280. package/skills/notis-query/cli.md +1 -1
  281. package/skills/notis-spaces/cli.md +540 -0
  282. package/src/cli.js +8 -8
  283. package/src/command-specs/agents.js +121 -8
  284. package/src/command-specs/auth.js +3 -3
  285. package/src/command-specs/diagnostics.js +97 -20
  286. package/src/command-specs/documents.js +158 -0
  287. package/src/command-specs/files.js +29 -0
  288. package/src/command-specs/handover.js +331 -79
  289. package/src/command-specs/helpers.js +50 -40
  290. package/src/command-specs/index.js +14 -5
  291. package/src/command-specs/meta.js +38 -9
  292. package/src/command-specs/onboarding.js +1 -1
  293. package/src/command-specs/profile.js +42 -1
  294. package/src/command-specs/skills.js +314 -9
  295. package/src/command-specs/smoke.js +16 -4
  296. package/src/command-specs/spaces.js +783 -0
  297. package/src/command-specs/tools.js +118 -21
  298. package/src/command-specs/version-history.js +20 -0
  299. package/src/command-specs/views.js +69 -0
  300. package/src/runtime/agent-browser.js +178 -267
  301. package/src/runtime/agent-setup.js +172 -13
  302. package/src/runtime/app-boundary-validator.js +338 -2
  303. package/src/runtime/app-data-literals.js +82 -0
  304. package/src/runtime/app-platform.js +750 -1570
  305. package/src/runtime/auth-recovery.js +17 -0
  306. package/src/runtime/base-skills.d.ts +11 -0
  307. package/src/runtime/base-skills.js +100 -3
  308. package/src/runtime/computer-identity.d.ts +30 -0
  309. package/src/runtime/computer-identity.js +151 -0
  310. package/src/runtime/config-lock.js +135 -0
  311. package/src/runtime/confined-space-build.js +74 -0
  312. package/src/runtime/confined-space-verify.js +49 -0
  313. package/src/runtime/delegated-context.js +5 -21
  314. package/src/runtime/native-record-files.js +47 -0
  315. package/src/runtime/oauth.js +342 -31
  316. package/src/runtime/profiles.js +19 -112
  317. package/src/runtime/retired-sdk-files.js +24 -0
  318. package/src/runtime/retry.js +150 -0
  319. package/src/runtime/skill-sync/cloud-client.ts +33 -8
  320. package/src/runtime/skill-sync/index.ts +443 -106
  321. package/src/runtime/skill-sync/local-scanner.ts +248 -32
  322. package/src/runtime/skill-sync/native-identity.ts +102 -0
  323. package/src/runtime/skill-sync/native-plan.ts +70 -0
  324. package/src/runtime/skill-sync/symlink-manager.ts +66 -16
  325. package/src/runtime/skill-sync/sync-plan.ts +34 -2
  326. package/src/runtime/skill-sync/types.ts +54 -0
  327. package/src/runtime/skill-sync/write-cloud-skill.ts +4 -4
  328. package/src/runtime/skill-sync-service.js +109 -0
  329. package/src/runtime/source-root.js +32 -0
  330. package/src/runtime/space-collection-scaffold.js +107 -0
  331. package/src/runtime/space-database-structures.js +229 -0
  332. package/src/runtime/space-fixture-broker.js +68 -0
  333. package/src/runtime/space-lock.js +337 -0
  334. package/src/runtime/space-platform.js +133 -0
  335. package/src/runtime/space-source.js +290 -0
  336. package/src/runtime/space-test-server.js +354 -0
  337. package/src/runtime/space-verification.js +94 -0
  338. package/src/runtime/space-verify-browser-worker.js +51 -0
  339. package/src/runtime/space-view-manifest.js +368 -0
  340. package/src/runtime/sync-skills.d.ts +5 -1
  341. package/src/runtime/sync-skills.js +11 -2
  342. package/src/runtime/tool-confirmation.js +138 -0
  343. package/src/runtime/transport.js +118 -24
  344. package/src/runtime/upsell.js +219 -0
  345. package/src/runtime/view-render.js +49 -0
  346. package/src/skill-sync-worker-entry.js +2 -0
  347. package/src/skill-sync-worker.js +114 -0
  348. package/src/space-harness-host.ts +122 -0
  349. package/template/.harness/index.html.tmpl +9 -352
  350. package/template/app/globals.css +29 -3
  351. package/template/app/layout.tsx +2 -6
  352. package/template/components/ui/badge.tsx +7 -4
  353. package/template/components/ui/button.tsx +1 -1
  354. package/template/components/ui/native-select.tsx +24 -0
  355. package/template/notis.config.ts +4 -36
  356. package/template/package-lock.json +1800 -1867
  357. package/template/package.json +16 -14
  358. package/template/postcss.config.mjs +1 -1
  359. package/template/tailwind.config.ts +1 -6
  360. package/template/tsconfig.json +1 -0
  361. package/template/vite.config.ts +2 -2
  362. package/dist/base-skills/notis-apps/SKILL.md +0 -613
  363. package/skills/notis-apps/cli.md +0 -285
  364. package/src/command-specs/apps.js +0 -3011
  365. package/src/runtime/app-changelog.js +0 -79
  366. package/src/runtime/app-dev-consumers.js +0 -154
  367. package/src/runtime/app-dev-host-lock.js +0 -80
  368. package/src/runtime/app-dev-process-identity.js +0 -111
  369. package/src/runtime/app-dev-roots.js +0 -284
  370. package/src/runtime/app-dev-server.js +0 -1140
  371. package/src/runtime/app-dev-sessions.js +0 -185
  372. package/src/runtime/app-registry-scaffolds.js +0 -367
  373. package/src/runtime/assets/store-screenshot-dark.png +0 -0
  374. package/src/runtime/cli-mode.generated.js +0 -5
  375. package/src/runtime/cli-mode.js +0 -34
  376. package/src/runtime/store-screenshot.js +0 -143
  377. package/template/CHANGELOG.md +0 -5
  378. package/template/app/page.tsx +0 -60
  379. package/template/components/ui/card.tsx +0 -56
  380. package/template/packages/sdk/src/components/DocumentEditor.tsx +0 -103
  381. package/template/packages/sdk/src/config.ts +0 -259
  382. package/template/packages/sdk/src/hooks/useCloudComputer.ts +0 -97
  383. package/template/packages/sdk/src/hooks/useDatabaseSchema.ts +0 -85
  384. package/template/packages/sdk/src/hooks/useDocument.ts +0 -78
  385. package/template/packages/sdk/src/hooks/useNotisNavigation.ts +0 -52
  386. package/template/packages/sdk/src/hooks/useTopBarSearch.ts +0 -73
  387. package/template/packages/sdk/src/runtime.ts +0 -439
  388. package/template/packages/sdk/src/vite.ts +0 -56
  389. package/template/packages/sdk/tsconfig.json +0 -15
  390. /package/{template/packages → dist}/sdk/src/components/MultiSelectDragOverlay.tsx +0 -0
  391. /package/{template/packages → dist}/sdk/src/components/ShortcutHints.tsx +0 -0
  392. /package/{template/packages → dist}/sdk/src/hooks/useActiveResource.ts +0 -0
  393. /package/{template/packages → dist}/sdk/src/hooks/useTool.ts +0 -0
  394. /package/{template/packages → dist}/sdk/src/hooks/useTools.ts +0 -0
  395. /package/{template/packages → dist}/sdk/src/ui.ts +0 -0
@@ -0,0 +1,783 @@
1
+ import { scaffoldSpaceCollection } from '../runtime/space-collection-scaffold.js';
2
+ import { appFilesDigest, pullAppSource, resolveProjectDir, saveSpaceVerification, spacePublicationIntent, syncSpaceSdk } from '../runtime/app-platform.js';
3
+ import { buildSpaceArtifact, prepareSpaceRelease } from '../runtime/space-platform.js';
4
+ import { verifySpaceRelease } from '../runtime/space-verification.js';
5
+ import { loadSelectedSpace } from '../runtime/space-source.js';
6
+ import { refreshSpaceAuthoring } from '../runtime/space-database-structures.js';
7
+ import { RESOURCES_FILE, SKILLS_DIR, applyPublication, collectSkillFolders, computeSpaceSourceChanges, describeAppliedChanges,
8
+ lockAfterPull, readResourceList, readSpaceLock, resourceEntries, skillFolderFiles, writeResourceList, writeSkillFolder, writeSpaceLock, isFolderAlias } from '../runtime/space-lock.js';
9
+ import { CliError, usageError } from '../runtime/errors.js';
10
+ import { existsSync, readFileSync } from 'node:fs';
11
+ import { join, resolve } from 'node:path';
12
+ import { httpRequest } from '../runtime/transport.js';
13
+ import { assertSourceRoot } from '../runtime/source-root.js';
14
+ import { parseJson } from './helpers.js';
15
+ import { unifiedDiffSummary, versionName } from './version-history.js';
16
+ import { createHash } from 'node:crypto';
17
+ import { viewRenderCommandSpec } from './views.js';
18
+
19
+ function selected(ctx) {
20
+ if (!ctx.options.space) throw usageError('Choose the independent Space with --space <key>.');
21
+ // build, verify, preview, deploy and inspect write or stage inside this folder.
22
+ return { directory: assertSourceRoot(resolveProjectDir(ctx.args.dir || '.')), key: ctx.options.space };
23
+ }
24
+
25
+ function requestId(ctx) {
26
+ const value = ctx.options.requestId || ctx.globalOptions.idempotencyKey;
27
+ if (!value || value.length > 200) throw usageError('Pass --request-id <stable-key>; keep it unchanged when retrying this intent.');
28
+ return value;
29
+ }
30
+
31
+ function revision(ctx) {
32
+ const value = Number(ctx.options.revision);
33
+ if (!Number.isSafeInteger(value) || value < 1) throw usageError('Pass the published --revision <number> from spaces get.');
34
+ return value;
35
+ }
36
+
37
+ function schemaRevision(ctx) {
38
+ if (ctx.options.schemaRevision === undefined) return undefined;
39
+ const value = Number(ctx.options.schemaRevision);
40
+ if (!Number.isSafeInteger(value) || value < 0) throw usageError('Pass the exact --schema-revision <number> returned by the native schema or query.');
41
+ return value;
42
+ }
43
+
44
+ function object(value, label) {
45
+ const parsed = parseJson(value, label);
46
+ if (!parsed || typeof parsed !== 'object' || Array.isArray(parsed)) throw usageError(`${label} must be a JSON object.`);
47
+ return parsed;
48
+ }
49
+
50
+ async function remote(ctx, operation, { method = 'POST', body, query } = {}) {
51
+ const suffix = query ? `?${new URLSearchParams(Object.entries(query).filter(([, value]) => value != null))}` : '';
52
+ const result = await httpRequest({ runtime: ctx.runtime, method, path: `/portal_spaces/${operation}${suffix}`, body, spacesProtocol: true });
53
+ return ctx.output.emitSuccess({ command: ctx.spec.command_path.join(' '), data: result.payload,
54
+ humanSummary: JSON.stringify(result.payload, null, 2), meta: { mutating: method !== 'GET' && !body?.dry_run && ctx.spec.mutates !== false, request_id: result.requestId } });
55
+ }
56
+
57
+ /**
58
+ * What changed since the pull in `skills/` and `resources.json` (R5). Without a
59
+ * lock file the workspace was authored, not pulled: those files are ignored and
60
+ * the caller is told so. `recorded` replaces the lock and list on disk with the
61
+ * ones a deploy intent was first made from (its Skill folders are still read).
62
+ */
63
+ function localSpaceChanges(directory, spaceId, manifest, recorded = null) {
64
+ const lock = recorded ? recorded.lock : readSpaceLock(directory);
65
+ const folders = collectSkillFolders(directory);
66
+ const resources = recorded ? recorded.resources ?? null : readResourceList(directory);
67
+ if (!lock) {
68
+ const warnings = Object.keys(folders).length || resources
69
+ ? [`${SKILLS_DIR}/ and ${RESOURCES_FILE} are deployed only from a pulled Space (notis spaces pull); this workspace has no .notis/space-lock.json, so they were ignored.`]
70
+ : [];
71
+ return { lock: null, folders, changes: null, summary: null, fingerprint: null, warnings };
72
+ }
73
+ if (lock.space_id !== spaceId) {
74
+ throw usageError(`This workspace was pulled from Space ${lock.space_id}. Deploy it with --space-id ${lock.space_id}, or pull the other Space into another directory.`);
75
+ }
76
+ return { lock, resources, folders, ...computeSpaceSourceChanges({ lock, resources, folders, manifest }), warnings: [] };
77
+ }
78
+
79
+ /**
80
+ * The local changes a deploy intent stands for. A publication this request ID
81
+ * completed rewrote the lock and list (recordPublication), so the identical
82
+ * command finds nothing left to deploy against them: an untouched workspace is
83
+ * then checked against the lock and list the intent was first made from, and
84
+ * replays the same release. Anything edited since is still a different intent.
85
+ */
86
+ function intentChanges(directory, spaceId, manifest, local, intent) {
87
+ if (!intent?.local_state || local.fingerprint !== null
88
+ || (intent.local_changes_fingerprint ?? null) === local.fingerprint) return local;
89
+ const original = localSpaceChanges(directory, spaceId, manifest, intent.local_state);
90
+ return (original.fingerprint ?? null) === (intent.local_changes_fingerprint ?? null) ? original : local;
91
+ }
92
+
93
+ /** After a publication, the lock and list record the applied versions and the Space's current links. */
94
+ function recordPublication(directory, local, result) {
95
+ if (!local?.lock || !result || result.published_revision === undefined) return;
96
+ const updated = applyPublication({ lock: local.lock, folders: local.folders, result });
97
+ writeSpaceLock(directory, updated.lock);
98
+ if (updated.resources) writeResourceList(directory, updated.resources);
99
+ }
100
+
101
+ /** Reviewable: a page with viewer reads shows each signed-in viewer what they can open, with their own access. */
102
+ function describeViewerReads(capabilities) {
103
+ const families = capabilities?.viewerReads;
104
+ return Array.isArray(families) && families.length
105
+ ? ` Viewer reads: this page lists the ${families.join(' and ')} each signed-in viewer can open, with that viewer's own access (never on a Site).`
106
+ : '';
107
+ }
108
+
109
+ /** Reviewable: a page that uses the cloud computer runs on each viewer's own one, after that viewer allows it. */
110
+ function describeCloudComputer(capabilities) {
111
+ const level = capabilities?.cloudComputer;
112
+ if (level === 'shell') return " Cloud computer: this page runs commands and writes files on each signed-in viewer's own cloud computer, once that viewer allows it (never on a Site).";
113
+ if (level === 'read') return " Cloud computer: this page reads whether each signed-in viewer's own cloud computer is running and signed in to GitHub, once that viewer allows it.";
114
+ return '';
115
+ }
116
+
117
+ function describePendingChanges(summary) {
118
+ if (!summary) return '';
119
+ const parts = [];
120
+ if (summary.skills.edited.length) parts.push(`Skills to update: ${summary.skills.edited.join(', ')}`);
121
+ if (summary.skills.created.length) parts.push(`Skills to create: ${summary.skills.created.join(', ')}`);
122
+ if (summary.links.added.length) parts.push(`Links to add: ${summary.links.added.join(', ')}`);
123
+ if (summary.links.removed.length) parts.push(`Links to remove: ${summary.links.removed.join(', ')}`);
124
+ return parts.length ? ` ${parts.join('. ')}.` : '';
125
+ }
126
+
127
+ async function sourceRelease(ctx, publish, stageOnly = false) {
128
+ const { directory, key } = selected(ctx);
129
+ if (!ctx.options.spaceId) throw usageError('Pass --space-id <id> for the existing Space.');
130
+ const intentKey = publish ? requestId(ctx) : null;
131
+ const release = await prepareSpaceRelease(directory, key);
132
+ try {
133
+ const identity = [ctx.runtime.apiBase.replace(/\/$/, ''), ctx.runtime.profileName, key, ctx.options.spaceId, intentKey];
134
+ let verification = publish && !ctx.options.dryRun ? spacePublicationIntent(directory, identity) : null;
135
+ const local = intentChanges(directory, ctx.options.spaceId, release.manifest,
136
+ localSpaceChanges(directory, ctx.options.spaceId, release.manifest), verification);
137
+ const body = { space_id: ctx.options.spaceId, manifest: release.manifest, source_files: release.sourceFiles,
138
+ artifact_files: release.files, reuse_grants: object(ctx.options.reuseGrants || '{}', '--reuse-grants'),
139
+ ...(ctx.options.reuseGrantsOnly ? { authorization_mode: 'reuse_only' } : {}),
140
+ ...(local.changes ? { changes: local.changes } : {}) };
141
+ const sourceDigest = appFilesDigest(release.sourceFiles), artifactDigest = appFilesDigest(release.files);
142
+ const call = (operation, data) => httpRequest({ runtime: { ...ctx.runtime, timeoutMs: Math.max(ctx.runtime.timeoutMs || 0, 600_000) },
143
+ method: 'POST', path: `/portal_spaces/${operation}`, body: data, spacesProtocol: true });
144
+ if (verification && (verification.local_changes_fingerprint ?? null) !== local.fingerprint) {
145
+ throw usageError('This request ID belongs to different Skill folders or list entries. Restore the state you first deployed to retry, or use a new request ID for a new release.');
146
+ }
147
+ if (!verification) {
148
+ verification = (await call('source-verify', body)).payload;
149
+ }
150
+ if (verification?.valid !== true || verification.space_id !== body.space_id || verification.source_digest !== sourceDigest
151
+ || verification.artifact_digest !== artifactDigest || JSON.stringify(Object.entries(verification.reuse_grants || {}).sort())
152
+ !== JSON.stringify(Object.entries(body.reuse_grants).sort())
153
+ || (verification.authorization_mode ?? 'authorize_missing') !== (body.authorization_mode ?? 'authorize_missing')) {
154
+ throw usageError('This request ID belongs to different source or grant choices. Keep the original bytes to retry, or use a new request ID for a new release.');
155
+ }
156
+ const controller = new AbortController();
157
+ const cancel = () => controller.abort();
158
+ process.once('SIGINT', cancel); process.once('SIGTERM', cancel);
159
+ let render;
160
+ try { render = await verifySpaceRelease(release, { capabilities: verification.capabilities, signal: controller.signal,
161
+ authorizationMode: body.authorization_mode, unavailableActions: verification.unavailable_actions }); }
162
+ finally { process.removeListener('SIGINT', cancel); process.removeListener('SIGTERM', cancel); }
163
+ const diagnosticPath = saveSpaceVerification(directory, { ...render, space_id: body.space_id });
164
+ if (!['passed', 'not_applicable_container'].includes(render.status)) {
165
+ const reason = render.errors?.find(value => value.phase === 'fixture')?.message;
166
+ throw usageError(`The built Space did not pass offline render verification. Nothing was published.${reason ? ` ${reason}` : ''}`,
167
+ { render, diagnostic_path: diagnosticPath });
168
+ }
169
+ if (publish && !ctx.options.dryRun) {
170
+ // The retained intent pins the local change set next to the server verification, with the
171
+ // lock and list it was computed from: the lock and list this publication then rewrites can
172
+ // never turn the identical command into a different intent.
173
+ verification = spacePublicationIntent(directory, identity, { ...verification, local_changes_fingerprint: local.fingerprint,
174
+ local_state: local.lock ? { lock: local.lock, resources: local.resources ?? null } : null });
175
+ }
176
+ const { local_changes_fingerprint: _fingerprint, local_state: _state, ...serverVerification } = verification;
177
+ // Close/check the pinned local snapshot before the first remote mutation.
178
+ // The upload uses the captured bytes, not mutable working-tree paths.
179
+ release.close();
180
+ const result = publish && !ctx.options.dryRun
181
+ ? (await call(stageOnly ? 'source-stage' : 'source-publish', { ...body, verification: serverVerification, request_id: intentKey })).payload
182
+ : serverVerification;
183
+ if (publish && !ctx.options.dryRun && !stageOnly) recordPublication(directory, local, result);
184
+ const data = { ...result, render, diagnostic_path: diagnosticPath, ...(local.summary ? { local_changes: local.summary } : {}) };
185
+ return ctx.output.emitSuccess({ command: ctx.spec.command_path.join(' '), data,
186
+ humanSummary: publish && !ctx.options.dryRun
187
+ ? stageOnly ? `Prepared ${key} as revision ${result.candidate_revision}. ${result.preview_url || 'This container has no executable preview.'} Live source was not changed.${describePendingChanges(local.summary).replace(/\.$/, '; they go live with notis spaces promote.')}`
188
+ : `Published ${key} as revision ${result.published_revision}${result.replayed ? ' (confirmed retry)' : ''}.${describeAppliedChanges(result)}${describeViewerReads(serverVerification.capabilities)}${describeCloudComputer(serverVerification.capabilities)}`
189
+ : `Verified ${key}'s frozen source${render.status === 'passed' ? ', rendered fixtures' : ' (container)'} and current authorizations; nothing was published.${describePendingChanges(local.summary)}${describeViewerReads(serverVerification.capabilities)}${describeCloudComputer(serverVerification.capabilities)}`,
190
+ warnings: local.warnings, meta: { mutating: publish && !ctx.options.dryRun } });
191
+ } finally { release.close(); }
192
+ }
193
+
194
+ /** Pull the Space's links as its resource list and every editable linked Skill as a folder (R5). */
195
+ async function materializeSpaceLinks(ctx, { projectDir, spaceId, sourceRevision }) {
196
+ const sourceSkillFolders = collectSkillFolders(projectDir);
197
+ const query = new URLSearchParams({ space_id: spaceId });
198
+ const links = (await httpRequest({ runtime: ctx.runtime, method: 'GET', path: `/portal_spaces/space-links?${query}`, spacesProtocol: true })).payload?.links;
199
+ const list = Array.isArray(links) ? links : [];
200
+ const skills = {}, skipped = [];
201
+ for (const link of list.filter((entry) => entry?.kind === 'skill')) {
202
+ if (link.available === false) { skipped.push({ alias: link.alias, reason: 'unavailable' }); continue; }
203
+ if (!isFolderAlias(link.alias)) {
204
+ // A staged `skill:<id>` alias cannot name a folder: rename the link first to edit the Skill here.
205
+ skipped.push({ alias: link.alias, reason: `rename with notis spaces resources name ${spaceId} ${link.binding_id}` }); continue;
206
+ }
207
+ let read;
208
+ try {
209
+ read = (await httpRequest({ runtime: ctx.runtime, method: 'POST', path: '/portal_skills/native-authoring', spacesProtocol: true,
210
+ body: { operation: 'read', target: { space_id: spaceId, binding_id: link.binding_id }, include_files: true } })).payload;
211
+ } catch (error) {
212
+ // A curated or otherwise read-only Skill stays listed without a folder; the deploy never edits it.
213
+ if (error instanceof CliError && ['forbidden', 'conflict', 'usage_error'].includes(error.code)) {
214
+ skipped.push({ alias: link.alias, reason: 'read-only' }); continue;
215
+ }
216
+ throw error;
217
+ }
218
+ const files = read?.definition?.files;
219
+ if (!files || typeof files !== 'object' || typeof files['SKILL.md'] !== 'string' || read.skill_id !== link.resource_id
220
+ || read.target?.binding_id !== link.binding_id || !read.version) {
221
+ throw usageError(`The server did not return the verified files of Skill ${link.alias}.`);
222
+ }
223
+ // Hash exactly what the folder keeps, so an untouched folder never reads as an edit (bytecode caches are not kept).
224
+ const kept = skillFolderFiles(files);
225
+ writeSkillFolder(projectDir, link.alias, kept, { sourceSnapshot: sourceSkillFolders[link.alias]?.files });
226
+ skills[link.alias] = { skill_id: read.skill_id, binding_id: read.target.binding_id, binding_revision: read.target.binding_revision,
227
+ version: read.version, folder_hash: appFilesDigest(kept) };
228
+ }
229
+ writeResourceList(projectDir, resourceEntries(list));
230
+ writeSpaceLock(projectDir, lockAfterPull({ spaceId, apiBase: ctx.runtime.apiBase.replace(/\/$/, ''), sourceRevision, links: list, skills }));
231
+ return { links: resourceEntries(list), skills: Object.keys(skills).sort(), skipped };
232
+ }
233
+
234
+ /**
235
+ * A pulled Space runs `npm ci` and `npm test` at once: its packages/sdk becomes
236
+ * this CLI's Space SDK, with the Node test runtime in dist/ that no upload
237
+ * carries, and package-lock.json follows it. The source is already written,
238
+ * so a failure here is a warning; every build refreshes the SDK again.
239
+ */
240
+ function refreshPulledSdk(projectDir) {
241
+ if (!existsSync(join(projectDir, 'packages', 'sdk', 'package.json'))) return { sdk: null, notes: [], warnings: [] };
242
+ const messages = [];
243
+ let sdk = null;
244
+ try {
245
+ const synced = syncSpaceSdk(projectDir, { log: (message) => messages.push(message) });
246
+ if (!synced.reason) sdk = { version: synced.templateVersion, files_updated: synced.changed.length, lockfile: synced.lockfile };
247
+ } catch (error) {
248
+ sdk = { error: error.message };
249
+ messages.push(`Warning: packages/sdk was not refreshed (${error.message.replace(/\.$/, '')}); the next notis spaces build retries.`);
250
+ }
251
+ const warning = /^Warning: /;
252
+ return { sdk, notes: messages.filter((message) => !warning.test(message)),
253
+ warnings: messages.filter((message) => warning.test(message)).map((message) => message.replace(warning, '')) };
254
+ }
255
+
256
+ const idArgument = { token: '<space-id>', key: 'spaceId', description: 'Exact existing Space ID.' };
257
+ const recordOption = { flags: '--record <record-key>', description: 'Fixed record context, when this is a record-scoped Space.' };
258
+ const requestOption = { flags: '--request-id <key>', description: 'Stable intent key. Reuse after timeouts or lost replies.' };
259
+
260
+ function historyRevision(value, flag) {
261
+ if (typeof value !== 'string' || !/^\d+$/.test(value) || !Number.isSafeInteger(Number(value))) {
262
+ throw usageError(`Pass ${flag} with a saved version number from history list.`);
263
+ }
264
+ return Number(value);
265
+ }
266
+
267
+ export const spacesCommandSpecs = [
268
+ {
269
+ command_path: ['spaces', 'history', 'list'],
270
+ summary: 'Read a Space’s saved view versions, newest first.',
271
+ when_to_use: 'Inspect when and by whom a view changed, before restoring or undoing a change.',
272
+ args_schema: { arguments: [idArgument], options: [{ flags: '--before <revision>', description: 'Read versions before this cursor.' }] },
273
+ examples: ['notis spaces history list <space-id>'], mutates: false, idempotent: true, require_auth: true,
274
+ backend_call: { type: 'http', path: '/portal_spaces/history' },
275
+ handler(ctx) {
276
+ if (ctx.options.before !== undefined && !/^\d+$/.test(ctx.options.before)) throw usageError('Use the returned numeric --before cursor.');
277
+ return remote(ctx, 'history', { method: 'GET', query: { space_id: ctx.args.spaceId, before: ctx.options.before } });
278
+ },
279
+ },
280
+ {
281
+ command_path: ['spaces', 'history', 'restore'],
282
+ summary: 'Republish a saved view and its settings as a new version; records stay unchanged.',
283
+ when_to_use: 'Restore a chosen version, or undo your last change using the version immediately before it.',
284
+ args_schema: { arguments: [idArgument], options: [
285
+ { flags: '--target-revision <revision>', description: 'The saved version to restore.' },
286
+ { flags: '--revision <revision>', description: 'Current Space metadata revision from history list.' },
287
+ { flags: '--request-id <id>', description: 'Stable restore request ID; reuse on retry.' },
288
+ { flags: '--dry-run', description: 'Check current access, bindings and saved source without publishing.' },
289
+ ] }, examples: ['notis spaces history restore <space-id> --target-revision 2 --revision 5 --request-id undo-last-change --dry-run'],
290
+ mutates: true, idempotent: true, require_auth: true,
291
+ backend_call: { type: 'http', path: '/portal_spaces/history-restore' },
292
+ handler(ctx) {
293
+ const target = ctx.options.targetRevision, revision = ctx.options.revision;
294
+ if (![target, revision].every(value => typeof value === 'string' && /^\d+$/.test(value) && Number.isSafeInteger(Number(value)))
295
+ || !ctx.options.requestId) throw usageError('Pass --target-revision, current --revision and a stable --request-id.');
296
+ return remote(ctx, 'history-restore', { body: { space_id: ctx.args.spaceId, target_revision: Number(target),
297
+ revision: Number(revision), request_id: ctx.options.requestId, dry_run: Boolean(ctx.options.dryRun) } });
298
+ },
299
+ },
300
+ {
301
+ command_path: ['spaces', 'history', 'read'],
302
+ summary: 'Read the files of one saved Space version.',
303
+ when_to_use: 'See exactly what a saved version contained, before comparing or restoring it. Editors only.',
304
+ args_schema: { arguments: [idArgument], options: [
305
+ { flags: '--revision <revision>', description: 'The saved version from history list.' },
306
+ { flags: '--path <path>', description: 'Read only this file or folder.' },
307
+ ] },
308
+ examples: ['notis spaces history read <space-id> --revision 3 --path app/page.tsx'],
309
+ mutates: false, idempotent: true, require_auth: true,
310
+ backend_call: { type: 'http', path: '/portal_spaces/history-read' },
311
+ handler(ctx) {
312
+ return remote(ctx, 'history-read', { method: 'GET', query: { space_id: ctx.args.spaceId,
313
+ revision: historyRevision(ctx.options.revision, '--revision'), path: ctx.options.path } });
314
+ },
315
+ },
316
+ {
317
+ command_path: ['spaces', 'history', 'diff'],
318
+ summary: 'Compare two saved Space versions, or one with the current version.',
319
+ when_to_use: 'See what changed between versions. Prints the unified diff; pass --json for the full comparison.',
320
+ args_schema: { arguments: [idArgument], options: [
321
+ { flags: '--from <revision>', description: 'The older saved version.' },
322
+ { flags: '--to <revision>', description: 'The newer saved version, or current (default).' },
323
+ { flags: '--path <path>', description: 'Compare only this file or folder.' },
324
+ ] },
325
+ examples: ['notis spaces history diff <space-id> --from 3', 'notis spaces history diff <space-id> --from 3 --to 5 --json'],
326
+ mutates: false, idempotent: true, require_auth: true,
327
+ backend_call: { type: 'http', path: '/portal_spaces/history-diff' },
328
+ async handler(ctx) {
329
+ const to = ctx.options.to === undefined || ctx.options.to === 'current' ? 'current' : historyRevision(ctx.options.to, '--to');
330
+ const query = { space_id: ctx.args.spaceId, from: historyRevision(ctx.options.from, '--from'), to, path: ctx.options.path };
331
+ const suffix = new URLSearchParams(Object.entries(query).filter(([, value]) => value != null));
332
+ const result = await httpRequest({ runtime: ctx.runtime, method: 'GET', path: `/portal_spaces/history-diff?${suffix}`, spacesProtocol: true });
333
+ return ctx.output.emitSuccess({ command: ctx.spec.command_path.join(' '), data: result.payload,
334
+ humanSummary: unifiedDiffSummary(result.payload), meta: { mutating: false, request_id: result.requestId } });
335
+ },
336
+ },
337
+ {
338
+ command_path: ['spaces', 'history', 'name'],
339
+ summary: 'Name a saved Space version, or clear its name.',
340
+ when_to_use: 'Mark a version worth keeping, such as a launch. Named versions are kept past the usual retention.',
341
+ args_schema: { arguments: [idArgument], options: [
342
+ { flags: '--revision <revision>', description: 'The saved version from history list.' },
343
+ { flags: '--name <name>', description: 'The version name.' },
344
+ { flags: '--clear', description: 'Remove the version name.' },
345
+ ] },
346
+ examples: ['notis spaces history name <space-id> --revision 3 --name "Launch"', 'notis spaces history name <space-id> --revision 3 --clear'],
347
+ mutates: true, idempotent: true, require_auth: true,
348
+ backend_call: { type: 'http', path: '/portal_spaces/history-name' },
349
+ handler(ctx) {
350
+ return remote(ctx, 'history-name', { body: { space_id: ctx.args.spaceId,
351
+ revision: historyRevision(ctx.options.revision, '--revision'), name: versionName(ctx.options) } });
352
+ },
353
+ },
354
+ {
355
+ command_path: ['spaces', 'history', 'delete'],
356
+ summary: 'Delete one saved Space version for good.',
357
+ when_to_use: 'Only when the user asked to delete this version. It cannot be restored afterwards.',
358
+ args_schema: { arguments: [idArgument], options: [
359
+ { flags: '--revision <revision>', description: 'The saved version from history list.' },
360
+ { flags: '--yes', description: 'Confirm the user asked to delete this version for good.' },
361
+ ] },
362
+ examples: ['notis spaces history delete <space-id> --revision 3 --yes'],
363
+ mutates: true, idempotent: true, require_auth: true,
364
+ backend_call: { type: 'http', path: '/portal_spaces/history-delete' },
365
+ handler(ctx) {
366
+ const revision = historyRevision(ctx.options.revision, '--revision');
367
+ if (!ctx.options.yes) throw usageError('Deleting a saved version cannot be undone. Only do it when the user asked, then pass --yes.');
368
+ return remote(ctx, 'history-delete', { body: { space_id: ctx.args.spaceId, revision } });
369
+ },
370
+ },
371
+ viewRenderCommandSpec({ screenshotOnly: true }),
372
+ {
373
+ command_path: ['spaces', 'init'], summary: 'Scaffold an editable collection view with first-party record components.',
374
+ when_to_use: 'Start a local Space collection source for an existing linked database. Does not create or deploy remote resources.',
375
+ args_schema: { arguments: [{ token: '<name>', key: 'name', description: 'Space display name.' },
376
+ { token: '[dir]', key: 'dir', description: 'New or empty local source directory.' }], options: [
377
+ { flags: '--database-key <key>', description: 'Portable key of the database this Space links.' },
378
+ { flags: '--path <path>', description: 'Descriptive view path; defaults to records.' },
379
+ { flags: '--title-property <id>', description: 'Canonical title/number property ID, or the title column (default).' },
380
+ ] },
381
+ examples: ['notis spaces init Notes ./notes --database-key notes --path notes'],
382
+ mutates: true, idempotent: false, require_auth: false, backend_call: { type: 'local', name: 'scaffold_space_collection' },
383
+ async handler(ctx) {
384
+ const result = await scaffoldSpaceCollection({ projectDir: resolveProjectDir(ctx.args.dir || ctx.args.name), name: ctx.args.name,
385
+ databaseKey: ctx.options.databaseKey, path: ctx.options.path, titleProperty: ctx.options.titleProperty });
386
+ return ctx.output.emitSuccess({ command: 'spaces init', data: result,
387
+ humanSummary: `Collection source created in ${result.projectDir}. Build with --space collection after linking the declared database.` });
388
+ },
389
+ },
390
+ {
391
+ command_path: ['spaces', 'navigation', 'bind'],
392
+ summary: 'Bind a named destination for the next source publication without granting target access.',
393
+ when_to_use: 'Connect a portable navigation alias to an existing Space or exact record before building and publishing.',
394
+ args_schema: { arguments: [idArgument], options: [
395
+ { flags: '--alias <name>', description: 'Declared portable navigation alias.' },
396
+ { flags: '--target <json>', description: 'Exact {space_id,document_id?}, or null to remove the draft binding.' },
397
+ { flags: '--revision <number>', description: 'Current source Space metadata revision.' },
398
+ { flags: '--dry-run', description: 'Check current source/target access and revision without changing anything.' },
399
+ ] },
400
+ examples: ['notis spaces navigation bind <space-id> --alias history --target \'{"space_id":"<history-id>"}\' --revision 2 --dry-run'],
401
+ mutates: true, idempotent: true, require_auth: true,
402
+ backend_call: { type: 'http', path: '/portal_spaces/navigation-binding' },
403
+ handler(ctx) {
404
+ const expected = ctx.options.revision;
405
+ if (typeof expected !== 'string' || !/^\d+$/.test(expected) || !Number.isSafeInteger(Number(expected))) throw usageError('Pass the current metadata --revision.');
406
+ if (typeof ctx.options.alias !== 'string' || !/^[a-z][a-z0-9_-]{0,99}$/.test(ctx.options.alias)) throw usageError('Pass a stable lowercase --alias.');
407
+ const target = ctx.options.target === 'null' ? null : object(ctx.options.target, '--target');
408
+ return remote(ctx, 'navigation-binding', { body: { space_id: ctx.args.spaceId, alias: ctx.options.alias,
409
+ target, revision: Number(expected), dry_run: Boolean(ctx.options.dryRun) } });
410
+ },
411
+ },
412
+ // Linking a database, Skill or automation into a Space is the resource's own
413
+ // operation (`notis skills links`, LOCAL_NOTIS_UPDATE_AUTOMATION links,
414
+ // LOCAL_NOTIS_DATABASE_UPSERT_DATABASE links); there is no Space-side include.
415
+ {
416
+ command_path: ['spaces','resources','name'],
417
+ summary: 'Give an included resource a portable alias before the first source revision.',
418
+ when_to_use: 'Prepare staged resources for source authoring without replacing their IDs, scope or owners.',
419
+ args_schema: { arguments: [idArgument,{token:'<binding-id>',key:'bindingId',description:'Exact included binding ID from spaces get.'}], options:[
420
+ {flags:'--alias <name>',description:'Stable lowercase alias to use in source.'},
421
+ {flags:'--revision <number>',description:'Current Space metadata revision.'},
422
+ {flags:'--dry-run',description:'Check naming, current access and revision without changing metadata.'},
423
+ ]},
424
+ examples:['notis spaces resources name <space-id> <binding-id> --alias weekly-report --revision 0 --dry-run'],
425
+ mutates:true,idempotent:true,require_auth:true,
426
+ backend_call:{type:'http',path:'/portal_spaces/resource-name'},
427
+ handler(ctx) {
428
+ const expected=ctx.options.revision;
429
+ if(typeof expected!=='string'||!/^\d+$/.test(expected)||!Number.isSafeInteger(Number(expected))) throw usageError('Pass the current --revision from spaces get.');
430
+ if(typeof ctx.options.alias!=='string'||!/^[a-z][a-z0-9_-]{0,99}$/.test(ctx.options.alias)) throw usageError('Pass a stable lowercase --alias.');
431
+ return remote(ctx,'resource-name',{body:{space_id:ctx.args.spaceId,binding_id:ctx.args.bindingId,
432
+ alias:ctx.options.alias,revision:Number(expected),dry_run:Boolean(ctx.options.dryRun)}});
433
+ },
434
+ },
435
+ {
436
+ command_path: ['spaces', 'links', 'list'],
437
+ summary: 'List the databases, Skills, automations and records linked into one Space.',
438
+ when_to_use: 'Find a link binding and its revision before removing it, including a link left behind by a deleted Skill.',
439
+ args_schema: { arguments: [idArgument], options: [] },
440
+ examples: ['notis spaces links list <space-id>'],
441
+ mutates: false, idempotent: true, require_auth: true,
442
+ backend_call: { type: 'http', path: '/portal_spaces/space-links' },
443
+ handler(ctx) {
444
+ return remote(ctx, 'space-links', { method: 'GET', query: { space_id: ctx.args.spaceId } });
445
+ },
446
+ },
447
+ {
448
+ command_path: ['spaces', 'links', 'remove'],
449
+ summary: 'Remove one database, Skill or automation link from a Space by its binding.',
450
+ when_to_use: 'Unlist a resource from a Space you edit, including a link whose Skill was deleted. The resource keeps its ID; removing the last available link of a live resource keeps it as its owner\'s standalone resource.',
451
+ args_schema: { arguments: [idArgument, { token: '<binding-id>', key: 'bindingId', description: 'Exact link binding ID from spaces links list.' }], options: [
452
+ { flags: '--revision <number>', description: 'Current binding revision from spaces links list.' },
453
+ { flags: '--request-id <key>', description: 'Stable intent key; reuse it unchanged when retrying this removal.' },
454
+ ] },
455
+ examples: ['notis spaces links remove <space-id> <binding-id> --revision 1 --request-id remove-link-1'],
456
+ mutates: true, idempotent: true, require_auth: true,
457
+ backend_call: { type: 'http', path: '/portal_spaces/space-link-remove' },
458
+ handler(ctx) {
459
+ const expected = ctx.options.revision;
460
+ if (typeof expected !== 'string' || !/^\d+$/.test(expected) || !Number.isSafeInteger(Number(expected))) throw usageError('Pass the link\'s current --revision from spaces links list.');
461
+ return remote(ctx, 'space-link-remove', { body: { space_id: ctx.args.spaceId, binding_id: ctx.args.bindingId,
462
+ binding_revision: Number(expected), request_id: requestId(ctx) } });
463
+ },
464
+ },
465
+ {
466
+ command_path: ['spaces', 'aliases', 'list'],
467
+ summary: 'List the old App page links you manage and the Space each one opens.',
468
+ when_to_use: 'Find an old App page link that opens a binned Space (trashed: true) before repointing it.',
469
+ args_schema: { arguments: [], options: [
470
+ { flags: '--legacy-id <id>', description: 'Old App ID the links belong to.' },
471
+ { flags: '--kind <kind>', description: 'app (default), report or html.' },
472
+ ] },
473
+ examples: ['notis spaces aliases list --legacy-id <app-id>'],
474
+ mutates: false, idempotent: true, require_auth: true,
475
+ backend_call: { type: 'http', path: '/portal_spaces/legacy-aliases' },
476
+ handler(ctx) {
477
+ if (!ctx.options.legacyId) throw usageError('Pass the old App ID with --legacy-id <id>.');
478
+ return remote(ctx, 'legacy-aliases', { method: 'GET', query: { kind: ctx.options.kind || 'app', legacy_id: ctx.options.legacyId } });
479
+ },
480
+ },
481
+ {
482
+ command_path: ['spaces', 'aliases', 'repoint'],
483
+ summary: 'Point one old App page link at a live Space you edit.',
484
+ when_to_use: 'Repair an old App page link whose Space was binned. You must own the old App or edit the Space the link opens now, and edit the new Space; everyone who opens the link still needs their own access to it.',
485
+ args_schema: { arguments: [], options: [
486
+ { flags: '--legacy-id <id>', description: 'Old App ID the link belongs to.' },
487
+ { flags: '--route <slug>', description: 'Old App page route from spaces aliases list.' },
488
+ { flags: '--space <space-id>', description: 'Live Space the old link should open.' },
489
+ { flags: '--expected-space <space-id>', description: 'Space the link opens now; the change is refused if it moved since you listed it.' },
490
+ { flags: '--selection-param <name>', description: 'Declared view parameter that receives the old page selection, if any.' },
491
+ { flags: '--request-id <key>', description: 'Stable intent key; reuse it unchanged when retrying this change.' },
492
+ { flags: '--dry-run', description: 'Check authority and the new Space without changing the link.' },
493
+ ] },
494
+ examples: ['notis spaces aliases repoint --legacy-id <app-id> --route pages --space <space-id> --expected-space <binned-space-id> --request-id repoint-pages --dry-run'],
495
+ mutates: true, idempotent: true, require_auth: true,
496
+ backend_call: { type: 'http', path: '/portal_spaces/legacy-alias' },
497
+ handler(ctx) {
498
+ if (!ctx.options.legacyId) throw usageError('Pass the old App ID with --legacy-id <id>.');
499
+ if (typeof ctx.options.route !== 'string') throw usageError('Pass the old App page with --route <slug>.');
500
+ if (!ctx.options.space) throw usageError('Pass the Space the old link should open with --space <space-id>.');
501
+ const body = { kind: 'app', legacy_id: ctx.options.legacyId, route_slug: ctx.options.route, space_id: ctx.options.space,
502
+ request_id: requestId(ctx), dry_run: Boolean(ctx.options.dryRun) };
503
+ if (ctx.options.expectedSpace) body.expected_space_id = ctx.options.expectedSpace;
504
+ if (ctx.options.selectionParam) body.selection_param = ctx.options.selectionParam;
505
+ return remote(ctx, 'legacy-alias', { body });
506
+ },
507
+ },
508
+ {
509
+ command_path: ['spaces', 'pull'], summary: 'Pull one editable Space source snapshot, its resource list and its Skill folders into an empty directory.',
510
+ when_to_use: `Edit a Space you can administer: the source, its links (${RESOURCES_FILE}) and every editable linked Skill (${SKILLS_DIR}/<alias>/) arrive together, with a local lock file so a later deploy sends only what you changed.`,
511
+ args_schema: { arguments: [idArgument, { token: '[dir]', key: 'dir', description: 'New or empty destination directory (defaults to the Space ID).' }],
512
+ options: [{ flags: '--revision <number>', description: 'Exact published source revision; defaults to the current one.' }] },
513
+ examples: ['notis spaces pull <space-id> ./space-source'], mutates: true, idempotent: true, require_auth: true,
514
+ backend_call: { type: 'http', path: '/portal_spaces/source' },
515
+ async handler(ctx) {
516
+ const requestedRevision = ctx.options.revision === undefined ? null : revision(ctx);
517
+ const sourceLink = { space_id: ctx.args.spaceId, api_base: ctx.runtime.apiBase.replace(/\/$/, '') };
518
+ let databaseStructures, authoringOverrides;
519
+ const result = await pullAppSource({ targetDir: resolve(ctx.args.dir || ctx.args.spaceId), force: false,
520
+ sourceLink, version: requestedRevision || 'latest',
521
+ async downloadSource() {
522
+ const query = new URLSearchParams({ space_id: ctx.args.spaceId, format: 'archive',
523
+ ...(requestedRevision === null ? {} : { revision: String(requestedRevision) }) });
524
+ const source = (await httpRequest({ runtime: ctx.runtime, method: 'GET',
525
+ path: `/portal_spaces/source?${query}`, spacesProtocol: true })).payload;
526
+ if (source?.space_id !== ctx.args.spaceId || source.archive_format !== 'tar-gzip-v1'
527
+ || typeof source.source_archive !== 'string' || !/^[a-f0-9]{64}$/.test(source.source_digest || '')) {
528
+ throw usageError('The server did not return a verified Space source snapshot.');
529
+ }
530
+ const archiveBuffer = Buffer.from(source.source_archive, 'base64');
531
+ if (archiveBuffer.toString('base64') !== source.source_archive
532
+ || createHash('sha256').update(archiveBuffer).digest('hex') !== source.archive_sha256) {
533
+ throw usageError('The downloaded Space source bytes do not match their fingerprint.');
534
+ }
535
+ sourceLink.source_digest = source.source_digest;
536
+ sourceLink.local_key = source.manifest?.local_key;
537
+ databaseStructures = source.database_structures;
538
+ authoringOverrides = source.authoring_overrides;
539
+ return { archiveBuffer, revision: source.revision };
540
+ } });
541
+ const authoringFiles = refreshSpaceAuthoring(result.projectDir, sourceLink.local_key, { databaseStructures, authoringOverrides });
542
+ const materialized = await materializeSpaceLinks(ctx, { projectDir: result.projectDir, spaceId: ctx.args.spaceId, sourceRevision: result.version });
543
+ const skipped = materialized.skipped.map((entry) => `${entry.alias} (${entry.reason})`);
544
+ const refreshed = refreshPulledSdk(result.projectDir);
545
+ return ctx.output.emitSuccess({ command: 'spaces pull',
546
+ data: { ...result, space_id: ctx.args.spaceId, ...materialized, authoring_projection_files: authoringFiles, sdk: refreshed.sdk },
547
+ humanSummary: `Pulled source revision ${result.version} into ${result.projectDir}: ${materialized.links.length} linked resources in ${RESOURCES_FILE}, `
548
+ + `${materialized.skills.length} Skill folders under ${SKILLS_DIR}/.${skipped.length ? ` Listed without a folder: ${skipped.join(', ')}.` : ''}`
549
+ + refreshed.notes.map((note) => ` ${note}`).join(''),
550
+ warnings: [...(skipped.length ? [`Skills listed without an editable folder: ${skipped.join(', ')}.`] : []), ...refreshed.warnings] });
551
+ },
552
+ },
553
+ ...['verify', 'preview', 'deploy'].map(operation => ({
554
+ command_path: ['spaces', operation],
555
+ summary: operation === 'verify' ? 'Render one frozen Space with offline fixtures and validate its current action authorizations.'
556
+ : operation === 'preview' ? 'Seal a checked unpublished revision of the same Space and return its authenticated preview link.'
557
+ : 'Publish one built Space with a retry-safe, independent source revision.',
558
+ when_to_use: operation === 'verify' ? 'Check immutable build bytes and authoring compatibility without executing actions or publishing.'
559
+ : operation === 'preview' ? 'Review a checked candidate using current Editor access without changing live source or cloning the Space.'
560
+ : 'Update only the selected existing Space; this does not publish to the Store or change siblings.',
561
+ args_schema: { arguments: [{ token: '[dir]', key: 'dir', description: 'Source workspace (default current directory).' }],
562
+ options: [{ flags: '--space <key>', description: 'Exact source key from the workspace index.' },
563
+ { flags: '--space-id <id>', description: 'Exact existing destination Space.' },
564
+ { flags: '--reuse-grants <json>', description: 'Explicit action-key to saved-grant-ID map. Otherwise your own connections are used.' },
565
+ { flags: '--reuse-grants-only', description: 'Never authorize new actions. Reuse only the supplied saved grants; other declarations remain unavailable. Requires existing bound databases.' },
566
+ ...(operation !== 'verify' ? [requestOption, { flags: '--dry-run', description: 'Validate without uploading or publishing.' }] : [])] },
567
+ examples: [`notis spaces ${operation} . --space overview --space-id <space-id>${operation !== 'verify' ? ' --request-id release-1' : ''}`],
568
+ mutates: operation !== 'verify', idempotent: true, require_auth: true,
569
+ backend_call: { type: 'http', path: operation === 'deploy' ? '/portal_spaces/source-publish'
570
+ : operation === 'preview' ? '/portal_spaces/source-stage' : '/portal_spaces/source-verify' },
571
+ handler: ctx => sourceRelease(ctx, operation !== 'verify', operation === 'preview'),
572
+ })),
573
+ ...['promote', 'abandon'].map(operation => ({
574
+ command_path: ['spaces', operation],
575
+ summary: operation === 'promote' ? 'Publish the exact sealed Space revision, with its pending Skill and link changes, after current permission and concurrency checks.'
576
+ : 'Close an unpublished Space preview without changing its live source, Skills or links, or deleting action history.',
577
+ when_to_use: operation === 'promote' ? 'Finish a reviewed preview without rebuilding or substituting a different candidate. The Skill edits, new Skills and list changes the preview carried go live in the same transaction.'
578
+ : 'Stop using a candidate while preserving live source and completed effects.',
579
+ args_schema: { arguments: [{ token: '<release-id>', key: 'releaseId', description: 'Exact immutable release ID returned by spaces preview.' },
580
+ ...(operation === 'promote' ? [{ token: '[dir]', key: 'dir', description: 'The pulled workspace of this Space; its lock file and resources.json then record what went live.' }] : [])] },
581
+ examples: [`notis spaces ${operation} <release-id>`], mutates: true, idempotent: true, require_auth: true,
582
+ backend_call: { type: 'http', path: `/portal_spaces/source-${operation}` },
583
+ async handler(ctx) {
584
+ const result = await httpRequest({ runtime: ctx.runtime, method: 'POST', path: `/portal_spaces/source-${operation}`,
585
+ body: { release_id: ctx.args.releaseId }, spacesProtocol: true });
586
+ if (operation === 'promote' && ctx.args.dir) {
587
+ const directory = resolveProjectDir(ctx.args.dir);
588
+ const lock = readSpaceLock(directory);
589
+ if (lock && result.payload?.space_id && lock.space_id !== result.payload.space_id) {
590
+ throw usageError(`${directory} was pulled from Space ${lock.space_id}, not from the promoted Space.`);
591
+ }
592
+ if (lock) recordPublication(directory, { lock, folders: collectSkillFolders(directory) }, result.payload);
593
+ }
594
+ return ctx.output.emitSuccess({ command: ctx.spec.command_path.join(' '), data: result.payload,
595
+ humanSummary: operation === 'promote' && result.payload?.published_revision !== undefined
596
+ ? `Published revision ${result.payload.published_revision}${result.payload.replayed ? ' (confirmed retry)' : ''}.${describeAppliedChanges(result.payload)}`
597
+ : JSON.stringify(result.payload, null, 2),
598
+ meta: { mutating: true, request_id: result.requestId } });
599
+ },
600
+ })),
601
+ {
602
+ command_path: ['spaces', 'list'], summary: 'List Spaces currently available to your account.',
603
+ when_to_use: 'Discover accessible Spaces without exposing an issuer-wide resource catalogue.',
604
+ args_schema: {}, examples: ['notis spaces list'], mutates: false, idempotent: true, require_auth: true,
605
+ backend_call: { type: 'http', path: '/portal_spaces/list' },
606
+ handler: ctx => remote(ctx, 'list', { method: 'GET' }),
607
+ },
608
+ {
609
+ command_path: ['spaces', 'get'], summary: 'Read one accessible Space and its scoped resources.',
610
+ when_to_use: 'Inspect the current published revision and named capabilities before executing.',
611
+ args_schema: { arguments: [idArgument], options: [recordOption] },
612
+ examples: ['notis spaces get <space-id>'], mutates: false, idempotent: true, require_auth: true,
613
+ backend_call: { type: 'http', path: '/portal_spaces/get' },
614
+ handler: ctx => remote(ctx, 'get', { method: 'GET', query: { space_id: ctx.args.spaceId, document_id: ctx.options.record } }),
615
+ },
616
+ {
617
+ command_path: ['spaces', 'action'], summary: 'Execute a declared action using its saved authorization.',
618
+ when_to_use: 'Use Space capabilities as the actual actor, with the saved issuer connection and payer.',
619
+ args_schema: { arguments: [idArgument, { token: '<action-id>', key: 'actionId', description: 'Declared action key.' }],
620
+ options: [{ flags: '--revision <number>', description: 'Exact published source revision.' },
621
+ { flags: '--preview-release <id>', description: 'Exact sealed candidate ID; requires current Editor access.' },
622
+ { flags: '--schema-revision <number>', description: 'Native schema revision; required for native row writes.' },
623
+ { flags: '--inputs <json>', description: 'Declared action input values; defaults to {}.' }, recordOption, requestOption,
624
+ { flags: '--dry-run', description: 'Validate current access and action inputs without executing or billing.' }] },
625
+ examples: ['notis spaces action <space-id> refresh --revision 1 --inputs \'{}\' --request-id refresh-1 --dry-run'],
626
+ mutates: true, idempotent: true, require_auth: true,
627
+ backend_call: { type: 'http', path: '/portal_spaces/action' },
628
+ handler: ctx => remote(ctx, 'action', { body: { space_id: ctx.args.spaceId, action_id: ctx.args.actionId,
629
+ revision: revision(ctx), inputs: object(ctx.options.inputs || '{}', '--inputs'), request_id: requestId(ctx),
630
+ ...(ctx.options.previewRelease ? { preview_release_id: ctx.options.previewRelease } : {}),
631
+ document_id: ctx.options.record, schema_revision: schemaRevision(ctx), dry_run: Boolean(ctx.options.dryRun) } }),
632
+ },
633
+ {
634
+ command_path: ['spaces', 'viewer-read'], summary: 'Run one declared viewer read as yourself.',
635
+ when_to_use: 'Check what an explorer Space shows you: the databases or Skills you can open, with your own access and never an issuer grant.',
636
+ args_schema: { arguments: [idArgument, { token: '<operation>', key: 'operation',
637
+ description: 'list_databases, get_database, query_database, list_skills or get_skill.' }],
638
+ options: [{ flags: '--revision <number>', description: 'Exact published source revision.' },
639
+ { flags: '--preview-release <id>', description: 'Exact sealed candidate ID; requires current Editor access.' },
640
+ { flags: '--input <json>', description: 'Read input, e.g. {"database_id":"<id>"}; defaults to {}.' }, recordOption] },
641
+ examples: ['notis spaces viewer-read <space-id> list_databases --revision 3',
642
+ 'notis spaces viewer-read <space-id> query_database --revision 3 --input \'{"database_id":"<id>","request":{"page_size":20}}\''],
643
+ mutates: false, idempotent: true, require_auth: true,
644
+ backend_call: { type: 'http', path: '/portal_spaces/viewer-read' },
645
+ handler: ctx => remote(ctx, 'viewer-read', { body: { space_id: ctx.args.spaceId, operation: ctx.args.operation,
646
+ revision: revision(ctx), input: object(ctx.options.input || '{}', '--input'), document_id: ctx.options.record,
647
+ ...(ctx.options.previewRelease ? { preview_release_id: ctx.options.previewRelease } : {}) } }),
648
+ },
649
+ {
650
+ command_path: ['spaces', 'grants', 'list'], summary: 'Inspect authorization records for a Space you can edit.',
651
+ when_to_use: 'Inspect issuers and exact constraints without exposing connection secrets.',
652
+ args_schema: { arguments: [idArgument], options: [recordOption] }, examples: ['notis spaces grants list <space-id>'],
653
+ mutates: false, idempotent: true, require_auth: true, backend_call: { type: 'http', path: '/portal_spaces/grants' },
654
+ handler: ctx => remote(ctx, 'grants', { method: 'GET', query: { space_id: ctx.args.spaceId, document_id: ctx.options.record } }),
655
+ },
656
+ {
657
+ command_path: ['spaces', 'grants', 'create'], summary: 'Authorize an exact template using your current connection.',
658
+ when_to_use: 'Create a Space-bound authorization; the server resolves the signed-in issuer connection.',
659
+ args_schema: { arguments: [idArgument], options: [{ flags: '--template <file>', description: 'Canonical action template JSON file.' },
660
+ recordOption, requestOption, { flags: '--dry-run', description: 'Validate the template and Editor access without creating a grant.' }] },
661
+ examples: ['notis spaces grants create <space-id> --template action.json --request-id grant-1 --dry-run'],
662
+ mutates: true, idempotent: true, require_auth: true, backend_call: { type: 'http', path: '/portal_spaces/grant-authorize' },
663
+ handler(ctx) {
664
+ if (!ctx.options.template) throw usageError('Pass --template <file> with the canonical action template.');
665
+ return remote(ctx, 'grant-authorize', { body: { space_id: ctx.args.spaceId, request_id: requestId(ctx),
666
+ template: object(readFileSync(resolve(ctx.options.template), 'utf8'), '--template'),
667
+ document_id: ctx.options.record, dry_run: Boolean(ctx.options.dryRun) } });
668
+ },
669
+ },
670
+ {
671
+ command_path: ['spaces', 'grants', 'revoke'], summary: 'Revoke one saved Space authorization.',
672
+ when_to_use: 'Stop future dispatches for an authorization you issued or can administer.',
673
+ args_schema: { arguments: [{ token: '<grant-id>', key: 'grantId', description: 'Exact grant ID from grants list.' }] },
674
+ examples: ['notis spaces grants revoke <grant-id>'], mutates: true, idempotent: true, require_auth: true,
675
+ backend_call: { type: 'http', path: '/portal_spaces/grant-revoke' },
676
+ handler: ctx => remote(ctx, 'grant-revoke', { body: { grant_id: ctx.args.grantId } }),
677
+ },
678
+ {
679
+ command_path: ['spaces', 'store', 'list'], summary: 'List Space Store listings you can see.',
680
+ when_to_use: 'Find Space templates published to your team or approved for everyone, and your own listings.',
681
+ args_schema: { options: [{ flags: '--channel <team|public>', description: 'Only one channel.' }] },
682
+ examples: ['notis spaces store list --channel team'], mutates: false, idempotent: true, require_auth: true,
683
+ backend_call: { type: 'http', path: '/portal_spaces/store-listings' },
684
+ handler: ctx => remote(ctx, 'store-listings', { method: 'GET', query: { channel: ctx.options.channel } }),
685
+ },
686
+ {
687
+ command_path: ['spaces', 'store', 'get'], summary: 'Read one Store listing with its versions and your installs.',
688
+ when_to_use: 'Check what a listing contains, its review state and whether your copies have an update.',
689
+ args_schema: { arguments: [{ token: '<listing-id>', key: 'listingId', description: 'Exact listing ID.' }] },
690
+ examples: ['notis spaces store get <listing-id>'], mutates: false, idempotent: true, require_auth: true,
691
+ backend_call: { type: 'http', path: '/portal_spaces/store-listing' },
692
+ handler: ctx => remote(ctx, 'store-listing', { method: 'GET', query: { listing_id: ctx.args.listingId } }),
693
+ },
694
+ {
695
+ command_path: ['spaces', 'store', 'installs'], summary: 'List the Spaces you installed from the Store.',
696
+ when_to_use: 'See which installed copies have an update or conflicts to resolve.',
697
+ args_schema: {}, examples: ['notis spaces store installs'], mutates: false, idempotent: true, require_auth: true,
698
+ backend_call: { type: 'http', path: '/portal_spaces/store-installs' },
699
+ handler: ctx => remote(ctx, 'store-installs', { method: 'GET' }),
700
+ },
701
+ {
702
+ command_path: ['spaces', 'store', 'publish'], summary: 'Publish a Space and its sub-Spaces as the next Store version.',
703
+ when_to_use: 'Share a Space you own as an installable template: team listings publish at once, public ones wait for review. Notis runs the same safety scan and Owner check as the submission page and explains any refusal.',
704
+ args_schema: { arguments: [idArgument], options: [{ flags: '--channel <team|public>', description: 'Where to publish.' },
705
+ { flags: '--metadata <json>', description: 'Listing name, tagline, description, category, icon, accent.' },
706
+ { flags: '--starter <json>', description: 'Starter row IDs per database ({"<database-id>":["<record-key>"]}).' },
707
+ { flags: '--notes <text>', description: 'Release notes for this version.' },
708
+ requestOption, { flags: '--dry-run', description: 'Show the package counts and review diff without publishing.' }] },
709
+ examples: ['notis spaces store publish <space-id> --channel team --request-id publish-1 --dry-run'],
710
+ mutates: true, idempotent: true, require_auth: true, backend_call: { type: 'http', path: '/portal_spaces/store-publish' },
711
+ handler(ctx) {
712
+ if (!['team', 'public'].includes(ctx.options.channel)) throw usageError('Pass --channel team or --channel public.');
713
+ return remote(ctx, 'store-publish', { body: { space_id: ctx.args.spaceId, channel: ctx.options.channel, request_id: requestId(ctx),
714
+ ...(ctx.options.metadata ? { metadata: object(ctx.options.metadata, '--metadata') } : {}),
715
+ ...(ctx.options.starter ? { starter: object(ctx.options.starter, '--starter') } : {}),
716
+ ...(ctx.options.notes ? { notes: ctx.options.notes } : {}), dry_run: Boolean(ctx.options.dryRun) } });
717
+ },
718
+ },
719
+ {
720
+ command_path: ['spaces', 'store', 'install'], summary: 'Install a Store listing as your own independent copy.',
721
+ when_to_use: 'Add a published Space template to your account; your copy keeps your changes on later updates.',
722
+ args_schema: { arguments: [{ token: '<listing-id>', key: 'listingId', description: 'Exact listing ID.' }],
723
+ options: [{ flags: '--name <name>', description: 'Name of the installed top Space.' },
724
+ { flags: '--parent <space-id>', description: 'Install under one of your Spaces.' },
725
+ requestOption, { flags: '--dry-run', description: 'Show what the copy contains without installing.' }] },
726
+ examples: ['notis spaces store install <listing-id> --request-id install-1'],
727
+ mutates: true, idempotent: true, require_auth: true, backend_call: { type: 'http', path: '/portal_spaces/store-install' },
728
+ handler: ctx => remote(ctx, 'store-install', { body: { listing_id: ctx.args.listingId, request_id: requestId(ctx),
729
+ ...(ctx.options.name ? { name: ctx.options.name } : {}), ...(ctx.options.parent ? { parent_space_id: ctx.options.parent } : {}),
730
+ dry_run: Boolean(ctx.options.dryRun) } }),
731
+ },
732
+ {
733
+ command_path: ['spaces', 'store', 'update'], summary: 'Update an installed copy, keeping your changes, or resolve its conflicts.',
734
+ when_to_use: 'Take a newer Store version: unchanged parts update, your edits stay, and parts both sides changed become conflicts you resolve.',
735
+ args_schema: { arguments: [{ token: '<install-id>', key: 'installId', description: 'Exact install ID from store installs.' }],
736
+ options: [{ flags: '--resolve <json>', description: 'Per conflict key: "keep_mine" or "take_theirs".' },
737
+ requestOption, { flags: '--dry-run', description: 'Show the three-way plan without changing anything.' }] },
738
+ examples: ['notis spaces store update <install-id> --request-id update-1 --dry-run',
739
+ 'notis spaces store update <install-id> --resolve \'{"skill:<key>":"keep_mine"}\' --request-id resolve-1'],
740
+ mutates: true, idempotent: true, require_auth: true, backend_call: { type: 'http', path: '/portal_spaces/store-update' },
741
+ handler: ctx => remote(ctx, 'store-update', { body: { install_id: ctx.args.installId, request_id: requestId(ctx),
742
+ ...(ctx.options.resolve ? { resolution: object(ctx.options.resolve, '--resolve') } : {}), dry_run: Boolean(ctx.options.dryRun) } }),
743
+ },
744
+ {
745
+ command_path: ['spaces', 'store', 'unpublish'], summary: 'Stop new installs of a Store listing.',
746
+ when_to_use: 'Withdraw a listing: existing copies keep working and a version waiting for review is withdrawn.',
747
+ args_schema: { arguments: [{ token: '<listing-id>', key: 'listingId', description: 'Exact listing ID.' }], options: [requestOption] },
748
+ examples: ['notis spaces store unpublish <listing-id> --request-id unpublish-1'],
749
+ mutates: true, idempotent: true, require_auth: true, backend_call: { type: 'http', path: '/portal_spaces/store-unpublish' },
750
+ handler: ctx => remote(ctx, 'store-unpublish', { body: { listing_id: ctx.args.listingId, request_id: requestId(ctx) } }),
751
+ },
752
+ {
753
+ command_path: ['spaces', 'build'],
754
+ summary: 'Build one independent Space and save its frozen source/artifact snapshot.',
755
+ when_to_use: 'Prepare a selected Space for verification without publishing it or changing siblings.',
756
+ args_schema: { arguments: [{ token: '[dir]', key: 'dir', description: 'Source workspace (default current directory).' }],
757
+ options: [{ flags: '--space <key>', description: 'Exact source key from the workspace Space index.' }] },
758
+ examples: ['notis spaces build . --space overview'],
759
+ mutates: true, idempotent: true, require_auth: false,
760
+ backend_call: { type: 'local', name: 'space_build' },
761
+ async handler(ctx) {
762
+ const { directory, key } = selected(ctx);
763
+ const result = await buildSpaceArtifact(directory, key, { stdio: ctx.output.isMachineMode() ? 'pipe' : 'inherit' });
764
+ return ctx.output.emitSuccess({ command: 'spaces build', data: result, humanSummary: `Built ${key}; no Space was published.` });
765
+ },
766
+ },
767
+ {
768
+ command_path: ['spaces', 'inspect'],
769
+ summary: 'Inspect one local Space definition and its declared capabilities.',
770
+ when_to_use: 'Read the selected source definition without loading sibling definitions or account resources.',
771
+ args_schema: { arguments: [{ token: '[dir]', key: 'dir', description: 'Source workspace (default current directory).' }],
772
+ options: [{ flags: '--space <key>', description: 'Exact source key from the workspace Space index.' }] },
773
+ examples: ['notis spaces inspect . --space overview'],
774
+ mutates: false, idempotent: true, require_auth: false,
775
+ backend_call: { type: 'local', name: 'space_source_inspect' },
776
+ async handler(ctx) {
777
+ const { directory, key } = selected(ctx);
778
+ const selection = await loadSelectedSpace(directory, key);
779
+ return ctx.output.emitSuccess({ command: 'spaces inspect', data: { key, definition: selection.definition, definition_path: selection.definitionPath },
780
+ humanSummary: `${selection.definition.name} (${key})` });
781
+ },
782
+ },
783
+ ];