@notis_ai/cli 0.2.14 → 0.2.16

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 (299) hide show
  1. package/README.md +83 -21
  2. package/dist/agent-hooks/notis-agent-hook.mjs +20168 -0
  3. package/{skills → dist/base-skills}/notis-apps/SKILL.md +167 -72
  4. package/{skills → dist/base-skills}/notis-cli/SKILL.md +86 -28
  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 +39 -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 +1021 -424
  16. package/src/command-specs/auth.js +63 -10
  17. package/src/command-specs/index.js +6 -0
  18. package/src/command-specs/onboarding.js +69 -5
  19. package/src/command-specs/skills.js +56 -0
  20. package/src/runtime/agent-memory-state.js +126 -0
  21. package/src/runtime/agent-setup.js +383 -0
  22. package/src/runtime/app-dev-consumers.js +154 -0
  23. package/src/runtime/app-dev-host-lock.js +80 -0
  24. package/src/runtime/app-dev-process-identity.js +111 -0
  25. package/src/runtime/app-dev-roots.js +284 -0
  26. package/src/runtime/app-dev-server.js +447 -29
  27. package/src/runtime/app-dev-sessions.js +105 -145
  28. package/src/runtime/app-platform.js +1314 -166
  29. package/src/runtime/app-registry-scaffolds.js +367 -0
  30. package/src/runtime/base-skills.d.ts +20 -0
  31. package/src/runtime/base-skills.js +167 -0
  32. package/src/runtime/login-listener.js +15 -0
  33. package/src/runtime/oauth.js +1506 -141
  34. package/src/runtime/output.js +7 -0
  35. package/src/runtime/ports.js +16 -0
  36. package/src/runtime/profiles.js +19 -16
  37. package/src/runtime/skill-sync/cloud-client.ts +96 -0
  38. package/src/runtime/skill-sync/index.ts +644 -0
  39. package/src/runtime/skill-sync/local-scanner.ts +1046 -0
  40. package/src/runtime/skill-sync/symlink-manager.ts +383 -0
  41. package/src/runtime/skill-sync/sync-plan.ts +22 -0
  42. package/src/runtime/skill-sync/types.ts +103 -0
  43. package/src/runtime/skill-sync/write-cloud-skill.ts +50 -0
  44. package/src/runtime/store-screenshot.js +6 -1
  45. package/src/runtime/sync-skills.d.ts +37 -0
  46. package/src/runtime/sync-skills.js +231 -0
  47. package/template/app/layout.tsx +5 -1
  48. package/template/notis.config.ts +1 -0
  49. package/template/packages/sdk/package.json +2 -1
  50. package/template/packages/sdk/src/components/DocumentEditor.tsx +13 -3
  51. package/template/packages/sdk/src/components/MarkdownEditor.tsx +121 -0
  52. package/template/packages/sdk/src/components/MultiSelectActionBar.tsx +43 -119
  53. package/template/packages/sdk/src/components/MultiSelectCheckbox.tsx +5 -1
  54. package/template/packages/sdk/src/components/MultiSelectDragOverlay.tsx +1 -1
  55. package/template/packages/sdk/src/components/NotisSelectionBoundary.tsx +55 -0
  56. package/template/packages/sdk/src/components/ShortcutHints.tsx +56 -0
  57. package/template/packages/sdk/src/config.ts +25 -0
  58. package/template/packages/sdk/src/documents.ts +7 -1
  59. package/template/packages/sdk/src/hooks/useActiveResource.ts +19 -0
  60. package/template/packages/sdk/src/hooks/useCollectionInteractions.ts +726 -0
  61. package/template/packages/sdk/src/hooks/useHandover.ts +3 -0
  62. package/template/packages/sdk/src/hooks/useMultiSelect.ts +44 -482
  63. package/template/packages/sdk/src/hooks/useNotis.ts +3 -0
  64. package/template/packages/sdk/src/hooks/useNotisNavigation.ts +7 -4
  65. package/template/packages/sdk/src/hooks/useTool.ts +5 -4
  66. package/template/packages/sdk/src/index.ts +47 -2
  67. package/template/packages/sdk/src/interactions/actions.ts +46 -0
  68. package/template/packages/sdk/src/interactions/shortcuts.tsx +634 -0
  69. package/template/packages/sdk/src/interactions.ts +41 -0
  70. package/template/packages/sdk/src/provider.tsx +2 -1
  71. package/template/packages/sdk/src/runtime.ts +92 -4
  72. package/dist/scaffolds/notis-database/CHANGELOG.md +0 -5
  73. package/dist/scaffolds/notis-database/app/globals.css +0 -44
  74. package/dist/scaffolds/notis-database/app/layout.tsx +0 -6
  75. package/dist/scaffolds/notis-database/app/page.tsx +0 -1088
  76. package/dist/scaffolds/notis-database/components/ui/badge.tsx +0 -28
  77. package/dist/scaffolds/notis-database/components/ui/button.tsx +0 -53
  78. package/dist/scaffolds/notis-database/components/ui/card.tsx +0 -56
  79. package/dist/scaffolds/notis-database/components/ui/table.tsx +0 -120
  80. package/dist/scaffolds/notis-database/components.json +0 -20
  81. package/dist/scaffolds/notis-database/index.html +0 -12
  82. package/dist/scaffolds/notis-database/lib/types.ts +0 -132
  83. package/dist/scaffolds/notis-database/lib/utils.ts +0 -6
  84. package/dist/scaffolds/notis-database/metadata/screenshot-1.png +0 -0
  85. package/dist/scaffolds/notis-database/metadata/screenshot-2.png +0 -0
  86. package/dist/scaffolds/notis-database/metadata/screenshot-3.png +0 -0
  87. package/dist/scaffolds/notis-database/metadata/screenshot-4.png +0 -0
  88. package/dist/scaffolds/notis-database/metadata/screenshot-5.png +0 -0
  89. package/dist/scaffolds/notis-database/metadata/screenshot-fixtures.json +0 -1839
  90. package/dist/scaffolds/notis-database/notis.config.ts +0 -73
  91. package/dist/scaffolds/notis-database/package-lock.json +0 -3935
  92. package/dist/scaffolds/notis-database/package.json +0 -32
  93. package/dist/scaffolds/notis-database/packages/sdk/package.json +0 -36
  94. package/dist/scaffolds/notis-database/packages/sdk/src/components/DocumentEditor.tsx +0 -93
  95. package/dist/scaffolds/notis-database/packages/sdk/src/components/Markdown.tsx +0 -60
  96. package/dist/scaffolds/notis-database/packages/sdk/src/components/MultiSelectActionBar.tsx +0 -278
  97. package/dist/scaffolds/notis-database/packages/sdk/src/components/MultiSelectCheckbox.tsx +0 -91
  98. package/dist/scaffolds/notis-database/packages/sdk/src/components/MultiSelectDragOverlay.tsx +0 -39
  99. package/dist/scaffolds/notis-database/packages/sdk/src/config.ts +0 -234
  100. package/dist/scaffolds/notis-database/packages/sdk/src/documents.ts +0 -250
  101. package/dist/scaffolds/notis-database/packages/sdk/src/hooks/useBackend.ts +0 -41
  102. package/dist/scaffolds/notis-database/packages/sdk/src/hooks/useCloudComputer.ts +0 -97
  103. package/dist/scaffolds/notis-database/packages/sdk/src/hooks/useDatabaseSchema.ts +0 -85
  104. package/dist/scaffolds/notis-database/packages/sdk/src/hooks/useDatabaseSubscription.ts +0 -76
  105. package/dist/scaffolds/notis-database/packages/sdk/src/hooks/useDocument.ts +0 -78
  106. package/dist/scaffolds/notis-database/packages/sdk/src/hooks/useDocuments.ts +0 -121
  107. package/dist/scaffolds/notis-database/packages/sdk/src/hooks/useHandover.ts +0 -75
  108. package/dist/scaffolds/notis-database/packages/sdk/src/hooks/useMultiSelect.ts +0 -539
  109. package/dist/scaffolds/notis-database/packages/sdk/src/hooks/useNotis.ts +0 -34
  110. package/dist/scaffolds/notis-database/packages/sdk/src/hooks/useNotisNavigation.ts +0 -49
  111. package/dist/scaffolds/notis-database/packages/sdk/src/hooks/useTool.ts +0 -64
  112. package/dist/scaffolds/notis-database/packages/sdk/src/hooks/useTools.ts +0 -56
  113. package/dist/scaffolds/notis-database/packages/sdk/src/hooks/useTopBarSearch.ts +0 -73
  114. package/dist/scaffolds/notis-database/packages/sdk/src/hooks/useUpsertDocument.ts +0 -95
  115. package/dist/scaffolds/notis-database/packages/sdk/src/index.ts +0 -100
  116. package/dist/scaffolds/notis-database/packages/sdk/src/provider.tsx +0 -43
  117. package/dist/scaffolds/notis-database/packages/sdk/src/runtime.ts +0 -351
  118. package/dist/scaffolds/notis-database/packages/sdk/src/styles.css +0 -186
  119. package/dist/scaffolds/notis-database/packages/sdk/src/ui.ts +0 -15
  120. package/dist/scaffolds/notis-database/packages/sdk/src/vite.ts +0 -56
  121. package/dist/scaffolds/notis-database/packages/sdk/tsconfig.json +0 -15
  122. package/dist/scaffolds/notis-database/postcss.config.mjs +0 -8
  123. package/dist/scaffolds/notis-database/src/dev-main.tsx +0 -23
  124. package/dist/scaffolds/notis-database/src/mock-runtime.ts +0 -561
  125. package/dist/scaffolds/notis-database/tailwind.config.ts +0 -59
  126. package/dist/scaffolds/notis-database/tsconfig.json +0 -23
  127. package/dist/scaffolds/notis-database/vite.config.ts +0 -22
  128. package/dist/scaffolds/notis-journal/CHANGELOG.md +0 -29
  129. package/dist/scaffolds/notis-journal/app/globals.css +0 -37
  130. package/dist/scaffolds/notis-journal/app/insights/page.tsx +0 -513
  131. package/dist/scaffolds/notis-journal/app/journal-core.tsx +0 -362
  132. package/dist/scaffolds/notis-journal/app/journal-ui.tsx +0 -337
  133. package/dist/scaffolds/notis-journal/app/layout.tsx +0 -6
  134. package/dist/scaffolds/notis-journal/app/page.tsx +0 -486
  135. package/dist/scaffolds/notis-journal/components/ui/badge.tsx +0 -28
  136. package/dist/scaffolds/notis-journal/components/ui/button.tsx +0 -53
  137. package/dist/scaffolds/notis-journal/components/ui/card.tsx +0 -56
  138. package/dist/scaffolds/notis-journal/components.json +0 -20
  139. package/dist/scaffolds/notis-journal/index.html +0 -12
  140. package/dist/scaffolds/notis-journal/lib/utils.ts +0 -6
  141. package/dist/scaffolds/notis-journal/metadata/screenshot-1.png +0 -0
  142. package/dist/scaffolds/notis-journal/metadata/screenshot-2.png +0 -0
  143. package/dist/scaffolds/notis-journal/metadata/screenshot-3.png +0 -0
  144. package/dist/scaffolds/notis-journal/metadata/screenshot-4.png +0 -0
  145. package/dist/scaffolds/notis-journal/metadata/screenshot-5.png +0 -0
  146. package/dist/scaffolds/notis-journal/metadata/screenshot-6.png +0 -0
  147. package/dist/scaffolds/notis-journal/metadata/screenshot-fixtures.json +0 -132
  148. package/dist/scaffolds/notis-journal/notis.config.ts +0 -93
  149. package/dist/scaffolds/notis-journal/package-lock.json +0 -4615
  150. package/dist/scaffolds/notis-journal/package.json +0 -34
  151. package/dist/scaffolds/notis-journal/packages/sdk/package.json +0 -36
  152. package/dist/scaffolds/notis-journal/packages/sdk/src/components/DocumentEditor.tsx +0 -93
  153. package/dist/scaffolds/notis-journal/packages/sdk/src/components/Markdown.tsx +0 -60
  154. package/dist/scaffolds/notis-journal/packages/sdk/src/components/MultiSelectActionBar.tsx +0 -278
  155. package/dist/scaffolds/notis-journal/packages/sdk/src/components/MultiSelectCheckbox.tsx +0 -91
  156. package/dist/scaffolds/notis-journal/packages/sdk/src/components/MultiSelectDragOverlay.tsx +0 -39
  157. package/dist/scaffolds/notis-journal/packages/sdk/src/config.ts +0 -234
  158. package/dist/scaffolds/notis-journal/packages/sdk/src/documents.ts +0 -250
  159. package/dist/scaffolds/notis-journal/packages/sdk/src/hooks/useBackend.ts +0 -41
  160. package/dist/scaffolds/notis-journal/packages/sdk/src/hooks/useCloudComputer.ts +0 -97
  161. package/dist/scaffolds/notis-journal/packages/sdk/src/hooks/useDatabaseSchema.ts +0 -85
  162. package/dist/scaffolds/notis-journal/packages/sdk/src/hooks/useDatabaseSubscription.ts +0 -76
  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/useHandover.ts +0 -75
  166. package/dist/scaffolds/notis-journal/packages/sdk/src/hooks/useMultiSelect.ts +0 -539
  167. package/dist/scaffolds/notis-journal/packages/sdk/src/hooks/useNotis.ts +0 -34
  168. package/dist/scaffolds/notis-journal/packages/sdk/src/hooks/useNotisNavigation.ts +0 -49
  169. package/dist/scaffolds/notis-journal/packages/sdk/src/hooks/useTool.ts +0 -64
  170. package/dist/scaffolds/notis-journal/packages/sdk/src/hooks/useTools.ts +0 -56
  171. package/dist/scaffolds/notis-journal/packages/sdk/src/hooks/useTopBarSearch.ts +0 -73
  172. package/dist/scaffolds/notis-journal/packages/sdk/src/hooks/useUpsertDocument.ts +0 -95
  173. package/dist/scaffolds/notis-journal/packages/sdk/src/index.ts +0 -100
  174. package/dist/scaffolds/notis-journal/packages/sdk/src/provider.tsx +0 -43
  175. package/dist/scaffolds/notis-journal/packages/sdk/src/runtime.ts +0 -351
  176. package/dist/scaffolds/notis-journal/packages/sdk/src/styles.css +0 -186
  177. package/dist/scaffolds/notis-journal/packages/sdk/src/ui.ts +0 -15
  178. package/dist/scaffolds/notis-journal/packages/sdk/src/vite.ts +0 -56
  179. package/dist/scaffolds/notis-journal/packages/sdk/tsconfig.json +0 -15
  180. package/dist/scaffolds/notis-journal/postcss.config.mjs +0 -8
  181. package/dist/scaffolds/notis-journal/skills/journal-onboarding/SKILL.md +0 -120
  182. package/dist/scaffolds/notis-journal/src/dev-main.tsx +0 -58
  183. package/dist/scaffolds/notis-journal/src/mock-runtime.ts +0 -199
  184. package/dist/scaffolds/notis-journal/tailwind.config.ts +0 -58
  185. package/dist/scaffolds/notis-journal/tsconfig.json +0 -23
  186. package/dist/scaffolds/notis-journal/vite.config.ts +0 -10
  187. package/dist/scaffolds/notis-notes/CHANGELOG.md +0 -5
  188. package/dist/scaffolds/notis-notes/app/globals.css +0 -3
  189. package/dist/scaffolds/notis-notes/app/layout.tsx +0 -6
  190. package/dist/scaffolds/notis-notes/app/page.tsx +0 -1943
  191. package/dist/scaffolds/notis-notes/app/phosphor-icons.ts +0 -596
  192. package/dist/scaffolds/notis-notes/components/ui/badge.tsx +0 -28
  193. package/dist/scaffolds/notis-notes/components/ui/button.tsx +0 -53
  194. package/dist/scaffolds/notis-notes/components/ui/card.tsx +0 -56
  195. package/dist/scaffolds/notis-notes/components.json +0 -20
  196. package/dist/scaffolds/notis-notes/lib/utils.ts +0 -6
  197. package/dist/scaffolds/notis-notes/lib/visible-properties.ts +0 -144
  198. package/dist/scaffolds/notis-notes/metadata/screenshot-1.png +0 -0
  199. package/dist/scaffolds/notis-notes/metadata/screenshot-2.png +0 -0
  200. package/dist/scaffolds/notis-notes/metadata/screenshot-3.png +0 -0
  201. package/dist/scaffolds/notis-notes/metadata/screenshot-4.png +0 -0
  202. package/dist/scaffolds/notis-notes/metadata/screenshot-5.png +0 -0
  203. package/dist/scaffolds/notis-notes/metadata/screenshot-fixtures.json +0 -752
  204. package/dist/scaffolds/notis-notes/notis.config.ts +0 -80
  205. package/dist/scaffolds/notis-notes/package-lock.json +0 -4636
  206. package/dist/scaffolds/notis-notes/package.json +0 -35
  207. package/dist/scaffolds/notis-notes/packages/sdk/package.json +0 -36
  208. package/dist/scaffolds/notis-notes/packages/sdk/src/components/DocumentEditor.tsx +0 -93
  209. package/dist/scaffolds/notis-notes/packages/sdk/src/components/Markdown.tsx +0 -60
  210. package/dist/scaffolds/notis-notes/packages/sdk/src/components/MultiSelectActionBar.tsx +0 -278
  211. package/dist/scaffolds/notis-notes/packages/sdk/src/components/MultiSelectCheckbox.tsx +0 -91
  212. package/dist/scaffolds/notis-notes/packages/sdk/src/components/MultiSelectDragOverlay.tsx +0 -39
  213. package/dist/scaffolds/notis-notes/packages/sdk/src/config.ts +0 -234
  214. package/dist/scaffolds/notis-notes/packages/sdk/src/documents.ts +0 -250
  215. package/dist/scaffolds/notis-notes/packages/sdk/src/hooks/useBackend.ts +0 -41
  216. package/dist/scaffolds/notis-notes/packages/sdk/src/hooks/useCloudComputer.ts +0 -97
  217. package/dist/scaffolds/notis-notes/packages/sdk/src/hooks/useDatabaseSchema.ts +0 -85
  218. package/dist/scaffolds/notis-notes/packages/sdk/src/hooks/useDatabaseSubscription.ts +0 -76
  219. package/dist/scaffolds/notis-notes/packages/sdk/src/hooks/useDocument.ts +0 -78
  220. package/dist/scaffolds/notis-notes/packages/sdk/src/hooks/useDocuments.ts +0 -121
  221. package/dist/scaffolds/notis-notes/packages/sdk/src/hooks/useHandover.ts +0 -75
  222. package/dist/scaffolds/notis-notes/packages/sdk/src/hooks/useMultiSelect.ts +0 -539
  223. package/dist/scaffolds/notis-notes/packages/sdk/src/hooks/useNotis.ts +0 -34
  224. package/dist/scaffolds/notis-notes/packages/sdk/src/hooks/useNotisNavigation.ts +0 -49
  225. package/dist/scaffolds/notis-notes/packages/sdk/src/hooks/useTool.ts +0 -64
  226. package/dist/scaffolds/notis-notes/packages/sdk/src/hooks/useTools.ts +0 -56
  227. package/dist/scaffolds/notis-notes/packages/sdk/src/hooks/useTopBarSearch.ts +0 -73
  228. package/dist/scaffolds/notis-notes/packages/sdk/src/hooks/useUpsertDocument.ts +0 -95
  229. package/dist/scaffolds/notis-notes/packages/sdk/src/index.ts +0 -100
  230. package/dist/scaffolds/notis-notes/packages/sdk/src/provider.tsx +0 -43
  231. package/dist/scaffolds/notis-notes/packages/sdk/src/runtime.ts +0 -351
  232. package/dist/scaffolds/notis-notes/packages/sdk/src/styles.css +0 -186
  233. package/dist/scaffolds/notis-notes/packages/sdk/src/ui.ts +0 -15
  234. package/dist/scaffolds/notis-notes/packages/sdk/src/vite.ts +0 -56
  235. package/dist/scaffolds/notis-notes/packages/sdk/tsconfig.json +0 -15
  236. package/dist/scaffolds/notis-notes/postcss.config.mjs +0 -8
  237. package/dist/scaffolds/notis-notes/tailwind.config.ts +0 -58
  238. package/dist/scaffolds/notis-notes/tsconfig.json +0 -23
  239. package/dist/scaffolds/notis-notes/vite.config.ts +0 -10
  240. package/dist/scaffolds/notis-random/CHANGELOG.md +0 -15
  241. package/dist/scaffolds/notis-random/README.md +0 -33
  242. package/dist/scaffolds/notis-random/app/globals.css +0 -11
  243. package/dist/scaffolds/notis-random/app/history/page.tsx +0 -67
  244. package/dist/scaffolds/notis-random/app/layout.tsx +0 -7
  245. package/dist/scaffolds/notis-random/app/page.tsx +0 -289
  246. package/dist/scaffolds/notis-random/components/ui/button.tsx +0 -50
  247. package/dist/scaffolds/notis-random/components/ui/card.tsx +0 -16
  248. package/dist/scaffolds/notis-random/components/ui/input.tsx +0 -23
  249. package/dist/scaffolds/notis-random/components.json +0 -20
  250. package/dist/scaffolds/notis-random/index.html +0 -12
  251. package/dist/scaffolds/notis-random/lib/notis-tools.ts +0 -128
  252. package/dist/scaffolds/notis-random/lib/rng.ts +0 -202
  253. package/dist/scaffolds/notis-random/lib/roll-record.ts +0 -189
  254. package/dist/scaffolds/notis-random/lib/utils.ts +0 -25
  255. package/dist/scaffolds/notis-random/metadata/screenshot-1.png +0 -0
  256. package/dist/scaffolds/notis-random/metadata/screenshot-2.png +0 -0
  257. package/dist/scaffolds/notis-random/metadata/screenshot-3.png +0 -0
  258. package/dist/scaffolds/notis-random/metadata/screenshot-4.png +0 -0
  259. package/dist/scaffolds/notis-random/metadata/screenshot-5.png +0 -0
  260. package/dist/scaffolds/notis-random/metadata/screenshot-fixtures.json +0 -753
  261. package/dist/scaffolds/notis-random/notis.config.ts +0 -86
  262. package/dist/scaffolds/notis-random/package-lock.json +0 -4513
  263. package/dist/scaffolds/notis-random/package.json +0 -36
  264. package/dist/scaffolds/notis-random/packages/sdk/package.json +0 -36
  265. package/dist/scaffolds/notis-random/packages/sdk/src/components/DocumentEditor.tsx +0 -93
  266. package/dist/scaffolds/notis-random/packages/sdk/src/components/Markdown.tsx +0 -60
  267. package/dist/scaffolds/notis-random/packages/sdk/src/components/MultiSelectActionBar.tsx +0 -278
  268. package/dist/scaffolds/notis-random/packages/sdk/src/components/MultiSelectCheckbox.tsx +0 -91
  269. package/dist/scaffolds/notis-random/packages/sdk/src/components/MultiSelectDragOverlay.tsx +0 -39
  270. package/dist/scaffolds/notis-random/packages/sdk/src/config.ts +0 -234
  271. package/dist/scaffolds/notis-random/packages/sdk/src/documents.ts +0 -250
  272. package/dist/scaffolds/notis-random/packages/sdk/src/hooks/useBackend.ts +0 -41
  273. package/dist/scaffolds/notis-random/packages/sdk/src/hooks/useCloudComputer.ts +0 -97
  274. package/dist/scaffolds/notis-random/packages/sdk/src/hooks/useDatabaseSchema.ts +0 -85
  275. package/dist/scaffolds/notis-random/packages/sdk/src/hooks/useDatabaseSubscription.ts +0 -76
  276. package/dist/scaffolds/notis-random/packages/sdk/src/hooks/useDocument.ts +0 -78
  277. package/dist/scaffolds/notis-random/packages/sdk/src/hooks/useDocuments.ts +0 -121
  278. package/dist/scaffolds/notis-random/packages/sdk/src/hooks/useHandover.ts +0 -75
  279. package/dist/scaffolds/notis-random/packages/sdk/src/hooks/useMultiSelect.ts +0 -539
  280. package/dist/scaffolds/notis-random/packages/sdk/src/hooks/useNotis.ts +0 -34
  281. package/dist/scaffolds/notis-random/packages/sdk/src/hooks/useNotisNavigation.ts +0 -49
  282. package/dist/scaffolds/notis-random/packages/sdk/src/hooks/useTool.ts +0 -64
  283. package/dist/scaffolds/notis-random/packages/sdk/src/hooks/useTools.ts +0 -56
  284. package/dist/scaffolds/notis-random/packages/sdk/src/hooks/useTopBarSearch.ts +0 -73
  285. package/dist/scaffolds/notis-random/packages/sdk/src/hooks/useUpsertDocument.ts +0 -95
  286. package/dist/scaffolds/notis-random/packages/sdk/src/index.ts +0 -100
  287. package/dist/scaffolds/notis-random/packages/sdk/src/provider.tsx +0 -43
  288. package/dist/scaffolds/notis-random/packages/sdk/src/runtime.ts +0 -351
  289. package/dist/scaffolds/notis-random/packages/sdk/src/styles.css +0 -186
  290. package/dist/scaffolds/notis-random/packages/sdk/src/ui.ts +0 -15
  291. package/dist/scaffolds/notis-random/packages/sdk/src/vite.ts +0 -56
  292. package/dist/scaffolds/notis-random/packages/sdk/tsconfig.json +0 -15
  293. package/dist/scaffolds/notis-random/postcss.config.mjs +0 -6
  294. package/dist/scaffolds/notis-random/src/dev-main.tsx +0 -70
  295. package/dist/scaffolds/notis-random/src/mock-runtime.ts +0 -129
  296. package/dist/scaffolds/notis-random/tailwind.config.ts +0 -50
  297. package/dist/scaffolds/notis-random/tsconfig.json +0 -23
  298. package/dist/scaffolds/notis-random/vite.config.ts +0 -11
  299. package/dist/scaffolds.json +0 -49
