@notis_ai/cli 0.2.5 → 0.2.7

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 (157) hide show
  1. package/README.md +33 -9
  2. package/dist/scaffolds/notis-affiliate-prospects/CHANGELOG.md +7 -0
  3. package/dist/scaffolds/notis-affiliate-prospects/README.md +20 -0
  4. package/dist/scaffolds/notis-affiliate-prospects/app/globals.css +42 -0
  5. package/dist/scaffolds/notis-affiliate-prospects/app/layout.tsx +7 -0
  6. package/dist/scaffolds/notis-affiliate-prospects/app/page.tsx +248 -0
  7. package/dist/scaffolds/notis-affiliate-prospects/app/prospects/page.tsx +134 -0
  8. package/dist/scaffolds/notis-affiliate-prospects/app/segments/page.tsx +108 -0
  9. package/dist/scaffolds/notis-affiliate-prospects/components/ui/card.tsx +35 -0
  10. package/dist/scaffolds/notis-affiliate-prospects/components.json +20 -0
  11. package/dist/scaffolds/notis-affiliate-prospects/index.html +12 -0
  12. package/dist/scaffolds/notis-affiliate-prospects/lib/affiliate-data.ts +218 -0
  13. package/dist/scaffolds/notis-affiliate-prospects/lib/demo-prospects.ts +52 -0
  14. package/dist/scaffolds/notis-affiliate-prospects/lib/notis-tools.ts +100 -0
  15. package/dist/scaffolds/notis-affiliate-prospects/lib/utils.ts +25 -0
  16. package/dist/scaffolds/notis-affiliate-prospects/metadata/screenshot-1.png +0 -0
  17. package/dist/scaffolds/notis-affiliate-prospects/metadata/screenshot-2.png +0 -0
  18. package/dist/scaffolds/notis-affiliate-prospects/metadata/screenshot-3.png +0 -0
  19. package/dist/scaffolds/notis-affiliate-prospects/metadata/screenshot-fixtures.json +187 -0
  20. package/dist/scaffolds/notis-affiliate-prospects/notis.config.ts +65 -0
  21. package/dist/scaffolds/notis-affiliate-prospects/package.json +32 -0
  22. package/dist/scaffolds/notis-affiliate-prospects/packages/sdk/package.json +32 -0
  23. package/dist/scaffolds/notis-affiliate-prospects/packages/sdk/src/components/MultiSelectActionBar.tsx +273 -0
  24. package/dist/scaffolds/notis-affiliate-prospects/packages/sdk/src/components/MultiSelectCheckbox.tsx +91 -0
  25. package/dist/scaffolds/notis-affiliate-prospects/packages/sdk/src/components/MultiSelectDragOverlay.tsx +39 -0
  26. package/dist/scaffolds/notis-affiliate-prospects/packages/sdk/src/config.ts +90 -0
  27. package/dist/scaffolds/notis-affiliate-prospects/packages/sdk/src/hooks/useBackend.ts +41 -0
  28. package/dist/scaffolds/notis-affiliate-prospects/packages/sdk/src/hooks/useMultiSelect.ts +503 -0
  29. package/dist/scaffolds/notis-affiliate-prospects/packages/sdk/src/hooks/useNotis.ts +34 -0
  30. package/dist/scaffolds/notis-affiliate-prospects/packages/sdk/src/hooks/useNotisNavigation.ts +49 -0
  31. package/dist/scaffolds/notis-affiliate-prospects/packages/sdk/src/hooks/useTool.ts +64 -0
  32. package/dist/scaffolds/notis-affiliate-prospects/packages/sdk/src/hooks/useTools.ts +56 -0
  33. package/dist/scaffolds/notis-affiliate-prospects/packages/sdk/src/hooks/useTopBarSearch.ts +73 -0
  34. package/dist/scaffolds/notis-affiliate-prospects/packages/sdk/src/hooks/useUpsertDocument.ts +50 -0
  35. package/dist/scaffolds/notis-affiliate-prospects/packages/sdk/src/index.ts +54 -0
  36. package/dist/scaffolds/notis-affiliate-prospects/packages/sdk/src/provider.tsx +43 -0
  37. package/dist/scaffolds/notis-affiliate-prospects/packages/sdk/src/runtime.ts +161 -0
  38. package/dist/scaffolds/notis-affiliate-prospects/packages/sdk/src/styles.css +38 -0
  39. package/dist/scaffolds/notis-affiliate-prospects/packages/sdk/src/ui.ts +15 -0
  40. package/dist/scaffolds/notis-affiliate-prospects/packages/sdk/src/vite.ts +56 -0
  41. package/dist/scaffolds/notis-affiliate-prospects/packages/sdk/tsconfig.json +15 -0
  42. package/dist/scaffolds/notis-affiliate-prospects/postcss.config.mjs +6 -0
  43. package/dist/scaffolds/notis-affiliate-prospects/src/dev-main.tsx +72 -0
  44. package/dist/scaffolds/notis-affiliate-prospects/src/mock-runtime.ts +242 -0
  45. package/dist/scaffolds/notis-affiliate-prospects/tailwind.config.ts +51 -0
  46. package/dist/scaffolds/notis-affiliate-prospects/tsconfig.json +23 -0
  47. package/dist/scaffolds/notis-affiliate-prospects/vite.config.ts +11 -0
  48. package/dist/scaffolds/notis-database/CHANGELOG.md +5 -0
  49. package/dist/scaffolds/notis-database/notis.config.ts +0 -1
  50. package/dist/scaffolds/notis-database/src/mock-runtime.ts +1 -0
  51. package/dist/scaffolds/notis-journal/CHANGELOG.md +25 -0
  52. package/dist/scaffolds/notis-journal/app/globals.css +37 -0
  53. package/dist/scaffolds/notis-journal/app/insights/page.tsx +513 -0
  54. package/dist/scaffolds/notis-journal/app/journal-core.tsx +362 -0
  55. package/dist/scaffolds/notis-journal/app/journal-ui.tsx +337 -0
  56. package/dist/scaffolds/notis-journal/app/layout.tsx +6 -0
  57. package/dist/scaffolds/notis-journal/app/page.tsx +485 -0
  58. package/dist/scaffolds/notis-journal/components/ui/badge.tsx +28 -0
  59. package/dist/scaffolds/notis-journal/components/ui/button.tsx +53 -0
  60. package/dist/scaffolds/notis-journal/components/ui/card.tsx +56 -0
  61. package/dist/scaffolds/notis-journal/components.json +20 -0
  62. package/dist/scaffolds/notis-journal/index.html +12 -0
  63. package/dist/scaffolds/notis-journal/lib/utils.ts +6 -0
  64. package/dist/scaffolds/notis-journal/metadata/screenshot-1.png +0 -0
  65. package/dist/scaffolds/notis-journal/metadata/screenshot-2.png +0 -0
  66. package/dist/scaffolds/notis-journal/metadata/screenshot-3.png +0 -0
  67. package/dist/scaffolds/notis-journal/metadata/screenshot-4.png +0 -0
  68. package/dist/scaffolds/notis-journal/metadata/screenshot-5.png +0 -0
  69. package/dist/scaffolds/notis-journal/metadata/screenshot-6.png +0 -0
  70. package/dist/scaffolds/notis-journal/metadata/screenshot-fixtures.json +132 -0
  71. package/dist/scaffolds/notis-journal/notis.config.ts +93 -0
  72. package/dist/scaffolds/notis-journal/package.json +34 -0
  73. package/dist/scaffolds/notis-journal/packages/sdk/package.json +36 -0
  74. package/dist/scaffolds/notis-journal/packages/sdk/src/components/DocumentEditor.tsx +93 -0
  75. package/dist/scaffolds/notis-journal/packages/sdk/src/components/Markdown.tsx +60 -0
  76. package/dist/scaffolds/notis-journal/packages/sdk/src/components/MultiSelectActionBar.tsx +278 -0
  77. package/dist/scaffolds/notis-journal/packages/sdk/src/components/MultiSelectCheckbox.tsx +91 -0
  78. package/dist/scaffolds/notis-journal/packages/sdk/src/components/MultiSelectDragOverlay.tsx +39 -0
  79. package/dist/scaffolds/notis-journal/packages/sdk/src/config.ts +145 -0
  80. package/dist/scaffolds/notis-journal/packages/sdk/src/documents.ts +229 -0
  81. package/dist/scaffolds/notis-journal/packages/sdk/src/hooks/useBackend.ts +41 -0
  82. package/dist/scaffolds/notis-journal/packages/sdk/src/hooks/useDatabaseSchema.ts +85 -0
  83. package/dist/scaffolds/notis-journal/packages/sdk/src/hooks/useDocument.ts +78 -0
  84. package/dist/scaffolds/notis-journal/packages/sdk/src/hooks/useDocuments.ts +121 -0
  85. package/dist/scaffolds/notis-journal/packages/sdk/src/hooks/useMultiSelect.ts +539 -0
  86. package/dist/scaffolds/notis-journal/packages/sdk/src/hooks/useNotis.ts +34 -0
  87. package/dist/scaffolds/notis-journal/packages/sdk/src/hooks/useNotisNavigation.ts +49 -0
  88. package/dist/scaffolds/notis-journal/packages/sdk/src/hooks/useTool.ts +64 -0
  89. package/dist/scaffolds/notis-journal/packages/sdk/src/hooks/useTools.ts +56 -0
  90. package/dist/scaffolds/notis-journal/packages/sdk/src/hooks/useTopBarSearch.ts +73 -0
  91. package/dist/scaffolds/notis-journal/packages/sdk/src/hooks/useUpsertDocument.ts +95 -0
  92. package/dist/scaffolds/notis-journal/packages/sdk/src/index.ts +83 -0
  93. package/dist/scaffolds/notis-journal/packages/sdk/src/provider.tsx +43 -0
  94. package/dist/scaffolds/notis-journal/packages/sdk/src/runtime.ts +220 -0
  95. package/dist/scaffolds/notis-journal/packages/sdk/src/styles.css +186 -0
  96. package/dist/scaffolds/notis-journal/packages/sdk/src/ui.ts +15 -0
  97. package/dist/scaffolds/notis-journal/packages/sdk/src/vite.ts +56 -0
  98. package/dist/scaffolds/notis-journal/packages/sdk/tsconfig.json +15 -0
  99. package/dist/scaffolds/notis-journal/postcss.config.mjs +8 -0
  100. package/dist/scaffolds/notis-journal/skills/journal-onboarding/SKILL.md +120 -0
  101. package/dist/scaffolds/notis-journal/src/dev-main.tsx +58 -0
  102. package/dist/scaffolds/notis-journal/src/mock-runtime.ts +197 -0
  103. package/dist/scaffolds/notis-journal/tailwind.config.ts +58 -0
  104. package/dist/scaffolds/notis-journal/tsconfig.json +23 -0
  105. package/dist/scaffolds/notis-journal/vite.config.ts +10 -0
  106. package/dist/scaffolds/notis-notes/CHANGELOG.md +5 -0
  107. package/dist/scaffolds/notis-notes/app/page.tsx +17 -373
  108. package/dist/scaffolds/notis-notes/notis.config.ts +0 -1
  109. package/dist/scaffolds/notis-random/CHANGELOG.md +5 -0
  110. package/dist/scaffolds/notis-random/notis.config.ts +0 -1
  111. package/dist/scaffolds/notis-random/src/mock-runtime.ts +1 -0
  112. package/dist/scaffolds.json +22 -0
  113. package/package.json +3 -3
  114. package/skills/notis-apps/SKILL.md +43 -5
  115. package/skills/notis-apps/cli.md +22 -6
  116. package/skills/notis-cli/SKILL.md +20 -2
  117. package/skills/notis-query/cli.md +1 -1
  118. package/src/cli.js +8 -1
  119. package/src/command-specs/apps.js +307 -36
  120. package/src/command-specs/diagnostics.js +674 -0
  121. package/src/command-specs/helpers.js +4 -1
  122. package/src/command-specs/index.js +7 -1
  123. package/src/command-specs/meta.js +8 -2
  124. package/src/command-specs/smoke.js +386 -0
  125. package/src/command-specs/tools.js +237 -44
  126. package/src/runtime/agent-browser.js +204 -21
  127. package/src/runtime/app-changelog.js +79 -0
  128. package/src/runtime/app-dev-server.js +21 -4
  129. package/src/runtime/app-dev-sessions.js +99 -1
  130. package/src/runtime/app-platform.js +197 -18
  131. package/src/runtime/assets/store-screenshot-dark.png +0 -0
  132. package/src/runtime/desktop-auth.js +93 -0
  133. package/src/runtime/output.js +12 -1
  134. package/src/runtime/profiles.js +72 -16
  135. package/src/runtime/store-screenshot.js +138 -0
  136. package/src/runtime/transport.js +24 -82
  137. package/template/.harness/index.html.tmpl +128 -6
  138. package/template/CHANGELOG.md +5 -0
  139. package/template/app/page.tsx +11 -41
  140. package/template/notis.config.ts +0 -1
  141. package/template/package-lock.json +4137 -0
  142. package/template/package.json +1 -0
  143. package/template/packages/sdk/package.json +4 -0
  144. package/template/packages/sdk/src/components/DocumentEditor.tsx +93 -0
  145. package/template/packages/sdk/src/components/Markdown.tsx +60 -0
  146. package/template/packages/sdk/src/components/MultiSelectActionBar.tsx +7 -2
  147. package/template/packages/sdk/src/config.ts +74 -0
  148. package/template/packages/sdk/src/documents.ts +229 -0
  149. package/template/packages/sdk/src/hooks/useDatabaseSchema.ts +85 -0
  150. package/template/packages/sdk/src/hooks/useDocument.ts +78 -0
  151. package/template/packages/sdk/src/hooks/useDocuments.ts +121 -0
  152. package/template/packages/sdk/src/hooks/useMultiSelect.ts +38 -2
  153. package/template/packages/sdk/src/hooks/useUpsertDocument.ts +54 -9
  154. package/template/packages/sdk/src/index.ts +30 -1
  155. package/template/packages/sdk/src/runtime.ts +76 -17
  156. package/template/packages/sdk/src/styles.css +148 -0
  157. /package/{template → dist/scaffolds/notis-affiliate-prospects}/packages/sdk/src/hooks/useDatabase.ts +0 -0
