@craft-ts/dev-tools 0.7.0-beta.13

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (237) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +154 -0
  3. package/generators.json +26 -0
  4. package/package.json +119 -0
  5. package/src/bin/craft-architecture-check.d.ts +2 -0
  6. package/src/bin/craft-architecture-check.js +51 -0
  7. package/src/bin/craft-architecture-check.js.map +1 -0
  8. package/src/bin/craft-brand.d.ts +2 -0
  9. package/src/bin/craft-brand.js +9 -0
  10. package/src/bin/craft-brand.js.map +1 -0
  11. package/src/bin/craft-graph.d.ts +2 -0
  12. package/src/bin/craft-graph.js +72 -0
  13. package/src/bin/craft-graph.js.map +1 -0
  14. package/src/bin/craft-migrate-architecture.d.ts +2 -0
  15. package/src/bin/craft-migrate-architecture.js +64 -0
  16. package/src/bin/craft-migrate-architecture.js.map +1 -0
  17. package/src/bin/craft-migrate-components.d.ts +2 -0
  18. package/src/bin/craft-migrate-components.js +67 -0
  19. package/src/bin/craft-migrate-components.js.map +1 -0
  20. package/src/bin/craft-migrate-primitives.d.ts +2 -0
  21. package/src/bin/craft-migrate-primitives.js +71 -0
  22. package/src/bin/craft-migrate-primitives.js.map +1 -0
  23. package/src/bin/craft-migrate-routes.d.ts +2 -0
  24. package/src/bin/craft-migrate-routes.js +77 -0
  25. package/src/bin/craft-migrate-routes.js.map +1 -0
  26. package/src/bin/craft-migrate-services.d.ts +2 -0
  27. package/src/bin/craft-migrate-services.js +75 -0
  28. package/src/bin/craft-migrate-services.js.map +1 -0
  29. package/src/bin/craft-migrate-template.d.ts +2 -0
  30. package/src/bin/craft-migrate-template.js +68 -0
  31. package/src/bin/craft-migrate-template.js.map +1 -0
  32. package/src/bin/craft-migrate.d.ts +2 -0
  33. package/src/bin/craft-migrate.js +91 -0
  34. package/src/bin/craft-migrate.js.map +1 -0
  35. package/src/bin/craft.d.ts +2 -0
  36. package/src/bin/craft.js +318 -0
  37. package/src/bin/craft.js.map +1 -0
  38. package/src/eslint-rules/anchor-has-href.cjs +35 -0
  39. package/src/eslint-rules/app-start-registry-match.cjs +355 -0
  40. package/src/eslint-rules/button-has-type.cjs +50 -0
  41. package/src/eslint-rules/control-has-accessible-name.cjs +56 -0
  42. package/src/eslint-rules/craft-component-name-match.cjs +8 -0
  43. package/src/eslint-rules/craft-computed-name-match.cjs +63 -0
  44. package/src/eslint-rules/craft-css-token-registry.cjs +17 -0
  45. package/src/eslint-rules/craft-css-var-naming.cjs +18 -0
  46. package/src/eslint-rules/craft-css-vars-contract.cjs +34 -0
  47. package/src/eslint-rules/craft-directive-name-match.cjs +8 -0
  48. package/src/eslint-rules/craft-method-name-match.cjs +8 -0
  49. package/src/eslint-rules/craft-name-match-utils.cjs +179 -0
  50. package/src/eslint-rules/craft-primitive-context.cjs +46 -0
  51. package/src/eslint-rules/craft-signal-source-name-match.cjs +8 -0
  52. package/src/eslint-rules/craft-source-name-match.cjs +8 -0
  53. package/src/eslint-rules/craft-styles-scope-safe.cjs +25 -0
  54. package/src/eslint-rules/css-rule-utils.cjs +109 -0
  55. package/src/eslint-rules/global-exception-registry-match.cjs +562 -0
  56. package/src/eslint-rules/heading-has-content.cjs +31 -0
  57. package/src/eslint-rules/html-helpers.cjs +115 -0
  58. package/src/eslint-rules/hyperscript-walk.cjs +249 -0
  59. package/src/eslint-rules/iframe-has-title.cjs +26 -0
  60. package/src/eslint-rules/img-has-alt.cjs +28 -0
  61. package/src/eslint-rules/index.cjs +209 -0
  62. package/src/eslint-rules/index.d.cts +5 -0
  63. package/src/eslint-rules/label-has-associated-control.cjs +67 -0
  64. package/src/eslint-rules/no-async-await.cjs +46 -0
  65. package/src/eslint-rules/no-craft-computed-side-effects.cjs +301 -0
  66. package/src/eslint-rules/no-craft-use-in-template.cjs +89 -0
  67. package/src/eslint-rules/no-craft-use.cjs +42 -0
  68. package/src/eslint-rules/no-direct-temporal-globals.cjs +73 -0
  69. package/src/eslint-rules/no-effect-in-params.cjs +265 -0
  70. package/src/eslint-rules/no-effect-outside-loaders.cjs +3 -0
  71. package/src/eslint-rules/no-ephemeral-template-form-state.cjs +131 -0
  72. package/src/eslint-rules/no-hardcoded-design-values.cjs +15 -0
  73. package/src/eslint-rules/no-heading-level-skip.cjs +101 -0
  74. package/src/eslint-rules/no-imperative-craft-resource-trigger.cjs +291 -0
  75. package/src/eslint-rules/no-important-in-component-styles.cjs +12 -0
  76. package/src/eslint-rules/no-injection-token.cjs +173 -0
  77. package/src/eslint-rules/no-invalid-insertion-pipe.cjs +162 -0
  78. package/src/eslint-rules/no-manual-route-provider-list.cjs +85 -0
  79. package/src/eslint-rules/no-noninteractive-element-interactions.cjs +66 -0
  80. package/src/eslint-rules/no-positive-tabindex.cjs +44 -0
  81. package/src/eslint-rules/no-redundant-primitive-insertion.cjs +361 -0
  82. package/src/eslint-rules/no-render-writes.cjs +106 -0
  83. package/src/eslint-rules/no-throw.cjs +132 -0
  84. package/src/eslint-rules/no-transition-actions.cjs +133 -0
  85. package/src/eslint-rules/no-type-assertions-in-template.cjs +211 -0
  86. package/src/eslint-rules/no-widened-route-provider-context.cjs +39 -0
  87. package/src/eslint-rules/prefer-browser-boundaries.cjs +92 -0
  88. package/src/eslint-rules/prefer-craft-http-transport.cjs +103 -0
  89. package/src/eslint-rules/prefer-craft-template-blocks.cjs +492 -0
  90. package/src/eslint-rules/prefer-direct-yieldable-callback.cjs +105 -0
  91. package/src/eslint-rules/prefer-named-html-helpers.cjs +33 -0
  92. package/src/eslint-rules/prefer-query-method-over-state-trigger.cjs +282 -0
  93. package/src/eslint-rules/prefer-relative-heading.cjs +43 -0
  94. package/src/eslint-rules/provide-host-name-match-component.cjs +455 -0
  95. package/src/eslint-rules/recommended-config.cjs +77 -0
  96. package/src/eslint-rules/require-assert-exhaustive-route-exceptions.cjs +259 -0
  97. package/src/eslint-rules/require-cascade-route-di-check.cjs +223 -0
  98. package/src/eslint-rules/require-child-route-mount-check.cjs +224 -0
  99. package/src/eslint-rules/require-component-monitoring.cjs +212 -0
  100. package/src/eslint-rules/require-craft-exception-handler.cjs +142 -0
  101. package/src/eslint-rules/require-craft-method-for-yieldable-callback.cjs +236 -0
  102. package/src/eslint-rules/require-craft-resource-trigger-yield.cjs +209 -0
  103. package/src/eslint-rules/require-effect-adapters.cjs +86 -0
  104. package/src/eslint-rules/require-exception-component-di-check.cjs +314 -0
  105. package/src/eslint-rules/require-focus-visible.cjs +36 -0
  106. package/src/eslint-rules/require-interactive-local-name.cjs +105 -0
  107. package/src/eslint-rules/require-lazy-load-with-retry.cjs +148 -0
  108. package/src/eslint-rules/require-outlet-heading-section.cjs +70 -0
  109. package/src/eslint-rules/require-pending-component-di-check.cjs +490 -0
  110. package/src/eslint-rules/require-primitive-context.cjs +156 -0
  111. package/src/eslint-rules/require-primitive-derived-property.cjs +598 -0
  112. package/src/eslint-rules/require-primitive-generator-unwrap.cjs +274 -0
  113. package/src/eslint-rules/require-reactive-template-bindings.cjs +305 -0
  114. package/src/eslint-rules/require-reduced-motion.cjs +38 -0
  115. package/src/eslint-rules/require-route-heading-outline.cjs +166 -0
  116. package/src/eslint-rules/require-yieldable-insertion-write.cjs +98 -0
  117. package/src/eslint-rules/require-yieldable-reactive-read.cjs +102 -0
  118. package/src/eslint-rules/require-yieldable-template-method.cjs +424 -0
  119. package/src/eslint-rules/role-has-required-aria.cjs +38 -0
  120. package/src/eslint-rules/server-function-client-match.cjs +283 -0
  121. package/src/eslint-rules/target-blank-noopener.cjs +31 -0
  122. package/src/eslint-rules/template-element-name-unique.cjs +124 -0
  123. package/src/eslint-rules/valid-aria.cjs +38 -0
  124. package/src/generators/route/compat.d.ts +3 -0
  125. package/src/generators/route/compat.js +6 -0
  126. package/src/generators/route/compat.js.map +1 -0
  127. package/src/generators/route/generator.d.ts +31 -0
  128. package/src/generators/route/generator.js +402 -0
  129. package/src/generators/route/generator.js.map +1 -0
  130. package/src/generators/route/schema.json +50 -0
  131. package/src/generators/route/split-schema.json +19 -0
  132. package/src/index.d.ts +21 -0
  133. package/src/index.js +23 -0
  134. package/src/index.js.map +1 -0
  135. package/src/scripts/angular-brand-codemod.d.ts +139 -0
  136. package/src/scripts/angular-brand-codemod.js +2318 -0
  137. package/src/scripts/angular-brand-codemod.js.map +1 -0
  138. package/src/scripts/angular-brand-codemod.ts +3985 -0
  139. package/src/scripts/architecture/migrate-architecture.d.ts +14 -0
  140. package/src/scripts/architecture/migrate-architecture.js +534 -0
  141. package/src/scripts/architecture/migrate-architecture.js.map +1 -0
  142. package/src/scripts/architecture/migrate-architecture.ts +653 -0
  143. package/src/scripts/architecture-graph.d.ts +368 -0
  144. package/src/scripts/architecture-graph.js +2191 -0
  145. package/src/scripts/architecture-graph.js.map +1 -0
  146. package/src/scripts/architecture-graph.ts +3136 -0
  147. package/src/scripts/components/migrate-components.d.ts +20 -0
  148. package/src/scripts/components/migrate-components.js +163 -0
  149. package/src/scripts/components/migrate-components.js.map +1 -0
  150. package/src/scripts/components/migrate-components.ts +227 -0
  151. package/src/scripts/components/migration-diagnostic.d.ts +7 -0
  152. package/src/scripts/components/migration-diagnostic.js +2 -0
  153. package/src/scripts/components/migration-diagnostic.js.map +1 -0
  154. package/src/scripts/components/migration-diagnostic.ts +8 -0
  155. package/src/scripts/create/create-project.d.ts +20 -0
  156. package/src/scripts/create/create-project.js +783 -0
  157. package/src/scripts/create/create-project.js.map +1 -0
  158. package/src/scripts/create/create-project.ts +849 -0
  159. package/src/scripts/data-flow-graph.d.ts +36 -0
  160. package/src/scripts/data-flow-graph.js +186 -0
  161. package/src/scripts/data-flow-graph.js.map +1 -0
  162. package/src/scripts/data-flow-graph.ts +240 -0
  163. package/src/scripts/dependency-graph.d.ts +152 -0
  164. package/src/scripts/dependency-graph.js +4456 -0
  165. package/src/scripts/dependency-graph.js.map +1 -0
  166. package/src/scripts/dependency-graph.ts +5908 -0
  167. package/src/scripts/effect-dependency-graph.d.ts +89 -0
  168. package/src/scripts/effect-dependency-graph.js +487 -0
  169. package/src/scripts/effect-dependency-graph.js.map +1 -0
  170. package/src/scripts/effect-dependency-graph.ts +630 -0
  171. package/src/scripts/migrate.d.ts +37 -0
  172. package/src/scripts/migrate.js +98 -0
  173. package/src/scripts/migrate.js.map +1 -0
  174. package/src/scripts/migrate.ts +162 -0
  175. package/src/scripts/migration-workspace.d.ts +2 -0
  176. package/src/scripts/migration-workspace.js +67 -0
  177. package/src/scripts/migration-workspace.js.map +1 -0
  178. package/src/scripts/migration-workspace.ts +93 -0
  179. package/src/scripts/primitives/migrate-named-primitives.d.ts +50 -0
  180. package/src/scripts/primitives/migrate-named-primitives.js +487 -0
  181. package/src/scripts/primitives/migrate-named-primitives.js.map +1 -0
  182. package/src/scripts/primitives/migrate-named-primitives.ts +627 -0
  183. package/src/scripts/primitives/migrate-primitive-generators.d.ts +30 -0
  184. package/src/scripts/primitives/migrate-primitive-generators.js +309 -0
  185. package/src/scripts/primitives/migrate-primitive-generators.js.map +1 -0
  186. package/src/scripts/primitives/migrate-primitive-generators.ts +410 -0
  187. package/src/scripts/primitives/migrate-primitives.d.ts +27 -0
  188. package/src/scripts/primitives/migrate-primitives.js +427 -0
  189. package/src/scripts/primitives/migrate-primitives.js.map +1 -0
  190. package/src/scripts/primitives/migrate-primitives.ts +655 -0
  191. package/src/scripts/primitives/migrate-yieldable-reactive-reads.d.ts +23 -0
  192. package/src/scripts/primitives/migrate-yieldable-reactive-reads.js +382 -0
  193. package/src/scripts/primitives/migrate-yieldable-reactive-reads.js.map +1 -0
  194. package/src/scripts/primitives/migrate-yieldable-reactive-reads.ts +499 -0
  195. package/src/scripts/primitives/migration-diagnostic.d.ts +8 -0
  196. package/src/scripts/primitives/migration-diagnostic.js +2 -0
  197. package/src/scripts/primitives/migration-diagnostic.js.map +1 -0
  198. package/src/scripts/primitives/migration-diagnostic.ts +15 -0
  199. package/src/scripts/routes/migrate-routes.d.ts +34 -0
  200. package/src/scripts/routes/migrate-routes.js +657 -0
  201. package/src/scripts/routes/migrate-routes.js.map +1 -0
  202. package/src/scripts/routes/migrate-routes.ts +895 -0
  203. package/src/scripts/routes/migration-diagnostic.d.ts +16 -0
  204. package/src/scripts/routes/migration-diagnostic.js +2 -0
  205. package/src/scripts/routes/migration-diagnostic.js.map +1 -0
  206. package/src/scripts/routes/migration-diagnostic.ts +25 -0
  207. package/src/scripts/routes/route-command.d.ts +75 -0
  208. package/src/scripts/routes/route-command.js +1169 -0
  209. package/src/scripts/routes/route-command.js.map +1 -0
  210. package/src/scripts/routes/route-command.ts +1807 -0
  211. package/src/scripts/routes/verify-routes.d.ts +60 -0
  212. package/src/scripts/routes/verify-routes.js +1121 -0
  213. package/src/scripts/routes/verify-routes.js.map +1 -0
  214. package/src/scripts/routes/verify-routes.ts +1511 -0
  215. package/src/scripts/services/config.d.ts +2 -0
  216. package/src/scripts/services/config.js +38 -0
  217. package/src/scripts/services/config.js.map +1 -0
  218. package/src/scripts/services/config.ts +60 -0
  219. package/src/scripts/services/migrate-services.d.ts +29 -0
  220. package/src/scripts/services/migrate-services.js +924 -0
  221. package/src/scripts/services/migrate-services.js.map +1 -0
  222. package/src/scripts/services/migrate-services.ts +1348 -0
  223. package/src/scripts/services/migration-diagnostic.d.ts +8 -0
  224. package/src/scripts/services/migration-diagnostic.js +2 -0
  225. package/src/scripts/services/migration-diagnostic.js.map +1 -0
  226. package/src/scripts/services/migration-diagnostic.ts +28 -0
  227. package/src/scripts/template/migrate-template.d.ts +16 -0
  228. package/src/scripts/template/migrate-template.js +349 -0
  229. package/src/scripts/template/migrate-template.js.map +1 -0
  230. package/src/scripts/template/migrate-template.ts +418 -0
  231. package/src/template-migration.d.ts +1 -0
  232. package/src/template-migration.js +2 -0
  233. package/src/template-migration.js.map +1 -0
  234. package/src/testing.d.ts +19 -0
  235. package/src/testing.js +2 -0
  236. package/src/testing.js.map +1 -0
  237. package/src/tsconfig.codemod.json +13 -0
