@notis_ai/cli 0.2.0-beta.16.1 → 0.2.0-beta.160.1

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.
Files changed (154) hide show
  1. package/README.md +433 -133
  2. package/config/notis_app_boundary_rules.json +50 -0
  3. package/config/notis_app_design_rules.json +135 -0
  4. package/dist/agent-hooks/notis-agent-hook.mjs +18672 -0
  5. package/dist/base-skills/notis-apps/SKILL.md +70 -0
  6. package/dist/base-skills/notis-apps/references/architecture.md +164 -0
  7. package/dist/base-skills/notis-apps/references/context.md +81 -0
  8. package/dist/base-skills/notis-apps/references/design.md +165 -0
  9. package/dist/base-skills/notis-apps/references/reading.md +89 -0
  10. package/dist/base-skills/notis-apps/references/release.md +99 -0
  11. package/dist/base-skills/notis-apps/references/sdk.md +62 -0
  12. package/dist/base-skills/notis-apps/references/troubleshooting.md +23 -0
  13. package/dist/base-skills/notis-cli/SKILL.md +140 -0
  14. package/dist/base-skills/notis-cli/references/app-delivery.md +18 -0
  15. package/dist/base-skills/notis-cli/references/native-databases.md +20 -0
  16. package/dist/base-skills/notis-cli/references/tool-examples.md +56 -0
  17. package/dist/base-skills/notis-cli/references/troubleshooting.md +39 -0
  18. package/dist/base-skills/notis-query/SKILL.md +67 -0
  19. package/dist/base-skills/notis-query/references/database-discovery.md +59 -0
  20. package/dist/base-skills/notis-query/references/documents.md +50 -0
  21. package/dist/base-skills/notis-query/references/query.md +543 -0
  22. package/dist/skill-sync/index.js +1626 -0
  23. package/dist/skill-sync/index.js.map +7 -0
  24. package/dist/skill-sync-worker.mjs +2990 -0
  25. package/package.json +16 -6
  26. package/skills/notis-apps/cli.md +313 -0
  27. package/skills/notis-cli/AGENT_INSTRUCTIONS.md +39 -0
  28. package/skills/notis-onboarding/BRIEF.md +129 -0
  29. package/skills/notis-query/cli.md +39 -0
  30. package/src/agent-hook-entry.js +5 -0
  31. package/src/cli.js +294 -25
  32. package/src/command-specs/agents.js +392 -0
  33. package/src/command-specs/apps.js +1470 -202
  34. package/src/command-specs/auth.js +114 -137
  35. package/src/command-specs/diagnostics.js +716 -0
  36. package/src/command-specs/handover.js +374 -0
  37. package/src/command-specs/helpers.js +84 -82
  38. package/src/command-specs/index.js +25 -6
  39. package/src/command-specs/meta.js +150 -18
  40. package/src/command-specs/onboarding.js +290 -0
  41. package/src/command-specs/profile.js +358 -0
  42. package/src/command-specs/reports.js +86 -0
  43. package/src/command-specs/skills.js +75 -0
  44. package/src/command-specs/smoke.js +386 -0
  45. package/src/command-specs/tools.js +455 -139
  46. package/src/runtime/agent-browser.js +632 -0
  47. package/src/runtime/agent-memory-state.js +126 -0
  48. package/src/runtime/agent-setup.js +383 -0
  49. package/src/runtime/app-boundary-validator.js +404 -0
  50. package/src/runtime/app-changelog.js +79 -0
  51. package/src/runtime/app-platform.js +2633 -210
  52. package/src/runtime/app-registry-scaffolds.js +367 -0
  53. package/src/runtime/app-test-server.js +292 -0
  54. package/src/runtime/assets/store-screenshot-dark.png +0 -0
  55. package/src/runtime/auth-recovery.js +110 -0
  56. package/src/runtime/base-skills.d.ts +20 -0
  57. package/src/runtime/base-skills.js +167 -0
  58. package/src/runtime/channel.js +133 -0
  59. package/src/runtime/delegated-context.js +68 -0
  60. package/src/runtime/errors.js +1 -0
  61. package/src/runtime/git.js +233 -0
  62. package/src/runtime/login-listener.js +15 -0
  63. package/src/runtime/oauth.js +2622 -0
  64. package/src/runtime/output.js +37 -5
  65. package/src/runtime/ports.js +31 -0
  66. package/src/runtime/profiles.js +906 -55
  67. package/src/runtime/skill-sync/cloud-client.ts +99 -0
  68. package/src/runtime/skill-sync/index.ts +697 -0
  69. package/src/runtime/skill-sync/local-scanner.ts +1046 -0
  70. package/src/runtime/skill-sync/symlink-manager.ts +433 -0
  71. package/src/runtime/skill-sync/sync-plan.ts +22 -0
  72. package/src/runtime/skill-sync/types.ts +110 -0
  73. package/src/runtime/skill-sync/write-cloud-skill.ts +50 -0
  74. package/src/runtime/skill-sync-service.js +109 -0
  75. package/src/runtime/store-screenshot.js +143 -0
  76. package/src/runtime/sync-skills.d.ts +37 -0
  77. package/src/runtime/sync-skills.js +231 -0
  78. package/src/runtime/telemetry.js +92 -0
  79. package/src/runtime/transport.js +324 -45
  80. package/src/skill-sync-worker-entry.js +2 -0
  81. package/src/skill-sync-worker.js +50 -0
  82. package/template/.harness/index.html.tmpl +430 -0
  83. package/template/CHANGELOG.md +5 -0
  84. package/template/app/layout.tsx +5 -2
  85. package/template/app/page.tsx +49 -42
  86. package/template/components/page-heading.tsx +23 -0
  87. package/template/components/ui/badge.tsx +7 -4
  88. package/template/components/ui/card.tsx +24 -11
  89. package/template/components/ui/native-select.tsx +24 -0
  90. package/template/notis.config.ts +24 -6
  91. package/template/package-lock.json +4137 -0
  92. package/template/package.json +5 -5
  93. package/template/packages/{notis-sdk → sdk}/package.json +13 -3
  94. package/template/packages/sdk/src/agentContext.ts +36 -0
  95. package/template/packages/sdk/src/components/DocumentEditor.tsx +103 -0
  96. package/template/packages/sdk/src/components/Markdown.tsx +60 -0
  97. package/template/packages/sdk/src/components/MarkdownEditor.tsx +121 -0
  98. package/template/packages/sdk/src/components/MultiSelectActionBar.tsx +285 -0
  99. package/template/packages/sdk/src/components/MultiSelectCheckbox.tsx +97 -0
  100. package/template/packages/sdk/src/components/MultiSelectDragOverlay.tsx +39 -0
  101. package/template/packages/sdk/src/components/NotisCommentBoundary.tsx +172 -0
  102. package/template/packages/sdk/src/components/NotisSelectionBoundary.tsx +59 -0
  103. package/template/packages/sdk/src/components/ShortcutHints.tsx +56 -0
  104. package/template/packages/sdk/src/components/Skeleton.tsx +24 -0
  105. package/template/packages/sdk/src/config.ts +257 -0
  106. package/template/packages/sdk/src/documents.ts +256 -0
  107. package/template/packages/sdk/src/hooks/useActiveResource.ts +19 -0
  108. package/template/packages/sdk/src/hooks/useAgentContext.ts +23 -0
  109. package/template/packages/sdk/src/hooks/useCloudComputer.ts +64 -0
  110. package/template/packages/sdk/src/hooks/useCollectionInteractions.ts +836 -0
  111. package/template/packages/sdk/src/hooks/useDatabaseSchema.ts +49 -0
  112. package/template/packages/sdk/src/hooks/useDatabaseSubscription.ts +76 -0
  113. package/template/packages/sdk/src/hooks/useDocument.ts +43 -0
  114. package/template/packages/sdk/src/hooks/useDocuments.ts +84 -0
  115. package/template/packages/sdk/src/hooks/useHandover.ts +78 -0
  116. package/template/packages/sdk/src/hooks/useLongPressSelection.ts +79 -0
  117. package/template/packages/sdk/src/hooks/useMultiSelect.ts +95 -0
  118. package/template/packages/{notis-sdk → sdk}/src/hooks/useNotis.ts +10 -4
  119. package/template/packages/{notis-sdk → sdk}/src/hooks/useNotisNavigation.ts +11 -8
  120. package/template/packages/sdk/src/hooks/useQuery.ts +71 -0
  121. package/template/packages/sdk/src/hooks/useTool.ts +65 -0
  122. package/template/packages/sdk/src/hooks/useToolQuery.ts +12 -0
  123. package/template/packages/sdk/src/hooks/useTopBarSearch.ts +81 -0
  124. package/template/packages/sdk/src/hooks/useUpsertDocument.ts +95 -0
  125. package/template/packages/sdk/src/index.ts +161 -0
  126. package/template/packages/sdk/src/interactions/actions.ts +59 -0
  127. package/template/packages/sdk/src/interactions/shortcuts.tsx +694 -0
  128. package/template/packages/sdk/src/interactions/visibility.ts +13 -0
  129. package/template/packages/sdk/src/interactions.ts +45 -0
  130. package/template/packages/sdk/src/provider.tsx +44 -0
  131. package/template/packages/sdk/src/queryCache.ts +170 -0
  132. package/template/packages/sdk/src/runtime.ts +451 -0
  133. package/template/packages/sdk/src/styles.css +213 -0
  134. package/template/packages/sdk/src/tailwind.ts +56 -0
  135. package/template/packages/{notis-sdk → sdk}/src/vite.ts +5 -1
  136. package/template/tailwind.config.ts +1 -0
  137. package/src/command-specs/db.js +0 -163
  138. package/src/runtime/app-preview-server.js +0 -312
  139. package/template/packages/notis-sdk/src/config.ts +0 -48
  140. package/template/packages/notis-sdk/src/helpers.ts +0 -131
  141. package/template/packages/notis-sdk/src/hooks/useAppState.ts +0 -50
  142. package/template/packages/notis-sdk/src/hooks/useCollectionItem.ts +0 -58
  143. package/template/packages/notis-sdk/src/hooks/useDatabase.ts +0 -87
  144. package/template/packages/notis-sdk/src/hooks/useDocument.ts +0 -61
  145. package/template/packages/notis-sdk/src/hooks/useTool.ts +0 -49
  146. package/template/packages/notis-sdk/src/hooks/useUpsertDocument.ts +0 -57
  147. package/template/packages/notis-sdk/src/index.ts +0 -47
  148. package/template/packages/notis-sdk/src/provider.tsx +0 -44
  149. package/template/packages/notis-sdk/src/runtime.ts +0 -159
  150. package/template/packages/notis-sdk/src/styles.css +0 -123
  151. /package/template/packages/{notis-sdk → sdk}/src/hooks/useBackend.ts +0 -0
  152. /package/template/packages/{notis-sdk → sdk}/src/hooks/useTools.ts +0 -0
  153. /package/template/packages/{notis-sdk → sdk}/src/ui.ts +0 -0
  154. /package/template/packages/{notis-sdk → sdk}/tsconfig.json +0 -0
