@weft-js/cli 0.1.1 → 0.1.3

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 (171) hide show
  1. package/README.md +534 -54
  2. package/bin/weft.mjs +8 -1
  3. package/docs/async.md +16 -6
  4. package/docs/browser.md +22 -8
  5. package/docs/config.md +39 -9
  6. package/docs/core.md +30 -0
  7. package/docs/deploy.md +33 -8
  8. package/docs/design.md +12 -3
  9. package/docs/entrypoints.md +79 -0
  10. package/docs/fonts.md +12 -2
  11. package/docs/icons.md +13 -7
  12. package/docs/lists.md +3 -1
  13. package/docs/meta.md +13 -3
  14. package/docs/plugins.md +32 -9
  15. package/docs/props.md +13 -2
  16. package/docs/reactivity.md +26 -0
  17. package/docs/rules.md +19 -12
  18. package/docs/rust.md +47 -17
  19. package/docs/start-short.md +3 -2
  20. package/docs/start.md +226 -52
  21. package/docs/state.md +33 -7
  22. package/docs/style.md +31 -8
  23. package/docs/surface.md +61 -15
  24. package/docs/testing.md +15 -5
  25. package/docs/theme.md +182 -0
  26. package/docs/ui-state.md +19 -6
  27. package/docs/verify.md +13 -0
  28. package/package.json +13 -4
  29. package/src/add/config.mjs +93 -0
  30. package/src/add/errors.mjs +2 -0
  31. package/src/add/setup.mjs +88 -0
  32. package/src/add-rust.mjs +82 -27
  33. package/src/add.mjs +209 -181
  34. package/src/agents-md.mjs +144 -43
  35. package/src/alias.mjs +16 -5
  36. package/src/audit/page.mjs +313 -0
  37. package/src/audit/report.mjs +69 -0
  38. package/src/audit/utils.mjs +5 -0
  39. package/src/audit.mjs +159 -206
  40. package/src/browser/cli.mjs +24 -5
  41. package/src/browser/client.mjs +189 -25
  42. package/src/browser/commands.mjs +166 -51
  43. package/src/catalog/audit.mjs +129 -0
  44. package/src/catalog/auth.mjs +40 -0
  45. package/src/catalog/compiler.mjs +337 -0
  46. package/src/catalog/contracts.mjs +18 -0
  47. package/src/catalog/database.mjs +52 -0
  48. package/src/catalog/deploy.mjs +117 -0
  49. package/src/catalog/design.mjs +159 -0
  50. package/src/catalog/forms.mjs +25 -0
  51. package/src/catalog/hydration.mjs +29 -0
  52. package/src/catalog/icons.mjs +24 -0
  53. package/src/catalog/media.mjs +163 -0
  54. package/src/catalog/metadata.mjs +116 -0
  55. package/src/catalog/plugins.mjs +228 -0
  56. package/src/catalog/realtime.mjs +195 -0
  57. package/src/catalog/rust.mjs +75 -0
  58. package/src/catalog/server-functions.mjs +92 -0
  59. package/src/catalog/state.mjs +39 -0
  60. package/src/catalog/style-groups.mjs +54 -0
  61. package/src/catalog/tests.mjs +127 -0
  62. package/src/catalog/text.mjs +200 -0
  63. package/src/catalog/ui-state.mjs +42 -0
  64. package/src/catalog.mjs +67 -460
  65. package/src/check/runtime.mjs +280 -0
  66. package/src/check.mjs +198 -208
  67. package/src/cli/authoring.mjs +213 -0
  68. package/src/cli/project.mjs +302 -0
  69. package/src/cli.mjs +222 -250
  70. package/src/config.mjs +1 -1
  71. package/src/contributions.mjs +75 -25
  72. package/src/db-layer.mjs +13 -5
  73. package/src/deploy/cdn.mjs +94 -16
  74. package/src/deploy/files.mjs +100 -21
  75. package/src/deploy/fit.mjs +127 -23
  76. package/src/deploy/hosts.mjs +90 -20
  77. package/src/deploy/index/cli.mjs +20 -0
  78. package/src/deploy/index/environment.mjs +69 -0
  79. package/src/deploy/index/steps.mjs +87 -0
  80. package/src/deploy/index.mjs +228 -114
  81. package/src/deploy/needs.mjs +88 -31
  82. package/src/design-checks/accessible-names.mjs +62 -0
  83. package/src/design-checks/contrast.mjs +43 -0
  84. package/src/design-checks/csp.mjs +60 -0
  85. package/src/design-checks/files.mjs +59 -0
  86. package/src/design-checks/heading-links.mjs +61 -0
  87. package/src/design-checks/interaction.mjs +65 -0
  88. package/src/design-checks/jsx.mjs +67 -0
  89. package/src/design-checks/layout-values.mjs +68 -0
  90. package/src/design-checks/native-controls.mjs +48 -0
  91. package/src/design-checks/outline.mjs +39 -0
  92. package/src/design-checks/reporter.mjs +37 -0
  93. package/src/design-checks/rules.mjs +274 -0
  94. package/src/design-checks/values.mjs +262 -0
  95. package/src/design-checks.mjs +10 -400
  96. package/src/diag.mjs +32 -8
  97. package/src/docs.mjs +138 -43
  98. package/src/doctor.mjs +20 -8
  99. package/src/files.mjs +18 -5
  100. package/src/fix.mjs +10 -2
  101. package/src/gate/after.mjs +40 -0
  102. package/src/gate/before.mjs +54 -0
  103. package/src/gate/classify.mjs +72 -0
  104. package/src/gate/mutation.mjs +209 -0
  105. package/src/gate/resolve-cases.mjs +22 -0
  106. package/src/gate/result.mjs +63 -0
  107. package/src/gate/run.mjs +75 -0
  108. package/src/gate.mjs +104 -202
  109. package/src/infer/box-props.cjs +140 -0
  110. package/src/infer/core-boxes.cjs +46 -0
  111. package/src/infer/derived-writes.cjs +95 -0
  112. package/src/infer/engine/checks.cjs +193 -0
  113. package/src/infer/engine/flow.cjs +293 -0
  114. package/src/infer/engine/report.cjs +158 -0
  115. package/src/infer/engine/utils.cjs +15 -0
  116. package/src/infer/engine.cjs +108 -368
  117. package/src/infer/facts/sites.cjs +114 -0
  118. package/src/infer/facts.cjs +210 -117
  119. package/src/infer/hosts.cjs +69 -22
  120. package/src/infer/lsp.mjs +149 -40
  121. package/src/infer/session.cjs +73 -17
  122. package/src/infer/shadow.cjs +13 -5
  123. package/src/infer/worker.cjs +57 -9
  124. package/src/jsx-scan.mjs +63 -21
  125. package/src/load-config/schema.mjs +281 -0
  126. package/src/load-config.mjs +137 -138
  127. package/src/map.mjs +93 -26
  128. package/src/mcp/browser-tools.mjs +155 -0
  129. package/src/mcp.mjs +147 -69
  130. package/src/meta-table.mjs +43 -8
  131. package/src/mutate.mjs +120 -36
  132. package/src/plugin-checks.mjs +52 -23
  133. package/src/plugins.mjs +125 -39
  134. package/src/project.mjs +170 -79
  135. package/src/props-checks.mjs +24 -6
  136. package/src/props-listener.mjs +27 -9
  137. package/src/react-checks.mjs +35 -4
  138. package/src/server-state-checks.mjs +34 -0
  139. package/src/share-checks.mjs +121 -37
  140. package/src/site.mjs +33 -6
  141. package/src/slots.mjs +104 -19
  142. package/src/start/build.mjs +116 -0
  143. package/src/start/routes.mjs +38 -0
  144. package/src/start.mjs +193 -95
  145. package/src/state-checks/core-state.mjs +223 -0
  146. package/src/state-checks/modules.mjs +17 -0
  147. package/src/state-checks/scan.mjs +232 -0
  148. package/src/state-checks/server-graph.mjs +65 -0
  149. package/src/state-checks.mjs +109 -133
  150. package/src/style/find.mjs +121 -0
  151. package/src/style.mjs +133 -127
  152. package/src/surface-checks.mjs +212 -46
  153. package/src/tailwind.mjs +5 -4
  154. package/src/tasks.mjs +189 -27
  155. package/src/test-rules/context.mjs +109 -0
  156. package/src/test-rules/file-rules.mjs +279 -0
  157. package/src/test-rules/rule-helpers.mjs +244 -0
  158. package/src/test-rules/syntax.mjs +223 -0
  159. package/src/test-rules/test-rules.mjs +127 -0
  160. package/src/testdiff.mjs +94 -32
  161. package/src/tests.mjs +31 -483
  162. package/src/text-checks.mjs +229 -69
  163. package/src/tokens.mjs +12 -4
  164. package/src/trust/scan.mjs +95 -0
  165. package/src/trust.mjs +214 -102
  166. package/src/weft.mjs +41 -9
  167. package/src/workspace.mjs +96 -19
  168. package/ts-plugin/context.js +57 -0
  169. package/ts-plugin/index.js +4 -124
  170. package/ts-plugin/mapping.js +65 -0
  171. package/ts-plugin/proxy.js +144 -0
