@notis_ai/cli 0.2.0-beta.139.1 → 0.2.0-beta.144.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
@@ -4,10 +4,11 @@ import { installLocalAgentContext } from './agents.js';
4
4
  async function loginHandler(ctx) {
5
5
  const result = await loginWithOAuth(ctx.runtime, ctx.options, ctx.output);
6
6
  if (result.agentAuthorization) {
7
+ const presentation = loginAgentAuthorizationPresentation(result.agentAuthorization);
7
8
  return ctx.output.emitSuccess({
8
9
  command: 'login',
9
10
  data: result.agentAuthorization,
10
- humanSummary: 'Open the authorization URL in a browser to continue.',
11
+ ...presentation,
11
12
  });
12
13
  }
13
14
  let agentSetup = [];
@@ -44,6 +45,27 @@ async function loginHandler(ctx) {
44
45
  });
45
46
  }
46
47
 
48
+ export function loginAgentAuthorizationPresentation(authorization) {
49
+ const browserHandOff = authorization?.hand_off === 'browser_callback';
50
+ const nextCommand = browserHandOff
51
+ ? authorization?.confirm_command
52
+ : authorization?.redeem_command;
53
+ return {
54
+ humanSummary: browserHandOff
55
+ ? 'Give the user the authorization URL. Signing in there completes it — there is no code to ask them for.'
56
+ : 'Give the user the authorization URL, then ask them for the code it shows.',
57
+ hints: nextCommand
58
+ ? [{
59
+ command: nextCommand,
60
+ reason: browserHandOff
61
+ ? 'Confirm the account once they have finished in the browser'
62
+ : 'Redeem the code shown in the browser',
63
+ }]
64
+ : [],
65
+ renderHuman: () => `Authorize Notis CLI: ${authorization.authorize_url}`,
66
+ };
67
+ }
68
+
47
69
  async function logoutHandler(ctx) {
48
70
  const result = await logoutOAuth(ctx.runtime, {
49
71
  allProfiles: Boolean(ctx.options.allProfiles),
@@ -54,12 +76,24 @@ async function logoutHandler(ctx) {
54
76
  oauth_connected: false,
55
77
  cleared_profiles: result.profiles,
56
78
  },
57
- humanSummary: ctx.options.allProfiles
58
- ? 'OAuth credentials were removed from all CLI profiles.'
59
- : `OAuth credentials were removed from profile "${ctx.runtime.profileName}".`,
79
+ humanSummary: logoutHumanSummary(result, {
80
+ allProfiles: Boolean(ctx.options.allProfiles),
81
+ profileName: ctx.runtime.profileName,
82
+ }),
60
83
  });
61
84
  }
62
85
 
