@craft-ts/dev-tools 0.7.0-beta.15 → 0.7.0-beta.17

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,9 +1,9 @@
1
- import { existsSync, mkdirSync, readFileSync, rmSync, writeFileSync, } from 'node:fs';
1
+ import { existsSync, mkdirSync, readFileSync, writeFileSync, } from 'node:fs';
2
2
  import { mkdir, readdir, readFile, writeFile } from 'node:fs/promises';
3
3
  import { execFileSync } from 'node:child_process';
4
4
  import { dirname, isAbsolute, join, relative, resolve } from 'node:path';
5
5
  import { runArchitectureMigration } from '../architecture/migrate-architecture.js';
6
- export const CRAFT_TS_STARTER_VERSION = '^0.7.0-beta.13';
6
+ export const CRAFT_TS_STARTER_VERSION = '^0.7.0-beta.15';
7
7
  export const EFFECT_V4_VERSION = '^4.0.0-rc.110';
8
8
  const DEFAULT_AGENTS = ['codex'];
9
9
  const GENERATED_GITIGNORE = `node_modules/
@@ -88,23 +88,105 @@ This project deliberately uses Effect v4 (effect@${EFFECT_V4_VERSION}) and
88
88
  Do not silently replace Effect v4 APIs with v3 examples. Confirm a symbol in
89
89
  the installed package before using it.
90
90
  `;
91
- const AGENTS_MD = (mode, config) => `# CraftTS project
91
+ function referenceAgentGuidance(config) {
92
+ if (!config || (!config.references.craftTs && !config.references.effectTs))
93
+ return '';
94
+ const root = config.workspace.kind === 'nx' ? '../../.references' : '.references';
95
+ const entries = [
96
+ config.references.craftTs ? `- CraftTS source: \`${root}/craft-ts\`` : '',
97
+ config.references.effectTs ? `- EffectTS source: \`${root}/effect-ts\`` : '',
98
+ ].filter(Boolean);
99
+ return `
100
+ ## Local source references
101
+
102
+ The following repositories are cloned for agent context only:
103
+ ${entries.join('\n')}
104
+ Use them to inspect implementations, types, tests and examples when the
105
+ installed package or project documentation is not enough. The application must
106
+ always import CraftTS and EffectTS from the npm dependencies declared in
107
+ \`package.json\`; do not add TypeScript, Vite or package \`file:\` aliases to
108
+ these clones.
109
+ `;
110
+ }
111
+ function agentsMd(mode, config) {
112
+ const frontend = config?.frontendRuntime ?? (mode === 'effect' ? 'effect' : 'plain');
113
+ const backend = config?.backendRuntime ?? 'none';
114
+ const i18n = config?.i18n.enabled ?? true;
115
+ const designSystem = config?.designSystem !== 'none';
116
+ const typedCss = config?.typedCss ?? true;
117
+ const effectFrontend = frontend === 'effect';
118
+ const effectBackend = backend === 'effect';
119
+ const effect = effectFrontend || effectBackend;
120
+ const effectSkillPaths = (config?.agents ?? []).map((agent) => agent === 'codex'
121
+ ? '.agents/skills/craft-ts-effect-v4/SKILL.md'
122
+ : agent === 'cursor'
123
+ ? '.cursor/skills/craft-ts-project/SKILL.md'
124
+ : agent === 'claude-code'
125
+ ? '.claude/skills/craft-ts-effect-v4/SKILL.md'
126
+ : '.gemini/skills/craft-ts-effect-v4/SKILL.md');
127
+ const effectSkillLine = effectSkillPaths.length
128
+ ? `Read the Effect-specific guidance in ${effectSkillPaths.map((path) => `\`${path}\``).join(', ')}.`
129
+ : 'Use the Effect v4 guidance in the project documentation when adding Effect code.';
130
+ const verify = [
131
+ 'npm run lint',
132
+ 'npm run typecheck',
133
+ 'npm run typecheck-spec',
134
+ ...(i18n ? ['npm run i18n:check', 'npm run i18n:test'] : []),
135
+ ...(typedCss ? ['npm run style:check'] : []),
136
+ ...(backend !== 'none' ? ['npm run server:test'] : []),
137
+ ...(effect ? ['npm run effect-check'] : []),
138
+ 'npm test',
139
+ 'npm run architecture',
140
+ 'npm run typecheck-architecture',
141
+ 'npm run build',
142
+ ];
143
+ const effectGuidance = effect
144
+ ? `
145
+ ## Effect boundary
146
+
147
+ ${effectFrontend ? '- Effect v4 is enabled in the browser; use `queryEffect` and the installed Craft Effect bridge.' : '- The browser runtime is plain; keep Effect imports out of browser components.'}
148
+ ${effectBackend ? '- Effect v4 is enabled on the backend; keep server Effects in `src/server/`, provide services through `Layer`, and run `npm run effect-check` after changes.' : '- The backend does not use Effect; do not add Effect dependencies or server Effects unless the runtime choice changes.'}
149
+ ${effectSkillLine}
150
+ `
151
+ : `
152
+ ## Effect boundary
153
+
154
+ Effect is not selected in this starter. Keep the application on the plain
155
+ CraftTS runtime and do not add Effect imports or dependencies incidentally.
156
+ `;
157
+ return `# CraftTS project
92
158
 
