@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
package/src/runtime/output.js
CHANGED
|
@@ -94,6 +94,14 @@ export class OutputManager {
|
|
|
94
94
|
process.stderr.write(`${message}\n`);
|
|
95
95
|
}
|
|
96
96
|
|
|
97
|
+
emitProgress({ phase, message, requestId = null }) {
|
|
98
|
+
if (this.runtime.quiet) {
|
|
99
|
+
return;
|
|
100
|
+
}
|
|
101
|
+
const suffix = requestId ? ` (${requestId})` : '';
|
|
102
|
+
process.stderr.write(`[${phase}] ${message}${suffix}\n`);
|
|
103
|
+
}
|
|
104
|
+
|
|
97
105
|
emitSuccess({
|
|
98
106
|
ok = true,
|
|
99
107
|
command,
|
|
@@ -116,7 +124,10 @@ export class OutputManager {
|
|
|
116
124
|
meta: {
|
|
117
125
|
profile: this.runtime.profileName,
|
|
118
126
|
api_base: this.runtime.apiBase,
|
|
119
|
-
mutating:
|
|
127
|
+
mutating:
|
|
128
|
+
meta.mutating === true
|
|
129
|
+
? true
|
|
130
|
+
: (meta.mutating === false ? false : null),
|
|
120
131
|
...meta,
|
|
121
132
|
},
|
|
122
133
|
};
|
package/src/runtime/profiles.js
CHANGED
|
@@ -2,6 +2,7 @@ import { existsSync, mkdirSync, readFileSync, writeFileSync } from 'node:fs';
|
|
|
2
2
|
import { homedir } from 'node:os';
|
|
3
3
|
import { join } from 'node:path';
|
|
4
4
|
import { CliError, EXIT_CODES } from './errors.js';
|
|
5
|
+
import { getDesktopAuthRecovery } from './desktop-auth.js';
|
|
5
6
|
|
|
6
7
|
export const CONFIG_DIR = join(homedir(), '.notis');
|
|
7
8
|
export const CONFIG_FILE = join(CONFIG_DIR, 'config.json');
|
|
@@ -37,6 +38,9 @@ export function normalizeConfig(rawConfig = {}) {
|
|
|
37
38
|
typeof profile.access_expires_at === 'number' ? profile.access_expires_at : undefined,
|
|
38
39
|
refresh_expires_at:
|
|
39
40
|
typeof profile.refresh_expires_at === 'number' ? profile.refresh_expires_at : undefined,
|
|
41
|
+
desktop_app_name:
|
|
42
|
+
typeof profile.desktop_app_name === 'string' ? profile.desktop_app_name : undefined,
|
|
43
|
+
desktop_pid: typeof profile.desktop_pid === 'number' ? profile.desktop_pid : undefined,
|
|
40
44
|
};
|
|
41
45
|
}
|
|
42
46
|
|
|
@@ -65,6 +69,9 @@ export function normalizeConfig(rawConfig = {}) {
|
|
|
65
69
|
typeof raw.access_expires_at === 'number' ? raw.access_expires_at : undefined,
|
|
66
70
|
refresh_expires_at:
|
|
67
71
|
typeof raw.refresh_expires_at === 'number' ? raw.refresh_expires_at : undefined,
|
|
72
|
+
desktop_app_name:
|
|
73
|
+
typeof raw.desktop_app_name === 'string' ? raw.desktop_app_name : undefined,
|
|
74
|
+
desktop_pid: typeof raw.desktop_pid === 'number' ? raw.desktop_pid : undefined,
|
|
68
75
|
},
|
|
69
76
|
},
|
|
70
77
|
};
|
|
@@ -187,7 +194,37 @@ export function resolveTimeoutMs(globalOptions = {}) {
|
|
|
187
194
|
return Number.isFinite(parsed) && parsed > 0 ? parsed : 30000;
|
|
188
195
|
}
|
|
189
196
|
|
|
190
|
-
export function
|
|
197
|
+
export function parseDebugEntitlementOverride(value = process.env.NOTIS_DEBUG_ENTITLEMENT_OVERRIDE) {
|
|
198
|
+
if (!value) {
|
|
199
|
+
return null;
|
|
200
|
+
}
|
|
201
|
+
const candidates = [value];
|
|
202
|
+
try {
|
|
203
|
+
candidates.push(Buffer.from(value, 'base64url').toString('utf-8'));
|
|
204
|
+
} catch {
|
|
205
|
+
// The plain JSON candidate below will provide the actionable error.
|
|
206
|
+
}
|
|
207
|
+
for (const candidate of candidates) {
|
|
208
|
+
try {
|
|
209
|
+
const parsed = JSON.parse(candidate);
|
|
210
|
+
if (parsed && typeof parsed === 'object' && !Array.isArray(parsed)) {
|
|
211
|
+
return parsed;
|
|
212
|
+
}
|
|
213
|
+
} catch {
|
|
214
|
+
// Try the next supported encoding.
|
|
215
|
+
}
|
|
216
|
+
}
|
|
217
|
+
throw new CliError({
|
|
218
|
+
code: 'debug_entitlement_override_invalid',
|
|
219
|
+
message: 'NOTIS_DEBUG_ENTITLEMENT_OVERRIDE must be a JSON object or base64url-encoded JSON object.',
|
|
220
|
+
exitCode: EXIT_CODES.usage,
|
|
221
|
+
});
|
|
222
|
+
}
|
|
223
|
+
|
|
224
|
+
export function resolveRuntimeProfile(
|
|
225
|
+
globalOptions = {},
|
|
226
|
+
{ requireAuth = true, includeDebugEntitlementOverride = true } = {},
|
|
227
|
+
) {
|
|
191
228
|
const config = loadConfig();
|
|
192
229
|
const profileName = getCurrentProfileName(config, globalOptions.profile);
|
|
193
230
|
const apiBase = getApiBase(config, profileName, globalOptions.apiBase);
|
|
@@ -197,43 +234,62 @@ export function resolveRuntimeProfile(globalOptions = {}, { requireAuth = true }
|
|
|
197
234
|
const nonInteractive = isNonInteractive(globalOptions);
|
|
198
235
|
const outputMode = resolveOutputMode(globalOptions);
|
|
199
236
|
const timeoutMs = resolveTimeoutMs(globalOptions);
|
|
237
|
+
const debugEntitlementOverride = includeDebugEntitlementOverride
|
|
238
|
+
? parseDebugEntitlementOverride()
|
|
239
|
+
: null;
|
|
200
240
|
|
|
201
241
|
if (requireAuth && !jwt) {
|
|
242
|
+
const recovery = getDesktopAuthRecovery({
|
|
243
|
+
apiBase,
|
|
244
|
+
desktopAppName: profile.desktop_app_name,
|
|
245
|
+
desktopPid: profile.desktop_pid,
|
|
246
|
+
});
|
|
202
247
|
throw new CliError({
|
|
203
248
|
code: 'auth_missing',
|
|
204
249
|
message: `No JWT configured for profile ${profileName}`,
|
|
205
250
|
exitCode: EXIT_CODES.auth,
|
|
206
|
-
hints:
|
|
207
|
-
{
|
|
208
|
-
command: 'Open the Notis desktop app and sign in',
|
|
209
|
-
reason: 'Set NOTIS_JWT or sign in through the Notis desktop app',
|
|
210
|
-
},
|
|
211
|
-
],
|
|
251
|
+
hints: recovery.hints,
|
|
212
252
|
});
|
|
213
253
|
}
|
|
214
254
|
|
|
215
|
-
// An explicit NOTIS_JWT is a complete credential override
|
|
216
|
-
//
|
|
217
|
-
// stale or belong to a different session). Without this, a long-lived shell with an
|
|
218
|
-
// expired profile refresh_token fails every command with invalid_grant even though
|
|
219
|
-
// the supplied NOTIS_JWT is valid.
|
|
255
|
+
// An explicit NOTIS_JWT is a complete credential override. Use it verbatim
|
|
256
|
+
// and never replace it with a token later synced by the desktop profile.
|
|
220
257
|
const usingEnvJwt = Boolean(process.env.NOTIS_JWT);
|
|
221
258
|
return {
|
|
222
259
|
config,
|
|
223
260
|
profileName,
|
|
224
261
|
apiBase,
|
|
225
262
|
jwt,
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
refreshExpiresAt: usingEnvJwt ? undefined : profile.refresh_expires_at,
|
|
263
|
+
credentialSource: usingEnvJwt ? 'env' : 'profile',
|
|
264
|
+
desktopAppName: usingEnvJwt ? undefined : profile.desktop_app_name,
|
|
265
|
+
desktopPid: usingEnvJwt ? undefined : profile.desktop_pid,
|
|
230
266
|
agentMode,
|
|
231
267
|
nonInteractive,
|
|
232
268
|
outputMode,
|
|
233
269
|
timeoutMs,
|
|
270
|
+
debugEntitlementOverride,
|
|
234
271
|
};
|
|
235
272
|
}
|
|
236
273
|
|
|
274
|
+
export function getJwtExpiration(jwt) {
|
|
275
|
+
if (typeof jwt !== 'string' || !jwt) {
|
|
276
|
+
return null;
|
|
277
|
+
}
|
|
278
|
+
try {
|
|
279
|
+
const parts = jwt.split('.');
|
|
280
|
+
if (parts.length !== 3) return null;
|
|
281
|
+
const payload = JSON.parse(Buffer.from(parts[1], 'base64url').toString());
|
|
282
|
+
return typeof payload.exp === 'number' ? payload.exp : null;
|
|
283
|
+
} catch {
|
|
284
|
+
return null;
|
|
285
|
+
}
|
|
286
|
+
}
|
|
287
|
+
|
|
288
|
+
export function isJwtExpired(jwt, nowSeconds = Math.floor(Date.now() / 1000)) {
|
|
289
|
+
const expiration = getJwtExpiration(jwt);
|
|
290
|
+
return expiration !== null && expiration <= nowSeconds;
|
|
291
|
+
}
|
|
292
|
+
|
|
237
293
|
export function workspacePath(appId) {
|
|
238
294
|
return join(WORKSPACE_DIR, appId);
|
|
239
295
|
}
|
|
@@ -0,0 +1,138 @@
|
|
|
1
|
+
import sharp from 'sharp';
|
|
2
|
+
import { fileURLToPath } from 'node:url';
|
|
3
|
+
|
|
4
|
+
const STORE_SCREENSHOT_ASPECT = 16 / 10;
|
|
5
|
+
const SHARED_BACKDROP_PATH = fileURLToPath(
|
|
6
|
+
new URL('./assets/store-screenshot-dark.png', import.meta.url),
|
|
7
|
+
);
|
|
8
|
+
|
|
9
|
+
const ACCENT_PALETTES = {
|
|
10
|
+
blue: { glow: '#4f7cff', glow2: '#274690' },
|
|
11
|
+
violet: { glow: '#9b87f5', glow2: '#5038a1' },
|
|
12
|
+
emerald: { glow: '#34d399', glow2: '#176b55' },
|
|
13
|
+
amber: { glow: '#f2a94a', glow2: '#8b4f20' },
|
|
14
|
+
rose: { glow: '#fb7185', glow2: '#8f3048' },
|
|
15
|
+
sky: { glow: '#38bdf8', glow2: '#176887' },
|
|
16
|
+
fuchsia: { glow: '#d946ef', glow2: '#772786' },
|
|
17
|
+
teal: { glow: '#2dd4bf', glow2: '#176e68' },
|
|
18
|
+
};
|
|
19
|
+
|
|
20
|
+
const ACCENT_NAMES = Object.keys(ACCENT_PALETTES);
|
|
21
|
+
|
|
22
|
+
function stableHash(value) {
|
|
23
|
+
let hash = 2166136261;
|
|
24
|
+
for (const character of String(value || 'notis-app')) {
|
|
25
|
+
hash ^= character.codePointAt(0);
|
|
26
|
+
hash = Math.imul(hash, 16777619);
|
|
27
|
+
}
|
|
28
|
+
return hash >>> 0;
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
export function resolveStoreScreenshotAccent(accent, seed = 'notis-app') {
|
|
32
|
+
if (accent && ACCENT_PALETTES[accent]) {
|
|
33
|
+
return accent;
|
|
34
|
+
}
|
|
35
|
+
return ACCENT_NAMES[stableHash(seed) % ACCENT_NAMES.length];
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
export function storeScreenshotLayout(width = 2000, height = 1250) {
|
|
39
|
+
const horizontalInset = Math.max(48, Math.round(width * 0.06));
|
|
40
|
+
const verticalInset = Math.max(40, Math.round(height * 0.06));
|
|
41
|
+
let cardWidth = width - horizontalInset * 2;
|
|
42
|
+
let cardHeight = Math.round(cardWidth / STORE_SCREENSHOT_ASPECT);
|
|
43
|
+
const maximumCardHeight = height - verticalInset * 2;
|
|
44
|
+
if (cardHeight > maximumCardHeight) {
|
|
45
|
+
cardHeight = maximumCardHeight;
|
|
46
|
+
cardWidth = Math.round(cardHeight * STORE_SCREENSHOT_ASPECT);
|
|
47
|
+
}
|
|
48
|
+
const cardLeft = Math.round((width - cardWidth) / 2);
|
|
49
|
+
const cardTop = Math.round((height - cardHeight) / 2) - Math.round(height * 0.008);
|
|
50
|
+
return {
|
|
51
|
+
width,
|
|
52
|
+
height,
|
|
53
|
+
cardWidth,
|
|
54
|
+
cardHeight,
|
|
55
|
+
cardLeft,
|
|
56
|
+
cardTop,
|
|
57
|
+
radius: Math.max(18, Math.round(width * 0.016)),
|
|
58
|
+
};
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
async function backdropImage(layout) {
|
|
62
|
+
return sharp(SHARED_BACKDROP_PATH)
|
|
63
|
+
.resize(layout.width, layout.height, { fit: 'cover', position: 'centre' })
|
|
64
|
+
.png()
|
|
65
|
+
.toBuffer();
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
function roundedRectSvg(width, height, radius, { fill = '#ffffff', stroke = null } = {}) {
|
|
69
|
+
const strokeAttribute = stroke
|
|
70
|
+
? ` fill="none" stroke="${stroke}" stroke-width="2"`
|
|
71
|
+
: ` fill="${fill}"`;
|
|
72
|
+
return Buffer.from(
|
|
73
|
+
`<svg xmlns="http://www.w3.org/2000/svg" width="${width}" height="${height}"><rect x="1" y="1" width="${width - 2}" height="${height - 2}" rx="${radius}" ry="${radius}"${strokeAttribute}/></svg>`,
|
|
74
|
+
);
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
/**
|
|
78
|
+
* Composite a truthful browser capture into the deterministic Store frame.
|
|
79
|
+
* The source pixels are only resized/cropped; the app UI is never redrawn.
|
|
80
|
+
*/
|
|
81
|
+
export async function composeStoreScreenshot({
|
|
82
|
+
inputPath,
|
|
83
|
+
outputPath,
|
|
84
|
+
width = 2000,
|
|
85
|
+
height = 1250,
|
|
86
|
+
accent = null,
|
|
87
|
+
seed = 'notis-app',
|
|
88
|
+
focused = false,
|
|
89
|
+
theme = 'light',
|
|
90
|
+
}) {
|
|
91
|
+
const resolvedTheme = theme === 'dark' ? 'dark' : 'light';
|
|
92
|
+
const layout = storeScreenshotLayout(width, height);
|
|
93
|
+
const resolvedAccent = resolveStoreScreenshotAccent(accent, seed);
|
|
94
|
+
|
|
95
|
+
const roundedCapture = await sharp(inputPath)
|
|
96
|
+
.resize(layout.cardWidth, layout.cardHeight, {
|
|
97
|
+
fit: 'cover',
|
|
98
|
+
position: focused ? 'top' : 'centre',
|
|
99
|
+
})
|
|
100
|
+
.composite([{
|
|
101
|
+
input: roundedRectSvg(layout.cardWidth, layout.cardHeight, layout.radius),
|
|
102
|
+
blend: 'dest-in',
|
|
103
|
+
}])
|
|
104
|
+
.png()
|
|
105
|
+
.toBuffer();
|
|
106
|
+
|
|
107
|
+
const border = roundedRectSvg(
|
|
108
|
+
layout.cardWidth,
|
|
109
|
+
layout.cardHeight,
|
|
110
|
+
layout.radius,
|
|
111
|
+
{ stroke: resolvedTheme === 'dark' ? '#FFFFFF2A' : '#0F172A22' },
|
|
112
|
+
);
|
|
113
|
+
|
|
114
|
+
await sharp(await backdropImage(layout))
|
|
115
|
+
.composite([
|
|
116
|
+
{ input: roundedCapture, left: layout.cardLeft, top: layout.cardTop },
|
|
117
|
+
{ input: border, left: layout.cardLeft, top: layout.cardTop },
|
|
118
|
+
])
|
|
119
|
+
.png({
|
|
120
|
+
compressionLevel: 9,
|
|
121
|
+
adaptiveFiltering: true,
|
|
122
|
+
})
|
|
123
|
+
.toFile(outputPath);
|
|
124
|
+
|
|
125
|
+
return {
|
|
126
|
+
mode: 'framed',
|
|
127
|
+
accent: resolvedAccent,
|
|
128
|
+
focused: Boolean(focused),
|
|
129
|
+
theme: resolvedTheme,
|
|
130
|
+
card: {
|
|
131
|
+
left: layout.cardLeft,
|
|
132
|
+
top: layout.cardTop,
|
|
133
|
+
width: layout.cardWidth,
|
|
134
|
+
height: layout.cardHeight,
|
|
135
|
+
radius: layout.radius,
|
|
136
|
+
},
|
|
137
|
+
};
|
|
138
|
+
}
|
package/src/runtime/transport.js
CHANGED
|
@@ -3,11 +3,11 @@ import { createReadStream } from 'node:fs';
|
|
|
3
3
|
import { CliError, EXIT_CODES } from './errors.js';
|
|
4
4
|
import {
|
|
5
5
|
DEFAULT_PROFILE,
|
|
6
|
-
ensureProfile,
|
|
7
6
|
getProfile,
|
|
7
|
+
isJwtExpired,
|
|
8
8
|
loadConfig,
|
|
9
|
-
saveConfig,
|
|
10
9
|
} from './profiles.js';
|
|
10
|
+
import { createExpiredAuthError, createInvalidAuthHints } from './desktop-auth.js';
|
|
11
11
|
|
|
12
12
|
function escapeMultipartHeaderValue(value) {
|
|
13
13
|
return String(value ?? '')
|
|
@@ -76,7 +76,7 @@ function createMultipartFileUpload(payload, bindingMetadata, fileBindings) {
|
|
|
76
76
|
};
|
|
77
77
|
}
|
|
78
78
|
|
|
79
|
-
function normalizeBackendError(status, payload) {
|
|
79
|
+
function normalizeBackendError(status, payload, runtime) {
|
|
80
80
|
const backendError = payload?.error;
|
|
81
81
|
const message =
|
|
82
82
|
backendError?.message ||
|
|
@@ -91,10 +91,7 @@ function normalizeBackendError(status, payload) {
|
|
|
91
91
|
message,
|
|
92
92
|
exitCode: EXIT_CODES.auth,
|
|
93
93
|
hints: [
|
|
94
|
-
|
|
95
|
-
command: 'Open the Notis desktop app and sign in',
|
|
96
|
-
reason: 'Open the Notis desktop app to refresh CLI auth, or set NOTIS_JWT',
|
|
97
|
-
},
|
|
94
|
+
...createInvalidAuthHints(runtime),
|
|
98
95
|
],
|
|
99
96
|
details: payload || {},
|
|
100
97
|
});
|
|
@@ -145,46 +142,10 @@ function normalizeBackendError(status, payload) {
|
|
|
145
142
|
});
|
|
146
143
|
}
|
|
147
144
|
|
|
148
|
-
function
|
|
149
|
-
if (runtime.
|
|
150
|
-
return false;
|
|
151
|
-
}
|
|
152
|
-
|
|
153
|
-
if (force) {
|
|
154
|
-
return true;
|
|
155
|
-
}
|
|
156
|
-
|
|
157
|
-
if (typeof runtime.accessExpiresAt !== 'number' || runtime.accessExpiresAt <= 0) {
|
|
145
|
+
function reloadJwtFromConfig(runtime) {
|
|
146
|
+
if (runtime.credentialSource === 'env') {
|
|
158
147
|
return false;
|
|
159
148
|
}
|
|
160
|
-
|
|
161
|
-
const thresholdSeconds = 60;
|
|
162
|
-
return runtime.accessExpiresAt - Math.floor(Date.now() / 1000) <= thresholdSeconds;
|
|
163
|
-
}
|
|
164
|
-
|
|
165
|
-
function persistDevPortalRuntimeAuth(runtime, session) {
|
|
166
|
-
const config = ensureProfile(loadConfig(), runtime.profileName || DEFAULT_PROFILE);
|
|
167
|
-
const profileName = runtime.profileName || DEFAULT_PROFILE;
|
|
168
|
-
config.current_profile = profileName;
|
|
169
|
-
config.profiles[profileName] = {
|
|
170
|
-
...config.profiles[profileName],
|
|
171
|
-
jwt: session.access_token,
|
|
172
|
-
api_base: runtime.apiBase,
|
|
173
|
-
auth_mode: 'dev_portal',
|
|
174
|
-
refresh_token: session.refresh_token,
|
|
175
|
-
access_expires_at: session.access_expires_at,
|
|
176
|
-
refresh_expires_at: session.refresh_expires_at,
|
|
177
|
-
};
|
|
178
|
-
saveConfig(config);
|
|
179
|
-
|
|
180
|
-
runtime.jwt = session.access_token;
|
|
181
|
-
runtime.authMode = 'dev_portal';
|
|
182
|
-
runtime.refreshToken = session.refresh_token;
|
|
183
|
-
runtime.accessExpiresAt = session.access_expires_at;
|
|
184
|
-
runtime.refreshExpiresAt = session.refresh_expires_at;
|
|
185
|
-
}
|
|
186
|
-
|
|
187
|
-
function reloadJwtFromConfig(runtime) {
|
|
188
149
|
const profileName = runtime.profileName || DEFAULT_PROFILE;
|
|
189
150
|
let profile;
|
|
190
151
|
try {
|
|
@@ -197,39 +158,8 @@ function reloadJwtFromConfig(runtime) {
|
|
|
197
158
|
return false;
|
|
198
159
|
}
|
|
199
160
|
runtime.jwt = nextJwt;
|
|
200
|
-
runtime.
|
|
201
|
-
runtime.
|
|
202
|
-
runtime.accessExpiresAt = profile.access_expires_at;
|
|
203
|
-
runtime.refreshExpiresAt = profile.refresh_expires_at;
|
|
204
|
-
return true;
|
|
205
|
-
}
|
|
206
|
-
|
|
207
|
-
async function maybeRefreshDevPortalAuth(runtime, { force = false } = {}) {
|
|
208
|
-
if (!shouldAttemptDevPortalRefresh(runtime, { force })) {
|
|
209
|
-
return false;
|
|
210
|
-
}
|
|
211
|
-
|
|
212
|
-
const response = await fetch(`${runtime.apiBase}/portal_auth/dev-refresh`, {
|
|
213
|
-
method: 'POST',
|
|
214
|
-
headers: {
|
|
215
|
-
'Content-Type': 'application/json',
|
|
216
|
-
'X-Notis-CLI-Version': runtime.cliVersion,
|
|
217
|
-
},
|
|
218
|
-
body: JSON.stringify({ refresh_token: runtime.refreshToken }),
|
|
219
|
-
});
|
|
220
|
-
|
|
221
|
-
let payload = null;
|
|
222
|
-
try {
|
|
223
|
-
payload = await response.json();
|
|
224
|
-
} catch {
|
|
225
|
-
payload = null;
|
|
226
|
-
}
|
|
227
|
-
|
|
228
|
-
if (!response.ok || !payload?.session?.access_token || !payload?.session?.refresh_token) {
|
|
229
|
-
throw normalizeBackendError(response.status, payload);
|
|
230
|
-
}
|
|
231
|
-
|
|
232
|
-
persistDevPortalRuntimeAuth(runtime, payload.session);
|
|
161
|
+
runtime.desktopAppName = profile.desktop_app_name;
|
|
162
|
+
runtime.desktopPid = profile.desktop_pid;
|
|
233
163
|
return true;
|
|
234
164
|
}
|
|
235
165
|
|
|
@@ -241,7 +171,14 @@ export async function httpRequest({
|
|
|
241
171
|
multipart = false,
|
|
242
172
|
requireAuth = true,
|
|
243
173
|
}) {
|
|
244
|
-
|
|
174
|
+
// The desktop renderer is the single owner of the rotating Supabase refresh
|
|
175
|
+
// token. Each CLI request only consumes the newest access token it synced to
|
|
176
|
+
// disk, avoiding refresh-token races between independent CLI processes and
|
|
177
|
+
// the running desktop session.
|
|
178
|
+
reloadJwtFromConfig(runtime);
|
|
179
|
+
if (requireAuth && isJwtExpired(runtime.jwt)) {
|
|
180
|
+
throw createExpiredAuthError(runtime);
|
|
181
|
+
}
|
|
245
182
|
|
|
246
183
|
let controller = new AbortController();
|
|
247
184
|
let timeout = setTimeout(() => controller.abort(), runtime.timeoutMs);
|
|
@@ -298,8 +235,7 @@ export async function httpRequest({
|
|
|
298
235
|
}
|
|
299
236
|
|
|
300
237
|
if (response.status === 401) {
|
|
301
|
-
const refreshed = (
|
|
302
|
-
|| reloadJwtFromConfig(runtime);
|
|
238
|
+
const refreshed = reloadJwtFromConfig(runtime) && !isJwtExpired(runtime.jwt);
|
|
303
239
|
if (refreshed) {
|
|
304
240
|
if (requireAuth && runtime.jwt) {
|
|
305
241
|
headers.Authorization = `Bearer ${runtime.jwt}`;
|
|
@@ -324,7 +260,10 @@ export async function httpRequest({
|
|
|
324
260
|
clearTimeout(timeout);
|
|
325
261
|
|
|
326
262
|
if (!response.ok) {
|
|
327
|
-
|
|
263
|
+
if (response.status === 401 && isJwtExpired(runtime.jwt)) {
|
|
264
|
+
throw createExpiredAuthError(runtime);
|
|
265
|
+
}
|
|
266
|
+
throw normalizeBackendError(response.status, payload, runtime);
|
|
328
267
|
}
|
|
329
268
|
|
|
330
269
|
return {
|
|
@@ -379,6 +318,9 @@ export async function callTool({
|
|
|
379
318
|
cwd: process.cwd(),
|
|
380
319
|
agent_mode: runtime.agentMode,
|
|
381
320
|
cli_version: runtime.cliVersion,
|
|
321
|
+
...(runtime.debugEntitlementOverride
|
|
322
|
+
? { debug_entitlement_override: runtime.debugEntitlementOverride }
|
|
323
|
+
: {}),
|
|
382
324
|
},
|
|
383
325
|
};
|
|
384
326
|
|
|
@@ -3,6 +3,11 @@
|
|
|
3
3
|
<head>
|
|
4
4
|
<meta charset="utf-8">
|
|
5
5
|
<title>Notis App Harness</title>
|
|
6
|
+
<script>
|
|
7
|
+
const harnessTheme = new URLSearchParams(window.location.search).get('theme') === 'dark' ? 'dark' : 'light';
|
|
8
|
+
document.documentElement.classList.toggle('dark', harnessTheme === 'dark');
|
|
9
|
+
document.documentElement.dataset.theme = harnessTheme;
|
|
10
|
+
</script>
|
|
6
11
|
<link rel="stylesheet" href="./bundle/app.css">
|
|
7
12
|
<script type="importmap">
|
|
8
13
|
{
|
|
@@ -14,8 +19,80 @@
|
|
|
14
19
|
}
|
|
15
20
|
}
|
|
16
21
|
</script>
|
|
22
|
+
<link rel="preconnect" href="https://fonts.googleapis.com">
|
|
23
|
+
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
|
|
24
|
+
<link rel="stylesheet" href="https://fonts.googleapis.com/css2?family=Inter:wght@400;500;600;700&display=swap">
|
|
17
25
|
<style>
|
|
18
|
-
|
|
26
|
+
/*
|
|
27
|
+
* Portal-parity theme tokens. In production the portal copies its computed
|
|
28
|
+
* :root custom properties onto the app root, so app CSS written against
|
|
29
|
+
* hsl(var(--token)) resolves to the real portal palette. The harness mirrors
|
|
30
|
+
* the portal's light theme here so dev renders and listing screenshots look
|
|
31
|
+
* exactly like the app inside the portal. Keep in sync with
|
|
32
|
+
* portal/src/app/globals.css (:root).
|
|
33
|
+
*/
|
|
34
|
+
:root {
|
|
35
|
+
color-scheme: light;
|
|
36
|
+
--background: 0 0% 100%;
|
|
37
|
+
--foreground: 222.2 84% 4.9%;
|
|
38
|
+
--card: 0 0% 100%;
|
|
39
|
+
--card-foreground: 222.2 84% 4.9%;
|
|
40
|
+
--popover: 0 0% 100%;
|
|
41
|
+
--popover-foreground: 222.2 84% 4.9%;
|
|
42
|
+
--primary: 222.2 47.4% 11.2%;
|
|
43
|
+
--primary-foreground: 210 40% 98%;
|
|
44
|
+
--secondary: 210 40% 96.1%;
|
|
45
|
+
--secondary-foreground: 222.2 47.4% 11.2%;
|
|
46
|
+
--muted: 210 40% 96.1%;
|
|
47
|
+
--muted-foreground: 0 0% 46.9%;
|
|
48
|
+
--accent: 210 40% 96.1%;
|
|
49
|
+
--accent-foreground: 222.2 47.4% 11.2%;
|
|
50
|
+
--destructive: 0 84.2% 60.2%;
|
|
51
|
+
--destructive-foreground: 210 40% 98%;
|
|
52
|
+
--border: 214.3 31.8% 91.4%;
|
|
53
|
+
--input: 214.3 31.8% 91.4%;
|
|
54
|
+
--ring: 222.2 84% 4.9%;
|
|
55
|
+
--radius: 0.5rem;
|
|
56
|
+
--chart-1: 210 100% 49%;
|
|
57
|
+
--chart-2: 173 58% 39%;
|
|
58
|
+
--chart-3: 197 37% 24%;
|
|
59
|
+
--chart-4: 43 74% 66%;
|
|
60
|
+
--chart-5: 27 87% 67%;
|
|
61
|
+
}
|
|
62
|
+
:root.dark {
|
|
63
|
+
color-scheme: dark;
|
|
64
|
+
--background: 0 0% 13%;
|
|
65
|
+
--foreground: 210 40% 98%;
|
|
66
|
+
--card: 0 0% 13%;
|
|
67
|
+
--card-foreground: 210 40% 98%;
|
|
68
|
+
--popover: 0 0% 13%;
|
|
69
|
+
--popover-foreground: 210 40% 98%;
|
|
70
|
+
--primary: 210 40% 98%;
|
|
71
|
+
--primary-foreground: 222.2 47.4% 11.2%;
|
|
72
|
+
--secondary: 0 0% 18%;
|
|
73
|
+
--secondary-foreground: 210 40% 98%;
|
|
74
|
+
--muted: 0 0% 18%;
|
|
75
|
+
--muted-foreground: 0 0% 65.1%;
|
|
76
|
+
--accent: 0 0% 18%;
|
|
77
|
+
--accent-foreground: 210 40% 98%;
|
|
78
|
+
--destructive: 0 84.2% 60.2%;
|
|
79
|
+
--destructive-foreground: 210 40% 98%;
|
|
80
|
+
--border: 0 0% 23.5%;
|
|
81
|
+
--input: 0 0% 23.5%;
|
|
82
|
+
--ring: 212.7 26.8% 83.9%;
|
|
83
|
+
--chart-1: 220 70% 50%;
|
|
84
|
+
--chart-2: 160 60% 45%;
|
|
85
|
+
--chart-3: 30 80% 55%;
|
|
86
|
+
--chart-4: 280 65% 60%;
|
|
87
|
+
--chart-5: 340 75% 55%;
|
|
88
|
+
}
|
|
89
|
+
body {
|
|
90
|
+
margin: 0;
|
|
91
|
+
font-family: 'Inter', system-ui, -apple-system, sans-serif;
|
|
92
|
+
background: hsl(var(--background));
|
|
93
|
+
color: hsl(var(--foreground));
|
|
94
|
+
-webkit-font-smoothing: antialiased;
|
|
95
|
+
}
|
|
19
96
|
#harness-status { position: fixed; top: 0; left: 0; right: 0; padding: 4px 8px; font-size: 11px; background: #111; color: #0f0; font-family: ui-monospace, SFMono-Regular, Menlo, monospace; z-index: 9999; }
|
|
20
97
|
#root { padding-top: 24px; min-height: 100vh; }
|
|
21
98
|
</style>
|
|
@@ -29,6 +106,7 @@
|
|
|
29
106
|
const mode = {{MODE}};
|
|
30
107
|
const apiBase = {{API_BASE}};
|
|
31
108
|
const jwt = {{JWT}};
|
|
109
|
+
const fixtures = {{FIXTURES}};
|
|
32
110
|
const status = document.getElementById('harness-status');
|
|
33
111
|
const setStatus = (msg, color = '#0f0') => {
|
|
34
112
|
status.textContent = `harness: ${msg}`;
|
|
@@ -42,6 +120,7 @@
|
|
|
42
120
|
mounted: false,
|
|
43
121
|
mode,
|
|
44
122
|
route: descriptor.route && descriptor.route.slug,
|
|
123
|
+
scenario: descriptor.context && descriptor.context.screenshotScenario,
|
|
45
124
|
};
|
|
46
125
|
|
|
47
126
|
window.addEventListener('error', (event) => {
|
|
@@ -118,15 +197,48 @@
|
|
|
118
197
|
},
|
|
119
198
|
callTool: async (name, args) => {
|
|
120
199
|
record('callTool', { name, arguments: args || {} });
|
|
200
|
+
if (fixtures && fixtures.tools && Object.prototype.hasOwnProperty.call(fixtures.tools, name)) {
|
|
201
|
+
return structuredClone(fixtures.tools[name]);
|
|
202
|
+
}
|
|
121
203
|
return { ok: true, result: null };
|
|
122
204
|
},
|
|
123
205
|
request: async (path, options) => {
|
|
124
206
|
record('request', { path, options });
|
|
207
|
+
if (fixtures && fixtures.requests && Object.prototype.hasOwnProperty.call(fixtures.requests, path)) {
|
|
208
|
+
return structuredClone(fixtures.requests[path]);
|
|
209
|
+
}
|
|
125
210
|
return null;
|
|
126
211
|
},
|
|
127
212
|
};
|
|
128
213
|
}
|
|
129
214
|
|
|
215
|
+
const delay = (ms) => new Promise((resolve) => setTimeout(resolve, ms));
|
|
216
|
+
|
|
217
|
+
async function applyScenarioActions() {
|
|
218
|
+
const actions = fixtures && fixtures.scenario && Array.isArray(fixtures.scenario.actions)
|
|
219
|
+
? fixtures.scenario.actions
|
|
220
|
+
: [];
|
|
221
|
+
for (const action of actions) {
|
|
222
|
+
if (action && action.type === 'wait') {
|
|
223
|
+
await delay(Math.max(0, Number(action.ms) || 0));
|
|
224
|
+
continue;
|
|
225
|
+
}
|
|
226
|
+
if (action && action.type === 'click' && typeof action.selector === 'string') {
|
|
227
|
+
const deadline = Date.now() + 5000;
|
|
228
|
+
let element = null;
|
|
229
|
+
while (!element && Date.now() < deadline) {
|
|
230
|
+
element = document.querySelector(action.selector);
|
|
231
|
+
if (!element) await delay(50);
|
|
232
|
+
}
|
|
233
|
+
if (!element || typeof element.click !== 'function') {
|
|
234
|
+
throw new Error(`Screenshot scenario could not click ${action.selector}`);
|
|
235
|
+
}
|
|
236
|
+
element.click();
|
|
237
|
+
await delay(Number(action.settle_ms) || 350);
|
|
238
|
+
}
|
|
239
|
+
}
|
|
240
|
+
}
|
|
241
|
+
|
|
130
242
|
function liveRuntime() {
|
|
131
243
|
return {
|
|
132
244
|
...stubRuntime(),
|
|
@@ -192,11 +304,21 @@
|
|
|
192
304
|
window.__harness.renderStarted = true;
|
|
193
305
|
createRoot(document.getElementById('root')).render(React.createElement(Root));
|
|
194
306
|
|
|
195
|
-
requestAnimationFrame(() => requestAnimationFrame(() => {
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
307
|
+
requestAnimationFrame(() => requestAnimationFrame(async () => {
|
|
308
|
+
try {
|
|
309
|
+
await delay(500);
|
|
310
|
+
await applyScenarioActions();
|
|
311
|
+
window.__harness.mounted = true;
|
|
312
|
+
const errors = window.__harness.errors.length;
|
|
313
|
+
const calls = window.__harness.runtimeCalls.length;
|
|
314
|
+
setStatus(`mounted (errors=${errors}, runtimeCalls=${calls})`, errors ? '#f55' : '#0f0');
|
|
315
|
+
} catch (error) {
|
|
316
|
+
window.__harness.errors.push({
|
|
317
|
+
type: 'scenario',
|
|
318
|
+
message: error && error.message ? error.message : String(error),
|
|
319
|
+
});
|
|
320
|
+
setStatus(`scenario failed: ${error && error.message ? error.message : String(error)}`, '#f55');
|
|
321
|
+
}
|
|
200
322
|
}));
|
|
201
323
|
} catch (error) {
|
|
202
324
|
window.__harness.errors.push({
|