kaloko 0.12.2 → 0.13.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/package.json +2 -2
- package/skills/kaloko/SKILL.md +10 -1
- package/skills/kaloko/reference/ci.md +8 -0
- package/skills/kaloko/reference/documentation.md +4 -2
- package/skills/kaloko/reference/mcp.md +13 -5
- package/skills/kaloko/reference/recording.md +4 -2
- package/src/cli.mjs +122 -63
- package/src/lib/cli-args.mjs +139 -0
- package/src/lib/design-cli.mjs +9 -9
- package/src/lib/design-export.mjs +1 -1
- package/src/lib/integrations-cli.mjs +5 -5
- package/src/lib/screens-cli.mjs +4 -2
- package/src/lib/service.mjs +37 -12
- 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 +14 -1
- package/src/lib/video-cli.mjs +2 -2
- package/viewer/index.html +20 -20
package/src/lib/design-cli.mjs
CHANGED
|
@@ -3,7 +3,7 @@
|
|
|
3
3
|
import { readFile, writeFile, mkdir, rm, readdir } from 'node:fs/promises'
|
|
4
4
|
import { gzipSync, gunzipSync } from 'node:zlib'
|
|
5
5
|
import { join, dirname, relative } from 'node:path'
|
|
6
|
-
import { readJson, writeJson, UserError } from './util.mjs'
|
|
6
|
+
import { readJson, writeJson, UserError, UsageError } from './util.mjs'
|
|
7
7
|
import { readCaptures, addCapture, runPaths } from './run.mjs'
|
|
8
8
|
import { latestCaptures, criteriaHash, evaluationSource } from './evaluate.mjs'
|
|
9
9
|
import { captureStep } from './capture.mjs'
|
|
@@ -198,7 +198,7 @@ export async function uploadBlobs ({ client, org, files, say }) {
|
|
|
198
198
|
const raw = await readFile(f.abs)
|
|
199
199
|
const gz = TEXT_TYPES.has(f.ext) && raw.length > 512
|
|
200
200
|
const body = gz ? gzipSync(raw, { level: 6 }) : raw
|
|
201
|
-
await client.upload(`/orgs/${org}/blobs/${h}`, body, f.abs, { 'X-QAwalk-Type': f.ext, ...(gz ? { 'Content-Encoding': 'gzip' } : {}) })
|
|
201
|
+
await client.upload(`/orgs/${org}/blobs/${h}`, body, f.abs, { 'X-Kaloko-Type': f.ext, 'X-QAwalk-Type': f.ext, ...(gz ? { 'Content-Encoding': 'gzip' } : {}) })
|
|
202
202
|
bytes += body.length
|
|
203
203
|
}
|
|
204
204
|
}
|
|
@@ -267,7 +267,7 @@ export async function syncDesign ({ config, root, run, scenario, env, client, ar
|
|
|
267
267
|
try { if (await sha256Hex(await readFile(target)) === hash) continue } catch {}
|
|
268
268
|
const res = await client.fetch(`/orgs/${org}/revisions/${rev.id}/files/${p.split('/').map(enc).join('/')}`)
|
|
269
269
|
let buf = Buffer.from(await res.arrayBuffer())
|
|
270
|
-
if (res.headers.get('X-QAwalk-Encoding') === 'gzip') buf = gunzipSync(buf)
|
|
270
|
+
if ((res.headers.get('X-Kaloko-Encoding') || res.headers.get('X-QAwalk-Encoding')) === 'gzip') buf = gunzipSync(buf)
|
|
271
271
|
await mkdir(dirname(target), { recursive: true })
|
|
272
272
|
await writeFile(target, buf)
|
|
273
273
|
}
|
|
@@ -470,7 +470,7 @@ export async function tokensCmd ({ config, root, env, client, args, say, project
|
|
|
470
470
|
if (args.json) { say(JSON.stringify(report, null, 2)); return }
|
|
471
471
|
if (!report.offenders.length) say(`✓ Every color, font, size, spacing and radius in ${relative(process.cwd(), dir)} comes from the token set (${report.checked} values).`)
|
|
472
472
|
else { say(`${report.offenders.length} raw value(s) outside the token set:\n - ${report.offenders.slice(0, 40).map((o) => `${o.file}:${o.line} ${o.property}: ${o.value}${o.deprecated ? ` (deprecated token ${o.deprecated}${o.note ? `: ${o.note}` : ''})` : ''}${o.suggest ? ` → ${o.suggest}` : ''}`).join('\n - ')}`); process.exitCode = 1 }
|
|
473
|
-
} else throw new
|
|
473
|
+
} else throw new UsageError('Usage: kaloko tokens pull|push|check|validate [--project p] [--file tokens.json|tokens.resolver.json] [--mode m] [--json]')
|
|
474
474
|
}
|
|
475
475
|
|
|
476
476
|
/** Writes the live files of a version (or the draft) into a folder: the handoff, or taking back canvas edits. */
|
|
@@ -488,7 +488,7 @@ export async function exportLive ({ config, scenarioId, client, args, say, out }
|
|
|
488
488
|
try { if (await sha256Hex(await readFile(target)) === hash) continue } catch {}
|
|
489
489
|
const res = await client.fetch(`/orgs/${config.org}/revisions/${s.revision}/files/${p.split('/').map(enc).join('/')}`)
|
|
490
490
|
let buf = Buffer.from(await res.arrayBuffer())
|
|
491
|
-
if (res.headers.get('X-QAwalk-Encoding') === 'gzip') buf = gunzipSync(buf)
|
|
491
|
+
if ((res.headers.get('X-Kaloko-Encoding') || res.headers.get('X-QAwalk-Encoding')) === 'gzip') buf = gunzipSync(buf)
|
|
492
492
|
await mkdir(dirname(target), { recursive: true })
|
|
493
493
|
await writeFile(target, buf)
|
|
494
494
|
written++
|
|
@@ -510,13 +510,13 @@ export async function branchCmd ({ config, scenarioId, client, args, say }) {
|
|
|
510
510
|
return
|
|
511
511
|
}
|
|
512
512
|
if (action === 'create') {
|
|
513
|
-
if (!name) throw new
|
|
513
|
+
if (!name) throw new UsageError('Usage: kaloko branch create <name> [--from vN|draft] [--git <branch>]')
|
|
514
514
|
const r = await client.request('POST', base, { name, ...(typeof args.from === 'string' ? { from: args.from } : {}), ...(typeof args.git === 'string' ? { git_branch: args.git } : {}) })
|
|
515
515
|
say(`Branch ${r.branch} created from ${r.based_on_version || 'the draft'} (${r.steps} step×locale). Work on it with --branch ${r.branch}.`)
|
|
516
516
|
return
|
|
517
517
|
}
|
|
518
518
|
if (action === 'merge') {
|
|
519
|
-
if (!name) throw new
|
|
519
|
+
if (!name) throw new UsageError('Usage: kaloko branch merge <name> [--take-branch a,b] [--keep-main c]')
|
|
520
520
|
const choices = {}
|
|
521
521
|
try {
|
|
522
522
|
const r = await client.request('POST', `${base}/${enc(name)}/merge`, { choices: await choicesFrom(args, client, config, scenarioId, name) })
|
|
@@ -528,7 +528,7 @@ export async function branchCmd ({ config, scenarioId, client, args, say }) {
|
|
|
528
528
|
}
|
|
529
529
|
return choices
|
|
530
530
|
}
|
|
531
|
-
throw new
|
|
531
|
+
throw new UsageError('Usage: kaloko branch list | create <name> [--from vN] [--git <branch>] | merge <name> [--take-branch a,b] [--keep-main c]')
|
|
532
532
|
}
|
|
533
533
|
|
|
534
534
|
async function choicesFrom (args, client, config, scenarioId, name) {
|
|
@@ -556,7 +556,7 @@ async function forkViews ({ config, client, args, say, out, data }) {
|
|
|
556
556
|
for (const [p] of Object.entries(v.files)) {
|
|
557
557
|
const res = await client.fetch(`/orgs/${config.org}/revisions/${v.revision}/files/${p.split('/').map(enc).join('/')}`)
|
|
558
558
|
let buf = Buffer.from(await res.arrayBuffer())
|
|
559
|
-
if (res.headers.get('X-QAwalk-Encoding') === 'gzip') buf = gunzipSync(buf)
|
|
559
|
+
if ((res.headers.get('X-Kaloko-Encoding') || res.headers.get('X-QAwalk-Encoding')) === 'gzip') buf = gunzipSync(buf)
|
|
560
560
|
const target = join(out, prefix, p)
|
|
561
561
|
await mkdir(dirname(target), { recursive: true })
|
|
562
562
|
await writeFile(target, buf)
|
|
@@ -136,7 +136,7 @@ export async function exportDesign ({ config, scenarioId, client, ref = 'design'
|
|
|
136
136
|
for (const [p, { revision }] of paths) {
|
|
137
137
|
const res = await client.fetch(`/orgs/${org}/revisions/${revision}/files/${p.split('/').map(enc).join('/')}`)
|
|
138
138
|
let buf = Buffer.from(await res.arrayBuffer())
|
|
139
|
-
if (res.headers.get('X-QAwalk-Encoding') === 'gzip') buf = gunzipSync(buf)
|
|
139
|
+
if ((res.headers.get('X-Kaloko-Encoding') || res.headers.get('X-QAwalk-Encoding')) === 'gzip') buf = gunzipSync(buf)
|
|
140
140
|
await sink.put(p, buf)
|
|
141
141
|
}
|
|
142
142
|
const steps = []
|
|
@@ -4,7 +4,7 @@
|
|
|
4
4
|
import { readFile, writeFile, mkdir } from 'node:fs/promises'
|
|
5
5
|
import { existsSync } from 'node:fs'
|
|
6
6
|
import { resolve, relative, dirname, join } from 'node:path'
|
|
7
|
-
import { UserError, NO_ORG } from './util.mjs'
|
|
7
|
+
import { UserError, UsageError, NO_ORG } from './util.mjs'
|
|
8
8
|
import { gateOf, accountFor } from './access.mjs'
|
|
9
9
|
import { secret } from './secrets.mjs'
|
|
10
10
|
import { runner } from './ci.mjs'
|
|
@@ -31,7 +31,7 @@ export async function issueCmd ({ args, config, client, runId, say }) {
|
|
|
31
31
|
say(items.length ? items.map(issueLine).join('\n') : 'No issues created from steps yet.')
|
|
32
32
|
return items
|
|
33
33
|
}
|
|
34
|
-
if (sub !== 'create') throw new
|
|
34
|
+
if (sub !== 'create') throw new UsageError('Usage: kaloko issue create --step <id> --to jira|linear [--criterion AC2] [--title "…"] [--note "…"] [--run id]\n kaloko issue list [--run id] [--scenario id] [--state open|done|canceled] [--json]')
|
|
35
35
|
const to = String(args.to || '')
|
|
36
36
|
if (!['jira', 'linear'].includes(to)) throw new UserError('--to jira or --to linear says where the issue goes.')
|
|
37
37
|
if (typeof args.step !== 'string' || !args.step) throw new UserError('--step <id> names the step the issue is about.')
|
|
@@ -145,7 +145,7 @@ export async function scheduleCmd ({ args, config, root, say, deps }) {
|
|
|
145
145
|
const org = config.org
|
|
146
146
|
const client = () => deps.service(config)
|
|
147
147
|
if (sub === 'export') {
|
|
148
|
-
if (id !== 'github') throw new
|
|
148
|
+
if (id !== 'github') throw new UsageError('Usage: kaloko schedule export github --scenario <file> --env <name> [--every day|week] [--at 5] [--weekday mon] [--out file] [--print] [--force]')
|
|
149
149
|
const file = typeof args.scenario === 'string' ? resolve(root, args.scenario) : null
|
|
150
150
|
if (!file) throw new UserError('--scenario <file> names the scenario to walk.')
|
|
151
151
|
const envName = typeof args.env === 'string' ? args.env : null
|
|
@@ -202,10 +202,10 @@ export async function scheduleCmd ({ args, config, root, say, deps }) {
|
|
|
202
202
|
if ((s.skipped || []).length) say(' Steps that click or type need the full walk: kaloko schedule export github')
|
|
203
203
|
return s
|
|
204
204
|
}
|
|
205
|
-
if (!id) throw new
|
|
205
|
+
if (!id) throw new UsageError(`Usage: kaloko schedule ${sub} <schedule id> (kaloko schedule list shows them)`)
|
|
206
206
|
const path = `/orgs/${org}/schedules/${encodeURIComponent(id)}`
|
|
207
207
|
if (sub === 'run') { await client().request('POST', `${path}/run`); say(`✓ ${id} starts within 15 minutes; the run lands in your inbox.`); return }
|
|
208
208
|
if (sub === 'pause' || sub === 'resume') { const s = await client().request('PATCH', path, { enabled: sub === 'resume' }); say(`✓ ${id} ${s.enabled ? `resumed: ${when(s)}` : 'paused'}`); return s }
|
|
209
209
|
if (sub === 'remove') { await client().request('DELETE', path); say(`✓ ${id} removed; its runs stay.`); return }
|
|
210
|
-
throw new
|
|
210
|
+
throw new UsageError('Usage: kaloko schedule add|list|run|pause|resume|remove|export github (kaloko help)')
|
|
211
211
|
}
|
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 } 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,10 +42,11 @@ 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
|
|
37
50
|
return err
|
|
38
51
|
}
|
|
39
52
|
|
|
@@ -51,15 +64,23 @@ export function service (config) {
|
|
|
51
64
|
const tokenEnv = config.share?.token_env || 'KALOKO_TOKEN'
|
|
52
65
|
const token = process.env[tokenEnv]
|
|
53
66
|
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)
|
|
67
|
+
const opts = { tokenEnv, url: root, org: config.org || null }
|
|
68
|
+
// an agent names itself (KALOKO_AGENT, e.g. "claude-code"); the service shows it in presence, history and audit (#42).
|
|
69
|
+
// Both header names: services before the rename read X-QAwalk-Agent only
|
|
56
70
|
const ci = ciName()
|
|
57
|
-
const
|
|
71
|
+
const agent = process.env.KALOKO_AGENT ? process.env.KALOKO_AGENT.slice(0, 60) : null
|
|
72
|
+
const headers = { Authorization: `Bearer ${token}`, ...(agent ? { 'X-Kaloko-Agent': agent, 'X-QAwalk-Agent': agent } : {}), ...(ci ? { 'X-Kaloko-CI': ci } : {}) }
|
|
73
|
+
// an error answer: its message, and the reason code and billing link the service adds ({ error, code?, billing_url? })
|
|
74
|
+
const failed = async (res, what) => {
|
|
75
|
+
const body = await res.json().catch(() => ({}))
|
|
76
|
+
return httpError(what, res.status, body.error || res.statusText, { ...opts, code: body.code || null, billing: body.billing_url || null })
|
|
77
|
+
}
|
|
58
78
|
const check = async (res, what) => {
|
|
59
79
|
if (res.ok) return res.json()
|
|
60
|
-
|
|
61
|
-
throw httpError(what, res.status, body.error || res.statusText, opts)
|
|
80
|
+
throw await failed(res, what)
|
|
62
81
|
}
|
|
82
|
+
// no org: in the config would ask for /orgs/undefined/…: say where the slug comes from instead
|
|
83
|
+
const orgOf = (path) => { if (/^\/orgs\/(?:undefined|null)?(?:[/?]|$)/.test(path)) throw new UserError(NO_ORG) }
|
|
63
84
|
// fetch rejects without an answer (offline, refused, DNS): say so instead of a stack trace
|
|
64
85
|
const call = (send) => send().catch((e) => { if (e instanceof UserError) throw e; throw unreachable(root, e) })
|
|
65
86
|
// the service limits requests per token (HTTP 429, nothing was processed): wait as told and try again
|
|
@@ -76,21 +97,25 @@ export function service (config) {
|
|
|
76
97
|
}
|
|
77
98
|
return {
|
|
78
99
|
async request (method, path, body) {
|
|
100
|
+
orgOf(path)
|
|
79
101
|
const res = await call(() => limited(() => fetch(base + path, { method, headers: { ...headers, ...(body ? { 'Content-Type': 'application/json' } : {}) }, body: body ? JSON.stringify(body) : undefined })))
|
|
80
102
|
return check(res, `${method} ${path}`)
|
|
81
103
|
},
|
|
82
104
|
/** A request whose answer is not JSON (an export): the response, or an error with the service's message. */
|
|
83
105
|
async raw (method, path, body) {
|
|
106
|
+
orgOf(path)
|
|
84
107
|
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)
|
|
108
|
+
if (!res.ok) throw await failed(res, `${method} ${path}`)
|
|
86
109
|
return res
|
|
87
110
|
},
|
|
88
111
|
async fetch (path) {
|
|
112
|
+
orgOf(path)
|
|
89
113
|
const res = await call(() => limited(() => fetch(base + path, { headers })))
|
|
90
|
-
if (!res.ok)
|
|
114
|
+
if (!res.ok) throw await failed(res, `GET ${path}`)
|
|
91
115
|
return res
|
|
92
116
|
},
|
|
93
117
|
async upload (path, data, name, extra = {}) {
|
|
118
|
+
orgOf(path)
|
|
94
119
|
for (let attempt = 0; ; attempt++) {
|
|
95
120
|
// a dropped connection (flaky Wi-Fi, CI runner hiccup) is tried again like a 5xx; the next share resumes otherwise
|
|
96
121
|
let res
|
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,20 @@ 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 = '', ...options } = {}) {
|
|
74
|
+
super([message, next].filter(Boolean).join(' '), options)
|
|
75
|
+
this.reason = message
|
|
76
|
+
this.next = next || ''
|
|
77
|
+
}
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
/** A command used the wrong way (unknown option, missing argument): exit code 2 and the command's help, no support line. */
|
|
81
|
+
export class UsageError extends UserError {}
|
|
69
82
|
|
|
70
83
|
/** Every CLI error ends with a way to a person (docs/HARDENING.md phase 6, item 12). */
|
|
71
84
|
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 } from './util.mjs'
|
|
12
12
|
import { videoTools, encoderCaps, ffmpeg, preset, installHint, outputSize } from './video.mjs'
|
|
13
13
|
import { formatTrace } from './trace.mjs'
|
|
14
14
|
|
|
@@ -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)
|