@notis_ai/cli 0.2.13 → 0.2.15
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 +125 -21
- package/dist/agent-hooks/notis-agent-hook.mjs +19750 -0
- package/{skills → dist/base-skills}/notis-apps/SKILL.md +93 -58
- package/{skills → dist/base-skills}/notis-cli/SKILL.md +9 -9
- package/dist/base-skills/notis-query/SKILL.md +705 -0
- package/dist/skill-sync/index.js +1528 -0
- package/dist/skill-sync/index.js.map +7 -0
- package/package.json +5 -2
- package/skills/notis-apps/cli.md +44 -18
- package/skills/notis-cli/AGENT_INSTRUCTIONS.md +39 -0
- package/skills/notis-onboarding/BRIEF.md +16 -0
- package/src/agent-hook-entry.js +5 -0
- package/src/cli.js +23 -14
- package/src/command-specs/agents.js +392 -0
- package/src/command-specs/apps.js +1090 -418
- package/src/command-specs/auth.js +63 -10
- package/src/command-specs/handover.js +374 -0
- package/src/command-specs/index.js +9 -0
- package/src/command-specs/onboarding.js +69 -5
- package/src/command-specs/skills.js +56 -0
- package/src/command-specs/tools.js +6 -0
- package/src/runtime/agent-memory-state.js +126 -0
- package/src/runtime/agent-setup.js +383 -0
- 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 +229 -30
- package/src/runtime/app-dev-sessions.js +105 -145
- package/src/runtime/app-platform.js +1499 -161
- package/src/runtime/app-registry-scaffolds.js +367 -0
- package/src/runtime/base-skills.d.ts +20 -0
- package/src/runtime/base-skills.js +167 -0
- package/src/runtime/delegated-context.js +68 -0
- package/src/runtime/git.js +233 -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/src/runtime/skill-sync/cloud-client.ts +96 -0
- package/src/runtime/skill-sync/index.ts +644 -0
- package/src/runtime/skill-sync/local-scanner.ts +1046 -0
- package/src/runtime/skill-sync/symlink-manager.ts +383 -0
- package/src/runtime/skill-sync/sync-plan.ts +22 -0
- package/src/runtime/skill-sync/types.ts +103 -0
- package/src/runtime/skill-sync/write-cloud-skill.ts +50 -0
- package/src/runtime/store-screenshot.js +6 -1
- package/src/runtime/sync-skills.d.ts +37 -0
- package/src/runtime/sync-skills.js +215 -0
- package/src/runtime/transport.js +19 -2
- package/template/.harness/index.html.tmpl +116 -47
- 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/DocumentEditor.tsx +13 -3
- 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 +71 -0
- package/template/packages/sdk/src/documents.ts +28 -1
- package/template/packages/sdk/src/hooks/useActiveResource.ts +19 -0
- package/template/packages/sdk/src/hooks/useCloudComputer.ts +97 -0
- package/template/packages/sdk/src/hooks/useCollectionInteractions.ts +709 -0
- package/template/packages/sdk/src/hooks/useDatabaseSubscription.ts +76 -0
- package/template/packages/sdk/src/hooks/useHandover.ts +78 -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 +64 -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 +219 -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 -196
- package/dist/scaffolds/notis-database/packages/sdk/src/documents.ts +0 -229
- package/dist/scaffolds/notis-database/packages/sdk/src/hooks/useBackend.ts +0 -41
- package/dist/scaffolds/notis-database/packages/sdk/src/hooks/useDatabaseSchema.ts +0 -85
- 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/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 -83
- package/dist/scaffolds/notis-database/packages/sdk/src/provider.tsx +0 -43
- package/dist/scaffolds/notis-database/packages/sdk/src/runtime.ts +0 -220
- 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 -196
- package/dist/scaffolds/notis-journal/packages/sdk/src/documents.ts +0 -229
- package/dist/scaffolds/notis-journal/packages/sdk/src/hooks/useBackend.ts +0 -41
- package/dist/scaffolds/notis-journal/packages/sdk/src/hooks/useDatabaseSchema.ts +0 -85
- 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/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 -83
- package/dist/scaffolds/notis-journal/packages/sdk/src/provider.tsx +0 -43
- package/dist/scaffolds/notis-journal/packages/sdk/src/runtime.ts +0 -220
- 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 -197
- 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 -196
- package/dist/scaffolds/notis-notes/packages/sdk/src/documents.ts +0 -229
- package/dist/scaffolds/notis-notes/packages/sdk/src/hooks/useBackend.ts +0 -41
- package/dist/scaffolds/notis-notes/packages/sdk/src/hooks/useDatabaseSchema.ts +0 -85
- 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/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 -83
- package/dist/scaffolds/notis-notes/packages/sdk/src/provider.tsx +0 -43
- package/dist/scaffolds/notis-notes/packages/sdk/src/runtime.ts +0 -220
- 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 -196
- package/dist/scaffolds/notis-random/packages/sdk/src/documents.ts +0 -229
- package/dist/scaffolds/notis-random/packages/sdk/src/hooks/useBackend.ts +0 -41
- package/dist/scaffolds/notis-random/packages/sdk/src/hooks/useDatabaseSchema.ts +0 -85
- 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/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 -83
- package/dist/scaffolds/notis-random/packages/sdk/src/provider.tsx +0 -43
- package/dist/scaffolds/notis-random/packages/sdk/src/runtime.ts +0 -220
- 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
- package/template/metadata/screenshot-1.png +0 -0
- package/template/metadata/screenshot-2.png +0 -0
- package/template/metadata/screenshot-3.png +0 -0
|
@@ -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
|
+
}
|
|
@@ -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',
|
|
@@ -251,17 +254,26 @@ function buildHarnessDescriptor({ state, manifest, appConfig, route, scenario =
|
|
|
251
254
|
};
|
|
252
255
|
}
|
|
253
256
|
|
|
257
|
+
function plainObject(value) {
|
|
258
|
+
return value && typeof value === 'object' && !Array.isArray(value) ? value : {};
|
|
259
|
+
}
|
|
260
|
+
|
|
261
|
+
/**
|
|
262
|
+
* Resolve the fixture payload injected into one harness page load.
|
|
263
|
+
*
|
|
264
|
+
* A scenario may override individual `tools` / `requests` keys on top of the
|
|
265
|
+
* file-level defaults, which is how one route renders both its populated and
|
|
266
|
+
* its empty state. Each capture is its own page load, so a shallow per-key
|
|
267
|
+
* merge is all the isolation a scenario needs.
|
|
268
|
+
*/
|
|
254
269
|
function harnessFixtures(projectDir, scenario) {
|
|
255
270
|
const fixtureConfig = readJsonFile(join(projectDir, 'metadata', 'screenshot-fixtures.json')) || {};
|
|
256
|
-
const scenarios = fixtureConfig.scenarios
|
|
257
|
-
|
|
258
|
-
: {};
|
|
271
|
+
const scenarios = plainObject(fixtureConfig.scenarios);
|
|
272
|
+
const selected = scenario ? plainObject(scenarios[scenario]) : null;
|
|
259
273
|
return {
|
|
260
|
-
tools:
|
|
261
|
-
requests:
|
|
262
|
-
scenario:
|
|
263
|
-
? scenarios[scenario]
|
|
264
|
-
: null,
|
|
274
|
+
tools: { ...plainObject(fixtureConfig.tools), ...plainObject(selected?.tools) },
|
|
275
|
+
requests: { ...plainObject(fixtureConfig.requests), ...plainObject(selected?.requests) },
|
|
276
|
+
scenario: selected && Object.keys(selected).length > 0 ? selected : null,
|
|
265
277
|
};
|
|
266
278
|
}
|
|
267
279
|
|
|
@@ -288,7 +300,7 @@ function renderHarnessHtml({ state, manifest, appConfig, route, harnessOptions,
|
|
|
288
300
|
/**
|
|
289
301
|
* Start the dev server for one or more apps.
|
|
290
302
|
*
|
|
291
|
-
* @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
|
|
292
304
|
*/
|
|
293
305
|
export async function startAppDevServer({
|
|
294
306
|
apps,
|
|
@@ -304,22 +316,43 @@ export async function startAppDevServer({
|
|
|
304
316
|
}
|
|
305
317
|
|
|
306
318
|
const appState = new Map();
|
|
307
|
-
for
|
|
308
|
-
|
|
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 {
|
|
309
330
|
slug: app.slug,
|
|
310
331
|
projectDir: app.projectDir,
|
|
311
332
|
appId: app.appId || null,
|
|
312
333
|
targetAppId: app.targetAppId || null,
|
|
313
334
|
userId: app.userId || null,
|
|
335
|
+
profileKey: app.profileKey || null,
|
|
336
|
+
sessionId: app.sessionId || null,
|
|
337
|
+
mountNonce: app.mountNonce || null,
|
|
314
338
|
canonicalBundleDir: getBundleDir(app.projectDir),
|
|
315
339
|
bundleDir: null,
|
|
316
340
|
jsPath: null,
|
|
317
341
|
sseClients: new Set(),
|
|
318
342
|
watcher: null,
|
|
343
|
+
sourceWatchers: [],
|
|
344
|
+
prepareTimer: null,
|
|
345
|
+
reloadTimer: null,
|
|
319
346
|
buildProcess: null,
|
|
320
347
|
lastMtimeMs: 0,
|
|
321
348
|
watchPollTimer: null,
|
|
322
|
-
|
|
349
|
+
bundleReady: false,
|
|
350
|
+
bundleReadyPromise,
|
|
351
|
+
resolveBundleReady,
|
|
352
|
+
};
|
|
353
|
+
};
|
|
354
|
+
for (const app of apps) {
|
|
355
|
+
appState.set(app.slug, createAppState(app));
|
|
323
356
|
}
|
|
324
357
|
|
|
325
358
|
function broadcastReload(slug) {
|
|
@@ -333,6 +366,14 @@ export async function startAppDevServer({
|
|
|
333
366
|
state.sseClients.delete(res);
|
|
334
367
|
}
|
|
335
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
|
+
}
|
|
336
377
|
}
|
|
337
378
|
|
|
338
379
|
function matchAppRoute(pathname) {
|
|
@@ -365,6 +406,10 @@ export async function startAppDevServer({
|
|
|
365
406
|
state.jsPath = join(bundleDir, 'app.js');
|
|
366
407
|
try {
|
|
367
408
|
state.lastMtimeMs = statSync(state.jsPath).mtimeMs;
|
|
409
|
+
if (!state.bundleReady) {
|
|
410
|
+
state.bundleReady = true;
|
|
411
|
+
state.resolveBundleReady();
|
|
412
|
+
}
|
|
368
413
|
} catch {
|
|
369
414
|
state.lastMtimeMs = 0;
|
|
370
415
|
}
|
|
@@ -442,6 +487,9 @@ export async function startAppDevServer({
|
|
|
442
487
|
try {
|
|
443
488
|
const body = await readRequestJson(req);
|
|
444
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;
|
|
445
493
|
if (!appId) {
|
|
446
494
|
res.writeHead(400, {
|
|
447
495
|
...headers,
|
|
@@ -450,20 +498,69 @@ export async function startAppDevServer({
|
|
|
450
498
|
res.end(JSON.stringify({ error: 'app_id is required' }));
|
|
451
499
|
return;
|
|
452
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
|
+
}
|
|
453
534
|
|
|
454
|
-
const currentState = readLinkedState(
|
|
535
|
+
const currentState = readLinkedState(
|
|
536
|
+
state.projectDir,
|
|
537
|
+
proofSession.profileKey || state.profileKey,
|
|
538
|
+
) || {};
|
|
455
539
|
const now = new Date().toISOString();
|
|
456
|
-
|
|
540
|
+
const promotedInPlace = proofSession.appId === appId;
|
|
541
|
+
const nextState = {
|
|
457
542
|
...currentState,
|
|
458
543
|
app_id: appId,
|
|
459
|
-
dev_app_id: state.appId || currentState.dev_app_id,
|
|
460
544
|
linked_at: currentState.linked_at || now,
|
|
461
|
-
|
|
545
|
+
version,
|
|
546
|
+
deployed_at: now,
|
|
462
547
|
updated_at: now,
|
|
463
|
-
}
|
|
464
|
-
|
|
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
|
+
);
|
|
465
561
|
linkAppDevSessionTarget({
|
|
466
|
-
|
|
562
|
+
sessionId: proofSession.sessionId,
|
|
563
|
+
appId: proofSession.appId,
|
|
467
564
|
devSlug: state.slug,
|
|
468
565
|
targetAppId: appId,
|
|
469
566
|
lastHeartbeatAt: now,
|
|
@@ -471,8 +568,9 @@ export async function startAppDevServer({
|
|
|
471
568
|
const response = {
|
|
472
569
|
ok: true,
|
|
473
570
|
app_id: appId,
|
|
474
|
-
dev_app_id:
|
|
571
|
+
dev_app_id: promotedInPlace ? null : proofSession.appId || null,
|
|
475
572
|
target_app_id: appId,
|
|
573
|
+
version,
|
|
476
574
|
};
|
|
477
575
|
res.writeHead(200, {
|
|
478
576
|
...headers,
|
|
@@ -516,6 +614,18 @@ export async function startAppDevServer({
|
|
|
516
614
|
return;
|
|
517
615
|
}
|
|
518
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
|
+
|
|
519
629
|
const routed = matchAppRoute(url.pathname);
|
|
520
630
|
if (!routed) {
|
|
521
631
|
res.writeHead(404, headers);
|
|
@@ -635,15 +745,23 @@ export async function startAppDevServer({
|
|
|
635
745
|
try {
|
|
636
746
|
state.watcher = fsWatch(state.bundleDir, { persistent: false }, (_event, filename) => {
|
|
637
747
|
if (!filename || filename !== 'app.js') return;
|
|
638
|
-
|
|
639
|
-
|
|
640
|
-
|
|
641
|
-
|
|
642
|
-
|
|
643
|
-
|
|
644
|
-
|
|
645
|
-
|
|
646
|
-
|
|
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);
|
|
647
765
|
});
|
|
648
766
|
} catch (error) {
|
|
649
767
|
logError(`[notis apps dev] ${state.slug}: watch failed: ${error.message}`);
|
|
@@ -660,9 +778,53 @@ export async function startAppDevServer({
|
|
|
660
778
|
state.watchPollTimer = setTimeout(() => pollForBundleAndWatch(state), 300);
|
|
661
779
|
}
|
|
662
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
|
+
|
|
663
824
|
for (const state of appState.values()) {
|
|
664
825
|
if (watch) {
|
|
665
826
|
await prepareArtifactBuild(state.projectDir);
|
|
827
|
+
watchManifestInputs(state);
|
|
666
828
|
pollForBundleAndWatch(state);
|
|
667
829
|
|
|
668
830
|
state.buildProcess = spawn('npm', ['run', 'build', '--', '--watch'], {
|
|
@@ -685,8 +847,37 @@ export async function startAppDevServer({
|
|
|
685
847
|
|
|
686
848
|
return {
|
|
687
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
|
+
},
|
|
688
869
|
async close() {
|
|
689
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 = [];
|
|
690
881
|
if (state.watchPollTimer) clearTimeout(state.watchPollTimer);
|
|
691
882
|
if (state.watcher) {
|
|
692
883
|
try {
|
|
@@ -707,6 +898,14 @@ export async function startAppDevServer({
|
|
|
707
898
|
state.buildProcess.kill('SIGTERM');
|
|
708
899
|
}
|
|
709
900
|
}
|
|
901
|
+
for (const res of hostSseClients) {
|
|
902
|
+
try {
|
|
903
|
+
res.end();
|
|
904
|
+
} catch {
|
|
905
|
+
// ignore
|
|
906
|
+
}
|
|
907
|
+
}
|
|
908
|
+
hostSseClients.clear();
|
|
710
909
|
await new Promise((resolvePromise) => server.close(() => resolvePromise()));
|
|
711
910
|
},
|
|
712
911
|
};
|