@@ -0,0 +1,88 @@
1
+ import { existsSync, mkdirSync, readFileSync, readdirSync, writeFileSync } from 'node:fs'
2
+ import { createRequire } from 'node:module'
3
+ import { dirname, join } from 'node:path'
4
+ import { pathToFileURL } from 'node:url'
5
+ import { database } from '../map.mjs'
6
+ import { dbLayer } from '../db-layer.mjs'
7
+ import { refused } from './errors.mjs'
8
+
9
+ /** The plugin's `setup`, once the package is installed: a plain import, since the package can't run anything by being read. */
10
+ export async function setupOf(root, pkg) {
11
+ let path
12
+ try {
13
+ path = createRequire(join(root, 'package.json')).resolve(`${pkg}/plugin`)
14
+ } catch {
15
+ return undefined // no plugin entry, or a dry run before the install
16
+ }
17
+ const mod = await import(pathToFileURL(path).href)
18
+ const make = mod.default ?? Object.values(mod).find((v) => typeof v === 'function')
19
+ return make?.().contribute({ command: 'check', mode: 'development', root }).setup
20
+ }
21
+
22
+ /** What `setup` wants, checked in full before anything is written, so a refusal changes nothing. */
23
+ export async function planSetup(root, setup) {
24
+ const layer = await dbLayer(root)
25
+ if (setup.needs?.includes('db') && !layer)
26
+ throw refused('This needs a database: run `weft add db` first. Nothing was changed.')
27
+ if (setup.needs?.includes('auth') && !existsSync(join(root, 'src/auth.server.ts')))
28
+ throw refused('This needs users: run `weft add auth` first. Nothing was changed.')
29
+ const read = (rel) =>
30
+ existsSync(join(root, rel)) ? readFileSync(join(root, rel), 'utf8') : undefined
31
+ let plan
32
+ try {
33
+ plan = setup.run({
34
+ engine: layer ? database(root, layer).engine : null,
35
+ layer: layer?.layer ?? null,
36
+ read,
37
+ })
38
+ } catch (error) {
39
+ throw refused(`${error.message}. Nothing was changed.`)
40
+ }
41
+ const taken = Object.keys(plan.files ?? {}).filter((f) => existsSync(join(root, f)))
42
+ if (taken.length) throw refused(`${taken.join(', ')} already exist. Nothing was changed.`)
43
+ return plan
44
+ }
45
+
46
+ export function applySetup(root, plan) {
47
+ for (const [file, text] of Object.entries({ ...plan.files, ...plan.edits })) {
48
+ mkdirSync(dirname(join(root, file)), { recursive: true })
49
+ writeFileSync(join(root, file), text)
50
+ }
51
+ for (const name of ['.env', '.env.example']) {
52
+ const file = join(root, name)
53
+ const had = existsSync(file) ? readFileSync(file, 'utf8') : ''
54
+ const lines = Object.entries(plan.env ?? {})
55
+ .filter(([k]) => !had.includes(`${k}=`))
56
+ .map(([k, v]) => `${k}=${v}\n`)
57
+ if (lines.length) writeFileSync(file, `${had && `${had.trimEnd()}\n`}${lines.join('')}`)
58
+ }
59
+ }
60
+
61
+ export const setupActions = (plan) => [
62
+ ...Object.keys({ ...plan.files, ...plan.edits }).map((f) => `write ${f}`),
63
+ ...(plan.env ? [`add ${Object.keys(plan.env).join(', ')} to .env and .env.example`] : []),
64
+ ...(plan.next ?? []).map((n) => `next: ${n}`),
65
+ ]
66
+
67
+ export function authEdits({ root, files, context, tables, envText, layer }) {
68
+ const edits = { ...files, 'src/context.server.ts': context }
69
+ if (!envText.includes('BETTER_AUTH_SECRET'))
70
+ edits['src/env.server.ts'] = envText.replace(
71
+ 'server: {\n',
72
+ 'server: {\n BETTER_AUTH_SECRET: z.string().min(32),\n',
73
+ )
74
+ if (layer.layer === 'drizzle') {
75
+ edits['src/db/auth.schema.ts'] = tables
76
+ edits['src/db/schema.ts'] =
77
+ `${readFileSync(join(root, 'src/db/schema.ts'), 'utf8').trimEnd()}\n\nexport * from './auth.schema'\n`
78
+ } else if (layer.layer === 'prisma') {
79
+ edits['prisma/schema.prisma'] =
80
+ `${readFileSync(join(root, 'prisma/schema.prisma'), 'utf8').trimEnd()}\n${tables}`
81
+ } else {
82
+ const next = existsSync(join(root, 'migrations'))
83
+ ? readdirSync(join(root, 'migrations')).filter((f) => f.endsWith('.sql')).length
84
+ : 0
85
+ edits[`migrations/${String(next).padStart(4, '0')}_auth.sql`] = tables
86
+ }
87
+ return edits
88
+ }
package/src/add-rust.mjs CHANGED
@@ -6,28 +6,39 @@ import { existsSync, mkdirSync, readFileSync, writeFileSync } from 'node:fs'
6
6
  import { dirname, join, relative } from 'node:path'
