@xemahq/create-biome 0.1.2 → 0.2.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.
@@ -1,263 +1,514 @@
1
- import { BiomeTemplateId } from './template.js';
2
- export function renderTemplateFiles(templateId, ctx) {
3
- switch (templateId) {
4
- case BiomeTemplateId.Connector:
5
- return renderConnectorTemplate(ctx);
6
- case BiomeTemplateId.WorkflowsOnly:
7
- return renderWorkflowsOnlyTemplate(ctx);
8
- }
1
+ import { apiNameForBiome } from './manifest-builder.js';
2
+ function camelize(kebab) {
3
+ return kebab.replaceAll(/-([a-z0-9])/g, (_, ch) => ch.toUpperCase());
9
4
  }
10
- const TSCONFIG = `{
11
- "extends": "../../tsconfig.base.json",
12
- "compilerOptions": {
13
- "outDir": "dist",
14
- "rootDir": "src",
15
- "module": "ESNext",
16
- "moduleResolution": "Bundler"
17
- },
18
- "include": ["src/**/*.ts"]
5
+ function pascalize(kebab) {
6
+ const camel = camelize(kebab);
7
+ return camel.charAt(0).toUpperCase() + camel.slice(1);
19
8
  }
20
- `;
21
- const GITIGNORE = `node_modules
22
- dist
23
- `;
24
- const VITEST_CONFIG = `import { defineConfig } from 'vitest/config';
9
+ function constCase(kebab) {
10
+ return kebab.replaceAll('-', '_').toUpperCase();
11
+ }
12
+ function serverBiomeRootFiles(ctx) {
13
+ return {
14
+ 'README.md': `# ${ctx.displayName}
25
15
 
26
- export default defineConfig({
27
- test: {
28
- environment: 'node',
29
- include: ['src/**/*.test.ts'],
30
- },
31
- });
32
- `;
33
- function readmeFor(ctx, summary) {
34
- return `# ${ctx.displayName}
16
+ ${ctx.description}
17
+
18
+ A first-party Xema **server biome** scaffolded by \`@xemahq/create-biome\`.
35
19
 
36
- ${summary}
20
+ ## Layout
37
21
 
38
- ## Develop
22
+ - \`xema-biome.json\` — the biome manifest (validated by \`BiomeManifestSchema\` at boot).
23
+ - \`api/${apiNameForBiome(ctx.biomeId)}/\` — the NestJS service this biome ships,
24
+ bootstrapped by \`XemaServiceModule.forBiome(...)\`.
25
+ - \`skills/\` — Skill folder bundles (\`<slug>/SKILL.md\`) seeded into the agent runtime.
26
+ - \`agents/\` — Agent definitions (\`<slug>.md\`); declare each in \`xema.agents[]\`.
27
+ - \`contributions/\` — \`*.contribution.json\` envelopes (capabilities, etc.).
28
+ - \`workspace-manifests/\` — \`<slug>.workspace.yaml\` agent workspace manifests.
29
+
30
+ ## After scaffolding
39
31
 
40
32
  \`\`\`sh
33
+ # from the monorepo root
41
34
  pnpm install
42
- pnpm build
43
- pnpm test
35
+ node tooling/codegen/generate-service-bootstrap.mjs # emit the bootstrap descriptor
36
+ pnpm refresh # openapi -> client -> build
44
37
  \`\`\`
38
+ `,
39
+ 'skills/README.md': `# Skills
40
+
41
+ Drop Skill folder bundles here, one per directory:
45
42
 
46
- ## Manifest
43
+ \`\`\`
44
+ skills/<slug>/SKILL.md
45
+ \`\`\`
47
46
 
48
- Edit \`xema-biome.json\` to adjust capabilities, integration requirements, or contributions.
49
- The Xema biome host validates the manifest on boot — fail-fast errors point at the field to fix.
47
+ \`SKILL.md\` is the only strict file (YAML frontmatter \`name\` + \`description\`,
48
+ then a free-form markdown body). \`reference/\`, \`scripts/\`, and \`assets/\` are
49
+ optional and mounted as-is. \`scope\` and \`phases\` are NOT frontmatter — they
50
+ are server-resolved.
51
+ `,
52
+ 'agents/README.md': `# Agents
50
53
 
51
- ## Publish
54
+ Drop agent definitions here, one \`<slug>.md\` per agent. Each agent MUST also
55
+ be declared in \`xema-biome.json\` under \`xema.agents[]\` (\`slug\` + \`mode\`) AND
56
+ \`xema.ships.content\` must include \`"agents"\` — the boot-time cross-validator
57
+ enforces parity between the manifest roster and the on-disk files.
58
+ `,
59
+ 'contributions/.gitkeep': '',
60
+ 'workspace-manifests/README.md': `# Workspace manifests
52
61
 
53
- Biomes are npm packages. Set the version in \`package.json\` and publish to your registry.
54
- Operators install them on Xema by adding \`@xemahq-biomes/${ctx.biomeId}\` to their org's biome catalog.
55
- `;
62
+ Drop agent workspace manifests here, one \`<slug>.workspace.yaml\` per agent
63
+ workspace. See \`xema://manifest/primary-agent-base\` for the base to extend.
64
+ `,
65
+ };
56
66
  }
