@weft-js/cli 0.0.0-stage → 0.1.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (91) hide show
  1. package/LICENSE +16 -0
  2. package/README.md +74 -2
  3. package/bin/weft.mjs +8 -0
  4. package/docs/async.md +40 -0
  5. package/docs/browser.md +39 -0
  6. package/docs/config.md +93 -0
  7. package/docs/deploy.md +83 -0
  8. package/docs/design.md +97 -0
  9. package/docs/fonts.md +70 -0
  10. package/docs/icons.md +102 -0
  11. package/docs/lists.md +23 -0
  12. package/docs/meta.md +82 -0
  13. package/docs/plugins.md +143 -0
  14. package/docs/props.md +77 -0
  15. package/docs/rules.md +32 -0
  16. package/docs/rust.md +223 -0
  17. package/docs/start-short.md +8 -0
  18. package/docs/start.md +422 -0
  19. package/docs/state.md +96 -0
  20. package/docs/style.md +59 -0
  21. package/docs/surface.md +111 -0
  22. package/docs/testing.md +71 -0
  23. package/docs/ui-state.md +92 -0
  24. package/docs/verify.md +48 -0
  25. package/package.json +39 -4
  26. package/src/add-rust.mjs +123 -0
  27. package/src/add.mjs +358 -0
  28. package/src/agents-md.mjs +134 -0
  29. package/src/alias.mjs +59 -0
  30. package/src/audit.mjs +366 -0
  31. package/src/browser/cli.mjs +34 -0
  32. package/src/browser/client.mjs +132 -0
  33. package/src/browser/commands.mjs +143 -0
  34. package/src/catalog.mjs +496 -0
  35. package/src/check.mjs +330 -0
  36. package/src/cli.mjs +364 -0
  37. package/src/config.d.ts +1 -0
  38. package/src/config.mjs +5 -0
  39. package/src/contributions.mjs +69 -0
  40. package/src/db-layer.mjs +33 -0
  41. package/src/deploy/cdn.mjs +64 -0
  42. package/src/deploy/files.mjs +187 -0
  43. package/src/deploy/fit.mjs +62 -0
  44. package/src/deploy/hosts.mjs +56 -0
  45. package/src/deploy/index.mjs +273 -0
  46. package/src/deploy/needs.mjs +92 -0
  47. package/src/design-checks.mjs +439 -0
  48. package/src/diag.mjs +74 -0
  49. package/src/docs.mjs +132 -0
  50. package/src/doctor.mjs +30 -0
  51. package/src/files.mjs +47 -0
  52. package/src/fix.mjs +40 -0
  53. package/src/gate.mjs +276 -0
  54. package/src/index.mjs +9 -0
  55. package/src/infer/engine.cjs +525 -0
  56. package/src/infer/facts.cjs +252 -0
  57. package/src/infer/hosts.cjs +114 -0
  58. package/src/infer/lsp.mjs +131 -0
  59. package/src/infer/session.cjs +105 -0
  60. package/src/infer/shadow.cjs +46 -0
  61. package/src/infer/worker.cjs +97 -0
  62. package/src/jsx-scan.mjs +86 -0
  63. package/src/load-config.mjs +303 -0
  64. package/src/map.mjs +110 -0
  65. package/src/mcp.mjs +195 -0
  66. package/src/meta-table.mjs +28 -0
  67. package/src/mutate.mjs +140 -0
  68. package/src/plugin-checks.mjs +89 -0
  69. package/src/plugins.mjs +172 -0
  70. package/src/project.mjs +219 -0
  71. package/src/props-checks.mjs +46 -0
  72. package/src/props-listener.mjs +38 -0
  73. package/src/react-checks.mjs +36 -0
  74. package/src/share-checks.mjs +74 -0
  75. package/src/site.mjs +19 -0
  76. package/src/slots.mjs +96 -0
  77. package/src/start.mjs +296 -0
  78. package/src/state-checks.mjs +213 -0
  79. package/src/style.mjs +320 -0
  80. package/src/surface-checks.mjs +218 -0
  81. package/src/tailwind.mjs +22 -0
  82. package/src/tasks.mjs +99 -0
  83. package/src/testdiff.mjs +157 -0
  84. package/src/tests.mjs +572 -0
  85. package/src/text-checks.mjs +172 -0
  86. package/src/tokens.mjs +24 -0
  87. package/src/trust.mjs +282 -0
  88. package/src/weft.mjs +66 -0
  89. package/src/workspace.mjs +105 -0
  90. package/ts-plugin/index.js +137 -0
  91. package/ts-plugin/package.json +1 -0
