@weft-js/cli 0.1.0 → 0.1.2

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 +38 -14
  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 +108 -0
  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 +174 -188
  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 +68 -444
  65. package/src/check/runtime.mjs +280 -0
  66. package/src/check.mjs +199 -207
  67. package/src/cli/authoring.mjs +213 -0
  68. package/src/cli/project.mjs +302 -0
  69. package/src/cli.mjs +228 -224
  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 +142 -0
  74. package/src/deploy/files.mjs +266 -0
  75. package/src/deploy/fit.mjs +166 -0
  76. package/src/deploy/hosts.mjs +126 -0
  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 +387 -0
  81. package/src/deploy/needs.mjs +149 -0
  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 -42
  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 +144 -116
  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 +194 -92
  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
package/src/cli.mjs CHANGED
@@ -24,8 +24,55 @@ import { formatGate, gate } from './gate.mjs'
24
24
  import { browserCommand } from './browser/cli.mjs'
25
25
  import { metaTable } from './meta-table.mjs'
26
26
  import { audit } from './audit.mjs'
27
- import { buildCommand, dbCommand, devCommand, doctorCommand, routesCommand, startCommand } from './start.mjs'
28
- import { explainTarget, findGroups, formatExplain, formatFind, styleMatrix, writeMatrixFile } from './style.mjs'
27
+ import {
28
+ buildCommand,
29
+ dbCommand,
30
+ devCommand,
31
+ doctorCommand,
32
+ routesCommand,
33
+ startCommand,
34
+ unsafeAllowed,
35
+ } from './start.mjs'
36
+ import { deployCommand, formatHosts, hostsCommand, initCommand } from './deploy/index.mjs'
37
+ import {
38
+ explainTarget,
39
+ findGroups,
40
+ formatExplain,
41
+ formatFind,
42
+ styleMatrix,
43
+ writeMatrixFile,
44
+ } from './style.mjs'
45
+
46
+ import {
47
+ commandLsp,
48
+ commandTsc,
49
+ commandCheck,
50
+ commandDoctor,
51
+ commandTest,
52
+ commandBrowser,
53
+ commandDev,
54
+ commandBuild,
55
+ commandAudit,
56
+ commandDeploy,
57
+ commandStart,
58
+ commandDb,
59
+ commandRoutes,
60
+ commandSurfaces,
61
+ commandTasks,
62
+ commandRun,
63
+ commandDocs,
64
+ commandAgentsMd,
65
+ } from './cli/project.mjs'
66
+ import {
67
+ commandMap,
68
+ commandIcons,
69
+ commandAdd,
70
+ commandPlugins,
71
+ commandFix,
72
+ commandTokens,
73
+ commandStyle,
74
+ commandMcp,
75
+ } from './cli/authoring.mjs'
29
76
 
30
77
  const usage = `usage: weft <command> [--json] [--root <dir>]
31
78
 
@@ -42,18 +89,25 @@ const usage = `usage: weft <command> [--json] [--root <dir>]
42
89
  build [--runtime node|bun|deno] [--preset <p>] production build to .output, and .weft/contract.json; then the
43
90
  built pages are audited (L-codes) and an error fails the build
44
91
  [--unsafe-allow <codes>] go past these codes (R002, L003, a family like L, or all): forces a deploy
45
- audit [routes…] [--url <u>] [--all] [--max <n>] the built pages, or a running site, in a clean Chrome at a phone's
92
+ audit [routes…] [--url <u>] [--indexing public|private|auto] [--all] [--max <n>] the built pages, or a running site, in a clean Chrome at a phone's
46
93
  and a desktop's width: axe-core's WCAG rules, console, deprecations, head, sideways scroll, shift
