@notis_ai/cli 0.2.18 → 0.2.19
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 +542 -146
- package/bin/check-runtime.js +15 -0
- package/bin/notis.js +2 -0
- package/config/notis_app_design_rules.json +320 -0
- package/config/portal-route-roots.json +45 -0
- package/dist/agent-hooks/notis-agent-hook.mjs +42325 -18273
- package/dist/base-skills/notis-cli/.notis-base-skill.json +1 -0
- package/dist/base-skills/notis-cli/SKILL.md +51 -280
- package/dist/base-skills/notis-cli/references/app-delivery.md +17 -0
- package/dist/base-skills/notis-cli/references/intelligence.md +94 -0
- package/dist/base-skills/notis-cli/references/native-databases.md +21 -0
- package/dist/base-skills/notis-cli/references/tool-examples.md +56 -0
- package/dist/base-skills/notis-cli/references/troubleshooting.md +67 -0
- package/dist/base-skills/notis-query/.notis-base-skill.json +1 -0
- package/dist/base-skills/notis-query/SKILL.md +35 -699
- package/dist/base-skills/notis-query/references/database-discovery.md +59 -0
- package/dist/base-skills/notis-query/references/documents.md +74 -0
- package/dist/base-skills/notis-query/references/query.md +76 -0
- package/dist/base-skills/notis-spaces/.notis-base-skill.json +1 -0
- package/dist/base-skills/notis-spaces/SKILL.md +218 -0
- package/dist/base-skills/notis-spaces/references/architecture.md +116 -0
- package/dist/base-skills/notis-spaces/references/context.md +81 -0
- package/dist/base-skills/notis-spaces/references/design.md +141 -0
- package/dist/base-skills/notis-spaces/references/fixtures.md +29 -0
- package/dist/base-skills/notis-spaces/references/memory.md +58 -0
- package/dist/base-skills/notis-spaces/references/reading.md +73 -0
- package/dist/base-skills/notis-spaces/references/release.md +120 -0
- package/dist/base-skills/notis-spaces/references/reports.md +77 -0
- package/dist/base-skills/notis-spaces/references/sdk.md +273 -0
- package/dist/base-skills/notis-spaces/references/store.md +118 -0
- package/dist/base-skills/notis-spaces/references/troubleshooting.md +28 -0
- package/dist/base-skills/notis-spaces/references/views.md +229 -0
- package/{template/packages → dist}/sdk/package.json +9 -10
- package/dist/sdk/src/agentContext.ts +36 -0
- package/dist/sdk/src/bundleModule.ts +175 -0
- package/dist/sdk/src/components/Dialog.tsx +103 -0
- package/dist/sdk/src/components/DocumentEditor.tsx +113 -0
- package/dist/sdk/src/components/DocumentPage.tsx +184 -0
- package/{template/packages → dist}/sdk/src/components/Markdown.tsx +9 -3
- package/{template/packages → dist}/sdk/src/components/MarkdownEditor.tsx +26 -5
- package/{template/packages → dist}/sdk/src/components/MultiSelectActionBar.tsx +95 -12
- package/{template/packages → dist}/sdk/src/components/MultiSelectCheckbox.tsx +4 -2
- package/dist/sdk/src/components/NotisCommentBoundary.tsx +172 -0
- package/{template/packages → dist}/sdk/src/components/NotisSelectionBoundary.tsx +9 -5
- package/dist/sdk/src/components/RecordComponents.tsx +111 -0
- package/dist/sdk/src/components/Skeleton.tsx +38 -0
- package/dist/sdk/src/config.ts +31 -0
- package/{template/packages → dist}/sdk/src/documents.ts +21 -6
- package/dist/sdk/src/hooks/useAction.ts +28 -0
- package/dist/sdk/src/hooks/useAgentContext.ts +23 -0
- package/{template/packages → dist}/sdk/src/hooks/useBackend.ts +3 -2
- package/dist/sdk/src/hooks/useCloudComputer.ts +94 -0
- package/dist/sdk/src/hooks/useCloudComputerShell.ts +114 -0
- package/{template/packages → dist}/sdk/src/hooks/useCollectionInteractions.ts +157 -147
- package/dist/sdk/src/hooks/useDatabaseSchema.ts +50 -0
- package/{template/packages → dist}/sdk/src/hooks/useDatabaseSubscription.ts +2 -2
- package/dist/sdk/src/hooks/useDocument.ts +51 -0
- package/{template/packages → dist}/sdk/src/hooks/useDocuments.ts +25 -59
- package/{template/packages → dist}/sdk/src/hooks/useHandover.ts +7 -7
- package/dist/sdk/src/hooks/useLongPressSelection.ts +79 -0
- package/dist/sdk/src/hooks/useMarqueeSelection.ts +194 -0
- package/{template/packages → dist}/sdk/src/hooks/useMultiSelect.ts +2 -8
- package/dist/sdk/src/hooks/useNativeDocumentBody.ts +50 -0
- package/dist/sdk/src/hooks/useNativeDocuments.ts +143 -0
- package/{template/packages → dist}/sdk/src/hooks/useNotis.ts +6 -1
- package/dist/sdk/src/hooks/useNotisNavigation.ts +70 -0
- package/dist/sdk/src/hooks/useQuery.ts +81 -0
- package/dist/sdk/src/hooks/useToolQuery.ts +12 -0
- package/dist/sdk/src/hooks/useTopBarSearch.ts +102 -0
- package/dist/sdk/src/hooks/useTopBarTitleLeaf.ts +75 -0
- package/{template/packages → dist}/sdk/src/hooks/useUpsertDocument.ts +1 -1
- package/dist/sdk/src/hooks/useViewParams.ts +195 -0
- package/dist/sdk/src/hooks/useViewerReads.ts +129 -0
- package/{template/packages → dist}/sdk/src/index.ts +64 -1
- package/{template/packages → dist}/sdk/src/interactions/actions.ts +14 -1
- package/{template/packages → dist}/sdk/src/interactions/shortcuts.tsx +129 -23
- package/dist/sdk/src/interactions/visibility.ts +13 -0
- package/{template/packages → dist}/sdk/src/interactions.ts +5 -1
- package/dist/sdk/src/nativeData.ts +39 -0
- package/{template/packages → dist}/sdk/src/provider.tsx +9 -4
- package/dist/sdk/src/queryCache.ts +292 -0
- package/dist/sdk/src/queryErrors.ts +51 -0
- package/dist/sdk/src/runtime.ts +675 -0
- package/dist/sdk/src/runtimeModules.ts +33 -0
- package/dist/sdk/src/space.ts +442 -0
- package/dist/sdk/src/standardViewParams.ts +36 -0
- package/{template/packages → dist}/sdk/src/styles.css +81 -16
- package/dist/sdk/src/tailwind.ts +66 -0
- package/dist/sdk/src/ui/charts/RenderChartData.tsx +25 -0
- package/dist/sdk/src/ui/engine/core.ts +392 -0
- package/dist/sdk/src/ui/engine/dom.ts +183 -0
- package/dist/sdk/src/ui/engine/hooks.ts +195 -0
- package/dist/sdk/src/ui/engine/instance.ts +63 -0
- package/dist/sdk/src/ui/engine/keycapContext.ts +2 -0
- package/dist/sdk/src/ui/engine/registry.ts +262 -0
- package/dist/sdk/src/ui/engine/types.ts +62 -0
- package/dist/sdk/src/ui/icons.tsx +91 -0
- package/dist/sdk/src/ui/util.ts +83 -0
- package/dist/sdk/src/viewPeriod.ts +26 -0
- package/dist/sdk/src/vite.ts +92 -0
- package/dist/sdk/src/words/actions.ts +295 -0
- package/dist/sdk/src/words/clashes.ts +158 -0
- package/dist/sdk/src/words/format.ts +136 -0
- package/dist/sdk/src/words/index.ts +13 -0
- package/dist/sdk/src/words/keymap.ts +72 -0
- package/dist/sdk/src/words/keys.ts +297 -0
- package/dist/sdk/src/words/shell.ts +46 -0
- package/dist/sdk/src/words/types.ts +65 -0
- package/dist/skill-sync/index.js +832 -151
- package/dist/skill-sync/index.js.map +4 -4
- package/dist/skill-sync-worker.mjs +3941 -0
- package/dist/space-harness/host.js +1460 -0
- package/dist/space-sdk/dist/chunk-ZSN57ATH.js +5 -0
- package/dist/space-sdk/dist/index.js +15 -0
- package/dist/space-sdk/dist/ui.js +18 -0
- package/dist/space-sdk/package.json +33 -0
- package/dist/space-sdk/src/agentContext.d.ts +36 -0
- package/dist/space-sdk/src/components/Dialog.d.ts +20 -0
- package/dist/space-sdk/src/components/DocumentEditor.d.ts +46 -0
- package/dist/space-sdk/src/components/DocumentPage.d.ts +53 -0
- package/dist/space-sdk/src/components/Markdown.d.ts +18 -0
- package/dist/space-sdk/src/components/MarkdownEditor.d.ts +11 -0
- package/dist/space-sdk/src/components/MultiSelectActionBar.d.ts +31 -0
- package/dist/space-sdk/src/components/MultiSelectCheckbox.d.ts +14 -0
- package/dist/space-sdk/src/components/MultiSelectDragOverlay.d.ts +8 -0
- package/dist/space-sdk/src/components/NotisCommentBoundary.d.ts +36 -0
- package/dist/space-sdk/src/components/NotisSelectionBoundary.d.ts +16 -0
- package/dist/space-sdk/src/components/RecordComponents.d.ts +64 -0
- package/dist/space-sdk/src/components/ShortcutHints.d.ts +12 -0
- package/dist/space-sdk/src/components/Skeleton.d.ts +23 -0
- package/dist/space-sdk/src/config.ts +31 -0
- package/dist/space-sdk/src/documents.d.ts +59 -0
- package/dist/space-sdk/src/hooks/useAction.d.ts +8 -0
- package/dist/space-sdk/src/hooks/useActiveResource.d.ts +3 -0
- package/dist/space-sdk/src/hooks/useAgentContext.d.ts +7 -0
- package/dist/space-sdk/src/hooks/useBackend.d.ts +23 -0
- package/dist/space-sdk/src/hooks/useCloudComputer.d.ts +45 -0
- package/dist/space-sdk/src/hooks/useCloudComputerShell.d.ts +49 -0
- package/dist/space-sdk/src/hooks/useCollectionInteractions.d.ts +118 -0
- package/dist/space-sdk/src/hooks/useDatabaseSchema.d.ts +14 -0
- package/dist/space-sdk/src/hooks/useDatabaseSubscription.d.ts +26 -0
- package/dist/space-sdk/src/hooks/useDocument.d.ts +22 -0
- package/dist/space-sdk/src/hooks/useDocuments.d.ts +31 -0
- package/dist/space-sdk/src/hooks/useHandover.d.ts +43 -0
- package/dist/space-sdk/src/hooks/useLongPressSelection.d.ts +9 -0
- package/dist/space-sdk/src/hooks/useMarqueeSelection.d.ts +57 -0
- package/dist/space-sdk/src/hooks/useMultiSelect.d.ts +51 -0
- package/dist/space-sdk/src/hooks/useNativeDocumentBody.d.ts +24 -0
- package/dist/space-sdk/src/hooks/useNativeDocuments.d.ts +55 -0
- package/dist/space-sdk/src/hooks/useNotis.d.ts +25 -0
- package/dist/space-sdk/src/hooks/useNotisNavigation.d.ts +33 -0
- package/dist/space-sdk/src/hooks/useQuery.d.ts +22 -0
- package/dist/space-sdk/src/hooks/useTool.d.ts +20 -0
- package/dist/space-sdk/src/hooks/useToolQuery.d.ts +3 -0
- package/dist/space-sdk/src/hooks/useTools.d.ts +15 -0
- package/dist/space-sdk/src/hooks/useTopBarSearch.d.ts +48 -0
- package/dist/space-sdk/src/hooks/useTopBarTitleLeaf.d.ts +24 -0
- package/dist/space-sdk/src/hooks/useUpsertDocument.d.ts +27 -0
- package/dist/space-sdk/src/hooks/useViewParams.d.ts +76 -0
- package/dist/space-sdk/src/hooks/useViewerReads.d.ts +38 -0
- package/dist/space-sdk/src/index.d.ts +98 -0
- package/dist/space-sdk/src/interactions/actions.d.ts +33 -0
- package/dist/space-sdk/src/interactions/shortcuts.d.ts +42 -0
- package/dist/space-sdk/src/interactions/visibility.d.ts +2 -0
- package/dist/space-sdk/src/nativeData.d.ts +66 -0
- package/dist/space-sdk/src/provider.d.ts +17 -0
- package/dist/space-sdk/src/queryCache.d.ts +45 -0
- package/dist/space-sdk/src/queryErrors.ts +51 -0
- package/dist/space-sdk/src/runtime.d.ts +615 -0
- package/dist/space-sdk/src/runtimeModules.ts +33 -0
- package/dist/space-sdk/src/space.ts +442 -0
- package/dist/space-sdk/src/standardViewParams.d.ts +15 -0
- package/dist/space-sdk/src/tailwind-candidates.d.ts +3418 -0
- package/dist/space-sdk/src/tailwind.ts +66 -0
- package/dist/space-sdk/src/ui/actions/ActionToast.d.ts +15 -0
- package/dist/space-sdk/src/ui/actions/actionWords.d.ts +16 -0
- package/dist/space-sdk/src/ui/actions/index.d.ts +7 -0
- package/dist/space-sdk/src/ui/actions/useActions.d.ts +69 -0
- package/dist/space-sdk/src/ui/actions/useToast.d.ts +41 -0
- package/dist/space-sdk/src/ui/blocks/Tabs.d.ts +139 -0
- package/dist/space-sdk/src/ui/blocks/blocks.d.ts +217 -0
- package/dist/space-sdk/src/ui/blocks/index.d.ts +4 -0
- package/dist/space-sdk/src/ui/board/BoardView.d.ts +36 -0
- package/dist/space-sdk/src/ui/board/boardModel.d.ts +46 -0
- package/dist/space-sdk/src/ui/board/boardWords.d.ts +9 -0
- package/dist/space-sdk/src/ui/board/index.d.ts +5 -0
- package/dist/space-sdk/src/ui/calendar/CalendarView.d.ts +49 -0
- package/dist/space-sdk/src/ui/calendar/DayPicker.d.ts +23 -0
- package/dist/space-sdk/src/ui/calendar/calendarMath.d.ts +81 -0
- package/dist/space-sdk/src/ui/charts/Chart.d.ts +203 -0
- package/dist/space-sdk/src/ui/charts/ChartTable.d.ts +37 -0
- package/dist/space-sdk/src/ui/charts/RenderChartData.d.ts +16 -0
- package/dist/space-sdk/src/ui/charts/StatRow.d.ts +59 -0
- package/dist/space-sdk/src/ui/charts/chartData.d.ts +84 -0
- package/dist/space-sdk/src/ui/comments/CommentPane.d.ts +58 -0
- package/dist/space-sdk/src/ui/comments/commentModel.d.ts +157 -0
- package/dist/space-sdk/src/ui/comments/commentWords.d.ts +60 -0
- package/dist/space-sdk/src/ui/comments/index.d.ts +4 -0
- package/dist/space-sdk/src/ui/comments/useComments.d.ts +146 -0
- package/dist/space-sdk/src/ui/engine/HelpSheet.d.ts +12 -0
- package/dist/space-sdk/src/ui/engine/KeyboardEngine.d.ts +27 -0
- package/dist/space-sdk/src/ui/engine/core.d.ts +73 -0
- package/dist/space-sdk/src/ui/engine/hooks.d.ts +64 -0
- package/dist/space-sdk/src/ui/engine/index.d.ts +14 -0
- package/dist/space-sdk/src/ui/engine/instance.d.ts +21 -0
- package/dist/space-sdk/src/ui/engine/keycapContext.d.ts +2 -0
- package/dist/space-sdk/src/ui/engine/registry.d.ts +77 -0
- package/dist/space-sdk/src/ui/engine/step.d.ts +11 -0
- package/dist/space-sdk/src/ui/engine/types.d.ts +54 -0
- package/dist/space-sdk/src/ui/fields/fieldWords.d.ts +56 -0
- package/dist/space-sdk/src/ui/fields/fields.d.ts +162 -0
- package/dist/space-sdk/src/ui/fields/index.d.ts +5 -0
- package/dist/space-sdk/src/ui/fields/useFieldDraft.d.ts +22 -0
- package/dist/space-sdk/src/ui/gallery/AttachmentGallery.d.ts +53 -0
- package/dist/space-sdk/src/ui/gallery/ImageViewer.d.ts +27 -0
- package/dist/space-sdk/src/ui/gallery/galleryWords.d.ts +95 -0
- package/dist/space-sdk/src/ui/gallery/index.d.ts +5 -0
- package/dist/space-sdk/src/ui/icons.d.ts +13 -0
- package/dist/space-sdk/src/ui/index.d.ts +54 -0
- package/dist/space-sdk/src/ui/keycap/Keycap.d.ts +22 -0
- package/dist/space-sdk/src/ui/keycap/index.d.ts +2 -0
- package/dist/space-sdk/src/ui/layout/Panels.d.ts +50 -0
- package/dist/space-sdk/src/ui/layout/chatPanelEdge.d.ts +77 -0
- package/dist/space-sdk/src/ui/layout/index.d.ts +5 -0
- package/dist/space-sdk/src/ui/layout/panelMath.d.ts +91 -0
- package/dist/space-sdk/src/ui/list/ListReader.d.ts +380 -0
- package/dist/space-sdk/src/ui/list/Row.d.ts +145 -0
- package/dist/space-sdk/src/ui/list/SelectionBar.d.ts +65 -0
- package/dist/space-sdk/src/ui/list/index.d.ts +14 -0
- package/dist/space-sdk/src/ui/list/rowState.d.ts +22 -0
- package/dist/space-sdk/src/ui/list/useListKeys.d.ts +89 -0
- package/dist/space-sdk/src/ui/list/useReorder.d.ts +86 -0
- package/dist/space-sdk/src/ui/list/useSelection.d.ts +90 -0
- package/dist/space-sdk/src/ui/open/OpenAsMenu.d.ts +15 -0
- package/dist/space-sdk/src/ui/open/index.d.ts +6 -0
- package/dist/space-sdk/src/ui/open/openModeStore.d.ts +39 -0
- package/dist/space-sdk/src/ui/open/useOpenMode.d.ts +17 -0
- package/dist/space-sdk/src/ui/overlay/Menu.d.ts +88 -0
- package/dist/space-sdk/src/ui/overlay/Popover.d.ts +28 -0
- package/dist/space-sdk/src/ui/overlay/Tooltip.d.ts +19 -0
- package/dist/space-sdk/src/ui/overlay/index.d.ts +9 -0
- package/dist/space-sdk/src/ui/overlay/layerRoot.d.ts +24 -0
- package/dist/space-sdk/src/ui/overlay/position.d.ts +31 -0
- package/dist/space-sdk/src/ui/period/PeriodPicker.d.ts +30 -0
- package/dist/space-sdk/src/ui/period/dateFormat.d.ts +50 -0
- package/dist/space-sdk/src/ui/period/dateWords.d.ts +88 -0
- package/dist/space-sdk/src/ui/period/periods.d.ts +70 -0
- package/dist/space-sdk/src/ui/period/usePeriod.d.ts +23 -0
- package/dist/space-sdk/src/ui/present/index.d.ts +3 -0
- package/dist/space-sdk/src/ui/present/presentWords.d.ts +27 -0
- package/dist/space-sdk/src/ui/present/screens.d.ts +36 -0
- package/dist/space-sdk/src/ui/present/useAudienceScreen.d.ts +38 -0
- package/dist/space-sdk/src/ui/styles.d.ts +21 -0
- package/dist/space-sdk/src/ui/timeline/TimelineView.d.ts +44 -0
- package/dist/space-sdk/src/ui/timeline/timelineMath.d.ts +123 -0
- package/dist/space-sdk/src/viewPeriod.d.ts +8 -0
- package/dist/space-sdk/src/vite.ts +92 -0
- package/dist/space-sdk/src/words/actions.ts +295 -0
- package/dist/space-sdk/src/words/clashes.ts +158 -0
- package/dist/space-sdk/src/words/format.ts +136 -0
- package/dist/space-sdk/src/words/index.ts +13 -0
- package/dist/space-sdk/src/words/keymap.ts +72 -0
- package/dist/space-sdk/src/words/keys.ts +297 -0
- package/dist/space-sdk/src/words/shell.ts +46 -0
- package/dist/space-sdk/src/words/types.ts +65 -0
- package/dist/view-renderer/dist/host.css +1 -0
- package/dist/view-renderer/dist/host.js +2374 -0
- package/dist/view-renderer/dist/theme.css +73 -0
- package/dist/view-renderer/src/dom.js +212 -0
- package/dist/view-renderer/src/errors.js +8 -0
- package/dist/view-renderer/src/host.tsx +250 -0
- package/dist/view-renderer/src/index.js +4 -0
- package/dist/view-renderer/src/png.js +213 -0
- package/dist/view-renderer/src/pool.js +110 -0
- package/dist/view-renderer/src/remote-broker.js +59 -0
- package/dist/view-renderer/src/renderer.js +271 -0
- package/package.json +21 -11
- package/skills/notis-cli/AGENT_INSTRUCTIONS.md +63 -12
- package/skills/notis-onboarding/BRIEF.md +14 -9
- package/skills/notis-query/cli.md +1 -1
- package/skills/notis-spaces/cli.md +540 -0
- package/src/cli.js +8 -8
- package/src/command-specs/agents.js +121 -8
- package/src/command-specs/auth.js +3 -3
- package/src/command-specs/diagnostics.js +97 -20
- package/src/command-specs/documents.js +158 -0
- package/src/command-specs/files.js +29 -0
- package/src/command-specs/handover.js +331 -79
- package/src/command-specs/helpers.js +50 -40
- package/src/command-specs/index.js +14 -5
- package/src/command-specs/meta.js +38 -9
- package/src/command-specs/onboarding.js +1 -1
- package/src/command-specs/profile.js +42 -1
- package/src/command-specs/skills.js +314 -9
- package/src/command-specs/smoke.js +16 -4
- package/src/command-specs/spaces.js +783 -0
- package/src/command-specs/tools.js +118 -21
- package/src/command-specs/version-history.js +20 -0
- package/src/command-specs/views.js +69 -0
- package/src/runtime/agent-browser.js +178 -267
- package/src/runtime/agent-setup.js +172 -13
- package/src/runtime/app-boundary-validator.js +338 -2
- package/src/runtime/app-data-literals.js +82 -0
- package/src/runtime/app-platform.js +750 -1570
- package/src/runtime/auth-recovery.js +17 -0
- package/src/runtime/base-skills.d.ts +11 -0
- package/src/runtime/base-skills.js +100 -3
- package/src/runtime/computer-identity.d.ts +30 -0
- package/src/runtime/computer-identity.js +151 -0
- package/src/runtime/config-lock.js +135 -0
- package/src/runtime/confined-space-build.js +74 -0
- package/src/runtime/confined-space-verify.js +49 -0
- package/src/runtime/delegated-context.js +5 -21
- package/src/runtime/native-record-files.js +47 -0
- package/src/runtime/oauth.js +342 -31
- package/src/runtime/profiles.js +19 -112
- package/src/runtime/retired-sdk-files.js +24 -0
- package/src/runtime/retry.js +150 -0
- package/src/runtime/skill-sync/cloud-client.ts +33 -8
- package/src/runtime/skill-sync/index.ts +443 -106
- package/src/runtime/skill-sync/local-scanner.ts +248 -32
- package/src/runtime/skill-sync/native-identity.ts +102 -0
- package/src/runtime/skill-sync/native-plan.ts +70 -0
- package/src/runtime/skill-sync/symlink-manager.ts +66 -16
- package/src/runtime/skill-sync/sync-plan.ts +34 -2
- package/src/runtime/skill-sync/types.ts +54 -0
- package/src/runtime/skill-sync/write-cloud-skill.ts +4 -4
- package/src/runtime/skill-sync-service.js +109 -0
- package/src/runtime/source-root.js +32 -0
- package/src/runtime/space-collection-scaffold.js +107 -0
- package/src/runtime/space-database-structures.js +229 -0
- package/src/runtime/space-fixture-broker.js +68 -0
- package/src/runtime/space-lock.js +337 -0
- package/src/runtime/space-platform.js +133 -0
- package/src/runtime/space-source.js +290 -0
- package/src/runtime/space-test-server.js +354 -0
- package/src/runtime/space-verification.js +94 -0
- package/src/runtime/space-verify-browser-worker.js +51 -0
- package/src/runtime/space-view-manifest.js +368 -0
- package/src/runtime/sync-skills.d.ts +5 -1
- package/src/runtime/sync-skills.js +11 -2
- package/src/runtime/tool-confirmation.js +138 -0
- package/src/runtime/transport.js +118 -24
- package/src/runtime/upsell.js +219 -0
- package/src/runtime/view-render.js +49 -0
- package/src/skill-sync-worker-entry.js +2 -0
- package/src/skill-sync-worker.js +114 -0
- package/src/space-harness-host.ts +122 -0
- package/template/.harness/index.html.tmpl +9 -352
- package/template/app/globals.css +29 -3
- package/template/app/layout.tsx +2 -6
- package/template/components/ui/badge.tsx +7 -4
- package/template/components/ui/button.tsx +1 -1
- package/template/components/ui/native-select.tsx +24 -0
- package/template/notis.config.ts +4 -36
- package/template/package-lock.json +1800 -1867
- package/template/package.json +16 -14
- package/template/postcss.config.mjs +1 -1
- package/template/tailwind.config.ts +1 -6
- package/template/tsconfig.json +1 -0
- package/template/vite.config.ts +2 -2
- package/dist/base-skills/notis-apps/SKILL.md +0 -613
- package/skills/notis-apps/cli.md +0 -285
- package/src/command-specs/apps.js +0 -3011
- package/src/runtime/app-changelog.js +0 -79
- package/src/runtime/app-dev-consumers.js +0 -154
- package/src/runtime/app-dev-host-lock.js +0 -80
- package/src/runtime/app-dev-process-identity.js +0 -111
- package/src/runtime/app-dev-roots.js +0 -284
- package/src/runtime/app-dev-server.js +0 -1140
- package/src/runtime/app-dev-sessions.js +0 -185
- package/src/runtime/app-registry-scaffolds.js +0 -367
- package/src/runtime/assets/store-screenshot-dark.png +0 -0
- package/src/runtime/cli-mode.generated.js +0 -5
- package/src/runtime/cli-mode.js +0 -34
- package/src/runtime/store-screenshot.js +0 -143
- package/template/CHANGELOG.md +0 -5
- package/template/app/page.tsx +0 -60
- package/template/components/ui/card.tsx +0 -56
- package/template/packages/sdk/src/components/DocumentEditor.tsx +0 -103
- package/template/packages/sdk/src/config.ts +0 -259
- package/template/packages/sdk/src/hooks/useCloudComputer.ts +0 -97
- package/template/packages/sdk/src/hooks/useDatabaseSchema.ts +0 -85
- package/template/packages/sdk/src/hooks/useDocument.ts +0 -78
- package/template/packages/sdk/src/hooks/useNotisNavigation.ts +0 -52
- package/template/packages/sdk/src/hooks/useTopBarSearch.ts +0 -73
- package/template/packages/sdk/src/runtime.ts +0 -439
- package/template/packages/sdk/src/vite.ts +0 -56
- package/template/packages/sdk/tsconfig.json +0 -15
- /package/{template/packages → dist}/sdk/src/components/MultiSelectDragOverlay.tsx +0 -0
- /package/{template/packages → dist}/sdk/src/components/ShortcutHints.tsx +0 -0
- /package/{template/packages → dist}/sdk/src/hooks/useActiveResource.ts +0 -0
- /package/{template/packages → dist}/sdk/src/hooks/useTool.ts +0 -0
- /package/{template/packages → dist}/sdk/src/hooks/useTools.ts +0 -0
- /package/{template/packages → dist}/sdk/src/ui.ts +0 -0
|
@@ -0,0 +1,229 @@
|
|
|
1
|
+
/** Refresh only pulled database creation declarations; never execute authored code. */
|
|
2
|
+
import { parse } from '@babel/parser';
|
|
3
|
+
import { existsSync, readFileSync, realpathSync, writeFileSync } from 'node:fs';
|
|
4
|
+
import { dirname, join, relative, resolve, sep } from 'node:path';
|
|
5
|
+
import { usageError } from './errors.js';
|
|
6
|
+
|
|
7
|
+
const nameOf = node => node?.type === 'Identifier' ? node.name : node?.type === 'StringLiteral' ? node.value : null;
|
|
8
|
+
|
|
9
|
+
export function refreshDatabaseStructures(projectDir, localKey, structures) {
|
|
10
|
+
return refreshSpaceAuthoring(projectDir, localKey, { databaseStructures: structures });
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
/** Exact current-host overrides for copied source. Archives remain byte-preserved. */
|
|
14
|
+
export function refreshSpaceAuthoring(projectDir, localKey, { databaseStructures, authoringOverrides } = {}) {
|
|
15
|
+
let rewritten = new Map();
|
|
16
|
+
if (Object.keys(databaseStructures || {}).length) rewritten = planSpaceAuthoring(projectDir, localKey, { databaseStructures }, rewritten);
|
|
17
|
+
if (authoringOverrides) rewritten = planSpaceAuthoring(projectDir, localKey, { authoringOverrides }, rewritten);
|
|
18
|
+
// Both stages must succeed before mutating even one downloaded file.
|
|
19
|
+
for (const [file, source] of rewritten) writeFileSync(file, source);
|
|
20
|
+
return [...rewritten.keys()].map(file => relative(realpathSync(projectDir), file).split(sep).join('/'));
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
function planSpaceAuthoring(projectDir, localKey, { databaseStructures: structures, authoringOverrides: overrides }, staged) {
|
|
24
|
+
const root = realpathSync(projectDir), modules = new Map();
|
|
25
|
+
const fail = () => usageError('The pulled database declaration is not statically addressable. Keep resources and create declarations as objects, local constants or relative imports; the downloaded source has not been rewritten.');
|
|
26
|
+
function load(file) {
|
|
27
|
+
file = realpathSync(file);
|
|
28
|
+
if (!file.startsWith(root + sep) || file.includes(`${sep}node_modules${sep}`)) throw fail();
|
|
29
|
+
if (modules.has(file)) return modules.get(file);
|
|
30
|
+
const source = staged.get(file) ?? readFileSync(file, 'utf8');
|
|
31
|
+
const ast = parse(source, { sourceType: 'module', plugins: ['typescript', 'jsx'] });
|
|
32
|
+
const unit = { file, source, ast, bindings: new Map(), exports: new Map() };
|
|
33
|
+
modules.set(file, unit);
|
|
34
|
+
for (const statement of ast.program.body) {
|
|
35
|
+
const declaration = statement.type === 'ExportNamedDeclaration' ? statement.declaration : statement;
|
|
36
|
+
if (declaration?.type === 'VariableDeclaration') {
|
|
37
|
+
for (const item of declaration.declarations) if (item.id.type === 'Identifier' && item.init) {
|
|
38
|
+
unit.bindings.set(item.id.name, { node: item.init, unit });
|
|
39
|
+
if (statement.type === 'ExportNamedDeclaration') unit.exports.set(item.id.name, { node: item.init, unit });
|
|
40
|
+
}
|
|
41
|
+
}
|
|
42
|
+
if (statement.type === 'ImportDeclaration') {
|
|
43
|
+
for (const item of statement.specifiers) unit.bindings.set(item.local.name, {
|
|
44
|
+
imported: item.type === 'ImportDefaultSpecifier' ? 'default' : item.type === 'ImportNamespaceSpecifier' ? '*' : nameOf(item.imported),
|
|
45
|
+
from: statement.source.value, unit,
|
|
46
|
+
});
|
|
47
|
+
}
|
|
48
|
+
if (statement.type === 'ExportDefaultDeclaration') unit.exports.set('default', { node: statement.declaration, unit });
|
|
49
|
+
if (statement.type === 'ExportNamedDeclaration') for (const item of statement.specifiers || []) {
|
|
50
|
+
unit.exports.set(nameOf(item.exported), statement.source
|
|
51
|
+
? { imported: nameOf(item.local), from: statement.source.value, unit }
|
|
52
|
+
: { node: item.local, unit });
|
|
53
|
+
}
|
|
54
|
+
}
|
|
55
|
+
return unit;
|
|
56
|
+
}
|
|
57
|
+
function imported(reference) {
|
|
58
|
+
if (!reference.from?.startsWith('.')) throw fail();
|
|
59
|
+
const base = resolve(dirname(reference.unit.file), reference.from);
|
|
60
|
+
const file = ['', '.ts', '.tsx', '.js', '.mjs', '/index.ts', '/index.js'].map(suffix => base + suffix).find(existsSync);
|
|
61
|
+
if (!file) throw fail();
|
|
62
|
+
return load(file);
|
|
63
|
+
}
|
|
64
|
+
function unwrap(reference, depth = 0) {
|
|
65
|
+
if (!reference || depth > 50) throw fail();
|
|
66
|
+
if (reference.from) {
|
|
67
|
+
if (reference.imported === '*') return { namespace: imported(reference) };
|
|
68
|
+
return unwrap(imported(reference).exports.get(reference.imported), depth + 1);
|
|
69
|
+
}
|
|
70
|
+
const { node, unit } = reference;
|
|
71
|
+
if (node.type === 'Identifier') return unwrap(unit.bindings.get(node.name), depth + 1);
|
|
72
|
+
if (['TSAsExpression', 'TSSatisfiesExpression', 'TSNonNullExpression', 'ParenthesizedExpression', 'TSTypeAssertion'].includes(node.type)) {
|
|
73
|
+
return unwrap({ node: node.expression, unit }, depth + 1);
|
|
74
|
+
}
|
|
75
|
+
if (node.type === 'CallExpression' && node.arguments.length === 1 && node.callee.type === 'Identifier') {
|
|
76
|
+
const helper = unit.bindings.get(node.callee.name);
|
|
77
|
+
if (helper?.from === '@notis/sdk/config' && ['defineSpace', 'defineSpaces'].includes(helper.imported)) {
|
|
78
|
+
return unwrap({ node: node.arguments[0], unit }, depth + 1);
|
|
79
|
+
}
|
|
80
|
+
}
|
|
81
|
+
if (node.type === 'MemberExpression' && (!node.computed || node.property.type === 'StringLiteral')) {
|
|
82
|
+
const object = unwrap({ node: node.object, unit }, depth + 1);
|
|
83
|
+
return object.namespace ? unwrap(object.namespace.exports.get(nameOf(node.property)), depth + 1)
|
|
84
|
+
: unwrap(property(object, nameOf(node.property), depth + 1), depth + 1);
|
|
85
|
+
}
|
|
86
|
+
return reference;
|
|
87
|
+
}
|
|
88
|
+
function property(reference, name, depth = 0) {
|
|
89
|
+
const object = unwrap(reference, depth + 1);
|
|
90
|
+
if (object.node?.type !== 'ObjectExpression') throw fail();
|
|
91
|
+
// Later properties/spreads win, exactly as the authored object does.
|
|
92
|
+
for (const entry of [...object.node.properties].reverse()) {
|
|
93
|
+
if (entry.type === 'SpreadElement') {
|
|
94
|
+
const found = property({ node: entry.argument, unit: object.unit }, name, depth + 1);
|
|
95
|
+
if (found) return found;
|
|
96
|
+
} else if (entry.type === 'ObjectProperty' && !entry.computed && nameOf(entry.key) === name) {
|
|
97
|
+
return { node: entry.value, unit: object.unit };
|
|
98
|
+
}
|
|
99
|
+
}
|
|
100
|
+
return null;
|
|
101
|
+
}
|
|
102
|
+
function scalar(reference) {
|
|
103
|
+
const value = unwrap(reference);
|
|
104
|
+
if (value.node?.type !== 'StringLiteral') throw fail();
|
|
105
|
+
return value.node.value;
|
|
106
|
+
}
|
|
107
|
+
const indexFile = ['notis.config.ts', 'notis.config.js', 'notis.config.mjs'].map(file => join(root, file)).find(existsSync);
|
|
108
|
+
if (!indexFile) throw fail();
|
|
109
|
+
const index = load(indexFile);
|
|
110
|
+
const workspace = index.exports.get('default');
|
|
111
|
+
const definition = scalar(property(property(property(workspace, 'spaces'), localKey), 'definition'));
|
|
112
|
+
const unit = load(resolve(root, definition));
|
|
113
|
+
const resources = property(unit.exports.get('default'), 'resources');
|
|
114
|
+
const edits = new Map();
|
|
115
|
+
for (const [alias, structure] of Object.entries(structures || {})) {
|
|
116
|
+
if (!structure || typeof structure.name !== 'string' || !structure.schema || typeof structure.schema !== 'object' || Array.isArray(structure.schema)) throw usageError('The server returned an incomplete live database structure.');
|
|
117
|
+
const resource = property(resources, alias);
|
|
118
|
+
if (scalar(property(resource, 'kind')) !== 'database') throw fail();
|
|
119
|
+
const create = property(resource, 'create');
|
|
120
|
+
if (!create || !Number.isInteger(create.node.start) || !Number.isInteger(create.node.end)) throw fail();
|
|
121
|
+
const list = edits.get(create.unit.file) || [];
|
|
122
|
+
// Inline declarations are edited in place. Constants retain their shared
|
|
123
|
+
// helper and get one explicit local override, not another wrapper per pull.
|
|
124
|
+
let node = create.node;
|
|
125
|
+
while (['TSAsExpression', 'TSSatisfiesExpression', 'ParenthesizedExpression'].includes(node.type)) node = node.expression;
|
|
126
|
+
if (node.type === 'ObjectExpression') {
|
|
127
|
+
const additions = [];
|
|
128
|
+
for (const [name, value] of [['name', structure.name], ['schema', structure.schema]]) {
|
|
129
|
+
const field = [...node.properties].reverse().find(item => item.type === 'ObjectProperty' && !item.computed && nameOf(item.key) === name);
|
|
130
|
+
const replacement = JSON.stringify(value, null, 2);
|
|
131
|
+
if (field && !field.shorthand) list.push({ start: field.value.start, end: field.value.end, replacement });
|
|
132
|
+
else if (field) list.push({ start: field.start, end: field.end, replacement: `${name}: ${replacement}` });
|
|
133
|
+
else additions.push(`${name}: ${replacement}`);
|
|
134
|
+
}
|
|
135
|
+
if (additions.length) list.push({ start: node.end - 1, end: node.end - 1,
|
|
136
|
+
replacement: `${node.properties.length && !create.unit.source.slice(node.properties.at(-1).end, node.end - 1).includes(',') ? ',' : ''} ${additions.join(', ')} ` });
|
|
137
|
+
} else {
|
|
138
|
+
const original = create.unit.source.slice(create.node.start, create.node.end);
|
|
139
|
+
list.push({ start: create.node.start, end: create.node.end,
|
|
140
|
+
replacement: `({ ...(${original}), name: ${JSON.stringify(structure.name)}, schema: ${JSON.stringify(structure.schema, null, 2)} })` });
|
|
141
|
+
}
|
|
142
|
+
edits.set(create.unit.file, list);
|
|
143
|
+
}
|
|
144
|
+
if (overrides) {
|
|
145
|
+
if (!overrides || typeof overrides !== 'object' || Array.isArray(overrides)
|
|
146
|
+
|| Object.keys(overrides).some(key => !['record_params', 'resource_keys'].includes(key))) throw fail();
|
|
147
|
+
const recordParams = overrides.record_params || {}, resourceKeys = overrides.resource_keys || {};
|
|
148
|
+
if ([recordParams, resourceKeys].some(value => !value || typeof value !== 'object' || Array.isArray(value))) throw fail();
|
|
149
|
+
// Pin authoring to this selected definition. Shared imports/constants are
|
|
150
|
+
// overridden at their local use site, never rewritten for sibling Spaces.
|
|
151
|
+
const rootDefinition = unwrap(unit.exports.get('default'));
|
|
152
|
+
if (rootDefinition.unit !== unit || rootDefinition.node.type !== 'ObjectExpression') throw fail();
|
|
153
|
+
const rawField = name => {
|
|
154
|
+
const field = [...rootDefinition.node.properties].reverse().find(item => item.type === 'ObjectProperty'
|
|
155
|
+
&& !item.computed && nameOf(item.key) === name);
|
|
156
|
+
if (!field || field.shorthand) throw fail();
|
|
157
|
+
return { node: field.value, unit };
|
|
158
|
+
};
|
|
159
|
+
const text = reference => reference.unit.source.slice(reference.node.start, reference.node.end);
|
|
160
|
+
const patchObject = (reference, fields) => {
|
|
161
|
+
let node = reference.node;
|
|
162
|
+
while (['TSAsExpression', 'TSSatisfiesExpression', 'ParenthesizedExpression'].includes(node.type)) node = node.expression;
|
|
163
|
+
if (node.type !== 'ObjectExpression') {
|
|
164
|
+
return `({ ...(${text(reference)}), ${Object.entries(fields).map(([key, value]) => `${JSON.stringify(key)}: ${value}`).join(', ')} })`;
|
|
165
|
+
}
|
|
166
|
+
let source = text(reference), changes = [], additions = [];
|
|
167
|
+
for (const [key, replacement] of Object.entries(fields)) {
|
|
168
|
+
const field = [...node.properties].reverse().find(item => item.type === 'ObjectProperty' && !item.computed && nameOf(item.key) === key);
|
|
169
|
+
if (field) changes.push({ start: (field.shorthand ? field.start : field.value.start) - reference.node.start,
|
|
170
|
+
end: (field.shorthand ? field.end : field.value.end) - reference.node.start,
|
|
171
|
+
replacement: field.shorthand ? `${JSON.stringify(key)}: ${replacement}` : replacement });
|
|
172
|
+
else additions.push(`${JSON.stringify(key)}: ${replacement}`);
|
|
173
|
+
}
|
|
174
|
+
if (additions.length) changes.push({ start: node.end - 1 - reference.node.start, end: node.end - 1 - reference.node.start,
|
|
175
|
+
replacement: `${node.properties.length && !reference.unit.source.slice(node.properties.at(-1).end, node.end - 1).includes(',') ? ',' : ''} ${additions.join(', ')} ` });
|
|
176
|
+
for (const change of changes.sort((a, b) => b.start - a.start)) source = source.slice(0, change.start) + change.replacement + source.slice(change.end);
|
|
177
|
+
return source;
|
|
178
|
+
};
|
|
179
|
+
const queue = (reference, replacement) => {
|
|
180
|
+
const list = edits.get(reference.unit.file) || [];
|
|
181
|
+
list.push({ start: reference.node.start, end: reference.node.end, replacement }); edits.set(reference.unit.file, list);
|
|
182
|
+
};
|
|
183
|
+
for (const [fieldName, requests] of [['params', recordParams], ['resources', resourceKeys]]) {
|
|
184
|
+
if (!Object.keys(requests).length) continue;
|
|
185
|
+
const reference = rawField(fieldName), object = unwrap(reference), replacements = {};
|
|
186
|
+
for (const [name, expected] of Object.entries(requests)) {
|
|
187
|
+
if (!expected || typeof expected !== 'object' || Array.isArray(expected)) throw fail();
|
|
188
|
+
const current = property(reference, name);
|
|
189
|
+
if (!current) throw fail();
|
|
190
|
+
const source = object.node.type === 'ObjectExpression' && object.unit === unit && current.unit === unit ? text(current)
|
|
191
|
+
: `(${text(reference)})[${JSON.stringify(name)}]`;
|
|
192
|
+
if (fieldName === 'params') {
|
|
193
|
+
if (Object.keys(expected).some(key => !['database', 'main'].includes(key)) || typeof expected.main !== 'boolean'
|
|
194
|
+
|| scalar(property(current, 'type')) !== 'record' || scalar(property(current, 'database')) !== expected.database) throw fail();
|
|
195
|
+
replacements[name] = current.unit === unit && object.node.type === 'ObjectExpression'
|
|
196
|
+
? patchObject(current, { main: JSON.stringify(expected.main) }) : `({ ...(${source}), main: ${JSON.stringify(expected.main)} })`;
|
|
197
|
+
} else {
|
|
198
|
+
if (Object.keys(expected).some(key => !['kind', 'key'].includes(key)) || typeof expected.key !== 'string' || !expected.key
|
|
199
|
+
|| scalar(property(current, 'kind')) !== expected.kind) throw fail();
|
|
200
|
+
const create = property(current, 'create'), key = property(current, 'key');
|
|
201
|
+
if (create) {
|
|
202
|
+
// Copied source archives retain their original create declaration.
|
|
203
|
+
// A reused database (or one whose main moved) is now an existing
|
|
204
|
+
// binding: replace only this definition's resource, never a shared
|
|
205
|
+
// imported creation or a sibling view's source.
|
|
206
|
+
if (expected.kind !== 'database' || key) throw fail();
|
|
207
|
+
replacements[name] = JSON.stringify(expected);
|
|
208
|
+
} else {
|
|
209
|
+
if (!key) throw fail();
|
|
210
|
+
replacements[name] = current.unit === unit && object.node.type === 'ObjectExpression'
|
|
211
|
+
? patchObject(current, { key: JSON.stringify(expected.key) }) : `({ ...(${source}), key: ${JSON.stringify(expected.key)} })`;
|
|
212
|
+
}
|
|
213
|
+
}
|
|
214
|
+
}
|
|
215
|
+
queue(reference, patchObject(reference, replacements));
|
|
216
|
+
}
|
|
217
|
+
}
|
|
218
|
+
const rewritten = new Map(staged);
|
|
219
|
+
for (const [file, list] of edits) {
|
|
220
|
+
let source = modules.get(file).source, previous = source.length;
|
|
221
|
+
for (const edit of list.sort((a, b) => b.start - a.start)) {
|
|
222
|
+
if (edit.end > previous) throw fail();
|
|
223
|
+
source = source.slice(0, edit.start) + edit.replacement + source.slice(edit.end); previous = edit.start;
|
|
224
|
+
}
|
|
225
|
+
parse(source, { sourceType: 'module', plugins: ['typescript', 'jsx'] });
|
|
226
|
+
rewritten.set(file, source);
|
|
227
|
+
}
|
|
228
|
+
return rewritten;
|
|
229
|
+
}
|
|
@@ -0,0 +1,68 @@
|
|
|
1
|
+
/** Pure fixture adapter for the exact CLI/service renderer; no network client. */
|
|
2
|
+
import { createSpaceFixtureEngine, spaceHarnessSnapshot } from './space-test-server.js';
|
|
3
|
+
import { usageError } from './errors.js';
|
|
4
|
+
|
|
5
|
+
const object = value => value && typeof value === 'object' && !Array.isArray(value);
|
|
6
|
+
const exact = (value, keys) => object(value) && Object.keys(value).sort().join(',') === [...keys].sort().join(',');
|
|
7
|
+
|
|
8
|
+
export function createSpaceFixtureBroker({ release, capabilities, width, outputs = ['markdown', 'screenshot'],
|
|
9
|
+
authorizationMode = 'authorize_missing', unavailableActions = [] }) {
|
|
10
|
+
const engine = createSpaceFixtureEngine(release);
|
|
11
|
+
const frozen = spaceHarnessSnapshot(release, capabilities, engine, { authorizationMode, unavailableActions });
|
|
12
|
+
const calls = [];
|
|
13
|
+
const params = structuredClone(engine.context.params || {});
|
|
14
|
+
// The fixture's own "now" pins the browser clock, so a view that asks for the
|
|
15
|
+
// last hour sends the exact inputs its fixtures recorded.
|
|
16
|
+
const now = engine.context.now;
|
|
17
|
+
const fixedClock = typeof now === 'string' && Number.isFinite(Date.parse(now)) ? { fixed_clock: now } : {};
|
|
18
|
+
const value = { ...frozen, url: `https://app.notis.ai/verification/${encodeURIComponent(release.manifest.local_key)}`,
|
|
19
|
+
view: { space_id: frozen.descriptor.resource.id, name: release.manifest.name, revision: 1 },
|
|
20
|
+
locale: engine.context.locale === 'fr' ? 'fr' : 'en', params,
|
|
21
|
+
markdownExport: release.manifest.markdown?.export_name || null, render_options: { outputs, width }, ...fixedClock };
|
|
22
|
+
return {
|
|
23
|
+
fixtureDigest: engine.digest,
|
|
24
|
+
calls,
|
|
25
|
+
async snapshot() { return structuredClone(value); },
|
|
26
|
+
async read(request) {
|
|
27
|
+
let result;
|
|
28
|
+
const log = { operation: request?.operation, ...(request?.action_id ? { actionId: request.action_id } : {}),
|
|
29
|
+
...(typeof request?.record_key === 'string' ? { recordKey: request.record_key } : {}), ok: false };
|
|
30
|
+
calls.push(log);
|
|
31
|
+
try {
|
|
32
|
+
result = readFixture(request);
|
|
33
|
+
} catch (error) {
|
|
34
|
+
// Keep the reason with the call so verify reports which read failed and what the fixture lacks.
|
|
35
|
+
log.error = String(error?.message || error).slice(0, 2000);
|
|
36
|
+
throw error;
|
|
37
|
+
}
|
|
38
|
+
log.ok = true;
|
|
39
|
+
return result;
|
|
40
|
+
},
|
|
41
|
+
async finish() {
|
|
42
|
+
const failed = calls.find(value => !value.ok);
|
|
43
|
+
if (failed) throw usageError(failed.error ? `A synthetic fixture read failed: ${failed.error}` : 'A synthetic fixture read failed.');
|
|
44
|
+
return { valid: true };
|
|
45
|
+
},
|
|
46
|
+
};
|
|
47
|
+
|
|
48
|
+
function readFixture(request) {
|
|
49
|
+
let result;
|
|
50
|
+
if (calls.length > 256) throw usageError('The offline fixture read limit was reached.');
|
|
51
|
+
if (request?.operation === 'action' && exact(request, ['operation', 'action_id', 'inputs', 'request_id'])) {
|
|
52
|
+
if (frozen.descriptor.space.actions[request.action_id]?.readOnly !== true) throw usageError('Offline verification never runs write actions.');
|
|
53
|
+
result = engine.execute(request.action_id, request.inputs);
|
|
54
|
+
} else if (request?.operation === 'shown' && exact(request, ['operation', 'name', 'params', 'options'])) {
|
|
55
|
+
result = engine.executeShown(request.name, request.params, request.options);
|
|
56
|
+
} else if (request?.operation === 'viewer_read' && exact(request, ['operation', 'name', 'input'])) {
|
|
57
|
+
result = engine.executeViewerRead(request.name, request.input);
|
|
58
|
+
} else if (request?.operation === 'document_body' && exact(request, ['operation', 'record_key', 'binding', 'read_action'])) {
|
|
59
|
+
if (frozen.descriptor.space.actions[request.read_action]?.readOnly !== true) throw usageError('This document read action is unavailable.');
|
|
60
|
+
result = engine.executeBody({ operation: 'read', recordKey: request.record_key, binding: request.binding, readAction: request.read_action });
|
|
61
|
+
} else if (['record', 'html', 'report'].includes(request?.operation) && exact(request, ['operation', 'record_key'])) {
|
|
62
|
+
result = engine.executeRecordView(request.operation, request.record_key);
|
|
63
|
+
} else {
|
|
64
|
+
throw usageError('Only declared read-only synthetic fixtures are available.');
|
|
65
|
+
}
|
|
66
|
+
return result;
|
|
67
|
+
}
|
|
68
|
+
}
|
|
@@ -0,0 +1,337 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Local record of a pulled Space: its resource list and the version of every
|
|
3
|
+
* editable Skill folder (R5).
|
|
4
|
+
*
|
|
5
|
+
* `notis spaces pull` writes the Space's links to `resources.json`, every
|
|
6
|
+
* editable linked Skill to `skills/<alias>/`, and this lock to
|
|
7
|
+
* `.notis/space-lock.json` (local-only state, never part of the portable
|
|
8
|
+
* source). `notis spaces deploy` compares the working tree with the lock and
|
|
9
|
+
* sends only what changed since the pull, each item with the version it was
|
|
10
|
+
* pulled at, so edits and links made elsewhere are kept and a conflict is named
|
|
11
|
+
* before anything is uploaded.
|
|
12
|
+
*/
|
|
13
|
+
import { createHash } from 'node:crypto';
|
|
14
|
+
import { existsSync, lstatSync, mkdirSync, readdirSync, readFileSync, renameSync, rmSync, writeFileSync } from 'node:fs';
|
|
15
|
+
import { randomUUID } from 'node:crypto';
|
|
16
|
+
import { dirname, join, resolve } from 'node:path';
|
|
17
|
+
|
|
18
|
+
import { appFilesDigest, readSpaceStateFile, writeSpaceStateFile } from './app-platform.js';
|
|
19
|
+
import { usageError } from './errors.js';
|
|
20
|
+
|
|
21
|
+
export const SPACE_LOCK_FILE = 'space-lock.json';
|
|
22
|
+
export const RESOURCES_FILE = 'resources.json';
|
|
23
|
+
export const SKILLS_DIR = 'skills';
|
|
24
|
+
const ALIAS = /^[a-z][a-z0-9_-]{0,99}$/;
|
|
25
|
+
// Links staged from a legacy App without a declared name keep the migration's `<kind>:<id>` alias until renamed
|
|
26
|
+
// (`notis spaces resources name`); they can be listed and kept, never used as a Skill folder or a new entry.
|
|
27
|
+
const STAGED_ALIAS = /^(database|skill|automation):[A-Za-z0-9-]{1,64}$/;
|
|
28
|
+
export function isFolderAlias(alias) { return typeof alias === 'string' && ALIAS.test(alias); }
|
|
29
|
+
const LINK_KINDS = new Set(['database', 'skill', 'automation']);
|
|
30
|
+
const EXCLUDED_ENTRIES = new Set(['node_modules', '__pycache__', '.ds_store', '.git', '.notis', '.context']);
|
|
31
|
+
|
|
32
|
+
function stableJson(value) {
|
|
33
|
+
if (Array.isArray(value)) return `[${value.map(stableJson).join(',')}]`;
|
|
34
|
+
if (value && typeof value === 'object') {
|
|
35
|
+
return `{${Object.keys(value).sort().map((key) => `${JSON.stringify(key)}:${stableJson(value[key])}`).join(',')}}`;
|
|
36
|
+
}
|
|
37
|
+
return JSON.stringify(value);
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
function excluded(name) {
|
|
41
|
+
const lower = name.toLowerCase();
|
|
42
|
+
return EXCLUDED_ENTRIES.has(lower) || lower.startsWith('.env') || lower.endsWith('.pyc') || lower.endsWith('.pyo');
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
function safeRelativePath(value, label) {
|
|
46
|
+
if (typeof value !== 'string' || !value || value.includes('\\') || value.includes('\0') || value.startsWith('/')
|
|
47
|
+
|| value.split('/').some((part) => !part || part === '.' || part === '..')) {
|
|
48
|
+
throw usageError(`${label} contains an unsafe file path: ${String(value)}`);
|
|
49
|
+
}
|
|
50
|
+
return value;
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
/** Links of the three linkable kinds as the portable list `[{kind, id, alias}]`, sorted by alias. */
|
|
54
|
+
export function resourceEntries(links) {
|
|
55
|
+
return (Array.isArray(links) ? links : [])
|
|
56
|
+
.filter((link) => LINK_KINDS.has(link?.kind))
|
|
57
|
+
.map((link) => ({ kind: link.kind, id: link.resource_id, alias: link.alias }))
|
|
58
|
+
.sort((a, b) => (a.alias < b.alias ? -1 : a.alias > b.alias ? 1 : 0));
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
/** The pulled list as the lock records it: identity, alias, binding and availability. */
|
|
62
|
+
export function lockLinks(links) {
|
|
63
|
+
return (Array.isArray(links) ? links : [])
|
|
64
|
+
.filter((link) => LINK_KINDS.has(link?.kind))
|
|
65
|
+
.map((link) => ({ kind: link.kind, id: link.resource_id, alias: link.alias, binding_id: link.binding_id,
|
|
66
|
+
binding_revision: link.revision, available: link.available !== false }))
|
|
67
|
+
.sort((a, b) => (a.alias < b.alias ? -1 : a.alias > b.alias ? 1 : 0));
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
export function readSpaceLock(projectDir) {
|
|
71
|
+
const lock = readSpaceStateFile(projectDir, SPACE_LOCK_FILE);
|
|
72
|
+
if (lock === null) return null;
|
|
73
|
+
if (!lock || typeof lock !== 'object' || lock.version !== 1 || typeof lock.space_id !== 'string' || !Array.isArray(lock.links)
|
|
74
|
+
|| !lock.skills || typeof lock.skills !== 'object' || Array.isArray(lock.skills)) {
|
|
75
|
+
throw usageError('The Space lock file (.notis/space-lock.json) is unreadable. Pull the Space again.');
|
|
76
|
+
}
|
|
77
|
+
return lock;
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
export function writeSpaceLock(projectDir, lock) {
|
|
81
|
+
return writeSpaceStateFile(projectDir, SPACE_LOCK_FILE, lock);
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
export function readResourceList(projectDir) {
|
|
85
|
+
const path = join(resolve(projectDir), RESOURCES_FILE);
|
|
86
|
+
if (!existsSync(path)) return null;
|
|
87
|
+
if (lstatSync(path).isSymbolicLink()) throw usageError(`${RESOURCES_FILE} must be a regular file.`);
|
|
88
|
+
let parsed;
|
|
89
|
+
try { parsed = JSON.parse(readFileSync(path, 'utf8')); } catch { throw usageError(`${RESOURCES_FILE} must be valid JSON.`); }
|
|
90
|
+
if (!Array.isArray(parsed)) throw usageError(`${RESOURCES_FILE} must be a JSON array of {kind, id, alias} entries.`);
|
|
91
|
+
const seen = new Set();
|
|
92
|
+
return parsed.map((entry, index) => {
|
|
93
|
+
if (!entry || typeof entry !== 'object' || Array.isArray(entry) || Object.keys(entry).sort().join(',') !== 'alias,id,kind'
|
|
94
|
+
|| !LINK_KINDS.has(entry.kind) || typeof entry.id !== 'string' || !entry.id || typeof entry.alias !== 'string'
|
|
95
|
+
|| !(ALIAS.test(entry.alias) || STAGED_ALIAS.test(entry.alias))) {
|
|
96
|
+
throw usageError(`${RESOURCES_FILE}[${index}] must be {kind: database|skill|automation, id, alias} with a lowercase alias.`);
|
|
97
|
+
}
|
|
98
|
+
const key = `${entry.kind}:${entry.id}`;
|
|
99
|
+
if (seen.has(key)) throw usageError(`${RESOURCES_FILE} lists ${entry.kind} ${entry.id} twice.`);
|
|
100
|
+
seen.add(key);
|
|
101
|
+
return { kind: entry.kind, id: entry.id, alias: entry.alias };
|
|
102
|
+
});
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
export function writeResourceList(projectDir, entries) {
|
|
106
|
+
const root = resolve(projectDir);
|
|
107
|
+
const temporary = join(root, `.${RESOURCES_FILE}-${randomUUID()}`);
|
|
108
|
+
try {
|
|
109
|
+
writeFileSync(temporary, JSON.stringify(entries, null, 2) + '\n', { flag: 'wx' });
|
|
110
|
+
renameSync(temporary, join(root, RESOURCES_FILE));
|
|
111
|
+
} finally { rmSync(temporary, { force: true }); }
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
function walkSkillFolder(directory, prefix, files) {
|
|
115
|
+
for (const entry of readdirSync(directory, { withFileTypes: true })) {
|
|
116
|
+
if (entry.isSymbolicLink() || excluded(entry.name)) continue;
|
|
117
|
+
const path = prefix ? `${prefix}/${entry.name}` : entry.name;
|
|
118
|
+
if (entry.isDirectory()) walkSkillFolder(join(directory, entry.name), path, files);
|
|
119
|
+
else if (entry.isFile()) files[path] = readFileSync(join(directory, entry.name)).toString('base64');
|
|
120
|
+
}
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
/** Every `skills/<alias>/` folder as {alias: {files: {path: base64}, digest}}. */
|
|
124
|
+
export function collectSkillFolders(projectDir) {
|
|
125
|
+
const root = join(resolve(projectDir), SKILLS_DIR);
|
|
126
|
+
const folders = {};
|
|
127
|
+
if (!existsSync(root) || lstatSync(root).isSymbolicLink() || !lstatSync(root).isDirectory()) return folders;
|
|
128
|
+
for (const entry of readdirSync(root, { withFileTypes: true })) {
|
|
129
|
+
if (entry.isSymbolicLink() || !entry.isDirectory()) continue;
|
|
130
|
+
if (!ALIAS.test(entry.name)) throw usageError(`${SKILLS_DIR}/${entry.name}: name the folder with the Skill alias (lowercase letters, digits, - or _).`);
|
|
131
|
+
const files = {};
|
|
132
|
+
walkSkillFolder(join(root, entry.name), '', files);
|
|
133
|
+
if (!files['SKILL.md']) throw usageError(`${SKILLS_DIR}/${entry.name} needs a SKILL.md.`);
|
|
134
|
+
folders[entry.name] = { files, digest: appFilesDigest(files) };
|
|
135
|
+
}
|
|
136
|
+
return folders;
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
/** The files of a Skill bundle a local folder keeps (the same exclusions as collectSkillFolders). */
|
|
140
|
+
export function skillFolderFiles(files) {
|
|
141
|
+
return Object.fromEntries(Object.entries(files || {}).filter(([path]) => !path.split('/').some(excluded)));
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
/** Current linked Skill files own their folder; replacement requires the exact pulled snapshot. */
|
|
145
|
+
export function writeSkillFolder(projectDir, alias, files, { sourceSnapshot } = {}) {
|
|
146
|
+
if (!ALIAS.test(alias)) throw usageError(`Unsafe Skill alias: ${alias}`);
|
|
147
|
+
const root = join(resolve(projectDir), SKILLS_DIR, alias);
|
|
148
|
+
const entries = Object.entries(files || {}).map(([path, encoded]) => {
|
|
149
|
+
safeRelativePath(path, `Skill ${alias}`);
|
|
150
|
+
return [path, Buffer.from(encoded, 'base64')];
|
|
151
|
+
});
|
|
152
|
+
if (sourceSnapshot !== undefined) {
|
|
153
|
+
const changed = () => usageError(`${SKILLS_DIR}/${alias} changed while pulling its current Skill. Keep these local files and pull into a new directory.`);
|
|
154
|
+
const readSnapshot = () => {
|
|
155
|
+
const current = {};
|
|
156
|
+
if (!existsSync(root) || lstatSync(root).isSymbolicLink() || !lstatSync(root).isDirectory()
|
|
157
|
+
|| lstatSync(dirname(root)).isSymbolicLink()) throw changed();
|
|
158
|
+
const walk = (directory, prefix = '') => {
|
|
159
|
+
for (const entry of readdirSync(directory, { withFileTypes: true })) {
|
|
160
|
+
if (entry.isSymbolicLink() || excluded(entry.name)) throw changed();
|
|
161
|
+
const path = prefix + entry.name;
|
|
162
|
+
if (entry.isDirectory()) walk(join(directory, entry.name), path + '/');
|
|
163
|
+
else if (entry.isFile()) current[path] = readFileSync(join(directory, entry.name)).toString('base64');
|
|
164
|
+
else throw changed();
|
|
165
|
+
}
|
|
166
|
+
};
|
|
167
|
+
walk(root);
|
|
168
|
+
return current;
|
|
169
|
+
};
|
|
170
|
+
const expected = appFilesDigest(sourceSnapshot);
|
|
171
|
+
if (appFilesDigest(readSnapshot()) !== expected) throw changed();
|
|
172
|
+
if (appFilesDigest(files) === expected) return;
|
|
173
|
+
// The archive remains immutable on the server. The editable folder follows
|
|
174
|
+
// the verified linked Skill head, including normalized frontmatter/deletions.
|
|
175
|
+
const staging = join(resolve(projectDir), '.notis', `skill-pull-${randomUUID()}`);
|
|
176
|
+
const backup = `${staging}-previous`;
|
|
177
|
+
mkdirSync(staging, { recursive: true });
|
|
178
|
+
let displaced = false;
|
|
179
|
+
try {
|
|
180
|
+
for (const [path, bytes] of entries) {
|
|
181
|
+
const target = join(staging, path);
|
|
182
|
+
mkdirSync(dirname(target), { recursive: true });
|
|
183
|
+
writeFileSync(target, bytes, { flag: 'wx' });
|
|
184
|
+
}
|
|
185
|
+
if (appFilesDigest(readSnapshot()) !== expected) throw changed();
|
|
186
|
+
renameSync(root, backup); displaced = true;
|
|
187
|
+
try { renameSync(staging, root); }
|
|
188
|
+
catch (error) { renameSync(backup, root); displaced = false; throw error; }
|
|
189
|
+
displaced = false;
|
|
190
|
+
rmSync(backup, { recursive: true });
|
|
191
|
+
} finally {
|
|
192
|
+
rmSync(staging, { recursive: true, force: true });
|
|
193
|
+
// If restoring the prior folder failed, retain its backup for recovery.
|
|
194
|
+
if (!displaced) rmSync(backup, { recursive: true, force: true });
|
|
195
|
+
}
|
|
196
|
+
return;
|
|
197
|
+
}
|
|
198
|
+
for (const [path, bytes] of entries) {
|
|
199
|
+
const target = join(root, path);
|
|
200
|
+
mkdirSync(dirname(target), { recursive: true });
|
|
201
|
+
try {
|
|
202
|
+
writeFileSync(target, bytes, { flag: 'wx' });
|
|
203
|
+
} catch (error) {
|
|
204
|
+
if (error?.code === 'EEXIST') throw usageError(`${SKILLS_DIR}/${alias}/${path} already exists in the pulled source; the Skill folder cannot be written over it.`);
|
|
205
|
+
throw error;
|
|
206
|
+
}
|
|
207
|
+
}
|
|
208
|
+
}
|
|
209
|
+
|
|
210
|
+
export function lockAfterPull({ spaceId, apiBase, sourceRevision, links, skills }) {
|
|
211
|
+
return { version: 1, space_id: spaceId, api_base: apiBase, source_revision: sourceRevision, pulled_at: new Date().toISOString(),
|
|
212
|
+
links: lockLinks(links), skills };
|
|
213
|
+
}
|
|
214
|
+
|
|
215
|
+
function byAlias(entries) {
|
|
216
|
+
return new Map(entries.map((entry) => [entry.alias, entry]));
|
|
217
|
+
}
|
|
218
|
+
|
|
219
|
+
/**
|
|
220
|
+
* What changed since the pull, as the server expects it:
|
|
221
|
+
* edited and new Skill folders, deleted tracked folders and list additions or
|
|
222
|
+
* removals. Returns { changes: null } when nothing changed. A deleted tracked
|
|
223
|
+
* folder removes its link even while the list still names it. Ambiguous local
|
|
224
|
+
* states (a kept folder whose list entry was removed, a renamed alias) are
|
|
225
|
+
* usage errors, not conflicts.
|
|
226
|
+
*/
|
|
227
|
+
export function computeSpaceSourceChanges({ lock, resources, folders, manifest }) {
|
|
228
|
+
const tracked = lock.skills || {};
|
|
229
|
+
const lockLinksByKey = new Map((lock.links || []).map((link) => [`${link.kind}:${link.id}`, link]));
|
|
230
|
+
const lockAliases = byAlias(lock.links || []);
|
|
231
|
+
const declared = new Set(Object.keys(manifest?.resources || {}));
|
|
232
|
+
const list = resources ?? resourceEntries(lock.links.map((link) => ({ ...link, resource_id: link.id })));
|
|
233
|
+
const listByKey = new Map(list.map((entry) => [`${entry.kind}:${entry.id}`, entry]));
|
|
234
|
+
const listAliases = byAlias(list);
|
|
235
|
+
const edits = {}, creations = {}, add = [], remove = [];
|
|
236
|
+
const removedSkills = new Set();
|
|
237
|
+
|
|
238
|
+
for (const [alias, folder] of Object.entries(folders)) {
|
|
239
|
+
const entry = tracked[alias];
|
|
240
|
+
if (entry) {
|
|
241
|
+
if (!listByKey.has(`skill:${entry.skill_id}`)) {
|
|
242
|
+
throw usageError(`${RESOURCES_FILE} no longer lists the Skill of ${SKILLS_DIR}/${alias}. Delete the folder too to unlink it, or keep the entry.`);
|
|
243
|
+
}
|
|
244
|
+
if (entry.folder_hash !== folder.digest) {
|
|
245
|
+
edits[alias] = { skill_id: entry.skill_id, binding_id: entry.binding_id, binding_revision: entry.binding_revision,
|
|
246
|
+
version: entry.version, files: folder.files };
|
|
247
|
+
}
|
|
248
|
+
} else if (lockAliases.has(alias) || listAliases.has(alias)) {
|
|
249
|
+
throw usageError(`${SKILLS_DIR}/${alias} matches a listed resource that is not an editable Skill folder of this pull. Choose another folder name or pull again.`);
|
|
250
|
+
} else {
|
|
251
|
+
creations[alias] = { files: folder.files };
|
|
252
|
+
}
|
|
253
|
+
}
|
|
254
|
+
for (const [alias, entry] of Object.entries(tracked)) {
|
|
255
|
+
if (folders[alias]) continue;
|
|
256
|
+
// R5: deleting a tracked folder removes its link; a list entry left behind is rewritten after the deploy.
|
|
257
|
+
if (declared.has(alias)) {
|
|
258
|
+
throw usageError(`'${alias}' is declared by this Space's definition; remove it from the definition before deleting ${SKILLS_DIR}/${alias}.`);
|
|
259
|
+
}
|
|
260
|
+
removedSkills.add(entry.skill_id);
|
|
261
|
+
remove.push({ kind: 'skill', resource_id: entry.skill_id, binding_id: entry.binding_id, binding_revision: entry.binding_revision,
|
|
262
|
+
version: entry.version });
|
|
263
|
+
}
|
|
264
|
+
for (const entry of list) {
|
|
265
|
+
const before = lockLinksByKey.get(`${entry.kind}:${entry.id}`);
|
|
266
|
+
if (!before) {
|
|
267
|
+
if (!ALIAS.test(entry.alias)) {
|
|
268
|
+
throw usageError(`${RESOURCES_FILE}: '${entry.alias}' is not a valid alias for a new link; use lowercase letters, digits, - or _.`);
|
|
269
|
+
}
|
|
270
|
+
if (lockAliases.has(entry.alias) || folders[entry.alias] || creations[entry.alias]) {
|
|
271
|
+
throw usageError(`${RESOURCES_FILE}: alias '${entry.alias}' is already used by another resource or Skill folder.`);
|
|
272
|
+
}
|
|
273
|
+
add.push({ kind: entry.kind, resource_id: entry.id, alias: entry.alias });
|
|
274
|
+
} else if (before.alias !== entry.alias) {
|
|
275
|
+
throw usageError(`${RESOURCES_FILE}: '${before.alias}' cannot be renamed to '${entry.alias}' here; aliases are stable once linked.`);
|
|
276
|
+
}
|
|
277
|
+
}
|
|
278
|
+
for (const link of lock.links || []) {
|
|
279
|
+
if (listByKey.has(`${link.kind}:${link.id}`) || removedSkills.has(link.id)) continue;
|
|
280
|
+
if (declared.has(link.alias)) {
|
|
281
|
+
throw usageError(`'${link.alias}' is declared by this Space's definition; remove it from the definition before removing it from ${RESOURCES_FILE}.`);
|
|
282
|
+
}
|
|
283
|
+
remove.push({ kind: link.kind, resource_id: link.id, binding_id: link.binding_id, binding_revision: link.binding_revision });
|
|
284
|
+
}
|
|
285
|
+
const changes = {};
|
|
286
|
+
if (Object.keys(edits).length || Object.keys(creations).length) {
|
|
287
|
+
changes.skills = { ...(Object.keys(edits).length ? { edits } : {}), ...(Object.keys(creations).length ? { creations } : {}) };
|
|
288
|
+
}
|
|
289
|
+
if (add.length || remove.length) changes.links = { ...(add.length ? { add } : {}), ...(remove.length ? { remove } : {}) };
|
|
290
|
+
if (!Object.keys(changes).length) return { changes: null, summary: null, fingerprint: null };
|
|
291
|
+
const summary = { skills: { edited: Object.keys(edits).sort(), created: Object.keys(creations).sort() },
|
|
292
|
+
links: { added: add.map((entry) => `${entry.alias} (${entry.kind})`), removed: remove.map((entry) => `${entry.kind} ${entry.resource_id}`) } };
|
|
293
|
+
return { changes, summary, fingerprint: createHash('sha256').update(stableJson(changes)).digest('hex') };
|
|
294
|
+
}
|
|
295
|
+
|
|
296
|
+
/**
|
|
297
|
+
* The lock and list after a publication: the applied Skill versions, the new
|
|
298
|
+
* Skills' identities and the Space's current links (including links made
|
|
299
|
+
* elsewhere since the pull, which the deploy kept).
|
|
300
|
+
*/
|
|
301
|
+
export function applyPublication({ lock, folders, result }) {
|
|
302
|
+
const applied = result?.applied;
|
|
303
|
+
const next = { ...lock, source_revision: result?.published_revision ?? lock.source_revision, updated_at: new Date().toISOString() };
|
|
304
|
+
if (!applied) return { lock: next, resources: null };
|
|
305
|
+
const skills = {};
|
|
306
|
+
for (const [alias, folder] of Object.entries(folders)) {
|
|
307
|
+
const change = applied.skills?.[alias];
|
|
308
|
+
if (change) {
|
|
309
|
+
skills[alias] = { skill_id: change.skill_id, binding_id: change.binding_id, binding_revision: change.binding_revision,
|
|
310
|
+
version: change.version, folder_hash: folder.digest };
|
|
311
|
+
} else if (lock.skills?.[alias]) {
|
|
312
|
+
skills[alias] = lock.skills[alias];
|
|
313
|
+
}
|
|
314
|
+
}
|
|
315
|
+
next.skills = skills;
|
|
316
|
+
next.links = lockLinks(applied.space_links || []);
|
|
317
|
+
return { lock: next, resources: resourceEntries(applied.space_links || []) };
|
|
318
|
+
}
|
|
319
|
+
|
|
320
|
+
/** One line describing what a deploy changed, with the review link of every changed Skill. */
|
|
321
|
+
export function describeAppliedChanges(result) {
|
|
322
|
+
const applied = result?.applied;
|
|
323
|
+
if (!applied) return '';
|
|
324
|
+
const parts = [];
|
|
325
|
+
const edited = Object.entries(applied.skills || {}).filter(([, change]) => change.operation === 'update').map(([alias]) => alias);
|
|
326
|
+
const created = Object.entries(applied.skills || {}).filter(([, change]) => change.operation === 'create').map(([alias]) => alias);
|
|
327
|
+
if (edited.length) parts.push(`Skills updated: ${edited.join(', ')}`);
|
|
328
|
+
if (created.length) parts.push(`Skills created: ${created.join(', ')}`);
|
|
329
|
+
const added = (applied.links || []).flatMap((link) => (link.added || []).map((entry) => `${entry.alias} (${link.kind})`));
|
|
330
|
+
const removed = (applied.links || []).flatMap((link) => (link.removed || []).map((entry) => `${entry.alias} (${link.kind})`));
|
|
331
|
+
if (added.length) parts.push(`Links added: ${added.join(', ')}`);
|
|
332
|
+
if (removed.length) parts.push(`Links removed: ${removed.join(', ')}`);
|
|
333
|
+
for (const review of result.reviews || []) {
|
|
334
|
+
if (review.review_url) parts.push(`Review ${review.alias}: ${review.review_url}`);
|
|
335
|
+
}
|
|
336
|
+
return parts.length ? ` ${parts.join('. ')}.` : '';
|
|
337
|
+
}
|