@notis_ai/cli 0.2.1 → 0.2.2

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 (66) hide show
  1. package/README.md +98 -163
  2. package/dist/scaffolds/notes/app/globals.css +3 -0
  3. package/dist/scaffolds/notes/app/layout.tsx +6 -0
  4. package/dist/scaffolds/notes/app/page.tsx +1465 -0
  5. package/dist/scaffolds/notes/components/ui/badge.tsx +28 -0
  6. package/dist/scaffolds/notes/components/ui/button.tsx +53 -0
  7. package/dist/scaffolds/notes/components/ui/card.tsx +56 -0
  8. package/dist/scaffolds/notes/components.json +20 -0
  9. package/dist/scaffolds/notes/lib/utils.ts +6 -0
  10. package/dist/scaffolds/notes/notis.config.ts +31 -0
  11. package/dist/scaffolds/notes/package.json +31 -0
  12. package/dist/scaffolds/notes/postcss.config.mjs +8 -0
  13. package/dist/scaffolds/notes/tailwind.config.ts +58 -0
  14. package/dist/scaffolds/notes/tsconfig.json +22 -0
  15. package/dist/scaffolds/notes/vite.config.ts +10 -0
  16. package/dist/scaffolds.json +13 -0
  17. package/package.json +7 -3
  18. package/skills/notis-apps/SKILL.md +162 -0
  19. package/skills/notis-apps/cli.md +208 -0
  20. package/skills/notis-cli/SKILL.md +258 -0
  21. package/skills/notis-query/cli.md +40 -0
  22. package/src/cli.js +1 -1
  23. package/src/command-specs/apps.js +211 -46
  24. package/src/command-specs/helpers.js +0 -60
  25. package/src/command-specs/index.js +0 -6
  26. package/src/command-specs/meta.js +7 -6
  27. package/src/command-specs/tools.js +1 -33
  28. package/src/runtime/app-dev-server.js +32 -4
  29. package/src/runtime/app-platform.js +404 -24
  30. package/src/runtime/profiles.js +2 -2
  31. package/src/runtime/transport.js +2 -2
  32. package/template/.harness/index.html.tmpl +1 -50
  33. package/template/app/page.tsx +41 -6
  34. package/template/metadata/cover.png +0 -0
  35. package/template/metadata/screenshot-1.png +0 -0
  36. package/template/metadata/screenshot-2.png +0 -0
  37. package/template/metadata/screenshot-3.png +0 -0
  38. package/template/notis.config.ts +10 -6
  39. package/template/package.json +2 -2
  40. package/template/packages/{notis-sdk → sdk}/package.json +6 -0
  41. package/template/packages/{notis-sdk → sdk}/src/components/MultiSelectActionBar.tsx +2 -1
  42. package/template/packages/{notis-sdk → sdk}/src/components/MultiSelectCheckbox.tsx +2 -2
  43. package/template/packages/{notis-sdk → sdk}/src/components/MultiSelectDragOverlay.tsx +2 -2
  44. package/template/packages/{notis-sdk → sdk}/src/config.ts +1 -1
  45. package/template/packages/{notis-sdk → sdk}/src/hooks/useDatabase.ts +1 -13
  46. package/template/packages/{notis-sdk → sdk}/src/hooks/useMultiSelect.ts +4 -3
  47. package/template/packages/{notis-sdk → sdk}/src/hooks/useNotis.ts +4 -3
  48. package/template/packages/{notis-sdk → sdk}/src/hooks/useNotisNavigation.ts +3 -3
  49. package/template/packages/{notis-sdk → sdk}/src/hooks/useTool.ts +22 -7
  50. package/template/packages/{notis-sdk → sdk}/src/hooks/useUpsertDocument.ts +0 -8
  51. package/template/packages/{notis-sdk → sdk}/src/index.ts +2 -15
  52. package/template/packages/{notis-sdk → sdk}/src/provider.tsx +1 -1
  53. package/template/packages/{notis-sdk → sdk}/src/runtime.ts +5 -26
  54. package/src/command-specs/auth.js +0 -178
  55. package/src/command-specs/db.js +0 -163
  56. package/template/packages/notis-sdk/src/helpers.ts +0 -131
  57. package/template/packages/notis-sdk/src/hooks/useAppState.ts +0 -50
  58. package/template/packages/notis-sdk/src/hooks/useCollectionItem.ts +0 -58
  59. package/template/packages/notis-sdk/src/hooks/useDocument.ts +0 -61
  60. /package/template/packages/{notis-sdk → sdk}/src/hooks/useBackend.ts +0 -0
  61. /package/template/packages/{notis-sdk → sdk}/src/hooks/useTools.ts +0 -0
  62. /package/template/packages/{notis-sdk → sdk}/src/hooks/useTopBarSearch.ts +0 -0
  63. /package/template/packages/{notis-sdk → sdk}/src/styles.css +0 -0
  64. /package/template/packages/{notis-sdk → sdk}/src/ui.ts +0 -0
  65. /package/template/packages/{notis-sdk → sdk}/src/vite.ts +0 -0
  66. /package/template/packages/{notis-sdk → sdk}/tsconfig.json +0 -0
