tovuk 0.1.49 → 0.1.50
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.
- package/README.md +2 -2
- package/package.json +1 -1
- package/src/internal/config-validation.ts +35 -7
- package/src/internal/constants.ts +26 -2
- package/src/internal/rust-doctor.ts +27 -15
- package/src/internal/templates.ts +18 -0
package/README.md
CHANGED
|
@@ -12,8 +12,8 @@ npx tovuk deploy --wait --json
|
|
|
12
12
|
`npx tovuk` is the public npm command.
|
|
13
13
|
|
|
14
14
|
Rust backends expect `Cargo.toml`, `Cargo.lock`, and `tovuk.toml`. They must
|
|
15
|
-
pass `cargo fmt --all --check`, locked
|
|
16
|
-
`0.0.0.0:$PORT`, and expose the configured health endpoint.
|
|
15
|
+
pass `cargo fmt --all --check`, locked release-mode check/test/Clippy gates,
|
|
16
|
+
listen on `0.0.0.0:$PORT`, and expose the configured health endpoint.
|
|
17
17
|
|
|
18
18
|
Static frontends must use TypeScript browser source, stable native type-aware
|
|
19
19
|
TypeScript checks, native linting such as `oxlint`, `biome check`, or
|
package/package.json
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { JAVASCRIPT_BACKEND_RUNTIMES, PROJECT_KINDS } from './constants.ts'
|
|
1
|
+
import { JAVASCRIPT_BACKEND_RUNTIMES, PROJECT_KINDS, RUST_STRICT_CLIPPY_DENY_LINTS } from './constants.ts'
|
|
2
2
|
import { commandTokens, hasFrontendInstallCommand, hasFrontendScriptRun, usesJavascriptLinter } from './frontend-policy.ts'
|
|
3
3
|
import { isSafeRelativePath } from './project.ts'
|
|
4
4
|
import type { ProjectKind, TovukConfig } from './types.ts'
|
|
@@ -108,11 +108,16 @@ function validateOutput(value: string | undefined, field: string): void {
|
|
|
108
108
|
}
|
|
109
109
|
|
|
110
110
|
function validateResourceConfig(config: TovukConfig): void {
|
|
111
|
-
|
|
112
|
-
|
|
111
|
+
const memoryMib = memoryToMib(config.resources.memory)
|
|
112
|
+
if (memoryMib < 128 || memoryMib > 2048) {
|
|
113
|
+
throw new Error('[resources].memory must be between 128mb and 2gb; use the smallest working value')
|
|
113
114
|
}
|
|
114
|
-
|
|
115
|
-
|
|
115
|
+
const cpuMillis = cpuToMillis(config.resources.cpu)
|
|
116
|
+
if (cpuMillis < 50 || cpuMillis > 2000) {
|
|
117
|
+
throw new Error('[resources].cpu must be between 0.05 and 2; use the smallest working value')
|
|
118
|
+
}
|
|
119
|
+
if (!Number.isInteger(config.resources.idle_timeout_minutes) || config.resources.idle_timeout_minutes < 1 || config.resources.idle_timeout_minutes > 60) {
|
|
120
|
+
throw new Error('[resources].idle_timeout_minutes must be between 1 and 60')
|
|
116
121
|
}
|
|
117
122
|
}
|
|
118
123
|
|
|
@@ -126,11 +131,18 @@ function validateCheckCommand(kind: ProjectKind, command: string): void {
|
|
|
126
131
|
}
|
|
127
132
|
|
|
128
133
|
function validateRustCheckCommand(command: string): void {
|
|
129
|
-
const required = [
|
|
134
|
+
const required = [
|
|
135
|
+
'cargo fmt --all --check',
|
|
136
|
+
'cargo check --locked --release --all-targets --all-features',
|
|
137
|
+
'cargo test --locked --release --all-targets --all-features',
|
|
138
|
+
'cargo clippy --locked --release --all-targets --all-features',
|
|
139
|
+
'-D warnings',
|
|
140
|
+
...RUST_STRICT_CLIPPY_DENY_LINTS.map((lint) => `-D ${lint}`)
|
|
141
|
+
]
|
|
130
142
|
if (required.every((fragment) => command.includes(fragment))) {
|
|
131
143
|
return
|
|
132
144
|
}
|
|
133
|
-
throw new Error('[build].check must
|
|
145
|
+
throw new Error('[build].check must run rustfmt, locked release-mode cargo check, locked release-mode tests, and strict Clippy resource lints')
|
|
134
146
|
}
|
|
135
147
|
|
|
136
148
|
function validateRustBuildCommand(command: string): void {
|
|
@@ -190,4 +202,20 @@ function commandNameFromToken(token: string): string {
|
|
|
190
202
|
return token.split('/').pop() ?? ''
|
|
191
203
|
}
|
|
192
204
|
|
|
205
|
+
function memoryToMib(value: string): number {
|
|
206
|
+
const match = value.trim().toLowerCase().match(/^(\d+)\s*(mb|mib|gb|gib)$/u)
|
|
207
|
+
if (!match) {
|
|
208
|
+
throw new Error('[resources].memory must look like 256mb, 512mb, or 1gb')
|
|
209
|
+
}
|
|
210
|
+
const amount = Number.parseInt(match[1] ?? '', 10)
|
|
211
|
+
return amount * ((match[2] ?? '').startsWith('g') ? 1024 : 1)
|
|
212
|
+
}
|
|
213
|
+
|
|
214
|
+
function cpuToMillis(value: string): number {
|
|
215
|
+
if (!/^\d+(?:\.\d{1,3})?$/u.test(value.trim())) {
|
|
216
|
+
throw new Error('[resources].cpu must look like 0.25, 0.5, 1, or 2')
|
|
217
|
+
}
|
|
218
|
+
return Math.round(Number.parseFloat(value) * 1000)
|
|
219
|
+
}
|
|
220
|
+
|
|
193
221
|
export { validateConfig }
|
|
@@ -15,7 +15,30 @@ export const SESSION_ACCOUNT: string = 'session-token'
|
|
|
15
15
|
export const SESSION_LABEL: string = 'Tovuk session'
|
|
16
16
|
export const DEFAULT_LOGIN_EXPIRES_SECONDS: number = 600
|
|
17
17
|
export const DEFAULT_LOGIN_INTERVAL_SECONDS: number = 5
|
|
18
|
-
export const
|
|
18
|
+
export const RUST_STRICT_CLIPPY_DENY_LINTS: readonly string[] = [
|
|
19
|
+
'clippy::all',
|
|
20
|
+
'clippy::pedantic',
|
|
21
|
+
'clippy::dbg_macro',
|
|
22
|
+
'clippy::todo',
|
|
23
|
+
'clippy::unimplemented',
|
|
24
|
+
'clippy::panic',
|
|
25
|
+
'clippy::unwrap_used',
|
|
26
|
+
'clippy::expect_used',
|
|
27
|
+
'clippy::large_futures',
|
|
28
|
+
'clippy::large_include_file',
|
|
29
|
+
'clippy::large_stack_frames',
|
|
30
|
+
'clippy::mem_forget',
|
|
31
|
+
'clippy::rc_buffer',
|
|
32
|
+
'clippy::rc_mutex',
|
|
33
|
+
'clippy::redundant_clone',
|
|
34
|
+
'clippy::clone_on_ref_ptr'
|
|
35
|
+
]
|
|
36
|
+
export const DEFAULT_RUST_CHECK_COMMAND: string = [
|
|
37
|
+
'cargo fmt --all --check',
|
|
38
|
+
'cargo check --locked --release --all-targets --all-features',
|
|
39
|
+
'cargo test --locked --release --all-targets --all-features',
|
|
40
|
+
`cargo clippy --locked --release --all-targets --all-features -- -D warnings ${RUST_STRICT_CLIPPY_DENY_LINTS.map((lint) => `-D ${lint}`).join(' ')}`
|
|
41
|
+
].join(' && ')
|
|
19
42
|
export const DEFAULT_NPM_FRONTEND_CHECK_COMMAND: string = 'npm ci --prefer-offline --no-audit --fund=false && npm run typecheck && npm run lint'
|
|
20
43
|
export const DEFAULT_BUN_FRONTEND_CHECK_COMMAND: string = 'bun ci && bun run typecheck && bun run lint'
|
|
21
44
|
export const PROJECT_KINDS: ReadonlySet<string> = new Set(['fullstack', 'rust_backend', 'static_frontend'])
|
|
@@ -127,7 +150,7 @@ Usage:
|
|
|
127
150
|
|
|
128
151
|
Agent contract:
|
|
129
152
|
- Fullstack apps set kind = "fullstack", keep backend and frontend roots in one tovuk.toml, serve the frontend at /, and serve the Rust API under /api.
|
|
130
|
-
- Rust backends keep Cargo.lock committed, pass rustfmt, listen on 0.0.0.0:$PORT, and return HTTP 200 from health.
|
|
153
|
+
- Rust backends keep Cargo.lock committed, pass rustfmt plus locked release-mode check/test/Clippy gates, listen on 0.0.0.0:$PORT, and return HTTP 200 from health.
|
|
131
154
|
- Static frontends set kind = "static_frontend", keep TypeScript source, a package lockfile, stable native typecheck, native lint, and Fallow quality gates.
|
|
132
155
|
- Plain static HTML/CSS/JS frontends may use kind = "static_frontend" with check = ":", command = ":", and output = ".".
|
|
133
156
|
- JavaScript and TypeScript are frontend-only on Tovuk; backend build and runtime commands must be Cargo release builds and Rust release binaries.
|
|
@@ -138,6 +161,7 @@ Agent contract:
|
|
|
138
161
|
- Create support tickets only with command output, app id, build id, deploy id, and the first actionable log line.
|
|
139
162
|
- Resolve support tickets after the issue is fixed so later agents do not duplicate work.
|
|
140
163
|
- Keep direct unsafe out of Rust source.
|
|
164
|
+
- Keep Rust backend resources small: 128mb-2gb memory, 0.05-2 CPU, and 1-60 minute idle timeout.
|
|
141
165
|
`
|
|
142
166
|
|
|
143
167
|
function packageVersion(): string {
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { spawnSync } from 'node:child_process'
|
|
2
2
|
import { readFileSync } from 'node:fs'
|
|
3
3
|
import path from 'node:path'
|
|
4
|
+
import { RUST_STRICT_CLIPPY_DENY_LINTS } from './constants.ts'
|
|
4
5
|
import { walkProjectFiles } from './project.ts'
|
|
5
6
|
import type { DoctorCheck } from './types.ts'
|
|
6
7
|
|
|
@@ -14,7 +15,7 @@ interface CargoCheckSpec {
|
|
|
14
15
|
function rustDoctorChecks(projectDir: string, configValid: boolean): DoctorCheck[] {
|
|
15
16
|
const checks = [cargoLints(projectDir), unsafeCheck(projectDir)]
|
|
16
17
|
if (configValid) {
|
|
17
|
-
checks.push(cargoFmt(projectDir), cargoCheck(projectDir), cargoClippy(projectDir))
|
|
18
|
+
checks.push(cargoFmt(projectDir), cargoCheck(projectDir), cargoTest(projectDir), cargoClippy(projectDir))
|
|
18
19
|
}
|
|
19
20
|
return checks
|
|
20
21
|
}
|
|
@@ -46,9 +47,18 @@ function scanUnsafe(projectDir: string): string[] {
|
|
|
46
47
|
function cargoCheck(projectDir: string): DoctorCheck {
|
|
47
48
|
return cargoCommandCheck(projectDir, {
|
|
48
49
|
name: 'cargo check',
|
|
49
|
-
args: ['check', '--locked', '--quiet'],
|
|
50
|
-
missing: 'Install Rust and Cargo, then run `cargo check --locked` locally before deploying.',
|
|
51
|
-
failed: 'Run `cargo check --locked`, fix every compiler error and warning, then redeploy.'
|
|
50
|
+
args: ['check', '--locked', '--release', '--all-targets', '--all-features', '--quiet'],
|
|
51
|
+
missing: 'Install Rust and Cargo, then run `cargo check --locked --release --all-targets --all-features` locally before deploying.',
|
|
52
|
+
failed: 'Run `cargo check --locked --release --all-targets --all-features`, fix every compiler error and warning, then redeploy.'
|
|
53
|
+
})
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
function cargoTest(projectDir: string): DoctorCheck {
|
|
57
|
+
return cargoCommandCheck(projectDir, {
|
|
58
|
+
name: 'cargo test',
|
|
59
|
+
args: ['test', '--locked', '--release', '--all-targets', '--all-features', '--quiet'],
|
|
60
|
+
missing: 'Install Rust and Cargo, then run `cargo test --locked --release --all-targets --all-features` locally before deploying.',
|
|
61
|
+
failed: 'Run `cargo test --locked --release --all-targets --all-features`, fix every failed test, then redeploy.'
|
|
52
62
|
})
|
|
53
63
|
}
|
|
54
64
|
|
|
@@ -64,9 +74,9 @@ function cargoFmt(projectDir: string): DoctorCheck {
|
|
|
64
74
|
function cargoClippy(projectDir: string): DoctorCheck {
|
|
65
75
|
return cargoCommandCheck(projectDir, {
|
|
66
76
|
name: 'cargo clippy',
|
|
67
|
-
args: ['clippy', '--locked', '--all-targets', '--all-features', '--quiet', '--', '-D', 'warnings'],
|
|
68
|
-
missing: 'Install Rust clippy, then run
|
|
69
|
-
failed: 'Run
|
|
77
|
+
args: ['clippy', '--locked', '--release', '--all-targets', '--all-features', '--quiet', '--', '-D', 'warnings', ...RUST_STRICT_CLIPPY_DENY_LINTS.flatMap((lint) => ['-D', lint])],
|
|
78
|
+
missing: 'Install Rust clippy, then run Tovuk strict Clippy checks before deploying.',
|
|
79
|
+
failed: 'Run the strict Tovuk Clippy command from tovuk.toml, fix every warning, panic/unwrap issue, and resource lint, then redeploy.'
|
|
70
80
|
})
|
|
71
81
|
}
|
|
72
82
|
|
|
@@ -110,17 +120,19 @@ function cargoLints(projectDir: string): DoctorCheck {
|
|
|
110
120
|
}
|
|
111
121
|
}
|
|
112
122
|
|
|
113
|
-
const
|
|
114
|
-
|
|
123
|
+
const requiredClippyLints = RUST_STRICT_CLIPPY_DENY_LINTS.map((lint) => lint.replace(/^clippy::/u, ''))
|
|
124
|
+
const ok = cargoLintLevel(source, 'rust', 'unsafe_code') === 'forbid' &&
|
|
125
|
+
cargoLintLevel(source, 'rust', 'warnings') === 'deny' &&
|
|
126
|
+
requiredClippyLints.every((lint) => cargoLintLevel(source, 'clippy', lint) === 'deny')
|
|
115
127
|
return {
|
|
116
128
|
name: 'cargo lints',
|
|
117
129
|
ok,
|
|
118
|
-
message: ok ? 'strict' : 'missing
|
|
119
|
-
agent_instruction: ok ? null : 'Add `[lints.rust]` with `unsafe_code = "forbid"` and `warnings = "deny"`, then retry.'
|
|
130
|
+
message: ok ? 'strict' : 'missing strict Rust or Clippy resource lints',
|
|
131
|
+
agent_instruction: ok ? null : 'Add `[lints.rust]` with `unsafe_code = "forbid"` and `warnings = "deny"`, plus `[lints.clippy]` deny entries for all, pedantic, panic/unwrap bans, and resource lints, then retry.'
|
|
120
132
|
}
|
|
121
133
|
}
|
|
122
134
|
|
|
123
|
-
function cargoLintLevel(source: string, lintName: string): string {
|
|
135
|
+
function cargoLintLevel(source: string, lintGroup: 'clippy' | 'rust', lintName: string): string {
|
|
124
136
|
let section = ''
|
|
125
137
|
for (const rawLine of source.split(/\r?\n/u)) {
|
|
126
138
|
const line = rawLine.replace(/#.*/u, '').trim()
|
|
@@ -129,7 +141,7 @@ function cargoLintLevel(source: string, lintName: string): string {
|
|
|
129
141
|
section = nextSection
|
|
130
142
|
continue
|
|
131
143
|
}
|
|
132
|
-
if (!
|
|
144
|
+
if (!isLintSection(section, lintGroup)) {
|
|
133
145
|
continue
|
|
134
146
|
}
|
|
135
147
|
const level = lintAssignmentLevel(line, lintName)
|
|
@@ -145,8 +157,8 @@ function tomlSection(line: string): string | null {
|
|
|
145
157
|
return line.match(/^\[([^\]]+)\]$/u)?.[1] ?? null
|
|
146
158
|
}
|
|
147
159
|
|
|
148
|
-
function
|
|
149
|
-
return section ===
|
|
160
|
+
function isLintSection(section: string, lintGroup: 'clippy' | 'rust'): boolean {
|
|
161
|
+
return section === `lints.${lintGroup}` || section === `workspace.lints.${lintGroup}`
|
|
150
162
|
}
|
|
151
163
|
|
|
152
164
|
function lintAssignmentLevel(line: string, lintName: string): string {
|
|
@@ -67,6 +67,24 @@ publish = false
|
|
|
67
67
|
[lints.rust]
|
|
68
68
|
unsafe_code = "forbid"
|
|
69
69
|
warnings = "deny"
|
|
70
|
+
|
|
71
|
+
[lints.clippy]
|
|
72
|
+
all = { level = "deny", priority = -1 }
|
|
73
|
+
pedantic = { level = "deny", priority = -1 }
|
|
74
|
+
dbg_macro = "deny"
|
|
75
|
+
todo = "deny"
|
|
76
|
+
unimplemented = "deny"
|
|
77
|
+
panic = "deny"
|
|
78
|
+
unwrap_used = "deny"
|
|
79
|
+
expect_used = "deny"
|
|
80
|
+
large_futures = "deny"
|
|
81
|
+
large_include_file = "deny"
|
|
82
|
+
large_stack_frames = "deny"
|
|
83
|
+
mem_forget = "deny"
|
|
84
|
+
rc_buffer = "deny"
|
|
85
|
+
rc_mutex = "deny"
|
|
86
|
+
redundant_clone = "deny"
|
|
87
|
+
clone_on_ref_ptr = "deny"
|
|
70
88
|
`],
|
|
71
89
|
['Cargo.lock', `# This file is automatically @generated by Cargo.
|
|
72
90
|
version = 4
|