@@ -0,0 +1,383 @@
1
+ import {
2
+ chmodSync,
3
+ existsSync,
4
+ mkdirSync,
5
+ readFileSync,
6
+ readdirSync,
7
+ renameSync,
8
+ writeFileSync,
9
+ } from 'node:fs';
10
+ import { createHash } from 'node:crypto';
11
+ import { homedir } from 'node:os';
12
+ import { dirname, join } from 'node:path';
13
+ import { fileURLToPath } from 'node:url';
14
+
15
+ const HERE = dirname(fileURLToPath(import.meta.url));
16
+ const INSTRUCTIONS_PATH = join(HERE, '..', '..', 'skills', 'notis-cli', 'AGENT_INSTRUCTIONS.md');
17
+ const HOOK_BUNDLE_PATH = join(HERE, '..', '..', 'dist', 'agent-hooks', 'notis-agent-hook.mjs');
18
+ const START_MARKER = '<!-- notis-cli:instructions:start -->';
19
+ const END_MARKER = '<!-- notis-cli:instructions:end -->';
20
+ const LEGACY_SENTINEL = 'Use the Notis CLI (`npx --package @notis_ai/cli@latest -- notis ...`)';
21
+ const MANAGED_HOOK_MARKER = '--notis-managed-agent-hook';
22
+ const LEGACY_MANAGED_HOOK_MARKER = 'NOTIS_MANAGED_AGENT_HOOK=1';
23
+
24
+ export const AGENT_IDS = Object.freeze(['codex', 'claude-code']);
25
+
26
+ function atomicWrite(filePath, contents, mode = 0o600) {
27
+ mkdirSync(dirname(filePath), { recursive: true, mode: 0o700 });
28
+ const temporaryPath = `${filePath}.notis-${process.pid}-${Date.now()}.tmp`;
29
+ writeFileSync(temporaryPath, contents, { mode });
30
+ renameSync(temporaryPath, filePath);
31
+ }
32
+
33
+ function readText(filePath) {
34
+ return existsSync(filePath) ? readFileSync(filePath, 'utf-8') : '';
35
+ }
36
+
37
+ function activeCodexInstructionsPath(home) {
38
+ const overridePath = join(home, '.codex', 'AGENTS.override.md');
39
+ if (existsSync(overridePath) && readText(overridePath).trim()) {
40
+ return overridePath;
41
+ }
42
+ return join(home, '.codex', 'AGENTS.md');
43
+ }
44
+
45
+ function upsertInstructionBlock(filePath, block) {
46
+ const existing = readText(filePath);
47
+ const managedBlockPattern = new RegExp(
48
+ `${START_MARKER.replace(/[.*+?^${}()|[\]\\]/g, '\\$&')}[\\s\\S]*?${END_MARKER.replace(/[.*+?^${}()|[\]\\]/g, '\\$&')}`,
49
+ 'g',
50
+ );
51
+ const managedBlocks = [...existing.matchAll(managedBlockPattern)];
52
+ let next;
53
+ let status;
54
+
55
+ if (managedBlocks.length) {
56
+ let replaced = false;
57
+ next = existing.replace(managedBlockPattern, () => {
58
+ if (replaced) return '';
59
+ replaced = true;
60
+ return block;
61
+ });
62
+ status = next === existing ? 'unchanged' : 'updated';
63
+ } else if (existing.includes(LEGACY_SENTINEL)) {
64
+ // An unmarked block belongs to the user or another installer. Do not risk
65
+ // rewriting adjacent personal instructions merely to take ownership of it.
66
+ return { path: filePath, status: 'already_present_unmanaged' };
67
+ } else {
68
+ const prefix = existing.trimEnd();
69
+ next = `${prefix ? `${prefix}\n\n` : ''}${block.trim()}\n`;
70
+ status = 'installed';
71
+ }
72
+
73
+ if (status !== 'unchanged') {
74
+ atomicWrite(filePath, next);
75
+ }
76
+ return { path: filePath, status };
77
+ }
78
+
79
+ function readJsonObject(filePath) {
80
+ if (!existsSync(filePath)) return {};
81
+ const raw = readFileSync(filePath, 'utf-8');
82
+ try {
83
+ const parsed = JSON.parse(raw);
84
+ if (!parsed || typeof parsed !== 'object' || Array.isArray(parsed)) {
85
+ throw new Error('root value must be an object');
86
+ }
87
+ return parsed;
88
+ } catch (error) {
89
+ throw new Error(`Refusing to modify invalid JSON at ${filePath}: ${error.message}`);
90
+ }
91
+ }
92
+
93
+ function isManagedNotisHookCommand(command) {
94
+ if (typeof command !== 'string') return false;
95
+ if (
96
+ command.includes(MANAGED_HOOK_MARKER)
97
+ || command.includes(LEGACY_MANAGED_HOOK_MARKER)
98
+ ) return true;
99
+ if (
100
+ command.includes('.notis/agent-hooks/bin/notis-agent-hook')
101
+ && /\sagent-(?:context|capture)\b/.test(command)
102
+ ) return true;
103
+ // Supported legacy installers used either this package's bin/notis.js path
104
+ // or the exact public npx package. Do not claim arbitrary commands merely
105
+ // because they happen to use the same subcommand words.
106
+ return (
107
+ /(?:^|\s)(?:'[^']*|"[^"]*"|\S*)bin\/notis\.js(?:'|")?\s[^\n]*\sagent-(?:context|capture)\b/.test(command)
108
+ || /@notis_ai\/cli@[^\s]+\s+--\s+notis\s[^\n]*\sagent-(?:context|capture)\b/.test(command)
109
+ || /^\s*notis\s+(?:--[^\s]+\s+)*agent-(?:context|capture)\b/.test(command)
110
+ );
111
+ }
112
+
113
+ function objectHasManagedNotisHook(value) {
114
+ if (Array.isArray(value)) return value.some(objectHasManagedNotisHook);
115
+ if (!value || typeof value !== 'object') return false;
116
+ if (isManagedNotisHookCommand(value.command)) return true;
117
+ return Object.values(value).some(objectHasManagedNotisHook);
118
+ }
119
+
120
+ function fileHasManagedNotisHook(filePath) {
121
+ try {
122
+ return objectHasManagedNotisHook(readJsonObject(filePath));
123
+ } catch {
124
+ return false;
125
+ }
126
+ }
127
+
128
+ function removePriorNotisPromptHooks(groups) {
129
+ if (!Array.isArray(groups)) return [];
130
+ const kept = [];
131
+ for (const group of groups) {
132
+ if (!group || typeof group !== 'object' || !Array.isArray(group.hooks)) {
133
+ kept.push(group);
134
+ continue;
135
+ }
136
+ const hooks = group.hooks.filter((hook) => !isManagedNotisHookCommand(hook?.command));
137
+ if (hooks.length) kept.push({ ...group, hooks });
138
+ }
139
+ return kept;
140
+ }
141
+
142
+ function shellQuote(value) {
143
+ return `'${String(value).replaceAll("'", `'"'"'`)}'`;
144
+ }
145
+
146
+ export function installManagedHookRuntime({
147
+ home = homedir(),
148
+ bundlePath = HOOK_BUNDLE_PATH,
149
+ nodePath = process.execPath,
150
+ platform = process.platform,
151
+ } = {}) {
152
+ const bundle = readFileSync(bundlePath);
153
+ const digest = createHash('sha256').update(bundle).digest('hex');
154
+ const runtimePath = join(
155
+ home,
156
+ '.notis',
157
+ 'agent-hooks',
158
+ 'runtime',
159
+ digest,
160
+ 'notis-agent-hook.mjs',
161
+ );
162
+ const existingDigest = existsSync(runtimePath)
163
+ ? createHash('sha256').update(readFileSync(runtimePath)).digest('hex')
164
+ : null;
165
+ if (existingDigest !== digest) atomicWrite(runtimePath, bundle, 0o500);
166
+ chmodSync(runtimePath, 0o500);
167
+
168
+ const launcherPath = join(
169
+ home,
170
+ '.notis',
171
+ 'agent-hooks',
172
+ 'bin',
173
+ platform === 'win32' ? 'notis-agent-hook.cmd' : 'notis-agent-hook',
174
+ );
175
+ const launcher = platform === 'win32'
176
+ ? [
177
+ '@echo off',
178
+ `if not exist "${nodePath}" (`,
179
+ ' >&2 echo Notis memory hook runtime needs repair; run notis agents install again.',
180
+ ' exit /b 1',
181
+ ')',
182
+ `"${nodePath}" "${runtimePath}" %*`,
183
+ '',
184
+ ].join('\r\n')
185
+ : [
186
+ '#!/bin/sh',
187
+ `if [ -x ${shellQuote(nodePath)} ]; then`,
188
+ ` exec ${shellQuote(nodePath)} ${shellQuote(runtimePath)} "$@"`,
189
+ 'fi',
190
+ `echo 'Notis memory hook runtime needs repair; run notis agents install again.' >&2`,
191
+ 'exit 1',
192
+ '',
193
+ ].join('\n');
194
+ if (readText(launcherPath) !== launcher) {
195
+ // Windows maps the owner write bit to the read-only file attribute. A
196
+ // launcher installed by an older CLI is deliberately read-only, so make
197
+ // our owned file replaceable before the atomic rename used for upgrades.
198
+ if (platform === 'win32' && existsSync(launcherPath)) {
199
+ chmodSync(launcherPath, 0o700);
200
+ }
201
+ atomicWrite(launcherPath, launcher, 0o500);
202
+ }
203
+ chmodSync(launcherPath, 0o500);
204
+ return { launcherPath, runtimePath, digest };
205
+ }
206
+
207
+ function shellHookCommand(launcherPath, profileName, command, agentId = null, platform = process.platform) {
208
+ // Stored profile names follow a shell-safe grammar enforced by the CLI.
209
+ const agentFlag = agentId ? ` --agent ${agentId}` : '';
210
+ const quotedLauncher = platform === 'win32'
211
+ ? `"${launcherPath}"`
212
+ : shellQuote(launcherPath);
213
+ return `${quotedLauncher} ${MANAGED_HOOK_MARKER} --profile ${profileName} --timeout-ms 30000 ${command}${agentFlag}`;
214
+ }
215
+
216
+ function contextHandler(agentId, profileName, launcherPath, platform) {
217
+ return {
218
+ type: 'command',
219
+ command: shellHookCommand(launcherPath, profileName, 'agent-context', null, platform),
220
+ timeout: 30,
221
+ statusMessage: 'Loading relevant Notis memory',
222
+ ...(agentId === 'codex' ? { additionalContextLimit: 2500 } : {}),
223
+ };
224
+ }
225
+
226
+ function captureHandler(agentId, profileName, launcherPath, platform) {
227
+ return {
228
+ type: 'command',
229
+ command: shellHookCommand(launcherPath, profileName, 'agent-capture', agentId, platform),
230
+ timeout: 30,
231
+ statusMessage: 'Saving durable Notis context',
232
+ };
233
+ }
234
+
235
+ function upsertPromptHooks(
236
+ filePath,
237
+ agentId,
238
+ profileName,
239
+ enabled,
240
+ launcherPath = '',
241
+ platform = process.platform,
242
+ ) {
243
+ const settings = readJsonObject(filePath);
244
+ const hooks = settings.hooks && typeof settings.hooks === 'object' && !Array.isArray(settings.hooks)
245
+ ? { ...settings.hooks }
246
+ : {};
247
+ let removedManagedHook = false;
248
+ for (const event of ['SessionStart', 'UserPromptSubmit', 'Stop']) {
249
+ const previousGroups = Array.isArray(hooks[event]) ? hooks[event] : [];
250
+ const groups = removePriorNotisPromptHooks(previousGroups);
251
+ if (JSON.stringify(groups) !== JSON.stringify(previousGroups)) removedManagedHook = true;
252
+ if (enabled) {
253
+ groups.push({ hooks: [event === 'Stop'
254
+ ? captureHandler(agentId, profileName, launcherPath, platform)
255
+ : contextHandler(agentId, profileName, launcherPath, platform)] });
256
+ }
257
+ if (groups.length) hooks[event] = groups;
258
+ else delete hooks[event];
259
+ }
260
+ const next = { ...settings, hooks };
261
+ const serialized = `${JSON.stringify(next, null, 2)}\n`;
262
+ const existing = readText(filePath);
263
+ if (!enabled && !removedManagedHook) return { path: filePath, status: 'unchanged' };
264
+ if (existing === serialized) return { path: filePath, status: 'unchanged' };
265
+ atomicWrite(filePath, serialized);
266
+ return {
267
+ path: filePath,
268
+ status: enabled
269
+ ? existsSync(filePath) && existing ? 'updated' : 'installed'
270
+ : 'removed',
271
+ };
272
+ }
273
+
274
+ function agentPaths(agentId, home) {
275
+ if (agentId === 'codex') {
276
+ return {
277
+ root: join(home, '.codex'),
278
+ instructions: activeCodexInstructionsPath(home),
279
+ hooks: join(home, '.codex', 'hooks.json'),
280
+ };
281
+ }
282
+ if (agentId === 'claude-code') {
283
+ return {
284
+ root: join(home, '.claude'),
285
+ instructions: join(home, '.claude', 'CLAUDE.md'),
286
+ hooks: join(home, '.claude', 'settings.json'),
287
+ };
288
+ }
289
+ throw new Error(`Unsupported agent: ${agentId}`);
290
+ }
291
+
292
+ export function shouldInstallLocalAgentSetup(env = process.env, home = homedir()) {
293
+ if (env.NOTIS_JWT || env.NOTIS_AGENT === '1' || env.NOTIS_DELEGATED_CONTEXT === '1') return false;
294
+ if (env.CONDUCTOR_IS_LOCAL === '0') return false;
295
+ if (existsSync('/vercel/sandbox')) return false;
296
+ return Boolean(home);
297
+ }
298
+
299
+ export function installAgentSetup({
300
+ profileName,
301
+ agents = AGENT_IDS,
302
+ memoryHooks = true,
303
+ onlyExisting = false,
304
+ detectedAgents = null,
305
+ home = homedir(),
306
+ platform = process.platform,
307
+ } = {}) {
308
+ if (!/^[A-Za-z0-9][A-Za-z0-9._-]{0,63}$/.test(String(profileName || ''))) {
309
+ throw new Error('A valid authenticated Notis profile is required for agent setup');
310
+ }
311
+ const instructions = readFileSync(INSTRUCTIONS_PATH, 'utf-8').trim();
312
+ const results = [];
313
+ const shouldRepairManagedHooks = memoryHooks === true || (
314
+ memoryHooks === null
315
+ && agents.some((agentId) => fileHasManagedNotisHook(agentPaths(agentId, home).hooks))
316
+ );
317
+ const hookRuntime = shouldRepairManagedHooks
318
+ ? installManagedHookRuntime({ home, platform })
319
+ : null;
320
+
321
+ for (const agentId of agents) {
322
+ const paths = agentPaths(agentId, home);
323
+ const detected = Array.isArray(detectedAgents)
324
+ ? detectedAgents.includes(agentId)
325
+ : existsSync(paths.root);
326
+ if (onlyExisting && !detected) {
327
+ results.push({ agent: agentId, status: 'not_detected' });
328
+ continue;
329
+ }
330
+ const result = {
331
+ agent: agentId,
332
+ instructions: upsertInstructionBlock(paths.instructions, instructions),
333
+ memory_hook: null,
334
+ };
335
+ if (memoryHooks === null && !fileHasManagedNotisHook(paths.hooks)) {
336
+ result.memory_hook = { path: paths.hooks, status: 'preserved' };
337
+ } else {
338
+ try {
339
+ result.memory_hook = upsertPromptHooks(
340
+ paths.hooks,
341
+ agentId,
342
+ profileName,
343
+ memoryHooks !== false,
344
+ hookRuntime?.launcherPath,
345
+ platform,
346
+ );
347
+ } catch (error) {
348
+ result.memory_hook = {
349
+ path: paths.hooks,
350
+ status: 'error',
351
+ message: error instanceof Error ? error.message : String(error),
352
+ };
353
+ }
354
+ }
355
+ results.push(result);
356
+ }
357
+ return results;
358
+ }
359
+
360
+ export function detectedAgentIds(home = homedir()) {
361
+ return AGENT_IDS.filter((agentId) => {
362
+ const root = agentPaths(agentId, home).root;
363
+ if (!existsSync(root)) return false;
364
+ try {
365
+ const entries = readdirSync(root, { withFileTypes: true });
366
+ if (entries.some((entry) => entry.name !== 'skills')) return true;
367
+ const skillsRoot = join(root, 'skills');
368
+ if (!existsSync(skillsRoot)) return false;
369
+ // Base reconciliation intentionally creates every supported vendor root.
370
+ // A root containing only our three links is not evidence that the vendor
371
+ // itself is installed; any other skill entry is a genuine user signal.
372
+ return readdirSync(skillsRoot).some((name) => (
373
+ !['notis-apps', 'notis-query', 'notis-cli'].includes(name)
374
+ ));
375
+ } catch {
376
+ return false;
377
+ }
378
+ });
379
+ }
380
+
381
+ export function instructionTemplatePath() {
382
+ return INSTRUCTIONS_PATH;
383
+ }
@@ -0,0 +1,154 @@
1
+ import { randomUUID } from 'node:crypto';
2
+ import {
3
+ existsSync,
4
+ lstatSync,
5
+ mkdirSync,
6
+ readFileSync,
7
+ renameSync,
8
+ rmSync,
9
+ writeFileSync,
10
+ } from 'node:fs';
11
+ import { homedir } from 'node:os';
12
+ import { dirname, join } from 'node:path';
13
+
14
+ export const DEFAULT_APP_DEV_CONSUMERS_FILE = join(homedir(), '.notis', 'app-dev-consumers.json');
15
+ export const APP_DEV_CONSUMER_EXPIRES_AFTER_MS = 10_000;
16
+ const waitArray = new Int32Array(new SharedArrayBuffer(4));
17
+ const CONSUMER_LOCK_STALE_AFTER_MS = 30_000;
18
+
19
+ function processIsRunning(pid) {
20
+ try {
21
+ process.kill(pid, 0);
22
+ return true;
23
+ } catch (error) {
24
+ return error?.code !== 'ESRCH';
25
+ }
26
+ }
27
+
28
+ function reclaimConsumerLock(lockPath) {
29
+ const stat = lstatSync(lockPath);
30
+ if (!stat.isDirectory() || stat.isSymbolicLink()) {
31
+ throw new Error(`Refusing unsafe app development consumer lock: ${lockPath}`);
32
+ }
33
+ let ownerPid = null;
34
+ try {
35
+ const owner = readFileSync(join(lockPath, 'owner'), 'utf8').trim();
36
+ const parsed = Number.parseInt(owner.split('.')[0] || '', 10);
37
+ ownerPid = Number.isSafeInteger(parsed) && parsed > 0 ? parsed : null;
38
+ } catch {
39
+ // An interrupted owner write is reclaimed after the bounded stale window.
40
+ }
41
+ if (ownerPid !== null) return !processIsRunning(ownerPid);
42
+ return Date.now() - stat.mtimeMs >= CONSUMER_LOCK_STALE_AFTER_MS;
43
+ }
44
+
45
+ function withLock(filePath, callback) {
46
+ mkdirSync(dirname(filePath), { recursive: true, mode: 0o700 });
47
+ const lockPath = `${filePath}.lock`;
48
+ const owner = `${process.pid}.${randomUUID()}`;
49
+ const startedAt = Date.now();
50
+ while (true) {
51
+ try {
52
+ mkdirSync(lockPath, { mode: 0o700 });
53
+ writeFileSync(join(lockPath, 'owner'), owner, { mode: 0o600 });
54
+ break;
55
+ } catch (error) {
56
+ if (error?.code !== 'EEXIST') throw error;
57
+ try {
58
+ if (reclaimConsumerLock(lockPath)) {
59
+ rmSync(lockPath, { recursive: true, force: true });
60
+ continue;
61
+ }
62
+ } catch (lockError) {
63
+ if (lockError?.code === 'ENOENT') continue;
64
+ throw lockError;
65
+ }
66
+ if (Date.now() - startedAt > 2_000) {
67
+ throw new Error('App development consumer registry is busy.');
68
+ }
69
+ Atomics.wait(waitArray, 0, 0, 10);
70
+ }
71
+ }
72
+ try {
73
+ return callback();
74
+ } finally {
75
+ try {
76
+ if (readFileSync(join(lockPath, 'owner'), 'utf8').trim() === owner) {
77
+ rmSync(lockPath, { recursive: true, force: true });
78
+ }
79
+ } catch {
80
+ // A replaced lock belongs to another writer.
81
+ }
82
+ }
83
+ }
84
+
85
+ function normalize(value, now) {
86
+ const raw = value && typeof value === 'object' && Array.isArray(value.consumers)
87
+ ? value.consumers
88
+ : [];
89
+ return raw.filter((lease) => {
90
+ const heartbeat = Date.parse(String(lease?.lastHeartbeatAt || ''));
91
+ return typeof lease?.instanceId === 'string'
92
+ && typeof lease?.userId === 'string'
93
+ && typeof lease?.apiBase === 'string'
94
+ && Number.isSafeInteger(lease?.pid)
95
+ && Number.isFinite(heartbeat)
96
+ && now - heartbeat <= APP_DEV_CONSUMER_EXPIRES_AFTER_MS;
97
+ });
98
+ }
99
+
100
+ export function readAppDevConsumers(filePath = DEFAULT_APP_DEV_CONSUMERS_FILE, now = Date.now()) {
101
+ if (!existsSync(filePath)) return [];
102
+ try {
103
+ return normalize(JSON.parse(readFileSync(filePath, 'utf8')), now);
104
+ } catch {
105
+ return [];
106
+ }
107
+ }
108
+
109
+ function writeAppDevConsumers(filePath, consumers) {
110
+ mkdirSync(dirname(filePath), { recursive: true, mode: 0o700 });
111
+ const temporary = `${filePath}.${process.pid}.${randomUUID()}.tmp`;
112
+ writeFileSync(temporary, JSON.stringify({ version: 1, consumers }, null, 2), { mode: 0o600 });
113
+ renameSync(temporary, filePath);
114
+ }
115
+
116
+ export function heartbeatAppDevConsumer(
117
+ lease,
118
+ filePath = DEFAULT_APP_DEV_CONSUMERS_FILE,
119
+ ) {
120
+ return withLock(filePath, () => {
121
+ const now = Date.now();
122
+ const next = readAppDevConsumers(filePath, now)
123
+ .filter((entry) => entry.instanceId !== lease.instanceId);
124
+ next.push({
125
+ ...lease,
126
+ apiBase: lease.apiBase.replace(/\/$/, ''),
127
+ lastHeartbeatAt: new Date(now).toISOString(),
128
+ });
129
+ writeAppDevConsumers(filePath, next);
130
+ return next;
131
+ });
132
+ }
133
+
134
+ export function removeAppDevConsumer(
135
+ instanceId,
136
+ filePath = DEFAULT_APP_DEV_CONSUMERS_FILE,
137
+ ) {
138
+ return withLock(filePath, () => {
139
+ const next = readAppDevConsumers(filePath)
140
+ .filter((entry) => entry.instanceId !== instanceId);
141
+ writeAppDevConsumers(filePath, next);
142
+ return next;
143
+ });
144
+ }
145
+
146
+ export function hasAppDevConsumer(consumers, { mode, userId, apiBase }) {
147
+ if (mode === 'machine') return consumers.length > 0;
148
+ if (mode !== 'environment') return true;
149
+ const normalizedApiBase = String(apiBase || '').replace(/\/$/, '');
150
+ return consumers.some((consumer) => (
151
+ consumer.userId === userId
152
+ && consumer.apiBase.replace(/\/$/, '') === normalizedApiBase
153
+ ));
154
+ }
@@ -0,0 +1,80 @@
1
+ import { createHash, randomUUID } from 'node:crypto';
2
+ import { lstatSync, mkdirSync, readFileSync, rmSync, writeFileSync } from 'node:fs';
3
+ import { homedir } from 'node:os';
4
+ import { join } from 'node:path';
5
+
6
+ const DEFAULT_LOCK_ROOT = join(homedir(), '.notis', 'app-dev-host-locks');
7
+ const OWNER_FILE = 'owner.json';
8
+
9
+ function processIsRunning(pid) {
10
+ try {
11
+ process.kill(pid, 0);
12
+ return true;
13
+ } catch (error) {
14
+ return error?.code !== 'ESRCH';
15
+ }
16
+ }
17
+
18
+ function lockName(identity, apiBase, projectDir) {
19
+ return createHash('sha256').update(`${identity}\0${apiBase}\0${projectDir}`).digest('hex');
20
+ }
21
+
22
+ function readOwner(path) {
23
+ try {
24
+ const value = JSON.parse(readFileSync(join(path, OWNER_FILE), 'utf8'));
25
+ return Number.isSafeInteger(value?.pid) && typeof value?.ownerId === 'string'
26
+ ? { pid: value.pid, ownerId: value.ownerId }
27
+ : null;
28
+ } catch {
29
+ return null;
30
+ }
31
+ }
32
+
33
+ function reclaimable(path, now, staleAfterMs) {
34
+ const stat = lstatSync(path);
35
+ if (!stat.isDirectory() || stat.isSymbolicLink()) {
36
+ throw new Error(`Refusing unsafe app development host lock: ${path}`);
37
+ }
38
+ const owner = readOwner(path);
39
+ if (owner) return !processIsRunning(owner.pid);
40
+ return now - stat.mtimeMs >= staleAfterMs;
41
+ }
42
+
43
+ export function tryAcquireAppDevHostLock(options) {
44
+ const lockRoot = options.lockRoot || DEFAULT_LOCK_ROOT;
45
+ mkdirSync(lockRoot, { recursive: true, mode: 0o700 });
46
+ const path = join(lockRoot, lockName(options.identity, options.apiBase, options.projectDir));
47
+ const ownerId = `${process.pid}.${randomUUID()}`;
48
+ for (let attempt = 0; attempt < 2; attempt += 1) {
49
+ try {
50
+ mkdirSync(path, { mode: 0o700 });
51
+ try {
52
+ writeFileSync(join(path, OWNER_FILE), JSON.stringify({ pid: process.pid, ownerId }), {
53
+ mode: 0o600,
54
+ });
55
+ } catch (error) {
56
+ rmSync(path, { recursive: true, force: true });
57
+ throw error;
58
+ }
59
+ return { path, ownerId };
60
+ } catch (error) {
61
+ if (error?.code !== 'EEXIST') throw error;
62
+ if (reclaimable(path, options.now ?? Date.now(), options.staleAfterMs ?? 60_000)) {
63
+ rmSync(path, { recursive: true, force: true });
64
+ continue;
65
+ }
66
+ return null;
67
+ }
68
+ }
69
+ return null;
70
+ }
71
+
72
+ export function releaseAppDevHostLock(lock) {
73
+ try {
74
+ if (readOwner(lock.path)?.ownerId === lock.ownerId) {
75
+ rmSync(lock.path, { recursive: true, force: true });
76
+ }
77
+ } catch {
78
+ // A replaced lock belongs to another process.
79
+ }
80
+ }