@@ -0,0 +1,56 @@
1
+ /** Build-only SDK content discovery. Author configuration and PostCSS plugins stay intact. */
2
+ import { existsSync, mkdtempSync, rmSync, writeFileSync } from 'node:fs';
3
+ import { tmpdir } from 'node:os';
4
+ import { dirname, join, relative, resolve } from 'node:path';
5
+ import { fileURLToPath } from 'node:url';
6
+
7
+ export function notisTailwindContent() {
8
+ let root = process.cwd();
9
+ let temporary: string | undefined;
10
+ const wrappers = new Map<string, string>();
11
+ const cleanup = () => {
12
+ if (temporary) rmSync(temporary, { recursive: true, force: true });
13
+ temporary = undefined;
14
+ wrappers.clear();
15
+ };
16
+ return {
17
+ name: 'notis-sdk-tailwind-content',
18
+ enforce: 'pre' as const,
19
+ configResolved(config: { root: string }) { root = config.root; },
20
+ transform(source: string, id: string) {
21
+ if (!id.split('?')[0].endsWith('.css') || !/@tailwind\s/.test(source)) return null;
22
+ const explicit = source.match(/@config\s+(['"])(.*?)\1\s*;/);
23
+ const config = explicit
24
+ ? resolve(dirname(id.split('?')[0]), explicit[2])
25
+ : ['ts', 'js', 'cjs', 'mjs'].map(ext => join(root, `tailwind.config.${ext}`)).find(existsSync);
26
+ if (!config) return null;
27
+ let wrapper = wrappers.get(config);
28
+ if (!wrapper) {
29
+ temporary ||= mkdtempSync(join(tmpdir(), 'notis-tailwind-'));
30
+ wrapper = join(temporary, `config-${wrappers.size}.ts`);
31
+ const sdkGlob = join(dirname(fileURLToPath(import.meta.url)), '**/*.{ts,tsx}').replaceAll('\\', '/');
32
+ writeFileSync(wrapper, `import config from ${JSON.stringify(config)};
33
+ import { resolve } from 'node:path';
34
+ const content = config.content || [];
35
+ const files = Array.isArray(content) ? content : (content.files || []);
36
+ const base = ${JSON.stringify(root)};
37
+ const relativeBase = ${JSON.stringify(dirname(config))};
38
+ export default { ...config, content: {
39
+ ...(Array.isArray(content) ? {} : content),
40
+ relative: false,
41
+ files: [...files.map(file => typeof file !== 'string' ? file :
42
+ (file.startsWith('!') ? '!' : '') + resolve(content.relative ? relativeBase : base, file.replace(/^!/, ''))),
43
+ ${JSON.stringify(sdkGlob)}],
44
+ } };
45
+ `);
46
+ wrappers.set(config, wrapper);
47
+ }
48
+ const directive = `@config ${JSON.stringify('./' + relative(dirname(id.split('?')[0]), wrapper).replaceAll('\\', '/'))};`;
49
+ // Imports must remain before @config. Vite resolves them before PostCSS.
50
+ const code = explicit ? source.replace(explicit[0], directive) : `${source}\n${directive}\n`;
51
+ return { code, map: null };
52
+ },
53
+ closeBundle: cleanup,
54
+ closeWatcher: cleanup,
55
+ };
56
+ }
@@ -14,10 +14,12 @@
14
14
  */
15
15
 
16
16
  import type { NotisAppConfig } from './config';
17
+ import { notisTailwindContent } from './tailwind';
17
18
 
18
19
  export function notisViteConfig(appConfig: NotisAppConfig) {
19
20
  return {
20
21
  plugins: [
22
+ notisTailwindContent(),
21
23
  // @vitejs/plugin-react is added by the consumer's vite.config.ts
22
24
  // or auto-detected. We provide the config shape only.
23
25
  ],
@@ -28,14 +30,16 @@ export function notisViteConfig(appConfig: NotisAppConfig) {
28
30
  fileName: () => 'app.js',
29
31
  },
30
32
  rollupOptions: {
31
- external: ['react', 'react-dom', 'react/jsx-runtime'],
33
+ external: ['react', 'react-dom', 'react-dom/client', 'react/jsx-runtime', 'react/jsx-dev-runtime'],
32
34
  output: {
33
35
  globals: {
34
36
  react: 'window.React',
35
37
  'react-dom': 'window.ReactDOM',
38
+ 'react-dom/client': 'window.ReactDOMClient',
36
39
  'react/jsx-runtime': 'window.React',
37
40
  },
38
41
  assetFileNames: 'app[extname]',
42
+ inlineDynamicImports: true,
39
43
  },
40
44
  },
41
45
  outDir: '.notis/output/bundle',
@@ -7,6 +7,7 @@ const config: Config = {
7
7
  './app/**/*.{ts,tsx}',
8
8
  './components/**/*.{ts,tsx}',
9
9
  './lib/**/*.{ts,tsx}',
10
+ './packages/sdk/src/**/*.{ts,tsx}',
10
11
  ],
11
12
  theme: {
12
13
  extend: {
@@ -1,163 +0,0 @@
1
- import { formatTable } from '../runtime/output.js';
2
- import { nextIdempotencyKey, parseMaybeJson, runToolCommand } from './helpers.js';
3
-
4
- async function dbListHandler(ctx) {
5
- const result = await runToolCommand({
6
- runtime: ctx.runtime,
7
- toolName: 'notis_list_databases',
8
- });
9
- const databases = result.payload.databases || [];
10
- const firstSlug = databases[0]?.slug;
11
- return ctx.output.emitSuccess({
12
- command: ctx.spec.command_path.join(' '),
13
- data: { databases },
14
- humanSummary: databases.length ? `Found ${databases.length} databases` : 'No databases found.',
15
- hints: firstSlug
16
- ? [{ command: `notis db query ${firstSlug}`, reason: 'Query this database' }]
17
- : [{ command: 'notis db upsert --operation create --title "My DB"', reason: 'Create a new database' }],
18
- renderHuman: () =>
19
- databases.length
20
- ? formatTable(databases, [
21
- { label: 'ID', value: (database) => database.id || '' },
22
- { label: 'Name', value: (database) => database.name || 'Untitled' },
23
- { label: 'Slug', value: (database) => database.slug || '-' },
24
- ])
25
- : 'No databases found.',
26
- });
27
- }
28
-
29
- async function dbUpsertHandler(ctx) {
30
- const idempotencyKey = nextIdempotencyKey(ctx.globalOptions);
31
- const payload = {
32
- operation: ctx.options.operation,
33
- };
34
- if (ctx.options.databaseId) {
35
- payload.database_id = ctx.options.databaseId;
36
- }
37
- if (ctx.options.title) {
38
- payload.title = ctx.options.title;
39
- }
40
- if (ctx.options.description) {
41
- payload.description = ctx.options.description;
42
- }
43
- if (ctx.options.icon) {
44
- const v = ctx.options.icon;
45
- payload.icon = v.startsWith('lucide:') ? v : `lucide:${v}`;
46
- }
47
- if (ctx.options.properties) {
48
- payload.properties = parseMaybeJson(ctx.options.properties, 'properties');
49
- }
50
-
51
- const result = await runToolCommand({
52
- runtime: ctx.runtime,
53
- toolName: 'notis_upsert_database',
54
- arguments_: payload,
55
- mutating: true,
56
- idempotencyKey,
57
- });
58
- return ctx.output.emitSuccess({
59
- command: ctx.spec.command_path.join(' '),
60
- data: result.payload,
61
- humanSummary: `Database ${ctx.options.operation} completed`,
62
- meta: { mutating: true, idempotency_key: idempotencyKey || null },
63
- });
64
- }
65
-
66
- async function dbQueryHandler(ctx) {
67
- const query = {};
68
- if (ctx.options.filter) {
69
- query.filter = parseMaybeJson(ctx.options.filter, 'filter');
70
- }
71
- if (ctx.options.sort) {
72
- const parsedSorts = parseMaybeJson(ctx.options.sort, 'sort');
73
- query.sorts = Array.isArray(parsedSorts) ? parsedSorts : [parsedSorts];
74
- }
75
- if (ctx.options.pageSize) {
76
- query.page_size = Number.parseInt(ctx.options.pageSize, 10);
77
- }
78
-
79
- const payload = {
80
- database_slug: ctx.args.databaseSlug,
81
- query,
82
- };
83
- if (ctx.options.offset) {
84
- payload.offset = Number.parseInt(ctx.options.offset, 10);
85
- }
86
- if (ctx.options.cursor) {
87
- payload.start_cursor = ctx.options.cursor;
88
- }
89
-
90
- const result = await runToolCommand({
91
- runtime: ctx.runtime,
92
- toolName: 'notis_query',
93
- arguments_: payload,
94
- });
95
-
96
- return ctx.output.emitSuccess({
97
- command: ctx.spec.command_path.join(' '),
98
- data: result.payload,
99
- humanSummary: `Queried database ${ctx.args.databaseSlug}`,
100
- renderHuman: () => JSON.stringify(result.payload, null, 2),
101
- });
102
- }
103
-
104
- export const dbCommandSpecs = [
105
- {
106
- command_path: ['db', 'list'],
107
- summary: 'List native Notis databases.',
108
- when_to_use: 'Use this to find database ids and slugs before querying or updating schemas.',
109
- args_schema: { arguments: [], options: [] },
110
- examples: ['notis db list', 'notis db list --json'],
111
- output_schema: 'Returns an array of native database records.',
112
- mutates: false,
113
- idempotent: true,
114
- related_commands: ['notis db query <database-slug>', 'notis db upsert'],
115
- backend_call: { type: 'tool', name: 'notis_list_databases' },
116
- handler: dbListHandler,
117
- },
118
- {
119
- command_path: ['db', 'upsert'],
120
- summary: 'Create or update a native database schema.',
121
- when_to_use: 'Use this when you need to provision a new database or adjust an existing schema.',
122
- args_schema: {
123
- arguments: [],
124
- options: [
125
- { flags: '--operation <create|update>', description: 'Create a new database or update an existing one.' },
126
- { flags: '--database-id <id>', description: 'Database id for update operations.' },
127
- { flags: '--title <text>', description: 'Database title.' },
128
- { flags: '--description <text>', description: 'Database description.' },
129
- { flags: '--icon <lucide-icon-name>', description: 'Database icon (Lucide icon name, e.g. database).' },
130
- { flags: '--properties <json>', description: 'JSON array of property definitions.' },
131
- ],
132
- },
133
- examples: ['notis db upsert --operation create --title "Tasks"', 'notis db upsert --operation update --database-id db_123 --title "Tasks V2"'],
134
- output_schema: 'Returns the backend database upsert payload.',
135
- mutates: true,
136
- idempotent: true,
137
- related_commands: ['notis db list', 'notis db query <database-slug>'],
138
- backend_call: { type: 'tool', name: 'notis_upsert_database' },
139
- handler: dbUpsertHandler,
140
- },
141
- {
142
- command_path: ['db', 'query'],
143
- summary: 'Run a structured query against a native Notis database.',
144
- when_to_use: 'Use this when the database slug is known and you need direct filters, sorts, or pagination.',
145
- args_schema: {
146
- arguments: [{ token: '<database-slug>', description: 'Slug of the native database to query.' }],
147
- options: [
148
- { flags: '--filter <json>', description: 'Structured query filter JSON.' },
149
- { flags: '--sort <json>', description: 'Sort JSON object or array.' },
150
- { flags: '--page-size <n>', description: 'Page size between 1 and 100.' },
151
- { flags: '--offset <n>', description: 'Zero-based offset.' },
152
- { flags: '--cursor <value>', description: 'Pagination cursor alias for next_offset.' },
153
- ],
154
- },
155
- examples: ['notis db query tasks --page-size 50', 'notis db query tasks --filter \'{"property":"Status"}\''],
156
- output_schema: 'Returns the native database query payload from `notis_query`.',
157
- mutates: false,
158
- idempotent: true,
159
- related_commands: ['notis db list', 'notis db upsert'],
160
- backend_call: { type: 'tool', name: 'notis_query' },
161
- handler: dbQueryHandler,
162
- },
163
- ];
@@ -1,312 +0,0 @@
1
- /**
2
- * Local preview server for built Notis app bundles.
3
- *
4
- * Serves a single index.html that loads the app bundle via <script type="module">.
5
- * Injects mock window.__NOTIS_RUNTIME__ before the bundle loads. Route switching
6
- * uses pathname navigation between route pages.
7
- *
8
- * Usage: `notis apps preview [dir]` starts this server on localhost:8787.
9
- */
10
-
11
- import { createServer } from 'node:http';
12
- import { existsSync, readFileSync } from 'node:fs';
13
- import { extname, resolve, sep } from 'node:path';
14
-
15
- import { readManifest } from './app-platform.js';
16
-
17
- // ---------------------------------------------------------------------------
18
- // Static file serving
19
- // ---------------------------------------------------------------------------
20
-
21
- function contentTypeFor(path) {
22
- const types = {
23
- '.html': 'text/html; charset=utf-8',
24
- '.css': 'text/css; charset=utf-8',
25
- '.js': 'text/javascript; charset=utf-8',
26
- '.json': 'application/json; charset=utf-8',
27
- '.svg': 'image/svg+xml',
28
- '.png': 'image/png',
29
- '.jpg': 'image/jpeg',
30
- '.jpeg': 'image/jpeg',
31
- '.ico': 'image/x-icon',
32
- '.woff': 'font/woff',
33
- '.woff2': 'font/woff2',
34
- };
35
- return types[extname(path)] || 'application/octet-stream';
36
- }
37
-
38
- // ---------------------------------------------------------------------------
39
- // Mock runtime generation
40
- // ---------------------------------------------------------------------------
41
-
42
- function buildSeedDocuments(databases) {
43
- const state = {};
44
- for (const slug of databases) {
45
- state[slug] = Array.from({ length: 3 }, (_, i) => {
46
- return {
47
- id: `${slug}-seed-${i + 1}`,
48
- databaseSlug: slug,
49
- title: `${slug} ${i + 1}`,
50
- properties: {},
51
- icon: null,
52
- };
53
- });
54
- }
55
- return state;
56
- }
57
-
58
- function escapeHtml(value) {
59
- return String(value)
60
- .replaceAll('&', '&amp;')
61
- .replaceAll('<', '&lt;')
62
- .replaceAll('>', '&gt;')
63
- .replaceAll('"', '&quot;')
64
- .replaceAll("'", '&#39;');
65
- }
66
-
67
- function sanitizeBundlePath(value, fallback) {
68
- if (typeof value !== 'string') return fallback;
69
- const normalized = value.trim().replaceAll('\\', '/').replace(/^\/+/, '');
70
- if (!normalized || normalized.includes('..')) return fallback;
71
- return /^[A-Za-z0-9._/-]+$/.test(normalized) ? normalized : fallback;
72
- }
73
-
74
- function serializeForInlineScript(value) {
75
- return JSON.stringify(value).replaceAll('</', '<\\/').replaceAll('<!--', '<\\!--');
76
- }
77
-
78
- function buildPreviewHtml({ manifest, route, databases }) {
79
- const app = manifest.app;
80
- const seedState = buildSeedDocuments(databases);
81
- const bundleJs = sanitizeBundlePath(manifest.bundle?.js, 'bundle/app.js');
82
- const bundleCss = sanitizeBundlePath(manifest.bundle?.css, 'bundle/app.css');
83
- const exportName = typeof route.export_name === 'string' && route.export_name ? route.export_name : 'index';
84
- const pageTitle = escapeHtml(`${app.name} - ${route.name}`);
85
- const bundleCssHref = escapeHtml(`/${bundleCss}`);
86
- const bundleJsPathLiteral = JSON.stringify(`/${bundleJs}`);
87
- const exportNameLiteral = JSON.stringify(exportName);
88
-
89
- return `<!DOCTYPE html>
90
- <html lang="en">
91
- <head>
92
- <meta charset="UTF-8" />
93
- <meta name="viewport" content="width=device-width, initial-scale=1.0" />
94
- <title>${pageTitle}</title>
95
- <link rel="stylesheet" href="${bundleCssHref}" />
96
- <script>
97
- (function() {
98
- var DB = ${serializeForInlineScript(databases.map(function(slug) {
99
- return { slug: slug, title: slug, properties: [] };
100
- }))};
101
- var STATE = ${serializeForInlineScript(seedState)};
102
- var COLLECTION = ${serializeForInlineScript(route.collection || null)};
103
-
104
- function titleProp(slug) {
105
- var db = DB.find(function(d) { return d.slug === slug; });
106
- return db && db.properties ? (db.properties.find(function(p) { return p.type === 'title'; }) || {}).name || 'title' : 'title';
107
- }
108
-
109
- function dbSlug(v) { return v || (COLLECTION && COLLECTION.database) || null; }
110
-
111
- window.__NOTIS_RUNTIME__ = {
112
- app: ${serializeForInlineScript(app)},
113
- route: ${serializeForInlineScript(route)},
114
- databases: DB,
115
-
116
- navigate: function(payload) {
117
- console.log('[notis-preview] Navigate:', payload);
118
- if (payload.kind === 'route' && payload.path) {
119
- window.location.assign(payload.path);
120
- }
121
- },
122
-
123
- listTools: function() {
124
- return Promise.resolve(${serializeForInlineScript((manifest.tools || []).map((name) => ({ name })))});
125
- },
126
-
127
- callTool: function(name, args) {
128
- console.log('[notis-preview] Tool call:', name, args);
129
- return Promise.reject(new Error('Tool calls are not available in local preview. Deploy the app to use real tools.'));
130
- },
131
-
132
- queryDatabase: function(args) {
133
- var slug = dbSlug(args && args.databaseSlug);
134
- var docs = (STATE[slug] || []).slice(args && args.offset || 0);
135
- return Promise.resolve({ documents: docs });
136
- },
137
-
138
- getDocument: function(args) {
139
- var id = args && args.documentId;
140
- for (var slug in STATE) {
141
- var match = STATE[slug].find(function(d) { return d.id === id; });
142
- if (match) return Promise.resolve(match);
143
- }
144
- return Promise.reject(new Error('Document not found in preview.'));
145
- },
146
-
147
- upsertDocument: function(args) {
148
- var slug = dbSlug(args && args.databaseSlug);
149
- if (!slug) return Promise.reject(new Error('No database for upsert.'));
150
- var docs = STATE[slug] || [];
151
- var tp = titleProp(slug);
152
- var existing = docs.find(function(d) { return d.id === (args && args.documentId); });
153
- var props = Object.assign({}, existing && existing.properties, args && args.properties);
154
- if (args && args.title) props[tp] = args.title;
155
- var doc = {
156
- id: existing ? existing.id : slug + '-' + Date.now(),
157
- databaseSlug: slug,
158
- title: props[tp] || 'Untitled',
159
- properties: props,
160
- icon: null
161
- };
162
- if (existing) {
163
- var idx = docs.indexOf(existing);
164
- docs[idx] = doc;
165
- } else {
166
- docs.unshift(doc);
167
- }
168
- STATE[slug] = docs;
169
- return Promise.resolve({ status: 'success', document: doc });
170
- },
171
-
172
- listCollectionItems: function(args) {
173
- var slug = dbSlug(args && args.databaseSlug);
174
- if (!slug) return Promise.resolve({ items: [] });
175
- var tp = (args && args.titleProperty) || titleProp(slug);
176
- var docs = (STATE[slug] || []).slice(0, (args && args.pageSize) || 100);
177
- return Promise.resolve({
178
- items: docs.map(function(d) {
179
- return { id: d.id, title: (d.properties && d.properties[tp]) || d.title || 'Untitled', icon: d.icon };
180
- })
181
- });
182
- },
183
-
184
- request: function() {
185
- return Promise.reject(new Error('Backend requests are not available in local preview.'));
186
- }
187
- };
188
- })();
189
- </script>
190
- </head>
191
- <body class="min-h-screen bg-background text-foreground antialiased">
192
- <div id="notis-app-root"></div>
193
- <script type="importmap">
194
- {
195
- "imports": {
196
- "react": "https://esm.sh/react@19",
197
- "react-dom": "https://esm.sh/react-dom@19?external=react",
198
- "react-dom/client": "https://esm.sh/react-dom@19/client?external=react",
199
- "react/jsx-runtime": "https://esm.sh/react@19/jsx-runtime"
200
- }
201
- }
202
- </script>
203
- <script type="module">
204
- import React from 'react';
205
- import { createRoot } from 'react-dom/client';
206
- import * as AppBundle from ${bundleJsPathLiteral};
207
-
208
- // Try to render: AppShell wrapping the route component, or just the route component
209
- const exportName = ${exportNameLiteral};
210
- const RouteComponent = AppBundle[exportName] || AppBundle['default'];
211
- const AppShell = AppBundle['__AppShell'];
212
-
213
- if (RouteComponent) {
214
- const root = document.getElementById('notis-app-root');
215
- if (root) {
216
- const reactRoot = createRoot(root);
217
- if (AppShell) {
218
- reactRoot.render(React.createElement(AppShell, null, React.createElement(RouteComponent)));
219
- } else {
220
- reactRoot.render(React.createElement(RouteComponent));
221
- }
222
- }
223
- } else {
224
- document.getElementById('notis-app-root').innerHTML =
225
- '<p style="padding:2rem;color:red;">No component found for export "' + exportName + '". Available exports: ' +
226
- Object.keys(AppBundle).join(', ') + '</p>';
227
- }
228
- </script>
229
- </body>
230
- </html>`;
231
- }
232
-
233
- // ---------------------------------------------------------------------------
234
- // Server
235
- // ---------------------------------------------------------------------------
236
-
237
- function normalizePathname(pathname) {
238
- if (!pathname || pathname === '/') return '/';
239
- return pathname.endsWith('/') ? pathname.slice(0, -1) || '/' : pathname;
240
- }
241
-
242
- function resolveSafePath(baseDir, pathname) {
243
- const normalizedBaseDir = resolve(baseDir);
244
- const resolvedPath = resolve(normalizedBaseDir, pathname.replace(/^\/+/, ''));
245
- if (resolvedPath === normalizedBaseDir || resolvedPath.startsWith(`${normalizedBaseDir}${sep}`)) {
246
- return resolvedPath;
247
- }
248
- return null;
249
- }
250
-
251
- export async function startPreviewServer({ projectDir, port }) {
252
- const manifest = readManifest(projectDir);
253
- const outputDir = resolve(projectDir, '.notis/output');
254
- const bundleDir = resolve(projectDir, '.notis/output/bundle');
255
- const databases = (manifest.databases || []).filter((slug) => typeof slug === 'string' && slug);
256
- const routesByPath = new Map((manifest.routes || []).map((r) => [normalizePathname(r.path), r]));
257
- const defaultRoute = (manifest.routes || []).find((r) => r.default) || manifest.routes?.[0];
258
-
259
- if (!defaultRoute) {
260
- throw new Error('Manifest contains no routes.');
261
- }
262
-
263
- const server = createServer((request, response) => {
264
- const url = new URL(request.url || '/', `http://${request.headers.host || 'localhost'}`);
265
- const pathname = normalizePathname(url.pathname);
266
-
267
- // Serve bundle files directly
268
- if (pathname.startsWith('/bundle/')) {
269
- const filePath = resolveSafePath(outputDir, pathname);
270
- if (filePath && existsSync(filePath)) {
271
- response.writeHead(200, { 'Content-Type': contentTypeFor(filePath), 'Cache-Control': 'no-store' });
272
- response.end(readFileSync(filePath));
273
- return;
274
- }
275
- }
276
-
277
- // Redirect / to default route if it's not /
278
- if (pathname === '/' && defaultRoute.path !== '/') {
279
- response.writeHead(302, { Location: defaultRoute.path });
280
- response.end();
281
- return;
282
- }
283
-
284
- // Serve route HTML with runtime injection
285
- const route = routesByPath.get(pathname) || (pathname === '/' ? defaultRoute : null);
286
- if (route) {
287
- const html = buildPreviewHtml({ manifest, route, databases });
288
- response.writeHead(200, { 'Content-Type': 'text/html; charset=utf-8', 'Cache-Control': 'no-store' });
289
- response.end(html);
290
- return;
291
- }
292
-
293
- // Serve static files from bundle dir
294
- const filePath = resolveSafePath(bundleDir, pathname);
295
- if (filePath && existsSync(filePath)) {
296
- response.writeHead(200, { 'Content-Type': contentTypeFor(filePath), 'Cache-Control': 'no-store' });
297
- response.end(readFileSync(filePath));
298
- return;
299
- }
300
-
301
- response.writeHead(404, { 'Content-Type': 'text/plain' });
302
- response.end('Not found');
303
- });
304
-
305
- await new Promise((resolveP, rejectP) => {
306
- server.on('error', rejectP);
307
- server.listen(port, '127.0.0.1', resolveP);
308
- });
309
-
310
- // Keep running until interrupted
311
- await new Promise(() => {});
312
- }
@@ -1,48 +0,0 @@
1
- /**
2
- * Configuration utilities for notis.config.ts.
3
- *
4
- * Usage:
5
- * ```ts
6
- * // notis.config.ts
7
- * import { defineNotisApp } from '@notis/sdk/config';
8
- *
9
- * export default defineNotisApp({
10
- * name: 'My App',
11
- * description: 'Does things',
12
- * icon: 'lucide:layout-dashboard',
13
- * databases: ['tasks'],
14
- * routes: [...],
15
- * tools: [...],
16
- * });
17
- * ```
18
- */
19
-
20
- export interface NotisRouteConfig {
21
- path: string;
22
- name: string;
23
- icon?: string;
24
- default?: boolean;
25
- exportName?: string;
26
- collection?: {
27
- database: string;
28
- titleProperty: string;
29
- };
30
- }
31
-
32
- export interface NotisAppConfig {
33
- name: string;
34
- description?: string;
35
- icon?: string;
36
- databases?: string[];
37
- routes?: NotisRouteConfig[];
38
- tools?: string[];
39
- }
40
-
41
- /**
42
- * Identity function that provides type checking and autocomplete for the
43
- * Notis app configuration. The returned object is read at build time by
44
- * `notis apps build` to generate the manifest.
45
- */
46
- export function defineNotisApp(config: NotisAppConfig): NotisAppConfig {
47
- return config;
48
- }