@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
@@ -4,14 +4,17 @@ import {
4
4
  mkdirSync,
5
5
  readFileSync,
6
6
  readdirSync,
7
+ realpathSync,
7
8
  renameSync,
8
9
  writeFileSync,
9
10
  } from 'node:fs';
10
11
  import { createHash } from 'node:crypto';
11
12
  import { homedir } from 'node:os';
12
- import { dirname, join } from 'node:path';
13
+ import { dirname, isAbsolute, join } from 'node:path';
13
14
  import { fileURLToPath } from 'node:url';
14
15
 
16
+ import { BASE_SKILL_NAMES, RETIRED_BASE_SKILL_NAMES, isBaseSkillLink } from './base-skills.js';
17
+
15
18
  const HERE = dirname(fileURLToPath(import.meta.url));
16
19
  const INSTRUCTIONS_PATH = join(HERE, '..', '..', 'skills', 'notis-cli', 'AGENT_INSTRUCTIONS.md');
17
20
  const HOOK_BUNDLE_PATH = join(HERE, '..', '..', 'dist', 'agent-hooks', 'notis-agent-hook.mjs');
@@ -20,6 +23,16 @@ const END_MARKER = '<!-- notis-cli:instructions:end -->';
20
23
  const LEGACY_SENTINEL = 'Use the Notis CLI (`npx --package @notis_ai/cli@latest -- notis ...`)';
21
24
  const MANAGED_HOOK_MARKER = '--notis-managed-agent-hook';
22
25
  const LEGACY_MANAGED_HOOK_MARKER = 'NOTIS_MANAGED_AGENT_HOOK=1';
26
+ const CODEX_TRUST_START = '# notis-cli:hook-trust:start';
27
+ const CODEX_TRUST_END = '# notis-cli:hook-trust:end';
28
+ const CODEX_EVENT_LABELS = Object.freeze({
29
+ SessionStart: 'session_start',
30
+ UserPromptSubmit: 'user_prompt_submit',
31
+ Stop: 'stop',
32
+ });
33
+ // Recall-only hooks inject memory and never capture turns, so they run only
34
+ // on these events.
35
+ export const RECALL_ONLY_HOOK_EVENTS = Object.freeze(['SessionStart', 'UserPromptSubmit']);
23
36
 
24
37
  export const AGENT_IDS = Object.freeze(['codex', 'claude-code']);
25
38
 