94
+ deploy [--prod] [--yes] [--target t] fit check, build for the target, then the host's own CLI; without --yes a dry
95
+ run that prints what would run. Netlify, Vercel, Cloudflare, Fly, Render; the live URL at the end
96
+ deploy hosts which hosts can run this app, why not, what each costs, and the one weft suggests
97
+ deploy init [target] [--dry-run] [--force] the host's files (netlify.toml, fly.toml + Dockerfile…), a GitHub
98
+ workflow that checks and deploys on push, and \`deploy\` in the config
47
99
  start [--runtime r] run .output/ on its runtime (the config's \`runtime\`, node by default)
48
100
  db generate [name] a migration for the schema's changes (it applies on the next request)
49
101
  db live a migration with change triggers for the tables in \`changes\`
50
102
  db listen the change feed as its own process, for an app on functions
51
103
  routes [--meta] the route table; --meta: every route's merged metadata, tags, card mode and H-diagnostics
104
+ surfaces the canvas and WebGL surfaces this project already defines
52
105
  tasks commands and project tasks
53
106
  run <task> run a project task from weft.config.ts
54
107
  docs [topic|code] docs for agents (rules, props, lists, async, testing, config, diagnostics, commands, W014…)
55
108
  agents-md [--check] write AGENTS.md (--check: exit 1 when stale; --stdout: print it)
56
109
  map [--stdout] write .weft/map.md and .json: a matrix of what to call in this app
110
+ icons publish <dir> every icon and variant as static .svg files with a manifest.json, for a CDN
57
111
  add <feature> install a plugin and wire it into the config; also tailwind, auth, db [engine] [--layer drizzle|prisma|native], and rust [name] [--remote] [--dry-run] [--no-install]
58
112
  plugins [list] plugins, the slots they provide and bind, permissions, diagnostics
59
113
  plugins explain <slot> binding chain: config → plugin → module → .weft/slots/<id>.ts
@@ -64,16 +118,67 @@ const usage = `usage: weft <command> [--json] [--root <dir>]
64
118
  style explain <group|selector> [--<axis> <value>] [--states a,b] [--accent intent:token] [--part p]
65
119
  resolved CSS for one variation, and where each value came from
66
120
  style matrix write .weft/styles.json (every group: axes, parts, accents, resolved CSS, uses)
67
- mcp MCP server on stdio: docs, check, tasks, style_find, style_explain, test_gate, browser_*`
121
+ mcp MCP server on stdio: docs, check, audit, routes_meta, map, tasks, surfaces, style_find,
122
+ style_explain, test_gate, browser_*`
68
123
 
69
124
  /** Flags that take a value (`--base main` or `--base=main`), and boolean flags, beyond the global ones. */
70
- const valued = new Set(['layer', 'base', 'budget', 'max-mutants', 'concurrency', 'url', 'tab', 'out', 'component', 'from', 'props', 'name', 'scope', 'timeout', 'preset', 'port', 'runtime', 'unsafe-allow', 'max'])
125
+ const valued = new Set([
126
+ 'layer',
127
+ 'base',
128
+ 'budget',
129
+ 'max-mutants',
130
+ 'concurrency',
131
+ 'url',
132
+ 'indexing',
133
+ 'tab',
134
+ 'out',
135
+ 'component',
136
+ 'from',
137
+ 'props',
138
+ 'name',
139
+ 'scope',
140
+ 'timeout',
141
+ 'preset',
142
+ 'port',
143
+ 'runtime',
144
+ 'unsafe-allow',
145
+ 'max',
146
+ 'target',
147
+ ])
71
148
  /** `weft style explain` takes the group's own axes as flags (`--status late`); these are its fixed ones. */
72
149
  const styleFlags = new Set(['states', 'accent', 'part'])
73
- const booleans = new Set(['dry-run', 'no-install', 'remote', 'gate', 'refactor', 'no-mutation', 'interactive', 'delta', 'graph-only', 'no-graph', 'headed', 'full', 'reload', 'meta', 'all', 'verbose'])
150
+ const booleans = new Set([
151
+ 'dry-run',
152
+ 'no-install',
153
+ 'remote',
154
+ 'gate',
155
+ 'refactor',
156
+ 'no-mutation',
157
+ 'interactive',
158
+ 'delta',
159
+ 'graph-only',
160
+ 'no-graph',
161
+ 'headed',
162
+ 'full',
163
+ 'reload',
164
+ 'meta',
165
+ 'all',
166
+ 'verbose',
167
+ 'prod',
168
+ 'yes',
169
+ 'force',
170
+ ])
74
171
 