@@ -7,7 +7,7 @@
7
7
  */
8
8
 
9
9
  import { spawn } from 'node:child_process';
10
- import { cpSync, existsSync, mkdirSync, readFileSync, writeFileSync, readdirSync, rmSync } from 'node:fs';
10
+ import { cpSync, existsSync, mkdirSync, readFileSync, writeFileSync, readdirSync, rmSync, statSync } from 'node:fs';
11
11
  import { createRequire } from 'node:module';
12
12
  import { dirname, join, resolve } from 'node:path';
13
13
  import { fileURLToPath } from 'node:url';
@@ -21,15 +21,34 @@ const STATE_FILE = join(NOTIS_DIR, 'state.json');
21
21
  const OUTPUT_DIR = join(NOTIS_DIR, 'output');
22
22
  const BUNDLE_DIR = join(OUTPUT_DIR, 'bundle');
23
23
  const MANIFEST_FILE = join(OUTPUT_DIR, 'manifest.json');
24
+ const METADATA_DIR = 'metadata';
25
+ const CLI_ROOT = resolve(dirname(fileURLToPath(import.meta.url)), '../..');
26
+ const MONOREPO_APPS_DIR = resolve(CLI_ROOT, '../..', 'apps');
27
+ const DIST_DIR = join(CLI_ROOT, 'dist');
28
+ const SCAFFOLD_CATALOG_FILE = join(DIST_DIR, 'scaffolds.json');
29
+ const SCAFFOLD_SOURCE_DIR = join(DIST_DIR, 'scaffolds');
30
+ const TEMPLATE_SDK_DIR = join(CLI_ROOT, 'template', 'packages', 'sdk');
31
+ export const NOTIS_APP_CATEGORIES = [
32
+ 'Productivity',
33
+ 'Sales & Marketing',
34
+ 'Operations',
35
+ 'Product & Engineering',
36
+ 'Personal',
37
+ ];
24
38
  const SOURCE_COPY_EXCLUDES = new Set([
25
39
  'node_modules',
26
40
  '.notis',
27
41
  '.git',
28
42
  'dist',
29
- 'package-lock.json',
30
43
  'tsconfig.tsbuildinfo',
31
44
  '.DS_Store',
32
45
  ]);
46
+ const SCAFFOLD_COPY_EXCLUDES = new Set([
47
+ ...SOURCE_COPY_EXCLUDES,
48
+ 'coverage',
49
+ '.next',
50
+ '.turbo',
51
+ ]);
33
52
  let appConfigImportNonce = 0;
34
53
 
35
54
  // ---------------------------------------------------------------------------
@@ -214,9 +233,160 @@ export function detectProjectWarnings(projectDir, appConfig = null) {
214
233
  warnings.push('No database references declared in notis.config.ts.');
215
234
  }
216
235
 
236
+ if (appConfig) {
237
+ try {
238
+ const listing = inspectListingReadiness(projectDir, appConfig);
239
+ warnings.push(...listing.warnings);
240
+ } catch (error) {
241
+ warnings.push(error instanceof Error ? error.message : String(error));
242
+ }
243
+ }
244
+
217
245
  return warnings;
218
246
  }
219
247
 
