kaloko 0.5.1 → 0.6.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 +103 -0
- package/package.json +1 -1
- package/schemas/changelog.v1.json +83 -0
- package/schemas/config.v1.json +46 -0
- package/schemas/run.v1.json +27 -0
- package/schemas/scenario.v1.json +14 -1
- package/skills/kaloko/SKILL.md +39 -4
- package/skills/kaloko/reference/design.md +55 -0
- package/skills/kaloko/reference/docs.md +246 -0
- package/skills/kaloko/reference/integrations.md +67 -0
- package/skills/kaloko/reference/mcp.md +10 -1
- package/src/cli.mjs +304 -10
- package/src/lib/changelog-core.mjs +323 -0
- package/src/lib/changelog.mjs +399 -0
- package/src/lib/ci.mjs +71 -0
- package/src/lib/design-cli.mjs +22 -2
- package/src/lib/design-notes.mjs +191 -0
- package/src/lib/docs-core.mjs +450 -0
- package/src/lib/docs-export.mjs +271 -0
- package/src/lib/docs.mjs +221 -0
- package/src/lib/doctor.mjs +6 -1
- package/src/lib/figma.mjs +584 -0
- package/src/lib/git.mjs +23 -3
- package/src/lib/integrations-cli.mjs +211 -0
- package/src/lib/run.mjs +2 -2
- package/src/lib/screens-cli.mjs +36 -0
- package/src/lib/service.mjs +20 -1
- package/viewer/index.html +266 -30
package/src/cli.mjs
CHANGED
|
@@ -36,6 +36,7 @@ import { upsertPrComment } from './lib/pr.mjs'
|
|
|
36
36
|
import { formatReviewQueue, shareReviewLine } from './lib/review.mjs'
|
|
37
37
|
import { htmlViewAllowed } from './lib/htmlview.mjs'
|
|
38
38
|
import { service, serviceUrl } from './lib/service.mjs'
|
|
39
|
+
import { issueCmd, scheduleCmd } from './lib/integrations-cli.mjs'
|
|
39
40
|
import { login } from './lib/login.mjs'
|
|
40
41
|
import { initNextSteps, loginNextSteps } from './lib/onboarding.mjs'
|
|
41
42
|
import { specCmd, driftCmd } from './lib/spec-cli.mjs'
|
|
@@ -45,7 +46,8 @@ import { formatAffected, selectedIds } from './lib/affected-core.mjs'
|
|
|
45
46
|
import { healStep, formatHeal } from './lib/heal.mjs'
|
|
46
47
|
import { readSource, stepFor, viewportFor, adhocScenario, writeImported, attachImported, keepVideos, slug } from './lib/import.mjs'
|
|
47
48
|
import { findPreview, pullRequestOf, envLines, writeEnvFile, githubWorkflow, gitlabJob, addPreviewEnvironment } from './lib/ci.mjs'
|
|
48
|
-
import { walkDesign, syncDesign, publishVersion, historyCmd, referenceCmd, tokensCmd, exportLive, localRevisions, branchCmd, uploadBlobs } from './lib/design-cli.mjs'
|
|
49
|
+
import { walkDesign, syncDesign, publishVersion, previewNotes, historyCmd, referenceCmd, tokensCmd, exportLive, localRevisions, branchCmd, uploadBlobs } from './lib/design-cli.mjs'
|
|
50
|
+
import { screensCmd } from './lib/screens-cli.mjs'
|
|
49
51
|
import { designDir, readBase, headKey } from './lib/design.mjs'
|
|
50
52
|
import { lintDesign, formatLint } from './lib/design-lint.mjs'
|
|
51
53
|
import { exportDesign } from './lib/design-export.mjs'
|
|
@@ -69,15 +71,21 @@ Project
|
|
|
69
71
|
draft <url> [url…] [--id SITE-X] [--title t] [--out qa/flows/site.yml] [--force]
|
|
70
72
|
First plan + read-only production scenario for public pages, drafted from the
|
|
71
73
|
pages (title, description, H1) with the seo, a11y, perf and console packs
|
|
72
|
-
init [--agent claude|codex|all] [--org <slug>] [--no-detect] [--no-scenario]
|
|
74
|
+
init [--agent claude|codex|all] [--org <slug>] [--no-detect] [--no-scenario] [--docs=off|changelog|full]
|
|
73
75
|
Create ${CONFIG_FILE} and install the kaloko skill for your agent; detects the
|
|
74
76
|
framework, dev server URL, locales and main routes, and drafts a first scenario
|
|
75
|
-
(qa/flows/main-pages.yml) when the project has none
|
|
77
|
+
(qa/flows/main-pages.yml) when the project has none; asks "Keep a visual changelog?"
|
|
78
|
+
(--docs: changelog adds CHANGELOG.md and the config block, full also docs/ folders
|
|
79
|
+
with Diátaxis templates: tutorials, how-to, reference, explanation)
|
|
76
80
|
ci github|gitlab [--scenario a.yml,b.yml] [--env preview] [--trigger deployment|pull_request] [--no-affected]
|
|
77
81
|
[--out file] [--print] [--force]
|
|
78
82
|
Write a workflow that walks what each pull request affects on its preview
|
|
79
83
|
deployment (Vercel, Netlify, Cloudflare Pages, review apps) and comments on it;
|
|
80
84
|
adds the preview environment (base_url \${KALOKO_PREVIEW_URL}) to the config
|
|
85
|
+
ci github --release [--snippet] [--print] [--force]
|
|
86
|
+
Write .github/workflows/kaloko-release.yml: when release-please, changesets or
|
|
87
|
+
semantic-release publishes a GitHub release, kaloko release records it on Kaloko
|
|
88
|
+
(--snippet prints the step to add after the tool's own step instead)
|
|
81
89
|
ci preview-url [--wait 600] [--github-env] [--dotenv file]
|
|
82
90
|
In CI: the preview URL (KALOKO_PREVIEW_URL, the deployment event, provider
|
|
83
91
|
variables or the commit's GitHub deployments), the pull request and its base
|
|
@@ -227,10 +235,60 @@ Results
|
|
|
227
235
|
stability --pull [--scenario id] [--json]
|
|
228
236
|
Regions people drew on screenshots on kaloko.app (step detail → Page → Draw a
|
|
229
237
|
region) go into the scenario's ignore: of their steps; walk them again after
|
|
238
|
+
screens [words] [--project p] [--product p] [--module m] [--scenario id] [--locale l] [--viewport v] [--device d]
|
|
239
|
+
[--status pass|partial|fail|skip] [--environment e] [--designs] [--all] [--json] [--cursor c]
|
|
240
|
+
The screen library: the newest screenshot of every step across your flows, searched
|
|
241
|
+
by title, text, URL or id, with the canvas link of each (--designs adds design versions)
|
|
230
242
|
compare [--run id] [--baseline id | --design] [--json]
|
|
231
243
|
Text diff of two runs: step statuses, criterion verdicts, scores and evidence
|
|
232
244
|
(default: current run against the scenario baseline, else the previous run)
|
|
233
245
|
|
|
246
|
+
Jira and Linear (connected by an admin in Settings → Integrations)
|
|
247
|
+
issue create --step <id> --to jira|linear [--criterion AC2] [--title "…"] [--note "…"] [--run id] [--json]
|
|
248
|
+
An issue from a returned step or a failing criterion: the screenshot, the
|
|
249
|
+
criterion, the reviewers' notes and a link back; the step's comment is resolved
|
|
250
|
+
when the issue is done. Branches and PR titles that name a key (WEB-12) link runs
|
|
251
|
+
issue list [--run id] [--scenario id] [--state open|done|canceled] [--json]
|
|
252
|
+
|
|
253
|
+
Hosted runs (read-only scenarios on Kaloko's browsers, Business plan)
|
|
254
|
+
schedule add --scenario <file> --env <name> [--every day|week|manual] [--at 5] [--weekday mon] [--account key]
|
|
255
|
+
Kaloko opens the scenario's pages every day or week (hour in UTC) and shares the
|
|
256
|
+
run as you: public pages, HTTP Basic from the environment, or a saved session
|
|
257
|
+
(--account). Steps that click or type are left out; they need the full walk
|
|
258
|
+
schedule list | run <id> | pause <id> | resume <id> | remove <id> [--json]
|
|
259
|
+
schedule export github --scenario <file> --env <name> [--every day|week] [--at 5] [--out file] [--print]
|
|
260
|
+
A GitHub Actions workflow that walks the scenario on a schedule with the full CLI
|
|
261
|
+
|
|
262
|
+
Changelog (Keep a Changelog in CHANGELOG.md; pictures are step references kaloko:<scenario>/<step>)
|
|
263
|
+
changelog draft [--product p] [--since <tag|version>] [--apply] [--json] [--offline]
|
|
264
|
+
The Unreleased entry since the last release: merged pull requests and commits
|
|
265
|
+
(Conventional Commits when used, gh for titles) grouped Added / Changed / Fixed /
|
|
266
|
+
Removed, plus before/after pictures of the screens accepted runs show differently;
|
|
267
|
+
printed, or --apply writes it into CHANGELOG.md (not when a release tool owns it)
|
|
268
|
+
release [<version>] [--product p] [--date YYYY-MM-DD] [--runs id,id] [--no-write] [--no-docs] [--dry-run] [--json]
|
|
269
|
+
Record a release on Kaloko: the version (default: from git tags, package.json,
|
|
270
|
+
release-please or changesets), its CHANGELOG.md entry (Unreleased becomes the
|
|
271
|
+
version unless --no-write) and the accepted runs that are its pictures; prints the
|
|
272
|
+
illustrated changelog page. The docs folder is published as that version's docs
|
|
273
|
+
(Business) unless --no-docs
|
|
274
|
+
|
|
275
|
+
Docs (Markdown in docs/ with step references; Diátaxis folders; translations as guide.cs.md next to guide.md)
|
|
276
|
+
docs publish [--product p] [--version latest|<v>] [--reviewed] [--dry-run] [--json]
|
|
277
|
+
Send the docs folder to Kaloko as the docs of a version (default latest): readers
|
|
278
|
+
get a version selector, language and screen switches, search and training mode;
|
|
279
|
+
--reviewed says the text was checked, which clears its stale notes
|
|
280
|
+
docs check [--product p] [--version v] [--offline] [--strict] [--json]
|
|
281
|
+
Broken or unknown step references, stale sections (the screen changed since the
|
|
282
|
+
text was published), missing or outdated translations and user-visible changelog
|
|
283
|
+
items without a picture; exit 1 on references or stale sections (--strict: on
|
|
284
|
+
anything)
|
|
285
|
+
export --format md|docusaurus|vitepress|mkdocs|html|pdf|json [--product p] [--version v] [--lang l]
|
|
286
|
+
[--what all|docs|changelog] [--local] [--out dir|file]
|
|
287
|
+
The docs and changelog with their pictures as files: Markdown with an assets
|
|
288
|
+
folder, a Docusaurus, VitePress or MkDocs folder with sidebars, offline HTML, PDF
|
|
289
|
+
(printed by the browser) or the changelog as JSON; --local exports the files in
|
|
290
|
+
the repository instead of the published ones
|
|
291
|
+
|
|
234
292
|
Design (environments with kind: design; live HTML steps in a folder)
|
|
235
293
|
start --scenario <file> --env <design env> A run of the design folder (same scenario as development and QA)
|
|
236
294
|
walk [--steps a,b] [--no-cache] Render the steps; unchanged revisions reuse their captures
|
|
@@ -241,7 +299,15 @@ Design (environments with kind: design; live HTML steps in a folder)
|
|
|
241
299
|
design lint [--env e | --dir design/] [--scenario file] [--all] [--strict] [--json]
|
|
242
300
|
Check the folder against the portable HTML rules (relative URLs, no CDNs, no storage,
|
|
243
301
|
deterministic content, viewport, lang, links that also work from disk); --strict fails on warnings
|
|
244
|
-
share [--name <text>] [--branch b]
|
|
302
|
+
share [--name <text>] [--branch b] [--notes file|-] [--notes-preview]
|
|
303
|
+
Publish the draft as a version of the flow (a run): "v4 · 3 changed, 17 unchanged",
|
|
304
|
+
with "what changed" notes per step (screens per viewport, text, tokens); --notes-preview
|
|
305
|
+
prints them without publishing, --notes replaces them (a summary, then "step: text" lines)
|
|
306
|
+
design import figma <file-url|key|export folder> [--scenario file] [--frames a,b] [--page p] [--map frames.json]
|
|
307
|
+
[--token-env FIGMA_TOKEN] [--tokens auto|write|skip] [--force] [--dry-run] [--publish [--notes file]]
|
|
308
|
+
Figma frames become design steps (frame PNG at 1× and 2×, text layers with their
|
|
309
|
+
typography, variables or styles as tokens.json) matched to steps by name or --map;
|
|
310
|
+
a folder exported from Figma works without a token. One way: no prototype links
|
|
245
311
|
history [--scenario id] [--step id] [--locale l] [--json]
|
|
246
312
|
Versions of the flow; revisions of a step with who (person or agent) and when
|
|
247
313
|
spec [step] [--ref design|draft|vN|<run id>] [--locale l] [--viewport v] [--local] [--json]
|
|
@@ -381,6 +447,49 @@ function stepOf (scenario, id) {
|
|
|
381
447
|
}
|
|
382
448
|
|
|
383
449
|
/** Scenario of --scenario (id or file) or of the current run. */
|
|
450
|
+
/**
|
|
451
|
+
* kaloko design import figma <file-url|key|folder>: Figma frames (or a folder exported from Figma) become the steps of
|
|
452
|
+
* the design folder; --publish then walks them and publishes a version (the design reference to be).
|
|
453
|
+
*/
|
|
454
|
+
async function designImport (args) {
|
|
455
|
+
const [, kind, source] = args._
|
|
456
|
+
if (kind !== 'figma' || !source) throw new UserError('Usage: kaloko design import figma <file-url|key|export folder> [--scenario file] [--env <design env>] [--frames a,b] [--page name] [--map frames.json|yml] [--token-env FIGMA_TOKEN] [--tokens auto|write|skip] [--force] [--dry-run] [--publish [--notes file]]')
|
|
457
|
+
const { config, root } = await context(args)
|
|
458
|
+
let run = null
|
|
459
|
+
try { run = await resolveRun({ root, output: config.output, run: args.run }) } catch {}
|
|
460
|
+
const runEnv = run ? environment(config, run.meta.environment.name) : null
|
|
461
|
+
const env = typeof args.env === 'string' ? environment(config, args.env) : runEnv?.kind === 'design' ? runEnv : Object.keys(config.environments).map((n) => environment(config, n)).find((e) => e.kind === 'design')
|
|
462
|
+
if (!env || env.kind !== 'design') throw new UserError('No design environment: add one with kind: design and serve: design/ to kaloko.config.yml, or pass --env <design env>.')
|
|
463
|
+
const scenarioFile = typeof args.scenario === 'string' ? resolve(args.scenario) : run && runEnv?.kind === 'design' ? resolve(root, run.meta.scenario_file) : null
|
|
464
|
+
if (!scenarioFile) throw new UserError('Name the scenario whose steps the frames are: --scenario <file>.')
|
|
465
|
+
const scenario = await loadScenario(scenarioFile)
|
|
466
|
+
const { importFigma, formatImport, readMap } = await import('./lib/figma.mjs')
|
|
467
|
+
const scope = startScope({ args: {}, scenario, config, env })
|
|
468
|
+
const viewports = Object.fromEntries(baseViewports(scope).map((v) => [v, scope.viewports[v]]))
|
|
469
|
+
const tokenVar = typeof args['token-env'] === 'string' ? args['token-env'] : typeof args.token === 'string' && /^[A-Z_][A-Z0-9_]*$/.test(args.token) ? args.token : 'FIGMA_TOKEN'
|
|
470
|
+
const dir = typeof args.dir === 'string' ? resolve(args.dir) : designDir(env, root)
|
|
471
|
+
const r = await importFigma({
|
|
472
|
+
source, scenario, viewports, locales: scenario.locales || ['default'], dir,
|
|
473
|
+
token: process.env[tokenVar] || (tokenVar === 'FIGMA_TOKEN' ? process.env.FIGMA_ACCESS_TOKEN : null) || null,
|
|
474
|
+
map: typeof args.map === 'string' ? await readMap(resolve(args.map)) : null, only: list(args.frames), page: typeof args.page === 'string' ? args.page : null,
|
|
475
|
+
tokensMode: ['write', 'skip'].includes(args.tokens) ? args.tokens : 'auto', force: !!args.force, dryRun: !!args['dry-run'], say
|
|
476
|
+
})
|
|
477
|
+
if (args.json) say(JSON.stringify(r, null, 2))
|
|
478
|
+
else say(formatImport(r, { dir: relative(process.cwd(), dir) || '.' }))
|
|
479
|
+
if (args['dry-run']) return r
|
|
480
|
+
const lint = await lintDesign({ dir, scenario })
|
|
481
|
+
if (lint.errors) say(`\n${formatLint(lint, { max: 10 })}`)
|
|
482
|
+
if (!args.publish) {
|
|
483
|
+
say(`\nNext: kaloko start --scenario ${relative(process.cwd(), scenarioFile)} --env ${env.name} && kaloko walk && kaloko share (or rerun with --publish).\nAccept the version on the canvas and set it as the design reference (kaloko reference --design --version <n>): implementation runs with packs: [design] are then checked against these frames.`)
|
|
484
|
+
return r
|
|
485
|
+
}
|
|
486
|
+
await commands.start({ ...args, _: [], scenario: scenarioFile, env: env.name })
|
|
487
|
+
const current = await resolveRun({ root, output: config.output })
|
|
488
|
+
await commands.walk({ _: [], run: current.dir })
|
|
489
|
+
await commands.share({ _: [], run: current.dir, ...(typeof args.notes === 'string' ? { notes: args.notes } : {}), ...(typeof args.name === 'string' ? { name: args.name } : {}), ...(typeof args.branch === 'string' ? { branch: args.branch } : {}) })
|
|
490
|
+
return r
|
|
491
|
+
}
|
|
492
|
+
|
|
384
493
|
async function scenarioIdOf (args, config, root) {
|
|
385
494
|
if (typeof args.scenario === 'string') {
|
|
386
495
|
if (/\.ya?ml$/.test(args.scenario)) return (await loadScenario(resolve(args.scenario))).id
|
|
@@ -773,6 +882,38 @@ const commands = {
|
|
|
773
882
|
await writeFile(file, tpl)
|
|
774
883
|
say(`Created ${CONFIG_FILE}${detected ? ` (local: ${detected.url})` : ''}. Edit org and the staging and production environments.`)
|
|
775
884
|
} else if (!existsSync(join(cwd, LEGACY_CONFIG_FILE))) say(`${CONFIG_FILE} already exists; kept.`)
|
|
885
|
+
// the visual changelog (docs/DOCS-CHANGELOG.md): asked once; --docs=off|changelog|full for scripts and agents
|
|
886
|
+
let docsMode = typeof args.docs === 'string' ? args.docs : null
|
|
887
|
+
if (docsMode && !['off', 'changelog', 'full'].includes(docsMode)) throw new UserError('--docs must be off, changelog or full.')
|
|
888
|
+
if (!docsMode && process.stdin.isTTY && process.stdout.isTTY && !process.env.CI && !process.env.NODE_TEST_CONTEXT) {
|
|
889
|
+
const rl = (await import('node:readline/promises')).createInterface({ input: process.stdin, output: process.stdout })
|
|
890
|
+
const a = (await rl.question('Keep a visual changelog? (recommended) [Y/n] ')).trim().toLowerCase(); rl.close()
|
|
891
|
+
docsMode = a === 'n' || a === 'no' ? 'off' : 'changelog'
|
|
892
|
+
}
|
|
893
|
+
if (docsMode && docsMode !== 'off') {
|
|
894
|
+
const { docsConfigBlock, detectProducts, DOCS_SKELETON } = await import('./lib/changelog.mjs')
|
|
895
|
+
const { CHANGELOG_SKELETON } = await import('./lib/changelog-core.mjs')
|
|
896
|
+
const { mkdir: mkdirp } = await import('node:fs/promises')
|
|
897
|
+
const cfgFile = existsSync(file) ? file : join(cwd, LEGACY_CONFIG_FILE)
|
|
898
|
+
const cfg = await readFile(cfgFile, 'utf8')
|
|
899
|
+
const products = /^products:/m.test(cfg) ? [] : await detectProducts(cwd)
|
|
900
|
+
if (!/^docs:/m.test(cfg) && !/^products:/m.test(cfg)) { await writeFile(cfgFile, `${cfg.replace(/\n*$/, '\n')}${docsConfigBlock({ products, full: docsMode === 'full' })}`); say(`Added the changelog settings to ${relative(cwd, cfgFile)}${products.length ? ` with ${products.length} products (${products.map((p) => p.slug).join(', ')})` : ''}`) }
|
|
901
|
+
for (const f of products.length ? products.map((p) => join(cwd, p.dir, 'CHANGELOG.md')) : [join(cwd, 'CHANGELOG.md')]) {
|
|
902
|
+
if (existsSync(f)) continue
|
|
903
|
+
await mkdirp(dirname(f), { recursive: true })
|
|
904
|
+
await writeFile(f, CHANGELOG_SKELETON)
|
|
905
|
+
say(`Created ${relative(cwd, f)} (Keep a Changelog; kaloko changelog draft fills Unreleased, kaloko release records a version)`)
|
|
906
|
+
}
|
|
907
|
+
if (docsMode === 'full') {
|
|
908
|
+
for (const [rel, text] of Object.entries(DOCS_SKELETON)) {
|
|
909
|
+
const f = join(cwd, rel)
|
|
910
|
+
if (existsSync(f)) continue
|
|
911
|
+
await mkdirp(dirname(f), { recursive: true })
|
|
912
|
+
await writeFile(f, text)
|
|
913
|
+
}
|
|
914
|
+
say('Docs folders in docs/ (tutorials, how-to, reference, explanation) with an index and a template in each; kaloko docs publish sends them, kaloko docs check checks them')
|
|
915
|
+
}
|
|
916
|
+
} else if (!docsMode) say('Visual changelog: kaloko init --docs=changelog adds CHANGELOG.md and the config block (recommended).')
|
|
776
917
|
const agent = typeof args.agent === 'string' ? args.agent : 'none'
|
|
777
918
|
if (!['claude', 'codex', 'all', 'none'].includes(agent)) throw new UserError('--agent must be claude, codex, all or none.')
|
|
778
919
|
const { cp, mkdir, appendFile, rm } = await import('node:fs/promises')
|
|
@@ -875,9 +1016,11 @@ const commands = {
|
|
|
875
1016
|
const vpNames = baseViewports(scope)
|
|
876
1017
|
const git = await gitMeta(root)
|
|
877
1018
|
const executor = { kind: args.executor ? 'agent' : 'cli', name: args.executor || 'kaloko-cli' }
|
|
878
|
-
const { project, issue } = await projectAndIssue({ cwd: root, config, scenario, branch: git?.branch })
|
|
1019
|
+
const { project, issue } = await projectAndIssue({ cwd: root, config, scenario, branch: git?.branch, title: git?.pr?.title || null })
|
|
879
1020
|
const appVer = typeof args['app-version'] === 'string' ? args['app-version'].slice(0, 80) : await appVersion(root)
|
|
880
|
-
|
|
1021
|
+
// the product the flow documents (visual changelog): its tag, or the only product of the config
|
|
1022
|
+
const product = scenario.product || (Object.keys(config.products || {}).length === 1 ? Object.keys(config.products)[0] : null)
|
|
1023
|
+
const { dir, meta } = await createRun({ output: config.output, scenario, scenarioFile, env, scope, git, executor, root, project, issue, appVersion: appVer, product })
|
|
881
1024
|
say(`Run ${meta.id}\n dir: ${relative(process.cwd(), dir)}\n env: ${env.name}${env.readonly ? ' (readonly)' : ''}\n locales: ${locales.join(', ')} viewports: ${vpNames.length > 12 ? `${vpNames.slice(0, 12).join(', ')} … (${vpNames.length})` : vpNames.join(', ')}${dimensionLine(scope)}\n project: ${project.slug}${issue ? ` issue: ${issue.repo ? issue.repo + '#' : '#'}${issue.number}` : ''}`)
|
|
882
1025
|
const addr = recipient(env.mail, meta.short)
|
|
883
1026
|
if (addr) say(` test e-mail: ${addr}`)
|
|
@@ -1343,8 +1486,32 @@ const commands = {
|
|
|
1343
1486
|
console.log(preview.url)
|
|
1344
1487
|
return
|
|
1345
1488
|
}
|
|
1346
|
-
if (!['github', 'gitlab'].includes(action)) throw new UserError('Usage: kaloko ci github|gitlab [--scenario a.yml,b.yml] [--env preview] [--trigger deployment|pull_request] [--no-affected] [--out file] [--print] [--force]\n kaloko ci preview-url [--wait 600] [--github-env] [--dotenv file]')
|
|
1489
|
+
if (!['github', 'gitlab'].includes(action)) throw new UserError('Usage: kaloko ci github|gitlab [--scenario a.yml,b.yml] [--env preview] [--trigger deployment|pull_request] [--no-affected] [--out file] [--print] [--force]\n kaloko ci github --release [--snippet] [--print] [--force]\n kaloko ci preview-url [--wait 600] [--github-env] [--dotenv file]')
|
|
1347
1490
|
const { config, root, file } = await context(args)
|
|
1491
|
+
if (args.release) {
|
|
1492
|
+
// release-tool integrations: record each published release on Kaloko (docs/DOCS-CHANGELOG.md)
|
|
1493
|
+
if (action !== 'github') throw new UserError('--release writes a GitHub Actions workflow; on GitLab run kaloko release <version> --no-write in your release job.')
|
|
1494
|
+
const { productsOf, detectVersionSource } = await import('./lib/changelog.mjs')
|
|
1495
|
+
const { releaseWorkflow, releaseSnippet } = await import('./lib/ci.mjs')
|
|
1496
|
+
const products = productsOf(config, root)
|
|
1497
|
+
const sources = await Promise.all(products.map((p) => detectVersionSource(root, p)))
|
|
1498
|
+
const tool = sources.map((s) => s.tool).find(Boolean) || null
|
|
1499
|
+
let pkg = null
|
|
1500
|
+
try { pkg = JSON.parse(await readFile(join(root, 'package.json'), 'utf8')) } catch {}
|
|
1501
|
+
const pm = existsSync(join(root, 'pnpm-lock.yaml')) ? 'pnpm' : existsSync(join(root, 'yarn.lock')) ? 'yarn' : existsSync(join(root, 'bun.lockb')) || existsSync(join(root, 'bun.lock')) ? 'bun' : 'npm'
|
|
1502
|
+
const dependency = !!(pkg && ({ ...pkg.dependencies, ...pkg.devDependencies }).kaloko)
|
|
1503
|
+
const version = `^${JSON.parse(await readFile(join(packageRoot, 'package.json'), 'utf8')).version}`
|
|
1504
|
+
if (args.snippet) { say(releaseSnippet({ tool, pm, dependency, version, product: products.length > 1 ? '<product>' : null })); return }
|
|
1505
|
+
const content = releaseWorkflow({ products: products.map((p, i) => ({ slug: products.length > 1 || !p.implicit ? p.slug : null, pattern: sources[i].pattern || 'v*' })), pm, dependency, version, tool })
|
|
1506
|
+
if (args.print) { say(content); return }
|
|
1507
|
+
const out = resolve(root, typeof args.out === 'string' ? args.out : '.github/workflows/kaloko-release.yml')
|
|
1508
|
+
if (existsSync(out) && !args.force) throw new UserError(`${relative(process.cwd(), out)} exists; pass --force to replace it (or --print to compare).`)
|
|
1509
|
+
await mkdir(dirname(out), { recursive: true })
|
|
1510
|
+
await writeFile(out, content)
|
|
1511
|
+
say(`Wrote ${relative(process.cwd(), out)}: every published GitHub release${tool ? ` (${tool} publishes them)` : ''} is recorded on Kaloko with its changelog entry, pictures and docs.`)
|
|
1512
|
+
say('Next:\n add the repository secret KALOKO_TOKEN (tester role, upload scope)\n publish releases with your release tool as before; nothing else changes in your release workflow')
|
|
1513
|
+
return
|
|
1514
|
+
}
|
|
1348
1515
|
const envName = typeof args.env === 'string' ? args.env : 'preview'
|
|
1349
1516
|
const all = (await scenarioFiles(config, root)).map((f) => relative(root, f))
|
|
1350
1517
|
const scenarios = list(args.scenario) || all
|
|
@@ -1521,6 +1688,7 @@ const commands = {
|
|
|
1521
1688
|
if (env.kind === 'design') {
|
|
1522
1689
|
const r = await syncDesign({ config, root, run, scenario, env, client, args: { ...args, strict: true }, say, pull: false, render: (steps) => walkDesign({ config, root, run, scenario, env, pick: steps, locales: run.meta.scope.locales, viewports: run.meta.scope.viewports, say }).then(() => evaluateRun({ run, scenario, config, root })) })
|
|
1523
1690
|
if (r.conflicts) return
|
|
1691
|
+
if (args['notes-preview']) { await previewNotes({ config, scenario, client, args, say }); return }
|
|
1524
1692
|
const v = await publishVersion({ config, scenario, client, args, say })
|
|
1525
1693
|
const meta = await readJson(join(run.dir, 'meta.json'))
|
|
1526
1694
|
meta.shared = { url: v.url, at: new Date().toISOString(), version: v.version, run: v.id }
|
|
@@ -1616,7 +1784,8 @@ const commands = {
|
|
|
1616
1784
|
},
|
|
1617
1785
|
|
|
1618
1786
|
async design (args) {
|
|
1619
|
-
if (args._[0]
|
|
1787
|
+
if (args._[0] === 'import') return designImport(args)
|
|
1788
|
+
if (args._[0] !== 'lint') throw new UserError('Usage: kaloko design lint [--env <design env> | --dir design/] [--scenario file] [--all] [--strict] [--json]\n kaloko design import figma <file-url|key|export folder> [--frames a,b] [--map frames.json] [--token-env FIGMA_TOKEN] [--publish]')
|
|
1620
1789
|
const { config, root } = await context(args)
|
|
1621
1790
|
let run = null
|
|
1622
1791
|
try { run = await resolveRun({ root, output: config.output, run: args.run }) } catch {}
|
|
@@ -1633,6 +1802,12 @@ const commands = {
|
|
|
1633
1802
|
if (report.errors || (args.strict && report.warnings)) process.exitCode = 1
|
|
1634
1803
|
},
|
|
1635
1804
|
|
|
1805
|
+
/** `kaloko screens [words]`: the screen library of the organization (newest capture of every step). */
|
|
1806
|
+
async screens (args) {
|
|
1807
|
+
const { config } = await context(args)
|
|
1808
|
+
await screensCmd({ config, client: service(config), args, say })
|
|
1809
|
+
},
|
|
1810
|
+
|
|
1636
1811
|
async history (args) {
|
|
1637
1812
|
const { config, root } = await context(args)
|
|
1638
1813
|
const scenarioId = await scenarioIdOf(args, config, root)
|
|
@@ -1734,6 +1909,20 @@ const commands = {
|
|
|
1734
1909
|
* The signed acceptance record of an accepted run: JSON and PDF downloaded and checked against the service's public
|
|
1735
1910
|
* key; --verify checks a record someone handed over (no token, no project needed).
|
|
1736
1911
|
*/
|
|
1912
|
+
/** Jira and Linear (#21): an issue from a step of a shared run, and the issues created so far. */
|
|
1913
|
+
async issue (args) {
|
|
1914
|
+
const { config, root } = await context(args)
|
|
1915
|
+
let runId = null
|
|
1916
|
+
if (args._[0] === 'create' && typeof args.run !== 'string') { try { const r = await resolveRun({ root, output: config.output }); if (r.meta.shared) runId = r.meta.id } catch {} }
|
|
1917
|
+
return issueCmd({ args, config, client: service(config), runId, say })
|
|
1918
|
+
},
|
|
1919
|
+
|
|
1920
|
+
/** Hosted runs (#16): schedules of read-only scenarios on Kaloko's browsers, or a scheduled GitHub workflow. */
|
|
1921
|
+
async schedule (args) {
|
|
1922
|
+
const { config, root } = await context(args)
|
|
1923
|
+
return scheduleCmd({ args, config, root, say, deps: { loadScenario, environment, baseUrl, projectAndIssue, packageRoot, service } })
|
|
1924
|
+
},
|
|
1925
|
+
|
|
1737
1926
|
async signoff (args) {
|
|
1738
1927
|
if (typeof args.verify === 'string') {
|
|
1739
1928
|
const config = await context(args).then((c) => c.config).catch(() => ({}))
|
|
@@ -1992,6 +2181,27 @@ const commands = {
|
|
|
1992
2181
|
return
|
|
1993
2182
|
}
|
|
1994
2183
|
if (typeof args.scenario === 'string') return exportScenario({ args, config, root, client })
|
|
2184
|
+
if (typeof args.format === 'string') {
|
|
2185
|
+
// living docs and the changelog as files (docs/DOCS-CHANGELOG.md → Exports)
|
|
2186
|
+
if (!config.org) throw new UserError('Set org: in the config first.')
|
|
2187
|
+
const { pickProduct } = await import('./lib/changelog.mjs')
|
|
2188
|
+
const { exportDocs } = await import('./lib/docs.mjs')
|
|
2189
|
+
const product = pickProduct(config, root, args.product)
|
|
2190
|
+
const printPdf = async (htmlFile, outFile) => {
|
|
2191
|
+
const { chromium } = await import('playwright')
|
|
2192
|
+
const browser = await chromium.launch(config.browser?.channel ? { channel: config.browser.channel } : {}).catch(() => chromium.launch())
|
|
2193
|
+
try {
|
|
2194
|
+
const page = await browser.newPage()
|
|
2195
|
+
await page.goto(pathToFileURL(htmlFile).href, { waitUntil: 'load', timeout: 60000 })
|
|
2196
|
+
await page.pdf({ path: outFile, format: 'A4', printBackground: true, margin: { top: '16mm', bottom: '16mm', left: '14mm', right: '14mm' } })
|
|
2197
|
+
} finally { await browser.close() }
|
|
2198
|
+
}
|
|
2199
|
+
const r = await exportDocs({ root, config, product, client, format: args.format, version: typeof args.version === 'string' ? args.version : 'latest', lang: typeof args.lang === 'string' ? args.lang : null, what: ['docs', 'changelog'].includes(args.what) ? args.what : 'all', out: typeof args.out === 'string' ? resolve(args.out) : null, local: !!args.local, printPdf: args['server-pdf'] ? null : printPdf, say })
|
|
2200
|
+
if (r.kind === 'file') { say(`Exported ${product.slug} as ${r.format} → ${relative(process.cwd(), r.file) || r.file}`); return }
|
|
2201
|
+
const m = r.manifest
|
|
2202
|
+
say(`Exported ${product.slug} ${m?.version || ''} as ${r.format} → ${relative(process.cwd(), r.dir) || r.dir}\n ${m ? `${m.pages} page(s), ${m.releases} release(s), ${m.pictures} picture(s)` : `${r.files} file(s)`}${m?.missing?.length ? `\n ! no accepted picture for: ${m.missing.slice(0, 8).join(', ')}${m.missing.length > 8 ? ' …' : ''}` : ''}`)
|
|
2203
|
+
return
|
|
2204
|
+
}
|
|
1995
2205
|
let runId = args.run
|
|
1996
2206
|
if (!runId) { try { runId = (await resolveRun({ root, output: config.output })).meta.id } catch {} }
|
|
1997
2207
|
if (!runId || !/^[0-9]{4}-[0-9]{2}-[0-9]{2}-[a-z0-9-]+$/.test(runId)) throw new UserError('--run <id> is required (a shared run id).')
|
|
@@ -2019,12 +2229,96 @@ const commands = {
|
|
|
2019
2229
|
const { config, root, file } = await context(args)
|
|
2020
2230
|
// engines the scenarios ask for (browsers: [webkit, firefox]), so doctor checks they are installed
|
|
2021
2231
|
const engines = []
|
|
2022
|
-
|
|
2023
|
-
const
|
|
2232
|
+
const scenarios = []
|
|
2233
|
+
for (const f of await scenarioFiles(config, root).catch(() => [])) { try { const s = yaml.load(await readFile(f, 'utf8')) || {}; engines.push(...(s.browsers || [])); scenarios.push({ file: relative(root, f), product: s.product || null, module: s.module || null }) } catch {} }
|
|
2234
|
+
const r = await doctor({ config, root, file, fix: !!args.fix, engines, scenarios })
|
|
2024
2235
|
say(args.json ? JSON.stringify(r, null, 2) : formatDoctor(r))
|
|
2025
2236
|
if (!r.ok) process.exitCode = 1
|
|
2026
2237
|
},
|
|
2027
2238
|
|
|
2239
|
+
/** `kaloko changelog draft`: the Unreleased entry from merged work and the screens accepted runs show differently. */
|
|
2240
|
+
async changelog (args) {
|
|
2241
|
+
const sub = args._[0]
|
|
2242
|
+
if (sub !== 'draft') throw new UserError('Usage: kaloko changelog draft [--product p] [--since <tag|version>] [--apply] [--json] [--offline]')
|
|
2243
|
+
const { config, root } = await context(args)
|
|
2244
|
+
const { pickProduct, draftChangelog, draftText, applyToChangelog } = await import('./lib/changelog.mjs')
|
|
2245
|
+
const product = pickProduct(config, root, args.product)
|
|
2246
|
+
let client = null
|
|
2247
|
+
if (!args.offline) { try { client = service(config) } catch {} }
|
|
2248
|
+
const d = await draftChangelog({ root, config, product, since: typeof args.since === 'string' ? args.since : null, client })
|
|
2249
|
+
if (args.json) { say(JSON.stringify(d, null, 2)); return }
|
|
2250
|
+
const pictures = d.changes.reduce((n, s) => n + (s.steps?.length || 0), 0)
|
|
2251
|
+
const head = `${product.slug}: ${d.commits} change(s) since ${d.since.ref || 'the first commit'}${d.service === 'ok' ? `, ${pictures} changed screen(s) in accepted runs since ${d.previous ? `release ${d.previous.version}` : 'the first accepted runs'}` : d.service === 'offline' ? ' (no service: pictures of changed screens need a token)' : ` (service: ${d.service})`}; ${d.internal.length} internal left out.`
|
|
2252
|
+
if (args.apply) {
|
|
2253
|
+
const r = await applyToChangelog(d)
|
|
2254
|
+
say(`${head}\n${r.created ? 'Created' : 'Updated'} ${relative(process.cwd(), d.file) || d.file}: ${r.added} new item(s) in Unreleased. Edit them for people, then kaloko release <version>.`)
|
|
2255
|
+
return
|
|
2256
|
+
}
|
|
2257
|
+
console.error(head)
|
|
2258
|
+
say(draftText(d))
|
|
2259
|
+
},
|
|
2260
|
+
|
|
2261
|
+
/** `kaloko docs publish|check`: the docs folder on Kaloko per version, and what in it needs attention. */
|
|
2262
|
+
async docs (args) {
|
|
2263
|
+
const sub = args._[0]
|
|
2264
|
+
if (!['publish', 'check'].includes(sub)) throw new UserError('Usage: kaloko docs publish [--product p] [--version latest|<v>] [--reviewed] [--dry-run] [--json]\n kaloko docs check [--product p] [--version v] [--offline] [--strict] [--json]')
|
|
2265
|
+
const { config, root } = await context(args)
|
|
2266
|
+
const { pickProduct } = await import('./lib/changelog.mjs')
|
|
2267
|
+
const docs = await import('./lib/docs.mjs')
|
|
2268
|
+
const product = pickProduct(config, root, args.product)
|
|
2269
|
+
const version = typeof args.version === 'string' ? args.version : 'latest'
|
|
2270
|
+
if (sub === 'publish') {
|
|
2271
|
+
if (!config.org && !args['dry-run']) throw new UserError('Set org: in the config first.')
|
|
2272
|
+
const r = await docs.publishDocs({ root, config, product, client: args['dry-run'] ? null : service(config), version, reviewed: !!args.reviewed, dryRun: !!args['dry-run'] })
|
|
2273
|
+
if (args.json) { say(JSON.stringify(r, null, 2)); return }
|
|
2274
|
+
if (r.dry_run) { say(`${product.slug}: ${r.pages} page(s) from ${r.dir} would go to Kaloko as ${r.version} (${r.languages.join(', ')}); dry run, nothing sent.${r.broken.length ? `\n ! broken step references: ${r.broken.join('; ')}` : ''}`); return }
|
|
2275
|
+
const lines = [`${product.slug} docs ${r.version}: ${r.pages} page(s) from ${r.dir}, ${r.changed} changed, ${r.unchanged} unchanged${r.removed ? `, ${r.removed} removed` : ''}${args.reviewed ? ' (marked as checked)' : ''}`, ` ${r.url}`]
|
|
2276
|
+
for (const b of r.broken_refs || []) lines.push(` ! broken step reference in ${b.path || 'index'}${b.lang ? ` (${b.lang})` : ''}: ${b.ref}`)
|
|
2277
|
+
for (const u of (r.unresolved || []).slice(0, 10)) lines.push(` ! no accepted picture yet: ${u.ref} (${u.path || 'index'})`)
|
|
2278
|
+
for (const n of r.notes || []) lines.push(` ! ${n}`)
|
|
2279
|
+
say(lines.join('\n'))
|
|
2280
|
+
return
|
|
2281
|
+
}
|
|
2282
|
+
// check: the scenario files say which steps exist
|
|
2283
|
+
const scenarios = new Map()
|
|
2284
|
+
for (const f of await scenarioFiles(config, root).catch(() => [])) { try { const s = yaml.load(await readFile(f, 'utf8')) || {}; if (s.id) scenarios.set(String(s.id), new Set((s.nodes || []).map((n) => String(n.id)))) } catch {} }
|
|
2285
|
+
let client = null
|
|
2286
|
+
if (!args.offline) { try { client = service(config) } catch {} }
|
|
2287
|
+
const r = await docs.docsCheck({ root, config, product, client, scenarios: scenarios.size ? scenarios : null, version })
|
|
2288
|
+
say(args.json ? JSON.stringify(r, null, 2) : docs.formatCheck(r))
|
|
2289
|
+
if (!r.ok || (args.strict && r.warnings)) process.exitCode = 1
|
|
2290
|
+
},
|
|
2291
|
+
|
|
2292
|
+
/** `kaloko release <version>`: the version's changelog entry and its reference runs on the service. */
|
|
2293
|
+
async release (args) {
|
|
2294
|
+
const { config, root } = await context(args)
|
|
2295
|
+
if (!config.org) throw new UserError('Set org: in the config first.')
|
|
2296
|
+
const { pickProduct, releaseProduct } = await import('./lib/changelog.mjs')
|
|
2297
|
+
const product = pickProduct(config, root, args.product)
|
|
2298
|
+
const client = args['dry-run'] ? null : service(config)
|
|
2299
|
+
const r = await releaseProduct({ root, config, product, version: args._[0] || null, date: typeof args.date === 'string' ? args.date : null, client, write: !args['no-write'], runs: list(args.runs), dryRun: !!args['dry-run'] })
|
|
2300
|
+
// the docs of this version: what the docs folder says now (Business; latest stays as kaloko docs publish left it)
|
|
2301
|
+
if (r.release && !args['no-docs']) {
|
|
2302
|
+
const docs = await import('./lib/docs.mjs')
|
|
2303
|
+
const pages = await docs.collectDocs(root, product).then((x) => x.pages.length).catch(() => 0)
|
|
2304
|
+
if (pages) {
|
|
2305
|
+
try { r.docs = await docs.publishDocs({ root, config, product, client, version: r.version }) } catch (e) { r.docs = { error: String(e.message || e).split('\n')[0] } }
|
|
2306
|
+
}
|
|
2307
|
+
}
|
|
2308
|
+
if (args.json) { say(JSON.stringify(r, null, 2)); return }
|
|
2309
|
+
const file = relative(process.cwd(), r.file) || r.file
|
|
2310
|
+
const lines = [`${product.slug} ${r.version} (${r.date}) · version source: ${r.source.tool || r.source.source}${r.source.path ? ` (${r.source.path})` : ''}`]
|
|
2311
|
+
lines.push(r.section ? ` ${file}: ${r.finalized ? 'Unreleased became' : 'entry'} ${r.version}, ${r.items} item(s), ${r.refs} picture reference(s)` : ` ${file}: no entry for ${r.version}`)
|
|
2312
|
+
if (r.release) {
|
|
2313
|
+
lines.push(` ${r.release.created ? 'Recorded' : 'Updated'} on the service with ${r.release.runs.length} reference run(s)${r.release.versioned_runs ? `; ${r.release.versioned_runs} accepted run(s) now carry ${r.version}` : ''}`)
|
|
2314
|
+
lines.push(` ${r.release.url}`)
|
|
2315
|
+
} else lines.push(' Dry run: nothing sent.')
|
|
2316
|
+
if (r.docs) lines.push(r.docs.error ? ` ! Docs of ${r.version} not published: ${r.docs.error}` : ` Docs of ${r.version}: ${r.docs.pages} page(s)${r.docs.changed ? `, ${r.docs.changed} changed since the last publish of ${r.version}` : ''}`)
|
|
2317
|
+
for (const n of r.notes) lines.push(` ! ${n}`)
|
|
2318
|
+
if (r.finalized) lines.push(` Commit ${file} with the release.`)
|
|
2319
|
+
say(lines.join('\n'))
|
|
2320
|
+
},
|
|
2321
|
+
|
|
2028
2322
|
async help () { say(HELP) }
|
|
2029
2323
|
}
|
|
2030
2324
|
|