@notis_ai/cli 0.2.4 → 0.2.6
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 +47 -5
- package/dist/scaffolds/notis-database/CHANGELOG.md +5 -0
- package/dist/scaffolds/notis-database/metadata/screenshot-1.png +0 -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 +325 -543
- package/dist/scaffolds/notis-notes/metadata/screenshot-1.png +0 -0
- package/dist/scaffolds/notis-notes/notis.config.ts +0 -1
- package/dist/scaffolds/notis-random/CHANGELOG.md +5 -0
- package/dist/scaffolds/notis-random/metadata/screenshot-1.png +0 -0
- package/dist/scaffolds/notis-random/metadata/screenshot-2.png +0 -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 +11 -0
- package/package.json +3 -3
- package/skills/notis-apps/SKILL.md +64 -6
- package/skills/notis-apps/cli.md +38 -2
- package/skills/notis-cli/SKILL.md +20 -2
- package/skills/notis-query/cli.md +1 -1
- package/src/command-specs/apps.js +529 -16
- package/src/command-specs/index.js +1 -1
- package/src/command-specs/meta.js +8 -2
- package/src/command-specs/tools.js +50 -37
- package/src/runtime/agent-browser.js +273 -1
- package/src/runtime/app-changelog.js +79 -0
- package/src/runtime/app-dev-server.js +113 -6
- package/src/runtime/app-dev-sessions.js +176 -12
- package/src/runtime/app-platform.js +200 -39
- package/src/runtime/assets/store-screenshot-dark.png +0 -0
- package/src/runtime/desktop-auth.js +93 -0
- package/src/runtime/profiles.js +37 -15
- package/src/runtime/store-screenshot.js +138 -0
- package/src/runtime/transport.js +21 -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/dist/scaffolds/notis-database/metadata/cover.png +0 -0
- package/dist/scaffolds/notis-database/metadata/screenshot-2.png +0 -0
- package/dist/scaffolds/notis-database/metadata/screenshot-3.png +0 -0
- package/dist/scaffolds/notis-notes/metadata/cover.png +0 -0
- package/dist/scaffolds/notis-notes/metadata/screenshot-2.png +0 -0
- package/dist/scaffolds/notis-notes/metadata/screenshot-3.png +0 -0
- package/dist/scaffolds/notis-random/metadata/cover.png +0 -0
- package/dist/scaffolds/notis-random/metadata/screenshot-3.png +0 -0
- package/template/metadata/cover.png +0 -0
- package/template/packages/sdk/src/hooks/useDatabase.ts +0 -76
|
@@ -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, readdirSync } 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,13 +28,16 @@ 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';
|
|
35
38
|
import { startAppDevServer } from '../runtime/app-dev-server.js';
|
|
36
39
|
import {
|
|
40
|
+
captureHarnessScreenshot,
|
|
37
41
|
closeAgentBrowserSession,
|
|
38
42
|
isAgentBrowserAvailable,
|
|
39
43
|
runHarnessRoute,
|
|
@@ -42,9 +46,12 @@ import {
|
|
|
42
46
|
heartbeatAppDevSession,
|
|
43
47
|
removeAppDevSession,
|
|
44
48
|
upsertAppDevSessions,
|
|
49
|
+
waitForAppDevSessionMountAcknowledgements,
|
|
45
50
|
} from '../runtime/app-dev-sessions.js';
|
|
46
51
|
import { getAvailablePort } from '../runtime/ports.js';
|
|
47
52
|
import { getCliMode } from '../runtime/cli-mode.js';
|
|
53
|
+
import { composeStoreScreenshot } from '../runtime/store-screenshot.js';
|
|
54
|
+
import { httpRequest } from '../runtime/transport.js';
|
|
48
55
|
import {
|
|
49
56
|
localNotisToolSlug,
|
|
50
57
|
nextIdempotencyKey,
|
|
@@ -236,6 +243,15 @@ function parsePort(value) {
|
|
|
236
243
|
return port;
|
|
237
244
|
}
|
|
238
245
|
|
|
246
|
+
function parsePositiveInt(value) {
|
|
247
|
+
if (!value) return null;
|
|
248
|
+
const parsed = Number.parseInt(value, 10);
|
|
249
|
+
if (!Number.isInteger(parsed) || parsed <= 0) {
|
|
250
|
+
throw usageError('Expected a positive integer.');
|
|
251
|
+
}
|
|
252
|
+
return parsed;
|
|
253
|
+
}
|
|
254
|
+
|
|
239
255
|
function parseRouteSlugs(value) {
|
|
240
256
|
if (!value) return null;
|
|
241
257
|
const slugs = String(value)
|
|
@@ -277,6 +293,36 @@ function routeSelection(manifest, rawRouteSlugs) {
|
|
|
277
293
|
return selected;
|
|
278
294
|
}
|
|
279
295
|
|
|
296
|
+
export function pruneStaleScreenshotFiles(outputDir, keepCount) {
|
|
297
|
+
for (const entry of readdirSync(outputDir, { withFileTypes: true })) {
|
|
298
|
+
if (!entry.isFile()) continue;
|
|
299
|
+
const match = /^screenshot-(\d+)\.png$/i.exec(entry.name);
|
|
300
|
+
if (match && Number.parseInt(match[1], 10) > keepCount) {
|
|
301
|
+
rmSync(join(outputDir, entry.name), { force: true });
|
|
302
|
+
}
|
|
303
|
+
}
|
|
304
|
+
}
|
|
305
|
+
|
|
306
|
+
export function shouldPruneStaleScreenshotFiles(selectedRouteSlugs, failedCount) {
|
|
307
|
+
return failedCount === 0 && !selectedRouteSlugs;
|
|
308
|
+
}
|
|
309
|
+
|
|
310
|
+
export function screenshotIndexByRouteSlug(manifest) {
|
|
311
|
+
const routes = Array.isArray(manifest?.routes) ? manifest.routes : [];
|
|
312
|
+
return new Map(routes.map((route, index) => [route.slug, index + 1]));
|
|
313
|
+
}
|
|
314
|
+
|
|
315
|
+
export function appRowFieldsFromManifest(manifest) {
|
|
316
|
+
const app = manifest?.app && typeof manifest.app === 'object' ? manifest.app : {};
|
|
317
|
+
return {
|
|
318
|
+
accent: app.accent ?? null,
|
|
319
|
+
};
|
|
320
|
+
}
|
|
321
|
+
|
|
322
|
+
export function screenshotExitCode(failedCount) {
|
|
323
|
+
return failedCount === 0 ? EXIT_CODES.ok : EXIT_CODES.unexpected;
|
|
324
|
+
}
|
|
325
|
+
|
|
280
326
|
function declaredDatabaseSlugs(appConfig, manifest, route) {
|
|
281
327
|
const slugs = new Set();
|
|
282
328
|
for (const entry of appConfig?.databases || manifest?.databases || []) {
|
|
@@ -395,15 +441,23 @@ function buildManifestForDev(appConfig) {
|
|
|
395
441
|
},
|
|
396
442
|
databases: appConfig.databases || [],
|
|
397
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,
|
|
398
451
|
};
|
|
399
452
|
}
|
|
400
453
|
|
|
401
|
-
export function buildEnsureDevInstallArguments({ appConfig, manifest, linkedState }) {
|
|
454
|
+
export function buildEnsureDevInstallArguments({ appConfig, manifest, linkedState, skills }) {
|
|
402
455
|
const devSlug = buildDevInstallSlug(appConfig.name);
|
|
403
456
|
const arguments_ = {
|
|
404
457
|
dev_slug: devSlug,
|
|
405
458
|
name: appConfig.name,
|
|
406
459
|
manifest,
|
|
460
|
+
skills,
|
|
407
461
|
};
|
|
408
462
|
if (linkedState?.dev_app_id) {
|
|
409
463
|
arguments_.app_id = linkedState.dev_app_id;
|
|
@@ -427,8 +481,24 @@ export async function ensureDevInstall({
|
|
|
427
481
|
}
|
|
428
482
|
|
|
429
483
|
const manifest = buildManifestForDev(appConfig);
|
|
430
|
-
const
|
|
431
|
-
|
|
484
|
+
const skills = resolveConfiguredAppSkills(appConfig, projectDir);
|
|
485
|
+
let linkedState = readLinkedState(projectDir);
|
|
486
|
+
if (linkedState?.app_id) {
|
|
487
|
+
const linkedApp = await getAccessibleApp(ctx.runtime, linkedState.app_id, runTool);
|
|
488
|
+
if (!linkedApp || linkedApp.manifest?.is_dev === true) {
|
|
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);
|
|
491
|
+
linkedState = {
|
|
492
|
+
...rest,
|
|
493
|
+
...(devAppId ? { dev_app_id: devAppId } : {}),
|
|
494
|
+
...(devAppId ? {
|
|
495
|
+
dev_linked_at: linkedState.dev_linked_at || linkedState.linked_at || new Date().toISOString(),
|
|
496
|
+
} : {}),
|
|
497
|
+
};
|
|
498
|
+
writeLinkedState(projectDir, linkedState);
|
|
499
|
+
}
|
|
500
|
+
}
|
|
501
|
+
const ensureArguments = buildEnsureDevInstallArguments({ appConfig, manifest, linkedState, skills });
|
|
432
502
|
const ensureResult = await runTool({
|
|
433
503
|
runtime: ctx.runtime,
|
|
434
504
|
toolName: ENSURE_DEV_APP_INSTALLATION_TOOL,
|
|
@@ -440,6 +510,14 @@ export async function ensureDevInstall({
|
|
|
440
510
|
if (!appId) {
|
|
441
511
|
throw usageError('Dev installation tool did not return an app_id.');
|
|
442
512
|
}
|
|
513
|
+
if (linkedState?.dev_app_id !== appId) {
|
|
514
|
+
const now = new Date().toISOString();
|
|
515
|
+
writeLinkedState(projectDir, {
|
|
516
|
+
...(linkedState || {}),
|
|
517
|
+
dev_app_id: appId,
|
|
518
|
+
dev_linked_at: linkedState?.dev_linked_at || now,
|
|
519
|
+
});
|
|
520
|
+
}
|
|
443
521
|
return {
|
|
444
522
|
slug: ensureResult.payload.slug || devSlug,
|
|
445
523
|
devSlug,
|
|
@@ -449,6 +527,7 @@ export async function ensureDevInstall({
|
|
|
449
527
|
manifest,
|
|
450
528
|
created: ensureResult.payload.created || false,
|
|
451
529
|
linkedAppId: linkedState?.app_id || null,
|
|
530
|
+
targetAppId: linkedState?.app_id || null,
|
|
452
531
|
databaseMaterialization: ensureResult.payload.database_materialization || { created: [], unresolved: [] },
|
|
453
532
|
};
|
|
454
533
|
}
|
|
@@ -468,16 +547,48 @@ function databaseMaterializationWarnings(apps) {
|
|
|
468
547
|
return warnings;
|
|
469
548
|
}
|
|
470
549
|
|
|
471
|
-
async function
|
|
472
|
-
const result = await
|
|
550
|
+
async function getAccessibleApp(runtime, appId, runTool = runToolCommand) {
|
|
551
|
+
const result = await runTool({
|
|
473
552
|
runtime,
|
|
474
|
-
toolName:
|
|
553
|
+
toolName: GET_APP_TOOL,
|
|
554
|
+
arguments_: { app_id: appId },
|
|
475
555
|
});
|
|
476
|
-
|
|
477
|
-
|
|
478
|
-
|
|
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);
|
|
571
|
+
if (!app) {
|
|
479
572
|
throw usageError(`Direct deploy requires access to app ${appId}.`);
|
|
480
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
|
+
}
|
|
591
|
+
return app;
|
|
481
592
|
}
|
|
482
593
|
|
|
483
594
|
// ---------------------------------------------------------------------------
|
|
@@ -540,6 +651,7 @@ async function appsCreateHandler(ctx) {
|
|
|
540
651
|
name: ctx.args.name,
|
|
541
652
|
description: appConfig?.description || undefined,
|
|
542
653
|
icon: appConfig?.icon || undefined,
|
|
654
|
+
accent: appConfig?.accent ?? undefined,
|
|
543
655
|
},
|
|
544
656
|
mutating: true,
|
|
545
657
|
idempotencyKey,
|
|
@@ -651,6 +763,7 @@ async function appsDevHandler(ctx) {
|
|
|
651
763
|
return {
|
|
652
764
|
...app,
|
|
653
765
|
bundleBaseUrl,
|
|
766
|
+
mountNonce: randomUUID(),
|
|
654
767
|
appHref: buildAppHref({
|
|
655
768
|
appSlug: app.slug,
|
|
656
769
|
appId: app.appId,
|
|
@@ -669,6 +782,7 @@ async function appsDevHandler(ctx) {
|
|
|
669
782
|
slug: app.devSlug,
|
|
670
783
|
projectDir: app.projectDir,
|
|
671
784
|
appId: app.appId,
|
|
785
|
+
targetAppId: app.targetAppId || null,
|
|
672
786
|
userId: identity,
|
|
673
787
|
})),
|
|
674
788
|
port,
|
|
@@ -681,6 +795,8 @@ async function appsDevHandler(ctx) {
|
|
|
681
795
|
userId: identity,
|
|
682
796
|
apiBase,
|
|
683
797
|
appId: app.appId,
|
|
798
|
+
targetAppId: app.targetAppId || undefined,
|
|
799
|
+
mountNonce: app.mountNonce,
|
|
684
800
|
devSlug: app.devSlug,
|
|
685
801
|
bundleBaseUrl: app.bundleBaseUrl,
|
|
686
802
|
projectDir: app.projectDir,
|
|
@@ -705,6 +821,13 @@ async function appsDevHandler(ctx) {
|
|
|
705
821
|
}
|
|
706
822
|
}, DEV_HEARTBEAT_INTERVAL_MS);
|
|
707
823
|
|
|
824
|
+
const expectedMountAcknowledgements = apps.map((app) => ({
|
|
825
|
+
sessionId,
|
|
826
|
+
appId: app.appId,
|
|
827
|
+
devSlug: app.devSlug,
|
|
828
|
+
mountNonce: app.mountNonce,
|
|
829
|
+
}));
|
|
830
|
+
|
|
708
831
|
ctx.output.emitSuccess({
|
|
709
832
|
command: ctx.spec.command_path.join(' '),
|
|
710
833
|
data: {
|
|
@@ -712,10 +835,12 @@ async function appsDevHandler(ctx) {
|
|
|
712
835
|
api_base: apiBase,
|
|
713
836
|
development_url: developmentTabUrl,
|
|
714
837
|
session_id: sessionId,
|
|
838
|
+
mount_status: 'serving',
|
|
715
839
|
identity,
|
|
716
840
|
apps: apps.map((app) => ({
|
|
717
841
|
slug: app.devSlug,
|
|
718
842
|
app_id: app.appId,
|
|
843
|
+
target_app_id: app.targetAppId,
|
|
719
844
|
name: app.name,
|
|
720
845
|
project_dir: app.projectDir,
|
|
721
846
|
bundle_base_url: app.bundleBaseUrl,
|
|
@@ -733,6 +858,8 @@ async function appsDevHandler(ctx) {
|
|
|
733
858
|
'',
|
|
734
859
|
...apps.map((app) => ` ${app.name.padEnd(24)} ${app.bundleBaseUrl} -> ${app.appHref}`),
|
|
735
860
|
'',
|
|
861
|
+
`Serving locally; waiting for Notis to mount ${apps.length === 1 ? 'the app' : `${apps.length} apps`}.`,
|
|
862
|
+
'',
|
|
736
863
|
'Press Ctrl-C to stop.',
|
|
737
864
|
].join('\n'),
|
|
738
865
|
});
|
|
@@ -741,6 +868,27 @@ async function appsDevHandler(ctx) {
|
|
|
741
868
|
openInBrowser(developmentTabUrl);
|
|
742
869
|
}
|
|
743
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
|
+
|
|
744
892
|
let shuttingDown = false;
|
|
745
893
|
const shutdown = async (signal) => {
|
|
746
894
|
if (shuttingDown) return;
|
|
@@ -999,10 +1147,229 @@ async function appsVerifyHandler(ctx) {
|
|
|
999
1147
|
}
|
|
1000
1148
|
}
|
|
1001
1149
|
|
|
1150
|
+
async function appsScreenshotHandler(ctx) {
|
|
1151
|
+
const projectDir = resolveProjectDir(ctx.args.dir || '.');
|
|
1152
|
+
const problems = detectProjectProblems(projectDir);
|
|
1153
|
+
if (problems.length) {
|
|
1154
|
+
throw usageError(`Project has problems:\n${problems.map((p) => ` - ${p}`).join('\n')}`);
|
|
1155
|
+
}
|
|
1156
|
+
|
|
1157
|
+
if (!isAgentBrowserAvailable()) {
|
|
1158
|
+
throw usageError('agent-browser is not available on PATH. It ships with the Notis desktop app; open it once, then retry.');
|
|
1159
|
+
}
|
|
1160
|
+
|
|
1161
|
+
const mode = ctx.options.mode || 'stub';
|
|
1162
|
+
if (!['stub', 'live'].includes(mode)) {
|
|
1163
|
+
throw usageError('--mode must be either "stub" or "live".');
|
|
1164
|
+
}
|
|
1165
|
+
|
|
1166
|
+
let linkedState = null;
|
|
1167
|
+
if (mode === 'live') {
|
|
1168
|
+
if (!ctx.runtime.jwt) {
|
|
1169
|
+
throw usageError('Live mode requires CLI auth. Open the Notis desktop app, sign in, and retry.');
|
|
1170
|
+
}
|
|
1171
|
+
linkedState = readLinkedState(projectDir);
|
|
1172
|
+
if (!linkedState?.app_id) {
|
|
1173
|
+
throw usageError('Live mode requires a linked app. Run `notis apps link <app-id> .` first.');
|
|
1174
|
+
}
|
|
1175
|
+
}
|
|
1176
|
+
|
|
1177
|
+
if (!ctx.options.skipBuild) {
|
|
1178
|
+
await buildArtifact(projectDir);
|
|
1179
|
+
}
|
|
1180
|
+
|
|
1181
|
+
const manifest = readManifest(projectDir);
|
|
1182
|
+
const appConfig = await loadAppConfig(projectDir);
|
|
1183
|
+
const selectedRouteSlugs = parseRouteSlugs(ctx.options.routes);
|
|
1184
|
+
const routes = routeSelection(manifest, selectedRouteSlugs);
|
|
1185
|
+
const screenshotSlots = screenshotIndexByRouteSlug(manifest);
|
|
1186
|
+
if (!routes.length) {
|
|
1187
|
+
throw usageError('No routes to screenshot.');
|
|
1188
|
+
}
|
|
1189
|
+
|
|
1190
|
+
const width = parsePositiveInt(ctx.options.width) || 2000;
|
|
1191
|
+
const height = parsePositiveInt(ctx.options.height) || 1250;
|
|
1192
|
+
const outputDir = ctx.options.outputDir
|
|
1193
|
+
? resolveProjectDir(ctx.options.outputDir)
|
|
1194
|
+
: join(projectDir, 'metadata');
|
|
1195
|
+
|
|
1196
|
+
const port = parsePort(ctx.options.port) || await getAvailablePort();
|
|
1197
|
+
const appSlug = slugify(appConfig.name || manifest.app?.name || 'app') || 'app';
|
|
1198
|
+
const baseUrl = `http://127.0.0.1:${port}/a/${appSlug}`;
|
|
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-'));
|
|
1232
|
+
let devServer = null;
|
|
1233
|
+
let browserTouched = false;
|
|
1234
|
+
|
|
1235
|
+
try {
|
|
1236
|
+
devServer = await startAppDevServer({
|
|
1237
|
+
apps: [{ slug: appSlug, projectDir, appId: linkedState?.app_id || 'harness-app' }],
|
|
1238
|
+
port,
|
|
1239
|
+
watch: false,
|
|
1240
|
+
harness: {
|
|
1241
|
+
mode,
|
|
1242
|
+
apiBase: ctx.runtime.apiBase,
|
|
1243
|
+
jwt: mode === 'live' ? ctx.runtime.jwt : null,
|
|
1244
|
+
},
|
|
1245
|
+
log: () => {},
|
|
1246
|
+
logError: (message) => process.stderr.write(`${message}\n`),
|
|
1247
|
+
});
|
|
1248
|
+
browserTouched = true;
|
|
1249
|
+
|
|
1250
|
+
mkdirSync(outputDir, { recursive: true });
|
|
1251
|
+
const results = [];
|
|
1252
|
+
for (const capture of captures) {
|
|
1253
|
+
const { route, scenario, focus, theme, fileName } = capture;
|
|
1254
|
+
const screenshotPath = join(outputDir, fileName);
|
|
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,
|
|
1270
|
+
width,
|
|
1271
|
+
height,
|
|
1272
|
+
timeoutMs: Number.parseInt(ctx.globalOptions.timeoutMs || '', 10) || 15_000,
|
|
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
|
+
}
|
|
1299
|
+
results.push({
|
|
1300
|
+
route: route.slug,
|
|
1301
|
+
path: route.path,
|
|
1302
|
+
file: relative(projectDir, screenshotPath),
|
|
1303
|
+
ok: result.ok,
|
|
1304
|
+
errors: result.errors || [],
|
|
1305
|
+
timed_out: Boolean(result.timed_out),
|
|
1306
|
+
tool_error: result.tool_error,
|
|
1307
|
+
framing: result.framing || null,
|
|
1308
|
+
theme: theme || 'light',
|
|
1309
|
+
presentation,
|
|
1310
|
+
});
|
|
1311
|
+
}
|
|
1312
|
+
|
|
1313
|
+
const captured = results.filter((r) => r.ok);
|
|
1314
|
+
const failed = results.filter((r) => !r.ok);
|
|
1315
|
+
const warnings = [];
|
|
1316
|
+
|
|
1317
|
+
// Drop stale screenshots only after a full refresh. A selected-route
|
|
1318
|
+
// capture intentionally leaves other listing screenshots untouched.
|
|
1319
|
+
if (shouldPruneStaleScreenshotFiles(selectedRouteSlugs, failed.length)) {
|
|
1320
|
+
pruneStaleScreenshotFiles(outputDir, captures.length);
|
|
1321
|
+
}
|
|
1322
|
+
if (failed.length) {
|
|
1323
|
+
warnings.push(`${failed.length}/${results.length} routes failed to capture; see results.`);
|
|
1324
|
+
}
|
|
1325
|
+
|
|
1326
|
+
ctx.output.emitSuccess({
|
|
1327
|
+
ok: failed.length === 0,
|
|
1328
|
+
command: ctx.spec.command_path.join(' '),
|
|
1329
|
+
data: {
|
|
1330
|
+
project_dir: projectDir,
|
|
1331
|
+
output_dir: outputDir,
|
|
1332
|
+
mode,
|
|
1333
|
+
presentation: ctx.options.raw ? 'raw' : 'framed',
|
|
1334
|
+
viewport: { width, height },
|
|
1335
|
+
summary: { total: results.length, captured: captured.length, failed: failed.length },
|
|
1336
|
+
results,
|
|
1337
|
+
},
|
|
1338
|
+
humanSummary: failed.length === 0
|
|
1339
|
+
? `Captured ${captured.length} screenshot(s) to ${relative(projectDir, outputDir) || 'metadata'}/.`
|
|
1340
|
+
: `Captured ${captured.length}/${results.length} screenshots; ${failed.length} failed.`,
|
|
1341
|
+
warnings,
|
|
1342
|
+
});
|
|
1343
|
+
return screenshotExitCode(failed.length);
|
|
1344
|
+
} finally {
|
|
1345
|
+
if (browserTouched) {
|
|
1346
|
+
await Promise.all(browserSessionNames.map(async (sessionName) => {
|
|
1347
|
+
try {
|
|
1348
|
+
await closeAgentBrowserSession(sessionName);
|
|
1349
|
+
} catch {
|
|
1350
|
+
// Ignore browser cleanup failures.
|
|
1351
|
+
}
|
|
1352
|
+
}));
|
|
1353
|
+
}
|
|
1354
|
+
if (devServer) {
|
|
1355
|
+
try {
|
|
1356
|
+
await devServer.close();
|
|
1357
|
+
} catch {
|
|
1358
|
+
// Ignore server cleanup failures.
|
|
1359
|
+
}
|
|
1360
|
+
}
|
|
1361
|
+
if (rawOutputDir) {
|
|
1362
|
+
rmSync(rawOutputDir, { recursive: true, force: true });
|
|
1363
|
+
}
|
|
1364
|
+
}
|
|
1365
|
+
}
|
|
1366
|
+
|
|
1002
1367
|
async function appsLinkHandler(ctx) {
|
|
1003
1368
|
const projectDir = resolveProjectDir(ctx.args.dir || '.');
|
|
1004
1369
|
const appId = ctx.args.appId;
|
|
1005
1370
|
|
|
1371
|
+
await assertLinkTarget(ctx.runtime, appId);
|
|
1372
|
+
|
|
1006
1373
|
writeLinkedState(projectDir, {
|
|
1007
1374
|
app_id: appId,
|
|
1008
1375
|
linked_at: new Date().toISOString(),
|
|
@@ -1106,6 +1473,7 @@ async function appsDeployHandler(ctx) {
|
|
|
1106
1473
|
files,
|
|
1107
1474
|
source_files: sourceFiles,
|
|
1108
1475
|
manifest,
|
|
1476
|
+
...appRowFieldsFromManifest(manifest),
|
|
1109
1477
|
...(baseVersion !== undefined ? { base_version: baseVersion } : {}),
|
|
1110
1478
|
},
|
|
1111
1479
|
mutating: true,
|
|
@@ -1165,6 +1533,95 @@ async function appsDeployHandler(ctx) {
|
|
|
1165
1533
|
});
|
|
1166
1534
|
}
|
|
1167
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
|
+
|
|
1168
1625
|
async function appsDoctorHandler(ctx) {
|
|
1169
1626
|
const projectDir = resolveProjectDir(ctx.args.dir || '.');
|
|
1170
1627
|
const problems = detectProjectProblems(projectDir);
|
|
@@ -1316,10 +1773,10 @@ export const appsCommandSpecs = [
|
|
|
1316
1773
|
},
|
|
1317
1774
|
{
|
|
1318
1775
|
command_path: ['apps', 'verify'],
|
|
1319
|
-
summary: '
|
|
1776
|
+
summary: 'Validate every route and the production Store listing contract.',
|
|
1320
1777
|
when_to_use:
|
|
1321
|
-
'After notis apps
|
|
1322
|
-
'
|
|
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.',
|
|
1323
1780
|
args_schema: {
|
|
1324
1781
|
arguments: [
|
|
1325
1782
|
{ token: '[dir]', key: 'dir', description: 'Project directory (default: current dir).' },
|
|
@@ -1345,6 +1802,41 @@ export const appsCommandSpecs = [
|
|
|
1345
1802
|
backend_call: { type: 'local', name: 'verify_harness' },
|
|
1346
1803
|
handler: appsVerifyHandler,
|
|
1347
1804
|
},
|
|
1805
|
+
{
|
|
1806
|
+
command_path: ['apps', 'screenshot'],
|
|
1807
|
+
summary: 'Capture configured listing route/scenario states via the headless harness.',
|
|
1808
|
+
when_to_use:
|
|
1809
|
+
'Generate the 3–6 declared metadata/screenshot-N.png files for the App Store listing. Apps are ' +
|
|
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. ' +
|
|
1812
|
+
'Run before notis apps verify / deploy / publish.',
|
|
1813
|
+
args_schema: {
|
|
1814
|
+
arguments: [
|
|
1815
|
+
{ token: '[dir]', key: 'dir', description: 'Project directory (default: current dir).' },
|
|
1816
|
+
],
|
|
1817
|
+
options: [
|
|
1818
|
+
{ flags: '--routes <slugs>', description: 'Comma-separated route slugs. Default: every configured screenshot state.' },
|
|
1819
|
+
{ flags: '--port <n>', description: 'Loopback port. Default: auto-pick.' },
|
|
1820
|
+
{ flags: '--width <px>', description: 'Viewport width. Default: 2000.' },
|
|
1821
|
+
{ flags: '--height <px>', description: 'Viewport height. Default: 1250 (16:10).' },
|
|
1822
|
+
{ flags: '--output-dir <dir>', description: 'Where to write screenshot-N.png. Default: metadata/.' },
|
|
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.' },
|
|
1825
|
+
{ flags: '--skip-build', description: 'Skip notis apps build; reuse existing .notis/output/.' },
|
|
1826
|
+
],
|
|
1827
|
+
},
|
|
1828
|
+
examples: [
|
|
1829
|
+
'notis apps screenshot # honors notis.config.ts screenshot scenarios',
|
|
1830
|
+
'notis apps screenshot --routes home,history',
|
|
1831
|
+
'notis apps screenshot --mode live # populated screenshots from real data',
|
|
1832
|
+
'notis apps screenshot --raw # diagnostic capture without Store framing',
|
|
1833
|
+
],
|
|
1834
|
+
mutates: true,
|
|
1835
|
+
idempotent: true,
|
|
1836
|
+
require_auth: false,
|
|
1837
|
+
backend_call: { type: 'local', name: 'screenshot_routes' },
|
|
1838
|
+
handler: appsScreenshotHandler,
|
|
1839
|
+
},
|
|
1348
1840
|
{
|
|
1349
1841
|
command_path: ['apps', 'link'],
|
|
1350
1842
|
summary: 'Link a local project to a remote Notis app.',
|
|
@@ -1406,6 +1898,27 @@ export const appsCommandSpecs = [
|
|
|
1406
1898
|
backend_call: { type: 'tool', name: SAVE_APP_FILES_TOOL },
|
|
1407
1899
|
handler: appsDeployHandler,
|
|
1408
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
|
+
},
|
|
1409
1922
|
{
|
|
1410
1923
|
command_path: ['apps', 'doctor'],
|
|
1411
1924
|
summary: 'Check project health and readiness.',
|