@frontera-sdk/cli 1.43.10 → 1.44.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 +140 -12
- package/package.json +3 -3
- package/src/adopt.ts +436 -0
- package/src/api/apps-api.ts +30 -0
- package/src/api/blueprint-authoring-api.ts +13 -2
- package/src/api/governed-action-api.ts +192 -0
- package/src/api/platform-api.ts +4 -0
- package/src/blueprint/ontology-edit-plan.ts +195 -0
- package/src/blueprint-types.ts +252 -0
- package/src/commands/action/deploy.ts +135 -0
- package/src/commands/action/grant.ts +68 -0
- package/src/commands/action/index-commands.ts +29 -0
- package/src/commands/action/list.ts +49 -0
- package/src/commands/action/prepare.ts +48 -0
- package/src/commands/action/review.ts +94 -0
- package/src/commands/app/deploy.ts +16 -5
- package/src/commands/app/dev.ts +173 -0
- package/src/commands/app/init.ts +270 -28
- package/src/commands/app/sdk.ts +31 -0
- package/src/commands/app/versions.ts +8 -1
- package/src/commands/blueprint/editable.ts +151 -0
- package/src/commands/blueprint/generate-types.ts +58 -0
- package/src/commands/blueprint/get.ts +29 -34
- package/src/commands/blueprint/list.ts +2 -1
- package/src/commands/registry.ts +12 -0
- package/src/context.ts +4 -4
- package/src/dev-broker.ts +71 -0
- package/src/flag-help.ts +24 -1
- package/src/heal.ts +37 -2
- package/src/manifest.ts +89 -8
- package/src/packaging.ts +6 -0
- package/src/project-bootstrap.ts +176 -0
- package/src/project.ts +68 -35
- package/src/provenance.ts +89 -0
- package/src/render-evidence.ts +28 -0
- package/src/sdk-sync.ts +41 -0
- package/src/shadcn-components.ts +106 -0
- package/src/static-app-validation.ts +67 -0
- package/src/template.ts +211 -32
- package/src/templates/next-app-files.ts +1052 -0
- package/src/templates/next-skills.ts +1216 -0
- package/src/vendor/sdk-sources.json +21 -15
package/src/template.ts
CHANGED
|
@@ -1,23 +1,23 @@
|
|
|
1
1
|
import { mkdirSync, writeFileSync } from 'node:fs'
|
|
2
2
|
import { join, dirname } from 'node:path'
|
|
3
3
|
|
|
4
|
+
import { nextAppFiles } from './templates/next-app-files'
|
|
5
|
+
import { nextSkillFiles } from './templates/next-skills'
|
|
4
6
|
import vendored from './vendor/sdk-sources.json'
|
|
5
7
|
|
|
6
8
|
/**
|
|
7
9
|
* Project scaffold.
|
|
8
10
|
*
|
|
9
|
-
*
|
|
10
|
-
*
|
|
11
|
-
*
|
|
12
|
-
*
|
|
13
|
-
*
|
|
14
|
-
* dependency — see `scripts/sync-sdk.ts` for why. The import specifiers still
|
|
15
|
-
* read `@frontera-sdk/core/…`; `tsconfig` paths and a matching Vite alias
|
|
16
|
-
* resolve them. So `package.json` needs nothing but public npm, and a project
|
|
17
|
-
* pulled onto a machine that has never seen this monorepo still builds.
|
|
11
|
+
* New projects are ordinary Next.js applications using published Frontera SDK
|
|
12
|
+
* packages. The explicit legacy React template keeps its vendored SDK tree so
|
|
13
|
+
* existing Vite applications remain reproducible and can opt into `sdk sync`.
|
|
14
|
+
* In both cases package.json stays standard package metadata; the static-host
|
|
15
|
+
* contract belongs to frontera.config.json.
|
|
18
16
|
*/
|
|
19
|
-
export
|
|
20
|
-
|
|
17
|
+
export type AppFramework = 'next' | 'react'
|
|
18
|
+
|
|
19
|
+
export function scaffold(target: string, name: string, framework: AppFramework = 'next'): void {
|
|
20
|
+
for (const [rel, content] of Object.entries(scaffoldFiles(name, framework))) {
|
|
21
21
|
const full = join(target, rel)
|
|
22
22
|
mkdirSync(dirname(full), { recursive: true })
|
|
23
23
|
writeFileSync(full, content)
|
|
@@ -30,8 +30,11 @@ export function scaffold(target: string, name: string): void {
|
|
|
30
30
|
* shipped SKILL.md actually exists, and that every `@frontera-sdk/…` import
|
|
31
31
|
* resolves to a file the scaffold actually writes.
|
|
32
32
|
*/
|
|
33
|
-
export function scaffoldFiles(
|
|
34
|
-
|
|
33
|
+
export function scaffoldFiles(
|
|
34
|
+
name: string,
|
|
35
|
+
framework: AppFramework = 'next',
|
|
36
|
+
): Record<string, string> {
|
|
37
|
+
return framework === 'react' ? files(name) : nextFiles(name)
|
|
35
38
|
}
|
|
36
39
|
|
|
37
40
|
/**
|
|
@@ -117,18 +120,6 @@ function files(name: string): Record<string, string> {
|
|
|
117
120
|
private: true,
|
|
118
121
|
version: '0.1.0',
|
|
119
122
|
type: 'module',
|
|
120
|
-
frontera: {
|
|
121
|
-
// No slug: the platform owns it and can rename it, so a committed
|
|
122
|
-
// copy would go stale. `name` above seeds it on the first deploy, and
|
|
123
|
-
// `frontera.appId` is written back once the app exists.
|
|
124
|
-
displayName: name,
|
|
125
|
-
// Origins this app may reach at runtime, enforced as a CSP by the
|
|
126
|
-
// host. `frontera app deploy` turns these into the version's
|
|
127
|
-
// manifest, so an addition takes effect on the next deploy — add one
|
|
128
|
-
// deliberately rather than discovering the block in production.
|
|
129
|
-
connectDomains: [],
|
|
130
|
-
resourceDomains: [],
|
|
131
|
-
},
|
|
132
123
|
scripts: {
|
|
133
124
|
dev: 'vite',
|
|
134
125
|
build: 'vite build',
|
|
@@ -158,6 +149,21 @@ function files(name: string): Record<string, string> {
|
|
|
158
149
|
2,
|
|
159
150
|
)}\n`,
|
|
160
151
|
|
|
152
|
+
// Frontera deployment configuration is deliberately separate from npm's
|
|
153
|
+
// package manifest. package.json remains standard package metadata; this
|
|
154
|
+
// file describes how the already-built artifact is hosted.
|
|
155
|
+
'frontera.config.json': `${JSON.stringify(
|
|
156
|
+
{
|
|
157
|
+
displayName: name,
|
|
158
|
+
outputDirectory: 'dist',
|
|
159
|
+
routing: 'spa',
|
|
160
|
+
connectDomains: [],
|
|
161
|
+
resourceDomains: [],
|
|
162
|
+
},
|
|
163
|
+
null,
|
|
164
|
+
2,
|
|
165
|
+
)}\n`,
|
|
166
|
+
|
|
161
167
|
'vite.config.ts': `import { fileURLToPath, URL } from 'node:url'
|
|
162
168
|
import { defineConfig } from 'vite'
|
|
163
169
|
import react from '@vitejs/plugin-react'
|
|
@@ -306,7 +312,7 @@ dist/
|
|
|
306
312
|
|
|
307
313
|
'.agents/skills/using-frontera-sdk/SKILL.md': `---
|
|
308
314
|
name: using-frontera-sdk
|
|
309
|
-
description: Use at the start of ANY task in a Frontera app project
|
|
315
|
+
description: Use at the start of ANY task in a Frontera Vite app project with frontera.config.json that deploys via the frontera CLI. Establishes the project shape and routes to the right pattern.
|
|
310
316
|
---
|
|
311
317
|
|
|
312
318
|
# Frontera App Development
|
|
@@ -395,13 +401,13 @@ handshake must not ship with the app.
|
|
|
395
401
|
|
|
396
402
|
'.agents/skills/frontera-app-data/SKILL.md': `---
|
|
397
403
|
name: frontera-app-data
|
|
398
|
-
description: Use when reading platform data in a Frontera app — useObjects, useAggregate, useObjectInstance, where clauses, paging, and why filtering must happen on the server.
|
|
404
|
+
description: Use when reading platform data in a Frontera app — useObjects, useAggregate, useMetric, useObjectInstance, where clauses, paging, and why filtering must happen on the server.
|
|
399
405
|
---
|
|
400
406
|
|
|
401
407
|
# Reading Blueprint data
|
|
402
408
|
|
|
403
409
|
\`\`\`tsx
|
|
404
|
-
import { useObjects, useAggregate } from '@frontera-sdk/blueprint/hooks'
|
|
410
|
+
import { useObjects, useAggregate, useMetric } from '@frontera-sdk/blueprint/hooks'
|
|
405
411
|
import { objectsOf, type WhereNode } from '@frontera-sdk/blueprint/types'
|
|
406
412
|
\`\`\`
|
|
407
413
|
|
|
@@ -419,7 +425,7 @@ const where: WhereNode | undefined = status
|
|
|
419
425
|
? { property: 'deliveryStatus', op: 'eq', value: status }
|
|
420
426
|
: undefined
|
|
421
427
|
|
|
422
|
-
const rows = useObjects<Shipment>('Shipment', { where,
|
|
428
|
+
const rows = useObjects<Shipment>('Shipment', { where, pageToken, pageSize: 20 })
|
|
423
429
|
\`\`\`
|
|
424
430
|
|
|
425
431
|
Filtering \`rows.data.rows\` in the component instead is the classic mistake: it
|
|
@@ -429,15 +435,165 @@ renders 5 of them and the footer claims "Page 1 of 1".
|
|
|
429
435
|
For the same reason, a total is its own query — \`useAggregate\` with a count
|
|
430
436
|
over the SAME object set — not \`rows.length\`.
|
|
431
437
|
|
|
432
|
-
## Paging
|
|
438
|
+
## Paging is a cursor
|
|
433
439
|
|
|
434
|
-
\`
|
|
435
|
-
|
|
440
|
+
Omit \`pageToken\` for the first request, then pass the response's opaque
|
|
441
|
+
\`nextPageToken\` to advance. Keep earlier tokens in UI state if Previous is
|
|
442
|
+
required, and reset them whenever filters, sort or projection change. Numeric
|
|
443
|
+
page labels and aggregate totals are presentation; they are not Blueprint
|
|
444
|
+
navigation inputs.
|
|
445
|
+
\`\`\`tsx
|
|
446
|
+
const [token, setToken] = useState<string | undefined>()
|
|
447
|
+
const page = useObjects<Ticket>('SupportTicket', { pageSize: 25, pageToken: token })
|
|
448
|
+
// next setToken(page.data?.nextPageToken)
|
|
449
|
+
// restart setToken(undefined)
|
|
450
|
+
\`\`\`
|
|
451
|
+
|
|
452
|
+
There is **no page number**. Offset paging was removed — it has no defined
|
|
453
|
+
meaning over an unordered scan — and the service refuses a request carrying
|
|
454
|
+
\`page\` outright. \`hasMore\` is false and \`nextPageToken\` absent on the last
|
|
455
|
+
page; its absence is how a scan learns it has finished.
|
|
456
|
+
|
|
457
|
+
There is also no total, so a "Page 3 of 12" footer cannot be built from this.
|
|
458
|
+
Show a count from \`useAggregate\` over the same object set if you need one.
|
|
436
459
|
|
|
437
460
|
## Identity
|
|
438
461
|
|
|
439
462
|
\`useObjectInstance(objectType, pk)\` returns one record and is disabled while
|
|
440
463
|
\`pk\` is null, so a detail panel can mount before a row is selected.
|
|
464
|
+
|
|
465
|
+
## Metrics the organization already defined
|
|
466
|
+
|
|
467
|
+
\`\`\`tsx
|
|
468
|
+
const revenue = useMetric('codShare', { from, to })
|
|
469
|
+
\`\`\`
|
|
470
|
+
|
|
471
|
+
A metric exists so every reader computes it the same way. Prefer one over
|
|
472
|
+
\`useAggregate\` whenever it exists: re-deriving a defined figure from raw
|
|
473
|
+
columns is how two dashboards disagree about one number, and it is where the
|
|
474
|
+
arithmetic bugs live. \`frontera blueprint list\` shows what is defined.
|
|
475
|
+
|
|
476
|
+
Do NOT hand-roll a hook around \`useBlueprintClient\` for this — that escape
|
|
477
|
+
hatch is for calls with no hook, and a metric has one.
|
|
478
|
+
|
|
479
|
+
## The escape hatch
|
|
480
|
+
|
|
481
|
+
\`useBlueprintClient()\` returns the raw client for anything the hooks do not
|
|
482
|
+
cover. Everything above is built on it, so reach for it last, not first.
|
|
483
|
+
`,
|
|
484
|
+
|
|
485
|
+
'.agents/skills/frontera-app-actions/SKILL.md': `---
|
|
486
|
+
name: frontera-app-actions
|
|
487
|
+
description: Use when an app must CHANGE something — governed Actions, useAction/useSubmitAction/useActionRequest, why a button may not exist for every user, and why submitting is not the same as done.
|
|
488
|
+
---
|
|
489
|
+
|
|
490
|
+
# Changing data through a governed Action
|
|
491
|
+
|
|
492
|
+
\`\`\`tsx
|
|
493
|
+
import { useAction, useSubmitAction, useActionRequest } from '@frontera-sdk/blueprint/action-hooks'
|
|
494
|
+
\`\`\`
|
|
495
|
+
|
|
496
|
+
An app never writes to the Blueprint directly. It submits a **Request** against
|
|
497
|
+
a published Action, and the platform decides whether it runs, who must approve
|
|
498
|
+
it, and when it dispatches. There is no write hook, and adding one is not the
|
|
499
|
+
answer to "this is slow".
|
|
500
|
+
|
|
501
|
+
## Render controls from discovery, never from a list you wrote
|
|
502
|
+
|
|
503
|
+
\`\`\`tsx
|
|
504
|
+
const { action } = useAction('escalateTicket') // null when this user may not
|
|
505
|
+
const submit = useSubmitAction(action)
|
|
506
|
+
\`\`\`
|
|
507
|
+
|
|
508
|
+
Authorization is per **person**, not per app: the signed-in user's role decides.
|
|
509
|
+
The same page must offer a control to one colleague and not another, and only
|
|
510
|
+
the server knows which. A hard-coded button that 403s on click is worse than one
|
|
511
|
+
that was never drawn — so gate on \`action\`, and render a disabled control with
|
|
512
|
+
a reason when you need the affordance to stay visible.
|
|
513
|
+
|
|
514
|
+
An empty result during development is far more often permissions than a bug.
|
|
515
|
+
An Action is hidden unless it is published, deployed, AND its invoke capability
|
|
516
|
+
is held by the caller's role.
|
|
517
|
+
|
|
518
|
+
## Submitting is not applying
|
|
519
|
+
|
|
520
|
+
\`\`\`tsx
|
|
521
|
+
submit.mutate({
|
|
522
|
+
objectId: ticket.ticketId,
|
|
523
|
+
input: { priority: 'Critical', escalated: true },
|
|
524
|
+
expectedVersion: recordVersionOf(instance.data),
|
|
525
|
+
reason: 'Customer escalated.',
|
|
526
|
+
})
|
|
527
|
+
|
|
528
|
+
const request = useActionRequest(submit.data?.id) // polls until it settles
|
|
529
|
+
\`\`\`
|
|
530
|
+
|
|
531
|
+
\`mutate\` resolves when the Request is RECORDED. Dispatch happens on a
|
|
532
|
+
background worker, and approval may sit with someone else for hours. A success
|
|
533
|
+
toast fired on \`onSuccess\` is claiming something the app does not know.
|
|
534
|
+
|
|
535
|
+
**Ask \`actionEffectOf\`, never the lifecycle.**
|
|
536
|
+
|
|
537
|
+
\`\`\`tsx
|
|
538
|
+
import { actionEffectOf } from '@frontera-sdk/blueprint/action-types'
|
|
539
|
+
|
|
540
|
+
switch (actionEffectOf(request.data)) {
|
|
541
|
+
case 'applied': return <Done /> // the write committed. Close the dialog.
|
|
542
|
+
case 'refused': return <Refusal />
|
|
543
|
+
case 'uncertain': return <Checking /> // NOT failure — see below
|
|
544
|
+
case 'pending': return <Spinner />
|
|
545
|
+
}
|
|
546
|
+
\`\`\`
|
|
547
|
+
|
|
548
|
+
\`lifecycle\` says where the Request sits in the pipeline; \`effectCertainty\`
|
|
549
|
+
says whether the change landed. They settle at different times, and the gap is
|
|
550
|
+
not small. A Request reaches \`confirmed_applied\` the instant the write commits,
|
|
551
|
+
then sits in \`finalizing\` while the platform verifies its own promise —
|
|
552
|
+
re-reading the target, checking the properties the Action declared it would
|
|
553
|
+
change. **That verification never undoes the write.** Its worst outcome still
|
|
554
|
+
carries \`confirmed_applied\` and only means a human should look at why the proof
|
|
555
|
+
was inconclusive. So blocking a dialog on \`succeeded\` leaves someone watching a
|
|
556
|
+
spinner over a ticket that already exists.
|
|
557
|
+
|
|
558
|
+
\`uncertain\` is the one that must not be collapsed into either neighbour. A
|
|
559
|
+
dispatch was attempted and its outcome could not be established — the connection
|
|
560
|
+
died mid-commit, and the write may well have landed. Rendering it as failure
|
|
561
|
+
invites a duplicate; rendering it as success invites a lie. Say it is being
|
|
562
|
+
checked and leave it: the platform reconciles it against the target and the
|
|
563
|
+
answer arrives on its own.
|
|
564
|
+
|
|
565
|
+
## Concurrency — and where the version actually comes from
|
|
566
|
+
|
|
567
|
+
\`\`\`tsx
|
|
568
|
+
import { recordVersionOf } from '@frontera-sdk/blueprint/action-types'
|
|
569
|
+
|
|
570
|
+
const instance = useObjectInstance('SupportTicket', selectedId)
|
|
571
|
+
const version = recordVersionOf(instance.data)
|
|
572
|
+
\`\`\`
|
|
573
|
+
|
|
574
|
+
**A list row does not carry the version.** It arrives as \`_meta.recordVersion\`
|
|
575
|
+
from the INSTANCE route only, and only for an editable type. So a control that
|
|
576
|
+
edits a row in a table fetches that row's instance first — selecting a row and
|
|
577
|
+
submitting straight from \`useObjects\` data sends no version at all.
|
|
578
|
+
|
|
579
|
+
That failure is silent: a missing version does not error, the compare-and-set
|
|
580
|
+
is simply skipped, and two people overwrite each other with no refusal and no
|
|
581
|
+
evidence. Use \`recordVersionOf\` rather than reaching into \`_meta\` yourself.
|
|
582
|
+
|
|
583
|
+
A stale value IS refused, and that refusal is the feature — report it as
|
|
584
|
+
"someone else changed this, reload" rather than retrying with a fresher version
|
|
585
|
+
behind the user's back.
|
|
586
|
+
|
|
587
|
+
Do not hand-build the invocation. One version travels as two fields of
|
|
588
|
+
different types, and a wrong shape is refused without naming the field —
|
|
589
|
+
\`useSubmitAction\` assembles it.
|
|
590
|
+
|
|
591
|
+
## Idempotency
|
|
592
|
+
|
|
593
|
+
A key is minted per \`mutate\` call. A retried network failure reuses it and
|
|
594
|
+
cannot double-apply; a second click is a second intent and gets its own. Pass
|
|
595
|
+
\`idempotencyKey\` yourself only when you have a stronger notion of "the same
|
|
596
|
+
intended effect" than one click.
|
|
441
597
|
`,
|
|
442
598
|
}
|
|
443
599
|
}
|
|
@@ -568,3 +724,26 @@ export function devHostHtml(name: string): string {
|
|
|
568
724
|
</html>
|
|
569
725
|
`
|
|
570
726
|
}
|
|
727
|
+
|
|
728
|
+
/**
|
|
729
|
+
* Default public-package scaffold: an ordinary Next.js project with a static
|
|
730
|
+
* artifact contract, a working reference feature, and the pattern skills that
|
|
731
|
+
* describe it.
|
|
732
|
+
*
|
|
733
|
+
* The file map and the skill set live in `templates/` rather than here: they
|
|
734
|
+
* are the parts that change with the product, while this module owns the
|
|
735
|
+
* vendoring rules the legacy Vite tree still depends on. Keeping them apart
|
|
736
|
+
* means a scaffold edit cannot accidentally move a vendored SDK path.
|
|
737
|
+
*/
|
|
738
|
+
function nextFiles(name: string): Record<string, string> {
|
|
739
|
+
return {
|
|
740
|
+
...nextSkillFiles(),
|
|
741
|
+
...nextAppFiles({
|
|
742
|
+
name,
|
|
743
|
+
// The platform stylesheet, vendored at CLI build time. It carries the
|
|
744
|
+
// token contract every shipped component and every skill refers to, so
|
|
745
|
+
// the scaffold must not hand-roll a second one.
|
|
746
|
+
themeCss: vendoredFiles()['src/theme.css'] ?? '@import "tailwindcss";\n',
|
|
747
|
+
}),
|
|
748
|
+
}
|
|
749
|
+
}
|