@notis_ai/cli 0.2.17 → 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 +42323 -18258
- 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 -1568
- 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 -3000
- 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
|
@@ -4,14 +4,17 @@ import {
|
|
|
4
4
|
mkdirSync,
|
|
5
5
|
readFileSync,
|
|
6
6
|
readdirSync,
|
|
7
|
+
realpathSync,
|
|
7
8
|
renameSync,
|
|
8
9
|
writeFileSync,
|
|
9
10
|
} from 'node:fs';
|
|
10
11
|
import { createHash } from 'node:crypto';
|
|
11
12
|
import { homedir } from 'node:os';
|
|
12
|
-
import { dirname, join } from 'node:path';
|
|
13
|
+
import { dirname, isAbsolute, join } from 'node:path';
|
|
13
14
|
import { fileURLToPath } from 'node:url';
|
|
14
15
|
|
|
16
|
+
import { BASE_SKILL_NAMES, RETIRED_BASE_SKILL_NAMES, isBaseSkillLink } from './base-skills.js';
|
|
17
|
+
|
|
15
18
|
const HERE = dirname(fileURLToPath(import.meta.url));
|
|
16
19
|
const INSTRUCTIONS_PATH = join(HERE, '..', '..', 'skills', 'notis-cli', 'AGENT_INSTRUCTIONS.md');
|
|
17
20
|
const HOOK_BUNDLE_PATH = join(HERE, '..', '..', 'dist', 'agent-hooks', 'notis-agent-hook.mjs');
|
|
@@ -20,6 +23,16 @@ const END_MARKER = '<!-- notis-cli:instructions:end -->';
|
|
|
20
23
|
const LEGACY_SENTINEL = 'Use the Notis CLI (`npx --package @notis_ai/cli@latest -- notis ...`)';
|
|
21
24
|
const MANAGED_HOOK_MARKER = '--notis-managed-agent-hook';
|
|
22
25
|
const LEGACY_MANAGED_HOOK_MARKER = 'NOTIS_MANAGED_AGENT_HOOK=1';
|
|
26
|
+
const CODEX_TRUST_START = '# notis-cli:hook-trust:start';
|
|
27
|
+
const CODEX_TRUST_END = '# notis-cli:hook-trust:end';
|
|
28
|
+
const CODEX_EVENT_LABELS = Object.freeze({
|
|
29
|
+
SessionStart: 'session_start',
|
|
30
|
+
UserPromptSubmit: 'user_prompt_submit',
|
|
31
|
+
Stop: 'stop',
|
|
32
|
+
});
|
|
33
|
+
// Recall-only hooks inject memory and never capture turns, so they run only
|
|
34
|
+
// on these events.
|
|
35
|
+
export const RECALL_ONLY_HOOK_EVENTS = Object.freeze(['SessionStart', 'UserPromptSubmit']);
|
|
23
36
|
|
|
24
37
|
export const AGENT_IDS = Object.freeze(['codex', 'claude-code']);
|
|
25
38
|
|
|
@@ -148,7 +161,11 @@ export function installManagedHookRuntime({
|
|
|
148
161
|
bundlePath = HOOK_BUNDLE_PATH,
|
|
149
162
|
nodePath = process.execPath,
|
|
150
163
|
platform = process.platform,
|
|
164
|
+
envFile = null,
|
|
151
165
|
} = {}) {
|
|
166
|
+
if (envFile && (platform === 'win32' || !isAbsolute(envFile))) {
|
|
167
|
+
throw new Error('A session environment file must be an absolute POSIX path');
|
|
168
|
+
}
|
|
152
169
|
const bundle = readFileSync(bundlePath);
|
|
153
170
|
const digest = createHash('sha256').update(bundle).digest('hex');
|
|
154
171
|
const runtimePath = join(
|
|
@@ -184,6 +201,9 @@ export function installManagedHookRuntime({
|
|
|
184
201
|
].join('\r\n')
|
|
185
202
|
: [
|
|
186
203
|
'#!/bin/sh',
|
|
204
|
+
// A hosted session's sign-in is rotated into this file while the
|
|
205
|
+
// coding agent keeps running, so each hook reads the current one.
|
|
206
|
+
...(envFile ? [`if [ -r ${shellQuote(envFile)} ]; then . ${shellQuote(envFile)}; fi`] : []),
|
|
187
207
|
`if [ -x ${shellQuote(nodePath)} ]; then`,
|
|
188
208
|
` exec ${shellQuote(nodePath)} ${shellQuote(runtimePath)} "$@"`,
|
|
189
209
|
'fi',
|
|
@@ -213,10 +233,17 @@ function shellHookCommand(launcherPath, profileName, command, agentId = null, pl
|
|
|
213
233
|
return `${quotedLauncher} ${MANAGED_HOOK_MARKER} --profile ${profileName} --timeout-ms 30000 ${command}${agentFlag}`;
|
|
214
234
|
}
|
|
215
235
|
|
|
216
|
-
function
|
|
236
|
+
function recallOnlyHookCommand(launcherPath, stateDir) {
|
|
237
|
+
// No --profile: the hook signs in with the session's injected credential.
|
|
238
|
+
return `${shellQuote(launcherPath)} ${MANAGED_HOOK_MARKER} --timeout-ms 30000 agent-context --recall-only --state-dir ${shellQuote(stateDir)}`;
|
|
239
|
+
}
|
|
240
|
+
|
|
241
|
+
function contextHandler(agentId, profileName, launcherPath, platform, recallOnly = null) {
|
|
217
242
|
return {
|
|
218
243
|
type: 'command',
|
|
219
|
-
command:
|
|
244
|
+
command: recallOnly
|
|
245
|
+
? recallOnlyHookCommand(launcherPath, recallOnly.stateDir)
|
|
246
|
+
: shellHookCommand(launcherPath, profileName, 'agent-context', null, platform),
|
|
220
247
|
timeout: 30,
|
|
221
248
|
statusMessage: 'Loading relevant Notis memory',
|
|
222
249
|
...(agentId === 'codex' ? { additionalContextLimit: 2500 } : {}),
|
|
@@ -239,20 +266,24 @@ function upsertPromptHooks(
|
|
|
239
266
|
enabled,
|
|
240
267
|
launcherPath = '',
|
|
241
268
|
platform = process.platform,
|
|
269
|
+
recallOnly = null,
|
|
242
270
|
) {
|
|
243
271
|
const settings = readJsonObject(filePath);
|
|
244
272
|
const hooks = settings.hooks && typeof settings.hooks === 'object' && !Array.isArray(settings.hooks)
|
|
245
273
|
? { ...settings.hooks }
|
|
246
274
|
: {};
|
|
247
275
|
let removedManagedHook = false;
|
|
276
|
+
const managed = [];
|
|
248
277
|
for (const event of ['SessionStart', 'UserPromptSubmit', 'Stop']) {
|
|
249
278
|
const previousGroups = Array.isArray(hooks[event]) ? hooks[event] : [];
|
|
250
279
|
const groups = removePriorNotisPromptHooks(previousGroups);
|
|
251
280
|
if (JSON.stringify(groups) !== JSON.stringify(previousGroups)) removedManagedHook = true;
|
|
252
|
-
if (enabled) {
|
|
253
|
-
|
|
281
|
+
if (enabled && (!recallOnly || RECALL_ONLY_HOOK_EVENTS.includes(event))) {
|
|
282
|
+
const handler = event === 'Stop'
|
|
254
283
|
? captureHandler(agentId, profileName, launcherPath, platform)
|
|
255
|
-
: contextHandler(agentId, profileName, launcherPath, platform
|
|
284
|
+
: contextHandler(agentId, profileName, launcherPath, platform, recallOnly);
|
|
285
|
+
groups.push({ hooks: [handler] });
|
|
286
|
+
managed.push({ event, groupIndex: groups.length - 1, handlerIndex: 0, handler });
|
|
256
287
|
}
|
|
257
288
|
if (groups.length) hooks[event] = groups;
|
|
258
289
|
else delete hooks[event];
|
|
@@ -260,15 +291,90 @@ function upsertPromptHooks(
|
|
|
260
291
|
const next = { ...settings, hooks };
|
|
261
292
|
const serialized = `${JSON.stringify(next, null, 2)}\n`;
|
|
262
293
|
const existing = readText(filePath);
|
|
263
|
-
if (!enabled && !removedManagedHook) return { path: filePath, status: 'unchanged' };
|
|
264
|
-
if (existing === serialized) return { path: filePath, status: 'unchanged' };
|
|
294
|
+
if (!enabled && !removedManagedHook) return { path: filePath, status: 'unchanged', managed };
|
|
295
|
+
if (existing === serialized) return { path: filePath, status: 'unchanged', managed };
|
|
265
296
|
atomicWrite(filePath, serialized);
|
|
266
297
|
return {
|
|
267
298
|
path: filePath,
|
|
268
299
|
status: enabled
|
|
269
300
|
? existsSync(filePath) && existing ? 'updated' : 'installed'
|
|
270
301
|
: 'removed',
|
|
302
|
+
managed,
|
|
303
|
+
};
|
|
304
|
+
}
|
|
305
|
+
|
|
306
|
+
function canonicalJson(value) {
|
|
307
|
+
if (Array.isArray(value)) return value.map(canonicalJson);
|
|
308
|
+
if (value && typeof value === 'object') {
|
|
309
|
+
return Object.fromEntries(Object.keys(value).sort().map((key) => [key, canonicalJson(value[key])]));
|
|
310
|
+
}
|
|
311
|
+
return value;
|
|
312
|
+
}
|
|
313
|
+
|
|
314
|
+
/**
|
|
315
|
+
* The hash Codex records when a user trusts one hook in `/hooks`: SHA-256 of
|
|
316
|
+
* the event's normalized handler group, serialized with sorted keys. Matches
|
|
317
|
+
* the pinned Codex 0.160 app-server `hooks/list` currentHash for the handler
|
|
318
|
+
* shape this installer writes (no matcher; additionalContextLimit not hashed).
|
|
319
|
+
*/
|
|
320
|
+
export function codexHookTrustHash(event, handler) {
|
|
321
|
+
const label = CODEX_EVENT_LABELS[event];
|
|
322
|
+
if (!label) throw new Error(`Unsupported Codex hook event: ${event}`);
|
|
323
|
+
const normalized = {
|
|
324
|
+
type: 'command',
|
|
325
|
+
command: handler.command,
|
|
326
|
+
timeout: handler.timeout,
|
|
327
|
+
async: false,
|
|
328
|
+
...(handler.statusMessage ? { statusMessage: handler.statusMessage } : {}),
|
|
271
329
|
};
|
|
330
|
+
const digest = createHash('sha256')
|
|
331
|
+
.update(JSON.stringify(canonicalJson({ event_name: label, hooks: [normalized] })))
|
|
332
|
+
.digest('hex');
|
|
333
|
+
return `sha256:${digest}`;
|
|
334
|
+
}
|
|
335
|
+
|
|
336
|
+
/** The `hooks.state` key Codex uses for one handler in a hooks.json file. */
|
|
337
|
+
export function codexHookTrustKey(hooksPath, event, groupIndex, handlerIndex = 0) {
|
|
338
|
+
return `${hooksPath}:${CODEX_EVENT_LABELS[event]}:${groupIndex}:${handlerIndex}`;
|
|
339
|
+
}
|
|
340
|
+
|
|
341
|
+
function tomlBasicString(value) {
|
|
342
|
+
return `"${String(value).replace(/\\/g, '\\\\').replace(/"/g, '\\"')}"`;
|
|
343
|
+
}
|
|
344
|
+
|
|
345
|
+
/**
|
|
346
|
+
* Record trust for the managed Codex hooks in config.toml, so a headless Codex
|
|
347
|
+
* runs them without the interactive `/hooks` review. Only the block between
|
|
348
|
+
* the managed markers is rewritten; a key the user already trusts elsewhere is
|
|
349
|
+
* left alone, because a second TOML table with the same name breaks parsing.
|
|
350
|
+
*/
|
|
351
|
+
export function upsertCodexHookTrust(configPath, hooksPath, managed) {
|
|
352
|
+
const existing = readText(configPath);
|
|
353
|
+
const blockPattern = new RegExp(
|
|
354
|
+
`\\n?${CODEX_TRUST_START}[\\s\\S]*?${CODEX_TRUST_END}\\n?`,
|
|
355
|
+
'g',
|
|
356
|
+
);
|
|
357
|
+
const outside = existing.replace(blockPattern, '\n');
|
|
358
|
+
const resolvedHooksPath = realpathSync(hooksPath);
|
|
359
|
+
const entries = [];
|
|
360
|
+
const skipped = [];
|
|
361
|
+
for (const item of managed) {
|
|
362
|
+
const key = codexHookTrustKey(resolvedHooksPath, item.event, item.groupIndex, item.handlerIndex);
|
|
363
|
+
const header = `[hooks.state.${tomlBasicString(key)}]`;
|
|
364
|
+
if (outside.includes(header)) {
|
|
365
|
+
skipped.push(key);
|
|
366
|
+
continue;
|
|
367
|
+
}
|
|
368
|
+
entries.push(`${header}\ntrusted_hash = ${tomlBasicString(codexHookTrustHash(item.event, item.handler))}`);
|
|
369
|
+
}
|
|
370
|
+
const prefix = outside.replace(/\n+$/, '');
|
|
371
|
+
const block = entries.length
|
|
372
|
+
? `${CODEX_TRUST_START}\n${entries.join('\n\n')}\n${CODEX_TRUST_END}\n`
|
|
373
|
+
: '';
|
|
374
|
+
const next = prefix && block ? `${prefix}\n\n${block}` : prefix ? `${prefix}\n` : block;
|
|
375
|
+
if (next === existing) return { path: configPath, status: 'unchanged', skipped };
|
|
376
|
+
atomicWrite(configPath, next);
|
|
377
|
+
return { path: configPath, status: existing ? 'updated' : 'installed', skipped };
|
|
272
378
|
}
|
|
273
379
|
|
|
274
380
|
function agentPaths(agentId, home) {
|
|
@@ -336,7 +442,7 @@ export function installAgentSetup({
|
|
|
336
442
|
result.memory_hook = { path: paths.hooks, status: 'preserved' };
|
|
337
443
|
} else {
|
|
338
444
|
try {
|
|
339
|
-
|
|
445
|
+
const { managed: _managed, ...memoryHook } = upsertPromptHooks(
|
|
340
446
|
paths.hooks,
|
|
341
447
|
agentId,
|
|
342
448
|
profileName,
|
|
@@ -344,6 +450,7 @@ export function installAgentSetup({
|
|
|
344
450
|
hookRuntime?.launcherPath,
|
|
345
451
|
platform,
|
|
346
452
|
);
|
|
453
|
+
result.memory_hook = memoryHook;
|
|
347
454
|
} catch (error) {
|
|
348
455
|
result.memory_hook = {
|
|
349
456
|
path: paths.hooks,
|
|
@@ -357,6 +464,56 @@ export function installAgentSetup({
|
|
|
357
464
|
return results;
|
|
358
465
|
}
|
|
359
466
|
|
|
467
|
+
/**
|
|
468
|
+
* Recall-only memory hooks for a hosted coding agent (the Cloud Computer's
|
|
469
|
+
* native Codex and Claude Code). They go into the agent's own config folder
|
|
470
|
+
* (CODEX_HOME or CLAUDE_CONFIG_DIR), sign in with the session's injected
|
|
471
|
+
* credential, run only on SessionStart and UserPromptSubmit, and never capture
|
|
472
|
+
* a turn. No instruction file is written: the hosted contract already carries
|
|
473
|
+
* the Notis guidance. Codex trust is recorded so no `/hooks` review is needed.
|
|
474
|
+
*/
|
|
475
|
+
export function installRecallOnlyAgentHooks({
|
|
476
|
+
agentId,
|
|
477
|
+
configDir,
|
|
478
|
+
envFile = null,
|
|
479
|
+
bundlePath = HOOK_BUNDLE_PATH,
|
|
480
|
+
nodePath = process.execPath,
|
|
481
|
+
platform = process.platform,
|
|
482
|
+
} = {}) {
|
|
483
|
+
if (!AGENT_IDS.includes(agentId)) throw new Error(`Unsupported agent: ${agentId}`);
|
|
484
|
+
if (platform === 'win32') throw new Error('Recall-only hooks are only supported on POSIX hosts');
|
|
485
|
+
if (typeof configDir !== 'string' || !isAbsolute(configDir)) {
|
|
486
|
+
throw new Error('Recall-only hooks need the agent config folder as an absolute path');
|
|
487
|
+
}
|
|
488
|
+
const runtime = installManagedHookRuntime({
|
|
489
|
+
home: configDir, bundlePath, nodePath, platform, envFile,
|
|
490
|
+
});
|
|
491
|
+
const hooksPath = join(configDir, agentId === 'codex' ? 'hooks.json' : 'settings.json');
|
|
492
|
+
const result = { agent: agentId, mode: 'recall_only', instructions: null, memory_hook: null };
|
|
493
|
+
try {
|
|
494
|
+
const { managed, ...memoryHook } = upsertPromptHooks(
|
|
495
|
+
hooksPath,
|
|
496
|
+
agentId,
|
|
497
|
+
null,
|
|
498
|
+
true,
|
|
499
|
+
runtime.launcherPath,
|
|
500
|
+
platform,
|
|
501
|
+
{ stateDir: configDir },
|
|
502
|
+
);
|
|
503
|
+
result.memory_hook = memoryHook;
|
|
504
|
+
if (agentId === 'codex') {
|
|
505
|
+
result.hook_trust = upsertCodexHookTrust(join(configDir, 'config.toml'), hooksPath, managed);
|
|
506
|
+
}
|
|
507
|
+
} catch (error) {
|
|
508
|
+
result.memory_hook = {
|
|
509
|
+
path: hooksPath,
|
|
510
|
+
status: 'error',
|
|
511
|
+
message: error instanceof Error ? error.message : String(error),
|
|
512
|
+
};
|
|
513
|
+
}
|
|
514
|
+
return result;
|
|
515
|
+
}
|
|
516
|
+
|
|
360
517
|
export function detectedAgentIds(home = homedir()) {
|
|
361
518
|
return AGENT_IDS.filter((agentId) => {
|
|
362
519
|
const root = agentPaths(agentId, home).root;
|
|
@@ -367,10 +524,12 @@ export function detectedAgentIds(home = homedir()) {
|
|
|
367
524
|
const skillsRoot = join(root, 'skills');
|
|
368
525
|
if (!existsSync(skillsRoot)) return false;
|
|
369
526
|
// Base reconciliation intentionally creates every supported vendor root.
|
|
370
|
-
// A root containing only our
|
|
371
|
-
//
|
|
372
|
-
|
|
373
|
-
|
|
527
|
+
// A root containing only our base links, or the links an earlier CLI
|
|
528
|
+
// made under retired names, is not evidence that the vendor itself is
|
|
529
|
+
// installed; any other skill entry is a genuine user signal.
|
|
530
|
+
return readdirSync(skillsRoot).some((name) => !(
|
|
531
|
+
BASE_SKILL_NAMES.includes(name)
|
|
532
|
+
|| (RETIRED_BASE_SKILL_NAMES.includes(name) && isBaseSkillLink(join(skillsRoot, name), name, { home }))
|
|
374
533
|
));
|
|
375
534
|
} catch {
|
|
376
535
|
return false;
|
|
@@ -3,6 +3,7 @@ import { dirname, extname, join, resolve } from 'node:path';
|
|
|
3
3
|
import { fileURLToPath } from 'node:url';
|
|
4
4
|
|
|
5
5
|
import { usageError } from './errors.js';
|
|
6
|
+
import { maskSerializedJsonData } from './app-data-literals.js';
|
|
6
7
|
|
|
7
8
|
const moduleDir = dirname(fileURLToPath(import.meta.url));
|
|
8
9
|
|
|
@@ -133,10 +134,12 @@ function validateTextFile(relPath, content) {
|
|
|
133
134
|
const extension = extname(relPath);
|
|
134
135
|
const rules = getCompiledRules();
|
|
135
136
|
if (extension === '.css' || extension === '.scss' || extension === '.sass' || extension === '.pcss') {
|
|
136
|
-
|
|
137
|
+
// A comment that mentions html, body or :root is not a selector. Blanking
|
|
138
|
+
// comments keeps offsets and line numbers; every rule still runs unchanged.
|
|
139
|
+
return applyRules(maskComments(content, '.css'), rules.css, relPath);
|
|
137
140
|
}
|
|
138
141
|
if (extension === '.js' || extension === '.jsx' || extension === '.ts' || extension === '.tsx') {
|
|
139
|
-
return applyRules(content, rules.javascript, relPath);
|
|
142
|
+
return applyRules(maskSerializedJsonData(content, extension), rules.javascript, relPath);
|
|
140
143
|
}
|
|
141
144
|
return [];
|
|
142
145
|
}
|
|
@@ -181,3 +184,336 @@ export function validateArtifactBoundary(files) {
|
|
|
181
184
|
);
|
|
182
185
|
}
|
|
183
186
|
}
|
|
187
|
+
|
|
188
|
+
// ---------------------------------------------------------------------------
|
|
189
|
+
// Design rules
|
|
190
|
+
//
|
|
191
|
+
// The portal boundary rules above keep an app inside its surface. The design
|
|
192
|
+
// rules below keep it looking like a native, flat Notis page: no bordered
|
|
193
|
+
// boxes around items, no dividers, no palette colors, no eyebrows, no loading
|
|
194
|
+
// text. They are path-scoped, report line numbers, and only ever run on the
|
|
195
|
+
// app's own source files (never on the bundled artifact, where every
|
|
196
|
+
// dependency legitimately contains the word "border").
|
|
197
|
+
//
|
|
198
|
+
// The only override is an inline directive on the same line or the line
|
|
199
|
+
// before the match:
|
|
200
|
+
// // notis-design-allow: <rule-id> <reason of at least N characters>
|
|
201
|
+
// Allowed matches are reported with `allowed: true` so they stay visible.
|
|
202
|
+
// ---------------------------------------------------------------------------
|
|
203
|
+
|
|
204
|
+
export const DESIGN_RULES_PATH_CANDIDATES = [
|
|
205
|
+
resolve(moduleDir, '../../../../server/config/notis_app_design_rules.json'),
|
|
206
|
+
resolve(moduleDir, '../../config/notis_app_design_rules.json'),
|
|
207
|
+
];
|
|
208
|
+
|
|
209
|
+
export function resolveDesignRulesPath() {
|
|
210
|
+
for (const candidate of DESIGN_RULES_PATH_CANDIDATES) {
|
|
211
|
+
if (existsSync(candidate)) {
|
|
212
|
+
return candidate;
|
|
213
|
+
}
|
|
214
|
+
}
|
|
215
|
+
return DESIGN_RULES_PATH_CANDIDATES[DESIGN_RULES_PATH_CANDIDATES.length - 1];
|
|
216
|
+
}
|
|
217
|
+
|
|
218
|
+
const DEFAULT_DESIGN_EXTENSIONS = ['.js', '.jsx', '.ts', '.tsx', '.css'];
|
|
219
|
+
|
|
220
|
+
function globToRegExp(glob) {
|
|
221
|
+
let source = '';
|
|
222
|
+
for (let index = 0; index < glob.length; index += 1) {
|
|
223
|
+
const char = glob[index];
|
|
224
|
+
if (char === '*') {
|
|
225
|
+
if (glob[index + 1] === '*') {
|
|
226
|
+
index += 1;
|
|
227
|
+
if (glob[index + 1] === '/') {
|
|
228
|
+
index += 1;
|
|
229
|
+
source += '(?:.*/)?';
|
|
230
|
+
} else {
|
|
231
|
+
source += '.*';
|
|
232
|
+
}
|
|
233
|
+
} else {
|
|
234
|
+
source += '[^/]*';
|
|
235
|
+
}
|
|
236
|
+
} else if (char === '?') {
|
|
237
|
+
source += '[^/]';
|
|
238
|
+
} else {
|
|
239
|
+
source += char.replace(/[.+^${}()|[\]\\]/g, '\\$&');
|
|
240
|
+
}
|
|
241
|
+
}
|
|
242
|
+
return new RegExp(`^${source}$`);
|
|
243
|
+
}
|
|
244
|
+
|
|
245
|
+
function matchesAny(relPath, globs) {
|
|
246
|
+
return (globs || []).some((glob) => globToRegExp(glob).test(relPath));
|
|
247
|
+
}
|
|
248
|
+
|
|
249
|
+
const REGEX_PRECEDERS = new Set(['', '(', ',', '=', ':', '[', '!', '&', '|', '?', '{', '}', ';', '+', '-', '*', '%', '>', '~', '^']);
|
|
250
|
+
|
|
251
|
+
function blankRange(chars, start, end) {
|
|
252
|
+
for (let index = start; index < end; index += 1) {
|
|
253
|
+
if (chars[index] !== '\n') chars[index] = ' ';
|
|
254
|
+
}
|
|
255
|
+
}
|
|
256
|
+
|
|
257
|
+
/**
|
|
258
|
+
* Blank out comments so the design rules never match words inside them. Every
|
|
259
|
+
* character keeps its offset and newlines stay, so line numbers do not move.
|
|
260
|
+
* CSS has only block comments; JS and TS also have line comments. Quoted
|
|
261
|
+
* strings end at a newline so an apostrophe in JSX text cannot swallow a file,
|
|
262
|
+
* and "scheme://" is never a comment. Mirrored by mask_comments in
|
|
263
|
+
* server/lib/app_boundary_validator.py.
|
|
264
|
+
*/
|
|
265
|
+
export function maskComments(content, extension) {
|
|
266
|
+
const isCss = extension === '.css';
|
|
267
|
+
const chars = content.split('');
|
|
268
|
+
const length = chars.length;
|
|
269
|
+
let index = 0;
|
|
270
|
+
let previous = '';
|
|
271
|
+
while (index < length) {
|
|
272
|
+
const char = chars[index];
|
|
273
|
+
const next = chars[index + 1];
|
|
274
|
+
if (char === '/' && next === '*') {
|
|
275
|
+
const close = content.indexOf('*/', index + 2);
|
|
276
|
+
const end = close === -1 ? length : close + 2;
|
|
277
|
+
blankRange(chars, index, end);
|
|
278
|
+
index = end;
|
|
279
|
+
continue;
|
|
280
|
+
}
|
|
281
|
+
if (!isCss && char === '/' && next === '/' && !(chars[index - 1] === ':' && /[A-Za-z0-9]/.test(chars[index - 2] || ''))) {
|
|
282
|
+
let end = content.indexOf('\n', index);
|
|
283
|
+
if (end === -1) end = length;
|
|
284
|
+
blankRange(chars, index, end);
|
|
285
|
+
index = end;
|
|
286
|
+
continue;
|
|
287
|
+
}
|
|
288
|
+
if (char === '"' || char === "'" || (!isCss && char === '`')) {
|
|
289
|
+
let cursor = index + 1;
|
|
290
|
+
while (cursor < length) {
|
|
291
|
+
const inner = chars[cursor];
|
|
292
|
+
if (inner === '\\') { cursor += 2; continue; }
|
|
293
|
+
if (inner === char) { cursor += 1; break; }
|
|
294
|
+
if (inner === '\n' && char !== '`') break;
|
|
295
|
+
cursor += 1;
|
|
296
|
+
}
|
|
297
|
+
previous = char;
|
|
298
|
+
index = cursor;
|
|
299
|
+
continue;
|
|
300
|
+
}
|
|
301
|
+
if (!isCss && char === '/' && REGEX_PRECEDERS.has(previous)) {
|
|
302
|
+
let cursor = index + 1;
|
|
303
|
+
let inClass = false;
|
|
304
|
+
while (cursor < length && chars[cursor] !== '\n') {
|
|
305
|
+
const inner = chars[cursor];
|
|
306
|
+
if (inner === '\\') { cursor += 2; continue; }
|
|
307
|
+
if (inner === '[') inClass = true;
|
|
308
|
+
else if (inner === ']') inClass = false;
|
|
309
|
+
else if (inner === '/' && !inClass) { cursor += 1; break; }
|
|
310
|
+
cursor += 1;
|
|
311
|
+
}
|
|
312
|
+
previous = '/';
|
|
313
|
+
index = cursor;
|
|
314
|
+
continue;
|
|
315
|
+
}
|
|
316
|
+
if (!/\s/.test(char)) previous = char;
|
|
317
|
+
index += 1;
|
|
318
|
+
}
|
|
319
|
+
return chars.join('');
|
|
320
|
+
}
|
|
321
|
+
|
|
322
|
+
const compiledDesignRules = new Map();
|
|
323
|
+
|
|
324
|
+
/**
|
|
325
|
+
* Load the design rules for one profile. The default profile ("app") keeps the
|
|
326
|
+
* file's top-level include/exclude. A named profile (for example "space")
|
|
327
|
+
* replaces the include with its own, adds its excludes, and turns on the rules
|
|
328
|
+
* that list it in `profiles`. Rules naming a `toolkit_piece` stay warnings until
|
|
329
|
+
* that piece appears in `toolkit_live_pieces`, then they refuse like errors.
|
|
330
|
+
*/
|
|
331
|
+
export function loadDesignRules({ rulesPath = resolveDesignRulesPath(), profile = 'app' } = {}) {
|
|
332
|
+
let payload = null;
|
|
333
|
+
try {
|
|
334
|
+
const parsed = JSON.parse(readFileSync(rulesPath, 'utf-8'));
|
|
335
|
+
if (parsed && typeof parsed === 'object' && !Array.isArray(parsed)) {
|
|
336
|
+
payload = parsed;
|
|
337
|
+
}
|
|
338
|
+
} catch (error) {
|
|
339
|
+
process.stderr.write(
|
|
340
|
+
`Warning: could not load Notis app design rules (${error.message}); skipping design checks.\n`,
|
|
341
|
+
);
|
|
342
|
+
}
|
|
343
|
+
if (!payload) {
|
|
344
|
+
return { profile, include: [], exclude: [], extensions: DEFAULT_DESIGN_EXTENSIONS, rules: [], allowDirective: 'notis-design-allow', allowReasonMinLength: 12 };
|
|
345
|
+
}
|
|
346
|
+
const profiles = payload.profiles && typeof payload.profiles === 'object' && !Array.isArray(payload.profiles) ? payload.profiles : {};
|
|
347
|
+
const selected = profile !== 'app' && profiles[profile] && typeof profiles[profile] === 'object' ? profiles[profile] : null;
|
|
348
|
+
const baseInclude = Array.isArray(payload.include) ? payload.include : ['app/**', 'components/**'];
|
|
349
|
+
const baseExclude = Array.isArray(payload.exclude) ? payload.exclude : [];
|
|
350
|
+
const livePieces = new Set(Array.isArray(payload.toolkit_live_pieces) ? payload.toolkit_live_pieces : []);
|
|
351
|
+
return {
|
|
352
|
+
profile,
|
|
353
|
+
include: Array.isArray(selected?.include) ? selected.include : baseInclude,
|
|
354
|
+
exclude: [...baseExclude, ...(Array.isArray(selected?.exclude) ? selected.exclude : [])],
|
|
355
|
+
extensions: Array.isArray(payload.extensions) ? payload.extensions : DEFAULT_DESIGN_EXTENSIONS,
|
|
356
|
+
allowDirective: typeof payload.allow_directive === 'string' ? payload.allow_directive : 'notis-design-allow',
|
|
357
|
+
allowReasonMinLength: Number.isInteger(payload.allow_reason_min_length) ? payload.allow_reason_min_length : 12,
|
|
358
|
+
rules: (Array.isArray(payload.rules) ? payload.rules : [])
|
|
359
|
+
.filter((rule) => !Array.isArray(rule.profiles) || rule.profiles.includes(profile))
|
|
360
|
+
.map((rule) => ({
|
|
361
|
+
id: rule.id,
|
|
362
|
+
severity: typeof rule.toolkit_piece === 'string'
|
|
363
|
+
? (livePieces.has(rule.toolkit_piece) ? 'error' : 'warn')
|
|
364
|
+
: rule.severity === 'warn' ? 'warn' : 'error',
|
|
365
|
+
toolkitPiece: typeof rule.toolkit_piece === 'string' ? rule.toolkit_piece : null,
|
|
366
|
+
scope: rule.scope === 'file' ? 'file' : 'line',
|
|
367
|
+
regex: new RegExp(rule.pattern, rule.scope === 'file' ? '' : 'gm'),
|
|
368
|
+
message: typeof rule.profile_messages?.[profile] === 'string' ? rule.profile_messages[profile] : rule.message,
|
|
369
|
+
include: Array.isArray(rule.include) ? rule.include : null,
|
|
370
|
+
exclude: Array.isArray(rule.exclude) ? rule.exclude : [],
|
|
371
|
+
})),
|
|
372
|
+
};
|
|
373
|
+
}
|
|
374
|
+
|
|
375
|
+
function getCompiledDesignRules(profile = 'app') {
|
|
376
|
+
if (!compiledDesignRules.has(profile)) {
|
|
377
|
+
compiledDesignRules.set(profile, loadDesignRules({ profile }));
|
|
378
|
+
}
|
|
379
|
+
return compiledDesignRules.get(profile);
|
|
380
|
+
}
|
|
381
|
+
|
|
382
|
+
function lineNumberAt(content, index) {
|
|
383
|
+
let line = 1;
|
|
384
|
+
for (let cursor = 0; cursor < index; cursor += 1) {
|
|
385
|
+
if (content.charCodeAt(cursor) === 10) line += 1;
|
|
386
|
+
}
|
|
387
|
+
return line;
|
|
388
|
+
}
|
|
389
|
+
|
|
390
|
+
function findAllowDirective(lines, lineNumber, ruleId, directive) {
|
|
391
|
+
const pattern = new RegExp(`${directive}:\\s*${ruleId.replace(/[.*+?^${}()|[\]\\]/g, '\\$&')}\\b\\s*(.*)$`);
|
|
392
|
+
for (const candidate of [lines[lineNumber - 1], lines[lineNumber - 2]]) {
|
|
393
|
+
if (typeof candidate !== 'string') continue;
|
|
394
|
+
const match = candidate.match(pattern);
|
|
395
|
+
if (match) {
|
|
396
|
+
return match[1].replace(/\*\/\s*}?\s*$/, '').replace(/-->\s*$/, '').trim();
|
|
397
|
+
}
|
|
398
|
+
}
|
|
399
|
+
return null;
|
|
400
|
+
}
|
|
401
|
+
|
|
402
|
+
export function collectDesignViolationsForFile(relPath, content, config = getCompiledDesignRules()) {
|
|
403
|
+
const normalized = relPath.split('\\').join('/');
|
|
404
|
+
if (!config.extensions.includes(extname(normalized))) return [];
|
|
405
|
+
if (!matchesAny(normalized, config.include)) return [];
|
|
406
|
+
if (matchesAny(normalized, config.exclude)) return [];
|
|
407
|
+
|
|
408
|
+
const extension = extname(normalized);
|
|
409
|
+
// Data is masked first so it can neither break a rule nor supply an allow
|
|
410
|
+
// directive; comments are masked after, for matching only, so a real
|
|
411
|
+
// directive comment is still read from `lines`.
|
|
412
|
+
const unserialized = ['.js', '.jsx', '.ts', '.tsx'].includes(extension)
|
|
413
|
+
? maskSerializedJsonData(content, extension) : content;
|
|
414
|
+
const checked = maskComments(unserialized, extension);
|
|
415
|
+
const lines = unserialized.split('\n');
|
|
416
|
+
const violations = [];
|
|
417
|
+
for (const rule of config.rules) {
|
|
418
|
+
if (rule.include && !matchesAny(normalized, rule.include)) continue;
|
|
419
|
+
if (matchesAny(normalized, rule.exclude)) continue;
|
|
420
|
+
|
|
421
|
+
const matches = [];
|
|
422
|
+
if (rule.scope === 'file') {
|
|
423
|
+
const match = rule.regex.exec(checked);
|
|
424
|
+
if (match) matches.push(match.index);
|
|
425
|
+
} else {
|
|
426
|
+
rule.regex.lastIndex = 0;
|
|
427
|
+
let match;
|
|
428
|
+
while ((match = rule.regex.exec(checked)) !== null) {
|
|
429
|
+
matches.push(match.index);
|
|
430
|
+
if (match[0].length === 0) rule.regex.lastIndex += 1;
|
|
431
|
+
}
|
|
432
|
+
}
|
|
433
|
+
|
|
434
|
+
const seenLines = new Set();
|
|
435
|
+
for (const index of matches) {
|
|
436
|
+
const line = lineNumberAt(content, index);
|
|
437
|
+
if (seenLines.has(line)) continue;
|
|
438
|
+
seenLines.add(line);
|
|
439
|
+
const reason = findAllowDirective(lines, line, rule.id, config.allowDirective);
|
|
440
|
+
const allowed = reason !== null && reason.length >= config.allowReasonMinLength;
|
|
441
|
+
violations.push({
|
|
442
|
+
file: normalized,
|
|
443
|
+
line,
|
|
444
|
+
ruleId: rule.id,
|
|
445
|
+
severity: allowed ? 'warn' : rule.severity,
|
|
446
|
+
message: reason !== null && !allowed
|
|
447
|
+
? `${rule.message} (a notis-design-allow directive needs a reason of at least ${config.allowReasonMinLength} characters)`
|
|
448
|
+
: rule.message,
|
|
449
|
+
allowed,
|
|
450
|
+
reason: allowed ? reason : null,
|
|
451
|
+
});
|
|
452
|
+
}
|
|
453
|
+
}
|
|
454
|
+
return violations;
|
|
455
|
+
}
|
|
456
|
+
|
|
457
|
+
export function collectProjectDesignViolations(projectDir, config = getCompiledDesignRules()) {
|
|
458
|
+
const files = [];
|
|
459
|
+
collectProjectFiles(projectDir, projectDir, files);
|
|
460
|
+
return files.flatMap((file) => collectDesignViolationsForFile(file.relPath, file.content, config));
|
|
461
|
+
}
|
|
462
|
+
|
|
463
|
+
export function collectSourceDesignViolations(files, config = getCompiledDesignRules(), { encoding = 'utf-8' } = {}) {
|
|
464
|
+
return Object.entries(files).flatMap(([relPath, rawContent]) => {
|
|
465
|
+
const content = Buffer.isBuffer(rawContent)
|
|
466
|
+
? rawContent.toString('utf-8')
|
|
467
|
+
: typeof rawContent === 'string'
|
|
468
|
+
? (encoding === 'base64' ? Buffer.from(rawContent, 'base64').toString('utf-8') : rawContent)
|
|
469
|
+
: String(rawContent ?? '');
|
|
470
|
+
return collectDesignViolationsForFile(relPath, content, config);
|
|
471
|
+
});
|
|
472
|
+
}
|
|
473
|
+
|
|
474
|
+
export function formatDesignViolation(violation) {
|
|
475
|
+
const prefix = violation.allowed ? 'allowed' : violation.severity;
|
|
476
|
+
const suffix = violation.allowed ? ` (${violation.reason})` : '';
|
|
477
|
+
return `${violation.file}:${violation.line} [${violation.ruleId}] ${prefix}: ${violation.message}${suffix}`;
|
|
478
|
+
}
|
|
479
|
+
|
|
480
|
+
function enforceDesign(violations, { enforce, log, subject }) {
|
|
481
|
+
const blocking = violations.filter((violation) => !violation.allowed && violation.severity === 'error');
|
|
482
|
+
const nonBlocking = violations.filter((violation) => !blocking.includes(violation));
|
|
483
|
+
if (typeof log === 'function') {
|
|
484
|
+
for (const violation of nonBlocking) log(`[design] ${formatDesignViolation(violation)}`);
|
|
485
|
+
if (!enforce) for (const violation of blocking) log(`[design] ${formatDesignViolation(violation)}`);
|
|
486
|
+
}
|
|
487
|
+
if (enforce && blocking.length > 0) {
|
|
488
|
+
throw usageError(
|
|
489
|
+
`${subject} violates the Notis ${subject === 'Space' ? 'Spaces' : 'app'} design bar (${blocking.length} issue${blocking.length === 1 ? '' : 's'}). `
|
|
490
|
+
+ 'Fix each line below; the only override is an inline "// notis-design-allow: <rule-id> <reason>" comment on the line before.\n'
|
|
491
|
+
+ blocking.map((violation) => ` - ${formatDesignViolation(violation)}`).join('\n'),
|
|
492
|
+
);
|
|
493
|
+
}
|
|
494
|
+
return violations;
|
|
495
|
+
}
|
|
496
|
+
|
|
497
|
+
/**
|
|
498
|
+
* Enforce the design rules on an app's or a Space's source tree.
|
|
499
|
+
*
|
|
500
|
+
* Returns every violation (allowed ones included) so callers can print
|
|
501
|
+
* warnings. When `enforce` is true (build, verify, screenshot, deploy) any
|
|
502
|
+
* unallowed error-severity violation aborts with a usage error that lists the
|
|
503
|
+
* exact file and line to fix. Dev servers pass `enforce: false` so a
|
|
504
|
+
* half-edited page still reloads. Spaces pass `profile: 'space'`.
|
|
505
|
+
*/
|
|
506
|
+
export function validateProjectDesign(projectDir, { enforce = true, log = null, profile = 'app' } = {}) {
|
|
507
|
+
const violations = collectProjectDesignViolations(projectDir, getCompiledDesignRules(profile));
|
|
508
|
+
return enforceDesign(violations, { enforce, log, subject: profile === 'space' ? 'Space' : 'Project' });
|
|
509
|
+
}
|
|
510
|
+
|
|
511
|
+
/**
|
|
512
|
+
* The same check on an in-memory source snapshot (the exact bytes a Space
|
|
513
|
+
* release uploads). The server re-runs it with the same rules file on
|
|
514
|
+
* source-verify, source-stage and source-publish.
|
|
515
|
+
*/
|
|
516
|
+
export function validateSourceDesign(files, { enforce = true, log = null, profile = 'space', encoding = 'utf-8' } = {}) {
|
|
517
|
+
const violations = collectSourceDesignViolations(files, getCompiledDesignRules(profile), { encoding });
|
|
518
|
+
return enforceDesign(violations, { enforce, log, subject: profile === 'space' ? 'Space' : 'Project' });
|
|
519
|
+
}
|