@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
package/LICENSE ADDED
@@ -0,0 +1,5 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 ALMATO AG. All rights reserved.
4
+
5
+ This is an internal library for ALMATO AG.
package/README.md ADDED
@@ -0,0 +1,105 @@
1
+ # @bardioc/create-bardioc-app
2
+
3
+ Scaffold a Vite app wired for `@bardioc/app-sdk`.
4
+
5
+ ## Requirements
6
+
7
+ - Node.js `>=20.19.0`
8
+
9
+ ## Install
10
+
11
+ Configure the `@bardioc` scoped registry and install the CLI globally:
12
+
13
+ ```bash
14
+ npm config set @bardioc:registry http://nexus.almato.com/repository/npm-group/ --location=user && npm install -g @bardioc/create-bardioc-app
15
+ ```
16
+
17
+ If your default npm registry is not `https://registry.npmjs.org/`, set it in your user-level npm config before installing app dependencies.
18
+
19
+ You can also run it without a global install:
20
+
21
+ ```bash
22
+ npm exec --yes @bardioc/create-bardioc-app my-app -- --port 3005
23
+ ```
24
+
25
+ Or with pnpm:
26
+
27
+ ```bash
28
+ pnpm dlx @bardioc/create-bardioc-app my-app --port 3005
29
+ ```
30
+
31
+ ## Quick start
32
+
33
+ ```bash
34
+ create-bardioc-app my-app --port 3005
35
+ cd apps/my-app
36
+ pnpm install
37
+ bardioc login
38
+ pnpm dev
39
+ ```
40
+
41
+ ## Generated app
42
+
43
+ The generated app includes:
44
+
45
+ - React + Vite project structure (or your chosen `--template`)
46
+ - `@bardioc/app-sdk` dependency
47
+ - `AppSdkProvider` wiring
48
+ - optional host-backed standalone dev-session wiring via `.env`
49
+ - a compact SDK demo with notification and transport examples
50
+ - `public/app-manifest.json`
51
+
52
+ ### Release infra (out of the box)
53
+
54
+ Every scaffolded app ships a lightweight versioning toolkit:
55
+
56
+ - **Changesets** (`.changeset/` + `changeset`/`release:*` scripts) for semver-versioning the app
57
+ - **`scripts/stamp-manifest.mjs`** — folds `package.json`'s version into the built `app-manifest.json`
58
+ so the WebOS app store shows the real release version (wired into `pnpm build` / `pnpm bundle`)
59
+ - Two Claude skills under `.claude/commands/`: **`/changeset-app`** (bump locally) and
60
+ **`/refresh-bundle`** (rebuild the zip and swap it into a WebOS host checkout)
61
+
62
+ ### Opt-in tooling
63
+
64
+ Heavier, environment-specific tooling is **off by default** — pass a flag to include it:
65
+
66
+ - **`--with-pipeline`** — a **Bitbucket pipeline** (`bitbucket-pipelines.yml`) running
67
+ check-types/build/audit on PRs, with a publish step on `dev`. Publishing is **dormant** while the
68
+ app stays `"private": true` + unscoped (`pnpm changeset publish` no-ops); the generated `README.md`
69
+ documents how to enable it.
70
+ - **`--with-link`** — the **`link:source` / `unlink:source`** dev flow (`scripts/link-source.mjs` +
71
+ `pnpm-workspace.yaml` + `.npmrc`) for developing against an in-tree `@bardioc/*` source checkout.
72
+
73
+ ## Standalone dev session
74
+
75
+ Generated apps can opt into the same host-backed standalone dev-session flow used by the SDK examples.
76
+
77
+ 1. Copy `.env.example` to `.env`
78
+ 2. Fill in `VITE_APP_NAME`, `VITE_APP_ID`, and `DEV_AUTH_HOST_URL`
79
+ 3. Run `bardioc login` once for that host
80
+ 4. Set `VITE_ENABLE_DEV_AUTH=true`
81
+
82
+ Important: standalone login will not start unless `VITE_ENABLE_DEV_AUTH=true` is present in `.env`. Running `pnpm dev` with only `VITE_APP_NAME` and `VITE_APP_ID` is not enough.
83
+
84
+ When enabled, running `pnpm dev` outside the host iframe will use your stored CLI login to mint a fresh host dev session before the dev server starts, then route SDK transport calls through the local proxy into the host transport. `DEV_SESSION_KEY` no longer needs to be copied manually. If a request needs a specific scope, pass `scopeId` on that request.
85
+
86
+ For public testing, run `pnpm dev:live` and use the emitted tunnel URL as the app's live URL in the host App Store configuration.
87
+
88
+ ## Options
89
+
90
+ - `--template <name>` — framework template (default `vite`)
91
+ - `--port <port>` — dev server port, default `3005`
92
+ - `--with-pipeline` — add the Bitbucket CI/publish pipeline (off by default)
93
+ - `--with-link` — add the `link:source` dev flow for local `@bardioc/*` checkouts (off by default)
94
+
95
+ ## Support
96
+
97
+ For support, contact yevhenii.atlanov@almato.com.
98
+
99
+ ## License
100
+
101
+ MIT.
102
+
103
+ Copyright (c) 2026 ALMATO AG. All rights reserved.
104
+
105
+ This is an internal library for ALMATO AG.
package/bin/create.mjs ADDED
@@ -0,0 +1,76 @@
1
+ #!/usr/bin/env node
2
+
3
+ import { readFileSync } from 'node:fs';
4
+ import { join, resolve } from 'node:path';
5
+ import { fileURLToPath } from 'node:url';
6
+ import { getDefaultTemplate, listTemplates, parseArgs, scaffoldApp } from '../src/scaffold.js';
7
+
8
+ const __dirname = fileURLToPath(new URL('.', import.meta.url));
9
+ const TEMPLATES_DIR = join(__dirname, '..', 'templates');
10
+ const packageJsonPath = join(__dirname, '..', 'package.json');
11
+ const packageJson = JSON.parse(readFileSync(packageJsonPath, 'utf-8'));
12
+
13
+ const args = process.argv.slice(2);
14
+
15
+ if (args.includes('--help') || args.includes('-h') || args.length === 0) {
16
+ const templates = listTemplates(TEMPLATES_DIR);
17
+ const defaultTemplate = getDefaultTemplate(TEMPLATES_DIR);
18
+ console.log(`
19
+ Usage: create-bardioc-app <app-name> [options]
20
+
21
+ Options:
22
+ --template <name> Framework template (default: ${defaultTemplate})
23
+ --port <port> Dev server port (default: 3005)
24
+ --with-pipeline Add the Bitbucket CI/publish pipeline (off by default)
25
+ --with-link Add the link:source dev flow for local @bardioc/* checkouts (off by default)
26
+
27
+ Available templates:
28
+ ${templates.map(t => ` - ${t}`).join('\n')}
29
+
30
+ Examples:
31
+ npm exec --yes @bardioc/create-bardioc-app my-app
32
+ npm exec --yes @bardioc/create-bardioc-app my-app -- --template vue
33
+ npm exec --yes @bardioc/create-bardioc-app my-app -- --template angular --port 3010
34
+ npm exec --yes @bardioc/create-bardioc-app my-app -- --with-pipeline --with-link
35
+ `);
36
+ process.exit(0);
37
+ }
38
+
39
+ const argsResult = parseArgs(args, TEMPLATES_DIR);
40
+
41
+ if (!argsResult.valid) {
42
+ console.error(`Error: ${argsResult.error}`);
43
+ process.exit(1);
44
+ }
45
+
46
+ const { name, port, template, withPipeline, withLink } = argsResult;
47
+ const destBase = resolve(process.cwd());
48
+ const templateDir = join(TEMPLATES_DIR, template);
49
+
50
+ console.log(`\nCreating apps/${name}/ with ${template} template...\n`);
51
+
52
+ const result = scaffoldApp({
53
+ name,
54
+ port,
55
+ template,
56
+ templateDir,
57
+ destBase,
58
+ withPipeline,
59
+ withLink,
60
+ });
61
+
62
+ if (!result.success) {
63
+ console.error(`Error: ${result.error}`);
64
+ process.exit(1);
65
+ }
66
+
67
+ console.log(` Done! Next steps:\n`);
68
+ console.log(` cd apps/${name}`);
69
+ console.log(` pnpm install`);
70
+ console.log(` bardioc login`);
71
+ console.log(` pnpm dev\n`);
72
+ console.log(` Open http://localhost:${port} to see your app.`);
73
+ console.log(
74
+ ` Standalone host-backed auth now refreshes automatically from your CLI login during pnpm dev.`
75
+ );
76
+ console.log(` Use "pnpm dev:live" for a public tunnel/live URL.\n`);
package/package.json ADDED
@@ -0,0 +1,45 @@
1
+ {
2
+ "name": "@bardioc/create-bardioc-app",
3
+ "version": "0.4.0",
4
+ "description": "Scaffold a Bardioc app with framework templates (vite, react, vue, angular, svelte, solid, preact, nextjs)",
5
+ "license": "MIT",
6
+ "private": false,
7
+ "type": "module",
8
+ "publishConfig": {
9
+ "registry": "https://registry.npmjs.org/",
10
+ "access": "public"
11
+ },
12
+ "engines": {
13
+ "node": ">=20.19.0"
14
+ },
15
+ "bin": {
16
+ "create-bardioc-app": "./bin/create.mjs"
17
+ },
18
+ "files": [
19
+ "bin",
20
+ "src",
21
+ "templates",
22
+ "README.md",
23
+ "LICENSE"
24
+ ],
25
+ "keywords": [
26
+ "bardioc",
27
+ "create",
28
+ "scaffold",
29
+ "vite",
30
+ "react"
31
+ ],
32
+ "devDependencies": {
33
+ "@vitest/coverage-v8": "4.1.5",
34
+ "@types/node": "^25.6.0",
35
+ "typescript": "^6.0.3",
36
+ "vitest": "4.1.5"
37
+ },
38
+ "scripts": {
39
+ "check-types": "tsc --noEmit",
40
+ "pack:check": "npm pack --dry-run",
41
+ "test": "vitest run",
42
+ "test:coverage": "vitest run --coverage",
43
+ "test:watch": "vitest"
44
+ }
45
+ }
@@ -0,0 +1,50 @@
1
+ export function toPascalCase(str: string): string;
2
+
3
+ export function validateName(name: string | null): { valid: boolean; error?: string };
4
+
5
+ export function parsePort(rawPort: string): { valid: boolean; port?: number; error?: string };
6
+
7
+ export function replaceVars(content: string, vars: Record<string, string>): string;
8
+
9
+ export function isTextTemplateFile(filePath: string): boolean;
10
+
11
+ export function walkAndReplace(dir: string, vars: Record<string, string>): void;
12
+
13
+ export function loadTemplateCatalog(templatesDir?: string): {
14
+ schemaVersion: number;
15
+ defaultTemplate: string;
16
+ templates: string[];
17
+ };
18
+
19
+ export function listTemplates(templatesDir?: string): string[];
20
+
21
+ export function getDefaultTemplate(templatesDir?: string): string;
22
+
23
+ export function parseArgs(
24
+ args: string[],
25
+ templatesDir?: string
26
+ ): {
27
+ valid: boolean;
28
+ name?: string;
29
+ port?: number;
30
+ template?: string;
31
+ withPipeline?: boolean;
32
+ withLink?: boolean;
33
+ error?: string;
34
+ };
35
+
36
+ export function scaffoldApp(params: {
37
+ name: string;
38
+ port: number;
39
+ template?: string;
40
+ templateDir: string;
41
+ destBase: string;
42
+ withPipeline?: boolean;
43
+ withLink?: boolean;
44
+ }): {
45
+ success: boolean;
46
+ dest?: string;
47
+ name?: string;
48
+ port?: number;
49
+ error?: string;
50
+ };
@@ -0,0 +1,379 @@
1
+ import {
2
+ cpSync,
3
+ existsSync,
4
+ readdirSync,
5
+ readFileSync,
6
+ renameSync,
7
+ statSync,
8
+ writeFileSync,
9
+ } from 'node:fs';
10
+ import { dirname, extname, join } from 'node:path';
11
+
12
+ const TEMPLATE_MANIFEST_FILE = 'manifest.json';
13
+ const DEFAULT_TEMPLATE = 'vite';
14
+ const BASE_TEMPLATE_DIR = '_base';
15
+ const PIPELINE_OVERLAY_DIR = '_opt-pipeline';
16
+ const LINK_OVERLAY_DIR = '_opt-link';
17
+ const DOTFILE_RENAMES = { _gitignore: '.gitignore', _npmrc: '.npmrc' };
18
+
19
+ // Per-template values for the opt-in CI pipeline. Vite-family templates read the app slug from
20
+ // VITE_APP_NAME and build to dist/; only Next.js differs.
21
+ const DEFAULT_PIPELINE_VARS = { BUILD_ENV: 'VITE_APP_NAME', OUT_DIR: 'dist' };
22
+ const TEMPLATE_PIPELINE_VARS = {
23
+ nextjs: { BUILD_ENV: 'NEXT_PUBLIC_APP_NAME', OUT_DIR: 'out' },
24
+ };
25
+
26
+ // Added to package.json scripts only when scaffolding with --with-link.
27
+ const LINK_SCRIPTS = {
28
+ 'link:source': 'node scripts/link-source.mjs',
29
+ 'unlink:source': 'node scripts/link-source.mjs --remove',
30
+ };
31
+
32
+ // Boolean CLI flags → the result key they set.
33
+ const BOOLEAN_FLAGS = {
34
+ '--with-pipeline': 'withPipeline',
35
+ '--with-link': 'withLink',
36
+ };
37
+
38
+ const TEXT_FILE_EXTENSIONS = new Set([
39
+ '.css',
40
+ '.d.ts',
41
+ '.html',
42
+ '.json',
43
+ '.js',
44
+ '.jsx',
45
+ '.md',
46
+ '.mjs',
47
+ '.mts',
48
+ '.svg',
49
+ '.ts',
50
+ '.tsx',
51
+ '.txt',
52
+ '.yml',
53
+ '.yaml',
54
+ ]);
55
+ const TEXT_FILENAMES = new Set(['_gitignore', '_npmrc']);
56
+ const TEXT_FILE_SUFFIXES = ['.env.example'];
57
+
58
+ export function toPascalCase(str) {
59
+ return str
60
+ .split('-')
61
+ .map(w => w[0].toUpperCase() + w.slice(1))
62
+ .join('');
63
+ }
64
+
65
+ export function validateName(name) {
66
+ if (!name) {
67
+ return { valid: false, error: 'app name is required' };
68
+ }
69
+ if (!/^[a-z][a-z0-9-]*$/.test(name)) {
70
+ return { valid: false, error: 'name must be lowercase with hyphens (e.g. my-app)' };
71
+ }
72
+ return { valid: true };
73
+ }
74
+
75
+ export function parsePort(rawPort) {
76
+ if (!/^\d+$/.test(rawPort)) {
77
+ return { valid: false, error: 'port must be an integer between 1024 and 65535' };
78
+ }
79
+
80
+ const port = Number.parseInt(rawPort, 10);
81
+
82
+ if (Number.isNaN(port) || port < 1024 || port > 65535) {
83
+ return { valid: false, error: 'port must be an integer between 1024 and 65535' };
84
+ }
85
+
86
+ return { valid: true, port };
87
+ }
88
+
89
+ export function replaceVars(content, vars) {
90
+ let result = content;
91
+ for (const [key, value] of Object.entries(vars)) {
92
+ result = result.replaceAll(`{{${key}}}`, value);
93
+ }
94
+ return result;
95
+ }
96
+
97
+ export function isTextTemplateFile(filePath) {
98
+ return (
99
+ TEXT_FILENAMES.has(filePath.split('/').at(-1) ?? '') ||
100
+ TEXT_FILE_SUFFIXES.some(suffix => filePath.endsWith(suffix)) ||
101
+ TEXT_FILE_EXTENSIONS.has(extname(filePath))
102
+ );
103
+ }
104
+
105
+ export function walkAndReplace(dir, vars) {
106
+ for (const entry of readdirSync(dir)) {
107
+ const full = join(dir, entry);
108
+ if (statSync(full).isDirectory()) {
109
+ walkAndReplace(full, vars);
110
+ } else if (isTextTemplateFile(full)) {
111
+ const content = readFileSync(full, 'utf-8');
112
+ writeFileSync(full, replaceVars(content, vars));
113
+ }
114
+ }
115
+ }
116
+
117
+ function discoverTemplatesFromDirectory(templatesDir) {
118
+ if (!existsSync(templatesDir)) {
119
+ return [DEFAULT_TEMPLATE];
120
+ }
121
+
122
+ return readdirSync(templatesDir)
123
+ .filter(entry => {
124
+ if (entry === TEMPLATE_MANIFEST_FILE || entry.startsWith('_')) {
125
+ return false;
126
+ }
127
+ const full = join(templatesDir, entry);
128
+ return statSync(full).isDirectory();
129
+ })
130
+ .sort();
131
+ }
132
+
133
+ export function loadTemplateCatalog(templatesDir) {
134
+ const fallbackTemplates = discoverTemplatesFromDirectory(templatesDir);
135
+ const fallback = {
136
+ schemaVersion: 1,
137
+ defaultTemplate: fallbackTemplates.includes(DEFAULT_TEMPLATE)
138
+ ? DEFAULT_TEMPLATE
139
+ : (fallbackTemplates[0] ?? DEFAULT_TEMPLATE),
140
+ templates: fallbackTemplates,
141
+ };
142
+
143
+ if (!templatesDir) {
144
+ return fallback;
145
+ }
146
+
147
+ const manifestPath = join(templatesDir, TEMPLATE_MANIFEST_FILE);
148
+ if (!existsSync(manifestPath)) {
149
+ return fallback;
150
+ }
151
+
152
+ const manifest = JSON.parse(readFileSync(manifestPath, 'utf-8'));
153
+
154
+ if (manifest.schemaVersion !== 1) {
155
+ throw new Error(`unsupported template manifest schema version: ${manifest.schemaVersion}`);
156
+ }
157
+
158
+ if (!Array.isArray(manifest.templates) || manifest.templates.length === 0) {
159
+ throw new Error('template manifest must define a non-empty templates array');
160
+ }
161
+
162
+ const manifestTemplates = [...manifest.templates].sort();
163
+ const discoveredTemplates = [...fallbackTemplates].sort();
164
+ if (manifestTemplates.join(',') !== discoveredTemplates.join(',')) {
165
+ throw new Error(
166
+ `template manifest does not match templates directory. manifest=${manifestTemplates.join(',')} directory=${discoveredTemplates.join(',')}`
167
+ );
168
+ }
169
+
170
+ if (
171
+ typeof manifest.defaultTemplate !== 'string' ||
172
+ !manifestTemplates.includes(manifest.defaultTemplate)
173
+ ) {
174
+ throw new Error(
175
+ `template manifest defaultTemplate must be one of: ${manifestTemplates.join(', ')}`
176
+ );
177
+ }
178
+
179
+ return {
180
+ schemaVersion: manifest.schemaVersion,
181
+ defaultTemplate: manifest.defaultTemplate,
182
+ templates: manifestTemplates,
183
+ };
184
+ }
185
+
186
+ export function listTemplates(templatesDir) {
187
+ return loadTemplateCatalog(templatesDir).templates;
188
+ }
189
+
190
+ export function getDefaultTemplate(templatesDir) {
191
+ return loadTemplateCatalog(templatesDir).defaultTemplate;
192
+ }
193
+
194
+ function readFlagValue(args, index, flagName) {
195
+ const value = args[index + 1];
196
+ if (!value) {
197
+ return { valid: false, error: `missing value for ${flagName}` };
198
+ }
199
+
200
+ return { valid: true, value, nextIndex: index + 1 };
201
+ }
202
+
203
+ function parseTemplateArg(args, index, availableTemplates) {
204
+ const valueResult = readFlagValue(args, index, '--template');
205
+ if (!valueResult.valid) {
206
+ return valueResult;
207
+ }
208
+
209
+ if (!availableTemplates.includes(valueResult.value)) {
210
+ return {
211
+ valid: false,
212
+ error: `unknown template "${valueResult.value}". Available: ${availableTemplates.join(', ')}`,
213
+ };
214
+ }
215
+
216
+ return {
217
+ valid: true,
218
+ template: valueResult.value,
219
+ nextIndex: valueResult.nextIndex,
220
+ };
221
+ }
222
+
223
+ function parsePortArg(args, index) {
224
+ const valueResult = readFlagValue(args, index, '--port');
225
+ if (!valueResult.valid) {
226
+ return valueResult;
227
+ }
228
+
229
+ const portResult = parsePort(valueResult.value);
230
+ if (!portResult.valid) {
231
+ return portResult;
232
+ }
233
+
234
+ return {
235
+ valid: true,
236
+ port: /** @type {number} */ (portResult.port),
237
+ nextIndex: valueResult.nextIndex,
238
+ };
239
+ }
240
+
241
+ function isPositionalName(arg, index, commandOffset, name) {
242
+ return !arg.startsWith('--') && index >= commandOffset && !name && arg !== 'create';
243
+ }
244
+
245
+ function scanArgs(args, commandOffset, catalog) {
246
+ let name = null;
247
+ let port = 3005;
248
+ let template = catalog.defaultTemplate;
249
+ const flags = { withPipeline: false, withLink: false };
250
+ const availableTemplates = catalog.templates;
251
+
252
+ let index = 0;
253
+ while (index < args.length) {
254
+ const arg = args[index];
255
+
256
+ if (arg === '--port') {
257
+ const portResult = parsePortArg(args, index);
258
+ if (!portResult.valid) {
259
+ return portResult;
260
+ }
261
+ port = portResult.port;
262
+ index = portResult.nextIndex + 1;
263
+ continue;
264
+ }
265
+
266
+ if (arg === '--template') {
267
+ const templateResult = parseTemplateArg(args, index, availableTemplates);
268
+ if (!templateResult.valid) {
269
+ return templateResult;
270
+ }
271
+ template = templateResult.template;
272
+ index = templateResult.nextIndex + 1;
273
+ continue;
274
+ }
275
+
276
+ if (arg in BOOLEAN_FLAGS) {
277
+ flags[BOOLEAN_FLAGS[arg]] = true;
278
+ index++;
279
+ continue;
280
+ }
281
+
282
+ if (isPositionalName(arg, index, commandOffset, name)) {
283
+ name = arg;
284
+ }
285
+
286
+ index++;
287
+ }
288
+
289
+ return { valid: true, name, port, template, ...flags };
290
+ }
291
+
292
+ export function parseArgs(args, templatesDir) {
293
+ const firstPositionalArg = args.find(arg => !arg.startsWith('--'));
294
+ const commandOffset = firstPositionalArg === 'create' ? 1 : 0;
295
+ const catalog = loadTemplateCatalog(templatesDir);
296
+
297
+ const scan = scanArgs(args, commandOffset, catalog);
298
+ if (!scan.valid) {
299
+ return scan;
300
+ }
301
+
302
+ const nameValidation = validateName(scan.name);
303
+ if (!nameValidation.valid) {
304
+ return nameValidation;
305
+ }
306
+
307
+ return scan;
308
+ }
309
+
310
+ // Merges the link:source scripts into a scaffolded package.json (used only with --with-link).
311
+ function addLinkScripts(dest) {
312
+ const pkgPath = join(dest, 'package.json');
313
+ if (!existsSync(pkgPath)) {
314
+ return;
315
+ }
316
+ const pkg = JSON.parse(readFileSync(pkgPath, 'utf-8'));
317
+ pkg.scripts = { ...pkg.scripts, ...LINK_SCRIPTS };
318
+ writeFileSync(pkgPath, `${JSON.stringify(pkg, null, 2)}\n`);
319
+ }
320
+
321
+ /**
322
+ * @param {object} params
323
+ * @param {string} params.name
324
+ * @param {number} params.port
325
+ * @param {string} [params.template]
326
+ * @param {string} params.templateDir
327
+ * @param {string} params.destBase
328
+ * @param {boolean} [params.withPipeline]
329
+ * @param {boolean} [params.withLink]
330
+ * @returns {{ success: boolean; dest?: string; name?: string; port?: number; error?: string }}
331
+ */
332
+ export function scaffoldApp({
333
+ name,
334
+ port,
335
+ template,
336
+ templateDir,
337
+ destBase,
338
+ withPipeline = false,
339
+ withLink = false,
340
+ }) {
341
+ const dest = join(destBase, 'apps', name);
342
+
343
+ if (existsSync(dest)) {
344
+ return { success: false, error: `directory "apps/${name}" already exists` };
345
+ }
346
+
347
+ const vars = {
348
+ APP_NAME: name,
349
+ DISPLAY_NAME: toPascalCase(name),
350
+ PORT: String(port),
351
+ ...(TEMPLATE_PIPELINE_VARS[template] ?? DEFAULT_PIPELINE_VARS),
352
+ };
353
+
354
+ const templatesRoot = dirname(templateDir);
355
+ const baseDir = join(templatesRoot, BASE_TEMPLATE_DIR);
356
+ if (existsSync(baseDir)) {
357
+ cpSync(baseDir, dest, { recursive: true });
358
+ }
359
+ cpSync(templateDir, dest, { recursive: true });
360
+
361
+ if (withPipeline) {
362
+ cpSync(join(templatesRoot, PIPELINE_OVERLAY_DIR), dest, { recursive: true });
363
+ }
364
+ if (withLink) {
365
+ cpSync(join(templatesRoot, LINK_OVERLAY_DIR), dest, { recursive: true });
366
+ addLinkScripts(dest);
367
+ }
368
+
369
+ walkAndReplace(dest, vars);
370
+
371
+ for (const [shipped, final] of Object.entries(DOTFILE_RENAMES)) {
372
+ const shippedSrc = join(dest, shipped);
373
+ if (existsSync(shippedSrc)) {
374
+ renameSync(shippedSrc, join(dest, final));
375
+ }
376
+ }
377
+
378
+ return { success: true, dest, name, port };
379
+ }
@@ -0,0 +1,17 @@
1
+ # Changesets
2
+
3
+ This app versions itself with [Changesets](https://github.com/changesets/changesets) — the same flow
4
+ as the `@bardioc/*` libraries.
5
+
6
+ **To cut a release: run the `/changeset-app` skill** (or `pnpm changeset`) — it picks the semver bump
7
+ and applies it locally (`pnpm release:version`), so `package.json`'s `version` always reflects the
8
+ release. Commit the result with your PR; merging to `dev` runs the publish step in the pipeline.
9
+
10
+ The skill is named `changeset-app` (not `changeset`) to avoid clashing with the `/changeset` command
11
+ in `bardioc-desktop-frontend` when both repos are open in one workspace. It's app-agnostic — it reads
12
+ the published package name from `package.json` — so the same skill file works in any app repo.
13
+
14
+ > **This app ships private + unscoped, so publishing is dormant.** `pnpm changeset publish` no-ops on
15
+ > a private package — versioning works, but nothing is pushed to a registry. To actually publish to
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/`.
@@ -0,0 +1,12 @@
1
+ {
2
+ "$schema": "https://unpkg.com/@changesets/config@3.1.4/schema.json",
3
+ "changelog": "@changesets/cli/changelog",
4
+ "commit": false,
5
+ "fixed": [],
6
+ "linked": [],
7
+ "access": "restricted",
8
+ "baseBranch": "dev",
9
+ "updateInternalDependencies": "patch",
10
+ "onlyUpdatePeerDependentsWhenParentChanged": true,
11
+ "ignore": []
12
+ }