@notis_ai/cli 0.2.0-beta.177.1 → 0.2.0-beta.179.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 +315 -154
- package/dist/base-skills/notis-apps/SKILL.md +2 -0
- package/dist/base-skills/notis-apps/references/architecture.md +5 -2
- package/dist/base-skills/notis-apps/references/release.md +44 -0
- 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 +20 -15
- package/src/command-specs/reports.js +61 -50
- package/src/runtime/agent-browser.js +66 -16
- package/src/runtime/app-platform.js +25 -1
- package/src/runtime/app-test-server.js +1 -0
- package/src/runtime/screenshot-framing.js +85 -0
- package/src/runtime/store-screenshot.js +10 -7
- package/template/.harness/index.html.tmpl +12 -4
|
@@ -46,6 +46,8 @@ For every app UI create/edit task, read both [Design](references/design.md) and
|
|
|
46
46
|
no-deploy, and explicit-consent requirements. Store publication is separate.
|
|
47
47
|
- Preserve the exact app identity, account/team scope, permissions, and user data.
|
|
48
48
|
Reconcile an uncertain release instead of blindly retrying it.
|
|
49
|
+
- Before Store submission, complete the [publication privacy and portability gate](references/release.md#publication-privacy-and-portability-gate).
|
|
50
|
+
Public examples must be fictional; never publish the owner's workspace data.
|
|
49
51
|
- Use SDK hooks and declared tools. Let Notis own its sidebar, search, runtime,
|
|
50
52
|
and rendering boundary; do not query host DOM or recreate that chrome in the app.
|
|
51
53
|
|
|
@@ -7,8 +7,11 @@ All Notis apps are built using the Notis CLI, either locally in a repo workspace
|
|
|
7
7
|
- the portal renders it as a React component inside the portal's React tree
|
|
8
8
|
|
|
9
9
|
Use standard React pages in `app/`, not Next.js or a custom server. The host
|
|
10
|
-
|
|
11
|
-
|
|
10
|
+
uses one React/Shadow DOM runtime for authored apps, Store installations,
|
|
11
|
+
duplicates and SDK reports. Do not create your own iframe, query Portal-owned
|
|
12
|
+
DOM, or install a window-global runtime. Shadow DOM isolates styles, not
|
|
13
|
+
JavaScript or credentials: loaded bundles share the Portal browser context.
|
|
14
|
+
The host owns
|
|
12
15
|
theme injection and authentication. App code uses SDK hooks and the final tool
|
|
13
16
|
names discovered through the CLI, declared in `notis.config.ts` and enforced by
|
|
14
17
|
the backend. Runtime permissions stay least-authority; releasing an app does not
|
|
@@ -65,6 +65,50 @@ not required for an ordinary Workspace update.
|
|
|
65
65
|
|
|
66
66
|
## Special cases — read only when relevant
|
|
67
67
|
|
|
68
|
+
### Publication privacy and portability gate
|
|
69
|
+
|
|
70
|
+
Before submitting or updating a Store listing:
|
|
71
|
+
|
|
72
|
+
1. Inventory the exact public source archive, listing text/media, database schemas,
|
|
73
|
+
starter rows, bundled skills (including scripts/references), and automation
|
|
74
|
+
prompts/configuration. Review their actual contents, not just filenames or
|
|
75
|
+
a passing secret scan. Exclude personal records, transcripts, health/journal
|
|
76
|
+
history, customer details, private repository/account identifiers, credentials,
|
|
77
|
+
local paths, run logs, and private links. Do not merely replace a person's name
|
|
78
|
+
in otherwise real data. Rebuild examples from wholly fictional scenarios.
|
|
79
|
+
2. Keep live owner databases structure-only (`seedDocuments` absent or false).
|
|
80
|
+
Opting in seeds the database's live rows, including folders: it is not a
|
|
81
|
+
fixture selector. Use fictional screenshot fixtures and an explicit, idempotent
|
|
82
|
+
onboarding demo-data option. If starter rows are needed in the install snapshot,
|
|
83
|
+
publish only from an isolated, verified fictional dataset; never replace or
|
|
84
|
+
delete the owner's real data to prepare a submission. When the user asks for
|
|
85
|
+
examples to come with the app, include them in that verified install snapshot:
|
|
86
|
+
screenshot fixtures or an optional onboarding seed step do not satisfy this.
|
|
87
|
+
3. Bundle the full dependency closure of every app/automation skill, including
|
|
88
|
+
referenced helpers and resources. Remove private account-specific defaults;
|
|
89
|
+
resolve the installer's databases, connections, repository, timezone and
|
|
90
|
+
delivery choices at runtime. Preserve existing owner's schedules and data.
|
|
91
|
+
4. Declare a source-owned onboarding skill. It must work through available MCP
|
|
92
|
+
tools or the Notis CLI in any agent harness, without requiring Notis Manager,
|
|
93
|
+
vendor-specific delegation tools, hidden local files, or publisher access.
|
|
94
|
+
Discover tools and inspect schemas before calls; reconcile existing resources
|
|
95
|
+
before creating them. Obtain installer choices before enabling automation or
|
|
96
|
+
external actions. Installing examples must not activate external deliveries.
|
|
97
|
+
5. Test onboarding as an independent harness-native proof agent using an account
|
|
98
|
+
isolated from the publisher, then rerun to prove no duplicates. Exercise each
|
|
99
|
+
route and its interactions with fictional data, including empty/error states.
|
|
100
|
+
Record exact identities, versions, results and run-created resource cleanup;
|
|
101
|
+
never use owner records as writable test fixtures.
|
|
102
|
+
For bundled starter-data claims, install the actual published listing into an
|
|
103
|
+
empty test account and read back its rows before onboarding or any manual data
|
|
104
|
+
writes. Confirm the installed listing version and remapped relations; do not
|
|
105
|
+
substitute an editable-source deployment for this Store-install test.
|
|
106
|
+
6. Inspect the final submitted snapshot and media after packaging. Record the
|
|
107
|
+
privacy audit and verification against that exact source version. Any unknown
|
|
108
|
+
provenance, missing dependency, untested onboarding or suspected personal data
|
|
109
|
+
blocks submission until resolved. Never equate submission with review approval
|
|
110
|
+
or Store publication.
|
|
111
|
+
|
|
68
112
|
### Unreleased container or stale checkout
|
|
69
113
|
|
|
70
114
|
An unreleased container has no source to pull. Recover its original local source,
|
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();
|
|
@@ -931,6 +935,7 @@ async function appsScreenshotHandler(ctx) {
|
|
|
931
935
|
sessionName: captureSessionName,
|
|
932
936
|
screenshotPath: browserScreenshotPath,
|
|
933
937
|
focusSelector: ctx.options.raw ? null : focus,
|
|
938
|
+
frameContent: !ctx.options.raw,
|
|
934
939
|
width,
|
|
935
940
|
height,
|
|
936
941
|
timeoutMs: Number.parseInt(ctx.globalOptions.timeoutMs || '', 10) || 15_000,
|
|
@@ -945,7 +950,7 @@ async function appsScreenshotHandler(ctx) {
|
|
|
945
950
|
height,
|
|
946
951
|
accent: appConfig.accent,
|
|
947
952
|
seed: appConfig.name || manifest.app?.name || appSlug,
|
|
948
|
-
focused: Boolean(
|
|
953
|
+
focused: Boolean(result.framing?.focus_selector),
|
|
949
954
|
theme: theme || 'light',
|
|
950
955
|
});
|
|
951
956
|
} catch (error) {
|
|
@@ -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 });
|