248
+ function safeKebab(value) {
249
+ return String(value || '')
250
+ .trim()
251
+ .toLowerCase()
252
+ .replace(/[^a-z0-9]+/g, '-')
253
+ .replace(/(^-|-$)+/g, '');
254
+ }
255
+
256
+ function jsStringLiteral(value) {
257
+ return `'${String(value).replace(/\\/g, '\\\\').replace(/'/g, "\\'")}'`;
258
+ }
259
+
260
+ function normalizeCategories(categories = []) {
261
+ if (!Array.isArray(categories)) {
262
+ return [];
263
+ }
264
+ const allowed = new Set(NOTIS_APP_CATEGORIES);
265
+ const normalized = [];
266
+ for (const category of categories) {
267
+ if (typeof category !== 'string' || !category.trim()) {
268
+ continue;
269
+ }
270
+ const trimmed = category.trim();
271
+ if (!allowed.has(trimmed)) {
272
+ throw usageError(
273
+ `Invalid Notis app category "${trimmed}". Use one of: ${NOTIS_APP_CATEGORIES.join(', ')}.`,
274
+ );
275
+ }
276
+ if (!normalized.includes(trimmed)) {
277
+ normalized.push(trimmed);
278
+ }
279
+ }
280
+ return normalized;
281
+ }
282
+
283
+ function pngDimensions(buffer) {
284
+ const signature = '89504e470d0a1a0a';
285
+ if (!Buffer.isBuffer(buffer) || buffer.length < 24 || buffer.subarray(0, 8).toString('hex') !== signature) {
286
+ return null;
287
+ }
288
+ return {
289
+ width: buffer.readUInt32BE(16),
290
+ height: buffer.readUInt32BE(20),
291
+ };
292
+ }
293
+
294
+ function imageAssetMeta(projectDir, relPath, { maxBytes, recommendedRatio = 1.6 }) {
295
+ const fullPath = join(projectDir, relPath);
296
+ const content = readFileSync(fullPath);
297
+ const dimensions = pngDimensions(content);
298
+ const ratio = dimensions?.height ? dimensions.width / dimensions.height : null;
299
+ const errors = [];
300
+ const warnings = [];
301
+
302
+ if (!relPath.toLowerCase().endsWith('.png')) {
303
+ errors.push(`${relPath} must be a PNG file.`);
304
+ }
305
+ if (content.length > maxBytes) {
306
+ errors.push(`${relPath} must be ${Math.round(maxBytes / 1024 / 1024)} MB or smaller.`);
307
+ }
308
+ if (ratio !== null && (ratio < 1.3 || ratio > 1.9)) {
309
+ errors.push(`${relPath} should be close to a 16:10 aspect ratio.`);
310
+ } else if (ratio !== null && Math.abs(ratio - recommendedRatio) > 0.08) {
311
+ warnings.push(`${relPath} is not 2000x1250; 16:10 PNG is recommended.`);
312
+ }
313
+
314
+ return {
315
+ path: relPath,
316
+ content_type: 'image/png',
317
+ bytes: content.length,
318
+ width: dimensions?.width ?? null,
319
+ height: dimensions?.height ?? null,
320
+ errors,
321
+ warnings,
322
+ };
323
+ }
324
+
325
+ export function discoverMetadataAssets(projectDir) {
326
+ const metadataDir = join(projectDir, METADATA_DIR);
327
+ const coverPath = join(METADATA_DIR, 'cover.png');
328
+ const cover = existsSync(join(projectDir, coverPath))
329
+ ? imageAssetMeta(projectDir, coverPath, { maxBytes: 5 * 1024 * 1024 })
330
+ : null;
331
+ const screenshots = [];
332
+
333
+ if (existsSync(metadataDir)) {
334
+ for (const entry of readdirSync(metadataDir, { withFileTypes: true })) {
335
+ if (!entry.isFile()) continue;
336
+ const match = /^screenshot-(\d+)\.png$/i.exec(entry.name);
337
+ if (!match) continue;
338
+ screenshots.push({
339
+ index: Number.parseInt(match[1], 10),
340
+ ...imageAssetMeta(projectDir, join(METADATA_DIR, entry.name), {
341
+ maxBytes: 2 * 1024 * 1024,
342
+ }),
343
+ });
344
+ }
345
+ }
346
+
347
+ screenshots.sort((a, b) => a.index - b.index);
348
+ return {
349
+ cover,
350
+ screenshots: screenshots.slice(0, 6),
351
+ };
352
+ }
353
+
354
+ export function inspectListingReadiness(projectDir, appConfig = null) {
355
+ const config = appConfig || {};
356
+ const warnings = [];
357
+ const errors = [];
358
+ const metadata = discoverMetadataAssets(projectDir);
359
+ const categories = normalizeCategories(config.categories || []);
360
+
361
+ if (!String(config.tagline || '').trim()) {
362
+ warnings.push('Listing tagline missing in notis.config.ts.');
363
+ }
364
+ if (categories.length === 0) {
365
+ warnings.push(`Listing category missing in notis.config.ts. Use one of: ${NOTIS_APP_CATEGORIES.join(', ')}.`);
366
+ }
367
+ if (!metadata.cover) {
368
+ warnings.push('Listing cover missing at metadata/cover.png.');
369
+ } else {
370
+ errors.push(...metadata.cover.errors);
371
+ warnings.push(...metadata.cover.warnings);
372
+ }
373
+ if (metadata.screenshots.length < 3) {
374
+ warnings.push(`Listing screenshots: ${metadata.screenshots.length}/3 minimum in metadata/screenshot-N.png.`);
375
+ }
376
+ for (const screenshot of metadata.screenshots) {
377
+ errors.push(...screenshot.errors);
378
+ warnings.push(...screenshot.warnings);
379
+ }
380
+
381
+ return {
382
+ ready: errors.length === 0 && Boolean(config.tagline) && categories.length > 0 && Boolean(metadata.cover) && metadata.screenshots.length >= 1,
383
+ warnings,
384
+ errors,
385
+ metadata,
386
+ categories,
387
+ };
388
+ }
389
+
220
390
  // ---------------------------------------------------------------------------
