kaloko 0.12.2 → 0.14.0
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 +11 -5
- package/bin/qawalk.mjs +2 -1
- package/package.json +2 -2
- package/schemas/changelog.v1.json +5 -0
- package/schemas/config.v1.json +2 -1
- package/skills/kaloko/SKILL.md +11 -2
- package/skills/kaloko/reference/ci.md +40 -5
- package/skills/kaloko/reference/collaboration.md +8 -1
- package/skills/kaloko/reference/design.md +9 -8
- package/skills/kaloko/reference/docs.md +9 -3
- package/skills/kaloko/reference/documentation.md +6 -4
- package/skills/kaloko/reference/integrations.md +18 -4
- package/skills/kaloko/reference/mcp.md +22 -9
- package/skills/kaloko/reference/portable-html.md +1 -1
- package/skills/kaloko/reference/recording.md +4 -2
- package/src/cli.mjs +301 -107
- package/src/lib/changelog-core.mjs +11 -8
- package/src/lib/ci.mjs +1 -1
- package/src/lib/cli-args.mjs +144 -0
- package/src/lib/config.mjs +25 -1
- package/src/lib/design-cli.mjs +9 -9
- package/src/lib/design-export.mjs +1 -1
- package/src/lib/docs-export.mjs +44 -10
- package/src/lib/docs.mjs +3 -2
- package/src/lib/doctor.mjs +3 -1
- package/src/lib/integrations-cli.mjs +15 -6
- package/src/lib/legacy.mjs +28 -0
- package/src/lib/login.mjs +1 -1
- package/src/lib/screens-cli.mjs +4 -2
- package/src/lib/service.mjs +52 -15
- package/src/lib/signoff.mjs +2 -2
- package/src/lib/spec-cli.mjs +3 -3
- package/src/lib/upload.mjs +1 -1
- package/src/lib/util.mjs +40 -1
- package/src/lib/video-cli.mjs +3 -3
- package/viewer/index.html +36 -28
package/src/lib/screens-cli.mjs
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
// `kaloko screens`: the screen library from the terminal (batch G): the newest capture of every step of the
|
|
2
2
|
// organization's flows, searched on the service (GET /orgs/<org>/screens), one line per screen with its canvas link.
|
|
3
|
-
import { UserError } from './util.mjs'
|
|
3
|
+
import { UserError, NO_ORG } from './util.mjs'
|
|
4
4
|
|
|
5
5
|
const FILTERS = ['project', 'product', 'module', 'scenario', 'locale', 'viewport', 'device', 'status', 'environment', 'limit', 'cursor']
|
|
6
6
|
|
|
@@ -10,6 +10,8 @@ export function screensQuery (args) {
|
|
|
10
10
|
const q = (args._ || []).join(' ').trim() || (typeof args.q === 'string' ? args.q : '')
|
|
11
11
|
if (q) p.set('q', q)
|
|
12
12
|
for (const k of FILTERS) if (typeof args[k] === 'string' || typeof args[k] === 'number') p.set(k, String(args[k]))
|
|
13
|
+
// --env is the environment, as in every other command (--environment still works)
|
|
14
|
+
if (typeof args.env === 'string') p.set('environment', args.env)
|
|
13
15
|
if (args.designs || args.source === 'all') p.set('source', 'all')
|
|
14
16
|
else if (args.source === 'design') p.set('source', 'design')
|
|
15
17
|
if (args.all) p.set('group', 'none')
|
|
@@ -29,7 +31,7 @@ export function formatScreens (r) {
|
|
|
29
31
|
}
|
|
30
32
|
|
|
31
33
|
export async function screensCmd ({ config, client, args, say }) {
|
|
32
|
-
if (!config.org) throw new UserError(
|
|
34
|
+
if (!config.org) throw new UserError(NO_ORG)
|
|
33
35
|
const r = await client.request('GET', `/orgs/${config.org}/screens?${screensQuery(args)}`)
|
|
34
36
|
say(args.json ? JSON.stringify(r, null, 2) : formatScreens(r))
|
|
35
37
|
return r
|
package/src/lib/service.mjs
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
// Client of the Kaloko service API (kaloko.app or KALOKO_URL) with the organization token.
|
|
2
|
-
import { UserError } from './util.mjs'
|
|
2
|
+
import { UserError, NO_ORG, EXIT } from './util.mjs'
|
|
3
3
|
|
|
4
4
|
/** Base URL of the service; configs written before the rename name app.qawalk.com, which now means kaloko.app. */
|
|
5
5
|
export function serviceUrl (config, vars = process.env) {
|
|
@@ -19,8 +19,20 @@ export function ciName (vars = process.env) {
|
|
|
19
19
|
return vars.CI && vars.CI !== 'false' ? 'ci' : null
|
|
20
20
|
}
|
|
21
21
|
|
|
22
|
-
/**
|
|
23
|
-
export
|
|
22
|
+
/** Plan & billing of the organization on the service (the service names it for 402 answers). */
|
|
23
|
+
export const billingUrl = (url, org) => `${url}${org ? `/${org}` : ''}/billing`
|
|
24
|
+
|
|
25
|
+
/**
|
|
26
|
+
* What to do after the service answered `status` (and `code`, when it names the reason); '' when its own message
|
|
27
|
+
* says enough. `message` is the service's message: a link it already holds is not repeated.
|
|
28
|
+
*/
|
|
29
|
+
export function nextStepFor (status, { tokenEnv = 'KALOKO_TOKEN', url = 'https://kaloko.app', code = null, billing = null, org = null, message = '' } = {}) {
|
|
30
|
+
const plan = billing || billingUrl(url, org)
|
|
31
|
+
const link = message.includes(plan) ? '' : ` Plan & billing: ${plan}`
|
|
32
|
+
if (code === 'billing_restricted') return `Billing of the organization is restricted until the payment is fixed; an admin updates it.${link}`
|
|
33
|
+
if (code === 'seat_limit') return `An admin buys seats or upgrades the plan.${link}`
|
|
34
|
+
if (code === 'sso_required') return 'Sign in through your company sign-in on kaloko.app in the browser, then run kaloko login.'
|
|
35
|
+
if (status === 402 || code === 'plan') return `Your plan does not include this.${link}`
|
|
24
36
|
if (status === 401) return `The token in ${tokenEnv} is invalid, expired or revoked. Run kaloko login for a new one (or put a fresh token from Settings → API tokens into .env).`
|
|
25
37
|
if (status === 403) return 'Your role or the token\'s scopes do not allow this; ask an admin of the organization, or use a token that may do it.'
|
|
26
38
|
if (status === 413) return 'The file is larger than the service takes; leave it out of the run or make it smaller.'
|
|
@@ -30,17 +42,20 @@ export function nextStepFor (status, { tokenEnv = 'KALOKO_TOKEN', url = 'https:/
|
|
|
30
42
|
}
|
|
31
43
|
|
|
32
44
|
/** A UserError for an HTTP answer: what failed, the service's message, what to do next; `status` kept for callers. */
|
|
33
|
-
export function httpError (what, status, message, opts) {
|
|
34
|
-
const err = new UserError(
|
|
45
|
+
export function httpError (what, status, message, opts = {}) {
|
|
46
|
+
const err = new UserError(`${what} → HTTP ${status}: ${message}`, { next: nextStepFor(status, { ...opts, message: String(message || '') }) })
|
|
35
47
|
err.status = status
|
|
36
48
|
err.serviceMessage = message
|
|
49
|
+
if (opts.code) err.code = opts.code
|
|
50
|
+
// 401: the token is missing, invalid, expired or revoked (exit code 4); other answers are a failed command (1)
|
|
51
|
+
if (status === 401) { err.exitCode = EXIT.auth; err.code ||= 'unauthorized' }
|
|
37
52
|
return err
|
|
38
53
|
}
|
|
39
54
|
|
|
40
55
|
/** A UserError for a request that got no answer at all (offline, DNS, refused, TLS). */
|
|
41
56
|
export function unreachable (url, e) {
|
|
42
57
|
const code = e?.cause?.code || e?.code || e?.message || 'no answer'
|
|
43
|
-
const err = new UserError(`Cannot reach ${url} (${code}). Check your network or VPN and run the command again; KALOKO_URL points the CLI to another service
|
|
58
|
+
const err = new UserError(`Cannot reach ${url} (${code}). Check your network or VPN and run the command again; KALOKO_URL points the CLI to another service.`, { code: 'unreachable', exitCode: EXIT.unreachable })
|
|
44
59
|
err.offline = true
|
|
45
60
|
return err
|
|
46
61
|
}
|
|
@@ -50,16 +65,24 @@ export function service (config) {
|
|
|
50
65
|
const base = root + '/api/v1'
|
|
51
66
|
const tokenEnv = config.share?.token_env || 'KALOKO_TOKEN'
|
|
52
67
|
const token = process.env[tokenEnv]
|
|
53
|
-
if (!token) throw new UserError(`${tokenEnv} is not set, so the CLI cannot reach your organization on ${root}. Run kaloko login, or create a token in Settings → API tokens and put ${tokenEnv}=… into .env
|
|
54
|
-
const opts = { tokenEnv, url: root }
|
|
55
|
-
// an agent names itself (KALOKO_AGENT, e.g. "claude-code"); the service shows it in presence, history and audit (#42)
|
|
68
|
+
if (!token) throw new UserError(`${tokenEnv} is not set, so the CLI cannot reach your organization on ${root}. Run kaloko login, or create a token in Settings → API tokens and put ${tokenEnv}=… into .env.`, { code: 'no_token', exitCode: EXIT.auth })
|
|
69
|
+
const opts = { tokenEnv, url: root, org: config.org || null }
|
|
70
|
+
// an agent names itself (KALOKO_AGENT, e.g. "claude-code"); the service shows it in presence, history and audit (#42).
|
|
71
|
+
// Both header names: services before the rename read X-QAwalk-Agent only
|
|
56
72
|
const ci = ciName()
|
|
57
|
-
const
|
|
73
|
+
const agent = process.env.KALOKO_AGENT ? process.env.KALOKO_AGENT.slice(0, 60) : null
|
|
74
|
+
const headers = { Authorization: `Bearer ${token}`, ...(agent ? { 'X-Kaloko-Agent': agent, 'X-QAwalk-Agent': agent } : {}), ...(ci ? { 'X-Kaloko-CI': ci } : {}) }
|
|
75
|
+
// an error answer: its message, and the reason code and billing link the service adds ({ error, code?, billing_url? })
|
|
76
|
+
const failed = async (res, what) => {
|
|
77
|
+
const body = await res.json().catch(() => ({}))
|
|
78
|
+
return httpError(what, res.status, body.error || res.statusText, { ...opts, code: body.code || null, billing: body.billing_url || null })
|
|
79
|
+
}
|
|
58
80
|
const check = async (res, what) => {
|
|
59
81
|
if (res.ok) return res.json()
|
|
60
|
-
|
|
61
|
-
throw httpError(what, res.status, body.error || res.statusText, opts)
|
|
82
|
+
throw await failed(res, what)
|
|
62
83
|
}
|
|
84
|
+
// no org: in the config would ask for /orgs/undefined/…: say where the slug comes from instead
|
|
85
|
+
const orgOf = (path) => { if (/^\/orgs\/(?:undefined|null)?(?:[/?]|$)/.test(path)) throw new UserError(NO_ORG) }
|
|
63
86
|
// fetch rejects without an answer (offline, refused, DNS): say so instead of a stack trace
|
|
64
87
|
const call = (send) => send().catch((e) => { if (e instanceof UserError) throw e; throw unreachable(root, e) })
|
|
65
88
|
// the service limits requests per token (HTTP 429, nothing was processed): wait as told and try again
|
|
@@ -76,26 +99,40 @@ export function service (config) {
|
|
|
76
99
|
}
|
|
77
100
|
return {
|
|
78
101
|
async request (method, path, body) {
|
|
102
|
+
orgOf(path)
|
|
79
103
|
const res = await call(() => limited(() => fetch(base + path, { method, headers: { ...headers, ...(body ? { 'Content-Type': 'application/json' } : {}) }, body: body ? JSON.stringify(body) : undefined })))
|
|
80
104
|
return check(res, `${method} ${path}`)
|
|
81
105
|
},
|
|
106
|
+
/**
|
|
107
|
+
* One page of a list endpoint: { data, next }. `next` is the cursor of the following page (X-Next-Cursor, or
|
|
108
|
+
* `next` in an object answer), null on the last page and from services that do not page yet.
|
|
109
|
+
*/
|
|
110
|
+
async page (path) {
|
|
111
|
+
orgOf(path)
|
|
112
|
+
const res = await call(() => limited(() => fetch(base + path, { headers })))
|
|
113
|
+
const data = await check(res, `GET ${path}`)
|
|
114
|
+
return { data, next: res.headers.get('X-Next-Cursor') || (data && !Array.isArray(data) && typeof data.next === 'string' ? data.next : null) }
|
|
115
|
+
},
|
|
82
116
|
/** A request whose answer is not JSON (an export): the response, or an error with the service's message. */
|
|
83
117
|
async raw (method, path, body) {
|
|
118
|
+
orgOf(path)
|
|
84
119
|
const res = await call(() => limited(() => fetch(base + path, { method, headers: { ...headers, ...(body ? { 'Content-Type': 'application/json' } : {}) }, body: body ? JSON.stringify(body) : undefined })))
|
|
85
|
-
if (!res.ok)
|
|
120
|
+
if (!res.ok) throw await failed(res, `${method} ${path}`)
|
|
86
121
|
return res
|
|
87
122
|
},
|
|
88
123
|
async fetch (path) {
|
|
124
|
+
orgOf(path)
|
|
89
125
|
const res = await call(() => limited(() => fetch(base + path, { headers })))
|
|
90
|
-
if (!res.ok)
|
|
126
|
+
if (!res.ok) throw await failed(res, `GET ${path}`)
|
|
91
127
|
return res
|
|
92
128
|
},
|
|
93
129
|
async upload (path, data, name, extra = {}) {
|
|
130
|
+
orgOf(path)
|
|
94
131
|
for (let attempt = 0; ; attempt++) {
|
|
95
132
|
// a dropped connection (flaky Wi-Fi, CI runner hiccup) is tried again like a 5xx; the next share resumes otherwise
|
|
96
133
|
let res
|
|
97
134
|
try { res = await limited(() => fetch(base + path, { method: 'PUT', headers: { ...headers, ...extra }, body: data })) } catch (e) {
|
|
98
|
-
if (attempt >= 3) throw new UserError(`The upload of ${name} broke off (${e.cause?.code || e.message}). Check your network and run kaloko share again: files already stored are not sent twice
|
|
135
|
+
if (attempt >= 3) throw new UserError(`The upload of ${name} broke off (${e.cause?.code || e.message}). Check your network and run kaloko share again: files already stored are not sent twice.`, { code: 'unreachable', exitCode: EXIT.unreachable })
|
|
99
136
|
await new Promise((r) => setTimeout(r, 1000 * (attempt + 1)))
|
|
100
137
|
continue
|
|
101
138
|
}
|
package/src/lib/signoff.mjs
CHANGED
|
@@ -3,7 +3,7 @@
|
|
|
3
3
|
// record someone handed over, without a token. The keys come from <service>/.well-known/kaloko-signoff.json.
|
|
4
4
|
import { readFile, writeFile, mkdir } from 'node:fs/promises'
|
|
5
5
|
import { join, resolve } from 'node:path'
|
|
6
|
-
import { UserError } from './util.mjs'
|
|
6
|
+
import { UserError, UsageError } from './util.mjs'
|
|
7
7
|
import { serviceUrl } from './service.mjs'
|
|
8
8
|
import { verifyRecord, recordSha256, extractRecordFromPdf, canonicalJson } from './signoff-core.mjs'
|
|
9
9
|
|
|
@@ -64,7 +64,7 @@ export async function signoffCmd ({ config, args, say, client = null, runId = nu
|
|
|
64
64
|
if (!check.ok) throw new UserError(`The sign-off record in ${args.verify} does not verify: ${check.reason}`)
|
|
65
65
|
return check
|
|
66
66
|
}
|
|
67
|
-
if (!runId) throw new
|
|
67
|
+
if (!runId) throw new UsageError('Usage: kaloko signoff <run-id> [--out dir] [--lang cs|en] | kaloko signoff --verify <file.json|file.pdf> [--key file|url]')
|
|
68
68
|
if (!client) throw new UserError('Downloading a sign-off record needs the service token (KALOKO_TOKEN).')
|
|
69
69
|
const base = `/orgs/${config.org}/runs/${encodeURIComponent(runId)}`
|
|
70
70
|
const lang = args.lang === 'cs' ? 'cs' : 'en'
|
package/src/lib/spec-cli.mjs
CHANGED
|
@@ -3,7 +3,7 @@
|
|
|
3
3
|
// interaction states). The spec comes from the service (design reference, draft, a version or a shared run) or from
|
|
4
4
|
// a local run's captures; drift compares the local implementation run with the reference from the service.
|
|
5
5
|
import { join } from 'node:path'
|
|
6
|
-
import { readJson, UserError } from './util.mjs'
|
|
6
|
+
import { readJson, UserError, UsageError } from './util.mjs'
|
|
7
7
|
import { readCaptures } from './run.mjs'
|
|
8
8
|
import { latestCaptures } from './evaluate.mjs'
|
|
9
9
|
import { specText, specDrift, driftText } from './devspec.mjs'
|
|
@@ -31,7 +31,7 @@ export async function specCmd ({ config, client, run, scenario, scenarioId, args
|
|
|
31
31
|
// a step state (#46): its captures (keys <viewport>.state-<name>); default otherwise
|
|
32
32
|
const state = typeof args.state === 'string' ? args.state : null
|
|
33
33
|
const steps = step ? [step] : (scenario?.nodes || []).filter((n) => n.kind !== 'decision' && n.kind !== 'external').map((n) => n.id)
|
|
34
|
-
if (!steps.length) throw new
|
|
34
|
+
if (!steps.length) throw new UsageError('Usage: kaloko spec <step> [--ref design|draft|vN|<run id>] [--locale l] [--viewport v] [--state s] [--local] [--json]')
|
|
35
35
|
const out = []
|
|
36
36
|
for (const s of steps) {
|
|
37
37
|
if (args.local || !client) {
|
|
@@ -66,7 +66,7 @@ export async function driftCmd ({ config, client, run, scenarioId, args, say })
|
|
|
66
66
|
if (!client) throw new UserError('kaloko drift compares with the approved design on the service: set org and a token (kaloko login).')
|
|
67
67
|
const results = []
|
|
68
68
|
if (args.remote) {
|
|
69
|
-
if (!step) throw new
|
|
69
|
+
if (!step) throw new UsageError('Usage: kaloko drift <step> --remote <run id|latest>')
|
|
70
70
|
const q = new URLSearchParams(Object.entries({ run: typeof args.remote === 'string' ? args.remote : 'latest', ref, locale, viewport }).filter(([, v]) => v))
|
|
71
71
|
const d = await client.request('GET', `/orgs/${config.org}/scenarios/${enc(scenarioId)}/steps/${enc(step)}/drift?${q}`)
|
|
72
72
|
results.push({ node: step, locale: d.locale, viewport: d.viewport, ref: d.ref, findings: d.findings, text: d.text })
|
package/src/lib/upload.mjs
CHANGED
|
@@ -71,7 +71,7 @@ export async function uploadRunFiles ({ client, org, runId, dir, entries, missin
|
|
|
71
71
|
const tick = progressLine(write, 'uploaded')
|
|
72
72
|
await pool([...byHash.values()], workers, async (e) => {
|
|
73
73
|
const { data, gz } = encode(e.path, await readFile(join(dir, e.path)))
|
|
74
|
-
await client.upload(`/orgs/${org}/blobs/${e.hash}`, data, e.path, { 'X-QAwalk-Type': e.ext, ...(gz ? { 'Content-Encoding': 'gzip' } : {}) })
|
|
74
|
+
await client.upload(`/orgs/${org}/blobs/${e.hash}`, data, e.path, { 'X-Kaloko-Type': e.ext, 'X-QAwalk-Type': e.ext, ...(gz ? { 'Content-Encoding': 'gzip' } : {}) })
|
|
75
75
|
sent += data.length
|
|
76
76
|
}, () => tick(++done, byHash.size, sent))
|
|
77
77
|
// 2. the run's file list points at the blobs; a service that does not know references says so by leaving out `blobs`
|
package/src/lib/util.mjs
CHANGED
|
@@ -65,7 +65,46 @@ export function globToRegExp (glob) {
|
|
|
65
65
|
return new RegExp('^' + out + '$')
|
|
66
66
|
}
|
|
67
67
|
|
|
68
|
-
|
|
68
|
+
/**
|
|
69
|
+
* An error the person can act on: what happened, then (`next`) what to do about it, in one format for every command:
|
|
70
|
+
* "<what happened> <what to do next>". `reason` and `next` stay apart for callers that show them separately.
|
|
71
|
+
*/
|
|
72
|
+
export class UserError extends Error {
|
|
73
|
+
constructor (message, { next = '', code = null, exitCode = null, ...options } = {}) {
|
|
74
|
+
super([message, next].filter(Boolean).join(' '), options)
|
|
75
|
+
this.reason = message
|
|
76
|
+
this.next = next || ''
|
|
77
|
+
if (code) this.code = code
|
|
78
|
+
if (exitCode) this.exitCode = exitCode
|
|
79
|
+
}
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
/** A command used the wrong way (unknown option, missing argument): exit code 2 and the command's help, no support line. */
|
|
83
|
+
export class UsageError extends UserError {
|
|
84
|
+
constructor (message, options = {}) { super(message, { code: 'usage', exitCode: EXIT.usage, ...options }) }
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
/**
|
|
88
|
+
* Exit codes of every command (kaloko help, help/reference/cli.md, skills/kaloko/reference/ci.md). 3 and 4 are only
|
|
89
|
+
* for the Kaloko service: no connection at all (3), and no, an invalid or an expired token (4). An HTTP error answer
|
|
90
|
+
* other than 401 is 1, like any failed command.
|
|
91
|
+
*/
|
|
92
|
+
export const EXIT = { ok: 0, failed: 1, usage: 2, unreachable: 3, auth: 4 }
|
|
93
|
+
|
|
94
|
+
const UNIT_SECONDS = { s: 1, m: 60, h: 3600, d: 86400 }
|
|
95
|
+
const UNIT_NAMES = { s: 'seconds', m: 'minutes', h: 'hours', d: 'days' }
|
|
96
|
+
|
|
97
|
+
/**
|
|
98
|
+
* A duration option (--timeout, --wait, --expires, --older-than): a bare number in the option's own `unit` (what it
|
|
99
|
+
* always meant), or a number with s, m, h or d (90s, 10m, 2h, 14d). Returns the value in `unit`; null when the option
|
|
100
|
+
* is absent or has no value; a UsageError naming `flag` for anything else.
|
|
101
|
+
*/
|
|
102
|
+
export function parseDuration (value, unit, flag) {
|
|
103
|
+
if (value == null || value === true || value === '') return null
|
|
104
|
+
const m = String(value).trim().match(/^(\d+(?:\.\d+)?)\s*([smhd])?$/i)
|
|
105
|
+
if (!m) throw new UsageError(`--${flag} takes a duration: a number of ${UNIT_NAMES[unit]}, or one with s, m, h or d (90s, 10m, 2h, 14d); not "${value}".`)
|
|
106
|
+
return Number(m[1]) * UNIT_SECONDS[(m[2] || unit).toLowerCase()] / UNIT_SECONDS[unit]
|
|
107
|
+
}
|
|
69
108
|
|
|
70
109
|
/** Every CLI error ends with a way to a person (docs/HARDENING.md phase 6, item 12). */
|
|
71
110
|
export const SUPPORT_EMAIL = 'support@sinfin.cz'
|
package/src/lib/video-cli.mjs
CHANGED
|
@@ -8,7 +8,7 @@ import { resolveRun, addCapture } from './run.mjs'
|
|
|
8
8
|
import { loadScenario } from './scenario.mjs'
|
|
9
9
|
import { buildManifest } from './build.mjs'
|
|
10
10
|
import { launchEphemeral } from './browser.mjs'
|
|
11
|
-
import { readJson, localized, UserError } from './util.mjs'
|
|
11
|
+
import { readJson, localized, UserError, UsageError, parseDuration } from './util.mjs'
|
|
12
12
|
import { videoTools, encoderCaps, ffmpeg, preset, installHint, outputSize } from './video.mjs'
|
|
13
13
|
import { formatTrace } from './trace.mjs'
|
|
14
14
|
|
|
@@ -115,7 +115,7 @@ export async function exportWalkthrough ({ config, root, args, say }) {
|
|
|
115
115
|
const P = preset(args.quality || 'medium')
|
|
116
116
|
const fps = format === 'gif' ? 10 : P.fps
|
|
117
117
|
const [width, height] = format === 'gif' ? outputSize(1280, 800, 800) : outputSize(1280, 800, Math.min(P.width, 1280))
|
|
118
|
-
const card = Math.max(0.5, Math.min(6,
|
|
118
|
+
const card = Math.max(0.5, Math.min(6, parseDuration(args['card-seconds'], 's', 'card-seconds') || 1.6))
|
|
119
119
|
const browser = await launchEphemeral(config)
|
|
120
120
|
const tmp = await mkdtemp(join(tmpdir(), 'kaloko-walkthrough-'))
|
|
121
121
|
const defaultOut = join(run.dir, 'attachments', `walkthrough.rec.${format}`)
|
|
@@ -189,7 +189,7 @@ export async function videoCmd ({ config, root, args, say }) {
|
|
|
189
189
|
if (sub === 'trace') {
|
|
190
190
|
const step = typeof args.step === 'string' ? args.step : null
|
|
191
191
|
const n = manifest.nodes.find((x) => x.id === step)
|
|
192
|
-
if (!n) throw new
|
|
192
|
+
if (!n) throw new UsageError(`Usage: kaloko video trace --step <id> [--locale l] [--viewport v] [--json]. Steps: ${manifest.nodes.map((x) => x.id).join(', ')}`)
|
|
193
193
|
const loc = typeof args.locale === 'string' ? args.locale : Object.keys(n.captures || {})[0]
|
|
194
194
|
const vps = n.captures?.[loc] || {}
|
|
195
195
|
const vp = typeof args.viewport === 'string' ? args.viewport : Object.keys(vps).find((v) => vps[v]?.trace)
|