75
172
  export function parse(argv) {
76
- const out = { command: argv[0], positional: [], json: false, root: process.cwd(), check: false, stdout: false, flags: {} }
173
+ const out = {
174
+ command: argv[0],
175
+ positional: [],
176
+ json: false,
177
+ root: process.cwd(),
178
+ check: false,
179
+ stdout: false,
180
+ flags: {},
181
+ }
77
182
  for (let i = 1; i < argv.length; i++) {
78
183
  const a = argv[i]
79
184
  if (a === '--json') out.json = true
@@ -85,236 +190,134 @@ export function parse(argv) {
85
190
  const [name, inline] = a.slice(2).split(/=(.*)/s)
86
191
  if (valued.has(name)) out.flags[name] = inline ?? argv[++i]
87
192
  else if (booleans.has(name) && inline === undefined) out.flags[name] = true
88
- else if (out.command === 'style' && styleFlags.has(name)) out.flags[name] = inline ?? argv[++i]
89
- else if (out.command === 'style' && /^[a-z][\w-]*$/i.test(name) && (inline ?? argv[i + 1]) !== undefined) (out.flags.axes ??= {})[name] = inline ?? argv[++i]
193
+ else if (out.command === 'style' && styleFlags.has(name))
194
+ out.flags[name] = inline ?? argv[++i]
195
+ else if (
196
+ out.command === 'style' &&
197
+ /^[a-z][\w-]*$/i.test(name) &&
198
+ (inline ?? argv[i + 1]) !== undefined
199
+ )
200
+ (out.flags.axes ??= {})[name] = inline ?? argv[++i]
90
201
  else throw Object.assign(new Error(`unknown flag ${a}`), { code: 'E_USAGE' })
91
202
  } else out.positional.push(a)
92
203
  }
93
204
  return out
94
205
  }
95
206
 