93
- This project was created with \`craft create\` using frontend **${config?.frontendRuntime ?? (mode === 'effect' ? 'effect' : 'plain')}** and backend **${config?.backendRuntime ?? 'none'}** runtimes.
159
+ This project was created with \`craft create\`. Treat this file as the project
160
+ guide for coding agents: it records the selected runtime and feature surfaces.
161
+
162
+ ## Selected configuration
163
+
164
+ - Frontend runtime: **${frontend}**
165
+ - Backend runtime: **${backend}**
166
+ - Type-safe i18n: **${i18n ? 'enabled' : 'disabled'}**
167
+ - Design system: **${designSystem ? 'enabled' : 'disabled'}**
168
+ - Typed CSS: **${typedCss ? 'enabled' : 'disabled'}**
94
169
 
95
170
  Read \`.agents/skills/craft-ts-project/SKILL.md\` before changing application
96
- code. ${config?.i18n.enabled ? 'The type-safe i18n contract lives in `src/i18n/`; run `npm run i18n:check` and `npm run i18n:test` when changing it.' : ''} ${mode === 'effect' || config?.backendRuntime === 'effect' ? 'Effect-specific guidance is in `.agents/skills/craft-ts-effect-v4/SKILL.md`.' : ''}
171
+ code. ${i18n ? 'Translation keys live in `src/i18n/`; run `npm run i18n:check` and `npm run i18n:test` after changes.' : 'This starter has no i18n surface; do not add translation files unless the project configuration changes.'}
172
+ ${effectGuidance}
97
173
 
98
- ## Runtime boundaries
174
+ ## Workflow
99
175
 
100
- Frontend runtime: ${config?.frontendRuntime ?? (mode === 'effect' ? 'effect' : 'plain')}.
101
- Backend runtime: ${config?.backendRuntime ?? 'none'}. A backend Effect runtime
102
- never authorizes importing Effect into browser components.
176
+ Use Craft primitives and yield every Craft reader. Keep the browser, server and
177
+ transport boundaries aligned with the selected runtimes. The architecture
178
+ suite is a graph contract: run \`npm run architecture\` after structural
179
+ changes, and do not add a test per feature or a rule for a smell already
180
+ covered by the baseline helpers.
103
181
 
104
- The architecture suite is a graph contract. Run \`npm run architecture\`;
105
- do not add a test per feature. Add a rule only for a recurring product-level
106
- dependency smell not already covered by the baseline helpers.
107
- `;
182
+ ## Verification
183
+
184
+ Run the checks relevant to this generated configuration:
185
+
186
+ ${verify.map((command) => `- \`${command}\``).join('\n')}
187
+ ${referenceAgentGuidance(config)}`;
188
+ }
189
+ const AGENTS_MD = agentsMd;
108
190
  function json(value) {
109
191
  return `${JSON.stringify(value, null, 2)}\n`;
110
192
  }
@@ -115,15 +197,9 @@ function packageJson(context) {
115
197
  const hasI18n = context.config.i18n.enabled;
116
198
  const hasTypedCss = context.config.typedCss;
117
199
  const hasServer = context.config.backendRuntime !== 'none';
118
- const localReferences = context.config.references.mode === 'local';
119
- const sourceReferences = context.config.references.mode === 'source';
120
200
  const packageVersion = context.packageVersion ?? CRAFT_TS_STARTER_VERSION;
121
- const craftPackage = (name) => (localReferences || sourceReferences) && context.config.references.craftTs
122
- ? `file:.references/craft-ts/${sourceReferences ? craftReferencePackagePath(name) : localCraftReferencePackagePath(name)}`
123
- : packageVersion;
124
- const effectPackage = localReferences && context.config.references.effectTs
125
- ? 'file:.references/effect-ts/packages/effect'
126
- : EFFECT_V4_VERSION;
201
+ const craftPackage = () => packageVersion;
202
+ const effectPackage = EFFECT_V4_VERSION;
127
203
  return json({
128
204
  name: context.projectName,
129
205
  private: true,
@@ -170,37 +246,36 @@ function packageJson(context) {
170
246
  'typecheck-architecture': 'tsc -p tsconfig.architecture.json --noEmit',
171
247
  },
172
248
  dependencies: {
173
- '@craft-ts/component': craftPackage('component'),
174
- '@craft-ts/core': craftPackage('core'),
175
- ...(hasI18n ? { '@craft-ts/i18n': craftPackage('i18n') } : {}),
176
- ...(hasTypedCss ? { '@craft-ts/style': craftPackage('style') } : {}),
249
+ '@craft-ts/component': craftPackage(),
250
+ '@craft-ts/core': craftPackage(),
251
+ ...(hasI18n ? { '@craft-ts/i18n': craftPackage() } : {}),
252
+ ...(hasTypedCss ? { '@craft-ts/style': craftPackage() } : {}),
177
253
  ...(hasEffect && hasI18n
178
- ? { '@craft-ts/i18n-effect': craftPackage('i18n-effect') }
254
+ ? { '@craft-ts/i18n-effect': craftPackage() }
179
255
  : {}),
180
256
  ...(hasEffect
181
- ? { '@craft-ts/effect': craftPackage('effect'), effect: effectPackage }
257
+ ? { '@craft-ts/effect': craftPackage(), effect: effectPackage }
182
258
  : {}),
183
259
  },
184
260
  devDependencies: {
185
- '@craft-ts/dev-tools': craftPackage('dev-tools'),
186
- '@craft-ts/mcp': craftPackage('mcp'),
187
- '@craft-ts/function-registry-mcp': craftPackage('function-registry-mcp'),
188
- '@craft-ts/log-mcp': craftPackage('log-mcp'),
189
- '@craft-ts/log-server': craftPackage('log-server'),
190
- effect: effectPackage,
261
+ '@craft-ts/dev-tools': craftPackage(),
262
+ '@craft-ts/mcp': craftPackage(),
263
+ '@craft-ts/function-registry-mcp': craftPackage(),
264
+ '@craft-ts/log-mcp': craftPackage(),
265
+ '@craft-ts/log-server': craftPackage(),
266
+ ...(hasEffect ? { effect: effectPackage } : {}),
191
267
  ...(hasTypedCss
192
- ? { '@craft-ts/style-testing': craftPackage('style-testing') }
268
+ ? { '@craft-ts/style-testing': craftPackage() }
193
269
  : {}),
194
270
  '@playwright/test': '^1.52.0',
195
271
  '@types/node': '^22.0.0',
272
+ 'aria-query': '^5.3.2',
196
273
  jsdom: '^27.1.0',
197
274
  rxjs: '^7.8.0',
198
275
  tslib: '^2.3.0',
199
276
  ...(hasEffect
200
277
  ? {
201
- '@effect/tsgo': localReferences && context.config.references.effectTs
202
- ? 'file:.references/effect-ts/packages/tsgo'
203
- : '^0.24.3',
278
+ '@effect/tsgo': '^0.24.3',
204
279
  '@typescript/native': 'npm:typescript@7.0.2',
205
280
  }
206
281
  : {}),
@@ -215,61 +290,10 @@ function packageJson(context) {
215
290
  },
216
291
  });
217
292
  }
218
- function craftReferencePackagePath(name) {
219
- if (name === 'mcp' ||
220
- name === 'function-registry-mcp' ||
221
- name === 'log-mcp') {
222
- return `packages/${name}`;
223
- }
224
- if (name === 'log-server')
225
- return 'apps/log-server';
226
- return `libs/${name}`;
227
- }
228
- export function localCraftReferencePackagePath(name) {
229
- if (name === 'mcp' ||
230
- name === 'function-registry-mcp' ||
231
- name === 'log-mcp') {
232
- return `packages/${name}`;
233
- }
234
- if (name === 'log-server')
235
- return 'apps/log-server';
236
- return `dist/libs/${name}`;
237
- }
238
- function sourceReferenceRoot(context) {
239
- return context.config.workspace.kind === 'nx'
240
- ? '../../.references/craft-ts'
241
- : './.references/craft-ts';
242
- }
243
- function sourceReferenceAliases(context) {
244
- const root = sourceReferenceRoot(context);
245
- const aliases = {
246
- '@craft-ts/core': `${root}/libs/core/src/index.ts`,
247
- '@craft-ts/component': `${root}/libs/component/src/index.ts`,
248
- '@craft-ts/i18n': `${root}/libs/i18n/src/index.ts`,
249
- '@craft-ts/style': `${root}/libs/style/src/index.ts`,
250
- '@craft-ts/style/vite': `${root}/libs/style/src/plugin/vite.ts`,
251
- '@craft-ts/style-testing': `${root}/libs/style-testing/src/index.ts`,
252
- '@craft-ts/dev-tools': `${root}/libs/dev-tools/src/index.ts`,
253
- };
254
- if (context.config.frontendRuntime === 'effect' ||
255
- context.config.backendRuntime === 'effect') {
256
- aliases['@craft-ts/effect'] = `${root}/libs/effect/src/index.ts`;
257
- }
258
- if (context.config.i18n.enabled &&
259
- (context.config.frontendRuntime === 'effect' ||
260
- context.config.backendRuntime === 'effect')) {
261
- aliases['@craft-ts/i18n-effect'] = `${root}/libs/i18n-effect/src/index.ts`;
262
- }
263
- return aliases;
264
- }
265
293
  function tsconfig(context) {
266
294
  const hasEffect = context.config.frontendRuntime === 'effect' ||
267
295
  context.config.backendRuntime === 'effect';
268
296
  const hasServer = context.config.backendRuntime !== 'none';
269
- const sourceAliases = context.config.references.mode === 'source' &&
270
- context.config.references.craftTs
271
- ? Object.fromEntries(Object.entries(sourceReferenceAliases(context)).map(([name, path]) => [name, [path]]))
272
- : undefined;
273
297
  return json({
274
298
  compilerOptions: {
275
299
  target: 'ES2022',
@@ -282,7 +306,6 @@ function tsconfig(context) {
282
306
  erasableSyntaxOnly: false,
283
307
  skipLibCheck: true,
284
308
  types: ['node'],
285
- ...(sourceAliases ? { paths: sourceAliases } : {}),
286
309
  },
287
310
  references: [
288
311
  { path: './tsconfig.app.json' },
@@ -308,39 +331,49 @@ const tsconfigSpec = `{
308
331
  }\n`;
309
332
  const tsconfigEffect = `{
310
333
  "extends": "./tsconfig.json",
311
- "compilerOptions": { "noEmit": true },
334
+ "compilerOptions": {
335
+ "noEmit": true,
336
+ "plugins": [{
337
+ "name": "@effect/language-service",
338
+ "diagnostics": true,
339
+ "diagnosticsName": true,
340
+ "overrides": [{
341
+ "include": ["src/**/*.ts"],
342
+ "exclude": ["src/**/*.spec.ts", "src/**/*.test.ts"],
343
+ "options": {
344
+ "diagnosticSeverity": {
345
+ "floatingEffect": "error",
346
+ "missingEffectContext": "error",
347
+ "missingEffectError": "error",
348
+ "missingLayerContext": "error",
349
+ "missingReturnYieldStar": "error",
350
+ "missingStarInYieldEffectGen": "error",
351
+ "outdatedApi": "error",
352
+ "unsafeEffectTypeAssertion": "error",
353
+ "asyncFunction": "warning",
354
+ "newPromise": "warning",
355
+ "nodeBuiltinImport": "warning",
356
+ "preferSchemaOverJson": "warning"
357
+ }
358
+ }
359
+ }]
360
+ }]
361
+ },
312
362
  "include": ["src/**/*.ts"],
313
363
  "exclude": ["src/**/*.spec.ts", "src/**/*.test.ts"]
314
364
  }\n`;
315
365
  function viteConfig(context) {
316
366
  const typedCss = context.config.typedCss;
317
367
  const hasServer = context.config.backendRuntime !== 'none';
318
- const sourceReferences = context.config.references.mode === 'source' &&
319
- context.config.references.craftTs;
320
- const sourceRoot = sourceReferenceRoot(context);
321
- const styleImport = sourceReferences
322
- ? `import { craftStyle } from '${sourceRoot}/libs/style/src/plugin/vite.ts';`
323
- : "import { craftStyle } from '@craft-ts/style/vite';";
324
- const sourceAliasEntries = Object.entries(sourceReferenceAliases(context))
325
- .map(([name, path]) => ` ${JSON.stringify(name)}: resolvePath(import.meta.dirname, ${JSON.stringify(path)})`)
326
- .join(',\n');
327
- const styleAliasEntries = sourceReferences
328
- ? sourceAliasEntries
329
- : ` '@craft-ts/style': resolvePath(import.meta.dirname, 'node_modules/@craft-ts/style/src/index.js')`;
330
- const sourceAliasConfig = sourceReferences
331
- ? `resolve: {
332
- alias: {
333
- ${sourceAliasEntries}
334
- },
335
- },`
336
- : '';
368
+ const styleImport = "import { craftStyle } from '@craft-ts/style/vite';";
369
+ const styleAliasEntries = ` '@craft-ts/style': resolvePath(import.meta.dirname, 'node_modules/@craft-ts/style/src/index.js')`;
337
370
  const stylePlugin = typedCss
338
371
  ? ` craftStyle({ dumpPath: '.craft/style-graph.json', alias: {
339
372
  ${styleAliasEntries}
340
373
  } }),`
341
374
  : '';
342
375
  return `import { readFileSync } from 'node:fs';
