@notis_ai/cli 0.2.14 → 0.2.16
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +83 -21
- package/dist/agent-hooks/notis-agent-hook.mjs +20168 -0
- package/{skills → dist/base-skills}/notis-apps/SKILL.md +167 -72
- package/{skills → dist/base-skills}/notis-cli/SKILL.md +86 -28
- 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 +39 -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 +1021 -424
- package/src/command-specs/auth.js +63 -10
- package/src/command-specs/index.js +6 -0
- package/src/command-specs/onboarding.js +69 -5
- package/src/command-specs/skills.js +56 -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-process-identity.js +111 -0
- package/src/runtime/app-dev-roots.js +284 -0
- package/src/runtime/app-dev-server.js +447 -29
- package/src/runtime/app-dev-sessions.js +105 -145
- package/src/runtime/app-platform.js +1314 -166
- 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/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 +19 -16
- 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 +231 -0
- 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 +43 -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 +25 -0
- package/template/packages/sdk/src/documents.ts +7 -1
- package/template/packages/sdk/src/hooks/useActiveResource.ts +19 -0
- package/template/packages/sdk/src/hooks/useCollectionInteractions.ts +726 -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/useNotis.ts +3 -0
- package/template/packages/sdk/src/hooks/useNotisNavigation.ts +7 -4
- 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 +634 -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 +92 -4
- 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
|
@@ -8,25 +8,25 @@
|
|
|
8
8
|
*/
|
|
9
9
|
|
|
10
10
|
import { randomUUID } from 'node:crypto';
|
|
11
|
-
import {
|
|
12
|
-
import { existsSync, mkdirSync, mkdtempSync, readdirSync, rmSync } from 'node:fs';
|
|
11
|
+
import { mkdirSync, mkdtempSync, readFileSync, readdirSync, rmSync } from 'node:fs';
|
|
13
12
|
import { tmpdir } from 'node:os';
|
|
14
|
-
import { basename, join, relative, resolve } from 'node:path';
|
|
13
|
+
import { basename, isAbsolute, join, relative, resolve, sep } from 'node:path';
|
|
15
14
|
|
|
16
15
|
import { CliError, EXIT_CODES, usageError } from '../runtime/errors.js';
|
|
17
16
|
import { formatTable } from '../runtime/output.js';
|
|
18
17
|
import {
|
|
18
|
+
defaultAppProjectDir,
|
|
19
19
|
resolveProjectDir,
|
|
20
20
|
loadAppConfig,
|
|
21
21
|
detectProjectProblems,
|
|
22
22
|
detectProjectWarnings,
|
|
23
23
|
buildArtifact,
|
|
24
|
+
appLinkedStateProfileKey,
|
|
24
25
|
readManifest,
|
|
25
26
|
readLinkedState,
|
|
26
27
|
writeLinkedState,
|
|
27
28
|
requireLinkedAppId,
|
|
28
29
|
scaffoldProject,
|
|
29
|
-
loadScaffoldCatalog,
|
|
30
30
|
findUnknownScreenshotScenarios,
|
|
31
31
|
inspectListingReadiness,
|
|
32
32
|
resolveListingScreenshots,
|
|
@@ -34,11 +34,25 @@ import {
|
|
|
34
34
|
collectSourceFiles,
|
|
35
35
|
resolveConfiguredAppSkills,
|
|
36
36
|
normalizeAppCapabilities,
|
|
37
|
+
normalizeAppToolBindings,
|
|
37
38
|
normalizeAppSkillManifestPath,
|
|
39
|
+
appRowFieldsFromManifest,
|
|
38
40
|
directDeploy,
|
|
39
41
|
pullAppSource,
|
|
40
42
|
} from '../runtime/app-platform.js';
|
|
43
|
+
import {
|
|
44
|
+
filterScaffoldCatalog,
|
|
45
|
+
loadScaffoldCatalog,
|
|
46
|
+
scaffoldRegistryLabel,
|
|
47
|
+
} from '../runtime/app-registry-scaffolds.js';
|
|
41
48
|
import { startAppDevServer } from '../runtime/app-dev-server.js';
|
|
49
|
+
import { captureDesktopHostOwnership } from '../runtime/app-dev-process-identity.js';
|
|
50
|
+
import {
|
|
51
|
+
discoverRegisteredAppProjects,
|
|
52
|
+
readAppDevRoots,
|
|
53
|
+
registerAppDevRoot,
|
|
54
|
+
removeAppDevRoot,
|
|
55
|
+
} from '../runtime/app-dev-roots.js';
|
|
42
56
|
import {
|
|
43
57
|
captureHarnessScreenshot,
|
|
44
58
|
closeAgentBrowserSession,
|
|
@@ -48,12 +62,22 @@ import {
|
|
|
48
62
|
import {
|
|
49
63
|
getAppDevSessionsFile,
|
|
50
64
|
heartbeatAppDevSession,
|
|
65
|
+
linkAppDevSessionTarget,
|
|
66
|
+
readAppDevSessions,
|
|
51
67
|
removeAppDevSession,
|
|
52
68
|
upsertAppDevSessions,
|
|
53
|
-
waitForAppDevSessionMountAcknowledgements,
|
|
54
|
-
waitForAppDevSessionRenderAcknowledgements,
|
|
55
69
|
} from '../runtime/app-dev-sessions.js';
|
|
56
|
-
import {
|
|
70
|
+
import {
|
|
71
|
+
releaseAppDevHostLock,
|
|
72
|
+
tryAcquireAppDevHostLock,
|
|
73
|
+
} from '../runtime/app-dev-host-lock.js';
|
|
74
|
+
import {
|
|
75
|
+
heartbeatAppDevConsumer,
|
|
76
|
+
hasAppDevConsumer,
|
|
77
|
+
readAppDevConsumers,
|
|
78
|
+
removeAppDevConsumer,
|
|
79
|
+
} from '../runtime/app-dev-consumers.js';
|
|
80
|
+
import { getAvailablePort, getAvailablePortPreferring } from '../runtime/ports.js';
|
|
57
81
|
import { getCliMode } from '../runtime/cli-mode.js';
|
|
58
82
|
import { composeStoreScreenshot } from '../runtime/store-screenshot.js';
|
|
59
83
|
import { httpRequest } from '../runtime/transport.js';
|
|
@@ -65,16 +89,212 @@ import {
|
|
|
65
89
|
toolConflictToError,
|
|
66
90
|
} from './helpers.js';
|
|
67
91
|
|
|
92
|
+
export { appRowFieldsFromManifest } from '../runtime/app-platform.js';
|
|
93
|
+
|
|
68
94
|
const DEFAULT_DEV_PORT = 5173;
|
|
69
95
|
const DEV_HEARTBEAT_INTERVAL_MS = 10_000;
|
|
70
|
-
const
|
|
96
|
+
const DEV_CONSUMER_HEARTBEAT_INTERVAL_MS = 3_000;
|
|
97
|
+
const DEV_CONSUMER_POLL_INTERVAL_MS = 5_000;
|
|
98
|
+
const SHARED_APP_DEV_HOST_KEY = '__registered_roots__';
|
|
99
|
+
|
|
100
|
+
function projectIsWithinRoot(projectDir, rootDir) {
|
|
101
|
+
const nested = relative(rootDir, projectDir);
|
|
102
|
+
return nested === '' || (nested !== '..' && !nested.startsWith(`..${sep}`) && !isAbsolute(nested));
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
function parseNotisAppVersion(value) {
|
|
106
|
+
const match = /^(0|[1-9]\d*)\.(0|[1-9]\d*)\.(0|[1-9]\d*)(?:-([0-9A-Za-z-]+(?:\.[0-9A-Za-z-]+)*))?(?:\+[0-9A-Za-z-]+(?:\.[0-9A-Za-z-]+)*)?$/.exec(
|
|
107
|
+
String(value || '').trim(),
|
|
108
|
+
);
|
|
109
|
+
if (!match) return null;
|
|
110
|
+
const prerelease = match[4] ? match[4].split('.') : [];
|
|
111
|
+
if (prerelease.some((identifier) => /^\d+$/.test(identifier) && identifier.length > 1 && identifier.startsWith('0'))) {
|
|
112
|
+
return null;
|
|
113
|
+
}
|
|
114
|
+
return {
|
|
115
|
+
major: match[1],
|
|
116
|
+
minor: match[2],
|
|
117
|
+
patch: match[3],
|
|
118
|
+
prerelease,
|
|
119
|
+
};
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
function compareNumericSemverIdentifiers(left, right) {
|
|
123
|
+
if (left.length !== right.length) return left.length > right.length ? 1 : -1;
|
|
124
|
+
return left === right ? 0 : left > right ? 1 : -1;
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
function compareNotisPrerelease(left, right) {
|
|
128
|
+
if (left.length === 0 || right.length === 0) {
|
|
129
|
+
return left.length === right.length ? 0 : left.length === 0 ? 1 : -1;
|
|
130
|
+
}
|
|
131
|
+
const length = Math.max(left.length, right.length);
|
|
132
|
+
for (let index = 0; index < length; index += 1) {
|
|
133
|
+
const leftIdentifier = left[index];
|
|
134
|
+
const rightIdentifier = right[index];
|
|
135
|
+
if (leftIdentifier === undefined || rightIdentifier === undefined) {
|
|
136
|
+
return leftIdentifier === rightIdentifier ? 0 : leftIdentifier === undefined ? -1 : 1;
|
|
137
|
+
}
|
|
138
|
+
if (leftIdentifier === rightIdentifier) continue;
|
|
139
|
+
const leftNumeric = /^\d+$/.test(leftIdentifier);
|
|
140
|
+
const rightNumeric = /^\d+$/.test(rightIdentifier);
|
|
141
|
+
if (leftNumeric && rightNumeric) {
|
|
142
|
+
return compareNumericSemverIdentifiers(leftIdentifier, rightIdentifier);
|
|
143
|
+
}
|
|
144
|
+
if (leftNumeric !== rightNumeric) return leftNumeric ? -1 : 1;
|
|
145
|
+
return leftIdentifier > rightIdentifier ? 1 : -1;
|
|
146
|
+
}
|
|
147
|
+
return 0;
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
export function compareNotisAppVersions(leftValue, rightValue) {
|
|
151
|
+
const left = parseNotisAppVersion(leftValue);
|
|
152
|
+
const right = parseNotisAppVersion(rightValue);
|
|
153
|
+
if (!left || !right) return null;
|
|
154
|
+
for (const key of ['major', 'minor', 'patch']) {
|
|
155
|
+
const comparison = compareNumericSemverIdentifiers(left[key], right[key]);
|
|
156
|
+
if (comparison !== 0) return comparison;
|
|
157
|
+
}
|
|
158
|
+
return compareNotisPrerelease(left.prerelease, right.prerelease);
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
function readLocalNotisAppVersion(projectDir) {
|
|
162
|
+
try {
|
|
163
|
+
const packageJson = JSON.parse(readFileSync(join(projectDir, 'package.json'), 'utf8'));
|
|
164
|
+
const version = String(packageJson.notisAppVersion || '').trim();
|
|
165
|
+
return parseNotisAppVersion(version) ? version : null;
|
|
166
|
+
} catch {
|
|
167
|
+
return null;
|
|
168
|
+
}
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
/**
|
|
172
|
+
* A CLI launch may add one root, but the shared host always serves the complete
|
|
173
|
+
* machine registry. Desktop sets skipRootRegistration because it is reconciling
|
|
174
|
+
* an already-registered snapshot; this must never narrow discovery to the one
|
|
175
|
+
* project path used to start the host.
|
|
176
|
+
*/
|
|
177
|
+
export function discoverAppDevLaunchProjects(rootDir, {
|
|
178
|
+
skipRootRegistration = false,
|
|
179
|
+
registerRoot = registerAppDevRoot,
|
|
180
|
+
discoverProjects = discoverRegisteredAppProjects,
|
|
181
|
+
} = {}) {
|
|
182
|
+
if (!skipRootRegistration) registerRoot(rootDir);
|
|
183
|
+
return discoverProjects();
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
/**
|
|
187
|
+
* Collapse duplicate local sources before they reach the shared loopback host.
|
|
188
|
+
* An explicitly registered root is more intentional than the implicit
|
|
189
|
+
* ~/.notis/apps root. If two equally intentional roots claim the same dev
|
|
190
|
+
* slug, omit that slug and keep serving every unrelated app.
|
|
191
|
+
*/
|
|
192
|
+
export function selectCanonicalDevApps(candidates, rootsRegistry) {
|
|
193
|
+
const roots = Array.isArray(rootsRegistry?.roots) ? rootsRegistry.roots : [];
|
|
194
|
+
const groups = new Map();
|
|
195
|
+
for (const candidate of candidates) {
|
|
196
|
+
groups.set(candidate.devSlug, [...(groups.get(candidate.devSlug) || []), candidate]);
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
const selected = [];
|
|
200
|
+
const warnings = [];
|
|
201
|
+
for (const [devSlug, group] of groups) {
|
|
202
|
+
if (group.length === 1) {
|
|
203
|
+
selected.push(group[0]);
|
|
204
|
+
continue;
|
|
205
|
+
}
|
|
206
|
+
const ranked = group.map((candidate) => {
|
|
207
|
+
const explicitRoots = roots.filter((root) => (
|
|
208
|
+
root?.implicit !== true
|
|
209
|
+
&& typeof root?.path === 'string'
|
|
210
|
+
&& projectIsWithinRoot(candidate.projectDir, root.path)
|
|
211
|
+
));
|
|
212
|
+
const newestRegistration = explicitRoots.reduce((latest, root) => {
|
|
213
|
+
const timestamp = Date.parse(root.registeredAt || '');
|
|
214
|
+
return Number.isFinite(timestamp) ? Math.max(latest, timestamp) : latest;
|
|
215
|
+
}, 0);
|
|
216
|
+
return {
|
|
217
|
+
candidate,
|
|
218
|
+
rank: explicitRoots.length > 0 ? 1 : 0,
|
|
219
|
+
newestRegistration,
|
|
220
|
+
};
|
|
221
|
+
}).sort((left, right) => (
|
|
222
|
+
right.rank - left.rank
|
|
223
|
+
|| right.newestRegistration - left.newestRegistration
|
|
224
|
+
));
|
|
225
|
+
const winner = ranked[0];
|
|
226
|
+
const runnerUp = ranked[1];
|
|
227
|
+
const unambiguous = winner.rank > runnerUp.rank
|
|
228
|
+
|| winner.newestRegistration > runnerUp.newestRegistration;
|
|
229
|
+
if (unambiguous) {
|
|
230
|
+
selected.push(winner.candidate);
|
|
231
|
+
warnings.push(
|
|
232
|
+
`Using ${winner.candidate.projectDir} for development slug "${devSlug}"; ignored duplicate source(s): ${ranked.slice(1).map(({ candidate }) => candidate.projectDir).join(', ')}.`,
|
|
233
|
+
);
|
|
234
|
+
continue;
|
|
235
|
+
}
|
|
236
|
+
warnings.push(
|
|
237
|
+
`Skipped ambiguous development slug "${devSlug}" because multiple equally ranked sources are registered: ${ranked.map(({ candidate }) => candidate.projectDir).join(', ')}. Remove a root or set a unique devSlug.`,
|
|
238
|
+
);
|
|
239
|
+
}
|
|
240
|
+
return { selected, warnings };
|
|
241
|
+
}
|
|
242
|
+
|
|
243
|
+
export function findSharedSourceBundleUrls(projectDirs, sessionsFilePath) {
|
|
244
|
+
const now = Date.now();
|
|
245
|
+
const sourceSessions = readAppDevSessions(sessionsFilePath).sessions
|
|
246
|
+
.filter((session) => {
|
|
247
|
+
if (session.sourceHost !== true) return false;
|
|
248
|
+
const heartbeatAt = Date.parse(session.lastHeartbeatAt || '');
|
|
249
|
+
if (!Number.isFinite(heartbeatAt) || now - heartbeatAt > 45_000) return false;
|
|
250
|
+
if (!Number.isInteger(session.hostPid) || session.hostPid <= 0) return false;
|
|
251
|
+
try {
|
|
252
|
+
process.kill(session.hostPid, 0);
|
|
253
|
+
return true;
|
|
254
|
+
} catch {
|
|
255
|
+
return false;
|
|
256
|
+
}
|
|
257
|
+
});
|
|
258
|
+
const groups = new Map();
|
|
259
|
+
for (const session of sourceSessions) {
|
|
260
|
+
const key = `${session.hostPid || 0}:${session.sessionId}`;
|
|
261
|
+
groups.set(key, [...(groups.get(key) || []), session]);
|
|
262
|
+
}
|
|
263
|
+
for (const sessions of groups.values()) {
|
|
264
|
+
const byProject = new Map(sessions.map((session) => [session.projectDir, session]));
|
|
265
|
+
const canonicalProjects = sessions.find((session) => (
|
|
266
|
+
Array.isArray(session.canonicalProjects)
|
|
267
|
+
))?.canonicalProjects;
|
|
268
|
+
const discoveredProjects = sessions.find((session) => (
|
|
269
|
+
Array.isArray(session.discoveredProjects)
|
|
270
|
+
))?.discoveredProjects;
|
|
271
|
+
const discoveryMatches = discoveredProjects
|
|
272
|
+
&& JSON.stringify([...discoveredProjects].sort()) === JSON.stringify([...projectDirs].sort());
|
|
273
|
+
if (
|
|
274
|
+
discoveryMatches
|
|
275
|
+
&& canonicalProjects
|
|
276
|
+
&& canonicalProjects.every((projectDir) => byProject.has(projectDir))
|
|
277
|
+
) {
|
|
278
|
+
return new Map(canonicalProjects.map((projectDir) => [
|
|
279
|
+
projectDir,
|
|
280
|
+
byProject.get(projectDir).bundleBaseUrl,
|
|
281
|
+
]));
|
|
282
|
+
}
|
|
283
|
+
if (!projectDirs.every((projectDir) => byProject.has(projectDir))) continue;
|
|
284
|
+
return new Map(projectDirs.map((projectDir) => [
|
|
285
|
+
projectDir,
|
|
286
|
+
byProject.get(projectDir).bundleBaseUrl,
|
|
287
|
+
]));
|
|
288
|
+
}
|
|
289
|
+
return null;
|
|
290
|
+
}
|
|
71
291
|
const ENSURE_DEV_APP_INSTALLATION_TOOL = 'LOCAL_NOTIS_ENSURE_DEV_APP_INSTALLATION';
|
|
72
292
|
const GET_APP_TOOL = 'LOCAL_NOTIS_GET_APP';
|
|
73
293
|
const LIST_APPS_TOOL = 'LOCAL_NOTIS_LIST_APPS';
|
|
74
294
|
const CREATE_APP_TOOL = 'LOCAL_NOTIS_CREATE_APP';
|
|
75
295
|
const DUPLICATE_APP_TOOL = 'LOCAL_NOTIS_DUPLICATE_APP';
|
|
76
296
|
const SAVE_APP_FILES_TOOL = 'LOCAL_NOTIS_SAVE_APP_FILES';
|
|
77
|
-
const APP_DEPLOY_TIMEOUT_MS =
|
|
297
|
+
export const APP_DEPLOY_TIMEOUT_MS = 600_000;
|
|
78
298
|
|
|
79
299
|
// ---------------------------------------------------------------------------
|
|
80
300
|
// Formatters
|
|
@@ -98,40 +318,12 @@ function scaffoldsTable(scaffolds) {
|
|
|
98
318
|
]);
|
|
99
319
|
}
|
|
100
320
|
|
|
101
|
-
function
|
|
102
|
-
return
|
|
103
|
-
}
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
return [projectDir];
|
|
108
|
-
}
|
|
109
|
-
|
|
110
|
-
const appsDir = join(projectDir, 'apps');
|
|
111
|
-
if (!existsSync(appsDir)) {
|
|
112
|
-
throw usageError(
|
|
113
|
-
`No notis.config.ts in ${projectDir}. Run from a Notis app project or a monorepo root with apps/<name>/notis.config.ts.`,
|
|
114
|
-
);
|
|
115
|
-
}
|
|
116
|
-
|
|
117
|
-
const entries = readdirSync(appsDir, { withFileTypes: true });
|
|
118
|
-
const candidates = [];
|
|
119
|
-
for (const entry of entries) {
|
|
120
|
-
if (!entry.isDirectory()) continue;
|
|
121
|
-
if (entry.name.startsWith('.') || entry.name.startsWith('_')) continue;
|
|
122
|
-
const dir = join(appsDir, entry.name);
|
|
123
|
-
if (hasNotisConfig(dir)) {
|
|
124
|
-
candidates.push(dir);
|
|
125
|
-
}
|
|
126
|
-
}
|
|
127
|
-
|
|
128
|
-
if (candidates.length === 0) {
|
|
129
|
-
throw usageError(
|
|
130
|
-
`Found apps/ in ${projectDir} but no apps/<name>/notis.config.ts inside it.`,
|
|
131
|
-
);
|
|
132
|
-
}
|
|
133
|
-
|
|
134
|
-
return candidates;
|
|
321
|
+
function appDevRootsTable(roots) {
|
|
322
|
+
return formatTable(roots, [
|
|
323
|
+
{ label: 'Folder', value: (root) => root.path },
|
|
324
|
+
{ label: 'Source', value: (root) => root.implicit ? 'default' : 'registered' },
|
|
325
|
+
{ label: 'Registered', value: (root) => root.registeredAt || '' },
|
|
326
|
+
]);
|
|
135
327
|
}
|
|
136
328
|
|
|
137
329
|
function decodeJwtSub(jwt) {
|
|
@@ -146,42 +338,11 @@ function decodeJwtSub(jwt) {
|
|
|
146
338
|
}
|
|
147
339
|
}
|
|
148
340
|
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
bundleId = null,
|
|
155
|
-
scheme = 'notis',
|
|
156
|
-
} = {},
|
|
157
|
-
) {
|
|
158
|
-
let command;
|
|
159
|
-
let args;
|
|
160
|
-
if (platform === 'darwin') {
|
|
161
|
-
command = 'open';
|
|
162
|
-
args = bundleId && scheme === 'notis'
|
|
163
|
-
? ['-b', bundleId, url]
|
|
164
|
-
: appName && scheme === 'notis'
|
|
165
|
-
? ['-a', appName, url]
|
|
166
|
-
: [url];
|
|
167
|
-
} else if (platform === 'win32') {
|
|
168
|
-
command = 'cmd';
|
|
169
|
-
args = ['/c', 'start', '', url];
|
|
170
|
-
} else {
|
|
171
|
-
command = 'xdg-open';
|
|
172
|
-
args = [url];
|
|
173
|
-
}
|
|
174
|
-
return { command, args };
|
|
175
|
-
}
|
|
176
|
-
|
|
177
|
-
function openInBrowser(url, options = {}) {
|
|
178
|
-
const { command, args } = developmentDesktopOpenCommand(url, options);
|
|
179
|
-
try {
|
|
180
|
-
const child = spawn(command, args, { stdio: 'ignore' });
|
|
181
|
-
child.on('error', () => {});
|
|
182
|
-
} catch {
|
|
183
|
-
// Non-fatal. The URL is printed in the CLI output.
|
|
184
|
-
}
|
|
341
|
+
function linkedStateProfileKey(runtime) {
|
|
342
|
+
return appLinkedStateProfileKey({
|
|
343
|
+
apiBase: runtime?.apiBase,
|
|
344
|
+
userId: decodeJwtSub(runtime?.jwt),
|
|
345
|
+
});
|
|
185
346
|
}
|
|
186
347
|
|
|
187
348
|
function slugify(value) {
|
|
@@ -192,8 +353,8 @@ function slugify(value) {
|
|
|
192
353
|
.replace(/(^-|-$)+/g, '');
|
|
193
354
|
}
|
|
194
355
|
|
|
195
|
-
function buildDevInstallSlug(
|
|
196
|
-
const base = slugify(
|
|
356
|
+
function buildDevInstallSlug(appConfig) {
|
|
357
|
+
const base = slugify(appConfig?.devSlug || appConfig?.name);
|
|
197
358
|
if (!base) {
|
|
198
359
|
return '';
|
|
199
360
|
}
|
|
@@ -226,61 +387,6 @@ function pickDefaultRouteSlug(manifest) {
|
|
|
226
387
|
return firstWithSlug ? firstWithSlug.slug : null;
|
|
227
388
|
}
|
|
228
389
|
|
|
229
|
-
const DESKTOP_DEEP_LINK_SCHEME_PATTERN = /^[a-z][a-z0-9-]*$/;
|
|
230
|
-
|
|
231
|
-
export function resolveDevelopmentDesktopScheme(env = process.env, worktreeRuntime = null) {
|
|
232
|
-
// Mirror the desktop app's own scheme resolution (electron main + forge config):
|
|
233
|
-
// local dev launches register `notis-dev`, while installed prod/beta builds claim
|
|
234
|
-
// `notis`. Hardcoding `notis` here is what made `apps dev` open the installed
|
|
235
|
-
// prod/beta app instead of the local dev app.
|
|
236
|
-
const scheme = (
|
|
237
|
-
worktreeRuntime?.desktop_deep_link_scheme
|
|
238
|
-
|| env.NOTIS_DESKTOP_DEEP_LINK_SCHEME
|
|
239
|
-
|| ''
|
|
240
|
-
).trim();
|
|
241
|
-
return DESKTOP_DEEP_LINK_SCHEME_PATTERN.test(scheme) ? scheme : 'notis';
|
|
242
|
-
}
|
|
243
|
-
|
|
244
|
-
export function resolveDevelopmentDesktopAppName(runtime = {}) {
|
|
245
|
-
const explicit = String(runtime.worktreeRuntime?.desktop_app_name || '').trim();
|
|
246
|
-
if (explicit) {
|
|
247
|
-
return explicit;
|
|
248
|
-
}
|
|
249
|
-
try {
|
|
250
|
-
return new URL(runtime.apiBase).hostname === 'api-beta.notis.ai'
|
|
251
|
-
? 'Notis Beta'
|
|
252
|
-
: 'Notis';
|
|
253
|
-
} catch {
|
|
254
|
-
return 'Notis';
|
|
255
|
-
}
|
|
256
|
-
}
|
|
257
|
-
|
|
258
|
-
export function resolveDevelopmentDesktopBundleId(runtime = {}) {
|
|
259
|
-
return resolveDevelopmentDesktopAppName(runtime) === 'Notis Beta'
|
|
260
|
-
? 'ai.notis.desktop.beta'
|
|
261
|
-
: 'ai.notis.desktop';
|
|
262
|
-
}
|
|
263
|
-
|
|
264
|
-
export function buildDevelopmentDesktopUrl(appHref = null, scheme = 'notis') {
|
|
265
|
-
const route = String(appHref || '/store').replace(/^\/+/, '');
|
|
266
|
-
const normalizedScheme = DESKTOP_DEEP_LINK_SCHEME_PATTERN.test(scheme) ? scheme : 'notis';
|
|
267
|
-
return `${normalizedScheme}://${route || 'store'}`;
|
|
268
|
-
}
|
|
269
|
-
|
|
270
|
-
export function buildMountedDevelopmentDesktopUrl(appHref, scheme, sessionId) {
|
|
271
|
-
const url = new URL(buildDevelopmentDesktopUrl(appHref, scheme));
|
|
272
|
-
url.searchParams.set('notis_dev_session', sessionId);
|
|
273
|
-
return url.toString();
|
|
274
|
-
}
|
|
275
|
-
|
|
276
|
-
export function shouldOpenDevelopmentTab(options = {}) {
|
|
277
|
-
// Commander stores the negatable `--no-open` flag as `options.open === false`;
|
|
278
|
-
// it never sets `options.noOpen`. Reading the non-existent `noOpen` key meant
|
|
279
|
-
// `--no-open` was silently ignored and `apps dev` always ran `open notis://…`,
|
|
280
|
-
// which macOS routes to the installed prod/beta desktop app.
|
|
281
|
-
return options.open !== false;
|
|
282
|
-
}
|
|
283
|
-
|
|
284
390
|
export function buildDevelopmentAppHref({
|
|
285
391
|
appSlug,
|
|
286
392
|
appId,
|
|
@@ -374,13 +480,6 @@ export function screenshotIndexByRouteSlug(manifest) {
|
|
|
374
480
|
return new Map(routes.map((route, index) => [route.slug, index + 1]));
|
|
375
481
|
}
|
|
376
482
|
|
|
377
|
-
export function appRowFieldsFromManifest(manifest) {
|
|
378
|
-
const app = manifest?.app && typeof manifest.app === 'object' ? manifest.app : {};
|
|
379
|
-
return {
|
|
380
|
-
accent: app.accent ?? null,
|
|
381
|
-
};
|
|
382
|
-
}
|
|
383
|
-
|
|
384
483
|
export function screenshotExitCode(failedCount) {
|
|
385
484
|
return failedCount === 0 ? EXIT_CODES.ok : EXIT_CODES.unexpected;
|
|
386
485
|
}
|
|
@@ -534,7 +633,7 @@ function renderVerifyReport({ summary, results, noBrowser }) {
|
|
|
534
633
|
return lines.join('\n');
|
|
535
634
|
}
|
|
536
635
|
|
|
537
|
-
function buildManifestForDev(appConfig) {
|
|
636
|
+
function buildManifestForDev(appConfig, projectDir) {
|
|
538
637
|
const routes = Array.isArray(appConfig.routes) ? appConfig.routes : [];
|
|
539
638
|
return {
|
|
540
639
|
version: 1,
|
|
@@ -543,6 +642,7 @@ function buildManifestForDev(appConfig) {
|
|
|
543
642
|
name: appConfig.name,
|
|
544
643
|
description: appConfig.description || null,
|
|
545
644
|
icon: appConfig.icon || null,
|
|
645
|
+
release_version: readLocalNotisAppVersion(projectDir),
|
|
546
646
|
},
|
|
547
647
|
routes: routes.map((route) => ({
|
|
548
648
|
path: route.path,
|
|
@@ -551,6 +651,7 @@ function buildManifestForDev(appConfig) {
|
|
|
551
651
|
icon: route.icon || null,
|
|
552
652
|
parentSlug: route.parentSlug || null,
|
|
553
653
|
default: route.default || false,
|
|
654
|
+
resourceDeepLinks: route.resourceDeepLinks === true,
|
|
554
655
|
export_name: route.exportName || route.export_name,
|
|
555
656
|
collection: route.collection || null,
|
|
556
657
|
})),
|
|
@@ -561,6 +662,7 @@ function buildManifestForDev(appConfig) {
|
|
|
561
662
|
databases: appConfig.databases || [],
|
|
562
663
|
capabilities: normalizeAppCapabilities(appConfig.capabilities),
|
|
563
664
|
tools: appConfig.tools || [],
|
|
665
|
+
tool_bindings: normalizeAppToolBindings(appConfig.toolBindings),
|
|
564
666
|
skills: (appConfig.skills || []).map((skill) => ({
|
|
565
667
|
key: skill.key,
|
|
566
668
|
path: normalizeAppSkillManifestPath(skill.path),
|
|
@@ -579,7 +681,7 @@ export function buildEnsureDevInstallArguments({
|
|
|
579
681
|
useInstalledDatabases = false,
|
|
580
682
|
approvedCapabilities = null,
|
|
581
683
|
}) {
|
|
582
|
-
const devSlug = buildDevInstallSlug(appConfig
|
|
684
|
+
const devSlug = buildDevInstallSlug(appConfig);
|
|
583
685
|
const arguments_ = {
|
|
584
686
|
dev_slug: devSlug,
|
|
585
687
|
name: appConfig.name,
|
|
@@ -589,8 +691,12 @@ export function buildEnsureDevInstallArguments({
|
|
|
589
691
|
if (linkedState?.dev_app_id) {
|
|
590
692
|
arguments_.app_id = linkedState.dev_app_id;
|
|
591
693
|
}
|
|
592
|
-
|
|
593
|
-
|
|
694
|
+
if (useInstalledDatabases && linkedState?.app_id) {
|
|
695
|
+
arguments_.installed_app_id = linkedState.app_id;
|
|
696
|
+
}
|
|
697
|
+
// Sent on every ensure call, including as `false`, so a `--scratch` session
|
|
698
|
+
// puts the app on its own dev copies and the next plain session puts it back
|
|
699
|
+
// on the installed app's databases.
|
|
594
700
|
arguments_.use_installed_databases = Boolean(useInstalledDatabases);
|
|
595
701
|
if (Array.isArray(approvedCapabilities) && approvedCapabilities.length > 0) {
|
|
596
702
|
arguments_.approved_capabilities = approvedCapabilities;
|
|
@@ -614,15 +720,20 @@ export async function resolveCloudShellConsent({
|
|
|
614
720
|
projectDir,
|
|
615
721
|
grantCloudShell = false,
|
|
616
722
|
logger = console,
|
|
723
|
+
profileKey = null,
|
|
617
724
|
}) {
|
|
618
725
|
const capabilities = normalizeAppCapabilities(appConfig.capabilities);
|
|
619
726
|
if (capabilities.cloudComputer !== 'shell') {
|
|
620
727
|
return null;
|
|
621
728
|
}
|
|
622
729
|
const approved = ['cloud_computer_read', 'cloud_computer_shell'];
|
|
623
|
-
const linkedState = readLinkedState(projectDir) || {};
|
|
730
|
+
const linkedState = readLinkedState(projectDir, profileKey) || {};
|
|
624
731
|
const recordDecision = (decision) => {
|
|
625
|
-
writeLinkedState(
|
|
732
|
+
writeLinkedState(
|
|
733
|
+
projectDir,
|
|
734
|
+
{ ...linkedState, [CLOUD_SHELL_CONSENT_KEY]: decision },
|
|
735
|
+
profileKey,
|
|
736
|
+
);
|
|
626
737
|
};
|
|
627
738
|
|
|
628
739
|
if (grantCloudShell) {
|
|
@@ -675,21 +786,22 @@ export async function ensureDevInstall({
|
|
|
675
786
|
if (!appConfig.name) {
|
|
676
787
|
throw usageError(`notis.config.ts in ${projectDir} must define a non-empty name.`);
|
|
677
788
|
}
|
|
678
|
-
const devSlug = buildDevInstallSlug(appConfig
|
|
789
|
+
const devSlug = buildDevInstallSlug(appConfig);
|
|
679
790
|
if (!devSlug) {
|
|
680
|
-
throw usageError(`notis.config.ts name in ${projectDir} must slugify to a non-empty value.`);
|
|
791
|
+
throw usageError(`notis.config.ts devSlug or name in ${projectDir} must slugify to a non-empty value.`);
|
|
681
792
|
}
|
|
682
793
|
|
|
683
|
-
const manifest = buildManifestForDev(appConfig);
|
|
794
|
+
const manifest = buildManifestForDev(appConfig, projectDir);
|
|
684
795
|
const skills = resolveConfiguredAppSkills(appConfig, projectDir);
|
|
685
|
-
|
|
796
|
+
const profileKey = linkedStateProfileKey(ctx.runtime);
|
|
797
|
+
let linkedState = readLinkedState(projectDir, profileKey);
|
|
686
798
|
let linkedApp = null;
|
|
687
799
|
if (linkedState?.dev_app_id) {
|
|
688
800
|
const devApp = await getAccessibleApp(ctx.runtime, linkedState.dev_app_id, runTool);
|
|
689
801
|
if (!devApp || devApp.manifest?.is_dev !== true) {
|
|
690
802
|
const { dev_app_id: _devAppId, dev_linked_at: _devLinkedAt, ...rest } = linkedState;
|
|
691
803
|
linkedState = rest;
|
|
692
|
-
writeLinkedState(projectDir, linkedState);
|
|
804
|
+
writeLinkedState(projectDir, linkedState, profileKey);
|
|
693
805
|
}
|
|
694
806
|
}
|
|
695
807
|
if (linkedState?.app_id) {
|
|
@@ -704,25 +816,102 @@ export async function ensureDevInstall({
|
|
|
704
816
|
dev_linked_at: linkedState.dev_linked_at || linkedState.linked_at || new Date().toISOString(),
|
|
705
817
|
} : {}),
|
|
706
818
|
};
|
|
707
|
-
writeLinkedState(projectDir, linkedState);
|
|
819
|
+
writeLinkedState(projectDir, linkedState, profileKey);
|
|
708
820
|
linkedApp = null;
|
|
709
821
|
}
|
|
710
822
|
}
|
|
823
|
+
// A checkout can be shared by Beta and a source-workspace desktop. Its
|
|
824
|
+
// installed app link is valid for the Beta account but intentionally
|
|
825
|
+
// inaccessible to the worktree's test user. Preserve that durable link for
|
|
826
|
+
// Beta, while omitting it from this run so the local dev app uses its own
|
|
827
|
+
// resources instead of sending an unauthorized installed_app_id to ensure.
|
|
828
|
+
if (!linkedState?.app_id) {
|
|
829
|
+
const installedResolution = await findInstalledLinkCandidatesForLegacyDevState(
|
|
830
|
+
ctx.runtime,
|
|
831
|
+
devSlug,
|
|
832
|
+
linkedState?.dev_app_id,
|
|
833
|
+
projectDir,
|
|
834
|
+
profileKey,
|
|
835
|
+
runTool,
|
|
836
|
+
);
|
|
837
|
+
const installedCandidates = installedResolution.candidates;
|
|
838
|
+
if (installedCandidates.length > 1) {
|
|
839
|
+
if (installedResolution.source === 'persisted-project-link') {
|
|
840
|
+
throw usageError(
|
|
841
|
+
`Multiple accessible installed apps are persisted for ${projectDir}. `
|
|
842
|
+
+ `Run \`notis apps link <app-id> ${projectDir}\` to choose one for this environment.`,
|
|
843
|
+
);
|
|
844
|
+
}
|
|
845
|
+
throw usageError(
|
|
846
|
+
`Multiple accessible installed apps use the exact slug "${devSlug.slice(0, -4)}". `
|
|
847
|
+
+ `Run \`notis apps link <app-id> ${projectDir}\` to choose one.`,
|
|
848
|
+
);
|
|
849
|
+
}
|
|
850
|
+
if (installedCandidates.length === 1) {
|
|
851
|
+
const now = new Date().toISOString();
|
|
852
|
+
linkedApp = installedCandidates[0];
|
|
853
|
+
linkedState = {
|
|
854
|
+
...(linkedState || {}),
|
|
855
|
+
app_id: linkedApp.app_id || linkedApp.id,
|
|
856
|
+
linked_at: now,
|
|
857
|
+
auto_linked_at: now,
|
|
858
|
+
};
|
|
859
|
+
writeLinkedState(projectDir, linkedState, profileKey);
|
|
860
|
+
}
|
|
861
|
+
}
|
|
862
|
+
const runtimeLinkedState = linkedState?.app_id && !linkedApp
|
|
863
|
+
? Object.fromEntries(
|
|
864
|
+
Object.entries(linkedState).filter(([key]) => ![
|
|
865
|
+
'app_id', 'linked_at', 'deployed_at', 'version',
|
|
866
|
+
].includes(key)),
|
|
867
|
+
)
|
|
868
|
+
: linkedState;
|
|
869
|
+
const localReleaseVersion = manifest.app.release_version || null;
|
|
870
|
+
const installedReleaseVersion = linkedApp?.manifest?.app?.release_version || '0.0.0';
|
|
871
|
+
const mountEligible = !linkedApp
|
|
872
|
+
|| compareNotisAppVersions(localReleaseVersion, installedReleaseVersion) === 1;
|
|
711
873
|
const ensureArguments = buildEnsureDevInstallArguments({
|
|
712
874
|
appConfig,
|
|
713
875
|
manifest,
|
|
714
|
-
linkedState,
|
|
876
|
+
linkedState: runtimeLinkedState,
|
|
715
877
|
skills,
|
|
716
878
|
useInstalledDatabases,
|
|
717
879
|
approvedCapabilities,
|
|
718
880
|
});
|
|
719
|
-
|
|
720
|
-
|
|
721
|
-
|
|
722
|
-
|
|
723
|
-
|
|
724
|
-
|
|
725
|
-
|
|
881
|
+
let ensureResult;
|
|
882
|
+
try {
|
|
883
|
+
ensureResult = await runTool({
|
|
884
|
+
runtime: ctx.runtime,
|
|
885
|
+
toolName: ENSURE_DEV_APP_INSTALLATION_TOOL,
|
|
886
|
+
arguments_: ensureArguments,
|
|
887
|
+
mutating: true,
|
|
888
|
+
idempotencyKey,
|
|
889
|
+
});
|
|
890
|
+
} catch (error) {
|
|
891
|
+
const backendCode = error?.details?.error?.code;
|
|
892
|
+
if (linkedState?.dev_app_id && backendCode === 'dev_app_slug_conflict') {
|
|
893
|
+
// The authenticated environment can outlive an older local API profile.
|
|
894
|
+
// If that profile remembered a different dev row while this user already
|
|
895
|
+
// owns the exact canonical dev slug, the backend refuses a duplicate.
|
|
896
|
+
// Retry without the stale row id so the backend deterministically adopts
|
|
897
|
+
// the unique slug owner; preserve the installed target and every grant.
|
|
898
|
+
const staleDevAppId = linkedState.dev_app_id;
|
|
899
|
+
const { dev_app_id: _devAppId, dev_linked_at: _devLinkedAt, ...rest } = linkedState;
|
|
900
|
+
linkedState = rest;
|
|
901
|
+
writeLinkedState(projectDir, linkedState, profileKey);
|
|
902
|
+
const retryArguments = { ...ensureArguments };
|
|
903
|
+
delete retryArguments.app_id;
|
|
904
|
+
ensureResult = await runTool({
|
|
905
|
+
runtime: ctx.runtime,
|
|
906
|
+
toolName: ENSURE_DEV_APP_INSTALLATION_TOOL,
|
|
907
|
+
arguments_: retryArguments,
|
|
908
|
+
mutating: true,
|
|
909
|
+
idempotencyKey: `${idempotencyKey}:adopt-slug-owner:${staleDevAppId}`,
|
|
910
|
+
});
|
|
911
|
+
} else {
|
|
912
|
+
throw error;
|
|
913
|
+
}
|
|
914
|
+
}
|
|
726
915
|
const appId = ensureResult.payload.app_id;
|
|
727
916
|
if (!appId) {
|
|
728
917
|
throw usageError('Dev installation tool did not return an app_id.');
|
|
@@ -733,7 +922,7 @@ export async function ensureDevInstall({
|
|
|
733
922
|
...(linkedState || {}),
|
|
734
923
|
dev_app_id: appId,
|
|
735
924
|
dev_linked_at: linkedState?.dev_linked_at || now,
|
|
736
|
-
});
|
|
925
|
+
}, profileKey);
|
|
737
926
|
}
|
|
738
927
|
return {
|
|
739
928
|
slug: ensureResult.payload.slug || devSlug,
|
|
@@ -743,9 +932,12 @@ export async function ensureDevInstall({
|
|
|
743
932
|
projectDir,
|
|
744
933
|
manifest,
|
|
745
934
|
created: ensureResult.payload.created || false,
|
|
746
|
-
linkedAppId:
|
|
747
|
-
targetAppId:
|
|
935
|
+
linkedAppId: runtimeLinkedState?.app_id || null,
|
|
936
|
+
targetAppId: runtimeLinkedState?.app_id || null,
|
|
748
937
|
targetAppSlug: linkedApp?.slug || null,
|
|
938
|
+
localReleaseVersion,
|
|
939
|
+
installedReleaseVersion: linkedApp ? installedReleaseVersion : null,
|
|
940
|
+
mountEligible,
|
|
749
941
|
databaseMaterialization: ensureResult.payload.database_materialization || { created: [], unresolved: [] },
|
|
750
942
|
liveData: ensureResult.payload.live_data || null,
|
|
751
943
|
};
|
|
@@ -772,6 +964,16 @@ function liveDataWarnings(apps) {
|
|
|
772
964
|
.map((app) => `${app.name}: ${app.liveData.warning}`);
|
|
773
965
|
}
|
|
774
966
|
|
|
967
|
+
function versionPrecedenceWarnings(apps) {
|
|
968
|
+
return apps
|
|
969
|
+
.filter((app) => app.targetAppId && app.mountEligible === false)
|
|
970
|
+
.map((app) => {
|
|
971
|
+
const localVersion = app.localReleaseVersion || 'missing or invalid';
|
|
972
|
+
const pullCommand = `npx --package @notis_ai/cli@latest -- notis apps pull ${app.targetAppId} ${JSON.stringify(app.projectDir)} --force`;
|
|
973
|
+
return `${app.name}: local version ${localVersion} is not strictly newer than installed version ${app.installedReleaseVersion}; Workspace keeps serving the online bundle. Preserve any local edits, pull latest with \`${pullCommand}\`, then bump package.json notisAppVersion before development.`;
|
|
974
|
+
});
|
|
975
|
+
}
|
|
976
|
+
|
|
775
977
|
async function getAccessibleApp(runtime, appId, runTool = runToolCommand) {
|
|
776
978
|
const result = await runTool({
|
|
777
979
|
runtime,
|
|
@@ -795,6 +997,63 @@ async function getAccessibleApp(runtime, appId, runTool = runToolCommand) {
|
|
|
795
997
|
throw usageError(`Could not verify access to app ${appId}${message ? `: ${message}` : '.'}`);
|
|
796
998
|
}
|
|
797
999
|
|
|
1000
|
+
async function findInstalledLinkCandidatesForLegacyDevState(
|
|
1001
|
+
runtime,
|
|
1002
|
+
devSlug,
|
|
1003
|
+
devAppId,
|
|
1004
|
+
projectDir,
|
|
1005
|
+
profileKey,
|
|
1006
|
+
runTool = runToolCommand,
|
|
1007
|
+
) {
|
|
1008
|
+
const installedSlug = devSlug.endsWith('-dev') ? devSlug.slice(0, -4) : '';
|
|
1009
|
+
if (!installedSlug) return { candidates: [], source: null };
|
|
1010
|
+
|
|
1011
|
+
const result = await runTool({
|
|
1012
|
+
runtime,
|
|
1013
|
+
toolName: LIST_APPS_TOOL,
|
|
1014
|
+
});
|
|
1015
|
+
const accessibleInstalledApps = (result.payload?.apps || []).filter((app) => (
|
|
1016
|
+
(app?.app_id || app?.id)
|
|
1017
|
+
&& (app.app_id || app.id) !== devAppId
|
|
1018
|
+
&& app?.manifest?.is_dev !== true
|
|
1019
|
+
));
|
|
1020
|
+
|
|
1021
|
+
// A project-local app id is stronger evidence than a slug. Older CLI runs
|
|
1022
|
+
// could persist that explicit link under another API/user profile before
|
|
1023
|
+
// the machine-local discovery host mounted the same source here. Migrate it
|
|
1024
|
+
// only when exactly one such id is accessible in the current environment;
|
|
1025
|
+
// multiple accessible ids remain ambiguous and fail closed below.
|
|
1026
|
+
const rawState = readLinkedState(projectDir) || {};
|
|
1027
|
+
const persistedAppIds = new Set();
|
|
1028
|
+
if (rawState.app_id) persistedAppIds.add(rawState.app_id);
|
|
1029
|
+
const profiles = rawState.profiles && typeof rawState.profiles === 'object'
|
|
1030
|
+
? rawState.profiles
|
|
1031
|
+
: {};
|
|
1032
|
+
for (const [storedProfileKey, state] of Object.entries(profiles)) {
|
|
1033
|
+
if (storedProfileKey === profileKey || !state || typeof state !== 'object') continue;
|
|
1034
|
+
if (state.app_id) persistedAppIds.add(state.app_id);
|
|
1035
|
+
}
|
|
1036
|
+
const persistedCandidates = accessibleInstalledApps.filter((app) => (
|
|
1037
|
+
persistedAppIds.has(app.app_id || app.id)
|
|
1038
|
+
));
|
|
1039
|
+
if (persistedCandidates.length) {
|
|
1040
|
+
return { candidates: persistedCandidates, source: 'persisted-project-link' };
|
|
1041
|
+
}
|
|
1042
|
+
|
|
1043
|
+
// The database row slug can change when a Store app is installed or an old
|
|
1044
|
+
// development app is promoted. The shipped source manifest keeps the
|
|
1045
|
+
// canonical app slug, so accept either exact field. Display names are never
|
|
1046
|
+
// considered.
|
|
1047
|
+
const exactSlugCandidates = accessibleInstalledApps.filter((app) => {
|
|
1048
|
+
const canonicalSlugs = new Set([
|
|
1049
|
+
app?.slug,
|
|
1050
|
+
app?.manifest?.app?.slug,
|
|
1051
|
+
].map(slugify).filter(Boolean));
|
|
1052
|
+
return canonicalSlugs.has(installedSlug);
|
|
1053
|
+
});
|
|
1054
|
+
return { candidates: exactSlugCandidates, source: 'exact-slug' };
|
|
1055
|
+
}
|
|
1056
|
+
|
|
798
1057
|
export async function assertDirectDeployAccess(runtime, appId, runTool = runToolCommand) {
|
|
799
1058
|
const app = await getAccessibleApp(runtime, appId, runTool);
|
|
800
1059
|
if (!app) {
|
|
@@ -803,6 +1062,14 @@ export async function assertDirectDeployAccess(runtime, appId, runTool = runTool
|
|
|
803
1062
|
if (app.apps_access?.has_access !== true) {
|
|
804
1063
|
throw usageError('Notis Apps require a PRO+ or ULTRA plan after your trial.');
|
|
805
1064
|
}
|
|
1065
|
+
if (app.can_edit !== true) {
|
|
1066
|
+
throw usageError(`Direct deploy requires edit access to app ${appId}.`);
|
|
1067
|
+
}
|
|
1068
|
+
if (app.manifest?.is_dev === true) {
|
|
1069
|
+
throw usageError(
|
|
1070
|
+
'A development app cannot be deployed directly. Retry without --direct so first deploy can promote it safely.',
|
|
1071
|
+
);
|
|
1072
|
+
}
|
|
806
1073
|
return app;
|
|
807
1074
|
}
|
|
808
1075
|
|
|
@@ -839,10 +1106,12 @@ async function appsListHandler(ctx) {
|
|
|
839
1106
|
}
|
|
840
1107
|
|
|
841
1108
|
async function appsInitHandler(ctx) {
|
|
842
|
-
const projectDir =
|
|
1109
|
+
const projectDir = ctx.args.dir
|
|
1110
|
+
? resolveProjectDir(ctx.args.dir)
|
|
1111
|
+
: defaultAppProjectDir(slugify(ctx.args.name));
|
|
843
1112
|
const fromSlug = ctx.options.from || null;
|
|
844
1113
|
|
|
845
|
-
scaffoldProject({ projectDir, appName: ctx.args.name, fromSlug });
|
|
1114
|
+
await scaffoldProject({ projectDir, appName: ctx.args.name, fromSlug });
|
|
846
1115
|
|
|
847
1116
|
return ctx.output.emitSuccess({
|
|
848
1117
|
command: ctx.spec.command_path.join(' '),
|
|
@@ -858,14 +1127,20 @@ async function appsInitHandler(ctx) {
|
|
|
858
1127
|
}
|
|
859
1128
|
|
|
860
1129
|
async function appsScaffoldsListHandler(ctx) {
|
|
861
|
-
const
|
|
1130
|
+
const searchTerm = ctx.options.search || null;
|
|
1131
|
+
const catalog = await loadScaffoldCatalog();
|
|
1132
|
+
const scaffolds = filterScaffoldCatalog(catalog, searchTerm);
|
|
1133
|
+
const registry = scaffoldRegistryLabel();
|
|
1134
|
+
const emptyMessage = searchTerm
|
|
1135
|
+
? `No published scaffolds match "${searchTerm}" (${catalog.length} available; run without --search to see all).`
|
|
1136
|
+
: `No published scaffolds found in ${registry}.`;
|
|
862
1137
|
return ctx.output.emitSuccess({
|
|
863
1138
|
command: ctx.spec.command_path.join(' '),
|
|
864
|
-
data: { scaffolds },
|
|
1139
|
+
data: { scaffolds, registry, search: searchTerm },
|
|
865
1140
|
humanSummary: scaffolds.length
|
|
866
|
-
? `Found ${scaffolds.length}
|
|
867
|
-
:
|
|
868
|
-
renderHuman: () => (scaffolds.length ? scaffoldsTable(scaffolds) :
|
|
1141
|
+
? `Found ${scaffolds.length} published scaffolds in ${registry}`
|
|
1142
|
+
: emptyMessage,
|
|
1143
|
+
renderHuman: () => (scaffolds.length ? scaffoldsTable(scaffolds) : emptyMessage),
|
|
869
1144
|
});
|
|
870
1145
|
}
|
|
871
1146
|
|
|
@@ -895,7 +1170,7 @@ async function appsCreateHandler(ctx) {
|
|
|
895
1170
|
writeLinkedState(projectDir, {
|
|
896
1171
|
app_id: app.id,
|
|
897
1172
|
linked_at: new Date().toISOString(),
|
|
898
|
-
});
|
|
1173
|
+
}, linkedStateProfileKey(ctx.runtime));
|
|
899
1174
|
}
|
|
900
1175
|
|
|
901
1176
|
return ctx.output.emitSuccess({
|
|
@@ -918,7 +1193,94 @@ async function appsCreateHandler(ctx) {
|
|
|
918
1193
|
|
|
919
1194
|
async function appsDevHandler(ctx) {
|
|
920
1195
|
const rootDir = resolveProjectDir(ctx.args.dir || '.');
|
|
921
|
-
const
|
|
1196
|
+
const skipRootRegistration = process.env.NOTIS_APPS_DEV_ALL_REGISTERED_ROOTS === '1';
|
|
1197
|
+
let appDirs = discoverAppDevLaunchProjects(rootDir, { skipRootRegistration });
|
|
1198
|
+
const discoveredAppDirs = [...appDirs];
|
|
1199
|
+
const sessionsFilePath = getAppDevSessionsFile();
|
|
1200
|
+
let sharedBundleBaseUrls = null;
|
|
1201
|
+
const sharedBundlesRaw = process.env.NOTIS_APPS_DEV_SHARED_BUNDLE_URLS;
|
|
1202
|
+
if (sharedBundlesRaw) {
|
|
1203
|
+
let parsed;
|
|
1204
|
+
try {
|
|
1205
|
+
parsed = JSON.parse(sharedBundlesRaw);
|
|
1206
|
+
} catch {
|
|
1207
|
+
throw usageError('The shared app development bundle map is invalid.');
|
|
1208
|
+
}
|
|
1209
|
+
if (!parsed || typeof parsed !== 'object' || Array.isArray(parsed)) {
|
|
1210
|
+
throw usageError('The shared app development bundle map must be an object.');
|
|
1211
|
+
}
|
|
1212
|
+
const sharedProjectDirs = Object.keys(parsed);
|
|
1213
|
+
const discoveredProjects = new Set(appDirs);
|
|
1214
|
+
for (const projectDir of sharedProjectDirs) {
|
|
1215
|
+
if (!discoveredProjects.has(projectDir)) {
|
|
1216
|
+
throw usageError(`The shared bundle map references an unregistered app: ${projectDir}.`);
|
|
1217
|
+
}
|
|
1218
|
+
}
|
|
1219
|
+
appDirs = sharedProjectDirs;
|
|
1220
|
+
sharedBundleBaseUrls = new Map();
|
|
1221
|
+
for (const projectDir of appDirs) {
|
|
1222
|
+
const value = parsed[projectDir];
|
|
1223
|
+
let url;
|
|
1224
|
+
try {
|
|
1225
|
+
url = new URL(String(value || ''));
|
|
1226
|
+
} catch {
|
|
1227
|
+
throw usageError(`The shared bundle URL is missing or invalid for ${projectDir}.`);
|
|
1228
|
+
}
|
|
1229
|
+
if (
|
|
1230
|
+
url.protocol !== 'http:'
|
|
1231
|
+
|| !['127.0.0.1', 'localhost', '::1', '[::1]'].includes(url.hostname)
|
|
1232
|
+
) {
|
|
1233
|
+
throw usageError(`The shared bundle URL must use loopback HTTP for ${projectDir}.`);
|
|
1234
|
+
}
|
|
1235
|
+
sharedBundleBaseUrls.set(projectDir, url.toString().replace(/\/$/, ''));
|
|
1236
|
+
}
|
|
1237
|
+
}
|
|
1238
|
+
|
|
1239
|
+
if (appDirs.length === 0) {
|
|
1240
|
+
return ctx.output.emitSuccess({
|
|
1241
|
+
command: ctx.spec.command_path.join(' '),
|
|
1242
|
+
data: {
|
|
1243
|
+
registered_root: skipRootRegistration ? null : rootDir,
|
|
1244
|
+
apps: [],
|
|
1245
|
+
watching_on_desktop_launch: true,
|
|
1246
|
+
},
|
|
1247
|
+
humanSummary: [
|
|
1248
|
+
skipRootRegistration
|
|
1249
|
+
? 'No registered Notis apps are present yet.'
|
|
1250
|
+
: `Registered app development root: ${rootDir}`,
|
|
1251
|
+
'No Notis apps are present yet. A running Notis Desktop will discover and mount apps created here automatically.',
|
|
1252
|
+
].join('\n'),
|
|
1253
|
+
hints: [
|
|
1254
|
+
{ command: 'notis apps roots list', reason: 'Show every persistent development root' },
|
|
1255
|
+
],
|
|
1256
|
+
meta: { mutating: true },
|
|
1257
|
+
});
|
|
1258
|
+
}
|
|
1259
|
+
|
|
1260
|
+
if (!sharedBundleBaseUrls) {
|
|
1261
|
+
sharedBundleBaseUrls = findSharedSourceBundleUrls(appDirs, sessionsFilePath);
|
|
1262
|
+
}
|
|
1263
|
+
let sourceHostLock = null;
|
|
1264
|
+
if (
|
|
1265
|
+
!sharedBundleBaseUrls
|
|
1266
|
+
&& process.env.NOTIS_APPS_DEV_HOST_LOCK_HELD !== '1'
|
|
1267
|
+
) {
|
|
1268
|
+
sourceHostLock = tryAcquireAppDevHostLock({
|
|
1269
|
+
identity: '__mac_user__',
|
|
1270
|
+
apiBase: 'loopback-source',
|
|
1271
|
+
projectDir: SHARED_APP_DEV_HOST_KEY,
|
|
1272
|
+
});
|
|
1273
|
+
if (!sourceHostLock) {
|
|
1274
|
+
const startedAt = Date.now();
|
|
1275
|
+
while (!sharedBundleBaseUrls && Date.now() - startedAt < 45_000) {
|
|
1276
|
+
await new Promise((resolvePromise) => setTimeout(resolvePromise, 100));
|
|
1277
|
+
sharedBundleBaseUrls = findSharedSourceBundleUrls(appDirs, sessionsFilePath);
|
|
1278
|
+
}
|
|
1279
|
+
if (!sharedBundleBaseUrls) {
|
|
1280
|
+
throw usageError('The shared app development host is busy but did not become ready. Retry after checking ~/.notis/app-dev-host.log.');
|
|
1281
|
+
}
|
|
1282
|
+
}
|
|
1283
|
+
}
|
|
922
1284
|
|
|
923
1285
|
for (const projectDir of appDirs) {
|
|
924
1286
|
const problems = detectProjectProblems(projectDir);
|
|
@@ -927,9 +1289,19 @@ async function appsDevHandler(ctx) {
|
|
|
927
1289
|
}
|
|
928
1290
|
}
|
|
929
1291
|
|
|
930
|
-
|
|
931
|
-
if (
|
|
932
|
-
|
|
1292
|
+
let port = null;
|
|
1293
|
+
if (sharedBundleBaseUrls) {
|
|
1294
|
+
// The source host owns the only loopback listener and build watchers.
|
|
1295
|
+
} else if (ctx.options.port) {
|
|
1296
|
+
port = Number.parseInt(ctx.options.port, 10);
|
|
1297
|
+
if (!Number.isInteger(port) || port <= 0 || port > 65535) {
|
|
1298
|
+
throw usageError('Port must be between 1 and 65535.');
|
|
1299
|
+
}
|
|
1300
|
+
} else {
|
|
1301
|
+
// Concurrent sessions must not fight over the default port: the desktop
|
|
1302
|
+
// auto-starts every known project at launch, so only the first one can
|
|
1303
|
+
// have 5173 and the rest fall back to an ephemeral port.
|
|
1304
|
+
port = await getAvailablePortPreferring(DEFAULT_DEV_PORT);
|
|
933
1305
|
}
|
|
934
1306
|
|
|
935
1307
|
const mode = getCliMode();
|
|
@@ -938,17 +1310,39 @@ async function appsDevHandler(ctx) {
|
|
|
938
1310
|
throw usageError('Could not determine the current user from the CLI credential. Run notis login and retry.');
|
|
939
1311
|
}
|
|
940
1312
|
const apiBase = String(ctx.runtime.apiBase || '').replace(/\/$/, '');
|
|
941
|
-
const sessionsFilePath = getAppDevSessionsFile(
|
|
942
|
-
ctx.runtime.worktreeRuntime?.app_dev_sessions_file,
|
|
943
|
-
);
|
|
944
|
-
const desktopScheme = resolveDevelopmentDesktopScheme(
|
|
945
|
-
process.env,
|
|
946
|
-
ctx.runtime.worktreeRuntime,
|
|
947
|
-
);
|
|
948
|
-
const desktopAppName = resolveDevelopmentDesktopAppName(ctx.runtime);
|
|
949
|
-
const desktopBundleId = resolveDevelopmentDesktopBundleId(ctx.runtime);
|
|
950
1313
|
const sessionId = randomUUID();
|
|
951
|
-
const
|
|
1314
|
+
const profileKey = linkedStateProfileKey(ctx.runtime);
|
|
1315
|
+
const consumerMode = process.env.NOTIS_APPS_DEV_CONSUMER_MODE;
|
|
1316
|
+
const manualConsumerInstanceId = sharedBundleBaseUrls && !consumerMode
|
|
1317
|
+
? `cli.${process.pid}.${sessionId}`
|
|
1318
|
+
: null;
|
|
1319
|
+
let manualConsumerTimer = null;
|
|
1320
|
+
if (manualConsumerInstanceId) {
|
|
1321
|
+
const heartbeatManualConsumer = () => heartbeatAppDevConsumer({
|
|
1322
|
+
instanceId: manualConsumerInstanceId,
|
|
1323
|
+
userId: identity,
|
|
1324
|
+
apiBase,
|
|
1325
|
+
pid: process.pid,
|
|
1326
|
+
});
|
|
1327
|
+
heartbeatManualConsumer();
|
|
1328
|
+
manualConsumerTimer = setInterval(() => {
|
|
1329
|
+
try {
|
|
1330
|
+
heartbeatManualConsumer();
|
|
1331
|
+
} catch (error) {
|
|
1332
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
1333
|
+
process.stderr.write(`[notis apps dev] consumer heartbeat failed: ${message}\n`);
|
|
1334
|
+
}
|
|
1335
|
+
}, DEV_CONSUMER_HEARTBEAT_INTERVAL_MS);
|
|
1336
|
+
}
|
|
1337
|
+
// One app, one dataset: a dev session runs the app from local source against
|
|
1338
|
+
// the same databases the installed app uses, the way `npm run dev` serves the
|
|
1339
|
+
// same database as the deployed site. `--scratch` is the marked case; the
|
|
1340
|
+
// old `--live-data` flag now names the default and is kept as a no-op so
|
|
1341
|
+
// muscle memory and scripts keep working.
|
|
1342
|
+
if (ctx.options?.scratch && ctx.options?.liveData) {
|
|
1343
|
+
throw usageError('--scratch and --live-data contradict each other; pass at most one.');
|
|
1344
|
+
}
|
|
1345
|
+
const useInstalledDatabases = !ctx.options?.scratch;
|
|
952
1346
|
|
|
953
1347
|
const candidates = [];
|
|
954
1348
|
for (const projectDir of appDirs) {
|
|
@@ -956,122 +1350,123 @@ async function appsDevHandler(ctx) {
|
|
|
956
1350
|
if (!appConfig.name) {
|
|
957
1351
|
throw usageError(`notis.config.ts in ${projectDir} must define a non-empty name.`);
|
|
958
1352
|
}
|
|
959
|
-
const devSlug = buildDevInstallSlug(appConfig
|
|
1353
|
+
const devSlug = buildDevInstallSlug(appConfig);
|
|
960
1354
|
if (!devSlug) {
|
|
961
|
-
throw usageError(`notis.config.ts name in ${projectDir} must slugify to a non-empty value.`);
|
|
1355
|
+
throw usageError(`notis.config.ts devSlug or name in ${projectDir} must slugify to a non-empty value.`);
|
|
962
1356
|
}
|
|
1357
|
+
candidates.push({ appConfig, devSlug, projectDir });
|
|
1358
|
+
}
|
|
1359
|
+
|
|
1360
|
+
const canonicalSelection = selectCanonicalDevApps(candidates, readAppDevRoots());
|
|
1361
|
+
const canonicalCandidates = [];
|
|
1362
|
+
for (const { appConfig, devSlug, projectDir } of canonicalSelection.selected) {
|
|
963
1363
|
// Sequential on purpose: the shell-consent prompt must never interleave
|
|
964
1364
|
// with another app's, and the ensure fan-out below runs in parallel.
|
|
965
1365
|
const approvedCapabilities = await resolveCloudShellConsent({
|
|
966
1366
|
appConfig,
|
|
967
1367
|
projectDir,
|
|
968
1368
|
grantCloudShell: Boolean(ctx.options?.grantCloudShell),
|
|
1369
|
+
profileKey,
|
|
1370
|
+
});
|
|
1371
|
+
canonicalCandidates.push({
|
|
1372
|
+
appConfig,
|
|
1373
|
+
devSlug,
|
|
1374
|
+
projectDir,
|
|
1375
|
+
approvedCapabilities,
|
|
1376
|
+
mountNonce: randomUUID(),
|
|
969
1377
|
});
|
|
970
|
-
candidates.push({ appConfig, devSlug, projectDir, approvedCapabilities });
|
|
971
|
-
}
|
|
972
|
-
|
|
973
|
-
const seenSlugs = new Map();
|
|
974
|
-
for (const app of candidates) {
|
|
975
|
-
if (seenSlugs.has(app.devSlug)) {
|
|
976
|
-
throw usageError(
|
|
977
|
-
`Multiple apps slugify to "${app.devSlug}" (${seenSlugs.get(app.devSlug).projectDir} and ${app.projectDir}). Rename one in notis.config.ts.`,
|
|
978
|
-
);
|
|
979
|
-
}
|
|
980
|
-
seenSlugs.set(app.devSlug, app);
|
|
981
1378
|
}
|
|
982
1379
|
|
|
983
|
-
|
|
984
|
-
|
|
985
|
-
|
|
986
|
-
|
|
987
|
-
|
|
988
|
-
|
|
989
|
-
|
|
990
|
-
|
|
991
|
-
|
|
992
|
-
|
|
993
|
-
|
|
994
|
-
approvedCapabilities,
|
|
995
|
-
});
|
|
996
|
-
} finally {
|
|
997
|
-
logAppsTiming('ensure-dev-install', {
|
|
998
|
-
slug: devSlug,
|
|
999
|
-
ms: timingMs(appStartedAt).toFixed(1),
|
|
1000
|
-
});
|
|
1380
|
+
// Start every build watcher before the backend registrations finish. A
|
|
1381
|
+
// machine with several apps must not show an empty sidebar for minutes while
|
|
1382
|
+
// unrelated registrations run sequentially. Each successful registration is
|
|
1383
|
+
// published below as soon as its own last-known bundle is ready.
|
|
1384
|
+
const devServer = sharedBundleBaseUrls
|
|
1385
|
+
? {
|
|
1386
|
+
close: async () => {},
|
|
1387
|
+
isBundleReady: () => true,
|
|
1388
|
+
updateApp: () => {},
|
|
1389
|
+
waitForBundle: async () => {},
|
|
1390
|
+
getWatcherOwnership: () => null,
|
|
1001
1391
|
}
|
|
1002
|
-
|
|
1003
|
-
|
|
1004
|
-
|
|
1005
|
-
|
|
1006
|
-
|
|
1007
|
-
|
|
1008
|
-
|
|
1009
|
-
|
|
1010
|
-
|
|
1011
|
-
|
|
1012
|
-
|
|
1013
|
-
|
|
1014
|
-
|
|
1015
|
-
|
|
1016
|
-
|
|
1017
|
-
|
|
1018
|
-
|
|
1019
|
-
targetAppId: app.targetAppId,
|
|
1020
|
-
targetAppSlug: app.targetAppSlug,
|
|
1021
|
-
manifest: app.manifest,
|
|
1022
|
-
}),
|
|
1023
|
-
};
|
|
1024
|
-
});
|
|
1025
|
-
const developmentTabUrl = buildDevelopmentDesktopUrl(
|
|
1026
|
-
apps[0]?.appHref,
|
|
1027
|
-
desktopScheme,
|
|
1028
|
-
);
|
|
1029
|
-
const desktopWakeUrl = buildDevelopmentDesktopUrl('/manage', desktopScheme);
|
|
1030
|
-
const mountedDevelopmentTabUrl = buildMountedDevelopmentDesktopUrl(
|
|
1031
|
-
apps[0]?.appHref,
|
|
1032
|
-
desktopScheme,
|
|
1033
|
-
sessionId,
|
|
1034
|
-
);
|
|
1035
|
-
const warnings = [...databaseMaterializationWarnings(apps), ...liveDataWarnings(apps)];
|
|
1036
|
-
|
|
1037
|
-
const devServer = await startAppDevServer({
|
|
1038
|
-
apps: apps.map((app) => ({
|
|
1039
|
-
slug: app.devSlug,
|
|
1040
|
-
projectDir: app.projectDir,
|
|
1041
|
-
appId: app.appId,
|
|
1042
|
-
targetAppId: app.targetAppId || null,
|
|
1043
|
-
userId: identity,
|
|
1044
|
-
})),
|
|
1045
|
-
port,
|
|
1046
|
-
sessionsFilePath,
|
|
1392
|
+
: await startAppDevServer({
|
|
1393
|
+
apps: canonicalCandidates.map((app) => ({
|
|
1394
|
+
slug: app.devSlug,
|
|
1395
|
+
projectDir: app.projectDir,
|
|
1396
|
+
appId: null,
|
|
1397
|
+
targetAppId: null,
|
|
1398
|
+
userId: identity,
|
|
1399
|
+
profileKey,
|
|
1400
|
+
sessionId,
|
|
1401
|
+
mountNonce: app.mountNonce,
|
|
1402
|
+
})),
|
|
1403
|
+
port,
|
|
1404
|
+
sessionsFilePath,
|
|
1405
|
+
});
|
|
1406
|
+
const desktopHostOwnership = captureDesktopHostOwnership({
|
|
1407
|
+
desktopOwnerId: process.env.NOTIS_APPS_DEV_DESKTOP_OWNER_ID,
|
|
1408
|
+
desktopOwnerScope: process.env.NOTIS_APPS_DEV_DESKTOP_OWNER_SCOPE,
|
|
1047
1409
|
});
|
|
1048
1410
|
|
|
1049
|
-
|
|
1050
|
-
|
|
1051
|
-
|
|
1052
|
-
|
|
1053
|
-
|
|
1054
|
-
|
|
1055
|
-
|
|
1056
|
-
|
|
1057
|
-
|
|
1058
|
-
|
|
1059
|
-
|
|
1060
|
-
|
|
1061
|
-
|
|
1062
|
-
|
|
1063
|
-
|
|
1064
|
-
|
|
1065
|
-
|
|
1411
|
+
let heartbeatTimer = null;
|
|
1412
|
+
let consumerTimer = null;
|
|
1413
|
+
let shuttingDown = false;
|
|
1414
|
+
const shutdown = async (signal) => {
|
|
1415
|
+
if (shuttingDown) return;
|
|
1416
|
+
shuttingDown = true;
|
|
1417
|
+
process.stdout.write(`\n[notis apps dev] stopping (${signal})...\n`);
|
|
1418
|
+
if (heartbeatTimer) {
|
|
1419
|
+
clearInterval(heartbeatTimer);
|
|
1420
|
+
heartbeatTimer = null;
|
|
1421
|
+
}
|
|
1422
|
+
if (consumerTimer) {
|
|
1423
|
+
clearInterval(consumerTimer);
|
|
1424
|
+
consumerTimer = null;
|
|
1425
|
+
}
|
|
1426
|
+
if (manualConsumerTimer) {
|
|
1427
|
+
clearInterval(manualConsumerTimer);
|
|
1428
|
+
manualConsumerTimer = null;
|
|
1429
|
+
}
|
|
1430
|
+
if (manualConsumerInstanceId) {
|
|
1431
|
+
try {
|
|
1432
|
+
removeAppDevConsumer(manualConsumerInstanceId);
|
|
1433
|
+
} catch {
|
|
1434
|
+
// A crashed CLI lease expires automatically after the heartbeat window.
|
|
1435
|
+
}
|
|
1436
|
+
}
|
|
1066
1437
|
try {
|
|
1067
1438
|
await devServer.close();
|
|
1068
1439
|
} catch {
|
|
1069
|
-
// ignore cleanup
|
|
1440
|
+
// ignore cleanup failures during shutdown
|
|
1070
1441
|
}
|
|
1071
|
-
|
|
1072
|
-
|
|
1442
|
+
// Keep ownership records until every watcher group has stopped. If the
|
|
1443
|
+
// Desktop must force this host down, the next launch can still recover a
|
|
1444
|
+
// verified orphan instead of losing its only ownership proof.
|
|
1445
|
+
try {
|
|
1446
|
+
removeAppDevSession(sessionId, sessionsFilePath);
|
|
1447
|
+
} catch {
|
|
1448
|
+
// ignore cleanup failures during shutdown
|
|
1449
|
+
}
|
|
1450
|
+
if (sourceHostLock) {
|
|
1451
|
+
releaseAppDevHostLock(sourceHostLock);
|
|
1452
|
+
sourceHostLock = null;
|
|
1453
|
+
}
|
|
1454
|
+
process.exit(EXIT_CODES.ok);
|
|
1455
|
+
};
|
|
1456
|
+
const handleSigint = () => {
|
|
1457
|
+
void shutdown('SIGINT');
|
|
1458
|
+
};
|
|
1459
|
+
const handleSigterm = () => {
|
|
1460
|
+
void shutdown('SIGTERM');
|
|
1461
|
+
};
|
|
1462
|
+
|
|
1463
|
+
// Electron can stop a partially registered host. Install cleanup before the
|
|
1464
|
+
// first remote registration so every watcher group is still terminated when
|
|
1465
|
+
// registration is slow or stuck.
|
|
1466
|
+
process.on('SIGINT', handleSigint);
|
|
1467
|
+
process.on('SIGTERM', handleSigterm);
|
|
1073
1468
|
|
|
1074
|
-
|
|
1469
|
+
heartbeatTimer = setInterval(() => {
|
|
1075
1470
|
try {
|
|
1076
1471
|
heartbeatAppDevSession(sessionId, new Date().toISOString(), sessionsFilePath);
|
|
1077
1472
|
} catch (error) {
|
|
@@ -1080,23 +1475,147 @@ async function appsDevHandler(ctx) {
|
|
|
1080
1475
|
}
|
|
1081
1476
|
}, DEV_HEARTBEAT_INTERVAL_MS);
|
|
1082
1477
|
|
|
1083
|
-
const
|
|
1084
|
-
|
|
1085
|
-
|
|
1086
|
-
|
|
1087
|
-
|
|
1088
|
-
|
|
1478
|
+
const registrationStartedAt = process.hrtime.bigint();
|
|
1479
|
+
const apps = [];
|
|
1480
|
+
const registrationWarnings = [];
|
|
1481
|
+
let firstRegistrationError = null;
|
|
1482
|
+
// The local development backend is deliberately a single worker. Register
|
|
1483
|
+
// apps sequentially so a large root cannot queue many long mutations behind
|
|
1484
|
+
// one another. Unlike the old all-or-nothing startup, publish each healthy
|
|
1485
|
+
// app immediately while later registrations continue in the background.
|
|
1486
|
+
for (const {
|
|
1487
|
+
appConfig,
|
|
1488
|
+
devSlug,
|
|
1489
|
+
projectDir,
|
|
1490
|
+
approvedCapabilities,
|
|
1491
|
+
mountNonce,
|
|
1492
|
+
} of canonicalCandidates) {
|
|
1493
|
+
const appStartedAt = process.hrtime.bigint();
|
|
1494
|
+
try {
|
|
1495
|
+
const registeredApp = await ensureDevInstall({
|
|
1496
|
+
ctx,
|
|
1497
|
+
appConfig,
|
|
1498
|
+
projectDir,
|
|
1499
|
+
idempotencyKey: nextDevInstallIdempotencyKey(ctx.globalOptions, devSlug),
|
|
1500
|
+
useInstalledDatabases,
|
|
1501
|
+
approvedCapabilities,
|
|
1502
|
+
});
|
|
1503
|
+
const bundleBaseUrl = sharedBundleBaseUrls?.get(registeredApp.projectDir)
|
|
1504
|
+
|| `http://127.0.0.1:${port}/a/${registeredApp.devSlug}`;
|
|
1505
|
+
const app = {
|
|
1506
|
+
...registeredApp,
|
|
1507
|
+
bundleBaseUrl,
|
|
1508
|
+
mountNonce,
|
|
1509
|
+
appHref: buildDevelopmentAppHref({
|
|
1510
|
+
appSlug: registeredApp.slug,
|
|
1511
|
+
appId: registeredApp.appId,
|
|
1512
|
+
devSlug: registeredApp.devSlug,
|
|
1513
|
+
targetAppId: registeredApp.targetAppId,
|
|
1514
|
+
targetAppSlug: registeredApp.targetAppSlug,
|
|
1515
|
+
manifest: registeredApp.manifest,
|
|
1516
|
+
}),
|
|
1517
|
+
};
|
|
1518
|
+
devServer.updateApp(app.devSlug, {
|
|
1519
|
+
appId: app.appId,
|
|
1520
|
+
targetAppId: app.targetAppId || null,
|
|
1521
|
+
});
|
|
1522
|
+
const now = new Date().toISOString();
|
|
1523
|
+
const sessionRecord = {
|
|
1524
|
+
sessionId,
|
|
1525
|
+
hostPid: process.pid,
|
|
1526
|
+
sourceHost: !sharedBundleBaseUrls,
|
|
1527
|
+
...(desktopHostOwnership || {}),
|
|
1528
|
+
...(devServer.getWatcherOwnership(app.devSlug) || {}),
|
|
1529
|
+
bundleReady: devServer.isBundleReady(app.devSlug),
|
|
1530
|
+
...(!sharedBundleBaseUrls ? {
|
|
1531
|
+
discoveredProjects: discoveredAppDirs,
|
|
1532
|
+
canonicalProjects: canonicalCandidates.map((candidate) => candidate.projectDir),
|
|
1533
|
+
} : {}),
|
|
1534
|
+
userId: identity,
|
|
1535
|
+
apiBase,
|
|
1536
|
+
profileKey,
|
|
1537
|
+
appId: app.appId,
|
|
1538
|
+
targetAppId: app.targetAppId || undefined,
|
|
1539
|
+
mountNonce: app.mountNonce,
|
|
1540
|
+
devSlug: app.devSlug,
|
|
1541
|
+
bundleBaseUrl: app.bundleBaseUrl,
|
|
1542
|
+
projectDir: app.projectDir,
|
|
1543
|
+
startedAt: now,
|
|
1544
|
+
lastHeartbeatAt: now,
|
|
1545
|
+
};
|
|
1546
|
+
upsertAppDevSessions(sessionRecord, sessionsFilePath);
|
|
1547
|
+
apps.push(app);
|
|
1548
|
+
if (!sessionRecord.bundleReady) {
|
|
1549
|
+
void devServer.waitForBundle(app.devSlug).then(() => {
|
|
1550
|
+
upsertAppDevSessions({
|
|
1551
|
+
...sessionRecord,
|
|
1552
|
+
bundleReady: true,
|
|
1553
|
+
lastHeartbeatAt: new Date().toISOString(),
|
|
1554
|
+
}, sessionsFilePath);
|
|
1555
|
+
}).catch((error) => {
|
|
1556
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
1557
|
+
process.stderr.write(`[notis apps dev] ${app.devSlug}: initial bundle failed: ${message}\n`);
|
|
1558
|
+
});
|
|
1559
|
+
}
|
|
1560
|
+
} catch (error) {
|
|
1561
|
+
firstRegistrationError ||= error;
|
|
1562
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
1563
|
+
registrationWarnings.push(
|
|
1564
|
+
`${projectDir} could not be mounted: ${message}. It will be retried when the shared host reconciles again.`,
|
|
1565
|
+
);
|
|
1566
|
+
process.stderr.write(`[notis apps dev] ${devSlug}: registration failed: ${message}\n`);
|
|
1567
|
+
} finally {
|
|
1568
|
+
logAppsTiming('ensure-dev-install', {
|
|
1569
|
+
slug: devSlug,
|
|
1570
|
+
ms: timingMs(appStartedAt).toFixed(1),
|
|
1571
|
+
});
|
|
1572
|
+
}
|
|
1573
|
+
}
|
|
1574
|
+
if (apps.length === 0) {
|
|
1575
|
+
process.off('SIGINT', handleSigint);
|
|
1576
|
+
process.off('SIGTERM', handleSigterm);
|
|
1577
|
+
clearInterval(heartbeatTimer);
|
|
1578
|
+
heartbeatTimer = null;
|
|
1579
|
+
try {
|
|
1580
|
+
await devServer.close();
|
|
1581
|
+
} catch {
|
|
1582
|
+
// preserve the original registration error
|
|
1583
|
+
}
|
|
1584
|
+
if (sourceHostLock) {
|
|
1585
|
+
releaseAppDevHostLock(sourceHostLock);
|
|
1586
|
+
sourceHostLock = null;
|
|
1587
|
+
}
|
|
1588
|
+
if (manualConsumerTimer) clearInterval(manualConsumerTimer);
|
|
1589
|
+
if (manualConsumerInstanceId) {
|
|
1590
|
+
try {
|
|
1591
|
+
removeAppDevConsumer(manualConsumerInstanceId);
|
|
1592
|
+
} catch {
|
|
1593
|
+
// Preserve the registration error; a failed lease cleanup expires.
|
|
1594
|
+
}
|
|
1595
|
+
}
|
|
1596
|
+
throw firstRegistrationError || usageError('No unambiguous development apps could be mounted.');
|
|
1597
|
+
}
|
|
1598
|
+
logAppsTiming('ensure-dev-install:all', {
|
|
1599
|
+
apps: apps.length,
|
|
1600
|
+
ms: timingMs(registrationStartedAt).toFixed(1),
|
|
1601
|
+
});
|
|
1602
|
+
|
|
1603
|
+
const warnings = [
|
|
1604
|
+
...canonicalSelection.warnings,
|
|
1605
|
+
...registrationWarnings,
|
|
1606
|
+
...databaseMaterializationWarnings(apps),
|
|
1607
|
+
...liveDataWarnings(apps),
|
|
1608
|
+
...versionPrecedenceWarnings(apps),
|
|
1609
|
+
];
|
|
1089
1610
|
|
|
1090
1611
|
ctx.output.emitSuccess({
|
|
1091
1612
|
command: ctx.spec.command_path.join(' '),
|
|
1092
1613
|
data: {
|
|
1093
1614
|
mode,
|
|
1094
1615
|
api_base: apiBase,
|
|
1095
|
-
development_url: developmentTabUrl,
|
|
1096
1616
|
session_id: sessionId,
|
|
1097
1617
|
mount_status: 'serving',
|
|
1098
|
-
|
|
1099
|
-
desktop_target: desktopAppName,
|
|
1618
|
+
source_host: !sharedBundleBaseUrls,
|
|
1100
1619
|
identity,
|
|
1101
1620
|
apps: apps.map((app) => ({
|
|
1102
1621
|
slug: app.devSlug,
|
|
@@ -1110,113 +1629,79 @@ async function appsDevHandler(ctx) {
|
|
|
1110
1629
|
linked_app_id: app.linkedAppId,
|
|
1111
1630
|
database_materialization: app.databaseMaterialization,
|
|
1112
1631
|
live_data: app.liveData,
|
|
1632
|
+
local_release_version: app.localReleaseVersion,
|
|
1633
|
+
installed_release_version: app.installedReleaseVersion,
|
|
1634
|
+
mount_eligible: app.mountEligible,
|
|
1113
1635
|
})),
|
|
1114
1636
|
},
|
|
1115
1637
|
warnings,
|
|
1116
1638
|
humanSummary: [
|
|
1117
1639
|
`Running apps dev against ${apiBase} as ${identity} (mode: ${mode})`,
|
|
1118
|
-
`
|
|
1640
|
+
...(!skipRootRegistration ? [`Registered development root: ${rootDir}`] : []),
|
|
1641
|
+
`Watching ${readAppDevRoots().roots.length} persistent development root(s).`,
|
|
1119
1642
|
...(useInstalledDatabases
|
|
1120
1643
|
? [`Databases: ${apps.filter((app) => app.liveData?.enabled).length}/${apps.length} app(s) reading the installed app's live rows`]
|
|
1121
1644
|
: []),
|
|
1122
1645
|
'',
|
|
1123
|
-
|
|
1124
|
-
|
|
1125
|
-
|
|
1646
|
+
...apps.map((app) => (
|
|
1647
|
+
app.mountEligible
|
|
1648
|
+
? ` ${app.name.padEnd(24)} ${app.bundleBaseUrl} -> ${app.appHref}`
|
|
1649
|
+
: ` ${app.name.padEnd(24)} online v${app.installedReleaseVersion} (local ${app.localReleaseVersion || 'version missing'})`
|
|
1650
|
+
)),
|
|
1126
1651
|
'',
|
|
1127
|
-
|
|
1652
|
+
sharedBundleBaseUrls
|
|
1653
|
+
? `Attached to the shared source host: ${apps.filter((app) => app.mountEligible).length}/${apps.length} app${apps.length === 1 ? '' : 's'} eligible to substitute.`
|
|
1654
|
+
: `Serving one shared loopback host for ${apps.length} app${apps.length === 1 ? '' : 's'}; ${apps.filter((app) => app.mountEligible).length} eligible to substitute.`,
|
|
1128
1655
|
'',
|
|
1129
1656
|
'Press Ctrl-C to stop.',
|
|
1130
1657
|
].join('\n'),
|
|
1131
1658
|
});
|
|
1132
1659
|
|
|
1133
|
-
if (
|
|
1134
|
-
|
|
1135
|
-
|
|
1136
|
-
|
|
1137
|
-
|
|
1138
|
-
|
|
1660
|
+
if (consumerMode === 'machine' || consumerMode === 'environment') {
|
|
1661
|
+
consumerTimer = setInterval(() => {
|
|
1662
|
+
if (!hasAppDevConsumer(readAppDevConsumers(), {
|
|
1663
|
+
mode: consumerMode,
|
|
1664
|
+
userId: identity,
|
|
1665
|
+
apiBase,
|
|
1666
|
+
})) {
|
|
1667
|
+
void shutdown('last consumer detached');
|
|
1668
|
+
}
|
|
1669
|
+
}, DEV_CONSUMER_POLL_INTERVAL_MS);
|
|
1139
1670
|
}
|
|
1140
1671
|
|
|
1141
|
-
|
|
1142
|
-
let result = await waitForAppDevSessionMountAcknowledgements(expectedMountAcknowledgements, {
|
|
1143
|
-
sessionsFilePath,
|
|
1144
|
-
});
|
|
1145
|
-
if (!result.mounted) {
|
|
1146
|
-
process.stderr.write(
|
|
1147
|
-
`[notis apps dev] Serving locally, but ${desktopAppName} has not acknowledged ${result.missing.length === 1 ? 'the app' : `${result.missing.length} apps`} in its Local development sidebar yet. Keep this command running and open ${desktopAppName}.\n`,
|
|
1148
|
-
);
|
|
1149
|
-
}
|
|
1150
|
-
while (!result.mounted) {
|
|
1151
|
-
result = await waitForAppDevSessionMountAcknowledgements(expectedMountAcknowledgements, {
|
|
1152
|
-
sessionsFilePath,
|
|
1153
|
-
timeoutMs: 60_000,
|
|
1154
|
-
pollIntervalMs: 250,
|
|
1155
|
-
});
|
|
1156
|
-
}
|
|
1157
|
-
process.stderr.write(
|
|
1158
|
-
`[notis apps dev] Mounted in ${desktopAppName}: ${apps.map((app) => app.name).join(', ')}.\n`,
|
|
1159
|
-
);
|
|
1160
|
-
if (shouldOpenDevelopmentTab(ctx.options)) {
|
|
1161
|
-
openInBrowser(mountedDevelopmentTabUrl, {
|
|
1162
|
-
appName: desktopAppName,
|
|
1163
|
-
bundleId: desktopBundleId,
|
|
1164
|
-
scheme: desktopScheme,
|
|
1165
|
-
});
|
|
1166
|
-
}
|
|
1167
|
-
|
|
1168
|
-
const firstAppRender = expectedMountAcknowledgements.slice(0, 1);
|
|
1169
|
-
let renderResult = await waitForAppDevSessionRenderAcknowledgements(firstAppRender, {
|
|
1170
|
-
sessionsFilePath,
|
|
1171
|
-
timeoutMs: 0,
|
|
1172
|
-
});
|
|
1173
|
-
while (!renderResult.mounted) {
|
|
1174
|
-
renderResult = await waitForAppDevSessionRenderAcknowledgements(firstAppRender, {
|
|
1175
|
-
sessionsFilePath,
|
|
1176
|
-
timeoutMs: 60_000,
|
|
1177
|
-
pollIntervalMs: 250,
|
|
1178
|
-
});
|
|
1179
|
-
}
|
|
1180
|
-
process.stderr.write(
|
|
1181
|
-
`[notis apps dev] Rendered in ${desktopAppName}: ${apps[0].name}.\n`,
|
|
1182
|
-
);
|
|
1183
|
-
})().catch((error) => {
|
|
1184
|
-
const message = error instanceof Error ? error.message : String(error);
|
|
1185
|
-
process.stderr.write(`[notis apps dev] mount acknowledgement check failed: ${message}\n`);
|
|
1186
|
-
});
|
|
1672
|
+
await new Promise(() => {});
|
|
1187
1673
|
|
|
1188
|
-
|
|
1189
|
-
|
|
1190
|
-
if (shuttingDown) return;
|
|
1191
|
-
shuttingDown = true;
|
|
1192
|
-
process.stdout.write(`\n[notis apps dev] stopping (${signal})...\n`);
|
|
1193
|
-
if (heartbeatTimer) {
|
|
1194
|
-
clearInterval(heartbeatTimer);
|
|
1195
|
-
heartbeatTimer = null;
|
|
1196
|
-
}
|
|
1197
|
-
try {
|
|
1198
|
-
removeAppDevSession(sessionId, sessionsFilePath);
|
|
1199
|
-
} catch {
|
|
1200
|
-
// ignore cleanup failures during shutdown
|
|
1201
|
-
}
|
|
1202
|
-
try {
|
|
1203
|
-
await devServer.close();
|
|
1204
|
-
} catch {
|
|
1205
|
-
// ignore cleanup failures during shutdown
|
|
1206
|
-
}
|
|
1207
|
-
process.exit(EXIT_CODES.ok);
|
|
1208
|
-
};
|
|
1674
|
+
return EXIT_CODES.ok;
|
|
1675
|
+
}
|
|
1209
1676
|
|
|
1210
|
-
|
|
1211
|
-
|
|
1212
|
-
|
|
1213
|
-
|
|
1214
|
-
|
|
1677
|
+
async function appsRootsListHandler(ctx) {
|
|
1678
|
+
const registry = readAppDevRoots();
|
|
1679
|
+
return ctx.output.emitSuccess({
|
|
1680
|
+
command: ctx.spec.command_path.join(' '),
|
|
1681
|
+
data: { roots: registry.roots },
|
|
1682
|
+
humanSummary: appDevRootsTable(registry.roots),
|
|
1683
|
+
meta: { mutating: false },
|
|
1215
1684
|
});
|
|
1685
|
+
}
|
|
1216
1686
|
|
|
1217
|
-
|
|
1218
|
-
|
|
1219
|
-
|
|
1687
|
+
async function appsRootsRemoveHandler(ctx) {
|
|
1688
|
+
const rootDir = resolve(ctx.args.dir);
|
|
1689
|
+
const result = removeAppDevRoot(rootDir);
|
|
1690
|
+
return ctx.output.emitSuccess({
|
|
1691
|
+
command: ctx.spec.command_path.join(' '),
|
|
1692
|
+
data: {
|
|
1693
|
+
removed: result.removed,
|
|
1694
|
+
root: result.path,
|
|
1695
|
+
roots: result.registry.roots,
|
|
1696
|
+
},
|
|
1697
|
+
humanSummary: result.removed
|
|
1698
|
+
? `Removed app development root: ${result.path}`
|
|
1699
|
+
: `App development root was not registered: ${result.path}`,
|
|
1700
|
+
hints: result.removed
|
|
1701
|
+
? []
|
|
1702
|
+
: [{ command: 'notis apps roots list', reason: 'Show registered roots' }],
|
|
1703
|
+
meta: { mutating: result.removed },
|
|
1704
|
+
});
|
|
1220
1705
|
}
|
|
1221
1706
|
|
|
1222
1707
|
async function appsBuildHandler(ctx) {
|
|
@@ -1260,7 +1745,7 @@ async function appsVerifyHandler(ctx) {
|
|
|
1260
1745
|
if (!ctx.runtime.jwt) {
|
|
1261
1746
|
throw usageError('Live verify mode requires CLI auth. Run notis login and retry.');
|
|
1262
1747
|
}
|
|
1263
|
-
linkedState = readLinkedState(projectDir);
|
|
1748
|
+
linkedState = readLinkedState(projectDir, linkedStateProfileKey(ctx.runtime));
|
|
1264
1749
|
if (!linkedState?.app_id) {
|
|
1265
1750
|
throw usageError('Live verify mode requires a linked app. Run `notis apps link <app-id> .` first.');
|
|
1266
1751
|
}
|
|
@@ -1493,7 +1978,7 @@ async function appsScreenshotHandler(ctx) {
|
|
|
1493
1978
|
if (!ctx.runtime.jwt) {
|
|
1494
1979
|
throw usageError('Live mode requires CLI auth. Run notis login and retry.');
|
|
1495
1980
|
}
|
|
1496
|
-
linkedState = readLinkedState(projectDir);
|
|
1981
|
+
linkedState = readLinkedState(projectDir, linkedStateProfileKey(ctx.runtime));
|
|
1497
1982
|
if (!linkedState?.app_id) {
|
|
1498
1983
|
throw usageError('Live mode requires a linked app. Run `notis apps link <app-id> .` first.');
|
|
1499
1984
|
}
|
|
@@ -1692,10 +2177,15 @@ async function appsScreenshotHandler(ctx) {
|
|
|
1692
2177
|
}
|
|
1693
2178
|
}
|
|
1694
2179
|
|
|
1695
|
-
export function buildLinkedAppState(
|
|
2180
|
+
export function buildLinkedAppState(
|
|
2181
|
+
existingState,
|
|
2182
|
+
appId,
|
|
2183
|
+
linkedAt = new Date().toISOString(),
|
|
2184
|
+
) {
|
|
2185
|
+
const sameIdPromotion = existingState?.dev_app_id === appId;
|
|
1696
2186
|
return {
|
|
1697
|
-
...(existingState?.dev_app_id ? { dev_app_id: existingState.dev_app_id } : {}),
|
|
1698
|
-
...(existingState?.dev_linked_at ? { dev_linked_at: existingState.dev_linked_at } : {}),
|
|
2187
|
+
...(!sameIdPromotion && existingState?.dev_app_id ? { dev_app_id: existingState.dev_app_id } : {}),
|
|
2188
|
+
...(!sameIdPromotion && existingState?.dev_linked_at ? { dev_linked_at: existingState.dev_linked_at } : {}),
|
|
1699
2189
|
app_id: appId,
|
|
1700
2190
|
linked_at: linkedAt,
|
|
1701
2191
|
};
|
|
@@ -1709,7 +2199,12 @@ async function appsLinkHandler(ctx) {
|
|
|
1709
2199
|
|
|
1710
2200
|
writeLinkedState(
|
|
1711
2201
|
projectDir,
|
|
1712
|
-
buildLinkedAppState(
|
|
2202
|
+
buildLinkedAppState(
|
|
2203
|
+
readLinkedState(projectDir, linkedStateProfileKey(ctx.runtime)),
|
|
2204
|
+
appId,
|
|
2205
|
+
new Date().toISOString(),
|
|
2206
|
+
),
|
|
2207
|
+
linkedStateProfileKey(ctx.runtime),
|
|
1713
2208
|
);
|
|
1714
2209
|
|
|
1715
2210
|
return ctx.output.emitSuccess({
|
|
@@ -1726,8 +2221,10 @@ async function appsPullHandler(ctx) {
|
|
|
1726
2221
|
const appId = ctx.args.appId;
|
|
1727
2222
|
const result = await runToolCommand({
|
|
1728
2223
|
runtime: ctx.runtime,
|
|
1729
|
-
|
|
1730
|
-
|
|
2224
|
+
// Pull is source retrieval plus local link state. LIST_APPS is deliberately
|
|
2225
|
+
// non-materializing; GET_APP hydrates missing declared databases and would
|
|
2226
|
+
// turn a read-only pull into a remote mutation before build/verification.
|
|
2227
|
+
toolName: LIST_APPS_TOOL,
|
|
1731
2228
|
});
|
|
1732
2229
|
if (
|
|
1733
2230
|
ctx.runtime.credentialKind === 'oauth'
|
|
@@ -1735,10 +2232,16 @@ async function appsPullHandler(ctx) {
|
|
|
1735
2232
|
) {
|
|
1736
2233
|
throw usageError('Pulling app source requires a current OAuth grant. Run `notis login` and retry.');
|
|
1737
2234
|
}
|
|
1738
|
-
const
|
|
1739
|
-
const
|
|
1740
|
-
|
|
1741
|
-
|
|
2235
|
+
const apps = Array.isArray(result.payload?.apps) ? result.payload.apps : [];
|
|
2236
|
+
const app = apps.find((candidate) => (candidate?.app_id || candidate?.id) === appId);
|
|
2237
|
+
if (!app) {
|
|
2238
|
+
throw usageError(`App ${appId} is not accessible to the active profile.`);
|
|
2239
|
+
}
|
|
2240
|
+
const defaultDir = slugify(app.slug) || slugify(app.name) || slugify(appId);
|
|
2241
|
+
const targetDir = ctx.args.dir
|
|
2242
|
+
? resolveProjectDir(ctx.args.dir)
|
|
2243
|
+
: defaultAppProjectDir(defaultDir);
|
|
2244
|
+
const version = ctx.options.sourceVersion || 'latest';
|
|
1742
2245
|
|
|
1743
2246
|
const pulled = await pullAppSource({
|
|
1744
2247
|
apiBase: ctx.runtime.apiBase,
|
|
@@ -1747,6 +2250,7 @@ async function appsPullHandler(ctx) {
|
|
|
1747
2250
|
targetDir,
|
|
1748
2251
|
version,
|
|
1749
2252
|
force: Boolean(ctx.options.force),
|
|
2253
|
+
profileKey: linkedStateProfileKey(ctx.runtime),
|
|
1750
2254
|
});
|
|
1751
2255
|
|
|
1752
2256
|
const versionLabel = pulled.version === 'latest' ? 'latest version' : `v${pulled.version}`;
|
|
@@ -1757,11 +2261,11 @@ async function appsPullHandler(ctx) {
|
|
|
1757
2261
|
project_dir: pulled.projectDir,
|
|
1758
2262
|
version: pulled.version,
|
|
1759
2263
|
},
|
|
1760
|
-
humanSummary: `Pulled ${versionLabel} to ${pulled.projectDir}.
|
|
2264
|
+
humanSummary: `Pulled ${versionLabel} to ${pulled.projectDir}. Increment package.json notisAppVersion above the pulled release, then run \`cd ${pulled.projectDir} && npm install && notis apps dev\` to substitute the online bundle.`,
|
|
1761
2265
|
});
|
|
1762
2266
|
}
|
|
1763
2267
|
|
|
1764
|
-
function updateLinkedDeployState(projectDir, linkedState, appId, version) {
|
|
2268
|
+
function updateLinkedDeployState(projectDir, linkedState, appId, version, profileKey = null) {
|
|
1765
2269
|
if (!linkedState || linkedState.app_id !== appId || !Number.isFinite(version)) {
|
|
1766
2270
|
return;
|
|
1767
2271
|
}
|
|
@@ -1771,14 +2275,29 @@ function updateLinkedDeployState(projectDir, linkedState, appId, version) {
|
|
|
1771
2275
|
version,
|
|
1772
2276
|
linked_at: linkedState.linked_at || new Date().toISOString(),
|
|
1773
2277
|
deployed_at: new Date().toISOString(),
|
|
1774
|
-
});
|
|
2278
|
+
}, profileKey);
|
|
2279
|
+
}
|
|
2280
|
+
|
|
2281
|
+
// Resolve first deploy without mutating. Promotion is part of the final
|
|
2282
|
+
// SAVE_APP_FILES update, after build and uploads succeed, so a failed deploy
|
|
2283
|
+
// cannot leave an empty installed app behind.
|
|
2284
|
+
function resolveDeployTarget(ctx, projectDir) {
|
|
2285
|
+
if (ctx.options.appId) return { appId: ctx.options.appId, needsPromotion: false };
|
|
2286
|
+
const profileKey = linkedStateProfileKey(ctx.runtime);
|
|
2287
|
+
const state = readLinkedState(projectDir, profileKey);
|
|
2288
|
+
if (state?.app_id) return { appId: state.app_id, needsPromotion: false };
|
|
2289
|
+
if (!state?.dev_app_id) {
|
|
2290
|
+
return { appId: requireLinkedAppId(projectDir, ctx.options.appId, profileKey), needsPromotion: false };
|
|
2291
|
+
}
|
|
2292
|
+
return { appId: state.dev_app_id, needsPromotion: true };
|
|
1775
2293
|
}
|
|
1776
2294
|
|
|
1777
2295
|
async function appsDeployHandler(ctx) {
|
|
1778
2296
|
const projectDir = resolveProjectDir(ctx.args.dir || '.');
|
|
1779
|
-
const
|
|
2297
|
+
const profileKey = linkedStateProfileKey(ctx.runtime);
|
|
2298
|
+
const { appId, needsPromotion } = resolveDeployTarget(ctx, projectDir);
|
|
1780
2299
|
const idempotencyKey = nextIdempotencyKey(ctx.globalOptions);
|
|
1781
|
-
const linkedState = readLinkedState(projectDir);
|
|
2300
|
+
const linkedState = readLinkedState(projectDir, profileKey);
|
|
1782
2301
|
const baseVersion = linkedState?.app_id === appId && Number.isFinite(linkedState?.version)
|
|
1783
2302
|
? linkedState.version
|
|
1784
2303
|
: undefined;
|
|
@@ -1792,9 +2311,12 @@ async function appsDeployHandler(ctx) {
|
|
|
1792
2311
|
|
|
1793
2312
|
// Direct deploy mode: upload to Supabase storage directly
|
|
1794
2313
|
if (ctx.options.direct) {
|
|
2314
|
+
if (needsPromotion) {
|
|
2315
|
+
throw usageError('The first deploy must use the backend so promotion and save are atomic. Retry without --direct.');
|
|
2316
|
+
}
|
|
1795
2317
|
await assertDirectDeployAccess(ctx.runtime, appId);
|
|
1796
2318
|
const { version } = await directDeploy(projectDir, appId);
|
|
1797
|
-
updateLinkedDeployState(projectDir, linkedState, appId, version);
|
|
2319
|
+
updateLinkedDeployState(projectDir, linkedState, appId, version, profileKey);
|
|
1798
2320
|
return ctx.output.emitSuccess({
|
|
1799
2321
|
command: ctx.spec.command_path.join(' '),
|
|
1800
2322
|
data: { app_id: appId, version, mode: 'direct' },
|
|
@@ -1827,6 +2349,7 @@ async function appsDeployHandler(ctx) {
|
|
|
1827
2349
|
manifest,
|
|
1828
2350
|
...appRowFieldsFromManifest(manifest),
|
|
1829
2351
|
...(baseVersion !== undefined ? { base_version: baseVersion } : {}),
|
|
2352
|
+
...(needsPromotion ? { promote_dev_app: true } : {}),
|
|
1830
2353
|
},
|
|
1831
2354
|
mutating: true,
|
|
1832
2355
|
idempotencyKey,
|
|
@@ -1868,7 +2391,35 @@ async function appsDeployHandler(ctx) {
|
|
|
1868
2391
|
});
|
|
1869
2392
|
}
|
|
1870
2393
|
|
|
1871
|
-
|
|
2394
|
+
if (needsPromotion) {
|
|
2395
|
+
const now = new Date().toISOString();
|
|
2396
|
+
writeLinkedState(projectDir, {
|
|
2397
|
+
...linkedState,
|
|
2398
|
+
app_id: appId,
|
|
2399
|
+
linked_at: linkedState?.linked_at || now,
|
|
2400
|
+
version: deployedVersion,
|
|
2401
|
+
deployed_at: now,
|
|
2402
|
+
dev_app_id: undefined,
|
|
2403
|
+
dev_linked_at: undefined,
|
|
2404
|
+
}, profileKey);
|
|
2405
|
+
try {
|
|
2406
|
+
linkAppDevSessionTarget(
|
|
2407
|
+
{ appId, targetAppId: appId },
|
|
2408
|
+
getAppDevSessionsFile(),
|
|
2409
|
+
);
|
|
2410
|
+
} catch (error) {
|
|
2411
|
+
ctx.output.emitProgress({
|
|
2412
|
+
phase: 'warning',
|
|
2413
|
+
message: `Deploy succeeded, but the local development session could not be retargeted: ${error.message}`,
|
|
2414
|
+
});
|
|
2415
|
+
}
|
|
2416
|
+
ctx.output.emitProgress({
|
|
2417
|
+
phase: 'promote',
|
|
2418
|
+
message: `Promoted development app to installed app ${appId}`,
|
|
2419
|
+
});
|
|
2420
|
+
} else {
|
|
2421
|
+
updateLinkedDeployState(projectDir, linkedState, appId, deployedVersion, profileKey);
|
|
2422
|
+
}
|
|
1872
2423
|
return ctx.output.emitSuccess({
|
|
1873
2424
|
command: ctx.spec.command_path.join(' '),
|
|
1874
2425
|
data: {
|
|
@@ -1891,7 +2442,7 @@ async function appsDuplicateHandler(ctx) {
|
|
|
1891
2442
|
const projectDir = resolveProjectDir(ctx.args.dir || '.');
|
|
1892
2443
|
// Either target an app explicitly, or duplicate whatever this project is
|
|
1893
2444
|
// linked to, so `notis apps duplicate` works from inside a project.
|
|
1894
|
-
const appId = requireLinkedAppId(projectDir, ctx.options.appId);
|
|
2445
|
+
const appId = requireLinkedAppId(projectDir, ctx.options.appId, linkedStateProfileKey(ctx.runtime));
|
|
1895
2446
|
const idempotencyKey = nextIdempotencyKey(ctx.globalOptions);
|
|
1896
2447
|
|
|
1897
2448
|
const copyDocuments = ctx.options.copyDocuments || 'declared';
|
|
@@ -1957,8 +2508,8 @@ async function appsPublishHandler(ctx) {
|
|
|
1957
2508
|
}
|
|
1958
2509
|
|
|
1959
2510
|
const projectDir = resolveProjectDir(ctx.args.dir || '.');
|
|
1960
|
-
const appId = requireLinkedAppId(projectDir, ctx.options.appId);
|
|
1961
|
-
const linkedState = readLinkedState(projectDir);
|
|
2511
|
+
const appId = requireLinkedAppId(projectDir, ctx.options.appId, linkedStateProfileKey(ctx.runtime));
|
|
2512
|
+
const linkedState = readLinkedState(projectDir, linkedStateProfileKey(ctx.runtime));
|
|
1962
2513
|
const appConfig = await loadAppConfig(projectDir);
|
|
1963
2514
|
const readiness = inspectListingReadiness(projectDir, appConfig);
|
|
1964
2515
|
if (!readiness.ready) {
|
|
@@ -2051,7 +2602,7 @@ async function appsDoctorHandler(ctx) {
|
|
|
2051
2602
|
}
|
|
2052
2603
|
}
|
|
2053
2604
|
|
|
2054
|
-
const linkedState = readLinkedState(projectDir);
|
|
2605
|
+
const linkedState = readLinkedState(projectDir, linkedStateProfileKey(ctx.runtime));
|
|
2055
2606
|
const status = problems.length ? 'unhealthy' : warnings.length ? 'warnings' : 'healthy';
|
|
2056
2607
|
|
|
2057
2608
|
return ctx.output.emitSuccess({
|
|
@@ -2094,21 +2645,21 @@ export const appsCommandSpecs = [
|
|
|
2094
2645
|
{
|
|
2095
2646
|
command_path: ['apps', 'init'],
|
|
2096
2647
|
summary: 'Scaffold a new Notis app project.',
|
|
2097
|
-
when_to_use: 'Start a new Notis app. Use --from with a
|
|
2648
|
+
when_to_use: 'Start a new Notis app. Use --from with a published Store app when one is close to the desired app; otherwise creates the bare Vite + React project.',
|
|
2098
2649
|
args_schema: {
|
|
2099
2650
|
arguments: [
|
|
2100
2651
|
{ token: '<name>', description: 'Display name for the app.' },
|
|
2101
|
-
{ token: '[dir]', key: 'dir', description: 'Target directory
|
|
2652
|
+
{ token: '[dir]', key: 'dir', description: 'Target directory, resolved from the current directory. Defaults to ~/.notis/apps/<slug>; pass a path to place the project elsewhere, such as a tracked git repo or an existing monorepo.' },
|
|
2102
2653
|
],
|
|
2103
2654
|
options: [
|
|
2104
|
-
{ flags: '--from <slug>', description: 'Start from a
|
|
2655
|
+
{ flags: '--from <slug>', description: 'Start from a published Store app listed by `notis apps scaffolds list`. Downloads its source from the public app registry.' },
|
|
2105
2656
|
],
|
|
2106
2657
|
},
|
|
2107
2658
|
examples: [
|
|
2108
2659
|
'notis apps scaffolds list',
|
|
2109
2660
|
'notis apps init "Mind the Flo"',
|
|
2110
|
-
'notis apps init "My CRM" --from
|
|
2111
|
-
'notis apps init "My App"
|
|
2661
|
+
'notis apps init "My CRM" --from databases',
|
|
2662
|
+
'notis apps init "My App" ~/code/my-app',
|
|
2112
2663
|
],
|
|
2113
2664
|
mutates: true,
|
|
2114
2665
|
idempotent: false,
|
|
@@ -2118,10 +2669,19 @@ export const appsCommandSpecs = [
|
|
|
2118
2669
|
},
|
|
2119
2670
|
{
|
|
2120
2671
|
command_path: ['apps', 'scaffolds', 'list'],
|
|
2121
|
-
summary: 'List
|
|
2122
|
-
when_to_use: 'Discover
|
|
2123
|
-
args_schema: {
|
|
2124
|
-
|
|
2672
|
+
summary: 'List published Store apps available as scaffolds.',
|
|
2673
|
+
when_to_use: 'Discover published Store apps to start from before creating a new app. Every app published to the public Store is automatically a scaffold; use --search to narrow the catalog.',
|
|
2674
|
+
args_schema: {
|
|
2675
|
+
arguments: [],
|
|
2676
|
+
options: [
|
|
2677
|
+
{ flags: '--search <term>', description: 'Filter scaffolds by name, tagline, description, or category.' },
|
|
2678
|
+
],
|
|
2679
|
+
},
|
|
2680
|
+
examples: [
|
|
2681
|
+
'notis apps scaffolds list',
|
|
2682
|
+
'notis apps scaffolds list --search journal',
|
|
2683
|
+
'notis apps init "My App" --from databases',
|
|
2684
|
+
],
|
|
2125
2685
|
mutates: false,
|
|
2126
2686
|
idempotent: true,
|
|
2127
2687
|
require_auth: false,
|
|
@@ -2150,21 +2710,26 @@ export const appsCommandSpecs = [
|
|
|
2150
2710
|
},
|
|
2151
2711
|
{
|
|
2152
2712
|
command_path: ['apps', 'dev'],
|
|
2153
|
-
summary: '
|
|
2713
|
+
summary: 'Register a development root and connect its apps to the shared local development host.',
|
|
2154
2714
|
when_to_use:
|
|
2155
|
-
'Run this
|
|
2715
|
+
'Run this once for any folder that should be watched permanently. The folder itself, direct child apps, and apps/* are discovered automatically by every signed-in Notis Desktop instance. A linked app substitutes its online bundle only when local notisAppVersion is strictly greater than the installed release.',
|
|
2156
2716
|
args_schema: {
|
|
2157
2717
|
arguments: [
|
|
2158
2718
|
{ token: '[dir]', key: 'dir', description: 'Project directory or monorepo root (default: current dir).' },
|
|
2159
2719
|
],
|
|
2160
2720
|
options: [
|
|
2161
2721
|
{ flags: '--port <number>', description: `Local bundle server port (default: ${DEFAULT_DEV_PORT}).` },
|
|
2162
|
-
{
|
|
2722
|
+
{
|
|
2723
|
+
flags: '--scratch',
|
|
2724
|
+
description:
|
|
2725
|
+
'Use isolated empty databases, bundled skills, and bundled automations for this session '
|
|
2726
|
+
+ "instead of the installed app's resources. For fixture work and destructive experiments.",
|
|
2727
|
+
},
|
|
2163
2728
|
{
|
|
2164
2729
|
flags: '--live-data',
|
|
2165
2730
|
description:
|
|
2166
|
-
|
|
2167
|
-
+ '
|
|
2731
|
+
'Deprecated: using the installed app\'s real resources is now the default. '
|
|
2732
|
+
+ 'Accepted as a no-op; use `--scratch` for the old isolated behavior.',
|
|
2168
2733
|
},
|
|
2169
2734
|
{
|
|
2170
2735
|
flags: '--grant-cloud-shell',
|
|
@@ -2178,7 +2743,7 @@ export const appsCommandSpecs = [
|
|
|
2178
2743
|
'notis apps dev',
|
|
2179
2744
|
'notis apps dev ./my-app',
|
|
2180
2745
|
'notis apps dev ./workspace --port 5200',
|
|
2181
|
-
'notis apps dev --
|
|
2746
|
+
'notis apps dev --scratch # isolated resources for fixture or schema experiments',
|
|
2182
2747
|
],
|
|
2183
2748
|
mutates: true,
|
|
2184
2749
|
idempotent: true,
|
|
@@ -2186,6 +2751,35 @@ export const appsCommandSpecs = [
|
|
|
2186
2751
|
backend_call: { type: 'tool', name: ENSURE_DEV_APP_INSTALLATION_TOOL },
|
|
2187
2752
|
handler: appsDevHandler,
|
|
2188
2753
|
},
|
|
2754
|
+
{
|
|
2755
|
+
command_path: ['apps', 'roots', 'list'],
|
|
2756
|
+
summary: 'List persistent machine-local Notis app development roots.',
|
|
2757
|
+
when_to_use: 'See which folders every local Notis Desktop instance watches for development apps.',
|
|
2758
|
+
args_schema: { arguments: [], options: [] },
|
|
2759
|
+
examples: ['notis apps roots list'],
|
|
2760
|
+
mutates: false,
|
|
2761
|
+
idempotent: true,
|
|
2762
|
+
require_auth: false,
|
|
2763
|
+
backend_call: { type: 'local', name: 'list_app_development_roots' },
|
|
2764
|
+
handler: appsRootsListHandler,
|
|
2765
|
+
},
|
|
2766
|
+
{
|
|
2767
|
+
command_path: ['apps', 'roots', 'remove'],
|
|
2768
|
+
summary: 'Stop watching a registered Notis app development root.',
|
|
2769
|
+
when_to_use: 'Remove a persistent development root. The built-in ~/.notis/apps root cannot be removed.',
|
|
2770
|
+
args_schema: {
|
|
2771
|
+
arguments: [
|
|
2772
|
+
{ token: '<folder>', key: 'dir', description: 'Previously registered development root.' },
|
|
2773
|
+
],
|
|
2774
|
+
options: [],
|
|
2775
|
+
},
|
|
2776
|
+
examples: ['notis apps roots remove ./old-apps'],
|
|
2777
|
+
mutates: true,
|
|
2778
|
+
idempotent: true,
|
|
2779
|
+
require_auth: false,
|
|
2780
|
+
backend_call: { type: 'local', name: 'remove_app_development_root' },
|
|
2781
|
+
handler: appsRootsRemoveHandler,
|
|
2782
|
+
},
|
|
2189
2783
|
{
|
|
2190
2784
|
command_path: ['apps', 'build'],
|
|
2191
2785
|
summary: 'Build and package the app into .notis/output/.',
|
|
@@ -2294,18 +2888,21 @@ export const appsCommandSpecs = [
|
|
|
2294
2888
|
command_path: ['apps', 'pull'],
|
|
2295
2889
|
summary: 'Download a Notis app source snapshot into a local project folder.',
|
|
2296
2890
|
when_to_use:
|
|
2297
|
-
'Edit an installed app locally.
|
|
2891
|
+
'Edit an installed app locally. Preserve any local edits, pull and link the latest persisted source, then increment package.json notisAppVersion above that release before notis apps dev; continue with build and deploy.',
|
|
2298
2892
|
args_schema: {
|
|
2299
2893
|
arguments: [
|
|
2300
2894
|
{ token: '<app-id>', description: 'Remote app ID to pull.' },
|
|
2301
|
-
{ token: '[dir]', key: 'dir', description: 'Target directory (defaults to
|
|
2895
|
+
{ token: '[dir]', key: 'dir', description: 'Target directory (defaults to ~/.notis/apps/<app-slug>).' },
|
|
2302
2896
|
],
|
|
2303
2897
|
options: [
|
|
2304
2898
|
{ flags: '--force', description: 'Overwrite a non-empty target directory.' },
|
|
2305
|
-
{ flags: '--version <n>', description: 'Pull a specific app source version (default: latest).' },
|
|
2899
|
+
{ flags: '--source-version <n>', description: 'Pull a specific app source version (default: latest).' },
|
|
2306
2900
|
],
|
|
2307
2901
|
},
|
|
2308
|
-
examples: [
|
|
2902
|
+
examples: [
|
|
2903
|
+
'notis apps pull abc123',
|
|
2904
|
+
'notis apps pull abc123 ./my-app --force --source-version 3',
|
|
2905
|
+
],
|
|
2309
2906
|
mutates: true,
|
|
2310
2907
|
idempotent: true,
|
|
2311
2908
|
require_auth: true,
|
|
@@ -2316,7 +2913,7 @@ export const appsCommandSpecs = [
|
|
|
2316
2913
|
command_path: ['apps', 'deploy'],
|
|
2317
2914
|
summary: 'Build and upload the app to the linked Notis app.',
|
|
2318
2915
|
when_to_use:
|
|
2319
|
-
'Ship the installed app to production for the linked user/team app.
|
|
2916
|
+
'Ship the installed app to production for the linked user/team app. A project that has only a development app is promoted in place on first deploy: same app id, same databases, dev markers removed. This command does not publish to the app store.',
|
|
2320
2917
|
args_schema: {
|
|
2321
2918
|
arguments: [
|
|
2322
2919
|
{ token: '[dir]', key: 'dir', description: 'Project directory (default: current dir).' },
|
|
@@ -2324,7 +2921,7 @@ export const appsCommandSpecs = [
|
|
|
2324
2921
|
options: [
|
|
2325
2922
|
{ flags: '--app-id <id>', description: 'Override linked app ID.' },
|
|
2326
2923
|
{ flags: '--skip-build', description: 'Skip the build step (use existing .notis/output/).' },
|
|
2327
|
-
{ flags: '--direct', description: '
|
|
2924
|
+
{ flags: '--direct', description: 'Explicitly upload to Supabase storage, bypassing the backend server.' },
|
|
2328
2925
|
],
|
|
2329
2926
|
},
|
|
2330
2927
|
examples: ['notis apps deploy', 'notis apps deploy --skip-build', 'notis apps deploy --app-id abc123', 'notis apps deploy --direct'],
|