@notis_ai/cli 0.2.0-beta.102.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 (191) hide show
  1. package/README.md +334 -0
  2. package/bin/notis.js +2 -0
  3. package/config/notis_app_boundary_rules.json +50 -0
  4. package/dist/scaffolds/notis-database/CHANGELOG.md +5 -0
  5. package/dist/scaffolds/notis-database/app/globals.css +44 -0
  6. package/dist/scaffolds/notis-database/app/layout.tsx +6 -0
  7. package/dist/scaffolds/notis-database/app/page.tsx +1091 -0
  8. package/dist/scaffolds/notis-database/components/ui/badge.tsx +28 -0
  9. package/dist/scaffolds/notis-database/components/ui/button.tsx +53 -0
  10. package/dist/scaffolds/notis-database/components/ui/card.tsx +56 -0
  11. package/dist/scaffolds/notis-database/components/ui/table.tsx +120 -0
  12. package/dist/scaffolds/notis-database/components.json +20 -0
  13. package/dist/scaffolds/notis-database/index.html +12 -0
  14. package/dist/scaffolds/notis-database/lib/types.ts +134 -0
  15. package/dist/scaffolds/notis-database/lib/utils.ts +6 -0
  16. package/dist/scaffolds/notis-database/metadata/screenshot-1.png +0 -0
  17. package/dist/scaffolds/notis-database/notis.config.ts +26 -0
  18. package/dist/scaffolds/notis-database/package.json +31 -0
  19. package/dist/scaffolds/notis-database/postcss.config.mjs +8 -0
  20. package/dist/scaffolds/notis-database/src/dev-main.tsx +23 -0
  21. package/dist/scaffolds/notis-database/src/mock-runtime.ts +558 -0
  22. package/dist/scaffolds/notis-database/tailwind.config.ts +59 -0
  23. package/dist/scaffolds/notis-database/tsconfig.json +23 -0
  24. package/dist/scaffolds/notis-database/vite.config.ts +22 -0
  25. package/dist/scaffolds/notis-journal/CHANGELOG.md +25 -0
  26. package/dist/scaffolds/notis-journal/app/globals.css +37 -0
  27. package/dist/scaffolds/notis-journal/app/insights/page.tsx +513 -0
  28. package/dist/scaffolds/notis-journal/app/journal-core.tsx +362 -0
  29. package/dist/scaffolds/notis-journal/app/journal-ui.tsx +337 -0
  30. package/dist/scaffolds/notis-journal/app/layout.tsx +6 -0
  31. package/dist/scaffolds/notis-journal/app/page.tsx +485 -0
  32. package/dist/scaffolds/notis-journal/components/ui/badge.tsx +28 -0
  33. package/dist/scaffolds/notis-journal/components/ui/button.tsx +53 -0
  34. package/dist/scaffolds/notis-journal/components/ui/card.tsx +56 -0
  35. package/dist/scaffolds/notis-journal/components.json +20 -0
  36. package/dist/scaffolds/notis-journal/index.html +12 -0
  37. package/dist/scaffolds/notis-journal/lib/utils.ts +6 -0
  38. package/dist/scaffolds/notis-journal/metadata/screenshot-1.png +0 -0
  39. package/dist/scaffolds/notis-journal/metadata/screenshot-2.png +0 -0
  40. package/dist/scaffolds/notis-journal/metadata/screenshot-3.png +0 -0
  41. package/dist/scaffolds/notis-journal/metadata/screenshot-4.png +0 -0
  42. package/dist/scaffolds/notis-journal/metadata/screenshot-5.png +0 -0
  43. package/dist/scaffolds/notis-journal/metadata/screenshot-6.png +0 -0
  44. package/dist/scaffolds/notis-journal/metadata/screenshot-fixtures.json +132 -0
  45. package/dist/scaffolds/notis-journal/notis.config.ts +93 -0
  46. package/dist/scaffolds/notis-journal/package.json +34 -0
  47. package/dist/scaffolds/notis-journal/packages/sdk/package.json +36 -0
  48. package/dist/scaffolds/notis-journal/packages/sdk/src/components/DocumentEditor.tsx +93 -0
  49. package/dist/scaffolds/notis-journal/packages/sdk/src/components/Markdown.tsx +60 -0
  50. package/dist/scaffolds/notis-journal/packages/sdk/src/components/MultiSelectActionBar.tsx +278 -0
  51. package/dist/scaffolds/notis-journal/packages/sdk/src/components/MultiSelectCheckbox.tsx +91 -0
  52. package/dist/scaffolds/notis-journal/packages/sdk/src/components/MultiSelectDragOverlay.tsx +39 -0
  53. package/dist/scaffolds/notis-journal/packages/sdk/src/config.ts +145 -0
  54. package/dist/scaffolds/notis-journal/packages/sdk/src/documents.ts +229 -0
  55. package/dist/scaffolds/notis-journal/packages/sdk/src/hooks/useBackend.ts +41 -0
  56. package/dist/scaffolds/notis-journal/packages/sdk/src/hooks/useDatabaseSchema.ts +85 -0
  57. package/dist/scaffolds/notis-journal/packages/sdk/src/hooks/useDocument.ts +78 -0
  58. package/dist/scaffolds/notis-journal/packages/sdk/src/hooks/useDocuments.ts +121 -0
  59. package/dist/scaffolds/notis-journal/packages/sdk/src/hooks/useMultiSelect.ts +539 -0
  60. package/dist/scaffolds/notis-journal/packages/sdk/src/hooks/useNotis.ts +34 -0
  61. package/dist/scaffolds/notis-journal/packages/sdk/src/hooks/useNotisNavigation.ts +49 -0
  62. package/dist/scaffolds/notis-journal/packages/sdk/src/hooks/useTool.ts +64 -0
  63. package/dist/scaffolds/notis-journal/packages/sdk/src/hooks/useTools.ts +56 -0
  64. package/dist/scaffolds/notis-journal/packages/sdk/src/hooks/useTopBarSearch.ts +73 -0
  65. package/dist/scaffolds/notis-journal/packages/sdk/src/hooks/useUpsertDocument.ts +95 -0
  66. package/dist/scaffolds/notis-journal/packages/sdk/src/index.ts +83 -0
  67. package/dist/scaffolds/notis-journal/packages/sdk/src/provider.tsx +43 -0
  68. package/dist/scaffolds/notis-journal/packages/sdk/src/runtime.ts +220 -0
  69. package/dist/scaffolds/notis-journal/packages/sdk/src/styles.css +186 -0
  70. package/dist/scaffolds/notis-journal/packages/sdk/src/ui.ts +15 -0
  71. package/dist/scaffolds/notis-journal/packages/sdk/src/vite.ts +56 -0
  72. package/dist/scaffolds/notis-journal/packages/sdk/tsconfig.json +15 -0
  73. package/dist/scaffolds/notis-journal/postcss.config.mjs +8 -0
  74. package/dist/scaffolds/notis-journal/skills/journal-onboarding/SKILL.md +120 -0
  75. package/dist/scaffolds/notis-journal/src/dev-main.tsx +58 -0
  76. package/dist/scaffolds/notis-journal/src/mock-runtime.ts +197 -0
  77. package/dist/scaffolds/notis-journal/tailwind.config.ts +58 -0
  78. package/dist/scaffolds/notis-journal/tsconfig.json +23 -0
  79. package/dist/scaffolds/notis-journal/vite.config.ts +10 -0
  80. package/dist/scaffolds/notis-notes/CHANGELOG.md +5 -0
  81. package/dist/scaffolds/notis-notes/app/globals.css +3 -0
  82. package/dist/scaffolds/notis-notes/app/layout.tsx +6 -0
  83. package/dist/scaffolds/notis-notes/app/page.tsx +1950 -0
  84. package/dist/scaffolds/notis-notes/components/ui/badge.tsx +28 -0
  85. package/dist/scaffolds/notis-notes/components/ui/button.tsx +53 -0
  86. package/dist/scaffolds/notis-notes/components/ui/card.tsx +56 -0
  87. package/dist/scaffolds/notis-notes/components.json +20 -0
  88. package/dist/scaffolds/notis-notes/lib/utils.ts +6 -0
  89. package/dist/scaffolds/notis-notes/metadata/screenshot-1.png +0 -0
  90. package/dist/scaffolds/notis-notes/notis.config.ts +35 -0
  91. package/dist/scaffolds/notis-notes/package.json +31 -0
  92. package/dist/scaffolds/notis-notes/postcss.config.mjs +8 -0
  93. package/dist/scaffolds/notis-notes/tailwind.config.ts +58 -0
  94. package/dist/scaffolds/notis-notes/tsconfig.json +23 -0
  95. package/dist/scaffolds/notis-notes/vite.config.ts +10 -0
  96. package/dist/scaffolds/notis-random/CHANGELOG.md +5 -0
  97. package/dist/scaffolds/notis-random/README.md +33 -0
  98. package/dist/scaffolds/notis-random/app/globals.css +11 -0
  99. package/dist/scaffolds/notis-random/app/history/page.tsx +66 -0
  100. package/dist/scaffolds/notis-random/app/layout.tsx +7 -0
  101. package/dist/scaffolds/notis-random/app/page.tsx +222 -0
  102. package/dist/scaffolds/notis-random/index.html +12 -0
  103. package/dist/scaffolds/notis-random/lib/notis-tools.ts +109 -0
  104. package/dist/scaffolds/notis-random/lib/rng.ts +42 -0
  105. package/dist/scaffolds/notis-random/lib/roll-record.ts +102 -0
  106. package/dist/scaffolds/notis-random/lib/utils.ts +25 -0
  107. package/dist/scaffolds/notis-random/metadata/screenshot-1.png +0 -0
  108. package/dist/scaffolds/notis-random/metadata/screenshot-2.png +0 -0
  109. package/dist/scaffolds/notis-random/notis.config.ts +42 -0
  110. package/dist/scaffolds/notis-random/package.json +32 -0
  111. package/dist/scaffolds/notis-random/postcss.config.mjs +6 -0
  112. package/dist/scaffolds/notis-random/src/dev-main.tsx +70 -0
  113. package/dist/scaffolds/notis-random/src/mock-runtime.ts +129 -0
  114. package/dist/scaffolds/notis-random/tailwind.config.ts +43 -0
  115. package/dist/scaffolds/notis-random/tsconfig.json +23 -0
  116. package/dist/scaffolds/notis-random/vite.config.ts +11 -0
  117. package/dist/scaffolds.json +47 -0
  118. package/package.json +45 -0
  119. package/skills/notis-apps/SKILL.md +219 -0
  120. package/skills/notis-apps/cli.md +244 -0
  121. package/skills/notis-cli/SKILL.md +269 -0
  122. package/skills/notis-query/cli.md +39 -0
  123. package/src/cli.js +175 -0
  124. package/src/command-specs/apps.js +1939 -0
  125. package/src/command-specs/helpers.js +186 -0
  126. package/src/command-specs/index.js +14 -0
  127. package/src/command-specs/meta.js +161 -0
  128. package/src/command-specs/tools.js +499 -0
  129. package/src/runtime/agent-browser.js +464 -0
  130. package/src/runtime/app-boundary-validator.js +183 -0
  131. package/src/runtime/app-changelog.js +79 -0
  132. package/src/runtime/app-dev-server.js +712 -0
  133. package/src/runtime/app-dev-sessions.js +251 -0
  134. package/src/runtime/app-platform.js +1500 -0
  135. package/src/runtime/assets/store-screenshot-dark.png +0 -0
  136. package/src/runtime/cli-mode.generated.js +4 -0
  137. package/src/runtime/cli-mode.js +29 -0
  138. package/src/runtime/desktop-auth.js +93 -0
  139. package/src/runtime/errors.js +55 -0
  140. package/src/runtime/help.js +60 -0
  141. package/src/runtime/output.js +180 -0
  142. package/src/runtime/ports.js +15 -0
  143. package/src/runtime/profiles.js +261 -0
  144. package/src/runtime/store-screenshot.js +138 -0
  145. package/src/runtime/transport.js +347 -0
  146. package/template/.harness/index.html.tmpl +333 -0
  147. package/template/CHANGELOG.md +5 -0
  148. package/template/app/globals.css +3 -0
  149. package/template/app/layout.tsx +6 -0
  150. package/template/app/page.tsx +60 -0
  151. package/template/components/ui/badge.tsx +28 -0
  152. package/template/components/ui/button.tsx +53 -0
  153. package/template/components/ui/card.tsx +56 -0
  154. package/template/components.json +20 -0
  155. package/template/lib/utils.ts +6 -0
  156. package/template/metadata/screenshot-1.png +0 -0
  157. package/template/metadata/screenshot-2.png +0 -0
  158. package/template/metadata/screenshot-3.png +0 -0
  159. package/template/notis.config.ts +36 -0
  160. package/template/package-lock.json +4137 -0
  161. package/template/package.json +32 -0
  162. package/template/packages/sdk/package.json +36 -0
  163. package/template/packages/sdk/src/components/DocumentEditor.tsx +93 -0
  164. package/template/packages/sdk/src/components/Markdown.tsx +60 -0
  165. package/template/packages/sdk/src/components/MultiSelectActionBar.tsx +278 -0
  166. package/template/packages/sdk/src/components/MultiSelectCheckbox.tsx +91 -0
  167. package/template/packages/sdk/src/components/MultiSelectDragOverlay.tsx +39 -0
  168. package/template/packages/sdk/src/config.ts +145 -0
  169. package/template/packages/sdk/src/documents.ts +229 -0
  170. package/template/packages/sdk/src/hooks/useBackend.ts +41 -0
  171. package/template/packages/sdk/src/hooks/useDatabaseSchema.ts +85 -0
  172. package/template/packages/sdk/src/hooks/useDocument.ts +78 -0
  173. package/template/packages/sdk/src/hooks/useDocuments.ts +121 -0
  174. package/template/packages/sdk/src/hooks/useMultiSelect.ts +539 -0
  175. package/template/packages/sdk/src/hooks/useNotis.ts +34 -0
  176. package/template/packages/sdk/src/hooks/useNotisNavigation.ts +49 -0
  177. package/template/packages/sdk/src/hooks/useTool.ts +64 -0
  178. package/template/packages/sdk/src/hooks/useTools.ts +56 -0
  179. package/template/packages/sdk/src/hooks/useTopBarSearch.ts +73 -0
  180. package/template/packages/sdk/src/hooks/useUpsertDocument.ts +95 -0
  181. package/template/packages/sdk/src/index.ts +83 -0
  182. package/template/packages/sdk/src/provider.tsx +43 -0
  183. package/template/packages/sdk/src/runtime.ts +220 -0
  184. package/template/packages/sdk/src/styles.css +186 -0
  185. package/template/packages/sdk/src/ui.ts +15 -0
  186. package/template/packages/sdk/src/vite.ts +56 -0
  187. package/template/packages/sdk/tsconfig.json +15 -0
  188. package/template/postcss.config.mjs +8 -0
  189. package/template/tailwind.config.ts +58 -0
  190. package/template/tsconfig.json +22 -0
  191. package/template/vite.config.ts +10 -0
