Follow

kickstartDS CLI

Everyday design-system chores, as commands

Building on kickstartDS means repeating a handful of tasks: compiling tokens, dereferencing schemas, generating types, producing CMS configuration, previewing components.

The CLI wraps those into single commands, so a project's package.json stays readable and the pipeline stays the same across projects.


FactDetail
CommandsTokens, schemas, types, CMS config, previews
StackTypeScript / Node
StartedMay 2022

What's in it

Shared pipeline

The same commands this site's build runs for tokens, schemas and CMS configuration.

Small surface, big payoff

Cuts the per-project glue code that used to be copy-pasted between repositories.

In detail

What is actually inside it

Every command has a schema

rc files plus .schema.json siblings: editors validate them, CI fails early, and the CLI can explain a wrong key instead of ignoring it.

Refs, not copies

Configurations are dereferenced with @apidevtools/json-schema-ref-parser before use, which is what lets layer-based schemas stay readable in the repository.

DetailValue
Binaries`kickstartDS`, `ksDS`
Conventionsper-command `.…rc.json` + JSON Schema
StartedMay 2022

Technical notes

The decisions behind it

Why rc files rather than flags?
Because most of these commands run in CI. A checked-in, schema-validated rc file is reviewable and reproducible; a long flag list in a package.json script is neither.

Every command, end to end

Five groups, fourteen leaf commands, one shared task runner

Each command is a pair: a Command declaration in src/commands/<group>/<name>.ts that only describes options, and the real logic in src/tasks/<group>/<name>-task.ts. index.ts registers the five groups — tokens, schema, cms, completion, example — through Commander's executableFile mechanism, so every group runs in its own Node process and can be invoked directly as dist/src/commands/<group>.js.

The shared runner is src/tasks/task.ts. init() resolves configuration; start() then runs up to three phases: prerequisite checks, the run steps, or the matching revert steps when --revert is passed. All work happens inside ${tempdir()}/<module>-<command>-<identifier> — for example /tmp/tokens-init-init-task — via pushd/popd; results are copied back to the project directory only after a step returns, so a crashed command leaves the checkout untouched.

What the runner is meant to offer is not quite what it does today. The pre-run wipe is gated on tmpDir.length > 5 && tmpDir.startsWith('/tmp'), so it silently skips whenever tempdir() points somewhere else — the macOS default $TMPDIR, for instance — and stale files from an earlier run survive. And all twelve task modules still carry // TODO implement revert subtask as their revert body: --revert logs that it finished successfully and changes nothing.

CommandWhat a run does
`tokens init`Reads the branding token file (`src/token/branding-token.json` by default) and writes a Style Dictionary dictionary to `src/token/dictionary`.
`tokens compile`Builds that dictionary with Style Dictionary, using the project's `sd.config.cjs` or a built-in config when it is absent, then copies each platform's files back to its `buildPath`.
`tokens tofigma`Validates and serialises tokens against the Figma template schema — work in progress.
`schema types``<name>.schema.json` → a colocated `<Name>Props.ts`; imports resolve per `$id`, so kickstartDS-hosted schemas come from `@kickstartds/<module>/lib/<name>/typing`.
`schema dereference`Inlines `$ref` / `allOf` and writes `<name>.schema.dereffed.json` next to the source.
`schema layer`Emits `declare module "@kickstartds/<module>/lib/<name>/typing"` overrides into `src/types/<Name>Props.d.ts`.
`schema defaults`Derives JSON Schema defaults and writes them as `DeepPartial<...>` objects to `<Name>Defaults.ts`.
`cms storyblok | uniform | stackbit | staticcms`Classifies schemas into templates, globals and components, then writes `components.123456.json`, `components.json`, `models.json` or `config.yml` under `src/cms`.

Wired, unused, unreachable

What the option tables and the dependency list actually say

5
Command groups
tokens, schema, cms, completion, example — 14 leaf commands across 12 task modules.
0
Working `--revert` steps
Every one of the 12 task modules ships a no-op revert body; --revert reports success regardless.
38
TODO comments
in src/ and index.ts, including the three self-documented defects up top: --debug, unknown commands, completions.
4
Utility helpers used
of the 13 instantiated on every run (shell, schema, tokens, json); git, npm, docker and onepassword have no caller.
Flags you cannot turn off

