@bardioc/create-bardioc-app 0.4.0

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.
Files changed (108) hide show
  1. package/LICENSE +5 -0
  2. package/README.md +105 -0
  3. package/bin/create.mjs +76 -0
  4. package/package.json +45 -0
  5. package/src/scaffold.d.ts +50 -0
  6. package/src/scaffold.js +379 -0
  7. package/templates/_base/.changeset/README.md +17 -0
  8. package/templates/_base/.changeset/config.json +12 -0
  9. package/templates/_base/.claude/commands/changeset-app.md +104 -0
  10. package/templates/_base/.claude/commands/refresh-bundle.md +101 -0
  11. package/templates/_base/README.md +89 -0
  12. package/templates/_base/public/app-manifest.json +10 -0
  13. package/templates/_base/scripts/stamp-manifest.mjs +17 -0
  14. package/templates/_opt-link/_npmrc +2 -0
  15. package/templates/_opt-link/pnpm-workspace.yaml +3 -0
  16. package/templates/_opt-link/scripts/link-source.mjs +188 -0
  17. package/templates/_opt-pipeline/bitbucket-pipelines.yml +100 -0
  18. package/templates/angular/.env.example +5 -0
  19. package/templates/angular/_gitignore +7 -0
  20. package/templates/angular/angular.json +64 -0
  21. package/templates/angular/package.json +49 -0
  22. package/templates/angular/postcss.config.mjs +5 -0
  23. package/templates/angular/public/icon.svg +1 -0
  24. package/templates/angular/public/runtime-env.js +1 -0
  25. package/templates/angular/scripts/dev-auth-proxy.mjs +92 -0
  26. package/templates/angular/scripts/sync-runtime-env.mjs +89 -0
  27. package/templates/angular/src/app/app.component.ts +181 -0
  28. package/templates/angular/src/app/bardioc-bridge.ts +5 -0
  29. package/templates/angular/src/app/bardioc.token.ts +4 -0
  30. package/templates/angular/src/index.html +15 -0
  31. package/templates/angular/src/main.ts +82 -0
  32. package/templates/angular/src/runtime-env.ts +17 -0
  33. package/templates/angular/src/styles.css +258 -0
  34. package/templates/angular/src/vite-env.d.ts +10 -0
  35. package/templates/angular/tsconfig.json +27 -0
  36. package/templates/manifest.json +13 -0
  37. package/templates/nextjs/.env.example +8 -0
  38. package/templates/nextjs/_gitignore +7 -0
  39. package/templates/nextjs/app/globals.css +75 -0
  40. package/templates/nextjs/app/layout.tsx +17 -0
  41. package/templates/nextjs/app/page.tsx +222 -0
  42. package/templates/nextjs/app/proxy/route.ts +85 -0
  43. package/templates/nextjs/global.d.ts +1 -0
  44. package/templates/nextjs/next-env.d.ts +5 -0
  45. package/templates/nextjs/next.config.ts +21 -0
  46. package/templates/nextjs/package.json +32 -0
  47. package/templates/nextjs/postcss.config.mjs +5 -0
  48. package/templates/nextjs/public/icon.svg +1 -0
  49. package/templates/nextjs/tailwind.config.ts +10 -0
  50. package/templates/nextjs/tsconfig.json +27 -0
  51. package/templates/preact/.env.example +13 -0
  52. package/templates/preact/_gitignore +9 -0
  53. package/templates/preact/index.html +13 -0
  54. package/templates/preact/package.json +32 -0
  55. package/templates/preact/public/icon.svg +1 -0
  56. package/templates/preact/src/App.tsx +139 -0
  57. package/templates/preact/src/index.css +76 -0
  58. package/templates/preact/src/main.tsx +46 -0
  59. package/templates/preact/src/vite-env.d.ts +11 -0
  60. package/templates/preact/tsconfig.json +19 -0
  61. package/templates/preact/vite.config.ts +17 -0
  62. package/templates/solid/.env.example +13 -0
  63. package/templates/solid/_gitignore +9 -0
  64. package/templates/solid/index.html +13 -0
  65. package/templates/solid/package.json +32 -0
  66. package/templates/solid/public/icon.svg +1 -0
  67. package/templates/solid/src/App.tsx +150 -0
  68. package/templates/solid/src/bardioc-sdk.tsx +33 -0
  69. package/templates/solid/src/index.css +76 -0
  70. package/templates/solid/src/main.tsx +50 -0
  71. package/templates/solid/src/vite-env.d.ts +11 -0
  72. package/templates/solid/tsconfig.json +20 -0
  73. package/templates/solid/vite.config.ts +17 -0
  74. package/templates/svelte/.env.example +5 -0
  75. package/templates/svelte/_gitignore +6 -0
  76. package/templates/svelte/index.html +13 -0
  77. package/templates/svelte/package.json +32 -0
  78. package/templates/svelte/public/icon.svg +1 -0
  79. package/templates/svelte/src/App.svelte +135 -0
  80. package/templates/svelte/src/index.css +76 -0
  81. package/templates/svelte/src/main.ts +42 -0
  82. package/templates/svelte/src/vite-env.d.ts +11 -0
  83. package/templates/svelte/tsconfig.json +13 -0
  84. package/templates/svelte/vite.config.ts +17 -0
  85. package/templates/vite/.env.example +14 -0
  86. package/templates/vite/CLAUDE.md +114 -0
  87. package/templates/vite/_gitignore +9 -0
  88. package/templates/vite/index.html +13 -0
  89. package/templates/vite/package.json +34 -0
  90. package/templates/vite/public/icon.svg +1 -0
  91. package/templates/vite/src/App.tsx +141 -0
  92. package/templates/vite/src/index.css +76 -0
  93. package/templates/vite/src/main.tsx +44 -0
  94. package/templates/vite/src/vite-env.d.ts +11 -0
  95. package/templates/vite/tsconfig.json +18 -0
  96. package/templates/vite/vite.config.ts +17 -0
  97. package/templates/vue/.env.example +5 -0
  98. package/templates/vue/_gitignore +6 -0
  99. package/templates/vue/index.html +13 -0
  100. package/templates/vue/package.json +32 -0
  101. package/templates/vue/public/icon.svg +1 -0
  102. package/templates/vue/src/App.vue +127 -0
  103. package/templates/vue/src/bardioc-sdk.ts +23 -0
  104. package/templates/vue/src/index.css +76 -0
  105. package/templates/vue/src/main.ts +43 -0
  106. package/templates/vue/src/vite-env.d.ts +17 -0
  107. package/templates/vue/tsconfig.json +26 -0
  108. package/templates/vue/vite.config.ts +17 -0