package/src/check.mjs ADDED
@@ -0,0 +1,330 @@
1
+ // `weft check`: compile every project file in check mode, add the project view, the X-codes (text-checks.mjs), plugin
2
+ // `check` hooks, the test T-codes, the config P-codes, the style D-codes (style.mjs) and the design D-codes (design-checks.mjs), and keep `.weft/styles.json`
3
+ // current when the project has style groups.
4
+
5
+ import { existsSync, readdirSync, readFileSync } from 'node:fs'
6
+ import { createRequire } from 'node:module'
7
+ import { isAbsolute, join, relative, resolve } from 'node:path'
8
+ import { pathToFileURL } from 'node:url'
9
+ import { diagnostic, locate, positions, sortDiagnostics, summarize } from './diag.mjs'
10
+ import { discover } from './files.mjs'
11
+ import { isDataConfig, loadConfig, writeConfigSchema } from './load-config.mjs'
12
+ import { sync } from './plugins.mjs'
13
+ import { writeMap } from './map.mjs'
14
+ import { projectView } from './project.mjs'
15
+ import { designChecks, hasTokens } from './design-checks.mjs'
16
+ import { classSoup, matrixDiagnostics, styleFiles, styleModule, usesTailwind, writeMatrixFile } from './style.mjs'
17
+ import { checkTests, isTestFile, testGlobs } from './tests.mjs'
18
+ import { pluginChecks } from './plugin-checks.mjs'
19
+ import { propChecks } from './props-checks.mjs'
20
+ import { reactChecks } from './react-checks.mjs'
21
+ import { trustDiagnostics } from './trust.mjs'
22
+ import { shareChecks } from './share-checks.mjs'
23
+ import { stateChecks } from './state-checks.mjs'
24
+ import { surfaceChecks } from './surface-checks.mjs'
25
+ import { textChecks } from './text-checks.mjs'
26
+ import { loadCompiler, resolveWeft } from './weft.mjs'
27
+ import { dbLayer } from './db-layer.mjs'
28
+ import { routeDiagnostics, schemaHash, siteOf, start } from './start.mjs'
29
+ import { deployChecks } from './deploy/index.mjs'
30
+
31
+ const posix = p => p.split('\\').join('/')
32
+
33
+ /** Q015: the schema's hash against the one `weft db generate` recorded (research 28 §2). */
34
+ function schemaDrift(root, layer) {
35
+ const schema = layer?.schema && join(root, layer.schema)
36
+ if (!schema || !existsSync(schema)) return []
37
+ const file = join(root, layer.migrations, '.schema-hash')
38
+ if (existsSync(file) && readFileSync(file, 'utf8').trim() === schemaHash(readFileSync(schema))) return []
39
+ return [diagnostic({ code: 'Q015', file: layer.schema, why: existsSync(file) ? 'The schema changed since the last `weft db generate`: the database will not match it.' : 'No migrations yet: the first request that reads a table fails with Q014.' })]
40
+ }
41
+
42
+ /** I001–I003: the walk a build does (@weft-js/warp-icons/compile), when the project has the package. */
43
+ async function iconChecks(root) {
44
+ const require = createRequire(join(root, 'package.json'))
45
+ let compile
46
+ for (const from of [() => require.resolve('@weft-js/warp-icons/compile'), () => createRequire(require.resolve('@weft-js/warp'))
47
+ .resolve('@weft-js/warp-icons/compile')]) {
48
+ try { compile = from(); break } catch { /* not this way */ }
49
+ }
50
+ if (!compile) return []
51
+ const { findings } = await import(pathToFileURL(compile).href)
52
+ return findings(root).map(f => diagnostic({ code: f.code, file: f.file, span: [f.line, f.column, f.line, f.column], why: f.why }))
53
+ }
54
+
55
+ /** H010: what the last build or `weft dev` found keeps an island from waiting (`.weft/hydration.json`, @weft-js/core/hydration). */
56
+ function hydrationChecks(root) {
57
+ const file = join(root, '.weft', 'hydration.json')
58
+ if (!existsSync(file)) return []
59
+ try {
60
+ return JSON.parse(readFileSync(file, 'utf8')).diagnostics.filter(d => existsSync(join(root, d.file))).map(d => diagnostic({ code: d.code, file: d.file, span: [d.line, 1, d.line, 1], why: d.why, help: d.fix }))
61
+ } catch {
62
+ return []
63
+ }
64
+ }
65
+
66
+ /**
67
+ * H001–H007, H011–H016: page metadata, per route, on what the routes really produce (`inspect.mjs`). With `files`, only
68
+ * when a route is among them. An app that can't run here is still read: a check never fails for what it couldn't start.
69
+ */
70
+ async function routeChecks(root, loaded, given, reported) {
71
+ if (!existsSync(join(root, 'src', 'routes')) || loaded.config?.check?.routes === false || loaded.diagnostics.some(d => d.code === 'P007')) return []
72
+ if (given?.length && !given.some(f => f.startsWith('src/routes/'))) return []
73
+ let inspect
74
+ try { inspect = await start(root, 'inspect') } catch { return [] }
75
+ const keep = list => routeDiagnostics(root, list).filter(d => !(d.code === 'H001' && reported.has('H001')))
76
+ try {
77
+ return keep((await inspect.routesMeta({ root, site: siteOf(loaded) })).diagnostics)
78
+ } catch (error) {
79
+ return keep([...inspect.staticRouteChecks(root), { code: 'H016', severity: 'info', file: 'src/routes', route: '*', why: `The routes didn't load here, so only their source was checked: ${String(error?.message ?? error).split('\n')[0]}` }])
80
+ }
81
+ }
82
+
83
+ /**
84
+ * @param {string} root
85
+ * @param {{ files?: string[] }} [options] files relative to `root` (or absolute); default: discovered
86
+ */
87
+ export async function check(root, options = {}) {
88
+ const started = performance.now()
89
+ root = resolve(root)
90
+ const weft = resolveWeft(root)
91
+ const compiler = loadCompiler(weft)
92
+ const loaded = await loadConfig(root)
93
+ const { config } = loaded
94
+ // Slot modules and plugin token files stay current with the config (research 13 §2.6).
95
+ if (!loaded.diagnostics.some(d => d.code === 'P007') && (config.plugins?.length || existsSync(join(root, '.weft', 'slots')))) sync(root, loaded)
96
+
97
+ if (loaded.file && isDataConfig(loaded.file)) writeConfigSchema(root)
98
+ if (existsSync(join(root, '.weft'))) await writeMap(root)
99
+
100
+ const exclude = [...(config.check?.exclude ?? []), ...(config.scope?.forbid ?? [])]
101
+ const given = options.files?.map(f => posix(relative(root, isAbsolute(f) ? f : join(root, f))))
102
+ const include = config.check?.include ?? ['**/*.{tsx,jsx}']
103
+ const rels = given?.length ? given.filter(f => /\.[jt]sx$/.test(f) || !isTestFile(f)) : discover(root, { include, exclude })
104
+ const testRels = given?.length ? given.filter(isTestFile) : discover(root, { include: config.test?.include ?? testGlobs, exclude })
105
+
106
+ // Style groups (research 23): the project's definitions first, so each file compiles with the groups it imports.
107
+ const style = await styleModule(weft)
108
+ const sfiles = style ? styleFiles(root, config) : []
109
+ const scan = sfiles.some(f => readFileSync(join(root, f), 'utf8').includes('@weft-js/core/style')) ? style.scanProject(root, sfiles, (...a) => compiler.transform(...a)) : null
110
+
111
+ const files = []
112
+ const posByAbs = new Map()
113
+ for (const rel of rels) {
114
+ const abs = join(root, rel)
115
+ const text = readFileSync(abs, 'utf8')
116
+ const pos = positions(text)
117
+ posByAbs.set(abs, pos)
118
+ const groups = scan ? style.importedGroups(abs, text, f => scan.groups.filter(g => join(root, g.file) === f)) : {}
119
+ const out = compiler.transform(text, Object.keys(groups).length ? { filename: abs, checkOnly: true, groups } : { filename: rel, checkOnly: true })
120
+ const diagnostics = out.diagnostics.map(d => {
121
+ const from = pos.fromByte(d.start)
122
+ const end = pos.fromByte(d.end)
123
+ const fix = d.fix && {
124
+ description: d.fix.description,
125
+ safe: d.fix.safe,
126
+ edits: d.fix.edits.map(e => {
127
+ const a = pos.fromByte(e.start)
128
+ const b = pos.fromByte(e.end)
129
+ return { file: rel, span: pos.span(a, b), offset: [a, b], text: e.text }
130
+ }),
131
+ }
132
+ return diagnostic({ code: d.code, severity: d.severity, file: rel, span: pos.span(from, end), offset: [from, end], why: d.message, help: d.help, fix })
133
+ })
134
+ files.push({ abs, rel, text, diagnostics })
135
+ }
136
+
137
+ projectView(files, file => posByAbs.get(file.abs))
138
+
139
+ const matrix = scan && style.buildMatrix(scan.groups, scan.uses)
140
+ if (matrix && scan.groups.length) writeMatrixFile(root, matrix)
141
+ const styles = [...(usesTailwind(root) ? [] : classSoup(files.filter(f => /\.[jt]sx$/.test(f.rel)))), ...(matrix ? matrixDiagnostics(root, matrix) : [])]
142
+
143
+ const fontBuild = typeof loaded.slots['font.build'] === 'string' && !loaded.slots['font.build'].startsWith('weft:default')
144
+ const diagnostics = [
145
+ ...loaded.diagnostics,
146
+ ...(loaded.diagnostics.some(d => d.code === 'P007') ? [] : trustDiagnostics(root, loaded)),
147
+ ...files.flatMap(f => f.diagnostics),
148
+ ...styles,
149
+ ...(config.check?.design === false ? [] : designChecks(root, { files, exclude, tokens: hasTokens(root) })),
150
+ ...textChecks(root, { files, exclude, fontBuild }),
151
+ ...stateChecks(root, { files, exclude }),
152
+ ...shareChecks(root, { files }),
153
+ ...reactChecks({ files }),
154
+ ...surfaceChecks(root, { files, exclude, slots: loaded.slots }),
155
+ ...(await pluginChecks(root, loaded, { files, exclude })),
156
+ ...(await propChecks(root, config)),
157
+ ...checkTests(root, testRels),
158
+ ...(await iconChecks(root)),
159
+ ...hydrationChecks(root),
160
+ ...hygiene(root),
161
+ ...(await socketChecks(root)),
162
+ ...schemaDrift(root, await dbLayer(root, loaded)),
163
+ ...(await rustChecks(root, loaded)),
164
+ ...(await liveChecks(root, loaded, options.preset)),
165
+ ...(loaded.diagnostics.some(d => d.code === 'P007') ? [] : deployChecks(root, config, { preset: options.preset })),
166
+ ]
167
+ diagnostics.push(...await routeChecks(root, loaded, given, new Set(loaded.diagnostics.map(d => d.code))))
168
+ sortDiagnostics(diagnostics)
169
+ const summary = { files: files.length, tests: testRels.length, ...summarize(diagnostics) }
170
+ return {
171
+ version: 1,
172
+ ok: summary.errors === 0,
173
+ root,
174
+ weft: { version: weft.version, from: weft.from, compiler: weft.compilerPath },
175
+ config: loaded.file ? posix(relative(root, loaded.file)) : null,
176
+ summary,
177
+ diagnostics,
178
+ slots: loaded.slots,
179
+ styles: matrix ? { groups: Object.keys(matrix.groups).length, matrix: '.weft/styles.json' } : null,
180
+ files: rels,
181
+ tests: testRels,
182
+ ms: Math.round(performance.now() - started),
183
+ }
184
+ }
185
+
186
+ /**
187
+ * R101, R102 from the contract, and R103, R109 from `cargo check`, for an app with a sidecar: an agent gets the same
188
+ * answer here as from `weft build`, with the function's id on it.
189
+ */
190
+ async function rustChecks(root, loaded) {
191
+ let sidecar
192
+ try { sidecar = await start(root, 'sidecar') } catch { return [] }
193
+ if (!sidecar.usesRemote(root)) return []
194
+ const rust = loaded.config?.rust
195
+ const held = process.env.WEFT_BUILD
196
+ process.env.WEFT_BUILD = '1'
197
+ try {
198
+ const { writeContract } = await start(root, 'contract')
199
+ const preset = { node: 'node', bun: 'bun', deno: 'deno_server' }[loaded.config?.runtime ?? 'node']
200
+ const { contract, diagnostics } = await writeContract({ root, site: loaded.config?.site, rust, preset })
201
+ const out = diagnostics.filter(d => d.code.startsWith('R1')).map(d => diagnostic({ ...d, file: '.weft/contract.json' }))
202
+ if (out.some(d => d.severity === 'error')) return out
203
+ for (const service of Object.keys(contract.services ?? {})) {
204
+ if (rust?.services?.[service]?.external) continue
205
+ const dir = sidecar.crateDir(root, rust, service)
206
+ if (!existsSync(dir)) {
207
+ out.push(diagnostic({ code: 'R109', file: '.weft/contract.json', why: `\`rust://${service}\` has no crate at \`${posix(relative(root, dir))}\`.` }))
208
+ continue
209
+ }
210
+ const checked = await sidecar.cargo('check', { root, dir, service, contract })
211
+ out.push(...checked.diagnostics.map(({ rendered, ...d }) => diagnostic({ ...d, help: rendered })))
212
+ out.push(...untouchedQueries(root, dir, contract, service))
213
+ if (contract.services[service].kind !== 'native' || !checked.ok) continue
214
+ const wasm = await sidecar.checkWasm({ root, dir, service, contract, preset, rust })
215
+ out.push(...wasm.diagnostics.map(d => diagnostic(d)))
216
+ }
217
+ return out
218
+ } catch (e) {
219
+ return [diagnostic({ code: 'R109', severity: 'warning', file: '.weft/contract.json', why: `the contract couldn't be written, so the sidecars weren't checked: ${e.message.split('\n')[0]}` })]
220
+ } finally {
221
+ if (held === undefined) delete process.env.WEFT_BUILD
222
+ else process.env.WEFT_BUILD = held
223
+ }
224
+ }
225
+
226
+ /** R105 before anything runs: a cached query with tags whose Rust never mentions `touch`, or hands `ctx` to anything. */
227
+ function untouchedQueries(root, dir, contract, service) {
228
+ const sources = readdirSync(join(dir, 'src'), { recursive: true, withFileTypes: true }).filter(f => f.isFile() && f.name.endsWith('.rs')).map(f => join(f.parentPath, f.name))
229
+ const out = []
230
+ for (const f of contract.functions) {
231
+ if (f.impl?.service !== service || !f.cache || !(f.tags === 'dynamic' || f.tags?.length)) continue
232
+ const method = f.name.replace(/[A-Z]/g, (c, i) => `${i ? '_' : ''}${c.toLowerCase()}`)
233
+ for (const file of sources) {
234
+ const text = readFileSync(file, 'utf8')
235
+ const at = text.search(new RegExp(`\\bfn\\s+${method}\\s*\\(`))
236
+ if (at < 0) continue
237
+ const open = text.indexOf('{', at)
238
+ let depth = 0, end = open
239
+ for (; end < text.length; end++) {
240
+ if (text[end] === '{') depth++
241
+ if (text[end] === '}' && --depth === 0) break
242
+ }
243
+ const body = text.slice(open, end)
244
+ const line = text.slice(0, at).split('\n').length
245
+ if (!/\.touch\(|\(\s*&?ctx\b|,\s*&?ctx\b/.test(body)) out.push(diagnostic({ code: 'R105', severity: 'warning', file: posix(relative(root, file)), span: [line, 1, line, 1], why: `\`${f.id}\` is cached under tags and reports no table: nothing can tell whether they cover what it reads. Call \`ctx.touch("table")\`, or drop \`tags\` if it reads none.` }))
246
+ break
247
+ }
248
+ }
249
+ return out
250
+ }
251
+
252
+ /** R013, R014 and R018: what the deploy target does to live data. Only an app with `changes` or `required` loads anything. */
253
+ async function liveChecks(root, loaded, preset) {
254
+ const file = join(root, 'src/realtime.server.ts')
255
+ let target
256
+ try { target = await start(root, 'live-target') } catch { return [] }
257
+ const required = target.scanRequired(root)
258
+ if (!required.length && !target.literalSecrets(root).length && !(existsSync(file) && /\bchanges\s*[:,}]/.test(readFileSync(file, 'utf8')))) return []
259
+ const held = process.env.WEFT_BUILD
260
+ process.env.WEFT_BUILD = '1'
261
+ try {
262
+ const { readLivePlan } = await start(root, 'contract')
263
+ const plan = await readLivePlan({ root, site: loaded.config?.site, live: loaded.config?.live, preset: preset ?? { node: 'node', bun: 'bun', deno: 'deno_server' }[loaded.config?.runtime ?? 'node'] })
264
+ return plan.diagnostics.map(d => diagnostic({ ...d, file: d.file ?? required[0]?.file ?? 'src/realtime.server.ts' }))
265
+ } catch (e) {
266
+ return [diagnostic({ code: 'R014', severity: 'warning', file: 'src/realtime.server.ts', why: `the deploy target couldn't be checked: ${e.message.split('\n')[0]}` })]
267
+ } finally {
268
+ if (held === undefined) delete process.env.WEFT_BUILD
269
+ else process.env.WEFT_BUILD = held
270
+ }
271
+ }
272
+
273
+ /** R001 for a server module's `channel`, and R004 for a socket handler of the app's own (research 32 §1). */
274
+ async function socketChecks(root) {
275
+ const src = join(root, 'src')
276
+ if (!existsSync(src)) return []
277
+ const out = []
278
+ let withIds
279
+ for (const f of readdirSync(src, { recursive: true, withFileTypes: true })) {
280
+ if (!f.isFile() || !/\.[cm]?[jt]sx?$/.test(f.name) || f.name.endsWith('.d.ts')) continue
281
+ const abs = join(f.parentPath, f.name), rel = relative(root, abs).replaceAll('\\', '/'), text = readFileSync(abs, 'utf8')
282
+ const hand = /from\s+['"]crossws|\bdefineWebSocketHandler\b/.exec(text)
283
+ if (hand) out.push(diagnostic({ code: 'R004', file: rel, ...locate(text, hand[0]), why: 'A WebSocket handler of its own skips the Origin check, `authorize`, the limits and resume. Use `channel`.' }))
284
+ if (!/\b(channel|sharedState)\s*[(<]/.test(text) || !/^src\/server\/|\.server\.[cm]?[jt]sx?$/.test(rel)) continue
285
+ if (withIds === undefined) {
286
+ try { withIds = (await import(pathToFileURL(createRequire(join(root, 'package.json')).resolve('@weft-js/start/vite')).href)).withIds } catch { withIds = null }
287
+ }
288
+ try { withIds?.(text, abs, rel) } catch (e) {
289
+ if (e.code !== 'R001') continue
290
+ const line = +/:(\d+) /.exec(e.message)?.[1]
291
+ out.push(diagnostic({ code: 'R001', file: rel, span: [line, 1, line, 1], why: e.message.split('\n')[0].replace(/^R001: \S+ /, '') }))
292
+ }
293
+ }
294
+ return out
295
+ }
296
+
297
+ /** weft owns the connection: these bypass reconnects, resume and the fake transport (research 30 §8). */
298
+ const SOCKET_PACKAGES = ['ws', 'socket.io-client', 'reconnecting-websocket', '@microsoft/fetch-event-source']
299
+
300
+ /** P002 (`package.json` scripts that aren't `weft …`), P120 (weft plugins wired straight into Vite), R007 (socket libraries). */
301
+ function hygiene(root) {
302
+ const out = []
303
+ const pkgFile = join(root, 'package.json')
304
+ if (existsSync(pkgFile)) {
305
+ const text = readFileSync(pkgFile, 'utf8')
306
+ let scripts = {}, deps = {}
307
+ try {
308
+ const pkg = JSON.parse(text)
309
+ scripts = pkg.scripts ?? {}
310
+ deps = pkg.dependencies ?? {}
311
+ } catch {}
312
+ for (const name of SOCKET_PACKAGES) {
313
+ if (deps[name]) out.push(diagnostic({ code: 'R007', file: 'package.json', ...locate(text, `"${name}"`), why: `\`${name}\` is a dependency: its connection has no resume, no cleanup and no fake in @weft-js/core/test. Use \`stream\`, \`.live()\` or \`channel\`.` }))
314
+ }
315
+ for (const [name, cmd] of Object.entries(scripts)) {
316
+ if (!/^weft(\s|$)/.test(cmd.trim())) {
317
+ out.push(diagnostic({ code: 'P002', file: 'package.json', ...locate(text, `"${name}"`), why: `Script \`${name}\` runs \`${cmd}\`, not \`weft …\`: agents can't see what it does or what it touches.` }))
318
+ }
319
+ }
320
+ }
321
+ for (const name of ['vite.config.ts', 'vite.config.mts', 'vite.config.js', 'vite.config.mjs']) {
322
+ const file = join(root, name)
323
+ if (!existsSync(file)) continue
324
+ const text = readFileSync(file, 'utf8')
325
+ for (const m of text.matchAll(/from\s+['"](@weft-js\/(?:plugin-[\w-]+|text))/g)) {
326
+ out.push(diagnostic({ code: 'P120', file: name, ...locate(text, m[1]), why: `\`${m[1]}\` is registered in ${name}, which bypasses weft.config.ts.` }))
327
+ }
328
+ }
329
+ return out
330
+ }