--rc-only and --cleanup are plain Commander booleans defaulting to true, with no negated form. Against Commander 9.2.0, --no-rc-only, --rc-only=false and --cleanup=false are all rejected as unknown options. The JSON-Schema-driven Inquirer prompt flow sits behind --rc-only=false, and the README's "resume a late failure with --cleanup false" tip cannot be expressed on the command line at all.

Default config only for tokens

The package ships four base rc files — .tokens-initrc.json, .tokens-compilerc.json, .tokens-tofigmarc.json and .example-demorc.json — matched by "files": [".*rc.json"]. cosmiconfig("<module>-<command>") looks for .schema-typesrc.json, .schema-storyblokrc.json and friends for the schema and CMS commands, but those are absent — as are the .<module>-<command>rc.schema.json files that prompting.ts reads synchronously on every prompt.

Telemetry that stays off

util/analytics.ts activates only when all four of INFLUXDB_URL, INFLUXDB_TOKEN, INFLUXDB_ORG and INFLUXDB_BUCKET are set. It then writes an InfluxDB timing point tagged with module, command, instance, a fresh uuid and the OS user. The same 180-day mean query also decides whether to swap the console logger for a progress bar, and only when the cached step timings sum above 2500 ms.

Helpers waiting for commands

task.ts wires in git (simple-git against ssh://git@github.com), npm (install, unpublish, getVersion), docker (dockerode streaming container logs through byline) and onepassword (hard-coded ruhmesmeile.1password.com). None is referenced by a shipped task; they are the scaffolding of an internal extension-migration workflow the public CLI does not expose.

Rough edges, one by one

Where a senior engineer gets bitten

Which packages must a project install for the CLI to work?
The generators are peer dependencies, not bundled ones. Since v2.0.0 @kickstartds/jsonschema-utils, @kickstartds/jsonschema2types, jsonschema2storyblok, jsonschema2stackbit, jsonschema2staticcms, jsonschema2uniform and @kickstartds/core all sit in peerDependencies. util/schema.ts imports them lazily (await import('@kickstartds/jsonschema2storyblok')), so a missing generator fails at the step that needs it rather than at startup. util/tokens.ts resolves @kickstartds/style-dictionary from the calling project with createRequire, and its default Style Dictionary config adds the icon SVGs from <core>/source/design-tokens/icons/*.svg.
What does `cms storyblok` actually write?
It globs src/components/**/*.(schema|definitions|interface).json, classifies each $id by its schema name using the --templates / --globals / --components lists, and writes the result to the literal path src/cms/components.123456.json. --update-config reads that same file back and iterates its existing components — but the loop body is empty, so the merge path is a stub and the output file name is hard-coded.
Why does `tokens tofigma` finish without syncing anything?
It validates figmaFile.json against assets/tokens/figma/figma-tokens.schema.json with Ajv (removeAdditional, allErrors; invalid input dumps the errors and exits 1), dereferences that schema, merges allOf, compiles it to figma-file.d.ts, walks it with json-schema-traverse and writes parsed-tokens.json. Two things stop it short: parsedTokens is still initialised as {} behind a TODO, and the syncToFigma call is not awaited. Its prerequisite check also demands a tokens/ directory in the project root even though the command advertises --token-dictionary-path — an option the tofigma task never receives.
Where do generated files land?
schema types, dereference and defaults write next to the schema they came from: ButtonProps.ts, button.schema.dereffed.json, ButtonDefaults.ts. schema layer is the exception, writing src/types/<Name>Props.d.ts files that wrap the generated interfaces in declare module "@kickstartds/<module>/lib/<name>/typing". CMS output goes under --configuration-path (default src/cms). The globs follow the schema naming convention: *.schema.json, plus *.definitions.json and *.interface.json for the CMS and layer tasks.
Are `kickstartDS` and `ksDS` different binaries?
No. Both bin entries map to bin/index.js, which is a shebang plus import '../dist/index.js'. The figlet banner and version line are a top-level console.log executed before program.parse, so they print for --version too. The global -d, --debug flag is declared in index.ts but the logger is created with hard-coded settings and a // TODO this debugging switch does not work right now; only the per-command --debug reaches the task runner, where it flips ShellJS silent mode off.
Is the Node requirement enforced?
No. package.json carries "node": ">=18.1" at the top level instead of under engines, so no package manager checks it; .nvmrc pins 18 and CircleCI builds on cimg/node:18.16.1 with yarn tsc as the only build step. The published tarball contains dist plus the .*rc.json / .*rc.schema.json files, and main points at bin/index.js while exports points at dist/index.js.