207
+ const dependencies = {
208
+ fix,
209
+ writeTokens,
210
+ createRequire,
211
+ existsSync,
212
+ join,
213
+ resolve,
214
+ pathToFileURL,
215
+ addCommand,
216
+ writeAgentsMd,
217
+ check,
218
+ formatHuman,
219
+ sortDiagnostics,
220
+ docs,
221
+ compilerLine,
222
+ doctor,
223
+ formatDoctor,
224
+ formatMap,
225
+ writeMap,
226
+ serve,
227
+ explain,
228
+ list,
229
+ sync,
230
+ approveCommand,
231
+ auditCommand,
232
+ formatTrust,
233
+ loadConfig,
234
+ listSurfaces,
235
+ runTask,
236
+ tasks,
237
+ formatGate,
238
+ gate,
239
+ browserCommand,
240
+ metaTable,
241
+ audit,
242
+ buildCommand,
243
+ dbCommand,
244
+ devCommand,
245
+ doctorCommand,
246
+ routesCommand,
247
+ startCommand,
248
+ unsafeAllowed,
249
+ deployCommand,
250
+ formatHosts,
251
+ hostsCommand,
252
+ initCommand,
253
+ explainTarget,
254
+ findGroups,
255
+ formatExplain,
256
+ formatFind,
257
+ styleMatrix,
258
+ writeMatrixFile,
259
+ }
260
+
96
261
  export async function main(argv, io = { stdout: process.stdout, stderr: process.stderr }) {
97
- const print = data => io.stdout.write(`${JSON.stringify(data, null, 2)}\n`)
98
- const say = text => text && io.stderr.write(`${text}\n`)
262
+ const print = (data) => io.stdout.write(`${JSON.stringify(data, null, 2)}\n`)
263
+ const say = (text) => text && io.stderr.write(`${text}\n`)
99
264
  let args
100
265
  try {
101
266
  args = parse(argv)
267
+ const context = { ...dependencies, args, print, say, io }
102
268
  switch (args.command) {
103
- case 'lsp': {
104
- const { lsp } = await import('./infer/lsp.mjs')
105
- const out = lsp(args.root)
106
- return await new Promise(ok => { out.server.on('exit', () => ok(0)) })
107
- }
108
- case 'tsc': {
109
- const { propTsc } = await import('./props-checks.mjs')
110
- const diagnostics = sortDiagnostics(await propTsc(args.root))
111
- if (args.json) print({ ok: !diagnostics.some(d => d.severity === 'error'), diagnostics })
112
- say(formatHuman(diagnostics))
113
- say(`weft tsc: ${diagnostics.filter(d => d.severity === 'error').length} errors`)
114
- return diagnostics.some(d => d.severity === 'error') ? 1 : 0
115
- }
116
- case 'check': {
117
- const result = await check(args.root, { files: args.positional, preset: args.flags.preset })
118
- if (args.json) print(result)
119
- say(formatHuman(result.diagnostics))
120
- if (args.flags.verbose) say(compilerLine(doctor(args.root).compiler))
121
- say(`weft check: ${result.summary.files} files, ${result.summary.errors} errors, ${result.summary.warnings} warnings (${result.ms} ms)`)
122
- return result.ok ? 0 : 1
123
- }
124
- case 'doctor': {
125
- // What this install compiles with, always; what live data does on the target, for an app that has any.
126
- const result = doctor(args.root)
127
- const plan = args.flags.preset || existsSync(join(args.root, 'src/realtime.server.ts')) ? await doctorCommand(args.root, { preset: args.flags.preset, runtime: args.flags.runtime }) : undefined
128
- if (args.json) print({ ...result, live: plan })
129
- else say(formatDoctor(result))
130
- if (!plan) return 0
131
- say(`weft doctor: ${plan.preset} (${plan.host})\n live mode: ${plan.mode}\n ${plan.says}${plan.required.length ? `\n required: ${plan.required.join(', ')}` : ''}${plan.options.length ? `\n options:\n${plan.options.map(o => ` - ${o}`).join('\n')}` : ''}`)
132
- say(formatHuman(plan.diagnostics))
133
- return plan.diagnostics.some(d => d.severity === 'error') ? 1 : 0
134
- }
135
- case 'test': {
136
- if (!args.flags.gate) throw Object.assign(new Error('`weft test` runs the gate with --gate; run the suite itself with vitest'), { code: 'E_USAGE' })
137
- const num = name => (args.flags[name] === undefined ? undefined : Number(args.flags[name]))
138
- const result = await gate(args.root, {
139
- base: args.flags.base, budgetSec: num('budget'), maxMutants: num('max-mutants'), concurrency: num('concurrency'),
140
- refactor: !!args.flags.refactor, mutation: !args.flags['no-mutation'], log: line => say(line),
141
- })
142
- if (args.json) print(result)
143
- say(formatHuman(result.diagnostics))
144
- say(formatGate(result))
145
- say(`weft test --gate: ${result.ok ? 'pass' : 'fail'}, ${result.summary.changedTests} changed tests, ${result.summary.errors} errors, ${result.summary.warnings} warnings (${result.ms} ms)`)
146
- return result.ok ? 0 : 1
147
- }
269
+ case 'lsp':
270
+ return await commandLsp(context)
271
+ case 'tsc':
272
+ return await commandTsc(context)
273
+ case 'check':
274
+ return await commandCheck(context)
275
+ case 'doctor':
276
+ return await commandDoctor(context)
277
+ case 'test':
278
+ return await commandTest(context)
148
279
  case 'browser':
149
- return await browserCommand(args, { print, say, io })
150
- case 'dev': {
151
- const server = await devCommand(args.root, { port: args.flags.port && Number(args.flags.port) })
152
- say(`weft dev: ${server.resolvedUrls.local[0]}`)
153
- await new Promise(ok => { process.once('SIGINT', ok) })
154
- await server.close()
155
- return 0
156
- }
157
- case 'build': {
158
- const result = await buildCommand(args.root, { preset: args.flags.preset, runtime: args.flags.runtime, unsafeAllow: args.flags['unsafe-allow'] })
159
- if (args.json) print(result)
160
- say(formatHuman(result.diagnostics))
161
- say(`weft build: ${result.preset} → ${result.output}, ${result.functions} functions in the contract, ${result.audited} pages audited (${result.ms} ms)`)
162
- if (!result.ok) say('weft build: failed. The output is written, but fix the errors above before it ships, or `--unsafe-allow <code>` to force it.')
163
- return result.ok ? 0 : 1
164
- }
165
- case 'audit': {
166
- const result = await audit(args.root, { url: args.flags.url, routes: args.positional.length ? args.positional : undefined, all: !!args.flags.all, max: args.flags.max && Number(args.flags.max) })
167
- if (args.json) print(result)
168
- say(formatHuman(result.diagnostics))
169
- say(`weft audit: ${result.skipped ?? `${result.pages.length} pages at a phone's and a desktop's width, ${result.diagnostics.length} findings (${result.ms} ms)`}`)
170
- return result.ok ? 0 : 1
171
- }
280
+ return await commandBrowser(context)
281
+ case 'dev':
282
+ return await commandDev(context)
283
+ case 'build':
284
+ return await commandBuild(context)
285
+ case 'audit':
286
+ return await commandAudit(context)
287
+ case 'deploy':
288
+ return await commandDeploy(context)
172
289
  case 'start':
173
- return await startCommand(args.root, { runtime: args.flags.runtime })
174
- case 'db': {
175
- const result = await dbCommand(args.root, args.positional[0], args.positional[1])
176
- if (args.json) print(result)
177
- say(result.output)
178
- return 0
179
- }
180
- case 'routes': {
181
- const result = await routesCommand(args.root, { meta: !!args.flags.meta, preset: args.flags.preset })
182
- if (args.json) print(result)
183
- else say(args.flags.meta ? metaTable(result) : result.routes.map(r => `${r.path.padEnd(24)} ${r.file}`).join('\n'))
184
- say(formatHuman(result.diagnostics))
185
- return result.ok ? 0 : 1
186
- }
187
- case 'surfaces': {
188
- const surfaces = listSurfaces(args.root)
189
- if (args.json) print({ surfaces })
190
- else say(surfaces.length ? surfaces.map(x => `${String(x.name).padEnd(20)} ${String(x.kind).padEnd(10)} ${x.file}:${x.line} props: ${x.props.join(', ') || '(none)'}`).join('\n') : 'No surfaces yet: `weft docs surface`.')
191
- return 0
192
- }
193
- case 'tasks': {
194
- const result = await tasks(args.root)
195
- if (args.json) print(result)
196
- else say(result.tasks.map(t => `${t.available ? ' ' : '·'} ${t.name.padEnd(10)} ${t.description ?? ''}`).join('\n'))
197
- say(formatHuman(result.diagnostics))
198
- return result.ok ? 0 : 1
199
- }
200
- case 'run': {
201
- if (!args.positional[0]) throw Object.assign(new Error('weft run <task>'), { code: 'E_USAGE' })
202
- return await runTask(args.root, args.positional[0])
203
- }
204
- case 'docs': {
205
- const page = await docs(args.root, args.positional[0])
206
- if (args.json) print(page)
207
- else io.stdout.write(`${page.text}\n`)
208
- return 0
209
- }
210
- case 'agents-md': {
211
- const out = await writeAgentsMd(args.root, { check: args.check || args.stdout })
212
- if (args.stdout) io.stdout.write(out.text)
213
- if (args.json) print({ path: out.path, bytes: out.bytes, limit: 8192, overLimit: out.overLimit, current: out.current, written: out.written })
214
- say(`AGENTS.md: ${out.bytes} bytes${out.overLimit ? ' (over the 8 KB limit)' : ''}, ${out.written ? 'written' : out.current ? 'current' : 'stale'}`)
215
- return (args.check && !out.current) || out.overLimit ? 1 : 0
216
- }
217
- case 'map': {
218
- const map = await writeMap(args.root)
219
- if (args.json) print(map)
220
- else if (args.stdout) process.stdout.write(formatMap(map))
221
- else say(`.weft/map.md: ${map.rows.length} rows`)
222
- return 0
223
- }
224
- case 'icons': {
225
- if (args.positional[0] !== 'publish' || !args.positional[1]) { say('usage: weft icons publish <dir>'); return 1 }
226
- const { publish } = await import(pathToFileURL(createRequire(join(args.root, 'package.json')).resolve('@weft-js/warp-icons/compile')).href)
227
- const out = await publish(resolve(args.root, args.positional[1]))
228
- if (args.json) print(out)
229
- else say(`${out.files} files for ${out.icons} icons in ${out.dir}. Nothing was uploaded: host that folder, then set icons({ source }).`)
230
- return 0
231
- }
232
- case 'add': {
233
- const result = await addCommand(args.root, args.positional[0], { dryRun: args.flags['dry-run'], install: !args.flags['no-install'], engine: args.positional[1], layer: args.flags.layer, remote: args.flags.remote })
234
- if (args.json) print(result)
235
- else say(result.already ? `${result.package} is already in ${result.config}` : result.actions.map(a => `${args.flags['dry-run'] ? 'would ' : ''}${a}`).join('\n'))
236
- return 0
237
- }
238
- case 'plugins': {
239
- const sub = args.positional[0] ?? 'list'
240
- if (sub === 'list') {
241
- const result = await list(args.root)
242
- if (args.json) print(result)
243
- else say(result.plugins.map(p => `${p.name}@${p.version ?? '?'} ${p.provides.map(s => `${s.slot}${s.key ? `[${s.key}]` : ''}${s.bound ? '' : ' (not bound)'}`).join(', ') || 'no slots'}${p.tokens ? ` tokens --${p.tokens}-*` : ''}`).join('\n') || 'no plugins')
244
- say(formatHuman(result.diagnostics))
245
- return result.ok ? 0 : 1
246
- }
247
- if (sub === 'explain') {
248
- const result = await explain(args.root, args.positional[1])
249
- if (args.json) print(result)
250
- else say(JSON.stringify(result.binding, null, 2))
251
- return 0
252
- }
253
- if (sub === 'sync') {
254
- const loaded = await loadConfig(args.root)
255
- const errors = loaded.diagnostics.filter(d => d.severity === 'error')
256
- const written = loaded.diagnostics.some(d => d.code === 'P007') ? [] : sync(args.root, loaded)
257
- if (args.json) print({ ok: errors.length === 0, written, diagnostics: loaded.diagnostics })
258
- say(formatHuman(loaded.diagnostics))
259
- say(`weft plugins sync: ${written.length} files written`)
260
- return errors.length ? 1 : 0
261
- }
262
- if (sub === 'audit') {
263
- const loaded = await loadConfig(args.root)
264
- const result = auditCommand(args.root, loaded)
265
- if (args.json) print(result)
266
- else say(result.plugins.map(formatTrust).join('\n') || 'no plugins')
267
- say(formatHuman(result.diagnostics))
268
- return result.ok ? 0 : 1
269
- }
270
- if (sub === 'approve') {
271
- const result = approveCommand(args.root, await loadConfig(args.root), { names: args.positional.slice(1), all: !!args.flags.all })
272
- if (args.json) print(result)
273
- else say([...(result.written ? result.approved : result.pending).map(formatTrust), result.written ? `${result.lock}: ${result.approved.length} approved` : result.pending.length ? 'nothing written: name a plugin, or pass --all' : 'nothing waiting', ...result.written ? result.pending.map(p => `still waiting: ${p.name}`) : [], ...(result.gitignore ? ['.gitignore: the lock is no longer ignored, so commit it'] : [])].join('\n'))
274
- return result.ok ? 0 : 1
275
- }
276
- throw Object.assign(new Error(`unknown plugins subcommand ${String(sub)}`), { code: 'E_USAGE' })
277
- }
278
- case 'fix': {
279
- const result = await fix(args.root, { files: args.positional })
280
- if (args.json) print(result)
281
- else say(result.applied.length ? result.applied.map(a => `${a.file}:${a.span[0]}:${a.span[1]} ${a.code}`).join('\n') : 'nothing to fix')
282
- return 0
283
- }
284
- case 'tokens': {
285
- const result = await writeTokens(args.root)
286
- if (args.json) print(result)
287
- else say(`${result.dir}: ${result.files.length} files`)
288
- return 0
289
- }
290
- case 'style': {
291
- const sub = args.positional[0]
292
- if (sub === 'find') {
293
- const result = findGroups((await styleMatrix(args.root)).matrix, args.positional.slice(1).join(' '))
294
- if (args.json) print(result)
295
- else say(formatFind(result))
296
- return 0
297
- }
298
- if (sub === 'explain') {
299
- const accent = args.flags.accent ? Object.fromEntries(args.flags.accent.split(/[\s,]+/).filter(Boolean).map(w => w.split(':'))) : undefined
300
- const states = args.flags.states?.split(',').map(s => s.trim()).filter(Boolean)
301
- const result = explainTarget(await styleMatrix(args.root), args.positional.slice(1).join(' '), { axes: args.flags.axes, states, accent, part: args.flags.part })
302
- if (args.json) print(result)
303
- else say(formatExplain(result))
304
- return 0
305
- }
306
- if (sub === 'matrix') {
307
- const { matrix } = await styleMatrix(args.root)
308
- const written = args.stdout ? false : writeMatrixFile(args.root, matrix)
309
- if (args.stdout || args.json) print(matrix)
310
- say(`.weft/styles.json: ${Object.keys(matrix.groups).length} groups, ${matrix.diagnostics.length} diagnostics, ${args.stdout ? 'not written' : written ? 'written' : 'current'}`)
311
- return 0
312
- }
313
- throw Object.assign(new Error(`weft style ${sub ?? ''}: use find <query>, explain <group|selector> or matrix`), { code: 'E_USAGE' })
314
- }
290
+ return await commandStart(context)
291
+ case 'db':
292
+ return await commandDb(context)
293
+ case 'routes':
294
+ return await commandRoutes(context)
295
+ case 'surfaces':
296
+ return commandSurfaces(context)
297
+ case 'tasks':
298
+ return await commandTasks(context)
299
+ case 'run':
300
+ return await commandRun(context)
301
+ case 'docs':
302
+ return await commandDocs(context)
303
+ case 'agents-md':
304
+ return await commandAgentsMd(context)
305
+ case 'map':
306
+ return await commandMap(context)
307
+ case 'icons':
308
+ return await commandIcons(context)
309
+ case 'add':
310
+ return await commandAdd(context)
311
+ case 'plugins':
312
+ return await commandPlugins(context)
313
+ case 'fix':
314
+ return await commandFix(context)
315
+ case 'tokens':
316
+ return await commandTokens(context)
317
+ case 'style':
318
+ return await commandStyle(context)
315
319
  case 'mcp':
316
- await serve(args.root)
317
- return 0
320
+ return await commandMcp(context)
318
321
  case undefined:
319
322
  case 'help':
320
323
  case '--help':
@@ -325,7 +328,8 @@ export async function main(argv, io = { stdout: process.stdout, stderr: process.
325
328
  }
326
329
  } catch (error) {
327
330
  const code = error.code ?? 'E_INTERNAL'
328
- if (args?.json || argv.includes('--json')) print({ ok: false, error: { code, message: error.message } })
331
+ if (args?.json || argv.includes('--json'))
332
+ print({ ok: false, error: { code, message: error.message } })
329
333
  say(`weft: ${error.message}${code === 'E_USAGE' ? `\n\n${usage}` : ''}`)
330
334
  return code === 'E_REFUSED' ? 1 : 2
331
335
  }
package/src/config.mjs CHANGED
@@ -2,4 +2,4 @@
2
2
  // Apps import it from `@weft-js/core/config`; the CLI's loader falls back here when a project's weft doesn't resolve.
3
3
 
4
4
  /** @type {import('@weft-js/core/config').defineConfig} */
5
- export const defineConfig = config => config
5
+ export const defineConfig = (config) => config
@@ -9,7 +9,12 @@ const byName = (a, b) => (a.name < b.name ? -1 : a.name > b.name ? 1 : 0)
9
9
  function stable(value) {
10
10
  return JSON.stringify(value, (_, v) => {
11
11
  if (typeof v === 'function') return `[function ${v.name}]`
12
- if (v && typeof v === 'object' && !Array.isArray(v)) return Object.fromEntries(Object.keys(v).sort().map(k => [k, v[k]]))
12
+ if (v && typeof v === 'object' && !Array.isArray(v))
13
+ return Object.fromEntries(
14
+ Object.keys(v)
15
+ .sort()
16
+ .map((k) => [k, v[k]]),
17
+ )
13
18
  return v
14
19
  })
15
20
  }
@@ -27,43 +32,88 @@ export function contributions(plugins, report, env) {
27
32
  const catalogOwner = new Map()
28
33
  for (const p of [...plugins].sort(byName)) {
29
34
  if (typeof p.contribute !== 'function') continue
30
- const run = () => p.contribute({ command: env.command ?? 'check', mode: 'development', root: env.root })
35
+ const run = () =>
36
+ p.contribute({ command: env.command ?? 'check', mode: 'development', root: env.root })
31
37
  let manifest
32
38
  try {
33
39
  manifest = run()
34
40
  } catch (error) {
35
41
  const code = typeof error?.code === 'string' ? error.code : 'P007'
36
- report(code, `\`${p.name}\`: ${String(error?.message ?? error).split('\n')[0]}`, p.name, { owner: code === 'P007' ? 'core' : p.name, help: error?.help })
42
+ report(code, `\`${p.name}\`: ${String(error?.message ?? error).split('\n')[0]}`, p.name, {
43
+ owner: code === 'P007' ? 'core' : p.name,
44
+ help: error?.help,
45
+ })
37
46
  continue
38
47
  }
39
48
  let again
40
49
  try {
41
50
  again = run()
42
51
  } catch {}
43
- if (!again || stable(again) !== stable(manifest)) report('P117', `\`${p.name}\`'s contribute() returned a different manifest on a second call.`, p.name)
52
+ if (!again || stable(again) !== stable(manifest))
53
+ report(
54
+ 'P117',
55
+ `\`${p.name}\`'s contribute() returned a different manifest on a second call.`,
56
+ p.name,
57
+ )
44
58
 
45
- const ns = p.namespace ?? `${p.name.replace(/^@[^/]+\//, '').replace(/^plugin-/, '')}/`
46
- for (const d of manifest.diagnostics ?? []) {
47
- if (!d.code.startsWith(ns)) report('P111', `\`${p.name}\` declares \`${d.code}\`, outside its namespace \`${ns}\`.`, p.name)
48
- }
49
- const agents = manifest.agents
50
- if (agents) {
51
- const bytes = Buffer.byteLength(agentsSection(agents))
52
- if (bytes > agentsLimit) report('P112', `\`${p.name}\`'s AGENTS section is ${bytes} bytes (limit ${agentsLimit}).`, p.name)
53
- }
54
- if ((manifest.map?.length ?? 0) > mapLimit) report('P112', `\`${p.name}\` adds ${manifest.map.length} map rows (limit ${mapLimit}).`, p.name)
55
- const tns = manifest.tokens?.namespace
56
- if (tns) {
57
- if (tokenOwner.has(tns)) report('P113', `Token namespace \`${tns}\` is contributed by both \`${tokenOwner.get(tns)}\` and \`${p.name}\`.`, p.name)
58
- else tokenOwner.set(tns, p.name)
59
- }
60
- for (const c of manifest.catalog ?? []) {
61
- if (catalogOwner.has(c.name)) report('P113', `Catalog entry \`${c.name}\` is contributed by both \`${catalogOwner.get(c.name)}\` and \`${p.name}\`.`, p.name)
62
- else catalogOwner.set(c.name, p.name)
63
- }
64
- out.push({ name: p.name, version: p.version, namespace: ns, permissions: p.permissions ?? {}, manifest })
59
+ const ns = checkManifest(p, manifest, report, tokenOwner, catalogOwner)
60
+ out.push({
61
+ name: p.name,
62
+ version: p.version,
63
+ namespace: ns,
64
+ permissions: p.permissions ?? {},
65
+ manifest,
66
+ })
65
67
  }
66
68
  return out
67
69
  }
68
70
 
69
- export const agentsSection = agents => `### ${agents.title}\n\n${agents.text.trim()}\n`
71
+ function checkManifest(p, manifest, report, tokenOwner, catalogOwner) {
72
+ const ns = p.namespace ?? `${p.name.replace(/^@[^/]+\//, '').replace(/^plugin-/, '')}/`
73
+ for (const d of manifest.diagnostics ?? []) {
74
+ if (!d.code.startsWith(ns))
75
+ report(
76
+ 'P111',
77
+ `\`${p.name}\` declares \`${d.code}\`, outside its namespace \`${ns}\`.`,
78
+ p.name,
79
+ )
80
+ }
81
+ const agents = manifest.agents
82
+ if (agents) {
83
+ const bytes = Buffer.byteLength(agentsSection(agents))
84
+ if (bytes > agentsLimit)
85
+ report(
86
+ 'P112',
87
+ `\`${p.name}\`'s AGENTS section is ${bytes} bytes (limit ${agentsLimit}).`,
88
+ p.name,
89
+ )
90
+ }
91
+ if ((manifest.map?.length ?? 0) > mapLimit)
92
+ report(
93
+ 'P112',
94
+ `\`${p.name}\` adds ${manifest.map.length} map rows (limit ${mapLimit}).`,
95
+ p.name,
96
+ )
97
+ const tns = manifest.tokens?.namespace
98
+ if (tns) {
99
+ if (tokenOwner.has(tns))
100
+ report(
101
+ 'P113',
102
+ `Token namespace \`${tns}\` is contributed by both \`${tokenOwner.get(tns)}\` and \`${p.name}\`.`,
103
+ p.name,
104
+ )
105
+ else tokenOwner.set(tns, p.name)
106
+ }
107
+ for (const c of manifest.catalog ?? []) {
108
+ if (catalogOwner.has(c.name))
109
+ report(
110
+ 'P113',
111
+ `Catalog entry \`${c.name}\` is contributed by both \`${catalogOwner.get(c.name)}\` and \`${p.name}\`.`,
112
+ p.name,
113
+ )
114
+ else catalogOwner.set(c.name, p.name)
115
+ }
116
+ return ns
117
+ }
118
+
119
+ export const agentsSection = (agents) => `### ${agents.title}\n\n${agents.text.trim()}\n`