86
+ export function logoutHumanSummary(result, { allProfiles, profileName }) {
87
+ if (allProfiles) {
88
+ return result.profiles.length > 0
89
+ ? 'OAuth state was cleared from all connected or pending CLI profiles.'
90
+ : 'No OAuth credentials were connected in any CLI profile.';
91
+ }
92
+ return result.profiles.includes(profileName)
93
+ ? `OAuth state was cleared for profile "${profileName}", including any pending authorization.`
94
+ : `No OAuth credential was connected for profile "${profileName}".`;
95
+ }
96
+
63
97
  export const authCommandSpecs = [
64
98
  {
65
99
  command_path: ['login'],
@@ -71,8 +105,9 @@ export const authCommandSpecs = [
71
105
  options: [
72
106
  { flags: '--no-browser', description: 'Print the authorization URL without opening a browser.' },
73
107
  { flags: '--print-url', description: 'Print the authorization URL even when opening a browser.' },
74
- { flags: '--paste-code', description: 'Use the copy-paste callback for SSH and headless machines.' },
75
- { flags: '--timeout-seconds <n>', description: 'How long to wait for authorization (default 300).' },
108
+ { flags: '--mode <mode>', description: 'auto (default) hands the browser callback to a background listener when this command cannot wait; browser waits in-process; code shows a one-time code to copy.' },
109
+ { flags: '--paste-code', description: 'Alias for --mode code.' },
110
+ { flags: '--timeout-seconds <n>', description: 'Authorization lifetime in seconds (default 300 while waiting in a terminal; 1800 for detached or code hand-offs).' },
76
111
  { flags: '--scope <scope>', description: 'OAuth permission to request (repeatable).', collect: true },
77
112
  { flags: '--code <code>', description: 'Redeem the code shown in the browser after a non-interactive login.' },
78
113
  ],
@@ -82,11 +117,12 @@ export const authCommandSpecs = [
82
117
  'notis login --profile work',
83
118
  'notis login --profile beta --api-base https://api-beta.notis.ai',
84
119
  'notis login --no-browser --print-url',
85
- 'notis login --paste-code',
120
+ 'notis login --mode browser',
121
+ 'notis login --mode code',
86
122
  'notis login --code 4f3c2b1a',
87
123
  ],
88
124
  output_schema:
89
- 'Returns credential_source, profile, api_base, user_id, scopes, and credential expiries; agent mode returns authorize_url and expires_in.',
125
+ 'Returns credential_source, profile, api_base, user_id, scopes, and credential expiries; non-blocking agent, machine-output, or non-interactive modes return authorize_url, expires_in, and hand_off ("browser_callback" needs no code, "code" must be redeemed with redeem_command).',
90
126
  mutates: true,
91
127
  idempotent: true,
92
128
  require_auth: false,
@@ -103,14 +139,15 @@ export const authCommandSpecs = [
103
139
  args_schema: {
104
140
  arguments: [],
105
141
  options: [
106
- { flags: '--all-profiles', description: 'Remove OAuth credentials from every CLI profile.' },
142
+ { flags: '--all-profiles', description: 'Clear OAuth credentials and pending authorizations from every CLI profile.' },
107
143
  ],
108
144
  },
109
145
  examples: ['notis logout', 'notis logout --profile work', 'notis logout --all-profiles'],
110
- output_schema: 'Returns oauth_connected=false and the profiles whose OAuth credentials were removed.',
146
+ output_schema: 'Returns oauth_connected=false and the profiles whose OAuth credentials or pending authorizations were cleared.',
111
147
  mutates: true,
112
148
  idempotent: true,
113
149
  require_auth: false,
150
+ allow_unknown_profile: true,
114
151
  related_commands: ['notis login', 'notis profile list'],
115
152
  backend_call: { type: 'oauth', name: 'revocation' },
116
153
  handler: logoutHandler,
@@ -11,6 +11,7 @@ import {
11
11
  } from '../runtime/profiles.js';
12
12
  import { ensureFreshOAuthCredential, loginWithOAuth } from '../runtime/oauth.js';
13
13
  import { runToolCommand } from './helpers.js';
14
+ import { loginAgentAuthorizationPresentation } from './auth.js';
14
15
  import { installLocalAgentContext } from './agents.js';
15
16
 
16
17
  const HERE = dirname(fileURLToPath(import.meta.url));
@@ -223,8 +224,13 @@ async function startHandler(ctx) {
223
224
  // Browser authorization is the whole signup path: it creates the account when
224
225
  // the address is new and authorizes this machine either way. There is nothing
225
226
  // for the CLI to collect up front, and nothing to wait on afterwards.
226
- const authorization = await loginWithOAuth(runtime, { browser: true }, output);
227
+ const authorization = await loginWithOAuth(
228
+ runtime,
229
+ { browser: true, mode: options.mode, reusePersistedCredential: true },
230
+ output,
231
+ );
227
232
  if (authorization?.agentAuthorization) {
233
+ const presentation = loginAgentAuthorizationPresentation(authorization.agentAuthorization);
228
234
  return output.emitSuccess({
229
235
  command: 'start',
230
236
  data: {
@@ -232,8 +238,7 @@ async function startHandler(ctx) {
232
238
  profile: runtime.profileName,
233
239
  ...authorization.agentAuthorization,
234
240
  },
235
- humanSummary: 'Open the authorization URL to create or access the Notis account.',
236
- renderHuman: () => `Authorize Notis CLI: ${authorization.agentAuthorization.authorize_url}`,
241
+ ...presentation,
237
242
  });
238
243
  }
239
244
 
@@ -262,16 +267,18 @@ export const onboardingCommandSpecs = [
262
267
  arguments: [],
263
268
  options: [
264
269
  { flags: '--brief-only', description: 'Print the onboarding brief for an already-signed-in profile.' },
270
+ { flags: '--mode <mode>', description: 'auto (default) hands the browser callback to a background listener when this command cannot wait; browser waits in-process; code shows a one-time code to copy.' },
265
271
  ],
266
272
  },
267
273
  examples: [
268
274
  'notis start',
269
275
  'notis start --profile work',
276
+ 'notis start --mode browser',
270
277
  'notis start --brief-only',
271
278
  'notis start --json',
272
279
  ],
273
280
  output_schema:
274
- 'Returns {authenticated, profile, api_base, onboarding_complete, known_settings, missing_settings, agent_setup, brief, brief_source} once signed in — brief is null when onboarding_complete is true — or {authorize_url, expires_in, redeem_command} while waiting for browser authorization.',
281
+ 'Returns {authenticated, profile, api_base, onboarding_complete, known_settings, missing_settings, agent_setup, brief, brief_source} once signed in — brief is null when onboarding_complete is true — or {authorize_url, expires_in, hand_off, ...} while waiting for browser authorization. hand_off is "browser_callback" when signing in finishes the login by itself (re-run confirm_command to see it land) or "code" when the user must be asked for the code the page shows and it redeemed with redeem_command.',
275
282
  mutates: true,
276
283
  idempotent: true,
277
284
  require_auth: false,
@@ -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
+ }
@@ -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
+ }