@notis_ai/cli 0.2.0-beta.175.1 → 0.2.0-beta.178.1
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 +25 -39
- package/dist/agent-hooks/notis-agent-hook.mjs +197 -155
- package/{template/packages → dist}/sdk/src/config.ts +4 -0
- package/{template/packages → dist}/sdk/src/hooks/useNotis.ts +3 -1
- package/{template/packages → dist}/sdk/src/index.ts +1 -0
- package/{template/packages → dist}/sdk/src/runtime.ts +13 -0
- package/{template/packages → dist}/sdk/src/styles.css +72 -74
- package/package.json +1 -1
- package/skills/notis-apps/cli.md +23 -37
- package/src/command-specs/apps.js +18 -14
- package/src/command-specs/diagnostics.js +15 -14
- package/src/command-specs/reports.js +61 -50
- package/src/runtime/agent-browser.js +10 -5
- package/src/runtime/app-platform.js +27 -3
- package/src/runtime/app-test-server.js +2 -1
- package/template/.harness/index.html.tmpl +12 -4
- package/template/packages/sdk/tsconfig.json +0 -16
- /package/{template/packages → dist}/sdk/package.json +0 -0
- /package/{template/packages → dist}/sdk/src/agentContext.ts +0 -0
- /package/{template/packages → dist}/sdk/src/components/Dialog.tsx +0 -0
- /package/{template/packages → dist}/sdk/src/components/DocumentEditor.tsx +0 -0
- /package/{template/packages → dist}/sdk/src/components/Markdown.tsx +0 -0
- /package/{template/packages → dist}/sdk/src/components/MarkdownEditor.tsx +0 -0
- /package/{template/packages → dist}/sdk/src/components/MultiSelectActionBar.tsx +0 -0
- /package/{template/packages → dist}/sdk/src/components/MultiSelectCheckbox.tsx +0 -0
- /package/{template/packages → dist}/sdk/src/components/MultiSelectDragOverlay.tsx +0 -0
- /package/{template/packages → dist}/sdk/src/components/NotisCommentBoundary.tsx +0 -0
- /package/{template/packages → dist}/sdk/src/components/NotisSelectionBoundary.tsx +0 -0
- /package/{template/packages → dist}/sdk/src/components/ShortcutHints.tsx +0 -0
- /package/{template/packages → dist}/sdk/src/components/Skeleton.tsx +0 -0
- /package/{template/packages → dist}/sdk/src/documents.ts +0 -0
- /package/{template/packages → dist}/sdk/src/hooks/useActiveResource.ts +0 -0
- /package/{template/packages → dist}/sdk/src/hooks/useAgentContext.ts +0 -0
- /package/{template/packages → dist}/sdk/src/hooks/useBackend.ts +0 -0
- /package/{template/packages → dist}/sdk/src/hooks/useCloudComputer.ts +0 -0
- /package/{template/packages → dist}/sdk/src/hooks/useCollectionInteractions.ts +0 -0
- /package/{template/packages → dist}/sdk/src/hooks/useDatabaseSchema.ts +0 -0
- /package/{template/packages → dist}/sdk/src/hooks/useDatabaseSubscription.ts +0 -0
- /package/{template/packages → dist}/sdk/src/hooks/useDocument.ts +0 -0
- /package/{template/packages → dist}/sdk/src/hooks/useDocuments.ts +0 -0
- /package/{template/packages → dist}/sdk/src/hooks/useHandover.ts +0 -0
- /package/{template/packages → dist}/sdk/src/hooks/useLongPressSelection.ts +0 -0
- /package/{template/packages → dist}/sdk/src/hooks/useMultiSelect.ts +0 -0
- /package/{template/packages → dist}/sdk/src/hooks/useNotisNavigation.ts +0 -0
- /package/{template/packages → dist}/sdk/src/hooks/useQuery.ts +0 -0
- /package/{template/packages → dist}/sdk/src/hooks/useTool.ts +0 -0
- /package/{template/packages → dist}/sdk/src/hooks/useToolQuery.ts +0 -0
- /package/{template/packages → dist}/sdk/src/hooks/useTools.ts +0 -0
- /package/{template/packages → dist}/sdk/src/hooks/useTopBarSearch.ts +0 -0
- /package/{template/packages → dist}/sdk/src/hooks/useUpsertDocument.ts +0 -0
- /package/{template/packages → dist}/sdk/src/interactions/actions.ts +0 -0
- /package/{template/packages → dist}/sdk/src/interactions/shortcuts.tsx +0 -0
- /package/{template/packages → dist}/sdk/src/interactions/visibility.ts +0 -0
- /package/{template/packages → dist}/sdk/src/interactions.ts +0 -0
- /package/{template/packages → dist}/sdk/src/provider.tsx +0 -0
- /package/{template/packages → dist}/sdk/src/queryCache.ts +0 -0
- /package/{template/packages → dist}/sdk/src/tailwind.ts +0 -0
- /package/{template/packages → dist}/sdk/src/ui.ts +0 -0
- /package/{template/packages → dist}/sdk/src/vite.ts +0 -0
|
@@ -1,82 +1,93 @@
|
|
|
1
|
-
import { readFileSync } from 'node:fs';
|
|
2
|
-
import { resolve } from 'node:path';
|
|
3
|
-
import { appsCommandSpecs } from './apps.js';
|
|
4
|
-
import { buildArtifact, prepareAppRelease, resolveProjectDir } from '../runtime/app-platform.js';
|
|
1
|
+
import { readFileSync, writeFileSync, rmSync } from 'node:fs';
|
|
2
|
+
import { resolve, join } from 'node:path';
|
|
3
|
+
import { appsCommandSpecs, appsInitHandler, appsBuildHandler, appsVerifyHandler } from './apps.js';
|
|
4
|
+
import { buildArtifact, prepareAppRelease, resolveProjectDir, loadAppConfig } from '../runtime/app-platform.js';
|
|
5
5
|
import { nextIdempotencyKey, runToolCommand } from './helpers.js';
|
|
6
6
|
import { usageError, EXIT_CODES } from '../runtime/errors.js';
|
|
7
7
|
|
|
8
|
-
|
|
9
|
-
const
|
|
10
|
-
|
|
11
|
-
|
|
8
|
+
async function assertReportProject(ctx) {
|
|
9
|
+
const dir = resolveProjectDir(ctx.args.dir || '.');
|
|
10
|
+
if ((await loadAppConfig(dir)).kind !== 'report') throw usageError('Set kind: "report" in notis.config.ts. Reports have no app installation or owned resources.');
|
|
11
|
+
return dir;
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
async function initReport(ctx) {
|
|
15
|
+
const dir = resolveProjectDir(ctx.args.dir || `./${ctx.args.name.toLowerCase().replace(/[^a-z0-9]+/g, '-')}`);
|
|
16
|
+
const result = await appsInitHandler({ ...ctx, args: { ...ctx.args, dir }, options: {}, output: { emitSuccess: value => value } });
|
|
17
|
+
writeFileSync(join(dir, 'notis.config.ts'), `import { defineNotisApp } from '@notis/sdk/config';
|
|
18
|
+
export default defineNotisApp({
|
|
19
|
+
kind: 'report', name: ${JSON.stringify(ctx.args.name)},
|
|
20
|
+
routes: [{ path: '/', slug: 'home', name: 'Report', default: true }],
|
|
21
|
+
tools: [],
|
|
22
|
+
});
|
|
23
|
+
`);
|
|
24
|
+
writeFileSync(join(dir, 'app/page.tsx'), `'use client';
|
|
25
|
+
import { useNotis, NotisCommentBoundary } from '@notis/sdk';
|
|
26
|
+
export default function Report() {
|
|
27
|
+
const { app } = useNotis();
|
|
28
|
+
return <NotisCommentBoundary><main className="notis-app-shell space-y-6">
|
|
29
|
+
<h1 className="text-2xl font-semibold">{app?.name || 'Report'}</h1>
|
|
30
|
+
<p className="text-muted-foreground">Your report is ready to build.</p>
|
|
31
|
+
</main></NotisCommentBoundary>;
|
|
32
|
+
}
|
|
33
|
+
`);
|
|
34
|
+
rmSync(join(dir, 'CHANGELOG.md'), { force: true });
|
|
35
|
+
return ctx.output.emitSuccess({ ...result, command: 'reports init', hints: [
|
|
36
|
+
{ command: `cd ${dir} && npm install`, reason: 'Install dependencies' },
|
|
37
|
+
{ command: `notis reports build ${dir}`, reason: 'Build the standalone report' },
|
|
38
|
+
] });
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
const localReportCommand = (command, name = command) => {
|
|
42
|
+
const app = appsCommandSpecs.find(item => item.command_path.join(' ') === `apps ${command}`);
|
|
43
|
+
return { ...app, command_path: ['reports', name],
|
|
44
|
+
summary: `${name[0].toUpperCase() + name.slice(1)} a standalone live SDK report.`,
|
|
45
|
+
when_to_use: 'Author a private standalone document using the shared view runtime; no app or database is required.',
|
|
46
|
+
args_schema: { ...app.args_schema, options: [...(app.args_schema.options || []).filter(option => !['--listing', '--from <slug>'].includes(option.flags)), ...(command === 'verify' ? [{ flags: '--document-id <id>', description: 'Saved report for live verification.' }, { flags: '--expected-revision <revision>', description: 'Saved report revision for live verification.' }] : [])] },
|
|
47
|
+
examples: [`notis reports ${name} ${command === 'init' ? '"Weekly report" ./weekly-report' : './weekly-report'}`],
|
|
12
48
|
handler: async ctx => {
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
const value = { ...result, warnings: (result.warnings || []).filter(warning => !warning.startsWith('Store readiness:')) };
|
|
17
|
-
if (value.data?.listing) { value.data = { ...value.data }; delete value.data.listing; }
|
|
18
|
-
return target.emitSuccess(value);
|
|
19
|
-
};
|
|
20
|
-
const value = Reflect.get(target, key);
|
|
21
|
-
return typeof value === 'function' ? value.bind(target) : value;
|
|
22
|
-
},
|
|
23
|
-
});
|
|
24
|
-
return spec.handler({ ...ctx, options: { ...ctx.options, listing: false, ...(name === 'preview' ? { keepOpen: true } : {}) }, output });
|
|
25
|
-
},
|
|
26
|
-
command_path: ['reports', name],
|
|
27
|
-
summary: `${name[0].toUpperCase() + name.slice(1)} a record-owned SDK report locally.${name === 'preview' ? ' Keeps the preview server and browser session open.' : ''}`,
|
|
28
|
-
args_schema: {
|
|
29
|
-
...spec.args_schema,
|
|
30
|
-
options: (spec.args_schema?.options || []).map(option => option.flags === '--listing'
|
|
31
|
-
? { ...option, description: 'Ignored for reports; saving a report does not publish a Store listing.' }
|
|
32
|
-
: option),
|
|
49
|
+
if (command === 'init') return initReport(ctx);
|
|
50
|
+
await assertReportProject(ctx);
|
|
51
|
+
return (command === 'build' ? appsBuildHandler : appsVerifyHandler)({ ...ctx, options: { ...ctx.options, ...(name === 'preview' ? { keepOpen: true } : {}) } });
|
|
33
52
|
},
|
|
34
|
-
examples: (spec.examples || []).filter(example => !example.includes('--listing')).map(example => example.replace(`apps ${command}`, `reports ${name}`)),
|
|
35
|
-
when_to_use: 'Author an independent report without deploying its owning app.',
|
|
36
53
|
};
|
|
37
54
|
};
|
|
38
55
|
export const reportsCommandSpecs = [
|
|
39
|
-
|
|
56
|
+
localReportCommand('init'), localReportCommand('build'), localReportCommand('verify'), localReportCommand('verify', 'preview'),
|
|
40
57
|
{
|
|
41
58
|
command_path: ['reports', 'save'],
|
|
42
|
-
summary: 'Build, verify and save a
|
|
43
|
-
when_to_use: 'Persist an independently
|
|
59
|
+
summary: 'Build, verify and save a standalone live report document.',
|
|
60
|
+
when_to_use: 'Persist an independently owned report, not an app release.',
|
|
44
61
|
args_schema: {
|
|
45
62
|
arguments: [{ token: '[dir]', key: 'dir', description: 'Report source directory.' }],
|
|
46
63
|
options: [
|
|
47
|
-
{ flags: '--
|
|
48
|
-
{ flags: '--
|
|
49
|
-
{ flags: '--
|
|
50
|
-
{ flags: '--expected-revision <revision>', description: 'Fresh view revision (0 for a record without a view).' },
|
|
51
|
-
{ flags: '--title <title>', description: 'Required, including updates. Record title.' },
|
|
64
|
+
{ flags: '--document-id <id>', description: 'Existing report document to update.' },
|
|
65
|
+
{ flags: '--expected-revision <revision>', description: 'Current saved report revision; required for updates.' },
|
|
66
|
+
{ flags: '--title <title>', description: 'Required, including updates. Document title.' },
|
|
52
67
|
{ flags: '--context-file <file>', description: 'Required. UTF-8 readable report content and structure.' },
|
|
53
|
-
{ flags: '--properties-file <file>', description: 'JSON database property values keyed by name.' },
|
|
54
68
|
],
|
|
55
69
|
},
|
|
56
|
-
examples: ['notis reports save ./weekly-report --
|
|
70
|
+
examples: ['notis reports save ./weekly-report --title \"Weekly review\" --context-file ./context.md'], mutates: true, idempotent: true,
|
|
57
71
|
backend_call: { type: 'tool', name: 'LOCAL_NOTIS_SAVE_REPORT' },
|
|
58
72
|
async handler(ctx) {
|
|
59
|
-
const dir =
|
|
60
|
-
if (!ctx.options.
|
|
61
|
-
if (ctx.options.documentId && (!/^\d+$/.test(String(ctx.options.expectedRevision ?? '')) || !Number.isSafeInteger(Number(ctx.options.expectedRevision)))) throw usageError('--expected-revision is required for update
|
|
62
|
-
if (ctx.options.attach && !ctx.options.documentId) throw usageError('--attach requires --document-id.');
|
|
73
|
+
const dir = await assertReportProject(ctx);
|
|
74
|
+
if (!ctx.options.title || !ctx.options.contextFile) throw usageError('--title and --context-file are required.');
|
|
75
|
+
if (ctx.options.documentId && (!/^\d+$/.test(String(ctx.options.expectedRevision ?? '')) || !Number.isSafeInteger(Number(ctx.options.expectedRevision)))) throw usageError('--expected-revision is required for update.');
|
|
63
76
|
const context = readFileSync(resolve(ctx.options.contextFile), 'utf8');
|
|
64
|
-
const properties = ctx.options.propertiesFile ? JSON.parse(readFileSync(resolve(ctx.options.propertiesFile), 'utf8')) : {};
|
|
65
77
|
await buildArtifact(dir, { stdio: ctx.output.isMachineMode() ? 'pipe' : 'inherit' });
|
|
66
78
|
const release = prepareAppRelease(dir);
|
|
67
79
|
try {
|
|
68
80
|
if (release.manifest.routes?.length !== 1) throw usageError('Reports require exactly one SDK route.');
|
|
69
81
|
let verification;
|
|
70
|
-
const
|
|
71
|
-
const exit = await verify.handler({ ...ctx, args: { dir: release.projectDir }, options: { skipBuild: true, mode: 'stub' }, output: { ...ctx.output, isMachineMode: () => true, emitSuccess: value => { verification = value; } } });
|
|
82
|
+
const exit = await appsVerifyHandler({ ...ctx, args: { dir: release.projectDir }, options: { skipBuild: true, mode: 'stub' }, output: { ...ctx.output, isMachineMode: () => true, emitSuccess: value => { verification = value; } } });
|
|
72
83
|
if (exit !== EXIT_CODES.ok || verification?.data?.status !== 'passed') throw usageError('Report verification failed; nothing saved.');
|
|
73
84
|
const files = { ...release.files, ...Object.fromEntries(Object.entries(release.sourceFiles).map(([path, data]) => [`source/${path}`, data])) };
|
|
74
85
|
const result = await runToolCommand({ runtime: { ...ctx.runtime, timeoutMs: Math.max(ctx.runtime.timeoutMs || 0, 90000) }, toolName: 'LOCAL_NOTIS_SAVE_REPORT', mutating: true,
|
|
75
86
|
idempotencyKey: nextIdempotencyKey(ctx.globalOptions), arguments_: {
|
|
76
|
-
operation: ctx.options.
|
|
77
|
-
|
|
87
|
+
operation: ctx.options.documentId ? 'update' : 'create',
|
|
88
|
+
title: ctx.options.title,
|
|
78
89
|
...(ctx.options.documentId ? { document_id: ctx.options.documentId, expected_revision: Number(ctx.options.expectedRevision) } : {}),
|
|
79
|
-
report: { schema: 'notis-report/
|
|
90
|
+
report: { schema: 'notis-report/v3', context, artifact: { manifest: release.manifest, files, encoding: 'base64' } },
|
|
80
91
|
} });
|
|
81
92
|
if (!result?.payload?.document?.id || !result.payload.document.view_revision) throw usageError('Save returned no record identity. Read back before retrying; the outcome may be unknown.');
|
|
82
93
|
return ctx.output.emitSuccess({ command: 'reports save', data: result.payload });
|
|
@@ -228,6 +228,7 @@ export async function runHarnessRoute({
|
|
|
228
228
|
timeoutMs = 10_000,
|
|
229
229
|
snapshotPath = null,
|
|
230
230
|
designViewports = DEFAULT_DESIGN_VIEWPORTS,
|
|
231
|
+
waitForRuntime = false,
|
|
231
232
|
}) {
|
|
232
233
|
const opened = await runAgentBrowser(['--session', sessionName, 'open', url], {
|
|
233
234
|
timeoutMs: Math.min(Math.max(timeoutMs, 5000), 30_000),
|
|
@@ -244,17 +245,21 @@ export async function runHarnessRoute({
|
|
|
244
245
|
}
|
|
245
246
|
|
|
246
247
|
const deadline = Date.now() + timeoutMs;
|
|
248
|
+
let readySince = null;
|
|
247
249
|
while (Date.now() < deadline) {
|
|
248
250
|
const lastRead = await readHarness(sessionName, 5000);
|
|
249
251
|
if (!lastRead.ok) {
|
|
252
|
+
readySince = null;
|
|
250
253
|
await delay(250);
|
|
251
254
|
continue;
|
|
252
255
|
}
|
|
253
256
|
const harness = lastRead.harness;
|
|
254
|
-
if (
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
257
|
+
if (Array.isArray(harness?.errors) && harness.errors.length > 0) break;
|
|
258
|
+
const pending = (harness?.runtimeCalls || []).some(call => call?.ok == null);
|
|
259
|
+
if (harness?.mounted && (!waitForRuntime || !pending)) {
|
|
260
|
+
readySince ??= Date.now();
|
|
261
|
+
if (Date.now() - readySince >= 250) break;
|
|
262
|
+
} else readySince = null;
|
|
258
263
|
await delay(250);
|
|
259
264
|
}
|
|
260
265
|
|
|
@@ -283,8 +288,8 @@ export async function runHarnessRoute({
|
|
|
283
288
|
}
|
|
284
289
|
}
|
|
285
290
|
|
|
286
|
-
const timedOut = !harness.mounted && Date.now() >= deadline;
|
|
287
291
|
const runtimeCalls = Array.isArray(harness.runtimeCalls) ? harness.runtimeCalls : [];
|
|
292
|
+
const timedOut = (!harness.mounted || (waitForRuntime && runtimeCalls.some(call => call?.ok == null))) && Date.now() >= deadline;
|
|
288
293
|
|
|
289
294
|
// Design assertions only mean something on a mounted route. Loading
|
|
290
295
|
// placeholders are excused while a runtime call is still in flight.
|
|
@@ -27,7 +27,7 @@ const BUNDLE_DIR = join(OUTPUT_DIR, 'bundle');
|
|
|
27
27
|
const MANIFEST_FILE = join(OUTPUT_DIR, 'manifest.json');
|
|
28
28
|
const METADATA_DIR = 'metadata';
|
|
29
29
|
const CLI_ROOT = resolve(dirname(fileURLToPath(import.meta.url)), '../..');
|
|
30
|
-
const TEMPLATE_SDK_DIR = join(CLI_ROOT, '
|
|
30
|
+
const TEMPLATE_SDK_DIR = join(CLI_ROOT, 'dist', 'sdk');
|
|
31
31
|
export const NOTIS_APP_CATEGORIES = [
|
|
32
32
|
'Productivity',
|
|
33
33
|
'Sales & Marketing',
|
|
@@ -286,13 +286,14 @@ export function detectProjectWarnings(projectDir, appConfig = null) {
|
|
|
286
286
|
appConfig?.capabilities?.workspaceDatabases === 'read';
|
|
287
287
|
if (
|
|
288
288
|
appConfig
|
|
289
|
+
&& appConfig.kind !== 'report'
|
|
289
290
|
&& !readsWorkspaceDatabases
|
|
290
291
|
&& (!Array.isArray(appConfig.databases) || appConfig.databases.length === 0)
|
|
291
292
|
) {
|
|
292
293
|
warnings.push('No database references declared in notis.config.ts.');
|
|
293
294
|
}
|
|
294
295
|
|
|
295
|
-
if (appConfig) {
|
|
296
|
+
if (appConfig && appConfig.kind !== 'report') {
|
|
296
297
|
try {
|
|
297
298
|
const listing = inspectListingReadiness(projectDir, appConfig);
|
|
298
299
|
warnings.push(...listing.errors, ...listing.warnings);
|
|
@@ -813,6 +814,29 @@ export function generateManifest(appConfig, projectDir) {
|
|
|
813
814
|
return entry;
|
|
814
815
|
});
|
|
815
816
|
|
|
817
|
+
if (appConfig.kind === 'report') {
|
|
818
|
+
if (routes.length !== 1 || routes[0].collection || routes[0].resourceDeepLinks) {
|
|
819
|
+
throw usageError('Reports require one standalone route, without app collections or resource deep links.');
|
|
820
|
+
}
|
|
821
|
+
if (['databases', 'skills', 'automations', 'onboarding', 'tagline', 'categories', 'author', 'screenshots'].some(key => {
|
|
822
|
+
const value = appConfig[key];
|
|
823
|
+
return Array.isArray(value) ? value.length > 0 : Boolean(value);
|
|
824
|
+
})) throw usageError('Reports cannot own databases, skills, automations or Store metadata.');
|
|
825
|
+
if (Object.values(appConfig.capabilities || {}).some(value => value !== 'read')) {
|
|
826
|
+
throw usageError('Reports support read capabilities only; shell grants require an installed app.');
|
|
827
|
+
}
|
|
828
|
+
const databaseAccess = appConfig.databaseAccess || [];
|
|
829
|
+
if (!Array.isArray(databaseAccess) || databaseAccess.some(entry =>
|
|
830
|
+
!entry || !/^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i.test(entry.id || '') ||
|
|
831
|
+
!['read', 'write'].includes(entry.access)
|
|
832
|
+
)) throw usageError('databaseAccess entries require an existing database UUID and read/write access.');
|
|
833
|
+
return { kind: 'report', version: 1, spec_version: 4,
|
|
834
|
+
routes: routes.map(route => ({ ...route, default: true, tool_access: { base: { tools: appConfig.tools || [] } } })),
|
|
835
|
+
bundle: { js: 'bundle/app.js', css: 'bundle/app.css' },
|
|
836
|
+
tools: appConfig.tools || [], tool_bindings: normalizeAppToolBindings(appConfig.toolBindings),
|
|
837
|
+
capabilities: normalizeAppCapabilities(appConfig.capabilities), database_access: databaseAccess };
|
|
838
|
+
}
|
|
839
|
+
|
|
816
840
|
// Database entries may be a bare slug (structure only) or an object opting
|
|
817
841
|
// into shipping rows to installers. Normalize to the manifest's snake_case.
|
|
818
842
|
const databases = (appConfig.databases || []).map((entry) => {
|
|
@@ -1847,7 +1871,7 @@ function listFilesRecursive(dir, base = dir, results = []) {
|
|
|
1847
1871
|
/**
|
|
1848
1872
|
* Keep an app's embedded `packages/sdk` copy identical to the SDK this CLI
|
|
1849
1873
|
* ships. The copy is a mirror by contract (the parity test in the CLI keeps
|
|
1850
|
-
* the
|
|
1874
|
+
* the bundled artifact generated from packages/sdk), so an app never edits it; before
|
|
1851
1875
|
* this sync existed every app silently ran whatever SDK snapshot it was
|
|
1852
1876
|
* scaffolded with and never received hook or style updates.
|
|
1853
1877
|
*
|
|
@@ -80,7 +80,7 @@ function resolveHarnessReactVersion(projectDir) {
|
|
|
80
80
|
const candidates = [
|
|
81
81
|
join(projectDir, 'node_modules', '@notis', 'sdk', 'package.json'),
|
|
82
82
|
join(REPO_ROOT, 'packages', 'sdk', 'package.json'),
|
|
83
|
-
join(CLI_ROOT, '
|
|
83
|
+
join(CLI_ROOT, 'dist', 'sdk', 'package.json'),
|
|
84
84
|
];
|
|
85
85
|
for (const candidate of candidates) {
|
|
86
86
|
const pkg = readJsonFile(candidate);
|
|
@@ -164,6 +164,7 @@ function buildHarnessDescriptor({ state, manifest, appConfig, route, scenario =
|
|
|
164
164
|
);
|
|
165
165
|
|
|
166
166
|
return {
|
|
167
|
+
...(state.resource ? { resource: state.resource } : {}),
|
|
167
168
|
app: {
|
|
168
169
|
id: state.appId || 'harness-app',
|
|
169
170
|
slug: state.slug,
|
|
@@ -207,8 +207,8 @@
|
|
|
207
207
|
}
|
|
208
208
|
|
|
209
209
|
async function runtimeQuery(body) {
|
|
210
|
-
if (!apiBase || !jwt || !descriptor.
|
|
211
|
-
throw new Error('live harness mode requires apiBase, jwt, and
|
|
210
|
+
if (!apiBase || !jwt || !(descriptor.resource?.kind === 'report' ? descriptor.resource.id : descriptor.app?.id)) {
|
|
211
|
+
throw new Error('live harness mode requires apiBase, jwt, and runtime resource identity');
|
|
212
212
|
}
|
|
213
213
|
const response = await fetch(`${String(apiBase).replace(/\/$/, '')}/portal_views/runtime_query`, {
|
|
214
214
|
method: 'POST',
|
|
@@ -217,7 +217,9 @@
|
|
|
217
217
|
Authorization: `Bearer ${jwt}`,
|
|
218
218
|
},
|
|
219
219
|
body: JSON.stringify({
|
|
220
|
-
|
|
220
|
+
...(descriptor.resource?.kind === 'report'
|
|
221
|
+
? { document_id: descriptor.resource.id, revision: descriptor.resource.revision }
|
|
222
|
+
: { app_id: descriptor.app.id }),
|
|
221
223
|
route_slug: descriptor.route && descriptor.route.slug,
|
|
222
224
|
...body,
|
|
223
225
|
}),
|
|
@@ -230,7 +232,10 @@
|
|
|
230
232
|
if (!payload) {
|
|
231
233
|
throw new Error(snippet ? `Runtime response was not JSON: ${snippet}` : 'Runtime response was empty');
|
|
232
234
|
}
|
|
233
|
-
if (payload.status === 'error' || payload.error
|
|
235
|
+
if (payload.status === 'error' || payload.error || (descriptor.resource?.kind === 'report' && (
|
|
236
|
+
['error', 'failed', 'failure'].includes(String(payload.status || '').trim().toLowerCase())
|
|
237
|
+
|| payload.ok === false || payload.success === false || payload.successful === false
|
|
238
|
+
))) {
|
|
234
239
|
throw new Error(payload.message || payload.error || 'Runtime request failed');
|
|
235
240
|
}
|
|
236
241
|
return payload;
|
|
@@ -238,6 +243,7 @@
|
|
|
238
243
|
|
|
239
244
|
function stubRuntime() {
|
|
240
245
|
return {
|
|
246
|
+
resource: descriptor.resource,
|
|
241
247
|
app: descriptor.app,
|
|
242
248
|
route: descriptor.route,
|
|
243
249
|
context: descriptor.context || {},
|
|
@@ -287,6 +293,7 @@
|
|
|
287
293
|
request: async (path, options) => tracked(
|
|
288
294
|
record('request', { path, options }),
|
|
289
295
|
async () => {
|
|
296
|
+
if (descriptor.resource?.kind === 'report') throw new Error('Reports access data through declared tools.');
|
|
290
297
|
if (fixtures && fixtures.requests && Object.prototype.hasOwnProperty.call(fixtures.requests, path)) {
|
|
291
298
|
return structuredClone(fixtures.requests[path]);
|
|
292
299
|
}
|
|
@@ -349,6 +356,7 @@
|
|
|
349
356
|
request: async (path, options) => tracked(
|
|
350
357
|
record('request', { path, options }),
|
|
351
358
|
async () => {
|
|
359
|
+
if (descriptor.resource?.kind === 'report') throw new Error('Reports access data through declared tools.');
|
|
352
360
|
const response = await fetch(`${String(apiBase).replace(/\/$/, '')}${path}`, {
|
|
353
361
|
method: (options && options.method) || 'GET',
|
|
354
362
|
headers: {
|
|
@@ -1,16 +0,0 @@
|
|
|
1
|
-
{
|
|
2
|
-
"compilerOptions": {
|
|
3
|
-
"types": ["node", "react", "react-dom"],
|
|
4
|
-
"target": "ES2022",
|
|
5
|
-
"module": "ESNext",
|
|
6
|
-
"moduleResolution": "bundler",
|
|
7
|
-
"jsx": "react-jsx",
|
|
8
|
-
"strict": true,
|
|
9
|
-
"esModuleInterop": true,
|
|
10
|
-
"skipLibCheck": true,
|
|
11
|
-
"declaration": true,
|
|
12
|
-
"outDir": "dist",
|
|
13
|
-
"rootDir": "src"
|
|
14
|
-
},
|
|
15
|
-
"include": ["src"]
|
|
16
|
-
}
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|