This file is for AI coding agents (and humans who like density) working on
@imqueue/cli. It captures how the codebase is built, tested and structured,
plus the invariants and gotchas that are easy to get wrong. Read it before
making changes. For contribution process/terms see
CONTRIBUTING.md; for end-user docs see the wiki/
directory and README.
imq — a RAD CLI for the @imqueue Redis-RPC microservice framework. It
scaffolds services from templates, wires them to a VCS host / CI / container
registry, generates typed RPC clients, and manages a local fleet of services.
- ESM only,
"type": "module". No barerequire()inlib/,src/,index.ts(it is not defined) — onlycreateRequire(import.meta.url)for loading JSON (lib/constants.ts,lib/autoupdate.ts,src/catalog/load.ts). Useimport. Import sibling modules with the.jsextension (NodeNext resolves it to the.tssource). - TypeScript 7 (the native compiler),
module/moduleResolution: nodenext,verbatimModuleSyntax: true,isolatedModules: true,strict. Useimport { type X }/import typefor type-only imports. - Lint/format:
oxlint+oxfmt. Runnpm run formatbefore committing; CI checksnpm run format:check. Do not add editor-specific ignore comments. - Node ≥ 22.12. Tests use the native
node:testrunner with--experimental-test-module-mocks. - Build emits
.js/.d.ts/.js.mapnext to sources; they are not committed.npm run buildrunsclean-compiledfirst, so stale artifacts never linger.
npm ci # install
npm run build # clean + tsc (emits alongside sources)
npm test # build + run every test/**/*.spec.js (node:test)
npm run lint # oxlint
npm run format # oxfmt (write) | npm run format:check (verify)
npm run test-coverage # tests + experimental coverageRun a single spec after a build:
node --experimental-test-module-mocks --import ./test/mocks/index.js --test test/src/ctl.spec.js
Real end-to-end (containerized, offline-capable):
bash test/docker/run.sh — see test/docker/README.md.
| Path | Role |
|---|---|
index.ts |
yargs entry; registers top-level commands |
src/*.ts, src/**/**.ts |
command modules (each exports {command,describe,builder,handler}) |
lib/*.ts |
shared library; re-exported from lib/index.ts |
src/providers/** |
VCS / CI / registry / scm providers + registry/types |
src/service/create-*.ts |
service creation: plan → scaffold → pipeline |
src/catalog/** + lib/catalog.json |
addon package catalog engine + data |
test/** |
node:test specs; test/mocks/ preload; test/docker/ harness |
A command is a module exporting a yargs command descriptor:
export const { command, describe, builder, handler } = {
command: 'ctl <action>',
describe: '…',
builder(yargs) { return yargs.option(/* … */); },
async handler(argv) { try { /* … */ } catch (e) { printError(e); } },
};Parent commands (service, config, completions) just .command(child) in
their builder. Register a new top-level command in index.ts.
Side-effectful commands factor their I/O behind an injectable deps
interface and expose pure-ish orchestration functions; the handler builds
defaultDeps() (real spawn/git/fs) and calls them. Tests pass fake deps and
assert the recorded calls. Examples: src/ctl.ts (CtlDeps), src/up.ts
(UpDeps). Prefer this over mocking node:child_process globally. Use temp
dirs (mkdtempSync) for filesystem tests; sandbox CLI home via
IMQ_CLI_HOME (the mocks preload sets it to /tmp).
Spec import depth matters: test/src/x.spec.ts uses ../mocks/index.js and
../../src/x.js; test/src/a/b.spec.ts uses ../../mocks and ../../../src.
Getting this wrong yields TS2307/TS2882 and a non-zero build that aborts
npm test.
- Config:
lib/config-schema.tskeeps the structured v4 keys (vcs/ci/registry/packages/templatesRef) and the legacy keys (gitBaseUrl,gitHubAuthToken,useGit,dockerHub*, …) in sync.deriveStructured()reads either; writes emit both. A 3.x config must keep working (github+travis+dockerhub) and a downgrade must still read what we wrote. - Templates: the CLI reads the templates repo at
templatesRef(defaultmaster, config-overridable). A template is v2 iff it has animq-template.jsonmanifest, else v1. Don't assume v2. - Option precedence everywhere: flag →
.imqrc.json(service root) → global config → prompt (TTY only) → default. Resolution lives inbuildCreatePlan(create-plan.ts); do NOT inject config values as yargs.default()s (they'd out-rank.imqrc.json).service createwrites.imqrc.json(secrets excluded);config setmirrors structured keys to legacy viaapplyStructured. - Never prompt when not a TTY — with two known exceptions:
client generateprompts before overwriting an existing client unless-o, andloadTemplateprompts before re-fetching an already-cached custom git template.
IMQ_CLI_HOME, IMQ_NO_UPDATE_CHECK, IMQ_TEMPLATES_REPO,
IMQ_GITHUB_API_URL, IMQ_GITLAB_API_URL, IMQ_BITBUCKET_API_URL,
IMQ_CIRCLECI_API_URL, IMQ_TRAVIS_API_URL, IMQ_GIT_REMOTE_BASE,
IMQ_SSH_DIR (ssh dir scanned for git-protocol auto-detection),
IMQ_CLI_MISSING_COMMANDS (test seam for commandExists).
Any new network endpoint MUST get an IMQ_*_API_URL override so it is testable
and enterprise-ready. Default the templates repo to public HTTPS (no SSH
key required). Non-IMQ_ env inputs the code reads: CIRCLE_TOKEN (CircleCI
token fallback), SHELL/ZSH_VERSION (shell detection), and the cloud-registry
credentials provisioned as CI secrets at create time — GCP_SA_KEY (google),
AWS_ACCESS_KEY_ID/AWS_SECRET_ACCESS_KEY (aws-ecr),
AZURE_CLIENT_ID/AZURE_CLIENT_SECRET (azure-acr). When those are unset the
registry's secrets() returns empty and the pipeline reports that no secrets
were provisioned (it does not claim otherwise).
imq ctl / imq log / imq up replaced imqctl/imqlog/imqup. Shared
source-level discovery lives in lib/services.ts
(discoverServices/isServiceDir — scan */src/**.ts for
extends IMQ(Service|Client)). Runtime state under ~/.imq/var
(VAR_HOME in lib/constants.ts): <svc>.log + .pids (svc:pid lines).
ctl starts services detached (own process group) and stops them with a
negative-pid SIGTERM, escalating to SIGKILL if a process refuses to die and
keeping its pid entry with a warning — no ps walking. restart waits for the
old process to exit before truncating the log and respawning. ctl actions:
start|stop|restart|status. Logs are truncated per start; calm mode checks
pid liveness (via isAlive) so a startup crash is reported at once; a
double-start is refused; start that finds nothing exits non-zero. up deps
throw on failure; runUp isolates each service and exits non-zero with a
summary, and refuses to commit a tree that was already dirty before the update.
up/update-version accept --bump as a synonym for the version keyword.
The -s/--services option is normalized via parseServices (comma list,
repeated flag, or array all work). The process model is POSIX-only
(process.kill(-pid) / detached groups) — Windows is unsupported.
Note two different service-detection strategies, by design:
- source scan (
lib/services.ts) forctl/log/up— no build/import needed; - module load + prototype-chain check (
src/service/update-version.tscontainsServiceClass) forupdate-version— detects a built, runnable service regardless of export name.
- Generated services target ESM + TS +
node:test; the boilerplate must match the current default template (norequire()/chai). service create's commit step sets a repo-local git identity from the author/email, so it works without a global git identity.service createpush transport is chosen byvcs.protocol(--git-protocolflag →.imqrc→ global →httpsdefault), resolved increate-plan.ts(resolveGitProtocol) and applied increate-pipeline.tsstep 6.https(default): push authenticated with the VCS access token (the one that created the repo), injected per-push viagit -c http.extraHeader=Authorization: Basic …— never persisted to.git/config(the remote URL stays token-free). This avoids the classic "Repository not found" SSH failure when the user's key/active account lacks access to a private org repo. Per-host basic-auth username isVcsHostProvider.httpAuthUser(x-access-token/oauth2/x-token-auth).ssh: push over the host ssh url using the user's keys, no token injection.git.initAndPushinjects the header only forhttps://remotes.IMQ_GIT_REMOTE_BASEstill overrides the target entirely (no token injection there) and takes precedence overvcs.protocol.imq config initauto-detects the default protocol vialib/ssh.tsdetectGitProtocol()(SSH when~/.sshhas keys, else HTTPS;IMQ_SSH_DIRoverrides the dir), reports it, and lets the user change it.update-versionmust not filter by export name containing "Service" — walk the prototype chain.- Some sandboxes/containers have no runtime network; the docker harness is network-aware (scaffold-only offline). Two lib tests that used to hit nodejs.org are stubbed — keep tests offline/deterministic.
- Author commits repo-locally as the project identity; never modify the machine-global git config.
- The CLI runs yargs
.strict(): a failed command exits non-zero. Two consequences to respect: (1) an option offered as--no-xMUST be declared as the positive booleanx(e.g.install), or strict rejects--no-xas unknown; (2) never readyargs.argvinside abuilder— that early parse runs strict validation before positionals are declared and rejects them. - Failure reporting is centralized:
printErrorsetsprocess.exitCode = 1, so every handler'scatch (e) { printError(e) }makes the command exit non-zero. Don't callprintErroron a recoverable path.
The v4 work (release 4.0.0) has been merged to master; master is now
the active branch — branch from and PR against it (v4 is retained as the
pre-merge snapshot, no longer updated). Templates repo master holds the v2
default template + catalog.json, and the CLI reads master by default. Full
suite green (npm test), lint + format clean, docker harness
"ALL CHECKS PASSED".