@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.
- package/README.md +33 -9
- package/dist/scaffolds/notis-affiliate-prospects/CHANGELOG.md +7 -0
- package/dist/scaffolds/notis-affiliate-prospects/README.md +20 -0
- package/dist/scaffolds/notis-affiliate-prospects/app/globals.css +42 -0
- package/dist/scaffolds/notis-affiliate-prospects/app/layout.tsx +7 -0
- package/dist/scaffolds/notis-affiliate-prospects/app/page.tsx +248 -0
- package/dist/scaffolds/notis-affiliate-prospects/app/prospects/page.tsx +134 -0
- package/dist/scaffolds/notis-affiliate-prospects/app/segments/page.tsx +108 -0
- package/dist/scaffolds/notis-affiliate-prospects/components/ui/card.tsx +35 -0
- package/dist/scaffolds/notis-affiliate-prospects/components.json +20 -0
- package/dist/scaffolds/notis-affiliate-prospects/index.html +12 -0
- package/dist/scaffolds/notis-affiliate-prospects/lib/affiliate-data.ts +218 -0
- package/dist/scaffolds/notis-affiliate-prospects/lib/demo-prospects.ts +52 -0
- package/dist/scaffolds/notis-affiliate-prospects/lib/notis-tools.ts +100 -0
- package/dist/scaffolds/notis-affiliate-prospects/lib/utils.ts +25 -0
- package/dist/scaffolds/notis-affiliate-prospects/metadata/screenshot-1.png +0 -0
- package/dist/scaffolds/notis-affiliate-prospects/metadata/screenshot-2.png +0 -0
- package/dist/scaffolds/notis-affiliate-prospects/metadata/screenshot-3.png +0 -0
- package/dist/scaffolds/notis-affiliate-prospects/metadata/screenshot-fixtures.json +187 -0
- package/dist/scaffolds/notis-affiliate-prospects/notis.config.ts +65 -0
- package/dist/scaffolds/notis-affiliate-prospects/package.json +32 -0
- package/dist/scaffolds/notis-affiliate-prospects/packages/sdk/package.json +32 -0
- package/dist/scaffolds/notis-affiliate-prospects/packages/sdk/src/components/MultiSelectActionBar.tsx +273 -0
- package/dist/scaffolds/notis-affiliate-prospects/packages/sdk/src/components/MultiSelectCheckbox.tsx +91 -0
- package/dist/scaffolds/notis-affiliate-prospects/packages/sdk/src/components/MultiSelectDragOverlay.tsx +39 -0
- package/dist/scaffolds/notis-affiliate-prospects/packages/sdk/src/config.ts +90 -0
- package/dist/scaffolds/notis-affiliate-prospects/packages/sdk/src/hooks/useBackend.ts +41 -0
- package/dist/scaffolds/notis-affiliate-prospects/packages/sdk/src/hooks/useMultiSelect.ts +503 -0
- package/dist/scaffolds/notis-affiliate-prospects/packages/sdk/src/hooks/useNotis.ts +34 -0
- package/dist/scaffolds/notis-affiliate-prospects/packages/sdk/src/hooks/useNotisNavigation.ts +49 -0
- package/dist/scaffolds/notis-affiliate-prospects/packages/sdk/src/hooks/useTool.ts +64 -0
- package/dist/scaffolds/notis-affiliate-prospects/packages/sdk/src/hooks/useTools.ts +56 -0
- package/dist/scaffolds/notis-affiliate-prospects/packages/sdk/src/hooks/useTopBarSearch.ts +73 -0
- package/dist/scaffolds/notis-affiliate-prospects/packages/sdk/src/hooks/useUpsertDocument.ts +50 -0
- package/dist/scaffolds/notis-affiliate-prospects/packages/sdk/src/index.ts +54 -0
- package/dist/scaffolds/notis-affiliate-prospects/packages/sdk/src/provider.tsx +43 -0
- package/dist/scaffolds/notis-affiliate-prospects/packages/sdk/src/runtime.ts +161 -0
- package/dist/scaffolds/notis-affiliate-prospects/packages/sdk/src/styles.css +38 -0
- package/dist/scaffolds/notis-affiliate-prospects/packages/sdk/src/ui.ts +15 -0
- package/dist/scaffolds/notis-affiliate-prospects/packages/sdk/src/vite.ts +56 -0
- package/dist/scaffolds/notis-affiliate-prospects/packages/sdk/tsconfig.json +15 -0
- package/dist/scaffolds/notis-affiliate-prospects/postcss.config.mjs +6 -0
- package/dist/scaffolds/notis-affiliate-prospects/src/dev-main.tsx +72 -0
- package/dist/scaffolds/notis-affiliate-prospects/src/mock-runtime.ts +242 -0
- package/dist/scaffolds/notis-affiliate-prospects/tailwind.config.ts +51 -0
- package/dist/scaffolds/notis-affiliate-prospects/tsconfig.json +23 -0
- package/dist/scaffolds/notis-affiliate-prospects/vite.config.ts +11 -0
- package/dist/scaffolds/notis-database/CHANGELOG.md +5 -0
- package/dist/scaffolds/notis-database/notis.config.ts +0 -1
- package/dist/scaffolds/notis-database/src/mock-runtime.ts +1 -0
- package/dist/scaffolds/notis-journal/CHANGELOG.md +25 -0
- package/dist/scaffolds/notis-journal/app/globals.css +37 -0
- package/dist/scaffolds/notis-journal/app/insights/page.tsx +513 -0
- package/dist/scaffolds/notis-journal/app/journal-core.tsx +362 -0
- package/dist/scaffolds/notis-journal/app/journal-ui.tsx +337 -0
- package/dist/scaffolds/notis-journal/app/layout.tsx +6 -0
- package/dist/scaffolds/notis-journal/app/page.tsx +485 -0
- package/dist/scaffolds/notis-journal/components/ui/badge.tsx +28 -0
- package/dist/scaffolds/notis-journal/components/ui/button.tsx +53 -0
- package/dist/scaffolds/notis-journal/components/ui/card.tsx +56 -0
- package/dist/scaffolds/notis-journal/components.json +20 -0
- package/dist/scaffolds/notis-journal/index.html +12 -0
- package/dist/scaffolds/notis-journal/lib/utils.ts +6 -0
- package/dist/scaffolds/notis-journal/metadata/screenshot-1.png +0 -0
- package/dist/scaffolds/notis-journal/metadata/screenshot-2.png +0 -0
- package/dist/scaffolds/notis-journal/metadata/screenshot-3.png +0 -0
- package/dist/scaffolds/notis-journal/metadata/screenshot-4.png +0 -0
- package/dist/scaffolds/notis-journal/metadata/screenshot-5.png +0 -0
- package/dist/scaffolds/notis-journal/metadata/screenshot-6.png +0 -0
- package/dist/scaffolds/notis-journal/metadata/screenshot-fixtures.json +132 -0
- package/dist/scaffolds/notis-journal/notis.config.ts +93 -0
- package/dist/scaffolds/notis-journal/package.json +34 -0
- package/dist/scaffolds/notis-journal/packages/sdk/package.json +36 -0
- package/dist/scaffolds/notis-journal/packages/sdk/src/components/DocumentEditor.tsx +93 -0
- package/dist/scaffolds/notis-journal/packages/sdk/src/components/Markdown.tsx +60 -0
- package/dist/scaffolds/notis-journal/packages/sdk/src/components/MultiSelectActionBar.tsx +278 -0
- package/dist/scaffolds/notis-journal/packages/sdk/src/components/MultiSelectCheckbox.tsx +91 -0
- package/dist/scaffolds/notis-journal/packages/sdk/src/components/MultiSelectDragOverlay.tsx +39 -0
- package/dist/scaffolds/notis-journal/packages/sdk/src/config.ts +145 -0
- package/dist/scaffolds/notis-journal/packages/sdk/src/documents.ts +229 -0
- package/dist/scaffolds/notis-journal/packages/sdk/src/hooks/useBackend.ts +41 -0
- package/dist/scaffolds/notis-journal/packages/sdk/src/hooks/useDatabaseSchema.ts +85 -0
- package/dist/scaffolds/notis-journal/packages/sdk/src/hooks/useDocument.ts +78 -0
- package/dist/scaffolds/notis-journal/packages/sdk/src/hooks/useDocuments.ts +121 -0
- package/dist/scaffolds/notis-journal/packages/sdk/src/hooks/useMultiSelect.ts +539 -0
- package/dist/scaffolds/notis-journal/packages/sdk/src/hooks/useNotis.ts +34 -0
- package/dist/scaffolds/notis-journal/packages/sdk/src/hooks/useNotisNavigation.ts +49 -0
- package/dist/scaffolds/notis-journal/packages/sdk/src/hooks/useTool.ts +64 -0
- package/dist/scaffolds/notis-journal/packages/sdk/src/hooks/useTools.ts +56 -0
- package/dist/scaffolds/notis-journal/packages/sdk/src/hooks/useTopBarSearch.ts +73 -0
- package/dist/scaffolds/notis-journal/packages/sdk/src/hooks/useUpsertDocument.ts +95 -0
- package/dist/scaffolds/notis-journal/packages/sdk/src/index.ts +83 -0
- package/dist/scaffolds/notis-journal/packages/sdk/src/provider.tsx +43 -0
- package/dist/scaffolds/notis-journal/packages/sdk/src/runtime.ts +220 -0
- package/dist/scaffolds/notis-journal/packages/sdk/src/styles.css +186 -0
- package/dist/scaffolds/notis-journal/packages/sdk/src/ui.ts +15 -0
- package/dist/scaffolds/notis-journal/packages/sdk/src/vite.ts +56 -0
- package/dist/scaffolds/notis-journal/packages/sdk/tsconfig.json +15 -0
- package/dist/scaffolds/notis-journal/postcss.config.mjs +8 -0
- package/dist/scaffolds/notis-journal/skills/journal-onboarding/SKILL.md +120 -0
- package/dist/scaffolds/notis-journal/src/dev-main.tsx +58 -0
- package/dist/scaffolds/notis-journal/src/mock-runtime.ts +197 -0
- package/dist/scaffolds/notis-journal/tailwind.config.ts +58 -0
- package/dist/scaffolds/notis-journal/tsconfig.json +23 -0
- package/dist/scaffolds/notis-journal/vite.config.ts +10 -0
- package/dist/scaffolds/notis-notes/CHANGELOG.md +5 -0
- package/dist/scaffolds/notis-notes/app/page.tsx +17 -373
- package/dist/scaffolds/notis-notes/notis.config.ts +0 -1
- package/dist/scaffolds/notis-random/CHANGELOG.md +5 -0
- package/dist/scaffolds/notis-random/notis.config.ts +0 -1
- package/dist/scaffolds/notis-random/src/mock-runtime.ts +1 -0
- package/dist/scaffolds.json +22 -0
- package/package.json +3 -3
- package/skills/notis-apps/SKILL.md +43 -5
- package/skills/notis-apps/cli.md +22 -6
- package/skills/notis-cli/SKILL.md +20 -2
- package/skills/notis-query/cli.md +1 -1
- package/src/cli.js +8 -1
- package/src/command-specs/apps.js +307 -36
- package/src/command-specs/diagnostics.js +674 -0
- package/src/command-specs/helpers.js +4 -1
- package/src/command-specs/index.js +7 -1
- package/src/command-specs/meta.js +8 -2
- package/src/command-specs/smoke.js +386 -0
- package/src/command-specs/tools.js +237 -44
- package/src/runtime/agent-browser.js +204 -21
- package/src/runtime/app-changelog.js +79 -0
- package/src/runtime/app-dev-server.js +21 -4
- package/src/runtime/app-dev-sessions.js +99 -1
- package/src/runtime/app-platform.js +197 -18
- package/src/runtime/assets/store-screenshot-dark.png +0 -0
- package/src/runtime/desktop-auth.js +93 -0
- package/src/runtime/output.js +12 -1
- package/src/runtime/profiles.js +72 -16
- package/src/runtime/store-screenshot.js +138 -0
- package/src/runtime/transport.js +24 -82
- package/template/.harness/index.html.tmpl +128 -6
- package/template/CHANGELOG.md +5 -0
- package/template/app/page.tsx +11 -41
- package/template/notis.config.ts +0 -1
- package/template/package-lock.json +4137 -0
- package/template/package.json +1 -0
- package/template/packages/sdk/package.json +4 -0
- package/template/packages/sdk/src/components/DocumentEditor.tsx +93 -0
- package/template/packages/sdk/src/components/Markdown.tsx +60 -0
- package/template/packages/sdk/src/components/MultiSelectActionBar.tsx +7 -2
- package/template/packages/sdk/src/config.ts +74 -0
- package/template/packages/sdk/src/documents.ts +229 -0
- package/template/packages/sdk/src/hooks/useDatabaseSchema.ts +85 -0
- package/template/packages/sdk/src/hooks/useDocument.ts +78 -0
- package/template/packages/sdk/src/hooks/useDocuments.ts +121 -0
- package/template/packages/sdk/src/hooks/useMultiSelect.ts +38 -2
- package/template/packages/sdk/src/hooks/useUpsertDocument.ts +54 -9
- package/template/packages/sdk/src/index.ts +30 -1
- package/template/packages/sdk/src/runtime.ts +76 -17
- package/template/packages/sdk/src/styles.css +148 -0
- /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 {
|
|
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
|
|
473
|
-
if (!
|
|
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:
|
|
478
|
-
|
|
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
|
|
550
|
+
async function getAccessibleApp(runtime, appId, runTool = runToolCommand) {
|
|
533
551
|
const result = await runTool({
|
|
534
552
|
runtime,
|
|
535
|
-
toolName:
|
|
553
|
+
toolName: GET_APP_TOOL,
|
|
554
|
+
arguments_: { app_id: appId },
|
|
536
555
|
});
|
|
537
|
-
|
|
538
|
-
|
|
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
|
-
|
|
1139
|
-
|
|
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
|
|
1145
|
-
|
|
1146
|
-
|
|
1147
|
-
|
|
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,
|
|
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
|
-
|
|
1196
|
-
|
|
1197
|
-
|
|
1198
|
-
|
|
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: '
|
|
1776
|
+
summary: 'Validate every route and the production Store listing contract.',
|
|
1530
1777
|
when_to_use:
|
|
1531
|
-
'After notis apps
|
|
1532
|
-
'
|
|
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
|
|
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
|
|
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.',
|