@notis_ai/cli 0.2.14 → 0.2.15
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +82 -20
- package/dist/agent-hooks/notis-agent-hook.mjs +19750 -0
- package/{skills → dist/base-skills}/notis-apps/SKILL.md +84 -53
- package/{skills → dist/base-skills}/notis-cli/SKILL.md +9 -9
- package/dist/base-skills/notis-query/SKILL.md +705 -0
- package/dist/skill-sync/index.js +1528 -0
- package/dist/skill-sync/index.js.map +7 -0
- package/package.json +5 -2
- package/skills/notis-apps/cli.md +38 -17
- 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 +871 -387
- 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-roots.js +284 -0
- package/src/runtime/app-dev-server.js +212 -22
- package/src/runtime/app-dev-sessions.js +105 -145
- package/src/runtime/app-platform.js +1291 -165
- package/src/runtime/app-registry-scaffolds.js +367 -0
- package/src/runtime/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 +0 -5
- package/src/runtime/skill-sync/cloud-client.ts +96 -0
- package/src/runtime/skill-sync/index.ts +644 -0
- package/src/runtime/skill-sync/local-scanner.ts +1046 -0
- package/src/runtime/skill-sync/symlink-manager.ts +383 -0
- package/src/runtime/skill-sync/sync-plan.ts +22 -0
- package/src/runtime/skill-sync/types.ts +103 -0
- package/src/runtime/skill-sync/write-cloud-skill.ts +50 -0
- package/src/runtime/store-screenshot.js +6 -1
- package/src/runtime/sync-skills.d.ts +37 -0
- package/src/runtime/sync-skills.js +215 -0
- package/template/app/layout.tsx +5 -1
- package/template/notis.config.ts +1 -0
- package/template/packages/sdk/package.json +2 -1
- package/template/packages/sdk/src/components/DocumentEditor.tsx +13 -3
- package/template/packages/sdk/src/components/MarkdownEditor.tsx +121 -0
- package/template/packages/sdk/src/components/MultiSelectActionBar.tsx +37 -119
- package/template/packages/sdk/src/components/MultiSelectCheckbox.tsx +5 -1
- package/template/packages/sdk/src/components/MultiSelectDragOverlay.tsx +1 -1
- package/template/packages/sdk/src/components/NotisSelectionBoundary.tsx +55 -0
- package/template/packages/sdk/src/components/ShortcutHints.tsx +56 -0
- package/template/packages/sdk/src/config.ts +19 -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 +709 -0
- package/template/packages/sdk/src/hooks/useHandover.ts +3 -0
- package/template/packages/sdk/src/hooks/useMultiSelect.ts +44 -482
- package/template/packages/sdk/src/hooks/useTool.ts +5 -4
- package/template/packages/sdk/src/index.ts +47 -2
- package/template/packages/sdk/src/interactions/actions.ts +46 -0
- package/template/packages/sdk/src/interactions/shortcuts.tsx +628 -0
- package/template/packages/sdk/src/interactions.ts +41 -0
- package/template/packages/sdk/src/provider.tsx +2 -1
- package/template/packages/sdk/src/runtime.ts +89 -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, 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,23 @@ import {
|
|
|
34
34
|
collectSourceFiles,
|
|
35
35
|
resolveConfiguredAppSkills,
|
|
36
36
|
normalizeAppCapabilities,
|
|
37
|
+
normalizeAppToolBindings,
|
|
37
38
|
normalizeAppSkillManifestPath,
|
|
38
39
|
directDeploy,
|
|
39
40
|
pullAppSource,
|
|
40
41
|
} from '../runtime/app-platform.js';
|
|
42
|
+
import {
|
|
43
|
+
filterScaffoldCatalog,
|
|
44
|
+
loadScaffoldCatalog,
|
|
45
|
+
scaffoldRegistryLabel,
|
|
46
|
+
} from '../runtime/app-registry-scaffolds.js';
|
|
41
47
|
import { startAppDevServer } from '../runtime/app-dev-server.js';
|
|
48
|
+
import {
|
|
49
|
+
discoverRegisteredAppProjects,
|
|
50
|
+
readAppDevRoots,
|
|
51
|
+
registerAppDevRoot,
|
|
52
|
+
removeAppDevRoot,
|
|
53
|
+
} from '../runtime/app-dev-roots.js';
|
|
42
54
|
import {
|
|
43
55
|
captureHarnessScreenshot,
|
|
44
56
|
closeAgentBrowserSession,
|
|
@@ -48,12 +60,22 @@ import {
|
|
|
48
60
|
import {
|
|
49
61
|
getAppDevSessionsFile,
|
|
50
62
|
heartbeatAppDevSession,
|
|
63
|
+
linkAppDevSessionTarget,
|
|
64
|
+
readAppDevSessions,
|
|
51
65
|
removeAppDevSession,
|
|
52
66
|
upsertAppDevSessions,
|
|
53
|
-
waitForAppDevSessionMountAcknowledgements,
|
|
54
|
-
waitForAppDevSessionRenderAcknowledgements,
|
|
55
67
|
} from '../runtime/app-dev-sessions.js';
|
|
56
|
-
import {
|
|
68
|
+
import {
|
|
69
|
+
releaseAppDevHostLock,
|
|
70
|
+
tryAcquireAppDevHostLock,
|
|
71
|
+
} from '../runtime/app-dev-host-lock.js';
|
|
72
|
+
import {
|
|
73
|
+
heartbeatAppDevConsumer,
|
|
74
|
+
hasAppDevConsumer,
|
|
75
|
+
readAppDevConsumers,
|
|
76
|
+
removeAppDevConsumer,
|
|
77
|
+
} from '../runtime/app-dev-consumers.js';
|
|
78
|
+
import { getAvailablePort, getAvailablePortPreferring } from '../runtime/ports.js';
|
|
57
79
|
import { getCliMode } from '../runtime/cli-mode.js';
|
|
58
80
|
import { composeStoreScreenshot } from '../runtime/store-screenshot.js';
|
|
59
81
|
import { httpRequest } from '../runtime/transport.js';
|
|
@@ -67,14 +89,142 @@ import {
|
|
|
67
89
|
|
|
68
90
|
const DEFAULT_DEV_PORT = 5173;
|
|
69
91
|
const DEV_HEARTBEAT_INTERVAL_MS = 10_000;
|
|
70
|
-
const
|
|
92
|
+
const DEV_CONSUMER_HEARTBEAT_INTERVAL_MS = 3_000;
|
|
93
|
+
const DEV_CONSUMER_POLL_INTERVAL_MS = 5_000;
|
|
94
|
+
const SHARED_APP_DEV_HOST_KEY = '__registered_roots__';
|
|
95
|
+
|
|
96
|
+
function projectIsWithinRoot(projectDir, rootDir) {
|
|
97
|
+
const nested = relative(rootDir, projectDir);
|
|
98
|
+
return nested === '' || (nested !== '..' && !nested.startsWith(`..${sep}`) && !isAbsolute(nested));
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
/**
|
|
102
|
+
* A CLI launch may add one root, but the shared host always serves the complete
|
|
103
|
+
* machine registry. Desktop sets skipRootRegistration because it is reconciling
|
|
104
|
+
* an already-registered snapshot; this must never narrow discovery to the one
|
|
105
|
+
* project path used to start the host.
|
|
106
|
+
*/
|
|
107
|
+
export function discoverAppDevLaunchProjects(rootDir, {
|
|
108
|
+
skipRootRegistration = false,
|
|
109
|
+
registerRoot = registerAppDevRoot,
|
|
110
|
+
discoverProjects = discoverRegisteredAppProjects,
|
|
111
|
+
} = {}) {
|
|
112
|
+
if (!skipRootRegistration) registerRoot(rootDir);
|
|
113
|
+
return discoverProjects();
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
/**
|
|
117
|
+
* Collapse duplicate local sources before they reach the shared loopback host.
|
|
118
|
+
* An explicitly registered root is more intentional than the implicit
|
|
119
|
+
* ~/.notis/apps root. If two equally intentional roots claim the same dev
|
|
120
|
+
* slug, omit that slug and keep serving every unrelated app.
|
|
121
|
+
*/
|
|
122
|
+
export function selectCanonicalDevApps(candidates, rootsRegistry) {
|
|
123
|
+
const roots = Array.isArray(rootsRegistry?.roots) ? rootsRegistry.roots : [];
|
|
124
|
+
const groups = new Map();
|
|
125
|
+
for (const candidate of candidates) {
|
|
126
|
+
groups.set(candidate.devSlug, [...(groups.get(candidate.devSlug) || []), candidate]);
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
const selected = [];
|
|
130
|
+
const warnings = [];
|
|
131
|
+
for (const [devSlug, group] of groups) {
|
|
132
|
+
if (group.length === 1) {
|
|
133
|
+
selected.push(group[0]);
|
|
134
|
+
continue;
|
|
135
|
+
}
|
|
136
|
+
const ranked = group.map((candidate) => {
|
|
137
|
+
const explicitRoots = roots.filter((root) => (
|
|
138
|
+
root?.implicit !== true
|
|
139
|
+
&& typeof root?.path === 'string'
|
|
140
|
+
&& projectIsWithinRoot(candidate.projectDir, root.path)
|
|
141
|
+
));
|
|
142
|
+
const newestRegistration = explicitRoots.reduce((latest, root) => {
|
|
143
|
+
const timestamp = Date.parse(root.registeredAt || '');
|
|
144
|
+
return Number.isFinite(timestamp) ? Math.max(latest, timestamp) : latest;
|
|
145
|
+
}, 0);
|
|
146
|
+
return {
|
|
147
|
+
candidate,
|
|
148
|
+
rank: explicitRoots.length > 0 ? 1 : 0,
|
|
149
|
+
newestRegistration,
|
|
150
|
+
};
|
|
151
|
+
}).sort((left, right) => (
|
|
152
|
+
right.rank - left.rank
|
|
153
|
+
|| right.newestRegistration - left.newestRegistration
|
|
154
|
+
));
|
|
155
|
+
const winner = ranked[0];
|
|
156
|
+
const runnerUp = ranked[1];
|
|
157
|
+
const unambiguous = winner.rank > runnerUp.rank
|
|
158
|
+
|| winner.newestRegistration > runnerUp.newestRegistration;
|
|
159
|
+
if (unambiguous) {
|
|
160
|
+
selected.push(winner.candidate);
|
|
161
|
+
warnings.push(
|
|
162
|
+
`Using ${winner.candidate.projectDir} for development slug "${devSlug}"; ignored duplicate source(s): ${ranked.slice(1).map(({ candidate }) => candidate.projectDir).join(', ')}.`,
|
|
163
|
+
);
|
|
164
|
+
continue;
|
|
165
|
+
}
|
|
166
|
+
warnings.push(
|
|
167
|
+
`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.`,
|
|
168
|
+
);
|
|
169
|
+
}
|
|
170
|
+
return { selected, warnings };
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
export function findSharedSourceBundleUrls(projectDirs, sessionsFilePath) {
|
|
174
|
+
const now = Date.now();
|
|
175
|
+
const sourceSessions = readAppDevSessions(sessionsFilePath).sessions
|
|
176
|
+
.filter((session) => {
|
|
177
|
+
if (session.sourceHost !== true) return false;
|
|
178
|
+
const heartbeatAt = Date.parse(session.lastHeartbeatAt || '');
|
|
179
|
+
if (!Number.isFinite(heartbeatAt) || now - heartbeatAt > 45_000) return false;
|
|
180
|
+
if (!Number.isInteger(session.hostPid) || session.hostPid <= 0) return false;
|
|
181
|
+
try {
|
|
182
|
+
process.kill(session.hostPid, 0);
|
|
183
|
+
return true;
|
|
184
|
+
} catch {
|
|
185
|
+
return false;
|
|
186
|
+
}
|
|
187
|
+
});
|
|
188
|
+
const groups = new Map();
|
|
189
|
+
for (const session of sourceSessions) {
|
|
190
|
+
const key = `${session.hostPid || 0}:${session.sessionId}`;
|
|
191
|
+
groups.set(key, [...(groups.get(key) || []), session]);
|
|
192
|
+
}
|
|
193
|
+
for (const sessions of groups.values()) {
|
|
194
|
+
const byProject = new Map(sessions.map((session) => [session.projectDir, session]));
|
|
195
|
+
const canonicalProjects = sessions.find((session) => (
|
|
196
|
+
Array.isArray(session.canonicalProjects)
|
|
197
|
+
))?.canonicalProjects;
|
|
198
|
+
const discoveredProjects = sessions.find((session) => (
|
|
199
|
+
Array.isArray(session.discoveredProjects)
|
|
200
|
+
))?.discoveredProjects;
|
|
201
|
+
const discoveryMatches = discoveredProjects
|
|
202
|
+
&& JSON.stringify([...discoveredProjects].sort()) === JSON.stringify([...projectDirs].sort());
|
|
203
|
+
if (
|
|
204
|
+
discoveryMatches
|
|
205
|
+
&& canonicalProjects
|
|
206
|
+
&& canonicalProjects.every((projectDir) => byProject.has(projectDir))
|
|
207
|
+
) {
|
|
208
|
+
return new Map(canonicalProjects.map((projectDir) => [
|
|
209
|
+
projectDir,
|
|
210
|
+
byProject.get(projectDir).bundleBaseUrl,
|
|
211
|
+
]));
|
|
212
|
+
}
|
|
213
|
+
if (!projectDirs.every((projectDir) => byProject.has(projectDir))) continue;
|
|
214
|
+
return new Map(projectDirs.map((projectDir) => [
|
|
215
|
+
projectDir,
|
|
216
|
+
byProject.get(projectDir).bundleBaseUrl,
|
|
217
|
+
]));
|
|
218
|
+
}
|
|
219
|
+
return null;
|
|
220
|
+
}
|
|
71
221
|
const ENSURE_DEV_APP_INSTALLATION_TOOL = 'LOCAL_NOTIS_ENSURE_DEV_APP_INSTALLATION';
|
|
72
222
|
const GET_APP_TOOL = 'LOCAL_NOTIS_GET_APP';
|
|
73
223
|
const LIST_APPS_TOOL = 'LOCAL_NOTIS_LIST_APPS';
|
|
74
224
|
const CREATE_APP_TOOL = 'LOCAL_NOTIS_CREATE_APP';
|
|
75
225
|
const DUPLICATE_APP_TOOL = 'LOCAL_NOTIS_DUPLICATE_APP';
|
|
76
226
|
const SAVE_APP_FILES_TOOL = 'LOCAL_NOTIS_SAVE_APP_FILES';
|
|
77
|
-
const APP_DEPLOY_TIMEOUT_MS =
|
|
227
|
+
export const APP_DEPLOY_TIMEOUT_MS = 600_000;
|
|
78
228
|
|
|
79
229
|
// ---------------------------------------------------------------------------
|
|
80
230
|
// Formatters
|
|
@@ -98,40 +248,12 @@ function scaffoldsTable(scaffolds) {
|
|
|
98
248
|
]);
|
|
99
249
|
}
|
|
100
250
|
|
|
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;
|
|
251
|
+
function appDevRootsTable(roots) {
|
|
252
|
+
return formatTable(roots, [
|
|
253
|
+
{ label: 'Folder', value: (root) => root.path },
|
|
254
|
+
{ label: 'Source', value: (root) => root.implicit ? 'default' : 'registered' },
|
|
255
|
+
{ label: 'Registered', value: (root) => root.registeredAt || '' },
|
|
256
|
+
]);
|
|
135
257
|
}
|
|
136
258
|
|
|
137
259
|
function decodeJwtSub(jwt) {
|
|
@@ -146,42 +268,11 @@ function decodeJwtSub(jwt) {
|
|
|
146
268
|
}
|
|
147
269
|
}
|
|
148
270
|
|
|
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
|
-
}
|
|
271
|
+
function linkedStateProfileKey(runtime) {
|
|
272
|
+
return appLinkedStateProfileKey({
|
|
273
|
+
apiBase: runtime?.apiBase,
|
|
274
|
+
userId: decodeJwtSub(runtime?.jwt),
|
|
275
|
+
});
|
|
185
276
|
}
|
|
186
277
|
|
|
187
278
|
function slugify(value) {
|
|
@@ -192,8 +283,8 @@ function slugify(value) {
|
|
|
192
283
|
.replace(/(^-|-$)+/g, '');
|
|
193
284
|
}
|
|
194
285
|
|
|
195
|
-
function buildDevInstallSlug(
|
|
196
|
-
const base = slugify(
|
|
286
|
+
function buildDevInstallSlug(appConfig) {
|
|
287
|
+
const base = slugify(appConfig?.devSlug || appConfig?.name);
|
|
197
288
|
if (!base) {
|
|
198
289
|
return '';
|
|
199
290
|
}
|
|
@@ -226,61 +317,6 @@ function pickDefaultRouteSlug(manifest) {
|
|
|
226
317
|
return firstWithSlug ? firstWithSlug.slug : null;
|
|
227
318
|
}
|
|
228
319
|
|
|
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
320
|
export function buildDevelopmentAppHref({
|
|
285
321
|
appSlug,
|
|
286
322
|
appId,
|
|
@@ -561,6 +597,7 @@ function buildManifestForDev(appConfig) {
|
|
|
561
597
|
databases: appConfig.databases || [],
|
|
562
598
|
capabilities: normalizeAppCapabilities(appConfig.capabilities),
|
|
563
599
|
tools: appConfig.tools || [],
|
|
600
|
+
tool_bindings: normalizeAppToolBindings(appConfig.toolBindings),
|
|
564
601
|
skills: (appConfig.skills || []).map((skill) => ({
|
|
565
602
|
key: skill.key,
|
|
566
603
|
path: normalizeAppSkillManifestPath(skill.path),
|
|
@@ -579,7 +616,7 @@ export function buildEnsureDevInstallArguments({
|
|
|
579
616
|
useInstalledDatabases = false,
|
|
580
617
|
approvedCapabilities = null,
|
|
581
618
|
}) {
|
|
582
|
-
const devSlug = buildDevInstallSlug(appConfig
|
|
619
|
+
const devSlug = buildDevInstallSlug(appConfig);
|
|
583
620
|
const arguments_ = {
|
|
584
621
|
dev_slug: devSlug,
|
|
585
622
|
name: appConfig.name,
|
|
@@ -589,8 +626,12 @@ export function buildEnsureDevInstallArguments({
|
|
|
589
626
|
if (linkedState?.dev_app_id) {
|
|
590
627
|
arguments_.app_id = linkedState.dev_app_id;
|
|
591
628
|
}
|
|
592
|
-
|
|
593
|
-
|
|
629
|
+
if (useInstalledDatabases && linkedState?.app_id) {
|
|
630
|
+
arguments_.installed_app_id = linkedState.app_id;
|
|
631
|
+
}
|
|
632
|
+
// Sent on every ensure call, including as `false`, so a `--scratch` session
|
|
633
|
+
// puts the app on its own dev copies and the next plain session puts it back
|
|
634
|
+
// on the installed app's databases.
|
|
594
635
|
arguments_.use_installed_databases = Boolean(useInstalledDatabases);
|
|
595
636
|
if (Array.isArray(approvedCapabilities) && approvedCapabilities.length > 0) {
|
|
596
637
|
arguments_.approved_capabilities = approvedCapabilities;
|
|
@@ -614,15 +655,20 @@ export async function resolveCloudShellConsent({
|
|
|
614
655
|
projectDir,
|
|
615
656
|
grantCloudShell = false,
|
|
616
657
|
logger = console,
|
|
658
|
+
profileKey = null,
|
|
617
659
|
}) {
|
|
618
660
|
const capabilities = normalizeAppCapabilities(appConfig.capabilities);
|
|
619
661
|
if (capabilities.cloudComputer !== 'shell') {
|
|
620
662
|
return null;
|
|
621
663
|
}
|
|
622
664
|
const approved = ['cloud_computer_read', 'cloud_computer_shell'];
|
|
623
|
-
const linkedState = readLinkedState(projectDir) || {};
|
|
665
|
+
const linkedState = readLinkedState(projectDir, profileKey) || {};
|
|
624
666
|
const recordDecision = (decision) => {
|
|
625
|
-
writeLinkedState(
|
|
667
|
+
writeLinkedState(
|
|
668
|
+
projectDir,
|
|
669
|
+
{ ...linkedState, [CLOUD_SHELL_CONSENT_KEY]: decision },
|
|
670
|
+
profileKey,
|
|
671
|
+
);
|
|
626
672
|
};
|
|
627
673
|
|
|
628
674
|
if (grantCloudShell) {
|
|
@@ -675,21 +721,22 @@ export async function ensureDevInstall({
|
|
|
675
721
|
if (!appConfig.name) {
|
|
676
722
|
throw usageError(`notis.config.ts in ${projectDir} must define a non-empty name.`);
|
|
677
723
|
}
|
|
678
|
-
const devSlug = buildDevInstallSlug(appConfig
|
|
724
|
+
const devSlug = buildDevInstallSlug(appConfig);
|
|
679
725
|
if (!devSlug) {
|
|
680
|
-
throw usageError(`notis.config.ts name in ${projectDir} must slugify to a non-empty value.`);
|
|
726
|
+
throw usageError(`notis.config.ts devSlug or name in ${projectDir} must slugify to a non-empty value.`);
|
|
681
727
|
}
|
|
682
728
|
|
|
683
729
|
const manifest = buildManifestForDev(appConfig);
|
|
684
730
|
const skills = resolveConfiguredAppSkills(appConfig, projectDir);
|
|
685
|
-
|
|
731
|
+
const profileKey = linkedStateProfileKey(ctx.runtime);
|
|
732
|
+
let linkedState = readLinkedState(projectDir, profileKey);
|
|
686
733
|
let linkedApp = null;
|
|
687
734
|
if (linkedState?.dev_app_id) {
|
|
688
735
|
const devApp = await getAccessibleApp(ctx.runtime, linkedState.dev_app_id, runTool);
|
|
689
736
|
if (!devApp || devApp.manifest?.is_dev !== true) {
|
|
690
737
|
const { dev_app_id: _devAppId, dev_linked_at: _devLinkedAt, ...rest } = linkedState;
|
|
691
738
|
linkedState = rest;
|
|
692
|
-
writeLinkedState(projectDir, linkedState);
|
|
739
|
+
writeLinkedState(projectDir, linkedState, profileKey);
|
|
693
740
|
}
|
|
694
741
|
}
|
|
695
742
|
if (linkedState?.app_id) {
|
|
@@ -704,25 +751,98 @@ export async function ensureDevInstall({
|
|
|
704
751
|
dev_linked_at: linkedState.dev_linked_at || linkedState.linked_at || new Date().toISOString(),
|
|
705
752
|
} : {}),
|
|
706
753
|
};
|
|
707
|
-
writeLinkedState(projectDir, linkedState);
|
|
754
|
+
writeLinkedState(projectDir, linkedState, profileKey);
|
|
708
755
|
linkedApp = null;
|
|
709
756
|
}
|
|
710
757
|
}
|
|
758
|
+
// A checkout can be shared by Beta and a source-workspace desktop. Its
|
|
759
|
+
// installed app link is valid for the Beta account but intentionally
|
|
760
|
+
// inaccessible to the worktree's test user. Preserve that durable link for
|
|
761
|
+
// Beta, while omitting it from this run so the local dev app uses its own
|
|
762
|
+
// resources instead of sending an unauthorized installed_app_id to ensure.
|
|
763
|
+
if (!linkedState?.app_id) {
|
|
764
|
+
const installedResolution = await findInstalledLinkCandidatesForLegacyDevState(
|
|
765
|
+
ctx.runtime,
|
|
766
|
+
devSlug,
|
|
767
|
+
linkedState?.dev_app_id,
|
|
768
|
+
projectDir,
|
|
769
|
+
profileKey,
|
|
770
|
+
runTool,
|
|
771
|
+
);
|
|
772
|
+
const installedCandidates = installedResolution.candidates;
|
|
773
|
+
if (installedCandidates.length > 1) {
|
|
774
|
+
if (installedResolution.source === 'persisted-project-link') {
|
|
775
|
+
throw usageError(
|
|
776
|
+
`Multiple accessible installed apps are persisted for ${projectDir}. `
|
|
777
|
+
+ `Run \`notis apps link <app-id> ${projectDir}\` to choose one for this environment.`,
|
|
778
|
+
);
|
|
779
|
+
}
|
|
780
|
+
throw usageError(
|
|
781
|
+
`Multiple accessible installed apps use the exact slug "${devSlug.slice(0, -4)}". `
|
|
782
|
+
+ `Run \`notis apps link <app-id> ${projectDir}\` to choose one.`,
|
|
783
|
+
);
|
|
784
|
+
}
|
|
785
|
+
if (installedCandidates.length === 1) {
|
|
786
|
+
const now = new Date().toISOString();
|
|
787
|
+
linkedApp = installedCandidates[0];
|
|
788
|
+
linkedState = {
|
|
789
|
+
...(linkedState || {}),
|
|
790
|
+
app_id: linkedApp.app_id || linkedApp.id,
|
|
791
|
+
linked_at: now,
|
|
792
|
+
auto_linked_at: now,
|
|
793
|
+
};
|
|
794
|
+
writeLinkedState(projectDir, linkedState, profileKey);
|
|
795
|
+
}
|
|
796
|
+
}
|
|
797
|
+
const runtimeLinkedState = linkedState?.app_id && !linkedApp
|
|
798
|
+
? Object.fromEntries(
|
|
799
|
+
Object.entries(linkedState).filter(([key]) => ![
|
|
800
|
+
'app_id', 'linked_at', 'deployed_at', 'version',
|
|
801
|
+
].includes(key)),
|
|
802
|
+
)
|
|
803
|
+
: linkedState;
|
|
711
804
|
const ensureArguments = buildEnsureDevInstallArguments({
|
|
712
805
|
appConfig,
|
|
713
806
|
manifest,
|
|
714
|
-
linkedState,
|
|
807
|
+
linkedState: runtimeLinkedState,
|
|
715
808
|
skills,
|
|
716
809
|
useInstalledDatabases,
|
|
717
810
|
approvedCapabilities,
|
|
718
811
|
});
|
|
719
|
-
|
|
720
|
-
|
|
721
|
-
|
|
722
|
-
|
|
723
|
-
|
|
724
|
-
|
|
725
|
-
|
|
812
|
+
let ensureResult;
|
|
813
|
+
try {
|
|
814
|
+
ensureResult = await runTool({
|
|
815
|
+
runtime: ctx.runtime,
|
|
816
|
+
toolName: ENSURE_DEV_APP_INSTALLATION_TOOL,
|
|
817
|
+
arguments_: ensureArguments,
|
|
818
|
+
mutating: true,
|
|
819
|
+
idempotencyKey,
|
|
820
|
+
});
|
|
821
|
+
} catch (error) {
|
|
822
|
+
const backendCode = error?.details?.error?.code;
|
|
823
|
+
if (linkedState?.dev_app_id && backendCode === 'dev_app_slug_conflict') {
|
|
824
|
+
// The authenticated environment can outlive an older local API profile.
|
|
825
|
+
// If that profile remembered a different dev row while this user already
|
|
826
|
+
// owns the exact canonical dev slug, the backend refuses a duplicate.
|
|
827
|
+
// Retry without the stale row id so the backend deterministically adopts
|
|
828
|
+
// the unique slug owner; preserve the installed target and every grant.
|
|
829
|
+
const staleDevAppId = linkedState.dev_app_id;
|
|
830
|
+
const { dev_app_id: _devAppId, dev_linked_at: _devLinkedAt, ...rest } = linkedState;
|
|
831
|
+
linkedState = rest;
|
|
832
|
+
writeLinkedState(projectDir, linkedState, profileKey);
|
|
833
|
+
const retryArguments = { ...ensureArguments };
|
|
834
|
+
delete retryArguments.app_id;
|
|
835
|
+
ensureResult = await runTool({
|
|
836
|
+
runtime: ctx.runtime,
|
|
837
|
+
toolName: ENSURE_DEV_APP_INSTALLATION_TOOL,
|
|
838
|
+
arguments_: retryArguments,
|
|
839
|
+
mutating: true,
|
|
840
|
+
idempotencyKey: `${idempotencyKey}:adopt-slug-owner:${staleDevAppId}`,
|
|
841
|
+
});
|
|
842
|
+
} else {
|
|
843
|
+
throw error;
|
|
844
|
+
}
|
|
845
|
+
}
|
|
726
846
|
const appId = ensureResult.payload.app_id;
|
|
727
847
|
if (!appId) {
|
|
728
848
|
throw usageError('Dev installation tool did not return an app_id.');
|
|
@@ -733,7 +853,7 @@ export async function ensureDevInstall({
|
|
|
733
853
|
...(linkedState || {}),
|
|
734
854
|
dev_app_id: appId,
|
|
735
855
|
dev_linked_at: linkedState?.dev_linked_at || now,
|
|
736
|
-
});
|
|
856
|
+
}, profileKey);
|
|
737
857
|
}
|
|
738
858
|
return {
|
|
739
859
|
slug: ensureResult.payload.slug || devSlug,
|
|
@@ -743,8 +863,8 @@ export async function ensureDevInstall({
|
|
|
743
863
|
projectDir,
|
|
744
864
|
manifest,
|
|
745
865
|
created: ensureResult.payload.created || false,
|
|
746
|
-
linkedAppId:
|
|
747
|
-
targetAppId:
|
|
866
|
+
linkedAppId: runtimeLinkedState?.app_id || null,
|
|
867
|
+
targetAppId: runtimeLinkedState?.app_id || null,
|
|
748
868
|
targetAppSlug: linkedApp?.slug || null,
|
|
749
869
|
databaseMaterialization: ensureResult.payload.database_materialization || { created: [], unresolved: [] },
|
|
750
870
|
liveData: ensureResult.payload.live_data || null,
|
|
@@ -795,6 +915,63 @@ async function getAccessibleApp(runtime, appId, runTool = runToolCommand) {
|
|
|
795
915
|
throw usageError(`Could not verify access to app ${appId}${message ? `: ${message}` : '.'}`);
|
|
796
916
|
}
|
|
797
917
|
|
|
918
|
+
async function findInstalledLinkCandidatesForLegacyDevState(
|
|
919
|
+
runtime,
|
|
920
|
+
devSlug,
|
|
921
|
+
devAppId,
|
|
922
|
+
projectDir,
|
|
923
|
+
profileKey,
|
|
924
|
+
runTool = runToolCommand,
|
|
925
|
+
) {
|
|
926
|
+
const installedSlug = devSlug.endsWith('-dev') ? devSlug.slice(0, -4) : '';
|
|
927
|
+
if (!installedSlug) return { candidates: [], source: null };
|
|
928
|
+
|
|
929
|
+
const result = await runTool({
|
|
930
|
+
runtime,
|
|
931
|
+
toolName: LIST_APPS_TOOL,
|
|
932
|
+
});
|
|
933
|
+
const accessibleInstalledApps = (result.payload?.apps || []).filter((app) => (
|
|
934
|
+
(app?.app_id || app?.id)
|
|
935
|
+
&& (app.app_id || app.id) !== devAppId
|
|
936
|
+
&& app?.manifest?.is_dev !== true
|
|
937
|
+
));
|
|
938
|
+
|
|
939
|
+
// A project-local app id is stronger evidence than a slug. Older CLI runs
|
|
940
|
+
// could persist that explicit link under another API/user profile before
|
|
941
|
+
// the machine-local discovery host mounted the same source here. Migrate it
|
|
942
|
+
// only when exactly one such id is accessible in the current environment;
|
|
943
|
+
// multiple accessible ids remain ambiguous and fail closed below.
|
|
944
|
+
const rawState = readLinkedState(projectDir) || {};
|
|
945
|
+
const persistedAppIds = new Set();
|
|
946
|
+
if (rawState.app_id) persistedAppIds.add(rawState.app_id);
|
|
947
|
+
const profiles = rawState.profiles && typeof rawState.profiles === 'object'
|
|
948
|
+
? rawState.profiles
|
|
949
|
+
: {};
|
|
950
|
+
for (const [storedProfileKey, state] of Object.entries(profiles)) {
|
|
951
|
+
if (storedProfileKey === profileKey || !state || typeof state !== 'object') continue;
|
|
952
|
+
if (state.app_id) persistedAppIds.add(state.app_id);
|
|
953
|
+
}
|
|
954
|
+
const persistedCandidates = accessibleInstalledApps.filter((app) => (
|
|
955
|
+
persistedAppIds.has(app.app_id || app.id)
|
|
956
|
+
));
|
|
957
|
+
if (persistedCandidates.length) {
|
|
958
|
+
return { candidates: persistedCandidates, source: 'persisted-project-link' };
|
|
959
|
+
}
|
|
960
|
+
|
|
961
|
+
// The database row slug can change when a Store app is installed or an old
|
|
962
|
+
// development app is promoted. The shipped source manifest keeps the
|
|
963
|
+
// canonical app slug, so accept either exact field. Display names are never
|
|
964
|
+
// considered.
|
|
965
|
+
const exactSlugCandidates = accessibleInstalledApps.filter((app) => {
|
|
966
|
+
const canonicalSlugs = new Set([
|
|
967
|
+
app?.slug,
|
|
968
|
+
app?.manifest?.app?.slug,
|
|
969
|
+
].map(slugify).filter(Boolean));
|
|
970
|
+
return canonicalSlugs.has(installedSlug);
|
|
971
|
+
});
|
|
972
|
+
return { candidates: exactSlugCandidates, source: 'exact-slug' };
|
|
973
|
+
}
|
|
974
|
+
|
|
798
975
|
export async function assertDirectDeployAccess(runtime, appId, runTool = runToolCommand) {
|
|
799
976
|
const app = await getAccessibleApp(runtime, appId, runTool);
|
|
800
977
|
if (!app) {
|
|
@@ -803,6 +980,14 @@ export async function assertDirectDeployAccess(runtime, appId, runTool = runTool
|
|
|
803
980
|
if (app.apps_access?.has_access !== true) {
|
|
804
981
|
throw usageError('Notis Apps require a PRO+ or ULTRA plan after your trial.');
|
|
805
982
|
}
|
|
983
|
+
if (app.can_edit !== true) {
|
|
984
|
+
throw usageError(`Direct deploy requires edit access to app ${appId}.`);
|
|
985
|
+
}
|
|
986
|
+
if (app.manifest?.is_dev === true) {
|
|
987
|
+
throw usageError(
|
|
988
|
+
'A development app cannot be deployed directly. Retry without --direct so first deploy can promote it safely.',
|
|
989
|
+
);
|
|
990
|
+
}
|
|
806
991
|
return app;
|
|
807
992
|
}
|
|
808
993
|
|
|
@@ -839,10 +1024,12 @@ async function appsListHandler(ctx) {
|
|
|
839
1024
|
}
|
|
840
1025
|
|
|
841
1026
|
async function appsInitHandler(ctx) {
|
|
842
|
-
const projectDir =
|
|
1027
|
+
const projectDir = ctx.args.dir
|
|
1028
|
+
? resolveProjectDir(ctx.args.dir)
|
|
1029
|
+
: defaultAppProjectDir(slugify(ctx.args.name));
|
|
843
1030
|
const fromSlug = ctx.options.from || null;
|
|
844
1031
|
|
|
845
|
-
scaffoldProject({ projectDir, appName: ctx.args.name, fromSlug });
|
|
1032
|
+
await scaffoldProject({ projectDir, appName: ctx.args.name, fromSlug });
|
|
846
1033
|
|
|
847
1034
|
return ctx.output.emitSuccess({
|
|
848
1035
|
command: ctx.spec.command_path.join(' '),
|
|
@@ -858,14 +1045,20 @@ async function appsInitHandler(ctx) {
|
|
|
858
1045
|
}
|
|
859
1046
|
|
|
860
1047
|
async function appsScaffoldsListHandler(ctx) {
|
|
861
|
-
const
|
|
1048
|
+
const searchTerm = ctx.options.search || null;
|
|
1049
|
+
const catalog = await loadScaffoldCatalog();
|
|
1050
|
+
const scaffolds = filterScaffoldCatalog(catalog, searchTerm);
|
|
1051
|
+
const registry = scaffoldRegistryLabel();
|
|
1052
|
+
const emptyMessage = searchTerm
|
|
1053
|
+
? `No published scaffolds match "${searchTerm}" (${catalog.length} available; run without --search to see all).`
|
|
1054
|
+
: `No published scaffolds found in ${registry}.`;
|
|
862
1055
|
return ctx.output.emitSuccess({
|
|
863
1056
|
command: ctx.spec.command_path.join(' '),
|
|
864
|
-
data: { scaffolds },
|
|
1057
|
+
data: { scaffolds, registry, search: searchTerm },
|
|
865
1058
|
humanSummary: scaffolds.length
|
|
866
|
-
? `Found ${scaffolds.length}
|
|
867
|
-
:
|
|
868
|
-
renderHuman: () => (scaffolds.length ? scaffoldsTable(scaffolds) :
|
|
1059
|
+
? `Found ${scaffolds.length} published scaffolds in ${registry}`
|
|
1060
|
+
: emptyMessage,
|
|
1061
|
+
renderHuman: () => (scaffolds.length ? scaffoldsTable(scaffolds) : emptyMessage),
|
|
869
1062
|
});
|
|
870
1063
|
}
|
|
871
1064
|
|
|
@@ -895,7 +1088,7 @@ async function appsCreateHandler(ctx) {
|
|
|
895
1088
|
writeLinkedState(projectDir, {
|
|
896
1089
|
app_id: app.id,
|
|
897
1090
|
linked_at: new Date().toISOString(),
|
|
898
|
-
});
|
|
1091
|
+
}, linkedStateProfileKey(ctx.runtime));
|
|
899
1092
|
}
|
|
900
1093
|
|
|
901
1094
|
return ctx.output.emitSuccess({
|
|
@@ -918,7 +1111,94 @@ async function appsCreateHandler(ctx) {
|
|
|
918
1111
|
|
|
919
1112
|
async function appsDevHandler(ctx) {
|
|
920
1113
|
const rootDir = resolveProjectDir(ctx.args.dir || '.');
|
|
921
|
-
const
|
|
1114
|
+
const skipRootRegistration = process.env.NOTIS_APPS_DEV_ALL_REGISTERED_ROOTS === '1';
|
|
1115
|
+
let appDirs = discoverAppDevLaunchProjects(rootDir, { skipRootRegistration });
|
|
1116
|
+
const discoveredAppDirs = [...appDirs];
|
|
1117
|
+
const sessionsFilePath = getAppDevSessionsFile();
|
|
1118
|
+
let sharedBundleBaseUrls = null;
|
|
1119
|
+
const sharedBundlesRaw = process.env.NOTIS_APPS_DEV_SHARED_BUNDLE_URLS;
|
|
1120
|
+
if (sharedBundlesRaw) {
|
|
1121
|
+
let parsed;
|
|
1122
|
+
try {
|
|
1123
|
+
parsed = JSON.parse(sharedBundlesRaw);
|
|
1124
|
+
} catch {
|
|
1125
|
+
throw usageError('The shared app development bundle map is invalid.');
|
|
1126
|
+
}
|
|
1127
|
+
if (!parsed || typeof parsed !== 'object' || Array.isArray(parsed)) {
|
|
1128
|
+
throw usageError('The shared app development bundle map must be an object.');
|
|
1129
|
+
}
|
|
1130
|
+
const sharedProjectDirs = Object.keys(parsed);
|
|
1131
|
+
const discoveredProjects = new Set(appDirs);
|
|
1132
|
+
for (const projectDir of sharedProjectDirs) {
|
|
1133
|
+
if (!discoveredProjects.has(projectDir)) {
|
|
1134
|
+
throw usageError(`The shared bundle map references an unregistered app: ${projectDir}.`);
|
|
1135
|
+
}
|
|
1136
|
+
}
|
|
1137
|
+
appDirs = sharedProjectDirs;
|
|
1138
|
+
sharedBundleBaseUrls = new Map();
|
|
1139
|
+
for (const projectDir of appDirs) {
|
|
1140
|
+
const value = parsed[projectDir];
|
|
1141
|
+
let url;
|
|
1142
|
+
try {
|
|
1143
|
+
url = new URL(String(value || ''));
|
|
1144
|
+
} catch {
|
|
1145
|
+
throw usageError(`The shared bundle URL is missing or invalid for ${projectDir}.`);
|
|
1146
|
+
}
|
|
1147
|
+
if (
|
|
1148
|
+
url.protocol !== 'http:'
|
|
1149
|
+
|| !['127.0.0.1', 'localhost', '::1', '[::1]'].includes(url.hostname)
|
|
1150
|
+
) {
|
|
1151
|
+
throw usageError(`The shared bundle URL must use loopback HTTP for ${projectDir}.`);
|
|
1152
|
+
}
|
|
1153
|
+
sharedBundleBaseUrls.set(projectDir, url.toString().replace(/\/$/, ''));
|
|
1154
|
+
}
|
|
1155
|
+
}
|
|
1156
|
+
|
|
1157
|
+
if (appDirs.length === 0) {
|
|
1158
|
+
return ctx.output.emitSuccess({
|
|
1159
|
+
command: ctx.spec.command_path.join(' '),
|
|
1160
|
+
data: {
|
|
1161
|
+
registered_root: skipRootRegistration ? null : rootDir,
|
|
1162
|
+
apps: [],
|
|
1163
|
+
watching_on_desktop_launch: true,
|
|
1164
|
+
},
|
|
1165
|
+
humanSummary: [
|
|
1166
|
+
skipRootRegistration
|
|
1167
|
+
? 'No registered Notis apps are present yet.'
|
|
1168
|
+
: `Registered app development root: ${rootDir}`,
|
|
1169
|
+
'No Notis apps are present yet. A running Notis Desktop will discover and mount apps created here automatically.',
|
|
1170
|
+
].join('\n'),
|
|
1171
|
+
hints: [
|
|
1172
|
+
{ command: 'notis apps roots list', reason: 'Show every persistent development root' },
|
|
1173
|
+
],
|
|
1174
|
+
meta: { mutating: true },
|
|
1175
|
+
});
|
|
1176
|
+
}
|
|
1177
|
+
|
|
1178
|
+
if (!sharedBundleBaseUrls) {
|
|
1179
|
+
sharedBundleBaseUrls = findSharedSourceBundleUrls(appDirs, sessionsFilePath);
|
|
1180
|
+
}
|
|
1181
|
+
let sourceHostLock = null;
|
|
1182
|
+
if (
|
|
1183
|
+
!sharedBundleBaseUrls
|
|
1184
|
+
&& process.env.NOTIS_APPS_DEV_HOST_LOCK_HELD !== '1'
|
|
1185
|
+
) {
|
|
1186
|
+
sourceHostLock = tryAcquireAppDevHostLock({
|
|
1187
|
+
identity: '__mac_user__',
|
|
1188
|
+
apiBase: 'loopback-source',
|
|
1189
|
+
projectDir: SHARED_APP_DEV_HOST_KEY,
|
|
1190
|
+
});
|
|
1191
|
+
if (!sourceHostLock) {
|
|
1192
|
+
const startedAt = Date.now();
|
|
1193
|
+
while (!sharedBundleBaseUrls && Date.now() - startedAt < 45_000) {
|
|
1194
|
+
await new Promise((resolvePromise) => setTimeout(resolvePromise, 100));
|
|
1195
|
+
sharedBundleBaseUrls = findSharedSourceBundleUrls(appDirs, sessionsFilePath);
|
|
1196
|
+
}
|
|
1197
|
+
if (!sharedBundleBaseUrls) {
|
|
1198
|
+
throw usageError('The shared app development host is busy but did not become ready. Retry after checking ~/.notis/app-dev-host.log.');
|
|
1199
|
+
}
|
|
1200
|
+
}
|
|
1201
|
+
}
|
|
922
1202
|
|
|
923
1203
|
for (const projectDir of appDirs) {
|
|
924
1204
|
const problems = detectProjectProblems(projectDir);
|
|
@@ -927,9 +1207,19 @@ async function appsDevHandler(ctx) {
|
|
|
927
1207
|
}
|
|
928
1208
|
}
|
|
929
1209
|
|
|
930
|
-
|
|
931
|
-
if (
|
|
932
|
-
|
|
1210
|
+
let port = null;
|
|
1211
|
+
if (sharedBundleBaseUrls) {
|
|
1212
|
+
// The source host owns the only loopback listener and build watchers.
|
|
1213
|
+
} else if (ctx.options.port) {
|
|
1214
|
+
port = Number.parseInt(ctx.options.port, 10);
|
|
1215
|
+
if (!Number.isInteger(port) || port <= 0 || port > 65535) {
|
|
1216
|
+
throw usageError('Port must be between 1 and 65535.');
|
|
1217
|
+
}
|
|
1218
|
+
} else {
|
|
1219
|
+
// Concurrent sessions must not fight over the default port: the desktop
|
|
1220
|
+
// auto-starts every known project at launch, so only the first one can
|
|
1221
|
+
// have 5173 and the rest fall back to an ephemeral port.
|
|
1222
|
+
port = await getAvailablePortPreferring(DEFAULT_DEV_PORT);
|
|
933
1223
|
}
|
|
934
1224
|
|
|
935
1225
|
const mode = getCliMode();
|
|
@@ -938,17 +1228,39 @@ async function appsDevHandler(ctx) {
|
|
|
938
1228
|
throw usageError('Could not determine the current user from the CLI credential. Run notis login and retry.');
|
|
939
1229
|
}
|
|
940
1230
|
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
1231
|
const sessionId = randomUUID();
|
|
951
|
-
const
|
|
1232
|
+
const profileKey = linkedStateProfileKey(ctx.runtime);
|
|
1233
|
+
const consumerMode = process.env.NOTIS_APPS_DEV_CONSUMER_MODE;
|
|
1234
|
+
const manualConsumerInstanceId = sharedBundleBaseUrls && !consumerMode
|
|
1235
|
+
? `cli.${process.pid}.${sessionId}`
|
|
1236
|
+
: null;
|
|
1237
|
+
let manualConsumerTimer = null;
|
|
1238
|
+
if (manualConsumerInstanceId) {
|
|
1239
|
+
const heartbeatManualConsumer = () => heartbeatAppDevConsumer({
|
|
1240
|
+
instanceId: manualConsumerInstanceId,
|
|
1241
|
+
userId: identity,
|
|
1242
|
+
apiBase,
|
|
1243
|
+
pid: process.pid,
|
|
1244
|
+
});
|
|
1245
|
+
heartbeatManualConsumer();
|
|
1246
|
+
manualConsumerTimer = setInterval(() => {
|
|
1247
|
+
try {
|
|
1248
|
+
heartbeatManualConsumer();
|
|
1249
|
+
} catch (error) {
|
|
1250
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
1251
|
+
process.stderr.write(`[notis apps dev] consumer heartbeat failed: ${message}\n`);
|
|
1252
|
+
}
|
|
1253
|
+
}, DEV_CONSUMER_HEARTBEAT_INTERVAL_MS);
|
|
1254
|
+
}
|
|
1255
|
+
// One app, one dataset: a dev session runs the app from local source against
|
|
1256
|
+
// the same databases the installed app uses, the way `npm run dev` serves the
|
|
1257
|
+
// same database as the deployed site. `--scratch` is the marked case; the
|
|
1258
|
+
// old `--live-data` flag now names the default and is kept as a no-op so
|
|
1259
|
+
// muscle memory and scripts keep working.
|
|
1260
|
+
if (ctx.options?.scratch && ctx.options?.liveData) {
|
|
1261
|
+
throw usageError('--scratch and --live-data contradict each other; pass at most one.');
|
|
1262
|
+
}
|
|
1263
|
+
const useInstalledDatabases = !ctx.options?.scratch;
|
|
952
1264
|
|
|
953
1265
|
const candidates = [];
|
|
954
1266
|
for (const projectDir of appDirs) {
|
|
@@ -956,147 +1268,206 @@ async function appsDevHandler(ctx) {
|
|
|
956
1268
|
if (!appConfig.name) {
|
|
957
1269
|
throw usageError(`notis.config.ts in ${projectDir} must define a non-empty name.`);
|
|
958
1270
|
}
|
|
959
|
-
const devSlug = buildDevInstallSlug(appConfig
|
|
1271
|
+
const devSlug = buildDevInstallSlug(appConfig);
|
|
960
1272
|
if (!devSlug) {
|
|
961
|
-
throw usageError(`notis.config.ts name in ${projectDir} must slugify to a non-empty value.`);
|
|
1273
|
+
throw usageError(`notis.config.ts devSlug or name in ${projectDir} must slugify to a non-empty value.`);
|
|
962
1274
|
}
|
|
1275
|
+
candidates.push({ appConfig, devSlug, projectDir });
|
|
1276
|
+
}
|
|
1277
|
+
|
|
1278
|
+
const canonicalSelection = selectCanonicalDevApps(candidates, readAppDevRoots());
|
|
1279
|
+
const canonicalCandidates = [];
|
|
1280
|
+
for (const { appConfig, devSlug, projectDir } of canonicalSelection.selected) {
|
|
963
1281
|
// Sequential on purpose: the shell-consent prompt must never interleave
|
|
964
1282
|
// with another app's, and the ensure fan-out below runs in parallel.
|
|
965
1283
|
const approvedCapabilities = await resolveCloudShellConsent({
|
|
966
1284
|
appConfig,
|
|
967
1285
|
projectDir,
|
|
968
1286
|
grantCloudShell: Boolean(ctx.options?.grantCloudShell),
|
|
1287
|
+
profileKey,
|
|
1288
|
+
});
|
|
1289
|
+
canonicalCandidates.push({
|
|
1290
|
+
appConfig,
|
|
1291
|
+
devSlug,
|
|
1292
|
+
projectDir,
|
|
1293
|
+
approvedCapabilities,
|
|
1294
|
+
mountNonce: randomUUID(),
|
|
969
1295
|
});
|
|
970
|
-
candidates.push({ appConfig, devSlug, projectDir, approvedCapabilities });
|
|
971
1296
|
}
|
|
972
1297
|
|
|
973
|
-
|
|
974
|
-
|
|
975
|
-
|
|
976
|
-
|
|
977
|
-
|
|
978
|
-
|
|
1298
|
+
// Start every build watcher before the backend registrations finish. A
|
|
1299
|
+
// machine with several apps must not show an empty sidebar for minutes while
|
|
1300
|
+
// unrelated registrations run sequentially. Each successful registration is
|
|
1301
|
+
// published below as soon as its own last-known bundle is ready.
|
|
1302
|
+
const devServer = sharedBundleBaseUrls
|
|
1303
|
+
? {
|
|
1304
|
+
close: async () => {},
|
|
1305
|
+
isBundleReady: () => true,
|
|
1306
|
+
updateApp: () => {},
|
|
1307
|
+
waitForBundle: async () => {},
|
|
1308
|
+
}
|
|
1309
|
+
: await startAppDevServer({
|
|
1310
|
+
apps: canonicalCandidates.map((app) => ({
|
|
1311
|
+
slug: app.devSlug,
|
|
1312
|
+
projectDir: app.projectDir,
|
|
1313
|
+
appId: null,
|
|
1314
|
+
targetAppId: null,
|
|
1315
|
+
userId: identity,
|
|
1316
|
+
profileKey,
|
|
1317
|
+
sessionId,
|
|
1318
|
+
mountNonce: app.mountNonce,
|
|
1319
|
+
})),
|
|
1320
|
+
port,
|
|
1321
|
+
sessionsFilePath,
|
|
1322
|
+
});
|
|
1323
|
+
|
|
1324
|
+
let heartbeatTimer = setInterval(() => {
|
|
1325
|
+
try {
|
|
1326
|
+
heartbeatAppDevSession(sessionId, new Date().toISOString(), sessionsFilePath);
|
|
1327
|
+
} catch (error) {
|
|
1328
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
1329
|
+
process.stderr.write(`[notis apps dev] heartbeat failed: ${message}\n`);
|
|
979
1330
|
}
|
|
980
|
-
|
|
981
|
-
}
|
|
1331
|
+
}, DEV_HEARTBEAT_INTERVAL_MS);
|
|
982
1332
|
|
|
983
1333
|
const registrationStartedAt = process.hrtime.bigint();
|
|
984
|
-
const
|
|
985
|
-
|
|
986
|
-
|
|
987
|
-
|
|
988
|
-
|
|
989
|
-
|
|
990
|
-
|
|
991
|
-
|
|
992
|
-
|
|
993
|
-
|
|
994
|
-
|
|
995
|
-
|
|
996
|
-
|
|
997
|
-
|
|
998
|
-
|
|
999
|
-
|
|
1000
|
-
|
|
1001
|
-
|
|
1002
|
-
|
|
1003
|
-
|
|
1004
|
-
|
|
1005
|
-
|
|
1006
|
-
|
|
1007
|
-
|
|
1008
|
-
|
|
1009
|
-
|
|
1010
|
-
|
|
1011
|
-
|
|
1012
|
-
|
|
1013
|
-
|
|
1014
|
-
|
|
1015
|
-
|
|
1016
|
-
|
|
1334
|
+
const apps = [];
|
|
1335
|
+
const registrationWarnings = [];
|
|
1336
|
+
let firstRegistrationError = null;
|
|
1337
|
+
// The local development backend is deliberately a single worker. Register
|
|
1338
|
+
// apps sequentially so a large root cannot queue many long mutations behind
|
|
1339
|
+
// one another. Unlike the old all-or-nothing startup, publish each healthy
|
|
1340
|
+
// app immediately while later registrations continue in the background.
|
|
1341
|
+
for (const {
|
|
1342
|
+
appConfig,
|
|
1343
|
+
devSlug,
|
|
1344
|
+
projectDir,
|
|
1345
|
+
approvedCapabilities,
|
|
1346
|
+
mountNonce,
|
|
1347
|
+
} of canonicalCandidates) {
|
|
1348
|
+
const appStartedAt = process.hrtime.bigint();
|
|
1349
|
+
try {
|
|
1350
|
+
const registeredApp = await ensureDevInstall({
|
|
1351
|
+
ctx,
|
|
1352
|
+
appConfig,
|
|
1353
|
+
projectDir,
|
|
1354
|
+
idempotencyKey: nextDevInstallIdempotencyKey(ctx.globalOptions, devSlug),
|
|
1355
|
+
useInstalledDatabases,
|
|
1356
|
+
approvedCapabilities,
|
|
1357
|
+
});
|
|
1358
|
+
const bundleBaseUrl = sharedBundleBaseUrls?.get(registeredApp.projectDir)
|
|
1359
|
+
|| `http://127.0.0.1:${port}/a/${registeredApp.devSlug}`;
|
|
1360
|
+
const app = {
|
|
1361
|
+
...registeredApp,
|
|
1362
|
+
bundleBaseUrl,
|
|
1363
|
+
mountNonce,
|
|
1364
|
+
appHref: buildDevelopmentAppHref({
|
|
1365
|
+
appSlug: registeredApp.slug,
|
|
1366
|
+
appId: registeredApp.appId,
|
|
1367
|
+
devSlug: registeredApp.devSlug,
|
|
1368
|
+
targetAppId: registeredApp.targetAppId,
|
|
1369
|
+
targetAppSlug: registeredApp.targetAppSlug,
|
|
1370
|
+
manifest: registeredApp.manifest,
|
|
1371
|
+
}),
|
|
1372
|
+
};
|
|
1373
|
+
devServer.updateApp(app.devSlug, {
|
|
1374
|
+
appId: app.appId,
|
|
1375
|
+
targetAppId: app.targetAppId || null,
|
|
1376
|
+
});
|
|
1377
|
+
const now = new Date().toISOString();
|
|
1378
|
+
const sessionRecord = {
|
|
1379
|
+
sessionId,
|
|
1380
|
+
hostPid: process.pid,
|
|
1381
|
+
sourceHost: !sharedBundleBaseUrls,
|
|
1382
|
+
bundleReady: devServer.isBundleReady(app.devSlug),
|
|
1383
|
+
...(!sharedBundleBaseUrls ? {
|
|
1384
|
+
discoveredProjects: discoveredAppDirs,
|
|
1385
|
+
canonicalProjects: canonicalCandidates.map((candidate) => candidate.projectDir),
|
|
1386
|
+
} : {}),
|
|
1387
|
+
userId: identity,
|
|
1388
|
+
apiBase,
|
|
1389
|
+
profileKey,
|
|
1017
1390
|
appId: app.appId,
|
|
1391
|
+
targetAppId: app.targetAppId || undefined,
|
|
1392
|
+
mountNonce: app.mountNonce,
|
|
1018
1393
|
devSlug: app.devSlug,
|
|
1019
|
-
|
|
1020
|
-
|
|
1021
|
-
|
|
1022
|
-
|
|
1023
|
-
|
|
1024
|
-
|
|
1025
|
-
|
|
1026
|
-
|
|
1027
|
-
|
|
1028
|
-
|
|
1029
|
-
|
|
1030
|
-
|
|
1031
|
-
|
|
1032
|
-
|
|
1033
|
-
|
|
1034
|
-
|
|
1035
|
-
|
|
1036
|
-
|
|
1037
|
-
|
|
1038
|
-
|
|
1039
|
-
|
|
1040
|
-
|
|
1041
|
-
|
|
1042
|
-
|
|
1043
|
-
|
|
1044
|
-
|
|
1045
|
-
|
|
1046
|
-
|
|
1047
|
-
|
|
1048
|
-
|
|
1049
|
-
|
|
1050
|
-
|
|
1051
|
-
|
|
1052
|
-
|
|
1053
|
-
|
|
1054
|
-
|
|
1055
|
-
appId: app.appId,
|
|
1056
|
-
targetAppId: app.targetAppId || undefined,
|
|
1057
|
-
mountNonce: app.mountNonce,
|
|
1058
|
-
devSlug: app.devSlug,
|
|
1059
|
-
bundleBaseUrl: app.bundleBaseUrl,
|
|
1060
|
-
projectDir: app.projectDir,
|
|
1061
|
-
startedAt: now,
|
|
1062
|
-
lastHeartbeatAt: now,
|
|
1063
|
-
desktopAppName,
|
|
1064
|
-
})), sessionsFilePath);
|
|
1065
|
-
} catch (error) {
|
|
1394
|
+
bundleBaseUrl: app.bundleBaseUrl,
|
|
1395
|
+
projectDir: app.projectDir,
|
|
1396
|
+
startedAt: now,
|
|
1397
|
+
lastHeartbeatAt: now,
|
|
1398
|
+
};
|
|
1399
|
+
upsertAppDevSessions(sessionRecord, sessionsFilePath);
|
|
1400
|
+
apps.push(app);
|
|
1401
|
+
if (!sessionRecord.bundleReady) {
|
|
1402
|
+
void devServer.waitForBundle(app.devSlug).then(() => {
|
|
1403
|
+
upsertAppDevSessions({
|
|
1404
|
+
...sessionRecord,
|
|
1405
|
+
bundleReady: true,
|
|
1406
|
+
lastHeartbeatAt: new Date().toISOString(),
|
|
1407
|
+
}, sessionsFilePath);
|
|
1408
|
+
}).catch((error) => {
|
|
1409
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
1410
|
+
process.stderr.write(`[notis apps dev] ${app.devSlug}: initial bundle failed: ${message}\n`);
|
|
1411
|
+
});
|
|
1412
|
+
}
|
|
1413
|
+
} catch (error) {
|
|
1414
|
+
firstRegistrationError ||= error;
|
|
1415
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
1416
|
+
registrationWarnings.push(
|
|
1417
|
+
`${projectDir} could not be mounted: ${message}. It will be retried when the shared host reconciles again.`,
|
|
1418
|
+
);
|
|
1419
|
+
process.stderr.write(`[notis apps dev] ${devSlug}: registration failed: ${message}\n`);
|
|
1420
|
+
} finally {
|
|
1421
|
+
logAppsTiming('ensure-dev-install', {
|
|
1422
|
+
slug: devSlug,
|
|
1423
|
+
ms: timingMs(appStartedAt).toFixed(1),
|
|
1424
|
+
});
|
|
1425
|
+
}
|
|
1426
|
+
}
|
|
1427
|
+
if (apps.length === 0) {
|
|
1428
|
+
clearInterval(heartbeatTimer);
|
|
1429
|
+
heartbeatTimer = null;
|
|
1066
1430
|
try {
|
|
1067
1431
|
await devServer.close();
|
|
1068
1432
|
} catch {
|
|
1069
|
-
//
|
|
1433
|
+
// preserve the original registration error
|
|
1070
1434
|
}
|
|
1071
|
-
|
|
1435
|
+
if (sourceHostLock) {
|
|
1436
|
+
releaseAppDevHostLock(sourceHostLock);
|
|
1437
|
+
sourceHostLock = null;
|
|
1438
|
+
}
|
|
1439
|
+
if (manualConsumerTimer) clearInterval(manualConsumerTimer);
|
|
1440
|
+
if (manualConsumerInstanceId) {
|
|
1441
|
+
try {
|
|
1442
|
+
removeAppDevConsumer(manualConsumerInstanceId);
|
|
1443
|
+
} catch {
|
|
1444
|
+
// Preserve the registration error; a failed lease cleanup expires.
|
|
1445
|
+
}
|
|
1446
|
+
}
|
|
1447
|
+
throw firstRegistrationError || usageError('No unambiguous development apps could be mounted.');
|
|
1072
1448
|
}
|
|
1449
|
+
logAppsTiming('ensure-dev-install:all', {
|
|
1450
|
+
apps: apps.length,
|
|
1451
|
+
ms: timingMs(registrationStartedAt).toFixed(1),
|
|
1452
|
+
});
|
|
1073
1453
|
|
|
1074
|
-
|
|
1075
|
-
|
|
1076
|
-
|
|
1077
|
-
|
|
1078
|
-
|
|
1079
|
-
|
|
1080
|
-
}
|
|
1081
|
-
}, DEV_HEARTBEAT_INTERVAL_MS);
|
|
1454
|
+
const warnings = [
|
|
1455
|
+
...canonicalSelection.warnings,
|
|
1456
|
+
...registrationWarnings,
|
|
1457
|
+
...databaseMaterializationWarnings(apps),
|
|
1458
|
+
...liveDataWarnings(apps),
|
|
1459
|
+
];
|
|
1082
1460
|
|
|
1083
|
-
|
|
1084
|
-
sessionId,
|
|
1085
|
-
appId: app.appId,
|
|
1086
|
-
devSlug: app.devSlug,
|
|
1087
|
-
mountNonce: app.mountNonce,
|
|
1088
|
-
}));
|
|
1461
|
+
let consumerTimer = null;
|
|
1089
1462
|
|
|
1090
1463
|
ctx.output.emitSuccess({
|
|
1091
1464
|
command: ctx.spec.command_path.join(' '),
|
|
1092
1465
|
data: {
|
|
1093
1466
|
mode,
|
|
1094
1467
|
api_base: apiBase,
|
|
1095
|
-
development_url: developmentTabUrl,
|
|
1096
1468
|
session_id: sessionId,
|
|
1097
1469
|
mount_status: 'serving',
|
|
1098
|
-
|
|
1099
|
-
desktop_target: desktopAppName,
|
|
1470
|
+
source_host: !sharedBundleBaseUrls,
|
|
1100
1471
|
identity,
|
|
1101
1472
|
apps: apps.map((app) => ({
|
|
1102
1473
|
slug: app.devSlug,
|
|
@@ -1115,76 +1486,22 @@ async function appsDevHandler(ctx) {
|
|
|
1115
1486
|
warnings,
|
|
1116
1487
|
humanSummary: [
|
|
1117
1488
|
`Running apps dev against ${apiBase} as ${identity} (mode: ${mode})`,
|
|
1118
|
-
`
|
|
1489
|
+
...(!skipRootRegistration ? [`Registered development root: ${rootDir}`] : []),
|
|
1490
|
+
`Watching ${readAppDevRoots().roots.length} persistent development root(s).`,
|
|
1119
1491
|
...(useInstalledDatabases
|
|
1120
1492
|
? [`Databases: ${apps.filter((app) => app.liveData?.enabled).length}/${apps.length} app(s) reading the installed app's live rows`]
|
|
1121
1493
|
: []),
|
|
1122
1494
|
'',
|
|
1123
|
-
`Open in desktop: ${developmentTabUrl}`,
|
|
1124
|
-
'',
|
|
1125
1495
|
...apps.map((app) => ` ${app.name.padEnd(24)} ${app.bundleBaseUrl} -> ${app.appHref}`),
|
|
1126
1496
|
'',
|
|
1127
|
-
|
|
1497
|
+
sharedBundleBaseUrls
|
|
1498
|
+
? `Mounted ${apps.length} shared source app${apps.length === 1 ? '' : 's'} for this authenticated environment.`
|
|
1499
|
+
: `Serving one shared loopback host for ${apps.length} app${apps.length === 1 ? '' : 's'}.`,
|
|
1128
1500
|
'',
|
|
1129
1501
|
'Press Ctrl-C to stop.',
|
|
1130
1502
|
].join('\n'),
|
|
1131
1503
|
});
|
|
1132
1504
|
|
|
1133
|
-
if (shouldOpenDevelopmentTab(ctx.options)) {
|
|
1134
|
-
openInBrowser(desktopWakeUrl, {
|
|
1135
|
-
appName: desktopAppName,
|
|
1136
|
-
bundleId: desktopBundleId,
|
|
1137
|
-
scheme: desktopScheme,
|
|
1138
|
-
});
|
|
1139
|
-
}
|
|
1140
|
-
|
|
1141
|
-
void (async () => {
|
|
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
|
-
});
|
|
1187
|
-
|
|
1188
1505
|
let shuttingDown = false;
|
|
1189
1506
|
const shutdown = async (signal) => {
|
|
1190
1507
|
if (shuttingDown) return;
|
|
@@ -1194,6 +1511,21 @@ async function appsDevHandler(ctx) {
|
|
|
1194
1511
|
clearInterval(heartbeatTimer);
|
|
1195
1512
|
heartbeatTimer = null;
|
|
1196
1513
|
}
|
|
1514
|
+
if (consumerTimer) {
|
|
1515
|
+
clearInterval(consumerTimer);
|
|
1516
|
+
consumerTimer = null;
|
|
1517
|
+
}
|
|
1518
|
+
if (manualConsumerTimer) {
|
|
1519
|
+
clearInterval(manualConsumerTimer);
|
|
1520
|
+
manualConsumerTimer = null;
|
|
1521
|
+
}
|
|
1522
|
+
if (manualConsumerInstanceId) {
|
|
1523
|
+
try {
|
|
1524
|
+
removeAppDevConsumer(manualConsumerInstanceId);
|
|
1525
|
+
} catch {
|
|
1526
|
+
// A crashed CLI lease expires automatically after the heartbeat window.
|
|
1527
|
+
}
|
|
1528
|
+
}
|
|
1197
1529
|
try {
|
|
1198
1530
|
removeAppDevSession(sessionId, sessionsFilePath);
|
|
1199
1531
|
} catch {
|
|
@@ -1204,6 +1536,10 @@ async function appsDevHandler(ctx) {
|
|
|
1204
1536
|
} catch {
|
|
1205
1537
|
// ignore cleanup failures during shutdown
|
|
1206
1538
|
}
|
|
1539
|
+
if (sourceHostLock) {
|
|
1540
|
+
releaseAppDevHostLock(sourceHostLock);
|
|
1541
|
+
sourceHostLock = null;
|
|
1542
|
+
}
|
|
1207
1543
|
process.exit(EXIT_CODES.ok);
|
|
1208
1544
|
};
|
|
1209
1545
|
|
|
@@ -1214,11 +1550,53 @@ async function appsDevHandler(ctx) {
|
|
|
1214
1550
|
void shutdown('SIGTERM');
|
|
1215
1551
|
});
|
|
1216
1552
|
|
|
1553
|
+
if (consumerMode === 'machine' || consumerMode === 'environment') {
|
|
1554
|
+
consumerTimer = setInterval(() => {
|
|
1555
|
+
if (!hasAppDevConsumer(readAppDevConsumers(), {
|
|
1556
|
+
mode: consumerMode,
|
|
1557
|
+
userId: identity,
|
|
1558
|
+
apiBase,
|
|
1559
|
+
})) {
|
|
1560
|
+
void shutdown('last consumer detached');
|
|
1561
|
+
}
|
|
1562
|
+
}, DEV_CONSUMER_POLL_INTERVAL_MS);
|
|
1563
|
+
}
|
|
1564
|
+
|
|
1217
1565
|
await new Promise(() => {});
|
|
1218
1566
|
|
|
1219
1567
|
return EXIT_CODES.ok;
|
|
1220
1568
|
}
|
|
1221
1569
|
|
|
1570
|
+
async function appsRootsListHandler(ctx) {
|
|
1571
|
+
const registry = readAppDevRoots();
|
|
1572
|
+
return ctx.output.emitSuccess({
|
|
1573
|
+
command: ctx.spec.command_path.join(' '),
|
|
1574
|
+
data: { roots: registry.roots },
|
|
1575
|
+
humanSummary: appDevRootsTable(registry.roots),
|
|
1576
|
+
meta: { mutating: false },
|
|
1577
|
+
});
|
|
1578
|
+
}
|
|
1579
|
+
|
|
1580
|
+
async function appsRootsRemoveHandler(ctx) {
|
|
1581
|
+
const rootDir = resolve(ctx.args.dir);
|
|
1582
|
+
const result = removeAppDevRoot(rootDir);
|
|
1583
|
+
return ctx.output.emitSuccess({
|
|
1584
|
+
command: ctx.spec.command_path.join(' '),
|
|
1585
|
+
data: {
|
|
1586
|
+
removed: result.removed,
|
|
1587
|
+
root: result.path,
|
|
1588
|
+
roots: result.registry.roots,
|
|
1589
|
+
},
|
|
1590
|
+
humanSummary: result.removed
|
|
1591
|
+
? `Removed app development root: ${result.path}`
|
|
1592
|
+
: `App development root was not registered: ${result.path}`,
|
|
1593
|
+
hints: result.removed
|
|
1594
|
+
? []
|
|
1595
|
+
: [{ command: 'notis apps roots list', reason: 'Show registered roots' }],
|
|
1596
|
+
meta: { mutating: result.removed },
|
|
1597
|
+
});
|
|
1598
|
+
}
|
|
1599
|
+
|
|
1222
1600
|
async function appsBuildHandler(ctx) {
|
|
1223
1601
|
const projectDir = resolveProjectDir(ctx.args.dir || '.');
|
|
1224
1602
|
const problems = detectProjectProblems(projectDir);
|
|
@@ -1260,7 +1638,7 @@ async function appsVerifyHandler(ctx) {
|
|
|
1260
1638
|
if (!ctx.runtime.jwt) {
|
|
1261
1639
|
throw usageError('Live verify mode requires CLI auth. Run notis login and retry.');
|
|
1262
1640
|
}
|
|
1263
|
-
linkedState = readLinkedState(projectDir);
|
|
1641
|
+
linkedState = readLinkedState(projectDir, linkedStateProfileKey(ctx.runtime));
|
|
1264
1642
|
if (!linkedState?.app_id) {
|
|
1265
1643
|
throw usageError('Live verify mode requires a linked app. Run `notis apps link <app-id> .` first.');
|
|
1266
1644
|
}
|
|
@@ -1493,7 +1871,7 @@ async function appsScreenshotHandler(ctx) {
|
|
|
1493
1871
|
if (!ctx.runtime.jwt) {
|
|
1494
1872
|
throw usageError('Live mode requires CLI auth. Run notis login and retry.');
|
|
1495
1873
|
}
|
|
1496
|
-
linkedState = readLinkedState(projectDir);
|
|
1874
|
+
linkedState = readLinkedState(projectDir, linkedStateProfileKey(ctx.runtime));
|
|
1497
1875
|
if (!linkedState?.app_id) {
|
|
1498
1876
|
throw usageError('Live mode requires a linked app. Run `notis apps link <app-id> .` first.');
|
|
1499
1877
|
}
|
|
@@ -1692,10 +2070,15 @@ async function appsScreenshotHandler(ctx) {
|
|
|
1692
2070
|
}
|
|
1693
2071
|
}
|
|
1694
2072
|
|
|
1695
|
-
export function buildLinkedAppState(
|
|
2073
|
+
export function buildLinkedAppState(
|
|
2074
|
+
existingState,
|
|
2075
|
+
appId,
|
|
2076
|
+
linkedAt = new Date().toISOString(),
|
|
2077
|
+
) {
|
|
2078
|
+
const sameIdPromotion = existingState?.dev_app_id === appId;
|
|
1696
2079
|
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 } : {}),
|
|
2080
|
+
...(!sameIdPromotion && existingState?.dev_app_id ? { dev_app_id: existingState.dev_app_id } : {}),
|
|
2081
|
+
...(!sameIdPromotion && existingState?.dev_linked_at ? { dev_linked_at: existingState.dev_linked_at } : {}),
|
|
1699
2082
|
app_id: appId,
|
|
1700
2083
|
linked_at: linkedAt,
|
|
1701
2084
|
};
|
|
@@ -1709,7 +2092,12 @@ async function appsLinkHandler(ctx) {
|
|
|
1709
2092
|
|
|
1710
2093
|
writeLinkedState(
|
|
1711
2094
|
projectDir,
|
|
1712
|
-
buildLinkedAppState(
|
|
2095
|
+
buildLinkedAppState(
|
|
2096
|
+
readLinkedState(projectDir, linkedStateProfileKey(ctx.runtime)),
|
|
2097
|
+
appId,
|
|
2098
|
+
new Date().toISOString(),
|
|
2099
|
+
),
|
|
2100
|
+
linkedStateProfileKey(ctx.runtime),
|
|
1713
2101
|
);
|
|
1714
2102
|
|
|
1715
2103
|
return ctx.output.emitSuccess({
|
|
@@ -1736,9 +2124,11 @@ async function appsPullHandler(ctx) {
|
|
|
1736
2124
|
throw usageError('Pulling app source requires a current OAuth grant. Run `notis login` and retry.');
|
|
1737
2125
|
}
|
|
1738
2126
|
const app = result.payload?.app || {};
|
|
1739
|
-
const defaultDir = app.slug || slugify(app.name) || appId;
|
|
1740
|
-
const targetDir =
|
|
1741
|
-
|
|
2127
|
+
const defaultDir = slugify(app.slug) || slugify(app.name) || slugify(appId);
|
|
2128
|
+
const targetDir = ctx.args.dir
|
|
2129
|
+
? resolveProjectDir(ctx.args.dir)
|
|
2130
|
+
: defaultAppProjectDir(defaultDir);
|
|
2131
|
+
const version = ctx.options.sourceVersion || 'latest';
|
|
1742
2132
|
|
|
1743
2133
|
const pulled = await pullAppSource({
|
|
1744
2134
|
apiBase: ctx.runtime.apiBase,
|
|
@@ -1747,6 +2137,7 @@ async function appsPullHandler(ctx) {
|
|
|
1747
2137
|
targetDir,
|
|
1748
2138
|
version,
|
|
1749
2139
|
force: Boolean(ctx.options.force),
|
|
2140
|
+
profileKey: linkedStateProfileKey(ctx.runtime),
|
|
1750
2141
|
});
|
|
1751
2142
|
|
|
1752
2143
|
const versionLabel = pulled.version === 'latest' ? 'latest version' : `v${pulled.version}`;
|
|
@@ -1761,7 +2152,7 @@ async function appsPullHandler(ctx) {
|
|
|
1761
2152
|
});
|
|
1762
2153
|
}
|
|
1763
2154
|
|
|
1764
|
-
function updateLinkedDeployState(projectDir, linkedState, appId, version) {
|
|
2155
|
+
function updateLinkedDeployState(projectDir, linkedState, appId, version, profileKey = null) {
|
|
1765
2156
|
if (!linkedState || linkedState.app_id !== appId || !Number.isFinite(version)) {
|
|
1766
2157
|
return;
|
|
1767
2158
|
}
|
|
@@ -1771,14 +2162,29 @@ function updateLinkedDeployState(projectDir, linkedState, appId, version) {
|
|
|
1771
2162
|
version,
|
|
1772
2163
|
linked_at: linkedState.linked_at || new Date().toISOString(),
|
|
1773
2164
|
deployed_at: new Date().toISOString(),
|
|
1774
|
-
});
|
|
2165
|
+
}, profileKey);
|
|
2166
|
+
}
|
|
2167
|
+
|
|
2168
|
+
// Resolve first deploy without mutating. Promotion is part of the final
|
|
2169
|
+
// SAVE_APP_FILES update, after build and uploads succeed, so a failed deploy
|
|
2170
|
+
// cannot leave an empty installed app behind.
|
|
2171
|
+
function resolveDeployTarget(ctx, projectDir) {
|
|
2172
|
+
if (ctx.options.appId) return { appId: ctx.options.appId, needsPromotion: false };
|
|
2173
|
+
const profileKey = linkedStateProfileKey(ctx.runtime);
|
|
2174
|
+
const state = readLinkedState(projectDir, profileKey);
|
|
2175
|
+
if (state?.app_id) return { appId: state.app_id, needsPromotion: false };
|
|
2176
|
+
if (!state?.dev_app_id) {
|
|
2177
|
+
return { appId: requireLinkedAppId(projectDir, ctx.options.appId, profileKey), needsPromotion: false };
|
|
2178
|
+
}
|
|
2179
|
+
return { appId: state.dev_app_id, needsPromotion: true };
|
|
1775
2180
|
}
|
|
1776
2181
|
|
|
1777
2182
|
async function appsDeployHandler(ctx) {
|
|
1778
2183
|
const projectDir = resolveProjectDir(ctx.args.dir || '.');
|
|
1779
|
-
const
|
|
2184
|
+
const profileKey = linkedStateProfileKey(ctx.runtime);
|
|
2185
|
+
const { appId, needsPromotion } = resolveDeployTarget(ctx, projectDir);
|
|
1780
2186
|
const idempotencyKey = nextIdempotencyKey(ctx.globalOptions);
|
|
1781
|
-
const linkedState = readLinkedState(projectDir);
|
|
2187
|
+
const linkedState = readLinkedState(projectDir, profileKey);
|
|
1782
2188
|
const baseVersion = linkedState?.app_id === appId && Number.isFinite(linkedState?.version)
|
|
1783
2189
|
? linkedState.version
|
|
1784
2190
|
: undefined;
|
|
@@ -1792,9 +2198,12 @@ async function appsDeployHandler(ctx) {
|
|
|
1792
2198
|
|
|
1793
2199
|
// Direct deploy mode: upload to Supabase storage directly
|
|
1794
2200
|
if (ctx.options.direct) {
|
|
2201
|
+
if (needsPromotion) {
|
|
2202
|
+
throw usageError('The first deploy must use the backend so promotion and save are atomic. Retry without --direct.');
|
|
2203
|
+
}
|
|
1795
2204
|
await assertDirectDeployAccess(ctx.runtime, appId);
|
|
1796
2205
|
const { version } = await directDeploy(projectDir, appId);
|
|
1797
|
-
updateLinkedDeployState(projectDir, linkedState, appId, version);
|
|
2206
|
+
updateLinkedDeployState(projectDir, linkedState, appId, version, profileKey);
|
|
1798
2207
|
return ctx.output.emitSuccess({
|
|
1799
2208
|
command: ctx.spec.command_path.join(' '),
|
|
1800
2209
|
data: { app_id: appId, version, mode: 'direct' },
|
|
@@ -1827,6 +2236,7 @@ async function appsDeployHandler(ctx) {
|
|
|
1827
2236
|
manifest,
|
|
1828
2237
|
...appRowFieldsFromManifest(manifest),
|
|
1829
2238
|
...(baseVersion !== undefined ? { base_version: baseVersion } : {}),
|
|
2239
|
+
...(needsPromotion ? { promote_dev_app: true } : {}),
|
|
1830
2240
|
},
|
|
1831
2241
|
mutating: true,
|
|
1832
2242
|
idempotencyKey,
|
|
@@ -1868,7 +2278,35 @@ async function appsDeployHandler(ctx) {
|
|
|
1868
2278
|
});
|
|
1869
2279
|
}
|
|
1870
2280
|
|
|
1871
|
-
|
|
2281
|
+
if (needsPromotion) {
|
|
2282
|
+
const now = new Date().toISOString();
|
|
2283
|
+
writeLinkedState(projectDir, {
|
|
2284
|
+
...linkedState,
|
|
2285
|
+
app_id: appId,
|
|
2286
|
+
linked_at: linkedState?.linked_at || now,
|
|
2287
|
+
version: deployedVersion,
|
|
2288
|
+
deployed_at: now,
|
|
2289
|
+
dev_app_id: undefined,
|
|
2290
|
+
dev_linked_at: undefined,
|
|
2291
|
+
}, profileKey);
|
|
2292
|
+
try {
|
|
2293
|
+
linkAppDevSessionTarget(
|
|
2294
|
+
{ appId, targetAppId: appId },
|
|
2295
|
+
getAppDevSessionsFile(),
|
|
2296
|
+
);
|
|
2297
|
+
} catch (error) {
|
|
2298
|
+
ctx.output.emitProgress({
|
|
2299
|
+
phase: 'warning',
|
|
2300
|
+
message: `Deploy succeeded, but the local development session could not be retargeted: ${error.message}`,
|
|
2301
|
+
});
|
|
2302
|
+
}
|
|
2303
|
+
ctx.output.emitProgress({
|
|
2304
|
+
phase: 'promote',
|
|
2305
|
+
message: `Promoted development app to installed app ${appId}`,
|
|
2306
|
+
});
|
|
2307
|
+
} else {
|
|
2308
|
+
updateLinkedDeployState(projectDir, linkedState, appId, deployedVersion, profileKey);
|
|
2309
|
+
}
|
|
1872
2310
|
return ctx.output.emitSuccess({
|
|
1873
2311
|
command: ctx.spec.command_path.join(' '),
|
|
1874
2312
|
data: {
|
|
@@ -1891,7 +2329,7 @@ async function appsDuplicateHandler(ctx) {
|
|
|
1891
2329
|
const projectDir = resolveProjectDir(ctx.args.dir || '.');
|
|
1892
2330
|
// Either target an app explicitly, or duplicate whatever this project is
|
|
1893
2331
|
// linked to, so `notis apps duplicate` works from inside a project.
|
|
1894
|
-
const appId = requireLinkedAppId(projectDir, ctx.options.appId);
|
|
2332
|
+
const appId = requireLinkedAppId(projectDir, ctx.options.appId, linkedStateProfileKey(ctx.runtime));
|
|
1895
2333
|
const idempotencyKey = nextIdempotencyKey(ctx.globalOptions);
|
|
1896
2334
|
|
|
1897
2335
|
const copyDocuments = ctx.options.copyDocuments || 'declared';
|
|
@@ -1957,8 +2395,8 @@ async function appsPublishHandler(ctx) {
|
|
|
1957
2395
|
}
|
|
1958
2396
|
|
|
1959
2397
|
const projectDir = resolveProjectDir(ctx.args.dir || '.');
|
|
1960
|
-
const appId = requireLinkedAppId(projectDir, ctx.options.appId);
|
|
1961
|
-
const linkedState = readLinkedState(projectDir);
|
|
2398
|
+
const appId = requireLinkedAppId(projectDir, ctx.options.appId, linkedStateProfileKey(ctx.runtime));
|
|
2399
|
+
const linkedState = readLinkedState(projectDir, linkedStateProfileKey(ctx.runtime));
|
|
1962
2400
|
const appConfig = await loadAppConfig(projectDir);
|
|
1963
2401
|
const readiness = inspectListingReadiness(projectDir, appConfig);
|
|
1964
2402
|
if (!readiness.ready) {
|
|
@@ -2051,7 +2489,7 @@ async function appsDoctorHandler(ctx) {
|
|
|
2051
2489
|
}
|
|
2052
2490
|
}
|
|
2053
2491
|
|
|
2054
|
-
const linkedState = readLinkedState(projectDir);
|
|
2492
|
+
const linkedState = readLinkedState(projectDir, linkedStateProfileKey(ctx.runtime));
|
|
2055
2493
|
const status = problems.length ? 'unhealthy' : warnings.length ? 'warnings' : 'healthy';
|
|
2056
2494
|
|
|
2057
2495
|
return ctx.output.emitSuccess({
|
|
@@ -2094,21 +2532,21 @@ export const appsCommandSpecs = [
|
|
|
2094
2532
|
{
|
|
2095
2533
|
command_path: ['apps', 'init'],
|
|
2096
2534
|
summary: 'Scaffold a new Notis app project.',
|
|
2097
|
-
when_to_use: 'Start a new Notis app. Use --from with a
|
|
2535
|
+
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
2536
|
args_schema: {
|
|
2099
2537
|
arguments: [
|
|
2100
2538
|
{ token: '<name>', description: 'Display name for the app.' },
|
|
2101
|
-
{ token: '[dir]', key: 'dir', description: 'Target directory
|
|
2539
|
+
{ 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
2540
|
],
|
|
2103
2541
|
options: [
|
|
2104
|
-
{ flags: '--from <slug>', description: 'Start from a
|
|
2542
|
+
{ 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
2543
|
],
|
|
2106
2544
|
},
|
|
2107
2545
|
examples: [
|
|
2108
2546
|
'notis apps scaffolds list',
|
|
2109
2547
|
'notis apps init "Mind the Flo"',
|
|
2110
|
-
'notis apps init "My CRM" --from
|
|
2111
|
-
'notis apps init "My App"
|
|
2548
|
+
'notis apps init "My CRM" --from databases',
|
|
2549
|
+
'notis apps init "My App" ~/code/my-app',
|
|
2112
2550
|
],
|
|
2113
2551
|
mutates: true,
|
|
2114
2552
|
idempotent: false,
|
|
@@ -2118,10 +2556,19 @@ export const appsCommandSpecs = [
|
|
|
2118
2556
|
},
|
|
2119
2557
|
{
|
|
2120
2558
|
command_path: ['apps', 'scaffolds', 'list'],
|
|
2121
|
-
summary: 'List
|
|
2122
|
-
when_to_use: 'Discover
|
|
2123
|
-
args_schema: {
|
|
2124
|
-
|
|
2559
|
+
summary: 'List published Store apps available as scaffolds.',
|
|
2560
|
+
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.',
|
|
2561
|
+
args_schema: {
|
|
2562
|
+
arguments: [],
|
|
2563
|
+
options: [
|
|
2564
|
+
{ flags: '--search <term>', description: 'Filter scaffolds by name, tagline, description, or category.' },
|
|
2565
|
+
],
|
|
2566
|
+
},
|
|
2567
|
+
examples: [
|
|
2568
|
+
'notis apps scaffolds list',
|
|
2569
|
+
'notis apps scaffolds list --search journal',
|
|
2570
|
+
'notis apps init "My App" --from databases',
|
|
2571
|
+
],
|
|
2125
2572
|
mutates: false,
|
|
2126
2573
|
idempotent: true,
|
|
2127
2574
|
require_auth: false,
|
|
@@ -2150,21 +2597,26 @@ export const appsCommandSpecs = [
|
|
|
2150
2597
|
},
|
|
2151
2598
|
{
|
|
2152
2599
|
command_path: ['apps', 'dev'],
|
|
2153
|
-
summary: '
|
|
2600
|
+
summary: 'Register a development root and connect its apps to the shared local development host.',
|
|
2154
2601
|
when_to_use:
|
|
2155
|
-
'Run this
|
|
2602
|
+
'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.',
|
|
2156
2603
|
args_schema: {
|
|
2157
2604
|
arguments: [
|
|
2158
2605
|
{ token: '[dir]', key: 'dir', description: 'Project directory or monorepo root (default: current dir).' },
|
|
2159
2606
|
],
|
|
2160
2607
|
options: [
|
|
2161
2608
|
{ flags: '--port <number>', description: `Local bundle server port (default: ${DEFAULT_DEV_PORT}).` },
|
|
2162
|
-
{
|
|
2609
|
+
{
|
|
2610
|
+
flags: '--scratch',
|
|
2611
|
+
description:
|
|
2612
|
+
'Use isolated empty databases, bundled skills, and bundled automations for this session '
|
|
2613
|
+
+ "instead of the installed app's resources. For fixture work and destructive experiments.",
|
|
2614
|
+
},
|
|
2163
2615
|
{
|
|
2164
2616
|
flags: '--live-data',
|
|
2165
2617
|
description:
|
|
2166
|
-
|
|
2167
|
-
+ '
|
|
2618
|
+
'Deprecated: using the installed app\'s real resources is now the default. '
|
|
2619
|
+
+ 'Accepted as a no-op; use `--scratch` for the old isolated behavior.',
|
|
2168
2620
|
},
|
|
2169
2621
|
{
|
|
2170
2622
|
flags: '--grant-cloud-shell',
|
|
@@ -2178,7 +2630,7 @@ export const appsCommandSpecs = [
|
|
|
2178
2630
|
'notis apps dev',
|
|
2179
2631
|
'notis apps dev ./my-app',
|
|
2180
2632
|
'notis apps dev ./workspace --port 5200',
|
|
2181
|
-
'notis apps dev --
|
|
2633
|
+
'notis apps dev --scratch # isolated resources for fixture or schema experiments',
|
|
2182
2634
|
],
|
|
2183
2635
|
mutates: true,
|
|
2184
2636
|
idempotent: true,
|
|
@@ -2186,6 +2638,35 @@ export const appsCommandSpecs = [
|
|
|
2186
2638
|
backend_call: { type: 'tool', name: ENSURE_DEV_APP_INSTALLATION_TOOL },
|
|
2187
2639
|
handler: appsDevHandler,
|
|
2188
2640
|
},
|
|
2641
|
+
{
|
|
2642
|
+
command_path: ['apps', 'roots', 'list'],
|
|
2643
|
+
summary: 'List persistent machine-local Notis app development roots.',
|
|
2644
|
+
when_to_use: 'See which folders every local Notis Desktop instance watches for development apps.',
|
|
2645
|
+
args_schema: { arguments: [], options: [] },
|
|
2646
|
+
examples: ['notis apps roots list'],
|
|
2647
|
+
mutates: false,
|
|
2648
|
+
idempotent: true,
|
|
2649
|
+
require_auth: false,
|
|
2650
|
+
backend_call: { type: 'local', name: 'list_app_development_roots' },
|
|
2651
|
+
handler: appsRootsListHandler,
|
|
2652
|
+
},
|
|
2653
|
+
{
|
|
2654
|
+
command_path: ['apps', 'roots', 'remove'],
|
|
2655
|
+
summary: 'Stop watching a registered Notis app development root.',
|
|
2656
|
+
when_to_use: 'Remove a persistent development root. The built-in ~/.notis/apps root cannot be removed.',
|
|
2657
|
+
args_schema: {
|
|
2658
|
+
arguments: [
|
|
2659
|
+
{ token: '<folder>', key: 'dir', description: 'Previously registered development root.' },
|
|
2660
|
+
],
|
|
2661
|
+
options: [],
|
|
2662
|
+
},
|
|
2663
|
+
examples: ['notis apps roots remove ./old-apps'],
|
|
2664
|
+
mutates: true,
|
|
2665
|
+
idempotent: true,
|
|
2666
|
+
require_auth: false,
|
|
2667
|
+
backend_call: { type: 'local', name: 'remove_app_development_root' },
|
|
2668
|
+
handler: appsRootsRemoveHandler,
|
|
2669
|
+
},
|
|
2189
2670
|
{
|
|
2190
2671
|
command_path: ['apps', 'build'],
|
|
2191
2672
|
summary: 'Build and package the app into .notis/output/.',
|
|
@@ -2298,14 +2779,17 @@ export const appsCommandSpecs = [
|
|
|
2298
2779
|
args_schema: {
|
|
2299
2780
|
arguments: [
|
|
2300
2781
|
{ token: '<app-id>', description: 'Remote app ID to pull.' },
|
|
2301
|
-
{ token: '[dir]', key: 'dir', description: 'Target directory (defaults to
|
|
2782
|
+
{ token: '[dir]', key: 'dir', description: 'Target directory (defaults to ~/.notis/apps/<app-slug>).' },
|
|
2302
2783
|
],
|
|
2303
2784
|
options: [
|
|
2304
2785
|
{ flags: '--force', description: 'Overwrite a non-empty target directory.' },
|
|
2305
|
-
{ flags: '--version <n>', description: 'Pull a specific app source version (default: latest).' },
|
|
2786
|
+
{ flags: '--source-version <n>', description: 'Pull a specific app source version (default: latest).' },
|
|
2306
2787
|
],
|
|
2307
2788
|
},
|
|
2308
|
-
examples: [
|
|
2789
|
+
examples: [
|
|
2790
|
+
'notis apps pull abc123',
|
|
2791
|
+
'notis apps pull abc123 ./my-app --force --source-version 3',
|
|
2792
|
+
],
|
|
2309
2793
|
mutates: true,
|
|
2310
2794
|
idempotent: true,
|
|
2311
2795
|
require_auth: true,
|
|
@@ -2316,7 +2800,7 @@ export const appsCommandSpecs = [
|
|
|
2316
2800
|
command_path: ['apps', 'deploy'],
|
|
2317
2801
|
summary: 'Build and upload the app to the linked Notis app.',
|
|
2318
2802
|
when_to_use:
|
|
2319
|
-
'Ship the installed app to production for the linked user/team app.
|
|
2803
|
+
'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
2804
|
args_schema: {
|
|
2321
2805
|
arguments: [
|
|
2322
2806
|
{ token: '[dir]', key: 'dir', description: 'Project directory (default: current dir).' },
|
|
@@ -2324,7 +2808,7 @@ export const appsCommandSpecs = [
|
|
|
2324
2808
|
options: [
|
|
2325
2809
|
{ flags: '--app-id <id>', description: 'Override linked app ID.' },
|
|
2326
2810
|
{ flags: '--skip-build', description: 'Skip the build step (use existing .notis/output/).' },
|
|
2327
|
-
{ flags: '--direct', description: '
|
|
2811
|
+
{ flags: '--direct', description: 'Explicitly upload to Supabase storage, bypassing the backend server.' },
|
|
2328
2812
|
],
|
|
2329
2813
|
},
|
|
2330
2814
|
examples: ['notis apps deploy', 'notis apps deploy --skip-build', 'notis apps deploy --app-id abc123', 'notis apps deploy --direct'],
|