343
- import { resolve as resolvePath } from 'node:path';
376
+ ${typedCss ? "import { resolve as resolvePath } from 'node:path';\n" : ''}
344
377
  import { defineConfig, type ViteDevServer } from 'vite';
345
378
  ${typedCss ? styleImport : ''}
346
379
 
@@ -378,9 +411,17 @@ ${hasServer
378
411
  return {
379
412
  name: 'craft-starter-server-functions',
380
413
  async configureServer(server: ViteDevServer) {
381
- const module = await server.ssrLoadModule('/src/server/server.ts') as typeof import('./src/server/server');
414
+ const module = await server.ssrLoadModule('/src/server/node-http.ts') as typeof import('./src/server/node-http');
382
415
  server.middlewares.use('/__server-functions', (request, response) => {
383
- void module.handleRequest(request, response);
416
+ void module.handleRequest(request, response).catch((error: unknown) => {
417
+ if (response.headersSent) {
418
+ response.destroy();
419
+ return;
420
+ }
421
+ response.statusCode = 500;
422
+ response.end('Internal Server Error');
423
+ console.error(error);
424
+ });
384
425
  });
385
426
  },
386
427
  };
@@ -401,7 +442,6 @@ ${hasServer ? ' serverFunctionsPlugin(),' : ''}
401
442
  port: starterPort,
402
443
  forwardConsole: true,
403
444
  },
404
- ${sourceAliasConfig}
405
445
  build: { target: 'es2022' },
406
446
  });
