tovuk 0.1.50 → 0.1.51
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 +5 -2
- package/bin/tovuk +3 -0
- package/install.mjs +70 -0
- package/package.json +7 -21
- package/src/internal/agent-error-enrichment.ts +0 -94
- package/src/internal/api-models.ts +0 -133
- package/src/internal/api.ts +0 -77
- package/src/internal/archive.ts +0 -35
- package/src/internal/args.ts +0 -185
- package/src/internal/auth.ts +0 -281
- package/src/internal/checks.ts +0 -12
- package/src/internal/commands.ts +0 -298
- package/src/internal/config-parser.ts +0 -269
- package/src/internal/config-validation.ts +0 -221
- package/src/internal/config.ts +0 -2
- package/src/internal/constants.ts +0 -181
- package/src/internal/deploy-plan.ts +0 -89
- package/src/internal/deploy.ts +0 -154
- package/src/internal/doctor.ts +0 -213
- package/src/internal/errors.ts +0 -46
- package/src/internal/frontend-policy.ts +0 -314
- package/src/internal/json.ts +0 -103
- package/src/internal/preview.ts +0 -178
- package/src/internal/project.ts +0 -151
- package/src/internal/rust-doctor.ts +0 -169
- package/src/internal/template-sources.ts +0 -197
- package/src/internal/templates.ts +0 -229
- package/src/internal/types.ts +0 -84
- package/src/internal/workspace.ts +0 -64
- package/src/tovuk.ts +0 -71
- package/tsconfig.json +0 -48
package/README.md
CHANGED
|
@@ -9,7 +9,9 @@ npx tovuk doctor --json
|
|
|
9
9
|
npx tovuk deploy --wait --json
|
|
10
10
|
```
|
|
11
11
|
|
|
12
|
-
|
|
12
|
+
The npm package installs the native Tovuk binary for the current platform.
|
|
13
|
+
Node is required by npm to install the package, but the `tovuk` command itself
|
|
14
|
+
does not delegate to `npx`, `tsx`, or any JavaScript runtime.
|
|
13
15
|
|
|
14
16
|
Rust backends expect `Cargo.toml`, `Cargo.lock`, and `tovuk.toml`. They must
|
|
15
17
|
pass `cargo fmt --all --check`, locked release-mode check/test/Clippy gates,
|
|
@@ -59,12 +61,13 @@ npx tovuk billing checkout --json
|
|
|
59
61
|
When Tovuk support is needed, include enough evidence for a support agent:
|
|
60
62
|
|
|
61
63
|
```sh
|
|
62
|
-
npx tovuk support create "Deploy failed" "Agent retried deploy after doctor." --app app_1 --build job_1 --deploy deploy_1 --failing-command "
|
|
64
|
+
npx tovuk support create "Deploy failed" "Agent retried deploy after doctor." --app app_1 --build job_1 --deploy deploy_1 --failing-command "tovuk deploy --wait --json" --first-log-line "cargo check failed in src/main.rs" --json
|
|
63
65
|
```
|
|
64
66
|
|
|
65
67
|
When the issue is fixed, resolve the ticket:
|
|
66
68
|
|
|
67
69
|
```sh
|
|
70
|
+
npx tovuk support list --json
|
|
68
71
|
npx tovuk support resolve ticket_0123456789abcdef0123 --json
|
|
69
72
|
```
|
|
70
73
|
|
package/bin/tovuk
ADDED
package/install.mjs
ADDED
|
@@ -0,0 +1,70 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
import { createWriteStream, chmodSync, copyFileSync, mkdirSync, readFileSync, renameSync, rmSync } from 'node:fs'
|
|
3
|
+
import { get } from 'node:https'
|
|
4
|
+
import { arch, platform, tmpdir } from 'node:os'
|
|
5
|
+
import { basename, dirname, join } from 'node:path'
|
|
6
|
+
import { fileURLToPath } from 'node:url'
|
|
7
|
+
|
|
8
|
+
const packageRoot = dirname(fileURLToPath(import.meta.url))
|
|
9
|
+
const manifest = JSON.parse(readFileSync(join(packageRoot, 'package.json'), 'utf8'))
|
|
10
|
+
const target = nativeTarget()
|
|
11
|
+
const binaryPath = join(packageRoot, 'bin', 'tovuk')
|
|
12
|
+
|
|
13
|
+
mkdirSync(dirname(binaryPath), { recursive: true })
|
|
14
|
+
|
|
15
|
+
if (process.env.TOVUK_NATIVE_BINARY) {
|
|
16
|
+
installFromLocal(process.env.TOVUK_NATIVE_BINARY)
|
|
17
|
+
} else {
|
|
18
|
+
await installFromRelease()
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
function installFromLocal(source) {
|
|
22
|
+
copyFileSync(source, binaryPath)
|
|
23
|
+
chmodSync(binaryPath, 0o755)
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
async function installFromRelease() {
|
|
27
|
+
const asset = `tovuk-${manifest.version}-${target}${target.endsWith('windows-msvc') ? '.exe' : ''}`
|
|
28
|
+
const url = `https://github.com/tovuk/tovuk/releases/download/v${manifest.version}/${asset}`
|
|
29
|
+
const tempPath = join(tmpdir(), `${basename(asset)}-${process.pid}`)
|
|
30
|
+
try {
|
|
31
|
+
await download(url, tempPath)
|
|
32
|
+
renameSync(tempPath, binaryPath)
|
|
33
|
+
chmodSync(binaryPath, 0o755)
|
|
34
|
+
} catch (error) {
|
|
35
|
+
rmSync(tempPath, { force: true })
|
|
36
|
+
throw new Error(`Could not install native Tovuk binary from ${url}: ${error instanceof Error ? error.message : String(error)}`)
|
|
37
|
+
}
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
function nativeTarget() {
|
|
41
|
+
const os = platform()
|
|
42
|
+
const cpu = arch()
|
|
43
|
+
if (os === 'darwin' && cpu === 'arm64') return 'aarch64-apple-darwin'
|
|
44
|
+
if (os === 'darwin' && cpu === 'x64') return 'x86_64-apple-darwin'
|
|
45
|
+
if (os === 'linux' && cpu === 'arm64') return 'aarch64-unknown-linux-gnu'
|
|
46
|
+
if (os === 'linux' && cpu === 'x64') return 'x86_64-unknown-linux-gnu'
|
|
47
|
+
if (os === 'win32' && cpu === 'x64') return 'x86_64-pc-windows-msvc'
|
|
48
|
+
throw new Error(`Unsupported Tovuk native target: ${os}/${cpu}`)
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
function download(url, destination) {
|
|
52
|
+
return new Promise((resolve, reject) => {
|
|
53
|
+
get(url, (response) => {
|
|
54
|
+
if (response.statusCode && response.statusCode >= 300 && response.statusCode < 400 && response.headers.location) {
|
|
55
|
+
response.resume()
|
|
56
|
+
download(response.headers.location, destination).then(resolve, reject)
|
|
57
|
+
return
|
|
58
|
+
}
|
|
59
|
+
if (response.statusCode !== 200) {
|
|
60
|
+
response.resume()
|
|
61
|
+
reject(new Error(`HTTP ${response.statusCode ?? 'unknown'}`))
|
|
62
|
+
return
|
|
63
|
+
}
|
|
64
|
+
const file = createWriteStream(destination, { mode: 0o755 })
|
|
65
|
+
response.pipe(file)
|
|
66
|
+
file.on('finish', () => file.close(resolve))
|
|
67
|
+
file.on('error', reject)
|
|
68
|
+
}).on('error', reject)
|
|
69
|
+
})
|
|
70
|
+
}
|
package/package.json
CHANGED
|
@@ -1,14 +1,14 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "tovuk",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.51",
|
|
4
4
|
"description": "Deploy Rust backends, static frontends, and fullstack apps to Tovuk.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"bin": {
|
|
7
|
-
"tovuk": "
|
|
7
|
+
"tovuk": "bin/tovuk"
|
|
8
8
|
},
|
|
9
9
|
"files": [
|
|
10
|
-
"
|
|
11
|
-
"
|
|
10
|
+
"bin",
|
|
11
|
+
"install.mjs",
|
|
12
12
|
"README.md"
|
|
13
13
|
],
|
|
14
14
|
"publishConfig": {
|
|
@@ -34,25 +34,11 @@
|
|
|
34
34
|
"url": "https://github.com/tovuk/tovuk/issues"
|
|
35
35
|
},
|
|
36
36
|
"license": "MIT",
|
|
37
|
-
"dependencies": {
|
|
38
|
-
"tsx": "^4.22.3"
|
|
39
|
-
},
|
|
40
|
-
"devDependencies": {
|
|
41
|
-
"@types/node": "^25.9.1",
|
|
42
|
-
"fallow": "^2.84.0",
|
|
43
|
-
"oxlint": "^1.67.0",
|
|
44
|
-
"oxlint-tsgolint": "^0.23.0"
|
|
45
|
-
},
|
|
46
37
|
"scripts": {
|
|
47
|
-
"
|
|
38
|
+
"postinstall": "node install.mjs",
|
|
39
|
+
"check": "npm run check:policy && npm run runtime && npm run pack:dry",
|
|
48
40
|
"check:policy": "node ../../scripts/check-npm-cli-package.mjs",
|
|
49
|
-
"
|
|
50
|
-
"lint": "oxlint src -D correctness -D suspicious -D perf -A no-await-in-loop --deny-warnings --type-aware --type-check --tsconfig tsconfig.json --promise-plugin --node-plugin --report-unused-disable-directives",
|
|
51
|
-
"lint:dead": "fallow dead-code --production --include-dupes --include-entry-exports --fail-on-issues",
|
|
52
|
-
"lint:dupes": "fallow dupes --production --mode semantic --threshold 1 --ignore-imports --fail-on-issues; dupes=$(fallow dupes --production --mode semantic --threshold 1 --ignore-imports --format json | jq '.clone_groups | length'); test \"$dupes\" = 0",
|
|
53
|
-
"lint:health": "fallow health --production --max-cyclomatic 10 --max-cognitive 15 --max-crap 20 --complexity --format json | jq -e '[.findings[] | select(.severity == \"critical\")] | length == 0' >/dev/null && fallow health --production --score --format json | jq -e '.health_score.score >= 90' >/dev/null",
|
|
54
|
-
"check:deps": "npm ls --all && npm audit --audit-level=moderate && npm audit signatures --omit=dev --json",
|
|
55
|
-
"runtime": "src/tovuk.ts --version",
|
|
41
|
+
"runtime": "node ../../scripts/check-npm-native-runtime.mjs",
|
|
56
42
|
"pack:dry": "npm pack --dry-run"
|
|
57
43
|
}
|
|
58
44
|
}
|
|
@@ -1,94 +0,0 @@
|
|
|
1
|
-
import { TovukError } from './errors.ts'
|
|
2
|
-
import { isJsonObject, parseJson, stringField } from './json.ts'
|
|
3
|
-
import type { AgentErrorPayload, CliOptions, JsonValue } from './types.ts'
|
|
4
|
-
|
|
5
|
-
interface AgentErrorContext {
|
|
6
|
-
cli: CliOptions
|
|
7
|
-
route: string
|
|
8
|
-
token: string | null
|
|
9
|
-
}
|
|
10
|
-
|
|
11
|
-
const BILLING_CHECKOUT_ROUTE = '/v1/billing/checkout'
|
|
12
|
-
|
|
13
|
-
async function enrichAgentErrorPayload(
|
|
14
|
-
context: AgentErrorContext,
|
|
15
|
-
payload: AgentErrorPayload
|
|
16
|
-
): Promise<void> {
|
|
17
|
-
if (!shouldCreateCheckoutUrl(context, payload)) {
|
|
18
|
-
return
|
|
19
|
-
}
|
|
20
|
-
|
|
21
|
-
const checkoutUrl = await createCheckoutUrl(context.cli, context.token, payload.message)
|
|
22
|
-
if (checkoutUrl) {
|
|
23
|
-
payload.checkout_url = checkoutUrl
|
|
24
|
-
}
|
|
25
|
-
}
|
|
26
|
-
|
|
27
|
-
async function paymentRequiredAgentError(
|
|
28
|
-
cli: CliOptions,
|
|
29
|
-
token: string,
|
|
30
|
-
message: string,
|
|
31
|
-
agentInstruction: string
|
|
32
|
-
): Promise<TovukError> {
|
|
33
|
-
const payload: AgentErrorPayload = {
|
|
34
|
-
code: 'payment_required',
|
|
35
|
-
message,
|
|
36
|
-
agent_instruction: agentInstruction,
|
|
37
|
-
docs_url: null,
|
|
38
|
-
checkout_url: null
|
|
39
|
-
}
|
|
40
|
-
await enrichAgentErrorPayload({ cli, route: 'local:preflight', token }, payload)
|
|
41
|
-
return new TovukError(payload, cli.json, 1)
|
|
42
|
-
}
|
|
43
|
-
|
|
44
|
-
function shouldCreateCheckoutUrl(
|
|
45
|
-
context: AgentErrorContext,
|
|
46
|
-
payload: AgentErrorPayload
|
|
47
|
-
): boolean {
|
|
48
|
-
return payload.code === 'payment_required'
|
|
49
|
-
&& !payload.checkout_url
|
|
50
|
-
&& Boolean(context.token)
|
|
51
|
-
&& context.route !== BILLING_CHECKOUT_ROUTE
|
|
52
|
-
}
|
|
53
|
-
|
|
54
|
-
async function createCheckoutUrl(
|
|
55
|
-
cli: CliOptions,
|
|
56
|
-
token: string | null,
|
|
57
|
-
reason: string
|
|
58
|
-
): Promise<string> {
|
|
59
|
-
if (!token) {
|
|
60
|
-
return ''
|
|
61
|
-
}
|
|
62
|
-
|
|
63
|
-
try {
|
|
64
|
-
const response = await fetch(`${cli.apiUrl}${BILLING_CHECKOUT_ROUTE}`, {
|
|
65
|
-
body: JSON.stringify({
|
|
66
|
-
reason: reason || 'Plan limit reached.',
|
|
67
|
-
target_plan: 'pro'
|
|
68
|
-
}),
|
|
69
|
-
headers: new Headers({
|
|
70
|
-
accept: 'application/json',
|
|
71
|
-
authorization: `Bearer ${token}`,
|
|
72
|
-
'content-type': 'application/json'
|
|
73
|
-
}),
|
|
74
|
-
method: 'POST'
|
|
75
|
-
})
|
|
76
|
-
if (!response.ok) {
|
|
77
|
-
return ''
|
|
78
|
-
}
|
|
79
|
-
return checkoutUrlFromJson(parseJson(await response.text()))
|
|
80
|
-
} catch {
|
|
81
|
-
return ''
|
|
82
|
-
}
|
|
83
|
-
}
|
|
84
|
-
|
|
85
|
-
function checkoutUrlFromJson(value: JsonValue | null): string {
|
|
86
|
-
if (!isJsonObject(value)) {
|
|
87
|
-
return ''
|
|
88
|
-
}
|
|
89
|
-
const checkoutValue = value['checkout'] ?? null
|
|
90
|
-
const checkout = isJsonObject(checkoutValue) ? checkoutValue : {}
|
|
91
|
-
return stringField(checkout, 'url')
|
|
92
|
-
}
|
|
93
|
-
|
|
94
|
-
export { enrichAgentErrorPayload, paymentRequiredAgentError }
|
|
@@ -1,133 +0,0 @@
|
|
|
1
|
-
import {
|
|
2
|
-
isJsonObject,
|
|
3
|
-
jsonArrayField,
|
|
4
|
-
jsonObjectField,
|
|
5
|
-
jsonObjectOrEmpty,
|
|
6
|
-
numberField,
|
|
7
|
-
optionalJsonObjectField,
|
|
8
|
-
stringField
|
|
9
|
-
} from './json.ts'
|
|
10
|
-
import type {
|
|
11
|
-
AppsResponse,
|
|
12
|
-
AppSummary,
|
|
13
|
-
BuildRecord,
|
|
14
|
-
BuildStatusResponse,
|
|
15
|
-
CheckoutResponse,
|
|
16
|
-
DeployResponse,
|
|
17
|
-
JsonObject,
|
|
18
|
-
JsonValue,
|
|
19
|
-
LogLine,
|
|
20
|
-
LogsResponse
|
|
21
|
-
} from './types.ts'
|
|
22
|
-
|
|
23
|
-
function appsResponseFromJson(value: JsonValue | null): AppsResponse {
|
|
24
|
-
return {
|
|
25
|
-
apps: jsonArrayField(jsonObjectOrEmpty(value), 'apps')
|
|
26
|
-
.map(appSummaryFromJson)
|
|
27
|
-
.filter((app): app is AppSummary => app !== null)
|
|
28
|
-
}
|
|
29
|
-
}
|
|
30
|
-
|
|
31
|
-
function appSummaryFromJson(value: JsonValue): AppSummary | null {
|
|
32
|
-
if (!isJsonObject(value)) {
|
|
33
|
-
return null
|
|
34
|
-
}
|
|
35
|
-
const app: AppSummary = {}
|
|
36
|
-
const id = stringField(value, 'id')
|
|
37
|
-
const name = stringField(value, 'name')
|
|
38
|
-
const url = stringField(value, 'url')
|
|
39
|
-
const databaseStorageMib = numberField(value, 'databaseStorageMib')
|
|
40
|
-
if (id) {
|
|
41
|
-
app.id = id
|
|
42
|
-
}
|
|
43
|
-
if (name) {
|
|
44
|
-
app.name = name
|
|
45
|
-
}
|
|
46
|
-
if (url) {
|
|
47
|
-
app.url = url
|
|
48
|
-
}
|
|
49
|
-
if (databaseStorageMib > 0) {
|
|
50
|
-
app.databaseStorageMib = databaseStorageMib
|
|
51
|
-
}
|
|
52
|
-
return app
|
|
53
|
-
}
|
|
54
|
-
|
|
55
|
-
function deployResponseFromJson(value: JsonValue | null): DeployResponse {
|
|
56
|
-
const source = jsonObjectOrEmpty(value)
|
|
57
|
-
const finalBuild = optionalJsonObjectField(source, 'final_build')
|
|
58
|
-
const response: DeployResponse = {
|
|
59
|
-
app: appDeployTargetFromJson(jsonObjectField(source, 'app')),
|
|
60
|
-
build_job: {
|
|
61
|
-
id: stringField(jsonObjectField(source, 'build_job'), 'id')
|
|
62
|
-
}
|
|
63
|
-
}
|
|
64
|
-
if (finalBuild) {
|
|
65
|
-
response.final_build = buildRecordFromJson(finalBuild)
|
|
66
|
-
}
|
|
67
|
-
return response
|
|
68
|
-
}
|
|
69
|
-
|
|
70
|
-
function appDeployTargetFromJson(source: JsonObject): DeployResponse['app'] {
|
|
71
|
-
return {
|
|
72
|
-
id: idFromJson(source),
|
|
73
|
-
url: stringField(source, 'url')
|
|
74
|
-
}
|
|
75
|
-
}
|
|
76
|
-
|
|
77
|
-
function buildStatusResponseFromJson(value: JsonValue | null): BuildStatusResponse {
|
|
78
|
-
const source = jsonObjectOrEmpty(value)
|
|
79
|
-
const build = optionalJsonObjectField(source, 'build')
|
|
80
|
-
return build ? { build: buildRecordFromJson(build) } : {}
|
|
81
|
-
}
|
|
82
|
-
|
|
83
|
-
function buildRecordFromJson(source: JsonObject): BuildRecord {
|
|
84
|
-
return {
|
|
85
|
-
id: idFromJson(source),
|
|
86
|
-
status: stringField(source, 'status')
|
|
87
|
-
}
|
|
88
|
-
}
|
|
89
|
-
|
|
90
|
-
function idFromJson(source: JsonObject): string {
|
|
91
|
-
return stringField(source, 'id')
|
|
92
|
-
}
|
|
93
|
-
|
|
94
|
-
function logsResponseFromJson(value: JsonValue | null): LogsResponse {
|
|
95
|
-
const source = jsonObjectOrEmpty(value)
|
|
96
|
-
const lines = jsonArrayField(source, 'lines')
|
|
97
|
-
.map(logLineFromJson)
|
|
98
|
-
.filter((line): line is LogLine => line !== null)
|
|
99
|
-
return {
|
|
100
|
-
lines,
|
|
101
|
-
has_more: source['has_more'] === true,
|
|
102
|
-
next_cursor: stringField(source, 'next_cursor')
|
|
103
|
-
}
|
|
104
|
-
}
|
|
105
|
-
|
|
106
|
-
function logLineFromJson(value: JsonValue): LogLine | null {
|
|
107
|
-
if (!isJsonObject(value)) {
|
|
108
|
-
return null
|
|
109
|
-
}
|
|
110
|
-
const timestamp = stringField(value, 'timestamp')
|
|
111
|
-
const stream = stringField(value, 'stream')
|
|
112
|
-
const message = stringField(value, 'message')
|
|
113
|
-
return timestamp && stream && message ? { timestamp, stream, message } : null
|
|
114
|
-
}
|
|
115
|
-
|
|
116
|
-
function checkoutResponseFromJson(value: JsonValue | null): CheckoutResponse {
|
|
117
|
-
const source = jsonObjectOrEmpty(value)
|
|
118
|
-
const checkout = jsonObjectField(source, 'checkout')
|
|
119
|
-
return {
|
|
120
|
-
checkout: {
|
|
121
|
-
reason: stringField(checkout, 'reason'),
|
|
122
|
-
url: stringField(checkout, 'url')
|
|
123
|
-
}
|
|
124
|
-
}
|
|
125
|
-
}
|
|
126
|
-
|
|
127
|
-
export {
|
|
128
|
-
appsResponseFromJson,
|
|
129
|
-
buildStatusResponseFromJson,
|
|
130
|
-
checkoutResponseFromJson,
|
|
131
|
-
deployResponseFromJson,
|
|
132
|
-
logsResponseFromJson
|
|
133
|
-
}
|
package/src/internal/api.ts
DELETED
|
@@ -1,77 +0,0 @@
|
|
|
1
|
-
import { TovukError, agentError } from './errors.ts'
|
|
2
|
-
import { enrichAgentErrorPayload } from './agent-error-enrichment.ts'
|
|
3
|
-
import { isJsonObject, parseJson } from './json.ts'
|
|
4
|
-
import type { AgentErrorPayload, ApiMethod, CliOptions, JsonValue } from './types.ts'
|
|
5
|
-
|
|
6
|
-
function requireApp(cli: CliOptions): string {
|
|
7
|
-
if (!cli.app) {
|
|
8
|
-
throw agentError('missing_app', 'App is required.', 'Pass `--app <app>` using either the app name from tovuk.toml or the app id printed by deploy.', cli.json)
|
|
9
|
-
}
|
|
10
|
-
return cli.app
|
|
11
|
-
}
|
|
12
|
-
|
|
13
|
-
function pageQuery(cli: CliOptions): string {
|
|
14
|
-
const params = new URLSearchParams()
|
|
15
|
-
if (cli.limit) {
|
|
16
|
-
params.set('limit', cli.limit)
|
|
17
|
-
}
|
|
18
|
-
if (cli.cursor) {
|
|
19
|
-
params.set('cursor', cli.cursor)
|
|
20
|
-
}
|
|
21
|
-
const value = params.toString()
|
|
22
|
-
return value ? `?${value}` : ''
|
|
23
|
-
}
|
|
24
|
-
|
|
25
|
-
async function apiRequest(
|
|
26
|
-
cli: CliOptions,
|
|
27
|
-
method: ApiMethod,
|
|
28
|
-
route: string,
|
|
29
|
-
token: string | null,
|
|
30
|
-
body: JsonValue | null
|
|
31
|
-
): Promise<JsonValue | null> {
|
|
32
|
-
const headers = new Headers({ accept: 'application/json' })
|
|
33
|
-
if (token) {
|
|
34
|
-
headers.set('authorization', `Bearer ${token}`)
|
|
35
|
-
}
|
|
36
|
-
if (body !== null) {
|
|
37
|
-
headers.set('content-type', 'application/json')
|
|
38
|
-
}
|
|
39
|
-
|
|
40
|
-
const init: RequestInit = {
|
|
41
|
-
method,
|
|
42
|
-
headers
|
|
43
|
-
}
|
|
44
|
-
if (body !== null) {
|
|
45
|
-
init.body = JSON.stringify(body)
|
|
46
|
-
}
|
|
47
|
-
const response = await fetch(`${cli.apiUrl}${route}`, init)
|
|
48
|
-
|
|
49
|
-
const text = await response.text()
|
|
50
|
-
const data = parseJson(text)
|
|
51
|
-
if (!response.ok) {
|
|
52
|
-
const payload: AgentErrorPayload = isAgentErrorPayload(data) ? data : {
|
|
53
|
-
code: 'api_error',
|
|
54
|
-
message: `Tovuk API returned HTTP ${response.status}.`,
|
|
55
|
-
agent_instruction: 'Retry the command. If it keeps failing, check Tovuk status before changing your project.',
|
|
56
|
-
docs_url: null,
|
|
57
|
-
checkout_url: null
|
|
58
|
-
}
|
|
59
|
-
await enrichAgentErrorPayload({ cli, route, token }, payload)
|
|
60
|
-
throw new TovukError(payload, cli.json, response.status >= 500 ? 2 : 1)
|
|
61
|
-
}
|
|
62
|
-
|
|
63
|
-
return data
|
|
64
|
-
}
|
|
65
|
-
|
|
66
|
-
function isAgentErrorPayload(value: JsonValue | null): value is AgentErrorPayload {
|
|
67
|
-
return isJsonObject(value)
|
|
68
|
-
&& typeof value['code'] === 'string'
|
|
69
|
-
&& typeof value['message'] === 'string'
|
|
70
|
-
&& (typeof value['agent_instruction'] === 'string' || value['agent_instruction'] === null)
|
|
71
|
-
}
|
|
72
|
-
|
|
73
|
-
export {
|
|
74
|
-
requireApp,
|
|
75
|
-
pageQuery,
|
|
76
|
-
apiRequest
|
|
77
|
-
}
|
package/src/internal/archive.ts
DELETED
|
@@ -1,35 +0,0 @@
|
|
|
1
|
-
import { spawnSync } from 'node:child_process'
|
|
2
|
-
import { ARCHIVE_EXCLUDES, ARCHIVE_LIMIT_BYTES } from './constants.ts'
|
|
3
|
-
import { agentError } from './errors.ts'
|
|
4
|
-
|
|
5
|
-
function createArchiveBase64(projectDir: string): string {
|
|
6
|
-
const excludeArgs = ARCHIVE_EXCLUDES.map((pattern) => `--exclude=${pattern}`)
|
|
7
|
-
const tar = spawnSync('tar', [...excludeArgs, '-czf', '-', '-C', projectDir, '.'], {
|
|
8
|
-
encoding: 'buffer',
|
|
9
|
-
env: { ...process.env, COPYFILE_DISABLE: '1' },
|
|
10
|
-
maxBuffer: ARCHIVE_LIMIT_BYTES + 1024 * 1024
|
|
11
|
-
})
|
|
12
|
-
|
|
13
|
-
if (tar.error) {
|
|
14
|
-
throw agentError('archive_failed', 'Could not create source archive.', 'Install `tar`, remove local build outputs, then retry `npx tovuk deploy`.', false)
|
|
15
|
-
}
|
|
16
|
-
if (tar.status !== 0) {
|
|
17
|
-
throw agentError('archive_failed', 'Could not create source archive.', String(tar.stderr || 'Check project files and retry.'), false)
|
|
18
|
-
}
|
|
19
|
-
if (tar.stdout.length > ARCHIVE_LIMIT_BYTES) {
|
|
20
|
-
throw agentError('archive_too_large', 'Source archive is too large.', 'Remove build outputs, target directories, logs, and local caches before deploying.', false)
|
|
21
|
-
}
|
|
22
|
-
|
|
23
|
-
return tar.stdout.toString('base64')
|
|
24
|
-
}
|
|
25
|
-
|
|
26
|
-
function gitCommitSha(projectDir: string): string | null {
|
|
27
|
-
const git = spawnSync('git', ['rev-parse', 'HEAD'], {
|
|
28
|
-
cwd: projectDir,
|
|
29
|
-
encoding: 'utf8',
|
|
30
|
-
stdio: ['ignore', 'pipe', 'ignore']
|
|
31
|
-
})
|
|
32
|
-
return git.status === 0 ? git.stdout.trim() || null : null
|
|
33
|
-
}
|
|
34
|
-
|
|
35
|
-
export { createArchiveBase64, gitCommitSha }
|
package/src/internal/args.ts
DELETED
|
@@ -1,185 +0,0 @@
|
|
|
1
|
-
import path from 'node:path'
|
|
2
|
-
import { DEFAULT_API_URL, DEFAULT_DEPLOY_WAIT_TIMEOUT_SECONDS } from './constants.ts'
|
|
3
|
-
import { agentError } from './errors.ts'
|
|
4
|
-
import type { CliOptions } from './types.ts'
|
|
5
|
-
|
|
6
|
-
type BooleanCliField = 'database' | 'help' | 'json' | 'version' | 'wait'
|
|
7
|
-
type StringCliField = 'apiUrl' | 'app' | 'build' | 'cursor' | 'deploy' | 'failingCommand' | 'firstLogLine' | 'limit' | 'severity' | 'template' | 'token'
|
|
8
|
-
type NumberCliField = 'port' | 'waitTimeoutSeconds'
|
|
9
|
-
type FlagValueKind = 'none' | 'string' | 'positiveInteger'
|
|
10
|
-
type FlagSetter = (cli: CliOptions, value: string | null, name: string) => void
|
|
11
|
-
|
|
12
|
-
interface FlagSpec {
|
|
13
|
-
valueKind: FlagValueKind
|
|
14
|
-
set: FlagSetter
|
|
15
|
-
}
|
|
16
|
-
|
|
17
|
-
interface ParsedFlag {
|
|
18
|
-
name: string
|
|
19
|
-
inlineValue: string | null
|
|
20
|
-
}
|
|
21
|
-
|
|
22
|
-
const FLAG_SPECS = new Map<string, FlagSpec>([
|
|
23
|
-
['--help', booleanOption('help', true)],
|
|
24
|
-
['-h', booleanOption('help', true)],
|
|
25
|
-
['--version', booleanOption('version', true)],
|
|
26
|
-
['-v', booleanOption('version', true)],
|
|
27
|
-
['-V', booleanOption('version', true)],
|
|
28
|
-
['--json', booleanOption('json', true)],
|
|
29
|
-
['--database', booleanOption('database', true)],
|
|
30
|
-
['--no-database', booleanOption('database', false)],
|
|
31
|
-
['--wait', booleanOption('wait', true)],
|
|
32
|
-
['--wait-timeout', positiveIntegerOption('waitTimeoutSeconds')],
|
|
33
|
-
['--api', stringOption('apiUrl')],
|
|
34
|
-
['--app', stringOption('app')],
|
|
35
|
-
['--build', stringOption('build')],
|
|
36
|
-
['--deploy', stringOption('deploy')],
|
|
37
|
-
['--failing-command', stringOption('failingCommand')],
|
|
38
|
-
['--first-log-line', stringOption('firstLogLine')],
|
|
39
|
-
['--limit', stringOption('limit')],
|
|
40
|
-
['--cursor', stringOption('cursor')],
|
|
41
|
-
['--severity', stringOption('severity')],
|
|
42
|
-
['--token', stringOption('token')],
|
|
43
|
-
['--template', stringOption('template')],
|
|
44
|
-
['--port', positiveIntegerOption('port')]
|
|
45
|
-
])
|
|
46
|
-
|
|
47
|
-
function parseArgs(argv: string[]): CliOptions {
|
|
48
|
-
const cli: CliOptions = {
|
|
49
|
-
command: 'help',
|
|
50
|
-
args: [],
|
|
51
|
-
apiUrl: DEFAULT_API_URL,
|
|
52
|
-
app: '',
|
|
53
|
-
build: '',
|
|
54
|
-
deploy: '',
|
|
55
|
-
limit: '',
|
|
56
|
-
cursor: '',
|
|
57
|
-
failingCommand: '',
|
|
58
|
-
firstLogLine: '',
|
|
59
|
-
token: '',
|
|
60
|
-
template: '',
|
|
61
|
-
severity: '',
|
|
62
|
-
port: 0,
|
|
63
|
-
waitTimeoutSeconds: DEFAULT_DEPLOY_WAIT_TIMEOUT_SECONDS,
|
|
64
|
-
json: false,
|
|
65
|
-
database: false,
|
|
66
|
-
wait: false,
|
|
67
|
-
help: false,
|
|
68
|
-
version: false
|
|
69
|
-
}
|
|
70
|
-
|
|
71
|
-
const positional: string[] = []
|
|
72
|
-
for (let index = 0; index < argv.length; index += 1) {
|
|
73
|
-
const arg = argv[index] ?? ''
|
|
74
|
-
if (arg === '--') {
|
|
75
|
-
positional.push(...argv.slice(index + 1))
|
|
76
|
-
break
|
|
77
|
-
}
|
|
78
|
-
const parsedFlag = parseFlag(arg)
|
|
79
|
-
const spec = FLAG_SPECS.get(parsedFlag.name)
|
|
80
|
-
if (spec) {
|
|
81
|
-
index = applyFlag(cli, spec, parsedFlag, argv, index)
|
|
82
|
-
} else if (arg.startsWith('-')) {
|
|
83
|
-
throw unknownFlag(cli, arg)
|
|
84
|
-
} else {
|
|
85
|
-
positional.push(arg)
|
|
86
|
-
}
|
|
87
|
-
}
|
|
88
|
-
|
|
89
|
-
if (positional.length > 0) {
|
|
90
|
-
cli.command = positional[0] ?? 'help'
|
|
91
|
-
cli.args = positional.slice(1)
|
|
92
|
-
}
|
|
93
|
-
|
|
94
|
-
cli.apiUrl = trimTrailingSlash(cli.apiUrl)
|
|
95
|
-
return cli
|
|
96
|
-
}
|
|
97
|
-
|
|
98
|
-
function booleanOption(field: BooleanCliField, value: boolean): FlagSpec {
|
|
99
|
-
return {
|
|
100
|
-
valueKind: 'none',
|
|
101
|
-
set: (cli): void => {
|
|
102
|
-
cli[field] = value
|
|
103
|
-
}
|
|
104
|
-
}
|
|
105
|
-
}
|
|
106
|
-
|
|
107
|
-
function stringOption(field: StringCliField): FlagSpec {
|
|
108
|
-
return {
|
|
109
|
-
valueKind: 'string',
|
|
110
|
-
set: (cli, value): void => {
|
|
111
|
-
cli[field] = value ?? ''
|
|
112
|
-
}
|
|
113
|
-
}
|
|
114
|
-
}
|
|
115
|
-
|
|
116
|
-
function positiveIntegerOption(field: NumberCliField): FlagSpec {
|
|
117
|
-
return {
|
|
118
|
-
valueKind: 'positiveInteger',
|
|
119
|
-
set: (cli, value, name): void => {
|
|
120
|
-
cli[field] = parsePositiveInteger(value ?? '', name)
|
|
121
|
-
}
|
|
122
|
-
}
|
|
123
|
-
}
|
|
124
|
-
|
|
125
|
-
function applyFlag(cli: CliOptions, spec: FlagSpec, flag: ParsedFlag, argv: readonly string[], index: number): number {
|
|
126
|
-
if (spec.valueKind === 'none') {
|
|
127
|
-
if (flag.inlineValue !== null) {
|
|
128
|
-
throw agentError('invalid_argument', `${flag.name} does not accept a value.`, `Use ${flag.name} without =value.`, cli.json)
|
|
129
|
-
}
|
|
130
|
-
spec.set(cli, null, flag.name)
|
|
131
|
-
return index
|
|
132
|
-
}
|
|
133
|
-
|
|
134
|
-
const value = flag.inlineValue ?? requireValue(argv, index, flag.name)
|
|
135
|
-
if (value === '') {
|
|
136
|
-
throw agentError('missing_argument', `${flag.name} requires a value.`, `Pass a value after ${flag.name}.`, cli.json)
|
|
137
|
-
}
|
|
138
|
-
spec.set(cli, value, flag.name)
|
|
139
|
-
return flag.inlineValue === null ? index + 1 : index
|
|
140
|
-
}
|
|
141
|
-
|
|
142
|
-
function parseFlag(arg: string): ParsedFlag {
|
|
143
|
-
if (!arg.startsWith('--')) {
|
|
144
|
-
return { name: arg, inlineValue: null }
|
|
145
|
-
}
|
|
146
|
-
const separator = arg.indexOf('=')
|
|
147
|
-
return separator > 2
|
|
148
|
-
? { name: arg.slice(0, separator), inlineValue: arg.slice(separator + 1) }
|
|
149
|
-
: { name: arg, inlineValue: null }
|
|
150
|
-
}
|
|
151
|
-
|
|
152
|
-
function parsePositiveInteger(value: string, name: string): number {
|
|
153
|
-
const parsed = Number.parseInt(value, 10)
|
|
154
|
-
if (!Number.isInteger(parsed) || parsed <= 0) {
|
|
155
|
-
throw agentError('invalid_argument', `${name} must be a positive integer.`, `Pass ${name} as seconds, for example ${name} 900.`, false)
|
|
156
|
-
}
|
|
157
|
-
return parsed
|
|
158
|
-
}
|
|
159
|
-
|
|
160
|
-
function requireValue(argv: readonly string[], index: number, name: string): string {
|
|
161
|
-
const value = argv[index + 1]
|
|
162
|
-
if (!value || value.startsWith('--')) {
|
|
163
|
-
throw agentError('missing_argument', `${name} requires a value.`, `Pass a value after ${name}.`, false)
|
|
164
|
-
}
|
|
165
|
-
return value
|
|
166
|
-
}
|
|
167
|
-
|
|
168
|
-
function unknownFlag(cli: CliOptions, value: string): never {
|
|
169
|
-
throw agentError(
|
|
170
|
-
'unknown_argument',
|
|
171
|
-
`Unknown Tovuk option: ${value}.`,
|
|
172
|
-
'Run `npx tovuk --help`, remove or correct the unsupported option, then retry.',
|
|
173
|
-
cli.json
|
|
174
|
-
)
|
|
175
|
-
}
|
|
176
|
-
|
|
177
|
-
function projectPath(value: string | undefined): string {
|
|
178
|
-
return path.resolve(value || process.cwd())
|
|
179
|
-
}
|
|
180
|
-
|
|
181
|
-
function trimTrailingSlash(value: string): string {
|
|
182
|
-
return value.replace(/\/+$/u, '')
|
|
183
|
-
}
|
|
184
|
-
|
|
185
|
-
export { parseArgs, projectPath }
|