@notis_ai/cli 0.2.0-beta.139.1 → 0.2.0-beta.145.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +44 -20
- package/dist/agent-hooks/notis-agent-hook.mjs +4579 -1449
- package/dist/base-skills/notis-apps/SKILL.md +83 -55
- package/dist/base-skills/notis-cli/SKILL.md +8 -8
- package/package.json +2 -2
- package/skills/notis-apps/cli.md +38 -17
- package/src/command-specs/apps.js +871 -387
- package/src/command-specs/auth.js +47 -10
- package/src/command-specs/onboarding.js +11 -4
- package/src/runtime/app-dev-consumers.js +154 -0
- package/src/runtime/app-dev-host-lock.js +80 -0
- package/src/runtime/app-dev-roots.js +284 -0
- package/src/runtime/app-dev-server.js +212 -22
- package/src/runtime/app-dev-sessions.js +105 -145
- package/src/runtime/app-platform.js +1291 -165
- package/src/runtime/app-registry-scaffolds.js +367 -0
- package/src/runtime/login-listener.js +15 -0
- package/src/runtime/oauth.js +1506 -141
- package/src/runtime/output.js +7 -0
- package/src/runtime/ports.js +16 -0
- package/src/runtime/profiles.js +0 -5
- package/template/app/layout.tsx +5 -1
- package/template/notis.config.ts +1 -0
- package/template/packages/sdk/package.json +2 -1
- package/template/packages/sdk/src/components/MarkdownEditor.tsx +121 -0
- package/template/packages/sdk/src/components/MultiSelectActionBar.tsx +37 -119
- package/template/packages/sdk/src/components/MultiSelectCheckbox.tsx +5 -1
- package/template/packages/sdk/src/components/MultiSelectDragOverlay.tsx +1 -1
- package/template/packages/sdk/src/components/NotisSelectionBoundary.tsx +55 -0
- package/template/packages/sdk/src/components/ShortcutHints.tsx +56 -0
- package/template/packages/sdk/src/config.ts +19 -0
- package/template/packages/sdk/src/hooks/useActiveResource.ts +19 -0
- package/template/packages/sdk/src/hooks/useCollectionInteractions.ts +709 -0
- package/template/packages/sdk/src/hooks/useHandover.ts +3 -0
- package/template/packages/sdk/src/hooks/useMultiSelect.ts +44 -482
- package/template/packages/sdk/src/hooks/useTool.ts +5 -4
- package/template/packages/sdk/src/index.ts +47 -2
- package/template/packages/sdk/src/interactions/actions.ts +46 -0
- package/template/packages/sdk/src/interactions/shortcuts.tsx +628 -0
- package/template/packages/sdk/src/interactions.ts +41 -0
- package/template/packages/sdk/src/provider.tsx +2 -1
- package/template/packages/sdk/src/runtime.ts +85 -3
- package/dist/scaffolds/notis-database/CHANGELOG.md +0 -5
- package/dist/scaffolds/notis-database/app/globals.css +0 -44
- package/dist/scaffolds/notis-database/app/layout.tsx +0 -6
- package/dist/scaffolds/notis-database/app/page.tsx +0 -1088
- package/dist/scaffolds/notis-database/components/ui/badge.tsx +0 -28
- package/dist/scaffolds/notis-database/components/ui/button.tsx +0 -53
- package/dist/scaffolds/notis-database/components/ui/card.tsx +0 -56
- package/dist/scaffolds/notis-database/components/ui/table.tsx +0 -120
- package/dist/scaffolds/notis-database/components.json +0 -20
- package/dist/scaffolds/notis-database/index.html +0 -12
- package/dist/scaffolds/notis-database/lib/types.ts +0 -132
- package/dist/scaffolds/notis-database/lib/utils.ts +0 -6
- package/dist/scaffolds/notis-database/metadata/screenshot-1.png +0 -0
- package/dist/scaffolds/notis-database/metadata/screenshot-2.png +0 -0
- package/dist/scaffolds/notis-database/metadata/screenshot-3.png +0 -0
- package/dist/scaffolds/notis-database/metadata/screenshot-4.png +0 -0
- package/dist/scaffolds/notis-database/metadata/screenshot-5.png +0 -0
- package/dist/scaffolds/notis-database/metadata/screenshot-fixtures.json +0 -1839
- package/dist/scaffolds/notis-database/notis.config.ts +0 -73
- package/dist/scaffolds/notis-database/package-lock.json +0 -3935
- package/dist/scaffolds/notis-database/package.json +0 -32
- package/dist/scaffolds/notis-database/packages/sdk/package.json +0 -36
- package/dist/scaffolds/notis-database/packages/sdk/src/components/DocumentEditor.tsx +0 -93
- package/dist/scaffolds/notis-database/packages/sdk/src/components/Markdown.tsx +0 -60
- package/dist/scaffolds/notis-database/packages/sdk/src/components/MultiSelectActionBar.tsx +0 -278
- package/dist/scaffolds/notis-database/packages/sdk/src/components/MultiSelectCheckbox.tsx +0 -91
- package/dist/scaffolds/notis-database/packages/sdk/src/components/MultiSelectDragOverlay.tsx +0 -39
- package/dist/scaffolds/notis-database/packages/sdk/src/config.ts +0 -234
- package/dist/scaffolds/notis-database/packages/sdk/src/documents.ts +0 -250
- package/dist/scaffolds/notis-database/packages/sdk/src/hooks/useBackend.ts +0 -41
- package/dist/scaffolds/notis-database/packages/sdk/src/hooks/useCloudComputer.ts +0 -97
- package/dist/scaffolds/notis-database/packages/sdk/src/hooks/useDatabaseSchema.ts +0 -85
- package/dist/scaffolds/notis-database/packages/sdk/src/hooks/useDatabaseSubscription.ts +0 -76
- package/dist/scaffolds/notis-database/packages/sdk/src/hooks/useDocument.ts +0 -78
- package/dist/scaffolds/notis-database/packages/sdk/src/hooks/useDocuments.ts +0 -121
- package/dist/scaffolds/notis-database/packages/sdk/src/hooks/useHandover.ts +0 -75
- package/dist/scaffolds/notis-database/packages/sdk/src/hooks/useMultiSelect.ts +0 -539
- package/dist/scaffolds/notis-database/packages/sdk/src/hooks/useNotis.ts +0 -34
- package/dist/scaffolds/notis-database/packages/sdk/src/hooks/useNotisNavigation.ts +0 -49
- package/dist/scaffolds/notis-database/packages/sdk/src/hooks/useTool.ts +0 -64
- package/dist/scaffolds/notis-database/packages/sdk/src/hooks/useTools.ts +0 -56
- package/dist/scaffolds/notis-database/packages/sdk/src/hooks/useTopBarSearch.ts +0 -73
- package/dist/scaffolds/notis-database/packages/sdk/src/hooks/useUpsertDocument.ts +0 -95
- package/dist/scaffolds/notis-database/packages/sdk/src/index.ts +0 -100
- package/dist/scaffolds/notis-database/packages/sdk/src/provider.tsx +0 -43
- package/dist/scaffolds/notis-database/packages/sdk/src/runtime.ts +0 -351
- package/dist/scaffolds/notis-database/packages/sdk/src/styles.css +0 -186
- package/dist/scaffolds/notis-database/packages/sdk/src/ui.ts +0 -15
- package/dist/scaffolds/notis-database/packages/sdk/src/vite.ts +0 -56
- package/dist/scaffolds/notis-database/packages/sdk/tsconfig.json +0 -15
- package/dist/scaffolds/notis-database/postcss.config.mjs +0 -8
- package/dist/scaffolds/notis-database/src/dev-main.tsx +0 -23
- package/dist/scaffolds/notis-database/src/mock-runtime.ts +0 -561
- package/dist/scaffolds/notis-database/tailwind.config.ts +0 -59
- package/dist/scaffolds/notis-database/tsconfig.json +0 -23
- package/dist/scaffolds/notis-database/vite.config.ts +0 -22
- package/dist/scaffolds/notis-journal/CHANGELOG.md +0 -29
- package/dist/scaffolds/notis-journal/app/globals.css +0 -37
- package/dist/scaffolds/notis-journal/app/insights/page.tsx +0 -513
- package/dist/scaffolds/notis-journal/app/journal-core.tsx +0 -362
- package/dist/scaffolds/notis-journal/app/journal-ui.tsx +0 -337
- package/dist/scaffolds/notis-journal/app/layout.tsx +0 -6
- package/dist/scaffolds/notis-journal/app/page.tsx +0 -486
- package/dist/scaffolds/notis-journal/components/ui/badge.tsx +0 -28
- package/dist/scaffolds/notis-journal/components/ui/button.tsx +0 -53
- package/dist/scaffolds/notis-journal/components/ui/card.tsx +0 -56
- package/dist/scaffolds/notis-journal/components.json +0 -20
- package/dist/scaffolds/notis-journal/index.html +0 -12
- package/dist/scaffolds/notis-journal/lib/utils.ts +0 -6
- package/dist/scaffolds/notis-journal/metadata/screenshot-1.png +0 -0
- package/dist/scaffolds/notis-journal/metadata/screenshot-2.png +0 -0
- package/dist/scaffolds/notis-journal/metadata/screenshot-3.png +0 -0
- package/dist/scaffolds/notis-journal/metadata/screenshot-4.png +0 -0
- package/dist/scaffolds/notis-journal/metadata/screenshot-5.png +0 -0
- package/dist/scaffolds/notis-journal/metadata/screenshot-6.png +0 -0
- package/dist/scaffolds/notis-journal/metadata/screenshot-fixtures.json +0 -132
- package/dist/scaffolds/notis-journal/notis.config.ts +0 -93
- package/dist/scaffolds/notis-journal/package-lock.json +0 -4615
- package/dist/scaffolds/notis-journal/package.json +0 -34
- package/dist/scaffolds/notis-journal/packages/sdk/package.json +0 -36
- package/dist/scaffolds/notis-journal/packages/sdk/src/components/DocumentEditor.tsx +0 -93
- package/dist/scaffolds/notis-journal/packages/sdk/src/components/Markdown.tsx +0 -60
- package/dist/scaffolds/notis-journal/packages/sdk/src/components/MultiSelectActionBar.tsx +0 -278
- package/dist/scaffolds/notis-journal/packages/sdk/src/components/MultiSelectCheckbox.tsx +0 -91
- package/dist/scaffolds/notis-journal/packages/sdk/src/components/MultiSelectDragOverlay.tsx +0 -39
- package/dist/scaffolds/notis-journal/packages/sdk/src/config.ts +0 -234
- package/dist/scaffolds/notis-journal/packages/sdk/src/documents.ts +0 -250
- package/dist/scaffolds/notis-journal/packages/sdk/src/hooks/useBackend.ts +0 -41
- package/dist/scaffolds/notis-journal/packages/sdk/src/hooks/useCloudComputer.ts +0 -97
- package/dist/scaffolds/notis-journal/packages/sdk/src/hooks/useDatabaseSchema.ts +0 -85
- package/dist/scaffolds/notis-journal/packages/sdk/src/hooks/useDatabaseSubscription.ts +0 -76
- package/dist/scaffolds/notis-journal/packages/sdk/src/hooks/useDocument.ts +0 -78
- package/dist/scaffolds/notis-journal/packages/sdk/src/hooks/useDocuments.ts +0 -121
- package/dist/scaffolds/notis-journal/packages/sdk/src/hooks/useHandover.ts +0 -75
- package/dist/scaffolds/notis-journal/packages/sdk/src/hooks/useMultiSelect.ts +0 -539
- package/dist/scaffolds/notis-journal/packages/sdk/src/hooks/useNotis.ts +0 -34
- package/dist/scaffolds/notis-journal/packages/sdk/src/hooks/useNotisNavigation.ts +0 -49
- package/dist/scaffolds/notis-journal/packages/sdk/src/hooks/useTool.ts +0 -64
- package/dist/scaffolds/notis-journal/packages/sdk/src/hooks/useTools.ts +0 -56
- package/dist/scaffolds/notis-journal/packages/sdk/src/hooks/useTopBarSearch.ts +0 -73
- package/dist/scaffolds/notis-journal/packages/sdk/src/hooks/useUpsertDocument.ts +0 -95
- package/dist/scaffolds/notis-journal/packages/sdk/src/index.ts +0 -100
- package/dist/scaffolds/notis-journal/packages/sdk/src/provider.tsx +0 -43
- package/dist/scaffolds/notis-journal/packages/sdk/src/runtime.ts +0 -351
- package/dist/scaffolds/notis-journal/packages/sdk/src/styles.css +0 -186
- package/dist/scaffolds/notis-journal/packages/sdk/src/ui.ts +0 -15
- package/dist/scaffolds/notis-journal/packages/sdk/src/vite.ts +0 -56
- package/dist/scaffolds/notis-journal/packages/sdk/tsconfig.json +0 -15
- package/dist/scaffolds/notis-journal/postcss.config.mjs +0 -8
- package/dist/scaffolds/notis-journal/skills/journal-onboarding/SKILL.md +0 -120
- package/dist/scaffolds/notis-journal/src/dev-main.tsx +0 -58
- package/dist/scaffolds/notis-journal/src/mock-runtime.ts +0 -199
- package/dist/scaffolds/notis-journal/tailwind.config.ts +0 -58
- package/dist/scaffolds/notis-journal/tsconfig.json +0 -23
- package/dist/scaffolds/notis-journal/vite.config.ts +0 -10
- package/dist/scaffolds/notis-notes/CHANGELOG.md +0 -5
- package/dist/scaffolds/notis-notes/app/globals.css +0 -3
- package/dist/scaffolds/notis-notes/app/layout.tsx +0 -6
- package/dist/scaffolds/notis-notes/app/page.tsx +0 -1943
- package/dist/scaffolds/notis-notes/app/phosphor-icons.ts +0 -596
- package/dist/scaffolds/notis-notes/components/ui/badge.tsx +0 -28
- package/dist/scaffolds/notis-notes/components/ui/button.tsx +0 -53
- package/dist/scaffolds/notis-notes/components/ui/card.tsx +0 -56
- package/dist/scaffolds/notis-notes/components.json +0 -20
- package/dist/scaffolds/notis-notes/lib/utils.ts +0 -6
- package/dist/scaffolds/notis-notes/lib/visible-properties.ts +0 -144
- package/dist/scaffolds/notis-notes/metadata/screenshot-1.png +0 -0
- package/dist/scaffolds/notis-notes/metadata/screenshot-2.png +0 -0
- package/dist/scaffolds/notis-notes/metadata/screenshot-3.png +0 -0
- package/dist/scaffolds/notis-notes/metadata/screenshot-4.png +0 -0
- package/dist/scaffolds/notis-notes/metadata/screenshot-5.png +0 -0
- package/dist/scaffolds/notis-notes/metadata/screenshot-fixtures.json +0 -752
- package/dist/scaffolds/notis-notes/notis.config.ts +0 -80
- package/dist/scaffolds/notis-notes/package-lock.json +0 -4636
- package/dist/scaffolds/notis-notes/package.json +0 -35
- package/dist/scaffolds/notis-notes/packages/sdk/package.json +0 -36
- package/dist/scaffolds/notis-notes/packages/sdk/src/components/DocumentEditor.tsx +0 -93
- package/dist/scaffolds/notis-notes/packages/sdk/src/components/Markdown.tsx +0 -60
- package/dist/scaffolds/notis-notes/packages/sdk/src/components/MultiSelectActionBar.tsx +0 -278
- package/dist/scaffolds/notis-notes/packages/sdk/src/components/MultiSelectCheckbox.tsx +0 -91
- package/dist/scaffolds/notis-notes/packages/sdk/src/components/MultiSelectDragOverlay.tsx +0 -39
- package/dist/scaffolds/notis-notes/packages/sdk/src/config.ts +0 -234
- package/dist/scaffolds/notis-notes/packages/sdk/src/documents.ts +0 -250
- package/dist/scaffolds/notis-notes/packages/sdk/src/hooks/useBackend.ts +0 -41
- package/dist/scaffolds/notis-notes/packages/sdk/src/hooks/useCloudComputer.ts +0 -97
- package/dist/scaffolds/notis-notes/packages/sdk/src/hooks/useDatabaseSchema.ts +0 -85
- package/dist/scaffolds/notis-notes/packages/sdk/src/hooks/useDatabaseSubscription.ts +0 -76
- package/dist/scaffolds/notis-notes/packages/sdk/src/hooks/useDocument.ts +0 -78
- package/dist/scaffolds/notis-notes/packages/sdk/src/hooks/useDocuments.ts +0 -121
- package/dist/scaffolds/notis-notes/packages/sdk/src/hooks/useHandover.ts +0 -75
- package/dist/scaffolds/notis-notes/packages/sdk/src/hooks/useMultiSelect.ts +0 -539
- package/dist/scaffolds/notis-notes/packages/sdk/src/hooks/useNotis.ts +0 -34
- package/dist/scaffolds/notis-notes/packages/sdk/src/hooks/useNotisNavigation.ts +0 -49
- package/dist/scaffolds/notis-notes/packages/sdk/src/hooks/useTool.ts +0 -64
- package/dist/scaffolds/notis-notes/packages/sdk/src/hooks/useTools.ts +0 -56
- package/dist/scaffolds/notis-notes/packages/sdk/src/hooks/useTopBarSearch.ts +0 -73
- package/dist/scaffolds/notis-notes/packages/sdk/src/hooks/useUpsertDocument.ts +0 -95
- package/dist/scaffolds/notis-notes/packages/sdk/src/index.ts +0 -100
- package/dist/scaffolds/notis-notes/packages/sdk/src/provider.tsx +0 -43
- package/dist/scaffolds/notis-notes/packages/sdk/src/runtime.ts +0 -351
- package/dist/scaffolds/notis-notes/packages/sdk/src/styles.css +0 -186
- package/dist/scaffolds/notis-notes/packages/sdk/src/ui.ts +0 -15
- package/dist/scaffolds/notis-notes/packages/sdk/src/vite.ts +0 -56
- package/dist/scaffolds/notis-notes/packages/sdk/tsconfig.json +0 -15
- package/dist/scaffolds/notis-notes/postcss.config.mjs +0 -8
- package/dist/scaffolds/notis-notes/tailwind.config.ts +0 -58
- package/dist/scaffolds/notis-notes/tsconfig.json +0 -23
- package/dist/scaffolds/notis-notes/vite.config.ts +0 -10
- package/dist/scaffolds/notis-random/CHANGELOG.md +0 -15
- package/dist/scaffolds/notis-random/README.md +0 -33
- package/dist/scaffolds/notis-random/app/globals.css +0 -11
- package/dist/scaffolds/notis-random/app/history/page.tsx +0 -67
- package/dist/scaffolds/notis-random/app/layout.tsx +0 -7
- package/dist/scaffolds/notis-random/app/page.tsx +0 -289
- package/dist/scaffolds/notis-random/components/ui/button.tsx +0 -50
- package/dist/scaffolds/notis-random/components/ui/card.tsx +0 -16
- package/dist/scaffolds/notis-random/components/ui/input.tsx +0 -23
- package/dist/scaffolds/notis-random/components.json +0 -20
- package/dist/scaffolds/notis-random/index.html +0 -12
- package/dist/scaffolds/notis-random/lib/notis-tools.ts +0 -128
- package/dist/scaffolds/notis-random/lib/rng.ts +0 -202
- package/dist/scaffolds/notis-random/lib/roll-record.ts +0 -189
- package/dist/scaffolds/notis-random/lib/utils.ts +0 -25
- package/dist/scaffolds/notis-random/metadata/screenshot-1.png +0 -0
- package/dist/scaffolds/notis-random/metadata/screenshot-2.png +0 -0
- package/dist/scaffolds/notis-random/metadata/screenshot-3.png +0 -0
- package/dist/scaffolds/notis-random/metadata/screenshot-4.png +0 -0
- package/dist/scaffolds/notis-random/metadata/screenshot-5.png +0 -0
- package/dist/scaffolds/notis-random/metadata/screenshot-fixtures.json +0 -753
- package/dist/scaffolds/notis-random/notis.config.ts +0 -86
- package/dist/scaffolds/notis-random/package-lock.json +0 -4513
- package/dist/scaffolds/notis-random/package.json +0 -36
- package/dist/scaffolds/notis-random/packages/sdk/package.json +0 -36
- package/dist/scaffolds/notis-random/packages/sdk/src/components/DocumentEditor.tsx +0 -93
- package/dist/scaffolds/notis-random/packages/sdk/src/components/Markdown.tsx +0 -60
- package/dist/scaffolds/notis-random/packages/sdk/src/components/MultiSelectActionBar.tsx +0 -278
- package/dist/scaffolds/notis-random/packages/sdk/src/components/MultiSelectCheckbox.tsx +0 -91
- package/dist/scaffolds/notis-random/packages/sdk/src/components/MultiSelectDragOverlay.tsx +0 -39
- package/dist/scaffolds/notis-random/packages/sdk/src/config.ts +0 -234
- package/dist/scaffolds/notis-random/packages/sdk/src/documents.ts +0 -250
- package/dist/scaffolds/notis-random/packages/sdk/src/hooks/useBackend.ts +0 -41
- package/dist/scaffolds/notis-random/packages/sdk/src/hooks/useCloudComputer.ts +0 -97
- package/dist/scaffolds/notis-random/packages/sdk/src/hooks/useDatabaseSchema.ts +0 -85
- package/dist/scaffolds/notis-random/packages/sdk/src/hooks/useDatabaseSubscription.ts +0 -76
- package/dist/scaffolds/notis-random/packages/sdk/src/hooks/useDocument.ts +0 -78
- package/dist/scaffolds/notis-random/packages/sdk/src/hooks/useDocuments.ts +0 -121
- package/dist/scaffolds/notis-random/packages/sdk/src/hooks/useHandover.ts +0 -75
- package/dist/scaffolds/notis-random/packages/sdk/src/hooks/useMultiSelect.ts +0 -539
- package/dist/scaffolds/notis-random/packages/sdk/src/hooks/useNotis.ts +0 -34
- package/dist/scaffolds/notis-random/packages/sdk/src/hooks/useNotisNavigation.ts +0 -49
- package/dist/scaffolds/notis-random/packages/sdk/src/hooks/useTool.ts +0 -64
- package/dist/scaffolds/notis-random/packages/sdk/src/hooks/useTools.ts +0 -56
- package/dist/scaffolds/notis-random/packages/sdk/src/hooks/useTopBarSearch.ts +0 -73
- package/dist/scaffolds/notis-random/packages/sdk/src/hooks/useUpsertDocument.ts +0 -95
- package/dist/scaffolds/notis-random/packages/sdk/src/index.ts +0 -100
- package/dist/scaffolds/notis-random/packages/sdk/src/provider.tsx +0 -43
- package/dist/scaffolds/notis-random/packages/sdk/src/runtime.ts +0 -351
- package/dist/scaffolds/notis-random/packages/sdk/src/styles.css +0 -186
- package/dist/scaffolds/notis-random/packages/sdk/src/ui.ts +0 -15
- package/dist/scaffolds/notis-random/packages/sdk/src/vite.ts +0 -56
- package/dist/scaffolds/notis-random/packages/sdk/tsconfig.json +0 -15
- package/dist/scaffolds/notis-random/postcss.config.mjs +0 -6
- package/dist/scaffolds/notis-random/src/dev-main.tsx +0 -70
- package/dist/scaffolds/notis-random/src/mock-runtime.ts +0 -129
- package/dist/scaffolds/notis-random/tailwind.config.ts +0 -50
- package/dist/scaffolds/notis-random/tsconfig.json +0 -23
- package/dist/scaffolds/notis-random/vite.config.ts +0 -11
- package/dist/scaffolds.json +0 -49
|
@@ -28,7 +28,10 @@ import {
|
|
|
28
28
|
readLinkedState,
|
|
29
29
|
writeLinkedState,
|
|
30
30
|
} from './app-platform.js';
|
|
31
|
-
import {
|
|
31
|
+
import {
|
|
32
|
+
linkAppDevSessionTarget,
|
|
33
|
+
readAppDevSessions,
|
|
34
|
+
} from './app-dev-sessions.js';
|
|
32
35
|
|
|
33
36
|
const CONTENT_TYPES = {
|
|
34
37
|
'.js': 'application/javascript; charset=utf-8',
|
|
@@ -297,7 +300,7 @@ function renderHarnessHtml({ state, manifest, appConfig, route, harnessOptions,
|
|
|
297
300
|
/**
|
|
298
301
|
* Start the dev server for one or more apps.
|
|
299
302
|
*
|
|
300
|
-
* @param {{apps: Array<{slug: string, projectDir: string, appId?: string, targetAppId?: string, userId?: string}>, port: number, watch?: boolean, sessionsFilePath?: string, harness?: { mode?: string, apiBase?: string, jwt?: string }, log?: (m: string) => void, logError?: (m: string) => void}} options
|
|
303
|
+
* @param {{apps: Array<{slug: string, projectDir: string, appId?: string, targetAppId?: string, userId?: string, profileKey?: string, sessionId?: string, mountNonce?: string}>, port: number, watch?: boolean, sessionsFilePath?: string, harness?: { mode?: string, apiBase?: string, jwt?: string }, log?: (m: string) => void, logError?: (m: string) => void}} options
|
|
301
304
|
*/
|
|
302
305
|
export async function startAppDevServer({
|
|
303
306
|
apps,
|
|
@@ -313,22 +316,43 @@ export async function startAppDevServer({
|
|
|
313
316
|
}
|
|
314
317
|
|
|
315
318
|
const appState = new Map();
|
|
316
|
-
for
|
|
317
|
-
|
|
319
|
+
// Sidebar reconciliation needs one stream for the shared host, not one
|
|
320
|
+
// long-lived HTTP/1.1 connection per discovered app. Keeping a stream per
|
|
321
|
+
// app exhausts Chromium's per-origin connection pool and can indefinitely
|
|
322
|
+
// queue the active view's bundle fetch during hot reload.
|
|
323
|
+
const hostSseClients = new Set();
|
|
324
|
+
const createAppState = (app) => {
|
|
325
|
+
let resolveBundleReady;
|
|
326
|
+
const bundleReadyPromise = new Promise((resolvePromise) => {
|
|
327
|
+
resolveBundleReady = resolvePromise;
|
|
328
|
+
});
|
|
329
|
+
return {
|
|
318
330
|
slug: app.slug,
|
|
319
331
|
projectDir: app.projectDir,
|
|
320
332
|
appId: app.appId || null,
|
|
321
333
|
targetAppId: app.targetAppId || null,
|
|
322
334
|
userId: app.userId || null,
|
|
335
|
+
profileKey: app.profileKey || null,
|
|
336
|
+
sessionId: app.sessionId || null,
|
|
337
|
+
mountNonce: app.mountNonce || null,
|
|
323
338
|
canonicalBundleDir: getBundleDir(app.projectDir),
|
|
324
339
|
bundleDir: null,
|
|
325
340
|
jsPath: null,
|
|
326
341
|
sseClients: new Set(),
|
|
327
342
|
watcher: null,
|
|
343
|
+
sourceWatchers: [],
|
|
344
|
+
prepareTimer: null,
|
|
345
|
+
reloadTimer: null,
|
|
328
346
|
buildProcess: null,
|
|
329
347
|
lastMtimeMs: 0,
|
|
330
348
|
watchPollTimer: null,
|
|
331
|
-
|
|
349
|
+
bundleReady: false,
|
|
350
|
+
bundleReadyPromise,
|
|
351
|
+
resolveBundleReady,
|
|
352
|
+
};
|
|
353
|
+
};
|
|
354
|
+
for (const app of apps) {
|
|
355
|
+
appState.set(app.slug, createAppState(app));
|
|
332
356
|
}
|
|
333
357
|
|
|
334
358
|
function broadcastReload(slug) {
|
|
@@ -342,6 +366,14 @@ export async function startAppDevServer({
|
|
|
342
366
|
state.sseClients.delete(res);
|
|
343
367
|
}
|
|
344
368
|
}
|
|
369
|
+
const hostPayload = `event: reload\ndata: ${JSON.stringify({ slug, at: Date.now() })}\n\n`;
|
|
370
|
+
for (const res of hostSseClients) {
|
|
371
|
+
try {
|
|
372
|
+
res.write(hostPayload);
|
|
373
|
+
} catch {
|
|
374
|
+
hostSseClients.delete(res);
|
|
375
|
+
}
|
|
376
|
+
}
|
|
345
377
|
}
|
|
346
378
|
|
|
347
379
|
function matchAppRoute(pathname) {
|
|
@@ -374,6 +406,10 @@ export async function startAppDevServer({
|
|
|
374
406
|
state.jsPath = join(bundleDir, 'app.js');
|
|
375
407
|
try {
|
|
376
408
|
state.lastMtimeMs = statSync(state.jsPath).mtimeMs;
|
|
409
|
+
if (!state.bundleReady) {
|
|
410
|
+
state.bundleReady = true;
|
|
411
|
+
state.resolveBundleReady();
|
|
412
|
+
}
|
|
377
413
|
} catch {
|
|
378
414
|
state.lastMtimeMs = 0;
|
|
379
415
|
}
|
|
@@ -451,6 +487,9 @@ export async function startAppDevServer({
|
|
|
451
487
|
try {
|
|
452
488
|
const body = await readRequestJson(req);
|
|
453
489
|
const appId = typeof body.app_id === 'string' ? body.app_id.trim() : '';
|
|
490
|
+
const version = Number.isInteger(body.version) && body.version > 0
|
|
491
|
+
? body.version
|
|
492
|
+
: null;
|
|
454
493
|
if (!appId) {
|
|
455
494
|
res.writeHead(400, {
|
|
456
495
|
...headers,
|
|
@@ -459,20 +498,69 @@ export async function startAppDevServer({
|
|
|
459
498
|
res.end(JSON.stringify({ error: 'app_id is required' }));
|
|
460
499
|
return;
|
|
461
500
|
}
|
|
501
|
+
if (version === null) {
|
|
502
|
+
res.writeHead(400, {
|
|
503
|
+
...headers,
|
|
504
|
+
'Content-Type': 'application/json; charset=utf-8',
|
|
505
|
+
});
|
|
506
|
+
res.end(JSON.stringify({ error: 'version must be a positive integer' }));
|
|
507
|
+
return;
|
|
508
|
+
}
|
|
509
|
+
const proofSession = readAppDevSessions(sessionsFilePath).sessions.find((session) => (
|
|
510
|
+
typeof body.session_id === 'string'
|
|
511
|
+
&& typeof body.mount_nonce === 'string'
|
|
512
|
+
&& session.sessionId === body.session_id
|
|
513
|
+
&& session.mountNonce === body.mount_nonce
|
|
514
|
+
&& session.devSlug === state.slug
|
|
515
|
+
&& session.projectDir === state.projectDir
|
|
516
|
+
));
|
|
517
|
+
if (!proofSession) {
|
|
518
|
+
res.writeHead(403, {
|
|
519
|
+
...headers,
|
|
520
|
+
'Content-Type': 'application/json; charset=utf-8',
|
|
521
|
+
});
|
|
522
|
+
res.end(JSON.stringify({ error: 'invalid app development session proof' }));
|
|
523
|
+
return;
|
|
524
|
+
}
|
|
525
|
+
const expectedAppId = proofSession.targetAppId || proofSession.appId;
|
|
526
|
+
if (!expectedAppId || appId !== expectedAppId) {
|
|
527
|
+
res.writeHead(409, {
|
|
528
|
+
...headers,
|
|
529
|
+
'Content-Type': 'application/json; charset=utf-8',
|
|
530
|
+
});
|
|
531
|
+
res.end(JSON.stringify({ error: 'app_id does not match this development session' }));
|
|
532
|
+
return;
|
|
533
|
+
}
|
|
462
534
|
|
|
463
|
-
const currentState = readLinkedState(
|
|
535
|
+
const currentState = readLinkedState(
|
|
536
|
+
state.projectDir,
|
|
537
|
+
proofSession.profileKey || state.profileKey,
|
|
538
|
+
) || {};
|
|
464
539
|
const now = new Date().toISOString();
|
|
465
|
-
|
|
540
|
+
const promotedInPlace = proofSession.appId === appId;
|
|
541
|
+
const nextState = {
|
|
466
542
|
...currentState,
|
|
467
543
|
app_id: appId,
|
|
468
|
-
dev_app_id: state.appId || currentState.dev_app_id,
|
|
469
544
|
linked_at: currentState.linked_at || now,
|
|
470
|
-
|
|
545
|
+
version,
|
|
546
|
+
deployed_at: now,
|
|
471
547
|
updated_at: now,
|
|
472
|
-
}
|
|
473
|
-
|
|
548
|
+
};
|
|
549
|
+
if (promotedInPlace) {
|
|
550
|
+
delete nextState.dev_app_id;
|
|
551
|
+
delete nextState.dev_linked_at;
|
|
552
|
+
} else {
|
|
553
|
+
nextState.dev_app_id = proofSession.appId || currentState.dev_app_id;
|
|
554
|
+
nextState.dev_linked_at = currentState.dev_linked_at || now;
|
|
555
|
+
}
|
|
556
|
+
writeLinkedState(
|
|
557
|
+
state.projectDir,
|
|
558
|
+
nextState,
|
|
559
|
+
proofSession.profileKey || state.profileKey,
|
|
560
|
+
);
|
|
474
561
|
linkAppDevSessionTarget({
|
|
475
|
-
|
|
562
|
+
sessionId: proofSession.sessionId,
|
|
563
|
+
appId: proofSession.appId,
|
|
476
564
|
devSlug: state.slug,
|
|
477
565
|
targetAppId: appId,
|
|
478
566
|
lastHeartbeatAt: now,
|
|
@@ -480,8 +568,9 @@ export async function startAppDevServer({
|
|
|
480
568
|
const response = {
|
|
481
569
|
ok: true,
|
|
482
570
|
app_id: appId,
|
|
483
|
-
dev_app_id:
|
|
571
|
+
dev_app_id: promotedInPlace ? null : proofSession.appId || null,
|
|
484
572
|
target_app_id: appId,
|
|
573
|
+
version,
|
|
485
574
|
};
|
|
486
575
|
res.writeHead(200, {
|
|
487
576
|
...headers,
|
|
@@ -525,6 +614,18 @@ export async function startAppDevServer({
|
|
|
525
614
|
return;
|
|
526
615
|
}
|
|
527
616
|
|
|
617
|
+
if (url.pathname === '/events') {
|
|
618
|
+
res.writeHead(200, {
|
|
619
|
+
...headers,
|
|
620
|
+
'Content-Type': 'text/event-stream; charset=utf-8',
|
|
621
|
+
Connection: 'keep-alive',
|
|
622
|
+
});
|
|
623
|
+
res.write(': connected\n\n');
|
|
624
|
+
hostSseClients.add(res);
|
|
625
|
+
req.on('close', () => hostSseClients.delete(res));
|
|
626
|
+
return;
|
|
627
|
+
}
|
|
628
|
+
|
|
528
629
|
const routed = matchAppRoute(url.pathname);
|
|
529
630
|
if (!routed) {
|
|
530
631
|
res.writeHead(404, headers);
|
|
@@ -644,15 +745,23 @@ export async function startAppDevServer({
|
|
|
644
745
|
try {
|
|
645
746
|
state.watcher = fsWatch(state.bundleDir, { persistent: false }, (_event, filename) => {
|
|
646
747
|
if (!filename || filename !== 'app.js') return;
|
|
647
|
-
|
|
648
|
-
|
|
649
|
-
|
|
650
|
-
|
|
651
|
-
|
|
652
|
-
|
|
653
|
-
|
|
654
|
-
|
|
655
|
-
|
|
748
|
+
// Vite may emit the filesystem notification while its output file is
|
|
749
|
+
// still being replaced. Debounce the notification so consumers never
|
|
750
|
+
// fetch the previous or partially-written bundle under the new build
|
|
751
|
+
// revision.
|
|
752
|
+
if (state.reloadTimer) clearTimeout(state.reloadTimer);
|
|
753
|
+
state.reloadTimer = setTimeout(() => {
|
|
754
|
+
state.reloadTimer = null;
|
|
755
|
+
try {
|
|
756
|
+
const stat = statSync(state.jsPath);
|
|
757
|
+
if (stat.mtimeMs === state.lastMtimeMs) return;
|
|
758
|
+
state.lastMtimeMs = stat.mtimeMs;
|
|
759
|
+
log(`[notis apps dev] ${state.slug}: bundle updated — reloading portal`);
|
|
760
|
+
broadcastReload(state.slug);
|
|
761
|
+
} catch {
|
|
762
|
+
// Bundle file may be missing mid-write; the next change retries.
|
|
763
|
+
}
|
|
764
|
+
}, 100);
|
|
656
765
|
});
|
|
657
766
|
} catch (error) {
|
|
658
767
|
logError(`[notis apps dev] ${state.slug}: watch failed: ${error.message}`);
|
|
@@ -669,9 +778,53 @@ export async function startAppDevServer({
|
|
|
669
778
|
state.watchPollTimer = setTimeout(() => pollForBundleAndWatch(state), 300);
|
|
670
779
|
}
|
|
671
780
|
|
|
781
|
+
function watchManifestInputs(state) {
|
|
782
|
+
const schedulePrepare = () => {
|
|
783
|
+
if (state.prepareTimer) clearTimeout(state.prepareTimer);
|
|
784
|
+
state.prepareTimer = setTimeout(() => {
|
|
785
|
+
state.prepareTimer = null;
|
|
786
|
+
void prepareArtifactBuild(state.projectDir).then(() => {
|
|
787
|
+
// Name, icon, and route metadata can change without changing the
|
|
788
|
+
// compiled JS bytes. Notify consumers immediately; a generated-entry
|
|
789
|
+
// change will produce the normal second reload after Vite rebuilds.
|
|
790
|
+
broadcastReload(state.slug);
|
|
791
|
+
}).catch((error) => {
|
|
792
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
793
|
+
logError(`[notis apps dev] ${state.slug}: ${message}`);
|
|
794
|
+
});
|
|
795
|
+
}, 75);
|
|
796
|
+
};
|
|
797
|
+
try {
|
|
798
|
+
state.sourceWatchers.push(fsWatch(
|
|
799
|
+
state.projectDir,
|
|
800
|
+
{ persistent: false },
|
|
801
|
+
(_event, filename) => {
|
|
802
|
+
if (filename && /^notis\.config\.(?:ts|js|mjs)$/.test(String(filename))) {
|
|
803
|
+
schedulePrepare();
|
|
804
|
+
}
|
|
805
|
+
},
|
|
806
|
+
));
|
|
807
|
+
} catch (error) {
|
|
808
|
+
logError(`[notis apps dev] ${state.slug}: config watch failed: ${error.message}`);
|
|
809
|
+
}
|
|
810
|
+
const appDir = join(state.projectDir, 'app');
|
|
811
|
+
if (existsSync(appDir)) {
|
|
812
|
+
try {
|
|
813
|
+
state.sourceWatchers.push(fsWatch(
|
|
814
|
+
appDir,
|
|
815
|
+
{ persistent: false, recursive: process.platform === 'darwin' || process.platform === 'win32' },
|
|
816
|
+
schedulePrepare,
|
|
817
|
+
));
|
|
818
|
+
} catch (error) {
|
|
819
|
+
logError(`[notis apps dev] ${state.slug}: route watch failed: ${error.message}`);
|
|
820
|
+
}
|
|
821
|
+
}
|
|
822
|
+
}
|
|
823
|
+
|
|
672
824
|
for (const state of appState.values()) {
|
|
673
825
|
if (watch) {
|
|
674
826
|
await prepareArtifactBuild(state.projectDir);
|
|
827
|
+
watchManifestInputs(state);
|
|
675
828
|
pollForBundleAndWatch(state);
|
|
676
829
|
|
|
677
830
|
state.buildProcess = spawn('npm', ['run', 'build', '--', '--watch'], {
|
|
@@ -694,8 +847,37 @@ export async function startAppDevServer({
|
|
|
694
847
|
|
|
695
848
|
return {
|
|
696
849
|
port,
|
|
850
|
+
updateApp(slug, updates = {}) {
|
|
851
|
+
const state = appState.get(slug);
|
|
852
|
+
if (!state) throw new Error(`unknown app: ${slug}`);
|
|
853
|
+
for (const key of ['appId', 'targetAppId', 'userId', 'profileKey', 'sessionId', 'mountNonce']) {
|
|
854
|
+
if (Object.prototype.hasOwnProperty.call(updates, key)) {
|
|
855
|
+
state[key] = updates[key] || null;
|
|
856
|
+
}
|
|
857
|
+
}
|
|
858
|
+
},
|
|
859
|
+
isBundleReady(slug) {
|
|
860
|
+
const state = appState.get(slug);
|
|
861
|
+
if (!state) throw new Error(`unknown app: ${slug}`);
|
|
862
|
+
return state.bundleReady;
|
|
863
|
+
},
|
|
864
|
+
waitForBundle(slug) {
|
|
865
|
+
const state = appState.get(slug);
|
|
866
|
+
if (!state) return Promise.reject(new Error(`unknown app: ${slug}`));
|
|
867
|
+
return state.bundleReadyPromise;
|
|
868
|
+
},
|
|
697
869
|
async close() {
|
|
698
870
|
for (const state of appState.values()) {
|
|
871
|
+
if (state.prepareTimer) clearTimeout(state.prepareTimer);
|
|
872
|
+
if (state.reloadTimer) clearTimeout(state.reloadTimer);
|
|
873
|
+
for (const watcher of state.sourceWatchers) {
|
|
874
|
+
try {
|
|
875
|
+
watcher.close();
|
|
876
|
+
} catch {
|
|
877
|
+
// ignore
|
|
878
|
+
}
|
|
879
|
+
}
|
|
880
|
+
state.sourceWatchers = [];
|
|
699
881
|
if (state.watchPollTimer) clearTimeout(state.watchPollTimer);
|
|
700
882
|
if (state.watcher) {
|
|
701
883
|
try {
|
|
@@ -716,6 +898,14 @@ export async function startAppDevServer({
|
|
|
716
898
|
state.buildProcess.kill('SIGTERM');
|
|
717
899
|
}
|
|
718
900
|
}
|
|
901
|
+
for (const res of hostSseClients) {
|
|
902
|
+
try {
|
|
903
|
+
res.end();
|
|
904
|
+
} catch {
|
|
905
|
+
// ignore
|
|
906
|
+
}
|
|
907
|
+
}
|
|
908
|
+
hostSseClients.clear();
|
|
719
909
|
await new Promise((resolvePromise) => server.close(() => resolvePromise()));
|
|
720
910
|
},
|
|
721
911
|
};
|
|
@@ -1,30 +1,77 @@
|
|
|
1
1
|
import { randomUUID } from 'node:crypto';
|
|
2
|
-
import { existsSync, mkdirSync, readFileSync, renameSync, writeFileSync } from 'node:fs';
|
|
3
|
-
import { dirname, join
|
|
2
|
+
import { existsSync, lstatSync, mkdirSync, readFileSync, renameSync, rmSync, writeFileSync } from 'node:fs';
|
|
3
|
+
import { dirname, join } from 'node:path';
|
|
4
4
|
import { CONFIG_DIR } from './profiles.js';
|
|
5
5
|
|
|
6
6
|
export const DEFAULT_APP_DEV_SESSIONS_FILE = join(CONFIG_DIR, 'app-dev-sessions.json');
|
|
7
7
|
export const APP_DEV_SESSIONS_FILE = DEFAULT_APP_DEV_SESSIONS_FILE;
|
|
8
8
|
export const APP_DEV_SESSIONS_VERSION = 1;
|
|
9
|
-
|
|
9
|
+
const REGISTRY_LOCK_TIMEOUT_MS = 5_000;
|
|
10
|
+
const registryLockWait = new Int32Array(new SharedArrayBuffer(4));
|
|
11
|
+
|
|
12
|
+
function readRegistryLockOwner(lockPath) {
|
|
13
|
+
try {
|
|
14
|
+
return readFileSync(join(lockPath, 'owner'), 'utf8').trim();
|
|
15
|
+
} catch {
|
|
16
|
+
return null;
|
|
17
|
+
}
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
function withRegistryLock(filePath, callback) {
|
|
21
|
+
mkdirSync(dirname(filePath), { recursive: true, mode: 0o700 });
|
|
22
|
+
const lockPath = `${filePath}.lock`;
|
|
23
|
+
const ownerId = `${process.pid}.${randomUUID()}`;
|
|
24
|
+
const startedAt = Date.now();
|
|
25
|
+
while (true) {
|
|
26
|
+
try {
|
|
27
|
+
mkdirSync(lockPath, { mode: 0o700 });
|
|
28
|
+
try {
|
|
29
|
+
writeFileSync(join(lockPath, 'owner'), ownerId, { mode: 0o600 });
|
|
30
|
+
} catch (error) {
|
|
31
|
+
rmSync(lockPath, { recursive: true, force: true });
|
|
32
|
+
throw error;
|
|
33
|
+
}
|
|
34
|
+
break;
|
|
35
|
+
} catch (error) {
|
|
36
|
+
if (error?.code !== 'EEXIST') throw error;
|
|
37
|
+
try {
|
|
38
|
+
const lockStat = lstatSync(lockPath);
|
|
39
|
+
if (lockStat.isSymbolicLink() || !lockStat.isDirectory()) {
|
|
40
|
+
throw new Error(`Refusing unsafe app development registry lock: ${lockPath}`);
|
|
41
|
+
}
|
|
42
|
+
} catch (statError) {
|
|
43
|
+
if (statError?.code === 'ENOENT') continue;
|
|
44
|
+
throw statError;
|
|
45
|
+
}
|
|
46
|
+
if (Date.now() - startedAt >= REGISTRY_LOCK_TIMEOUT_MS) {
|
|
47
|
+
throw new Error(
|
|
48
|
+
`Timed out waiting for app development registry lock: ${filePath}. `
|
|
49
|
+
+ `If no app development process is running, remove the orphaned lock: ${lockPath}`,
|
|
50
|
+
);
|
|
51
|
+
}
|
|
52
|
+
Atomics.wait(registryLockWait, 0, 0, 10);
|
|
53
|
+
}
|
|
54
|
+
}
|
|
55
|
+
try {
|
|
56
|
+
return callback();
|
|
57
|
+
} finally {
|
|
58
|
+
try {
|
|
59
|
+
if (readRegistryLockOwner(lockPath) === ownerId) {
|
|
60
|
+
rmSync(lockPath, { recursive: true, force: true });
|
|
61
|
+
}
|
|
62
|
+
} catch {
|
|
63
|
+
// Never remove or disrupt a lock that no longer belongs to this writer.
|
|
64
|
+
}
|
|
65
|
+
}
|
|
66
|
+
}
|
|
10
67
|
|
|
11
68
|
export function getAppDevSessionsFile(filePath) {
|
|
12
69
|
if (filePath) {
|
|
13
70
|
return filePath;
|
|
14
71
|
}
|
|
15
|
-
const envPath = process.env.NOTIS_APP_DEV_SESSIONS_FILE;
|
|
16
|
-
if (typeof envPath === 'string' && envPath.trim()) {
|
|
17
|
-
return envPath.trim();
|
|
18
|
-
}
|
|
19
72
|
return DEFAULT_APP_DEV_SESSIONS_FILE;
|
|
20
73
|
}
|
|
21
74
|
|
|
22
|
-
export function getAppDevSessionMountAcksFile(sessionsFilePath) {
|
|
23
|
-
const resolvedSessionsFile = getAppDevSessionsFile(sessionsFilePath);
|
|
24
|
-
const parsed = parsePath(resolvedSessionsFile);
|
|
25
|
-
return join(parsed.dir, `${parsed.name}-mount-acks${parsed.ext || '.json'}`);
|
|
26
|
-
}
|
|
27
|
-
|
|
28
75
|
function normalizeSessions(raw) {
|
|
29
76
|
if (!raw || typeof raw !== 'object' || !Array.isArray(raw.sessions)) {
|
|
30
77
|
return [];
|
|
@@ -62,7 +109,7 @@ export function readAppDevSessions(filePath) {
|
|
|
62
109
|
}
|
|
63
110
|
}
|
|
64
111
|
|
|
65
|
-
|
|
112
|
+
function writeAppDevSessionsUnlocked(registry, filePath) {
|
|
66
113
|
const resolvedFilePath = getAppDevSessionsFile(filePath);
|
|
67
114
|
mkdirSync(dirname(resolvedFilePath), { recursive: true, mode: 0o700 });
|
|
68
115
|
const normalized = {
|
|
@@ -75,151 +122,64 @@ export function writeAppDevSessions(registry, filePath) {
|
|
|
75
122
|
return normalized;
|
|
76
123
|
}
|
|
77
124
|
|
|
125
|
+
export function writeAppDevSessions(registry, filePath) {
|
|
126
|
+
const resolvedFilePath = getAppDevSessionsFile(filePath);
|
|
127
|
+
return withRegistryLock(resolvedFilePath, () => writeAppDevSessionsUnlocked(registry, resolvedFilePath));
|
|
128
|
+
}
|
|
129
|
+
|
|
78
130
|
export function upsertAppDevSessions(sessions, filePath) {
|
|
79
131
|
const nextSessions = Array.isArray(sessions) ? sessions : [sessions];
|
|
80
|
-
const
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
132
|
+
const resolvedFilePath = getAppDevSessionsFile(filePath);
|
|
133
|
+
return withRegistryLock(resolvedFilePath, () => {
|
|
134
|
+
const registry = readAppDevSessions(resolvedFilePath);
|
|
135
|
+
const keys = new Set(nextSessions.map((session) => `${session.sessionId}:${session.appId}`));
|
|
136
|
+
registry.sessions = [
|
|
137
|
+
...registry.sessions.filter((session) => !keys.has(`${session.sessionId}:${session.appId}`)),
|
|
138
|
+
...nextSessions,
|
|
139
|
+
];
|
|
140
|
+
return writeAppDevSessionsUnlocked(registry, resolvedFilePath);
|
|
141
|
+
});
|
|
87
142
|
}
|
|
88
143
|
|
|
89
144
|
export function heartbeatAppDevSession(sessionId, lastHeartbeatAt, filePath) {
|
|
90
|
-
const
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
: session,
|
|
95
|
-
|
|
96
|
-
|
|
145
|
+
const resolvedFilePath = getAppDevSessionsFile(filePath);
|
|
146
|
+
return withRegistryLock(resolvedFilePath, () => {
|
|
147
|
+
const registry = readAppDevSessions(resolvedFilePath);
|
|
148
|
+
registry.sessions = registry.sessions.map((session) =>
|
|
149
|
+
session.sessionId === sessionId ? { ...session, lastHeartbeatAt } : session,
|
|
150
|
+
);
|
|
151
|
+
return writeAppDevSessionsUnlocked(registry, resolvedFilePath);
|
|
152
|
+
});
|
|
97
153
|
}
|
|
98
154
|
|
|
99
|
-
export function linkAppDevSessionTarget({ appId, devSlug, targetAppId, lastHeartbeatAt = new Date().toISOString() }, filePath) {
|
|
155
|
+
export function linkAppDevSessionTarget({ sessionId, appId, devSlug, targetAppId, lastHeartbeatAt = new Date().toISOString() }, filePath) {
|
|
100
156
|
const target = typeof targetAppId === 'string' ? targetAppId.trim() : '';
|
|
101
157
|
if (!target) {
|
|
102
158
|
return readAppDevSessions(filePath);
|
|
103
159
|
}
|
|
104
|
-
const
|
|
105
|
-
|
|
106
|
-
const
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
targetAppId: target,
|
|
114
|
-
|
|
115
|
-
};
|
|
160
|
+
const resolvedFilePath = getAppDevSessionsFile(filePath);
|
|
161
|
+
const result = withRegistryLock(resolvedFilePath, () => {
|
|
162
|
+
const registry = readAppDevSessions(resolvedFilePath);
|
|
163
|
+
registry.sessions = registry.sessions.map((session) => {
|
|
164
|
+
const hasSelector = Boolean(appId || devSlug);
|
|
165
|
+
const matchesSession = !sessionId || session.sessionId === sessionId;
|
|
166
|
+
const matchesApp = !appId || session.appId === appId;
|
|
167
|
+
const matchesSlug = !devSlug || session.devSlug === devSlug;
|
|
168
|
+
if ((!hasSelector && !sessionId) || !matchesSession || !matchesApp || !matchesSlug) return session;
|
|
169
|
+
const linked = { ...session, targetAppId: target, lastHeartbeatAt };
|
|
170
|
+
return linked;
|
|
171
|
+
});
|
|
172
|
+
return writeAppDevSessionsUnlocked(registry, resolvedFilePath);
|
|
116
173
|
});
|
|
117
|
-
return writeAppDevSessions(registry, filePath);
|
|
118
|
-
}
|
|
119
|
-
|
|
120
|
-
export function removeAppDevSession(sessionId, filePath) {
|
|
121
|
-
const registry = readAppDevSessions(filePath);
|
|
122
|
-
registry.sessions = registry.sessions.filter((session) => session.sessionId !== sessionId);
|
|
123
|
-
const result = writeAppDevSessions(registry, filePath);
|
|
124
|
-
removeAppDevSessionMountAcknowledgements(sessionId, filePath);
|
|
125
174
|
return result;
|
|
126
175
|
}
|
|
127
176
|
|
|
128
|
-
function
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
typeof acknowledgement === 'object' &&
|
|
135
|
-
typeof acknowledgement.sessionId === 'string' &&
|
|
136
|
-
typeof acknowledgement.appId === 'string' &&
|
|
137
|
-
typeof acknowledgement.devSlug === 'string' &&
|
|
138
|
-
typeof acknowledgement.mountNonce === 'string' &&
|
|
139
|
-
typeof acknowledgement.acknowledgedAt === 'string' &&
|
|
140
|
-
['listed', 'rendered'].includes(acknowledgement.stage),
|
|
141
|
-
);
|
|
142
|
-
}
|
|
143
|
-
|
|
144
|
-
export function readAppDevSessionMountAcknowledgements(sessionsFilePath) {
|
|
145
|
-
const filePath = getAppDevSessionMountAcksFile(sessionsFilePath);
|
|
146
|
-
if (!existsSync(filePath)) {
|
|
147
|
-
return { version: APP_DEV_SESSION_MOUNT_ACKS_VERSION, acknowledgements: [] };
|
|
148
|
-
}
|
|
149
|
-
try {
|
|
150
|
-
const raw = JSON.parse(readFileSync(filePath, 'utf8'));
|
|
151
|
-
return {
|
|
152
|
-
version: APP_DEV_SESSION_MOUNT_ACKS_VERSION,
|
|
153
|
-
acknowledgements: normalizeMountAcknowledgements(raw),
|
|
154
|
-
};
|
|
155
|
-
} catch {
|
|
156
|
-
return { version: APP_DEV_SESSION_MOUNT_ACKS_VERSION, acknowledgements: [] };
|
|
157
|
-
}
|
|
158
|
-
}
|
|
159
|
-
|
|
160
|
-
function writeAppDevSessionMountAcknowledgements(registry, sessionsFilePath) {
|
|
161
|
-
const filePath = getAppDevSessionMountAcksFile(sessionsFilePath);
|
|
162
|
-
mkdirSync(dirname(filePath), { recursive: true, mode: 0o700 });
|
|
163
|
-
const normalized = {
|
|
164
|
-
version: APP_DEV_SESSION_MOUNT_ACKS_VERSION,
|
|
165
|
-
acknowledgements: normalizeMountAcknowledgements(registry),
|
|
166
|
-
};
|
|
167
|
-
const tempPath = `${filePath}.${process.pid}.${randomUUID()}.tmp`;
|
|
168
|
-
writeFileSync(tempPath, JSON.stringify(normalized, null, 2), { mode: 0o600 });
|
|
169
|
-
renameSync(tempPath, filePath);
|
|
170
|
-
return normalized;
|
|
171
|
-
}
|
|
172
|
-
|
|
173
|
-
export function removeAppDevSessionMountAcknowledgements(sessionId, sessionsFilePath) {
|
|
174
|
-
const filePath = getAppDevSessionMountAcksFile(sessionsFilePath);
|
|
175
|
-
if (!existsSync(filePath)) {
|
|
176
|
-
return { version: APP_DEV_SESSION_MOUNT_ACKS_VERSION, acknowledgements: [] };
|
|
177
|
-
}
|
|
178
|
-
const registry = readAppDevSessionMountAcknowledgements(sessionsFilePath);
|
|
179
|
-
registry.acknowledgements = registry.acknowledgements.filter(
|
|
180
|
-
(acknowledgement) => acknowledgement.sessionId !== sessionId,
|
|
181
|
-
);
|
|
182
|
-
return writeAppDevSessionMountAcknowledgements(registry, sessionsFilePath);
|
|
183
|
-
}
|
|
184
|
-
|
|
185
|
-
function mountAcknowledgementKey(value, fallbackStage = 'listed') {
|
|
186
|
-
return `${value.sessionId}:${value.appId}:${value.devSlug}:${value.mountNonce}:${value.stage || fallbackStage}`;
|
|
187
|
-
}
|
|
188
|
-
|
|
189
|
-
export async function waitForAppDevSessionMountAcknowledgements(expectedSessions, options = {}) {
|
|
190
|
-
const expected = Array.isArray(expectedSessions) ? expectedSessions : [expectedSessions];
|
|
191
|
-
const stage = options.stage === 'rendered' ? 'rendered' : 'listed';
|
|
192
|
-
const timeoutMs = Number.isFinite(options.timeoutMs) ? Math.max(0, options.timeoutMs) : 15_000;
|
|
193
|
-
const pollIntervalMs = Number.isFinite(options.pollIntervalMs)
|
|
194
|
-
? Math.max(1, options.pollIntervalMs)
|
|
195
|
-
: 100;
|
|
196
|
-
const now = typeof options.now === 'function' ? options.now : Date.now;
|
|
197
|
-
const sleep = typeof options.sleep === 'function'
|
|
198
|
-
? options.sleep
|
|
199
|
-
: (delayMs) => new Promise((resolve) => setTimeout(resolve, delayMs));
|
|
200
|
-
const startedAt = now();
|
|
201
|
-
|
|
202
|
-
while (true) {
|
|
203
|
-
const registry = readAppDevSessionMountAcknowledgements(options.sessionsFilePath);
|
|
204
|
-
const acknowledgedKeys = new Set(registry.acknowledgements.map(mountAcknowledgementKey));
|
|
205
|
-
const missing = expected.filter(
|
|
206
|
-
(session) => !acknowledgedKeys.has(mountAcknowledgementKey(session, stage)),
|
|
207
|
-
);
|
|
208
|
-
if (missing.length === 0) {
|
|
209
|
-
return { mounted: true, missing: [], acknowledgements: registry.acknowledgements };
|
|
210
|
-
}
|
|
211
|
-
|
|
212
|
-
const elapsedMs = now() - startedAt;
|
|
213
|
-
if (elapsedMs >= timeoutMs) {
|
|
214
|
-
return { mounted: false, missing, acknowledgements: registry.acknowledgements };
|
|
215
|
-
}
|
|
216
|
-
await sleep(Math.min(pollIntervalMs, timeoutMs - elapsedMs));
|
|
217
|
-
}
|
|
218
|
-
}
|
|
219
|
-
|
|
220
|
-
export function waitForAppDevSessionRenderAcknowledgements(expectedSessions, options = {}) {
|
|
221
|
-
return waitForAppDevSessionMountAcknowledgements(expectedSessions, {
|
|
222
|
-
...options,
|
|
223
|
-
stage: 'rendered',
|
|
177
|
+
export function removeAppDevSession(sessionId, filePath) {
|
|
178
|
+
const resolvedFilePath = getAppDevSessionsFile(filePath);
|
|
179
|
+
const result = withRegistryLock(resolvedFilePath, () => {
|
|
180
|
+
const registry = readAppDevSessions(resolvedFilePath);
|
|
181
|
+
registry.sessions = registry.sessions.filter((session) => session.sessionId !== sessionId);
|
|
182
|
+
return writeAppDevSessionsUnlocked(registry, resolvedFilePath);
|
|
224
183
|
});
|
|
184
|
+
return result;
|
|
225
185
|
}
|