@notis_ai/cli 0.2.13 → 0.2.15

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 (295) hide show
  1. package/README.md +125 -21
  2. package/dist/agent-hooks/notis-agent-hook.mjs +19750 -0
  3. package/{skills → dist/base-skills}/notis-apps/SKILL.md +93 -58
  4. package/{skills → dist/base-skills}/notis-cli/SKILL.md +9 -9
  5. package/dist/base-skills/notis-query/SKILL.md +705 -0
  6. package/dist/skill-sync/index.js +1528 -0
  7. package/dist/skill-sync/index.js.map +7 -0
  8. package/package.json +5 -2
  9. package/skills/notis-apps/cli.md +44 -18
  10. package/skills/notis-cli/AGENT_INSTRUCTIONS.md +39 -0
  11. package/skills/notis-onboarding/BRIEF.md +16 -0
  12. package/src/agent-hook-entry.js +5 -0
  13. package/src/cli.js +23 -14
  14. package/src/command-specs/agents.js +392 -0
  15. package/src/command-specs/apps.js +1090 -418
  16. package/src/command-specs/auth.js +63 -10
  17. package/src/command-specs/handover.js +374 -0
  18. package/src/command-specs/index.js +9 -0
  19. package/src/command-specs/onboarding.js +69 -5
  20. package/src/command-specs/skills.js +56 -0
  21. package/src/command-specs/tools.js +6 -0
  22. package/src/runtime/agent-memory-state.js +126 -0
  23. package/src/runtime/agent-setup.js +383 -0
  24. package/src/runtime/app-dev-consumers.js +154 -0
  25. package/src/runtime/app-dev-host-lock.js +80 -0
  26. package/src/runtime/app-dev-roots.js +284 -0
  27. package/src/runtime/app-dev-server.js +229 -30
  28. package/src/runtime/app-dev-sessions.js +105 -145
  29. package/src/runtime/app-platform.js +1499 -161
  30. package/src/runtime/app-registry-scaffolds.js +367 -0
  31. package/src/runtime/base-skills.d.ts +20 -0
  32. package/src/runtime/base-skills.js +167 -0
  33. package/src/runtime/delegated-context.js +68 -0
  34. package/src/runtime/git.js +233 -0
  35. package/src/runtime/login-listener.js +15 -0
  36. package/src/runtime/oauth.js +1506 -141
  37. package/src/runtime/output.js +7 -0
  38. package/src/runtime/ports.js +16 -0
  39. package/src/runtime/profiles.js +0 -5
  40. package/src/runtime/skill-sync/cloud-client.ts +96 -0
  41. package/src/runtime/skill-sync/index.ts +644 -0
  42. package/src/runtime/skill-sync/local-scanner.ts +1046 -0
  43. package/src/runtime/skill-sync/symlink-manager.ts +383 -0
  44. package/src/runtime/skill-sync/sync-plan.ts +22 -0
  45. package/src/runtime/skill-sync/types.ts +103 -0
  46. package/src/runtime/skill-sync/write-cloud-skill.ts +50 -0
  47. package/src/runtime/store-screenshot.js +6 -1
  48. package/src/runtime/sync-skills.d.ts +37 -0
  49. package/src/runtime/sync-skills.js +215 -0
  50. package/src/runtime/transport.js +19 -2
  51. package/template/.harness/index.html.tmpl +116 -47
  52. package/template/app/layout.tsx +5 -1
  53. package/template/notis.config.ts +1 -0
  54. package/template/packages/sdk/package.json +2 -1
  55. package/template/packages/sdk/src/components/DocumentEditor.tsx +13 -3
  56. package/template/packages/sdk/src/components/MarkdownEditor.tsx +121 -0
  57. package/template/packages/sdk/src/components/MultiSelectActionBar.tsx +37 -119
  58. package/template/packages/sdk/src/components/MultiSelectCheckbox.tsx +5 -1
  59. package/template/packages/sdk/src/components/MultiSelectDragOverlay.tsx +1 -1
  60. package/template/packages/sdk/src/components/NotisSelectionBoundary.tsx +55 -0
  61. package/template/packages/sdk/src/components/ShortcutHints.tsx +56 -0
  62. package/template/packages/sdk/src/config.ts +71 -0
  63. package/template/packages/sdk/src/documents.ts +28 -1
  64. package/template/packages/sdk/src/hooks/useActiveResource.ts +19 -0
  65. package/template/packages/sdk/src/hooks/useCloudComputer.ts +97 -0
  66. package/template/packages/sdk/src/hooks/useCollectionInteractions.ts +709 -0
  67. package/template/packages/sdk/src/hooks/useDatabaseSubscription.ts +76 -0
  68. package/template/packages/sdk/src/hooks/useHandover.ts +78 -0
  69. package/template/packages/sdk/src/hooks/useMultiSelect.ts +44 -482
  70. package/template/packages/sdk/src/hooks/useTool.ts +5 -4
  71. package/template/packages/sdk/src/index.ts +64 -2
  72. package/template/packages/sdk/src/interactions/actions.ts +46 -0
  73. package/template/packages/sdk/src/interactions/shortcuts.tsx +628 -0
  74. package/template/packages/sdk/src/interactions.ts +41 -0
  75. package/template/packages/sdk/src/provider.tsx +2 -1
  76. package/template/packages/sdk/src/runtime.ts +219 -3
  77. package/dist/scaffolds/notis-database/CHANGELOG.md +0 -5
  78. package/dist/scaffolds/notis-database/app/globals.css +0 -44
  79. package/dist/scaffolds/notis-database/app/layout.tsx +0 -6
  80. package/dist/scaffolds/notis-database/app/page.tsx +0 -1088
  81. package/dist/scaffolds/notis-database/components/ui/badge.tsx +0 -28
  82. package/dist/scaffolds/notis-database/components/ui/button.tsx +0 -53
  83. package/dist/scaffolds/notis-database/components/ui/card.tsx +0 -56
  84. package/dist/scaffolds/notis-database/components/ui/table.tsx +0 -120
  85. package/dist/scaffolds/notis-database/components.json +0 -20
  86. package/dist/scaffolds/notis-database/index.html +0 -12
  87. package/dist/scaffolds/notis-database/lib/types.ts +0 -132
  88. package/dist/scaffolds/notis-database/lib/utils.ts +0 -6
  89. package/dist/scaffolds/notis-database/metadata/screenshot-1.png +0 -0
  90. package/dist/scaffolds/notis-database/metadata/screenshot-2.png +0 -0
  91. package/dist/scaffolds/notis-database/metadata/screenshot-3.png +0 -0
  92. package/dist/scaffolds/notis-database/metadata/screenshot-4.png +0 -0
  93. package/dist/scaffolds/notis-database/metadata/screenshot-5.png +0 -0
  94. package/dist/scaffolds/notis-database/metadata/screenshot-fixtures.json +0 -1839
  95. package/dist/scaffolds/notis-database/notis.config.ts +0 -73
  96. package/dist/scaffolds/notis-database/package-lock.json +0 -3935
  97. package/dist/scaffolds/notis-database/package.json +0 -32
  98. package/dist/scaffolds/notis-database/packages/sdk/package.json +0 -36
  99. package/dist/scaffolds/notis-database/packages/sdk/src/components/DocumentEditor.tsx +0 -93
  100. package/dist/scaffolds/notis-database/packages/sdk/src/components/Markdown.tsx +0 -60
  101. package/dist/scaffolds/notis-database/packages/sdk/src/components/MultiSelectActionBar.tsx +0 -278
  102. package/dist/scaffolds/notis-database/packages/sdk/src/components/MultiSelectCheckbox.tsx +0 -91
  103. package/dist/scaffolds/notis-database/packages/sdk/src/components/MultiSelectDragOverlay.tsx +0 -39
  104. package/dist/scaffolds/notis-database/packages/sdk/src/config.ts +0 -196
  105. package/dist/scaffolds/notis-database/packages/sdk/src/documents.ts +0 -229
  106. package/dist/scaffolds/notis-database/packages/sdk/src/hooks/useBackend.ts +0 -41
  107. package/dist/scaffolds/notis-database/packages/sdk/src/hooks/useDatabaseSchema.ts +0 -85
  108. package/dist/scaffolds/notis-database/packages/sdk/src/hooks/useDocument.ts +0 -78
  109. package/dist/scaffolds/notis-database/packages/sdk/src/hooks/useDocuments.ts +0 -121
  110. package/dist/scaffolds/notis-database/packages/sdk/src/hooks/useMultiSelect.ts +0 -539
  111. package/dist/scaffolds/notis-database/packages/sdk/src/hooks/useNotis.ts +0 -34
  112. package/dist/scaffolds/notis-database/packages/sdk/src/hooks/useNotisNavigation.ts +0 -49
  113. package/dist/scaffolds/notis-database/packages/sdk/src/hooks/useTool.ts +0 -64
  114. package/dist/scaffolds/notis-database/packages/sdk/src/hooks/useTools.ts +0 -56
  115. package/dist/scaffolds/notis-database/packages/sdk/src/hooks/useTopBarSearch.ts +0 -73
  116. package/dist/scaffolds/notis-database/packages/sdk/src/hooks/useUpsertDocument.ts +0 -95
  117. package/dist/scaffolds/notis-database/packages/sdk/src/index.ts +0 -83
  118. package/dist/scaffolds/notis-database/packages/sdk/src/provider.tsx +0 -43
  119. package/dist/scaffolds/notis-database/packages/sdk/src/runtime.ts +0 -220
  120. package/dist/scaffolds/notis-database/packages/sdk/src/styles.css +0 -186
  121. package/dist/scaffolds/notis-database/packages/sdk/src/ui.ts +0 -15
  122. package/dist/scaffolds/notis-database/packages/sdk/src/vite.ts +0 -56
  123. package/dist/scaffolds/notis-database/packages/sdk/tsconfig.json +0 -15
  124. package/dist/scaffolds/notis-database/postcss.config.mjs +0 -8
  125. package/dist/scaffolds/notis-database/src/dev-main.tsx +0 -23
  126. package/dist/scaffolds/notis-database/src/mock-runtime.ts +0 -561
  127. package/dist/scaffolds/notis-database/tailwind.config.ts +0 -59
  128. package/dist/scaffolds/notis-database/tsconfig.json +0 -23
  129. package/dist/scaffolds/notis-database/vite.config.ts +0 -22
  130. package/dist/scaffolds/notis-journal/CHANGELOG.md +0 -29
  131. package/dist/scaffolds/notis-journal/app/globals.css +0 -37
  132. package/dist/scaffolds/notis-journal/app/insights/page.tsx +0 -513
  133. package/dist/scaffolds/notis-journal/app/journal-core.tsx +0 -362
  134. package/dist/scaffolds/notis-journal/app/journal-ui.tsx +0 -337
  135. package/dist/scaffolds/notis-journal/app/layout.tsx +0 -6
  136. package/dist/scaffolds/notis-journal/app/page.tsx +0 -486
  137. package/dist/scaffolds/notis-journal/components/ui/badge.tsx +0 -28
  138. package/dist/scaffolds/notis-journal/components/ui/button.tsx +0 -53
  139. package/dist/scaffolds/notis-journal/components/ui/card.tsx +0 -56
  140. package/dist/scaffolds/notis-journal/components.json +0 -20
  141. package/dist/scaffolds/notis-journal/index.html +0 -12
  142. package/dist/scaffolds/notis-journal/lib/utils.ts +0 -6
  143. package/dist/scaffolds/notis-journal/metadata/screenshot-1.png +0 -0
  144. package/dist/scaffolds/notis-journal/metadata/screenshot-2.png +0 -0
  145. package/dist/scaffolds/notis-journal/metadata/screenshot-3.png +0 -0
  146. package/dist/scaffolds/notis-journal/metadata/screenshot-4.png +0 -0
  147. package/dist/scaffolds/notis-journal/metadata/screenshot-5.png +0 -0
  148. package/dist/scaffolds/notis-journal/metadata/screenshot-6.png +0 -0
  149. package/dist/scaffolds/notis-journal/metadata/screenshot-fixtures.json +0 -132
  150. package/dist/scaffolds/notis-journal/notis.config.ts +0 -93
  151. package/dist/scaffolds/notis-journal/package-lock.json +0 -4615
  152. package/dist/scaffolds/notis-journal/package.json +0 -34
  153. package/dist/scaffolds/notis-journal/packages/sdk/package.json +0 -36
  154. package/dist/scaffolds/notis-journal/packages/sdk/src/components/DocumentEditor.tsx +0 -93
  155. package/dist/scaffolds/notis-journal/packages/sdk/src/components/Markdown.tsx +0 -60
  156. package/dist/scaffolds/notis-journal/packages/sdk/src/components/MultiSelectActionBar.tsx +0 -278
  157. package/dist/scaffolds/notis-journal/packages/sdk/src/components/MultiSelectCheckbox.tsx +0 -91
  158. package/dist/scaffolds/notis-journal/packages/sdk/src/components/MultiSelectDragOverlay.tsx +0 -39
  159. package/dist/scaffolds/notis-journal/packages/sdk/src/config.ts +0 -196
  160. package/dist/scaffolds/notis-journal/packages/sdk/src/documents.ts +0 -229
  161. package/dist/scaffolds/notis-journal/packages/sdk/src/hooks/useBackend.ts +0 -41
  162. package/dist/scaffolds/notis-journal/packages/sdk/src/hooks/useDatabaseSchema.ts +0 -85
  163. package/dist/scaffolds/notis-journal/packages/sdk/src/hooks/useDocument.ts +0 -78
  164. package/dist/scaffolds/notis-journal/packages/sdk/src/hooks/useDocuments.ts +0 -121
  165. package/dist/scaffolds/notis-journal/packages/sdk/src/hooks/useMultiSelect.ts +0 -539
  166. package/dist/scaffolds/notis-journal/packages/sdk/src/hooks/useNotis.ts +0 -34
  167. package/dist/scaffolds/notis-journal/packages/sdk/src/hooks/useNotisNavigation.ts +0 -49
  168. package/dist/scaffolds/notis-journal/packages/sdk/src/hooks/useTool.ts +0 -64
  169. package/dist/scaffolds/notis-journal/packages/sdk/src/hooks/useTools.ts +0 -56
  170. package/dist/scaffolds/notis-journal/packages/sdk/src/hooks/useTopBarSearch.ts +0 -73
  171. package/dist/scaffolds/notis-journal/packages/sdk/src/hooks/useUpsertDocument.ts +0 -95
  172. package/dist/scaffolds/notis-journal/packages/sdk/src/index.ts +0 -83
  173. package/dist/scaffolds/notis-journal/packages/sdk/src/provider.tsx +0 -43
  174. package/dist/scaffolds/notis-journal/packages/sdk/src/runtime.ts +0 -220
  175. package/dist/scaffolds/notis-journal/packages/sdk/src/styles.css +0 -186
  176. package/dist/scaffolds/notis-journal/packages/sdk/src/ui.ts +0 -15
  177. package/dist/scaffolds/notis-journal/packages/sdk/src/vite.ts +0 -56
  178. package/dist/scaffolds/notis-journal/packages/sdk/tsconfig.json +0 -15
  179. package/dist/scaffolds/notis-journal/postcss.config.mjs +0 -8
  180. package/dist/scaffolds/notis-journal/skills/journal-onboarding/SKILL.md +0 -120
  181. package/dist/scaffolds/notis-journal/src/dev-main.tsx +0 -58
  182. package/dist/scaffolds/notis-journal/src/mock-runtime.ts +0 -197
  183. package/dist/scaffolds/notis-journal/tailwind.config.ts +0 -58
  184. package/dist/scaffolds/notis-journal/tsconfig.json +0 -23
  185. package/dist/scaffolds/notis-journal/vite.config.ts +0 -10
  186. package/dist/scaffolds/notis-notes/CHANGELOG.md +0 -5
  187. package/dist/scaffolds/notis-notes/app/globals.css +0 -3
  188. package/dist/scaffolds/notis-notes/app/layout.tsx +0 -6
  189. package/dist/scaffolds/notis-notes/app/page.tsx +0 -1943
  190. package/dist/scaffolds/notis-notes/app/phosphor-icons.ts +0 -596
  191. package/dist/scaffolds/notis-notes/components/ui/badge.tsx +0 -28
  192. package/dist/scaffolds/notis-notes/components/ui/button.tsx +0 -53
  193. package/dist/scaffolds/notis-notes/components/ui/card.tsx +0 -56
  194. package/dist/scaffolds/notis-notes/components.json +0 -20
  195. package/dist/scaffolds/notis-notes/lib/utils.ts +0 -6
  196. package/dist/scaffolds/notis-notes/lib/visible-properties.ts +0 -144
  197. package/dist/scaffolds/notis-notes/metadata/screenshot-1.png +0 -0
  198. package/dist/scaffolds/notis-notes/metadata/screenshot-2.png +0 -0
  199. package/dist/scaffolds/notis-notes/metadata/screenshot-3.png +0 -0
  200. package/dist/scaffolds/notis-notes/metadata/screenshot-4.png +0 -0
  201. package/dist/scaffolds/notis-notes/metadata/screenshot-5.png +0 -0
  202. package/dist/scaffolds/notis-notes/metadata/screenshot-fixtures.json +0 -752
  203. package/dist/scaffolds/notis-notes/notis.config.ts +0 -80
  204. package/dist/scaffolds/notis-notes/package-lock.json +0 -4636
  205. package/dist/scaffolds/notis-notes/package.json +0 -35
  206. package/dist/scaffolds/notis-notes/packages/sdk/package.json +0 -36
  207. package/dist/scaffolds/notis-notes/packages/sdk/src/components/DocumentEditor.tsx +0 -93
  208. package/dist/scaffolds/notis-notes/packages/sdk/src/components/Markdown.tsx +0 -60
  209. package/dist/scaffolds/notis-notes/packages/sdk/src/components/MultiSelectActionBar.tsx +0 -278
  210. package/dist/scaffolds/notis-notes/packages/sdk/src/components/MultiSelectCheckbox.tsx +0 -91
  211. package/dist/scaffolds/notis-notes/packages/sdk/src/components/MultiSelectDragOverlay.tsx +0 -39
  212. package/dist/scaffolds/notis-notes/packages/sdk/src/config.ts +0 -196
  213. package/dist/scaffolds/notis-notes/packages/sdk/src/documents.ts +0 -229
  214. package/dist/scaffolds/notis-notes/packages/sdk/src/hooks/useBackend.ts +0 -41
  215. package/dist/scaffolds/notis-notes/packages/sdk/src/hooks/useDatabaseSchema.ts +0 -85
  216. package/dist/scaffolds/notis-notes/packages/sdk/src/hooks/useDocument.ts +0 -78
  217. package/dist/scaffolds/notis-notes/packages/sdk/src/hooks/useDocuments.ts +0 -121
  218. package/dist/scaffolds/notis-notes/packages/sdk/src/hooks/useMultiSelect.ts +0 -539
  219. package/dist/scaffolds/notis-notes/packages/sdk/src/hooks/useNotis.ts +0 -34
  220. package/dist/scaffolds/notis-notes/packages/sdk/src/hooks/useNotisNavigation.ts +0 -49
  221. package/dist/scaffolds/notis-notes/packages/sdk/src/hooks/useTool.ts +0 -64
  222. package/dist/scaffolds/notis-notes/packages/sdk/src/hooks/useTools.ts +0 -56
  223. package/dist/scaffolds/notis-notes/packages/sdk/src/hooks/useTopBarSearch.ts +0 -73
  224. package/dist/scaffolds/notis-notes/packages/sdk/src/hooks/useUpsertDocument.ts +0 -95
  225. package/dist/scaffolds/notis-notes/packages/sdk/src/index.ts +0 -83
  226. package/dist/scaffolds/notis-notes/packages/sdk/src/provider.tsx +0 -43
  227. package/dist/scaffolds/notis-notes/packages/sdk/src/runtime.ts +0 -220
  228. package/dist/scaffolds/notis-notes/packages/sdk/src/styles.css +0 -186
  229. package/dist/scaffolds/notis-notes/packages/sdk/src/ui.ts +0 -15
  230. package/dist/scaffolds/notis-notes/packages/sdk/src/vite.ts +0 -56
  231. package/dist/scaffolds/notis-notes/packages/sdk/tsconfig.json +0 -15
  232. package/dist/scaffolds/notis-notes/postcss.config.mjs +0 -8
  233. package/dist/scaffolds/notis-notes/tailwind.config.ts +0 -58
  234. package/dist/scaffolds/notis-notes/tsconfig.json +0 -23
  235. package/dist/scaffolds/notis-notes/vite.config.ts +0 -10
  236. package/dist/scaffolds/notis-random/CHANGELOG.md +0 -15
  237. package/dist/scaffolds/notis-random/README.md +0 -33
  238. package/dist/scaffolds/notis-random/app/globals.css +0 -11
  239. package/dist/scaffolds/notis-random/app/history/page.tsx +0 -67
  240. package/dist/scaffolds/notis-random/app/layout.tsx +0 -7
  241. package/dist/scaffolds/notis-random/app/page.tsx +0 -289
  242. package/dist/scaffolds/notis-random/components/ui/button.tsx +0 -50
  243. package/dist/scaffolds/notis-random/components/ui/card.tsx +0 -16
  244. package/dist/scaffolds/notis-random/components/ui/input.tsx +0 -23
  245. package/dist/scaffolds/notis-random/components.json +0 -20
  246. package/dist/scaffolds/notis-random/index.html +0 -12
  247. package/dist/scaffolds/notis-random/lib/notis-tools.ts +0 -128
  248. package/dist/scaffolds/notis-random/lib/rng.ts +0 -202
  249. package/dist/scaffolds/notis-random/lib/roll-record.ts +0 -189
  250. package/dist/scaffolds/notis-random/lib/utils.ts +0 -25
  251. package/dist/scaffolds/notis-random/metadata/screenshot-1.png +0 -0
  252. package/dist/scaffolds/notis-random/metadata/screenshot-2.png +0 -0
  253. package/dist/scaffolds/notis-random/metadata/screenshot-3.png +0 -0
  254. package/dist/scaffolds/notis-random/metadata/screenshot-4.png +0 -0
  255. package/dist/scaffolds/notis-random/metadata/screenshot-5.png +0 -0
  256. package/dist/scaffolds/notis-random/metadata/screenshot-fixtures.json +0 -753
  257. package/dist/scaffolds/notis-random/notis.config.ts +0 -86
  258. package/dist/scaffolds/notis-random/package-lock.json +0 -4513
  259. package/dist/scaffolds/notis-random/package.json +0 -36
  260. package/dist/scaffolds/notis-random/packages/sdk/package.json +0 -36
  261. package/dist/scaffolds/notis-random/packages/sdk/src/components/DocumentEditor.tsx +0 -93
  262. package/dist/scaffolds/notis-random/packages/sdk/src/components/Markdown.tsx +0 -60
  263. package/dist/scaffolds/notis-random/packages/sdk/src/components/MultiSelectActionBar.tsx +0 -278
  264. package/dist/scaffolds/notis-random/packages/sdk/src/components/MultiSelectCheckbox.tsx +0 -91
  265. package/dist/scaffolds/notis-random/packages/sdk/src/components/MultiSelectDragOverlay.tsx +0 -39
  266. package/dist/scaffolds/notis-random/packages/sdk/src/config.ts +0 -196
  267. package/dist/scaffolds/notis-random/packages/sdk/src/documents.ts +0 -229
  268. package/dist/scaffolds/notis-random/packages/sdk/src/hooks/useBackend.ts +0 -41
  269. package/dist/scaffolds/notis-random/packages/sdk/src/hooks/useDatabaseSchema.ts +0 -85
  270. package/dist/scaffolds/notis-random/packages/sdk/src/hooks/useDocument.ts +0 -78
  271. package/dist/scaffolds/notis-random/packages/sdk/src/hooks/useDocuments.ts +0 -121
  272. package/dist/scaffolds/notis-random/packages/sdk/src/hooks/useMultiSelect.ts +0 -539
  273. package/dist/scaffolds/notis-random/packages/sdk/src/hooks/useNotis.ts +0 -34
  274. package/dist/scaffolds/notis-random/packages/sdk/src/hooks/useNotisNavigation.ts +0 -49
  275. package/dist/scaffolds/notis-random/packages/sdk/src/hooks/useTool.ts +0 -64
  276. package/dist/scaffolds/notis-random/packages/sdk/src/hooks/useTools.ts +0 -56
  277. package/dist/scaffolds/notis-random/packages/sdk/src/hooks/useTopBarSearch.ts +0 -73
  278. package/dist/scaffolds/notis-random/packages/sdk/src/hooks/useUpsertDocument.ts +0 -95
  279. package/dist/scaffolds/notis-random/packages/sdk/src/index.ts +0 -83
  280. package/dist/scaffolds/notis-random/packages/sdk/src/provider.tsx +0 -43
  281. package/dist/scaffolds/notis-random/packages/sdk/src/runtime.ts +0 -220
  282. package/dist/scaffolds/notis-random/packages/sdk/src/styles.css +0 -186
  283. package/dist/scaffolds/notis-random/packages/sdk/src/ui.ts +0 -15
  284. package/dist/scaffolds/notis-random/packages/sdk/src/vite.ts +0 -56
  285. package/dist/scaffolds/notis-random/packages/sdk/tsconfig.json +0 -15
  286. package/dist/scaffolds/notis-random/postcss.config.mjs +0 -6
  287. package/dist/scaffolds/notis-random/src/dev-main.tsx +0 -70
  288. package/dist/scaffolds/notis-random/src/mock-runtime.ts +0 -129
  289. package/dist/scaffolds/notis-random/tailwind.config.ts +0 -50
  290. package/dist/scaffolds/notis-random/tsconfig.json +0 -23
  291. package/dist/scaffolds/notis-random/vite.config.ts +0 -11
  292. package/dist/scaffolds.json +0 -49
  293. package/template/metadata/screenshot-1.png +0 -0
  294. package/template/metadata/screenshot-2.png +0 -0
  295. package/template/metadata/screenshot-3.png +0 -0
