@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
|
@@ -3,7 +3,7 @@ import { toolsCommandSpecs } from './tools.js';
|
|
|
3
3
|
import { metaCommandSpecs } from './meta.js';
|
|
4
4
|
|
|
5
5
|
export const GROUP_SUMMARIES = {
|
|
6
|
-
apps: 'Develop,
|
|
6
|
+
apps: 'Develop, deploy, and submit Notis Apps.',
|
|
7
7
|
tools: 'Discover and execute generic tools exposed through Notis.',
|
|
8
8
|
};
|
|
9
9
|
|
|
@@ -1,5 +1,7 @@
|
|
|
1
1
|
import { COMPOSIO_SEARCH_TOOLS, healthCheck, probeAuth } from './helpers.js';
|
|
2
2
|
import { findCommandSpec, formatDescribe } from '../runtime/help.js';
|
|
3
|
+
import { createExpiredAuthError } from '../runtime/desktop-auth.js';
|
|
4
|
+
import { isJwtExpired } from '../runtime/profiles.js';
|
|
3
5
|
|
|
4
6
|
async function doctorHandler(ctx) {
|
|
5
7
|
const checks = {
|
|
@@ -9,7 +11,9 @@ async function doctorHandler(ctx) {
|
|
|
9
11
|
tool_roundtrip: 'unknown',
|
|
10
12
|
};
|
|
11
13
|
|
|
12
|
-
checks.auth = ctx.runtime.jwt
|
|
14
|
+
checks.auth = ctx.runtime.jwt
|
|
15
|
+
? (isJwtExpired(ctx.runtime.jwt) ? 'expired' : 'configured')
|
|
16
|
+
: 'missing';
|
|
13
17
|
|
|
14
18
|
try {
|
|
15
19
|
await healthCheck(ctx.runtime);
|
|
@@ -30,8 +34,10 @@ async function doctorHandler(ctx) {
|
|
|
30
34
|
const hints = [];
|
|
31
35
|
if (checks.auth === 'missing') {
|
|
32
36
|
hints.push({ command: 'Open the Notis desktop app and sign in', reason: 'Configure CLI credentials' });
|
|
37
|
+
} else if (checks.auth === 'expired') {
|
|
38
|
+
hints.push(...createExpiredAuthError(ctx.runtime).hints);
|
|
33
39
|
}
|
|
34
|
-
if (checks.health === 'error') {
|
|
40
|
+
if (checks.health === 'error' && checks.auth !== 'expired') {
|
|
35
41
|
hints.push({ command: 'Open the Notis desktop app and sign in again', reason: 'Refresh local CLI configuration' });
|
|
36
42
|
}
|
|
37
43
|
if (checks.tool_roundtrip === 'error') {
|
|
@@ -342,43 +342,48 @@ async function toolsExecParallelHandler(ctx) {
|
|
|
342
342
|
}
|
|
343
343
|
|
|
344
344
|
async function toolsLinkHandler(ctx) {
|
|
345
|
-
const
|
|
346
|
-
|
|
347
|
-
|
|
348
|
-
|
|
349
|
-
|
|
350
|
-
|
|
351
|
-
|
|
352
|
-
|
|
353
|
-
|
|
354
|
-
|
|
355
|
-
|
|
356
|
-
}
|
|
357
|
-
|
|
358
|
-
|
|
359
|
-
|
|
360
|
-
|
|
345
|
+
const credentials = ctx.options.credentials
|
|
346
|
+
? await resolveJsonInput(ctx.options.credentials, 'credentials')
|
|
347
|
+
: undefined;
|
|
348
|
+
if (credentials !== undefined && (typeof credentials !== 'object' || credentials === null || Array.isArray(credentials))) {
|
|
349
|
+
throw usageError('credentials must be a JSON object');
|
|
350
|
+
}
|
|
351
|
+
|
|
352
|
+
const arguments_ = {
|
|
353
|
+
toolkit: ctx.args.toolkit,
|
|
354
|
+
...(ctx.options.reconnect ? { reconnect: true } : {}),
|
|
355
|
+
...(ctx.options.connectionId ? { reconnect_connection_id: ctx.options.connectionId } : {}),
|
|
356
|
+
...(ctx.options.label ? { label: ctx.options.label } : {}),
|
|
357
|
+
...(credentials ? { credentials } : {}),
|
|
358
|
+
};
|
|
359
|
+
const idempotencyKey = nextIdempotencyKey(ctx.globalOptions);
|
|
360
|
+
const result = await runToolCommand({
|
|
361
|
+
runtime: ctx.runtime,
|
|
362
|
+
toolName: 'LOCAL_NOTIS_AUTHENTIFY',
|
|
363
|
+
arguments_,
|
|
364
|
+
mutating: true,
|
|
365
|
+
idempotencyKey,
|
|
366
|
+
});
|
|
367
|
+
const payload = result.payload || {};
|
|
368
|
+
if (payload.status === 'error') {
|
|
369
|
+
throw usageError(payload.message || `Failed to connect ${ctx.args.toolkit}`, payload);
|
|
361
370
|
}
|
|
362
|
-
const
|
|
371
|
+
const authUrl = payload.redirect_url || payload.integrations_url || payload.url || null;
|
|
363
372
|
|
|
364
373
|
return ctx.output.emitSuccess({
|
|
365
374
|
command: ctx.spec.command_path.join(' '),
|
|
366
375
|
data: {
|
|
367
|
-
|
|
368
|
-
auth_url:
|
|
376
|
+
...payload,
|
|
377
|
+
auth_url: authUrl,
|
|
369
378
|
},
|
|
370
|
-
humanSummary:
|
|
379
|
+
humanSummary: ctx.options.reconnect
|
|
380
|
+
? `Reconnected ${ctx.args.toolkit}`
|
|
381
|
+
: `Started connection for ${ctx.args.toolkit}`,
|
|
371
382
|
hints: [
|
|
372
383
|
{ command: 'notis tools toolkits', reason: 'Verify the toolkit is connected after setup' },
|
|
373
384
|
],
|
|
374
|
-
|
|
375
|
-
|
|
376
|
-
`Connect ${ctx.args.toolkit}:`,
|
|
377
|
-
` ${portalUrl}`,
|
|
378
|
-
'',
|
|
379
|
-
'After connecting, verify with:',
|
|
380
|
-
' notis tools toolkits',
|
|
381
|
-
].join('\n'),
|
|
385
|
+
meta: { mutating: true, idempotency_key: idempotencyKey },
|
|
386
|
+
renderHuman: () => JSON.stringify({ ...payload, auth_url: authUrl }, null, 2),
|
|
382
387
|
});
|
|
383
388
|
}
|
|
384
389
|
|
|
@@ -481,19 +486,27 @@ export const toolsCommandSpecs = [
|
|
|
481
486
|
},
|
|
482
487
|
{
|
|
483
488
|
command_path: ['tools', 'link'],
|
|
484
|
-
summary: '
|
|
485
|
-
when_to_use: 'Use this when a tool requires authentication
|
|
489
|
+
summary: 'Connect or reconnect an integration toolkit.',
|
|
490
|
+
when_to_use: 'Use this when a tool requires authentication or an active connection must be replaced.',
|
|
486
491
|
args_schema: {
|
|
487
492
|
arguments: [{ token: '<toolkit>', description: 'Toolkit name to connect (e.g. github, gmail, slack).' }],
|
|
488
|
-
options: [
|
|
493
|
+
options: [
|
|
494
|
+
{ flags: '--reconnect', description: 'Replace the existing account instead of adding another connection.' },
|
|
495
|
+
{ flags: '--connection-id <id>', key: 'connectionId', description: 'Exact connection id to replace when multiple accounts exist.' },
|
|
496
|
+
{ flags: '--label <label>', description: 'Account label for a new or replacement connection.' },
|
|
497
|
+
{ flags: '--credentials <json>', description: 'Credential JSON object, @file path, or - for stdin. Prefer stdin so secrets do not enter shell history.' },
|
|
498
|
+
],
|
|
489
499
|
},
|
|
490
|
-
examples: [
|
|
491
|
-
|
|
492
|
-
|
|
493
|
-
|
|
494
|
-
|
|
500
|
+
examples: [
|
|
501
|
+
'notis tools link github',
|
|
502
|
+
'notis tools link dataforseo --reconnect --credentials - < credentials.json',
|
|
503
|
+
],
|
|
504
|
+
output_schema: 'Returns the connection result and an authentication URL when provider authorization is still required.',
|
|
505
|
+
mutates: true,
|
|
506
|
+
idempotent: false,
|
|
507
|
+
require_auth: true,
|
|
495
508
|
related_commands: ['notis tools toolkits', 'notis tools search <query>'],
|
|
496
|
-
backend_call: { type: '
|
|
509
|
+
backend_call: { type: 'tool', name: 'LOCAL_NOTIS_AUTHENTIFY' },
|
|
497
510
|
handler: toolsLinkHandler,
|
|
498
511
|
},
|
|
499
512
|
];
|
|
@@ -1,7 +1,118 @@
|
|
|
1
1
|
import { spawn, spawnSync } from 'node:child_process';
|
|
2
|
-
import { mkdirSync, writeFileSync } from 'node:fs';
|
|
2
|
+
import { mkdirSync, readFileSync, writeFileSync } from 'node:fs';
|
|
3
3
|
import { dirname } from 'node:path';
|
|
4
4
|
|
|
5
|
+
export const HIDE_HARNESS_STATUS_SCRIPT =
|
|
6
|
+
"(() => { const el = document.getElementById('harness-status'); if (el) el.style.opacity = '0'; return true; })()";
|
|
7
|
+
|
|
8
|
+
/**
|
|
9
|
+
* Prepare the mounted harness page for a listing capture: remove the debug
|
|
10
|
+
* banner and the layout the harness adds around the app (top padding for the
|
|
11
|
+
* banner, 100vh min-height) so content height can be measured and framed
|
|
12
|
+
* without artificial whitespace.
|
|
13
|
+
*/
|
|
14
|
+
const PREPARE_CAPTURE_SCRIPT =
|
|
15
|
+
"(() => { const s = document.getElementById('harness-status'); if (s) s.style.display = 'none';" +
|
|
16
|
+
" const r = document.getElementById('root'); if (r) { r.style.paddingTop = '0'; r.style.minHeight = '0'; }" +
|
|
17
|
+
" document.body.style.minHeight = '0'; document.documentElement.style.minHeight = '0'; return true; })()";
|
|
18
|
+
|
|
19
|
+
const FONTS_STATUS_SCRIPT =
|
|
20
|
+
"(() => (document.fonts ? document.fonts.status : 'loaded'))()";
|
|
21
|
+
|
|
22
|
+
const CONTENT_HEIGHT_SCRIPT =
|
|
23
|
+
'(() => { const r = document.getElementById(\'root\');' +
|
|
24
|
+
' if (!r) return Math.ceil(document.documentElement.scrollHeight);' +
|
|
25
|
+
' return Math.ceil(Math.max(r.scrollHeight, r.getBoundingClientRect().height)); })()';
|
|
26
|
+
|
|
27
|
+
// CSS viewport bounds for content-aware framing. Content shorter than the
|
|
28
|
+
// minimum gets a little breathing room; content taller than the maximum is
|
|
29
|
+
// cropped at the frame rather than shrunk into unreadably small text.
|
|
30
|
+
const MIN_CSS_VIEWPORT_HEIGHT = 720;
|
|
31
|
+
const MAX_CSS_VIEWPORT_HEIGHT = 1500;
|
|
32
|
+
|
|
33
|
+
function pngDimensions(path) {
|
|
34
|
+
try {
|
|
35
|
+
const buffer = readFileSync(path);
|
|
36
|
+
if (buffer.length < 24 || buffer.readUInt32BE(12) !== 0x49484452) {
|
|
37
|
+
return null;
|
|
38
|
+
}
|
|
39
|
+
return { width: buffer.readUInt32BE(16), height: buffer.readUInt32BE(20) };
|
|
40
|
+
} catch {
|
|
41
|
+
return null;
|
|
42
|
+
}
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
/**
|
|
46
|
+
* Pick a CSS viewport that frames `contentHeight` at the output aspect ratio,
|
|
47
|
+
* with a deviceScaleFactor that maps it back to exactly outputWidth x
|
|
48
|
+
* outputHeight physical pixels. Width snaps to a multiple of 8 so the height
|
|
49
|
+
* stays integral for the common 16:10 target.
|
|
50
|
+
*/
|
|
51
|
+
function fitCssViewport(contentHeight, outputWidth, outputHeight) {
|
|
52
|
+
const aspect = outputWidth / outputHeight;
|
|
53
|
+
const cssH = Math.min(
|
|
54
|
+
MAX_CSS_VIEWPORT_HEIGHT,
|
|
55
|
+
Math.max(MIN_CSS_VIEWPORT_HEIGHT, Math.round(contentHeight)),
|
|
56
|
+
);
|
|
57
|
+
const cssW = Math.round((cssH * aspect) / 8) * 8;
|
|
58
|
+
return {
|
|
59
|
+
width: cssW,
|
|
60
|
+
height: Math.round(cssW / aspect),
|
|
61
|
+
scale: outputWidth / cssW,
|
|
62
|
+
};
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
async function evalNumber(sessionName, script, fallback = null) {
|
|
66
|
+
const result = await runAgentBrowser(
|
|
67
|
+
['--session', sessionName, '--json', 'eval', script],
|
|
68
|
+
{ timeoutMs: 5000 },
|
|
69
|
+
);
|
|
70
|
+
if (result.exitCode !== 0) {
|
|
71
|
+
return fallback;
|
|
72
|
+
}
|
|
73
|
+
try {
|
|
74
|
+
const payload = parseAgentBrowserJson(result.stdout);
|
|
75
|
+
const raw = payload?.data?.result ?? payload?.result ?? payload?.data ?? null;
|
|
76
|
+
const value = typeof raw === 'string' ? JSON.parse(raw) : raw;
|
|
77
|
+
return Number.isFinite(value) ? value : fallback;
|
|
78
|
+
} catch {
|
|
79
|
+
return fallback;
|
|
80
|
+
}
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
async function waitForFonts(sessionName, timeoutMs = 3000) {
|
|
84
|
+
const deadline = Date.now() + timeoutMs;
|
|
85
|
+
while (Date.now() < deadline) {
|
|
86
|
+
const result = await runAgentBrowser(
|
|
87
|
+
['--session', sessionName, '--json', 'eval', FONTS_STATUS_SCRIPT],
|
|
88
|
+
{ timeoutMs: 5000 },
|
|
89
|
+
);
|
|
90
|
+
if (result.exitCode !== 0) {
|
|
91
|
+
return;
|
|
92
|
+
}
|
|
93
|
+
try {
|
|
94
|
+
const payload = parseAgentBrowserJson(result.stdout);
|
|
95
|
+
const raw = payload?.data?.result ?? payload?.result ?? payload?.data ?? null;
|
|
96
|
+
const status = typeof raw === 'string' && raw.startsWith('"') ? JSON.parse(raw) : raw;
|
|
97
|
+
if (status !== 'loading') {
|
|
98
|
+
return;
|
|
99
|
+
}
|
|
100
|
+
} catch {
|
|
101
|
+
return;
|
|
102
|
+
}
|
|
103
|
+
await delay(150);
|
|
104
|
+
}
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
async function setViewport(sessionName, width, height, scale = null) {
|
|
108
|
+
const args = ['--session', sessionName, 'set', 'viewport', String(width), String(height)];
|
|
109
|
+
if (scale != null) {
|
|
110
|
+
args.push(String(scale));
|
|
111
|
+
}
|
|
112
|
+
const result = await runAgentBrowser(args, { timeoutMs: 5000 });
|
|
113
|
+
return result.exitCode === 0;
|
|
114
|
+
}
|
|
115
|
+
|
|
5
116
|
function delay(ms) {
|
|
6
117
|
return new Promise((resolvePromise) => setTimeout(resolvePromise, ms));
|
|
7
118
|
}
|
|
@@ -184,6 +295,167 @@ export async function runHarnessRoute({
|
|
|
184
295
|
};
|
|
185
296
|
}
|
|
186
297
|
|
|
298
|
+
/**
|
|
299
|
+
* Open a harness route, wait for it to mount, then capture a PNG screenshot of
|
|
300
|
+
* the rendered app. Output is always exactly `width` x `height` physical
|
|
301
|
+
* pixels (default 2000x1250, the 16:10 the listing validator expects), but the
|
|
302
|
+
* CSS viewport is fitted to the rendered content height: short pages render
|
|
303
|
+
* larger (no dead whitespace below the app), tall pages get more room before
|
|
304
|
+
* cropping. The deviceScaleFactor maps the fitted CSS viewport back onto the
|
|
305
|
+
* fixed output frame. Used by `notis apps screenshot`.
|
|
306
|
+
*/
|
|
307
|
+
export async function captureHarnessScreenshot({
|
|
308
|
+
url,
|
|
309
|
+
sessionName,
|
|
310
|
+
screenshotPath,
|
|
311
|
+
focusSelector = null,
|
|
312
|
+
width = 2000,
|
|
313
|
+
height = 1250,
|
|
314
|
+
timeoutMs = 15_000,
|
|
315
|
+
}) {
|
|
316
|
+
const opened = await runAgentBrowser(['--session', sessionName, 'open', url], {
|
|
317
|
+
timeoutMs: Math.min(Math.max(timeoutMs, 5000), 30_000),
|
|
318
|
+
});
|
|
319
|
+
if (opened.exitCode !== 0) {
|
|
320
|
+
return { ok: false, mounted: false, screenshotPath: null, tool_error: commandError('open', opened) };
|
|
321
|
+
}
|
|
322
|
+
|
|
323
|
+
// Initial fixed frame so mount/layout happen at a deterministic size; the
|
|
324
|
+
// scale keeps physical output at width x height from the very first paint.
|
|
325
|
+
const initial = fitCssViewport(900, width, height);
|
|
326
|
+
let scaledViewport = await setViewport(
|
|
327
|
+
sessionName,
|
|
328
|
+
initial.width,
|
|
329
|
+
initial.height,
|
|
330
|
+
initial.scale,
|
|
331
|
+
);
|
|
332
|
+
if (!scaledViewport) {
|
|
333
|
+
// Older agent-browser without deviceScaleFactor support: fall back to a
|
|
334
|
+
// plain fixed viewport at output size.
|
|
335
|
+
await setViewport(sessionName, width, height);
|
|
336
|
+
}
|
|
337
|
+
|
|
338
|
+
const deadline = Date.now() + timeoutMs;
|
|
339
|
+
let lastErrors = [];
|
|
340
|
+
let lastReadError = null;
|
|
341
|
+
let mounted = false;
|
|
342
|
+
while (Date.now() < deadline) {
|
|
343
|
+
const read = await readHarness(sessionName, 5000);
|
|
344
|
+
if (read.ok) {
|
|
345
|
+
const harness = read.harness || {};
|
|
346
|
+
mounted = Boolean(harness.mounted);
|
|
347
|
+
lastErrors = Array.isArray(harness.errors) ? harness.errors : [];
|
|
348
|
+
if (mounted || lastErrors.length > 0) {
|
|
349
|
+
break;
|
|
350
|
+
}
|
|
351
|
+
} else {
|
|
352
|
+
lastReadError = read.toolError;
|
|
353
|
+
}
|
|
354
|
+
await delay(250);
|
|
355
|
+
}
|
|
356
|
+
|
|
357
|
+
if (lastErrors.length > 0) {
|
|
358
|
+
return { ok: false, mounted, screenshotPath: null, errors: lastErrors, tool_error: null };
|
|
359
|
+
}
|
|
360
|
+
if (!mounted) {
|
|
361
|
+
return {
|
|
362
|
+
ok: false,
|
|
363
|
+
mounted: false,
|
|
364
|
+
screenshotPath: null,
|
|
365
|
+
errors: [],
|
|
366
|
+
timed_out: true,
|
|
367
|
+
tool_error: lastReadError || {
|
|
368
|
+
phase: 'harness',
|
|
369
|
+
message: 'Timed out waiting for window.__harness.mounted.',
|
|
370
|
+
},
|
|
371
|
+
};
|
|
372
|
+
}
|
|
373
|
+
|
|
374
|
+
// Strip harness chrome (debug banner, banner padding, 100vh min-height) so
|
|
375
|
+
// the frame contains only the app, then wait for webfonts before measuring.
|
|
376
|
+
await runAgentBrowser(
|
|
377
|
+
['--session', sessionName, 'eval', PREPARE_CAPTURE_SCRIPT],
|
|
378
|
+
{ timeoutMs: 5000 },
|
|
379
|
+
);
|
|
380
|
+
await waitForFonts(sessionName);
|
|
381
|
+
|
|
382
|
+
// Fit the CSS viewport to the rendered content. Content height depends on
|
|
383
|
+
// viewport width, so refine once after the first resize reflows the page.
|
|
384
|
+
let framing = null;
|
|
385
|
+
if (scaledViewport) {
|
|
386
|
+
let contentHeight = await evalNumber(sessionName, CONTENT_HEIGHT_SCRIPT);
|
|
387
|
+
for (let pass = 0; contentHeight != null && pass < 2; pass += 1) {
|
|
388
|
+
const fitted = fitCssViewport(contentHeight, width, height);
|
|
389
|
+
if (framing && fitted.width === framing.width) {
|
|
390
|
+
break;
|
|
391
|
+
}
|
|
392
|
+
scaledViewport = await setViewport(sessionName, fitted.width, fitted.height, fitted.scale);
|
|
393
|
+
if (!scaledViewport) {
|
|
394
|
+
framing = null;
|
|
395
|
+
await setViewport(sessionName, width, height);
|
|
396
|
+
break;
|
|
397
|
+
}
|
|
398
|
+
framing = { ...fitted, content_height: contentHeight };
|
|
399
|
+
await delay(150);
|
|
400
|
+
const remeasured = await evalNumber(sessionName, CONTENT_HEIGHT_SCRIPT);
|
|
401
|
+
if (remeasured == null || Math.abs(remeasured - contentHeight) <= 32) {
|
|
402
|
+
framing.content_height = remeasured ?? contentHeight;
|
|
403
|
+
break;
|
|
404
|
+
}
|
|
405
|
+
contentHeight = remeasured;
|
|
406
|
+
}
|
|
407
|
+
}
|
|
408
|
+
|
|
409
|
+
// Let the route settle (async data, resize transitions) before the capture.
|
|
410
|
+
await delay(500);
|
|
411
|
+
|
|
412
|
+
mkdirSync(dirname(screenshotPath), { recursive: true });
|
|
413
|
+
const screenshotArgs = ['--session', sessionName, 'screenshot'];
|
|
414
|
+
if (focusSelector) {
|
|
415
|
+
screenshotArgs.push(focusSelector);
|
|
416
|
+
}
|
|
417
|
+
screenshotArgs.push(screenshotPath, '--screenshot-format', 'png');
|
|
418
|
+
const shot = await runAgentBrowser(screenshotArgs, { timeoutMs: 15_000 });
|
|
419
|
+
if (shot.exitCode !== 0) {
|
|
420
|
+
return { ok: false, mounted: true, screenshotPath: null, errors: lastErrors, timed_out: false, tool_error: commandError('screenshot', shot) };
|
|
421
|
+
}
|
|
422
|
+
|
|
423
|
+
// The listing validator requires exact output dimensions. If the fitted
|
|
424
|
+
// capture came out wrong (e.g. the browser ignored the scale factor),
|
|
425
|
+
// recapture once at a plain fixed viewport.
|
|
426
|
+
const dimensions = pngDimensions(screenshotPath);
|
|
427
|
+
if (!focusSelector && framing && (!dimensions || dimensions.width !== width || dimensions.height !== height)) {
|
|
428
|
+
await setViewport(sessionName, width, height);
|
|
429
|
+
await delay(300);
|
|
430
|
+
const retry = await runAgentBrowser(
|
|
431
|
+
['--session', sessionName, 'screenshot', screenshotPath, '--screenshot-format', 'png'],
|
|
432
|
+
{ timeoutMs: 15_000 },
|
|
433
|
+
);
|
|
434
|
+
if (retry.exitCode !== 0) {
|
|
435
|
+
return { ok: false, mounted: true, screenshotPath: null, errors: lastErrors, timed_out: false, tool_error: commandError('screenshot', retry) };
|
|
436
|
+
}
|
|
437
|
+
framing = null;
|
|
438
|
+
}
|
|
439
|
+
|
|
440
|
+
if (focusSelector) {
|
|
441
|
+
framing = {
|
|
442
|
+
...(framing || {}),
|
|
443
|
+
focus_selector: focusSelector,
|
|
444
|
+
source_dimensions: dimensions,
|
|
445
|
+
};
|
|
446
|
+
}
|
|
447
|
+
|
|
448
|
+
return {
|
|
449
|
+
ok: true,
|
|
450
|
+
mounted: true,
|
|
451
|
+
screenshotPath,
|
|
452
|
+
errors: lastErrors,
|
|
453
|
+
timed_out: false,
|
|
454
|
+
tool_error: null,
|
|
455
|
+
framing,
|
|
456
|
+
};
|
|
457
|
+
}
|
|
458
|
+
|
|
187
459
|
export async function closeAgentBrowserSession(sessionName) {
|
|
188
460
|
const result = await runAgentBrowser(['--session', sessionName, 'close'], {
|
|
189
461
|
timeoutMs: 5000,
|
|
@@ -0,0 +1,79 @@
|
|
|
1
|
+
import { existsSync, readFileSync } from 'node:fs';
|
|
2
|
+
import { join } from 'node:path';
|
|
3
|
+
|
|
4
|
+
export const APP_CHANGELOG_FILE = 'CHANGELOG.md';
|
|
5
|
+
export const CHANGELOG_MERGE_DATE = '{PR_MERGE_DATE}';
|
|
6
|
+
|
|
7
|
+
const ENTRY_HEADING = /^##\s+\[([^\]]+)]\s+-\s+(\{PR_MERGE_DATE\}|\d{4}-\d{2}-\d{2})\s*$/;
|
|
8
|
+
|
|
9
|
+
function validCalendarDate(value) {
|
|
10
|
+
if (!/^\d{4}-\d{2}-\d{2}$/.test(value)) return false;
|
|
11
|
+
const date = new Date(`${value}T00:00:00Z`);
|
|
12
|
+
return !Number.isNaN(date.getTime()) && date.toISOString().slice(0, 10) === value;
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
export function parseAppChangelog(markdown, { sourcePath = APP_CHANGELOG_FILE } = {}) {
|
|
16
|
+
const lines = String(markdown || '').replace(/\r\n?/g, '\n').split('\n');
|
|
17
|
+
const entries = [];
|
|
18
|
+
const errors = [];
|
|
19
|
+
let current = null;
|
|
20
|
+
|
|
21
|
+
function finishCurrent() {
|
|
22
|
+
if (!current) return;
|
|
23
|
+
entries.push({
|
|
24
|
+
title: current.title,
|
|
25
|
+
date: current.date,
|
|
26
|
+
body: current.body.join('\n').trim(),
|
|
27
|
+
});
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
for (const [index, line] of lines.entries()) {
|
|
31
|
+
// Only a level-2 heading (`## ...`, not `###`/`####`) can start a release
|
|
32
|
+
// entry; deeper sub-section headings are legitimate release-body content.
|
|
33
|
+
if (!/^##(?!#)/.test(line)) {
|
|
34
|
+
if (current) current.body.push(line);
|
|
35
|
+
continue;
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
const match = line.match(ENTRY_HEADING);
|
|
39
|
+
if (!match) {
|
|
40
|
+
errors.push(
|
|
41
|
+
`${sourcePath}:${index + 1} must use \`## [Release title] - YYYY-MM-DD\` or \`## [Release title] - {PR_MERGE_DATE}\`.`,
|
|
42
|
+
);
|
|
43
|
+
continue;
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
finishCurrent();
|
|
47
|
+
const title = match[1].trim();
|
|
48
|
+
const date = match[2];
|
|
49
|
+
if (!title) {
|
|
50
|
+
errors.push(`${sourcePath}:${index + 1} must include a release title inside square brackets.`);
|
|
51
|
+
}
|
|
52
|
+
if (date !== CHANGELOG_MERGE_DATE && !validCalendarDate(date)) {
|
|
53
|
+
errors.push(`${sourcePath}:${index + 1} contains an invalid calendar date.`);
|
|
54
|
+
}
|
|
55
|
+
current = { title, date, body: [] };
|
|
56
|
+
}
|
|
57
|
+
finishCurrent();
|
|
58
|
+
|
|
59
|
+
if (entries.length === 0 && errors.length === 0) {
|
|
60
|
+
errors.push(
|
|
61
|
+
`${sourcePath} must include at least one \`## [Release title] - YYYY-MM-DD\` entry.`,
|
|
62
|
+
);
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
return { source_path: sourcePath, entries, errors, exists: true };
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
export function readAppChangelog(projectDir) {
|
|
69
|
+
const path = join(projectDir, APP_CHANGELOG_FILE);
|
|
70
|
+
if (!existsSync(path)) {
|
|
71
|
+
return {
|
|
72
|
+
source_path: APP_CHANGELOG_FILE,
|
|
73
|
+
entries: [],
|
|
74
|
+
errors: [`${APP_CHANGELOG_FILE} is required for Store publication.`],
|
|
75
|
+
exists: false,
|
|
76
|
+
};
|
|
77
|
+
}
|
|
78
|
+
return parseAppChangelog(readFileSync(path, 'utf-8'));
|
|
79
|
+
}
|