@@ -2,15 +2,16 @@
2
2
  * Notis apps CLI commands.
3
3
  *
4
4
  * Canonical Notis app workflow:
5
- * init -> dev -> build -> deploy
5
+ * init -> dev -> build -> deploy -> publish (after explicit approval)
6
6
  *
7
- * Supporting commands: list, link, doctor.
7
+ * Supporting commands: list, link, doctor, pull.
8
8
  */
9
9
 
10
10
  import { randomUUID } from 'node:crypto';
11
11
  import { spawn } from 'node:child_process';
12
- import { existsSync, mkdirSync, readdirSync, rmSync } from 'node:fs';
13
- import { join, relative, resolve } from 'node:path';
12
+ import { existsSync, mkdirSync, mkdtempSync, readdirSync, rmSync } from 'node:fs';
13
+ import { tmpdir } from 'node:os';
14
+ import { basename, join, relative, resolve } from 'node:path';
14
15
 
15
16
  import { EXIT_CODES, usageError } from '../runtime/errors.js';
16
17
  import { formatTable } from '../runtime/output.js';
@@ -27,8 +28,10 @@ import {
27
28
  scaffoldProject,
28
29
  loadScaffoldCatalog,
29
30
  inspectListingReadiness,
31
+ resolveListingScreenshots,
30
32
  collectArtifactFiles,
31
33
  collectSourceFiles,
34
+ resolveConfiguredAppSkills,
32
35
  directDeploy,
33
36
  pullAppSource,
34
37
  } from '../runtime/app-platform.js';