@@ -0,0 +1,104 @@
1
+ ---
2
+ name: changeset-app
3
+ description: Write a changeset for this app repo's single published 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
+ argument-hint: "[optional: summary, and/or 'minor'/'major' to override the bump]"
5
+ ---
6
+
7
+ # /changeset-app — Author + bump locally
8
+
9
+ This is a **single-package hosted-app repo**: it publishes exactly one package (read its name from
10
+ this repo's `package.json` `name` field). This skill writes the `.changeset/*.md`, picks the **bump**,
11
+ and **applies it** (`pnpm release:version`), so `package.json`'s `version` always reflects the
12
+ release. The result is committed with your PR; the pipeline only publishes.
13
+
14
+ > Named `changeset-app` (not `changeset`) on purpose: `bardioc-desktop-frontend` ships its own
15
+ > `/changeset`, and project commands don't namespace across repos — a shared name would collide
16
+ > when an app repo and the frontend are open in one workspace. Caveat: if you open **two app
17
+ > repos** at once they both expose `changeset-app`; in practice you work on one app at a time
18
+ > alongside the frontend, so this is fine.
19
+
20
+ ## Step 0 — Identify the package
21
+
22
+ Read the published package name from this repo's `package.json` `name` field. This is the **only**
23
+ app-specific value — use it verbatim in the changeset frontmatter (Step 2). Everything else below is
24
+ identical across app repos.
25
+
26
+ ## Arguments
27
+
28
+ The user invoked this with: `$ARGUMENTS`
29
+
30
+ - If it contains `major` / `minor` / `patch` → use that as the **bump override**.
31
+ - Otherwise treat it as the **one-line summary**. If empty, draft the summary from the diff.
32
+
33
+ ## Step 1 — Decide the bump
34
+
35
+ **Default: `patch`.** Then escalate based on what changed:
36
+
37
+ - **`major`** — a **breaking** change for consumers / the host integration. Signals:
38
+ - a public export, prop, type, or `bridge.*` contract that **existed is removed or renamed**,
39
+ - behavior changed in a way that breaks an embedding host or a saved session,
40
+ - a required new input where there wasn't one.
41
+ - **`minor`** — a notable backward-compatible **addition** (new feature/export). Optional;
42
+ purely additive props may stay `patch`.
43
+ - **`patch`** — fixes, internal refactors, additive optional props. **This is the default.**
44
+
45
+ `$ARGUMENTS` override wins. When genuinely unsure between patch and major, **choose major**
46
+ (under-bumping silently breaks consumers) and say why in one line.
47
+
48
+ > 0.x caret trap: on `^0.x.y`, a **minor** bump (`0.2→0.3`) breaks a consumer's `^` range and
49
+ > forces a manual cross-repo update, while a **patch** (`0.0.3→0.0.4`) is picked up
50
+ > automatically. Prefer `patch` for backward-compatible changes while pre-1.0.
51
+
52
+ ### Bumped a `@bardioc/*` dependency that the app needs? Bump the app too.
53
+
54
+ If this change raises a `@bardioc/*` dependency in `package.json` (e.g. `@bardioc/ui` ↑ because
55
+ 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 published app version must
57
+ move whenever its required dependency set moves — otherwise the package.json version no longer
58
+ identifies a distinct release.
59
+
60
+ ## Step 2 — Write the changeset, then apply the bump
61
+
62
+ Write `.changeset/<short-kebab-slug>.md` using **this repo's** package name (from Step 0):
63
+
64
+ ```markdown
65
+ ---
66
+ '<package-name-from-package.json>': patch
67
+ ---
68
+
69
+ <one-line consumer-facing summary>
70
+ ```
71
+
72
+ Then apply the version bump locally:
73
+
74
+ ```bash
75
+ pnpm release:version # = changeset version && pnpm install --lockfile-only
76
+ ```
77
+
78
+ This rewrites `package.json`'s `version`, updates `CHANGELOG.md`, syncs the lockfile, and
79
+ consumes the changeset file.
80
+
81
+ ## Step 3 — Report
82
+
83
+ Tell the user: from → to version, the bump level + one-line reasoning. Leave the changes
84
+ **uncommitted** — they ride along with the PR commit (don't commit unless asked). Then remind
85
+ them to run **`/refresh-bundle`** so the new version reaches the shipped artifact (see the
86
+ manifest note below) — a bump alone does not update the zip.
87
+
88
+ ## Notes
89
+
90
+ - **App-store version is auto-stamped — never hand-edit the manifest.** This app stamps its
91
+ `public/app-manifest.json` version from `package.json` at bundle time (via
92
+ `scripts/stamp-manifest.mjs`, wired into `pnpm bundle`/`pnpm build`), so the WebOS app store reads
93
+ that stamped version, and this skill's `package.json` bump is the _only_ edit needed. It reaches
94
+ the store only after a **re-bundle**: run **`/refresh-bundle`** after bumping (before merging) to
95
+ rebuild the zip and swap it into the host's `apps/webos-host/public/_apps/`. The committed
96
+ `app-manifest.json` `version` is just a seed — the stamp overwrites it in the build output, so
97
+ don't sync it by hand.
98
+ - **Bump before merging to `dev`** so the release carries a fresh version. There is **no
99
+ release gate**: if you forget, `pnpm changeset publish` just no-ops (green, "No unpublished
100
+ projects") rather than failing — the merge succeeds but publishes nothing until you bump. (While
101
+ the package is private, publish always no-ops regardless.)
102
+ - Bumping locally is why CI needs **no git-write token** — the version is already in the PR;
103
+ the pipeline only runs `pnpm changeset publish` (which publishes only if the version isn't on
104
+ Nexus yet and the package is publishable).
@@ -0,0 +1,101 @@
1
+ ---
2
+ name: refresh-bundle
3
+ description: Rebuild this app's zip and replace the committed copy in the WebOS host's apps/webos-host/public/_apps/. Removes the stale zip first, runs `pnpm bundle`, then copies the fresh zip over.
4
+ argument-hint: '[absolute path to the bardioc-desktop-frontend (WebOS host) checkout]'
5
+ ---
6
+
7
+ # /refresh-bundle — Rebuild & Replace this Hosted App Bundle
8
+
9
+ Re-bundle **this** standalone hosted app and swap the fresh zip into the WebOS host's
10
+ `public/_apps/` so the running shell serves the latest build.
11
+
12
+ The flow, end to end:
13
+
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 `pnpm bundle` script (`<build> && node scripts/stamp-manifest.mjs && zip → <app>.zip`).
16
+ 3. **Replace** `<host>/apps/webos-host/public/_apps/<app>.zip` with the freshly built one.
17
+
18
+ ## Arguments
19
+
20
+ The user invoked this command with: `$ARGUMENTS`
21
+
22
+ `REPO` below = **this** app repo (the directory this skill runs in — use `pwd`).
23
+ `HOST` = the WebOS host checkout (`bardioc-desktop-frontend`), resolved from `$ARGUMENTS`:
24
+
25
+ - **An absolute path** that exists → use it as `HOST`.
26
+ - **Empty** → **ask the user for the host repo path.** Do not guess — the app does not know where
27
+ the host is checked out.
28
+
29
+ ## Step 1 — Resolve the zip filename
30
+
31
+ The zip name is **not** hardcoded — derive it from this repo so the skill works for any app:
32
+
33
+ ```bash
34
+ test -f "$REPO/package.json" || { echo "No package.json at $REPO"; exit 1; }
35
+ grep -E '"bundle"' "$REPO/package.json"
36
+ ```
37
+
38
+ Parse the `*.zip` filename out of the `bundle` script and take its **basename** — strip any leading
39
+ directory / `../` segments so you get the bare `<slug>.zip` the bundle writes to the repo root.
40
+ (Most templates zip from `dist/` via `zip -r ../<slug>.zip`, but Angular zips from
41
+ `dist/<slug>/browser/` via `zip -r ../../../<slug>.zip` — the basename is `<slug>.zip` in every
42
+ case.) Call it `ZIP`.
43
+
44
+ Sanity-check the target exists so we know we're replacing, not silently adding:
45
+
46
+ ```bash
47
+ ls -l "$HOST/apps/webos-host/public/_apps/$ZIP"
48
+ ```
49
+
50
+ - If it **exists** → this is a replace (the normal case). Note its current byte size.
51
+ - If it **does NOT exist** → this would be an _add_, not a replace. Stop and confirm with the user
52
+ that adding a new entry to the host is intended before continuing.
53
+
54
+ ## Step 2 — Remove the stale zip in this repo
55
+
56
+ ```bash
57
+ rm -f "$REPO/$ZIP"
58
+ ```
59
+
60
+ ## Step 3 — Bundle in this repo
61
+
62
+ ```bash
63
+ cd "$REPO" && pnpm bundle
64
+ ```
65
+
66
+ Then verify the artifact was actually produced:
67
+
68
+ ```bash
69
+ test -f "$REPO/$ZIP" && ls -l "$REPO/$ZIP" || { echo "Bundle did not produce $ZIP"; exit 1; }
70
+ ```
71
+
72
+ If `pnpm bundle` fails (build errors), **stop and report the real failure** — do not copy anything.
73
+ You may fix an obvious issue and retry once; on a second failure, report it truthfully and ask the
74
+ user how to proceed.
75
+
76
+ ## Step 4 — Replace the bundle in the host's public/\_apps
77
+
78
+ ```bash
79
+ cp -f "$REPO/$ZIP" "$HOST/apps/webos-host/public/_apps/$ZIP"
80
+ ls -l "$HOST/apps/webos-host/public/_apps/$ZIP"
81
+ ```
82
+
83
+ ## Step 5 — Wrap up
84
+
85
+ Report in one block:
86
+
87
+ - App + the resolved `ZIP` filename, and the `HOST` path used.
88
+ - Old vs new byte size of `public/_apps/$ZIP` (so the user sees the swap actually happened).
89
+ - `git -C "$HOST" status --short apps/webos-host/public/_apps/` to confirm the tracked zip is now
90
+ modified, ready to commit **in the host repo**.
91
+
92
+ Do **not** commit unless the user asks.
93
+
94
+ ## Notes
95
+
96
+ - This app is decoupled from the host's workspace (its own `pnpm-lock.yaml`). Always run
97
+ `pnpm bundle` from inside `REPO`, never from the host root.
98
+ - Removing the repo zip first (Step 2) is deliberate: it guarantees the copy in Step 4 is a
99
+ brand-new build, never a leftover artifact.
100
+ - The `public/_apps/<app>.zip` is git-tracked in the host repo, so the previous bundle is always
101
+ recoverable via git if a refresh goes wrong.
@@ -0,0 +1,89 @@
1
+ # {{DISPLAY_NAME}}
2
+
3
+ A Bardioc hosted app, scaffolded with `@bardioc/create-bardioc-app`. Runs standalone (not as a
4
+ workspace member of `bardioc-desktop-frontend`) so it can be bundled and shipped as a hosted Bardioc
5
+ app.
6
+
7
+ ## Install
8
+
9
+ ```bash
10
+ pnpm install
11
+ ```
12
+
13
+ ## Develop
14
+
15
+ ```bash
16
+ pnpm dev # http://localhost:{{PORT}}
17
+ pnpm dev:live # public tunnel (BARDIOC_TUNNEL=1)
18
+ ```
19
+
20
+ Run `bardioc login` once; `pnpm dev` then mints and refreshes the standalone dev session
21
+ automatically. See `.env.example` for the required variables.
22
+
23
+ ## Develop against `@bardioc/*` source (optional)
24
+
25
+ > Available only if you scaffolded with `--with-link` (adds `scripts/link-source.mjs`,
26
+ > `pnpm-workspace.yaml`, and `.npmrc`). Otherwise this section does not apply — every `@bardioc/*`
27
+ > dep resolves from Nexus.
28
+
29
+ By default, every `@bardioc/*` dependency comes from Nexus. To develop against a local source
30
+ checkout instead — for example, to edit `@bardioc/ui` and see HMR in this app — symlink the source
31
+ repo into `.linked/`:
32
+
33
+ ```bash
34
+ pnpm link:source <path-to-source-repo> # e.g. ../bardioc-desktop-frontend
35
+ pnpm install
36
+ ```
37
+
38
+ The link name is derived from the path's basename, so this creates
39
+ `.linked/<name> → <path>` (`.linked/` is gitignored). `pnpm-workspace.yaml`'s `./.linked/*/packages/*`
40
+ glob picks it up; the dev server serves the linked source and HMR fires on edits. If the linked
41
+ source uses pnpm catalogs, `link:source` copies its `catalog:` / `catalogs:` blocks into a managed
42
+ block at the bottom of `pnpm-workspace.yaml` (don't hand-edit it — re-run `link:source` to refresh).
43
+
44
+ ```bash
45
+ pnpm link:source ../frontend-experiment --as bardioc-desktop-frontend # custom name
46
+ pnpm unlink:source <name> # remove a link
47
+ ```
48
+
49
+ ### CI / Docker
50
+
51
+ No `.linked/` directory exists, so the workspace glob matches nothing and `@bardioc/*` always
52
+ resolves from Nexus. No overlay or CI-only workspace file needed.
53
+
54
+ ## Build & Bundle
55
+
56
+ ```bash
57
+ pnpm build # type-check + build (+ stamp the manifest version) → output dir
58
+ pnpm bundle # build + zip → {{APP_NAME}}.zip
59
+ ```
60
+
61
+ `scripts/stamp-manifest.mjs` folds `package.json`'s `version` into the built `app-manifest.json` so
62
+ the WebOS app store shows the real release version. `{{APP_NAME}}.zip` is the deliverable uploaded
63
+ to the Bardioc host as a hosted app. To swap a fresh build into a local WebOS host checkout, run the
64
+ **`/refresh-bundle`** skill.
65
+
66
+ ## Release & Publishing
67
+
68
+ This app is versioned with [Changesets](https://github.com/changesets/changesets). **The version
69
+ bump is done locally** (so CI needs no git-write token), and there is **no release gate** — a
70
+ forgotten bump just publishes nothing rather than failing the deploy.
71
+
72
+ > The CI pipeline below (PR checks + the publish step) is present only if you scaffolded with
73
+ > `--with-pipeline`. Without it, version locally with the steps below and bundle/ship by hand.
74
+
75
+ **To cut a release:**
76
+
77
+ 1. On your feature branch, run the **`/changeset-app`** skill (or `pnpm changeset`) — it picks the
78
+ semver bump, writes `.changeset/*.md`, and applies it locally (`pnpm release:version`). Commit the
79
+ result with your PR. Then run **`/refresh-bundle`** so the new version reaches the shipped zip.
80
+ 2. Open a PR. The PR pipeline runs `check-types`, `build`, and `audit` in parallel. No publish.
81
+ 3. Merge to `dev`. CI runs again, then the publish step runs.
82
+
83
+ > **Publishing is dormant by default.** This app ships `"private": true` with an unscoped name, so
84
+ > `pnpm changeset publish` cleanly no-ops. To publish to Nexus, scope the package name
85
+ > (e.g. `@bardioc/{{APP_NAME}}`), set `"private": false`, and add a `publishConfig.registry` — then
86
+ > the pipeline's publish step ships the built output (set `"files": ["<output-dir>"]` accordingly).
87
+ >
88
+ > **CI prerequisite** (set as a Bitbucket repo/workspace variable, not in the repo):
89
+ > `NEXUS_REPO_USER` / `NEXUS_REPO_PASS` — an account with `npm-releases` deploy rights.
@@ -0,0 +1,10 @@
1
+ {
2
+ "manifestVersion": 2,
3
+ "id": "{{APP_NAME}}",
4
+ "name": "{{DISPLAY_NAME}}",
5
+ "description": "{{DISPLAY_NAME}} — a Bardioc hosted app",
6
+ "version": "0.0.1",
7
+ "entry": "index.html",
8
+ "icon": "icon.svg",
9
+ "permissions": ["notify", "transport"]
10
+ }
@@ -0,0 +1,17 @@
1
+ // Folds package.json's version into the built app-manifest.json so the WebOS app store (name +
2
+ // description only) shows the release version. Output dir comes from argv (defaults to dist). Idempotent.
3
+ import { readFileSync, writeFileSync } from 'node:fs';
4
+
5
+ const root = new URL('..', import.meta.url);
6
+ const outDir = process.argv[2] ?? 'dist';
7
+ const { version } = JSON.parse(readFileSync(new URL('package.json', root), 'utf8'));
8
+
9
+ const url = new URL(`${outDir}/app-manifest.json`, root);
10
+ const manifest = JSON.parse(readFileSync(url, 'utf8'));
11
+ const base = (manifest.description ?? '').replace(/\s*\(v[^)]*\)\s*$/, '').trim();
12
+
13
+ manifest.version = version;
14
+ manifest.description = base ? `${base} (v${version})` : `v${version}`;
15
+
16
+ writeFileSync(url, `${JSON.stringify(manifest, null, 2)}\n`);
17
+ console.log(`[stamp-manifest] ${version} → ${outDir}/app-manifest.json`);
@@ -0,0 +1,2 @@
1
+ link-workspace-packages=true
2
+ prefer-workspace-packages=true
@@ -0,0 +1,3 @@
1
+ packages:
2
+ - '.'
3
+ - './.linked/*/packages/*'
@@ -0,0 +1,188 @@
1
+ #!/usr/bin/env node
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 Nexus.
4
+ // node link-source.mjs <path> [--as <name>] link (name defaults to basename)
5
+ // node link-source.mjs --remove <name> remove a link
6
+
7
+ import {
8
+ existsSync,
9
+ lstatSync,
10
+ mkdirSync,
11
+ readdirSync,
12
+ readFileSync,
13
+ rmSync,
14
+ symlinkSync,
15
+ writeFileSync,
16
+ } from 'node:fs';
17
+ import { basename, resolve } from 'node:path';
18
+
19
+ const LINK_DIR = '.linked';
20
+ const WORKSPACE_FILE = 'pnpm-workspace.yaml';
21
+ const MARKER_BEGIN = '# BEGIN linked-catalogs (managed by link:source — do not edit)';
22
+ const MARKER_END = '# END linked-catalogs';
23
+
24
+ const argv = process.argv.slice(2);
25
+
26
+ if (argv[0] === '--remove') {
27
+ const name = argv[1];
28
+ if (!name) fail('Usage: pnpm unlink:source <name>');
29
+ const link = `${LINK_DIR}/${name}`;
30
+ if (!exists(link)) fail(`No link at ${link}`);
31
+ rmSync(link);
32
+ console.log(`✓ unlinked ${name}`);
33
+ syncCatalogs();
34
+ process.exit(0);
35
+ }
36
+
37
+ let target = null;
38
+ let customName = null;
39
+ for (let i = 0; i < argv.length; i++) {
40
+ if (argv[i] === '--as') customName = argv[++i];
41
+ else if (!argv[i].startsWith('--')) target = argv[i];
42
+ }
43
+
44
+ if (!target) fail('Usage: pnpm link:source <path-to-source-repo> [--as <name>]');
45
+
46
+ const abs = resolve(target);
47
+ if (!existsSync(`${abs}/packages`)) fail(`No packages/ at ${abs} — wrong path?`);
48
+
49
+ const name = customName ?? basename(abs);
50
+ if (!name) fail('Could not derive name from path — pass --as <name>');
51
+
52
+ mkdirSync(LINK_DIR, { recursive: true });
53
+ const link = `${LINK_DIR}/${name}`;
54
+ if (exists(link)) rmSync(link);
55
+ symlinkSync(abs, link, 'junction');
56
+ console.log(`✓ linked ${name} → ${abs}`);
57
+
58
+ syncCatalogs();
59
+
60
+ console.log('Next: pnpm install');
61
+
62
+ // ── helpers ──────────────────────────────────────────────────────────
63
+
64
+ function exists(path) {
65
+ try {
66
+ lstatSync(path);
67
+ return true;
68
+ } catch {
69
+ return false;
70
+ }
71
+ }
72
+
73
+ function fail(msg) {
74
+ console.error(msg);
75
+ process.exit(1);
76
+ }
77
+
78
+ function syncCatalogs() {
79
+ if (!existsSync(WORKSPACE_FILE)) return;
80
+ const sources = collectCatalogSources();
81
+ assertSingleSource(sources);
82
+ const marker = buildMarkerBlock(sources);
83
+ rewriteWorkspaceMarker(marker, sources);
84
+ }
85
+
86
+ function collectCatalogSources() {
87
+ if (!existsSync(LINK_DIR)) return [];
88
+ const sources = [];
89
+ for (const linkName of readdirSync(LINK_DIR).sort()) {
90
+ const linkedFile = `${LINK_DIR}/${linkName}/${WORKSPACE_FILE}`;
91
+ if (!existsSync(linkedFile)) continue;
92
+ const extracted = extractCatalogs(readFileSync(linkedFile, 'utf-8'));
93
+ if (extracted) sources.push({ name: linkName, content: extracted });
94
+ }
95
+ return sources;
96
+ }
97
+
98
+ function assertSingleSource(sources) {
99
+ if (sources.length > 1) {
100
+ const names = sources.map(s => s.name).join(', ');
101
+ fail(
102
+ `Multiple linked sources define catalogs (${names}). ` +
103
+ `pnpm requires a single catalog block — merge manually or unlink all but one.`
104
+ );
105
+ }
106
+ }
107
+
108
+ function buildMarkerBlock(sources) {
109
+ if (sources.length !== 1) return '';
110
+ const { name: sourceName, content } = sources[0];
111
+ return `${MARKER_BEGIN}\n# from .linked/${sourceName}\n${content}\n${MARKER_END}\n`;
112
+ }
113
+
114
+ function rewriteWorkspaceMarker(marker, sources) {
115
+ let workspace = readFileSync(WORKSPACE_FILE, 'utf-8');
116
+ const beginIdx = workspace.indexOf(MARKER_BEGIN);
117
+ const endIdx = workspace.indexOf(MARKER_END);
118
+ const hasExistingBlock = beginIdx >= 0 && endIdx > beginIdx;
119
+
120
+ if (hasExistingBlock) {
121
+ workspace = replaceMarkerBlock(workspace, beginIdx, endIdx, marker);
122
+ } else if (marker) {
123
+ workspace = workspace.replace(/\n*$/, '\n\n') + marker;
124
+ } else {
125
+ return;
126
+ }
127
+
128
+ writeFileSync(WORKSPACE_FILE, workspace);
129
+ logSyncOutcome(sources);
130
+ }
131
+
132
+ function replaceMarkerBlock(workspace, beginIdx, endIdx, marker) {
133
+ const before = workspace.slice(0, beginIdx).replace(/\n+$/, '\n');
134
+ const afterStart = endIdx + MARKER_END.length;
135
+ const afterRaw = workspace.slice(afterStart).replace(/^\n+/, '');
136
+ const after = afterRaw ? '\n' + afterRaw : '';
137
+ if (marker) return `${before}\n${marker}${after}`;
138
+ return `${before}${after}`;
139
+ }
140
+
141
+ function logSyncOutcome(sources) {
142
+ if (sources.length === 1) {
143
+ console.log(`✓ synced catalogs from .linked/${sources[0].name} into ${WORKSPACE_FILE}`);
144
+ } else {
145
+ console.log(`✓ removed managed catalogs block from ${WORKSPACE_FILE}`);
146
+ }
147
+ }
148
+
149
+ // Extracts top-level `catalog:` and `catalogs:` blocks from a pnpm-workspace.yaml.
150
+ // Line-based, no YAML parser. Assumes block headers start at column 0 and
151
+ // content is indented (spaces or tabs).
152
+ export function extractCatalogs(yamlContent) {
153
+ const lines = yamlContent.split('\n');
154
+ const out = [];
155
+ let i = 0;
156
+ while (i < lines.length) {
157
+ if (!/^(catalog|catalogs):\s*$/.test(lines[i])) {
158
+ i++;
159
+ continue;
160
+ }
161
+ const start = i;
162
+ i = consumeIndentedLines(lines, i + 1);
163
+ out.push(lines.slice(start, trimTrailingBlanks(lines, start, i)).join('\n'));
164
+ }
165
+ return out.length > 0 ? stripComments(out.join('\n\n')) : null;
166
+ }
167
+
168
+ function stripComments(yaml) {
169
+ return yaml
170
+ .split('\n')
171
+ .filter(line => !/^\s*#/.test(line))
172
+ .join('\n')
173
+ .replace(/\n{3,}/g, '\n\n');
174
+ }
175
+
176
+ function consumeIndentedLines(lines, from) {
177
+ let i = from;
178
+ while (i < lines.length && (lines[i] === '' || /^[ \t]/.test(lines[i]))) {
179
+ i++;
180
+ }
181
+ return i;
182
+ }
183
+
184
+ function trimTrailingBlanks(lines, start, end) {
185
+ let trimmed = end;
186
+ while (trimmed > start && lines[trimmed - 1] === '') trimmed--;
187
+ return trimmed;
188
+ }
@@ -0,0 +1,100 @@
1
+ image: node:24
2
+
3
+ options:
4
+ max-time: 15
5
+
6
+ definitions:
7
+ caches:
8
+ pnpm: .pnpm-store
9
+
10
+ steps:
11
+ - step: &check-types
12
+ name: Check types
13
+ runs-on: ['almai', 'self.hosted', 'linux']
14
+ caches:
15
+ - pnpm
16
+ script:
17
+ - corepack enable && corepack prepare pnpm@10.33.0 --activate
18
+ - pnpm config set store-dir .pnpm-store
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
23
+
24
+ - step: &build
25
+ name: Build
26
+ runs-on: ['almai', 'self.hosted', 'linux']
27
+ caches:
28
+ - pnpm
29
+ script:
30
+ - corepack enable && corepack prepare pnpm@10.33.0 --activate
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
35
+ - export {{BUILD_ENV}}={{APP_NAME}}
36
+ - pnpm build
37
+ artifacts:
38
+ - {{OUT_DIR}}/**
39
+
40
+ - step: &audit
41
+ name: Security audit
42
+ runs-on: ['almai', 'self.hosted', 'linux']
43
+ caches:
44
+ - pnpm
45
+ script:
46
+ - corepack enable && corepack prepare pnpm@10.33.0 --activate
47
+ - pnpm config set store-dir .pnpm-store
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
76
+
77
+ pipelines:
78
+ # PRs (and pushes to a PR's source branch) run everything except publish.
79
+ pull-requests:
80
+ '**':
81
+ - parallel:
82
+ - step: *check-types
83
+ - step: *build
84
+ - step: *audit
85
+
86
+ # Commits to `dev` run CI then publish. No gate: changeset publish no-ops when nothing is new.
87
+ branches:
88
+ dev:
89
+ - parallel:
90
+ - step: *check-types
91
+ - step: *build
92
+ - 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
@@ -0,0 +1,5 @@
1
+ VITE_APP_NAME={{APP_NAME}}
2
+ VITE_APP_ID=your-app-client-id-here
3
+ VITE_ENABLE_DEV_AUTH=false
4
+ DEV_AUTH_HOST_URL=
5
+ DEV_SESSION_KEY=
@@ -0,0 +1,7 @@
1
+ .angular
2
+ .env
3
+ .env.local
4
+ dist
5
+ node_modules
6
+ *.zip
7
+ .linked/