@@ -148,7 +161,11 @@ export function installManagedHookRuntime({
148
161
  bundlePath = HOOK_BUNDLE_PATH,
149
162
  nodePath = process.execPath,
150
163
  platform = process.platform,
164
+ envFile = null,
151
165
  } = {}) {
166
+ if (envFile && (platform === 'win32' || !isAbsolute(envFile))) {
167
+ throw new Error('A session environment file must be an absolute POSIX path');
168
+ }
152
169
  const bundle = readFileSync(bundlePath);
153
170
  const digest = createHash('sha256').update(bundle).digest('hex');
154
171
  const runtimePath = join(
@@ -184,6 +201,9 @@ export function installManagedHookRuntime({
184
201
  ].join('\r\n')
185
202
  : [
186
203
  '#!/bin/sh',
204
+ // A hosted session's sign-in is rotated into this file while the
205
+ // coding agent keeps running, so each hook reads the current one.
206
+ ...(envFile ? [`if [ -r ${shellQuote(envFile)} ]; then . ${shellQuote(envFile)}; fi`] : []),
187
207
  `if [ -x ${shellQuote(nodePath)} ]; then`,
188
208
  ` exec ${shellQuote(nodePath)} ${shellQuote(runtimePath)} "$@"`,
189
209
  'fi',
@@ -213,10 +233,17 @@ function shellHookCommand(launcherPath, profileName, command, agentId = null, pl
213
233
  return `${quotedLauncher} ${MANAGED_HOOK_MARKER} --profile ${profileName} --timeout-ms 30000 ${command}${agentFlag}`;
214
234
  }
215
235
 
216
- function contextHandler(agentId, profileName, launcherPath, platform) {
236
+ function recallOnlyHookCommand(launcherPath, stateDir) {
237
+ // No --profile: the hook signs in with the session's injected credential.
238
+ return `${shellQuote(launcherPath)} ${MANAGED_HOOK_MARKER} --timeout-ms 30000 agent-context --recall-only --state-dir ${shellQuote(stateDir)}`;
239
+ }
240
+
241
+ function contextHandler(agentId, profileName, launcherPath, platform, recallOnly = null) {
217
242
  return {
218
243
  type: 'command',
219
- command: shellHookCommand(launcherPath, profileName, 'agent-context', null, platform),
244
+ command: recallOnly
245
+ ? recallOnlyHookCommand(launcherPath, recallOnly.stateDir)
246
+ : shellHookCommand(launcherPath, profileName, 'agent-context', null, platform),
220
247
  timeout: 30,
221
248
  statusMessage: 'Loading relevant Notis memory',
222
249
  ...(agentId === 'codex' ? { additionalContextLimit: 2500 } : {}),
@@ -239,20 +266,24 @@ function upsertPromptHooks(
239
266
  enabled,
240
267
  launcherPath = '',
241
268
  platform = process.platform,
269
+ recallOnly = null,
242
270
  ) {
243
271
  const settings = readJsonObject(filePath);
244
272
  const hooks = settings.hooks && typeof settings.hooks === 'object' && !Array.isArray(settings.hooks)
245
273
  ? { ...settings.hooks }
246
274
  : {};
247
275
  let removedManagedHook = false;
276
+ const managed = [];
248
277
  for (const event of ['SessionStart', 'UserPromptSubmit', 'Stop']) {
249
278
  const previousGroups = Array.isArray(hooks[event]) ? hooks[event] : [];
250
279
  const groups = removePriorNotisPromptHooks(previousGroups);
251
280
  if (JSON.stringify(groups) !== JSON.stringify(previousGroups)) removedManagedHook = true;
252
- if (enabled) {
253
- groups.push({ hooks: [event === 'Stop'
281
+ if (enabled && (!recallOnly || RECALL_ONLY_HOOK_EVENTS.includes(event))) {
282
+ const handler = event === 'Stop'
254
283
  ? captureHandler(agentId, profileName, launcherPath, platform)
255
- : contextHandler(agentId, profileName, launcherPath, platform)] });
284
+ : contextHandler(agentId, profileName, launcherPath, platform, recallOnly);
285
+ groups.push({ hooks: [handler] });
286
+ managed.push({ event, groupIndex: groups.length - 1, handlerIndex: 0, handler });
256
287
  }
257
288
  if (groups.length) hooks[event] = groups;
258
289
  else delete hooks[event];
@@ -260,15 +291,90 @@ function upsertPromptHooks(
260
291
  const next = { ...settings, hooks };
261
292
  const serialized = `${JSON.stringify(next, null, 2)}\n`;
262
293
  const existing = readText(filePath);
263
- if (!enabled && !removedManagedHook) return { path: filePath, status: 'unchanged' };
264
- if (existing === serialized) return { path: filePath, status: 'unchanged' };
294
+ if (!enabled && !removedManagedHook) return { path: filePath, status: 'unchanged', managed };
295
+ if (existing === serialized) return { path: filePath, status: 'unchanged', managed };
265
296
  atomicWrite(filePath, serialized);
266
297
  return {
267
298
  path: filePath,
268
299
  status: enabled
269
300
  ? existsSync(filePath) && existing ? 'updated' : 'installed'
270
301
  : 'removed',
302
+ managed,
303
+ };
304
+ }
305
+
306
+ function canonicalJson(value) {
307
+ if (Array.isArray(value)) return value.map(canonicalJson);
308
+ if (value && typeof value === 'object') {
309
+ return Object.fromEntries(Object.keys(value).sort().map((key) => [key, canonicalJson(value[key])]));
310
+ }
311
+ return value;
312
+ }
313
+
314
+ /**
315
+ * The hash Codex records when a user trusts one hook in `/hooks`: SHA-256 of
316
+ * the event's normalized handler group, serialized with sorted keys. Matches
317
+ * the pinned Codex 0.160 app-server `hooks/list` currentHash for the handler
318
+ * shape this installer writes (no matcher; additionalContextLimit not hashed).
319
+ */
320
+ export function codexHookTrustHash(event, handler) {
321
+ const label = CODEX_EVENT_LABELS[event];
322
+ if (!label) throw new Error(`Unsupported Codex hook event: ${event}`);
323
+ const normalized = {
324
+ type: 'command',
325
+ command: handler.command,
326
+ timeout: handler.timeout,
327
+ async: false,
328
+ ...(handler.statusMessage ? { statusMessage: handler.statusMessage } : {}),
271
329
  };
330
+ const digest = createHash('sha256')
331
+ .update(JSON.stringify(canonicalJson({ event_name: label, hooks: [normalized] })))
332
+ .digest('hex');
333
+ return `sha256:${digest}`;
334
+ }
335
+
336
+ /** The `hooks.state` key Codex uses for one handler in a hooks.json file. */
337
+ export function codexHookTrustKey(hooksPath, event, groupIndex, handlerIndex = 0) {
338
+ return `${hooksPath}:${CODEX_EVENT_LABELS[event]}:${groupIndex}:${handlerIndex}`;
339
+ }
340
+
341
+ function tomlBasicString(value) {
342
+ return `"${String(value).replace(/\\/g, '\\\\').replace(/"/g, '\\"')}"`;
343
+ }
344
+
345
+ /**
346
+ * Record trust for the managed Codex hooks in config.toml, so a headless Codex
347
+ * runs them without the interactive `/hooks` review. Only the block between
348
+ * the managed markers is rewritten; a key the user already trusts elsewhere is
349
+ * left alone, because a second TOML table with the same name breaks parsing.
350
+ */
351
+ export function upsertCodexHookTrust(configPath, hooksPath, managed) {
352
+ const existing = readText(configPath);
353
+ const blockPattern = new RegExp(
354
+ `\\n?${CODEX_TRUST_START}[\\s\\S]*?${CODEX_TRUST_END}\\n?`,
355
+ 'g',
356
+ );
357
+ const outside = existing.replace(blockPattern, '\n');
358
+ const resolvedHooksPath = realpathSync(hooksPath);
359
+ const entries = [];
360
+ const skipped = [];
361
+ for (const item of managed) {
362
+ const key = codexHookTrustKey(resolvedHooksPath, item.event, item.groupIndex, item.handlerIndex);
363
+ const header = `[hooks.state.${tomlBasicString(key)}]`;
364
+ if (outside.includes(header)) {
365
+ skipped.push(key);
366
+ continue;
367
+ }
368
+ entries.push(`${header}\ntrusted_hash = ${tomlBasicString(codexHookTrustHash(item.event, item.handler))}`);
369
+ }
370
+ const prefix = outside.replace(/\n+$/, '');
371
+ const block = entries.length
372
+ ? `${CODEX_TRUST_START}\n${entries.join('\n\n')}\n${CODEX_TRUST_END}\n`
373
+ : '';
374
+ const next = prefix && block ? `${prefix}\n\n${block}` : prefix ? `${prefix}\n` : block;
375
+ if (next === existing) return { path: configPath, status: 'unchanged', skipped };
376
+ atomicWrite(configPath, next);
377
+ return { path: configPath, status: existing ? 'updated' : 'installed', skipped };
272
378
  }
273
379
 
274
380
  function agentPaths(agentId, home) {
@@ -336,7 +442,7 @@ export function installAgentSetup({
336
442
  result.memory_hook = { path: paths.hooks, status: 'preserved' };
337
443
  } else {
338
444
  try {
339
- result.memory_hook = upsertPromptHooks(
445
+ const { managed: _managed, ...memoryHook } = upsertPromptHooks(
340
446
  paths.hooks,
341
447
  agentId,
342
448
  profileName,
@@ -344,6 +450,7 @@ export function installAgentSetup({
344
450
  hookRuntime?.launcherPath,
345
451
  platform,
346
452
  );
453
+ result.memory_hook = memoryHook;
347
454
  } catch (error) {
348
455
  result.memory_hook = {
349
456
  path: paths.hooks,
@@ -357,6 +464,56 @@ export function installAgentSetup({
357
464
  return results;
358
465
  }
359
466
 
467
+ /**
468
+ * Recall-only memory hooks for a hosted coding agent (the Cloud Computer's
469
+ * native Codex and Claude Code). They go into the agent's own config folder
470
+ * (CODEX_HOME or CLAUDE_CONFIG_DIR), sign in with the session's injected
471
+ * credential, run only on SessionStart and UserPromptSubmit, and never capture
472
+ * a turn. No instruction file is written: the hosted contract already carries
473
+ * the Notis guidance. Codex trust is recorded so no `/hooks` review is needed.
474
+ */
475
+ export function installRecallOnlyAgentHooks({
476
+ agentId,
477
+ configDir,
478
+ envFile = null,
479
+ bundlePath = HOOK_BUNDLE_PATH,
480
+ nodePath = process.execPath,
481
+ platform = process.platform,
482
+ } = {}) {
483
+ if (!AGENT_IDS.includes(agentId)) throw new Error(`Unsupported agent: ${agentId}`);
484
+ if (platform === 'win32') throw new Error('Recall-only hooks are only supported on POSIX hosts');
485
+ if (typeof configDir !== 'string' || !isAbsolute(configDir)) {
486
+ throw new Error('Recall-only hooks need the agent config folder as an absolute path');
487
+ }
488
+ const runtime = installManagedHookRuntime({
489
+ home: configDir, bundlePath, nodePath, platform, envFile,
490
+ });
491
+ const hooksPath = join(configDir, agentId === 'codex' ? 'hooks.json' : 'settings.json');
492
+ const result = { agent: agentId, mode: 'recall_only', instructions: null, memory_hook: null };
493
+ try {
494
+ const { managed, ...memoryHook } = upsertPromptHooks(
495
+ hooksPath,
496
+ agentId,
497
+ null,
498
+ true,
499
+ runtime.launcherPath,
500
+ platform,
501
+ { stateDir: configDir },
502
+ );
503
+ result.memory_hook = memoryHook;
504
+ if (agentId === 'codex') {
505
+ result.hook_trust = upsertCodexHookTrust(join(configDir, 'config.toml'), hooksPath, managed);
506
+ }
507
+ } catch (error) {
508
+ result.memory_hook = {
509
+ path: hooksPath,
510
+ status: 'error',
511
+ message: error instanceof Error ? error.message : String(error),
512
+ };
513
+ }
514
+ return result;
515
+ }
516
+
360
517
  export function detectedAgentIds(home = homedir()) {
361
518
  return AGENT_IDS.filter((agentId) => {
362
519
  const root = agentPaths(agentId, home).root;
@@ -367,10 +524,12 @@ export function detectedAgentIds(home = homedir()) {
367
524
  const skillsRoot = join(root, 'skills');
368
525
  if (!existsSync(skillsRoot)) return false;
369
526
  // Base reconciliation intentionally creates every supported vendor root.
370
- // A root containing only our three links is not evidence that the vendor
371
- // itself is installed; any other skill entry is a genuine user signal.
372
- return readdirSync(skillsRoot).some((name) => (
373
- !['notis-apps', 'notis-query', 'notis-cli'].includes(name)
527
+ // A root containing only our base links, or the links an earlier CLI
528
+ // made under retired names, is not evidence that the vendor itself is
529
+ // installed; any other skill entry is a genuine user signal.
530
+ return readdirSync(skillsRoot).some((name) => !(
531
+ BASE_SKILL_NAMES.includes(name)
532
+ || (RETIRED_BASE_SKILL_NAMES.includes(name) && isBaseSkillLink(join(skillsRoot, name), name, { home }))
374
533
  ));
375
534
  } catch {
376
535
  return false;
@@ -3,6 +3,7 @@ import { dirname, extname, join, resolve } from 'node:path';
3
3
  import { fileURLToPath } from 'node:url';
4
4
 
5
5
  import { usageError } from './errors.js';
6
+ import { maskSerializedJsonData } from './app-data-literals.js';
6
7
 
7
8
  const moduleDir = dirname(fileURLToPath(import.meta.url));
8
9
 
@@ -133,10 +134,12 @@ function validateTextFile(relPath, content) {
133
134
  const extension = extname(relPath);
134
135
  const rules = getCompiledRules();
135
136
  if (extension === '.css' || extension === '.scss' || extension === '.sass' || extension === '.pcss') {
136
- return applyRules(content, rules.css, relPath);
137
+ // A comment that mentions html, body or :root is not a selector. Blanking
138
+ // comments keeps offsets and line numbers; every rule still runs unchanged.
139
+ return applyRules(maskComments(content, '.css'), rules.css, relPath);
137
140
  }
138
141
  if (extension === '.js' || extension === '.jsx' || extension === '.ts' || extension === '.tsx') {
139
- return applyRules(content, rules.javascript, relPath);
142
+ return applyRules(maskSerializedJsonData(content, extension), rules.javascript, relPath);
140
143
  }
141
144
  return [];
142
145
  }
@@ -181,3 +184,336 @@ export function validateArtifactBoundary(files) {
181
184
  );
182
185
  }
183
186
  }
187
+
188
+ // ---------------------------------------------------------------------------
189
+ // Design rules
190
+ //
191
+ // The portal boundary rules above keep an app inside its surface. The design
192
+ // rules below keep it looking like a native, flat Notis page: no bordered
193
+ // boxes around items, no dividers, no palette colors, no eyebrows, no loading
194
+ // text. They are path-scoped, report line numbers, and only ever run on the
195
+ // app's own source files (never on the bundled artifact, where every
196
+ // dependency legitimately contains the word "border").
197
+ //
198
+ // The only override is an inline directive on the same line or the line
199
+ // before the match:
200
+ // // notis-design-allow: <rule-id> <reason of at least N characters>
201
+ // Allowed matches are reported with `allowed: true` so they stay visible.
202
+ // ---------------------------------------------------------------------------
203
+
204
+ export const DESIGN_RULES_PATH_CANDIDATES = [
205
+ resolve(moduleDir, '../../../../server/config/notis_app_design_rules.json'),
206
+ resolve(moduleDir, '../../config/notis_app_design_rules.json'),
207
+ ];
208
+
209
+ export function resolveDesignRulesPath() {
210
+ for (const candidate of DESIGN_RULES_PATH_CANDIDATES) {
211
+ if (existsSync(candidate)) {
212
+ return candidate;
213
+ }
214
+ }
215
+ return DESIGN_RULES_PATH_CANDIDATES[DESIGN_RULES_PATH_CANDIDATES.length - 1];
216
+ }
217
+
218
+ const DEFAULT_DESIGN_EXTENSIONS = ['.js', '.jsx', '.ts', '.tsx', '.css'];
219
+
220
+ function globToRegExp(glob) {
221
+ let source = '';
222
+ for (let index = 0; index < glob.length; index += 1) {
223
+ const char = glob[index];
224
+ if (char === '*') {
225
+ if (glob[index + 1] === '*') {
226
+ index += 1;
227
+ if (glob[index + 1] === '/') {
228
+ index += 1;
229
+ source += '(?:.*/)?';
230
+ } else {
231
+ source += '.*';
232
+ }
233
+ } else {
234
+ source += '[^/]*';
235
+ }
236
+ } else if (char === '?') {
237
+ source += '[^/]';
238
+ } else {
239
+ source += char.replace(/[.+^${}()|[\]\\]/g, '\\$&');
240
+ }
241
+ }
242
+ return new RegExp(`^${source}$`);
243
+ }
244
+
245
+ function matchesAny(relPath, globs) {
246
+ return (globs || []).some((glob) => globToRegExp(glob).test(relPath));
247
+ }
248
+
249
+ const REGEX_PRECEDERS = new Set(['', '(', ',', '=', ':', '[', '!', '&', '|', '?', '{', '}', ';', '+', '-', '*', '%', '>', '~', '^']);
250
+
251
+ function blankRange(chars, start, end) {
252
+ for (let index = start; index < end; index += 1) {
253
+ if (chars[index] !== '\n') chars[index] = ' ';
254
+ }
255
+ }
256
+
257
+ /**
258
+ * Blank out comments so the design rules never match words inside them. Every
259
+ * character keeps its offset and newlines stay, so line numbers do not move.
260
+ * CSS has only block comments; JS and TS also have line comments. Quoted
261
+ * strings end at a newline so an apostrophe in JSX text cannot swallow a file,
262
+ * and "scheme://" is never a comment. Mirrored by mask_comments in
263
+ * server/lib/app_boundary_validator.py.
264
+ */
265
+ export function maskComments(content, extension) {
266
+ const isCss = extension === '.css';
267
+ const chars = content.split('');
268
+ const length = chars.length;
269
+ let index = 0;
270
+ let previous = '';
271
+ while (index < length) {
272
+ const char = chars[index];
273
+ const next = chars[index + 1];
274
+ if (char === '/' && next === '*') {
275
+ const close = content.indexOf('*/', index + 2);
276
+ const end = close === -1 ? length : close + 2;
277
+ blankRange(chars, index, end);
278
+ index = end;
279
+ continue;
280
+ }
281
+ if (!isCss && char === '/' && next === '/' && !(chars[index - 1] === ':' && /[A-Za-z0-9]/.test(chars[index - 2] || ''))) {
282
+ let end = content.indexOf('\n', index);
283
+ if (end === -1) end = length;
284
+ blankRange(chars, index, end);
285
+ index = end;
286
+ continue;
287
+ }
288
+ if (char === '"' || char === "'" || (!isCss && char === '`')) {
289
+ let cursor = index + 1;
290
+ while (cursor < length) {
291
+ const inner = chars[cursor];
292
+ if (inner === '\\') { cursor += 2; continue; }
293
+ if (inner === char) { cursor += 1; break; }
294
+ if (inner === '\n' && char !== '`') break;
295
+ cursor += 1;
296
+ }
297
+ previous = char;
298
+ index = cursor;
299
+ continue;
300
+ }
301
+ if (!isCss && char === '/' && REGEX_PRECEDERS.has(previous)) {
302
+ let cursor = index + 1;
303
+ let inClass = false;
304
+ while (cursor < length && chars[cursor] !== '\n') {
305
+ const inner = chars[cursor];
306
+ if (inner === '\\') { cursor += 2; continue; }
307
+ if (inner === '[') inClass = true;
308
+ else if (inner === ']') inClass = false;
309
+ else if (inner === '/' && !inClass) { cursor += 1; break; }
310
+ cursor += 1;
311
+ }
312
+ previous = '/';
313
+ index = cursor;
314
+ continue;
315
+ }
316
+ if (!/\s/.test(char)) previous = char;
317
+ index += 1;
318
+ }
319
+ return chars.join('');
320
+ }
321
+
322
+ const compiledDesignRules = new Map();
323
+
324
+ /**
325
+ * Load the design rules for one profile. The default profile ("app") keeps the
326
+ * file's top-level include/exclude. A named profile (for example "space")
327
+ * replaces the include with its own, adds its excludes, and turns on the rules
328
+ * that list it in `profiles`. Rules naming a `toolkit_piece` stay warnings until
329
+ * that piece appears in `toolkit_live_pieces`, then they refuse like errors.
330
+ */
331
+ export function loadDesignRules({ rulesPath = resolveDesignRulesPath(), profile = 'app' } = {}) {
332
+ let payload = null;
333
+ try {
334
+ const parsed = JSON.parse(readFileSync(rulesPath, 'utf-8'));
335
+ if (parsed && typeof parsed === 'object' && !Array.isArray(parsed)) {
336
+ payload = parsed;
337
+ }
338
+ } catch (error) {
339
+ process.stderr.write(
340
+ `Warning: could not load Notis app design rules (${error.message}); skipping design checks.\n`,
341
+ );
342
+ }
343
+ if (!payload) {
344
+ return { profile, include: [], exclude: [], extensions: DEFAULT_DESIGN_EXTENSIONS, rules: [], allowDirective: 'notis-design-allow', allowReasonMinLength: 12 };
345
+ }
346
+ const profiles = payload.profiles && typeof payload.profiles === 'object' && !Array.isArray(payload.profiles) ? payload.profiles : {};
347
+ const selected = profile !== 'app' && profiles[profile] && typeof profiles[profile] === 'object' ? profiles[profile] : null;
348
+ const baseInclude = Array.isArray(payload.include) ? payload.include : ['app/**', 'components/**'];
349
+ const baseExclude = Array.isArray(payload.exclude) ? payload.exclude : [];
350
+ const livePieces = new Set(Array.isArray(payload.toolkit_live_pieces) ? payload.toolkit_live_pieces : []);
351
+ return {
352
+ profile,
353
+ include: Array.isArray(selected?.include) ? selected.include : baseInclude,
354
+ exclude: [...baseExclude, ...(Array.isArray(selected?.exclude) ? selected.exclude : [])],
355
+ extensions: Array.isArray(payload.extensions) ? payload.extensions : DEFAULT_DESIGN_EXTENSIONS,
356
+ allowDirective: typeof payload.allow_directive === 'string' ? payload.allow_directive : 'notis-design-allow',
357
+ allowReasonMinLength: Number.isInteger(payload.allow_reason_min_length) ? payload.allow_reason_min_length : 12,
358
+ rules: (Array.isArray(payload.rules) ? payload.rules : [])
359
+ .filter((rule) => !Array.isArray(rule.profiles) || rule.profiles.includes(profile))
360
+ .map((rule) => ({
361
+ id: rule.id,
362
+ severity: typeof rule.toolkit_piece === 'string'
363
+ ? (livePieces.has(rule.toolkit_piece) ? 'error' : 'warn')
364
+ : rule.severity === 'warn' ? 'warn' : 'error',
365
+ toolkitPiece: typeof rule.toolkit_piece === 'string' ? rule.toolkit_piece : null,
366
+ scope: rule.scope === 'file' ? 'file' : 'line',
367
+ regex: new RegExp(rule.pattern, rule.scope === 'file' ? '' : 'gm'),
368
+ message: typeof rule.profile_messages?.[profile] === 'string' ? rule.profile_messages[profile] : rule.message,
369
+ include: Array.isArray(rule.include) ? rule.include : null,
370
+ exclude: Array.isArray(rule.exclude) ? rule.exclude : [],
371
+ })),
372
+ };
373
+ }
374
+
375
+ function getCompiledDesignRules(profile = 'app') {
376
+ if (!compiledDesignRules.has(profile)) {
377
+ compiledDesignRules.set(profile, loadDesignRules({ profile }));
378
+ }
379
+ return compiledDesignRules.get(profile);
380
+ }
381
+
382
+ function lineNumberAt(content, index) {
383
+ let line = 1;
384
+ for (let cursor = 0; cursor < index; cursor += 1) {
385
+ if (content.charCodeAt(cursor) === 10) line += 1;
386
+ }
387
+ return line;
388
+ }
389
+
390
+ function findAllowDirective(lines, lineNumber, ruleId, directive) {
391
+ const pattern = new RegExp(`${directive}:\\s*${ruleId.replace(/[.*+?^${}()|[\]\\]/g, '\\$&')}\\b\\s*(.*)$`);
392
+ for (const candidate of [lines[lineNumber - 1], lines[lineNumber - 2]]) {
393
+ if (typeof candidate !== 'string') continue;
394
+ const match = candidate.match(pattern);
395
+ if (match) {
396
+ return match[1].replace(/\*\/\s*}?\s*$/, '').replace(/-->\s*$/, '').trim();
397
+ }
398
+ }
399
+ return null;
400
+ }
401
+
402
+ export function collectDesignViolationsForFile(relPath, content, config = getCompiledDesignRules()) {
403
+ const normalized = relPath.split('\\').join('/');
404
+ if (!config.extensions.includes(extname(normalized))) return [];
405
+ if (!matchesAny(normalized, config.include)) return [];
406
+ if (matchesAny(normalized, config.exclude)) return [];
407
+
408
+ const extension = extname(normalized);
409
+ // Data is masked first so it can neither break a rule nor supply an allow
410
+ // directive; comments are masked after, for matching only, so a real
411
+ // directive comment is still read from `lines`.
412
+ const unserialized = ['.js', '.jsx', '.ts', '.tsx'].includes(extension)
413
+ ? maskSerializedJsonData(content, extension) : content;
414
+ const checked = maskComments(unserialized, extension);
415
+ const lines = unserialized.split('\n');
416
+ const violations = [];
417
+ for (const rule of config.rules) {
418
+ if (rule.include && !matchesAny(normalized, rule.include)) continue;
419
+ if (matchesAny(normalized, rule.exclude)) continue;
420
+
421
+ const matches = [];
422
+ if (rule.scope === 'file') {
423
+ const match = rule.regex.exec(checked);
424
+ if (match) matches.push(match.index);
425
+ } else {
426
+ rule.regex.lastIndex = 0;
427
+ let match;
428
+ while ((match = rule.regex.exec(checked)) !== null) {
429
+ matches.push(match.index);
430
+ if (match[0].length === 0) rule.regex.lastIndex += 1;
431
+ }
432
+ }
433
+
434
+ const seenLines = new Set();
435
+ for (const index of matches) {
436
+ const line = lineNumberAt(content, index);
437
+ if (seenLines.has(line)) continue;
438
+ seenLines.add(line);
439
+ const reason = findAllowDirective(lines, line, rule.id, config.allowDirective);
440
+ const allowed = reason !== null && reason.length >= config.allowReasonMinLength;
441
+ violations.push({
442
+ file: normalized,
443
+ line,
444
+ ruleId: rule.id,
445
+ severity: allowed ? 'warn' : rule.severity,
446
+ message: reason !== null && !allowed
447
+ ? `${rule.message} (a notis-design-allow directive needs a reason of at least ${config.allowReasonMinLength} characters)`
448
+ : rule.message,
449
+ allowed,
450
+ reason: allowed ? reason : null,
451
+ });
452
+ }
453
+ }
454
+ return violations;
455
+ }
456
+
457
+ export function collectProjectDesignViolations(projectDir, config = getCompiledDesignRules()) {
458
+ const files = [];
459
+ collectProjectFiles(projectDir, projectDir, files);
460
+ return files.flatMap((file) => collectDesignViolationsForFile(file.relPath, file.content, config));
461
+ }
462
+
463
+ export function collectSourceDesignViolations(files, config = getCompiledDesignRules(), { encoding = 'utf-8' } = {}) {
464
+ return Object.entries(files).flatMap(([relPath, rawContent]) => {
465
+ const content = Buffer.isBuffer(rawContent)
466
+ ? rawContent.toString('utf-8')
467
+ : typeof rawContent === 'string'
468
+ ? (encoding === 'base64' ? Buffer.from(rawContent, 'base64').toString('utf-8') : rawContent)
469
+ : String(rawContent ?? '');
470
+ return collectDesignViolationsForFile(relPath, content, config);
471
+ });
472
+ }
473
+
474
+ export function formatDesignViolation(violation) {
475
+ const prefix = violation.allowed ? 'allowed' : violation.severity;
476
+ const suffix = violation.allowed ? ` (${violation.reason})` : '';
477
+ return `${violation.file}:${violation.line} [${violation.ruleId}] ${prefix}: ${violation.message}${suffix}`;
478
+ }
479
+
480
+ function enforceDesign(violations, { enforce, log, subject }) {
481
+ const blocking = violations.filter((violation) => !violation.allowed && violation.severity === 'error');
482
+ const nonBlocking = violations.filter((violation) => !blocking.includes(violation));
483
+ if (typeof log === 'function') {
484
+ for (const violation of nonBlocking) log(`[design] ${formatDesignViolation(violation)}`);
485
+ if (!enforce) for (const violation of blocking) log(`[design] ${formatDesignViolation(violation)}`);
486
+ }
487
+ if (enforce && blocking.length > 0) {
488
+ throw usageError(
489
+ `${subject} violates the Notis ${subject === 'Space' ? 'Spaces' : 'app'} design bar (${blocking.length} issue${blocking.length === 1 ? '' : 's'}). `
490
+ + 'Fix each line below; the only override is an inline "// notis-design-allow: <rule-id> <reason>" comment on the line before.\n'
491
+ + blocking.map((violation) => ` - ${formatDesignViolation(violation)}`).join('\n'),
492
+ );
493
+ }
494
+ return violations;
495
+ }
496
+
497
+ /**
498
+ * Enforce the design rules on an app's or a Space's source tree.
499
+ *
500
+ * Returns every violation (allowed ones included) so callers can print
501
+ * warnings. When `enforce` is true (build, verify, screenshot, deploy) any
502
+ * unallowed error-severity violation aborts with a usage error that lists the
503
+ * exact file and line to fix. Dev servers pass `enforce: false` so a
504
+ * half-edited page still reloads. Spaces pass `profile: 'space'`.
505
+ */
506
+ export function validateProjectDesign(projectDir, { enforce = true, log = null, profile = 'app' } = {}) {
507
+ const violations = collectProjectDesignViolations(projectDir, getCompiledDesignRules(profile));
508
+ return enforceDesign(violations, { enforce, log, subject: profile === 'space' ? 'Space' : 'Project' });
509
+ }
510
+
511
+ /**
512
+ * The same check on an in-memory source snapshot (the exact bytes a Space
513
+ * release uploads). The server re-runs it with the same rules file on
514
+ * source-verify, source-stage and source-publish.
515
+ */
516
+ export function validateSourceDesign(files, { enforce = true, log = null, profile = 'space', encoding = 'utf-8' } = {}) {
517
+ const violations = collectSourceDesignViolations(files, getCompiledDesignRules(profile), { encoding });
518
+ return enforceDesign(violations, { enforce, log, subject: profile === 'space' ? 'Space' : 'Project' });
519
+ }