221
391
  // Build
222
392
  // ---------------------------------------------------------------------------
@@ -409,6 +579,21 @@ function generateEntryFile(projectDir, routes) {
409
579
  return entryPath;
410
580
  }
411
581
 
582
+ export async function prepareArtifactBuild(projectDir) {
583
+ validateProjectBoundary(projectDir);
584
+ const appConfig = await loadAppConfig(projectDir);
585
+ const detectedRoutes = resolveConfiguredRoutes(appConfig, projectDir);
586
+
587
+ generateEntryFile(projectDir, detectedRoutes);
588
+
589
+ const manifest = generateManifest(appConfig, projectDir);
590
+ const manifestPath = join(projectDir, MANIFEST_FILE);
591
+ mkdirSync(dirname(manifestPath), { recursive: true });
592
+ writeFileSync(manifestPath, JSON.stringify(manifest, null, 2) + '\n');
593
+
594
+ return { appConfig, manifest, routes: detectedRoutes };
595
+ }
596
+
412
597
  /**
413
598
  * Generate the manifest from app config and build output.
414
599
  */
@@ -431,15 +616,52 @@ export function generateManifest(appConfig, projectDir) {
431
616
  });
432
617
 
433
618
  const databases = appConfig.databases || [];
619
+ const categories = normalizeCategories(appConfig.categories || []);
620
+ const metadata = discoverMetadataAssets(projectDir);
621
+ const appSlug = safeKebab(appConfig.name);
622
+ const displayTitle = appConfig.title || appConfig.displayName || appConfig.name;
623
+ const listingMedia = {
624
+ cover: metadata.cover
625
+ ? {
626
+ path: metadata.cover.path,
627
+ content_type: metadata.cover.content_type,
628
+ width: metadata.cover.width,
629
+ height: metadata.cover.height,
630
+ bytes: metadata.cover.bytes,
631
+ }
632
+ : null,
633
+ screenshots: metadata.screenshots.map((screenshot) => ({
634
+ path: screenshot.path,
635
+ content_type: screenshot.content_type,
636
+ width: screenshot.width,
637
+ height: screenshot.height,
638
+ bytes: screenshot.bytes,
639
+ })),
640
+ };
434
641
 