57
- function renderConnectorTemplate(ctx) {
67
+ function serverApiFiles(ctx) {
68
+ const apiName = apiNameForBiome(ctx.biomeId);
69
+ const apiDir = `api/${apiName}`;
70
+ const constName = `${constCase(apiName)}_BOOTSTRAP`;
71
+ const pascal = pascalize(ctx.biomeId);
72
+ const featureModule = `${pascal}Module`;
73
+ const featureController = `${pascal}Controller`;
74
+ const featureService = `${pascal}Service`;
58
75
  return {
59
- 'tsconfig.json': TSCONFIG,
60
- '.gitignore': GITIGNORE,
61
- 'vitest.config.ts': VITEST_CONFIG,
62
- 'README.md': readmeFor(ctx, 'Integration provider biome scaffolded by `@xemahq/create-biome`. Ships a webhook verifier, event mapper, credential kind declaration, and a placeholder resource lister. Replace the placeholders with calls into the upstream provider SDK.'),
63
- 'contracts/index.ts': renderContractsIndex(ctx),
64
- 'agents/index.ts': renderAgentsIndex(ctx),
65
- 'src/integration-provider/index.ts': renderProviderModuleSource(ctx),
66
- 'src/integration-provider/index.test.ts': renderProviderModuleTest(ctx),
67
- 'test/manifest.test.ts': renderManifestTest(ctx),
76
+ [`${apiDir}/package.json`]: `${JSON.stringify({
77
+ name: apiName,
78
+ version: '0.1.0',
79
+ private: true,
80
+ scripts: {
81
+ clean: 'rm -rf dist',
82
+ build: 'nest build',
83
+ format: 'prettier --write "src/**/*.ts"',
84
+ start: 'nest start',
85
+ 'start:dev': 'nest start --watch',
86
+ lint: 'eslint "src/**/*.ts"',
87
+ 'lint:fix': 'eslint "src/**/*.ts" --fix',
88
+ typecheck: 'tsc --noEmit',
89
+ openapi: 'NODE_PATH=node_modules xema-openapi',
90
+ 'client:generate': 'xema-client-generate',
91
+ },
92
+ dependencies: {
93
+ '@nestjs/common': 'catalog:',
94
+ '@nestjs/config': 'catalog:',
95
+ '@nestjs/core': 'catalog:',
96
+ '@nestjs/platform-express': 'catalog:',
97
+ '@nestjs/swagger': 'catalog:',
98
+ '@xemahq/identity-client': 'catalog:',
99
+ '@xemahq/kernel-contracts': 'catalog:',
100
+ '@xemahq/platform-common': 'catalog:',
101
+ '@xemahq/service-registry-nest': 'catalog:',
102
+ '@xemahq/xema-decorators': 'catalog:',
103
+ '@xemahq/xema-service-nest': 'catalog:',
104
+ 'class-transformer': 'catalog:',
105
+ 'class-validator': 'catalog:',
106
+ 'reflect-metadata': 'catalog:',
107
+ rxjs: 'catalog:',
108
+ 'swagger-ui-express': 'catalog:',
109
+ },
110
+ devDependencies: {
111
+ '@eslint/js': 'catalog:',
112
+ '@nestjs/cli': 'catalog:',
113
+ '@nestjs/schematics': 'catalog:',
114
+ '@types/express': 'catalog:',
115
+ '@types/node': 'catalog:',
116
+ '@xemahq/api-client-generator': 'catalog:',
117
+ dotenv: 'catalog:',
118
+ eslint: 'catalog:',
119
+ 'eslint-config-prettier': 'catalog:',
120
+ orval: 'catalog:',
121
+ prettier: 'catalog:',
122
+ typescript: 'catalog:',
123
+ 'typescript-eslint': 'catalog:',
124
+ },
125
+ xema: {
126
+ api: {
127
+ serviceId: apiName,
128
+ biomeId: ctx.biomeId,
129
+ title: `${ctx.displayName} API`,
130
+ description: ctx.description,
131
+ },
132
+ },
133
+ }, null, 2)}\n`,
134
+ [`${apiDir}/tsconfig.json`]: `${JSON.stringify({
135
+ extends: '../../../../tsconfig.base.json',
136
+ compilerOptions: {
137
+ outDir: 'dist',
138
+ rootDir: 'src',
139
+ types: ['node'],
140
+ baseUrl: '.',
141
+ },
142
+ include: ['src/**/*.ts'],
143
+ exclude: ['node_modules', 'dist'],
144
+ }, null, 2)}\n`,
145
+ [`${apiDir}/nest-cli.json`]: `${JSON.stringify({
146
+ $schema: 'https://json.schemastore.org/nest-cli',
147
+ collection: '@nestjs/schematics',
148
+ sourceRoot: 'src',
149
+ compilerOptions: {
150
+ deleteOutDir: true,
151
+ plugins: [
152
+ {
153
+ name: '@nestjs/swagger',
154
+ options: {
155
+ classValidatorShim: true,
156
+ introspectComments: true,
157
+ },
158
+ },
159
+ ],
160
+ },
161
+ }, null, 2)}\n`,
162
+ [`${apiDir}/.gitignore`]: `node_modules
163
+ dist
164
+ .turbo
165
+ openapi.*.json
166
+ `,
167
+ [`${apiDir}/src/generated/${apiName}.bootstrap.generated.ts`]: renderBootstrapPlaceholder(apiName, constName, `${ctx.displayName} API`, ctx.biomeId),
168
+ [`${apiDir}/src/main.ts`]: renderMainTs(apiName, ctx),
169
+ [`${apiDir}/src/app.module.ts`]: renderAppModule(apiName, constName, featureModule),
170
+ [`${apiDir}/src/config/index.ts`]: renderConfigIndex(),
171
+ [`${apiDir}/src/health/health.module.ts`]: HEALTH_MODULE,
172
+ [`${apiDir}/src/health/health.controller.ts`]: HEALTH_CONTROLLER,
173
+ [`${apiDir}/src/${ctx.biomeId}/${ctx.biomeId}.module.ts`]: renderFeatureModule(ctx.biomeId, featureModule, featureController, featureService),
174
+ [`${apiDir}/src/${ctx.biomeId}/${ctx.biomeId}.controller.ts`]: renderFeatureController(ctx.biomeId, featureController, featureService),
175
+ [`${apiDir}/src/${ctx.biomeId}/${ctx.biomeId}.service.ts`]: renderFeatureService(ctx.biomeId, featureService),
68
176
  };
69
177
  }
70
- function renderContractsIndex(_ctx) {
71
- return `// Biome-local contract types. Export shared types used by both the
72
- // integration provider and any activity handlers shipped with this biome.
73
- export {};
178
+ function renderBootstrapPlaceholder(apiName, constName, displayName, biomeId) {
179
+ return `// AUTO-GENERATED by tooling/codegen/generate-service-bootstrap.mjs DO NOT EDIT.
180
+ // Source of truth: biomes/${biomeId}/xema-biome.json (xema.ships.apis[] + top-level version).
181
+ // Regenerate: \`node tooling/codegen/generate-service-bootstrap.mjs\`.
182
+ import {
183
+ ServiceKind,
184
+ type BiomeServiceDescriptor,
185
+ } from '@xemahq/xema-service-nest';
186
+
187
+ /**
188
+ * Bootstrap identity for ${apiName}, consumed by
189
+ * \`XemaServiceModule.forBiome(${constName})\`. Edit the biome manifest,
190
+ * not this file.
191
+ */
192
+ export const ${constName}: BiomeServiceDescriptor = {
193
+ name: '${apiName}',
194
+ semver: '0.1.0',
195
+ serviceKind: ServiceKind.BiomeApi,
196
+ displayName: '${displayName}',
197
+ requiresServices: ['identity-api'],
198
+ exposesCapabilities: [],
199
+ };
74
200
  `;
75
201
  }
76
- function renderAgentsIndex(_ctx) {
77
- return `// Agent definitions contributed by this biome. Export agent config
78
- // objects that the biome host registers with llm-registry-api on boot.
79
- export {};
202
+ function renderMainTs(apiName, ctx) {
203
+ return `import { bootstrapXemaService } from '@xemahq/xema-service-nest';
204
+
205
+ import { AppModule } from './app.module';
206
+ import { requiredEnvVars } from './config';
207
+
208
+ const swaggerDescription = \`
209
+ ${ctx.description}
210
+ \`.trim();
211
+
212
+ void bootstrapXemaService({
213
+ appModule: AppModule,
214
+ serviceName: '${apiName}',
215
+ swaggerTitle: '${ctx.displayName} API',
216
+ swaggerDescription,
217
+ requiredEnvVars,
218
+ });
80
219
  `;
81
220
  }
82
- function renderProviderModuleSource(ctx) {
83
- return `import {
84
- adapterError,
85
- defineIntegrationProvider,
86
- err,
87
- ok,
88
- type EventMapper,
89
- type IdempotencyKeyExtractor,
90
- type IntegrationProviderModule,
91
- type WebhookVerifier,
92
- } from '@xemahq/biome-sdk/adapter';
93
- import {
94
- BuiltInAdapterKind,
95
- CredentialFieldTransform,
96
- CredentialFieldType,
97
- CredentialKind,
98
- IntegrationOnboardingKind,
99
- ProjectBindingAdapterKey,
100
- type ProviderOnboardingManifest,
101
- } from '@xemahq/kernel-contracts/connector';
102
-
103
- // Replace the placeholder verifier below with the real signature
104
- // algorithm your provider uses. The kernel ships hmac-sha256,
105
- // hmac-sha1, ed25519, and 'none' (static-token); biomes re-use the
106
- // same primitive so the registry never has to know provider-specific
107
- // details.
108
- const verifier: WebhookVerifier = {
109
- signatureHeader: 'x-${ctx.biomeId}-signature',
110
- algorithm: 'hmac-sha256',
111
- secretSource: 'org-integration-secret',
112
- verify(_input) {
113
- return err(
114
- adapterError('internal', 'replace this with a real verify() before shipping'),
115
- );
116
- },
117
- };
118
-
119
- const eventMapper: EventMapper = {
120
- map({ rawEvent, headers }) {
121
- // Discriminate on the provider's event header or payload field,
122
- // then map onto the canonical envelope for your AdapterKind.
123
- if (!rawEvent || typeof rawEvent !== 'object') {
124
- return err(adapterError('malformed-payload', 'event body is not an object'));
125
- }
126
- // \`headers\` is lower-cased; useful when the discriminator lives
127
- // on a header instead of the payload.
128
- void headers;
129
- return ok(null);
130
- },
131
- };
221
+ function renderAppModule(apiName, constName, featureModule) {
222
+ const featureFile = featureModule
223
+ .replace(/Module$/, '')
224
+ .replace(/([a-z0-9])([A-Z])/g, '$1-$2')
225
+ .toLowerCase();
226
+ return `import { Module } from '@nestjs/common';
227
+ import { ConfigModule } from '@nestjs/config';
228
+ import { APP_INTERCEPTOR } from '@nestjs/core';
229
+ import { ResponseEnvelopeInterceptor } from '@xemahq/platform-common';
230
+ import { XemaServiceModule } from '@xemahq/xema-service-nest';
132
231
 
133
- const idempotencyKeyExtractor: IdempotencyKeyExtractor = ({ headers }) => {
134
- const id = headers['x-${ctx.biomeId}-delivery'];
135
- if (typeof id === 'string' && id.length > 0) {return \`${ctx.biomeId}:\${id}\`;}
136
- return \`${ctx.biomeId}:fallback:\${Date.now().toString(36)}\`;
137
- };
232
+ import { appConfig } from './config';
233
+ import { ${constName} } from './generated/${apiName}.bootstrap.generated';
234
+ import { HealthModule } from './health/health.module';
235
+ import { ${featureModule} } from './${featureFile}/${featureFile}.module';
138
236
 
139
- // Install-wizard onboarding manifest. Declares the "Connect" surface
140
- // the frontend renders for this provider — display copy, icon, and the
141
- // credential field schema (or APP_INSTALL launch copy for app-install
142
- // providers). Replace placeholder strings before shipping.
143
- const onboarding: ProviderOnboardingManifest = {
144
- provider: '${ctx.biomeId}',
145
- displayName: '${ctx.displayName}',
146
- description: 'Replace this with a one-sentence provider description.',
147
- iconName: 'Globe',
148
- kind: IntegrationOnboardingKind.CREDENTIALS,
149
- // Declare which project-binding adapter kinds your provider supports.
150
- // Keep this list explicit — an empty array means the provider has no
151
- // project-level bindings today and the project Bind dialog will hide
152
- // it. Available keys: SCM, TRACKER, DOCUMENTATION.
153
- supportedProjectBindingAdapterKinds: [] as readonly ProjectBindingAdapterKey[],
154
- fields: [
155
- {
156
- key: 'apiToken',
157
- label: 'API Token',
158
- type: CredentialFieldType.PASSWORD,
159
- required: true,
160
- placeholder: 'Paste the provider API token',
161
- transform: CredentialFieldTransform.TRIM,
162
- },
237
+ @Module({
238
+ imports: [
239
+ ConfigModule.forRoot({ isGlobal: true, load: [appConfig] }),
240
+ // One-call platform bootstrap: Service Registry + Identity bootstrap +
241
+ // Auth (JWT + request context) + the runtime route/capability scanner.
242
+ // Service identity is the single source of truth in xema-biome.json,
243
+ // surfaced via the generated descriptor.
244
+ XemaServiceModule.forBiome(${constName}),
245
+ HealthModule,
246
+ ${featureModule},
163
247
  ],
164
- hint: 'Issue a token under your provider settings — minimum scopes documented in the biome README.',
165
- };
248
+ providers: [
249
+ { provide: APP_INTERCEPTOR, useClass: ResponseEnvelopeInterceptor },
250
+ ],
251
+ })
252
+ export class AppModule {}
253
+ `;
254
+ }
255
+ function renderConfigIndex() {
256
+ return `import { registerAs } from '@nestjs/config';
166
257
 
167
- export const ${camelize(ctx.biomeId)}ProviderModule: IntegrationProviderModule = defineIntegrationProvider({
168
- adapterKind: BuiltInAdapterKind.Tracker,
169
- provider: '${ctx.biomeId}',
170
- displayName: '${ctx.displayName}',
171
- webhook: {
172
- verifier,
173
- eventMapper,
174
- idempotencyKeyExtractor,
175
- },
176
- credentialKind: CredentialKind.OAuthUser,
177
- resources: {},
178
- actions: {},
179
- onboarding,
180
- });
258
+ /**
259
+ * Environment variables this service requires at boot. \`bootstrapXemaService\`
260
+ * fail-fasts if any is missing. The platform bootstrap injects the registry /
261
+ * identity wiring; add service-specific required vars here as the biome grows.
262
+ */
263
+ export const requiredEnvVars: readonly string[] = ['IDENTITY_API_URL'];
181
264
 
182
- export default [${camelize(ctx.biomeId)}ProviderModule];
265
+ export const appConfig = registerAs('app', () => ({
266
+ port: Number(process.env.PORT ?? '3000'),
267
+ }));
183
268
  `;
184
269
  }
185
- function renderProviderModuleTest(ctx) {
186
- return `import { describe, expect, it } from 'vitest';
270
+ const HEALTH_MODULE = `import { Module } from '@nestjs/common';
187
271
 
188
- import { ${camelize(ctx.biomeId)}ProviderModule } from './index.js';
272
+ import { HealthController } from './health.controller';
189
273
 
190
- describe('${ctx.displayName} provider module', () => {
191
- it('declares an adapter kind and a provider slug', () => {
192
- expect(${camelize(ctx.biomeId)}ProviderModule.provider).toBe('${ctx.biomeId}');
193
- expect(${camelize(ctx.biomeId)}ProviderModule.adapterKind).toBeTruthy();
194
- });
195
- });
274
+ @Module({ controllers: [HealthController] })
275
+ export class HealthModule {}
196
276
  `;
277
+ const HEALTH_CONTROLLER = `import { Controller, Get } from '@nestjs/common';
278
+ import { XemaPublicRoute } from '@xemahq/xema-decorators';
279
+
280
+ @Controller('health')
281
+ export class HealthController {
282
+ @Get('live')
283
+ @XemaPublicRoute()
284
+ live(): { status: 'ok' } {
285
+ return { status: 'ok' };
286
+ }
287
+
288
+ @Get('ready')
289
+ @XemaPublicRoute()
290
+ ready(): { status: 'ok' } {
291
+ return { status: 'ok' };
292
+ }
197
293
  }
198
- function renderManifestTest(ctx) {
199
- return `import { describe, expect, it } from 'vitest';
200
- import { readFile } from 'node:fs/promises';
201
- import { join } from 'node:path';
202
- import { BiomeManifestSchema } from '@xemahq/kernel-contracts/biome';
203
-
204
- describe('xema-biome.json', () => {
205
- it('parses against the BiomeManifestSchema', async () => {
206
- const raw = await readFile(join(import.meta.dirname, '..', 'xema-biome.json'), 'utf8');
207
- const parsed = BiomeManifestSchema.safeParse(JSON.parse(raw));
208
- if (!parsed.success) {
209
- throw new Error(\`xema-biome.json validation failed:\\n\${JSON.stringify(parsed.error.issues, null, 2)}\`);
210
- }
211
- expect(parsed.data.id).toBe('${ctx.biomeId}');
212
- });
213
- });
294
+ `;
295
+ function renderFeatureModule(biomeId, featureModule, featureController, featureService) {
296
+ const file = biomeId;
297
+ return `import { Module } from '@nestjs/common';
298
+
299
+ import { ${featureController} } from './${file}.controller';
300
+ import { ${featureService} } from './${file}.service';
301
+
302
+ @Module({
303
+ controllers: [${featureController}],
304
+ providers: [${featureService}],
305
+ })
306
+ export class ${featureModule} {}
214
307
  `;
215
308
  }
216
- function renderWorkflowsOnlyTemplate(ctx) {
217
- return {
218
- 'tsconfig.json': TSCONFIG,
219
- '.gitignore': GITIGNORE,
220
- 'vitest.config.ts': VITEST_CONFIG,
221
- 'README.md': readmeFor(ctx, 'Workflow-only biome scaffolded by `@xemahq/create-biome`. Ships YAML workflows under `workflow-config/` plus an empty TS shell for activity handlers.'),
222
- 'contracts/index.ts': renderContractsIndex(ctx),
223
- 'agents/index.ts': renderAgentsIndex(ctx),
224
- 'workflow-config/workflows/sample.yaml': renderSampleWorkflow(ctx),
225
- 'src/index.ts': `// Biomes without modules export an empty placeholder. Add typed
226
- // helpers here if your YAML workflows reference custom activities.
227
- // defineBiome() will be called here once you add contributions.
228
- export {};
229
- `,
230
- 'src/index.test.ts': `import { describe, it, expect } from 'vitest';
309
+ function renderFeatureController(biomeId, featureController, featureService) {
310
+ const file = biomeId;
311
+ const serviceVar = camelize(biomeId);
312
+ return `import { Controller, Get } from '@nestjs/common';
313
+ import { ApiOkResponse, ApiTags } from '@nestjs/swagger';
314
+ import { XemaRoute } from '@xemahq/xema-decorators';
231
315
 
232
- describe('${ctx.displayName}', () => {
233
- it('boots', () => {
234
- expect(true).toBe(true);
235
- });
236
- });
237
- `,
238
- 'test/manifest.test.ts': renderManifestTest(ctx),
316
+ import { ${featureService} } from './${file}.service';
317
+ import { ${pascalizeForDto(biomeId)}StatusDto } from './${file}.service';
318
+
319
+ @ApiTags('${biomeId}')
320
+ @Controller('${biomeId}')
321
+ export class ${featureController} {
322
+ constructor(private readonly ${serviceVar}Service: ${featureService}) {}
323
+
324
+ @Get('status')
325
+ @XemaRoute()
326
+ @ApiOkResponse({ type: ${pascalizeForDto(biomeId)}StatusDto })
327
+ status(): ${pascalizeForDto(biomeId)}StatusDto {
328
+ return this.${serviceVar}Service.status();
329
+ }
330
+ }
331
+ `;
332
+ }
333
+ function pascalizeForDto(biomeId) {
334
+ return pascalize(biomeId);
335
+ }
336
+ function renderFeatureService(biomeId, featureService) {
337
+ const pascal = pascalize(biomeId);
338
+ return `import { Injectable } from '@nestjs/common';
339
+ import { ApiProperty } from '@nestjs/swagger';
340
+
341
+ /** Sample response DTO — replace with the biome's real domain types. */
342
+ export class ${pascal}StatusDto {
343
+ @ApiProperty({ example: '${biomeId}' })
344
+ biome!: string;
345
+
346
+ @ApiProperty({ example: 'ok' })
347
+ status!: string;
348
+ }
349
+
350
+ @Injectable()
351
+ export class ${featureService} {
352
+ status(): ${pascal}StatusDto {
353
+ return { biome: '${biomeId}', status: 'ok' };
354
+ }
355
+ }
356
+ `;
357
+ }
358
+ export function webBiomeFiles(ctx) {
359
+ const pageComponent = `${pascalize(ctx.biomeId)}Page`;
360
+ const navId = ctx.biomeId;
361
+ return {
362
+ 'package.json': `${JSON.stringify({
363
+ name: `@xemahq/biomes-${ctx.webBiomeId}`,
364
+ version: '0.1.0',
365
+ description: ctx.description,
366
+ license: 'Apache-2.0',
367
+ private: true,
368
+ type: 'module',
369
+ main: 'src/index.tsx',
370
+ types: 'src/index.tsx',
371
+ files: ['src', 'xema-biome.json'],
372
+ scripts: {
373
+ typecheck: 'tsc --noEmit',
374
+ lint: 'tsc --noEmit',
375
+ format: 'prettier --write "src/**/*.{ts,tsx}"',
376
+ },
377
+ peerDependencies: {
378
+ '@tanstack/react-query': '^5.0.0',
379
+ react: '^18.3.1',
380
+ 'react-dom': '^18.3.1',
381
+ },
382
+ devDependencies: {
383
+ '@tanstack/react-query': '^5.83.0',
384
+ '@types/react': '^18.3.0',
385
+ '@types/react-dom': '^18.3.0',
386
+ 'lucide-react': '^0.462.0',
387
+ prettier: '3.6.2',
388
+ react: '^18.3.1',
389
+ 'react-dom': '^18.3.1',
390
+ typescript: '5.9.3',
391
+ },
392
+ }, null, 2)}\n`,
393
+ 'tsconfig.json': WEB_TSCONFIG,
394
+ 'src/index.tsx': renderWebIndex(ctx, pageComponent, navId),
395
+ [`src/pages/${pageComponent}.tsx`]: renderWebPage(ctx, pageComponent),
239
396
  };
240
397
  }
241
- function renderSampleWorkflow(ctx) {
242
- return `id: ${ctx.biomeId}-sample
243
- name: ${ctx.displayName} sample workflow
244
- on:
245
- manual:
246
- inputs:
247
- message:
248
- type: string
249
- required: true
250
- jobs:
251
- log:
252
- steps:
253
- - id: emit
254
- uses: xema/log@v1
255
- with:
256
- level: info
257
- message: \${{ inputs.message }}
398
+ const WEB_TSCONFIG = `{
399
+ "compilerOptions": {
400
+ "target": "ES2022",
401
+ "lib": ["ES2022", "DOM", "DOM.Iterable"],
402
+ "module": "ESNext",
403
+ "moduleResolution": "bundler",
404
+ "jsx": "react-jsx",
405
+ "noEmit": true,
406
+ "strict": true,
407
+ "esModuleInterop": true,
408
+ "skipLibCheck": true,
409
+ "allowSyntheticDefaultImports": true,
410
+ "isolatedModules": true,
411
+ "useDefineForClassFields": true,
412
+ // Mirror the HOST root tsconfig — biome sources import host primitives
413
+ // authored under the host's compiler flags.
414
+ "exactOptionalPropertyTypes": false,
415
+ "baseUrl": ".",
416
+ "paths": {
417
+ "@/*": ["../../../src/*"],
418
+ "@xemahq/ui-kernel": ["../../../src/lib/shared/ui-kernel/index.ts"],
419
+ "@xemahq/ui-kernel/*": ["../../../src/lib/shared/ui-kernel/*"],
420
+ "@xemahq/*": ["../../../src/lib/shared/*", "../../../src/lib/api/orval/clients/*"]
421
+ }
422
+ },
423
+ "include": ["src/**/*.ts", "src/**/*.tsx"]
424
+ }
425
+ `;
426
+ function renderWebIndex(ctx, pageComponent, navId) {
427
+ return `import {
428
+ type FrontendBiome,
429
+ type FrontendBiomeFactory,
430
+ type HostBridge,
431
+ } from '@xemahq/ui-kernel';
432
+ import { LayoutGrid } from 'lucide-react';
433
+ import { Suspense, createElement, lazy } from 'react';
434
+
435
+ // Lazy-load the page so its dependency graph is fetched only when the user
436
+ // navigates into the route, not at biome registration.
437
+ const ${pageComponent} = lazy(() => import('./pages/${pageComponent}'));
438
+
439
+ /**
440
+ * Frontend module entry-point for the \`${ctx.webBiomeId}\` biome.
441
+ *
442
+ * The host bootstrap fetches \`GET /platform/biomes/web\`, dynamically imports
443
+ * this module for any entry where \`enabled === true\`, then calls this default
444
+ * export with its concrete \`HostBridge\` and registers the result via
445
+ * \`registerFrontendBiome()\` from \`@xemahq/ui-kernel\`.
446
+ */
447
+ const ${camelize(ctx.webBiomeId)}Biome: FrontendBiomeFactory = (
448
+ _bridge: HostBridge,
449
+ ): FrontendBiome => {
450
+ return {
451
+ id: '${ctx.webBiomeId}',
452
+ displayName: '${ctx.navLabel}',
453
+ navItems: [
454
+ {
455
+ id: '${navId}',
456
+ label: '${ctx.navLabel}',
457
+ route: '${navId}',
458
+ icon: LayoutGrid,
459
+ section: 'Workspace',
460
+ weight: 50,
461
+ },
462
+ ],
463
+ routes: [
464
+ {
465
+ path: '${navId}',
466
+ projectScoped: true,
467
+ element: () =>
468
+ createElement(
469
+ Suspense,
470
+ { fallback: null },
471
+ createElement(${pageComponent}),
472
+ ),
473
+ },
474
+ ],
475
+ };
476
+ };
477
+
478
+ export default ${camelize(ctx.webBiomeId)}Biome;
258
479
  `;
259
480
  }
260
- function camelize(kebab) {
261
- return kebab.replaceAll(/-([a-z0-9])/g, (_, ch) => ch.toUpperCase());
481
+ function renderWebPage(ctx, pageComponent) {
482
+ return `import { useBiomeRouteParams } from '@/lib/biomes/biome-host-next';
483
+
484
+ /**
485
+ * Sample project-scoped page for the \`${ctx.webBiomeId}\` biome. Reads the
486
+ * project id bound by the biome route matcher via the host
487
+ * \`useBiomeRouteParams\` bridge hook (NEVER \`next/navigation\` — biome pages
488
+ * must go through the host's route-params context).
489
+ */
490
+ export default function ${pageComponent}(): JSX.Element {
491
+ const { projectId } = useBiomeRouteParams<{ projectId?: string }>();
492
+
493
+ return (
494
+ <div className="p-6">
495
+ <h1 className="text-xl font-semibold">${ctx.navLabel}</h1>
496
+ <p className="mt-2 text-sm text-muted-foreground">
497
+ ${ctx.description}
498
+ </p>
499
+ <p className="mt-4 text-sm">
500
+ Project scope:{' '}
501
+ <code>{projectId ?? '(none — open from a project)'}</code>
502
+ </p>
503
+ </div>
504
+ );
505
+ }
506
+ `;
507
+ }
508
+ export function renderServerBiomeFiles(ctx) {
509
+ return {
510
+ ...serverBiomeRootFiles(ctx),
511
+ ...serverApiFiles(ctx),
512
+ };
262
513
  }
263
514
  //# sourceMappingURL=template-files.js.map