@@ -0,0 +1,284 @@
1
+ import { randomUUID } from 'node:crypto';
2
+ import {
3
+ existsSync,
4
+ lstatSync,
5
+ mkdirSync,
6
+ readFileSync,
7
+ readdirSync,
8
+ realpathSync,
9
+ renameSync,
10
+ rmSync,
11
+ statSync,
12
+ writeFileSync,
13
+ } from 'node:fs';
14
+ import { homedir } from 'node:os';
15
+ import { dirname, join, parse as parsePath, resolve } from 'node:path';
16
+
17
+ import { usageError } from './errors.js';
18
+ import { CONFIG_DIR } from './profiles.js';
19
+
20
+ export const APP_DEV_ROOTS_VERSION = 1;
21
+ export const DEFAULT_APP_DEV_ROOT = join(homedir(), '.notis', 'apps');
22
+ export const DEFAULT_APP_DEV_ROOTS_FILE = join(CONFIG_DIR, 'app-dev-roots.json');
23
+ const CONFIG_FILENAMES = ['notis.config.ts', 'notis.config.js', 'notis.config.mjs'];
24
+ const LOCK_TIMEOUT_MS = 5_000;
25
+ const lockWait = new Int32Array(new SharedArrayBuffer(4));
26
+
27
+ function rootsFile(filePath) {
28
+ if (filePath) return resolve(filePath);
29
+ const envPath = process.env.NOTIS_APP_DEV_ROOTS_FILE;
30
+ return typeof envPath === 'string' && envPath.trim()
31
+ ? resolve(envPath.trim())
32
+ : DEFAULT_APP_DEV_ROOTS_FILE;
33
+ }
34
+
35
+ function legacyProjectsFile(filePath) {
36
+ const sessionsFile = filePath
37
+ || join(CONFIG_DIR, 'app-dev-sessions.json');
38
+ const parsed = parsePath(resolve(sessionsFile));
39
+ return join(parsed.dir, `${parsed.name}-projects${parsed.ext || '.json'}`);
40
+ }
41
+
42
+ function readLockOwner(lockPath) {
43
+ try {
44
+ return readFileSync(join(lockPath, 'owner'), 'utf8').trim();
45
+ } catch {
46
+ return null;
47
+ }
48
+ }
49
+
50
+ function withLock(filePath, callback) {
51
+ mkdirSync(dirname(filePath), { recursive: true, mode: 0o700 });
52
+ const lockPath = `${filePath}.lock`;
53
+ const owner = `${process.pid}.${randomUUID()}`;
54
+ const startedAt = Date.now();
55
+ while (true) {
56
+ try {
57
+ mkdirSync(lockPath, { mode: 0o700 });
58
+ writeFileSync(join(lockPath, 'owner'), owner, { mode: 0o600 });
59
+ break;
60
+ } catch (error) {
61
+ if (error?.code !== 'EEXIST') throw error;
62
+ try {
63
+ const stat = lstatSync(lockPath);
64
+ if (!stat.isDirectory() || stat.isSymbolicLink()) {
65
+ throw new Error(`Refusing unsafe app development roots lock: ${lockPath}`);
66
+ }
67
+ const existingOwner = readLockOwner(lockPath);
68
+ const pid = Number.parseInt(String(existingOwner || '').split('.')[0] || '', 10);
69
+ if (Number.isInteger(pid) && pid > 0) {
70
+ try {
71
+ process.kill(pid, 0);
72
+ } catch (ownerError) {
73
+ if (ownerError?.code === 'ESRCH') {
74
+ rmSync(lockPath, { recursive: true, force: true });
75
+ continue;
76
+ }
77
+ }
78
+ }
79
+ } catch (statError) {
80
+ if (statError?.code === 'ENOENT') continue;
81
+ throw statError;
82
+ }
83
+ if (Date.now() - startedAt >= LOCK_TIMEOUT_MS) {
84
+ throw new Error(`Timed out waiting for app development roots lock: ${filePath}`);
85
+ }
86
+ Atomics.wait(lockWait, 0, 0, 10);
87
+ }
88
+ }
89
+ try {
90
+ return callback();
91
+ } finally {
92
+ if (readLockOwner(lockPath) === owner) {
93
+ rmSync(lockPath, { recursive: true, force: true });
94
+ }
95
+ }
96
+ }
97
+
98
+ function canonicalExistingDirectory(inputPath) {
99
+ const absolute = resolve(String(inputPath || ''));
100
+ let stat;
101
+ try {
102
+ stat = statSync(absolute);
103
+ } catch {
104
+ throw usageError(`App development root does not exist: ${absolute}`);
105
+ }
106
+ if (!stat.isDirectory()) {
107
+ throw usageError(`App development root is not a directory: ${absolute}`);
108
+ }
109
+ return realpathSync(absolute);
110
+ }
111
+
112
+ function normalizeRegistry(raw) {
113
+ const roots = Array.isArray(raw?.roots) ? raw.roots : [];
114
+ const seen = new Set();
115
+ return roots
116
+ .map((entry) => typeof entry === 'string' ? { path: entry } : entry)
117
+ .filter((entry) => entry && typeof entry.path === 'string' && entry.path.trim())
118
+ .map((entry) => ({
119
+ path: resolve(entry.path),
120
+ registeredAt: typeof entry.registeredAt === 'string'
121
+ ? entry.registeredAt
122
+ : new Date(0).toISOString(),
123
+ }))
124
+ .filter((entry) => {
125
+ if (seen.has(entry.path)) return false;
126
+ seen.add(entry.path);
127
+ return true;
128
+ });
129
+ }
130
+
131
+ function readRaw(filePath) {
132
+ if (!existsSync(filePath)) return { version: APP_DEV_ROOTS_VERSION, roots: [] };
133
+ try {
134
+ return {
135
+ version: APP_DEV_ROOTS_VERSION,
136
+ roots: normalizeRegistry(JSON.parse(readFileSync(filePath, 'utf8'))),
137
+ };
138
+ } catch {
139
+ return { version: APP_DEV_ROOTS_VERSION, roots: [] };
140
+ }
141
+ }
142
+
143
+ function writeRaw(registry, filePath) {
144
+ mkdirSync(dirname(filePath), { recursive: true, mode: 0o700 });
145
+ const normalized = {
146
+ version: APP_DEV_ROOTS_VERSION,
147
+ roots: normalizeRegistry(registry),
148
+ };
149
+ const temporary = `${filePath}.${process.pid}.${randomUUID()}.tmp`;
150
+ writeFileSync(temporary, JSON.stringify(normalized, null, 2), { mode: 0o600 });
151
+ renameSync(temporary, filePath);
152
+ return normalized;
153
+ }
154
+
155
+ function migrateLegacyProjectsUnlocked(registry, options) {
156
+ const path = legacyProjectsFile(options.legacySessionsFilePath);
157
+ if (!existsSync(path)) return registry;
158
+ let projects = [];
159
+ try {
160
+ const raw = JSON.parse(readFileSync(path, 'utf8'));
161
+ projects = Array.isArray(raw?.projects) ? raw.projects : [];
162
+ } catch {
163
+ return registry;
164
+ }
165
+ const byPath = new Map(registry.roots.map((entry) => [entry.path, entry]));
166
+ for (const project of projects) {
167
+ if (typeof project?.projectDir !== 'string' || !project.projectDir.trim()) continue;
168
+ try {
169
+ const path = canonicalExistingDirectory(project.projectDir);
170
+ if (path === realpathIfExists(DEFAULT_APP_DEV_ROOT)) continue;
171
+ if (!byPath.has(path)) {
172
+ byPath.set(path, {
173
+ path,
174
+ registeredAt: typeof project.lastMountedAt === 'string'
175
+ ? project.lastMountedAt
176
+ : new Date().toISOString(),
177
+ });
178
+ }
179
+ } catch {
180
+ // Deleted or inaccessible legacy projects are intentionally not migrated.
181
+ }
182
+ }
183
+ rmSync(path, { force: true });
184
+ return { version: APP_DEV_ROOTS_VERSION, roots: [...byPath.values()] };
185
+ }
186
+
187
+ function realpathIfExists(path) {
188
+ try {
189
+ return realpathSync(path);
190
+ } catch {
191
+ return resolve(path);
192
+ }
193
+ }
194
+
195
+ export function readAppDevRoots(options = {}) {
196
+ const filePath = rootsFile(options.filePath);
197
+ let registry = readRaw(filePath);
198
+ if (options.migrateLegacy !== false) {
199
+ registry = withLock(filePath, () => {
200
+ const before = readRaw(filePath);
201
+ const after = migrateLegacyProjectsUnlocked(before, options);
202
+ const changed = JSON.stringify(before.roots) !== JSON.stringify(after.roots);
203
+ return changed ? writeRaw(after, filePath) : before;
204
+ });
205
+ }
206
+ const implicit = realpathIfExists(options.defaultRoot || DEFAULT_APP_DEV_ROOT);
207
+ const roots = [
208
+ { path: implicit, registeredAt: null, implicit: true },
209
+ ...registry.roots
210
+ .filter((entry) => entry.path !== implicit)
211
+ .map((entry) => ({ ...entry, implicit: false })),
212
+ ];
213
+ return { version: APP_DEV_ROOTS_VERSION, roots };
214
+ }
215
+
216
+ export function registerAppDevRoot(inputPath, options = {}) {
217
+ const path = canonicalExistingDirectory(inputPath);
218
+ const implicit = realpathIfExists(options.defaultRoot || DEFAULT_APP_DEV_ROOT);
219
+ if (path === implicit) return readAppDevRoots(options);
220
+ const filePath = rootsFile(options.filePath);
221
+ withLock(filePath, () => {
222
+ const registry = readRaw(filePath);
223
+ if (!registry.roots.some((entry) => entry.path === path)) {
224
+ registry.roots.push({ path, registeredAt: new Date().toISOString() });
225
+ writeRaw(registry, filePath);
226
+ }
227
+ });
228
+ return readAppDevRoots({ ...options, migrateLegacy: false });
229
+ }
230
+
231
+ export function removeAppDevRoot(inputPath, options = {}) {
232
+ const path = realpathIfExists(resolve(String(inputPath || '')));
233
+ const implicit = realpathIfExists(options.defaultRoot || DEFAULT_APP_DEV_ROOT);
234
+ if (path === implicit) {
235
+ throw usageError(`The default app development root cannot be removed: ${implicit}`);
236
+ }
237
+ const filePath = rootsFile(options.filePath);
238
+ let removed = false;
239
+ withLock(filePath, () => {
240
+ const registry = readRaw(filePath);
241
+ const next = registry.roots.filter((entry) => entry.path !== path);
242
+ removed = next.length !== registry.roots.length;
243
+ if (removed) writeRaw({ ...registry, roots: next }, filePath);
244
+ });
245
+ return { removed, path, registry: readAppDevRoots({ ...options, migrateLegacy: false }) };
246
+ }
247
+
248
+ function hasConfig(path) {
249
+ return CONFIG_FILENAMES.some((name) => existsSync(join(path, name)));
250
+ }
251
+
252
+ function childAppDirs(parent) {
253
+ if (!existsSync(parent)) return [];
254
+ let entries = [];
255
+ try {
256
+ entries = readdirSync(parent, { withFileTypes: true });
257
+ } catch {
258
+ return [];
259
+ }
260
+ return entries
261
+ .filter((entry) => entry.isDirectory())
262
+ .filter((entry) => !entry.name.startsWith('.') && !entry.name.startsWith('_'))
263
+ .filter((entry) => entry.name !== 'node_modules')
264
+ .map((entry) => join(parent, entry.name))
265
+ .filter(hasConfig);
266
+ }
267
+
268
+ export function discoverAppProjectsInRoot(inputRoot) {
269
+ const root = realpathIfExists(resolve(inputRoot));
270
+ const candidates = [];
271
+ if (hasConfig(root)) candidates.push(root);
272
+ candidates.push(...childAppDirs(root));
273
+ candidates.push(...childAppDirs(join(root, 'apps')));
274
+ return [...new Set(candidates.map(realpathIfExists))].sort();
275
+ }
276
+
277
+ export function discoverRegisteredAppProjects(options = {}) {
278
+ const roots = readAppDevRoots(options).roots;
279
+ return [...new Set(roots.flatMap((entry) => discoverAppProjectsInRoot(entry.path)))].sort();
280
+ }
281
+
282
+ export function getAppDevRootsFile(filePath) {
283
+ return rootsFile(filePath);
284
+ }
@@ -28,7 +28,10 @@ import {
28
28
  readLinkedState,
29
29
  writeLinkedState,
30
30
  } from './app-platform.js';
