@bardioc/create-bardioc-app 0.6.1 → 0.7.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 +5 -1
- package/package.json +2 -2
- package/templates/_base/.changeset/README.md +4 -6
- package/templates/_base/.claude/commands/changeset-app.md +11 -12
- package/templates/_base/.claude/commands/refresh-bundle.md +3 -3
- package/templates/_base/AGENTS.md +128 -0
- package/templates/_base/CLAUDE.md +1 -0
- package/templates/_base/README.md +17 -20
- package/templates/_base/scripts/audit.mjs +50 -0
- package/templates/_base/scripts/compliance-summary.mjs +42 -0
- package/templates/_base/scripts/compliance.mjs +64 -0
- package/templates/_base/scripts/stamp-manifest.mjs +10 -4
- package/templates/_opt-link/scripts/link-source.mjs +1 -1
- package/templates/_opt-pipeline/bitbucket-pipelines.yml +12 -56
- package/templates/angular/_gitignore +6 -0
- package/templates/angular/package.json +8 -2
- package/templates/nextjs/_gitignore +6 -0
- package/templates/nextjs/package.json +8 -2
- package/templates/preact/_gitignore +6 -0
- package/templates/preact/package.json +8 -2
- package/templates/solid/_gitignore +6 -0
- package/templates/solid/package.json +8 -2
- package/templates/svelte/_gitignore +6 -0
- package/templates/svelte/package.json +8 -2
- package/templates/vite/CLAUDE.md +149 -135
- package/templates/vite/_gitignore +6 -0
- package/templates/vite/package.json +10 -4
- package/templates/vite/src/App.tsx +6 -3
- package/templates/vite/src/auth/onboarding.tsx +19 -23
- package/templates/vite/src/climate/climate-data.ts +19 -0
- package/templates/vite/src/climate/conditions-chart.tsx +11 -6
- package/templates/vite/src/climate/location-card.tsx +2 -5
- package/templates/vite/src/climate/location-select.tsx +7 -2
- package/templates/vite/src/climate/metrics-grid.tsx +6 -1
- package/templates/vite/src/climate/overview-view.tsx +26 -7
- package/templates/vite/src/climate/stations-view.tsx +73 -9
- package/templates/vite/src/climate/types.ts +5 -5
- package/templates/vite/src/dashboard-provider.tsx +15 -5
- package/templates/vite/src/events/live-events-view.tsx +67 -37
- package/templates/vite/src/events/mock-events.ts +33 -0
- package/templates/vite/src/graph/explorer-view.tsx +81 -63
- package/templates/vite/src/i18n.tsx +6 -2
- package/templates/vite/src/index.css +0 -6
- package/templates/vite/src/locales/de.json +55 -51
- package/templates/vite/src/locales/en.json +44 -40
- package/templates/vite/src/preview-state.tsx +29 -0
- package/templates/vite/src/profile/profile-view.tsx +43 -25
- package/templates/vite/src/shell/about-dialog.tsx +6 -5
- package/templates/vite/src/shell/app-shell.tsx +46 -45
- package/templates/vite/src/shell/debug-menu.tsx +86 -0
- package/templates/vite/src/shell/header.tsx +6 -4
- package/templates/vite/src/shell/view-content.tsx +69 -0
- package/templates/vue/_gitignore +6 -0
- package/templates/vue/package.json +8 -2
package/README.md
CHANGED
|
@@ -49,7 +49,11 @@ Every scaffolded app ships a lightweight versioning toolkit:
|
|
|
49
49
|
|
|
50
50
|
- **Changesets** (`.changeset/` + `changeset`/`release:*` scripts) for semver-versioning the app
|
|
51
51
|
- **`scripts/stamp-manifest.mjs`** — folds `package.json`'s version into the built `app-manifest.json`
|
|
52
|
-
|
|
52
|
+
and `manifest.yaml` (wired into `npm run build` / `npm run bundle`)
|
|
53
|
+
- **Compliance gate** (`scripts/compliance.mjs`, `audit.mjs`, `compliance-summary.mjs` + `sbom` /
|
|
54
|
+
`compliance` / `audit:security` / `compliance:all` scripts) — `npm run bundle` first writes a
|
|
55
|
+
CycloneDX SBOM, checks production licenses against an allow list and fails on high/critical audit
|
|
56
|
+
findings; the four reports land in `public/` and ship inside the zip
|
|
53
57
|
- Two Claude skills under `.claude/commands/`: **`/changeset-app`** (bump locally) and
|
|
54
58
|
**`/refresh-bundle`** (rebuild the zip and swap it into a WebOS host checkout)
|
|
55
59
|
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@bardioc/create-bardioc-app",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.7.1",
|
|
4
4
|
"description": "Scaffold a Bardioc app with framework templates (vite, react, vue, angular, svelte, solid, preact, nextjs)",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"private": false,
|
|
@@ -32,7 +32,7 @@
|
|
|
32
32
|
"devDependencies": {
|
|
33
33
|
"@vitest/coverage-v8": "^4.1.8",
|
|
34
34
|
"@types/node": "^25.9.1",
|
|
35
|
-
"typescript": "
|
|
35
|
+
"typescript": "^7.0.2",
|
|
36
36
|
"vitest": "^4.1.8"
|
|
37
37
|
},
|
|
38
38
|
"scripts": {
|
|
@@ -5,13 +5,11 @@ as the `@bardioc/*` libraries.
|
|
|
5
5
|
|
|
6
6
|
**To cut a release: run the `/changeset-app` skill** (or `npm run changeset`) — it picks the semver bump
|
|
7
7
|
and applies it locally (`npm run release:version`), so `package.json`'s `version` always reflects the
|
|
8
|
-
release. Commit the result with your PR
|
|
8
|
+
release. Commit the result with your PR.
|
|
9
9
|
|
|
10
10
|
The skill is named `changeset-app` (not `changeset`) to avoid clashing with the `/changeset` command
|
|
11
11
|
in `bardioc-desktop-frontend` when both repos are open in one workspace. It's app-agnostic — it reads
|
|
12
|
-
the
|
|
12
|
+
the package name from `package.json` — so the same skill file works in any app repo.
|
|
13
13
|
|
|
14
|
-
> **This app
|
|
15
|
-
>
|
|
16
|
-
> Nexus, scope the name (e.g. `@bardioc/<app>`), set `"private": false`, and add a
|
|
17
|
-
> `publishConfig.registry`; then the pipeline's publish step starts shipping the built `dist/`.
|
|
14
|
+
> **This app is not published to a registry.** It ships `"private": true`, so `npm run changeset publish`
|
|
15
|
+
> no-ops — versioning works, and the release is the zip built by `npm run bundle`.
|
|
@@ -1,15 +1,16 @@
|
|
|
1
1
|
---
|
|
2
2
|
name: changeset-app
|
|
3
|
-
description: Write a changeset for this app repo's single
|
|
3
|
+
description: Write a changeset for this app repo's single package, pick the semver bump (default patch, major on breaking change), and apply the bump locally — keeping package.json's version in step with the release.
|
|
4
4
|
argument-hint: "[optional: summary, and/or 'minor'/'major' to override the bump]"
|
|
5
5
|
---
|
|
6
6
|
|
|
7
7
|
# /changeset-app — Author + bump locally
|
|
8
8
|
|
|
9
|
-
This is a **single-package hosted-app repo**: it
|
|
9
|
+
This is a **single-package hosted-app repo**: it holds exactly one package (read its name from
|
|
10
10
|
this repo's `package.json` `name` field). This skill writes the `.changeset/*.md`, picks the **bump**,
|
|
11
11
|
and **applies it** (`npm run release:version`), so `package.json`'s `version` always reflects the
|
|
12
|
-
release. The result is committed with your PR
|
|
12
|
+
release. The result is committed with your PR. Nothing is published: the release is the zip built
|
|
13
|
+
by `npm run bundle`.
|
|
13
14
|
|
|
14
15
|
> Named `changeset-app` (not `changeset`) on purpose: `bardioc-desktop-frontend` ships its own
|
|
15
16
|
> `/changeset`, and project commands don't namespace across repos — a shared name would collide
|
|
@@ -19,7 +20,7 @@ release. The result is committed with your PR; the pipeline only publishes.
|
|
|
19
20
|
|
|
20
21
|
## Step 0 — Identify the package
|
|
21
22
|
|
|
22
|
-
Read the
|
|
23
|
+
Read the package name from this repo's `package.json` `name` field. This is the **only**
|
|
23
24
|
app-specific value — use it verbatim in the changeset frontmatter (Step 2). Everything else below is
|
|
24
25
|
identical across app repos.
|
|
25
26
|
|
|
@@ -53,7 +54,7 @@ The user invoked this with: `$ARGUMENTS`
|
|
|
53
54
|
|
|
54
55
|
If this change raises a `@bardioc/*` dependency in `package.json` (e.g. `@bardioc/ui` ↑ because
|
|
55
56
|
the app now uses a new component/prop/type from it), that is a **release-relevant change**:
|
|
56
|
-
write a changeset so the app's **own** version bumps as well. The
|
|
57
|
+
write a changeset so the app's **own** version bumps as well. The app version must
|
|
57
58
|
move whenever its required dependency set moves — otherwise the package.json version no longer
|
|
58
59
|
identifies a distinct release.
|
|
59
60
|
|
|
@@ -95,10 +96,8 @@ manifest note below) — a bump alone does not update the zip.
|
|
|
95
96
|
rebuild the zip and swap it into the host's `apps/webos-host/public/_apps/`. The committed
|
|
96
97
|
`app-manifest.json` `version` is just a seed — the stamp overwrites it in the build output, so
|
|
97
98
|
don't sync it by hand.
|
|
98
|
-
- **Bump before merging to `dev`** so the
|
|
99
|
-
release gate**: if you forget
|
|
100
|
-
|
|
101
|
-
the
|
|
102
|
-
|
|
103
|
-
the pipeline only runs `npm run changeset publish` (which publishes only if the version isn't on
|
|
104
|
-
Nexus yet and the package is publishable).
|
|
99
|
+
- **Bump before merging to `dev`** so the zip built from it carries a fresh version. There is **no
|
|
100
|
+
release gate**: nothing fails if you forget.
|
|
101
|
+
- Bumping locally is why CI needs **no git-write token** — the version is already in the PR.
|
|
102
|
+
Nothing is published: the app ships `"private": true`, and its release is the zip built by
|
|
103
|
+
`npm run bundle`.
|
|
@@ -12,7 +12,7 @@ Re-bundle **this** standalone hosted app and swap the fresh zip into the WebOS h
|
|
|
12
12
|
The flow, end to end:
|
|
13
13
|
|
|
14
14
|
1. **Remove** the stale zip in this app repo (forces a clean rebundle, never copies a stale artifact).
|
|
15
|
-
2. **Bundle** via this repo's `npm run bundle` script (
|
|
15
|
+
2. **Bundle** via this repo's `npm run bundle` script (`npm run compliance:all && <build> && node scripts/stamp-manifest.mjs && zip → <app>.zip`).
|
|
16
16
|
3. **Replace** `<host>/apps/webos-host/public/_apps/<app>.zip` with the freshly built one.
|
|
17
17
|
|
|
18
18
|
## Arguments
|
|
@@ -69,7 +69,7 @@ Then verify the artifact was actually produced:
|
|
|
69
69
|
test -f "$REPO/$ZIP" && ls -l "$REPO/$ZIP" || { echo "Bundle did not produce $ZIP"; exit 1; }
|
|
70
70
|
```
|
|
71
71
|
|
|
72
|
-
If `npm run bundle` fails (build errors), **stop and report the real failure** — do not copy anything.
|
|
72
|
+
If `npm run bundle` fails (compliance gate or build errors), **stop and report the real failure** — do not copy anything.
|
|
73
73
|
You may fix an obvious issue and retry once; on a second failure, report it truthfully and ask the
|
|
74
74
|
user how to proceed.
|
|
75
75
|
|
|
@@ -93,7 +93,7 @@ Do **not** commit unless the user asks.
|
|
|
93
93
|
|
|
94
94
|
## Notes
|
|
95
95
|
|
|
96
|
-
- This app is decoupled from the host's workspace (its own `
|
|
96
|
+
- This app is decoupled from the host's workspace (its own `package-lock.json`). Always run
|
|
97
97
|
`npm run bundle` from inside `REPO`, never from the host root.
|
|
98
98
|
- Removing the repo zip first (Step 2) is deliberate: it guarantees the copy in Step 4 is a
|
|
99
99
|
brand-new build, never a leftover artifact.
|
|
@@ -0,0 +1,128 @@
|
|
|
1
|
+
# {{DISPLAY_NAME}} — agent guide
|
|
2
|
+
|
|
3
|
+
A Bardioc hosted app, scaffolded with `@bardioc/create-bardioc-app`. It runs in an iframe inside
|
|
4
|
+
Bardioc OS and reaches the OS and the backend only through `@bardioc/app-sdk`. This file holds what
|
|
5
|
+
every Bardioc app shares, whatever its framework; `CLAUDE.md` may add notes for this template.
|
|
6
|
+
|
|
7
|
+
---
|
|
8
|
+
|
|
9
|
+
## Commands
|
|
10
|
+
|
|
11
|
+
The app uses **npm**, not pnpm or yarn: `npm run bundle` needs npm's lockfile for its SBOM and audit.
|
|
12
|
+
|
|
13
|
+
```bash
|
|
14
|
+
npm install
|
|
15
|
+
npm run dev # http://localhost:{{PORT}}
|
|
16
|
+
npm run build # → build output, with the manifest version stamped
|
|
17
|
+
npm run bundle # compliance gate + build → {{APP_NAME}}.zip
|
|
18
|
+
npm run check-types
|
|
19
|
+
npm run lint # @bardioc/configs preset (not in the angular template)
|
|
20
|
+
npm run format # @bardioc/configs preset (not in the angular template)
|
|
21
|
+
```
|
|
22
|
+
|
|
23
|
+
---
|
|
24
|
+
|
|
25
|
+
## Running it
|
|
26
|
+
|
|
27
|
+
- **Inside the host** the app needs no setup: the OS provides the session.
|
|
28
|
+
- **Standalone** (`npm run dev`), the dev host stands in for the OS (`installDevHost()` from
|
|
29
|
+
`@bardioc/app-sdk/dev`, in the app's entry: `src/main.*`, or `app/page.tsx` in nextjs). SDK
|
|
30
|
+
calls go through a dev-auth proxy that signs them with `DEV_SECRET`: the `bardiocDevAuth` Vite
|
|
31
|
+
plugin, or `scripts/dev.mjs` in the angular and nextjs templates. Set `DEV_SECRET` (from the App Store after registering the app) and
|
|
32
|
+
`API_BASE_URL` in `.env` (see `.env.example`). Without them every backend call fails with a
|
|
33
|
+
`BardiocError`; render that as an error state.
|
|
34
|
+
- **Check dev auth in the browser**, not with `curl`: the proxy relays the browser's session, so
|
|
35
|
+
`curl localhost:{{PORT}}/rest/…` gets 401 even with a valid `DEV_SECRET`.
|
|
36
|
+
- **`npm run preview`** (Vite templates) serves the build on port + 1000, not on `{{PORT}}`; trust
|
|
37
|
+
the `Local:` line it prints. A build opened outside the host renders in fallback fonts, because fonts come from the
|
|
38
|
+
host's `/fonts` contract.
|
|
39
|
+
- Gate the SDK on `isInsideIframe()` / `isDevStandalone()` (`@bardioc/app-sdk/dev`), as
|
|
40
|
+
the app's entry does: with neither a host nor a dev session there is nothing to connect to.
|
|
41
|
+
|
|
42
|
+
---
|
|
43
|
+
|
|
44
|
+
## SDK — `@bardioc/app-sdk` 1.x
|
|
45
|
+
|
|
46
|
+
Everything goes through one `Bardioc` object. The app's entry gets it: `connect()` from
|
|
47
|
+
`@bardioc/app-sdk`, or `useSdk()` from `@bardioc/app-sdk/react` under `AppSdkProvider`.
|
|
48
|
+
|
|
49
|
+
- **Graph** — `bardioc.graph.*`: `get(id)`, `getMany`, `getByXid`, `query(lucene, opts)`,
|
|
50
|
+
`gremlin(rootId, query)`, `create`, `update`, `delete`, `connect`, `history`, `timeseries.*`
|
|
51
|
+
(full list: the `GraphApi` type).
|
|
52
|
+
- `query` can resolve to a `{ items: [...] }` envelope: normalize before indexing.
|
|
53
|
+
- Escape `/` in Lucene as `\/` (`ogit\/_type:"…"`).
|
|
54
|
+
- Ids and types differ between instances: discover them with a wildcard `query('*')` instead of
|
|
55
|
+
hard-coding them.
|
|
56
|
+
- **OS** — `bardioc.os.profile.get()`, `bardioc.os.organization.getStructure()`,
|
|
57
|
+
`bardioc.os.applications.list()` (full list: the `OsApi` type). Any other backend path:
|
|
58
|
+
`bardioc.bff.request({ path })`.
|
|
59
|
+
- **Storage** — `bardioc.idb(store)`: app-scoped IndexedDB with `get`, `put`, `delete`,
|
|
60
|
+
`query({ prefix, limit })`. Standalone, it lives in this browser's IndexedDB.
|
|
61
|
+
- **Host UI** — `bardioc.ui.notify(message, level)`, `bardioc.ui.setMenu(...)` /
|
|
62
|
+
`setCommands(...)` with `onCommand(id, run)` for the OS menu bar.
|
|
63
|
+
- **Live events** — `bardioc.events.graph({ types }, onEvent)` resolves to the function that ends
|
|
64
|
+
the stream. No events arrive outside a valid session: render an empty state.
|
|
65
|
+
- **Context** — `bardioc.context` (`osConfig` with theme and language, the app's `settings`) and
|
|
66
|
+
`bardioc.onContextChange(listener)`. Follow the OS theme and language.
|
|
67
|
+
- **`bardioc.apps.*`** exists only inside the host; standalone it rejects with `unavailable`.
|
|
68
|
+
|
|
69
|
+
### Errors
|
|
70
|
+
|
|
71
|
+
Every failure is a `BardiocError` with a `code` (`invalid`, `unauthenticated`, `forbidden`,
|
|
72
|
+
`not_found`, `conflict`, `rate_limited`, `unavailable`, `timeout`, …). HTTP failures also carry
|
|
73
|
+
`status`. Both survive the trip through the host. Wrap every call and give each failure a visible
|
|
74
|
+
state; never fall back to made-up data without labeling it.
|
|
75
|
+
|
|
76
|
+
```typescript
|
|
77
|
+
import { BardiocError } from '@bardioc/app-sdk';
|
|
78
|
+
|
|
79
|
+
try {
|
|
80
|
+
return await bardioc.graph.get(id);
|
|
81
|
+
} catch (error) {
|
|
82
|
+
if (error instanceof BardiocError && error.code === 'not_found') return null;
|
|
83
|
+
throw error;
|
|
84
|
+
}
|
|
85
|
+
```
|
|
86
|
+
|
|
87
|
+
`unauthenticated` / `forbidden` mean the session is missing or lacks a permission: say so, rather
|
|
88
|
+
than showing an empty result.
|
|
89
|
+
|
|
90
|
+
---
|
|
91
|
+
|
|
92
|
+
## Manifest — `public/app-manifest.json`
|
|
93
|
+
|
|
94
|
+
Validated by `validateManifest` (`@bardioc/app-sdk`), also on build.
|
|
95
|
+
|
|
96
|
+
- Required: `manifestVersion` (1 or 2), `id` (lowercase), `name`, `version`. `version` is stamped
|
|
97
|
+
from `package.json` on build, so bump it there, not here.
|
|
98
|
+
- Optional: `description`, `entry`, `icon` (an `icon.svg` in `public/`, 64 KB or smaller),
|
|
99
|
+
`author` (`{ name, url? }`), `categories`, `showInDock`, `showInDockAppsMenu`.
|
|
100
|
+
- `rules.roles`: the graph access the app requests at review, as role name →
|
|
101
|
+
`{ vertexRule?, edgeRule? }`.
|
|
102
|
+
- `window`: `size` (`xs|sm|md|lg|xl` or `{ width, height, minWidth?, minHeight? }`), `resizable`,
|
|
103
|
+
`maximizable`, `minimizable`, `customScroll`, `centered`, `isHeaderless`, `isInstanceAware`,
|
|
104
|
+
`canHaveMultipleWindows`.
|
|
105
|
+
|
|
106
|
+
---
|
|
107
|
+
|
|
108
|
+
## Copy
|
|
109
|
+
|
|
110
|
+
American English. Labels (titles, buttons, menu items, tabs, columns, field labels, placeholders,
|
|
111
|
+
tooltips) in Title Case (`Add Station`, `Save and Close`). Descriptions, toasts, errors and
|
|
112
|
+
validation messages as sentences ending with a period.
|
|
113
|
+
|
|
114
|
+
---
|
|
115
|
+
|
|
116
|
+
## Ship
|
|
117
|
+
|
|
118
|
+
1. Bump `version` in `package.json`. For an app already in the Development Center it must be higher
|
|
119
|
+
than the last upload.
|
|
120
|
+
2. `npm run bundle` → `{{APP_NAME}}.zip`, with the build output at the zip root. It needs the system
|
|
121
|
+
`zip` binary; on Windows, run `npm run build` and zip the build output's contents instead.
|
|
122
|
+
3. Check the archive (`unzip -l {{APP_NAME}}.zip`): `app-manifest.json` and `index.html` at the top
|
|
123
|
+
level, with the version you set.
|
|
124
|
+
4. Upload in Bardioc OS: **Development Center** → **Create App** → drop the zip (first time), or the
|
|
125
|
+
app's page → **New version** → **Upload version**. A new app starts as a draft: **Install**
|
|
126
|
+
tries it in your own OS, **Submit for review** publishes it through the App Store review.
|
|
127
|
+
|
|
128
|
+
Don't ship any other way: `/refresh-bundle` only copies the zip into a local host checkout.
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
@AGENTS.md
|
|
@@ -38,9 +38,9 @@ template is the exception — Angular 19 pins TypeScript 5.8, which the presets
|
|
|
38
38
|
|
|
39
39
|
> Available only if you scaffolded with `--with-link` (adds `scripts/link-source.mjs`,
|
|
40
40
|
> `pnpm-workspace.yaml`, and `.npmrc`). Otherwise this section does not apply — every `@bardioc/*`
|
|
41
|
-
> dep resolves from
|
|
41
|
+
> dep resolves from the npm registry.
|
|
42
42
|
|
|
43
|
-
By default, every `@bardioc/*` dependency comes from
|
|
43
|
+
By default, every `@bardioc/*` dependency comes from the npm registry. To develop against a local source
|
|
44
44
|
checkout instead — for example, to edit `@bardioc/ui` and see HMR in this app — symlink the source
|
|
45
45
|
repo into `.linked/`:
|
|
46
46
|
|
|
@@ -63,27 +63,29 @@ pnpm unlink:source <name> # remove
|
|
|
63
63
|
### CI / Docker
|
|
64
64
|
|
|
65
65
|
No `.linked/` directory exists, so the workspace glob matches nothing and `@bardioc/*` always
|
|
66
|
-
resolves from
|
|
66
|
+
resolves from the npm registry. No overlay or CI-only workspace file needed.
|
|
67
67
|
|
|
68
68
|
## Build & Bundle
|
|
69
69
|
|
|
70
70
|
```bash
|
|
71
71
|
npm run build # type-check + build (+ stamp the manifest version) → output dir
|
|
72
|
-
npm run bundle # build + zip → {{APP_NAME}}.zip
|
|
72
|
+
npm run bundle # compliance gate + build + zip → {{APP_NAME}}.zip
|
|
73
|
+
npm run compliance:all # SBOM + license check + vulnerability audit → public/*.json
|
|
73
74
|
```
|
|
74
75
|
|
|
75
|
-
`scripts/stamp-manifest.mjs` folds `package.json`'s `version` into the built `app-manifest.json`
|
|
76
|
-
|
|
76
|
+
`scripts/stamp-manifest.mjs` folds `package.json`'s `version` into the built `app-manifest.json` and
|
|
77
|
+
`manifest.yaml`. `npm run bundle` starts with the compliance gate: it stops on a production dependency
|
|
78
|
+
whose license is not on the allow list in `scripts/compliance.mjs`, or on a high or critical audit
|
|
79
|
+
finding, and ships the SBOM and the reports inside the zip. `{{APP_NAME}}.zip` is the deliverable uploaded
|
|
77
80
|
to the Bardioc host as a hosted app. To swap a fresh build into a local WebOS host checkout, run the
|
|
78
81
|
**`/refresh-bundle`** skill.
|
|
79
82
|
|
|
80
|
-
## Release
|
|
83
|
+
## Release
|
|
81
84
|
|
|
82
85
|
This app is versioned with [Changesets](https://github.com/changesets/changesets). **The version
|
|
83
|
-
bump is done locally** (so CI needs no git-write token), and
|
|
84
|
-
forgotten bump just publishes nothing rather than failing the deploy.
|
|
86
|
+
bump is done locally** (so CI needs no git-write token), and nothing is published: the release is the zip built by `npm run bundle`.
|
|
85
87
|
|
|
86
|
-
> The CI pipeline below (PR checks
|
|
88
|
+
> The CI pipeline below (PR checks) is present only if you scaffolded with
|
|
87
89
|
> `--with-pipeline`. Without it, version locally with the steps below and bundle/ship by hand.
|
|
88
90
|
|
|
89
91
|
**To cut a release:**
|
|
@@ -91,13 +93,8 @@ forgotten bump just publishes nothing rather than failing the deploy.
|
|
|
91
93
|
1. On your feature branch, run the **`/changeset-app`** skill (or `npm run changeset`) — it picks the
|
|
92
94
|
semver bump, writes `.changeset/*.md`, and applies it locally (`npm run release:version`). Commit the
|
|
93
95
|
result with your PR. Then run **`/refresh-bundle`** so the new version reaches the shipped zip.
|
|
94
|
-
2. Open a PR. The PR pipeline runs `check-types`, `build`, and `audit` in parallel.
|
|
95
|
-
3. Merge to `dev`. CI runs again
|
|
96
|
-
|
|
97
|
-
> **Publishing is
|
|
98
|
-
>
|
|
99
|
-
> (e.g. `@bardioc/{{APP_NAME}}`), set `"private": false`, and add a `publishConfig.registry` — then
|
|
100
|
-
> the pipeline's publish step ships the built output (set `"files": ["<output-dir>"]` accordingly).
|
|
101
|
-
>
|
|
102
|
-
> **CI prerequisite** (set as a Bitbucket repo/workspace variable, not in the repo):
|
|
103
|
-
> `NEXUS_REPO_USER` / `NEXUS_REPO_PASS` — an account with `npm-releases` deploy rights.
|
|
96
|
+
2. Open a PR. The PR pipeline runs `check-types`, `build`, and `audit` in parallel.
|
|
97
|
+
3. Merge to `dev`. CI runs again.
|
|
98
|
+
|
|
99
|
+
> **Publishing is not used.** This app ships `"private": true`, so `npm run changeset publish` cleanly
|
|
100
|
+
> no-ops; the release is the zip built by `npm run bundle`.
|
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
// Vulnerability gate: scans production dependencies via npm audit, fails on high/critical findings.
|
|
2
|
+
import { spawnSync } from 'node:child_process';
|
|
3
|
+
import { mkdirSync, writeFileSync } from 'node:fs';
|
|
4
|
+
import { resolve } from 'node:path';
|
|
5
|
+
|
|
6
|
+
const FAIL_SEVERITIES = ['high', 'critical'];
|
|
7
|
+
|
|
8
|
+
const outDir = resolve(process.argv[2] ?? 'public');
|
|
9
|
+
mkdirSync(outDir, { recursive: true });
|
|
10
|
+
|
|
11
|
+
// npm audit exits non-zero when vulnerabilities exist, so exit code is not an error signal here.
|
|
12
|
+
const res = spawnSync('npm', ['audit', '--omit=dev', '--json'], {
|
|
13
|
+
encoding: 'utf8',
|
|
14
|
+
maxBuffer: 64 * 1024 * 1024,
|
|
15
|
+
});
|
|
16
|
+
if (!res.stdout) {
|
|
17
|
+
console.error('npm audit produced no output:', res.stderr);
|
|
18
|
+
process.exit(1);
|
|
19
|
+
}
|
|
20
|
+
const audit = JSON.parse(res.stdout);
|
|
21
|
+
const counts = audit.metadata?.vulnerabilities ?? {};
|
|
22
|
+
|
|
23
|
+
const findings = Object.values(audit.vulnerabilities ?? {}).map(v => ({
|
|
24
|
+
name: v.name,
|
|
25
|
+
severity: v.severity,
|
|
26
|
+
range: v.range,
|
|
27
|
+
fixAvailable: v.fixAvailable,
|
|
28
|
+
advisories: (v.via ?? [])
|
|
29
|
+
.filter(x => typeof x === 'object')
|
|
30
|
+
.map(x => ({ title: x.title, url: x.url, severity: x.severity, cwe: x.cwe, cvss: x.cvss })),
|
|
31
|
+
}));
|
|
32
|
+
|
|
33
|
+
const blocking = findings.filter(f => FAIL_SEVERITIES.includes(f.severity));
|
|
34
|
+
const report = {
|
|
35
|
+
generatedAt: new Date().toISOString(),
|
|
36
|
+
tool: 'npm audit (production dependencies)',
|
|
37
|
+
failOn: FAIL_SEVERITIES,
|
|
38
|
+
counts,
|
|
39
|
+
passed: blocking.length === 0,
|
|
40
|
+
findings,
|
|
41
|
+
};
|
|
42
|
+
writeFileSync(resolve(outDir, 'security-audit.json'), JSON.stringify(report, null, 2));
|
|
43
|
+
|
|
44
|
+
console.log('Vulnerability scan:', JSON.stringify(counts));
|
|
45
|
+
if (blocking.length > 0) {
|
|
46
|
+
console.error('Blocking vulnerabilities:');
|
|
47
|
+
for (const f of blocking) console.error(` ${f.name} (${f.severity}) ${f.range}`);
|
|
48
|
+
process.exit(1);
|
|
49
|
+
}
|
|
50
|
+
console.log('No high/critical vulnerabilities.');
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
// Aggregates SBOM, license, and vulnerability reports into one verdict a reviewer or CI can read.
|
|
2
|
+
import { readFileSync, writeFileSync } from 'node:fs';
|
|
3
|
+
import { resolve } from 'node:path';
|
|
4
|
+
|
|
5
|
+
const dir = resolve(process.argv[2] ?? 'public');
|
|
6
|
+
const readJson = f => JSON.parse(readFileSync(resolve(dir, f), 'utf8'));
|
|
7
|
+
|
|
8
|
+
const pkg = JSON.parse(readFileSync('package.json', 'utf8'));
|
|
9
|
+
const sbom = readJson('sbom.cdx.json');
|
|
10
|
+
const licenses = readJson('license-compliance.json');
|
|
11
|
+
const audit = readJson('security-audit.json');
|
|
12
|
+
|
|
13
|
+
const summary = {
|
|
14
|
+
app: { name: pkg.name, version: pkg.version },
|
|
15
|
+
generatedAt: new Date().toISOString(),
|
|
16
|
+
checks: {
|
|
17
|
+
sbom: {
|
|
18
|
+
passed: true,
|
|
19
|
+
format: `CycloneDX ${sbom.specVersion}`,
|
|
20
|
+
components: sbom.components?.length ?? 0,
|
|
21
|
+
report: 'sbom.cdx.json',
|
|
22
|
+
},
|
|
23
|
+
licenses: {
|
|
24
|
+
passed: licenses.violations.length === 0,
|
|
25
|
+
packagesScanned: licenses.totalPackages,
|
|
26
|
+
violations: licenses.violations.length,
|
|
27
|
+
allowedLicenses: licenses.allowedLicenses,
|
|
28
|
+
report: 'license-compliance.json',
|
|
29
|
+
},
|
|
30
|
+
vulnerabilities: {
|
|
31
|
+
passed: audit.passed,
|
|
32
|
+
counts: audit.counts,
|
|
33
|
+
failOn: audit.failOn,
|
|
34
|
+
report: 'security-audit.json',
|
|
35
|
+
},
|
|
36
|
+
},
|
|
37
|
+
};
|
|
38
|
+
summary.passed = Object.values(summary.checks).every(c => c.passed);
|
|
39
|
+
|
|
40
|
+
writeFileSync(resolve(dir, 'compliance-summary.json'), JSON.stringify(summary, null, 2));
|
|
41
|
+
console.log(`Compliance summary: ${summary.passed ? 'PASSED' : 'FAILED'}`);
|
|
42
|
+
if (!summary.passed) process.exit(1);
|
|
@@ -0,0 +1,64 @@
|
|
|
1
|
+
// License compliance gate: fails the build when a production dependency uses a non-allowed license.
|
|
2
|
+
import { execFileSync } from 'node:child_process';
|
|
3
|
+
import { mkdirSync, writeFileSync } from 'node:fs';
|
|
4
|
+
import { resolve } from 'node:path';
|
|
5
|
+
|
|
6
|
+
const ALLOWED_LICENSES = [
|
|
7
|
+
'MIT',
|
|
8
|
+
'ISC',
|
|
9
|
+
'Apache-2.0',
|
|
10
|
+
'BSD-2-Clause',
|
|
11
|
+
'BSD-3-Clause',
|
|
12
|
+
'0BSD',
|
|
13
|
+
'CC0-1.0',
|
|
14
|
+
'Unlicense',
|
|
15
|
+
// Permissive OSI license (PSF); argparse, a js-yaml dependency, ships under it.
|
|
16
|
+
'Python-2.0',
|
|
17
|
+
];
|
|
18
|
+
|
|
19
|
+
const outDir = resolve(process.argv[2] ?? 'dist/reports');
|
|
20
|
+
mkdirSync(outDir, { recursive: true });
|
|
21
|
+
|
|
22
|
+
const raw = execFileSync(
|
|
23
|
+
'npx',
|
|
24
|
+
['license-checker-rseidelsohn', '--production', '--json', '--excludePrivatePackages'],
|
|
25
|
+
{ encoding: 'utf8', maxBuffer: 64 * 1024 * 1024 }
|
|
26
|
+
);
|
|
27
|
+
const packages = JSON.parse(raw);
|
|
28
|
+
|
|
29
|
+
// SPDX expressions: "A AND B" needs every part allowed, "A OR B" lets us pick an
|
|
30
|
+
// allowed alternative (e.g. dompurify's "MPL-2.0 OR Apache-2.0").
|
|
31
|
+
const isAllowed = licenses => {
|
|
32
|
+
const expr = String(licenses).replaceAll(/[()]/g, '');
|
|
33
|
+
const conjuncts = expr.split(/\s+AND\s+/);
|
|
34
|
+
return conjuncts.every(part =>
|
|
35
|
+
part.split(/\s+OR\s+/).some(l => ALLOWED_LICENSES.includes(l.trim()))
|
|
36
|
+
);
|
|
37
|
+
};
|
|
38
|
+
|
|
39
|
+
const violations = [];
|
|
40
|
+
const summary = {};
|
|
41
|
+
for (const [name, info] of Object.entries(packages)) {
|
|
42
|
+
const license = String(info.licenses);
|
|
43
|
+
summary[license] = (summary[license] ?? 0) + 1;
|
|
44
|
+
if (!isAllowed(license)) violations.push({ package: name, license, repository: info.repository });
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
const report = {
|
|
48
|
+
generatedAt: new Date().toISOString(),
|
|
49
|
+
allowedLicenses: ALLOWED_LICENSES,
|
|
50
|
+
totalPackages: Object.keys(packages).length,
|
|
51
|
+
licenseSummary: summary,
|
|
52
|
+
violations,
|
|
53
|
+
packages,
|
|
54
|
+
};
|
|
55
|
+
writeFileSync(resolve(outDir, 'license-compliance.json'), JSON.stringify(report, null, 2));
|
|
56
|
+
|
|
57
|
+
console.log(`License compliance: ${report.totalPackages} packages scanned`);
|
|
58
|
+
console.table(summary);
|
|
59
|
+
if (violations.length > 0) {
|
|
60
|
+
console.error('License violations found:');
|
|
61
|
+
for (const v of violations) console.error(` ${v.package}: ${v.license}`);
|
|
62
|
+
process.exit(1);
|
|
63
|
+
}
|
|
64
|
+
console.log('No license violations.');
|
|
@@ -1,5 +1,5 @@
|
|
|
1
|
-
// Folds package.json's version into the built app-manifest.json
|
|
2
|
-
//
|
|
1
|
+
// Folds package.json's version into the built app-manifest.json and manifest.yaml. Output dir comes
|
|
2
|
+
// from argv (defaults to dist). Idempotent.
|
|
3
3
|
import { readFileSync, writeFileSync } from 'node:fs';
|
|
4
4
|
|
|
5
5
|
const root = new URL('..', import.meta.url);
|
|
@@ -8,10 +8,16 @@ const { version } = JSON.parse(readFileSync(new URL('package.json', root), 'utf8
|
|
|
8
8
|
|
|
9
9
|
const url = new URL(`${outDir}/app-manifest.json`, root);
|
|
10
10
|
const manifest = JSON.parse(readFileSync(url, 'utf8'));
|
|
11
|
-
const base = (manifest.description ?? '').replace(/\s*\(v[^)]*\)\s*$/, '').trim();
|
|
12
11
|
|
|
13
12
|
manifest.version = version;
|
|
14
|
-
|
|
13
|
+
// The version lives in its own field; a "(vX.Y.Z)" suffix left by an older stamp is dropped.
|
|
14
|
+
if (manifest.description) {
|
|
15
|
+
manifest.description = manifest.description.replace(/\s*\(v[^)]*\)\s*$/, '').trim();
|
|
16
|
+
}
|
|
15
17
|
|
|
16
18
|
writeFileSync(url, `${JSON.stringify(manifest, null, 2)}\n`);
|
|
19
|
+
|
|
20
|
+
// The legacy manifest.yaml carries the same version for the store's uploader.
|
|
21
|
+
const yamlUrl = new URL(`${outDir}/manifest.yaml`, root);
|
|
22
|
+
writeFileSync(yamlUrl, `name: ${manifest.id}\nversion: ${version}\n`);
|
|
17
23
|
process.stdout.write(`[stamp-manifest] ${version} → ${outDir}/app-manifest.json\n`);
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
2
|
// Symlinks a local @bardioc/* source checkout into .linked/ and syncs its pnpm catalogs into
|
|
3
|
-
// pnpm-workspace.yaml. Without a link the workspace glob matches nothing and deps resolve from
|
|
3
|
+
// pnpm-workspace.yaml. Without a link the workspace glob matches nothing and deps resolve from the npm registry.
|
|
4
4
|
// node link-source.mjs <path> [--as <name>] link (name defaults to basename)
|
|
5
5
|
// node link-source.mjs --remove <name> remove a link
|
|
6
6
|
|
|
@@ -5,35 +5,27 @@ options:
|
|
|
5
5
|
|
|
6
6
|
definitions:
|
|
7
7
|
caches:
|
|
8
|
-
|
|
8
|
+
npm: ~/.npm
|
|
9
9
|
|
|
10
10
|
steps:
|
|
11
11
|
- step: &check-types
|
|
12
12
|
name: Check types
|
|
13
13
|
runs-on: ['almai', 'self.hosted', 'linux']
|
|
14
14
|
caches:
|
|
15
|
-
-
|
|
15
|
+
- npm
|
|
16
16
|
script:
|
|
17
|
-
-
|
|
18
|
-
-
|
|
19
|
-
- npm config set @bardioc:registry http://nexus.almato.com/repository/npm-group/
|
|
20
|
-
- echo "//nexus.almato.com/repository/npm-group/:_auth=$(echo -n ${NEXUS_REPO_USER}:${NEXUS_REPO_PASS} | base64)" >> ~/.npmrc
|
|
21
|
-
- pnpm install --frozen-lockfile
|
|
22
|
-
- pnpm check-types
|
|
17
|
+
- npm ci
|
|
18
|
+
- npm run check-types
|
|
23
19
|
|
|
24
20
|
- step: &build
|
|
25
21
|
name: Build
|
|
26
22
|
runs-on: ['almai', 'self.hosted', 'linux']
|
|
27
23
|
caches:
|
|
28
|
-
-
|
|
24
|
+
- npm
|
|
29
25
|
script:
|
|
30
|
-
-
|
|
31
|
-
- pnpm config set store-dir .pnpm-store
|
|
32
|
-
- npm config set @bardioc:registry http://nexus.almato.com/repository/npm-group/
|
|
33
|
-
- echo "//nexus.almato.com/repository/npm-group/:_auth=$(echo -n ${NEXUS_REPO_USER}:${NEXUS_REPO_PASS} | base64)" >> ~/.npmrc
|
|
34
|
-
- pnpm install --frozen-lockfile
|
|
26
|
+
- npm ci
|
|
35
27
|
- export {{BUILD_ENV}}={{APP_NAME}}
|
|
36
|
-
-
|
|
28
|
+
- npm run build
|
|
37
29
|
artifacts:
|
|
38
30
|
- {{OUT_DIR}}/**
|
|
39
31
|
|
|
@@ -41,41 +33,14 @@ definitions:
|
|
|
41
33
|
name: Security audit
|
|
42
34
|
runs-on: ['almai', 'self.hosted', 'linux']
|
|
43
35
|
caches:
|
|
44
|
-
-
|
|
36
|
+
- npm
|
|
45
37
|
script:
|
|
46
|
-
-
|
|
47
|
-
-
|
|
48
|
-
- npm config set @bardioc:registry http://nexus.almato.com/repository/npm-group/
|
|
49
|
-
- echo "//nexus.almato.com/repository/npm-group/:_auth=$(echo -n ${NEXUS_REPO_USER}:${NEXUS_REPO_PASS} | base64)" >> ~/.npmrc
|
|
50
|
-
- pnpm install --frozen-lockfile
|
|
51
|
-
- pnpm audit --audit-level=high
|
|
52
|
-
|
|
53
|
-
# Publishes any version not yet on Nexus. The bump is done locally (pnpm release:version) and merged
|
|
54
|
-
# in the PR — no git write token, no release gate. Dormant while the app is `"private": true` +
|
|
55
|
-
# unscoped (changeset publish no-ops). To enable: scope the name, set private:false, add
|
|
56
|
-
# publishConfig.registry, and set `files` to the build output.
|
|
57
|
-
- step: &publish
|
|
58
|
-
name: Publish package to Nexus
|
|
59
|
-
runs-on: ['almai', 'self.hosted', 'linux']
|
|
60
|
-
caches:
|
|
61
|
-
- pnpm
|
|
62
|
-
script:
|
|
63
|
-
- corepack enable && corepack prepare pnpm@10.33.0 --activate
|
|
64
|
-
- pnpm config set store-dir .pnpm-store
|
|
65
|
-
- npm config set @bardioc:registry http://nexus.almato.com/repository/npm-group/
|
|
66
|
-
- echo "//nexus.almato.com/repository/npm-group/:_auth=$(echo -n ${NEXUS_REPO_USER}:${NEXUS_REPO_PASS} | base64)" >> ~/.npmrc
|
|
67
|
-
# npm publish auth must live in $HOME/.npmrc — changeset publish runs npm publish from the package dir.
|
|
68
|
-
- echo "//nexus.almato.com/repository/npm-releases/:_auth=$(echo -n ${NEXUS_REPO_USER}:${NEXUS_REPO_PASS} | base64)" >> "$HOME/.npmrc"
|
|
69
|
-
- echo "//nexus.almato.com/repository/npm-releases/:always-auth=true" >> "$HOME/.npmrc"
|
|
70
|
-
- pnpm install --frozen-lockfile
|
|
71
|
-
- export {{BUILD_ENV}}={{APP_NAME}}
|
|
72
|
-
- pnpm build
|
|
73
|
-
# Switch @bardioc to the writable npm-releases repo (npm-group is read-only; @scope:registry wins over publishConfig).
|
|
74
|
-
- npm config set @bardioc:registry http://nexus.almato.com/repository/npm-releases/
|
|
75
|
-
- pnpm changeset publish
|
|
38
|
+
- npm ci
|
|
39
|
+
- npm audit --omit=dev --audit-level=high
|
|
76
40
|
|
|
41
|
+
# Nothing is published: the app ships as the zip built by `npm run bundle`.
|
|
77
42
|
pipelines:
|
|
78
|
-
# PRs (and pushes to a PR's source branch)
|
|
43
|
+
# PRs (and pushes to a PR's source branch).
|
|
79
44
|
pull-requests:
|
|
80
45
|
'**':
|
|
81
46
|
- parallel:
|
|
@@ -83,18 +48,9 @@ pipelines:
|
|
|
83
48
|
- step: *build
|
|
84
49
|
- step: *audit
|
|
85
50
|
|
|
86
|
-
# Commits to `dev` run CI then publish. No gate: changeset publish no-ops when nothing is new.
|
|
87
51
|
branches:
|
|
88
52
|
dev:
|
|
89
53
|
- parallel:
|
|
90
54
|
- step: *check-types
|
|
91
55
|
- step: *build
|
|
92
56
|
- step: *audit
|
|
93
|
-
- step:
|
|
94
|
-
<<: *publish
|
|
95
|
-
trigger: automatic
|
|
96
|
-
|
|
97
|
-
custom:
|
|
98
|
-
# On-demand publish — re-run if a publish failed.
|
|
99
|
-
publish-package:
|
|
100
|
-
- step: *publish
|