@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
@@ -25,6 +25,8 @@ import {
25
25
  writeLinkedState,
26
26
  requireLinkedAppId,
27
27
  scaffoldProject,
28
+ loadScaffoldCatalog,
29
+ inspectListingReadiness,
28
30
  collectArtifactFiles,
29
31
  collectSourceFiles,
30
32
  directDeploy,
@@ -68,6 +70,15 @@ function appsTable(apps) {
68
70
  ]);
69
71
  }
70
72
 
73
+ function scaffoldsTable(scaffolds) {
74
+ return formatTable(scaffolds, [
75
+ { label: 'Slug', value: (scaffold) => scaffold.slug || '' },
76
+ { label: 'Name', value: (scaffold) => scaffold.name || scaffold.slug || '' },
77
+ { label: 'Category', value: (scaffold) => (scaffold.categories || [])[0] || '' },
78
+ { label: 'Tagline', value: (scaffold) => scaffold.tagline || scaffold.description || '' },
79
+ ]);
80
+ }
81
+
71
82
  function hasNotisConfig(dir) {
72
83
  return CONFIG_FILENAMES.some((name) => existsSync(join(dir, name)));
73
84
  }
@@ -155,6 +166,24 @@ function buildDevInstallSlug(appName) {
155
166
  return base.endsWith('-dev') ? base : `${base}-dev`;
156
167
  }
157
168
 