31
- import { linkAppDevSessionTarget } from './app-dev-sessions.js';
31
+ import {
32
+ linkAppDevSessionTarget,
33
+ readAppDevSessions,
34
+ } from './app-dev-sessions.js';
32
35
 
33
36
  const CONTENT_TYPES = {
34
37
  '.js': 'application/javascript; charset=utf-8',
@@ -251,17 +254,26 @@ function buildHarnessDescriptor({ state, manifest, appConfig, route, scenario =
251
254
  };
252
255
  }
253
256
 
257
+ function plainObject(value) {
258
+ return value && typeof value === 'object' && !Array.isArray(value) ? value : {};
259
+ }
260
+
261
+ /**
262
+ * Resolve the fixture payload injected into one harness page load.
263
+ *
264
+ * A scenario may override individual `tools` / `requests` keys on top of the
265
+ * file-level defaults, which is how one route renders both its populated and
266
+ * its empty state. Each capture is its own page load, so a shallow per-key
267
+ * merge is all the isolation a scenario needs.
268
+ */
254
269
  function harnessFixtures(projectDir, scenario) {
255
270
  const fixtureConfig = readJsonFile(join(projectDir, 'metadata', 'screenshot-fixtures.json')) || {};
256
- const scenarios = fixtureConfig.scenarios && typeof fixtureConfig.scenarios === 'object'
257
- ? fixtureConfig.scenarios
258
- : {};
271
+ const scenarios = plainObject(fixtureConfig.scenarios);
272
+ const selected = scenario ? plainObject(scenarios[scenario]) : null;
259
273
  return {
260
- tools: fixtureConfig.tools && typeof fixtureConfig.tools === 'object' ? fixtureConfig.tools : {},
261
- requests: fixtureConfig.requests && typeof fixtureConfig.requests === 'object' ? fixtureConfig.requests : {},
262
- scenario: scenario && scenarios[scenario] && typeof scenarios[scenario] === 'object'
263
- ? scenarios[scenario]
264
- : null,
274
+ tools: { ...plainObject(fixtureConfig.tools), ...plainObject(selected?.tools) },
275
+ requests: { ...plainObject(fixtureConfig.requests), ...plainObject(selected?.requests) },
276
+ scenario: selected && Object.keys(selected).length > 0 ? selected : null,
265
277
  };
266
278
  }
267
279
 
@@ -288,7 +300,7 @@ function renderHarnessHtml({ state, manifest, appConfig, route, harnessOptions,
288
300
  /**
289
301
  * Start the dev server for one or more apps.
290
302
  *
291
- * @param {{apps: Array<{slug: string, projectDir: string, appId?: string, targetAppId?: string, userId?: string}>, port: number, watch?: boolean, sessionsFilePath?: string, harness?: { mode?: string, apiBase?: string, jwt?: string }, log?: (m: string) => void, logError?: (m: string) => void}} options
303
+ * @param {{apps: Array<{slug: string, projectDir: string, appId?: string, targetAppId?: string, userId?: string, profileKey?: string, sessionId?: string, mountNonce?: string}>, port: number, watch?: boolean, sessionsFilePath?: string, harness?: { mode?: string, apiBase?: string, jwt?: string }, log?: (m: string) => void, logError?: (m: string) => void}} options
292
304
  */
293
305
  export async function startAppDevServer({
294
306
  apps,
@@ -304,22 +316,43 @@ export async function startAppDevServer({
304
316
  }
305
317
 
306
318
  const appState = new Map();
307
- for (const app of apps) {
308
- appState.set(app.slug, {
319
+ // Sidebar reconciliation needs one stream for the shared host, not one
320
+ // long-lived HTTP/1.1 connection per discovered app. Keeping a stream per
321
+ // app exhausts Chromium's per-origin connection pool and can indefinitely
322
+ // queue the active view's bundle fetch during hot reload.
323
+ const hostSseClients = new Set();
324
+ const createAppState = (app) => {
325
+ let resolveBundleReady;
326
+ const bundleReadyPromise = new Promise((resolvePromise) => {
327
+ resolveBundleReady = resolvePromise;
328
+ });
329
+ return {
309
330
  slug: app.slug,
310
331
  projectDir: app.projectDir,
311
332
  appId: app.appId || null,
312
333
  targetAppId: app.targetAppId || null,
313
334
  userId: app.userId || null,
335
+ profileKey: app.profileKey || null,
336
+ sessionId: app.sessionId || null,
337
+ mountNonce: app.mountNonce || null,
314
338
  canonicalBundleDir: getBundleDir(app.projectDir),
315
339
  bundleDir: null,
316
340
  jsPath: null,
317
341
  sseClients: new Set(),
318
342
  watcher: null,
343
+ sourceWatchers: [],
344
+ prepareTimer: null,
345
+ reloadTimer: null,
319
346
  buildProcess: null,
320
347
  lastMtimeMs: 0,
321
348
  watchPollTimer: null,
322
- });
349
+ bundleReady: false,
350
+ bundleReadyPromise,
351
+ resolveBundleReady,
352
+ };
353
+ };
354
+ for (const app of apps) {
355
+ appState.set(app.slug, createAppState(app));
323
356
  }
324
357
 
325
358
  function broadcastReload(slug) {
@@ -333,6 +366,14 @@ export async function startAppDevServer({
333
366
  state.sseClients.delete(res);
334
367
  }
335
368
  }
369
+ const hostPayload = `event: reload\ndata: ${JSON.stringify({ slug, at: Date.now() })}\n\n`;
370
+ for (const res of hostSseClients) {
371
+ try {
372
+ res.write(hostPayload);
373
+ } catch {
374
+ hostSseClients.delete(res);
375
+ }
376
+ }
336
377
  }
337
378
 
338
379
  function matchAppRoute(pathname) {
@@ -365,6 +406,10 @@ export async function startAppDevServer({
365
406
  state.jsPath = join(bundleDir, 'app.js');
366
407
  try {
367
408
  state.lastMtimeMs = statSync(state.jsPath).mtimeMs;
409
+ if (!state.bundleReady) {
410
+ state.bundleReady = true;
411
+ state.resolveBundleReady();
412
+ }
368
413
  } catch {
369
414
  state.lastMtimeMs = 0;
370
415
  }
@@ -442,6 +487,9 @@ export async function startAppDevServer({
442
487
  try {
443
488
  const body = await readRequestJson(req);
444
489
  const appId = typeof body.app_id === 'string' ? body.app_id.trim() : '';
490
+ const version = Number.isInteger(body.version) && body.version > 0
491
+ ? body.version
492
+ : null;
445
493
  if (!appId) {
446
494
  res.writeHead(400, {
447
495
  ...headers,
@@ -450,20 +498,69 @@ export async function startAppDevServer({
450
498
  res.end(JSON.stringify({ error: 'app_id is required' }));
451
499
  return;
452
500
  }
501
+ if (version === null) {
502
+ res.writeHead(400, {
503
+ ...headers,
504
+ 'Content-Type': 'application/json; charset=utf-8',
505
+ });
506
+ res.end(JSON.stringify({ error: 'version must be a positive integer' }));
507
+ return;
508
+ }
509
+ const proofSession = readAppDevSessions(sessionsFilePath).sessions.find((session) => (
510
+ typeof body.session_id === 'string'
511
+ && typeof body.mount_nonce === 'string'
512
+ && session.sessionId === body.session_id
513
+ && session.mountNonce === body.mount_nonce
514
+ && session.devSlug === state.slug
515
+ && session.projectDir === state.projectDir
516
+ ));
517
+ if (!proofSession) {
518
+ res.writeHead(403, {
519
+ ...headers,
520
+ 'Content-Type': 'application/json; charset=utf-8',
521
+ });
522
+ res.end(JSON.stringify({ error: 'invalid app development session proof' }));
523
+ return;
524
+ }
525
+ const expectedAppId = proofSession.targetAppId || proofSession.appId;
526
+ if (!expectedAppId || appId !== expectedAppId) {
527
+ res.writeHead(409, {
528
+ ...headers,
529
+ 'Content-Type': 'application/json; charset=utf-8',
530
+ });
531
+ res.end(JSON.stringify({ error: 'app_id does not match this development session' }));
532
+ return;
533
+ }
453
534
 
454
- const currentState = readLinkedState(state.projectDir) || {};
535
+ const currentState = readLinkedState(
536
+ state.projectDir,
537
+ proofSession.profileKey || state.profileKey,
538
+ ) || {};
455
539
  const now = new Date().toISOString();
456
- writeLinkedState(state.projectDir, {
540
+ const promotedInPlace = proofSession.appId === appId;
541
+ const nextState = {
457
542
  ...currentState,
458
543
  app_id: appId,
459
- dev_app_id: state.appId || currentState.dev_app_id,
460
544
  linked_at: currentState.linked_at || now,
461
- dev_linked_at: currentState.dev_linked_at || now,
545
+ version,
546
+ deployed_at: now,
462
547
  updated_at: now,
463
- });
464
- state.targetAppId = appId;
548
+ };
549
+ if (promotedInPlace) {
550
+ delete nextState.dev_app_id;
551
+ delete nextState.dev_linked_at;
552
+ } else {
553
+ nextState.dev_app_id = proofSession.appId || currentState.dev_app_id;
554
+ nextState.dev_linked_at = currentState.dev_linked_at || now;
555
+ }
556
+ writeLinkedState(
557
+ state.projectDir,
558
+ nextState,
559
+ proofSession.profileKey || state.profileKey,
560
+ );
465
561
  linkAppDevSessionTarget({
466
- appId: state.appId,
562
+ sessionId: proofSession.sessionId,
563
+ appId: proofSession.appId,
467
564
  devSlug: state.slug,
468
565
  targetAppId: appId,
469
566
  lastHeartbeatAt: now,
@@ -471,8 +568,9 @@ export async function startAppDevServer({
471
568
  const response = {
472
569
  ok: true,
473
570
  app_id: appId,
474
- dev_app_id: state.appId || null,
571
+ dev_app_id: promotedInPlace ? null : proofSession.appId || null,
475
572
  target_app_id: appId,
573
+ version,
476
574
  };
477
575
  res.writeHead(200, {
478
576
  ...headers,
@@ -516,6 +614,18 @@ export async function startAppDevServer({
516
614
  return;
517
615
  }
518
616
 
617
+ if (url.pathname === '/events') {
618
+ res.writeHead(200, {
619
+ ...headers,
620
+ 'Content-Type': 'text/event-stream; charset=utf-8',
621
+ Connection: 'keep-alive',
622
+ });
623
+ res.write(': connected\n\n');
624
+ hostSseClients.add(res);
625
+ req.on('close', () => hostSseClients.delete(res));
626
+ return;
627
+ }
628
+
519
629
  const routed = matchAppRoute(url.pathname);
520
630
  if (!routed) {
521
631
  res.writeHead(404, headers);
@@ -635,15 +745,23 @@ export async function startAppDevServer({
635
745
  try {
636
746
  state.watcher = fsWatch(state.bundleDir, { persistent: false }, (_event, filename) => {
637
747
  if (!filename || filename !== 'app.js') return;
638
- try {
639
- const stat = statSync(state.jsPath);
640
- if (stat.mtimeMs === state.lastMtimeMs) return;
641
- state.lastMtimeMs = stat.mtimeMs;
642
- log(`[notis apps dev] ${state.slug}: bundle updated — reloading portal`);
643
- broadcastReload(state.slug);
644
- } catch {
645
- // Bundle file may be missing mid-write; ignore.
646
- }
748
+ // Vite may emit the filesystem notification while its output file is
749
+ // still being replaced. Debounce the notification so consumers never
750
+ // fetch the previous or partially-written bundle under the new build
751
+ // revision.
752
+ if (state.reloadTimer) clearTimeout(state.reloadTimer);
753
+ state.reloadTimer = setTimeout(() => {
754
+ state.reloadTimer = null;
755
+ try {
756
+ const stat = statSync(state.jsPath);
757
+ if (stat.mtimeMs === state.lastMtimeMs) return;
758
+ state.lastMtimeMs = stat.mtimeMs;
759
+ log(`[notis apps dev] ${state.slug}: bundle updated — reloading portal`);
760
+ broadcastReload(state.slug);
761
+ } catch {
762
+ // Bundle file may be missing mid-write; the next change retries.
763
+ }
764
+ }, 100);
647
765
  });
648
766
  } catch (error) {
649
767
  logError(`[notis apps dev] ${state.slug}: watch failed: ${error.message}`);
@@ -660,9 +778,53 @@ export async function startAppDevServer({
660
778
  state.watchPollTimer = setTimeout(() => pollForBundleAndWatch(state), 300);
661
779
  }
662
780
 
781
+ function watchManifestInputs(state) {
782
+ const schedulePrepare = () => {
783
+ if (state.prepareTimer) clearTimeout(state.prepareTimer);
784
+ state.prepareTimer = setTimeout(() => {
785
+ state.prepareTimer = null;
786
+ void prepareArtifactBuild(state.projectDir).then(() => {
787
+ // Name, icon, and route metadata can change without changing the
788
+ // compiled JS bytes. Notify consumers immediately; a generated-entry
789
+ // change will produce the normal second reload after Vite rebuilds.
790
+ broadcastReload(state.slug);
791
+ }).catch((error) => {
792
+ const message = error instanceof Error ? error.message : String(error);
793
+ logError(`[notis apps dev] ${state.slug}: ${message}`);
794
+ });
795
+ }, 75);
796
+ };
797
+ try {
798
+ state.sourceWatchers.push(fsWatch(
799
+ state.projectDir,
800
+ { persistent: false },
801
+ (_event, filename) => {
802
+ if (filename && /^notis\.config\.(?:ts|js|mjs)$/.test(String(filename))) {
803
+ schedulePrepare();
804
+ }
805
+ },
806
+ ));
807
+ } catch (error) {
808
+ logError(`[notis apps dev] ${state.slug}: config watch failed: ${error.message}`);
809
+ }
810
+ const appDir = join(state.projectDir, 'app');
811
+ if (existsSync(appDir)) {
812
+ try {
813
+ state.sourceWatchers.push(fsWatch(
814
+ appDir,
815
+ { persistent: false, recursive: process.platform === 'darwin' || process.platform === 'win32' },
816
+ schedulePrepare,
817
+ ));
818
+ } catch (error) {
819
+ logError(`[notis apps dev] ${state.slug}: route watch failed: ${error.message}`);
820
+ }
821
+ }
822
+ }
823
+
663
824
  for (const state of appState.values()) {
664
825
  if (watch) {
665
826
  await prepareArtifactBuild(state.projectDir);
827
+ watchManifestInputs(state);
666
828
  pollForBundleAndWatch(state);
667
829
 
668
830
  state.buildProcess = spawn('npm', ['run', 'build', '--', '--watch'], {
@@ -685,8 +847,37 @@ export async function startAppDevServer({
685
847
 
686
848
  return {
687
849
  port,
850
+ updateApp(slug, updates = {}) {
851
+ const state = appState.get(slug);
852
+ if (!state) throw new Error(`unknown app: ${slug}`);
853
+ for (const key of ['appId', 'targetAppId', 'userId', 'profileKey', 'sessionId', 'mountNonce']) {
854
+ if (Object.prototype.hasOwnProperty.call(updates, key)) {
855
+ state[key] = updates[key] || null;
856
+ }
857
+ }
858
+ },
859
+ isBundleReady(slug) {
860
+ const state = appState.get(slug);
861
+ if (!state) throw new Error(`unknown app: ${slug}`);
862
+ return state.bundleReady;
863
+ },
864
+ waitForBundle(slug) {
865
+ const state = appState.get(slug);
866
+ if (!state) return Promise.reject(new Error(`unknown app: ${slug}`));
867
+ return state.bundleReadyPromise;
868
+ },
688
869
  async close() {
689
870
  for (const state of appState.values()) {
871
+ if (state.prepareTimer) clearTimeout(state.prepareTimer);
872
+ if (state.reloadTimer) clearTimeout(state.reloadTimer);
873
+ for (const watcher of state.sourceWatchers) {
874
+ try {
875
+ watcher.close();
876
+ } catch {
877
+ // ignore
878
+ }
879
+ }
880
+ state.sourceWatchers = [];
690
881
  if (state.watchPollTimer) clearTimeout(state.watchPollTimer);
691
882
  if (state.watcher) {
692
883
  try {
@@ -707,6 +898,14 @@ export async function startAppDevServer({
707
898
  state.buildProcess.kill('SIGTERM');
708
899
  }
709
900
  }
901
+ for (const res of hostSseClients) {
902
+ try {
903
+ res.end();
904
+ } catch {
905
+ // ignore
906
+ }
907
+ }
908
+ hostSseClients.clear();
710
909
  await new Promise((resolvePromise) => server.close(() => resolvePromise()));
711
910
  },
712
911
  };