@notis_ai/cli 0.2.13 → 0.2.15
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +125 -21
- package/dist/agent-hooks/notis-agent-hook.mjs +19750 -0
- package/{skills → dist/base-skills}/notis-apps/SKILL.md +93 -58
- package/{skills → dist/base-skills}/notis-cli/SKILL.md +9 -9
- package/dist/base-skills/notis-query/SKILL.md +705 -0
- package/dist/skill-sync/index.js +1528 -0
- package/dist/skill-sync/index.js.map +7 -0
- package/package.json +5 -2
- package/skills/notis-apps/cli.md +44 -18
- package/skills/notis-cli/AGENT_INSTRUCTIONS.md +39 -0
- package/skills/notis-onboarding/BRIEF.md +16 -0
- package/src/agent-hook-entry.js +5 -0
- package/src/cli.js +23 -14
- package/src/command-specs/agents.js +392 -0
- package/src/command-specs/apps.js +1090 -418
- package/src/command-specs/auth.js +63 -10
- package/src/command-specs/handover.js +374 -0
- package/src/command-specs/index.js +9 -0
- package/src/command-specs/onboarding.js +69 -5
- package/src/command-specs/skills.js +56 -0
- package/src/command-specs/tools.js +6 -0
- package/src/runtime/agent-memory-state.js +126 -0
- package/src/runtime/agent-setup.js +383 -0
- package/src/runtime/app-dev-consumers.js +154 -0
- package/src/runtime/app-dev-host-lock.js +80 -0
- package/src/runtime/app-dev-roots.js +284 -0
- package/src/runtime/app-dev-server.js +229 -30
- package/src/runtime/app-dev-sessions.js +105 -145
- package/src/runtime/app-platform.js +1499 -161
- package/src/runtime/app-registry-scaffolds.js +367 -0
- package/src/runtime/base-skills.d.ts +20 -0
- package/src/runtime/base-skills.js +167 -0
- package/src/runtime/delegated-context.js +68 -0
- package/src/runtime/git.js +233 -0
- package/src/runtime/login-listener.js +15 -0
- package/src/runtime/oauth.js +1506 -141
- package/src/runtime/output.js +7 -0
- package/src/runtime/ports.js +16 -0
- package/src/runtime/profiles.js +0 -5
- package/src/runtime/skill-sync/cloud-client.ts +96 -0
- package/src/runtime/skill-sync/index.ts +644 -0
- package/src/runtime/skill-sync/local-scanner.ts +1046 -0
- package/src/runtime/skill-sync/symlink-manager.ts +383 -0
- package/src/runtime/skill-sync/sync-plan.ts +22 -0
- package/src/runtime/skill-sync/types.ts +103 -0
- package/src/runtime/skill-sync/write-cloud-skill.ts +50 -0
- package/src/runtime/store-screenshot.js +6 -1
- package/src/runtime/sync-skills.d.ts +37 -0
- package/src/runtime/sync-skills.js +215 -0
- package/src/runtime/transport.js +19 -2
- package/template/.harness/index.html.tmpl +116 -47
- package/template/app/layout.tsx +5 -1
- package/template/notis.config.ts +1 -0
- package/template/packages/sdk/package.json +2 -1
- package/template/packages/sdk/src/components/DocumentEditor.tsx +13 -3
- package/template/packages/sdk/src/components/MarkdownEditor.tsx +121 -0
- package/template/packages/sdk/src/components/MultiSelectActionBar.tsx +37 -119
- package/template/packages/sdk/src/components/MultiSelectCheckbox.tsx +5 -1
- package/template/packages/sdk/src/components/MultiSelectDragOverlay.tsx +1 -1
- package/template/packages/sdk/src/components/NotisSelectionBoundary.tsx +55 -0
- package/template/packages/sdk/src/components/ShortcutHints.tsx +56 -0
- package/template/packages/sdk/src/config.ts +71 -0
- package/template/packages/sdk/src/documents.ts +28 -1
- package/template/packages/sdk/src/hooks/useActiveResource.ts +19 -0
- package/template/packages/sdk/src/hooks/useCloudComputer.ts +97 -0
- package/template/packages/sdk/src/hooks/useCollectionInteractions.ts +709 -0
- package/template/packages/sdk/src/hooks/useDatabaseSubscription.ts +76 -0
- package/template/packages/sdk/src/hooks/useHandover.ts +78 -0
- package/template/packages/sdk/src/hooks/useMultiSelect.ts +44 -482
- package/template/packages/sdk/src/hooks/useTool.ts +5 -4
- package/template/packages/sdk/src/index.ts +64 -2
- package/template/packages/sdk/src/interactions/actions.ts +46 -0
- package/template/packages/sdk/src/interactions/shortcuts.tsx +628 -0
- package/template/packages/sdk/src/interactions.ts +41 -0
- package/template/packages/sdk/src/provider.tsx +2 -1
- package/template/packages/sdk/src/runtime.ts +219 -3
- package/dist/scaffolds/notis-database/CHANGELOG.md +0 -5
- package/dist/scaffolds/notis-database/app/globals.css +0 -44
- package/dist/scaffolds/notis-database/app/layout.tsx +0 -6
- package/dist/scaffolds/notis-database/app/page.tsx +0 -1088
- package/dist/scaffolds/notis-database/components/ui/badge.tsx +0 -28
- package/dist/scaffolds/notis-database/components/ui/button.tsx +0 -53
- package/dist/scaffolds/notis-database/components/ui/card.tsx +0 -56
- package/dist/scaffolds/notis-database/components/ui/table.tsx +0 -120
- package/dist/scaffolds/notis-database/components.json +0 -20
- package/dist/scaffolds/notis-database/index.html +0 -12
- package/dist/scaffolds/notis-database/lib/types.ts +0 -132
- package/dist/scaffolds/notis-database/lib/utils.ts +0 -6
- package/dist/scaffolds/notis-database/metadata/screenshot-1.png +0 -0
- package/dist/scaffolds/notis-database/metadata/screenshot-2.png +0 -0
- package/dist/scaffolds/notis-database/metadata/screenshot-3.png +0 -0
- package/dist/scaffolds/notis-database/metadata/screenshot-4.png +0 -0
- package/dist/scaffolds/notis-database/metadata/screenshot-5.png +0 -0
- package/dist/scaffolds/notis-database/metadata/screenshot-fixtures.json +0 -1839
- package/dist/scaffolds/notis-database/notis.config.ts +0 -73
- package/dist/scaffolds/notis-database/package-lock.json +0 -3935
- package/dist/scaffolds/notis-database/package.json +0 -32
- package/dist/scaffolds/notis-database/packages/sdk/package.json +0 -36
- package/dist/scaffolds/notis-database/packages/sdk/src/components/DocumentEditor.tsx +0 -93
- package/dist/scaffolds/notis-database/packages/sdk/src/components/Markdown.tsx +0 -60
- package/dist/scaffolds/notis-database/packages/sdk/src/components/MultiSelectActionBar.tsx +0 -278
- package/dist/scaffolds/notis-database/packages/sdk/src/components/MultiSelectCheckbox.tsx +0 -91
- package/dist/scaffolds/notis-database/packages/sdk/src/components/MultiSelectDragOverlay.tsx +0 -39
- package/dist/scaffolds/notis-database/packages/sdk/src/config.ts +0 -196
- package/dist/scaffolds/notis-database/packages/sdk/src/documents.ts +0 -229
- package/dist/scaffolds/notis-database/packages/sdk/src/hooks/useBackend.ts +0 -41
- package/dist/scaffolds/notis-database/packages/sdk/src/hooks/useDatabaseSchema.ts +0 -85
- package/dist/scaffolds/notis-database/packages/sdk/src/hooks/useDocument.ts +0 -78
- package/dist/scaffolds/notis-database/packages/sdk/src/hooks/useDocuments.ts +0 -121
- package/dist/scaffolds/notis-database/packages/sdk/src/hooks/useMultiSelect.ts +0 -539
- package/dist/scaffolds/notis-database/packages/sdk/src/hooks/useNotis.ts +0 -34
- package/dist/scaffolds/notis-database/packages/sdk/src/hooks/useNotisNavigation.ts +0 -49
- package/dist/scaffolds/notis-database/packages/sdk/src/hooks/useTool.ts +0 -64
- package/dist/scaffolds/notis-database/packages/sdk/src/hooks/useTools.ts +0 -56
- package/dist/scaffolds/notis-database/packages/sdk/src/hooks/useTopBarSearch.ts +0 -73
- package/dist/scaffolds/notis-database/packages/sdk/src/hooks/useUpsertDocument.ts +0 -95
- package/dist/scaffolds/notis-database/packages/sdk/src/index.ts +0 -83
- package/dist/scaffolds/notis-database/packages/sdk/src/provider.tsx +0 -43
- package/dist/scaffolds/notis-database/packages/sdk/src/runtime.ts +0 -220
- package/dist/scaffolds/notis-database/packages/sdk/src/styles.css +0 -186
- package/dist/scaffolds/notis-database/packages/sdk/src/ui.ts +0 -15
- package/dist/scaffolds/notis-database/packages/sdk/src/vite.ts +0 -56
- package/dist/scaffolds/notis-database/packages/sdk/tsconfig.json +0 -15
- package/dist/scaffolds/notis-database/postcss.config.mjs +0 -8
- package/dist/scaffolds/notis-database/src/dev-main.tsx +0 -23
- package/dist/scaffolds/notis-database/src/mock-runtime.ts +0 -561
- package/dist/scaffolds/notis-database/tailwind.config.ts +0 -59
- package/dist/scaffolds/notis-database/tsconfig.json +0 -23
- package/dist/scaffolds/notis-database/vite.config.ts +0 -22
- package/dist/scaffolds/notis-journal/CHANGELOG.md +0 -29
- package/dist/scaffolds/notis-journal/app/globals.css +0 -37
- package/dist/scaffolds/notis-journal/app/insights/page.tsx +0 -513
- package/dist/scaffolds/notis-journal/app/journal-core.tsx +0 -362
- package/dist/scaffolds/notis-journal/app/journal-ui.tsx +0 -337
- package/dist/scaffolds/notis-journal/app/layout.tsx +0 -6
- package/dist/scaffolds/notis-journal/app/page.tsx +0 -486
- package/dist/scaffolds/notis-journal/components/ui/badge.tsx +0 -28
- package/dist/scaffolds/notis-journal/components/ui/button.tsx +0 -53
- package/dist/scaffolds/notis-journal/components/ui/card.tsx +0 -56
- package/dist/scaffolds/notis-journal/components.json +0 -20
- package/dist/scaffolds/notis-journal/index.html +0 -12
- package/dist/scaffolds/notis-journal/lib/utils.ts +0 -6
- package/dist/scaffolds/notis-journal/metadata/screenshot-1.png +0 -0
- package/dist/scaffolds/notis-journal/metadata/screenshot-2.png +0 -0
- package/dist/scaffolds/notis-journal/metadata/screenshot-3.png +0 -0
- package/dist/scaffolds/notis-journal/metadata/screenshot-4.png +0 -0
- package/dist/scaffolds/notis-journal/metadata/screenshot-5.png +0 -0
- package/dist/scaffolds/notis-journal/metadata/screenshot-6.png +0 -0
- package/dist/scaffolds/notis-journal/metadata/screenshot-fixtures.json +0 -132
- package/dist/scaffolds/notis-journal/notis.config.ts +0 -93
- package/dist/scaffolds/notis-journal/package-lock.json +0 -4615
- package/dist/scaffolds/notis-journal/package.json +0 -34
- package/dist/scaffolds/notis-journal/packages/sdk/package.json +0 -36
- package/dist/scaffolds/notis-journal/packages/sdk/src/components/DocumentEditor.tsx +0 -93
- package/dist/scaffolds/notis-journal/packages/sdk/src/components/Markdown.tsx +0 -60
- package/dist/scaffolds/notis-journal/packages/sdk/src/components/MultiSelectActionBar.tsx +0 -278
- package/dist/scaffolds/notis-journal/packages/sdk/src/components/MultiSelectCheckbox.tsx +0 -91
- package/dist/scaffolds/notis-journal/packages/sdk/src/components/MultiSelectDragOverlay.tsx +0 -39
- package/dist/scaffolds/notis-journal/packages/sdk/src/config.ts +0 -196
- package/dist/scaffolds/notis-journal/packages/sdk/src/documents.ts +0 -229
- package/dist/scaffolds/notis-journal/packages/sdk/src/hooks/useBackend.ts +0 -41
- package/dist/scaffolds/notis-journal/packages/sdk/src/hooks/useDatabaseSchema.ts +0 -85
- package/dist/scaffolds/notis-journal/packages/sdk/src/hooks/useDocument.ts +0 -78
- package/dist/scaffolds/notis-journal/packages/sdk/src/hooks/useDocuments.ts +0 -121
- package/dist/scaffolds/notis-journal/packages/sdk/src/hooks/useMultiSelect.ts +0 -539
- package/dist/scaffolds/notis-journal/packages/sdk/src/hooks/useNotis.ts +0 -34
- package/dist/scaffolds/notis-journal/packages/sdk/src/hooks/useNotisNavigation.ts +0 -49
- package/dist/scaffolds/notis-journal/packages/sdk/src/hooks/useTool.ts +0 -64
- package/dist/scaffolds/notis-journal/packages/sdk/src/hooks/useTools.ts +0 -56
- package/dist/scaffolds/notis-journal/packages/sdk/src/hooks/useTopBarSearch.ts +0 -73
- package/dist/scaffolds/notis-journal/packages/sdk/src/hooks/useUpsertDocument.ts +0 -95
- package/dist/scaffolds/notis-journal/packages/sdk/src/index.ts +0 -83
- package/dist/scaffolds/notis-journal/packages/sdk/src/provider.tsx +0 -43
- package/dist/scaffolds/notis-journal/packages/sdk/src/runtime.ts +0 -220
- package/dist/scaffolds/notis-journal/packages/sdk/src/styles.css +0 -186
- package/dist/scaffolds/notis-journal/packages/sdk/src/ui.ts +0 -15
- package/dist/scaffolds/notis-journal/packages/sdk/src/vite.ts +0 -56
- package/dist/scaffolds/notis-journal/packages/sdk/tsconfig.json +0 -15
- package/dist/scaffolds/notis-journal/postcss.config.mjs +0 -8
- package/dist/scaffolds/notis-journal/skills/journal-onboarding/SKILL.md +0 -120
- package/dist/scaffolds/notis-journal/src/dev-main.tsx +0 -58
- package/dist/scaffolds/notis-journal/src/mock-runtime.ts +0 -197
- package/dist/scaffolds/notis-journal/tailwind.config.ts +0 -58
- package/dist/scaffolds/notis-journal/tsconfig.json +0 -23
- package/dist/scaffolds/notis-journal/vite.config.ts +0 -10
- package/dist/scaffolds/notis-notes/CHANGELOG.md +0 -5
- package/dist/scaffolds/notis-notes/app/globals.css +0 -3
- package/dist/scaffolds/notis-notes/app/layout.tsx +0 -6
- package/dist/scaffolds/notis-notes/app/page.tsx +0 -1943
- package/dist/scaffolds/notis-notes/app/phosphor-icons.ts +0 -596
- package/dist/scaffolds/notis-notes/components/ui/badge.tsx +0 -28
- package/dist/scaffolds/notis-notes/components/ui/button.tsx +0 -53
- package/dist/scaffolds/notis-notes/components/ui/card.tsx +0 -56
- package/dist/scaffolds/notis-notes/components.json +0 -20
- package/dist/scaffolds/notis-notes/lib/utils.ts +0 -6
- package/dist/scaffolds/notis-notes/lib/visible-properties.ts +0 -144
- package/dist/scaffolds/notis-notes/metadata/screenshot-1.png +0 -0
- package/dist/scaffolds/notis-notes/metadata/screenshot-2.png +0 -0
- package/dist/scaffolds/notis-notes/metadata/screenshot-3.png +0 -0
- package/dist/scaffolds/notis-notes/metadata/screenshot-4.png +0 -0
- package/dist/scaffolds/notis-notes/metadata/screenshot-5.png +0 -0
- package/dist/scaffolds/notis-notes/metadata/screenshot-fixtures.json +0 -752
- package/dist/scaffolds/notis-notes/notis.config.ts +0 -80
- package/dist/scaffolds/notis-notes/package-lock.json +0 -4636
- package/dist/scaffolds/notis-notes/package.json +0 -35
- package/dist/scaffolds/notis-notes/packages/sdk/package.json +0 -36
- package/dist/scaffolds/notis-notes/packages/sdk/src/components/DocumentEditor.tsx +0 -93
- package/dist/scaffolds/notis-notes/packages/sdk/src/components/Markdown.tsx +0 -60
- package/dist/scaffolds/notis-notes/packages/sdk/src/components/MultiSelectActionBar.tsx +0 -278
- package/dist/scaffolds/notis-notes/packages/sdk/src/components/MultiSelectCheckbox.tsx +0 -91
- package/dist/scaffolds/notis-notes/packages/sdk/src/components/MultiSelectDragOverlay.tsx +0 -39
- package/dist/scaffolds/notis-notes/packages/sdk/src/config.ts +0 -196
- package/dist/scaffolds/notis-notes/packages/sdk/src/documents.ts +0 -229
- package/dist/scaffolds/notis-notes/packages/sdk/src/hooks/useBackend.ts +0 -41
- package/dist/scaffolds/notis-notes/packages/sdk/src/hooks/useDatabaseSchema.ts +0 -85
- package/dist/scaffolds/notis-notes/packages/sdk/src/hooks/useDocument.ts +0 -78
- package/dist/scaffolds/notis-notes/packages/sdk/src/hooks/useDocuments.ts +0 -121
- package/dist/scaffolds/notis-notes/packages/sdk/src/hooks/useMultiSelect.ts +0 -539
- package/dist/scaffolds/notis-notes/packages/sdk/src/hooks/useNotis.ts +0 -34
- package/dist/scaffolds/notis-notes/packages/sdk/src/hooks/useNotisNavigation.ts +0 -49
- package/dist/scaffolds/notis-notes/packages/sdk/src/hooks/useTool.ts +0 -64
- package/dist/scaffolds/notis-notes/packages/sdk/src/hooks/useTools.ts +0 -56
- package/dist/scaffolds/notis-notes/packages/sdk/src/hooks/useTopBarSearch.ts +0 -73
- package/dist/scaffolds/notis-notes/packages/sdk/src/hooks/useUpsertDocument.ts +0 -95
- package/dist/scaffolds/notis-notes/packages/sdk/src/index.ts +0 -83
- package/dist/scaffolds/notis-notes/packages/sdk/src/provider.tsx +0 -43
- package/dist/scaffolds/notis-notes/packages/sdk/src/runtime.ts +0 -220
- package/dist/scaffolds/notis-notes/packages/sdk/src/styles.css +0 -186
- package/dist/scaffolds/notis-notes/packages/sdk/src/ui.ts +0 -15
- package/dist/scaffolds/notis-notes/packages/sdk/src/vite.ts +0 -56
- package/dist/scaffolds/notis-notes/packages/sdk/tsconfig.json +0 -15
- package/dist/scaffolds/notis-notes/postcss.config.mjs +0 -8
- package/dist/scaffolds/notis-notes/tailwind.config.ts +0 -58
- package/dist/scaffolds/notis-notes/tsconfig.json +0 -23
- package/dist/scaffolds/notis-notes/vite.config.ts +0 -10
- package/dist/scaffolds/notis-random/CHANGELOG.md +0 -15
- package/dist/scaffolds/notis-random/README.md +0 -33
- package/dist/scaffolds/notis-random/app/globals.css +0 -11
- package/dist/scaffolds/notis-random/app/history/page.tsx +0 -67
- package/dist/scaffolds/notis-random/app/layout.tsx +0 -7
- package/dist/scaffolds/notis-random/app/page.tsx +0 -289
- package/dist/scaffolds/notis-random/components/ui/button.tsx +0 -50
- package/dist/scaffolds/notis-random/components/ui/card.tsx +0 -16
- package/dist/scaffolds/notis-random/components/ui/input.tsx +0 -23
- package/dist/scaffolds/notis-random/components.json +0 -20
- package/dist/scaffolds/notis-random/index.html +0 -12
- package/dist/scaffolds/notis-random/lib/notis-tools.ts +0 -128
- package/dist/scaffolds/notis-random/lib/rng.ts +0 -202
- package/dist/scaffolds/notis-random/lib/roll-record.ts +0 -189
- package/dist/scaffolds/notis-random/lib/utils.ts +0 -25
- package/dist/scaffolds/notis-random/metadata/screenshot-1.png +0 -0
- package/dist/scaffolds/notis-random/metadata/screenshot-2.png +0 -0
- package/dist/scaffolds/notis-random/metadata/screenshot-3.png +0 -0
- package/dist/scaffolds/notis-random/metadata/screenshot-4.png +0 -0
- package/dist/scaffolds/notis-random/metadata/screenshot-5.png +0 -0
- package/dist/scaffolds/notis-random/metadata/screenshot-fixtures.json +0 -753
- package/dist/scaffolds/notis-random/notis.config.ts +0 -86
- package/dist/scaffolds/notis-random/package-lock.json +0 -4513
- package/dist/scaffolds/notis-random/package.json +0 -36
- package/dist/scaffolds/notis-random/packages/sdk/package.json +0 -36
- package/dist/scaffolds/notis-random/packages/sdk/src/components/DocumentEditor.tsx +0 -93
- package/dist/scaffolds/notis-random/packages/sdk/src/components/Markdown.tsx +0 -60
- package/dist/scaffolds/notis-random/packages/sdk/src/components/MultiSelectActionBar.tsx +0 -278
- package/dist/scaffolds/notis-random/packages/sdk/src/components/MultiSelectCheckbox.tsx +0 -91
- package/dist/scaffolds/notis-random/packages/sdk/src/components/MultiSelectDragOverlay.tsx +0 -39
- package/dist/scaffolds/notis-random/packages/sdk/src/config.ts +0 -196
- package/dist/scaffolds/notis-random/packages/sdk/src/documents.ts +0 -229
- package/dist/scaffolds/notis-random/packages/sdk/src/hooks/useBackend.ts +0 -41
- package/dist/scaffolds/notis-random/packages/sdk/src/hooks/useDatabaseSchema.ts +0 -85
- package/dist/scaffolds/notis-random/packages/sdk/src/hooks/useDocument.ts +0 -78
- package/dist/scaffolds/notis-random/packages/sdk/src/hooks/useDocuments.ts +0 -121
- package/dist/scaffolds/notis-random/packages/sdk/src/hooks/useMultiSelect.ts +0 -539
- package/dist/scaffolds/notis-random/packages/sdk/src/hooks/useNotis.ts +0 -34
- package/dist/scaffolds/notis-random/packages/sdk/src/hooks/useNotisNavigation.ts +0 -49
- package/dist/scaffolds/notis-random/packages/sdk/src/hooks/useTool.ts +0 -64
- package/dist/scaffolds/notis-random/packages/sdk/src/hooks/useTools.ts +0 -56
- package/dist/scaffolds/notis-random/packages/sdk/src/hooks/useTopBarSearch.ts +0 -73
- package/dist/scaffolds/notis-random/packages/sdk/src/hooks/useUpsertDocument.ts +0 -95
- package/dist/scaffolds/notis-random/packages/sdk/src/index.ts +0 -83
- package/dist/scaffolds/notis-random/packages/sdk/src/provider.tsx +0 -43
- package/dist/scaffolds/notis-random/packages/sdk/src/runtime.ts +0 -220
- package/dist/scaffolds/notis-random/packages/sdk/src/styles.css +0 -186
- package/dist/scaffolds/notis-random/packages/sdk/src/ui.ts +0 -15
- package/dist/scaffolds/notis-random/packages/sdk/src/vite.ts +0 -56
- package/dist/scaffolds/notis-random/packages/sdk/tsconfig.json +0 -15
- package/dist/scaffolds/notis-random/postcss.config.mjs +0 -6
- package/dist/scaffolds/notis-random/src/dev-main.tsx +0 -70
- package/dist/scaffolds/notis-random/src/mock-runtime.ts +0 -129
- package/dist/scaffolds/notis-random/tailwind.config.ts +0 -50
- package/dist/scaffolds/notis-random/tsconfig.json +0 -23
- package/dist/scaffolds/notis-random/vite.config.ts +0 -11
- package/dist/scaffolds.json +0 -49
- package/template/metadata/screenshot-1.png +0 -0
- package/template/metadata/screenshot-2.png +0 -0
- package/template/metadata/screenshot-3.png +0 -0
|
@@ -0,0 +1,215 @@
|
|
|
1
|
+
import { createHash, randomUUID } from 'node:crypto';
|
|
2
|
+
import { mkdir, readFile, rename, rm, stat, writeFile } from 'node:fs/promises';
|
|
3
|
+
import { homedir } from 'node:os';
|
|
4
|
+
import { dirname, join } from 'node:path';
|
|
5
|
+
|
|
6
|
+
import { reconcileBaseSkills } from './base-skills.js';
|
|
7
|
+
|
|
8
|
+
const DEFAULT_LOCK_TIMEOUT_MS = 30_000;
|
|
9
|
+
const DEFAULT_LOCK_STALE_MS = 10 * 60_000;
|
|
10
|
+
const DEFAULT_LOCK_POLL_MS = 50;
|
|
11
|
+
|
|
12
|
+
async function lockSnapshot(lockDirectory) {
|
|
13
|
+
try {
|
|
14
|
+
const [raw, metadata] = await Promise.all([
|
|
15
|
+
readFile(join(lockDirectory, 'owner'), 'utf8').catch(() => ''),
|
|
16
|
+
stat(lockDirectory),
|
|
17
|
+
]);
|
|
18
|
+
let owner = {};
|
|
19
|
+
try {
|
|
20
|
+
owner = raw ? JSON.parse(raw) : {};
|
|
21
|
+
} catch {
|
|
22
|
+
// A process can die between mkdir and the atomic owner write. Preserve
|
|
23
|
+
// the directory mtime as a reclaimable ownerless lease.
|
|
24
|
+
}
|
|
25
|
+
return {
|
|
26
|
+
id: typeof owner.id === 'string' ? owner.id : null,
|
|
27
|
+
pid: Number.isInteger(Number(owner.pid)) ? Number(owner.pid) : null,
|
|
28
|
+
at: Number.isFinite(Number(owner.at)) ? Number(owner.at) : metadata.mtimeMs,
|
|
29
|
+
mtimeMs: metadata.mtimeMs,
|
|
30
|
+
};
|
|
31
|
+
} catch {
|
|
32
|
+
return null;
|
|
33
|
+
}
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
function sameLockSnapshot(left, right) {
|
|
37
|
+
return Boolean(
|
|
38
|
+
left
|
|
39
|
+
&& right
|
|
40
|
+
&& left.id === right.id
|
|
41
|
+
&& left.pid === right.pid
|
|
42
|
+
&& left.at === right.at
|
|
43
|
+
&& left.mtimeMs === right.mtimeMs,
|
|
44
|
+
);
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
function processIsAlive(pid) {
|
|
48
|
+
if (!Number.isInteger(pid) || pid <= 0) return false;
|
|
49
|
+
try {
|
|
50
|
+
process.kill(pid, 0);
|
|
51
|
+
return true;
|
|
52
|
+
} catch (error) {
|
|
53
|
+
return error?.code === 'EPERM';
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
function delay(milliseconds) {
|
|
58
|
+
return new Promise((resolve) => setTimeout(resolve, milliseconds));
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
async function quarantineStaleLock(lockDirectory, snapshot) {
|
|
62
|
+
const quarantineRoot = join(dirname(lockDirectory), '.stale-operation-locks');
|
|
63
|
+
const snapshotToken = createHash('sha256')
|
|
64
|
+
.update(JSON.stringify(snapshot))
|
|
65
|
+
.digest('hex');
|
|
66
|
+
await mkdir(quarantineRoot, { recursive: true, mode: 0o700 });
|
|
67
|
+
try {
|
|
68
|
+
// The deterministic destination is the compare-and-swap guard. If two
|
|
69
|
+
// waiters observed the same stale owner, only one can move it here. The
|
|
70
|
+
// retained non-empty tombstone prevents the loser from ever moving a new
|
|
71
|
+
// live lock that appeared at the shared pathname in the meantime.
|
|
72
|
+
await rename(lockDirectory, join(quarantineRoot, snapshotToken));
|
|
73
|
+
return true;
|
|
74
|
+
} catch (error) {
|
|
75
|
+
if (['EEXIST', 'ENOTEMPTY', 'ENOENT'].includes(error?.code)) return false;
|
|
76
|
+
throw error;
|
|
77
|
+
}
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
async function writeLockOwnerAtomically(lockDirectory, owner) {
|
|
81
|
+
const temporaryOwnerPath = join(lockDirectory, `.owner.${owner.id}.tmp`);
|
|
82
|
+
const ownerPath = join(lockDirectory, 'owner');
|
|
83
|
+
await writeFile(temporaryOwnerPath, JSON.stringify(owner), { mode: 0o600 });
|
|
84
|
+
await rename(temporaryOwnerPath, ownerPath);
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
/** Serialize Desktop and terminal skill sync across processes on one Mac. */
|
|
88
|
+
export async function withSkillSyncLock(callback, {
|
|
89
|
+
home = homedir(),
|
|
90
|
+
timeoutMs = DEFAULT_LOCK_TIMEOUT_MS,
|
|
91
|
+
staleMs = DEFAULT_LOCK_STALE_MS,
|
|
92
|
+
pollMs = DEFAULT_LOCK_POLL_MS,
|
|
93
|
+
now = () => Date.now(),
|
|
94
|
+
} = {}) {
|
|
95
|
+
const lockDirectory = join(home, '.notis', 'skills', '.operation-lock');
|
|
96
|
+
const ownerId = `${process.pid}.${randomUUID()}`;
|
|
97
|
+
const deadline = now() + timeoutMs;
|
|
98
|
+
await mkdir(dirname(lockDirectory), { recursive: true, mode: 0o700 });
|
|
99
|
+
|
|
100
|
+
for (;;) {
|
|
101
|
+
try {
|
|
102
|
+
await mkdir(lockDirectory, { mode: 0o700 });
|
|
103
|
+
try {
|
|
104
|
+
await writeLockOwnerAtomically(lockDirectory, {
|
|
105
|
+
id: ownerId,
|
|
106
|
+
pid: process.pid,
|
|
107
|
+
at: now(),
|
|
108
|
+
});
|
|
109
|
+
} catch (error) {
|
|
110
|
+
// This process exclusively created the directory and has not yet
|
|
111
|
+
// published an owner, so it is safe to undo a failed acquisition.
|
|
112
|
+
await rm(lockDirectory, { recursive: true, force: true });
|
|
113
|
+
throw error;
|
|
114
|
+
}
|
|
115
|
+
break;
|
|
116
|
+
} catch (error) {
|
|
117
|
+
if (error?.code !== 'EEXIST') throw error;
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
const observed = await lockSnapshot(lockDirectory);
|
|
121
|
+
if (
|
|
122
|
+
observed
|
|
123
|
+
&& now() - observed.at > staleMs
|
|
124
|
+
&& !processIsAlive(observed.pid)
|
|
125
|
+
) {
|
|
126
|
+
// Require an unchanged observation interval before stealing. This avoids
|
|
127
|
+
// racing a holder that is publishing or refreshing its owner lease.
|
|
128
|
+
await delay(Math.max(pollMs, 10));
|
|
129
|
+
const current = await lockSnapshot(lockDirectory);
|
|
130
|
+
if (sameLockSnapshot(observed, current) && !processIsAlive(current?.pid)) {
|
|
131
|
+
if (await quarantineStaleLock(lockDirectory, current)) continue;
|
|
132
|
+
}
|
|
133
|
+
}
|
|
134
|
+
if (now() >= deadline) {
|
|
135
|
+
throw new Error('Timed out waiting for another Notis skill sync to finish.');
|
|
136
|
+
}
|
|
137
|
+
await delay(pollMs);
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
const heartbeatMs = Math.max(10, Math.min(30_000, Math.floor(staleMs / 3)));
|
|
141
|
+
let heartbeatStopped = false;
|
|
142
|
+
let heartbeatInFlight = Promise.resolve();
|
|
143
|
+
const refreshHeartbeat = async () => {
|
|
144
|
+
if (heartbeatStopped) return;
|
|
145
|
+
const temporaryOwnerPath = join(lockDirectory, `.owner.${ownerId}.tmp`);
|
|
146
|
+
await writeFile(
|
|
147
|
+
temporaryOwnerPath,
|
|
148
|
+
JSON.stringify({ id: ownerId, pid: process.pid, at: now() }),
|
|
149
|
+
{ mode: 0o600 },
|
|
150
|
+
);
|
|
151
|
+
const owner = await lockSnapshot(lockDirectory);
|
|
152
|
+
if (owner?.id !== ownerId) {
|
|
153
|
+
await rm(temporaryOwnerPath, { force: true });
|
|
154
|
+
return;
|
|
155
|
+
}
|
|
156
|
+
// If a stale-lock reclaimer moved this directory after the ownership
|
|
157
|
+
// check, the source path disappears and rename fails instead of
|
|
158
|
+
// overwriting the owner of a newly acquired lock at the shared path.
|
|
159
|
+
await rename(temporaryOwnerPath, join(lockDirectory, 'owner'));
|
|
160
|
+
};
|
|
161
|
+
const heartbeat = setInterval(() => {
|
|
162
|
+
// Serialize refreshes and retain the active promise so cleanup cannot race
|
|
163
|
+
// a temporary owner write that was already in flight when the interval was
|
|
164
|
+
// cleared.
|
|
165
|
+
heartbeatInFlight = heartbeatInFlight
|
|
166
|
+
.then(refreshHeartbeat)
|
|
167
|
+
.catch(() => undefined);
|
|
168
|
+
}, heartbeatMs);
|
|
169
|
+
heartbeat.unref?.();
|
|
170
|
+
|
|
171
|
+
try {
|
|
172
|
+
return await callback();
|
|
173
|
+
} finally {
|
|
174
|
+
heartbeatStopped = true;
|
|
175
|
+
clearInterval(heartbeat);
|
|
176
|
+
await heartbeatInFlight;
|
|
177
|
+
const owner = await lockSnapshot(lockDirectory);
|
|
178
|
+
if (owner?.id === ownerId) {
|
|
179
|
+
await rm(lockDirectory, { recursive: true, force: true });
|
|
180
|
+
}
|
|
181
|
+
}
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
/**
|
|
185
|
+
* Shared CLI-owned orchestration for base-skill installation plus account sync.
|
|
186
|
+
* Desktop injects the source engine that webpack bundles; the CLI command
|
|
187
|
+
* injects the generated dist engine used by the published package.
|
|
188
|
+
*/
|
|
189
|
+
export async function reconcileAllSkills({
|
|
190
|
+
serverUrl,
|
|
191
|
+
jwt,
|
|
192
|
+
honorSyncEnabled,
|
|
193
|
+
userId = null,
|
|
194
|
+
home,
|
|
195
|
+
runAccountSync,
|
|
196
|
+
reconcileBase = reconcileBaseSkills,
|
|
197
|
+
lockOptions = {},
|
|
198
|
+
}) {
|
|
199
|
+
return withSkillSyncLock(async () => {
|
|
200
|
+
const base = reconcileBase({ userId, ...(home ? { home } : {}) });
|
|
201
|
+
const account = await runAccountSync(
|
|
202
|
+
serverUrl,
|
|
203
|
+
jwt,
|
|
204
|
+
{},
|
|
205
|
+
{ honorSyncEnabled },
|
|
206
|
+
);
|
|
207
|
+
return {
|
|
208
|
+
...account,
|
|
209
|
+
baseSkills: base.skills,
|
|
210
|
+
baseInstalled: base.installed,
|
|
211
|
+
baseLinked: base.linked,
|
|
212
|
+
baseBackups: base.backups,
|
|
213
|
+
};
|
|
214
|
+
}, { ...lockOptions, ...(home ? { home } : {}) });
|
|
215
|
+
}
|
package/src/runtime/transport.js
CHANGED
|
@@ -2,6 +2,7 @@ import { randomUUID } from 'node:crypto';
|
|
|
2
2
|
import { createReadStream } from 'node:fs';
|
|
3
3
|
import { dirname } from 'node:path';
|
|
4
4
|
import { CliError, EXIT_CODES } from './errors.js';
|
|
5
|
+
import { delegatedContextReason } from './delegated-context.js';
|
|
5
6
|
import {
|
|
6
7
|
credentialIsExpired,
|
|
7
8
|
getJwtExpiration,
|
|
@@ -327,9 +328,11 @@ export async function httpRequest({
|
|
|
327
328
|
});
|
|
328
329
|
|
|
329
330
|
let payload = null;
|
|
331
|
+
let payloadReadError = null;
|
|
330
332
|
try {
|
|
331
333
|
payload = await response.json();
|
|
332
|
-
} catch {
|
|
334
|
+
} catch (error) {
|
|
335
|
+
payloadReadError = error;
|
|
333
336
|
payload = null;
|
|
334
337
|
}
|
|
335
338
|
|
|
@@ -350,9 +353,11 @@ export async function httpRequest({
|
|
|
350
353
|
signal: controller.signal,
|
|
351
354
|
...(requestBody.duplex ? { duplex: requestBody.duplex } : {}),
|
|
352
355
|
});
|
|
356
|
+
payloadReadError = null;
|
|
353
357
|
try {
|
|
354
358
|
payload = await response.json();
|
|
355
|
-
} catch {
|
|
359
|
+
} catch (error) {
|
|
360
|
+
payloadReadError = error;
|
|
356
361
|
payload = null;
|
|
357
362
|
}
|
|
358
363
|
}
|
|
@@ -376,6 +381,14 @@ export async function httpRequest({
|
|
|
376
381
|
throw normalizeBackendError(response.status, payload, runtime);
|
|
377
382
|
}
|
|
378
383
|
|
|
384
|
+
// A successful status is not a successful tool call until its response
|
|
385
|
+
// body has been read. Mutations may already have committed before a socket
|
|
386
|
+
// reset truncates the JSON; surface that as an ambiguous network failure
|
|
387
|
+
// so callers never persist an undefined result or attempt a second write.
|
|
388
|
+
if (response.status !== 204 && payloadReadError) {
|
|
389
|
+
throw payloadReadError;
|
|
390
|
+
}
|
|
391
|
+
|
|
379
392
|
return {
|
|
380
393
|
requestId,
|
|
381
394
|
payload: payload || {},
|
|
@@ -428,6 +441,10 @@ export async function callTool({
|
|
|
428
441
|
cwd: process.cwd(),
|
|
429
442
|
agent_mode: runtime.agentMode,
|
|
430
443
|
cli_version: runtime.cliVersion,
|
|
444
|
+
// Reported so the server can refuse work-handing tools from a run Notis
|
|
445
|
+
// is itself driving. Advisory -- a determined agent can unset the markers
|
|
446
|
+
// -- so the server treats it as one layer, not the whole guard.
|
|
447
|
+
...(delegatedContextReason() ? { delegated_context: true } : {}),
|
|
431
448
|
...(runtime.debugEntitlementOverride
|
|
432
449
|
? { debug_entitlement_override: runtime.debugEntitlementOverride }
|
|
433
450
|
: {}),
|
|
@@ -164,8 +164,35 @@
|
|
|
164
164
|
});
|
|
165
165
|
});
|
|
166
166
|
|
|
167
|
+
/**
|
|
168
|
+
* Records one runtime call and hands back the entry so the caller can
|
|
169
|
+
* stamp its outcome once the promise settles. Verify reads `ok` to tell a
|
|
170
|
+
* route that rendered from real data apart from one that only rendered its
|
|
171
|
+
* error states, so every runtime path must settle its own entry.
|
|
172
|
+
*/
|
|
167
173
|
function record(op, args) {
|
|
168
|
-
|
|
174
|
+
const entry = { op, args, ok: null, error: null, durationMs: null };
|
|
175
|
+
entry.startedAt = Date.now();
|
|
176
|
+
window.__harness.runtimeCalls.push(entry);
|
|
177
|
+
return entry;
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
function settle(entry, error) {
|
|
181
|
+
entry.durationMs = Date.now() - entry.startedAt;
|
|
182
|
+
delete entry.startedAt;
|
|
183
|
+
entry.ok = !error;
|
|
184
|
+
entry.error = error ? (error.message ? String(error.message) : String(error)) : null;
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
async function tracked(entry, run) {
|
|
188
|
+
try {
|
|
189
|
+
const value = await run();
|
|
190
|
+
settle(entry, null);
|
|
191
|
+
return value;
|
|
192
|
+
} catch (error) {
|
|
193
|
+
settle(entry, error);
|
|
194
|
+
throw error;
|
|
195
|
+
}
|
|
169
196
|
}
|
|
170
197
|
|
|
171
198
|
async function readJsonOrSnippet(response) {
|
|
@@ -214,29 +241,58 @@
|
|
|
214
241
|
app: descriptor.app,
|
|
215
242
|
route: descriptor.route,
|
|
216
243
|
context: descriptor.context || {},
|
|
217
|
-
navigate: (args) =>
|
|
244
|
+
navigate: (args) => {
|
|
245
|
+
settle(record('navigate', args), null);
|
|
246
|
+
},
|
|
247
|
+
// There is no manager chat here, so the handover is recorded and
|
|
248
|
+
// reported as drafted without any UI.
|
|
249
|
+
handover: async (payload) => {
|
|
250
|
+
settle(record('handover', payload), null);
|
|
251
|
+
return { status: 'drafted' };
|
|
252
|
+
},
|
|
218
253
|
registerTopBarSearch: () => {},
|
|
219
254
|
setTopBarSearchValue: () => {},
|
|
220
255
|
setTopBarSearchLoading: () => {},
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
256
|
+
// No change feed here, so `useDatabaseSubscription` reports live=false
|
|
257
|
+
// and the app keeps whatever manual refresh it offers.
|
|
258
|
+
subscribeDatabase: () => () => {},
|
|
259
|
+
// There is no cloud computer behind the harness. Answering "not
|
|
260
|
+
// available" rather than throwing keeps `useCloudComputer` on the same
|
|
261
|
+
// fallback branch a user without a cloud computer would see.
|
|
262
|
+
cloudComputerFacts: async () => tracked(
|
|
263
|
+
record('cloudComputerFacts', {}),
|
|
264
|
+
async () => ({
|
|
265
|
+
available: false,
|
|
266
|
+
reason: 'unsupported_host',
|
|
267
|
+
sandbox: null,
|
|
268
|
+
cli_auth: {
|
|
269
|
+
gh: { authenticated: null, account: null, checked_at: null, reason: 'unsupported_host' },
|
|
270
|
+
},
|
|
271
|
+
}),
|
|
272
|
+
),
|
|
273
|
+
listTools: async () => tracked(
|
|
274
|
+
record('listTools', {}),
|
|
275
|
+
async () => descriptor.tools || [],
|
|
276
|
+
),
|
|
277
|
+
callTool: async (name, args) => tracked(
|
|
278
|
+
record('callTool', { name, arguments: args || {} }),
|
|
279
|
+
async () => {
|
|
280
|
+
const fixture = lookupToolFixture(name, args || {});
|
|
281
|
+
if (fixture !== undefined) {
|
|
282
|
+
return structuredClone(fixture);
|
|
283
|
+
}
|
|
284
|
+
return { ok: true, result: null };
|
|
285
|
+
},
|
|
286
|
+
),
|
|
287
|
+
request: async (path, options) => tracked(
|
|
288
|
+
record('request', { path, options }),
|
|
289
|
+
async () => {
|
|
290
|
+
if (fixtures && fixtures.requests && Object.prototype.hasOwnProperty.call(fixtures.requests, path)) {
|
|
291
|
+
return structuredClone(fixtures.requests[path]);
|
|
292
|
+
}
|
|
293
|
+
return null;
|
|
294
|
+
},
|
|
295
|
+
),
|
|
240
296
|
};
|
|
241
297
|
}
|
|
242
298
|
|
|
@@ -270,32 +326,45 @@
|
|
|
270
326
|
function liveRuntime() {
|
|
271
327
|
return {
|
|
272
328
|
...stubRuntime(),
|
|
273
|
-
listTools: async () =>
|
|
274
|
-
record('listTools', {})
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
},
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
|
|
329
|
+
listTools: async () => tracked(
|
|
330
|
+
record('listTools', {}),
|
|
331
|
+
async () => {
|
|
332
|
+
const result = await runtimeQuery({ method: 'tools/list' });
|
|
333
|
+
return result.tools || [];
|
|
334
|
+
},
|
|
335
|
+
),
|
|
336
|
+
callTool: async (name, args) => tracked(
|
|
337
|
+
record('callTool', { name, arguments: args || {} }),
|
|
338
|
+
async () => runtimeQuery({ method: 'tools/call', name, arguments: args || {} }),
|
|
339
|
+
),
|
|
340
|
+
// Live mode must not inherit the stub's canned answer: it would settle
|
|
341
|
+
// ok: true with fake data and soften the all-calls-failed assertion.
|
|
342
|
+
cloudComputerFacts: async (options) => tracked(
|
|
343
|
+
record('cloudComputerFacts', options || {}),
|
|
344
|
+
async () => runtimeQuery({
|
|
345
|
+
method: 'cloud_computer/facts',
|
|
346
|
+
...(options && options.refresh ? { refresh: true } : {}),
|
|
347
|
+
}),
|
|
348
|
+
),
|
|
349
|
+
request: async (path, options) => tracked(
|
|
350
|
+
record('request', { path, options }),
|
|
351
|
+
async () => {
|
|
352
|
+
const response = await fetch(`${String(apiBase).replace(/\/$/, '')}${path}`, {
|
|
353
|
+
method: (options && options.method) || 'GET',
|
|
354
|
+
headers: {
|
|
355
|
+
Authorization: `Bearer ${jwt}`,
|
|
356
|
+
...((options && options.body) ? { 'Content-Type': 'application/json' } : {}),
|
|
357
|
+
...((options && options.headers) || {}),
|
|
358
|
+
},
|
|
359
|
+
body: options && options.body ? JSON.stringify(options.body) : undefined,
|
|
360
|
+
});
|
|
361
|
+
const { payload, snippet } = await readJsonOrSnippet(response);
|
|
362
|
+
if (!response.ok) {
|
|
363
|
+
throw new Error((payload && (payload.error || payload.message)) || snippet || `Request failed with status ${response.status}`);
|
|
364
|
+
}
|
|
365
|
+
return payload;
|
|
366
|
+
},
|
|
367
|
+
),
|
|
299
368
|
};
|
|
300
369
|
}
|
|
301
370
|
|
package/template/app/layout.tsx
CHANGED
|
@@ -1,6 +1,10 @@
|
|
|
1
1
|
import '@notis/sdk/styles.css';
|
|
2
|
+
import { ShortcutProvider } from '@notis/sdk/interactions';
|
|
2
3
|
import './globals.css';
|
|
3
4
|
|
|
4
5
|
export default function AppShell({ children }: { children: React.ReactNode }) {
|
|
5
|
-
|
|
6
|
+
// NotisProvider supplies this in the product host. Keeping the shell-level
|
|
7
|
+
// provider makes the same scope arbitration available in the local harness;
|
|
8
|
+
// ShortcutProvider is nesting-safe, so the hosted app still has one registry.
|
|
9
|
+
return <ShortcutProvider>{children}</ShortcutProvider>;
|
|
6
10
|
}
|
package/template/notis.config.ts
CHANGED
|
@@ -2,6 +2,7 @@ import { defineNotisApp } from '@notis/sdk/config';
|
|
|
2
2
|
|
|
3
3
|
export default defineNotisApp({
|
|
4
4
|
name: 'my-notis-app',
|
|
5
|
+
devSlug: 'my-notis-app',
|
|
5
6
|
title: 'My Notis App',
|
|
6
7
|
description: 'A focused Notis app with one route and one database-backed workflow.',
|
|
7
8
|
icon: 'phosphor:squares-four',
|
|
@@ -1,10 +1,11 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@notis/sdk",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.2.0",
|
|
4
4
|
"type": "module",
|
|
5
5
|
"main": "./src/index.ts",
|
|
6
6
|
"exports": {
|
|
7
7
|
".": "./src/index.ts",
|
|
8
|
+
"./interactions": "./src/interactions.ts",
|
|
8
9
|
"./config": "./src/config.ts",
|
|
9
10
|
"./vite": "./src/vite.ts",
|
|
10
11
|
"./ui": "./src/ui.ts",
|
|
@@ -51,7 +51,7 @@ function DocumentEditorFallback({ documentId, variant = 'full', className }: Not
|
|
|
51
51
|
);
|
|
52
52
|
}
|
|
53
53
|
|
|
54
|
-
const isMarkdown = document.contentType
|
|
54
|
+
const isMarkdown = document.contentType === 'markdown' || !document.contentType;
|
|
55
55
|
return (
|
|
56
56
|
<div className={className}>
|
|
57
57
|
{variant === 'full' ? <h1 style={fallbackTitleStyle}>{document.title}</h1> : null}
|
|
@@ -62,6 +62,15 @@ function DocumentEditorFallback({ documentId, variant = 'full', className }: Not
|
|
|
62
62
|
<p style={fallbackNoticeStyle}>Read-only preview — editing is available inside the Notis portal.</p>
|
|
63
63
|
)}
|
|
64
64
|
</>
|
|
65
|
+
) : document.contentType === 'view' ? (
|
|
66
|
+
<div style={fallbackFrameStyle}>
|
|
67
|
+
{document.viewType === 'report'
|
|
68
|
+
? 'Interactive report'
|
|
69
|
+
: document.viewType === 'html'
|
|
70
|
+
? 'Interactive HTML document'
|
|
71
|
+
: 'View document'} — open it in Notis Documents for
|
|
72
|
+
the full native experience.
|
|
73
|
+
</div>
|
|
65
74
|
) : (
|
|
66
75
|
<div style={fallbackFrameStyle}>
|
|
67
76
|
{document.fileType ? `${document.fileType.toUpperCase()} document` : 'File document'} — open it in
|
|
@@ -76,8 +85,9 @@ function DocumentEditorFallback({ documentId, variant = 'full', className }: Not
|
|
|
76
85
|
* Embeds the document editor for a document anywhere in an app view. The host
|
|
77
86
|
* (portal) provides the implementation through `runtime.ui.DocumentEditor` and
|
|
78
87
|
* dispatches on the document's content type (markdown -> rich text editor,
|
|
79
|
-
* files -> matching viewer
|
|
80
|
-
* preview
|
|
88
|
+
* files -> matching viewer, views -> their trusted native Documents surface).
|
|
89
|
+
* Outside the portal this falls back to a read-only preview or an explicit
|
|
90
|
+
* open-in-Documents notice for native views.
|
|
81
91
|
*
|
|
82
92
|
* ```tsx
|
|
83
93
|
* <DocumentEditor documentId={entry.id} variant="body" className="min-h-[320px]" />
|
|
@@ -0,0 +1,121 @@
|
|
|
1
|
+
import type { ChangeEvent, KeyboardEvent, ReactElement } from 'react';
|
|
2
|
+
import { useCallback, useEffect, useRef, useState } from 'react';
|
|
3
|
+
import { useNotisRuntime } from '../provider';
|
|
4
|
+
import type { NotisMarkdownEditorProps } from '../runtime';
|
|
5
|
+
|
|
6
|
+
function MarkdownEditorFallback({
|
|
7
|
+
value,
|
|
8
|
+
revision,
|
|
9
|
+
readOnly,
|
|
10
|
+
autosaveMs = 1500,
|
|
11
|
+
placeholder = 'Write in Markdown…',
|
|
12
|
+
className,
|
|
13
|
+
onChange,
|
|
14
|
+
onSave,
|
|
15
|
+
onDirtyChange,
|
|
16
|
+
onSavingChange,
|
|
17
|
+
}: NotisMarkdownEditorProps) {
|
|
18
|
+
const [draft, setDraft] = useState(value);
|
|
19
|
+
const [saveAttempt, setSaveAttempt] = useState(0);
|
|
20
|
+
const draftRef = useRef(value);
|
|
21
|
+
const dirtyRef = useRef(false);
|
|
22
|
+
const savingRef = useRef(false);
|
|
23
|
+
const revisionRef = useRef(revision);
|
|
24
|
+
const onSaveRef = useRef(onSave);
|
|
25
|
+
const readOnlyRef = useRef(readOnly);
|
|
26
|
+
const unmountedRef = useRef(false);
|
|
27
|
+
const saveRef = useRef<() => Promise<void>>(async () => undefined);
|
|
28
|
+
|
|
29
|
+
useEffect(() => {
|
|
30
|
+
onSaveRef.current = onSave;
|
|
31
|
+
readOnlyRef.current = readOnly;
|
|
32
|
+
}, [onSave, readOnly]);
|
|
33
|
+
|
|
34
|
+
useEffect(() => {
|
|
35
|
+
if (dirtyRef.current) return;
|
|
36
|
+
draftRef.current = value;
|
|
37
|
+
revisionRef.current = revision;
|
|
38
|
+
setDraft(value);
|
|
39
|
+
}, [revision, value]);
|
|
40
|
+
|
|
41
|
+
const save = useCallback(async () => {
|
|
42
|
+
if (!onSave || !dirtyRef.current || readOnly || savingRef.current) return;
|
|
43
|
+
const markdown = draftRef.current;
|
|
44
|
+
savingRef.current = true;
|
|
45
|
+
if (!unmountedRef.current) onSavingChange?.(true);
|
|
46
|
+
try {
|
|
47
|
+
const result = await onSave({ markdown, expectedRevision: revisionRef.current });
|
|
48
|
+
if (result?.revision) revisionRef.current = result.revision;
|
|
49
|
+
if (draftRef.current === markdown) {
|
|
50
|
+
dirtyRef.current = false;
|
|
51
|
+
if (!unmountedRef.current) onDirtyChange?.(false);
|
|
52
|
+
}
|
|
53
|
+
} finally {
|
|
54
|
+
savingRef.current = false;
|
|
55
|
+
if (!unmountedRef.current) onSavingChange?.(false);
|
|
56
|
+
if (dirtyRef.current && draftRef.current !== markdown) {
|
|
57
|
+
if (unmountedRef.current) {
|
|
58
|
+
void saveRef.current().catch((error) => console.error('Could not save Markdown on unmount', error));
|
|
59
|
+
} else {
|
|
60
|
+
setSaveAttempt((attempt) => attempt + 1);
|
|
61
|
+
}
|
|
62
|
+
}
|
|
63
|
+
}
|
|
64
|
+
}, [onDirtyChange, onSave, onSavingChange, readOnly]);
|
|
65
|
+
saveRef.current = save;
|
|
66
|
+
|
|
67
|
+
useEffect(() => {
|
|
68
|
+
unmountedRef.current = false;
|
|
69
|
+
return () => {
|
|
70
|
+
unmountedRef.current = true;
|
|
71
|
+
if (!savingRef.current) {
|
|
72
|
+
void saveRef.current().catch((error) => console.error('Could not save Markdown on unmount', error));
|
|
73
|
+
}
|
|
74
|
+
};
|
|
75
|
+
}, []);
|
|
76
|
+
|
|
77
|
+
useEffect(() => {
|
|
78
|
+
if (!dirtyRef.current || !onSave || readOnly) return;
|
|
79
|
+
const timer = window.setTimeout(() => void save(), Math.max(250, autosaveMs));
|
|
80
|
+
return () => window.clearTimeout(timer);
|
|
81
|
+
}, [autosaveMs, draft, onSave, readOnly, save, saveAttempt]);
|
|
82
|
+
|
|
83
|
+
const handleChange = (event: ChangeEvent<HTMLTextAreaElement>) => {
|
|
84
|
+
const next = event.currentTarget.value;
|
|
85
|
+
draftRef.current = next;
|
|
86
|
+
setDraft(next);
|
|
87
|
+
if (!dirtyRef.current) {
|
|
88
|
+
dirtyRef.current = true;
|
|
89
|
+
onDirtyChange?.(true);
|
|
90
|
+
}
|
|
91
|
+
onChange?.(next);
|
|
92
|
+
};
|
|
93
|
+
|
|
94
|
+
const handleKeyDown = (event: KeyboardEvent<HTMLTextAreaElement>) => {
|
|
95
|
+
if ((event.metaKey || event.ctrlKey) && event.key.toLowerCase() === 's') {
|
|
96
|
+
event.preventDefault();
|
|
97
|
+
void save();
|
|
98
|
+
}
|
|
99
|
+
};
|
|
100
|
+
|
|
101
|
+
return (
|
|
102
|
+
<textarea
|
|
103
|
+
value={draft}
|
|
104
|
+
readOnly={readOnly}
|
|
105
|
+
placeholder={placeholder}
|
|
106
|
+
className={className}
|
|
107
|
+
onChange={handleChange}
|
|
108
|
+
onKeyDown={handleKeyDown}
|
|
109
|
+
style={{ minHeight: 360, width: '100%', resize: 'vertical' }}
|
|
110
|
+
/>
|
|
111
|
+
);
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
/** Use Notis' editor while leaving persistence entirely under app control. */
|
|
115
|
+
export function MarkdownEditor(props: NotisMarkdownEditorProps): ReactElement {
|
|
116
|
+
const runtime = useNotisRuntime();
|
|
117
|
+
const HostEditor = runtime?.ui?.MarkdownEditor;
|
|
118
|
+
return HostEditor
|
|
119
|
+
? <HostEditor key={props.resourceKey} {...props} />
|
|
120
|
+
: <MarkdownEditorFallback key={props.resourceKey} {...props} />;
|
|
121
|
+
}
|