7
7
  import { start } from './start.mjs'
8
8
 
9
- const refused = message => Object.assign(new Error(message), { code: 'E_ADD' })
10
- const usage = message => Object.assign(new Error(message), { code: 'E_USAGE' })
9
+ const refused = (message) => Object.assign(new Error(message), { code: 'E_ADD' })
10
+ const usage = (message) => Object.assign(new Error(message), { code: 'E_USAGE' })
11
11
  const TOOLCHAIN = '1.99.1'
12
12
 
13
- const camel = name => name.replace(/[-_](.)/g, (_, c) => c.toUpperCase())
14
- const pascal = name => { const c = camel(name); return c[0].toUpperCase() + c.slice(1) }
15
- const lib = name => name.replaceAll('-', '_')
13
+ const camel = (name) => name.replace(/[-_](.)/g, (_, c) => c.toUpperCase())
14
+ const pascal = (name) => {
15
+ const c = camel(name)
16
+ return c[0].toUpperCase() + c.slice(1)
17
+ }
18
+ const lib = (name) => name.replaceAll('-', '_')
16
19
 
17
20
  const manifest = (name, crate, remote) => `[package]
18
21
  name = "${name}"
19
22
  version = "0.0.0"
20
23
  edition = "2024"
21
24
  publish = false
22
- ${remote ? '' : `
25
+ ${
26
+ remote
27
+ ? ''
28
+ : `
23
29
  [lib]
24
30
  crate-type = ["cdylib"]
25
- `}
31
+ `
32
+ }
26
33
  [dependencies]
27
34
  serde = { version = "1", features = ["derive"] }
28
35
  serde_json = "1"
29
- ${remote ? `tokio = { version = "1", features = ["macros", "rt-multi-thread"] }
30
- weft = { path = "${crate}" }` : `weft = { path = "${crate}", default-features = false, features = ["native"] }`}
36
+ ${
37
+ remote
38
+ ? `tokio = { version = "1", features = ["macros", "rt-multi-thread"] }
39
+ weft = { path = "${crate}" }`
40
+ : `weft = { path = "${crate}", default-features = false, features = ["native"] }`
41
+ }
31
42
 
32
43
  [build-dependencies]
33
44
  weft = { path = "${crate}", default-features = false, features = ["build"] }
@@ -43,7 +54,10 @@ codegen-units = 1
43
54
  [workspace]
44
55
  `
