@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.
@@ -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
- chooses a trusted shadow root or an isolated Store frame; do not create your own
11
- iframe, query Portal-owned DOM, or install a window-global runtime. The host owns
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,
@@ -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 ?? [],
@@ -139,6 +139,7 @@ export type {
139
139
  NotisMarkdownEditorSavePayload,
140
140
  NotisMarkdownEditorSaveResult,
141
141
  NotisRuntime,
142
+ RuntimeResource,
142
143
  NotisRuntimeContext,
143
144
  NotisRuntimeUI,
144
145
  QueryFilter,
@@ -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
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@notis_ai/cli",
3
- "version": "0.2.0-beta.177.1",
3
+ "version": "0.2.0-beta.179.1",
4
4
  "description": "Agent-first Notis CLI for apps and generic tool execution",
5
5
  "type": "module",
6
6
  "bin": {
@@ -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 record-owned report, use `reports init → build → verify/preview → save` instead of the app deployment workflow above. Author exactly one route, select an existing app-owned database, and supply a readable context file. 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 the owning app or publish a Store listing.
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 record-owned SDK report locally.
232
+ Init a standalone live SDK report.
233
233
 
234
- When to use: Author an independent report without deploying its owning app.
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 apps scaffolds list`
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 record-owned SDK report locally.
241
+ Build a standalone live SDK report.
248
242
 
249
- When to use: Author an independent report without deploying its owning app.
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 record-owned SDK report locally.
250
+ Verify a standalone live SDK report.
258
251
 
259
- When to use: Author an independent report without deploying its owning app.
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 record-owned SDK report locally. Keeps the preview server and browser session open.
269
+ Preview a standalone live SDK report.
279
270
 
280
- When to use: Author an independent report without deploying its owning app.
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 report into an app-owned database record.
288
+ Build, verify and save a standalone live report document.
300
289
 
301
- When to use: Persist an independently authored report, not an app release.
290
+ When to use: Persist an independently owned report, not an app release.
302
291
 
303
292
  Options:
304
- - `--database-id <id>` — Required. Owning app database.
305
- - `--document-id <id>` — Existing record to update or attach to.
306
- - `--attach` — Attach to an existing non-view record.
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 --database-id <id> --title "Weekly review" --context-file ./context.md`
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
- linkedState = readLinkedState(projectDir, linkedStateProfileKey(ctx.runtime));
590
- if (!linkedState?.app_id) {
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 appConfig = await loadAppConfig(projectDir);
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(focus),
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
- const reuse = (command, name = command) => {
9
- const spec = appsCommandSpecs.find(item => item.command_path.join(' ') === `apps ${command}`);
10
- return {
11
- ...spec,
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
- const output = new Proxy(ctx.output, {
14
- get(target, key) {
15
- if (key === 'emitSuccess') return result => {
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
- reuse('init'), reuse('build'), reuse('verify'), reuse('verify', 'preview'),
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 report into an app-owned database record.',
43
- when_to_use: 'Persist an independently authored report, not an app release.',
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: '--database-id <id>', description: 'Required. Owning app database.' },
48
- { flags: '--document-id <id>', description: 'Existing record to update or attach to.' },
49
- { flags: '--attach', description: 'Attach to an existing non-view record.' },
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 --database-id <id> --title \"Weekly review\" --context-file ./context.md'], mutates: true, idempotent: true,
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 = resolveProjectDir(ctx.args.dir || '.');
60
- if (!ctx.options.databaseId || !ctx.options.title || !ctx.options.contextFile) throw usageError('--database-id, --title and --context-file are required.');
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/attach.');
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 verify = appsCommandSpecs.find(item => item.command_path.join(' ') === 'apps verify');
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.attach ? 'attach' : ctx.options.documentId ? 'update' : 'create',
77
- database_id: ctx.options.databaseId, title: ctx.options.title, properties,
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/v2', context, artifact: { manifest: release.manifest, files, encoding: 'base64' } },
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 });