169
+ function timingMs(startedAt) {
170
+ return Number(process.hrtime.bigint() - startedAt) / 1_000_000;
171
+ }
172
+
173
+ function logAppsTiming(label, details = {}) {
174
+ const suffix = Object.entries(details)
175
+ .map(([key, value]) => `${key}=${value}`)
176
+ .join(' ');
177
+ process.stderr.write(`[notis apps timing] ${label}${suffix ? ` ${suffix}` : ''}\n`);
178
+ }
179
+
180
+ function nextDevInstallIdempotencyKey(globalOptions = {}, devSlug) {
181
+ if (globalOptions.idempotencyKey) {
182
+ return `${globalOptions.idempotencyKey}:${devSlug}`;
183
+ }
184
+ return nextIdempotencyKey(globalOptions);
185
+ }
186
+
158
187
  function pickDefaultRouteSlug(manifest) {
159
188
  const routes = Array.isArray(manifest?.routes) ? manifest.routes : [];
160
189
  const explicit = routes.find((route) => route && route.default && typeof route.slug === 'string');
@@ -163,8 +192,9 @@ function pickDefaultRouteSlug(manifest) {
163
192
  return firstWithSlug ? firstWithSlug.slug : null;
164
193
  }
165
194
 
166
- export function buildDevelopmentDesktopUrl() {
167
- return 'notis://apps?tab=development';
195
+ export function buildDevelopmentDesktopUrl(appHref = null) {
196
+ const route = String(appHref || '/store').replace(/^\/+/, '');
197
+ return `notis://${route || 'store'}`;
168
198
  }
169
199
 
170
200
  function buildAppHref({ appSlug, appId, manifest }) {
@@ -279,13 +309,16 @@ function assertHarnessResult(result, route, databaseSlugs) {
279
309
  }
280
310
  for (const databaseSlug of databaseSlugs) {
281
311
  const seen = (result.runtimeCalls || []).some(
282
- (call) => call?.op === 'queryDatabase' && call?.args?.databaseSlug === databaseSlug,
312
+ (call) =>
313
+ call?.op === 'callTool' &&
314
+ call?.args?.name === 'notis-default-query' &&
315
+ call?.args?.arguments?.database_slug === databaseSlug,
283
316
  );
284
317
  if (!seen) {
285
318
  assertions.push({
286
319
  ok: false,
287
320
  code: 'missing_database_query',
288
- message: `Route "${route.slug}" did not call queryDatabase for "${databaseSlug}".`,
321
+ message: `Route "${route.slug}" did not call notis-default-query for "${databaseSlug}".`,
289
322
  details: { databaseSlug },
290
323
  });
291
324
  }
@@ -341,7 +374,26 @@ function buildManifestForDev(appConfig) {
341
374
  };
342
375
  }
343
376
 
344
- async function ensureDevInstall({ ctx, appConfig, projectDir, idempotencyKey }) {
377
+ export function buildEnsureDevInstallArguments({ appConfig, manifest, linkedState }) {
378
+ const devSlug = buildDevInstallSlug(appConfig.name);
379
+ const arguments_ = {
380
+ dev_slug: devSlug,
381
+ name: appConfig.name,
382
+ manifest,
383
+ };
384
+ if (linkedState?.dev_app_id) {
385
+ arguments_.app_id = linkedState.dev_app_id;
386
+ }
387
+ return arguments_;
388
+ }
389
+
390
+ export async function ensureDevInstall({
391
+ ctx,
392
+ appConfig,
393
+ projectDir,
394
+ idempotencyKey,
395
+ runTool = runToolCommand,
396
+ }) {
345
397
  if (!appConfig.name) {
346
398
  throw usageError(`notis.config.ts in ${projectDir} must define a non-empty name.`);
347
399
  }
@@ -351,14 +403,12 @@ async function ensureDevInstall({ ctx, appConfig, projectDir, idempotencyKey })
351
403
  }
352
404
 
353
405
  const manifest = buildManifestForDev(appConfig);
354
- const ensureResult = await runToolCommand({
406
+ const linkedState = readLinkedState(projectDir);
407
+ const ensureArguments = buildEnsureDevInstallArguments({ appConfig, manifest, linkedState });
408
+ const ensureResult = await runTool({
355
409
  runtime: ctx.runtime,
356
410
  toolName: ENSURE_DEV_APP_INSTALLATION_TOOL,
357
- arguments_: {
358
- dev_slug: devSlug,
359
- name: appConfig.name,
360
- manifest,
361
- },
411
+ arguments_: ensureArguments,
362
412
  mutating: true,
363
413
  idempotencyKey,
364
414
  });
@@ -374,9 +424,26 @@ async function ensureDevInstall({ ctx, appConfig, projectDir, idempotencyKey })
374
424
  projectDir,
375
425
  manifest,
376
426
  created: ensureResult.payload.created || false,
427
+ linkedAppId: linkedState?.app_id || null,
428
+ databaseMaterialization: ensureResult.payload.database_materialization || { created: [], unresolved: [] },
377
429
  };
378
430
  }
379
431
 
432
+ function databaseMaterializationWarnings(apps) {
433
+ const warnings = [];
434
+ for (const app of apps) {
435
+ const unresolved = app.databaseMaterialization?.unresolved || [];
436
+ if (!unresolved.length) {
437
+ continue;
438
+ }
439
+ warnings.push(
440
+ `${app.name}: database slug${unresolved.length === 1 ? '' : 's'} ${unresolved.join(', ')} ` +
441
+ 'could not be created because no store snapshot schema exists. Create them manually or link to a store-installed app.',
442
+ );
443
+ }
444
+ return warnings;
445
+ }
446
+
380
447
  async function assertDirectDeployAccess(runtime, appId) {
381
448
  const result = await runToolCommand({
382
449
  runtime,
@@ -409,13 +476,16 @@ async function appsListHandler(ctx) {
409
476
 
410
477
  async function appsInitHandler(ctx) {
411
478
  const projectDir = resolveProjectDir(ctx.args.dir || ctx.args.name.toLowerCase().replace(/[^a-z0-9]+/g, '-'));
479
+ const fromSlug = ctx.options.from || null;
412
480
 
413
- scaffoldProject({ projectDir, appName: ctx.args.name });
481
+ scaffoldProject({ projectDir, appName: ctx.args.name, fromSlug });
414
482
 
415
483
  return ctx.output.emitSuccess({
416
484
  command: ctx.spec.command_path.join(' '),
417
- data: { project_dir: projectDir, app_name: ctx.args.name },
418
- humanSummary: `Scaffolded "${ctx.args.name}" in ${projectDir}`,
485
+ data: { project_dir: projectDir, app_name: ctx.args.name, scaffold: fromSlug },
486
+ humanSummary: fromSlug
487
+ ? `Scaffolded "${ctx.args.name}" from ${fromSlug} in ${projectDir}`
488
+ : `Scaffolded "${ctx.args.name}" in ${projectDir}`,
419
489
  hints: [
420
490
  { command: `cd ${projectDir} && npm install`, reason: 'Install dependencies' },
421
491
  { command: `cd ${projectDir} && notis apps dev`, reason: 'Start the real-portal dev workflow' },
@@ -423,19 +493,29 @@ async function appsInitHandler(ctx) {
423
493
  });
424
494
  }
425
495
 
496
+ async function appsScaffoldsListHandler(ctx) {
497
+ const scaffolds = loadScaffoldCatalog();
498
+ return ctx.output.emitSuccess({
499
+ command: ctx.spec.command_path.join(' '),
500
+ data: { scaffolds },
501
+ humanSummary: scaffolds.length
502
+ ? `Found ${scaffolds.length} bundled scaffolds`
503
+ : 'No bundled scaffolds found. Run the CLI build step to generate the scaffold catalog.',
504
+ renderHuman: () => (scaffolds.length ? scaffoldsTable(scaffolds) : 'No bundled scaffolds found.'),
505
+ });
506
+ }
507
+
426
508
  async function appsCreateHandler(ctx) {
427
509
  const projectDir = ctx.args.dir ? resolveProjectDir(ctx.args.dir) : null;
510
+ const appConfig = projectDir ? await loadAppConfig(projectDir) : null;
428
511
  const idempotencyKey = nextIdempotencyKey(ctx.globalOptions);
429
- const icon = ctx.options.icon
430
- ? (ctx.options.icon.startsWith('lucide:') ? ctx.options.icon : `lucide:${ctx.options.icon}`)
431
- : undefined;
432
512
  const result = await runToolCommand({
433
513
  runtime: ctx.runtime,
434
514
  toolName: 'notis_create_app',
435
515
  arguments_: {
436
516
  name: ctx.args.name,
437
- description: ctx.options.description || undefined,
438
- icon,
517
+ description: appConfig?.description || undefined,
518
+ icon: appConfig?.icon || undefined,
439
519
  },
440
520
  mutating: true,
441
521
  idempotencyKey,
@@ -490,25 +570,26 @@ async function appsDevHandler(ctx) {
490
570
  const mode = getCliMode();
491
571
  const identity = decodeJwtSub(ctx.runtime.jwt);
492
572
  if (!identity) {
493
- throw usageError('Could not determine the current user from the CLI auth token. Run `notis auth login` from the desktop app and retry.');
573
+ throw usageError('Could not determine the current user from the CLI auth token. Open the Notis desktop app, sign in, and retry.');
494
574
  }
495
575
  const apiBase = String(ctx.runtime.apiBase || '').replace(/\/$/, '');
496
576
  const sessionId = randomUUID();
497
577
 
498
- const registered = [];
578
+ const candidates = [];
499
579
  for (const projectDir of appDirs) {
500
580
  const appConfig = await loadAppConfig(projectDir);
501
- const info = await ensureDevInstall({
502
- ctx,
503
- appConfig,
504
- projectDir,
505
- idempotencyKey: nextIdempotencyKey(ctx.globalOptions),
506
- });
507
- registered.push(info);
581
+ if (!appConfig.name) {
582
+ throw usageError(`notis.config.ts in ${projectDir} must define a non-empty name.`);
583
+ }
584
+ const devSlug = buildDevInstallSlug(appConfig.name);
585
+ if (!devSlug) {
586
+ throw usageError(`notis.config.ts name in ${projectDir} must slugify to a non-empty value.`);
587
+ }
588
+ candidates.push({ appConfig, devSlug, projectDir });
508
589
  }
509
590
 
510
591
  const seenSlugs = new Map();
511
- for (const app of registered) {
592
+ for (const app of candidates) {
512
593
  if (seenSlugs.has(app.devSlug)) {
513
594
  throw usageError(
514
595
  `Multiple apps slugify to "${app.devSlug}" (${seenSlugs.get(app.devSlug).projectDir} and ${app.projectDir}). Rename one in notis.config.ts.`,
@@ -517,6 +598,30 @@ async function appsDevHandler(ctx) {
517
598
  seenSlugs.set(app.devSlug, app);
518
599
  }
519
600
 
601
+ const registrationStartedAt = process.hrtime.bigint();
602
+ const registered = await Promise.all(
603
+ candidates.map(async ({ appConfig, devSlug, projectDir }) => {
604
+ const appStartedAt = process.hrtime.bigint();
605
+ try {
606
+ return await ensureDevInstall({
607
+ ctx,
608
+ appConfig,
609
+ projectDir,
610
+ idempotencyKey: nextDevInstallIdempotencyKey(ctx.globalOptions, devSlug),
611
+ });
612
+ } finally {
613
+ logAppsTiming('ensure-dev-install', {
614
+ slug: devSlug,
615
+ ms: timingMs(appStartedAt).toFixed(1),
616
+ });
617
+ }
618
+ }),
619
+ );
620
+ logAppsTiming('ensure-dev-install:all', {
621
+ apps: registered.length,
622
+ ms: timingMs(registrationStartedAt).toFixed(1),
623
+ });
624
+
520
625
  const apps = registered.map((app) => {
521
626
  const bundleBaseUrl = `http://127.0.0.1:${port}/a/${app.devSlug}`;
522
627
  return {
@@ -529,10 +634,16 @@ async function appsDevHandler(ctx) {
529
634
  }),
530
635
  };
531
636
  });
532
- const developmentTabUrl = buildDevelopmentDesktopUrl();
637
+ const developmentTabUrl = buildDevelopmentDesktopUrl(apps[0]?.appHref);
638
+ const warnings = databaseMaterializationWarnings(apps);
533
639
 
534
640
  const devServer = await startAppDevServer({
535
- apps: apps.map((app) => ({ slug: app.devSlug, projectDir: app.projectDir, appId: app.appId })),
641
+ apps: apps.map((app) => ({
642
+ slug: app.devSlug,
643
+ projectDir: app.projectDir,
644
+ appId: app.appId,
645
+ userId: identity,
646
+ })),
536
647
  port,
537
648
  });
538
649
 
@@ -583,12 +694,15 @@ async function appsDevHandler(ctx) {
583
694
  bundle_base_url: app.bundleBaseUrl,
584
695
  app_href: app.appHref,
585
696
  created: app.created,
697
+ linked_app_id: app.linkedAppId,
698
+ database_materialization: app.databaseMaterialization,
586
699
  })),
587
700
  },
701
+ warnings,
588
702
  humanSummary: [
589
703
  `Running apps dev against ${apiBase} as ${identity} (mode: ${mode})`,
590
704
  '',
591
- `Development tab: ${developmentTabUrl}`,
705
+ `Open in desktop: ${developmentTabUrl}`,
592
706
  '',
593
707
  ...apps.map((app) => ` ${app.name.padEnd(24)} ${app.bundleBaseUrl} -> ${app.appHref}`),
594
708
  '',
@@ -665,7 +779,7 @@ async function appsVerifyHandler(ctx) {
665
779
  let linkedState = null;
666
780
  if (mode === 'live') {
667
781
  if (!ctx.runtime.jwt) {
668
- throw usageError('Live verify mode requires CLI auth. Run `notis auth login` and retry.');
782
+ throw usageError('Live verify mode requires CLI auth. Open the Notis desktop app, sign in, and retry.');
669
783
  }
670
784
  linkedState = readLinkedState(projectDir);
671
785
  if (!linkedState?.app_id) {
@@ -679,6 +793,11 @@ async function appsVerifyHandler(ctx) {
679
793
 
680
794
  const manifest = readManifest(projectDir);
681
795
  const appConfig = await loadAppConfig(projectDir);
796
+ const listing = inspectListingReadiness(projectDir, appConfig);
797
+ if (listing.errors.length) {
798
+ throw usageError(`Listing metadata has problems:\n${listing.errors.map((error) => ` - ${error}`).join('\n')}`);
799
+ }
800
+ const listingWarnings = listing.warnings;
682
801
  const routes = routeSelection(manifest, parseRouteSlugs(ctx.options.routes));
683
802
  const port = parsePort(ctx.options.port) || await getAvailablePort();
684
803
  const appSlug = slugify(appConfig.name || manifest.app?.name || 'app') || 'app';
@@ -713,7 +832,7 @@ async function appsVerifyHandler(ctx) {
713
832
  }));
714
833
 
715
834
  let results;
716
- const warnings = [];
835
+ const warnings = [...listingWarnings];
717
836
  if (noBrowser) {
718
837
  results = urls.map(({ route, url }) => ({
719
838
  route: route.slug,
@@ -905,10 +1024,27 @@ async function appsPullHandler(ctx) {
905
1024
  });
906
1025
  }
907
1026
 
1027
+ function updateLinkedDeployState(projectDir, linkedState, appId, version) {
1028
+ if (!linkedState || linkedState.app_id !== appId || !Number.isFinite(version)) {
1029
+ return;
1030
+ }
1031
+ writeLinkedState(projectDir, {
1032
+ ...linkedState,
1033
+ app_id: appId,
1034
+ version,
1035
+ linked_at: linkedState.linked_at || new Date().toISOString(),
1036
+ deployed_at: new Date().toISOString(),
1037
+ });
1038
+ }
1039
+
908
1040
  async function appsDeployHandler(ctx) {
909
1041
  const projectDir = resolveProjectDir(ctx.args.dir || '.');
910
1042
  const appId = requireLinkedAppId(projectDir, ctx.options.appId);
911
1043
  const idempotencyKey = nextIdempotencyKey(ctx.globalOptions);
1044
+ const linkedState = readLinkedState(projectDir);
1045
+ const baseVersion = linkedState?.app_id === appId && Number.isFinite(linkedState?.version)
1046
+ ? linkedState.version
1047
+ : undefined;
912
1048
 
913
1049
  // Build if needed
914
1050
  if (!ctx.options.skipBuild) {
@@ -919,6 +1055,7 @@ async function appsDeployHandler(ctx) {
919
1055
  if (ctx.options.direct) {
920
1056
  await assertDirectDeployAccess(ctx.runtime, appId);
921
1057
  const { version } = await directDeploy(projectDir, appId);
1058
+ updateLinkedDeployState(projectDir, linkedState, appId, version);
922
1059
  return ctx.output.emitSuccess({
923
1060
  command: ctx.spec.command_path.join(' '),
924
1061
  data: { app_id: appId, version, mode: 'direct' },
@@ -942,6 +1079,7 @@ async function appsDeployHandler(ctx) {
942
1079
  files,
943
1080
  source_files: sourceFiles,
944
1081
  manifest,
1082
+ ...(baseVersion !== undefined ? { base_version: baseVersion } : {}),
945
1083
  },
946
1084
  mutating: true,
947
1085
  idempotencyKey,
@@ -968,6 +1106,7 @@ async function appsDeployHandler(ctx) {
968
1106
 
969
1107
  try {
970
1108
  const { version } = await directDeploy(projectDir, appId);
1109
+ updateLinkedDeployState(projectDir, linkedState, appId, version);
971
1110
  return ctx.output.emitSuccess({
972
1111
  command: ctx.spec.command_path.join(' '),
973
1112
  data: { app_id: appId, version, mode: 'direct-fallback' },
@@ -986,6 +1125,7 @@ async function appsDeployHandler(ctx) {
986
1125
  throw error;
987
1126
  }
988
1127
 
1128
+ updateLinkedDeployState(projectDir, linkedState, appId, Number(result.payload.version));
989
1129
  return ctx.output.emitSuccess({
990
1130
  command: ctx.spec.command_path.join(' '),
991
1131
  data: {
@@ -1009,13 +1149,21 @@ async function appsDoctorHandler(ctx) {
1009
1149
  problems.push('Failed to load notis.config.ts');
1010
1150
  }
1011
1151
  const warnings = detectProjectWarnings(projectDir, appConfig);
1152
+ let listing = null;
1153
+ if (appConfig) {
1154
+ try {
1155
+ listing = inspectListingReadiness(projectDir, appConfig);
1156
+ } catch (error) {
1157
+ warnings.push(error instanceof Error ? error.message : String(error));
1158
+ }
1159
+ }
1012
1160
 
1013
1161
  const linkedState = readLinkedState(projectDir);
1014
1162
  const status = problems.length ? 'unhealthy' : warnings.length ? 'warnings' : 'healthy';
1015
1163
 
1016
1164
  return ctx.output.emitSuccess({
1017
1165
  command: ctx.spec.command_path.join(' '),
1018
- data: { status, problems, warnings, linked: linkedState, config: appConfig },
1166
+ data: { status, problems, warnings, linked: linkedState, config: appConfig, listing },
1019
1167
  humanSummary: problems.length
1020
1168
  ? `Found ${problems.length} problems:\n${problems.map((p) => ` - ${p}`).join('\n')}`
1021
1169
  : warnings.length
@@ -1043,21 +1191,40 @@ export const appsCommandSpecs = [
1043
1191
  {
1044
1192
  command_path: ['apps', 'init'],
1045
1193
  summary: 'Scaffold a new Notis app project.',
1046
- when_to_use: 'Start a new Notis app. Creates a Vite + React project with @notis/sdk pre-configured.',
1194
+ when_to_use: 'Start a new Notis app. Use --from with a bundled scaffold when one is close to the desired app; otherwise creates the bare Vite + React project.',
1047
1195
  args_schema: {
1048
1196
  arguments: [
1049
1197
  { token: '<name>', description: 'Display name for the app.' },
1050
1198
  { token: '[dir]', key: 'dir', description: 'Target directory (defaults to kebab-case of name).' },
1051
1199
  ],
1052
- options: [],
1200
+ options: [
1201
+ { flags: '--from <slug>', description: 'Start from a bundled scaffold listed by `notis apps scaffolds list`.' },
1202
+ ],
1053
1203
  },
1054
- examples: ['notis apps init "Mind the Flo"', 'notis apps init "My App" ./my-app'],
1204
+ examples: [
1205
+ 'notis apps scaffolds list',
1206
+ 'notis apps init "Mind the Flo"',
1207
+ 'notis apps init "My CRM" --from notis-database',
1208
+ 'notis apps init "My App" ./my-app',
1209
+ ],
1055
1210
  mutates: true,
1056
1211
  idempotent: false,
1057
1212
  require_auth: false,
1058
1213
  backend_call: { type: 'local', name: 'scaffold_project' },
1059
1214
  handler: appsInitHandler,
1060
1215
  },
1216
+ {
1217
+ command_path: ['apps', 'scaffolds', 'list'],
1218
+ summary: 'List bundled Notis app scaffolds.',
1219
+ when_to_use: 'Discover the fixed scaffold catalog shipped with the CLI before starting a new app.',
1220
+ args_schema: { arguments: [], options: [] },
1221
+ examples: ['notis apps scaffolds list', 'notis apps init "My App" --from notis-database'],
1222
+ mutates: false,
1223
+ idempotent: true,
1224
+ require_auth: false,
1225
+ backend_call: { type: 'local', name: 'list_scaffolds' },
1226
+ handler: appsScaffoldsListHandler,
1227
+ },
1061
1228
  {
1062
1229
  command_path: ['apps', 'create'],
1063
1230
  summary: 'Create a new remote Notis app and optionally link a local project to it.',
@@ -1067,14 +1234,11 @@ export const appsCommandSpecs = [
1067
1234
  { token: '<name>', description: 'Display name for the remote app.' },
1068
1235
  { token: '[dir]', key: 'dir', description: 'Project directory to link after creation (default: do not link).' },
1069
1236
  ],
1070
- options: [
1071
- { flags: '--description <text>', description: 'Optional app description.' },
1072
- { flags: '--icon <lucide:icon>', description: 'Optional Lucide icon, for example lucide:dices.' },
1073
- ],
1237
+ options: [],
1074
1238
  },
1075
1239
  examples: [
1076
1240
  'notis apps create "My App"',
1077
- 'notis apps create "My App" . --description "Internal tool" --icon lucide:layout-dashboard',
1241
+ 'notis apps create "My App" .',
1078
1242
  ],
1079
1243
  mutates: true,
1080
1244
  idempotent: false,
@@ -1085,14 +1249,14 @@ export const appsCommandSpecs = [
1085
1249
  command_path: ['apps', 'dev'],
1086
1250
  summary: 'Develop Notis apps inside the Electron desktop Portal with automatic local bundle reloads.',
1087
1251
  when_to_use:
1088
- 'Run this inside a single app or a monorepo root with apps/<name>/notis.config.ts. It discovers every app, starts the local bundle server, registers desktop-local dev sessions, and opens the Electron Portal Development tab.',
1252
+ 'Run this inside a single app or a monorepo root with apps/<name>/notis.config.ts. It discovers every app, starts the local bundle server, registers desktop-local dev sessions, and opens the Electron Portal to the local development app.',
1089
1253
  args_schema: {
1090
1254
  arguments: [
1091
1255
  { token: '[dir]', key: 'dir', description: 'Project directory or monorepo root (default: current dir).' },
1092
1256
  ],
1093
1257
  options: [
1094
1258
  { flags: '--port <number>', description: `Local bundle server port (default: ${DEFAULT_DEV_PORT}).` },
1095
- { flags: '--no-open', description: 'Do not auto-open the desktop Portal Development tab.' },
1259
+ { flags: '--no-open', description: 'Do not auto-open the desktop Portal local development app.' },
1096
1260
  ],
1097
1261
  },
1098
1262
  examples: [
@@ -1175,7 +1339,8 @@ export const appsCommandSpecs = [
1175
1339
  {
1176
1340
  command_path: ['apps', 'pull'],
1177
1341
  summary: 'Download a Notis app source snapshot into a local project folder.',
1178
- when_to_use: 'Edit an installed app locally. Pulls the persisted source and links the directory to the app.',
1342
+ when_to_use:
1343
+ 'Edit an installed app locally. Pulls the persisted source, links the directory to the app/version, then continue with npm install, notis apps dev, notis apps build, and notis apps deploy.',
1179
1344
  args_schema: {
1180
1345
  arguments: [
1181
1346
  { token: '<app-id>', description: 'Remote app ID to pull.' },
@@ -1,13 +1,6 @@
1
- import { createInterface } from 'node:readline/promises';
2
1
  import { randomUUID } from 'node:crypto';
3
2
  import { CliError, EXIT_CODES, usageError } from '../runtime/errors.js';
4
3
  import { callTool, httpRequest } from '../runtime/transport.js';
5
- import {
6
- DEFAULT_PROFILE,
7
- ensureProfile,
8
- loadConfig,
9
- saveConfig,
10
- } from '../runtime/profiles.js';
11
4
 
12
5
  export function parseJson(value, label) {
13
6
  try {
@@ -41,16 +34,6 @@ export function normalizeToolkits(value) {
41
34
  .filter(Boolean);
42
35
  }
43
36
 
44
- export async function promptForJwt() {
45
- const rl = createInterface({ input: process.stdin, output: process.stdout });
46
- try {
47
- const jwt = await rl.question('Paste your Notis JWT: ');
48
- return jwt.trim();
49
- } finally {
50
- rl.close();
51
- }
52
- }
53
-
54
37
  export function nextIdempotencyKey(globalOptions) {
55
38
  return globalOptions.idempotencyKey || randomUUID();
56
39
  }
@@ -105,49 +88,6 @@ export async function healthCheck(runtime) {
105
88
  });
106
89
  }
107
90
 
108
- export function updateStoredProfile({
109
- profileName,
110
- jwt,
111
- apiBase,
112
- authMode,
113
- refreshToken,
114
- accessExpiresAt,
115
- refreshExpiresAt,
116
- setCurrent = true,
117
- }) {
118
- let config = ensureProfile(loadConfig(), profileName || DEFAULT_PROFILE);
119
- const resolvedName = profileName || DEFAULT_PROFILE;
120
- config.profiles[resolvedName] = {
121
- ...config.profiles[resolvedName],
122
- ...(jwt ? { jwt } : {}),
123
- ...(apiBase ? { api_base: apiBase } : {}),
124
- ...(authMode ? { auth_mode: authMode } : {}),
125
- ...(refreshToken ? { refresh_token: refreshToken } : {}),
126
- ...(typeof accessExpiresAt === 'number'
127
- ? { access_expires_at: accessExpiresAt }
128
- : {}),
129
- ...(typeof refreshExpiresAt === 'number'
130
- ? { refresh_expires_at: refreshExpiresAt }
131
- : {}),
132
- };
133
- if (setCurrent) {
134
- config.current_profile = resolvedName;
135
- }
136
- saveConfig(config);
137
- return config;
138
- }
139
-
140
- export function clearStoredJwt(profileName) {
141
- const config = ensureProfile(loadConfig(), profileName || DEFAULT_PROFILE);
142
- delete config.profiles[profileName || DEFAULT_PROFILE].jwt;
143
- delete config.profiles[profileName || DEFAULT_PROFILE].auth_mode;
144
- delete config.profiles[profileName || DEFAULT_PROFILE].refresh_token;
145
- delete config.profiles[profileName || DEFAULT_PROFILE].access_expires_at;
146
- delete config.profiles[profileName || DEFAULT_PROFILE].refresh_expires_at;
147
- saveConfig(config);
148
- return config;
149
- }
150
-
151
91
  export async function fetchToolSchema(runtime, toolName, rawToolkits) {
152
92
  const toolkits = normalizeToolkits(rawToolkits);
153
93
  const result = await runToolCommand({
@@ -1,20 +1,14 @@
1
- import { authCommandSpecs } from './auth.js';
2
1
  import { appsCommandSpecs } from './apps.js';
3
- import { dbCommandSpecs } from './db.js';
4
2
  import { toolsCommandSpecs } from './tools.js';
5
3
  import { metaCommandSpecs } from './meta.js';
6
4
 
7
5
  export const GROUP_SUMMARIES = {
8
- auth: 'Authentication and profile management.',
9
6
  apps: 'Develop, build, and deploy Notis Apps.',
10
- db: 'List, query, and update native Notis Databases.',
11
7
  tools: 'Discover and execute generic tools exposed through Notis.',
12
8
  };
13
9
 
14
10
  export const COMMAND_SPECS = [
15
- ...authCommandSpecs,
16
11
  ...appsCommandSpecs,
17
- ...dbCommandSpecs,
18
12
  ...toolsCommandSpecs,
19
13
  ...metaCommandSpecs,
20
14
  ];
@@ -29,10 +29,10 @@ async function doctorHandler(ctx) {
29
29
 
30
30
  const hints = [];
31
31
  if (checks.auth === 'missing') {
32
- hints.push({ command: 'notis auth login --jwt <token>', reason: 'Configure credentials' });
32
+ hints.push({ command: 'Open the Notis desktop app and sign in', reason: 'Configure CLI credentials' });
33
33
  }
34
34
  if (checks.health === 'error') {
35
- hints.push({ command: 'notis auth status', reason: 'Check API base URL configuration' });
35
+ hints.push({ command: 'Open the Notis desktop app and sign in again', reason: 'Refresh local CLI configuration' });
36
36
  }
37
37
  if (checks.tool_roundtrip === 'error') {
38
38
  hints.push({ command: 'notis whoami', reason: 'Verify your account and permissions' });
@@ -117,7 +117,7 @@ export const metaCommandSpecs = [
117
117
  output_schema: 'Returns profile, api_base, user_id, toolkit count, and CLI version.',
118
118
  mutates: false,
119
119
  idempotent: true,
120
- related_commands: ['notis auth status --verify', 'notis doctor'],
120
+ related_commands: ['notis doctor'],
121
121
  backend_call: { type: 'tool', name: 'notis_find_toolkits' },
122
122
  handler: whoamiHandler,
123
123
  },
@@ -130,7 +130,8 @@ export const metaCommandSpecs = [
130
130
  output_schema: 'Returns config, auth, health, and roundtrip check statuses.',
131
131
  mutates: false,
132
132
  idempotent: true,
133
- related_commands: ['notis auth status --verify', 'notis tools toolkits'],
133
+ require_auth: false,
134
+ related_commands: ['notis tools toolkits'],
134
135
  backend_call: { type: 'health+tool_roundtrip' },
135
136
  handler: doctorHandler,
136
137
  },
@@ -139,10 +140,10 @@ export const metaCommandSpecs = [
139
140
  summary: 'Describe a first-class CLI command in detail.',
140
141
  when_to_use: 'Use this when an agent or human needs the exact shape, examples, and semantics of a command.',
141
142
  args_schema: {
142
- arguments: [{ token: '<command...>', key: 'commandPath', description: 'Command path to describe, such as "apps push".' }],
143
+ arguments: [{ token: '<command...>', key: 'commandPath', description: 'Command path to describe, such as "apps deploy".' }],
143
144
  options: [],
144
145
  },
145
- examples: ['notis describe apps push', 'notis describe db query'],
146
+ examples: ['notis describe apps deploy', 'notis describe tools exec'],
146
147
  output_schema: 'Returns the command spec metadata for the requested command.',
147
148
  mutates: false,
148
149
  idempotent: true,