407
447
  `;
@@ -553,6 +593,12 @@ ${effect ? '' : " 'craft-ts/no-effect-import-in-frontend': 'error',"}
553
593
  },
554
594
  {
555
595
  files: ['src/**/*.spec.ts', 'src/**/*.test.ts'],
596
+ languageOptions: {
597
+ parserOptions: {
598
+ project: ['./tsconfig.spec.json'],
599
+ tsconfigRootDir: import.meta.dirname,
600
+ },
601
+ },
556
602
  rules: {
557
603
  'craft-ts/no-async-await': 'off',
558
604
  'craft-ts/no-throw': 'off',
@@ -564,6 +610,7 @@ ${effect ? '' : " 'craft-ts/no-effect-import-in-frontend': 'error',"}
564
610
  ${backendEffect
565
611
  ? ` {
566
612
  files: ['src/server/**/*.ts', 'src/**/*.fn-serveur.ts', 'src/**/*.mw-serveur.ts'],
613
+ ignores: ['src/**/*.spec.ts', 'src/**/*.test.ts'],
567
614
  plugins: { 'craft-ts': craftRules },
568
615
  languageOptions: {
569
616
  parserOptions: {
@@ -632,7 +679,8 @@ const styles = `:root {
632
679
  body { margin: 0; min-width: 320px; }
633
680
  a { color: #2457d6; }
634
681
  main { max-width: 860px; margin: 0 auto; padding: 3rem 1.25rem; }
635
- nav { display: flex; gap: 1rem; padding: 1rem 1.25rem; background: white; border-bottom: 1px solid #e3e7ef; }
682
+ nav { display: flex; align-items: center; gap: 1rem; padding: 1rem 1.25rem; background: white; border-bottom: 1px solid #e3e7ef; }
683
+ .starter-experimental-badge { display: inline-flex; align-items: center; margin-left: auto; padding: .25rem .55rem; border: 1px solid #f0c36d; border-radius: 999px; color: #7a4b00; background: #fff8e6; font-size: .75rem; font-weight: 600; line-height: 1.2; white-space: nowrap; }
636
684
  button:focus-visible, a:focus-visible { outline: 3px solid #7aa2ff; outline-offset: 3px; }
637
685
  .heading { font-size: var(--craft-font-size-heading); line-height: var(--craft-line-height-heading); font-weight: 700; }
638
686
  body, .body { font-size: var(--craft-font-size-body); line-height: var(--craft-line-height-body); }
@@ -1032,6 +1080,7 @@ function appTs(context) {
1032
1080
  div,
1033
1081
  main,
1034
1082
  nav,
1083
+ span,
1035
1084
  } from '@craft-ts/component';
1036
1085
  import { CraftRouterLink } from '@craft-ts/core';
1037
1086
  ${uiImport}
@@ -1046,6 +1095,7 @@ export const App = craftComponent(
1046
1095
  a('home', {}, 'Home').pipe(CraftRouterLink({ to: '' })),
1047
1096
  a('services', {}, 'Services').pipe(CraftRouterLink({ to: 'services' })),
1048
1097
  a('about', {}, 'About').pipe(CraftRouterLink({ to: 'about' })),
1098
+ span({ class: 'starter-experimental-badge' }, 'Experimental · feedback welcome'),
1049
1099
  ]),
1050
1100
  main(CraftRouterOutlet()),
1051
1101
  ${themeClose},
@@ -1065,7 +1115,15 @@ function routesTs(context) {
1065
1115
  }),
1066
1116
  `
1067
1117
  : '';
1068
- return `import { loadCraftComponent } from '@craft-ts/component';
1118
+ const backendEffectErrorHandler = context.config.frontendRuntime === 'plain' &&
1119
+ context.config.backendRuntime === 'effect'
1120
+ ? ` StarterRepositoryError: craftExceptionHandler(function* ({ globalError }) {
1121
+ return globalError();
1122
+ }),
1123
+ `
1124
+ : '';
1125
+ return `/* eslint-disable require-yield -- Route exception outcomes are synchronous by design. */
1126
+ import { loadCraftComponent } from '@craft-ts/component';
1069
1127
  import {
1070
1128
  assertExhaustiveRouteExceptions,
1071
1129
  craftExceptionHandler,
@@ -1085,6 +1143,7 @@ export const { appRoutes } = craftRoutes('app', [
1085
1143
  ),
1086
1144
  }, {
1087
1145
  ${httpErrorHandler}
1146
+ ${backendEffectErrorHandler}
1088
1147
  ${welcomeErrorHandler} }),
1089
1148
  craftRoute('about', {
1090
1149
  ...loadCraftComponent(({ withRetry }) =>
@@ -1200,23 +1259,30 @@ function serverFiles(context) {
1200
1259
  return {};
1201
1260
  const effect = context.config.backendRuntime === 'effect';
1202
1261
  const fnServer = effect
1203
- ? `import { serverFunction } from '@craft-ts/core';
1262
+ ? `import { serverFunction, type ServerFunctionSuccess } from '@craft-ts/core';
1204
1263
  import { Effect, Schema } from 'effect';
1205
1264
  import { StarterRepository } from './server/repository';
1265
+ import { starterMiddleware } from './starter.mw-serveur';
1206
1266
 
1207
1267
  const inputSchema = Schema.toStandardSchemaV1(Schema.Struct({ filter: Schema.String }));
1208
1268
  const outputSchema = Schema.toStandardSchemaV1(Schema.Struct({ title: Schema.String, body: Schema.String }));
1209
1269
 
1210
- export type StarterResponse = { readonly title: string; readonly body: string };
1211
-
1212
1270
  export const getStarterMessage = serverFunction(
1213
1271
  'starter.welcome', inputSchema, { exposure: 'client', output: outputSchema },
1214
- ).handler(({ input }) => Effect.gen(function* () {
1272
+ ).use(starterMiddleware).handler(({ input }) => Effect.gen(function* () {
1215
1273
  const repository = yield* StarterRepository;
1216
1274
  return yield* repository.welcome(input.filter);
1217
- })).exposeErrors({});
1275
+ })).exposeErrors({
1276
+ StarterRepositoryError: (errorPayload) => ({
1277
+ code: 'STARTER_REPOSITORY_FAILURE',
1278
+ status: 503,
1279
+ payload: { filter: errorPayload.filter, message: errorPayload.message },
1280
+ }),
1281
+ });
1282
+
1283
+ export type StarterResponse = ServerFunctionSuccess<typeof getStarterMessage>;
1218
1284
  `
1219
- : `import { flatMapContext, mapContext, portableServerFunction } from '@craft-ts/core';
1285
+ : `import { flatMapContext, mapContext, portableServerFunction, type SchemaOutput, type ServerFunctionContractOutput, type ServerFunctionSuccess } from '@craft-ts/core';
1220
1286
  import type { StandardSchemaV1 } from '@craft-ts/core';
1221
1287
  import { StarterRepository } from './server/repository';
1222
1288
 
@@ -1224,141 +1290,278 @@ type Input = { readonly filter: string };
1224
1290
  const inputSchema: StandardSchemaV1<Input, Input> = { '~standard': { version: 1, vendor: 'craft-starter', types: undefined,
1225
1291
  validate(value: unknown) { return typeof value === 'object' && value !== null && typeof (value as Input).filter === 'string'
1226
1292
  ? { value: value as Input } : { issues: [{ message: 'filter must be a string' }] }; } } };
1227
- export type StarterResponse = { readonly title: string; readonly body: string };
1293
+ const outputSchema: StandardSchemaV1<{ readonly title: string; readonly body: string }, { readonly title: string; readonly body: string }> = { '~standard': { version: 1, vendor: 'craft-starter', types: undefined,
1294
+ validate(value: unknown) { return typeof value === 'object' && value !== null && typeof (value as { title?: unknown }).title === 'string' && typeof (value as { body?: unknown }).body === 'string'
1295
+ ? { value: value as { readonly title: string; readonly body: string } } : { issues: [{ message: 'welcome response must contain title and body' }] }; } } };
1228
1296
 
1229
- export const getStarterMessage = portableServerFunction('starter.welcome', inputSchema, { exposure: 'client' })
1297
+ export const getStarterMessage = portableServerFunction('starter.welcome', inputSchema, { exposure: 'client', output: outputSchema })
1230
1298
  .pipe(
1231
1299
  mapContext(({ input }) => ({ normalizedFilter: input.filter.trim() })),
1232
1300
  flatMapContext(() => StarterRepository.welcome()),
1233
1301
  )
1234
- .handler(async ({ context }) => context.value)
1302
+ .handler(async ({ context }) => context.value as SchemaOutput<typeof outputSchema>)
1235
1303
  .exposeErrors({});
1304
+
1305
+ export type StarterResponse = ServerFunctionContractOutput<typeof getStarterMessage['contract']>;
1236
1306
  `;
1237
1307
  const repository = effect
1238
- ? `import { Context, Effect, Layer } from 'effect';
1308
+ ? `import { Context, Data, Effect, Layer } from 'effect';
1239
1309
 
1240
1310
  export type StarterRepositoryShape = {
1241
- readonly welcome: (filter: string) => Effect.Effect<{ readonly title: string; readonly body: string }>;
1311
+ readonly welcome: (filter: string) => Effect.Effect<
1312
+ { readonly title: string; readonly body: string },
1313
+ StarterRepositoryError
1314
+ >;
1242
1315
  };
1243
1316
  export class StarterRepository extends Context.Service<StarterRepository, StarterRepositoryShape>()('starter/StarterRepository') {}
1317
+ export class StarterRepositoryError extends Data.TaggedError('StarterRepositoryError')<{
1318
+ readonly filter: string;
1319
+ readonly message: string;
1320
+ }> {}
1321
+
1244
1322
  export const StarterRepositoryLive = Layer.succeed(StarterRepository, {
1245
- welcome: (filter) => Effect.succeed({ title: 'Hello from the server', body: 'Effect server function: ' + filter }),
1323
+ welcome: (filter) => Effect.tryPromise({
1324
+ try: () => filter === 'error'
1325
+ ? Promise.reject(new Error('The starter repository failed.'))
1326
+ : Promise.resolve({ title: 'Hello from the server', body: 'Effect server function: ' + filter }),
1327
+ catch: (cause) => new StarterRepositoryError({
1328
+ filter,
1329
+ message: cause instanceof Error ? cause.message : String(cause),
1330
+ }),
1331
+ }),
1246
1332
  });
1247
1333
  `
1248
1334
  : `export const StarterRepository = {
1249
1335
  welcome: async () => ({ value: { title: 'Hello from the server', body: 'Promise server function works.' } }),
1250
1336
  };
1251
1337
  `;
1252
- let server = '';
1253
- if (effect) {
1254
- server = `import { createServer } from '@craft-ts/core';
1338
+ const application = effect
1339
+ ? `import { createServer, type Server } from '@craft-ts/core';
1255
1340
  import { executeEffect } from '@craft-ts/effect';
1341
+ ${context.config.i18n.enabled ? "import { Layer } from 'effect';\n" : ''}
1256
1342
  import { getStarterMessage } from '../starter.fn-serveur';
1257
1343
  import { StarterRepositoryLive } from './repository';
1258
- import type { IncomingMessage, ServerResponse } from 'node:http';
1344
+ ${context.config.i18n.enabled ? "import { serverI18nLayer } from './i18n';\n" : ''}
1345
+
1346
+ export const runtimeLayer = ${context.config.i18n.enabled ? 'Layer.mergeAll(StarterRepositoryLive, serverI18nLayer)' : 'StarterRepositoryLive'};
1347
+
1348
+ export function createApplication(layer = runtimeLayer): Server {
1349
+ return createServer({
1350
+ functions: [getStarterMessage],
1351
+ execute: executeEffect(layer).run,
1352
+ runtimeOptions: {
1353
+ maxBodyBytes: 1_000_000,
1354
+ maxOutputBytes: 1_000_000,
1355
+ timeoutMs: 10_000,
1356
+ },
1357
+ });
1358
+ }
1259
1359
 
1260
- export const application = createServer({ functions: [getStarterMessage], execute: executeEffect(StarterRepositoryLive).run });
1261
- export const runtime = StarterRepositoryLive;
1262
- export async function handleRequest(request: IncomingMessage, response: ServerResponse) {
1263
- const chunks: Buffer[] = [];
1264
- for await (const chunk of request) chunks.push(Buffer.from(chunk));
1265
- const host = typeof request.headers.host === 'string' ? request.headers.host : '127.0.0.1';
1266
- const webResponse = await application.handle(new Request('http://' + host + '/__server-functions', {
1267
- method: request.method,
1268
- headers: Object.entries(request.headers).flatMap(([name, value]) => value === undefined ? [] : [[name, Array.isArray(value) ? value.join(', ') : value]]),
1269
- body: request.method === 'GET' || request.method === 'HEAD' ? undefined : Buffer.concat(chunks),
1270
- }));
1271
- response.statusCode = webResponse.status;
1272
- webResponse.headers.forEach((value, name) => response.setHeader(name, value));
1273
- response.end(Buffer.from(await webResponse.arrayBuffer()));
1360
+ export const application = createApplication();
1361
+ `
1362
+ : `import { createServer, type Server } from '@craft-ts/core';
1363
+ import { getStarterMessage } from '../starter.fn-serveur';
1364
+
1365
+ export function createApplication(): Server {
1366
+ return createServer({
1367
+ functions: [getStarterMessage],
1368
+ runtimeOptions: {
1369
+ maxBodyBytes: 1_000_000,
1370
+ maxOutputBytes: 1_000_000,
1371
+ timeoutMs: 10_000,
1372
+ },
1373
+ });
1274
1374
  }
1375
+
1376
+ export const application = createApplication();
1275
1377
  `;
1276
- }
1277
- if (!effect) {
1278
- server = `import { createServer } from '@craft-ts/core';
1279
- import { getStarterMessage } from '../starter.fn-serveur';
1378
+ const nodeHttp = `/* eslint-disable craft-ts/no-async-await -- The Node adapter is an async platform boundary. */
1379
+ import { application } from './application';
1280
1380
  import type { IncomingMessage, ServerResponse } from 'node:http';
1281
1381
 
1282
- export const application = createServer({ functions: [getStarterMessage] });
1283
- export async function handleRequest(request: IncomingMessage, response: ServerResponse) {
1284
- const chunks: Buffer[] = [];
1285
- for await (const chunk of request) chunks.push(Buffer.from(chunk));
1286
- const host = typeof request.headers.host === 'string' ? request.headers.host : '127.0.0.1';
1287
- const webResponse = await application.handle(new Request('http://' + host + '/__server-functions', {
1288
- method: request.method,
1289
- headers: Object.entries(request.headers).flatMap(([name, value]) => value === undefined ? [] : [[name, Array.isArray(value) ? value.join(', ') : value]]),
1290
- body: request.method === 'GET' || request.method === 'HEAD' ? undefined : Buffer.concat(chunks),
1291
- }));
1382
+ export async function handleRequest(request: IncomingMessage, response: ServerResponse): Promise<void> {
1383
+ const abortController = new AbortController();
1384
+ const abort = () => abortController.abort();
1385
+ const close = () => {
1386
+ if (!request.complete) abort();
1387
+ };
1388
+ request.once('aborted', abort);
1389
+ request.once('close', close);
1390
+ try {
1391
+ const webResponse = await application.handle(toWebRequest(request, abortController.signal));
1392
+ await writeWebResponse(webResponse, response, request.method === 'HEAD');
1393
+ } finally {
1394
+ request.off('aborted', abort);
1395
+ request.off('close', close);
1396
+ }
1397
+ }
1398
+
1399
+ function toWebRequest(request: IncomingMessage, signal: AbortSignal): Request {
1400
+ const headers = new Headers();
1401
+ for (const [name, value] of Object.entries(request.headers)) {
1402
+ if (value !== undefined) headers.set(name, Array.isArray(value) ? value.join(', ') : value);
1403
+ }
1404
+ const method = request.method ?? 'GET';
1405
+ const hasBody = method !== 'GET' && method !== 'HEAD';
1406
+ return new Request(
1407
+ 'http://' + (request.headers.host ?? '127.0.0.1') + (request.url ?? '/'),
1408
+ {
1409
+ method,
1410
+ headers,
1411
+ signal,
1412
+ ...(hasBody ? { body: request as unknown as BodyInit, duplex: 'half' } : {}),
1413
+ } as RequestInit,
1414
+ );
1415
+ }
1416
+
1417
+ async function writeWebResponse(
1418
+ webResponse: Response,
1419
+ response: ServerResponse,
1420
+ head: boolean,
1421
+ ): Promise<void> {
1292
1422
  response.statusCode = webResponse.status;
1293
1423
  webResponse.headers.forEach((value, name) => response.setHeader(name, value));
1424
+ if (head || webResponse.body === null) {
1425
+ response.end();
1426
+ return;
1427
+ }
1294
1428
  response.end(Buffer.from(await webResponse.arrayBuffer()));
1295
1429
  }
1296
1430
  `;
1297
- }
1298
- const client = `import { createServerFunctionClient, craftUnique } from '@craft-ts/core';
1431
+ const client = `import { createServerFunctionClient, craftUnique, type ServerFunctionClient } from '@craft-ts/core';
1299
1432
  import type { getStarterMessage as ServerGetStarterMessage, StarterResponse } from './starter.fn-serveur';
1300
1433
 
1301
1434
  export type { StarterResponse };
1302
- export const getStarterMessage = createServerFunctionClient<typeof ServerGetStarterMessage>(craftUnique('starter.welcome'));
1435
+ const starterMessageTransport = createServerFunctionClient<typeof ServerGetStarterMessage>(craftUnique('starter.welcome'));
1436
+ export const getStarterMessage = starterMessageTransport as ServerFunctionClient<typeof ServerGetStarterMessage, StarterResponse>;
1437
+ `;
1438
+ const compatibilityServer = `export { application, createApplication } from './application';
1439
+ ${effect ? 'export { runtimeLayer } from \'./application\';\n' : ''}export { handleRequest } from './node-http';
1303
1440
  `;
1304
1441
  return {
1305
1442
  'src/server/repository.ts': repository,
1306
- 'src/server/server.ts': server,
1443
+ 'src/server/application.ts': application,
1444
+ 'src/server/node-http.ts': nodeHttp,
1445
+ 'src/server/server.ts': compatibilityServer,
1307
1446
  'src/starter.fn-serveur.ts': fnServer,
1308
1447
  'src/starter.fn-client.ts': client,
1309
1448
  ...(effect && context.config.i18n.enabled
1310
1449
  ? {
1311
- 'src/server/i18n.ts': "import { provideI18nRuntime } from '@craft-ts/i18n-effect';\nimport { i18n } from '../i18n/runtime';\nexport const serverI18nLayer = provideI18nRuntime(i18n);\nexport { translateEffect } from '../i18n/effect';\n",
1450
+ 'src/server/i18n.ts': "import { provideI18nRuntime } from '@craft-ts/i18n-effect';\nimport { i18n } from '../i18n/runtime';\n\nexport const serverI18nLayer = provideI18nRuntime(i18n);\n",
1312
1451
  }
1313
1452
  : {}),
1314
1453
  ...(effect
1315
1454
  ? {
1316
- 'src/starter.mw-serveur.ts': "import { Effect } from 'effect';\nimport { effectServerMiddleware } from '@craft-ts/effect';\nexport const starterMiddleware = effectServerMiddleware('starter.middleware', () => Effect.succeed({ value: undefined }));\n",
1455
+ 'src/starter.mw-serveur.ts': "import { Effect } from 'effect';\nimport { effectServerMiddleware } from '@craft-ts/effect';\n\nexport const starterMiddleware = effectServerMiddleware('starter.middleware', () =>\n Effect.gen(function* () {\n yield* Effect.log('starter middleware executed');\n return { value: undefined };\n }),\n);\n",
1317
1456
  }
1318
1457
  : {}),
1319
1458
  'vitest.server.config.ts': `import { defineConfig } from 'vitest/config';
1320
1459
  export default defineConfig({ test: { name: 'craft-starter-server', globals: true, environment: 'node', include: ['src/server/**/*.spec.ts'] } });
1321
1460
  `,
1322
- 'src/server/server.spec.ts': `import { describe, expect, it } from 'vitest';
1323
- import { application } from './server';
1461
+ 'src/server/server.spec.ts': serverSpec(context),
1462
+ };
1463
+ }
1464
+ function serverSpec(context) {
1465
+ const effectFailure = context.config.backendRuntime === 'effect'
1466
+ ? `
1467
+ it('exposes the typed Effect repository failure', async () => {
1468
+ const response = await application.handle(new Request('http://127.0.0.1/__server-functions', {
1469
+ method: 'POST',
1470
+ headers: { 'content-type': 'application/json' },
1471
+ body: JSON.stringify({ id: 'starter.welcome', input: { filter: 'error' } }),
1472
+ }));
1473
+ expect(response.status).toBe(503);
1474
+ await expect(response.json()).resolves.toMatchObject({
1475
+ error: { _tag: 'StarterRepositoryError', code: 'STARTER_REPOSITORY_FAILURE', filter: 'error' },
1476
+ });
1477
+ });
1478
+ `
1479
+ : '';
1480
+ return `import { createServer as createNodeServer } from 'node:http';
1481
+ import { describe, expect, it } from 'vitest';
1482
+ import { createServer, serverFunction, type StandardSchemaV1 } from '@craft-ts/core';
1483
+ import { application, handleRequest } from './server';
1484
+
1485
+ const invalidInputSchema: StandardSchemaV1<unknown, unknown> = {
1486
+ '~standard': { version: 1, vendor: 'craft-starter-test', types: undefined,
1487
+ validate(value: unknown) { return { value }; } },
1488
+ };
1489
+ const invalidOutputSchema: StandardSchemaV1<{ readonly required: string }, { readonly required: string }> = {
1490
+ '~standard': { version: 1, vendor: 'craft-starter-test', types: undefined,
1491
+ validate(value: unknown) {
1492
+ return typeof value === 'object' && value !== null && typeof (value as { required?: unknown }).required === 'string'
1493
+ ? { value: value as { readonly required: string } }
1494
+ : { issues: [{ message: 'required must be a string' }] };
1495
+ } },
1496
+ };
1497
+ const invalidOutput = serverFunction(
1498
+ 'starter.invalid-output',
1499
+ invalidInputSchema,
1500
+ { exposure: 'server', output: invalidOutputSchema },
1501
+ ).handler(() => ({ required: 123 })).exposeErrors({});
1324
1502
 
1325
1503
  describe('server function registry', () => {
1326
- it('registers the starter function', () => expect(application).toBeDefined());
1504
+ it('invokes the starter function through the registry', async () => {
1505
+ await expect(application.invoke('starter.welcome', { filter: 'Ada' })).resolves.toMatchObject({
1506
+ title: 'Hello from the server',
1507
+ });
1508
+ });
1509
+
1510
+ it('rejects invalid input', async () => {
1511
+ await expect(application.invoke('starter.welcome', { filter: 123 })).rejects.toThrow(
1512
+ 'CRAFT_SERVER_FUNCTION_INPUT_INVALID',
1513
+ );
1514
+ });
1515
+
1516
+ it('rejects invalid output', async () => {
1517
+ const server = createServer({ functions: [invalidOutput] });
1518
+ await expect(server.invoke('starter.invalid-output', undefined)).rejects.toThrow(
1519
+ 'CRAFT_SERVER_FUNCTION_OUTPUT_INVALID',
1520
+ );
1521
+ });
1522
+ ${effectFailure}
1523
+
1524
+ it('serves a real HTTP request through the Node adapter', async () => {
1525
+ const nodeServer = createNodeServer((request, response) => {
1526
+ void handleRequest(request, response).catch((error: unknown) => {
1527
+ if (!response.headersSent) response.statusCode = 500;
1528
+ response.end('Internal Server Error');
1529
+ throw error;
1530
+ });
1531
+ });
1532
+ await new Promise<void>((resolve) => nodeServer.listen(0, '127.0.0.1', resolve));
1533
+ const address = nodeServer.address();
1534
+ if (!address || typeof address === 'string') throw new Error('Server did not start.');
1535
+ try {
1536
+ const response = await fetch('http://127.0.0.1:' + address.port + '/__server-functions', {
1537
+ method: 'POST',
1538
+ headers: { 'content-type': 'application/json' },
1539
+ body: JSON.stringify({ id: 'starter.welcome', input: { filter: 'Ada' } }),
1540
+ });
1541
+ expect(response.status).toBe(200);
1542
+ await expect(response.json()).resolves.toMatchObject({ title: 'Hello from the server' });
1543
+ } finally {
1544
+ await new Promise<void>((resolve, reject) => nodeServer.close((error) => error ? reject(error) : resolve()));
1545
+ }
1546
+ });
1547
+
1548
+ it('rejects unsupported method and content type', async () => {
1549
+ await expect(application.handle(new Request('http://127.0.0.1/__server-functions'))).resolves.toHaveProperty('status', 405);
1550
+ await expect(application.handle(new Request('http://127.0.0.1/__server-functions', {
1551
+ method: 'POST',
1552
+ headers: { 'content-type': 'text/plain' },
1553
+ body: '{}',
1554
+ }))).resolves.toHaveProperty('status', 415);
1555
+ });
1327
1556
  });
1328
- `,
1329
- };
1557
+ `;
1330
1558
  }
1331
- export const EFFECT_REFERENCE_PATHS = [
1332
- 'apps/demo-effect',
1333
- 'apps/quickstart-effect',
1334
- 'apps/demo-with-server-function',
1335
- 'apps/docs/learn-effect',
1336
- 'apps/docs/tsconfig.learn-effect.json',
1337
- 'apps/docs/tests/snippets/learn-effect',
1338
- 'apps/docs/guide/advanced/effect.md',
1339
- 'apps/docs/guide/i18n/effect.md',
1340
- 'apps/docs/guide/reactivity/craft-effect.md',
1341
- 'apps/docs/guide/testing/architecture/craft-effect-imperative-sync.md',
1342
- 'apps/docs/guide/testing/architecture/craft-effect-network.md',
1343
- 'apps/docs/resources/effect-adoption.md',
1344
- 'apps/docs/resources/effect-compatibility.md',
1345
- 'apps/docs/public/assets/effect-logo-black.png',
1346
- 'apps/docs/public/assets/effect-craft-ts-hover.png',
1347
- 'apps/docs/public/assets/effect-craft-mark-hover.png',
1348
- 'libs/effect',
1349
- 'libs/i18n-effect',
1350
- 'packages/mcp/skills/craft-ts-effect-v4',
1351
- 'tools/effect-diagnostics',
1352
- 'tools/compile-learn-effect-examples.mjs',
1353
- 'tools/run-effect-tsgo.mjs',
1354
- 'tools/effect-typecost',
1355
- ];
1356
1559
  function referenceFiles(context) {
1357
1560
  if (!context.config.references.craftTs && !context.config.references.effectTs)
1358
1561
  return {};
1359
1562
  const manifest = {
1360
1563
  schemaVersion: 1,
1361
- mode: context.config.references.mode,
1564
+ mode: 'context',
1362
1565
  effectEnabled: context.config.frontendRuntime === 'effect' ||
1363
1566
  context.config.backendRuntime === 'effect',
1364
1567
  };
@@ -1376,14 +1579,19 @@ function referenceFiles(context) {
1376
1579
  resolvedSha: '',
1377
1580
  path: '.references/effect-ts',
1378
1581
  };
1379
- const resolver = "export function resolveReferencePath(root, name) { const manifest = resolveReferenceManifest(root); return manifest[name] ? resolve(root, manifest[name].path) : undefined; }\nexport function resolveReferenceManifest(root) { return JSON.parse(readFileSync(join(root, '.references/manifest.json'), 'utf8')); }\nimport { readFileSync } from 'node:fs'; import { join, resolve } from 'node:path';\n";
1380
- const updater = `import { execFileSync } from 'node:child_process'; import { readFileSync, rmSync, writeFileSync } from 'node:fs'; import { join, resolve } from 'node:path';
1381
- const root = resolve(import.meta.dirname, '..'); const manifestPath = join(root, '.references/manifest.json'); const manifest = JSON.parse(readFileSync(manifestPath, 'utf8'));
1382
- const effectReferencePaths = ${JSON.stringify(EFFECT_REFERENCE_PATHS)};
1383
- function pruneEffectReference(referenceRoot) { for (const relativePath of effectReferencePaths) rmSync(join(referenceRoot, relativePath), { recursive: true, force: true }); }
1384
- const craftProjects = ['craft-ts-core', 'craft-ts-component', 'dev-tools', 'mcp', 'craft-ts-i18n', 'craft-ts-i18n-effect', 'craft-ts-style', 'craft-ts-style-testing', ...(manifest.effectEnabled ? ['craft-ts-effect'] : [])].join(',');
1385
- function buildReference(name, path) { if (name === 'craftTs' && manifest.mode === 'local') { execFileSync('npx', ['--no-install', 'nx', 'run-many', '--target=build', '--projects', craftProjects, '--skipSync', '--outputStyle=stream'], { cwd: path, stdio: 'inherit' }); for (const workspace of ['@craft-ts/log-server', '@craft-ts/log-mcp', '@craft-ts/function-registry-mcp']) execFileSync('npm', ['run', 'build', '--workspace', workspace], { cwd: path, stdio: 'inherit' }); } else if (name === 'effectTs' && (manifest.mode === 'local' || manifest.mode === 'source')) execFileSync('npm', ['run', 'build'], { cwd: path, stdio: 'inherit' }); }
1386
- for (const [name, entry] of Object.entries(manifest).filter(([key, value]) => !['schemaVersion', 'mode', 'effectEnabled'].includes(key) && value && value.path)) { const path = resolve(root, entry.path); if (execFileSync('git', ['status', '--short'], { cwd: path, encoding: 'utf8' }).trim()) throw new Error('Modified reference: ' + path); execFileSync('git', ['fetch', '--depth', '1', 'origin', entry.requestedRef], { cwd: path, stdio: 'inherit' }); execFileSync('git', ['checkout', '--detach', 'FETCH_HEAD'], { cwd: path, stdio: 'inherit' }); if (manifest.mode === 'local' || (manifest.mode === 'source' && name === 'effectTs')) { execFileSync('npm', ['install', '--no-audit', '--no-fund'], { cwd: path, stdio: 'inherit' }); buildReference(name, path); } if (name === 'craftTs' && manifest.effectEnabled === false) pruneEffectReference(path); entry.resolvedSha = execFileSync('git', ['rev-parse', 'HEAD'], { cwd: path, encoding: 'utf8' }).trim(); }
1582
+ const resolver = "import { readFileSync } from 'node:fs'; import { join, resolve } from 'node:path';\nexport function resolveReferenceManifest(root) { return JSON.parse(readFileSync(join(root, '.references/manifest.json'), 'utf8')); }\nexport function resolveReferencePath(root, name) { const manifest = resolveReferenceManifest(root); return manifest[name] ? resolve(root, manifest[name].path) : undefined; }\n";
1583
+ const updater = `import { execFileSync } from 'node:child_process'; import { existsSync, readFileSync, writeFileSync } from 'node:fs'; import { join, resolve } from 'node:path';
1584
+ const root = resolve(import.meta.dirname, '..');
1585
+ const manifestPath = join(root, '.references/manifest.json');
1586
+ const manifest = JSON.parse(readFileSync(manifestPath, 'utf8'));
1587
+ for (const [name, entry] of Object.entries(manifest).filter(([key, value]) => !['schemaVersion', 'mode', 'effectEnabled'].includes(key) && value && value.path)) {
1588
+ const path = resolve(root, entry.path);
1589
+ if (!existsSync(join(path, '.git'))) throw new Error('Missing reference clone: ' + path);
1590
+ if (execFileSync('git', ['status', '--short'], { cwd: path, encoding: 'utf8' }).trim()) throw new Error('Modified reference: ' + path);
1591
+ execFileSync('git', ['fetch', '--depth', '1', 'origin', entry.requestedRef], { cwd: path, stdio: 'inherit' });
1592
+ execFileSync('git', ['checkout', '--detach', 'FETCH_HEAD'], { cwd: path, stdio: 'inherit' });
1593
+ entry.resolvedSha = execFileSync('git', ['rev-parse', 'HEAD'], { cwd: path, encoding: 'utf8' }).trim();
1594
+ }
1387
1595
  writeFileSync(manifestPath, JSON.stringify(manifest, null, 2) + '\\n');
1388
1596
  `;
1389
1597
  return {
@@ -1422,22 +1630,6 @@ function cloneReferenceIfRequested(root, config) {
1422
1630
  mkdirSync(dirname(target), { recursive: true });
1423
1631
  execFileSync('git', ['clone', '--depth', '1', '--branch', ref, url, target], { cwd: root, stdio: 'inherit' });
1424
1632
  }
1425
- if (relativePath === '.references/craft-ts' &&
1426
- config.frontendRuntime !== 'effect' &&
1427
- config.backendRuntime !== 'effect') {
1428
- pruneEffectReference(target);
1429
- }
1430
- if (config.references.mode === 'local' ||
1431
- (config.references.mode === 'source' &&
1432
- relativePath === '.references/effect-ts')) {
1433
- execFileSync('npm', ['install', '--no-audit', '--no-fund'], {
1434
- cwd: target,
1435
- stdio: 'inherit',
1436
- });
1437
- for (const [command, args] of getReferenceBuildSteps(relativePath, config)) {
1438
- execFileSync(command, [...args], { cwd: target, stdio: 'inherit' });
1439
- }
1440
- }
1441
1633
  }
1442
1634
  const manifestPath = join(root, '.references', 'manifest.json');
1443
1635
  if (existsSync(manifestPath)) {
@@ -1454,51 +1646,6 @@ function cloneReferenceIfRequested(root, config) {
1454
1646
  writeFileSync(manifestPath, `${JSON.stringify(manifest, null, 2)}\n`, 'utf8');
1455
1647
  }
1456
1648
  }
1457
- /**
1458
- * CraftTS has no root `build` script: its published packages are Nx targets,
1459
- * while the MCP and log workspaces keep their own package build scripts.
1460
- */
1461
- export function getReferenceBuildSteps(relativePath, config) {
1462
- if (relativePath === '.references/effect-ts') {
1463
- return [['npm', ['run', 'build']]];
1464
- }
1465
- if (relativePath !== '.references/craft-ts')
1466
- return [];
1467
- const hasEffect = config.frontendRuntime === 'effect' || config.backendRuntime === 'effect';
1468
- const projects = [
1469
- 'craft-ts-core',
1470
- 'craft-ts-component',
1471
- 'dev-tools',
1472
- 'mcp',
1473
- ...(config.i18n.enabled ? ['craft-ts-i18n'] : []),
1474
- ...(config.i18n.enabled && hasEffect ? ['craft-ts-i18n-effect'] : []),
1475
- ...(config.typedCss
1476
- ? ['craft-ts-style', 'craft-ts-style-testing']
1477
- : []),
1478
- ...(hasEffect ? ['craft-ts-effect'] : []),
1479
- ];
1480
- return [
1481
- [
1482
- 'npx',
1483
- [
1484
- '--no-install',
1485
- 'nx',
1486
- 'run-many',
1487
- '--target=build',
1488
- '--projects',
1489
- projects.join(','),
1490
- '--skipSync',
1491
- '--outputStyle=stream',
1492
- ],
1493
- ],
1494
- ['npm', ['run', 'build', '--workspace', '@craft-ts/log-server']],
1495
- ['npm', ['run', 'build', '--workspace', '@craft-ts/log-mcp']],
1496
- [
1497
- 'npm',
1498
- ['run', 'build', '--workspace', '@craft-ts/function-registry-mcp'],
1499
- ],
1500
- ];
1501
- }
1502
1649
  function ensureGitignore(root) {
1503
1650
  const gitignorePath = join(root, '.gitignore');
1504
1651
  if (!existsSync(gitignorePath)) {
@@ -1531,11 +1678,6 @@ function initialiseGitRepository(root) {
1531
1678
  }
1532
1679
  execFileSync('git', ['init', '--quiet'], { cwd: root, stdio: 'ignore' });
1533
1680
  }
1534
- export function pruneEffectReference(root) {
1535
- for (const relativePath of EFFECT_REFERENCE_PATHS) {
1536
- rmSync(join(root, relativePath), { recursive: true, force: true });
1537
- }
1538
- }
1539
1681
  function aboutPageTs(context) {
1540
1682
  const designSystem = context.config.designSystem !== 'none';
1541
1683
  const surfaceImport = designSystem
@@ -1593,7 +1735,8 @@ const { StarterService } = craftService({ name: 'StarterService', providedIn: 'g
1593
1735
  return { label: 'resolved through Craft DI' };
1594
1736
  });
1595
1737
  `;
1596
- return `import { craftComponent, div, heading, p } from '@craft-ts/component';
1738
+ const lintComment = context.config.frontendRuntime === 'effect' ? '' : '/* eslint-disable require-yield -- Synchronous DI factory is intentional in this starter. */\n';
1739
+ return `${lintComment}import { craftComponent, div, heading, p } from '@craft-ts/component';
1597
1740
  ${uiImport}${i18n}${effectI18n}${service}
1598
1741
  export const ServicesPage = craftComponent(
1599
1742
  'ServicesPage',
@@ -1624,9 +1767,9 @@ function plainHomePageTs(context) {
1624
1767
  const summary = context.config.i18n.enabled
1625
1768
  ? "p('i18n: ' + i18n.t('order.summary', { amount: 1234.5, count: 2, date: Date.UTC(2026, 0, 15) })),"
1626
1769
  : "p('A framework-independent starter with a typed API boundary.'),";
1627
- const loadExpression = context.config.backendRuntime === 'none'
1628
- ? 'return yield* loadWelcome();'
1629
- : 'return loadWelcome();';
1770
+ const loadLoader = context.config.backendRuntime === 'none'
1771
+ ? 'function* () { return yield* loadWelcome(); }'
1772
+ : '() => loadWelcome()';
1630
1773
  return `import {
1631
1774
  craftComponent,
1632
1775
  div,
@@ -1646,9 +1789,7 @@ export const HomePage = craftComponent(
1646
1789
  'welcomeQuery',
1647
1790
  {
1648
1791
  params: () => true,
1649
- loader: function* () {
1650
- ${loadExpression}
1651
- },
1792
+ loader: ${loadLoader},
1652
1793
  },
1653
1794
  ({ resource }) => ({
1654
1795
  hasWelcome: craftComputed('hasWelcome', () => resource.hasValue()),
@@ -1664,12 +1805,10 @@ export const HomePage = craftComponent(
1664
1805
  ifNode(welcomeQuery.hasWelcome, () =>
1665
1806
  div([
1666
1807
  p(function* () {
1667
- const welcome = yield* welcomeQuery.value();
1668
- return 'API title: ' + (welcome?.title ?? '');
1808
+ return 'API title: ' + String((yield* welcomeQuery.value())?.title);
1669
1809
  }),
1670
1810
  p(function* () {
1671
- const welcome = yield* welcomeQuery.value();
1672
- return 'API body: ' + (welcome?.body ?? '');
1811
+ return 'API body: ' + String((yield* welcomeQuery.value())?.body);
1673
1812
  }),
1674
1813
  ]),
1675
1814
  ),
@@ -2051,7 +2190,7 @@ function readme(context) {
2051
2190
  : []),
2052
2191
  ...(references
2053
2192
  ? [
2054
- `- local CraftTS/EffectTS references under \`${referencePath}/\`; run \`npm run update:references\` to refresh them.`,
2193
+ `- cloned CraftTS/EffectTS source references for coding agents under \`${referencePath}/\`; run \`npm run update:references\` to refresh them.`,
2055
2194
  ]
2056
2195
  : []),
2057
2196
  '',
@@ -2105,7 +2244,8 @@ function readme(context) {
2105
2244
  '## References',
2106
2245
  '',
2107
2246
  `The \`${referencePath}/manifest.json\` file records the requested refs and resolved SHAs.`,
2108
- 'The `context` mode keeps npm dependencies portable; `local` is reserved for build artifacts.',
2247
+ 'The cloned repositories are read-only context for coding agents; the application always uses the npm dependencies declared in package.json.',
2248
+ 'Do not add file: dependencies or TypeScript/Vite aliases to the clones.',
2109
2249
  'Run `npm run update:references` after reviewing local changes in a clone.',
2110
2250
  ]
2111
2251
  : []),
@@ -2131,10 +2271,9 @@ function agentFiles(mode, agent, config) {
2131
2271
  const baseSkill = config?.i18n.enabled === false
2132
2272
  ? BASE_AGENT_SKILL.replace(/4\. Keep translations[\s\S]*?5\. Keep visual/, '5. Keep visual')
2133
2273
  : BASE_AGENT_SKILL;
2134
- const skill = `${baseSkill}${effectEnabled ? `\n${EFFECT_AGENT_SKILL}` : ''}`;
2274
+ const skill = `${baseSkill}${effectEnabled ? `\n${EFFECT_AGENT_SKILL}` : ''}${referenceAgentGuidance(config)}`;
2135
2275
  if (agent === 'codex') {
2136
2276
  return {
2137
- 'AGENTS.md': AGENTS_MD(mode, config),
2138
2277
  '.agents/skills/craft-ts-project/SKILL.md': skill,
2139
2278
  ...(effectEnabled
2140
2279
  ? { '.agents/skills/craft-ts-effect-v4/SKILL.md': EFFECT_AGENT_SKILL }
@@ -2179,6 +2318,7 @@ function templates(context) {
2179
2318
  : []);
2180
2319
  const files = {
2181
2320
  'package.json': packageJson(context),
2321
+ 'AGENTS.md': AGENTS_MD(context.mode, context.config),
2182
2322
  '.gitignore': GENERATED_GITIGNORE,
2183
2323
  'tsconfig.json': tsconfig(context),
2184
2324
  'tsconfig.app.json': tsconfigApp,
@@ -2214,7 +2354,7 @@ function templates(context) {
2214
2354
  }
2215
2355
  : {}),
2216
2356
  ...localeFiles,
2217
- ...(hasEffect && hasI18n
2357
+ ...(effect && hasI18n
2218
2358
  ? {
2219
2359
  'src/i18n/effect.ts': effectI18nTs,
2220
2360
  ...(effect ? { 'src/i18n/effect-layer.ts': effectLayerTs } : {}),
@@ -2382,17 +2522,14 @@ export function normalizeCreateOptions(options) {
2382
2522
  throw new Error(`Unknown references selection "${references}".`);
2383
2523
  }
2384
2524
  if (options.referenceMode !== undefined &&
2385
- !['context', 'local', 'source'].includes(options.referenceMode)) {
2386
- throw new Error(`Unknown reference mode "${options.referenceMode}".`);
2525
+ options.referenceMode !== 'context') {
2526
+ throw new Error(`Reference mode "${options.referenceMode}" is no longer supported; cloned references are context only and npm packages remain the runtime dependencies.`);
2387
2527
  }
2388
2528
  const craftTs = options.cloneCraftTs ?? (references === 'craft-ts' || references === 'all');
2389
2529
  const effectTs = options.cloneEffectTs ?? references === 'all';
2390
2530
  if (effectTs && frontendRuntime !== 'effect' && backendRuntime !== 'effect') {
2391
2531
  throw new Error('EffectTS references require an Effect frontend or backend runtime.');
2392
2532
  }
2393
- if (options.referenceMode === 'local' && !craftTs && !effectTs) {
2394
- throw new Error('--reference-mode=local requires at least one cloned reference.');
2395
- }
2396
2533
  const directory = resolve(rootDir, options.directory);
2397
2534
  const workspaceKind = options.workspace ??
2398
2535
  (existsSync(join(rootDir, 'nx.json')) ? 'nx' : 'standalone');
@@ -2413,7 +2550,7 @@ export function normalizeCreateOptions(options) {
2413
2550
  references: {
2414
2551
  craftTs,
2415
2552
  effectTs,
2416
- mode: options.referenceMode ?? 'context',
2553
+ mode: 'context',
2417
2554
  craftTsRef: options.craftTsRef ?? 'main',
2418
2555
  effectTsRef: options.effectTsRef ?? 'main',
2419
2556
  },