435
642
  return {
436
643
  version: 1,
437
- spec_version: 3,
644
+ spec_version: 4,
438
645
  app: {
439
- name: appConfig.name,
646
+ name: displayTitle,
647
+ slug: appSlug || null,
440
648
  description: appConfig.description || null,
441
649
  icon: appConfig.icon || null,
650
+ title: displayTitle,
651
+ tagline: appConfig.tagline || null,
652
+ categories,
653
+ author: appConfig.author || null,
654
+ version_notes: appConfig.versionNotes || appConfig.version_notes || null,
442
655
  },
656
+ listing: {
657
+ title: displayTitle,
658
+ tagline: appConfig.tagline || null,
659
+ categories,
660
+ author: appConfig.author || null,
661
+ version_notes: appConfig.versionNotes || appConfig.version_notes || null,
662
+ media: listingMedia,
663
+ },
664
+ metadata: listingMedia,
443
665
  routes,
444
666
  bundle: {
445
667
  js: 'bundle/app.js',
@@ -454,14 +676,7 @@ export function generateManifest(appConfig, projectDir) {
454
676
  * Build the app bundle: generate entry file, run `vite build`, package into .notis/output/.
455
677
  */
456
678
  export async function buildArtifact(projectDir) {
457
- validateProjectBoundary(projectDir);
458
- const appConfig = await loadAppConfig(projectDir);
459
-
460
- // Auto-detect or use configured routes
461
- const detectedRoutes = resolveConfiguredRoutes(appConfig, projectDir);
462
-
463
- // Generate the entry file that re-exports all route page components
464
- generateEntryFile(projectDir, detectedRoutes);
679
+ await prepareArtifactBuild(projectDir);
465
680
 
466
681
  // Run Vite build
467
682
  await runProjectScript({
@@ -477,14 +692,11 @@ export async function buildArtifact(projectDir) {
477
692
  );
478
693
  }
479
694
  normalizeBundleStylesheets(projectDir);
695
+ copyMetadataAssets(projectDir);
480
696
 
481
697
  validateArtifactBoundary(readArtifactFiles(projectDir));
482
698
 
483
- // Generate manifest
484
- const manifest = generateManifest(appConfig, projectDir);
485
- const manifestPath = join(projectDir, MANIFEST_FILE);
486
- mkdirSync(dirname(manifestPath), { recursive: true });
487
- writeFileSync(manifestPath, JSON.stringify(manifest, null, 2) + '\n');
699
+ const manifest = readManifest(projectDir);
488
700
 
489
701
  return { manifest, outputDir: join(projectDir, OUTPUT_DIR) };
490
702
  }
@@ -500,6 +712,22 @@ export function readManifest(projectDir) {
500
712
  return JSON.parse(readFileSync(manifestPath, 'utf-8'));
501
713
  }
502
714
 
715
+ function copyMetadataAssets(projectDir) {
716
+ const metadataDir = join(projectDir, METADATA_DIR);
717
+ if (!existsSync(metadataDir)) {
718
+ return;
719
+ }
720
+ const outputMetadataDir = join(projectDir, OUTPUT_DIR, METADATA_DIR);
721
+ mkdirSync(outputMetadataDir, { recursive: true });
722
+ for (const entry of readdirSync(metadataDir, { withFileTypes: true })) {
723
+ if (!entry.isFile()) continue;
724
+ if (entry.name !== 'cover.png' && !/^screenshot-\d+\.png$/i.test(entry.name)) {
725
+ continue;
726
+ }
727
+ cpSync(join(metadataDir, entry.name), join(outputMetadataDir, entry.name));
728
+ }
729
+ }
730
+
503
731
  // ---------------------------------------------------------------------------
504
732
  // Linking
505
733
  // ---------------------------------------------------------------------------
@@ -530,24 +758,37 @@ export function requireLinkedAppId(projectDir, explicitAppId) {
530
758
  /**
531
759
  * Scaffold a new Notis app project from the SDK template.
532
760
  */
533
- export function scaffoldProject({ projectDir, appName }) {
534
- const templateDir = resolve(
535
- dirname(fileURLToPath(import.meta.url)),
536
- '../../template',
537
- );
761
+ export function loadScaffoldCatalog() {
762
+ if (existsSync(SCAFFOLD_CATALOG_FILE)) {
763
+ const parsed = JSON.parse(readFileSync(SCAFFOLD_CATALOG_FILE, 'utf-8'));
764
+ const scaffolds = Array.isArray(parsed?.scaffolds) ? parsed.scaffolds : parsed;
765
+ return Array.isArray(scaffolds)
766
+ ? scaffolds.filter((entry) => entry && typeof entry.slug === 'string')
767
+ : [];
768
+ }
769
+ return loadMonorepoScaffoldCatalog();
770
+ }
771
+
772
+ export function scaffoldProject({ projectDir, appName, fromSlug = null }) {
773
+ const templateDir = fromSlug ? resolveScaffoldSourceDir(fromSlug) : join(CLI_ROOT, 'template');
538
774
 
539
775
  if (!existsSync(templateDir)) {
776
+ if (fromSlug) {
777
+ const known = loadScaffoldCatalog().map((entry) => entry.slug).join(', ') || 'none';
778
+ throw usageError(`Unknown scaffold "${fromSlug}". Available scaffolds: ${known}.`);
779
+ }
540
780
  throw usageError(`SDK template not found at ${templateDir}. Ensure @notis_ai/cli is installed correctly.`);
541
781
  }
542
782
 
543
783
  mkdirSync(projectDir, { recursive: true });
544
- cpSync(templateDir, projectDir, { recursive: true });
784
+ copyScaffoldSource(templateDir, projectDir);
545
785
 
546
786
  // Update package.json with the app name
547
787
  const pkgPath = join(projectDir, 'package.json');
548
788
  if (existsSync(pkgPath)) {
549
789
  const pkg = JSON.parse(readFileSync(pkgPath, 'utf-8'));
550
790
  pkg.name = appName.toLowerCase().replace(/[^a-z0-9-]/g, '-').replace(/-+/g, '-');
791
+ ensureScaffoldLocalSdk(projectDir, pkg);
551
792
  writeFileSync(pkgPath, JSON.stringify(pkg, null, 2) + '\n');
552
793
  }
553
794
 
@@ -555,13 +796,141 @@ export function scaffoldProject({ projectDir, appName }) {
555
796
  const configPath = join(projectDir, 'notis.config.ts');
556
797
  if (existsSync(configPath)) {
557
798
  let config = readFileSync(configPath, 'utf-8');
558
- config = config.replace(/'My Notis App'/, `'${appName.replace(/'/g, "\\'")}'`);
799
+ const displayName = jsStringLiteral(appName);
800
+ const slugName = jsStringLiteral(safeKebab(appName) || 'my-notis-app');
801
+ if (fromSlug) {
802
+ if (/name\s*:/.test(config)) {
803
+ config = config.replace(/name\s*:\s*(['"`])[\s\S]*?\1/, `name: ${slugName}`);
804
+ }
805
+ if (/title\s*:/.test(config)) {
806
+ config = config.replace(/title\s*:\s*(['"`])[\s\S]*?\1/, `title: ${displayName}`);
807
+ } else {
808
+ config = config.replace(/name\s*:\s*(['"`])[\s\S]*?\1/, `name: ${slugName},\n title: ${displayName}`);
809
+ }
810
+ } else {
811
+ if (/name\s*:/.test(config)) {
812
+ config = config.replace(/name\s*:\s*(['"`])[\s\S]*?\1/, `name: ${slugName}`);
813
+ }
814
+ if (/title\s*:/.test(config)) {
815
+ config = config.replace(/title\s*:\s*(['"`])[\s\S]*?\1/, `title: ${displayName}`);
816
+ } else {
817
+ config = config.replace(/'My Notis App'/, displayName);
818
+ }
819
+ }
559
820
  writeFileSync(configPath, config);
560
821
  }
561
822
 
562
823
  return { projectDir };
563
824
  }
564
825
 
826
+ function ensureScaffoldLocalSdk(projectDir, pkg) {
827
+ let shouldInstallLocalSdk = false;
828
+ for (const dependencyGroup of ['dependencies', 'devDependencies']) {
829
+ const dependencyValue = pkg[dependencyGroup]?.['@notis/sdk'];
830
+ if (typeof dependencyValue === 'string' && dependencyValue.startsWith('file:')) {
831
+ pkg[dependencyGroup]['@notis/sdk'] = 'file:./packages/sdk';
832
+ shouldInstallLocalSdk = true;
833
+ }
834
+ }
835
+
836
+ if (!shouldInstallLocalSdk) {
837
+ return;
838
+ }
839
+
840
+ const localSdkDir = join(projectDir, 'packages', 'sdk');
841
+ if (existsSync(join(localSdkDir, 'package.json'))) {
842
+ return;
843
+ }
844
+ if (!existsSync(join(TEMPLATE_SDK_DIR, 'package.json'))) {
845
+ throw usageError(`SDK template not found at ${TEMPLATE_SDK_DIR}. Ensure @notis_ai/cli is installed correctly.`);
846
+ }
847
+
848
+ mkdirSync(dirname(localSdkDir), { recursive: true });
849
+ cpSync(TEMPLATE_SDK_DIR, localSdkDir, { recursive: true, dereference: true });
850
+ }
851
+
852
+ function resolveScaffoldSourceDir(fromSlug) {
853
+ const bundledDir = join(SCAFFOLD_SOURCE_DIR, fromSlug);
854
+ if (existsSync(bundledDir)) {
855
+ return bundledDir;
856
+ }
857
+ const monorepoDir = join(MONOREPO_APPS_DIR, fromSlug);
858
+ if (existsSync(join(monorepoDir, 'notis.config.ts'))) {
859
+ return monorepoDir;
860
+ }
861
+ return bundledDir;
862
+ }
863
+
864
+ function loadMonorepoScaffoldCatalog() {
865
+ if (!existsSync(MONOREPO_APPS_DIR)) {
866
+ return [];
867
+ }
868
+ const scaffolds = [];
869
+ for (const entry of readdirSync(MONOREPO_APPS_DIR, { withFileTypes: true })) {
870
+ if (!entry.isDirectory() || entry.name.startsWith('.') || entry.name.startsWith('_')) {
871
+ continue;
872
+ }
873
+ const configPath = join(MONOREPO_APPS_DIR, entry.name, 'notis.config.ts');
874
+ if (!existsSync(configPath)) {
875
+ continue;
876
+ }
877
+ const configSource = readFileSync(configPath, 'utf-8');
878
+ const description = readTsStringProperty(configSource, 'description') || '';
879
+ scaffolds.push({
880
+ slug: entry.name,
881
+ name: readTsStringProperty(configSource, 'title') || readTsStringProperty(configSource, 'name') || entry.name,
882
+ description,
883
+ icon: readTsStringProperty(configSource, 'icon') || 'phosphor:squares-four',
884
+ categories: readTsStringArrayProperty(configSource, 'categories'),
885
+ tagline: readTsStringProperty(configSource, 'tagline') || description,
886
+ });
887
+ }
888
+ return scaffolds.sort((a, b) => a.slug.localeCompare(b.slug));
889
+ }
890
+
891
+ function readTsStringProperty(source, propertyName) {
892
+ const match = source.match(new RegExp(`\\b${propertyName}\\s*:\\s*(['"\`])([\\s\\S]*?)\\1`));
893
+ return match ? match[2] : null;
894
+ }
895
+
896
+ function readTsStringArrayProperty(source, propertyName) {
897
+ const match = source.match(new RegExp(`\\b${propertyName}\\s*:\\s*\\[([\\s\\S]*?)\\]`));
898
+ if (!match) {
899
+ return [];
900
+ }
901
+ return Array.from(match[1].matchAll(/(['"`])([\s\S]*?)\1/g), (entry) => entry[2].trim()).filter(Boolean);
902
+ }
903
+
904
+ function copyScaffoldSource(sourceDir, targetDir) {
905
+ function shouldCopy(path) {
906
+ const name = path.split(/[\\/]/).pop();
907
+ if (!name) return true;
908
+ return !SCAFFOLD_COPY_EXCLUDES.has(name)
909
+ && !name.startsWith('.env')
910
+ && !/\.(test|spec)\.[cm]?[jt]sx?$/i.test(name);
911
+ }
912
+
913
+ function walk(src, dest) {
914
+ if (!shouldCopy(src)) {
915
+ return;
916
+ }
917
+ const stat = statSync(src);
918
+ if (stat.isDirectory()) {
919
+ mkdirSync(dest, { recursive: true });
920
+ for (const entry of readdirSync(src)) {
921
+ walk(join(src, entry), join(dest, entry));
922
+ }
923
+ return;
924
+ }
925
+ if (stat.isFile()) {
926
+ mkdirSync(dirname(dest), { recursive: true });
927
+ cpSync(src, dest);
928
+ }
929
+ }
930
+
931
+ walk(sourceDir, targetDir);
932
+ }
933
+
565
934
  // ---------------------------------------------------------------------------
566
935
  // Deploy helpers
567
936
  // ---------------------------------------------------------------------------
@@ -700,8 +1069,14 @@ export async function pullAppSource({
700
1069
  const versionMatch = /-v(\d+)\.tar\.gz/i.exec(contentDisposition);
701
1070
  const pulledVersion = versionMatch ? Number.parseInt(versionMatch[1], 10) : null;
702
1071
  extractTarGz(Buffer.from(await response.arrayBuffer()), targetDir);
1072
+ const linkedVersion = pulledVersion || (
1073
+ String(version || 'latest') === 'latest'
1074
+ ? undefined
1075
+ : Number.parseInt(String(version), 10)
1076
+ );
703
1077
  writeLinkedState(targetDir, {
704
1078
  app_id: appId,
1079
+ ...(Number.isFinite(linkedVersion) ? { version: linkedVersion } : {}),
705
1080
  linked_at: new Date().toISOString(),
706
1081
  });
707
1082
 
@@ -898,6 +1273,9 @@ const CONTENT_TYPE_MAP = {
898
1273
  '.css': 'text/css',
899
1274
  '.json': 'application/json',
900
1275
  '.html': 'text/html',
1276
+ '.png': 'image/png',
1277
+ '.jpg': 'image/jpeg',
1278
+ '.jpeg': 'image/jpeg',
901
1279
  };
902
1280
 
903
1281
  /**
@@ -952,6 +1330,8 @@ export async function directDeploy(projectDir, appId) {
952
1330
  version: newVersion,
953
1331
  storage_bucket: bucket,
954
1332
  storage_prefix: storagePrefix,
1333
+ source_storage_bucket: 'app-source',
1334
+ source_storage_prefix: storagePrefix,
955
1335
  };
956
1336
  await updateAppVersion(supabaseUrl, supabaseKey, appId, newVersion, deployManifest);
957
1337
 
@@ -205,8 +205,8 @@ export function resolveRuntimeProfile(globalOptions = {}, { requireAuth = true }
205
205
  exitCode: EXIT_CODES.auth,
206
206
  hints: [
207
207
  {
208
- command: 'notis auth login --jwt <token>',
209
- reason: 'Configure a token for non-interactive use',
208
+ command: 'Open the Notis desktop app and sign in',
209
+ reason: 'Set NOTIS_JWT or sign in through the Notis desktop app',
210
210
  },
211
211
  ],
212
212
  });
@@ -51,8 +51,8 @@ function normalizeBackendError(status, payload) {
51
51
  exitCode: EXIT_CODES.auth,
52
52
  hints: [
53
53
  {
54
- command: 'notis auth login --jwt <token>',
55
- reason: 'Refresh the stored JWT for this profile',
54
+ command: 'Open the Notis desktop app and sign in',
55
+ reason: 'Open the Notis desktop app to refresh CLI auth, or set NOTIS_JWT',
56
56
  },
57
57
  ],
58
58
  details: payload || {},
@@ -107,8 +107,7 @@
107
107
  return {
108
108
  app: descriptor.app,
109
109
  route: descriptor.route,
110
- databases: descriptor.databases || [],
111
- context: descriptor.context || { collectionItem: null },
110
+ context: descriptor.context || {},
112
111
  navigate: (args) => record('navigate', args),
113
112
  registerTopBarSearch: () => {},
114
113
  setTopBarSearchValue: () => {},
@@ -121,34 +120,6 @@
121
120
  record('callTool', { name, arguments: args || {} });
122
121
  return { ok: true, result: null };
123
122
  },
124
- queryDatabase: async (args) => {
125
- record('queryDatabase', args || {});
126
- return { documents: [], next_offset: null };
127
- },
128
- getDocument: async (args) => {
129
- record('getDocument', args || {});
130
- return null;
131
- },
132
- upsertDocument: async (args) => {
133
- record('upsertDocument', args || {});
134
- return {
135
- status: 'ok',
136
- document: {
137
- id: (args && args.documentId) || 'mock-doc',
138
- title: (args && args.title) || '',
139
- properties: (args && args.properties) || {},
140
- databaseSlug: args && args.databaseSlug,
141
- },
142
- };
143
- },
144
- listCollectionItems: async (args) => {
145
- record('listCollectionItems', args || {});
146
- return { items: [] };
147
- },
148
- listCollectionTree: async (args) => {
149
- record('listCollectionTree', args || {});
150
- return { items: [] };
151
- },
152
123
  request: async (path, options) => {
153
124
  record('request', { path, options });
154
125
  return null;
@@ -168,26 +139,6 @@
168
139
  record('callTool', { name, arguments: args || {} });
169
140
  return runtimeQuery({ method: 'tools/call', name, arguments: args || {} });
170
141
  },
171
- queryDatabase: async (args) => {
172
- record('queryDatabase', args || {});
173
- const result = await runtimeQuery({ method: 'tools/call', name: 'notis_query_database', arguments: args || {} });
174
- return result.result || result;
175
- },
176
- getDocument: async (args) => {
177
- record('getDocument', args || {});
178
- const result = await runtimeQuery({ method: 'tools/call', name: 'notis_get_document', arguments: args || {} });
179
- return result.result || result;
180
- },
181
- upsertDocument: async (args) => {
182
- record('upsertDocument', args || {});
183
- const result = await runtimeQuery({ method: 'tools/call', name: 'notis_upsert_document', arguments: args || {} });
184
- return result.result || result;
185
- },
186
- listCollectionItems: async (args) => {
187
- record('listCollectionItems', args || {});
188
- const result = await runtimeQuery({ method: 'tools/call', name: 'notis_list_collection_items', arguments: args || {} });
189
- return result.result || result;
190
- },
191
142
  request: async (path, options) => {
192
143
  record('request', { path, options });
193
144
  const response = await fetch(`${String(apiBase).replace(/\/$/, '')}${path}`, {