@@ -0,0 +1,251 @@
1
+ import { randomUUID } from 'node:crypto';
2
+ import { existsSync, mkdirSync, readFileSync, renameSync, writeFileSync } from 'node:fs';
3
+ import { homedir } from 'node:os';
4
+ import { dirname, join, parse as parsePath } from 'node:path';
5
+ import { CONFIG_DIR } from './profiles.js';
6
+
7
+ export const DEFAULT_APP_DEV_SESSIONS_FILE = join(CONFIG_DIR, 'app-dev-sessions.json');
8
+ export const APP_DEV_SESSIONS_FILE = DEFAULT_APP_DEV_SESSIONS_FILE;
9
+ export const APP_DEV_SESSIONS_VERSION = 1;
10
+ export const APP_DEV_SESSION_MOUNT_ACKS_VERSION = 1;
11
+
12
+ /**
13
+ * Walk up from `startDir` looking for a workspace-scoped dev-sessions registry.
14
+ *
15
+ * The Notis desktop (and Conductor) reads a PER-WORKSPACE registry at
16
+ * `<workspace>/.context/app-dev-sessions.json` (it points its own `apps dev`
17
+ * at it via `NOTIS_APP_DEV_SESSIONS_FILE`). When a coding agent runs
18
+ * `apps dev` manually it does not inherit that env var, so its session would
19
+ * otherwise land in the global `~/.notis` registry and never appear in the
20
+ * desktop's Local development sidebar. Resolving the nearest `.context`
21
+ * registry keeps agent-run and desktop-run sessions in the same file.
22
+ *
23
+ * Returns the registry path if a `.context/` directory is found in `startDir`
24
+ * or an ancestor (stopping at the home directory / filesystem root), else null.
25
+ */
26
+ export function findWorkspaceAppDevSessionsFile(startDir = process.cwd()) {
27
+ let dir = startDir;
28
+ const home = homedir();
29
+ const { root } = parsePath(dir);
30
+ // Bounded walk; `.context` lives at the workspace root, never above $HOME.
31
+ for (let depth = 0; depth < 64; depth += 1) {
32
+ if (existsSync(join(dir, '.context'))) {
33
+ return join(dir, '.context', 'app-dev-sessions.json');
34
+ }
35
+ if (dir === home || dir === root) break;
36
+ const parent = dirname(dir);
37
+ if (parent === dir) break;
38
+ dir = parent;
39
+ }
40
+ return null;
41
+ }
42
+
43
+ export function getAppDevSessionsFile(filePath) {
44
+ if (filePath) {
45
+ return filePath;
46
+ }
47
+ const envPath = process.env.NOTIS_APP_DEV_SESSIONS_FILE;
48
+ if (typeof envPath === 'string' && envPath.trim()) {
49
+ return envPath.trim();
50
+ }
51
+ const workspaceFile = findWorkspaceAppDevSessionsFile();
52
+ if (workspaceFile) {
53
+ return workspaceFile;
54
+ }
55
+ return DEFAULT_APP_DEV_SESSIONS_FILE;
56
+ }
57
+
58
+ export function getAppDevSessionMountAcksFile(sessionsFilePath) {
59
+ const resolvedSessionsFile = getAppDevSessionsFile(sessionsFilePath);
60
+ const parsed = parsePath(resolvedSessionsFile);
61
+ return join(parsed.dir, `${parsed.name}-mount-acks${parsed.ext || '.json'}`);
62
+ }
63
+
64
+ function normalizeSessions(raw) {
65
+ if (!raw || typeof raw !== 'object' || !Array.isArray(raw.sessions)) {
66
+ return [];
67
+ }
68
+ return raw.sessions
69
+ .filter((session) =>
70
+ session &&
71
+ typeof session === 'object' &&
72
+ typeof session.sessionId === 'string' &&
73
+ typeof session.appId === 'string' &&
74
+ typeof session.bundleBaseUrl === 'string',
75
+ )
76
+ .map((session) => {
77
+ if (typeof session.targetAppId === 'string' && session.targetAppId.trim()) {
78
+ return { ...session, targetAppId: session.targetAppId.trim() };
79
+ }
80
+ const { targetAppId: _targetAppId, ...rest } = session;
81
+ return rest;
82
+ });
83
+ }
84
+
85
+ export function readAppDevSessions(filePath) {
86
+ const resolvedFilePath = getAppDevSessionsFile(filePath);
87
+ if (!existsSync(resolvedFilePath)) {
88
+ return { version: APP_DEV_SESSIONS_VERSION, sessions: [] };
89
+ }
90
+ try {
91
+ const raw = JSON.parse(readFileSync(resolvedFilePath, 'utf8'));
92
+ return {
93
+ version: APP_DEV_SESSIONS_VERSION,
94
+ sessions: normalizeSessions(raw),
95
+ };
96
+ } catch {
97
+ return { version: APP_DEV_SESSIONS_VERSION, sessions: [] };
98
+ }
99
+ }
100
+
101
+ export function writeAppDevSessions(registry, filePath) {
102
+ const resolvedFilePath = getAppDevSessionsFile(filePath);
103
+ mkdirSync(dirname(resolvedFilePath), { recursive: true, mode: 0o700 });
104
+ const normalized = {
105
+ version: APP_DEV_SESSIONS_VERSION,
106
+ sessions: normalizeSessions(registry),
107
+ };
108
+ const tempPath = `${resolvedFilePath}.${process.pid}.${randomUUID()}.tmp`;
109
+ writeFileSync(tempPath, JSON.stringify(normalized, null, 2), { mode: 0o600 });
110
+ renameSync(tempPath, resolvedFilePath);
111
+ return normalized;
112
+ }
113
+
114
+ export function upsertAppDevSessions(sessions, filePath) {
115
+ const nextSessions = Array.isArray(sessions) ? sessions : [sessions];
116
+ const registry = readAppDevSessions(filePath);
117
+ const keys = new Set(nextSessions.map((session) => `${session.sessionId}:${session.appId}`));
118
+ registry.sessions = [
119
+ ...registry.sessions.filter((session) => !keys.has(`${session.sessionId}:${session.appId}`)),
120
+ ...nextSessions,
121
+ ];
122
+ return writeAppDevSessions(registry, filePath);
123
+ }
124
+
125
+ export function heartbeatAppDevSession(sessionId, lastHeartbeatAt, filePath) {
126
+ const registry = readAppDevSessions(filePath);
127
+ registry.sessions = registry.sessions.map((session) =>
128
+ session.sessionId === sessionId
129
+ ? { ...session, lastHeartbeatAt }
130
+ : session,
131
+ );
132
+ return writeAppDevSessions(registry, filePath);
133
+ }
134
+
135
+ export function linkAppDevSessionTarget({ appId, devSlug, targetAppId, lastHeartbeatAt = new Date().toISOString() }, filePath) {
136
+ const target = typeof targetAppId === 'string' ? targetAppId.trim() : '';
137
+ if (!target) {
138
+ return readAppDevSessions(filePath);
139
+ }
140
+ const registry = readAppDevSessions(filePath);
141
+ registry.sessions = registry.sessions.map((session) => {
142
+ const matchesApp = appId && session.appId === appId;
143
+ const matchesSlug = devSlug && session.devSlug === devSlug;
144
+ if (!matchesApp || !matchesSlug) {
145
+ return session;
146
+ }
147
+ return {
148
+ ...session,
149
+ targetAppId: target,
150
+ lastHeartbeatAt,
151
+ };
152
+ });
153
+ return writeAppDevSessions(registry, filePath);
154
+ }
155
+
156
+ export function removeAppDevSession(sessionId, filePath) {
157
+ const registry = readAppDevSessions(filePath);
158
+ registry.sessions = registry.sessions.filter((session) => session.sessionId !== sessionId);
159
+ const result = writeAppDevSessions(registry, filePath);
160
+ removeAppDevSessionMountAcknowledgements(sessionId, filePath);
161
+ return result;
162
+ }
163
+
164
+ function normalizeMountAcknowledgements(raw) {
165
+ if (!raw || typeof raw !== 'object' || !Array.isArray(raw.acknowledgements)) {
166
+ return [];
167
+ }
168
+ return raw.acknowledgements.filter((acknowledgement) =>
169
+ acknowledgement &&
170
+ typeof acknowledgement === 'object' &&
171
+ typeof acknowledgement.sessionId === 'string' &&
172
+ typeof acknowledgement.appId === 'string' &&
173
+ typeof acknowledgement.devSlug === 'string' &&
174
+ typeof acknowledgement.mountNonce === 'string' &&
175
+ typeof acknowledgement.acknowledgedAt === 'string' &&
176
+ acknowledgement.stage === 'listed',
177
+ );
178
+ }
179
+
180
+ export function readAppDevSessionMountAcknowledgements(sessionsFilePath) {
181
+ const filePath = getAppDevSessionMountAcksFile(sessionsFilePath);
182
+ if (!existsSync(filePath)) {
183
+ return { version: APP_DEV_SESSION_MOUNT_ACKS_VERSION, acknowledgements: [] };
184
+ }
185
+ try {
186
+ const raw = JSON.parse(readFileSync(filePath, 'utf8'));
187
+ return {
188
+ version: APP_DEV_SESSION_MOUNT_ACKS_VERSION,
189
+ acknowledgements: normalizeMountAcknowledgements(raw),
190
+ };
191
+ } catch {
192
+ return { version: APP_DEV_SESSION_MOUNT_ACKS_VERSION, acknowledgements: [] };
193
+ }
194
+ }
195
+
196
+ function writeAppDevSessionMountAcknowledgements(registry, sessionsFilePath) {
197
+ const filePath = getAppDevSessionMountAcksFile(sessionsFilePath);
198
+ mkdirSync(dirname(filePath), { recursive: true, mode: 0o700 });
199
+ const normalized = {
200
+ version: APP_DEV_SESSION_MOUNT_ACKS_VERSION,
201
+ acknowledgements: normalizeMountAcknowledgements(registry),
202
+ };
203
+ const tempPath = `${filePath}.${process.pid}.${randomUUID()}.tmp`;
204
+ writeFileSync(tempPath, JSON.stringify(normalized, null, 2), { mode: 0o600 });
205
+ renameSync(tempPath, filePath);
206
+ return normalized;
207
+ }
208
+
209
+ export function removeAppDevSessionMountAcknowledgements(sessionId, sessionsFilePath) {
210
+ const filePath = getAppDevSessionMountAcksFile(sessionsFilePath);
211
+ if (!existsSync(filePath)) {
212
+ return { version: APP_DEV_SESSION_MOUNT_ACKS_VERSION, acknowledgements: [] };
213
+ }
214
+ const registry = readAppDevSessionMountAcknowledgements(sessionsFilePath);
215
+ registry.acknowledgements = registry.acknowledgements.filter(
216
+ (acknowledgement) => acknowledgement.sessionId !== sessionId,
217
+ );
218
+ return writeAppDevSessionMountAcknowledgements(registry, sessionsFilePath);
219
+ }
220
+
221
+ function mountAcknowledgementKey(value) {
222
+ return `${value.sessionId}:${value.appId}:${value.devSlug}:${value.mountNonce}`;
223
+ }
224
+
225
+ export async function waitForAppDevSessionMountAcknowledgements(expectedSessions, options = {}) {
226
+ const expected = Array.isArray(expectedSessions) ? expectedSessions : [expectedSessions];
227
+ const timeoutMs = Number.isFinite(options.timeoutMs) ? Math.max(0, options.timeoutMs) : 15_000;
228
+ const pollIntervalMs = Number.isFinite(options.pollIntervalMs)
229
+ ? Math.max(1, options.pollIntervalMs)
230
+ : 100;
231
+ const now = typeof options.now === 'function' ? options.now : Date.now;
232
+ const sleep = typeof options.sleep === 'function'
233
+ ? options.sleep
234
+ : (delayMs) => new Promise((resolve) => setTimeout(resolve, delayMs));
235
+ const startedAt = now();
236
+
237
+ while (true) {
238
+ const registry = readAppDevSessionMountAcknowledgements(options.sessionsFilePath);
239
+ const acknowledgedKeys = new Set(registry.acknowledgements.map(mountAcknowledgementKey));
240
+ const missing = expected.filter((session) => !acknowledgedKeys.has(mountAcknowledgementKey(session)));
241
+ if (missing.length === 0) {
242
+ return { mounted: true, missing: [], acknowledgements: registry.acknowledgements };
243
+ }
244
+
245
+ const elapsedMs = now() - startedAt;
246
+ if (elapsedMs >= timeoutMs) {
247
+ return { mounted: false, missing, acknowledgements: registry.acknowledgements };
248
+ }
249
+ await sleep(Math.min(pollIntervalMs, timeoutMs - elapsedMs));
250
+ }
251
+ }