tovuk 0.1.49 → 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 +7 -4
- 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 -193
- package/src/internal/config.ts +0 -2
- package/src/internal/constants.ts +0 -157
- 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 -157
- package/src/internal/template-sources.ts +0 -197
- package/src/internal/templates.ts +0 -211
- 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/src/internal/auth.ts
DELETED
|
@@ -1,281 +0,0 @@
|
|
|
1
|
-
import { spawnSync } from 'node:child_process'
|
|
2
|
-
import { chmodSync, existsSync, mkdirSync, readFileSync, writeFileSync } from 'node:fs'
|
|
3
|
-
import { homedir } from 'node:os'
|
|
4
|
-
import path from 'node:path'
|
|
5
|
-
import { DEFAULT_LOGIN_EXPIRES_SECONDS, DEFAULT_LOGIN_INTERVAL_SECONDS, SESSION_ACCOUNT, SESSION_DIR, SESSION_FILE, SESSION_LABEL, SESSION_SERVICE } from './constants.ts'
|
|
6
|
-
import { agentError } from './errors.ts'
|
|
7
|
-
import { apiRequest } from './api.ts'
|
|
8
|
-
import { jsonObjectOrEmpty, numberField, stringField } from './json.ts'
|
|
9
|
-
import { hasCommand, openUrl, progress, sleep } from './project.ts'
|
|
10
|
-
import type { CliOptions, JsonObject, LoginPollResponse, LoginStartResponse } from './types.ts'
|
|
11
|
-
|
|
12
|
-
type AliasSpec<Key extends string> = Readonly<{
|
|
13
|
-
field: Key
|
|
14
|
-
aliases: readonly string[]
|
|
15
|
-
}>
|
|
16
|
-
|
|
17
|
-
const LOGIN_START_STRING_FIELDS = [
|
|
18
|
-
{ field: 'deviceCode', aliases: ['deviceCode', 'device_code'] },
|
|
19
|
-
{ field: 'loginUrl', aliases: ['loginUrl', 'login_url'] },
|
|
20
|
-
{ field: 'userCode', aliases: ['userCode', 'user_code'] }
|
|
21
|
-
] as const
|
|
22
|
-
|
|
23
|
-
const LOGIN_START_NUMBER_FIELDS = [
|
|
24
|
-
{ field: 'expiresInSeconds', aliases: ['expiresInSeconds', 'expires_in_seconds'] },
|
|
25
|
-
{ field: 'intervalSeconds', aliases: ['intervalSeconds', 'interval_seconds'] }
|
|
26
|
-
] as const
|
|
27
|
-
|
|
28
|
-
const LOGIN_POLL_STRING_FIELDS = [
|
|
29
|
-
{ field: 'email', aliases: ['email'] },
|
|
30
|
-
{ field: 'status', aliases: ['status'] },
|
|
31
|
-
{ field: 'token', aliases: ['token'] }
|
|
32
|
-
] as const
|
|
33
|
-
|
|
34
|
-
const LOGIN_POLL_NUMBER_FIELDS = [
|
|
35
|
-
{ field: 'intervalSeconds', aliases: ['intervalSeconds', 'interval_seconds'] }
|
|
36
|
-
] as const
|
|
37
|
-
|
|
38
|
-
async function login(cli: CliOptions): Promise<void> {
|
|
39
|
-
if (cli.token) {
|
|
40
|
-
writeSessionToken(cli.token)
|
|
41
|
-
console.log('saved Tovuk session token')
|
|
42
|
-
return
|
|
43
|
-
}
|
|
44
|
-
|
|
45
|
-
await loginAndStore(cli)
|
|
46
|
-
}
|
|
47
|
-
|
|
48
|
-
async function readOrLoginToken(cli: CliOptions): Promise<string> {
|
|
49
|
-
const token = readStoredToken(cli)
|
|
50
|
-
if (token) {
|
|
51
|
-
return token
|
|
52
|
-
}
|
|
53
|
-
|
|
54
|
-
return loginAndStore(cli)
|
|
55
|
-
}
|
|
56
|
-
|
|
57
|
-
async function loginAndStore(cli: CliOptions): Promise<string> {
|
|
58
|
-
const start = loginStartResponse(await apiRequest(cli, 'POST', '/v1/login/device', null, null))
|
|
59
|
-
if (!start.loginUrl) {
|
|
60
|
-
throw agentError('login_failed', 'Tovuk login did not return a browser URL.', 'Retry `npx tovuk login`. If it keeps failing, check Tovuk status.', cli.json)
|
|
61
|
-
}
|
|
62
|
-
openUrl(start.loginUrl)
|
|
63
|
-
progress(cli, 'opened browser login')
|
|
64
|
-
progress(cli, `waiting for browser login code ${start.userCode ?? 'TOVUK'}`)
|
|
65
|
-
|
|
66
|
-
const session = await pollLogin(cli, start)
|
|
67
|
-
if (!session.token) {
|
|
68
|
-
throw agentError('login_failed', 'Tovuk login did not return a session token.', 'Run `npx tovuk login` again and complete the browser login.', cli.json)
|
|
69
|
-
}
|
|
70
|
-
|
|
71
|
-
writeSessionToken(session.token)
|
|
72
|
-
progress(cli, `logged in as ${session.email ?? 'Tovuk user'}`)
|
|
73
|
-
return session.token
|
|
74
|
-
}
|
|
75
|
-
|
|
76
|
-
async function pollLogin(cli: CliOptions, start: LoginStartResponse): Promise<LoginPollResponse> {
|
|
77
|
-
if (!start.deviceCode) {
|
|
78
|
-
throw agentError('login_failed', 'Tovuk login did not return a device code.', 'Retry `npx tovuk login`. If it keeps failing, check Tovuk status.', cli.json)
|
|
79
|
-
}
|
|
80
|
-
|
|
81
|
-
const expiresMs = (start.expiresInSeconds ?? DEFAULT_LOGIN_EXPIRES_SECONDS) * 1000
|
|
82
|
-
const deadline = Date.now() + expiresMs
|
|
83
|
-
let intervalMs = (start.intervalSeconds ?? DEFAULT_LOGIN_INTERVAL_SECONDS) * 1000
|
|
84
|
-
|
|
85
|
-
while (Date.now() < deadline) {
|
|
86
|
-
await sleep(intervalMs)
|
|
87
|
-
const response = loginPollResponse(await apiRequest(cli, 'GET', `/v1/login/device/${encodeURIComponent(start.deviceCode)}`, null, null))
|
|
88
|
-
if (response.status === 'complete') {
|
|
89
|
-
return response
|
|
90
|
-
}
|
|
91
|
-
if (response.status === 'expired') {
|
|
92
|
-
throw agentError('login_expired', 'Tovuk login expired before it completed.', 'Run `npx tovuk login` again and finish the browser login in the newly opened tab.', cli.json)
|
|
93
|
-
}
|
|
94
|
-
intervalMs = Math.max(
|
|
95
|
-
DEFAULT_LOGIN_INTERVAL_SECONDS * 1000,
|
|
96
|
-
(response.intervalSeconds ?? DEFAULT_LOGIN_INTERVAL_SECONDS) * 1000
|
|
97
|
-
)
|
|
98
|
-
}
|
|
99
|
-
|
|
100
|
-
throw agentError('login_expired', 'Tovuk login expired before it completed.', 'Run `npx tovuk login` again and finish the browser login in the newly opened tab.', cli.json)
|
|
101
|
-
}
|
|
102
|
-
|
|
103
|
-
function readStoredToken(cli: CliOptions): string {
|
|
104
|
-
if (cli.token) {
|
|
105
|
-
return cli.token
|
|
106
|
-
}
|
|
107
|
-
if (process.env['TOVUK_TOKEN']) {
|
|
108
|
-
return process.env['TOVUK_TOKEN']
|
|
109
|
-
}
|
|
110
|
-
|
|
111
|
-
const keychainToken = readKeychainToken()
|
|
112
|
-
if (keychainToken) {
|
|
113
|
-
return keychainToken
|
|
114
|
-
}
|
|
115
|
-
|
|
116
|
-
const userToken = readTokenFile(userSessionPath())
|
|
117
|
-
if (userToken) {
|
|
118
|
-
return userToken
|
|
119
|
-
}
|
|
120
|
-
|
|
121
|
-
const homeToken = path.join(homedir(), SESSION_DIR, SESSION_FILE)
|
|
122
|
-
return readTokenFile(homeToken)
|
|
123
|
-
}
|
|
124
|
-
|
|
125
|
-
function writeSessionToken(token: string): void {
|
|
126
|
-
const cleanToken = token.trim()
|
|
127
|
-
if (!cleanToken) {
|
|
128
|
-
throw agentError('login_failed', 'Tovuk session token is empty.', 'Run `npx tovuk login` again and complete the browser login.', false)
|
|
129
|
-
}
|
|
130
|
-
if (writeKeychainToken(cleanToken)) {
|
|
131
|
-
return
|
|
132
|
-
}
|
|
133
|
-
|
|
134
|
-
writeTokenFile(userSessionPath(), cleanToken)
|
|
135
|
-
}
|
|
136
|
-
|
|
137
|
-
function readTokenFile(filePath: string): string {
|
|
138
|
-
if (!existsSync(filePath)) {
|
|
139
|
-
return ''
|
|
140
|
-
}
|
|
141
|
-
return readFileSync(filePath, 'utf8').trim()
|
|
142
|
-
}
|
|
143
|
-
|
|
144
|
-
function writeTokenFile(filePath: string, token: string): void {
|
|
145
|
-
const dir = path.dirname(filePath)
|
|
146
|
-
mkdirSync(dir, { recursive: true, mode: 0o700 })
|
|
147
|
-
writeFileSync(filePath, `${token}\n`, { mode: 0o600 })
|
|
148
|
-
chmodSync(filePath, 0o600)
|
|
149
|
-
}
|
|
150
|
-
|
|
151
|
-
function userSessionPath(): string {
|
|
152
|
-
if (process.platform === 'win32' && process.env['APPDATA']) {
|
|
153
|
-
return path.join(process.env['APPDATA'], 'Tovuk', SESSION_FILE)
|
|
154
|
-
}
|
|
155
|
-
const configHome = process.env['XDG_CONFIG_HOME'] ?? path.join(homedir(), '.config')
|
|
156
|
-
return path.join(configHome, 'tovuk', SESSION_FILE)
|
|
157
|
-
}
|
|
158
|
-
|
|
159
|
-
function readKeychainToken(): string {
|
|
160
|
-
if (process.platform === 'darwin') {
|
|
161
|
-
const result = spawnSync('security', ['find-generic-password', '-s', SESSION_SERVICE, '-a', SESSION_ACCOUNT, '-w'], {
|
|
162
|
-
encoding: 'utf8',
|
|
163
|
-
stdio: ['ignore', 'pipe', 'ignore']
|
|
164
|
-
})
|
|
165
|
-
return result.status === 0 ? result.stdout.trim() : ''
|
|
166
|
-
}
|
|
167
|
-
|
|
168
|
-
if (process.platform === 'linux' && hasCommand('secret-tool')) {
|
|
169
|
-
const result = spawnSync('secret-tool', ['lookup', 'service', SESSION_SERVICE, 'account', SESSION_ACCOUNT], {
|
|
170
|
-
encoding: 'utf8',
|
|
171
|
-
stdio: ['ignore', 'pipe', 'ignore']
|
|
172
|
-
})
|
|
173
|
-
return result.status === 0 ? result.stdout.trim() : ''
|
|
174
|
-
}
|
|
175
|
-
|
|
176
|
-
return ''
|
|
177
|
-
}
|
|
178
|
-
|
|
179
|
-
function writeKeychainToken(token: string): boolean {
|
|
180
|
-
if (process.platform === 'darwin') {
|
|
181
|
-
const result = spawnSync('security', [
|
|
182
|
-
'add-generic-password',
|
|
183
|
-
'-U',
|
|
184
|
-
'-s',
|
|
185
|
-
SESSION_SERVICE,
|
|
186
|
-
'-a',
|
|
187
|
-
SESSION_ACCOUNT,
|
|
188
|
-
'-l',
|
|
189
|
-
SESSION_LABEL,
|
|
190
|
-
'-w',
|
|
191
|
-
token
|
|
192
|
-
], { stdio: 'ignore' })
|
|
193
|
-
return result.status === 0
|
|
194
|
-
}
|
|
195
|
-
|
|
196
|
-
if (process.platform === 'linux' && hasCommand('secret-tool')) {
|
|
197
|
-
const result = spawnSync('secret-tool', [
|
|
198
|
-
'store',
|
|
199
|
-
'--label',
|
|
200
|
-
SESSION_LABEL,
|
|
201
|
-
'service',
|
|
202
|
-
SESSION_SERVICE,
|
|
203
|
-
'account',
|
|
204
|
-
SESSION_ACCOUNT
|
|
205
|
-
], {
|
|
206
|
-
input: token,
|
|
207
|
-
stdio: ['pipe', 'ignore', 'ignore']
|
|
208
|
-
})
|
|
209
|
-
return result.status === 0
|
|
210
|
-
}
|
|
211
|
-
|
|
212
|
-
return false
|
|
213
|
-
}
|
|
214
|
-
|
|
215
|
-
function loginStartResponse(value: Awaited<ReturnType<typeof apiRequest>>): LoginStartResponse {
|
|
216
|
-
const source = jsonObjectOrEmpty(value)
|
|
217
|
-
return {
|
|
218
|
-
...stringAliasFields(source, LOGIN_START_STRING_FIELDS),
|
|
219
|
-
...numberAliasFields(source, LOGIN_START_NUMBER_FIELDS)
|
|
220
|
-
}
|
|
221
|
-
}
|
|
222
|
-
|
|
223
|
-
function loginPollResponse(value: Awaited<ReturnType<typeof apiRequest>>): LoginPollResponse {
|
|
224
|
-
const source = jsonObjectOrEmpty(value)
|
|
225
|
-
return {
|
|
226
|
-
...stringAliasFields(source, LOGIN_POLL_STRING_FIELDS),
|
|
227
|
-
...numberAliasFields(source, LOGIN_POLL_NUMBER_FIELDS)
|
|
228
|
-
}
|
|
229
|
-
}
|
|
230
|
-
|
|
231
|
-
function stringAliasFields<Key extends string>(
|
|
232
|
-
source: JsonObject,
|
|
233
|
-
specs: readonly AliasSpec<Key>[]
|
|
234
|
-
): Partial<Record<Key, string>> {
|
|
235
|
-
return aliasFields(source, specs, firstStringAlias, (value) => value !== '')
|
|
236
|
-
}
|
|
237
|
-
|
|
238
|
-
function numberAliasFields<Key extends string>(
|
|
239
|
-
source: JsonObject,
|
|
240
|
-
specs: readonly AliasSpec<Key>[]
|
|
241
|
-
): Partial<Record<Key, number>> {
|
|
242
|
-
return aliasFields(source, specs, firstPositiveNumberAlias, (value) => value > 0)
|
|
243
|
-
}
|
|
244
|
-
|
|
245
|
-
function aliasFields<Key extends string, Value>(
|
|
246
|
-
source: JsonObject,
|
|
247
|
-
specs: readonly AliasSpec<Key>[],
|
|
248
|
-
read: (source: JsonObject, aliases: readonly string[]) => Value,
|
|
249
|
-
keep: (value: Value) => boolean
|
|
250
|
-
): Partial<Record<Key, Value>> {
|
|
251
|
-
const response: Partial<Record<Key, Value>> = {}
|
|
252
|
-
for (const spec of specs) {
|
|
253
|
-
const value = read(source, spec.aliases)
|
|
254
|
-
if (keep(value)) {
|
|
255
|
-
response[spec.field] = value
|
|
256
|
-
}
|
|
257
|
-
}
|
|
258
|
-
return response
|
|
259
|
-
}
|
|
260
|
-
|
|
261
|
-
function firstStringAlias(source: JsonObject, aliases: readonly string[]): string {
|
|
262
|
-
for (const alias of aliases) {
|
|
263
|
-
const value = stringField(source, alias)
|
|
264
|
-
if (value) {
|
|
265
|
-
return value
|
|
266
|
-
}
|
|
267
|
-
}
|
|
268
|
-
return ''
|
|
269
|
-
}
|
|
270
|
-
|
|
271
|
-
function firstPositiveNumberAlias(source: JsonObject, aliases: readonly string[]): number {
|
|
272
|
-
for (const alias of aliases) {
|
|
273
|
-
const value = numberField(source, alias)
|
|
274
|
-
if (value > 0) {
|
|
275
|
-
return value
|
|
276
|
-
}
|
|
277
|
-
}
|
|
278
|
-
return 0
|
|
279
|
-
}
|
|
280
|
-
|
|
281
|
-
export { login, readOrLoginToken }
|
package/src/internal/checks.ts
DELETED
|
@@ -1,12 +0,0 @@
|
|
|
1
|
-
import type { DoctorCheck } from './types.ts'
|
|
2
|
-
|
|
3
|
-
function doctorCheck(name: string, ok: boolean, success: string, failure: string, instruction: string): DoctorCheck {
|
|
4
|
-
return {
|
|
5
|
-
name,
|
|
6
|
-
ok,
|
|
7
|
-
message: ok ? success : failure,
|
|
8
|
-
agent_instruction: ok ? null : instruction
|
|
9
|
-
}
|
|
10
|
-
}
|
|
11
|
-
|
|
12
|
-
export { doctorCheck }
|
package/src/internal/commands.ts
DELETED
|
@@ -1,298 +0,0 @@
|
|
|
1
|
-
import { agentError } from './errors.ts'
|
|
2
|
-
import { apiRequest, pageQuery, requireApp } from './api.ts'
|
|
3
|
-
import { checkoutResponseFromJson, logsResponseFromJson } from './api-models.ts'
|
|
4
|
-
import { readOrLoginToken } from './auth.ts'
|
|
5
|
-
import { openUrl, printJson } from './project.ts'
|
|
6
|
-
import type { ApiMethod, CliOptions, JsonObject, JsonValue } from './types.ts'
|
|
7
|
-
|
|
8
|
-
interface LogsRequest {
|
|
9
|
-
route: string
|
|
10
|
-
target: string
|
|
11
|
-
}
|
|
12
|
-
|
|
13
|
-
type SubcommandHandler = (cli: CliOptions) => Promise<void>
|
|
14
|
-
type SubcommandTable = Readonly<Record<string, SubcommandHandler>>
|
|
15
|
-
type DomainMethod = Extract<ApiMethod, 'DELETE' | 'POST'>
|
|
16
|
-
type DomainRoute = (app: string, domain: string) => string
|
|
17
|
-
type DomainBody = (domain: string) => JsonObject | null
|
|
18
|
-
type BillingAction = 'checkout' | 'portal'
|
|
19
|
-
|
|
20
|
-
const ENV_COMMANDS: SubcommandTable = {
|
|
21
|
-
list: async (cli): Promise<void> => printJson(await appGet(cli, 'env')),
|
|
22
|
-
set: envSet,
|
|
23
|
-
delete: envDelete
|
|
24
|
-
}
|
|
25
|
-
|
|
26
|
-
const DOMAIN_COMMANDS: SubcommandTable = {
|
|
27
|
-
list: async (cli): Promise<void> => printJson(await appGet(cli, 'domains')),
|
|
28
|
-
add: domainMutation('POST', (app): string => `/v1/apps/${encodeURIComponent(app)}/domains`, (domain): JsonObject => ({ domain })),
|
|
29
|
-
verify: domainMutation('POST', (app, domain): string => `/v1/apps/${encodeURIComponent(app)}/domains/${encodeURIComponent(domain)}/verify`, (): null => null),
|
|
30
|
-
delete: domainMutation('DELETE', (app, domain): string => `/v1/apps/${encodeURIComponent(app)}/domains/${encodeURIComponent(domain)}`, (): null => null)
|
|
31
|
-
}
|
|
32
|
-
|
|
33
|
-
const SUPPORT_COMMANDS: SubcommandTable = {
|
|
34
|
-
list: supportList,
|
|
35
|
-
create: supportCreate,
|
|
36
|
-
resolve: supportResolve
|
|
37
|
-
}
|
|
38
|
-
|
|
39
|
-
async function logs(cli: CliOptions): Promise<void> {
|
|
40
|
-
const token = await readOrLoginToken(cli)
|
|
41
|
-
const request = logsRequest(cli)
|
|
42
|
-
const response = logsResponseFromJson(await apiRequest(cli, 'GET', request.route, token, null))
|
|
43
|
-
if (cli.json) {
|
|
44
|
-
printJson(response)
|
|
45
|
-
return
|
|
46
|
-
}
|
|
47
|
-
for (const line of response.lines) {
|
|
48
|
-
console.log(`[${line.timestamp}] ${line.stream}: ${line.message}`)
|
|
49
|
-
}
|
|
50
|
-
if (response.has_more && response.next_cursor) {
|
|
51
|
-
console.log(`next npx tovuk logs ${request.target} --cursor ${response.next_cursor}`)
|
|
52
|
-
}
|
|
53
|
-
}
|
|
54
|
-
|
|
55
|
-
function logsRequest(cli: CliOptions): LogsRequest {
|
|
56
|
-
const page = pageQuery(cli)
|
|
57
|
-
if (cli.build) {
|
|
58
|
-
return {
|
|
59
|
-
route: `/v1/builds/${encodeURIComponent(cli.build)}/logs${page}`,
|
|
60
|
-
target: `--build ${cli.build}`
|
|
61
|
-
}
|
|
62
|
-
}
|
|
63
|
-
if (cli.deploy) {
|
|
64
|
-
return {
|
|
65
|
-
route: `/v1/deploys/${encodeURIComponent(cli.deploy)}/logs${page}`,
|
|
66
|
-
target: `--deploy ${cli.deploy}`
|
|
67
|
-
}
|
|
68
|
-
}
|
|
69
|
-
|
|
70
|
-
const app = requireApp(cli)
|
|
71
|
-
return {
|
|
72
|
-
route: `/v1/apps/${encodeURIComponent(app)}/logs${page}`,
|
|
73
|
-
target: `--app ${app}`
|
|
74
|
-
}
|
|
75
|
-
}
|
|
76
|
-
|
|
77
|
-
async function apps(cli: CliOptions): Promise<void> {
|
|
78
|
-
await printAuthenticated(cli, '/v1/apps')
|
|
79
|
-
}
|
|
80
|
-
|
|
81
|
-
async function capabilities(cli: CliOptions): Promise<void> {
|
|
82
|
-
const response = await apiRequest(cli, 'GET', '/v1/capabilities', null, null)
|
|
83
|
-
printJson(response)
|
|
84
|
-
}
|
|
85
|
-
|
|
86
|
-
async function me(cli: CliOptions): Promise<void> {
|
|
87
|
-
await printAuthenticated(cli, '/v1/me')
|
|
88
|
-
}
|
|
89
|
-
|
|
90
|
-
async function usage(cli: CliOptions): Promise<void> {
|
|
91
|
-
await printAuthenticated(cli, '/v1/usage')
|
|
92
|
-
}
|
|
93
|
-
|
|
94
|
-
async function activity(cli: CliOptions): Promise<void> {
|
|
95
|
-
await printPagedAuthenticated(cli, '/v1/activity')
|
|
96
|
-
}
|
|
97
|
-
|
|
98
|
-
async function overview(cli: CliOptions): Promise<void> {
|
|
99
|
-
await printPagedAuthenticated(cli, appRoute(cli, 'overview'))
|
|
100
|
-
}
|
|
101
|
-
|
|
102
|
-
async function deploys(cli: CliOptions): Promise<void> {
|
|
103
|
-
await printAuthenticated(cli, appScopedCollectionRoute(cli, 'deploys'))
|
|
104
|
-
}
|
|
105
|
-
|
|
106
|
-
async function builds(cli: CliOptions): Promise<void> {
|
|
107
|
-
await printAuthenticated(cli, appScopedCollectionRoute(cli, 'builds'))
|
|
108
|
-
}
|
|
109
|
-
|
|
110
|
-
function appScopedCollectionRoute(cli: CliOptions, collection: 'builds' | 'deploys'): string {
|
|
111
|
-
const route = cli.app
|
|
112
|
-
? `/v1/apps/${encodeURIComponent(cli.app)}/${collection}`
|
|
113
|
-
: `/v1/${collection}`
|
|
114
|
-
return `${route}${pageQuery(cli)}`
|
|
115
|
-
}
|
|
116
|
-
|
|
117
|
-
async function status(cli: CliOptions): Promise<void> {
|
|
118
|
-
printJson(await appGet(cli, 'status'))
|
|
119
|
-
}
|
|
120
|
-
|
|
121
|
-
async function inspect(cli: CliOptions): Promise<void> {
|
|
122
|
-
printJson(await appGet(cli, 'inspect'))
|
|
123
|
-
}
|
|
124
|
-
|
|
125
|
-
async function database(cli: CliOptions): Promise<void> {
|
|
126
|
-
printJson(await appGet(cli, 'database'))
|
|
127
|
-
}
|
|
128
|
-
|
|
129
|
-
async function envCommand(cli: CliOptions): Promise<void> {
|
|
130
|
-
await runSubcommand(cli, ENV_COMMANDS, 'list', 'Unknown env command.', 'Use `npx tovuk env list`, `env set`, or `env delete`.')
|
|
131
|
-
}
|
|
132
|
-
|
|
133
|
-
async function envDelete(cli: CliOptions): Promise<void> {
|
|
134
|
-
const name = requireCommandArg(cli, 'invalid_env', 'Environment variable name is required.', 'Use `npx tovuk env delete --app <app> KEY`.')
|
|
135
|
-
await printAuthenticatedMutation(cli, 'DELETE', appRoute(cli, `env/${encodeURIComponent(name)}`), null)
|
|
136
|
-
}
|
|
137
|
-
|
|
138
|
-
async function envSet(cli: CliOptions): Promise<void> {
|
|
139
|
-
const assignment = cli.args[1] ?? ''
|
|
140
|
-
const separator = assignment.indexOf('=')
|
|
141
|
-
if (separator <= 0) {
|
|
142
|
-
throw agentError('invalid_env', 'Environment assignment must be KEY=value.', 'Pass one uppercase shell-safe environment assignment, for example `API_KEY=value`.', cli.json)
|
|
143
|
-
}
|
|
144
|
-
|
|
145
|
-
const name = assignment.slice(0, separator)
|
|
146
|
-
const value = assignment.slice(separator + 1)
|
|
147
|
-
await printAuthenticatedMutation(cli, 'PUT', appRoute(cli, 'env'), { name, value })
|
|
148
|
-
}
|
|
149
|
-
|
|
150
|
-
async function domainsCommand(cli: CliOptions): Promise<void> {
|
|
151
|
-
await runSubcommand(cli, DOMAIN_COMMANDS, 'list', 'Unknown domains command.', 'Use `domains list`, `domains add`, `domains verify`, or `domains delete`.')
|
|
152
|
-
}
|
|
153
|
-
|
|
154
|
-
function domainMutation(method: DomainMethod, route: DomainRoute, body: DomainBody): SubcommandHandler {
|
|
155
|
-
return async (cli): Promise<void> => {
|
|
156
|
-
const domain = requireCommandArg(cli, 'missing_domain', 'Domain is required.', 'Use `npx tovuk domains add --app <app> api.example.com`.')
|
|
157
|
-
const app = requireApp(cli)
|
|
158
|
-
await printAuthenticatedMutation(cli, method, route(app, domain), body(domain))
|
|
159
|
-
}
|
|
160
|
-
}
|
|
161
|
-
|
|
162
|
-
async function billing(cli: CliOptions): Promise<void> {
|
|
163
|
-
const token = await readOrLoginToken(cli)
|
|
164
|
-
const action = billingAction(cli.args[0] ?? 'checkout', cli.json)
|
|
165
|
-
const route = action === 'portal' ? '/v1/billing/portal' : '/v1/billing/checkout'
|
|
166
|
-
const reason = cli.args.slice(1).join(' ').trim() || 'Upgrade to Tovuk Pro.'
|
|
167
|
-
const body: JsonObject | null = action === 'checkout'
|
|
168
|
-
? { target_plan: 'pro', reason }
|
|
169
|
-
: null
|
|
170
|
-
const response = checkoutResponseFromJson(await apiRequest(cli, 'POST', route, token, body))
|
|
171
|
-
if (cli.json) {
|
|
172
|
-
printJson(response)
|
|
173
|
-
return
|
|
174
|
-
}
|
|
175
|
-
console.log(response.checkout.url)
|
|
176
|
-
openUrl(response.checkout.url)
|
|
177
|
-
}
|
|
178
|
-
|
|
179
|
-
function billingAction(value: string, json: boolean): BillingAction {
|
|
180
|
-
if (!value || value === 'checkout') {
|
|
181
|
-
return 'checkout'
|
|
182
|
-
}
|
|
183
|
-
if (value === 'portal') {
|
|
184
|
-
return 'portal'
|
|
185
|
-
}
|
|
186
|
-
throw agentError('unknown_billing_command', 'Unknown billing command.', 'Use `npx tovuk billing checkout --json` or `npx tovuk billing portal`.', json)
|
|
187
|
-
}
|
|
188
|
-
|
|
189
|
-
async function support(cli: CliOptions): Promise<void> {
|
|
190
|
-
await runSubcommand(cli, SUPPORT_COMMANDS, 'list', 'Unknown support command.', 'Use `npx tovuk support list --json` or `support create` with subject and details.')
|
|
191
|
-
}
|
|
192
|
-
|
|
193
|
-
async function supportList(cli: CliOptions): Promise<void> {
|
|
194
|
-
await printPagedAuthenticated(cli, '/v1/support/tickets')
|
|
195
|
-
}
|
|
196
|
-
|
|
197
|
-
async function supportCreate(cli: CliOptions): Promise<void> {
|
|
198
|
-
const subject = cli.args[1] ?? ''
|
|
199
|
-
const details = cli.args.slice(2).join(' ').trim()
|
|
200
|
-
if (!subject || !details) {
|
|
201
|
-
throw agentError(
|
|
202
|
-
'invalid_support_ticket',
|
|
203
|
-
'Support ticket subject and details are required.',
|
|
204
|
-
'Use `npx tovuk support create "Short subject" "Command, app id, build id, deploy id, and first actionable log line" --json`.',
|
|
205
|
-
cli.json
|
|
206
|
-
)
|
|
207
|
-
}
|
|
208
|
-
|
|
209
|
-
const token = await readOrLoginToken(cli)
|
|
210
|
-
const body = supportTicketBody(cli, subject, details)
|
|
211
|
-
const response = await apiRequest(cli, 'POST', '/v1/support/tickets', token, body)
|
|
212
|
-
printJson(response)
|
|
213
|
-
}
|
|
214
|
-
|
|
215
|
-
function supportTicketBody(cli: CliOptions, subject: string, details: string): JsonObject {
|
|
216
|
-
const body: JsonObject = { details, severity: cli.severity || 'normal', subject }
|
|
217
|
-
addOptionalField(body, 'app_id', cli.app)
|
|
218
|
-
addOptionalField(body, 'failing_command', cli.failingCommand)
|
|
219
|
-
addOptionalField(body, 'build_id', cli.build)
|
|
220
|
-
addOptionalField(body, 'deploy_id', cli.deploy)
|
|
221
|
-
addOptionalField(body, 'first_log_line', cli.firstLogLine)
|
|
222
|
-
return body
|
|
223
|
-
}
|
|
224
|
-
|
|
225
|
-
function addOptionalField(body: JsonObject, key: string, value: string): void {
|
|
226
|
-
if (value) {
|
|
227
|
-
body[key] = value
|
|
228
|
-
}
|
|
229
|
-
}
|
|
230
|
-
|
|
231
|
-
async function supportResolve(cli: CliOptions): Promise<void> {
|
|
232
|
-
const ticketId = requireCommandArg(
|
|
233
|
-
cli,
|
|
234
|
-
'invalid_support_ticket',
|
|
235
|
-
'Support ticket id is required.',
|
|
236
|
-
'Use `npx tovuk support resolve <ticket_id> --json` with an id from support list.'
|
|
237
|
-
)
|
|
238
|
-
await printAuthenticatedMutation(cli, 'POST', `/v1/support/tickets/${encodeURIComponent(ticketId)}/resolve`, null)
|
|
239
|
-
}
|
|
240
|
-
|
|
241
|
-
async function printAuthenticated(cli: CliOptions, route: string): Promise<void> {
|
|
242
|
-
const token = await readOrLoginToken(cli)
|
|
243
|
-
const response = await apiRequest(cli, 'GET', route, token, null)
|
|
244
|
-
printJson(response)
|
|
245
|
-
}
|
|
246
|
-
|
|
247
|
-
async function printPagedAuthenticated(cli: CliOptions, route: string): Promise<void> {
|
|
248
|
-
await printAuthenticated(cli, `${route}${pageQuery(cli)}`)
|
|
249
|
-
}
|
|
250
|
-
|
|
251
|
-
async function printAuthenticatedMutation(cli: CliOptions, method: ApiMethod, route: string, body: JsonObject | null): Promise<void> {
|
|
252
|
-
const token = await readOrLoginToken(cli)
|
|
253
|
-
printJson(await apiRequest(cli, method, route, token, body))
|
|
254
|
-
}
|
|
255
|
-
|
|
256
|
-
function appRoute(cli: CliOptions, suffix: string): string {
|
|
257
|
-
return `/v1/apps/${encodeURIComponent(requireApp(cli))}/${suffix}`
|
|
258
|
-
}
|
|
259
|
-
|
|
260
|
-
async function appGet(cli: CliOptions, kind: string): Promise<JsonValue | null> {
|
|
261
|
-
const token = await readOrLoginToken(cli)
|
|
262
|
-
return apiRequest(cli, 'GET', appRoute(cli, kind), token, null)
|
|
263
|
-
}
|
|
264
|
-
|
|
265
|
-
async function runSubcommand(cli: CliOptions, commands: SubcommandTable, defaultName: string, message: string, instruction: string): Promise<void> {
|
|
266
|
-
const command = commands[cli.args[0] ?? defaultName]
|
|
267
|
-
if (!command) {
|
|
268
|
-
throw agentError('unknown_command', message, instruction, cli.json)
|
|
269
|
-
}
|
|
270
|
-
await command(cli)
|
|
271
|
-
}
|
|
272
|
-
|
|
273
|
-
function requireCommandArg(cli: CliOptions, code: string, message: string, instruction: string): string {
|
|
274
|
-
const value = cli.args[1] ?? ''
|
|
275
|
-
if (!value) {
|
|
276
|
-
throw agentError(code, message, instruction, cli.json)
|
|
277
|
-
}
|
|
278
|
-
return value
|
|
279
|
-
}
|
|
280
|
-
|
|
281
|
-
export {
|
|
282
|
-
logs,
|
|
283
|
-
apps,
|
|
284
|
-
capabilities,
|
|
285
|
-
me,
|
|
286
|
-
usage,
|
|
287
|
-
activity,
|
|
288
|
-
overview,
|
|
289
|
-
deploys,
|
|
290
|
-
builds,
|
|
291
|
-
status,
|
|
292
|
-
inspect,
|
|
293
|
-
database,
|
|
294
|
-
envCommand,
|
|
295
|
-
domainsCommand,
|
|
296
|
-
billing,
|
|
297
|
-
support
|
|
298
|
-
}
|