@notis_ai/cli 0.2.0-beta.139.1 → 0.2.0-beta.145.1

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 (270) hide show
  1. package/README.md +44 -20
  2. package/dist/agent-hooks/notis-agent-hook.mjs +4579 -1449
  3. package/dist/base-skills/notis-apps/SKILL.md +83 -55
  4. package/dist/base-skills/notis-cli/SKILL.md +8 -8
  5. package/package.json +2 -2
  6. package/skills/notis-apps/cli.md +38 -17
  7. package/src/command-specs/apps.js +871 -387
  8. package/src/command-specs/auth.js +47 -10
  9. package/src/command-specs/onboarding.js +11 -4
  10. package/src/runtime/app-dev-consumers.js +154 -0
  11. package/src/runtime/app-dev-host-lock.js +80 -0
  12. package/src/runtime/app-dev-roots.js +284 -0
  13. package/src/runtime/app-dev-server.js +212 -22
  14. package/src/runtime/app-dev-sessions.js +105 -145
  15. package/src/runtime/app-platform.js +1291 -165
  16. package/src/runtime/app-registry-scaffolds.js +367 -0
  17. package/src/runtime/login-listener.js +15 -0
  18. package/src/runtime/oauth.js +1506 -141
  19. package/src/runtime/output.js +7 -0
  20. package/src/runtime/ports.js +16 -0
  21. package/src/runtime/profiles.js +0 -5
  22. package/template/app/layout.tsx +5 -1
  23. package/template/notis.config.ts +1 -0
  24. package/template/packages/sdk/package.json +2 -1
  25. package/template/packages/sdk/src/components/MarkdownEditor.tsx +121 -0
  26. package/template/packages/sdk/src/components/MultiSelectActionBar.tsx +37 -119
  27. package/template/packages/sdk/src/components/MultiSelectCheckbox.tsx +5 -1
  28. package/template/packages/sdk/src/components/MultiSelectDragOverlay.tsx +1 -1
  29. package/template/packages/sdk/src/components/NotisSelectionBoundary.tsx +55 -0
  30. package/template/packages/sdk/src/components/ShortcutHints.tsx +56 -0
  31. package/template/packages/sdk/src/config.ts +19 -0
  32. package/template/packages/sdk/src/hooks/useActiveResource.ts +19 -0
  33. package/template/packages/sdk/src/hooks/useCollectionInteractions.ts +709 -0
  34. package/template/packages/sdk/src/hooks/useHandover.ts +3 -0
  35. package/template/packages/sdk/src/hooks/useMultiSelect.ts +44 -482
  36. package/template/packages/sdk/src/hooks/useTool.ts +5 -4
  37. package/template/packages/sdk/src/index.ts +47 -2
  38. package/template/packages/sdk/src/interactions/actions.ts +46 -0
  39. package/template/packages/sdk/src/interactions/shortcuts.tsx +628 -0
  40. package/template/packages/sdk/src/interactions.ts +41 -0
  41. package/template/packages/sdk/src/provider.tsx +2 -1
  42. package/template/packages/sdk/src/runtime.ts +85 -3
  43. package/dist/scaffolds/notis-database/CHANGELOG.md +0 -5
  44. package/dist/scaffolds/notis-database/app/globals.css +0 -44
  45. package/dist/scaffolds/notis-database/app/layout.tsx +0 -6
  46. package/dist/scaffolds/notis-database/app/page.tsx +0 -1088
  47. package/dist/scaffolds/notis-database/components/ui/badge.tsx +0 -28
  48. package/dist/scaffolds/notis-database/components/ui/button.tsx +0 -53
  49. package/dist/scaffolds/notis-database/components/ui/card.tsx +0 -56
  50. package/dist/scaffolds/notis-database/components/ui/table.tsx +0 -120
  51. package/dist/scaffolds/notis-database/components.json +0 -20
  52. package/dist/scaffolds/notis-database/index.html +0 -12
  53. package/dist/scaffolds/notis-database/lib/types.ts +0 -132
  54. package/dist/scaffolds/notis-database/lib/utils.ts +0 -6
  55. package/dist/scaffolds/notis-database/metadata/screenshot-1.png +0 -0
  56. package/dist/scaffolds/notis-database/metadata/screenshot-2.png +0 -0
  57. package/dist/scaffolds/notis-database/metadata/screenshot-3.png +0 -0
  58. package/dist/scaffolds/notis-database/metadata/screenshot-4.png +0 -0
  59. package/dist/scaffolds/notis-database/metadata/screenshot-5.png +0 -0
  60. package/dist/scaffolds/notis-database/metadata/screenshot-fixtures.json +0 -1839
  61. package/dist/scaffolds/notis-database/notis.config.ts +0 -73
  62. package/dist/scaffolds/notis-database/package-lock.json +0 -3935
  63. package/dist/scaffolds/notis-database/package.json +0 -32
  64. package/dist/scaffolds/notis-database/packages/sdk/package.json +0 -36
  65. package/dist/scaffolds/notis-database/packages/sdk/src/components/DocumentEditor.tsx +0 -93
  66. package/dist/scaffolds/notis-database/packages/sdk/src/components/Markdown.tsx +0 -60
  67. package/dist/scaffolds/notis-database/packages/sdk/src/components/MultiSelectActionBar.tsx +0 -278
  68. package/dist/scaffolds/notis-database/packages/sdk/src/components/MultiSelectCheckbox.tsx +0 -91
  69. package/dist/scaffolds/notis-database/packages/sdk/src/components/MultiSelectDragOverlay.tsx +0 -39
  70. package/dist/scaffolds/notis-database/packages/sdk/src/config.ts +0 -234
  71. package/dist/scaffolds/notis-database/packages/sdk/src/documents.ts +0 -250
  72. package/dist/scaffolds/notis-database/packages/sdk/src/hooks/useBackend.ts +0 -41
  73. package/dist/scaffolds/notis-database/packages/sdk/src/hooks/useCloudComputer.ts +0 -97
  74. package/dist/scaffolds/notis-database/packages/sdk/src/hooks/useDatabaseSchema.ts +0 -85
  75. package/dist/scaffolds/notis-database/packages/sdk/src/hooks/useDatabaseSubscription.ts +0 -76
  76. package/dist/scaffolds/notis-database/packages/sdk/src/hooks/useDocument.ts +0 -78
  77. package/dist/scaffolds/notis-database/packages/sdk/src/hooks/useDocuments.ts +0 -121
  78. package/dist/scaffolds/notis-database/packages/sdk/src/hooks/useHandover.ts +0 -75
  79. package/dist/scaffolds/notis-database/packages/sdk/src/hooks/useMultiSelect.ts +0 -539
  80. package/dist/scaffolds/notis-database/packages/sdk/src/hooks/useNotis.ts +0 -34
  81. package/dist/scaffolds/notis-database/packages/sdk/src/hooks/useNotisNavigation.ts +0 -49
  82. package/dist/scaffolds/notis-database/packages/sdk/src/hooks/useTool.ts +0 -64
  83. package/dist/scaffolds/notis-database/packages/sdk/src/hooks/useTools.ts +0 -56
  84. package/dist/scaffolds/notis-database/packages/sdk/src/hooks/useTopBarSearch.ts +0 -73
  85. package/dist/scaffolds/notis-database/packages/sdk/src/hooks/useUpsertDocument.ts +0 -95
  86. package/dist/scaffolds/notis-database/packages/sdk/src/index.ts +0 -100
  87. package/dist/scaffolds/notis-database/packages/sdk/src/provider.tsx +0 -43
  88. package/dist/scaffolds/notis-database/packages/sdk/src/runtime.ts +0 -351
  89. package/dist/scaffolds/notis-database/packages/sdk/src/styles.css +0 -186
  90. package/dist/scaffolds/notis-database/packages/sdk/src/ui.ts +0 -15
  91. package/dist/scaffolds/notis-database/packages/sdk/src/vite.ts +0 -56
  92. package/dist/scaffolds/notis-database/packages/sdk/tsconfig.json +0 -15
  93. package/dist/scaffolds/notis-database/postcss.config.mjs +0 -8
  94. package/dist/scaffolds/notis-database/src/dev-main.tsx +0 -23
  95. package/dist/scaffolds/notis-database/src/mock-runtime.ts +0 -561
  96. package/dist/scaffolds/notis-database/tailwind.config.ts +0 -59
  97. package/dist/scaffolds/notis-database/tsconfig.json +0 -23
  98. package/dist/scaffolds/notis-database/vite.config.ts +0 -22
  99. package/dist/scaffolds/notis-journal/CHANGELOG.md +0 -29
  100. package/dist/scaffolds/notis-journal/app/globals.css +0 -37
  101. package/dist/scaffolds/notis-journal/app/insights/page.tsx +0 -513
  102. package/dist/scaffolds/notis-journal/app/journal-core.tsx +0 -362
  103. package/dist/scaffolds/notis-journal/app/journal-ui.tsx +0 -337
  104. package/dist/scaffolds/notis-journal/app/layout.tsx +0 -6
  105. package/dist/scaffolds/notis-journal/app/page.tsx +0 -486
  106. package/dist/scaffolds/notis-journal/components/ui/badge.tsx +0 -28
  107. package/dist/scaffolds/notis-journal/components/ui/button.tsx +0 -53
  108. package/dist/scaffolds/notis-journal/components/ui/card.tsx +0 -56
  109. package/dist/scaffolds/notis-journal/components.json +0 -20
  110. package/dist/scaffolds/notis-journal/index.html +0 -12
  111. package/dist/scaffolds/notis-journal/lib/utils.ts +0 -6
  112. package/dist/scaffolds/notis-journal/metadata/screenshot-1.png +0 -0
  113. package/dist/scaffolds/notis-journal/metadata/screenshot-2.png +0 -0
  114. package/dist/scaffolds/notis-journal/metadata/screenshot-3.png +0 -0
  115. package/dist/scaffolds/notis-journal/metadata/screenshot-4.png +0 -0
  116. package/dist/scaffolds/notis-journal/metadata/screenshot-5.png +0 -0
  117. package/dist/scaffolds/notis-journal/metadata/screenshot-6.png +0 -0
  118. package/dist/scaffolds/notis-journal/metadata/screenshot-fixtures.json +0 -132
  119. package/dist/scaffolds/notis-journal/notis.config.ts +0 -93
  120. package/dist/scaffolds/notis-journal/package-lock.json +0 -4615
  121. package/dist/scaffolds/notis-journal/package.json +0 -34
  122. package/dist/scaffolds/notis-journal/packages/sdk/package.json +0 -36
  123. package/dist/scaffolds/notis-journal/packages/sdk/src/components/DocumentEditor.tsx +0 -93
  124. package/dist/scaffolds/notis-journal/packages/sdk/src/components/Markdown.tsx +0 -60
  125. package/dist/scaffolds/notis-journal/packages/sdk/src/components/MultiSelectActionBar.tsx +0 -278
  126. package/dist/scaffolds/notis-journal/packages/sdk/src/components/MultiSelectCheckbox.tsx +0 -91
  127. package/dist/scaffolds/notis-journal/packages/sdk/src/components/MultiSelectDragOverlay.tsx +0 -39
  128. package/dist/scaffolds/notis-journal/packages/sdk/src/config.ts +0 -234
  129. package/dist/scaffolds/notis-journal/packages/sdk/src/documents.ts +0 -250
  130. package/dist/scaffolds/notis-journal/packages/sdk/src/hooks/useBackend.ts +0 -41
  131. package/dist/scaffolds/notis-journal/packages/sdk/src/hooks/useCloudComputer.ts +0 -97
  132. package/dist/scaffolds/notis-journal/packages/sdk/src/hooks/useDatabaseSchema.ts +0 -85
  133. package/dist/scaffolds/notis-journal/packages/sdk/src/hooks/useDatabaseSubscription.ts +0 -76
  134. package/dist/scaffolds/notis-journal/packages/sdk/src/hooks/useDocument.ts +0 -78
  135. package/dist/scaffolds/notis-journal/packages/sdk/src/hooks/useDocuments.ts +0 -121
  136. package/dist/scaffolds/notis-journal/packages/sdk/src/hooks/useHandover.ts +0 -75
  137. package/dist/scaffolds/notis-journal/packages/sdk/src/hooks/useMultiSelect.ts +0 -539
  138. package/dist/scaffolds/notis-journal/packages/sdk/src/hooks/useNotis.ts +0 -34
  139. package/dist/scaffolds/notis-journal/packages/sdk/src/hooks/useNotisNavigation.ts +0 -49
  140. package/dist/scaffolds/notis-journal/packages/sdk/src/hooks/useTool.ts +0 -64
  141. package/dist/scaffolds/notis-journal/packages/sdk/src/hooks/useTools.ts +0 -56
  142. package/dist/scaffolds/notis-journal/packages/sdk/src/hooks/useTopBarSearch.ts +0 -73
  143. package/dist/scaffolds/notis-journal/packages/sdk/src/hooks/useUpsertDocument.ts +0 -95
  144. package/dist/scaffolds/notis-journal/packages/sdk/src/index.ts +0 -100
  145. package/dist/scaffolds/notis-journal/packages/sdk/src/provider.tsx +0 -43
  146. package/dist/scaffolds/notis-journal/packages/sdk/src/runtime.ts +0 -351
  147. package/dist/scaffolds/notis-journal/packages/sdk/src/styles.css +0 -186
  148. package/dist/scaffolds/notis-journal/packages/sdk/src/ui.ts +0 -15
  149. package/dist/scaffolds/notis-journal/packages/sdk/src/vite.ts +0 -56
  150. package/dist/scaffolds/notis-journal/packages/sdk/tsconfig.json +0 -15
  151. package/dist/scaffolds/notis-journal/postcss.config.mjs +0 -8
  152. package/dist/scaffolds/notis-journal/skills/journal-onboarding/SKILL.md +0 -120
  153. package/dist/scaffolds/notis-journal/src/dev-main.tsx +0 -58
  154. package/dist/scaffolds/notis-journal/src/mock-runtime.ts +0 -199
  155. package/dist/scaffolds/notis-journal/tailwind.config.ts +0 -58
  156. package/dist/scaffolds/notis-journal/tsconfig.json +0 -23
  157. package/dist/scaffolds/notis-journal/vite.config.ts +0 -10
  158. package/dist/scaffolds/notis-notes/CHANGELOG.md +0 -5
  159. package/dist/scaffolds/notis-notes/app/globals.css +0 -3
  160. package/dist/scaffolds/notis-notes/app/layout.tsx +0 -6
  161. package/dist/scaffolds/notis-notes/app/page.tsx +0 -1943
  162. package/dist/scaffolds/notis-notes/app/phosphor-icons.ts +0 -596
  163. package/dist/scaffolds/notis-notes/components/ui/badge.tsx +0 -28
  164. package/dist/scaffolds/notis-notes/components/ui/button.tsx +0 -53
  165. package/dist/scaffolds/notis-notes/components/ui/card.tsx +0 -56
  166. package/dist/scaffolds/notis-notes/components.json +0 -20
  167. package/dist/scaffolds/notis-notes/lib/utils.ts +0 -6
  168. package/dist/scaffolds/notis-notes/lib/visible-properties.ts +0 -144
  169. package/dist/scaffolds/notis-notes/metadata/screenshot-1.png +0 -0
  170. package/dist/scaffolds/notis-notes/metadata/screenshot-2.png +0 -0
  171. package/dist/scaffolds/notis-notes/metadata/screenshot-3.png +0 -0
  172. package/dist/scaffolds/notis-notes/metadata/screenshot-4.png +0 -0
  173. package/dist/scaffolds/notis-notes/metadata/screenshot-5.png +0 -0
  174. package/dist/scaffolds/notis-notes/metadata/screenshot-fixtures.json +0 -752
  175. package/dist/scaffolds/notis-notes/notis.config.ts +0 -80
  176. package/dist/scaffolds/notis-notes/package-lock.json +0 -4636
  177. package/dist/scaffolds/notis-notes/package.json +0 -35
  178. package/dist/scaffolds/notis-notes/packages/sdk/package.json +0 -36
  179. package/dist/scaffolds/notis-notes/packages/sdk/src/components/DocumentEditor.tsx +0 -93
  180. package/dist/scaffolds/notis-notes/packages/sdk/src/components/Markdown.tsx +0 -60
  181. package/dist/scaffolds/notis-notes/packages/sdk/src/components/MultiSelectActionBar.tsx +0 -278
  182. package/dist/scaffolds/notis-notes/packages/sdk/src/components/MultiSelectCheckbox.tsx +0 -91
  183. package/dist/scaffolds/notis-notes/packages/sdk/src/components/MultiSelectDragOverlay.tsx +0 -39
  184. package/dist/scaffolds/notis-notes/packages/sdk/src/config.ts +0 -234
  185. package/dist/scaffolds/notis-notes/packages/sdk/src/documents.ts +0 -250
  186. package/dist/scaffolds/notis-notes/packages/sdk/src/hooks/useBackend.ts +0 -41
  187. package/dist/scaffolds/notis-notes/packages/sdk/src/hooks/useCloudComputer.ts +0 -97
  188. package/dist/scaffolds/notis-notes/packages/sdk/src/hooks/useDatabaseSchema.ts +0 -85
  189. package/dist/scaffolds/notis-notes/packages/sdk/src/hooks/useDatabaseSubscription.ts +0 -76
  190. package/dist/scaffolds/notis-notes/packages/sdk/src/hooks/useDocument.ts +0 -78
  191. package/dist/scaffolds/notis-notes/packages/sdk/src/hooks/useDocuments.ts +0 -121
  192. package/dist/scaffolds/notis-notes/packages/sdk/src/hooks/useHandover.ts +0 -75
  193. package/dist/scaffolds/notis-notes/packages/sdk/src/hooks/useMultiSelect.ts +0 -539
  194. package/dist/scaffolds/notis-notes/packages/sdk/src/hooks/useNotis.ts +0 -34
  195. package/dist/scaffolds/notis-notes/packages/sdk/src/hooks/useNotisNavigation.ts +0 -49
  196. package/dist/scaffolds/notis-notes/packages/sdk/src/hooks/useTool.ts +0 -64
  197. package/dist/scaffolds/notis-notes/packages/sdk/src/hooks/useTools.ts +0 -56
  198. package/dist/scaffolds/notis-notes/packages/sdk/src/hooks/useTopBarSearch.ts +0 -73
  199. package/dist/scaffolds/notis-notes/packages/sdk/src/hooks/useUpsertDocument.ts +0 -95
  200. package/dist/scaffolds/notis-notes/packages/sdk/src/index.ts +0 -100
  201. package/dist/scaffolds/notis-notes/packages/sdk/src/provider.tsx +0 -43
  202. package/dist/scaffolds/notis-notes/packages/sdk/src/runtime.ts +0 -351
  203. package/dist/scaffolds/notis-notes/packages/sdk/src/styles.css +0 -186
  204. package/dist/scaffolds/notis-notes/packages/sdk/src/ui.ts +0 -15
  205. package/dist/scaffolds/notis-notes/packages/sdk/src/vite.ts +0 -56
  206. package/dist/scaffolds/notis-notes/packages/sdk/tsconfig.json +0 -15
  207. package/dist/scaffolds/notis-notes/postcss.config.mjs +0 -8
  208. package/dist/scaffolds/notis-notes/tailwind.config.ts +0 -58
  209. package/dist/scaffolds/notis-notes/tsconfig.json +0 -23
  210. package/dist/scaffolds/notis-notes/vite.config.ts +0 -10
  211. package/dist/scaffolds/notis-random/CHANGELOG.md +0 -15
  212. package/dist/scaffolds/notis-random/README.md +0 -33
  213. package/dist/scaffolds/notis-random/app/globals.css +0 -11
  214. package/dist/scaffolds/notis-random/app/history/page.tsx +0 -67
  215. package/dist/scaffolds/notis-random/app/layout.tsx +0 -7
  216. package/dist/scaffolds/notis-random/app/page.tsx +0 -289
  217. package/dist/scaffolds/notis-random/components/ui/button.tsx +0 -50
  218. package/dist/scaffolds/notis-random/components/ui/card.tsx +0 -16
  219. package/dist/scaffolds/notis-random/components/ui/input.tsx +0 -23
  220. package/dist/scaffolds/notis-random/components.json +0 -20
  221. package/dist/scaffolds/notis-random/index.html +0 -12
  222. package/dist/scaffolds/notis-random/lib/notis-tools.ts +0 -128
  223. package/dist/scaffolds/notis-random/lib/rng.ts +0 -202
  224. package/dist/scaffolds/notis-random/lib/roll-record.ts +0 -189
  225. package/dist/scaffolds/notis-random/lib/utils.ts +0 -25
  226. package/dist/scaffolds/notis-random/metadata/screenshot-1.png +0 -0
  227. package/dist/scaffolds/notis-random/metadata/screenshot-2.png +0 -0
  228. package/dist/scaffolds/notis-random/metadata/screenshot-3.png +0 -0
  229. package/dist/scaffolds/notis-random/metadata/screenshot-4.png +0 -0
  230. package/dist/scaffolds/notis-random/metadata/screenshot-5.png +0 -0
  231. package/dist/scaffolds/notis-random/metadata/screenshot-fixtures.json +0 -753
  232. package/dist/scaffolds/notis-random/notis.config.ts +0 -86
  233. package/dist/scaffolds/notis-random/package-lock.json +0 -4513
  234. package/dist/scaffolds/notis-random/package.json +0 -36
  235. package/dist/scaffolds/notis-random/packages/sdk/package.json +0 -36
  236. package/dist/scaffolds/notis-random/packages/sdk/src/components/DocumentEditor.tsx +0 -93
  237. package/dist/scaffolds/notis-random/packages/sdk/src/components/Markdown.tsx +0 -60
  238. package/dist/scaffolds/notis-random/packages/sdk/src/components/MultiSelectActionBar.tsx +0 -278
  239. package/dist/scaffolds/notis-random/packages/sdk/src/components/MultiSelectCheckbox.tsx +0 -91
  240. package/dist/scaffolds/notis-random/packages/sdk/src/components/MultiSelectDragOverlay.tsx +0 -39
  241. package/dist/scaffolds/notis-random/packages/sdk/src/config.ts +0 -234
  242. package/dist/scaffolds/notis-random/packages/sdk/src/documents.ts +0 -250
  243. package/dist/scaffolds/notis-random/packages/sdk/src/hooks/useBackend.ts +0 -41
  244. package/dist/scaffolds/notis-random/packages/sdk/src/hooks/useCloudComputer.ts +0 -97
  245. package/dist/scaffolds/notis-random/packages/sdk/src/hooks/useDatabaseSchema.ts +0 -85
  246. package/dist/scaffolds/notis-random/packages/sdk/src/hooks/useDatabaseSubscription.ts +0 -76
  247. package/dist/scaffolds/notis-random/packages/sdk/src/hooks/useDocument.ts +0 -78
  248. package/dist/scaffolds/notis-random/packages/sdk/src/hooks/useDocuments.ts +0 -121
  249. package/dist/scaffolds/notis-random/packages/sdk/src/hooks/useHandover.ts +0 -75
  250. package/dist/scaffolds/notis-random/packages/sdk/src/hooks/useMultiSelect.ts +0 -539
  251. package/dist/scaffolds/notis-random/packages/sdk/src/hooks/useNotis.ts +0 -34
  252. package/dist/scaffolds/notis-random/packages/sdk/src/hooks/useNotisNavigation.ts +0 -49
  253. package/dist/scaffolds/notis-random/packages/sdk/src/hooks/useTool.ts +0 -64
  254. package/dist/scaffolds/notis-random/packages/sdk/src/hooks/useTools.ts +0 -56
  255. package/dist/scaffolds/notis-random/packages/sdk/src/hooks/useTopBarSearch.ts +0 -73
  256. package/dist/scaffolds/notis-random/packages/sdk/src/hooks/useUpsertDocument.ts +0 -95
  257. package/dist/scaffolds/notis-random/packages/sdk/src/index.ts +0 -100
  258. package/dist/scaffolds/notis-random/packages/sdk/src/provider.tsx +0 -43
  259. package/dist/scaffolds/notis-random/packages/sdk/src/runtime.ts +0 -351
  260. package/dist/scaffolds/notis-random/packages/sdk/src/styles.css +0 -186
  261. package/dist/scaffolds/notis-random/packages/sdk/src/ui.ts +0 -15
  262. package/dist/scaffolds/notis-random/packages/sdk/src/vite.ts +0 -56
  263. package/dist/scaffolds/notis-random/packages/sdk/tsconfig.json +0 -15
  264. package/dist/scaffolds/notis-random/postcss.config.mjs +0 -6
  265. package/dist/scaffolds/notis-random/src/dev-main.tsx +0 -70
  266. package/dist/scaffolds/notis-random/src/mock-runtime.ts +0 -129
  267. package/dist/scaffolds/notis-random/tailwind.config.ts +0 -50
  268. package/dist/scaffolds/notis-random/tsconfig.json +0 -23
  269. package/dist/scaffolds/notis-random/vite.config.ts +0 -11
  270. package/dist/scaffolds.json +0 -49