@@ -43,9 +46,12 @@ import {
43
46
  heartbeatAppDevSession,
44
47
  removeAppDevSession,
45
48
  upsertAppDevSessions,
49
+ waitForAppDevSessionMountAcknowledgements,
46
50
  } from '../runtime/app-dev-sessions.js';
47
51
  import { getAvailablePort } from '../runtime/ports.js';
48
52
  import { getCliMode } from '../runtime/cli-mode.js';
53
+ import { composeStoreScreenshot } from '../runtime/store-screenshot.js';
54
+ import { httpRequest } from '../runtime/transport.js';
49
55
  import {
50
56
  localNotisToolSlug,
51
57
  nextIdempotencyKey,
@@ -435,15 +441,23 @@ function buildManifestForDev(appConfig) {
435
441
  },
436
442
  databases: appConfig.databases || [],
437
443
  tools: appConfig.tools || [],
444
+ skills: (appConfig.skills || []).map((skill) => ({
445
+ key: skill.key,
446
+ path: String(skill.path || '').replace(/^\.\/+/, ''),
447
+ name: skill.name,
448
+ description: skill.description || null,
449
+ })),
450
+ onboarding: appConfig.onboarding || null,
438
451
  };
439
452
  }
440
453
 
441
- export function buildEnsureDevInstallArguments({ appConfig, manifest, linkedState }) {
454
+ export function buildEnsureDevInstallArguments({ appConfig, manifest, linkedState, skills }) {
442
455
  const devSlug = buildDevInstallSlug(appConfig.name);
443
456
  const arguments_ = {
444
457
  dev_slug: devSlug,
445
458
  name: appConfig.name,
446
459
  manifest,
460
+ skills,
447
461
  };
448
462
  if (linkedState?.dev_app_id) {
449
463
  arguments_.app_id = linkedState.dev_app_id;
@@ -467,20 +481,24 @@ export async function ensureDevInstall({
467
481
  }
468
482
 
469
483
  const manifest = buildManifestForDev(appConfig);
484
+ const skills = resolveConfiguredAppSkills(appConfig, projectDir);
470
485
  let linkedState = readLinkedState(projectDir);
471
486
  if (linkedState?.app_id) {
472
- const linkedApp = await assertDirectDeployAccess(ctx.runtime, linkedState.app_id, runTool);
473
- if (!linkedState.dev_app_id && linkedApp?.manifest?.is_dev === true) {
487
+ const linkedApp = await getAccessibleApp(ctx.runtime, linkedState.app_id, runTool);
488
+ if (!linkedApp || linkedApp.manifest?.is_dev === true) {
474
489
  const { app_id: legacyDevAppId, linked_at: _linkedAt, deployed_at: _deployedAt, version: _version, ...rest } = linkedState;
490
+ const devAppId = linkedState.dev_app_id || (linkedApp ? legacyDevAppId : null);
475
491
  linkedState = {
476
492
  ...rest,
477
- dev_app_id: legacyDevAppId,
478
- dev_linked_at: linkedState.dev_linked_at || linkedState.linked_at || new Date().toISOString(),
493
+ ...(devAppId ? { dev_app_id: devAppId } : {}),
494
+ ...(devAppId ? {
495
+ dev_linked_at: linkedState.dev_linked_at || linkedState.linked_at || new Date().toISOString(),
496
+ } : {}),
479
497
  };
480
498
  writeLinkedState(projectDir, linkedState);
481
499
  }
482
500
  }
483
- const ensureArguments = buildEnsureDevInstallArguments({ appConfig, manifest, linkedState });
501
+ const ensureArguments = buildEnsureDevInstallArguments({ appConfig, manifest, linkedState, skills });
484
502
  const ensureResult = await runTool({
485
503
  runtime: ctx.runtime,
486
504
  toolName: ENSURE_DEV_APP_INSTALLATION_TOOL,
@@ -529,16 +547,47 @@ function databaseMaterializationWarnings(apps) {
529
547
  return warnings;
530
548
  }
531
549
 
532
- async function assertDirectDeployAccess(runtime, appId, runTool = runToolCommand) {
550
+ async function getAccessibleApp(runtime, appId, runTool = runToolCommand) {
533
551
  const result = await runTool({
534
552
  runtime,
535
- toolName: LIST_APPS_TOOL,
553
+ toolName: GET_APP_TOOL,
554
+ arguments_: { app_id: appId },
536
555
  });
537
- const apps = result.payload.apps || [];
538
- const app = apps.find((candidate) => (candidate.app_id || candidate.id) === appId) || null;
556
+ if (result.payload?.app) {
557
+ return {
558
+ ...result.payload.app,
559
+ apps_access: result.payload.apps_access,
560
+ };
561
+ }
562
+ const message = typeof result.payload?.message === 'string' ? result.payload.message : '';
563
+ if (result.payload?.status === 'error' && /PGRST116|0 rows|no rows|not found/i.test(message)) {
564
+ return null;
565
+ }
566
+ throw usageError(`Could not verify access to app ${appId}${message ? `: ${message}` : '.'}`);
567
+ }
568
+
569
+ export async function assertDirectDeployAccess(runtime, appId, runTool = runToolCommand) {
570
+ const app = await getAccessibleApp(runtime, appId, runTool);
539
571
  if (!app) {
540
572
  throw usageError(`Direct deploy requires access to app ${appId}.`);
541
573
  }
574
+ if (app.apps_access?.has_access !== true) {
575
+ throw usageError('Notis Apps require a PRO+ or ULTRA plan after your trial.');
576
+ }
577
+ return app;
578
+ }
579
+
580
+ export async function assertLinkTarget(runtime, appId, runTool = runToolCommand) {
581
+ const app = await getAccessibleApp(runtime, appId, runTool);
582
+ if (!app) {
583
+ throw usageError(`Cannot link to inaccessible app ${appId}.`);
584
+ }
585
+ if (app.manifest?.is_dev === true) {
586
+ throw usageError(
587
+ `Cannot link to development runtime app ${appId}. ` +
588
+ 'Link to an installed workspace app, or run `notis apps dev` without a target.',
589
+ );
590
+ }
542
591
  return app;
543
592
  }
544
593
 
@@ -714,6 +763,7 @@ async function appsDevHandler(ctx) {
714
763
  return {
715
764
  ...app,
716
765
  bundleBaseUrl,
766
+ mountNonce: randomUUID(),
717
767
  appHref: buildAppHref({
718
768
  appSlug: app.slug,
719
769
  appId: app.appId,
@@ -746,6 +796,7 @@ async function appsDevHandler(ctx) {
746
796
  apiBase,
747
797
  appId: app.appId,
748
798
  targetAppId: app.targetAppId || undefined,
799
+ mountNonce: app.mountNonce,
749
800
  devSlug: app.devSlug,
750
801
  bundleBaseUrl: app.bundleBaseUrl,
751
802
  projectDir: app.projectDir,
@@ -770,6 +821,13 @@ async function appsDevHandler(ctx) {
770
821
  }
771
822
  }, DEV_HEARTBEAT_INTERVAL_MS);
772
823
 
824
+ const expectedMountAcknowledgements = apps.map((app) => ({
825
+ sessionId,
826
+ appId: app.appId,
827
+ devSlug: app.devSlug,
828
+ mountNonce: app.mountNonce,
829
+ }));
830
+
773
831
  ctx.output.emitSuccess({
774
832
  command: ctx.spec.command_path.join(' '),
775
833
  data: {
@@ -777,6 +835,7 @@ async function appsDevHandler(ctx) {
777
835
  api_base: apiBase,
778
836
  development_url: developmentTabUrl,
779
837
  session_id: sessionId,
838
+ mount_status: 'serving',
780
839
  identity,
781
840
  apps: apps.map((app) => ({
782
841
  slug: app.devSlug,
@@ -799,6 +858,8 @@ async function appsDevHandler(ctx) {
799
858
  '',
800
859
  ...apps.map((app) => ` ${app.name.padEnd(24)} ${app.bundleBaseUrl} -> ${app.appHref}`),
801
860
  '',
861
+ `Serving locally; waiting for Notis to mount ${apps.length === 1 ? 'the app' : `${apps.length} apps`}.`,
862
+ '',
802
863
  'Press Ctrl-C to stop.',
803
864
  ].join('\n'),
804
865
  });
@@ -807,6 +868,27 @@ async function appsDevHandler(ctx) {
807
868
  openInBrowser(developmentTabUrl);
808
869
  }
809
870
 
871
+ void (async () => {
872
+ let result = await waitForAppDevSessionMountAcknowledgements(expectedMountAcknowledgements);
873
+ if (!result.mounted) {
874
+ process.stderr.write(
875
+ `[notis apps dev] Serving locally, but Notis has not acknowledged ${result.missing.length === 1 ? 'the app' : `${result.missing.length} apps`} in its Local development sidebar yet. Keep this command running and open Notis.\n`,
876
+ );
877
+ }
878
+ while (!result.mounted) {
879
+ result = await waitForAppDevSessionMountAcknowledgements(expectedMountAcknowledgements, {
880
+ timeoutMs: 60_000,
881
+ pollIntervalMs: 250,
882
+ });
883
+ }
884
+ process.stderr.write(
885
+ `[notis apps dev] Mounted in Notis: ${apps.map((app) => app.name).join(', ')}.\n`,
886
+ );
887
+ })().catch((error) => {
888
+ const message = error instanceof Error ? error.message : String(error);
889
+ process.stderr.write(`[notis apps dev] mount acknowledgement check failed: ${message}\n`);
890
+ });
891
+
810
892
  let shuttingDown = false;
811
893
  const shutdown = async (signal) => {
812
894
  if (shuttingDown) return;
@@ -1115,6 +1197,38 @@ async function appsScreenshotHandler(ctx) {
1115
1197
  const appSlug = slugify(appConfig.name || manifest.app?.name || 'app') || 'app';
1116
1198
  const baseUrl = `http://127.0.0.1:${port}/a/${appSlug}`;
1117
1199
  const browserSessionName = `notis-screenshot-${process.pid}`;
1200
+ const browserSessionNames = [];
1201
+ const configuredScreenshots = resolveListingScreenshots(projectDir, appConfig)
1202
+ .filter((screenshot) => screenshot.route)
1203
+ .filter((screenshot) => !selectedRouteSlugs || selectedRouteSlugs.includes(screenshot.route));
1204
+ const routeBySlug = new Map(routes.map((route) => [route.slug, route]));
1205
+ const captures = configuredScreenshots.length > 0
1206
+ ? configuredScreenshots.map((screenshot) => {
1207
+ const route = routeBySlug.get(screenshot.route);
1208
+ if (!route) {
1209
+ throw usageError(
1210
+ `Screenshot ${screenshot.path} references unavailable route slug "${screenshot.route}".`,
1211
+ { available_routes: routes.map((entry) => entry.slug) },
1212
+ );
1213
+ }
1214
+ return {
1215
+ route,
1216
+ scenario: screenshot.scenario,
1217
+ focus: screenshot.focus,
1218
+ theme: screenshot.theme || 'light',
1219
+ fileName: basename(screenshot.path),
1220
+ };
1221
+ })
1222
+ : routes.map((route, index) => ({
1223
+ route,
1224
+ scenario: null,
1225
+ focus: null,
1226
+ theme: 'light',
1227
+ fileName: `screenshot-${screenshotSlots.get(route.slug) || index + 1}.png`,
1228
+ }));
1229
+ const rawOutputDir = ctx.options.raw
1230
+ ? null
1231
+ : mkdtempSync(join(tmpdir(), 'notis-store-screenshots-'));
1118
1232
  let devServer = null;
1119
1233
  let browserTouched = false;
1120
1234
 
@@ -1135,20 +1249,53 @@ async function appsScreenshotHandler(ctx) {
1135
1249
 
1136
1250
  mkdirSync(outputDir, { recursive: true });
1137
1251
  const results = [];
1138
- let index = 0;
1139
- for (const route of routes) {
1140
- index += 1;
1141
- const slotIndex = screenshotSlots.get(route.slug) || index;
1142
- const fileName = `screenshot-${slotIndex}.png`;
1252
+ for (const capture of captures) {
1253
+ const { route, scenario, focus, theme, fileName } = capture;
1143
1254
  const screenshotPath = join(outputDir, fileName);
1144
- const result = await captureHarnessScreenshot({
1145
- url: `${baseUrl}/harness?route=${encodeURIComponent(route.slug)}`,
1146
- sessionName: browserSessionName,
1147
- screenshotPath,
1255
+ const browserScreenshotPath = rawOutputDir
1256
+ ? join(rawOutputDir, fileName)
1257
+ : screenshotPath;
1258
+ const scenarioParam = scenario ? `&scenario=${encodeURIComponent(scenario)}` : '';
1259
+ const themeParam = `&theme=${encodeURIComponent(theme || 'light')}`;
1260
+ // Keep scenario captures on independent pages. Chromium can otherwise
1261
+ // reuse stale compositor layers when the next screenshot changes the
1262
+ // same app route into a substantially different state.
1263
+ const captureSessionName = `${browserSessionName}-${results.length + 1}`;
1264
+ browserSessionNames.push(captureSessionName);
1265
+ let result = await captureHarnessScreenshot({
1266
+ url: `${baseUrl}/harness?route=${encodeURIComponent(route.slug)}${scenarioParam}${themeParam}`,
1267
+ sessionName: captureSessionName,
1268
+ screenshotPath: browserScreenshotPath,
1269
+ focusSelector: ctx.options.raw ? null : focus,
1148
1270
  width,
1149
1271
  height,
1150
1272
  timeoutMs: Number.parseInt(ctx.globalOptions.timeoutMs || '', 10) || 15_000,
1151
1273
  });
1274
+ let presentation = { mode: 'raw' };
1275
+ if (result.ok && rawOutputDir) {
1276
+ try {
1277
+ presentation = await composeStoreScreenshot({
1278
+ inputPath: browserScreenshotPath,
1279
+ outputPath: screenshotPath,
1280
+ width,
1281
+ height,
1282
+ accent: appConfig.accent,
1283
+ seed: appConfig.name || manifest.app?.name || appSlug,
1284
+ focused: Boolean(focus),
1285
+ theme: theme || 'light',
1286
+ });
1287
+ } catch (error) {
1288
+ result = {
1289
+ ...result,
1290
+ ok: false,
1291
+ screenshotPath: null,
1292
+ tool_error: {
1293
+ phase: 'compose',
1294
+ message: error instanceof Error ? error.message : String(error),
1295
+ },
1296
+ };
1297
+ }
1298
+ }
1152
1299
  results.push({
1153
1300
  route: route.slug,
1154
1301
  path: route.path,
@@ -1157,6 +1304,9 @@ async function appsScreenshotHandler(ctx) {
1157
1304
  errors: result.errors || [],
1158
1305
  timed_out: Boolean(result.timed_out),
1159
1306
  tool_error: result.tool_error,
1307
+ framing: result.framing || null,
1308
+ theme: theme || 'light',
1309
+ presentation,
1160
1310
  });
1161
1311
  }
1162
1312
 
@@ -1167,7 +1317,7 @@ async function appsScreenshotHandler(ctx) {
1167
1317
  // Drop stale screenshots only after a full refresh. A selected-route
1168
1318
  // capture intentionally leaves other listing screenshots untouched.
1169
1319
  if (shouldPruneStaleScreenshotFiles(selectedRouteSlugs, failed.length)) {
1170
- pruneStaleScreenshotFiles(outputDir, routes.length);
1320
+ pruneStaleScreenshotFiles(outputDir, captures.length);
1171
1321
  }
1172
1322
  if (failed.length) {
1173
1323
  warnings.push(`${failed.length}/${results.length} routes failed to capture; see results.`);
@@ -1180,6 +1330,7 @@ async function appsScreenshotHandler(ctx) {
1180
1330
  project_dir: projectDir,
1181
1331
  output_dir: outputDir,
1182
1332
  mode,
1333
+ presentation: ctx.options.raw ? 'raw' : 'framed',
1183
1334
  viewport: { width, height },
1184
1335
  summary: { total: results.length, captured: captured.length, failed: failed.length },
1185
1336
  results,
@@ -1192,11 +1343,13 @@ async function appsScreenshotHandler(ctx) {
1192
1343
  return screenshotExitCode(failed.length);
1193
1344
  } finally {
1194
1345
  if (browserTouched) {
1195
- try {
1196
- await closeAgentBrowserSession(browserSessionName);
1197
- } catch {
1198
- // Ignore browser cleanup failures.
1199
- }
1346
+ await Promise.all(browserSessionNames.map(async (sessionName) => {
1347
+ try {
1348
+ await closeAgentBrowserSession(sessionName);
1349
+ } catch {
1350
+ // Ignore browser cleanup failures.
1351
+ }
1352
+ }));
1200
1353
  }
1201
1354
  if (devServer) {
1202
1355
  try {
@@ -1205,6 +1358,9 @@ async function appsScreenshotHandler(ctx) {
1205
1358
  // Ignore server cleanup failures.
1206
1359
  }
1207
1360
  }
1361
+ if (rawOutputDir) {
1362
+ rmSync(rawOutputDir, { recursive: true, force: true });
1363
+ }
1208
1364
  }
1209
1365
  }
1210
1366
 
@@ -1212,6 +1368,8 @@ async function appsLinkHandler(ctx) {
1212
1368
  const projectDir = resolveProjectDir(ctx.args.dir || '.');
1213
1369
  const appId = ctx.args.appId;
1214
1370
 
1371
+ await assertLinkTarget(ctx.runtime, appId);
1372
+
1215
1373
  writeLinkedState(projectDir, {
1216
1374
  app_id: appId,
1217
1375
  linked_at: new Date().toISOString(),
@@ -1375,6 +1533,95 @@ async function appsDeployHandler(ctx) {
1375
1533
  });
1376
1534
  }
1377
1535
 
1536
+ function deployedAppVersion(app) {
1537
+ const value = app?.current_version ?? app?.manifest?.version;
1538
+ const parsed = Number(value);
1539
+ return Number.isFinite(parsed) ? parsed : 0;
1540
+ }
1541
+
1542
+ async function appsPublishHandler(ctx) {
1543
+ if (ctx.options.confirmReady !== true) {
1544
+ throw usageError(
1545
+ 'Store submission requires explicit user confirmation that App Details is ready. ' +
1546
+ 'After confirmation, rerun with --confirm-ready.',
1547
+ );
1548
+ }
1549
+
1550
+ const projectDir = resolveProjectDir(ctx.args.dir || '.');
1551
+ const appId = requireLinkedAppId(projectDir, ctx.options.appId);
1552
+ const linkedState = readLinkedState(projectDir);
1553
+ const appConfig = await loadAppConfig(projectDir);
1554
+ const readiness = inspectListingReadiness(projectDir, appConfig);
1555
+ if (!readiness.ready) {
1556
+ throw usageError(
1557
+ `Store listing is not ready:\n${readiness.errors.map((error) => ` - ${error}`).join('\n')}`,
1558
+ );
1559
+ }
1560
+
1561
+ const detailResult = await runToolCommand({
1562
+ runtime: ctx.runtime,
1563
+ toolName: GET_APP_TOOL,
1564
+ arguments_: { app_id: appId },
1565
+ });
1566
+ const detail = detailResult.payload || {};
1567
+ const app = detail.app || {};
1568
+ if (!app.id) {
1569
+ throw usageError(`Could not load deployed app ${appId}.`);
1570
+ }
1571
+ if (!['team', 'public_store_hidden'].includes(app.visibility)) {
1572
+ throw usageError('Set the app visibility to Team or Public before Store submission.');
1573
+ }
1574
+
1575
+ const remoteVersion = deployedAppVersion(app);
1576
+ if (remoteVersion <= 0) {
1577
+ throw usageError('App has no deployed source. Run `notis apps deploy` first.');
1578
+ }
1579
+ if (
1580
+ linkedState?.app_id !== appId
1581
+ || !Number.isFinite(linkedState?.version)
1582
+ || linkedState.version !== remoteVersion
1583
+ ) {
1584
+ throw usageError(
1585
+ `Local project is not confirmed at deployed version ${remoteVersion}. ` +
1586
+ 'Run `notis apps deploy` from this project before submitting it.',
1587
+ );
1588
+ }
1589
+
1590
+ const activeSubmission = detail.active_submission || app.active_submission || null;
1591
+ if (activeSubmission?.status === 'pending_review') {
1592
+ throw usageError(
1593
+ `A Store submission is already in review${activeSubmission.github_pr_url ? `: ${activeSubmission.github_pr_url}` : '.'}`,
1594
+ );
1595
+ }
1596
+ if (activeSubmission?.status === 'removal_pending_review') {
1597
+ throw usageError('Store removal is currently in review. Wait for it to finish before submitting an update.');
1598
+ }
1599
+
1600
+ const result = await httpRequest({
1601
+ runtime: ctx.runtime,
1602
+ method: 'POST',
1603
+ path: '/portal_apps/publish',
1604
+ body: { app_id: appId },
1605
+ });
1606
+ const submission = result.payload.submission || result.payload;
1607
+ const reviewStatus = submission.status || 'pending_review';
1608
+ return ctx.output.emitSuccess({
1609
+ command: ctx.spec.command_path.join(' '),
1610
+ data: {
1611
+ app_id: appId,
1612
+ source_version: submission.source_version || remoteVersion,
1613
+ submission,
1614
+ },
1615
+ humanSummary: reviewStatus === 'merged'
1616
+ ? `Published app ${appId} to the Store at version ${submission.source_version || remoteVersion}`
1617
+ : `Submitted app ${appId} version ${submission.source_version || remoteVersion} for Store review`,
1618
+ hints: submission.github_pr_url
1619
+ ? [{ command: submission.github_pr_url, reason: 'Review the Store registry pull request' }]
1620
+ : [],
1621
+ meta: { mutating: true, request_id: result.requestId },
1622
+ });
1623
+ }
1624
+
1378
1625
  async function appsDoctorHandler(ctx) {
1379
1626
  const projectDir = resolveProjectDir(ctx.args.dir || '.');
1380
1627
  const problems = detectProjectProblems(projectDir);
@@ -1526,10 +1773,10 @@ export const appsCommandSpecs = [
1526
1773
  },
1527
1774
  {
1528
1775
  command_path: ['apps', 'verify'],
1529
- summary: 'Headless render smoke each route in a stub-runtime harness.',
1776
+ summary: 'Validate every route and the production Store listing contract.',
1530
1777
  when_to_use:
1531
- 'After notis apps build, before deploy. Catches render-time crashes ' +
1532
- 'and missing runtime calls that the build step cannot detect.',
1778
+ 'After notis apps screenshot, before deploy. Catches render-time crashes, ' +
1779
+ 'missing runtime calls, and incomplete listing media that the build step cannot detect.',
1533
1780
  args_schema: {
1534
1781
  arguments: [
1535
1782
  { token: '[dir]', key: 'dir', description: 'Project directory (default: current dir).' },
@@ -1557,29 +1804,32 @@ export const appsCommandSpecs = [
1557
1804
  },
1558
1805
  {
1559
1806
  command_path: ['apps', 'screenshot'],
1560
- summary: 'Capture listing screenshots of each route via the headless harness.',
1807
+ summary: 'Capture configured listing route/scenario states via the headless harness.',
1561
1808
  when_to_use:
1562
- 'Generate metadata/screenshot-N.png for the App Store listing. Apps are ' +
1809
+ 'Generate the 3–6 declared metadata/screenshot-N.png files for the App Store listing. Apps are ' +
1563
1810
  'icon-led (like Raycast) — there is no cover image, only these screenshots. ' +
1811
+ 'Each screenshot may set a focus selector to remove empty canvas and a light or dark theme that also controls its Store frame. ' +
1564
1812
  'Run before notis apps verify / deploy / publish.',
1565
1813
  args_schema: {
1566
1814
  arguments: [
1567
1815
  { token: '[dir]', key: 'dir', description: 'Project directory (default: current dir).' },
1568
1816
  ],
1569
1817
  options: [
1570
- { flags: '--routes <slugs>', description: 'Comma-separated route slugs. Default: every route in manifest.' },
1818
+ { flags: '--routes <slugs>', description: 'Comma-separated route slugs. Default: every configured screenshot state.' },
1571
1819
  { flags: '--port <n>', description: 'Loopback port. Default: auto-pick.' },
1572
1820
  { flags: '--width <px>', description: 'Viewport width. Default: 2000.' },
1573
1821
  { flags: '--height <px>', description: 'Viewport height. Default: 1250 (16:10).' },
1574
1822
  { flags: '--output-dir <dir>', description: 'Where to write screenshot-N.png. Default: metadata/.' },
1575
1823
  { flags: '--mode <mode>', description: 'stub | live. Default stub. Live renders against real data via the CLI JWT (requires a linked app), so screenshots show actual content instead of empty states.' },
1824
+ { flags: '--raw', description: 'Write the unframed harness capture instead of the default Store presentation.' },
1576
1825
  { flags: '--skip-build', description: 'Skip notis apps build; reuse existing .notis/output/.' },
1577
1826
  ],
1578
1827
  },
1579
1828
  examples: [
1580
- 'notis apps screenshot',
1829
+ 'notis apps screenshot # honors notis.config.ts screenshot scenarios',
1581
1830
  'notis apps screenshot --routes home,history',
1582
1831
  'notis apps screenshot --mode live # populated screenshots from real data',
1832
+ 'notis apps screenshot --raw # diagnostic capture without Store framing',
1583
1833
  ],
1584
1834
  mutates: true,
1585
1835
  idempotent: true,
@@ -1648,6 +1898,27 @@ export const appsCommandSpecs = [
1648
1898
  backend_call: { type: 'tool', name: SAVE_APP_FILES_TOOL },
1649
1899
  handler: appsDeployHandler,
1650
1900
  },
1901
+ {
1902
+ command_path: ['apps', 'publish'],
1903
+ summary: 'Submit the deployed app for Store review.',
1904
+ when_to_use:
1905
+ 'After the user explicitly confirms the App Details page and Store listing are ready. Requires the current local project to match the latest deployed version.',
1906
+ args_schema: {
1907
+ arguments: [
1908
+ { token: '[dir]', key: 'dir', description: 'Project directory (default: current dir).' },
1909
+ ],
1910
+ options: [
1911
+ { flags: '--app-id <id>', description: 'Override linked app ID.' },
1912
+ { flags: '--confirm-ready', description: 'Confirm the user approved the current App Details page for Store submission.' },
1913
+ ],
1914
+ },
1915
+ examples: ['notis apps publish --confirm-ready', 'notis apps publish ./my-app --confirm-ready'],
1916
+ mutates: true,
1917
+ idempotent: false,
1918
+ require_auth: true,
1919
+ backend_call: { type: 'http', name: 'portal_apps/publish' },
1920
+ handler: appsPublishHandler,
1921
+ },
1651
1922
  {
1652
1923
  command_path: ['apps', 'doctor'],
1653
1924
  summary: 'Check project health and readiness.',