@notis_ai/cli 0.2.0-beta.177.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 +187 -142
- package/dist/sdk/src/config.ts +4 -0
- package/dist/sdk/src/hooks/useNotis.ts +3 -1
- package/dist/sdk/src/index.ts +1 -0
- package/dist/sdk/src/runtime.ts +13 -0
- 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/reports.js +61 -50
- package/src/runtime/agent-browser.js +10 -5
- package/src/runtime/app-platform.js +25 -1
- package/src/runtime/app-test-server.js +1 -0
- package/template/.harness/index.html.tmpl +12 -4
package/dist/sdk/src/config.ts
CHANGED
|
@@ -196,6 +196,10 @@ export interface NotisAppToolBinding {
|
|
|
196
196
|
}
|
|
197
197
|
|
|
198
198
|
export interface NotisAppConfig {
|
|
199
|
+
/** Standalone reports share the view engine without app installation. */
|
|
200
|
+
kind?: 'app' | 'report';
|
|
201
|
+
/** Existing data sources, never owned or materialized by a report. */
|
|
202
|
+
databaseAccess?: Array<{ id: string; access: 'read' | 'write' }>;
|
|
199
203
|
/** URL-safe app slug. Existing apps may still use a display name here. */
|
|
200
204
|
name: string;
|
|
201
205
|
/** Human display title, Raycast-style. Falls back to `name`. */
|
|
@@ -1,9 +1,10 @@
|
|
|
1
1
|
'use client';
|
|
2
2
|
|
|
3
3
|
import { useNotisRuntime } from '../provider';
|
|
4
|
-
import type { AppDescriptor, CollectionItemDetail, DatabaseDescriptor, RouteDescriptor } from '../runtime';
|
|
4
|
+
import type { RuntimeResource, AppDescriptor, CollectionItemDetail, DatabaseDescriptor, RouteDescriptor } from '../runtime';
|
|
5
5
|
|
|
6
6
|
interface NotisContext {
|
|
7
|
+
resource: RuntimeResource | null;
|
|
7
8
|
/** App metadata (id, name, icon, description). Null before runtime loads. */
|
|
8
9
|
app: AppDescriptor | null;
|
|
9
10
|
/** Current route descriptor. Null before runtime loads. */
|
|
@@ -27,6 +28,7 @@ export function useNotis(): NotisContext {
|
|
|
27
28
|
const runtime = useNotisRuntime();
|
|
28
29
|
|
|
29
30
|
return {
|
|
31
|
+
resource: runtime?.resource ?? null,
|
|
30
32
|
app: runtime?.app ?? null,
|
|
31
33
|
route: runtime?.route ?? null,
|
|
32
34
|
databases: runtime?.databases ?? [],
|
package/dist/sdk/src/index.ts
CHANGED
package/dist/sdk/src/runtime.ts
CHANGED
|
@@ -364,7 +364,20 @@ export interface CloudComputerFacts {
|
|
|
364
364
|
cli_auth: { gh: CloudComputerCliAuthFacts };
|
|
365
365
|
}
|
|
366
366
|
|
|
367
|
+
export interface RuntimeResource {
|
|
368
|
+
kind: 'app' | 'report';
|
|
369
|
+
/** Host-provided adoption eligibility, never report-authored authority. */
|
|
370
|
+
analytics_eligible?: boolean;
|
|
371
|
+
id: string;
|
|
372
|
+
revision: number;
|
|
373
|
+
name?: string;
|
|
374
|
+
icon?: string | null;
|
|
375
|
+
description?: string | null;
|
|
376
|
+
}
|
|
377
|
+
|
|
367
378
|
export interface NotisRuntime {
|
|
379
|
+
/** Authenticated runtime identity; app below is presentation metadata only. */
|
|
380
|
+
resource?: RuntimeResource;
|
|
368
381
|
/** Optional host-scoped in-memory read cache. Older hosts remain supported. */
|
|
369
382
|
queryClient?: NotisQueryClient;
|
|
370
383
|
app: AppDescriptor;
|
package/package.json
CHANGED
package/skills/notis-apps/cli.md
CHANGED
|
@@ -28,7 +28,7 @@ change the package release label, check resource compatibility and deploy as a n
|
|
|
28
28
|
|
|
29
29
|
## Reports
|
|
30
30
|
|
|
31
|
-
For a
|
|
31
|
+
For a standalone live report, use `reports init → build → verify/preview → save` instead of the app deployment workflow above. Author exactly one route with kind: report, declare its tools, and supply a readable context file. No installed app or database is required. Revisions preserve the record ID and require its freshly read revision. See the product `notis-reports` skill for source recovery, ownership and readback. These commands do not deploy an app or publish a Store listing.
|
|
32
32
|
|
|
33
33
|
## Commands
|
|
34
34
|
|
|
@@ -229,85 +229,71 @@ Examples:
|
|
|
229
229
|
|
|
230
230
|
### `npx --package @notis_ai/cli@latest -- notis reports init <name> [dir]`
|
|
231
231
|
|
|
232
|
-
Init a
|
|
232
|
+
Init a standalone live SDK report.
|
|
233
233
|
|
|
234
|
-
When to use: Author
|
|
235
|
-
|
|
236
|
-
Options:
|
|
237
|
-
- `--from <slug>` — Start from a published Store app listed by `notis apps scaffolds list`. Downloads its source from the public app registry.
|
|
234
|
+
When to use: Author a private standalone document using the shared view runtime; no app or database is required.
|
|
238
235
|
|
|
239
236
|
Examples:
|
|
240
|
-
- `npx --package @notis_ai/cli@latest -- notis
|
|
241
|
-
- `npx --package @notis_ai/cli@latest -- notis reports init "Mind the Flo"`
|
|
242
|
-
- `npx --package @notis_ai/cli@latest -- notis reports init "My CRM" --from databases`
|
|
243
|
-
- `npx --package @notis_ai/cli@latest -- notis reports init "My App" ~/code/my-app`
|
|
237
|
+
- `npx --package @notis_ai/cli@latest -- notis reports init "Weekly report" ./weekly-report`
|
|
244
238
|
|
|
245
239
|
### `npx --package @notis_ai/cli@latest -- notis reports build [dir]`
|
|
246
240
|
|
|
247
|
-
Build a
|
|
241
|
+
Build a standalone live SDK report.
|
|
248
242
|
|
|
249
|
-
When to use: Author
|
|
243
|
+
When to use: Author a private standalone document using the shared view runtime; no app or database is required.
|
|
250
244
|
|
|
251
245
|
Examples:
|
|
252
|
-
- `npx --package @notis_ai/cli@latest -- notis reports build`
|
|
253
|
-
- `npx --package @notis_ai/cli@latest -- notis reports build ./my-app`
|
|
246
|
+
- `npx --package @notis_ai/cli@latest -- notis reports build ./weekly-report`
|
|
254
247
|
|
|
255
248
|
### `npx --package @notis_ai/cli@latest -- notis reports verify [dir]`
|
|
256
249
|
|
|
257
|
-
Verify a
|
|
250
|
+
Verify a standalone live SDK report.
|
|
258
251
|
|
|
259
|
-
When to use: Author
|
|
252
|
+
When to use: Author a private standalone document using the shared view runtime; no app or database is required.
|
|
260
253
|
|
|
261
254
|
Options:
|
|
262
255
|
- `--routes <slugs>` — Comma-separated route slugs. Default: every route in manifest.
|
|
263
256
|
- `--port <n>` — Loopback port. Default: auto-pick.
|
|
264
257
|
- `--skip-build` — Skip notis apps build; reuse existing .notis/output/.
|
|
265
258
|
- `--mode <mode>` — stub | live. Default stub. Live posts to /portal_views/runtime_query with the CLI JWT and fails routes whose runtime calls all errored.
|
|
266
|
-
- `--listing` — Ignored for reports; saving a report does not publish a Store listing.
|
|
267
259
|
- `--no-browser` — Start the harness server and print URLs; do not drive agent-browser.
|
|
268
260
|
- `--keep-open` — Leave server + browser session running after report (for manual triage).
|
|
261
|
+
- `--document-id <id>` — Saved report for live verification.
|
|
262
|
+
- `--expected-revision <revision>` — Saved report revision for live verification.
|
|
269
263
|
|
|
270
264
|
Examples:
|
|
271
|
-
- `npx --package @notis_ai/cli@latest -- notis reports verify`
|
|
272
|
-
- `npx --package @notis_ai/cli@latest -- notis reports verify --routes notes`
|
|
273
|
-
- `npx --package @notis_ai/cli@latest -- notis reports verify --mode live`
|
|
274
|
-
- `npx --package @notis_ai/cli@latest -- notis reports verify --no-browser # start the harness, drive agent-browser yourself`
|
|
265
|
+
- `npx --package @notis_ai/cli@latest -- notis reports verify ./weekly-report`
|
|
275
266
|
|
|
276
267
|
### `npx --package @notis_ai/cli@latest -- notis reports preview [dir]`
|
|
277
268
|
|
|
278
|
-
Preview a
|
|
269
|
+
Preview a standalone live SDK report.
|
|
279
270
|
|
|
280
|
-
When to use: Author
|
|
271
|
+
When to use: Author a private standalone document using the shared view runtime; no app or database is required.
|
|
281
272
|
|
|
282
273
|
Options:
|
|
283
274
|
- `--routes <slugs>` — Comma-separated route slugs. Default: every route in manifest.
|
|
284
275
|
- `--port <n>` — Loopback port. Default: auto-pick.
|
|
285
276
|
- `--skip-build` — Skip notis apps build; reuse existing .notis/output/.
|
|
286
277
|
- `--mode <mode>` — stub | live. Default stub. Live posts to /portal_views/runtime_query with the CLI JWT and fails routes whose runtime calls all errored.
|
|
287
|
-
- `--listing` — Ignored for reports; saving a report does not publish a Store listing.
|
|
288
278
|
- `--no-browser` — Start the harness server and print URLs; do not drive agent-browser.
|
|
289
279
|
- `--keep-open` — Leave server + browser session running after report (for manual triage).
|
|
280
|
+
- `--document-id <id>` — Saved report for live verification.
|
|
281
|
+
- `--expected-revision <revision>` — Saved report revision for live verification.
|
|
290
282
|
|
|
291
283
|
Examples:
|
|
292
|
-
- `npx --package @notis_ai/cli@latest -- notis reports preview`
|
|
293
|
-
- `npx --package @notis_ai/cli@latest -- notis reports preview --routes notes`
|
|
294
|
-
- `npx --package @notis_ai/cli@latest -- notis reports preview --mode live`
|
|
295
|
-
- `npx --package @notis_ai/cli@latest -- notis reports preview --no-browser # start the harness, drive agent-browser yourself`
|
|
284
|
+
- `npx --package @notis_ai/cli@latest -- notis reports preview ./weekly-report`
|
|
296
285
|
|
|
297
286
|
### `npx --package @notis_ai/cli@latest -- notis reports save [dir]`
|
|
298
287
|
|
|
299
|
-
Build, verify and save a
|
|
288
|
+
Build, verify and save a standalone live report document.
|
|
300
289
|
|
|
301
|
-
When to use: Persist an independently
|
|
290
|
+
When to use: Persist an independently owned report, not an app release.
|
|
302
291
|
|
|
303
292
|
Options:
|
|
304
|
-
- `--
|
|
305
|
-
- `--
|
|
306
|
-
- `--
|
|
307
|
-
- `--expected-revision <revision>` — Fresh view revision (0 for a record without a view).
|
|
308
|
-
- `--title <title>` — Required, including updates. Record title.
|
|
293
|
+
- `--document-id <id>` — Existing report document to update.
|
|
294
|
+
- `--expected-revision <revision>` — Current saved report revision; required for updates.
|
|
295
|
+
- `--title <title>` — Required, including updates. Document title.
|
|
309
296
|
- `--context-file <file>` — Required. UTF-8 readable report content and structure.
|
|
310
|
-
- `--properties-file <file>` — JSON database property values keyed by name.
|
|
311
297
|
|
|
312
298
|
Examples:
|
|
313
|
-
- `npx --package @notis_ai/cli@latest -- notis reports save ./weekly-report --
|
|
299
|
+
- `npx --package @notis_ai/cli@latest -- notis reports save ./weekly-report --title "Weekly review" --context-file ./context.md`
|
|
@@ -276,6 +276,9 @@ function assertHarnessResult(result, route, databaseSlugs, mode = 'stub', capabi
|
|
|
276
276
|
});
|
|
277
277
|
}
|
|
278
278
|
if (mode === 'live') {
|
|
279
|
+
if (runtimeCalls.some(call => call?.ok == null)) {
|
|
280
|
+
assertions.push({ ok: false, code: 'runtime_calls_pending', message: 'Live verification ended before every started runtime call settled.' });
|
|
281
|
+
}
|
|
279
282
|
// In live mode an app that catches every failed call and renders its error
|
|
280
283
|
// state still mounts cleanly, so the render assertions above all pass. Only
|
|
281
284
|
// the recorded outcomes reveal that nothing real came back.
|
|
@@ -379,7 +382,7 @@ async function appsListHandler(ctx) {
|
|
|
379
382
|
});
|
|
380
383
|
}
|
|
381
384
|
|
|
382
|
-
async function appsInitHandler(ctx) {
|
|
385
|
+
export async function appsInitHandler(ctx) {
|
|
383
386
|
const projectDir = ctx.args.dir
|
|
384
387
|
? resolveProjectDir(ctx.args.dir)
|
|
385
388
|
: defaultAppProjectDir(slugify(ctx.args.name));
|
|
@@ -504,7 +507,7 @@ async function appsCreateHandler(ctx) {
|
|
|
504
507
|
});
|
|
505
508
|
}
|
|
506
509
|
|
|
507
|
-
async function appsBuildHandler(ctx) {
|
|
510
|
+
export async function appsBuildHandler(ctx) {
|
|
508
511
|
const projectDir = resolveProjectDir(ctx.args.dir || '.');
|
|
509
512
|
const problems = detectProjectProblems(projectDir);
|
|
510
513
|
if (problems.length) {
|
|
@@ -563,13 +566,15 @@ async function closeHarnessResources(sessionNames, testServer, rawOutputDir = nu
|
|
|
563
566
|
}
|
|
564
567
|
}
|
|
565
568
|
|
|
566
|
-
async function appsVerifyHandler(ctx) {
|
|
569
|
+
export async function appsVerifyHandler(ctx) {
|
|
567
570
|
const projectDir = resolveProjectDir(ctx.args.dir || '.');
|
|
568
571
|
const problems = detectProjectProblems(projectDir);
|
|
569
572
|
if (problems.length) {
|
|
570
573
|
throw usageError(`Project has problems:\n${problems.map((p) => ` - ${p}`).join('\n')}`);
|
|
571
574
|
}
|
|
572
575
|
|
|
576
|
+
const appConfig = await loadAppConfig(projectDir);
|
|
577
|
+
const isReport = appConfig.kind === 'report';
|
|
573
578
|
const mode = ctx.options.mode || 'stub';
|
|
574
579
|
if (!['stub', 'live'].includes(mode)) {
|
|
575
580
|
throw usageError('--mode must be either "stub" or "live".');
|
|
@@ -586,8 +591,10 @@ async function appsVerifyHandler(ctx) {
|
|
|
586
591
|
if (!ctx.runtime.jwt) {
|
|
587
592
|
throw usageError('Live verify mode requires CLI auth. Run notis login and retry.');
|
|
588
593
|
}
|
|
589
|
-
|
|
590
|
-
|
|
594
|
+
if (isReport) {
|
|
595
|
+
if (!ctx.options.documentId || !/^\d+$/.test(String(ctx.options.expectedRevision ?? ''))) throw usageError('Live report verification requires --document-id and --expected-revision of the saved report.');
|
|
596
|
+
} else linkedState = readLinkedState(projectDir, linkedStateProfileKey(ctx.runtime));
|
|
597
|
+
if (!isReport && !linkedState?.app_id) {
|
|
591
598
|
throw usageError('Live verify mode requires a linked app. Run `notis apps link <app-id> .` first.');
|
|
592
599
|
}
|
|
593
600
|
}
|
|
@@ -599,8 +606,7 @@ async function appsVerifyHandler(ctx) {
|
|
|
599
606
|
}
|
|
600
607
|
|
|
601
608
|
const manifest = readManifest(projectDir);
|
|
602
|
-
const
|
|
603
|
-
const listing = inspectListingReadiness(projectDir, appConfig);
|
|
609
|
+
const listing = isReport ? { ready: true, errors: [], warnings: [] } : inspectListingReadiness(projectDir, appConfig);
|
|
604
610
|
// Store readiness is a publish concern, not a render concern. Verify reports
|
|
605
611
|
// it so the gaps stay visible while the app is still being built; only
|
|
606
612
|
// --listing (and `apps publish`) turn it back into a hard gate.
|
|
@@ -609,7 +615,7 @@ async function appsVerifyHandler(ctx) {
|
|
|
609
615
|
}
|
|
610
616
|
const listingWarnings = [
|
|
611
617
|
...[...listing.errors, ...listing.warnings].map((message) => `Store readiness: ${message}`),
|
|
612
|
-
...findUnknownScreenshotScenarios(projectDir, resolveListingScreenshots(projectDir, appConfig)),
|
|
618
|
+
...(isReport ? [] : findUnknownScreenshotScenarios(projectDir, resolveListingScreenshots(projectDir, appConfig))),
|
|
613
619
|
];
|
|
614
620
|
const routes = routeSelection(manifest, parseRouteSlugs(ctx.options.routes));
|
|
615
621
|
const port = parsePort(ctx.options.port) || await getAvailablePort();
|
|
@@ -635,6 +641,7 @@ async function appsVerifyHandler(ctx) {
|
|
|
635
641
|
slug: appSlug,
|
|
636
642
|
projectDir,
|
|
637
643
|
appId: linkedState?.app_id || 'harness-app',
|
|
644
|
+
resource: isReport ? { kind: 'report', id: ctx.options.documentId || 'preview', revision: Number(ctx.options.expectedRevision || 0) } : undefined,
|
|
638
645
|
}],
|
|
639
646
|
port,
|
|
640
647
|
harness: {
|
|
@@ -708,7 +715,8 @@ async function appsVerifyHandler(ctx) {
|
|
|
708
715
|
const result = await runHarnessRoute({
|
|
709
716
|
url,
|
|
710
717
|
sessionName: browserSessionName,
|
|
711
|
-
timeoutMs: Number.parseInt(ctx.globalOptions.timeoutMs || '', 10) || 10_000,
|
|
718
|
+
timeoutMs: Number.parseInt(ctx.globalOptions.timeoutMs || '', 10) || (mode === 'live' ? 90_000 : 10_000),
|
|
719
|
+
waitForRuntime: mode === 'live',
|
|
712
720
|
snapshotPath,
|
|
713
721
|
});
|
|
714
722
|
const assertions = assertHarnessResult(
|
|
@@ -766,11 +774,7 @@ async function appsVerifyHandler(ctx) {
|
|
|
766
774
|
},
|
|
767
775
|
summary,
|
|
768
776
|
results,
|
|
769
|
-
listing: {
|
|
770
|
-
ready: listing.ready,
|
|
771
|
-
gated: ctx.options.listing === true,
|
|
772
|
-
problems: listing.errors,
|
|
773
|
-
},
|
|
777
|
+
...(!isReport ? { listing: { ready: listing.ready, gated: ctx.options.listing === true, problems: listing.errors } } : {}),
|
|
774
778
|
};
|
|
775
779
|
|
|
776
780
|
if (!keepOpen) await cleanup();
|
|
@@ -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.
|
|
@@ -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) => {
|
|
@@ -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: {
|