@@ -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',
@@ -297,7 +300,7 @@ function renderHarnessHtml({ state, manifest, appConfig, route, harnessOptions,
297
300
  /**
298
301
  * Start the dev server for one or more apps.
299
302
  *
300
- * @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
301
304
  */
302
305
  export async function startAppDevServer({
303
306
  apps,
@@ -313,22 +316,43 @@ export async function startAppDevServer({
313
316
  }
314
317
 
315
318
  const appState = new Map();
316
- for (const app of apps) {
317
- 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 {
318
330
  slug: app.slug,
319
331
  projectDir: app.projectDir,
320
332
  appId: app.appId || null,
321
333
  targetAppId: app.targetAppId || null,
322
334
  userId: app.userId || null,
335
+ profileKey: app.profileKey || null,
336
+ sessionId: app.sessionId || null,
337
+ mountNonce: app.mountNonce || null,
323
338
  canonicalBundleDir: getBundleDir(app.projectDir),
324
339
  bundleDir: null,
325
340
  jsPath: null,
326
341
  sseClients: new Set(),
327
342
  watcher: null,
343
+ sourceWatchers: [],
344
+ prepareTimer: null,
345
+ reloadTimer: null,
328
346
  buildProcess: null,
329
347
  lastMtimeMs: 0,
330
348
  watchPollTimer: null,
331
- });
349
+ bundleReady: false,
350
+ bundleReadyPromise,
351
+ resolveBundleReady,
352
+ };
353
+ };
354
+ for (const app of apps) {
355
+ appState.set(app.slug, createAppState(app));
332
356
  }
333
357
 
334
358
  function broadcastReload(slug) {
@@ -342,6 +366,14 @@ export async function startAppDevServer({
342
366
  state.sseClients.delete(res);
343
367
  }
344
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
+ }
345
377
  }
346
378
 
347
379
  function matchAppRoute(pathname) {
@@ -374,6 +406,10 @@ export async function startAppDevServer({
374
406
  state.jsPath = join(bundleDir, 'app.js');
375
407
  try {
376
408
  state.lastMtimeMs = statSync(state.jsPath).mtimeMs;
409
+ if (!state.bundleReady) {
410
+ state.bundleReady = true;
411
+ state.resolveBundleReady();
412
+ }
377
413
  } catch {
378
414
  state.lastMtimeMs = 0;
379
415
  }
@@ -451,6 +487,9 @@ export async function startAppDevServer({
451
487
  try {
452
488
  const body = await readRequestJson(req);
453
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;
454
493
  if (!appId) {
455
494
  res.writeHead(400, {
456
495
  ...headers,
@@ -459,20 +498,69 @@ export async function startAppDevServer({
459
498
  res.end(JSON.stringify({ error: 'app_id is required' }));
460
499
  return;
461
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
+ }
462
534
 
463
- const currentState = readLinkedState(state.projectDir) || {};
535
+ const currentState = readLinkedState(
536
+ state.projectDir,
537
+ proofSession.profileKey || state.profileKey,
538
+ ) || {};
464
539
  const now = new Date().toISOString();
465
- writeLinkedState(state.projectDir, {
540
+ const promotedInPlace = proofSession.appId === appId;
541
+ const nextState = {
466
542
  ...currentState,
467
543
  app_id: appId,
468
- dev_app_id: state.appId || currentState.dev_app_id,
469
544
  linked_at: currentState.linked_at || now,
470
- dev_linked_at: currentState.dev_linked_at || now,
545
+ version,
546
+ deployed_at: now,
471
547
  updated_at: now,
472
- });
473
- 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
+ );
474
561
  linkAppDevSessionTarget({
475
- appId: state.appId,
562
+ sessionId: proofSession.sessionId,
563
+ appId: proofSession.appId,
476
564
  devSlug: state.slug,
477
565
  targetAppId: appId,
478
566
  lastHeartbeatAt: now,
@@ -480,8 +568,9 @@ export async function startAppDevServer({
480
568
  const response = {
481
569
  ok: true,
482
570
  app_id: appId,
483
- dev_app_id: state.appId || null,
571
+ dev_app_id: promotedInPlace ? null : proofSession.appId || null,
484
572
  target_app_id: appId,
573
+ version,
485
574
  };
486
575
  res.writeHead(200, {
487
576
  ...headers,
@@ -525,6 +614,18 @@ export async function startAppDevServer({
525
614
  return;
526
615
  }
527
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
+
528
629
  const routed = matchAppRoute(url.pathname);
529
630
  if (!routed) {
530
631
  res.writeHead(404, headers);
@@ -644,15 +745,23 @@ export async function startAppDevServer({
644
745
  try {
645
746
  state.watcher = fsWatch(state.bundleDir, { persistent: false }, (_event, filename) => {
646
747
  if (!filename || filename !== 'app.js') return;
647
- try {
648
- const stat = statSync(state.jsPath);
649
- if (stat.mtimeMs === state.lastMtimeMs) return;
650
- state.lastMtimeMs = stat.mtimeMs;
651
- log(`[notis apps dev] ${state.slug}: bundle updated — reloading portal`);
652
- broadcastReload(state.slug);
653
- } catch {
654
- // Bundle file may be missing mid-write; ignore.
655
- }
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);
656
765
  });
657
766
  } catch (error) {
658
767
  logError(`[notis apps dev] ${state.slug}: watch failed: ${error.message}`);
@@ -669,9 +778,53 @@ export async function startAppDevServer({
669
778
  state.watchPollTimer = setTimeout(() => pollForBundleAndWatch(state), 300);
670
779
  }
671
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
+
672
824
  for (const state of appState.values()) {
673
825
  if (watch) {
674
826
  await prepareArtifactBuild(state.projectDir);
827
+ watchManifestInputs(state);
675
828
  pollForBundleAndWatch(state);
676
829
 
677
830
  state.buildProcess = spawn('npm', ['run', 'build', '--', '--watch'], {
@@ -694,8 +847,37 @@ export async function startAppDevServer({
694
847
 
695
848
  return {
696
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
+ },
697
869
  async close() {
698
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 = [];
699
881
  if (state.watchPollTimer) clearTimeout(state.watchPollTimer);
700
882
  if (state.watcher) {
701
883
  try {
@@ -716,6 +898,14 @@ export async function startAppDevServer({
716
898
  state.buildProcess.kill('SIGTERM');
717
899
  }
718
900
  }
901
+ for (const res of hostSseClients) {
902
+ try {
903
+ res.end();
904
+ } catch {
905
+ // ignore
906
+ }
907
+ }
908
+ hostSseClients.clear();
719
909
  await new Promise((resolvePromise) => server.close(() => resolvePromise()));
720
910
  },
721
911
  };
@@ -1,30 +1,77 @@
1
1
  import { randomUUID } from 'node:crypto';
2
- import { existsSync, mkdirSync, readFileSync, renameSync, writeFileSync } from 'node:fs';
3
- import { dirname, join, parse as parsePath } from 'node:path';
2
+ import { existsSync, lstatSync, mkdirSync, readFileSync, renameSync, rmSync, writeFileSync } from 'node:fs';
3
+ import { dirname, join } from 'node:path';
4
4
  import { CONFIG_DIR } from './profiles.js';
5
5
 
6
6
  export const DEFAULT_APP_DEV_SESSIONS_FILE = join(CONFIG_DIR, 'app-dev-sessions.json');
7
7
  export const APP_DEV_SESSIONS_FILE = DEFAULT_APP_DEV_SESSIONS_FILE;
8
8
  export const APP_DEV_SESSIONS_VERSION = 1;
9
- export const APP_DEV_SESSION_MOUNT_ACKS_VERSION = 1;
9
+ const REGISTRY_LOCK_TIMEOUT_MS = 5_000;
10
+ const registryLockWait = new Int32Array(new SharedArrayBuffer(4));
11
+
12
+ function readRegistryLockOwner(lockPath) {
13
+ try {
14
+ return readFileSync(join(lockPath, 'owner'), 'utf8').trim();
15
+ } catch {
16
+ return null;
17
+ }
18
+ }
19
+
20
+ function withRegistryLock(filePath, callback) {
21
+ mkdirSync(dirname(filePath), { recursive: true, mode: 0o700 });
22
+ const lockPath = `${filePath}.lock`;
23
+ const ownerId = `${process.pid}.${randomUUID()}`;
24
+ const startedAt = Date.now();
25
+ while (true) {
26
+ try {
27
+ mkdirSync(lockPath, { mode: 0o700 });
28
+ try {
29
+ writeFileSync(join(lockPath, 'owner'), ownerId, { mode: 0o600 });
30
+ } catch (error) {
31
+ rmSync(lockPath, { recursive: true, force: true });
32
+ throw error;
33
+ }
34
+ break;
35
+ } catch (error) {
36
+ if (error?.code !== 'EEXIST') throw error;
37
+ try {
38
+ const lockStat = lstatSync(lockPath);
39
+ if (lockStat.isSymbolicLink() || !lockStat.isDirectory()) {
40
+ throw new Error(`Refusing unsafe app development registry lock: ${lockPath}`);
41
+ }
42
+ } catch (statError) {
43
+ if (statError?.code === 'ENOENT') continue;
44
+ throw statError;
45
+ }
46
+ if (Date.now() - startedAt >= REGISTRY_LOCK_TIMEOUT_MS) {
47
+ throw new Error(
48
+ `Timed out waiting for app development registry lock: ${filePath}. `
49
+ + `If no app development process is running, remove the orphaned lock: ${lockPath}`,
50
+ );
51
+ }
52
+ Atomics.wait(registryLockWait, 0, 0, 10);
53
+ }
54
+ }
55
+ try {
56
+ return callback();
57
+ } finally {
58
+ try {
59
+ if (readRegistryLockOwner(lockPath) === ownerId) {
60
+ rmSync(lockPath, { recursive: true, force: true });
61
+ }
62
+ } catch {
63
+ // Never remove or disrupt a lock that no longer belongs to this writer.
64
+ }
65
+ }
66
+ }
10
67
 
11
68
  export function getAppDevSessionsFile(filePath) {
12
69
  if (filePath) {
13
70
  return filePath;
14
71
  }
15
- const envPath = process.env.NOTIS_APP_DEV_SESSIONS_FILE;
16
- if (typeof envPath === 'string' && envPath.trim()) {
17
- return envPath.trim();
18
- }
19
72
  return DEFAULT_APP_DEV_SESSIONS_FILE;
20
73
  }
21
74
 
22
- export function getAppDevSessionMountAcksFile(sessionsFilePath) {
23
- const resolvedSessionsFile = getAppDevSessionsFile(sessionsFilePath);
24
- const parsed = parsePath(resolvedSessionsFile);
25
- return join(parsed.dir, `${parsed.name}-mount-acks${parsed.ext || '.json'}`);
26
- }
27
-
28
75
  function normalizeSessions(raw) {
29
76
  if (!raw || typeof raw !== 'object' || !Array.isArray(raw.sessions)) {
30
77
  return [];
@@ -62,7 +109,7 @@ export function readAppDevSessions(filePath) {
62
109
  }
63
110
  }
64
111
 
65
- export function writeAppDevSessions(registry, filePath) {
112
+ function writeAppDevSessionsUnlocked(registry, filePath) {
66
113
  const resolvedFilePath = getAppDevSessionsFile(filePath);
67
114
  mkdirSync(dirname(resolvedFilePath), { recursive: true, mode: 0o700 });
68
115
  const normalized = {
@@ -75,151 +122,64 @@ export function writeAppDevSessions(registry, filePath) {
75
122
  return normalized;
76
123
  }
77
124
 
125
+ export function writeAppDevSessions(registry, filePath) {
126
+ const resolvedFilePath = getAppDevSessionsFile(filePath);
127
+ return withRegistryLock(resolvedFilePath, () => writeAppDevSessionsUnlocked(registry, resolvedFilePath));
128
+ }
129
+
78
130
  export function upsertAppDevSessions(sessions, filePath) {
79
131
  const nextSessions = Array.isArray(sessions) ? sessions : [sessions];
80
- const registry = readAppDevSessions(filePath);
81
- const keys = new Set(nextSessions.map((session) => `${session.sessionId}:${session.appId}`));
82
- registry.sessions = [
83
- ...registry.sessions.filter((session) => !keys.has(`${session.sessionId}:${session.appId}`)),
84
- ...nextSessions,
85
- ];
86
- return writeAppDevSessions(registry, filePath);
132
+ const resolvedFilePath = getAppDevSessionsFile(filePath);
133
+ return withRegistryLock(resolvedFilePath, () => {
134
+ const registry = readAppDevSessions(resolvedFilePath);
135
+ const keys = new Set(nextSessions.map((session) => `${session.sessionId}:${session.appId}`));
136
+ registry.sessions = [
137
+ ...registry.sessions.filter((session) => !keys.has(`${session.sessionId}:${session.appId}`)),
138
+ ...nextSessions,
139
+ ];
140
+ return writeAppDevSessionsUnlocked(registry, resolvedFilePath);
141
+ });
87
142
  }
88
143
 
89
144
  export function heartbeatAppDevSession(sessionId, lastHeartbeatAt, filePath) {
90
- const registry = readAppDevSessions(filePath);
91
- registry.sessions = registry.sessions.map((session) =>
92
- session.sessionId === sessionId
93
- ? { ...session, lastHeartbeatAt }
94
- : session,
95
- );
96
- return writeAppDevSessions(registry, filePath);
145
+ const resolvedFilePath = getAppDevSessionsFile(filePath);
146
+ return withRegistryLock(resolvedFilePath, () => {
147
+ const registry = readAppDevSessions(resolvedFilePath);
148
+ registry.sessions = registry.sessions.map((session) =>
149
+ session.sessionId === sessionId ? { ...session, lastHeartbeatAt } : session,
150
+ );
151
+ return writeAppDevSessionsUnlocked(registry, resolvedFilePath);
152
+ });
97
153
  }
98
154
 
99
- export function linkAppDevSessionTarget({ appId, devSlug, targetAppId, lastHeartbeatAt = new Date().toISOString() }, filePath) {
155
+ export function linkAppDevSessionTarget({ sessionId, appId, devSlug, targetAppId, lastHeartbeatAt = new Date().toISOString() }, filePath) {
100
156
  const target = typeof targetAppId === 'string' ? targetAppId.trim() : '';
101
157
  if (!target) {
102
158
  return readAppDevSessions(filePath);
103
159
  }
104
- const registry = readAppDevSessions(filePath);
105
- registry.sessions = registry.sessions.map((session) => {
106
- const matchesApp = appId && session.appId === appId;
107
- const matchesSlug = devSlug && session.devSlug === devSlug;
108
- if (!matchesApp || !matchesSlug) {
109
- return session;
110
- }
111
- return {
112
- ...session,
113
- targetAppId: target,
114
- lastHeartbeatAt,
115
- };
160
+ const resolvedFilePath = getAppDevSessionsFile(filePath);
161
+ const result = withRegistryLock(resolvedFilePath, () => {
162
+ const registry = readAppDevSessions(resolvedFilePath);
163
+ registry.sessions = registry.sessions.map((session) => {
164
+ const hasSelector = Boolean(appId || devSlug);
165
+ const matchesSession = !sessionId || session.sessionId === sessionId;
166
+ const matchesApp = !appId || session.appId === appId;
167
+ const matchesSlug = !devSlug || session.devSlug === devSlug;
168
+ if ((!hasSelector && !sessionId) || !matchesSession || !matchesApp || !matchesSlug) return session;
169
+ const linked = { ...session, targetAppId: target, lastHeartbeatAt };
170
+ return linked;
171
+ });
172
+ return writeAppDevSessionsUnlocked(registry, resolvedFilePath);
116
173
  });
117
- return writeAppDevSessions(registry, filePath);
118
- }
119
-
120
- export function removeAppDevSession(sessionId, filePath) {
121
- const registry = readAppDevSessions(filePath);
122
- registry.sessions = registry.sessions.filter((session) => session.sessionId !== sessionId);
123
- const result = writeAppDevSessions(registry, filePath);
124
- removeAppDevSessionMountAcknowledgements(sessionId, filePath);
125
174
  return result;
126
175
  }
127
176
 
128
- function normalizeMountAcknowledgements(raw) {
129
- if (!raw || typeof raw !== 'object' || !Array.isArray(raw.acknowledgements)) {
130
- return [];
131
- }
132
- return raw.acknowledgements.filter((acknowledgement) =>
133
- acknowledgement &&
134
- typeof acknowledgement === 'object' &&
135
- typeof acknowledgement.sessionId === 'string' &&
136
- typeof acknowledgement.appId === 'string' &&
137
- typeof acknowledgement.devSlug === 'string' &&
138
- typeof acknowledgement.mountNonce === 'string' &&
139
- typeof acknowledgement.acknowledgedAt === 'string' &&
140
- ['listed', 'rendered'].includes(acknowledgement.stage),
141
- );
142
- }
143
-
144
- export function readAppDevSessionMountAcknowledgements(sessionsFilePath) {
145
- const filePath = getAppDevSessionMountAcksFile(sessionsFilePath);
146
- if (!existsSync(filePath)) {
147
- return { version: APP_DEV_SESSION_MOUNT_ACKS_VERSION, acknowledgements: [] };
148
- }
149
- try {
150
- const raw = JSON.parse(readFileSync(filePath, 'utf8'));
151
- return {
152
- version: APP_DEV_SESSION_MOUNT_ACKS_VERSION,
153
- acknowledgements: normalizeMountAcknowledgements(raw),
154
- };
155
- } catch {
156
- return { version: APP_DEV_SESSION_MOUNT_ACKS_VERSION, acknowledgements: [] };
157
- }
158
- }
159
-
160
- function writeAppDevSessionMountAcknowledgements(registry, sessionsFilePath) {
161
- const filePath = getAppDevSessionMountAcksFile(sessionsFilePath);
162
- mkdirSync(dirname(filePath), { recursive: true, mode: 0o700 });
163
- const normalized = {
164
- version: APP_DEV_SESSION_MOUNT_ACKS_VERSION,
165
- acknowledgements: normalizeMountAcknowledgements(registry),
166
- };
167
- const tempPath = `${filePath}.${process.pid}.${randomUUID()}.tmp`;
168
- writeFileSync(tempPath, JSON.stringify(normalized, null, 2), { mode: 0o600 });
169
- renameSync(tempPath, filePath);
170
- return normalized;
171
- }
172
-
173
- export function removeAppDevSessionMountAcknowledgements(sessionId, sessionsFilePath) {
174
- const filePath = getAppDevSessionMountAcksFile(sessionsFilePath);
175
- if (!existsSync(filePath)) {
176
- return { version: APP_DEV_SESSION_MOUNT_ACKS_VERSION, acknowledgements: [] };
177
- }
178
- const registry = readAppDevSessionMountAcknowledgements(sessionsFilePath);
179
- registry.acknowledgements = registry.acknowledgements.filter(
180
- (acknowledgement) => acknowledgement.sessionId !== sessionId,
181
- );
182
- return writeAppDevSessionMountAcknowledgements(registry, sessionsFilePath);
183
- }
184
-
185
- function mountAcknowledgementKey(value, fallbackStage = 'listed') {
186
- return `${value.sessionId}:${value.appId}:${value.devSlug}:${value.mountNonce}:${value.stage || fallbackStage}`;
187
- }
188
-
189
- export async function waitForAppDevSessionMountAcknowledgements(expectedSessions, options = {}) {
190
- const expected = Array.isArray(expectedSessions) ? expectedSessions : [expectedSessions];
191
- const stage = options.stage === 'rendered' ? 'rendered' : 'listed';
192
- const timeoutMs = Number.isFinite(options.timeoutMs) ? Math.max(0, options.timeoutMs) : 15_000;
193
- const pollIntervalMs = Number.isFinite(options.pollIntervalMs)
194
- ? Math.max(1, options.pollIntervalMs)
195
- : 100;
196
- const now = typeof options.now === 'function' ? options.now : Date.now;
197
- const sleep = typeof options.sleep === 'function'
198
- ? options.sleep
199
- : (delayMs) => new Promise((resolve) => setTimeout(resolve, delayMs));
200
- const startedAt = now();
201
-
202
- while (true) {
203
- const registry = readAppDevSessionMountAcknowledgements(options.sessionsFilePath);
204
- const acknowledgedKeys = new Set(registry.acknowledgements.map(mountAcknowledgementKey));
205
- const missing = expected.filter(
206
- (session) => !acknowledgedKeys.has(mountAcknowledgementKey(session, stage)),
207
- );
208
- if (missing.length === 0) {
209
- return { mounted: true, missing: [], acknowledgements: registry.acknowledgements };
210
- }
211
-
212
- const elapsedMs = now() - startedAt;
213
- if (elapsedMs >= timeoutMs) {
214
- return { mounted: false, missing, acknowledgements: registry.acknowledgements };
215
- }
216
- await sleep(Math.min(pollIntervalMs, timeoutMs - elapsedMs));
217
- }
218
- }
219
-
220
- export function waitForAppDevSessionRenderAcknowledgements(expectedSessions, options = {}) {
221
- return waitForAppDevSessionMountAcknowledgements(expectedSessions, {
222
- ...options,
223
- stage: 'rendered',
177
+ export function removeAppDevSession(sessionId, filePath) {
178
+ const resolvedFilePath = getAppDevSessionsFile(filePath);
179
+ const result = withRegistryLock(resolvedFilePath, () => {
180
+ const registry = readAppDevSessions(resolvedFilePath);
181
+ registry.sessions = registry.sessions.filter((session) => session.sessionId !== sessionId);
182
+ return writeAppDevSessionsUnlocked(registry, resolvedFilePath);
224
183
  });
184
+ return result;
225
185
  }