45
56
 
46
- const body = (name, remote) => `weft::contract!(); // types and a \`Service\` trait, generated from your Zod: never hand-written
57
+ const body = (
58
+ name,
59
+ remote,
60
+ ) => `weft::contract!(); // types and a \`Service\` trait, generated from your Zod: never hand-written
47
61
  use contract::*;
48
62
 
49
63
  struct ${pascal(name)};
@@ -54,14 +68,18 @@ impl Service for ${pascal(name)} {
54
68
  Ok(DigestOutput { bytes: input.text.len() as i64, hash: format!("{hash:08x}") })
55
69
  }
56
70
  }
57
- ${remote ? `
71
+ ${
72
+ remote
73
+ ? `
58
74
  #[tokio::main]
59
75
  async fn main() -> std::io::Result<()> {
60
76
  weft::serve(weft::router(contract::functions(${pascal(name)}))).await
61
77
  }
62
- ` : `
78
+ `
79
+ : `
63
80
  weft::native!(contract::functions(${pascal(name)}));
64
- `}`
81
+ `
82
+ }`
65
83
 
66
84
  const fn = (name, binding) => `import { query } from '@weft-js/start/data'
67
85
  import { z } from 'zod'
@@ -78,33 +96,63 @@ export const digest = query({
78
96
  /** The path Cargo gets. `crate/` is in a packed @weft-js/start; a linked one is in this repository, next to the crates. */
79
97
  async function weftCrate(root, dir) {
80
98
  const { startDir } = await start(root, 'vite')
81
- const found = [join(startDir, '..', 'crate'), join(startDir, '..', '..', '..', 'crates', 'weft')].find(d => existsSync(join(d, 'Cargo.toml')))
99
+ const found = [
100
+ join(startDir, '..', 'crate'),
101
+ join(startDir, '..', '..', '..', 'crates', 'weft'),
102
+ ].find((d) => existsSync(join(d, 'Cargo.toml')))
82
103
  if (!found) throw refused('this @weft-js/start has no `weft` crate in it. Nothing was changed.')
83
104
  return relative(dir, found).replaceAll('\\', '/')
84
105
  }
85
106
 
86
- export async function addRust(root, name = 'media', { remote = false, dryRun = false, install, installSpecs }) {
87
- if (!/^[a-z][a-z0-9_-]*$/.test(name)) throw usage(`\`${name}\` isn't a service name: lower case, then letters, digits, \`-\` or \`_\``)
88
- if (!existsSync(join(root, 'package.json'))) throw refused('`weft add rust` needs a package.json. Nothing was changed.')
107
+ export async function addRust(
108
+ root,
109
+ name = 'media',
110
+ { remote = false, dryRun = false, install, installSpecs },
111
+ ) {
112
+ if (!/^[a-z][a-z0-9_-]*$/.test(name))
113
+ throw usage(
114
+ `\`${name}\` isn't a service name: lower case, then letters, digits, \`-\` or \`_\``,
115
+ )
116
+ if (!existsSync(join(root, 'package.json')))
117
+ throw refused('`weft add rust` needs a package.json. Nothing was changed.')
89
118
  const dir = join(root, 'crates', name)
90
119
  const module = join(root, 'src', 'server', `${name}.ts`)
91
- const taken = [dir, module].filter(existsSync).map(f => relative(root, f))
92
- if (taken.length) throw refused(`${taken.join(', ')} already exist${taken.length > 1 ? '' : 's'}. Pick another name: \`weft add rust <name>\`. Nothing was changed.`)
120
+ const taken = [dir, module].filter(existsSync).map((f) => relative(root, f))
121
+ if (taken.length)
122
+ throw refused(
123
+ `${taken.join(', ')} already exist${taken.length > 1 ? '' : 's'}. Pick another name: \`weft add rust <name>\`. Nothing was changed.`,
124
+ )
93
125
  const binding = camel(name)
94
126
  const factory = remote ? 'remote' : 'native'
95
127
  const call = remote ? `remote('rust://${name}')` : `native('${name}')`
96
128
  const rustFile = join(root, 'src', 'server', 'rust.ts')
97
- const had = existsSync(rustFile) ? readFileSync(rustFile, 'utf8') : '// The only place Rust targets are named.\n'
98
- if (new RegExp(`\\b${binding}\\b`).test(had)) throw refused(`src/server/rust.ts already names \`${binding}\`. Nothing was changed.`)
99
- const imported = new RegExp(`import\\s*\\{[^}]*\\b${factory}\\b[^}]*\\}\\s*from\\s*['"]@weft-js/start/rust['"]`).test(had)
129
+ const had = existsSync(rustFile)
130
+ ? readFileSync(rustFile, 'utf8')
131
+ : '// The only place Rust targets are named.\n'
132
+ if (new RegExp(`\\b${binding}\\b`).test(had))
133
+ throw refused(`src/server/rust.ts already names \`${binding}\`. Nothing was changed.`)
134
+ const imported = new RegExp(
135
+ `import\\s*\\{[^}]*\\b${factory}\\b[^}]*\\}\\s*from\\s*['"]@weft-js/start/rust['"]`,
136
+ ).test(had)
100
137
  const nextRust = `${imported ? had : had.replace(/^((?:\/\/.*\n)*)/, `$1import { ${factory} } from '@weft-js/start/rust'\n`)}${had.endsWith('\n') ? '' : '\n'}\nexport const ${binding} = ${call}\n`
101
138
 
102
139
  const pkg = JSON.parse(readFileSync(join(root, 'package.json'), 'utf8'))
103
140
  const needsZod = !pkg.dependencies?.zod && !pkg.devDependencies?.zod
104
141
  const gitignore = join(root, '.gitignore')
105
- const ignored = existsSync(gitignore) && /^\/?target\/?$|^\*\*\/target\/?$/m.test(readFileSync(gitignore, 'utf8'))
106
- const actions = [`create crates/${name}`, `create src/server/${name}.ts`, `add \`${binding}\` to src/server/rust.ts`, ...(needsZod && install ? ['install zod'] : []), ...(ignored ? [] : ['ignore target/']), 'this crate needs Rust (`cargo`, from rustup.rs); the rest of weft does not', 'next: `weft check`, then `weft dev`']
107
- if (dryRun) return { ok: true, feature: 'rust', service: name, kind: factory, actions, dryRun: true }
142
+ const ignored =
143
+ existsSync(gitignore) &&
144
+ /^\/?target\/?$|^\*\*\/target\/?$/m.test(readFileSync(gitignore, 'utf8'))
145
+ const actions = [
146
+ `create crates/${name}`,
147
+ `create src/server/${name}.ts`,
148
+ `add \`${binding}\` to src/server/rust.ts`,
149
+ ...(needsZod && install ? ['install zod'] : []),
150
+ ...(ignored ? [] : ['ignore target/']),
151
+ 'this crate needs Rust (`cargo`, from rustup.rs); the rest of weft does not',
152
+ 'next: `weft check`, then `weft dev`',
153
+ ]
154
+ if (dryRun)
155
+ return { ok: true, feature: 'rust', service: name, kind: factory, actions, dryRun: true }
108
156
 
109
157
  const crate = await weftCrate(root, dir)
110
158
  const write = (file, text) => {
@@ -113,11 +161,18 @@ export async function addRust(root, name = 'media', { remote = false, dryRun = f
113
161
  }
114
162
  write(join(dir, 'Cargo.toml'), manifest(name, crate, remote))
115
163
  write(join(dir, 'build.rs'), `fn main() {\n weft::build::generate("${name}");\n}\n`)
116
- write(join(dir, 'rust-toolchain.toml'), `[toolchain]\nchannel = "${TOOLCHAIN}"\n${remote ? '' : 'targets = ["wasm32-unknown-unknown"]\n'}`)
164
+ write(
165
+ join(dir, 'rust-toolchain.toml'),
166
+ `[toolchain]\nchannel = "${TOOLCHAIN}"\n${remote ? '' : 'targets = ["wasm32-unknown-unknown"]\n'}`,
167
+ )
117
168
  write(join(dir, 'src', remote ? 'main.rs' : 'lib.rs'), body(lib(name), remote))
118
169
  write(module, fn(name, binding))
119
170
  write(rustFile, nextRust)
120
- if (!ignored) writeFileSync(gitignore, `${existsSync(gitignore) ? readFileSync(gitignore, 'utf8').replace(/\n?$/, '\n') : ''}target/\n`)
171
+ if (!ignored)
172
+ writeFileSync(
173
+ gitignore,
174
+ `${existsSync(gitignore) ? readFileSync(gitignore, 'utf8').replace(/\n?$/, '\n') : ''}target/\n`,
175
+ )
121
176
  if (needsZod && install) installSpecs(['zod'])
122
177
  return { ok: true, feature: 'rust', service: name, kind: factory, actions }
123
178
  }