@@ -0,0 +1,783 @@
1
+ import { existsSync } from 'node:fs';
2
+ import { mkdir, readdir, writeFile } from 'node:fs/promises';
3
+ import { dirname, join, relative, resolve } from 'node:path';
4
+ import { runArchitectureMigration } from '../architecture/migrate-architecture.js';
5
+ export const CRAFT_TS_STARTER_VERSION = '^0.7.0-beta.11';
6
+ export const EFFECT_V4_VERSION = '^4.0.0-rc.110';
7
+ const DEFAULT_AGENTS = ['codex'];
8
+ const BASE_AGENT_SKILL = `---
9
+ name: craft-ts-project
10
+ description: Build and evolve a framework-independent CraftTS application with typed reactivity, routing, API boundaries, lint and architecture contracts. Use when adding or reviewing CraftTS state, queries, mutations, components, routes, tests, or project structure.
11
+ ---
12
+
13
+ # CraftTS project
14
+
15
+ This project uses CraftTS without Angular. The renderer is @craft-ts/component,
16
+ the reactive runtime is @craft-ts/core, and the application entry point is
17
+ bootstrapCraft.
18
+
19
+ ## Workflow
20
+
21
+ 1. Read the existing component, route and API boundary before editing.
22
+ 2. Use query for server reads, mutation for writes, and CraftHttpClient
23
+ for transport. Yield every Craft reader.
24
+ 3. Use craftComponent, craftRoutes, componentDeps and the route DI proof;
25
+ do not introduce Angular decorators or inject().
26
+ 4. Run the focused test, then npm run lint, npm run typecheck,
27
+ npm run architecture, and npm run e2e when the browser flow changed.
28
+
29
+ ## Architecture tests
30
+
31
+ The architecture/ suite is a graph-wide contract, not feature-level TDD. It
32
+ checks invariants such as one owner per HTTP endpoint, no dependency cycles,
33
+ declarative reactive code, named interactive elements and armed route DI.
34
+ Run it after each structural change. Do not add an architecture it() merely
35
+ because a new feature was added. Add a new rule only when a product invariant
36
+ must prevent a recurring graph smell and no baseline helper covers it.
37
+
38
+ The catalog is generated by architecture/load-graph.ts; never hand-edit
39
+ architecture/catalog.ts.
40
+ `;
41
+ const EFFECT_AGENT_SKILL = `---
42
+ name: craft-ts-effect-v4
43
+ description: Build the Effect v4 integration of a framework-independent CraftTS project with typed services, Layers, queryEffect and Effect diagnostics. Use when editing an Effect-enabled starter or introducing Effect domain code.
44
+ ---
45
+
46
+ # CraftTS + Effect v4
47
+
48
+ This project deliberately uses Effect v4 (effect@${EFFECT_V4_VERSION}) and
49
+ @craft-ts/effect. Keep the boundary explicit:
50
+
51
+ - domain operations return Effect.Effect and depend on Context.Service;
52
+ - production implementations are supplied by Layer;
53
+ - UI data loading uses queryEffect, never a component-side runPromise;
54
+ - installCraftEffectBridge() is installed once at bootstrap;
55
+ - run npm run effect-check after changing an Effect generator, service or
56
+ Layer, then run the architecture suite.
57
+
58
+ Do not silently replace Effect v4 APIs with v3 examples. Confirm a symbol in
59
+ the installed package before using it.
60
+ `;
61
+ const AGENTS_MD = (mode) => `# CraftTS project
62
+
63
+ This project was created with \`craft create\` in **${mode === 'effect' ? 'Effect v4' : 'plain CraftTS'}** mode.
64
+
65
+ Read \`.agents/skills/craft-ts-project/SKILL.md\` before changing application
66
+ code. ${mode === 'effect' ? 'Effect-specific guidance is in `.agents/skills/craft-ts-effect-v4/SKILL.md`.' : ''}
67
+
68
+ The architecture suite is a graph contract. Run \`npm run architecture\`;
69
+ do not add a test per feature. Add a rule only for a recurring product-level
70
+ dependency smell not already covered by the baseline helpers.
71
+ `;
72
+ function json(value) {
73
+ return `${JSON.stringify(value, null, 2)}\n`;
74
+ }
75
+ function packageJson(context) {
76
+ const effect = context.mode === 'effect';
77
+ return json({
78
+ name: context.projectName,
79
+ private: true,
80
+ type: 'module',
81
+ engines: { node: '>=20.19.0' },
82
+ scripts: {
83
+ dev: 'vite --host 127.0.0.1',
84
+ build: 'vite build',
85
+ lint: 'eslint .',
86
+ typecheck: 'tsc -p tsconfig.app.json --noEmit',
87
+ 'typecheck-spec': 'tsc -p tsconfig.spec.json --noEmit',
88
+ test: 'vitest run --config vitest.config.ts',
89
+ e2e: 'playwright test',
90
+ ...(effect
91
+ ? {
92
+ 'effect-check': 'node scripts/run-effect-tsgo.mjs diagnostics --project tsconfig.effect.json --severity error,warning',
93
+ }
94
+ : {}),
95
+ architecture: 'vitest run --config vitest.architecture.config.ts',
96
+ 'typecheck-architecture': 'tsc -p tsconfig.architecture.json --noEmit',
97
+ },
98
+ dependencies: {
99
+ '@craft-ts/component': CRAFT_TS_STARTER_VERSION,
100
+ '@craft-ts/core': CRAFT_TS_STARTER_VERSION,
101
+ ...(effect ? { '@craft-ts/effect': CRAFT_TS_STARTER_VERSION, effect: EFFECT_V4_VERSION } : {}),
102
+ },
103
+ devDependencies: {
104
+ '@craft-ts/dev-tools': CRAFT_TS_STARTER_VERSION,
105
+ '@craft-ts/mcp': CRAFT_TS_STARTER_VERSION,
106
+ '@playwright/test': '^1.52.0',
107
+ '@types/node': '^22.0.0',
108
+ ...(effect
109
+ ? {
110
+ '@effect/tsgo': '^0.24.3',
111
+ '@typescript/native': 'npm:typescript@7.0.2',
112
+ }
113
+ : {}),
114
+ '@eslint/js': '^9.0.0',
115
+ 'eslint': '^9.0.0',
116
+ 'eslint-config-prettier': '^10.0.0',
117
+ 'eslint-plugin-playwright': '^2.0.0',
118
+ 'typescript': '^6.0.3',
119
+ 'typescript-eslint': '^8.0.0',
120
+ 'vite': '^8.0.0',
121
+ 'vitest': '^4.0.0',
122
+ },
123
+ });
124
+ }
125
+ function tsconfig(context) {
126
+ return json({
127
+ compilerOptions: {
128
+ target: 'ES2022',
129
+ module: 'preserve',
130
+ moduleResolution: 'bundler',
131
+ strict: true,
132
+ noEmit: true,
133
+ isolatedModules: true,
134
+ verbatimModuleSyntax: true,
135
+ erasableSyntaxOnly: false,
136
+ skipLibCheck: true,
137
+ types: ['node'],
138
+ },
139
+ references: [
140
+ { path: './tsconfig.app.json' },
141
+ { path: './tsconfig.spec.json' },
142
+ { path: './tsconfig.architecture.json' },
143
+ ...(context.mode === 'effect' ? [{ path: './tsconfig.effect.json' }] : []),
144
+ ],
145
+ });
146
+ }
147
+ const tsconfigApp = `{
148
+ "extends": "./tsconfig.json",
149
+ "compilerOptions": { "types": ["vite/client"] },
150
+ "files": ["src/main.ts"],
151
+ "include": ["src/**/*.d.ts"],
152
+ "exclude": ["src/**/*.spec.ts", "src/**/*.test.ts"]
153
+ }\n`;
154
+ const tsconfigSpec = `{
155
+ "extends": "./tsconfig.json",
156
+ "compilerOptions": { "types": ["node", "vitest/globals", "vite/client"] },
157
+ "include": ["src/**/*.ts", "src/**/*.d.ts", "e2e/**/*.ts"],
158
+ "exclude": ["src/main.ts"]
159
+ }\n`;
160
+ const tsconfigEffect = `{
161
+ "extends": "./tsconfig.json",
162
+ "compilerOptions": { "noEmit": true },
163
+ "include": ["src/**/*.ts"],
164
+ "exclude": ["src/**/*.spec.ts", "src/**/*.test.ts"]
165
+ }\n`;
166
+ const viteConfig = `import { defineConfig } from 'vite';
167
+
168
+ export default defineConfig({
169
+ server: { host: '127.0.0.1', port: 4173 },
170
+ build: { target: 'es2022' },
171
+ });
172
+ `;
173
+ const effectTsgoRunner = `import { chmodSync, existsSync } from 'node:fs';
174
+ import { spawnSync } from 'node:child_process';
175
+ import { resolve } from 'node:path';
176
+
177
+ const projectRoot = resolve(import.meta.dirname, '..');
178
+ const effectTsgoPath = resolve(projectRoot, 'node_modules/.bin/effect-tsgo');
179
+ if (process.platform !== 'win32') {
180
+ const nativeBinary = resolve(
181
+ projectRoot,
182
+ 'node_modules',
183
+ '@effect',
184
+ 'tsgo-' + process.platform + '-' + process.arch,
185
+ 'lib',
186
+ 'tsc',
187
+ );
188
+ if (existsSync(nativeBinary)) chmodSync(nativeBinary, 0o755);
189
+ }
190
+ const result = spawnSync(effectTsgoPath, process.argv.slice(2), {
191
+ cwd: projectRoot,
192
+ stdio: 'inherit',
193
+ });
194
+ if (result.error) throw result.error;
195
+ process.exitCode = result.status ?? 1;
196
+ `;
197
+ const vitestConfig = `import { defineConfig } from 'vitest/config';
198
+
199
+ export default defineConfig({
200
+ test: {
201
+ name: 'craft-ts-app',
202
+ globals: true,
203
+ environment: 'jsdom',
204
+ include: ['src/**/*.spec.ts'],
205
+ },
206
+ });
207
+ `;
208
+ const eslintConfig = `import js from '@eslint/js';
209
+ import prettier from 'eslint-config-prettier';
210
+ import playwright from 'eslint-plugin-playwright';
211
+ import tseslint from 'typescript-eslint';
212
+ import craftRules from '@craft-ts/dev-tools/eslint-rules';
213
+
214
+ export default tseslint.config(
215
+ { ignores: ['dist/**', 'node_modules/**', '**/architecture/catalog.ts'] },
216
+ js.configs.recommended,
217
+ ...tseslint.configs.recommended,
218
+ {
219
+ files: ['src/**/*.ts'],
220
+ plugins: { 'craft-ts': craftRules },
221
+ languageOptions: {
222
+ parserOptions: {
223
+ project: ['./tsconfig.app.json', './tsconfig.spec.json'],
224
+ tsconfigRootDir: import.meta.dirname,
225
+ },
226
+ },
227
+ rules: {
228
+ ...craftRules.configs.recommended.rules,
229
+ '@typescript-eslint/no-unused-vars': ['error', { varsIgnorePattern: '^_' }],
230
+ },
231
+ },
232
+ {
233
+ files: ['src/**/*.spec.ts', 'src/**/*.test.ts'],
234
+ rules: {
235
+ 'craft-ts/no-async-await': 'off',
236
+ 'craft-ts/no-throw': 'off',
237
+ 'craft-ts/prefer-browser-boundaries': 'off',
238
+ 'craft-ts/prefer-craft-template-blocks': 'off',
239
+ 'craft-ts/no-ephemeral-template-form-state': 'off',
240
+ },
241
+ },
242
+ {
243
+ files: ['e2e/**/*.ts'],
244
+ ...playwright.configs['flat/recommended'],
245
+ },
246
+ {
247
+ files: ['scripts/**/*.mjs'],
248
+ languageOptions: {
249
+ globals: { process: 'readonly' },
250
+ },
251
+ },
252
+ prettier,
253
+ );
254
+ `;
255
+ const indexHtml = `<!doctype html>
256
+ <html lang="en">
257
+ <head>
258
+ <meta charset="UTF-8" />
259
+ <meta name="viewport" content="width=device-width, initial-scale=1" />
260
+ <title>CraftTS starter</title>
261
+ </head>
262
+ <body>
263
+ <craft-root></craft-root>
264
+ <script type="module" src="/src/main.ts"></script>
265
+ </body>
266
+ </html>
267
+ `;
268
+ const styles = `:root { font-family: Inter, ui-sans-serif, system-ui, sans-serif; color: #172033; background: #f7f8fb; }
269
+ * { box-sizing: border-box; }
270
+ body { margin: 0; min-width: 320px; }
271
+ a { color: #2457d6; }
272
+ main { max-width: 860px; margin: 0 auto; padding: 3rem 1.25rem; }
273
+ nav { display: flex; gap: 1rem; padding: 1rem 1.25rem; background: white; border-bottom: 1px solid #e3e7ef; }
274
+ .card { padding: 1.5rem; background: white; border: 1px solid #e3e7ef; border-radius: .75rem; box-shadow: 0 8px 30px #1720330d; }
275
+ .muted { color: #5c677d; }
276
+ .error { color: #b42318; }
277
+ button:focus-visible, a:focus-visible { outline: 3px solid #7aa2ff; outline-offset: 3px; }
278
+ `;
279
+ const mainTs = `import { bootstrapCraft } from '@craft-ts/component';
280
+ import { appConfig } from './app/app.config';
281
+ import './styles.css';
282
+
283
+ bootstrapCraft({ config: appConfig });
284
+ `;
285
+ const appTs = `import {
286
+ a,
287
+ CraftRouterOutlet,
288
+ craftComponent,
289
+ div,
290
+ main,
291
+ nav,
292
+ } from '@craft-ts/component';
293
+ import { CraftRouterLink } from '@craft-ts/core';
294
+
295
+ export const App = craftComponent(
296
+ 'App',
297
+ {},
298
+ () => ({}),
299
+ () =>
300
+ div([
301
+ nav([
302
+ a('home', { craftRouterLink: { to: '' } }, 'Home').pipe(CraftRouterLink),
303
+ a('about', { craftRouterLink: { to: 'about' } }, 'About').pipe(CraftRouterLink),
304
+ ]),
305
+ main(CraftRouterOutlet()),
306
+ ]),
307
+ );
308
+ `;
309
+ const routesTs = `import { loadCraftComponent } from '@craft-ts/component';
310
+ import {
311
+ assertExhaustiveRouteExceptions,
312
+ craftRoutes,
313
+ type CanRun,
314
+ type CraftRouter,
315
+ type ValidateCascadeRoutesFile,
316
+ } from '@craft-ts/core';
317
+
318
+ export const { appRoutes } = craftRoutes('app', [
319
+ {
320
+ path: '',
321
+ ...loadCraftComponent(({ withRetry }) =>
322
+ withRetry(import('./home-page')).then(
323
+ (module: typeof import('./home-page')) => module.default,
324
+ ),
325
+ ),
326
+ },
327
+ {
328
+ path: 'about',
329
+ ...loadCraftComponent(({ withRetry }) =>
330
+ withRetry(import('./about-page')).then(
331
+ (module: typeof import('./about-page')) => module.default,
332
+ ),
333
+ ),
334
+ },
335
+ ]);
336
+
337
+ assertExhaustiveRouteExceptions(appRoutes);
338
+
339
+ type _CheckAppDI = ValidateCascadeRoutesFile<
340
+ never,
341
+ CraftRouter,
342
+ typeof appRoutes
343
+ >;
344
+ type _CanRunAppDI = CanRun<_CheckAppDI>;
345
+
346
+ declare module '@craft-ts/core' {
347
+ interface CraftRouterRoutesRegistry {
348
+ App: typeof appRoutes.META_PATHS;
349
+ }
350
+ }
351
+ `;
352
+ const appConfigTs = `import { provideCraftRootComponent } from '@craft-ts/component';
353
+ import { craftAppConfig, provideCraftRouter } from '@craft-ts/core';
354
+ import { App } from './app';
355
+ import { appRoutes } from './app.routes';
356
+
357
+ export const appConfig = craftAppConfig({
358
+ routingDeps: appRoutes.META_PATHS,
359
+ providers: [
360
+ provideCraftRootComponent(App),
361
+ provideCraftRouter(appRoutes.toRoutes()),
362
+ ],
363
+ });
364
+ `;
365
+ const apiTs = `import { CraftHttpClient } from '@craft-ts/core';
366
+
367
+ export type WelcomeResponse = {
368
+ readonly title: string;
369
+ readonly body: string;
370
+ };
371
+
372
+ export function* loadWelcome() {
373
+ return yield* CraftHttpClient.get(({ response }) => ({
374
+ url: '/api/welcome',
375
+ success: response<WelcomeResponse>(),
376
+ }));
377
+ }
378
+ `;
379
+ const aboutPageTs = `import { craftComponent, div, heading, p } from '@craft-ts/component';
380
+
381
+ export const AboutPage = craftComponent(
382
+ 'AboutPage',
383
+ {},
384
+ () => ({}),
385
+ () =>
386
+ div({ class: 'card' }, [
387
+ heading('About this starter'),
388
+ p('This page proves that Craft routing and lazy component loading are wired.'),
389
+ ]),
390
+ );
391
+
392
+ export default AboutPage;
393
+ `;
394
+ const plainHomePageTs = `import {
395
+ craftComponent,
396
+ div,
397
+ heading,
398
+ ifBlock,
399
+ p,
400
+ span,
401
+ } from '@craft-ts/component';
402
+ import { craftComputed, query } from '@craft-ts/core';
403
+ import { loadWelcome } from './api';
404
+
405
+ export const HomePage = craftComponent(
406
+ 'HomePage',
407
+ {},
408
+ function* () {
409
+ const welcomeQuery = yield* query(
410
+ 'welcomeQuery',
411
+ {
412
+ params: () => true,
413
+ loader: function* () {
414
+ return yield* loadWelcome();
415
+ },
416
+ },
417
+ ({ resource }) => ({
418
+ hasWelcome: craftComputed('hasWelcome', () => resource.hasValue()),
419
+ }),
420
+ );
421
+ return { welcomeQuery };
422
+ },
423
+ ({ welcomeQuery }) =>
424
+ div({ class: 'card' }, [
425
+ heading('Welcome to CraftTS'),
426
+ p('A framework-independent starter with a typed API boundary.'),
427
+ ifBlock(welcomeQuery.isLoading, () => p({ class: 'muted' }, 'Loading API…')),
428
+ ifBlock(welcomeQuery.hasWelcome, () =>
429
+ div([
430
+ p(function* () {
431
+ return 'API title: ' + (yield* welcomeQuery.value()).title;
432
+ }),
433
+ p(function* () {
434
+ return 'API body: ' + (yield* welcomeQuery.value()).body;
435
+ }),
436
+ ]),
437
+ ),
438
+ ifBlock(welcomeQuery.hasException, () =>
439
+ p({ class: 'error' }, [span('The API request failed. Check the network tab.')]),
440
+ ),
441
+ ]),
442
+ );
443
+
444
+ export default HomePage;
445
+ `;
446
+ const effectDomainTs = `/* eslint-disable craft-ts/prefer-craft-http-transport, craft-ts/no-async-await, craft-ts/no-throw -- Effect owns this typed transport boundary. */
447
+ import { Context, Data, Effect, Layer } from 'effect';
448
+
449
+ export type WelcomeResponse = {
450
+ readonly title: string;
451
+ readonly body: string;
452
+ };
453
+
454
+ export class WelcomeApiError extends Data.TaggedError('WelcomeApiError')<{
455
+ readonly message: string;
456
+ }> {}
457
+
458
+ export type WelcomeRepository = {
459
+ readonly load: () => Effect.Effect<WelcomeResponse, WelcomeApiError>;
460
+ };
461
+
462
+ export class WelcomeRepositoryService extends Context.Service<
463
+ WelcomeRepositoryService,
464
+ WelcomeRepository
465
+ >()('starter/WelcomeRepository') {}
466
+
467
+ export const WelcomeRepositoryLive = Layer.succeed(WelcomeRepositoryService, {
468
+ load: () =>
469
+ Effect.tryPromise({
470
+ try: async () => {
471
+ const response = await fetch('/api/welcome');
472
+ if (!response.ok) throw new Error('HTTP ' + response.status);
473
+ return (await response.json()) as WelcomeResponse;
474
+ },
475
+ catch: (cause) => new WelcomeApiError({ message: String(cause) }),
476
+ }),
477
+ });
478
+
479
+ export const loadWelcome = (): Effect.Effect<
480
+ WelcomeResponse,
481
+ WelcomeApiError,
482
+ WelcomeRepositoryService
483
+ > =>
484
+ Effect.gen(function* () {
485
+ const repository = yield* WelcomeRepositoryService;
486
+ return yield* repository.load();
487
+ });
488
+ `;
489
+ const effectHomePageTs = `import {
490
+ craftComponent,
491
+ div,
492
+ heading,
493
+ ifBlock,
494
+ p,
495
+ span,
496
+ } from '@craft-ts/component';
497
+ import { craftComputed } from '@craft-ts/core';
498
+ import { queryEffect } from '@craft-ts/effect';
499
+ import { loadWelcome, type WelcomeApiError } from './domain';
500
+
501
+ export const HomePage = craftComponent(
502
+ 'HomePage',
503
+ {},
504
+ function* () {
505
+ const welcomeQuery = yield* queryEffect(
506
+ 'welcomeQuery',
507
+ {
508
+ method: (request: boolean) => request,
509
+ loader: () => loadWelcome(),
510
+ },
511
+ ({ resource, exceptions }) => ({
512
+ hasWelcome: craftComputed('hasWelcome', () => resource.hasValue()),
513
+ errorMessage: craftComputed('errorMessage', function* () {
514
+ return (yield* exceptions()).loader?.message ?? 'Unknown API error';
515
+ }),
516
+ }),
517
+ );
518
+ yield* welcomeQuery.call(true);
519
+ return { welcomeQuery };
520
+ },
521
+ ({ welcomeQuery }) =>
522
+ div({ class: 'card' }, [
523
+ heading('Welcome to CraftTS + Effect v4'),
524
+ p('The page uses queryEffect over a typed repository Layer.'),
525
+ ifBlock(welcomeQuery.isLoading, () => p({ class: 'muted' }, 'Loading API…')),
526
+ ifBlock(welcomeQuery.hasWelcome, () =>
527
+ div([
528
+ p(function* () {
529
+ return 'API title: ' + (yield* welcomeQuery.value()).title;
530
+ }),
531
+ p(function* () {
532
+ return 'API body: ' + (yield* welcomeQuery.value()).body;
533
+ }),
534
+ ]),
535
+ ),
536
+ ifBlock(welcomeQuery.hasException, () =>
537
+ p({ class: 'error' }, [
538
+ span(function* () {
539
+ return 'API error: ' + (yield* welcomeQuery.errorMessage());
540
+ }),
541
+ ]),
542
+ ),
543
+ ]),
544
+ );
545
+
546
+ export type HomePageError = WelcomeApiError;
547
+
548
+ export default HomePage;
549
+ `;
550
+ const plainAppConfig = appConfigTs;
551
+ const effectAppConfig = `import { provideCraftRootComponent } from '@craft-ts/component';
552
+ import {
553
+ craftAppConfig,
554
+ provideAppInitializer,
555
+ provideCraftRouter,
556
+ } from '@craft-ts/core';
557
+ import {
558
+ installCraftEffectBridge,
559
+ provideLayer,
560
+ } from '@craft-ts/effect';
561
+ import { App } from './app';
562
+ import { appRoutes } from './app.routes';
563
+ import {
564
+ WelcomeRepositoryLive,
565
+ } from './domain';
566
+
567
+ const effectProviders = [provideLayer(WelcomeRepositoryLive)] as const;
568
+
569
+ export const appConfig = craftAppConfig({
570
+ routingDeps: appRoutes.META_PATHS,
571
+ providers: [
572
+ provideCraftRootComponent(App),
573
+ provideCraftRouter(appRoutes.toRoutes()),
574
+ ...effectProviders,
575
+ provideAppInitializer(() => installCraftEffectBridge()),
576
+ ],
577
+ });
578
+
579
+ // effectProviders is a typed tuple: removing the Layer makes the app config
580
+ // fail at the provider boundary before the application can boot.
581
+ `;
582
+ function unitTestTs(context) {
583
+ const effect = context.mode === 'effect';
584
+ return `// @vitest-environment jsdom
585
+ import { mountCraftComponent } from '@craft-ts/component';
586
+ import { TestBed, ɵInjector as Injector } from '@craft-ts/core';
587
+ ${effect ? "import { installCraftEffectBridge, provideLayer } from '@craft-ts/effect';\nimport { WelcomeRepositoryLive } from './domain';" : ''}
588
+ import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest';
589
+ import { HomePage } from './home-page';
590
+
591
+ describe('HomePage', () => {
592
+ ${effect ? 'let disposeBridge: () => void;' : ''}
593
+ beforeEach(() => {
594
+ TestBed.resetTestingModule();
595
+ document.body.replaceChildren();
596
+ ${effect ? 'disposeBridge = installCraftEffectBridge();' : ''}
597
+ vi.stubGlobal('fetch', vi.fn().mockResolvedValue(
598
+ new Response(JSON.stringify({ title: 'Hello from the API', body: 'Typed transport works.' }), {
599
+ headers: { 'content-type': 'application/json' },
600
+ }),
601
+ ));
602
+ });
603
+
604
+ afterEach(() => {
605
+ ${effect ? 'disposeBridge();' : ''}
606
+ vi.unstubAllGlobals();
607
+ TestBed.resetTestingModule();
608
+ });
609
+
610
+ it('renders the API result', async () => {
611
+ const host = document.createElement('div');
612
+ document.body.append(host);
613
+ const injector = ${effect ? 'TestBed.rootInjector.createChild([provideLayer(WelcomeRepositoryLive)])' : 'TestBed.rootInjector'};
614
+ const mounted = mountCraftComponent(HomePage, host, injector as unknown as Injector);
615
+ TestBed.tick();
616
+ await vi.waitFor(() => expect(host.textContent).toContain('Hello from the API'));
617
+ mounted.destroy();
618
+ });
619
+ });
620
+ `;
621
+ }
622
+ const e2eTestTs = `import { expect, test } from '@playwright/test';
623
+
624
+ test.beforeEach(async ({ page }) => {
625
+ await page.route('**/api/welcome', async (route) =>
626
+ route.fulfill({
627
+ status: 200,
628
+ contentType: 'application/json',
629
+ body: JSON.stringify({ title: 'Hello from the API', body: 'E2E transport works.' }),
630
+ }),
631
+ );
632
+ });
633
+
634
+ test('loads the API page and navigates to About', async ({ page }) => {
635
+ await page.goto('/');
636
+ await expect(page.getByRole('heading', { name: /Welcome to CraftTS/ })).toBeVisible();
637
+ await expect(page.getByText('Hello from the API')).toBeVisible();
638
+ await page.getByRole('link', { name: 'About' }).click();
639
+ await expect(page.getByRole('heading', { name: 'About this starter' })).toBeVisible();
640
+ });
641
+ `;
642
+ const playwrightConfig = `import { defineConfig, devices } from '@playwright/test';
643
+
644
+ export default defineConfig({
645
+ testDir: './e2e',
646
+ use: { baseURL: 'http://127.0.0.1:4173', ...devices['Desktop Chrome'] },
647
+ webServer: {
648
+ command: 'npm run dev',
649
+ url: 'http://127.0.0.1:4173',
650
+ reuseExistingServer: !process.env.CI,
651
+ },
652
+ });
653
+ `;
654
+ function readme(context) {
655
+ const effect = context.mode === 'effect';
656
+ return `# ${context.projectName}\n\nA framework-independent CraftTS starter generated by craft create.\n\nThis project includes:\n\n- a routed Craft component page;\n- a typed CraftHttpClient API boundary${effect ? ' behind an Effect v4 repository service and Layer' : ''};\n- flat-config ESLint with Craft rules;\n- unit tests, graph-wide architecture tests and a Playwright E2E flow.\n\n## Start\n\nnpm install\nnpm run dev\n\n## Verify\n\nnpm run lint\nnpm run typecheck\nnpm run typecheck-spec\n${effect ? 'npm run effect-check\n' : ''}npm test\nnpm run architecture\nnpm run e2e\nnpm run build\n\n### Architecture tests\n\narchitecture/ is the dependency-graph contract. It runs without booting the\nbrowser and catches duplicate HTTP ownership, dependency cycles, declarative\ngraph violations, unnamed interactive elements and missing route DI proofs.\nRun it after structural changes. Add a rule only for a recurring product\ninvariant that the baseline helpers do not cover; do not add one test per\nfeature.\n\nThe generated catalog is refreshed by architecture/load-graph.ts; do not edit\narchitecture/catalog.ts by hand.\n`;
657
+ }
658
+ function agentFiles(mode, agent) {
659
+ const skill = `${BASE_AGENT_SKILL}${mode === 'effect' ? `\n${EFFECT_AGENT_SKILL}` : ''}`;
660
+ if (agent === 'codex') {
661
+ return {
662
+ 'AGENTS.md': AGENTS_MD(mode),
663
+ '.agents/skills/craft-ts-project/SKILL.md': skill,
664
+ ...(mode === 'effect'
665
+ ? { '.agents/skills/craft-ts-effect-v4/SKILL.md': EFFECT_AGENT_SKILL }
666
+ : {}),
667
+ };
668
+ }
669
+ if (agent === 'cursor') {
670
+ return {
671
+ '.cursor/skills/craft-ts-project/SKILL.md': skill,
672
+ '.cursor/rules/craft-ts.mdc': `---\ndescription: CraftTS project rules\nglobs: **/*.ts\nalwaysApply: true\n---\n\nRead .cursor/skills/craft-ts-project/SKILL.md before editing CraftTS code.\n`,
673
+ };
674
+ }
675
+ if (agent === 'claude-code') {
676
+ return {
677
+ 'CLAUDE.md': AGENTS_MD(mode),
678
+ '.claude/skills/craft-ts-project/SKILL.md': skill,
679
+ ...(mode === 'effect'
680
+ ? { '.claude/skills/craft-ts-effect-v4/SKILL.md': EFFECT_AGENT_SKILL }
681
+ : {}),
682
+ };
683
+ }
684
+ return {
685
+ 'GEMINI.md': AGENTS_MD(mode),
686
+ '.gemini/skills/craft-ts-project/SKILL.md': skill,
687
+ ...(mode === 'effect'
688
+ ? { '.gemini/skills/craft-ts-effect-v4/SKILL.md': EFFECT_AGENT_SKILL }
689
+ : {}),
690
+ };
691
+ }
692
+ function templates(context) {
693
+ const effect = context.mode === 'effect';
694
+ return {
695
+ 'package.json': packageJson(context),
696
+ 'tsconfig.json': tsconfig(context),
697
+ 'tsconfig.app.json': tsconfigApp,
698
+ 'tsconfig.spec.json': tsconfigSpec,
699
+ ...(effect ? { 'tsconfig.effect.json': tsconfigEffect } : {}),
700
+ ...(effect ? { 'scripts/run-effect-tsgo.mjs': effectTsgoRunner } : {}),
701
+ 'vite.config.ts': viteConfig,
702
+ 'vitest.config.ts': vitestConfig,
703
+ 'eslint.config.mjs': eslintConfig,
704
+ 'playwright.config.ts': playwrightConfig,
705
+ 'index.html': indexHtml,
706
+ 'README.md': readme(context),
707
+ 'src/main.ts': mainTs,
708
+ 'src/styles.css': styles,
709
+ 'src/types.d.ts': '/// <reference types="vite/client" />\n',
710
+ 'src/app/app.ts': appTs,
711
+ 'src/app/app.config.ts': effect ? effectAppConfig : plainAppConfig,
712
+ 'src/app/app.routes.ts': routesTs,
713
+ ...(effect ? { 'src/app/api.ts': "export { loadWelcome } from './domain';\nexport type { WelcomeResponse } from './domain';\n" } : { 'src/app/api.ts': apiTs }),
714
+ ...(effect ? { 'src/app/domain.ts': effectDomainTs, 'src/app/home-page.ts': effectHomePageTs } : { 'src/app/home-page.ts': plainHomePageTs }),
715
+ 'src/app/about-page.ts': aboutPageTs,
716
+ 'src/app/home-page.spec.ts': unitTestTs(context),
717
+ 'e2e/starter.spec.ts': e2eTestTs,
718
+ };
719
+ }
720
+ function normaliseAgent(agent) {
721
+ const value = agent.trim().toLowerCase();
722
+ if (value === 'codex')
723
+ return 'codex';
724
+ if (value === 'cursor')
725
+ return 'cursor';
726
+ if (value === 'claude' || value === 'claude-code')
727
+ return 'claude-code';
728
+ if (value === 'cloud-code' || value === 'cloudcode' || value === 'gemini')
729
+ return 'cloud-code';
730
+ throw new Error(`Unknown agent "${agent}". Use codex, cursor, claude-code, or cloud-code.`);
731
+ }
732
+ export function parseCreateAgents(value) {
733
+ if (!value)
734
+ return DEFAULT_AGENTS;
735
+ if (value.trim() === '' || value.trim().toLowerCase() === 'none')
736
+ return [];
737
+ return [...new Set(value.split(',').map(normaliseAgent))];
738
+ }
739
+ export async function createCraftProject(options) {
740
+ const rootDir = resolve(options.rootDir ?? process.cwd());
741
+ const directory = resolve(rootDir, options.directory);
742
+ const projectName = directory
743
+ .split(/[\\/]/)
744
+ .at(-1)
745
+ ?.replace(/[^a-zA-Z0-9._-]/g, '-') || 'craft-app';
746
+ const mode = options.mode ?? 'plain';
747
+ const agents = options.agents ?? DEFAULT_AGENTS;
748
+ if (existsSync(directory) && !options.force && (await readdir(directory)).length > 0) {
749
+ throw new Error(`Refusing to create in non-empty directory: ${directory}. Use --force to merge.`);
750
+ }
751
+ const files = {
752
+ ...templates({ projectName, mode }),
753
+ ...agents.reduce((all, agent) => Object.assign(all, agentFiles(mode, agent)), {}),
754
+ };
755
+ await Promise.all(Object.entries(files).map(async ([file, contents]) => {
756
+ const target = join(directory, file);
757
+ await mkdir(dirname(target), { recursive: true });
758
+ await writeFile(target, contents, 'utf8');
759
+ }));
760
+ // Reuse the same deterministic scaffold as migration, but with a standalone
761
+ // project root. It also adds architecture/typecheck scripts to package.json.
762
+ await runArchitectureMigration({
763
+ rootDir: join(directory, 'src'),
764
+ tsConfigFilePath: join(directory, 'tsconfig.app.json'),
765
+ write: true,
766
+ });
767
+ return {
768
+ directory,
769
+ mode,
770
+ agents,
771
+ changedFiles: Object.keys(files).map((file) => relative(directory, join(directory, file))),
772
+ };
773
+ }
774
+ export function createModeFromFlag(value) {
775
+ if (!value)
776
+ return undefined;
777
+ if (value === 'v4' || value === 'effect' || value === 'true')
778
+ return 'effect';
779
+ if (value === 'none' || value === 'plain' || value === 'false')
780
+ return 'plain';
781
+ throw new Error(`Unknown --effect value "${value}". Use v4 or none.`);
782
+ }
783
+ //# sourceMappingURL=create-project.js.map