@xemahq/create-biome 0.1.3 → 0.2.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.
@@ -1,263 +1,497 @@
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
+ }
293
+ }
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} {}
307
+ `;
308
+ }
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';
315
+
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
+ }
197
355
  }
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
- });
214
356
  `;
215
357
  }
216
- function renderWorkflowsOnlyTemplate(ctx) {
358
+ export function webBiomeFiles(ctx) {
359
+ const pageComponent = `${pascalize(ctx.biomeId)}Page`;
360
+ const navId = ctx.biomeId;
217
361
  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';
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),
396
+ };
397
+ }
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 { defineWebBiome } from '@xemahq/ui-kernel';
428
+ import { LayoutGrid } from 'lucide-react';
231
429
 
232
- describe('${ctx.displayName}', () => {
233
- it('boots', () => {
234
- expect(true).toBe(true);
235
- });
430
+ /**
431
+ * Frontend module entry-point for the \`${ctx.webBiomeId}\` biome.
432
+ *
433
+ * \`defineWebBiome\` builds the biome from a declarative page list: each page's
434
+ * \`slug\` single-sources BOTH its nav route and its route path (they can never
435
+ * drift), and the page is lazily loaded behind a \`<Suspense>\` boundary. Declare
436
+ * WHAT each surface is via \`category\` — the HOST owns WHERE that category renders
437
+ * in the menu, so you never pick a global section/weight.
438
+ *
439
+ * The host bootstrap fetches \`GET /platform/biomes/web\`, dynamically imports
440
+ * this module for any entry where \`enabled === true\`, and registers the default
441
+ * export via \`registerFrontendBiome()\`. Pass \`init\`/\`panels\`/\`session\` to
442
+ * \`defineWebBiome\` for bespoke contributions; add more entries to \`pages\` (with
443
+ * \`navHidden: true\` for deeplink-only detail routes).
444
+ */
445
+ const ${camelize(ctx.webBiomeId)}Biome = defineWebBiome({
446
+ id: '${ctx.webBiomeId}',
447
+ displayName: '${ctx.navLabel}',
448
+ pages: [
449
+ {
450
+ slug: '${navId}',
451
+ label: '${ctx.navLabel}',
452
+ icon: LayoutGrid,
453
+ // One of: primary | build | operate | knowledge | admin | account
454
+ category: 'build',
455
+ scope: 'project',
456
+ load: () => import('./pages/${pageComponent}'),
457
+ },
458
+ ],
236
459
  });
237
- `,
238
- 'test/manifest.test.ts': renderManifestTest(ctx),
239
- };
460
+
461
+ export default ${camelize(ctx.webBiomeId)}Biome;
462
+ `;
463
+ }
464
+ function renderWebPage(ctx, pageComponent) {
465
+ return `import { useBiomeRouteParams } from '@/lib/biomes/biome-host-next';
466
+
467
+ /**
468
+ * Sample project-scoped page for the \`${ctx.webBiomeId}\` biome. Reads the
469
+ * project id bound by the biome route matcher via the host
470
+ * \`useBiomeRouteParams\` bridge hook (NEVER \`next/navigation\` — biome pages
471
+ * must go through the host's route-params context).
472
+ */
473
+ export default function ${pageComponent}(): JSX.Element {
474
+ const { projectId } = useBiomeRouteParams<{ projectId?: string }>();
475
+
476
+ return (
477
+ <div className="p-6">
478
+ <h1 className="text-xl font-semibold">${ctx.navLabel}</h1>
479
+ <p className="mt-2 text-sm text-muted-foreground">
480
+ ${ctx.description}
481
+ </p>
482
+ <p className="mt-4 text-sm">
483
+ Project scope:{' '}
484
+ <code>{projectId ?? '(none — open from a project)'}</code>
485
+ </p>
486
+ </div>
487
+ );
240
488
  }
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 }}
258
489
  `;
259
490
  }
260
- function camelize(kebab) {
261
- return kebab.replaceAll(/-([a-z0-9])/g, (_, ch) => ch.toUpperCase());
491
+ export function renderServerBiomeFiles(ctx) {
492
+ return {
493
+ ...serverBiomeRootFiles(ctx),
494
+ ...serverApiFiles(ctx),
495
+ };
262
496
  }
263
497
  //# sourceMappingURL=template-files.js.map